diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index 53ec4d21c6b..e01e277e8fd 100755 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -431,6 +431,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/initialization.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/layer.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/layer_scene_builder.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/layer_tree.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/mask_filter.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/n_way_canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/painting.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/path.dart @@ -440,6 +441,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/picture_recorder.dar FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/platform_message.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/raster_cache.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/rasterizer.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/skia_object_cache.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/surface.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/text.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/util.dart diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine.dart b/engine/src/flutter/lib/web_ui/lib/src/engine.dart index e1ab01db9a4..96a145c1e4c 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine.dart @@ -5,7 +5,8 @@ library engine; import 'dart:async'; -import 'dart:collection' show ListBase, IterableBase; +import 'dart:collection' + show ListBase, IterableBase, DoubleLinkedQueue, DoubleLinkedQueueEntry; import 'dart:convert' hide Codec; import 'dart:developer' as developer; import 'dart:html' as html; @@ -37,6 +38,7 @@ part 'engine/compositor/initialization.dart'; part 'engine/compositor/layer.dart'; part 'engine/compositor/layer_scene_builder.dart'; part 'engine/compositor/layer_tree.dart'; +part 'engine/compositor/mask_filter.dart'; part 'engine/compositor/n_way_canvas.dart'; part 'engine/compositor/path.dart'; part 'engine/compositor/painting.dart'; @@ -46,6 +48,7 @@ part 'engine/compositor/picture_recorder.dart'; part 'engine/compositor/platform_message.dart'; part 'engine/compositor/raster_cache.dart'; part 'engine/compositor/rasterizer.dart'; +part 'engine/compositor/skia_object_cache.dart'; part 'engine/compositor/surface.dart'; part 'engine/compositor/text.dart'; part 'engine/compositor/util.dart'; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/canvas.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/canvas.dart index 3377aa8a837..9b983b1caf6 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/canvas.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/canvas.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. - part of engine; /// A Dart wrapper around Skia's SKCanvas. @@ -168,7 +167,7 @@ class SkCanvas { void drawParagraph(ui.Paragraph paragraph, ui.Offset offset) { final SkParagraph skParagraph = paragraph as SkParagraph; skCanvas.callMethod('drawParagraph', [ - skParagraph.skParagraph, + skParagraph.skiaObject, offset.dx, offset.dy, ]); @@ -183,7 +182,8 @@ class SkCanvas { void drawPicture(ui.Picture picture) { final SkPicture skPicture = picture as SkPicture; - skCanvas.callMethod('drawPicture', [skPicture.skPicture]); + skCanvas.callMethod( + 'drawPicture', [skPicture.skPicture.skiaObject]); } // TODO(hterkelsen): https://github.com/flutter/flutter/issues/58824 @@ -211,8 +211,8 @@ class SkCanvas { void drawShadow(ui.Path path, ui.Color color, double elevation, bool transparentOccluder) { - drawSkShadow(skCanvas, path as SkPath, color, elevation, transparentOccluder, - ui.window.devicePixelRatio); + drawSkShadow(skCanvas, path as SkPath, color, elevation, + transparentOccluder, ui.window.devicePixelRatio); } void drawVertices( @@ -243,7 +243,6 @@ class SkCanvas { } void saveLayer(ui.Rect bounds, SkPaint paint) { - assert(bounds != null, 'Use saveLayerWithoutBounds'); // ignore: unnecessary_null_comparison skCanvas.callMethod('saveLayer', [ makeSkRect(bounds), paint.skiaObject, @@ -260,7 +259,7 @@ class SkCanvas { 'saveLayer', [ null, - skImageFilter.skImageFilter, + skImageFilter.skiaObject, 0, makeSkRect(bounds), ], diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/color_filter.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/color_filter.dart index f788bd73015..8696f8543fb 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/color_filter.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/color_filter.dart @@ -6,36 +6,52 @@ part of engine; /// A [ui.ColorFilter] backed by Skia's [SkColorFilter]. -class SkColorFilter { - js.JsObject? skColorFilter; +class SkColorFilter extends ResurrectableSkiaObject { + final EngineColorFilter _engineFilter; - SkColorFilter.mode(EngineColorFilter filter) { - setSharedSkColor1(filter._color!); - skColorFilter = - canvasKit['SkColorFilter'].callMethod('MakeBlend', [ - sharedSkColor1, - makeSkBlendMode(filter._blendMode), - ]); - } + SkColorFilter.mode(EngineColorFilter filter) : _engineFilter = filter; - SkColorFilter.matrix(EngineColorFilter filter) { - // TODO(het): Find a way to remove these array conversions. - final js.JsArray colorMatrix = js.JsArray(); - colorMatrix.length = 20; - for (int i = 0; i < 20; i++) { - colorMatrix[i] = filter._matrix![i]; + SkColorFilter.matrix(EngineColorFilter filter) : _engineFilter = filter; + + SkColorFilter.linearToSrgbGamma(EngineColorFilter filter) + : _engineFilter = filter; + + SkColorFilter.srgbToLinearGamma(EngineColorFilter filter) + : _engineFilter = filter; + + js.JsObject _createSkiaObjectFromFilter() { + switch (_engineFilter._type) { + case EngineColorFilter._TypeMode: + setSharedSkColor1(_engineFilter._color!); + return canvasKit['SkColorFilter'].callMethod('MakeBlend', [ + sharedSkColor1, + makeSkBlendMode(_engineFilter._blendMode), + ]); + case EngineColorFilter._TypeMatrix: + final js.JsArray colorMatrix = js.JsArray(); + colorMatrix.length = 20; + for (int i = 0; i < 20; i++) { + colorMatrix[i] = _engineFilter._matrix![i]; + } + return canvasKit['SkColorFilter'] + .callMethod('MakeMatrix', [colorMatrix]); + case EngineColorFilter._TypeLinearToSrgbGamma: + return canvasKit['SkColorFilter'].callMethod('MakeLinearToSRGBGamma'); + case EngineColorFilter._TypeSrgbToLinearGamma: + return canvasKit['SkColorFilter'].callMethod('MakeSRGBToLinearGamma'); + default: + throw StateError( + 'Unknown mode ${_engineFilter._type} for ColorFilter.'); } - skColorFilter = canvasKit['SkColorFilter'] - .callMethod('MakeMatrix', [colorMatrix]); } - SkColorFilter.linearToSrgbGamma(EngineColorFilter filter) { - skColorFilter = - canvasKit['SkColorFilter'].callMethod('MakeLinearToSRGBGamma'); + @override + js.JsObject createDefault() { + return _createSkiaObjectFromFilter(); } - SkColorFilter.srgbToLinearGamma(EngineColorFilter filter) { - skColorFilter = - canvasKit['SkColorFilter'].callMethod('MakeSRGBToLinearGamma'); + @override + js.JsObject resurrect() { + return _createSkiaObjectFromFilter(); } } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/image_filter.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/image_filter.dart index 79cc64abadf..76aed376ebc 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/image_filter.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/image_filter.dart @@ -2,32 +2,35 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. - part of engine; /// The CanvasKit implementation of [ui.ImageFilter]. /// /// Currently only supports `blur`. -class SkImageFilter implements ui.ImageFilter { - js.JsObject? skImageFilter; - +class SkImageFilter extends ResurrectableSkiaObject implements ui.ImageFilter { SkImageFilter.blur({double sigmaX = 0.0, double sigmaY = 0.0}) : _sigmaX = sigmaX, - _sigmaY = sigmaY { - skImageFilter = canvasKit['SkImageFilter'].callMethod( - 'MakeBlur', - [ - sigmaX, - sigmaY, - canvasKit['TileMode']['Clamp'], - null, - ], - ); - } + _sigmaY = sigmaY; final double _sigmaX; final double _sigmaY; + @override + js.JsObject createDefault() => _initSkiaObject(); + + @override + js.JsObject resurrect() => _initSkiaObject(); + + js.JsObject _initSkiaObject() => canvasKit['SkImageFilter'].callMethod( + 'MakeBlur', + [ + _sigmaX, + _sigmaY, + canvasKit['TileMode']['Clamp'], + null, + ], + ); + @override bool operator ==(dynamic other) { if (other is! SkImageFilter) { diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/layer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/layer.dart index db70bbcab48..071d363b047 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/layer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/layer.dart @@ -150,7 +150,8 @@ class ClipPathLayer extends ContainerLayer { assert(needsPainting); paintContext.internalNodesCanvas.save(); - paintContext.internalNodesCanvas.clipPath(_clipPath, _clipBehavior != ui.Clip.hardEdge); + paintContext.internalNodesCanvas + .clipPath(_clipPath, _clipBehavior != ui.Clip.hardEdge); if (_clipBehavior == ui.Clip.antiAliasWithSaveLayer) { paintContext.internalNodesCanvas.saveLayer(paintBounds, null); diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/layer_scene_builder.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/layer_scene_builder.dart index e7377b915a6..a6327183911 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/layer_scene_builder.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/layer_scene_builder.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. - part of engine; class LayerScene implements ui.Scene { @@ -49,7 +48,8 @@ class LayerSceneBuilder implements ui.SceneBuilder { bool isComplexHint = false, bool willChangeHint = false, }) { - currentLayer!.add(PictureLayer(picture as SkPicture, offset, isComplexHint, willChangeHint)); + currentLayer!.add(PictureLayer( + picture as SkPicture, offset, isComplexHint, willChangeHint)); } @override diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/mask_filter.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/mask_filter.dart new file mode 100644 index 00000000000..18b3bff698a --- /dev/null +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/mask_filter.dart @@ -0,0 +1,42 @@ +// 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. + +part of engine; + +/// The CanvasKit implementation of [ui.MaskFilter]. +class SkMaskFilter extends ResurrectableSkiaObject { + SkMaskFilter.blur(ui.BlurStyle blurStyle, double sigma) + : _blurStyle = blurStyle, + _sigma = sigma; + + final ui.BlurStyle _blurStyle; + final double _sigma; + + @override + js.JsObject createDefault() => _initSkiaObject(); + + @override + js.JsObject resurrect() => _initSkiaObject(); + + js.JsObject _initSkiaObject() { + js.JsObject skBlurStyle; + switch (_blurStyle) { + case ui.BlurStyle.normal: + skBlurStyle = canvasKit['BlurStyle']['Normal']; + break; + case ui.BlurStyle.solid: + skBlurStyle = canvasKit['BlurStyle']['Solid']; + break; + case ui.BlurStyle.outer: + skBlurStyle = canvasKit['BlurStyle']['Outer']; + break; + case ui.BlurStyle.inner: + skBlurStyle = canvasKit['BlurStyle']['Inner']; + break; + } + + return canvasKit + .callMethod('MakeBlurMaskFilter', [skBlurStyle, _sigma, true]); + } +} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/painting.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/painting.dart index 1010018bf7f..3334015a92a 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/painting.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/painting.dart @@ -9,7 +9,7 @@ part of engine; /// /// This class is backed by a Skia object that must be explicitly /// deleted to avoid a memory leak. This is done by extending [SkiaObject]. -class SkPaint extends SkiaObject implements ui.Paint { +class SkPaint extends ResurrectableSkiaObject implements ui.Paint { SkPaint(); static const ui.Color _defaultPaintColor = ui.Color(0xFF000000); @@ -22,7 +22,7 @@ class SkPaint extends SkiaObject implements ui.Paint { @override set blendMode(ui.BlendMode value) { _blendMode = value; - _syncBlendMode(skiaObject!); + _syncBlendMode(skiaObject); } void _syncBlendMode(js.JsObject object) { @@ -41,7 +41,7 @@ class SkPaint extends SkiaObject implements ui.Paint { _syncStyle(skiaObject); } - void _syncStyle(js.JsObject? object) { + void _syncStyle(js.JsObject object) { js.JsObject? skPaintStyle; switch (_style) { case ui.PaintingStyle.stroke: @@ -51,7 +51,7 @@ class SkPaint extends SkiaObject implements ui.Paint { skPaintStyle = _skPaintStyleFill; break; } - object!.callMethod('setStyle', [skPaintStyle]); + object.callMethod('setStyle', [skPaintStyle]); } ui.PaintingStyle _style = ui.PaintingStyle.fill; @@ -61,7 +61,7 @@ class SkPaint extends SkiaObject implements ui.Paint { @override set strokeWidth(double value) { _strokeWidth = value; - _syncStrokeWidth(skiaObject!); + _syncStrokeWidth(skiaObject); } void _syncStrokeWidth(js.JsObject object) { @@ -95,7 +95,7 @@ class SkPaint extends SkiaObject implements ui.Paint { @override set isAntiAlias(bool value) { _isAntiAlias = value; - _syncAntiAlias(skiaObject!); + _syncAntiAlias(skiaObject); } void _syncAntiAlias(js.JsObject object) { @@ -109,7 +109,7 @@ class SkPaint extends SkiaObject implements ui.Paint { @override set color(ui.Color value) { _color = value; - _syncColor(skiaObject!); + _syncColor(skiaObject); } void _syncColor(js.JsObject object) { @@ -135,7 +135,7 @@ class SkPaint extends SkiaObject implements ui.Paint { @override set shader(ui.Shader? value) { _shader = value as EngineShader?; - _syncShader(skiaObject!); + _syncShader(skiaObject); } void _syncShader(js.JsObject object) { @@ -156,32 +156,15 @@ class SkPaint extends SkiaObject implements ui.Paint { _syncMaskFilter(skiaObject); } - void _syncMaskFilter(js.JsObject? object) { - js.JsObject? skMaskFilter; + void _syncMaskFilter(js.JsObject object) { + SkMaskFilter? skMaskFilter; if (_maskFilter != null) { final ui.BlurStyle blurStyle = _maskFilter!.webOnlyBlurStyle; final double sigma = _maskFilter!.webOnlySigma; - js.JsObject? skBlurStyle; - switch (blurStyle) { - case ui.BlurStyle.normal: - skBlurStyle = canvasKit['BlurStyle']['Normal']; - break; - case ui.BlurStyle.solid: - skBlurStyle = canvasKit['BlurStyle']['Solid']; - break; - case ui.BlurStyle.outer: - skBlurStyle = canvasKit['BlurStyle']['Outer']; - break; - case ui.BlurStyle.inner: - skBlurStyle = canvasKit['BlurStyle']['Inner']; - break; - } - - skMaskFilter = canvasKit.callMethod( - 'MakeBlurMaskFilter', [skBlurStyle, sigma, true]); + skMaskFilter = SkMaskFilter.blur(blurStyle, sigma); } - object!.callMethod('setMaskFilter', [skMaskFilter]); + object.callMethod('setMaskFilter', [skMaskFilter?.skiaObject]); } ui.MaskFilter? _maskFilter; @@ -220,14 +203,14 @@ class SkPaint extends SkiaObject implements ui.Paint { @override set colorFilter(ui.ColorFilter? value) { _colorFilter = value as EngineColorFilter?; - _syncColorFilter(skiaObject!); + _syncColorFilter(skiaObject); } void _syncColorFilter(js.JsObject object) { js.JsObject? skColorFilterJs; if (_colorFilter != null) { - SkColorFilter skFilter = _colorFilter!._toSkColorFilter()!; - skColorFilterJs = skFilter.skColorFilter; + SkColorFilter? skFilter = _colorFilter!._toSkColorFilter(); + skColorFilterJs = skFilter!.skiaObject; } object.callMethod('setColorFilter', [skColorFilterJs]); } @@ -249,13 +232,13 @@ class SkPaint extends SkiaObject implements ui.Paint { @override set imageFilter(ui.ImageFilter? value) { _imageFilter = value as SkImageFilter?; - _syncImageFilter(skiaObject!); + _syncImageFilter(skiaObject); } void _syncImageFilter(js.JsObject object) { js.JsObject? imageFilterJs; if (_imageFilter != null) { - imageFilterJs = _imageFilter!.skImageFilter; + imageFilterJs = _imageFilter!.skiaObject; } object.callMethod('setImageFilter', [imageFilterJs]); } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/picture.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/picture.dart index 817f9b187a7..b1a5a591080 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/picture.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/picture.dart @@ -5,7 +5,7 @@ part of engine; class SkPicture implements ui.Picture { - final js.JsObject? skPicture; + final SkiaObject skPicture; final ui.Rect? cullRect; SkPicture(this.skPicture, this.cullRect); @@ -15,7 +15,7 @@ class SkPicture implements ui.Picture { @override void dispose() { - // TODO: implement dispose + skPicture.delete(); } @override diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/picture_recorder.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/picture_recorder.dart index 15722eee3e5..8a8a561b011 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/picture_recorder.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/picture_recorder.dart @@ -2,7 +2,6 @@ // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. - part of engine; class SkPictureRecorder implements ui.PictureRecorder { @@ -29,7 +28,8 @@ class SkPictureRecorder implements ui.PictureRecorder { _recorder!.callMethod('finishRecordingAsPicture'); _recorder!.callMethod('delete'); _recorder = null; - return SkPicture(skPicture, _cullRect); + + return SkPicture(OneShotSkiaObject(skPicture), _cullRect); } @override diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/skia_object_cache.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/skia_object_cache.dart new file mode 100644 index 00000000000..d46f9ecaf3f --- /dev/null +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/skia_object_cache.dart @@ -0,0 +1,279 @@ +// 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. + +part of engine; + +/// A cache of Skia objects whose memory Flutter manages. +/// +/// When using Skia, Flutter creates Skia objects which are allocated in +/// WASM memory and which must be explicitly deleted. In the case of Flutter +/// mobile, the Skia objects are wrapped by a C++ class which is destroyed +/// when the associated Dart object is garbage collected. +/// +/// On the web, we cannot tell when a Dart object is garbage collected, so +/// we must use other strategies to know when to delete a Skia object. Some +/// objects, like [ui.Paint], can safely delete their associated Skia object +/// because they can always recreate the Skia object from data stored in the +/// Dart object. Other objects, like [ui.Picture], can be serialized to a +/// JS-managed data structure when they are deleted so that when the associated +/// object is garbage collected, so is the serialized data. +class SkiaObjectCache { + final int maximumSize; + + /// A doubly linked list of the objects in the cache. + /// + /// This makes it fast to move a recently used object to the front. + final DoubleLinkedQueue _itemQueue; + + /// A map of objects to their associated node in the [_itemQueue]. + /// + /// This makes it fast to find the node in the queue when we need to + /// move the object to the front of the queue. + final Map> _itemMap; + + SkiaObjectCache(this.maximumSize) + : _itemQueue = DoubleLinkedQueue(), + _itemMap = >{}; + + /// The number of objects in the cache. + int get length => _itemQueue.length; + + /// Whether or not [object] is in the cache. + /// + /// This is only for testing. + @visibleForTesting + bool debugContains(SkiaObject object) { + return _itemMap.containsKey(object); + } + + /// Adds [object] to the cache. + /// + /// If adding [object] causes the total size of the cache to exceed + /// [maximumSize], then the least recently used half of the cache + /// will be deleted. + void add(SkiaObject object) { + _itemQueue.addFirst(object); + _itemMap[object] = _itemQueue.firstEntry()!; + + if (_itemQueue.length > maximumSize) { + SkiaObjects.markCacheForResize(this); + } + } + + /// Records that [object] was used in the most recent frame. + void markUsed(SkiaObject object) { + DoubleLinkedQueueEntry item = _itemMap[object]!; + item.remove(); + _itemQueue.addFirst(object); + _itemMap[object] = _itemQueue.firstEntry()!; + } + + /// Deletes the least recently used half of this cache. + void resize() { + final int itemsToDelete = maximumSize ~/ 2; + for (int i = 0; i < itemsToDelete; i++) { + final SkiaObject oldObject = _itemQueue.removeLast(); + _itemMap.remove(oldObject); + oldObject.delete(); + } + } +} + +/// An object backed by a [js.JsObject] mapped onto a Skia C++ object in the +/// WebAssembly heap. +/// +/// These objects are automatically deleted when no longer used. +abstract class SkiaObject { + /// The JavaScript object that's mapped onto a Skia C++ object in the WebAssembly heap. + js.JsObject? get skiaObject; + + /// Deletes the associated C++ object from the WebAssembly heap. + void delete(); +} + +/// A [SkiaObject] that can resurrect its C++ counterpart. +/// +/// Because there is no feedback from JavaScript's GC (no destructors or +/// finalizers), we pessimistically delete the underlying C++ object before +/// the Dart object is garbage-collected. The current algorithm deletes objects +/// at the end of every frame. This allows reusing the C++ objects within the +/// frame. In the future we may add smarter strategies that will allow us to +/// reuse C++ objects across frames. +/// +/// The lifecycle of a C++ object is as follows: +/// +/// - Create default: when instantiating a C++ object for a Dart object for the +/// first time, the C++ object is populated with default data (the defaults are +/// defined by Flutter; Skia defaults are corrected if necessary). The +/// default object is created by [createDefault]. +/// - Zero or more cycles of delete + resurrect: when a Dart object is reused +/// after its C++ object is deleted we create a new C++ object populated with +/// data from the current state of the Dart object. This is done using the +/// [resurrect] method. +/// - Final delete: if a Dart object is never reused, it is GC'd after its +/// underlying C++ object is deleted. This is implemented by [SkiaObjects]. +abstract class ResurrectableSkiaObject extends SkiaObject { + ResurrectableSkiaObject() { + _skiaObject = createDefault(); + if (isResurrectionExpensive) { + SkiaObjects.manageExpensive(this); + } else { + SkiaObjects.manageResurrectable(this); + } + } + + @override + js.JsObject get skiaObject { + if (_skiaObject == null) { + _skiaObject = resurrect(); + if (isResurrectionExpensive) { + SkiaObjects.manageExpensive(this); + } else { + SkiaObjects.manageResurrectable(this); + } + } + return _skiaObject!; + } + + /// Do not use this field outside this class. Use [skiaObject] instead. + js.JsObject? _skiaObject; + + /// Instantiates a new Skia-backed JavaScript object containing default + /// values. + /// + /// The object is expected to represent Flutter's defaults. If Skia uses + /// different defaults from those used by Flutter, this method is expected + /// initialize the object to Flutter's defaults. + js.JsObject createDefault(); + + /// Creates a new Skia-backed JavaScript object containing data representing + /// the current state of the Dart object. + js.JsObject resurrect(); + + /// Whether or not it is expensive to resurrect this object. + /// + /// Defaults to false. + bool get isResurrectionExpensive => false; + + @override + void delete() { + _skiaObject!.callMethod('delete'); + _skiaObject = null; + } +} + +// TODO(hterkelsen): [OneShotSkiaObject] is dangerous because it might delete +// the underlying Skia object while the associated Dart object is still in +// use. This issue discusses ways to address this: +// https://github.com/flutter/flutter/issues/60401 +/// A [SkiaObject] which is deleted once and cannot be used again. +class OneShotSkiaObject extends SkiaObject { + js.JsObject? _skiaObject; + + OneShotSkiaObject(this._skiaObject) { + SkiaObjects.manageOneShot(this); + } + + @override + js.JsObject? get skiaObject { + if (_skiaObject == null) { + throw StateError('Attempting to use a Skia object that has been freed.'); + } + SkiaObjects.oneShotCache.markUsed(this); + return _skiaObject; + } + + @override + void delete() { + _skiaObject!.callMethod('delete'); + _skiaObject = null; + } +} + +/// Singleton that manages the lifecycles of [SkiaObject] instances. +class SkiaObjects { + // TODO(yjbanov): some sort of LRU strategy would allow us to reuse objects + // beyond a single frame. + @visibleForTesting + static final List resurrectableObjects = + []; + + @visibleForTesting + static int maximumCacheSize = 8192; + + @visibleForTesting + static final SkiaObjectCache oneShotCache = SkiaObjectCache(maximumCacheSize); + + @visibleForTesting + static final SkiaObjectCache expensiveCache = + SkiaObjectCache(maximumCacheSize); + + @visibleForTesting + static final List cachesToResize = []; + + static bool _addedCleanupCallback = false; + + @visibleForTesting + static void registerCleanupCallback() { + if (_addedCleanupCallback) { + return; + } + window.rasterizer!.addPostFrameCallback(postFrameCleanUp); + _addedCleanupCallback = true; + } + + /// Starts managing the lifecycle of resurrectable [object]. + /// + /// These can safely be deleted at any time. + static void manageResurrectable(ResurrectableSkiaObject object) { + registerCleanupCallback(); + resurrectableObjects.add(object); + } + + /// Starts managing the lifecycle of a one-shot [object]. + /// + /// We should avoid deleting these whenever we can, since we won't + /// be able to resurrect them. + static void manageOneShot(OneShotSkiaObject object) { + registerCleanupCallback(); + oneShotCache.add(object); + } + + /// Starts managing the lifecycle of a resurrectable object that is expensive. + /// + /// Since it's expensive to resurrect, we shouldn't just delete it after every + /// frame. Instead, add it to a cache and only delete it when the cache fills. + static void manageExpensive(ResurrectableSkiaObject object) { + registerCleanupCallback(); + expensiveCache.add(object); + } + + /// Marks that [cache] has overflown its maximum size and show be resized. + static void markCacheForResize(SkiaObjectCache cache) { + registerCleanupCallback(); + if (cachesToResize.contains(cache)) { + return; + } + cachesToResize.add(cache); + } + + /// Cleans up managed Skia memory. + static void postFrameCleanUp() { + if (resurrectableObjects.isEmpty && cachesToResize.isEmpty) { + return; + } + + for (int i = 0; i < resurrectableObjects.length; i++) { + final SkiaObject object = resurrectableObjects[i]; + object.delete(); + } + resurrectableObjects.clear(); + + for (int i = 0; i < cachesToResize.length; i++) { + final SkiaObjectCache cache = cachesToResize[i]; + cache.resize(); + } + cachesToResize.clear(); + } +} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/surface.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/surface.dart index 9d54f37adfa..ce26e4ed9c8 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/surface.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/surface.dart @@ -79,9 +79,8 @@ class Surface { final SkSurface? currentSurface = _surface; if (currentSurface != null) { - final bool isSameSize = - size.width == currentSurface.width() && - size.height == currentSurface.height(); + final bool isSameSize = size.width == currentSurface.width() && + size.height == currentSurface.height(); if (isSameSize) { // The existing surface is still reusable. return; @@ -131,7 +130,7 @@ class Surface { } htmlElement = htmlCanvas; - return SkSurface(skSurface, glContext); + return SkSurface(skSurface, grContext, glContext); } bool _presentSurface() { @@ -144,9 +143,10 @@ class Surface { /// A Dart wrapper around Skia's SkSurface. class SkSurface { final js.JsObject _surface; + final js.JsObject _grContext; final int _glContext; - SkSurface(this._surface, this._glContext); + SkSurface(this._surface, this._grContext, this._glContext); SkCanvas getCanvas() { final js.JsObject skCanvas = _surface.callMethod('getCanvas'); @@ -160,5 +160,7 @@ class SkSurface { void dispose() { _surface.callMethod('dispose'); + _grContext.callMethod('releaseResourcesAndAbandonContext'); + _grContext.callMethod('delete'); } } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/text.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/text.dart index 8b8be4914c5..18c4098f29b 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/text.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/text.dart @@ -282,40 +282,90 @@ Map toSkFontStyle( return style; } -class SkParagraph implements ui.Paragraph { - SkParagraph(this.skParagraph, this._textDirection, this._fontFamily); +class SkParagraph extends ResurrectableSkiaObject implements ui.Paragraph { + SkParagraph( + this._initialParagraph, this._paragraphStyle, this._paragraphCommands); - final js.JsObject? skParagraph; - final ui.TextDirection? _textDirection; - final String? _fontFamily; + /// The result of calling `build()` on the JS SkParagraphBuilder. + /// + /// This may be invalidated later. + final js.JsObject _initialParagraph; + + /// The paragraph style used to build this paragraph. + /// + /// This is used to resurrect the paragraph if the initial paragraph + /// is deleted. + final SkParagraphStyle _paragraphStyle; + + /// The paragraph builder commands used to build this paragraph. + /// + /// This is used to resurrect the paragraph if the initial paragraph + /// is deleted. + final List<_ParagraphCommand> _paragraphCommands; + + /// The constraints from the last time we layed the paragraph out. + /// + /// This is used to resurrect the paragraph if the initial paragraph + /// is deleted. + ui.ParagraphConstraints? _lastLayoutConstraints; + + @override + js.JsObject createDefault() => _initialParagraph; + + @override + js.JsObject resurrect() { + final builder = SkParagraphBuilder(_paragraphStyle); + for (_ParagraphCommand command in _paragraphCommands) { + switch (command.type) { + case _ParagraphCommandType.addText: + builder.addText(command.text!); + break; + case _ParagraphCommandType.pop: + builder.pop(); + break; + case _ParagraphCommandType.pushStyle: + builder.pushStyle(command.style!); + break; + } + } + + final js.JsObject result = builder._buildSkParagraph(); + if (_lastLayoutConstraints != null) { + // We need to set the Skia object early so layout works. + _skiaObject = result; + this.layout(_lastLayoutConstraints!); + } + return result; + } + + @override + bool get isResurrectionExpensive => true; @override double get alphabeticBaseline => - skParagraph!.callMethod('getAlphabeticBaseline'); + skiaObject.callMethod('getAlphabeticBaseline'); @override - bool get didExceedMaxLines => skParagraph!.callMethod('didExceedMaxLines'); + bool get didExceedMaxLines => skiaObject.callMethod('didExceedMaxLines'); @override - double get height => skParagraph!.callMethod('getHeight'); + double get height => skiaObject.callMethod('getHeight'); @override double get ideographicBaseline => - skParagraph!.callMethod('getIdeographicBaseline'); + skiaObject.callMethod('getIdeographicBaseline'); @override - double get longestLine => skParagraph!.callMethod('getLongestLine'); + double get longestLine => skiaObject.callMethod('getLongestLine'); @override - double get maxIntrinsicWidth => - skParagraph!.callMethod('getMaxIntrinsicWidth'); + double get maxIntrinsicWidth => skiaObject.callMethod('getMaxIntrinsicWidth'); @override - double get minIntrinsicWidth => - skParagraph!.callMethod('getMinIntrinsicWidth'); + double get minIntrinsicWidth => skiaObject.callMethod('getMinIntrinsicWidth'); @override - double get width => skParagraph!.callMethod('getMaxWidth'); + double get width => skiaObject.callMethod('getMaxWidth'); // TODO(hterkelsen): Implement placeholders once it's in CanvasKit @override @@ -361,7 +411,7 @@ class SkParagraph implements ui.Paragraph { } List skRects = - skParagraph!.callMethod('getRectsForRange', [ + skiaObject.callMethod('getRectsForRange', [ start, end, heightStyle, @@ -377,7 +427,7 @@ class SkParagraph implements ui.Paragraph { rect['fTop'], rect['fRight'], rect['fBottom'], - _textDirection!, + _paragraphStyle._textDirection!, )); } @@ -387,7 +437,7 @@ class SkParagraph implements ui.Paragraph { @override ui.TextPosition getPositionForOffset(ui.Offset offset) { js.JsObject positionWithAffinity = - skParagraph!.callMethod('getGlyphPositionAtCoordinate', [ + skiaObject.callMethod('getGlyphPositionAtCoordinate', [ offset.dx, offset.dy, ]); @@ -397,13 +447,14 @@ class SkParagraph implements ui.Paragraph { @override ui.TextRange getWordBoundary(ui.TextPosition position) { js.JsObject skRange = - skParagraph!.callMethod('getWordBoundary', [position.offset]); + skiaObject.callMethod('getWordBoundary', [position.offset]); return ui.TextRange(start: skRange['start'], end: skRange['end']); } @override void layout(ui.ParagraphConstraints constraints) { assert(constraints.width != null); // ignore: unnecessary_null_comparison + _lastLayoutConstraints = constraints; // Infinite width breaks layout, just use a very large number instead. // TODO(het): Remove this once https://bugs.chromium.org/p/skia/issues/detail?id=9874 @@ -418,10 +469,11 @@ class SkParagraph implements ui.Paragraph { // TODO(het): CanvasKit throws an exception when laid out with // a font that wasn't registered. try { - skParagraph!.callMethod('layout', [width]); + skiaObject.callMethod('layout', [width]); } catch (e) { html.window.console.warn('CanvasKit threw an exception while laying ' - 'out the paragraph. The font was "$_fontFamily". Exception:\n$e'); + 'out the paragraph. The font was "${_paragraphStyle._fontFamily}". ' + 'Exception:\n$e'); rethrow; } } @@ -441,17 +493,16 @@ class SkParagraph implements ui.Paragraph { class SkParagraphBuilder implements ui.ParagraphBuilder { js.JsObject? _paragraphBuilder; - ui.TextDirection? _textDirection; - String? _fontFamily; + final SkParagraphStyle _style; + final List<_ParagraphCommand> _commands; - SkParagraphBuilder(ui.ParagraphStyle style) { - SkParagraphStyle skStyle = style as SkParagraphStyle; - _textDirection = skStyle._textDirection; - _fontFamily = skStyle._fontFamily; + SkParagraphBuilder(ui.ParagraphStyle style) + : _commands = <_ParagraphCommand>[], + _style = style as SkParagraphStyle { _paragraphBuilder = canvasKit['ParagraphBuilder'].callMethod( 'Make', [ - skStyle.skParagraphStyle, + _style.skParagraphStyle, skiaFontCollection.skFontMgr, ], ); @@ -472,16 +523,22 @@ class SkParagraphBuilder implements ui.ParagraphBuilder { @override void addText(String text) { + _commands.add(_ParagraphCommand.addText(text)); _paragraphBuilder!.callMethod('addText', [text]); } @override ui.Paragraph build() { - final SkParagraph paragraph = SkParagraph( - _paragraphBuilder!.callMethod('build'), _textDirection, _fontFamily); + final builtParagraph = _buildSkParagraph(); + return SkParagraph(builtParagraph, _style, _commands); + } + + /// Builds the SkParagraph with the builder and deletes the builder. + js.JsObject _buildSkParagraph() { + final js.JsObject result = _paragraphBuilder!.callMethod('build'); _paragraphBuilder!.callMethod('delete'); _paragraphBuilder = null; - return paragraph; + return result; } @override @@ -493,13 +550,37 @@ class SkParagraphBuilder implements ui.ParagraphBuilder { @override void pop() { + _commands.add(const _ParagraphCommand.pop()); _paragraphBuilder!.callMethod('pop'); } @override void pushStyle(ui.TextStyle style) { final SkTextStyle skStyle = style as SkTextStyle; + _commands.add(_ParagraphCommand.pushStyle(skStyle)); _paragraphBuilder! .callMethod('pushStyle', [skStyle.skTextStyle]); } } + +class _ParagraphCommand { + final _ParagraphCommandType type; + final String? text; + final SkTextStyle? style; + + const _ParagraphCommand._(this.type, this.text, this.style); + + const _ParagraphCommand.addText(String text) + : this._(_ParagraphCommandType.addText, text, null); + + const _ParagraphCommand.pop() : this._(_ParagraphCommandType.pop, null, null); + + const _ParagraphCommand.pushStyle(SkTextStyle style) + : this._(_ParagraphCommandType.pushStyle, null, style); +} + +enum _ParagraphCommandType { + addText, + pop, + pushStyle, +} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/util.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/util.dart index 0f140de94d1..1d983ef6ef0 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/util.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/compositor/util.dart @@ -15,97 +15,6 @@ class CanvasKitError extends Error { String toString() => 'CanvasKitError: $message'; } -/// An object backed by a [js.JsObject] mapped onto a Skia C++ object in the -/// WebAssembly heap. -/// -/// These objects are automatically deleted when no longer used. -/// -/// Because there is no feedback from JavaScript's GC (no destructors or -/// finalizers), we pessimistically delete the underlying C++ object before -/// the Dart object is garbage-collected. The current algorithm deletes objects -/// at the end of every frame. This allows reusing the C++ objects within the -/// frame. In the future we may add smarter strategies that will allow us to -/// reuse C++ objects across frames. -/// -/// The lifecycle of a C++ object is as follows: -/// -/// - Create default: when instantiating a C++ object for a Dart object for the -/// first time, the C++ object is populated with default data (the defaults are -/// defined by Flutter; Skia defaults are corrected if necessary). The -/// default object is created by [createDefault]. -/// - Zero or more cycles of delete + resurrect: when a Dart object is reused -/// after its C++ object is deleted we create a new C++ object populated with -/// data from the current state of the Dart object. This is done using the -/// [resurrect] method. -/// - Final delete: if a Dart object is never reused, it is GC'd after its -/// underlying C++ object is deleted. This is implemented by [SkiaObjects]. -abstract class SkiaObject { - SkiaObject() { - _skiaObject = createDefault(); - SkiaObjects.manage(this); - } - - /// The JavaScript object that's mapped onto a Skia C++ object in the WebAssembly heap. - js.JsObject? get skiaObject { - if (_skiaObject == null) { - _skiaObject = resurrect(); - SkiaObjects.manage(this); - } - return _skiaObject; - } - - /// Do not use this field outside this class. Use [skiaObject] instead. - js.JsObject? _skiaObject; - - /// Instantiates a new Skia-backed JavaScript object containing default - /// values. - /// - /// The object is expected to represent Flutter's defaults. If Skia uses - /// different defaults from those used by Flutter, this method is expected - /// initialize the object to Flutter's defaults. - js.JsObject createDefault(); - - /// Creates a new Skia-backed JavaScript object containing data representing - /// the current state of the Dart object. - js.JsObject resurrect(); -} - -/// Singleton that manages the lifecycles of [SkiaObject] instances. -class SkiaObjects { - // TODO(yjbanov): some sort of LRU strategy would allow us to reuse objects - // beyond a single frame. - @visibleForTesting - static final List managedObjects = () { - window.rasterizer!.addPostFrameCallback(postFrameCleanUp); - return []; - }(); - - /// Starts managing the lifecycle of [object]. - /// - /// The object's underlying WASM object is deleted by calling the - /// "delete" method when it goes out of scope. - /// - /// The current implementation deletes objects at the end of every frame. - static void manage(SkiaObject object) { - managedObjects.add(object); - } - - /// Deletes all C++ objects created this frame. - static void postFrameCleanUp() { - if (managedObjects.isEmpty) { - return; - } - - for (int i = 0; i < managedObjects.length; i++) { - final SkiaObject object = managedObjects[i]; - object._skiaObject!.callMethod('delete'); - object._skiaObject = null; - } - - managedObjects.clear(); - } -} - /// Converts a list of [ui.Color] into the 2d array expected by CanvasKit. js.JsArray makeColorList(List colors) { var result = js.JsArray(); diff --git a/engine/src/flutter/lib/web_ui/test/canvaskit/skia_objects_cache_test.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/skia_objects_cache_test.dart new file mode 100644 index 00000000000..3999907c600 --- /dev/null +++ b/engine/src/flutter/lib/web_ui/test/canvaskit/skia_objects_cache_test.dart @@ -0,0 +1,170 @@ +// 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. + +// @dart = 2.6 +import 'dart:js'; + +import 'package:mockito/mockito.dart'; +import 'package:test/test.dart'; + +import 'package:ui/src/engine.dart'; + +void main() { + SkiaObjects.maximumCacheSize = 4; + group(ResurrectableSkiaObject, () { + test('implements create, cache, delete, resurrect, delete lifecycle', () { + int addPostFrameCallbackCount = 0; + + MockRasterizer mockRasterizer = MockRasterizer(); + when(mockRasterizer.addPostFrameCallback(any)).thenAnswer((_) { + addPostFrameCallbackCount++; + }); + window.rasterizer = mockRasterizer; + + // Trigger first create + final TestSkiaObject testObject = TestSkiaObject(); + expect(SkiaObjects.resurrectableObjects.single, testObject); + expect(testObject.createDefaultCount, 1); + expect(testObject.resurrectCount, 0); + expect(testObject.deleteCount, 0); + + // Check that the getter does not have side-effects + final JsObject skiaObject1 = testObject.skiaObject; + expect(skiaObject1, isNotNull); + expect(SkiaObjects.resurrectableObjects.single, testObject); + expect(testObject.createDefaultCount, 1); + expect(testObject.resurrectCount, 0); + expect(testObject.deleteCount, 0); + + // Trigger first delete + SkiaObjects.postFrameCleanUp(); + expect(SkiaObjects.resurrectableObjects, isEmpty); + expect(addPostFrameCallbackCount, 1); + expect(testObject.createDefaultCount, 1); + expect(testObject.resurrectCount, 0); + expect(testObject.deleteCount, 1); + + // Trigger resurrect + final JsObject skiaObject2 = testObject.skiaObject; + expect(skiaObject2, isNotNull); + expect(skiaObject2, isNot(same(skiaObject1))); + expect(SkiaObjects.resurrectableObjects.single, testObject); + expect(addPostFrameCallbackCount, 1); + expect(testObject.createDefaultCount, 1); + expect(testObject.resurrectCount, 1); + expect(testObject.deleteCount, 1); + + // Trigger final delete + SkiaObjects.postFrameCleanUp(); + expect(SkiaObjects.resurrectableObjects, isEmpty); + expect(addPostFrameCallbackCount, 1); + expect(testObject.createDefaultCount, 1); + expect(testObject.resurrectCount, 1); + expect(testObject.deleteCount, 2); + }); + + test('is added to SkiaObjects cache if expensive', () { + TestSkiaObject object1 = TestSkiaObject(isExpensive: true); + expect(SkiaObjects.expensiveCache.length, 1); + expect(SkiaObjects.expensiveCache.debugContains(object1), isTrue); + + TestSkiaObject object2 = TestSkiaObject(isExpensive: true); + expect(SkiaObjects.expensiveCache.length, 2); + expect(SkiaObjects.expensiveCache.debugContains(object2), isTrue); + + SkiaObjects.postFrameCleanUp(); + expect(SkiaObjects.expensiveCache.length, 2); + expect(SkiaObjects.expensiveCache.debugContains(object1), isTrue); + expect(SkiaObjects.expensiveCache.debugContains(object2), isTrue); + + /// Add 3 more objects to the cache to overflow it. + TestSkiaObject(isExpensive: true); + TestSkiaObject(isExpensive: true); + TestSkiaObject(isExpensive: true); + expect(SkiaObjects.expensiveCache.length, 5); + expect(SkiaObjects.cachesToResize.length, 1); + + SkiaObjects.postFrameCleanUp(); + expect(object1.deleteCount, 1); + expect(object2.deleteCount, 1); + expect(SkiaObjects.expensiveCache.length, 3); + expect(SkiaObjects.expensiveCache.debugContains(object1), isFalse); + expect(SkiaObjects.expensiveCache.debugContains(object2), isFalse); + }); + }); + + group(OneShotSkiaObject, () { + test('is added to SkiaObjects cache', () { + int deleteCount = 0; + JsObject _makeJsObject() { + return JsObject.jsify({ + 'delete': allowInterop(() { + deleteCount++; + }), + }); + } + + OneShotSkiaObject object1 = OneShotSkiaObject(_makeJsObject()); + expect(SkiaObjects.oneShotCache.length, 1); + expect(SkiaObjects.oneShotCache.debugContains(object1), isTrue); + + OneShotSkiaObject object2 = OneShotSkiaObject(_makeJsObject()); + expect(SkiaObjects.oneShotCache.length, 2); + expect(SkiaObjects.oneShotCache.debugContains(object2), isTrue); + + SkiaObjects.postFrameCleanUp(); + expect(SkiaObjects.oneShotCache.length, 2); + expect(SkiaObjects.oneShotCache.debugContains(object1), isTrue); + expect(SkiaObjects.oneShotCache.debugContains(object2), isTrue); + + // Add 3 more objects to the cache to overflow it. + OneShotSkiaObject(_makeJsObject()); + OneShotSkiaObject(_makeJsObject()); + OneShotSkiaObject(_makeJsObject()); + expect(SkiaObjects.oneShotCache.length, 5); + expect(SkiaObjects.cachesToResize.length, 1); + + SkiaObjects.postFrameCleanUp(); + expect(deleteCount, 2); + expect(SkiaObjects.oneShotCache.length, 3); + expect(SkiaObjects.oneShotCache.debugContains(object1), isFalse); + expect(SkiaObjects.oneShotCache.debugContains(object2), isFalse); + }); + }); +} + +class TestSkiaObject extends ResurrectableSkiaObject { + int createDefaultCount = 0; + int resurrectCount = 0; + int deleteCount = 0; + + final bool isExpensive; + + TestSkiaObject({this.isExpensive = false}); + + JsObject _makeJsObject() { + return JsObject.jsify({ + 'delete': allowInterop(() { + deleteCount++; + }), + }); + } + + @override + JsObject createDefault() { + createDefaultCount++; + return _makeJsObject(); + } + + @override + JsObject resurrect() { + resurrectCount++; + return _makeJsObject(); + } + + @override + bool get isResurrectionExpensive => isExpensive; +} + +class MockRasterizer extends Mock implements Rasterizer {} diff --git a/engine/src/flutter/lib/web_ui/test/canvaskit/util_test.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/util_test.dart deleted file mode 100644 index e9f1f442995..00000000000 --- a/engine/src/flutter/lib/web_ui/test/canvaskit/util_test.dart +++ /dev/null @@ -1,94 +0,0 @@ -// 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. - -// @dart = 2.6 -import 'dart:js'; - -import 'package:mockito/mockito.dart'; -import 'package:test/test.dart'; - -import 'package:ui/src/engine.dart'; - -void main() { - group(SkiaObject, () { - test('implements create, cache, delete, resurrect, delete lifecycle', () { - int addPostFrameCallbackCount = 0; - - MockRasterizer mockRasterizer = MockRasterizer(); - when(mockRasterizer.addPostFrameCallback(any)).thenAnswer((_) { - addPostFrameCallbackCount++; - }); - window.rasterizer = mockRasterizer; - - // Trigger first create - final TestSkiaObject testObject = TestSkiaObject(); - expect(SkiaObjects.managedObjects.single, testObject); - expect(testObject.createDefaultCount, 1); - expect(testObject.resurrectCount, 0); - expect(testObject.deleteCount, 0); - - // Check that the getter does not have side-effects - final JsObject skiaObject1 = testObject.skiaObject; - expect(skiaObject1, isNotNull); - expect(SkiaObjects.managedObjects.single, testObject); - expect(testObject.createDefaultCount, 1); - expect(testObject.resurrectCount, 0); - expect(testObject.deleteCount, 0); - - // Trigger first delete - SkiaObjects.postFrameCleanUp(); - expect(SkiaObjects.managedObjects, isEmpty); - expect(addPostFrameCallbackCount, 1); - expect(testObject.createDefaultCount, 1); - expect(testObject.resurrectCount, 0); - expect(testObject.deleteCount, 1); - - // Trigger resurrect - final JsObject skiaObject2 = testObject.skiaObject; - expect(skiaObject2, isNotNull); - expect(skiaObject2, isNot(same(skiaObject1))); - expect(SkiaObjects.managedObjects.single, testObject); - expect(addPostFrameCallbackCount, 1); - expect(testObject.createDefaultCount, 1); - expect(testObject.resurrectCount, 1); - expect(testObject.deleteCount, 1); - - // Trigger final delete - SkiaObjects.postFrameCleanUp(); - expect(SkiaObjects.managedObjects, isEmpty); - expect(addPostFrameCallbackCount, 1); - expect(testObject.createDefaultCount, 1); - expect(testObject.resurrectCount, 1); - expect(testObject.deleteCount, 2); - }); - }); -} - -class TestSkiaObject extends SkiaObject { - int createDefaultCount = 0; - int resurrectCount = 0; - int deleteCount = 0; - - JsObject _makeJsObject() { - return JsObject.jsify({ - 'delete': allowInterop(() { - deleteCount++; - }), - }); - } - - @override - JsObject createDefault() { - createDefaultCount++; - return _makeJsObject(); - } - - @override - JsObject resurrect() { - resurrectCount++; - return _makeJsObject(); - } -} - -class MockRasterizer extends Mock implements Rasterizer {}