-
Notifications
You must be signed in to change notification settings - Fork 38
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: Refactor screenshot capture for improved performance and reliability #147
base: main
Are you sure you want to change the base?
feat: Refactor screenshot capture for improved performance and reliability #147
Conversation
…ility This commit refactors the `captureScreenshot` method in `ScreenshotCapturer` to improve performance and reliability. The changes include: - **Synchronous Setup:** The method now performs synchronous operations first (getting context, render object, calculating pixel ratio, and getting the initial image) before executing the main process asynchronously. This ensures that the initial setup is done on the main UI thread, as required. - **Asynchronous Processing:** The main image processing and masking logic are now executed asynchronously using a `Future` and `Completer`, preventing UI jank. - **Snapshot Management:** The `_views` Expando is replaced with a `SnapshotManager` to manage the status of the view, improving code organization and maintainability.
@@ -30,8 +34,8 @@ class ViewTreeSnapshotStatus { | |||
class ScreenshotCapturer { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
what do you think about changing the class name? ScreenshotCapturer
looks weird to me
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
find to rename it, its only used internally
return; | ||
} | ||
|
||
// Ensure no asynchronous calls occur before this function, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
calling isSessionReplayActive
is important even if its inside captureScreenshot
because we don't want to pay the performance price of taking the screenshot if session replay is inactive
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
yes, I put this in
posthog-flutter/lib/src/replay/screenshot/screenshot_capturer.dart
Lines 111 to 117 in 25a3690
Future(() async { | |
final isSessionReplayActive = | |
await _nativeCommunicator.isSessionReplayActive(); | |
if (!isSessionReplayActive) { | |
return; | |
} | |
what do you think?
if (!isRoot) { | ||
rects.add(rect); | ||
} | ||
List<ElementData> extractRects([bool isRoot = true]) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
List<ElementData> extractRects([bool isRoot = true]) { | |
List<ElementData> extractRects({bool isRoot = true}) { |
I think optionals are better than positionals
/// - `List<ElementData>`: A list of [ElementData] objects representing the | ||
/// renderable elements. | ||
/// | ||
List<ElementData>? getCurrentWidgetsElements() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
List<ElementData>? getCurrentWidgetsElements() { | |
List<ElementData>? _getCurrentWidgetsElements() { |
can be private I assume
final ElementData? widgetElementsTree = | ||
_widgetScraper.parseRenderTree(context); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
final ElementData? widgetElementsTree = | |
_widgetScraper.parseRenderTree(context); | |
final widgetElementsTree = | |
_widgetScraper.parseRenderTree(context); |
the type is inferred already
try { | ||
final ByteData? byteData = | ||
await img.toByteData(format: ui.ImageByteFormat.png); | ||
if (byteData == null) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
why removing the byteData.lengthInBytes == 0
check?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
sorry, I had copy the function of old version mine
} | ||
|
||
Future<ImageInfo?> captureScreenshot() async { | ||
Future<ImageInfo?> captureScreenshot() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
the function has to be async if it returns a Future, right?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Not necessarily, a function does not need to be explicitly marked as async to return a Future.
In our case, we need to work synchronously with the function. Then we execute the main process asynchronously as a "task". Managing this with the Completer. When this is ready, we retrieve the Completer and return the Future.
This way, the frame image and the masks always move in the main thread.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
true that, nice
statusView.imageBytes = pngBytes; | ||
final recorder = ui.PictureRecorder(); | ||
final canvas = Canvas(recorder); | ||
final image = await syncImage; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
final image = await syncImage; | |
final image = await renderObject.toImage(pixelRatio: pixelRatio); |
would that work?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
would that work?
it doesn't work, because we need to first get the renderObject reference at that moment.
we do this with
final renderObject = context.findRenderObject() as RenderRepaintBoundary?;
then from the reference in the main thread of the renderObject we get the image reference
final syncImage = renderObject.toImage(pixelRatio: pixelRatio);
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
with
final image = await renderObject.toImage(pixelRatio: pixelRatio);
the renderObject reference may have changed in the main thread, so it would be out of sync
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we do the whole process inside of the Future then? so its all done in sequence
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can we do the whole process inside of the Future then? so its all done in sequence
I don't know if I understood what you meant, but if the entire process is done inside a single synchronous block, any computationally expensive operations (like rendering an image, processing masks, or manipulating large byte arrays) will block the main thread, this could lead to dropped frames or the image frames and masks got mixed up
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But if you have a solution to the whole process in the Future block, I would like to see it, maybe I missed something or something is beyond my ability haha
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
everything right now is being done on the main thread, Dart is single-threaded, unless you use background workers which is not available in all Dart/Flutter versions.
Maybe you are misunderstanding the concept of async/await with multithreading?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Isolates can also do background work
try { | ||
final srcWidth = renderObject.size.width; | ||
final srcHeight = renderObject.size.height; | ||
final pixelRatio = | ||
_getPixelRatio(srcWidth: srcWidth, srcHeight: srcHeight); | ||
|
||
final ui.Image image = await renderObject.toImage(pixelRatio: pixelRatio); | ||
final syncImage = renderObject.toImage(pixelRatio: pixelRatio); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
well this is misleading because its not sync, since returns a future and you don't await
.
nice, almost there. |
This commit refactors the
captureScreenshot
method inScreenshotCapturer
to improve performance and reliability. The changes include:
Synchronous Setup: The method now performs synchronous operations first
(getting context, render object, calculating pixel ratio, and getting the
initial image) before executing the main process asynchronously. This
ensures that the initial setup is done on the main UI thread, as required.
Asynchronous Processing: The main image processing and masking logic
are now executed asynchronously using a
Future
andCompleter
.💡 Motivation and Context
#140
💚 How did you test it?
📝 Checklist