Skip to content
This repository was archived by the owner on Feb 25, 2025. It is now read-only.

Commit 2827b64

Browse files
authored
Instrument Image and Picture for leak tracking. (#35274)
1 parent c294429 commit 2827b64

File tree

17 files changed

+383
-19
lines changed

17 files changed

+383
-19
lines changed

lib/ui/painting.dart

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1625,6 +1625,9 @@ enum PixelFormat {
16251625
bgra8888,
16261626
}
16271627

1628+
/// Signature for [Image] lifecycle events.
1629+
typedef ImageEventCallback = void Function(Image image);
1630+
16281631
/// Opaque handle to raw decoded image data (pixels).
16291632
///
16301633
/// To obtain an [Image] object, use the [ImageDescriptor] API.
@@ -1656,12 +1659,27 @@ class Image {
16561659
return true;
16571660
}());
16581661
_image._handles.add(this);
1662+
onCreate?.call(this);
16591663
}
16601664

16611665
// C++ unit tests access this.
16621666
@pragma('vm:entry-point')
16631667
final _Image _image;
16641668

1669+
/// A callback that is invoked to report an image creation.
1670+
///
1671+
/// It's preferred to use [MemoryAllocations] in flutter/foundation.dart
1672+
/// than to use [onCreate] directly because [MemoryAllocations]
1673+
/// allows multiple callbacks.
1674+
static ImageEventCallback? onCreate;
1675+
1676+
/// A callback that is invoked to report the image disposal.
1677+
///
1678+
/// It's preferred to use [MemoryAllocations] in flutter/foundation.dart
1679+
/// than to use [onDispose] directly because [MemoryAllocations]
1680+
/// allows multiple callbacks.
1681+
static ImageEventCallback? onDispose;
1682+
16651683
StackTrace? _debugStack;
16661684

16671685
/// The number of image pixels along the image's horizontal axis.
@@ -1682,6 +1700,7 @@ class Image {
16821700
/// useful when trying to determine what parts of the program are keeping an
16831701
/// image resident in memory.
16841702
void dispose() {
1703+
onDispose?.call(this);
16851704
assert(!_disposed && !_image._disposed);
16861705
assert(_image._handles.contains(this));
16871706
_disposed = true;
@@ -5745,6 +5764,9 @@ class Canvas extends NativeFieldWrapperClass1 {
57455764
external void _drawShadow(Path path, int color, double elevation, bool transparentOccluder);
57465765
}
57475766

5767+
/// Signature for [Picture] lifecycle events.
5768+
typedef PictureEventCallback = void Function(Picture picture);
5769+
57485770
/// An object representing a sequence of recorded graphical operations.
57495771
///
57505772
/// To create a [Picture], use a [PictureRecorder].
@@ -5761,6 +5783,20 @@ class Picture extends NativeFieldWrapperClass1 {
57615783
@pragma('vm:entry-point')
57625784
Picture._();
57635785

5786+
/// A callback that is invoked to report a picture creation.
5787+
///
5788+
/// It's preferred to use [MemoryAllocations] in flutter/foundation.dart
5789+
/// than to use [onCreate] directly because [MemoryAllocations]
5790+
/// allows multiple callbacks.
5791+
static PictureEventCallback? onCreate;
5792+
5793+
/// A callback that is invoked to report the picture disposal.
5794+
///
5795+
/// It's preferred to use [MemoryAllocations] in flutter/foundation.dart
5796+
/// than to use [onDispose] directly because [MemoryAllocations]
5797+
/// allows multiple callbacks.
5798+
static PictureEventCallback? onDispose;
5799+
57645800
/// Creates an image from this picture.
57655801
///
57665802
/// The returned image will be `width` pixels wide and `height` pixels high.
@@ -5824,6 +5860,7 @@ class Picture extends NativeFieldWrapperClass1 {
58245860
_disposed = true;
58255861
return true;
58265862
}());
5863+
onDispose?.call(this);
58275864
_dispose();
58285865
}
58295866

@@ -5890,6 +5927,9 @@ class PictureRecorder extends NativeFieldWrapperClass1 {
58905927
_endRecording(picture);
58915928
_canvas!._recorder = null;
58925929
_canvas = null;
5930+
// We invoke the handler here, not in the Picture constructor, because we want
5931+
// [picture.approximateBytesUsed] to be available for the handler.
5932+
Picture.onCreate?.call(picture);
58935933
return picture;
58945934
}
58955935

lib/web_ui/lib/canvas.dart

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,7 +125,11 @@ abstract class Canvas {
125125
);
126126
}
127127

128+
typedef PictureEventCallback = void Function(Picture picture);
129+
128130
abstract class Picture {
131+
static PictureEventCallback? onCreate;
132+
static PictureEventCallback? onDispose;
129133
Future<Image> toImage(int width, int height);
130134
Image toImageSync(int width, int height);
131135
void dispose();

lib/web_ui/lib/painting.dart

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -337,7 +337,12 @@ abstract class Gradient extends Shader {
337337
matrix4 != null ? engine.toMatrix32(matrix4) : null);
338338
}
339339

340+
typedef ImageEventCallback = void Function(Image image);
341+
340342
abstract class Image {
343+
static ImageEventCallback? onCreate;
344+
static ImageEventCallback? onDispose;
345+
341346
int get width;
342347
int get height;
343348
Future<ByteData?> toByteData({ImageByteFormat format = ImageByteFormat.rawRgba});

lib/web_ui/lib/src/engine/canvaskit/image.dart

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,7 @@ Future<Uint8List> fetchImage(
149149
/// A [ui.Image] backed by an `SkImage` from Skia.
150150
class CkImage implements ui.Image, StackTraceDebugger {
151151
CkImage(SkImage skImage, { this.videoFrame }) {
152-
if (assertionsEnabled) {
153-
_debugStackTrace = StackTrace.current;
154-
}
152+
_init();
155153
if (browserSupportsFinalizationRegistry) {
156154
box = SkiaObjectBox<CkImage, SkImage>(this, skImage);
157155
} else {
@@ -200,10 +198,15 @@ class CkImage implements ui.Image, StackTraceDebugger {
200198
}
201199

202200
CkImage.cloneOf(this.box) {
201+
_init();
202+
box.ref(this);
203+
}
204+
205+
void _init() {
203206
if (assertionsEnabled) {
204207
_debugStackTrace = StackTrace.current;
205208
}
206-
box.ref(this);
209+
ui.Image.onCreate?.call(this);
207210
}
208211

209212
@override
@@ -241,6 +244,7 @@ class CkImage implements ui.Image, StackTraceDebugger {
241244
!_disposed,
242245
'Cannot dispose an image that has already been disposed.',
243246
);
247+
ui.Image.onDispose?.call(this);
244248
_disposed = true;
245249
box.unref(this);
246250
}

lib/web_ui/lib/src/engine/canvaskit/picture.dart

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ class CkPicture extends ManagedSkiaObject<SkPicture> implements ui.Picture {
8181
_debugDisposalStackTrace = StackTrace.current;
8282
return true;
8383
}());
84+
ui.Picture.onDispose?.call(this);
8485
if (Instrumentation.enabled) {
8586
Instrumentation.instance.incrementCounter('Picture disposed');
8687
}

lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,12 @@ class CkPictureRecorder implements ui.PictureRecorder {
3838
final SkPicture skPicture = recorder.finishRecordingAsPicture();
3939
recorder.delete();
4040
_skRecorder = null;
41-
return CkPicture(skPicture, _cullRect, _recordingCanvas!.pictureSnapshot);
41+
final CkPicture result =
42+
CkPicture(skPicture, _cullRect, _recordingCanvas!.pictureSnapshot);
43+
// We invoke the handler here, not in the picture constructor, because we want
44+
// [result.approximateBytesUsed] to be available for the handler.
45+
ui.Picture.onCreate?.call(result);
46+
return result;
4247
}
4348

4449
@override

lib/web_ui/lib/src/engine/embedder.dart

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -412,9 +412,7 @@ class FlutterViewEmbedder {
412412
/// Called immediately after browser window language change.
413413
void _languageDidChange(DomEvent event) {
414414
EnginePlatformDispatcher.instance.updateLocales();
415-
if (ui.window.onLocaleChanged != null) {
416-
ui.window.onLocaleChanged!();
417-
}
415+
ui.window.onLocaleChanged?.call();
418416
}
419417

420418
static const String orientationLockTypeAny = 'any';

lib/web_ui/lib/src/engine/html_image_codec.dart

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -138,14 +138,17 @@ class SingleFrameInfo implements ui.FrameInfo {
138138
}
139139

140140
class HtmlImage implements ui.Image {
141-
HtmlImage(this.imgElement, this.width, this.height);
141+
HtmlImage(this.imgElement, this.width, this.height) {
142+
ui.Image.onCreate?.call(this);
143+
}
142144

143145
final DomHTMLImageElement imgElement;
144146
bool _requiresClone = false;
145147

146148
bool _disposed = false;
147149
@override
148150
void dispose() {
151+
ui.Image.onDispose?.call(this);
149152
// Do nothing. The codec that owns this image should take care of
150153
// releasing the object url.
151154
if (assertionsEnabled) {

lib/web_ui/lib/src/engine/picture.dart

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,11 @@ class EnginePictureRecorder implements ui.PictureRecorder {
4040
}
4141
_isRecording = false;
4242
_canvas!.endRecording();
43-
return EnginePicture(_canvas, cullRect);
43+
final EnginePicture result = EnginePicture(_canvas, cullRect);
44+
// We invoke the handler here, not in the Picture constructor, because we want
45+
// [result.approximateBytesUsed] to be available for the handler.
46+
ui.Picture.onCreate?.call(result);
47+
return result;
4448
}
4549
}
4650

@@ -97,6 +101,7 @@ class EnginePicture implements ui.Picture {
97101

98102
@override
99103
void dispose() {
104+
ui.Picture.onDispose?.call(this);
100105
_disposed = true;
101106
}
102107

lib/web_ui/test/canvaskit/image_test.dart

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,50 @@ void testMain() {
2424
image.dispose();
2525
// TODO(hterkelsen): https://github.com/flutter/flutter/issues/109265
2626
}, skip: isFirefox);
27+
28+
test('Image constructor invokes onCreate once', () async {
29+
int onCreateInvokedCount = 0;
30+
ui.Image? createdImage;
31+
ui.Image.onCreate = (ui.Image image) {
32+
onCreateInvokedCount++;
33+
createdImage = image;
34+
};
35+
36+
final ui.Image image1 = await _createImage();
37+
38+
expect(onCreateInvokedCount, 1);
39+
expect(createdImage, image1);
40+
41+
final ui.Image image2 = await _createImage();
42+
43+
expect(onCreateInvokedCount, 2);
44+
expect(createdImage, image2);
45+
46+
ui.Image.onCreate = null;
47+
// TODO(hterkelsen): https://github.com/flutter/flutter/issues/109265
48+
}, skip: isFirefox);
49+
50+
test('dispose() invokes onDispose once', () async {
51+
int onDisposeInvokedCount = 0;
52+
ui.Image? disposedImage;
53+
ui.Image.onDispose = (ui.Image image) {
54+
onDisposeInvokedCount++;
55+
disposedImage = image;
56+
};
57+
58+
final ui.Image image1 = await _createImage()..dispose();
59+
60+
expect(onDisposeInvokedCount, 1);
61+
expect(disposedImage, image1);
62+
63+
final ui.Image image2 = await _createImage()..dispose();
64+
65+
expect(onDisposeInvokedCount, 2);
66+
expect(disposedImage, image2);
67+
68+
ui.Image.onDispose = null;
69+
// TODO(hterkelsen): https://github.com/flutter/flutter/issues/109265
70+
}, skip: isFirefox);
2771
}
2872

2973
Future<ui.Image> _createImage() => _createPicture().toImage(10, 10);

0 commit comments

Comments
 (0)