diff --git a/lib/ui/painting.dart b/lib/ui/painting.dart index 7b44af766c4f3..3c0a7ad4ae07c 100644 --- a/lib/ui/painting.dart +++ b/lib/ui/painting.dart @@ -1625,6 +1625,9 @@ enum PixelFormat { bgra8888, } +/// Signature for [Image] lifecycle events. +typedef ImageEventCallback = void Function(Image image); + /// Opaque handle to raw decoded image data (pixels). /// /// To obtain an [Image] object, use the [ImageDescriptor] API. @@ -1656,12 +1659,27 @@ class Image { return true; }()); _image._handles.add(this); + onCreate?.call(this); } // C++ unit tests access this. @pragma('vm:entry-point') final _Image _image; + /// A callback that is invoked to report an image creation. + /// + /// It's preferred to use [MemoryAllocations] in flutter/foundation.dart + /// than to use [onCreate] directly because [MemoryAllocations] + /// allows multiple callbacks. + static ImageEventCallback? onCreate; + + /// A callback that is invoked to report the image disposal. + /// + /// It's preferred to use [MemoryAllocations] in flutter/foundation.dart + /// than to use [onDispose] directly because [MemoryAllocations] + /// allows multiple callbacks. + static ImageEventCallback? onDispose; + StackTrace? _debugStack; /// The number of image pixels along the image's horizontal axis. @@ -1682,6 +1700,7 @@ class Image { /// useful when trying to determine what parts of the program are keeping an /// image resident in memory. void dispose() { + onDispose?.call(this); assert(!_disposed && !_image._disposed); assert(_image._handles.contains(this)); _disposed = true; @@ -5745,6 +5764,9 @@ class Canvas extends NativeFieldWrapperClass1 { external void _drawShadow(Path path, int color, double elevation, bool transparentOccluder); } +/// Signature for [Picture] lifecycle events. +typedef PictureEventCallback = void Function(Picture picture); + /// An object representing a sequence of recorded graphical operations. /// /// To create a [Picture], use a [PictureRecorder]. @@ -5761,6 +5783,20 @@ class Picture extends NativeFieldWrapperClass1 { @pragma('vm:entry-point') Picture._(); + /// A callback that is invoked to report a picture creation. + /// + /// It's preferred to use [MemoryAllocations] in flutter/foundation.dart + /// than to use [onCreate] directly because [MemoryAllocations] + /// allows multiple callbacks. + static PictureEventCallback? onCreate; + + /// A callback that is invoked to report the picture disposal. + /// + /// It's preferred to use [MemoryAllocations] in flutter/foundation.dart + /// than to use [onDispose] directly because [MemoryAllocations] + /// allows multiple callbacks. + static PictureEventCallback? onDispose; + /// Creates an image from this picture. /// /// The returned image will be `width` pixels wide and `height` pixels high. @@ -5824,6 +5860,7 @@ class Picture extends NativeFieldWrapperClass1 { _disposed = true; return true; }()); + onDispose?.call(this); _dispose(); } @@ -5890,6 +5927,9 @@ class PictureRecorder extends NativeFieldWrapperClass1 { _endRecording(picture); _canvas!._recorder = null; _canvas = null; + // We invoke the handler here, not in the Picture constructor, because we want + // [picture.approximateBytesUsed] to be available for the handler. + Picture.onCreate?.call(picture); return picture; } diff --git a/lib/web_ui/lib/canvas.dart b/lib/web_ui/lib/canvas.dart index 4dc01537db414..afed170c199a3 100644 --- a/lib/web_ui/lib/canvas.dart +++ b/lib/web_ui/lib/canvas.dart @@ -125,7 +125,11 @@ abstract class Canvas { ); } +typedef PictureEventCallback = void Function(Picture picture); + abstract class Picture { + static PictureEventCallback? onCreate; + static PictureEventCallback? onDispose; Future toImage(int width, int height); Image toImageSync(int width, int height); void dispose(); diff --git a/lib/web_ui/lib/painting.dart b/lib/web_ui/lib/painting.dart index 8daf324d9d77c..3ab2ba0d3e69b 100644 --- a/lib/web_ui/lib/painting.dart +++ b/lib/web_ui/lib/painting.dart @@ -337,7 +337,12 @@ abstract class Gradient extends Shader { matrix4 != null ? engine.toMatrix32(matrix4) : null); } +typedef ImageEventCallback = void Function(Image image); + abstract class Image { + static ImageEventCallback? onCreate; + static ImageEventCallback? onDispose; + int get width; int get height; Future toByteData({ImageByteFormat format = ImageByteFormat.rawRgba}); diff --git a/lib/web_ui/lib/src/engine/canvaskit/image.dart b/lib/web_ui/lib/src/engine/canvaskit/image.dart index 921d5fe024f48..8a6a8222bd054 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/image.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/image.dart @@ -149,9 +149,7 @@ Future fetchImage( /// A [ui.Image] backed by an `SkImage` from Skia. class CkImage implements ui.Image, StackTraceDebugger { CkImage(SkImage skImage, { this.videoFrame }) { - if (assertionsEnabled) { - _debugStackTrace = StackTrace.current; - } + _init(); if (browserSupportsFinalizationRegistry) { box = SkiaObjectBox(this, skImage); } else { @@ -200,10 +198,15 @@ class CkImage implements ui.Image, StackTraceDebugger { } CkImage.cloneOf(this.box) { + _init(); + box.ref(this); + } + + void _init() { if (assertionsEnabled) { _debugStackTrace = StackTrace.current; } - box.ref(this); + ui.Image.onCreate?.call(this); } @override @@ -241,6 +244,7 @@ class CkImage implements ui.Image, StackTraceDebugger { !_disposed, 'Cannot dispose an image that has already been disposed.', ); + ui.Image.onDispose?.call(this); _disposed = true; box.unref(this); } diff --git a/lib/web_ui/lib/src/engine/canvaskit/picture.dart b/lib/web_ui/lib/src/engine/canvaskit/picture.dart index fa5a7c263f852..e70c28612254b 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/picture.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/picture.dart @@ -81,6 +81,7 @@ class CkPicture extends ManagedSkiaObject implements ui.Picture { _debugDisposalStackTrace = StackTrace.current; return true; }()); + ui.Picture.onDispose?.call(this); if (Instrumentation.enabled) { Instrumentation.instance.incrementCounter('Picture disposed'); } diff --git a/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart b/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart index 1b63de0693384..7a0d11e433548 100644 --- a/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart +++ b/lib/web_ui/lib/src/engine/canvaskit/picture_recorder.dart @@ -38,7 +38,12 @@ class CkPictureRecorder implements ui.PictureRecorder { final SkPicture skPicture = recorder.finishRecordingAsPicture(); recorder.delete(); _skRecorder = null; - return CkPicture(skPicture, _cullRect, _recordingCanvas!.pictureSnapshot); + final CkPicture result = + CkPicture(skPicture, _cullRect, _recordingCanvas!.pictureSnapshot); + // We invoke the handler here, not in the picture constructor, because we want + // [result.approximateBytesUsed] to be available for the handler. + ui.Picture.onCreate?.call(result); + return result; } @override diff --git a/lib/web_ui/lib/src/engine/embedder.dart b/lib/web_ui/lib/src/engine/embedder.dart index ff65ba564948f..fadec05e57a6d 100644 --- a/lib/web_ui/lib/src/engine/embedder.dart +++ b/lib/web_ui/lib/src/engine/embedder.dart @@ -412,9 +412,7 @@ class FlutterViewEmbedder { /// Called immediately after browser window language change. void _languageDidChange(DomEvent event) { EnginePlatformDispatcher.instance.updateLocales(); - if (ui.window.onLocaleChanged != null) { - ui.window.onLocaleChanged!(); - } + ui.window.onLocaleChanged?.call(); } static const String orientationLockTypeAny = 'any'; diff --git a/lib/web_ui/lib/src/engine/html_image_codec.dart b/lib/web_ui/lib/src/engine/html_image_codec.dart index 45c634f1b4a1f..11c4d29428d4d 100644 --- a/lib/web_ui/lib/src/engine/html_image_codec.dart +++ b/lib/web_ui/lib/src/engine/html_image_codec.dart @@ -138,7 +138,9 @@ class SingleFrameInfo implements ui.FrameInfo { } class HtmlImage implements ui.Image { - HtmlImage(this.imgElement, this.width, this.height); + HtmlImage(this.imgElement, this.width, this.height) { + ui.Image.onCreate?.call(this); + } final DomHTMLImageElement imgElement; bool _requiresClone = false; @@ -146,6 +148,7 @@ class HtmlImage implements ui.Image { bool _disposed = false; @override void dispose() { + ui.Image.onDispose?.call(this); // Do nothing. The codec that owns this image should take care of // releasing the object url. if (assertionsEnabled) { diff --git a/lib/web_ui/lib/src/engine/picture.dart b/lib/web_ui/lib/src/engine/picture.dart index dc432b2b9f34c..074dc9f27856d 100644 --- a/lib/web_ui/lib/src/engine/picture.dart +++ b/lib/web_ui/lib/src/engine/picture.dart @@ -40,7 +40,11 @@ class EnginePictureRecorder implements ui.PictureRecorder { } _isRecording = false; _canvas!.endRecording(); - return EnginePicture(_canvas, cullRect); + final EnginePicture result = EnginePicture(_canvas, cullRect); + // We invoke the handler here, not in the Picture constructor, because we want + // [result.approximateBytesUsed] to be available for the handler. + ui.Picture.onCreate?.call(result); + return result; } } @@ -97,6 +101,7 @@ class EnginePicture implements ui.Picture { @override void dispose() { + ui.Picture.onDispose?.call(this); _disposed = true; } diff --git a/lib/web_ui/test/canvaskit/image_test.dart b/lib/web_ui/test/canvaskit/image_test.dart index f6ba8031cf1a4..9f4d620c9fa58 100644 --- a/lib/web_ui/test/canvaskit/image_test.dart +++ b/lib/web_ui/test/canvaskit/image_test.dart @@ -24,6 +24,50 @@ void testMain() { image.dispose(); // TODO(hterkelsen): https://github.com/flutter/flutter/issues/109265 }, skip: isFirefox); + + test('Image constructor invokes onCreate once', () async { + int onCreateInvokedCount = 0; + ui.Image? createdImage; + ui.Image.onCreate = (ui.Image image) { + onCreateInvokedCount++; + createdImage = image; + }; + + final ui.Image image1 = await _createImage(); + + expect(onCreateInvokedCount, 1); + expect(createdImage, image1); + + final ui.Image image2 = await _createImage(); + + expect(onCreateInvokedCount, 2); + expect(createdImage, image2); + + ui.Image.onCreate = null; + // TODO(hterkelsen): https://github.com/flutter/flutter/issues/109265 + }, skip: isFirefox); + + test('dispose() invokes onDispose once', () async { + int onDisposeInvokedCount = 0; + ui.Image? disposedImage; + ui.Image.onDispose = (ui.Image image) { + onDisposeInvokedCount++; + disposedImage = image; + }; + + final ui.Image image1 = await _createImage()..dispose(); + + expect(onDisposeInvokedCount, 1); + expect(disposedImage, image1); + + final ui.Image image2 = await _createImage()..dispose(); + + expect(onDisposeInvokedCount, 2); + expect(disposedImage, image2); + + ui.Image.onDispose = null; + // TODO(hterkelsen): https://github.com/flutter/flutter/issues/109265 + }, skip: isFirefox); } Future _createImage() => _createPicture().toImage(10, 10); diff --git a/lib/web_ui/test/engine/image/image_test.dart b/lib/web_ui/test/engine/image/image_test.dart index 8d690483a7ea6..c0b6235368b3f 100644 --- a/lib/web_ui/test/engine/image/image_test.dart +++ b/lib/web_ui/test/engine/image/image_test.dart @@ -17,7 +17,53 @@ Future testMain() async { expect(image.runtimeType.toString(), equals('HtmlImage')); image.dispose(); // TODO(polina-c): unskip the test when bug is fixed: - // https://github.com/flutter/engine/pull/35791 + // https://github.com/flutter/flutter/issues/110599 + }, skip: true); + + test('Image constructor invokes onCreate once', () async { + int onCreateInvokedCount = 0; + ui.Image? createdImage; + ui.Image.onCreate = (ui.Image image) { + onCreateInvokedCount++; + createdImage = image; + }; + + final ui.Image image1 = await _createImage(); + + expect(onCreateInvokedCount, 1); + expect(createdImage, image1); + + final ui.Image image2 = await _createImage(); + + expect(onCreateInvokedCount, 2); + expect(createdImage, image2); + + ui.Image.onCreate = null; + // TODO(polina-c): unskip the test when bug is fixed: + // https://github.com/flutter/flutter/issues/110599 + }, skip: true); + + test('dispose() invokes onDispose once', () async { + int onDisposeInvokedCount = 0; + ui.Image? disposedImage; + ui.Image.onDispose = (ui.Image image) { + onDisposeInvokedCount++; + disposedImage = image; + }; + + final ui.Image image1 = await _createImage()..dispose(); + + expect(onDisposeInvokedCount, 1); + expect(disposedImage, image1); + + final ui.Image image2 = await _createImage()..dispose(); + + expect(onDisposeInvokedCount, 2); + expect(disposedImage, image2); + + ui.Image.onDispose = null; + // TODO(polina-c): unskip the test when bug is fixed: + // https://github.com/flutter/flutter/issues/110599 }, skip: true); } diff --git a/lib/web_ui/test/engine/picture_test.dart b/lib/web_ui/test/engine/picture_test.dart new file mode 100644 index 0000000000000..d4c5928d38fdb --- /dev/null +++ b/lib/web_ui/test/engine/picture_test.dart @@ -0,0 +1,74 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; +import 'package:ui/ui.dart' as ui; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +Future testMain() async { + test('Picture construction invokes onCreate once', () async { + int onCreateInvokedCount = 0; + ui.Picture? createdPicture; + ui.Picture.onCreate = (ui.Picture picture) { + onCreateInvokedCount++; + createdPicture = picture; + }; + + final ui.Picture picture1 = _createPicture(); + + expect(onCreateInvokedCount, 1); + expect(createdPicture, picture1); + + final ui.Picture picture2 = _createPicture(); + + expect(onCreateInvokedCount, 2); + expect(createdPicture, picture2); + ui.Picture.onCreate = null; + }); + + test('approximateBytesUsed is available for onCreate', () async { + int pictureSize = -1; + + ui.Picture.onCreate = (ui.Picture picture) => + pictureSize = picture.approximateBytesUsed; + + _createPicture(); + + expect(pictureSize >= 0, true); + ui.Picture.onCreate = null; + }); + + test('dispose() invokes onDispose once', () async { + int onDisposeInvokedCount = 0; + ui.Picture? disposedPicture; + ui.Picture.onDispose = (ui.Picture picture) { + onDisposeInvokedCount++; + disposedPicture = picture; + }; + + final ui.Picture picture1 = _createPicture()..dispose(); + + expect(onDisposeInvokedCount, 1); + expect(disposedPicture, picture1); + + final ui.Picture picture2 = _createPicture()..dispose(); + + expect(onDisposeInvokedCount, 2); + expect(disposedPicture, picture2); + + ui.Picture.onDispose = null; + }); +} + +ui.Picture _createPicture() { + final ui.PictureRecorder recorder = ui.PictureRecorder(); + final ui.Canvas canvas = ui.Canvas(recorder); + const ui.Rect rect = ui.Rect.fromLTWH(0.0, 0.0, 100.0, 100.0); + canvas.clipRect(rect); + return recorder.endRecording(); +} diff --git a/testing/dart/BUILD.gn b/testing/dart/BUILD.gn index 712bae0f1d56d..04035d13d8f2e 100644 --- a/testing/dart/BUILD.gn +++ b/testing/dart/BUILD.gn @@ -22,6 +22,7 @@ tests = [ "http_disallow_http_connections_test.dart", "image_descriptor_test.dart", "image_dispose_test.dart", + "image_events_test.dart", "image_filter_test.dart", "image_resize_test.dart", "image_shader_test.dart", @@ -34,6 +35,7 @@ tests = [ "paragraph_builder_test.dart", "paragraph_test.dart", "path_test.dart", + "picture_test.dart", "platform_view_test.dart", "plugin_utilities_test.dart", "semantics_test.dart", diff --git a/testing/dart/image_dispose_test.dart b/testing/dart/image_dispose_test.dart index fe0d11fda832a..e96ca0dcb44a8 100644 --- a/testing/dart/image_dispose_test.dart +++ b/testing/dart/image_dispose_test.dart @@ -17,7 +17,7 @@ void main() { }()); test('Handles are distinct', () async { - final Uint8List bytes = await readFile('2x2.png'); + final Uint8List bytes = await _readFile('2x2.png'); final Codec codec = await instantiateImageCodec(bytes); final FrameInfo frame = await codec.getNextFrame(); @@ -36,7 +36,7 @@ void main() { }); test('Canvas can paint image from handle and byte data from handle', () async { - final Uint8List bytes = await readFile('2x2.png'); + final Uint8List bytes = await _readFile('2x2.png'); final Codec codec = await instantiateImageCodec(bytes); final FrameInfo frame = await codec.getNextFrame(); @@ -66,7 +66,7 @@ void main() { }); test('Records stack traces', () async { - final Uint8List bytes = await readFile('2x2.png'); + final Uint8List bytes = await _readFile('2x2.png'); final Codec codec = await instantiateImageCodec(bytes); final FrameInfo frame = await codec.getNextFrame(); @@ -92,7 +92,7 @@ void main() { }, skip: !assertsEnabled); test('Clones can be compared', () async { - final Uint8List bytes = await readFile('2x2.png'); + final Uint8List bytes = await _readFile('2x2.png'); final Codec codec = await instantiateImageCodec(bytes); final FrameInfo frame = await codec.getNextFrame(); @@ -115,7 +115,7 @@ void main() { }); test('debugDisposed works', () async { - final Uint8List bytes = await readFile('2x2.png'); + final Uint8List bytes = await _readFile('2x2.png'); final Codec codec = await instantiateImageCodec(bytes); final FrameInfo frame = await codec.getNextFrame(); @@ -134,7 +134,7 @@ void main() { }); } -Future readFile(String fileName) async { +Future _readFile(String fileName) async { final File file = File(path.join( 'flutter', 'testing', diff --git a/testing/dart/image_events_test.dart b/testing/dart/image_events_test.dart new file mode 100644 index 0000000000000..92b856c0014e4 --- /dev/null +++ b/testing/dart/image_events_test.dart @@ -0,0 +1,56 @@ +// Copyright 2013 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:async'; +import 'dart:ui'; + +import 'package:litetest/litetest.dart'; + +void main() { + test('Image constructor and dispose invokes onCreate once', () async { + // We test constructor and dispose in one test because + // litetest runs the tests in parallel and static handlers + // are shared between tests. + + int onCreateInvokedCount = 0; + Image? createdImage; + int onDisposeInvokedCount = 0; + Image? disposedImage; + Image.onCreate = (Image image) { + onCreateInvokedCount++; + createdImage = image; + }; + Image.onDispose = (Image image) { + onDisposeInvokedCount++; + disposedImage = image; + }; + + final Image image1 = await _createImage()..dispose(); + + expect(onCreateInvokedCount, 1); + expect(createdImage, image1); + expect(onDisposeInvokedCount, 1); + expect(disposedImage, image1); + + final Image image2 = await _createImage()..dispose(); + + expect(onCreateInvokedCount, 2); + expect(createdImage, image2); + expect(onDisposeInvokedCount, 2); + expect(disposedImage, image2); + + Image.onCreate = null; + Image.onDispose = null; + }); +} + +Future _createImage() => _createPicture().toImage(10, 10); + +Picture _createPicture() { + final PictureRecorder recorder = PictureRecorder(); + final Canvas canvas = Canvas(recorder); + const Rect rect = Rect.fromLTWH(0.0, 0.0, 100.0, 100.0); + canvas.clipRect(rect); + return recorder.endRecording(); +} diff --git a/testing/dart/picture_test.dart b/testing/dart/picture_test.dart new file mode 100644 index 0000000000000..4ea13962fcaa8 --- /dev/null +++ b/testing/dart/picture_test.dart @@ -0,0 +1,70 @@ +// Copyright 2022 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'dart:ui'; + +import 'package:litetest/litetest.dart'; + +void main() { + test('Picture construction invokes onCreate once', () async { + int onCreateInvokedCount = 0; + Picture? createdPicture; + Picture.onCreate = (Picture picture) { + onCreateInvokedCount++; + createdPicture = picture; + }; + + final Picture picture1 = _createPicture(); + + expect(onCreateInvokedCount, 1); + expect(createdPicture, picture1); + + final Picture picture2 = _createPicture(); + + expect(onCreateInvokedCount, 2); + expect(createdPicture, picture2); + Picture.onCreate = null; + }); + + test('approximateBytesUsed is available for onCreate', () async { + int pictureSize = -1; + + Picture.onCreate = (Picture picture) => + pictureSize = picture.approximateBytesUsed; + + _createPicture(); + + expect(pictureSize >= 0, true); + Picture.onCreate = null; + }); + + test('dispose() invokes onDispose once', () async { + int onDisposeInvokedCount = 0; + Picture? disposedPicture; + Picture.onDispose = (Picture picture) { + onDisposeInvokedCount++; + disposedPicture = picture; + }; + + final Picture picture1 = _createPicture()..dispose(); + + expect(onDisposeInvokedCount, 1); + expect(disposedPicture, picture1); + + final Picture picture2 = _createPicture()..dispose(); + + expect(onDisposeInvokedCount, 2); + expect(disposedPicture, picture2); + + Picture.onDispose = null; + }); +} + +Picture _createPicture() { + final PictureRecorder recorder = PictureRecorder(); + final Canvas canvas = Canvas(recorder); + const Rect rect = Rect.fromLTWH(0.0, 0.0, 100.0, 100.0); + canvas.clipRect(rect); + return recorder.endRecording(); +} diff --git a/web_sdk/test/api_conform_test.dart b/web_sdk/test/api_conform_test.dart index 755b6f9a3345c..db5ffb8587ed7 100644 --- a/web_sdk/test/api_conform_test.dart +++ b/web_sdk/test/api_conform_test.dart @@ -247,8 +247,15 @@ void main() { final SimpleFormalParameter webParam = (webTypeDef.type as GenericFunctionType).parameters.parameters[i] as SimpleFormalParameter; - - if (webParam.identifier!.name != uiParam.identifier!.name) { + if (webParam.identifier == null) { + failed = true; + print('Warning: lib/web_ui/ui.dart $typeDefName parameter $i should have name.'); + } + if (uiParam.identifier == null) { + failed = true; + print('Warning: lib/ui/ui.dart $typeDefName parameter $i should have name.'); + } + if (webParam.identifier?.name != uiParam.identifier?.name) { failed = true; print('Warning: lib/ui/ui.dart $typeDefName parameter $i ' '${uiParam.identifier!.name} has a different name in lib/web_ui/ui.dart.');