Conversation
Instructions and example for changelogPlease add an entry to Example: ## Unreleased
- Add experimental flags to use a more efficient view renderer for Session Replay ([#4940](https://github.com/getsentry/sentry-cocoa/pull/4940))If none of the above apply, you can opt out of this check by adding |
Performance metrics 🚀
|
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 8aec30e | 1244.71 ms | 1262.20 ms | 17.49 ms |
| 313b1d9 | 1240.18 ms | 1258.44 ms | 18.26 ms |
| 649d940 | 1231.69 ms | 1250.76 ms | 19.06 ms |
| 1734d1b | 1198.69 ms | 1221.62 ms | 22.93 ms |
| ed1c644 | 1225.92 ms | 1241.24 ms | 15.33 ms |
| 99fe600 | 1226.16 ms | 1236.88 ms | 10.71 ms |
| 887502e | 1225.40 ms | 1249.92 ms | 24.51 ms |
| 2de284c | 1234.92 ms | 1254.45 ms | 19.53 ms |
| 3f6c30b | 1252.98 ms | 1266.22 ms | 13.24 ms |
| 2b19b82 | 1226.73 ms | 1243.27 ms | 16.53 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 8aec30e | 21.58 KiB | 616.76 KiB | 595.18 KiB |
| 313b1d9 | 22.85 KiB | 414.11 KiB | 391.26 KiB |
| 649d940 | 21.58 KiB | 695.36 KiB | 673.78 KiB |
| 1734d1b | 21.58 KiB | 418.82 KiB | 397.24 KiB |
| ed1c644 | 21.58 KiB | 670.39 KiB | 648.81 KiB |
| 99fe600 | 21.90 KiB | 708.13 KiB | 686.23 KiB |
| 887502e | 21.58 KiB | 704.26 KiB | 682.67 KiB |
| 2de284c | 21.58 KiB | 542.39 KiB | 520.80 KiB |
| 3f6c30b | 22.85 KiB | 408.88 KiB | 386.03 KiB |
| 2b19b82 | 21.58 KiB | 542.18 KiB | 520.60 KiB |
Previous results on branch: philprime/session-replay-custom-graphics-renderer
Startup times
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 5e61c3b | 1235.49 ms | 1253.18 ms | 17.69 ms |
| 12fe555 | 1221.86 ms | 1238.86 ms | 17.00 ms |
| 3953cf6 | 1217.63 ms | 1230.34 ms | 12.71 ms |
| 46b6763 | 1224.19 ms | 1242.85 ms | 18.66 ms |
| 4b2daf3 | 1211.94 ms | 1235.12 ms | 23.19 ms |
| c63a1ee | 1217.98 ms | 1252.40 ms | 34.42 ms |
| 289ec39 | 1206.51 ms | 1235.11 ms | 28.60 ms |
| 643bfcb | 1231.92 ms | 1255.00 ms | 23.08 ms |
App size
| Revision | Plain | With Sentry | Diff |
|---|---|---|---|
| 5e61c3b | 22.30 KiB | 826.76 KiB | 804.46 KiB |
| 12fe555 | 22.30 KiB | 832.44 KiB | 810.13 KiB |
| 3953cf6 | 22.30 KiB | 829.10 KiB | 806.80 KiB |
| 46b6763 | 22.30 KiB | 826.23 KiB | 803.92 KiB |
| 4b2daf3 | 22.30 KiB | 832.44 KiB | 810.13 KiB |
| c63a1ee | 22.30 KiB | 830.36 KiB | 808.06 KiB |
| 289ec39 | 22.30 KiB | 828.99 KiB | 806.69 KiB |
| 643bfcb | 22.30 KiB | 830.26 KiB | 807.96 KiB |
Sources/Swift/Integrations/SessionReplay/Preview/SentryMaskingPreviewView.swift
Outdated
Show resolved
Hide resolved
Codecov ReportAttention: Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## main #4940 +/- ##
=============================================
- Coverage 92.498% 92.415% -0.083%
=============================================
Files 666 669 +3
Lines 78996 79164 +168
Branches 28578 28637 +59
=============================================
+ Hits 73070 73160 +90
- Misses 5829 5907 +78
Partials 97 97
... and 17 files with indirect coverage changes Continue to review full report in Codecov by Sentry.
🚀 New features to boost your workflow:
|
eef381e to
0c33278
Compare
philipphofmann
left a comment
There was a problem hiding this comment.
This looks great. I think it's OK to skip automated test for now. Once we know the solution is working correctly, we can come up with proper tests to avoid regressions when changing the code.
Sources/Swift/Integrations/SessionReplay/Preview/SentryMaskingPreviewView.swift
Outdated
Show resolved
Hide resolved
Sources/Swift/Tools/ViewCapture/SentryExperimentalMaskRenderer.swift
Outdated
Show resolved
Hide resolved
Sources/Swift/Tools/ViewCapture/SentryGraphicsImageRenderer.swift
Outdated
Show resolved
Hide resolved
…lay-custom-graphics-renderer
philipphofmann
left a comment
There was a problem hiding this comment.
I don't have enough insights yet into SR to clearly answer the following question: The SentryViewPhotographer.image already dispatches masking to the background. Can't we dispatch more heavy work of capturing the screenshot to a BG thread?
Sources/Swift/Integrations/SessionReplay/SentryReplayOptions.swift
Outdated
Show resolved
Hide resolved
Sources/Swift/Tools/ViewCapture/SentryExperimentalMaskRenderer.swift
Outdated
Show resolved
Hide resolved
…wift Co-authored-by: Philipp Hofmann <philipp.hofmann@sentry.io>
|
@philipphofmann the capturing of the screenshot consists of three steps:
Step 1 and 2 must be run on main thread because all access to the view hierarchy must be on main. Step 3 can be performed in the background (as it is for session replay, not for screenshots), because it only uses the output of the previous two steps. |
philipphofmann
left a comment
There was a problem hiding this comment.
LGTM, as this is experimental. Once we know the approaches are working, we should think about how we can test this. Please also double check CI before merging.
|
Ready to merge after #4968 |
…lay-custom-graphics-renderer
|
Hey, this is amazing! “layer.render” was the first strategy used in the POV, and it seems fine at first with the Sentry sample, which is a small project. However, after trying it in other projects like Pocketcast or my own app, its performance was worse than So I suggest double-checking this—maybe it won’t be an issue with the new GraphicRenderer. P.S.: It’s missing some elements with “layer.render” because it doesn’t render CoreGraphics filters, so no blur, shadow, tinting (which I believe is related to the tabbar issue), and others. |
|
So, I was super curious about this one and did the checking already with my Bible app: Running with 0.03214001655578613 seconds Running with 0.04942798614501953 seconds Old Approach with As I expected, the performance seems to be in favor of the new SentryGraphicsRenderer and drawViewHierarchy (which also yields a better result). The difference from the old UIGraphicsImageRenderer is not 5x, but it is better. Tested with iPhone 14 Pro Max |
|
@brustolin hey thanks for chiming in and running the tests :) the perf improvements are definitely more relevant on lower-end devices, in iPhone 14 Pro Max it's expected to have less of an impact 👍 |
|
@kahest Hey, its a pleasure to participate.
Yeah, I can imagine that. My only take here is to stick to |
📜 Description
TL;DR: Reduces the time required to render a session replay frame from ~160ms to ~30-36ms (benchmarked on an iPhone 8)
enableExperimentalViewRendererto choose the view renderer (enables our graphics renderer)enableFastViewRenderingto choose the view draw method (switches from UIKit to CoreGraphics rendering, might be incomplete)Note
The mask renderer is using a points-to-pixel scale of 1, therefore any higher resolution view render will be downscaled.
💡 Motivation and Context
See #4000 for more information for the main issue of frame performance.
To understand the changes, we need to look at the implementation of the
SentryDefaultViewRendererused by theSentryViewPhotographer:The rendering consists of two nested blocks:
Setup: to create a rendering context which is then used to convert rendered bitmap data into an in imageDraw: which is drawing the view hierarchy into the bitmap contextBoth need to be analysed for their performance and potential improvement candidates.
In addition we need to reconsider how the resolutions and coordinate system works:
sizeof awidthandheightin points (i.e.375pt x 667ptfor iPhone 8)scaleat which the UI should be rendered (i.e.2.0for iPhone 8)size * scalepixels (i.e.750px x 1334pxfor iPhone 8)Therefore I also analyzed the impact of creating a scaled vs unscaled image, because the difference can be seen visually not only due to changes in resolution, but also due to blurriness:
Example - iOS-Swift at scale

1.0:Example - iOS-Swift at scale

2.0:The
drawHierarchyperformance has also been discussed in the Apple Developer Forums, leading to ReplayKit as an alternative (TBD)💚 How did you test it?
I tried to create unit tests to test the view photographer using a complex UI built in a Storyboard file. I was not able to get it working, because unit tests have no render server, therefore trying to render a view to an image fails with the following error:
Instead I performed manual testing by adapting the
SentryViewPhotographerto use this method, then looking at the values over time when running the sampleiOS-Swifton an iPhone 8:Baseline Performance
These are the results of using the currently released
SentryViewPhotographer.image(view:onComplete:). We are not measuring themaskRenderer.maskScreenshot(...)because it is run on the background thread and therefore not blocking the main thread.If a screen uses 60 frames per seconds to render graphics, the maximum time used to render one frame should be less than
1 / 60 = 16.667 ms. If a frame render takes longer, the next frame will be not be rendered (= a dropped frame) to catch up the lost time. This results in a stuttering user interface and visible to the user.Results at screen scale (i.e.
2.0for iPhone 8):The data shows that we have a significant frame delay with up to 161.5842ms = ~9 frames dropped every second.
Results at scale
1.0:No significant changes compared to the native screen scale.
Alternative Attempts
Using
UIView.snapshotView(afterScreenUpdates:)The first attempt to optimize the performance was using the method UIView.snapshotView(afterScreenUpdates:) to create a dummy view hierarchy as stated in the documentation, to see if the dummy view would render faster than the normal one:
During testing the snapshot view never displayed any visual data when rendered into an
UIImage.An Apple Engineer mentioned in the Apple Developer Forum that a snapshot can not be rendered to an image due to security reasons.
Results:
Discarded this approach because not possible.
Reuse the UIGraphicsImageRenderer
The documentation mentions that this renderer uses a built-in cache, therefore recommends to reuse renderer instances:
But the
UIGraphicsImageRendereris set to a fixed size and in our case the size of the view could eventually change.To reduce the memory footprint I decided to have a maximum of one cached renderer, discarding and re-creating it whenever the size changes. In the worst case it has to re-create the renderer every time, yielding the same performance as without the cache. In the best case the size never changes and the renderer is reused forever.
Results:
There is no significant change compared to the baseline.
Experimental View Renderer
UIKit is built on top of CoreGraphics and CoreAnimation also known as the
QuartzCore.The
UIGraphicsImageRendererhas been introduced withiOS 10.0to wrap around theCGContextprovided by CoreGraphics, as the setup can be tedious and complicated.The
SentryGraphicsImageRendereris a custom implementation creating aCGContextpixel buffer and also converting it into anUIImageafterwards.Results at native scale (i.e.
2.0):Results at
1.0scale:Replacing
view.drawHierarchy(in:afterScreenUpdates:)withview.layer.render(in:)Instead of drawing the view using the
drawHierarchyprovided on the UIKit-level, we can directly call the rendering of thelayerused by the UIView provided by CoreGraphics.Warning
During testing we noticed that the render is incomplete, in particular the tab bar icons have not been rendered. The exact impact is not known and can vary.
Results at native scale (i.e.
2.0):SentryGraphicsImageRenderer+drawHierarchyResults at
1.0scale:Experimental Renderer +
view.layer.render(in:)Combining the previous two approaches.
Warning
During testing we noticed that the render is incomplete, in particular the tab bar icons have not been rendered. The exact impact is not known and can vary.
Results at native scale (i.e.
2.0):view.drawHierarchy(...)UIGraphicsImageRenderer+view.layer.render(in:)Results at
1.0scale:Detailed Analysis of Implementation
Looking at one of the samples using
drawViewHierarchyInRectin detail we can analyze the duration of the different calls in detail. In particular we notice that the most expensive calls are inside UIKit and CoreGraphics.It seems like
UIViewis backed by anUIImagefrom a previous render, then draws it again into the bitmap context. As this is is a private API, we can not access it directly. No further optimization possible.