diff --git a/lib/web_ui/lib/src/engine/compositor/canvas.dart b/lib/web_ui/lib/src/engine/compositor/canvas.dart index 9a71ba282539a..65548560cb255 100644 --- a/lib/web_ui/lib/src/engine/compositor/canvas.dart +++ b/lib/web_ui/lib/src/engine/compositor/canvas.dart @@ -63,7 +63,7 @@ class SkCanvas { startAngle * toDegrees, sweepAngle * toDegrees, useCenter, - paint.makeSkPaint(), + paint.skiaObject, ]); } @@ -80,7 +80,7 @@ class SkCanvas { skAtlas.skImage, rects, rstTransforms, - paint.makeSkPaint(), + paint.skiaObject, makeSkBlendMode(blendMode), colors, ]); @@ -91,7 +91,7 @@ class SkCanvas { c.dx, c.dy, radius, - paint.makeSkPaint(), + paint.skiaObject, ]); } @@ -106,7 +106,7 @@ class SkCanvas { skCanvas.callMethod('drawDRRect', [ makeSkRRect(outer), makeSkRRect(inner), - paint.makeSkPaint(), + paint.skiaObject, ]); } @@ -116,7 +116,7 @@ class SkCanvas { skImage.skImage, offset.dx, offset.dy, - paint.makeSkPaint(), + paint.skiaObject, ]); } @@ -126,7 +126,7 @@ class SkCanvas { skImage.skImage, makeSkRect(src), makeSkRect(dst), - paint.makeSkPaint(), + paint.skiaObject, false, ]); } @@ -138,7 +138,7 @@ class SkCanvas { skImage.skImage, makeSkRect(center), makeSkRect(dst), - paint.makeSkPaint(), + paint.skiaObject, ]); } @@ -148,19 +148,19 @@ class SkCanvas { p1.dy, p2.dx, p2.dy, - paint.makeSkPaint(), + paint.skiaObject, ]); } void drawOval(ui.Rect rect, SkPaint paint) { skCanvas.callMethod('drawOval', [ makeSkRect(rect), - paint.makeSkPaint(), + paint.skiaObject, ]); } void drawPaint(SkPaint paint) { - skCanvas.callMethod('drawPaint', [paint.makeSkPaint()]); + skCanvas.callMethod('drawPaint', [paint.skiaObject]); } void drawParagraph(ui.Paragraph paragraph, ui.Offset offset) { @@ -173,7 +173,7 @@ class SkCanvas { } void drawPath(ui.Path path, SkPaint paint) { - final js.JsObject skPaint = paint.makeSkPaint(); + final js.JsObject skPaint = paint.skiaObject; final SkPath enginePath = path; final js.JsObject skPath = enginePath._skPath; skCanvas.callMethod('drawPath', [skPath, skPaint]); @@ -188,20 +188,20 @@ class SkCanvas { skCanvas.callMethod('drawPoints', [ makeSkPointMode(pointMode), points, - paint.makeSkPaint(), + paint.skiaObject, ]); } void drawRRect(ui.RRect rrect, SkPaint paint) { skCanvas.callMethod('drawRRect', [ makeSkRRect(rrect), - paint.makeSkPaint(), + paint.skiaObject, ]); } void drawRect(ui.Rect rect, SkPaint paint) { final js.JsObject skRect = makeSkRect(rect); - final js.JsObject skPaint = paint.makeSkPaint(); + final js.JsObject skPaint = paint.skiaObject; skCanvas.callMethod('drawRect', [skRect, skPaint]); } @@ -217,7 +217,7 @@ class SkCanvas { skCanvas.callMethod('drawVertices', [ skVertices.skVertices, makeSkBlendMode(blendMode), - paint.makeSkPaint() + paint.skiaObject ]); } @@ -241,12 +241,12 @@ class SkCanvas { void saveLayer(ui.Rect bounds, SkPaint paint) { skCanvas.callMethod('saveLayer', [ makeSkRect(bounds), - paint.makeSkPaint(), + paint.skiaObject, ]); } void saveLayerWithoutBounds(SkPaint paint) { - skCanvas.callMethod('saveLayer', [null, paint.makeSkPaint()]); + skCanvas.callMethod('saveLayer', [null, paint.skiaObject]); } void saveLayerWithFilter(ui.Rect bounds, ui.ImageFilter filter) { diff --git a/lib/web_ui/lib/src/engine/compositor/painting.dart b/lib/web_ui/lib/src/engine/compositor/painting.dart index fdc0ab8eac42e..883ab2f9e6c62 100644 --- a/lib/web_ui/lib/src/engine/compositor/painting.dart +++ b/lib/web_ui/lib/src/engine/compositor/painting.dart @@ -5,16 +5,26 @@ part of engine; /// The implementation of [ui.Paint] used by the CanvasKit backend. -class SkPaint implements ui.Paint { +/// +/// 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 { SkPaint(); static const ui.Color _defaultPaintColor = ui.Color(0xFF000000); + static final js.JsObject _skPaintStyleStroke = canvasKit['PaintStyle']['Stroke']; + static final js.JsObject _skPaintStyleFill = canvasKit['PaintStyle']['Fill']; @override ui.BlendMode get blendMode => _blendMode; @override set blendMode(ui.BlendMode value) { _blendMode = value; + _syncBlendMode(skiaObject); + } + void _syncBlendMode(js.JsObject object) { + final js.JsObject skBlendMode = makeSkBlendMode(_blendMode); + object.callMethod('setBlendMode', [skBlendMode]); } ui.BlendMode _blendMode = ui.BlendMode.srcOver; @@ -23,6 +33,19 @@ class SkPaint implements ui.Paint { @override set style(ui.PaintingStyle value) { _style = value; + _syncStyle(skiaObject); + } + void _syncStyle(js.JsObject object) { + js.JsObject skPaintStyle; + switch (_style) { + case ui.PaintingStyle.stroke: + skPaintStyle = _skPaintStyleStroke; + break; + case ui.PaintingStyle.fill: + skPaintStyle = _skPaintStyleFill; + break; + } + object.callMethod('setStyle', [skPaintStyle]); } ui.PaintingStyle _style = ui.PaintingStyle.fill; @@ -31,9 +54,14 @@ class SkPaint implements ui.Paint { @override set strokeWidth(double value) { _strokeWidth = value; + _syncStrokeWidth(skiaObject); + } + void _syncStrokeWidth(js.JsObject object) { + object.callMethod('setStrokeWidth', [strokeWidth]); } double _strokeWidth = 0.0; + // TODO(yjbanov): implement @override ui.StrokeCap get strokeCap => _strokeCap; @override @@ -42,6 +70,7 @@ class SkPaint implements ui.Paint { } ui.StrokeCap _strokeCap = ui.StrokeCap.butt; + // TODO(yjbanov): implement @override ui.StrokeJoin get strokeJoin => _strokeJoin; @override @@ -55,6 +84,10 @@ class SkPaint implements ui.Paint { @override set isAntiAlias(bool value) { _isAntiAlias = value; + _syncAntiAlias(skiaObject); + } + void _syncAntiAlias(js.JsObject object) { + object.callMethod('setAntiAlias', [_isAntiAlias]); } bool _isAntiAlias = true; @@ -63,9 +96,18 @@ class SkPaint implements ui.Paint { @override set color(ui.Color value) { _color = value; + _syncColor(skiaObject); + } + void _syncColor(js.JsObject object) { + int colorValue = _defaultPaintColor.value; + if (_color != null) { + colorValue = _color.value; + } + object.callMethod('setColor', [colorValue]); } ui.Color _color = _defaultPaintColor; + // TODO(yjbanov): implement @override bool get invertColors => _invertColors; @override @@ -79,17 +121,54 @@ class SkPaint implements ui.Paint { @override set shader(ui.Shader value) { _shader = value; + _syncShader(skiaObject); } - ui.Shader _shader; + void _syncShader(js.JsObject object) { + js.JsObject skShader; + if (_shader != null) { + skShader = _shader.createSkiaShader(); + } + object.callMethod('setShader', [skShader]); + } + EngineGradient _shader; @override ui.MaskFilter get maskFilter => _maskFilter; @override set maskFilter(ui.MaskFilter value) { _maskFilter = value; + _syncMaskFilter(skiaObject); + } + void _syncMaskFilter(js.JsObject object) { + js.JsObject 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]); + } + object.callMethod('setMaskFilter', [skMaskFilter]); } ui.MaskFilter _maskFilter; + // TODO(yjbanov): implement @override ui.FilterQuality get filterQuality => _filterQuality; @override @@ -103,9 +182,19 @@ class SkPaint implements ui.Paint { @override set colorFilter(ui.ColorFilter value) { _colorFilter = value; + _syncColorFilter(skiaObject); + } + void _syncColorFilter(js.JsObject object) { + js.JsObject skColorFilterJs; + if (_colorFilter != null) { + SkColorFilter skFilter = _colorFilter._toSkColorFilter(); + skColorFilterJs = skFilter.skColorFilter; + } + object.callMethod('setColorFilter', [skColorFilterJs]); } - ui.ColorFilter _colorFilter; + EngineColorFilter _colorFilter; + // TODO(yjbanov): implement @override double get strokeMiterLimit => _strokeMiterLimit; @override @@ -119,81 +208,38 @@ class SkPaint implements ui.Paint { @override set imageFilter(ui.ImageFilter value) { _imageFilter = value; + _syncImageFilter(skiaObject); } - ui.ImageFilter _imageFilter; - - js.JsObject makeSkPaint() { - final js.JsObject skPaint = js.JsObject(canvasKit['SkPaint']); - - if (shader != null) { - final EngineGradient engineShader = shader; - skPaint.callMethod( - 'setShader', [engineShader.createSkiaShader()]); - } - - if (color != null) { - skPaint.callMethod('setColor', [color.value]); - } - - js.JsObject skPaintStyle; - switch (style) { - case ui.PaintingStyle.stroke: - skPaintStyle = canvasKit['PaintStyle']['Stroke']; - break; - case ui.PaintingStyle.fill: - skPaintStyle = canvasKit['PaintStyle']['Fill']; - break; - } - skPaint.callMethod('setStyle', [skPaintStyle]); - - js.JsObject skBlendMode = makeSkBlendMode(blendMode); - if (skBlendMode != null) { - skPaint.callMethod('setBlendMode', [skBlendMode]); - } - - skPaint.callMethod('setAntiAlias', [isAntiAlias]); - - if (strokeWidth > 0.0) { - skPaint.callMethod('setStrokeWidth', [strokeWidth]); - } - - 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; - } - - final js.JsObject skMaskFilter = canvasKit - .callMethod('MakeBlurMaskFilter', [skBlurStyle, sigma, true]); - skPaint.callMethod('setMaskFilter', [skMaskFilter]); - } - - if (imageFilter != null) { - final SkImageFilter skImageFilter = imageFilter; - skPaint.callMethod( - 'setImageFilter', [skImageFilter.skImageFilter]); + void _syncImageFilter(js.JsObject object) { + js.JsObject imageFilterJs; + if (_imageFilter != null) { + imageFilterJs = _imageFilter.skImageFilter; } + object.callMethod('setImageFilter', [imageFilterJs]); + } + SkImageFilter _imageFilter; - if (colorFilter != null) { - EngineColorFilter engineFilter = colorFilter; - SkColorFilter skFilter = engineFilter._toSkColorFilter(); - skPaint.callMethod('setColorFilter', [skFilter.skColorFilter]); - } + @override + js.JsObject createDefault() { + final obj = js.JsObject(canvasKit['SkPaint']); + // Sync fields whose Skia defaults are different from Flutter's. + _syncAntiAlias(obj); + _syncColor(obj); + return obj; + } - return skPaint; + @override + js.JsObject resurrect() { + final obj = js.JsObject(canvasKit['SkPaint']); + _syncBlendMode(obj); + _syncStyle(obj); + _syncStrokeWidth(obj); + _syncAntiAlias(obj); + _syncColor(obj); + _syncShader(obj); + _syncMaskFilter(obj); + _syncColorFilter(obj); + _syncImageFilter(obj); + return obj; } } diff --git a/lib/web_ui/lib/src/engine/compositor/rasterizer.dart b/lib/web_ui/lib/src/engine/compositor/rasterizer.dart index 66c4b194764ea..c5246e7d03644 100644 --- a/lib/web_ui/lib/src/engine/compositor/rasterizer.dart +++ b/lib/web_ui/lib/src/engine/compositor/rasterizer.dart @@ -9,6 +9,7 @@ class Rasterizer { final Surface surface; final CompositorContext context = CompositorContext(); final HtmlViewEmbedder viewEmbedder = HtmlViewEmbedder(); + final List _postFrameCallbacks = []; Rasterizer(this.surface) { surface.viewEmbedder = viewEmbedder; @@ -17,29 +18,44 @@ class Rasterizer { /// Creates a new frame from this rasterizer's surface, draws the given /// [LayerTree] into it, and then submits the frame. void draw(LayerTree layerTree) { - if (layerTree == null) { - return; + try { + if (layerTree == null) { + return; + } + + final ui.Size physicalSize = ui.window.physicalSize; + final ui.Size frameSize = ui.Size( + physicalSize.width.truncate().toDouble(), + physicalSize.height.truncate().toDouble(), + ); + + if (frameSize.isEmpty) { + return; + } + layerTree.frameSize = frameSize; + + final SurfaceFrame frame = surface.acquireFrame(layerTree.frameSize); + surface.viewEmbedder.frameSize = layerTree.frameSize; + final SkCanvas canvas = frame.skiaCanvas; + final Frame compositorFrame = context.acquireFrame(canvas, surface.viewEmbedder); + + compositorFrame.raster(layerTree, ignoreRasterCache: true); + surface.addToScene(); + frame.submit(); + surface.viewEmbedder.submitFrame(); + } finally { + _runPostFrameCallbacks(); } + } - final ui.Size physicalSize = ui.window.physicalSize; - final ui.Size frameSize = ui.Size( - physicalSize.width.truncate().toDouble(), - physicalSize.height.truncate().toDouble(), - ); + void addPostFrameCallback(ui.VoidCallback callback) { + _postFrameCallbacks.add(callback); + } - if (frameSize.isEmpty) { - return; + void _runPostFrameCallbacks() { + for (int i = 0; i < _postFrameCallbacks.length; i++) { + final ui.VoidCallback callback = _postFrameCallbacks[i]; + callback(); } - layerTree.frameSize = frameSize; - - final SurfaceFrame frame = surface.acquireFrame(layerTree.frameSize); - surface.viewEmbedder.frameSize = layerTree.frameSize; - final SkCanvas canvas = frame.skiaCanvas; - final Frame compositorFrame = context.acquireFrame(canvas, surface.viewEmbedder); - - compositorFrame.raster(layerTree, ignoreRasterCache: true); - surface.addToScene(); - frame.submit(); - surface.viewEmbedder.submitFrame(); } } diff --git a/lib/web_ui/lib/src/engine/compositor/text.dart b/lib/web_ui/lib/src/engine/compositor/text.dart index 4a8c836497592..b266388f4cffc 100644 --- a/lib/web_ui/lib/src/engine/compositor/text.dart +++ b/lib/web_ui/lib/src/engine/compositor/text.dart @@ -172,7 +172,7 @@ class SkTextStyle implements ui.TextStyle { final Map style = {}; if (background != null) { - style['backgroundColor'] = background.makeSkPaint(); + style['backgroundColor'] = background.skiaObject; } if (color != null) { @@ -221,7 +221,7 @@ class SkTextStyle implements ui.TextStyle { } if (foreground != null) { - style['foreground'] = foreground.makeSkPaint(); + style['foreground'] = foreground.skiaObject; } // TODO(hterkelsen): Add support for diff --git a/lib/web_ui/lib/src/engine/compositor/util.dart b/lib/web_ui/lib/src/engine/compositor/util.dart index 9256d395b88ba..8a94bbfe80514 100644 --- a/lib/web_ui/lib/src/engine/compositor/util.dart +++ b/lib/web_ui/lib/src/engine/compositor/util.dart @@ -4,6 +4,97 @@ part of engine; +/// 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(); + } +} + js.JsObject makeSkRect(ui.Rect rect) { return js.JsObject(canvasKit['LTRBRect'], [rect.left, rect.top, rect.right, rect.bottom]); diff --git a/lib/web_ui/lib/src/engine/window.dart b/lib/web_ui/lib/src/engine/window.dart index 69aec0cbeecfe..2ffce9d8087ec 100644 --- a/lib/web_ui/lib/src/engine/window.dart +++ b/lib/web_ui/lib/src/engine/window.dart @@ -160,7 +160,7 @@ class EngineWindow extends ui.Window { case 'flutter/platform_views': if (experimentalUseSkia) { - _rasterizer.viewEmbedder.handlePlatformViewCall(data, callback); + rasterizer.viewEmbedder.handlePlatformViewCall(data, callback); } else { handlePlatformViewCall(data, callback); } @@ -279,15 +279,15 @@ class EngineWindow extends ui.Window { void render(ui.Scene scene) { if (experimentalUseSkia) { final LayerScene layerScene = scene; - _rasterizer.draw(layerScene.layerTree); + rasterizer.draw(layerScene.layerTree); } else { final SurfaceScene surfaceScene = scene; domRenderer.renderScene(surfaceScene.webOnlyRootElement); } } - final Rasterizer _rasterizer = - experimentalUseSkia ? Rasterizer(Surface()) : null; + @visibleForTesting + Rasterizer rasterizer = experimentalUseSkia ? Rasterizer(Surface()) : null; } /// The window singleton. diff --git a/lib/web_ui/test/engine/compositor/util_test.dart b/lib/web_ui/test/engine/compositor/util_test.dart new file mode 100644 index 0000000000000..e46568e0c749d --- /dev/null +++ b/lib/web_ui/test/engine/compositor/util_test.dart @@ -0,0 +1,93 @@ +// 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: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 {}