From a11524896eaecaa2a6a44c14cdb65aa31492e479 Mon Sep 17 00:00:00 2001 From: Jackson Gardner Date: Fri, 6 Jun 2025 09:27:08 -0700 Subject: [PATCH] Lazy paths and frame object arenas (#168996) The lifecycle of `Path` objects are currently not managed by the user. That is to say, there is no `dispose` method on path objects and therefore no explicit way to detect when the user is done with the path object and the native-side object can be exposed. As of right now, we use `FinalizationRegistry` to clean up the native-side objects when the dart-side objects are garbage collected. However, this has a number of issues: * Adding objects to the finalization registry actually ends up prolonging their lifetime in V8, since the V8 garbage collector will only collect them in a major GC and not a minor GC once they are registered with the finalization registry. See the following Chrome bug: https://issues.chromium.org/issues/340777103 * We can run into OOM issues where the linear memory of canvaskit/skwasm exceeds 2GB if the collection of paths go on too long. * Even if the paths do get collected by the GC, they often happen infrequently enough that paths over many frames have accumulated and are being collected all at once. This gap can often be dozens or hundreds of frames long, and when collection does occur it is freeing a lot of paths at once, which causes a janky frame. I have seen this take upwards of 800ms on my M1 Macbook Pro. There are some more details in https://github.com/flutter/flutter/issues/153678 This PR alleviates this issue by creating a `LazyPath` object. This object is added to an arena that explicitly collects the underlying native objects at the end of each frame. The object also tracks the API calls made to it so that if it is actually used across a frame boundary that we can recreate the native object if it was freed. Running our benchmarks, this has a non-trivial performance cost to building and using these paths (30-50% in a microbenchmark, 3-6% in a broader full app benchmark). However, as a team we've decided that this cost is worth it to avoid OOM issues as well as the non-deterministic jank associated with large collections of these objects. --- .../ci/licenses_golden/licenses_flutter | 4 + .../flutter/lib/web_ui/lib/src/engine.dart | 2 + .../lib/web_ui/lib/src/engine/arena.dart | 22 + .../engine/canvaskit/canvaskit_canvas.dart | 24 +- .../engine/canvaskit/layer_scene_builder.dart | 11 +- .../web_ui/lib/src/engine/canvaskit/path.dart | 21 +- .../src/engine/canvaskit/path_metrics.dart | 36 +- .../lib/src/engine/canvaskit/renderer.dart | 10 +- .../lib/web_ui/lib/src/engine/lazy_path.dart | 738 ++++++++++++++++++ .../lib/src/engine/platform_dispatcher.dart | 5 +- .../src/engine/skwasm/skwasm_impl/canvas.dart | 9 +- .../src/engine/skwasm/skwasm_impl/path.dart | 14 +- .../skwasm/skwasm_impl/path_metrics.dart | 14 +- .../engine/skwasm/skwasm_impl/renderer.dart | 8 +- .../test/canvaskit/canvaskit_api_test.dart | 10 +- .../lib/web_ui/test/canvaskit/path_test.dart | 23 +- .../web_ui/test/engine/lazy_path_test.dart | 297 +++++++ 17 files changed, 1168 insertions(+), 80 deletions(-) create mode 100644 engine/src/flutter/lib/web_ui/lib/src/engine/arena.dart create mode 100644 engine/src/flutter/lib/web_ui/lib/src/engine/lazy_path.dart create mode 100644 engine/src/flutter/lib/web_ui/test/engine/lazy_path_test.dart diff --git a/engine/src/flutter/ci/licenses_golden/licenses_flutter b/engine/src/flutter/ci/licenses_golden/licenses_flutter index e6fc889d9e2..e2eca45b00f 100644 --- a/engine/src/flutter/ci/licenses_golden/licenses_flutter +++ b/engine/src/flutter/ci/licenses_golden/licenses_flutter @@ -52156,6 +52156,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/semantics.dart + ../../../flutter/LICENS ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/alarm_clock.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/app_bootstrap.dart + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/arena.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/browser_detection.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvas.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart + ../../../flutter/LICENSE @@ -52217,6 +52218,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/js_interop/js_typed_data.dart ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/key_map.g.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/keyboard_binding.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/layers.dart + ../../../flutter/LICENSE +ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/lazy_path.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/mouse/context_menu.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/mouse/cursor.dart + ../../../flutter/LICENSE ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/mouse/prevent_default.dart + ../../../flutter/LICENSE @@ -55182,6 +55184,7 @@ FILE: ../../../flutter/lib/web_ui/lib/semantics.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/alarm_clock.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/app_bootstrap.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/arena.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/browser_detection.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvas.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart @@ -55243,6 +55246,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/js_interop/js_typed_data.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/key_map.g.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/keyboard_binding.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/layers.dart +FILE: ../../../flutter/lib/web_ui/lib/src/engine/lazy_path.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/mouse/context_menu.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/mouse/cursor.dart FILE: ../../../flutter/lib/web_ui/lib/src/engine/mouse/prevent_default.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 cf5b9920a01..4b3b9269eaa 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine.dart @@ -17,6 +17,7 @@ library engine; export 'engine/alarm_clock.dart'; export 'engine/app_bootstrap.dart'; +export 'engine/arena.dart'; export 'engine/browser_detection.dart'; export 'engine/canvaskit/canvas.dart'; export 'engine/canvaskit/canvaskit_api.dart'; @@ -77,6 +78,7 @@ export 'engine/js_interop/js_typed_data.dart'; export 'engine/key_map.g.dart'; export 'engine/keyboard_binding.dart'; export 'engine/layers.dart'; +export 'engine/lazy_path.dart'; export 'engine/mouse/context_menu.dart'; export 'engine/mouse/cursor.dart'; export 'engine/mouse/prevent_default.dart'; diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/arena.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/arena.dart new file mode 100644 index 00000000000..50080a62c2e --- /dev/null +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/arena.dart @@ -0,0 +1,22 @@ +// 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. + +abstract class Collectable { + void collect(); +} + +class Arena { + List _collectables = []; + void add(Collectable collectable) { + _collectables.add(collectable); + } + + void collect() { + final List collectables = _collectables; + _collectables = []; + for (final collect in collectables) { + collect.collect(); + } + } +} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart index a2af8cc4c81..b4b1c9e879a 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/canvaskit_canvas.dart @@ -4,20 +4,9 @@ import 'dart:typed_data'; +import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; -import '../validators.dart'; -import '../vector_math.dart'; -import 'canvas.dart'; -import 'canvaskit_api.dart'; -import 'image.dart'; -import 'painting.dart'; -import 'path.dart'; -import 'picture.dart'; -import 'picture_recorder.dart'; -import 'text.dart'; -import 'vertices.dart'; - /// An implementation of [ui.Canvas] that is backed by a CanvasKit canvas. class CanvasKitCanvas implements ui.Canvas { factory CanvasKitCanvas(ui.PictureRecorder recorder, [ui.Rect? cullRect]) { @@ -140,7 +129,7 @@ class CanvasKitCanvas implements ui.Canvas { @override void clipPath(ui.Path path, {bool doAntiAlias = true}) { - _canvas.clipPath(path as CkPath, doAntiAlias); + _canvas.clipPath((path as LazyPath).builtPath as CkPath, doAntiAlias); } @override @@ -264,7 +253,7 @@ class CanvasKitCanvas implements ui.Canvas { @override void drawPath(ui.Path path, ui.Paint paint) { - _canvas.drawPath(path as CkPath, paint as CkPaint); + _canvas.drawPath((path as LazyPath).builtPath as CkPath, paint as CkPaint); } @override @@ -436,6 +425,11 @@ class CanvasKitCanvas implements ui.Canvas { } void _drawShadow(ui.Path path, ui.Color color, double elevation, bool transparentOccluder) { - _canvas.drawShadow(path as CkPath, color, elevation, transparentOccluder); + _canvas.drawShadow( + (path as LazyPath).builtPath as CkPath, + color, + elevation, + transparentOccluder, + ); } } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart index 998dd94ccfa..9614dfdbdef 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.dart @@ -4,14 +4,9 @@ import 'dart:typed_data'; +import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; -import '../vector_math.dart'; -import 'layer.dart'; -import 'layer_tree.dart'; -import 'path.dart'; -import 'picture.dart'; - class LayerScene implements ui.Scene { LayerScene(RootLayer rootLayer) : layerTree = LayerTree(rootLayer); @@ -114,7 +109,9 @@ class LayerSceneBuilder implements ui.SceneBuilder { ui.Clip clipBehavior = ui.Clip.antiAlias, ui.EngineLayer? oldLayer, }) { - return pushLayer(ClipPathEngineLayer(path as CkPath, clipBehavior)); + return pushLayer( + ClipPathEngineLayer((path as LazyPath).builtPath as CkPath, clipBehavior), + ); } @override diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/path.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/path.dart index 16b8be4af94..014126a60b3 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/path.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/path.dart @@ -7,7 +7,7 @@ import 'dart:typed_data'; import 'package:ui/ui.dart' as ui; -import '../scene_painting.dart'; +import '../lazy_path.dart'; import '../vector_math.dart'; import 'canvaskit_api.dart'; import 'native_memory.dart'; @@ -16,7 +16,7 @@ import 'path_metrics.dart'; /// An implementation of [ui.Path] which is backed by an `SkPath`. /// /// The `SkPath` is required for `CkCanvas` methods which take a path. -class CkPath implements ScenePath { +class CkPath implements DisposablePath { factory CkPath() { final SkPath skPath = SkPath(); skPath.setFillType(toSkFillType(ui.PathFillType.nonZero)); @@ -44,6 +44,11 @@ class CkPath implements ScenePath { ui.PathFillType _fillType; + @override + void dispose() { + _ref.dispose(); + } + @override ui.PathFillType get fillType => _fillType; @@ -155,7 +160,7 @@ class CkPath implements ScenePath { } @override - ui.PathMetrics computeMetrics({bool forceClosed = false}) { + CkPathMetrics computeMetrics({bool forceClosed = false}) { return CkPathMetrics(this, forceClosed); } @@ -310,3 +315,13 @@ class CkPath implements ScenePath { return skiaObject.isEmpty(); } } + +class CkPathConstructors implements DisposablePathConstructors { + @override + CkPath createNew() => CkPath(); + + @override + CkPath combinePaths(ui.PathOperation operation, DisposablePath path1, DisposablePath path2) { + return CkPath.combine(operation, path1 as CkPath, path2 as CkPath); + } +} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/path_metrics.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/path_metrics.dart index 72da6df96e0..76242d95037 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/path_metrics.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/path_metrics.dart @@ -5,13 +5,10 @@ import 'dart:collection'; import 'dart:typed_data'; +import 'package:ui/src/engine.dart'; import 'package:ui/ui.dart' as ui; -import 'canvaskit_api.dart'; -import 'native_memory.dart'; -import 'path.dart'; - -class CkPathMetrics extends IterableBase implements ui.PathMetrics { +class CkPathMetrics extends IterableBase implements DisposablePathMetrics { CkPathMetrics(this._path, this._forceClosed); final CkPath _path; @@ -19,11 +16,11 @@ class CkPathMetrics extends IterableBase implements ui.PathMetric /// The [CkPath.isEmpty] case is special-cased to avoid booting the WASM machinery just to find out there are no contours. @override - late final Iterator iterator = + late final DisposablePathMetricIterator iterator = _path.isEmpty ? const CkPathMetricIteratorEmpty._() : CkContourMeasureIter(this); } -class CkContourMeasureIter implements Iterator { +class CkContourMeasureIter implements DisposablePathMetricIterator { CkContourMeasureIter(this._metrics) { _ref = UniqueRef( this, @@ -32,6 +29,11 @@ class CkContourMeasureIter implements Iterator { ); } + @override + void dispose() { + _ref.dispose(); + } + final CkPathMetrics _metrics; late final UniqueRef _ref; @@ -43,8 +45,8 @@ class CkContourMeasureIter implements Iterator { int _contourIndexCounter = 0; @override - ui.PathMetric get current { - final ui.PathMetric? currentMetric = _current; + CkContourMeasure get current { + final CkContourMeasure? currentMetric = _current; if (currentMetric == null) { throw RangeError( 'PathMetricIterator is not pointing to a PathMetric. This can happen in two situations:\n' @@ -71,7 +73,7 @@ class CkContourMeasureIter implements Iterator { } } -class CkContourMeasure implements ui.PathMetric { +class CkContourMeasure implements DisposablePathMetric { CkContourMeasure(this._metrics, SkContourMeasure skiaObject, this.contourIndex) { _ref = UniqueRef(this, skiaObject, 'PathMetric'); } @@ -85,11 +87,16 @@ class CkContourMeasure implements ui.PathMetric { SkContourMeasure get skiaObject => _ref.nativeObject; + @override + void dispose() { + _ref.dispose(); + } + @override final int contourIndex; @override - ui.Path extractPath(double start, double end, {bool startWithMoveTo = true}) { + CkPath extractPath(double start, double end, {bool startWithMoveTo = true}) { final SkPath skPath = skiaObject.getSegment(start, end, startWithMoveTo); return CkPath.fromSkPath(skPath, _metrics._path.fillType); } @@ -111,11 +118,11 @@ class CkContourMeasure implements ui.PathMetric { } } -class CkPathMetricIteratorEmpty implements Iterator { +class CkPathMetricIteratorEmpty implements DisposablePathMetricIterator { const CkPathMetricIteratorEmpty._(); @override - ui.PathMetric get current { + CkContourMeasure get current { throw RangeError('PathMetric iterator is empty.'); } @@ -123,4 +130,7 @@ class CkPathMetricIteratorEmpty implements Iterator { bool moveNext() { return false; } + + @override + void dispose() {} } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart index 7c2686cd7ba..49f7368969c 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart @@ -328,15 +328,17 @@ class CanvasKitRenderer implements Renderer { ui.FilterQuality? filterQuality, ) => CkImageShader(image, tmx, tmy, matrix4, filterQuality); - @override - ui.Path createPath() => CkPath(); + CkPathConstructors pathConstructors = CkPathConstructors(); @override - ui.Path copyPath(ui.Path src) => CkPath.from(src as CkPath); + ui.Path createPath() => LazyPath(pathConstructors); + + @override + ui.Path copyPath(ui.Path src) => LazyPath.fromLazyPath(src as LazyPath); @override ui.Path combinePaths(ui.PathOperation op, ui.Path path1, ui.Path path2) => - CkPath.combine(op, path1, path2); + LazyPath.combined(op, path1 as LazyPath, path2 as LazyPath); @override ui.TextStyle createTextStyle({ diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/lazy_path.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/lazy_path.dart new file mode 100644 index 00000000000..f4ef312b054 --- /dev/null +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/lazy_path.dart @@ -0,0 +1,738 @@ +// 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:collection'; +import 'dart:typed_data'; + +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart' as ui; + +abstract class DisposablePath implements ScenePath { + @override + DisposablePathMetrics computeMetrics({bool forceClosed = false}); + + void dispose(); +} + +abstract class DisposablePathMetricIterator implements Iterator { + @override + DisposablePathMetric get current; + + void dispose(); +} + +abstract class DisposablePathMetrics implements ui.PathMetrics { + @override + DisposablePathMetricIterator get iterator; +} + +abstract class DisposablePathMetric implements ui.PathMetric { + @override + DisposablePath extractPath(double start, double end, {bool startWithMoveTo = true}); + + void dispose(); +} + +sealed class PathCommand { + void apply(DisposablePath path); +} + +final class MoveToCommand implements PathCommand { + MoveToCommand(this.x, this.y); + + final double x; + final double y; + + @override + void apply(DisposablePath path) { + path.moveTo(x, y); + } +} + +final class RelativeMoveToCommand implements PathCommand { + RelativeMoveToCommand(this.dx, this.dy); + + final double dx; + final double dy; + + @override + void apply(DisposablePath path) { + path.relativeMoveTo(dx, dy); + } +} + +final class LineToCommand implements PathCommand { + LineToCommand(this.x, this.y); + + final double x; + final double y; + + @override + void apply(DisposablePath path) { + path.lineTo(x, y); + } +} + +final class RelativeLineToCommand implements PathCommand { + RelativeLineToCommand(this.dx, this.dy); + + final double dx; + final double dy; + + @override + void apply(DisposablePath path) { + path.relativeLineTo(dx, dy); + } +} + +final class QuadraticBezierToCommand implements PathCommand { + QuadraticBezierToCommand(this.x1, this.y1, this.x2, this.y2); + + final double x1; + final double y1; + final double x2; + final double y2; + + @override + void apply(DisposablePath path) { + path.quadraticBezierTo(x1, y1, x2, y2); + } +} + +final class RelativeQuadraticBezierToCommand implements PathCommand { + RelativeQuadraticBezierToCommand(this.x1, this.y1, this.x2, this.y2); + + final double x1; + final double y1; + final double x2; + final double y2; + + @override + void apply(DisposablePath path) { + path.relativeQuadraticBezierTo(x1, y1, x2, y2); + } +} + +final class CubicToCommand implements PathCommand { + CubicToCommand(this.x1, this.y1, this.x2, this.y2, this.x3, this.y3); + + final double x1; + final double y1; + final double x2; + final double y2; + final double x3; + final double y3; + + @override + void apply(DisposablePath path) { + path.cubicTo(x1, y1, x2, y2, x3, y3); + } +} + +final class RelativeCubicToCommand implements PathCommand { + RelativeCubicToCommand(this.x1, this.y1, this.x2, this.y2, this.x3, this.y3); + + final double x1; + final double y1; + final double x2; + final double y2; + final double x3; + final double y3; + + @override + void apply(DisposablePath path) { + path.relativeCubicTo(x1, y1, x2, y2, x3, y3); + } +} + +final class ConicToCommand implements PathCommand { + ConicToCommand(this.x1, this.y1, this.x2, this.y2, this.w); + + final double x1; + final double y1; + final double x2; + final double y2; + final double w; + + @override + void apply(DisposablePath path) { + path.conicTo(x1, y1, x2, y2, w); + } +} + +final class RelativeConicToCommand implements PathCommand { + RelativeConicToCommand(this.x1, this.y1, this.x2, this.y2, this.w); + + final double x1; + final double y1; + final double x2; + final double y2; + final double w; + + @override + void apply(DisposablePath path) { + path.relativeConicTo(x1, y1, x2, y2, w); + } +} + +final class ArcToCommand implements PathCommand { + ArcToCommand(this.rect, this.startAngle, this.sweepAngle, this.forceMoveTo); + + final ui.Rect rect; + final double startAngle; + final double sweepAngle; + bool forceMoveTo; + + @override + void apply(DisposablePath path) { + path.arcTo(rect, startAngle, sweepAngle, forceMoveTo); + } +} + +final class ArcToPointCommand implements PathCommand { + ArcToPointCommand( + this.arcEnd, { + required this.radius, + required this.rotation, + required this.largeArc, + required this.clockwise, + }); + + ui.Offset arcEnd; + ui.Radius radius; + double rotation; + bool largeArc; + bool clockwise; + + @override + void apply(DisposablePath path) { + path.arcToPoint( + arcEnd, + radius: radius, + rotation: rotation, + largeArc: largeArc, + clockwise: clockwise, + ); + } +} + +final class RelativeArcToPointCommand implements PathCommand { + RelativeArcToPointCommand( + this.arcEndDelta, { + required this.radius, + required this.rotation, + required this.largeArc, + required this.clockwise, + }); + + ui.Offset arcEndDelta; + ui.Radius radius; + double rotation; + bool largeArc; + bool clockwise; + + @override + void apply(DisposablePath path) { + path.relativeArcToPoint( + arcEndDelta, + radius: radius, + rotation: rotation, + largeArc: largeArc, + clockwise: clockwise, + ); + } +} + +final class AddRectCommand implements PathCommand { + AddRectCommand(this.rect); + + final ui.Rect rect; + + @override + void apply(DisposablePath path) { + path.addRect(rect); + } +} + +final class AddOvalCommand implements PathCommand { + AddOvalCommand(this.oval); + + final ui.Rect oval; + + @override + void apply(DisposablePath path) { + path.addOval(oval); + } +} + +final class AddArcCommand implements PathCommand { + AddArcCommand(this.oval, this.startAngle, this.sweepAngle); + + final ui.Rect oval; + final double startAngle; + final double sweepAngle; + + @override + void apply(DisposablePath path) { + path.addArc(oval, startAngle, sweepAngle); + } +} + +final class AddPolygonCommand implements PathCommand { + AddPolygonCommand(this.points, this.close); + + final List points; + final bool close; + + @override + void apply(DisposablePath path) { + path.addPolygon(points, close); + } +} + +final class AddRRectCommand implements PathCommand { + AddRRectCommand(this.rrect); + + final ui.RRect rrect; + + @override + void apply(DisposablePath path) { + path.addRRect(rrect); + } +} + +final class AddRSuperellipseCommand implements PathCommand { + AddRSuperellipseCommand(this.rSuperellipse); + + final ui.RSuperellipse rSuperellipse; + + @override + void apply(DisposablePath path) { + path.addRSuperellipse(rSuperellipse); + } +} + +final class AddPathCommand implements PathCommand { + AddPathCommand(this.path, this.offset, {this.matrix4}); + + final LazyPath path; + final ui.Offset offset; + final Float64List? matrix4; + + @override + void apply(DisposablePath p) { + p.addPath(path.builtPath, offset, matrix4: matrix4); + } +} + +final class ExtendWithPathCommand implements PathCommand { + ExtendWithPathCommand(this.path, this.offset, {this.matrix4}); + + final LazyPath path; + final ui.Offset offset; + final Float64List? matrix4; + + @override + void apply(DisposablePath p) { + p.extendWithPath(path.builtPath, offset, matrix4: matrix4); + } +} + +final class ClosePathCommand implements PathCommand { + @override + void apply(DisposablePath path) { + path.close(); + } +} + +abstract class DisposablePathConstructors { + DisposablePath createNew(); + DisposablePath combinePaths( + ui.PathOperation operation, + DisposablePath path1, + DisposablePath path2, + ); +} + +class LazyPath implements ScenePath, Collectable { + factory LazyPath(DisposablePathConstructors constructors) => + LazyPath._(constructors, ui.PathFillType.nonZero, () => constructors.createNew()); + LazyPath._(this.constructors, this._fillType, this.initializer) : _commands = []; + LazyPath.fromLazyPath(LazyPath other) + : _fillType = other._fillType, + constructors = other.constructors, + initializer = other.initializer, + _commands = List.from(other._commands); + factory LazyPath.shifted(LazyPath basePath, ui.Offset offset) { + final pathCopy = LazyPath.fromLazyPath(basePath); + return LazyPath._( + pathCopy.constructors, + pathCopy._fillType, + () => pathCopy.builtPath.shift(offset) as DisposablePath, + ); + } + factory LazyPath.transformed(LazyPath basePath, Float64List matrix4) { + final pathCopy = LazyPath.fromLazyPath(basePath); + return LazyPath._( + pathCopy.constructors, + pathCopy._fillType, + () => pathCopy.builtPath.transform(matrix4) as DisposablePath, + ); + } + factory LazyPath.combined(ui.PathOperation operation, LazyPath path1, LazyPath path2) { + final pathCopy1 = LazyPath.fromLazyPath(path1); + final pathCopy2 = LazyPath.fromLazyPath(path2); + return LazyPath._( + pathCopy1.constructors, + pathCopy1._fillType, + () => + pathCopy1.constructors.combinePaths(operation, pathCopy1.builtPath, pathCopy2.builtPath), + ); + } + LazyPath extracted( + LazyPathMetric pathMetric, + double start, + double end, { + bool startWithMoveTo = true, + }) { + return LazyPath._( + constructors, + pathMetric.iterator.path._fillType, + () => pathMetric.buildExtractedPath(start, end, startWithMoveTo: startWithMoveTo), + ); + } + + DisposablePathConstructors constructors; + DisposablePath Function() initializer; + + ui.PathFillType _fillType; + + @override + ui.PathFillType get fillType => _fillType; + + @override + set fillType(ui.PathFillType fillType) { + _fillType = fillType; + _cachedPath?.fillType = fillType; + } + + DisposablePath? _cachedPath; + final List _commands; + + DisposablePath get builtPath { + if (_cachedPath != null) { + return _cachedPath!; + } + final DisposablePath path = initializer(); + path.fillType = _fillType; + for (final command in _commands) { + command.apply(path); + } + + _cachedPath = path; + EnginePlatformDispatcher.instance.frameArena.add(this); + return path; + } + + void _addCommand(PathCommand command) { + _commands.add(command); + if (_cachedPath != null) { + command.apply(_cachedPath!); + } + } + + @override + void moveTo(double x, double y) { + _addCommand(MoveToCommand(x, y)); + } + + @override + void relativeMoveTo(double dx, double dy) { + _addCommand(RelativeMoveToCommand(dx, dy)); + } + + @override + void lineTo(double x, double y) { + _addCommand(LineToCommand(x, y)); + } + + @override + void relativeLineTo(double dx, double dy) { + _addCommand(RelativeLineToCommand(dx, dy)); + } + + @override + void quadraticBezierTo(double x1, double y1, double x2, double y2) { + _addCommand(QuadraticBezierToCommand(x1, y1, x2, y2)); + } + + @override + void relativeQuadraticBezierTo(double x1, double y1, double x2, double y2) { + _addCommand(RelativeQuadraticBezierToCommand(x1, y1, x2, y2)); + } + + @override + void cubicTo(double x1, double y1, double x2, double y2, double x3, double y3) { + _addCommand(CubicToCommand(x1, y1, x2, y2, x3, y3)); + } + + @override + void relativeCubicTo(double x1, double y1, double x2, double y2, double x3, double y3) { + _addCommand(RelativeCubicToCommand(x1, y1, x2, y2, x3, y3)); + } + + @override + void conicTo(double x1, double y1, double x2, double y2, double w) { + _addCommand(ConicToCommand(x1, y1, x2, y2, w)); + } + + @override + void relativeConicTo(double x1, double y1, double x2, double y2, double w) { + _addCommand(RelativeConicToCommand(x1, y1, x2, y2, w)); + } + + @override + void arcTo(ui.Rect rect, double startAngle, double sweepAngle, bool forceMoveTo) { + _addCommand(ArcToCommand(rect, startAngle, sweepAngle, forceMoveTo)); + } + + @override + void arcToPoint( + ui.Offset arcEnd, { + ui.Radius radius = ui.Radius.zero, + double rotation = 0.0, + bool largeArc = false, + bool clockwise = true, + }) { + _addCommand( + ArcToPointCommand( + arcEnd, + radius: radius, + rotation: rotation, + largeArc: largeArc, + clockwise: clockwise, + ), + ); + } + + @override + void relativeArcToPoint( + ui.Offset arcEndDelta, { + ui.Radius radius = ui.Radius.zero, + double rotation = 0.0, + bool largeArc = false, + bool clockwise = true, + }) { + _addCommand( + RelativeArcToPointCommand( + arcEndDelta, + radius: radius, + rotation: rotation, + largeArc: largeArc, + clockwise: clockwise, + ), + ); + } + + @override + void addRect(ui.Rect rect) { + _addCommand(AddRectCommand(rect)); + } + + @override + void addOval(ui.Rect oval) { + _addCommand(AddOvalCommand(oval)); + } + + @override + void addArc(ui.Rect oval, double startAngle, double sweepAngle) { + _addCommand(AddArcCommand(oval, startAngle, sweepAngle)); + } + + @override + void addPolygon(List points, bool close) { + _addCommand(AddPolygonCommand(points, close)); + } + + @override + void addRRect(ui.RRect rrect) { + _addCommand(AddRRectCommand(rrect)); + } + + @override + void addRSuperellipse(ui.RSuperellipse rSuperellipse) { + _addCommand(AddRSuperellipseCommand(rSuperellipse)); + } + + @override + void addPath(ui.Path path, ui.Offset offset, {Float64List? matrix4}) { + _addCommand(AddPathCommand(path as LazyPath, offset, matrix4: matrix4)); + } + + @override + void extendWithPath(ui.Path path, ui.Offset offset, {Float64List? matrix4}) { + _addCommand(ExtendWithPathCommand(path as LazyPath, offset, matrix4: matrix4)); + } + + @override + void close() { + _addCommand(ClosePathCommand()); + } + + @override + void reset() { + _commands.clear(); + _fillType = ui.PathFillType.nonZero; + _cachedPath?.dispose(); + _cachedPath = null; + initializer = constructors.createNew; + } + + @override + bool contains(ui.Offset point) { + return builtPath.contains(point); + } + + @override + ui.Path shift(ui.Offset offset) { + return LazyPath.shifted(this, offset); + } + + @override + ui.Path transform(Float64List matrix4) { + return LazyPath.transformed(this, matrix4); + } + + @override + ui.Rect getBounds() { + return builtPath.getBounds(); + } + + @override + LazyPathMetrics computeMetrics({bool forceClosed = false}) { + return LazyPathMetrics(path: this, forceClosed: forceClosed); + } + + @override + void collect() { + _cachedPath?.dispose(); + _cachedPath = null; + } + + @override + String toSvgString() { + return builtPath.toSvgString(); + } +} + +class LazyPathMetrics extends IterableBase implements ui.PathMetrics { + LazyPathMetrics({required LazyPath path, required bool forceClosed}) + : iterator = LazyPathMetricIterator(path, forceClosed); + + @override + final LazyPathMetricIterator iterator; +} + +class LazyPathMetricIterator implements Iterator, Collectable { + LazyPathMetricIterator(this.path, this.forceClosed); + + final LazyPath path; + final bool forceClosed; + int _nextIndex = 0; + DisposablePathMetricIterator? _cachedIterator; + final List _metrics = []; + bool _isAtEnd = false; + + @override + ui.PathMetric get current { + if (_nextIndex == 0 || _isAtEnd) { + throw RangeError( + 'PathMetricIterator is not pointing to a PathMetric. This can happen in two situations:\n' + '- The iteration has not started yet. If so, call "moveNext" to start iteration.\n' + '- The iterator ran out of elements. If so, check that "moveNext" returns true prior to calling "current".', + ); + } + return LazyPathMetric(this, _nextIndex - 1); + } + + @override + bool moveNext() { + if (_isAtEnd) { + return false; + } + buildIterator(); + assert(_cachedIterator != null); + assert(_nextIndex == _metrics.length); + _nextIndex++; + if (_cachedIterator!.moveNext()) { + _metrics.add(_cachedIterator!.current); + return true; + } else { + _isAtEnd = true; + return false; + } + } + + @override + void collect() { + _cachedIterator?.dispose(); + _cachedIterator = null; + + for (final metric in _metrics) { + metric.dispose(); + } + _metrics.clear(); + } + + void buildIterator() { + if (_cachedIterator != null) { + return; + } + _cachedIterator = path.builtPath.computeMetrics(forceClosed: forceClosed).iterator; + for (int i = 0; i < _nextIndex; i++) { + if (_cachedIterator!.moveNext()) { + _metrics.add(_cachedIterator!.current); + } else { + break; + } + } + EnginePlatformDispatcher.instance.frameArena.add(this); + } + + DisposablePathMetric builtMetricAtIndex(int index) { + buildIterator(); + return _metrics[index]; + } +} + +class LazyPathMetric implements ui.PathMetric { + LazyPathMetric(this.iterator, this.contourIndex); + + final LazyPathMetricIterator iterator; + + @override + final int contourIndex; + + DisposablePathMetric get builtMetric => iterator.builtMetricAtIndex(contourIndex); + + @override + ui.Path extractPath(double start, double end, {bool startWithMoveTo = true}) { + return iterator.path.extracted(this, start, end, startWithMoveTo: startWithMoveTo); + } + + DisposablePath buildExtractedPath(double start, double end, {required bool startWithMoveTo}) { + return builtMetric.extractPath(start, end, startWithMoveTo: startWithMoveTo); + } + + @override + ui.Tangent? getTangentForOffset(double distance) { + return builtMetric.getTangentForOffset(distance); + } + + @override + bool get isClosed => builtMetric.isClosed; + + @override + double get length => builtMetric.length; +} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart index 5bf16d1ec8c..bb42f9c8736 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/platform_dispatcher.dart @@ -44,6 +44,8 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { late StreamSubscription _onViewDisposedListener; + final Arena frameArena = Arena(); + /// The [EnginePlatformDispatcher] singleton. static EnginePlatformDispatcher get instance => _instance; static final EnginePlatformDispatcher _instance = EnginePlatformDispatcher(); @@ -260,6 +262,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { _viewsRenderedInCurrentFrame = {}; invoke(_onDrawFrame, _onDrawFrameZone); _viewsRenderedInCurrentFrame = null; + frameArena.collect(); } /// A callback that is invoked when pointer data is available. @@ -740,7 +743,7 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher { // order to perform golden tests in Flutter framework because on the HTML // renderer, golden tests render to DOM and then take a browser screenshot, // https://github.com/flutter/flutter/issues/137073. - if (shouldRender || renderer.rendererTag == 'html') { + if (shouldRender) { await renderer.renderScene(scene, target); } } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart index fee2a4eaf52..1049dbd35ab 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/canvas.dart @@ -143,8 +143,7 @@ class SkwasmCanvas implements SceneCanvas { @override void clipPath(ui.Path path, {bool doAntiAlias = true}) { - path as SkwasmPath; - canvasClipPath(_handle, path.handle, doAntiAlias); + canvasClipPath(_handle, ((path as LazyPath).builtPath as SkwasmPath).handle, doAntiAlias); } @override @@ -238,9 +237,8 @@ class SkwasmCanvas implements SceneCanvas { @override void drawPath(ui.Path path, ui.Paint paint) { - path as SkwasmPath; final paintHandle = (paint as SkwasmPaint).toRawPaint(); - canvasDrawPath(_handle, path.handle, paintHandle); + canvasDrawPath(_handle, ((path as LazyPath).builtPath as SkwasmPath).handle, paintHandle); paintDispose(paintHandle); } @@ -391,10 +389,9 @@ class SkwasmCanvas implements SceneCanvas { @override void drawShadow(ui.Path path, ui.Color color, double elevation, bool transparentOccluder) { - path as SkwasmPath; canvasDrawShadow( _handle, - path.handle, + ((path as LazyPath).builtPath as SkwasmPath).handle, elevation, EngineFlutterDisplay.instance.devicePixelRatio, color.value, diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/path.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/path.dart index b1753ddb7e8..f778f9cee57 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/path.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/path.dart @@ -14,7 +14,7 @@ enum PathDirection { clockwise, counterClockwise } enum PathArcSize { small, large } -class SkwasmPath extends SkwasmObjectWrapper implements ScenePath { +class SkwasmPath extends SkwasmObjectWrapper implements ScenePath, DisposablePath { factory SkwasmPath() { return SkwasmPath.fromHandle(pathCreate()); } @@ -234,7 +234,7 @@ class SkwasmPath extends SkwasmObjectWrapper implements ScenePath { SkwasmPath.fromHandle(pathCombine(operation.index, path1.handle, path2.handle)); @override - ui.PathMetrics computeMetrics({bool forceClosed = false}) { + SkwasmPathMetrics computeMetrics({bool forceClosed = false}) { return SkwasmPathMetrics(path: this, forceClosed: forceClosed); } @@ -249,3 +249,13 @@ class SkwasmPath extends SkwasmObjectWrapper implements ScenePath { return svgString; } } + +class SkwasmPathConstructors implements DisposablePathConstructors { + @override + SkwasmPath createNew() => SkwasmPath(); + + @override + SkwasmPath combinePaths(ui.PathOperation operation, DisposablePath path1, DisposablePath path2) { + return SkwasmPath.combine(operation, path1 as SkwasmPath, path2 as SkwasmPath); + } +} diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/path_metrics.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/path_metrics.dart index bb443109890..1035a0bd1ee 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/path_metrics.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/path_metrics.dart @@ -5,21 +5,22 @@ import 'dart:collection'; import 'dart:ffi'; +import 'package:ui/src/engine.dart'; import 'package:ui/src/engine/skwasm/skwasm_impl.dart'; import 'package:ui/ui.dart' as ui; -class SkwasmPathMetrics extends IterableBase implements ui.PathMetrics { +class SkwasmPathMetrics extends IterableBase implements DisposablePathMetrics { SkwasmPathMetrics({required this.path, required this.forceClosed}); SkwasmPath path; bool forceClosed; @override - late Iterator iterator = SkwasmPathMetricIterator(path, forceClosed); + late DisposablePathMetricIterator iterator = SkwasmPathMetricIterator(path, forceClosed); } class SkwasmPathMetricIterator extends SkwasmObjectWrapper - implements Iterator { + implements DisposablePathMetricIterator { SkwasmPathMetricIterator(SkwasmPath path, bool forceClosed) : super(contourMeasureIterCreate(path.handle, forceClosed, 1.0), _registry); @@ -32,7 +33,7 @@ class SkwasmPathMetricIterator extends SkwasmObjectWrapper implements ui.PathMetric { +class SkwasmPathMetric extends SkwasmObjectWrapper + implements DisposablePathMetric { SkwasmPathMetric(ContourMeasureHandle handle, this.contourIndex) : super(handle, _registry); static final SkwasmFinalizationRegistry _registry = @@ -69,7 +71,7 @@ class SkwasmPathMetric extends SkwasmObjectWrapper implements final int contourIndex; @override - ui.Path extractPath(double start, double end, {bool startWithMoveTo = true}) { + DisposablePath extractPath(double start, double end, {bool startWithMoveTo = true}) { return SkwasmPath.fromHandle(contourMeasureGetSegment(handle, start, end, startWithMoveTo)); } diff --git a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart index ea07ea716a0..eee758c0e64 100644 --- a/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart +++ b/engine/src/flutter/lib/web_ui/lib/src/engine/skwasm/skwasm_impl/renderer.dart @@ -21,17 +21,19 @@ class SkwasmRenderer implements Renderer { bool get isMultiThreaded => skwasmIsMultiThreaded(); + SkwasmPathConstructors pathConstructors = SkwasmPathConstructors(); + @override final SkwasmFontCollection fontCollection = SkwasmFontCollection(); @override ui.Path combinePaths(ui.PathOperation op, ui.Path path1, ui.Path path2) { - return SkwasmPath.combine(op, path1 as SkwasmPath, path2 as SkwasmPath); + return LazyPath.combined(op, path1 as LazyPath, path2 as LazyPath); } @override ui.Path copyPath(ui.Path src) { - return SkwasmPath.from(src as SkwasmPath); + return LazyPath.fromLazyPath(src as LazyPath); } @override @@ -153,7 +155,7 @@ class SkwasmRenderer implements Renderer { ); @override - ui.Path createPath() => SkwasmPath(); + ui.Path createPath() => LazyPath(pathConstructors); @override ui.PictureRecorder createPictureRecorder() => SkwasmPictureRecorder(); diff --git a/engine/src/flutter/lib/web_ui/test/canvaskit/canvaskit_api_test.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/canvaskit_api_test.dart index 806dcf2e31a..f121cf16dbb 100644 --- a/engine/src/flutter/lib/web_ui/test/canvaskit/canvaskit_api_test.dart +++ b/engine/src/flutter/lib/web_ui/test/canvaskit/canvaskit_api_test.dart @@ -206,23 +206,21 @@ void _pathOpTests() { }); test('Path.combine test', () { - final ui.Path path1 = ui.Path(); - expect(path1, isA()); + final CkPath path1 = CkPath(); path1.addRect(const ui.Rect.fromLTRB(0, 0, 10, 10)); path1.addOval(const ui.Rect.fromLTRB(10, 10, 100, 100)); - final ui.Path path2 = ui.Path(); - expect(path2, isA()); + final CkPath path2 = CkPath(); path2.addRect(const ui.Rect.fromLTRB(5, 5, 15, 15)); path2.addOval(const ui.Rect.fromLTRB(15, 15, 105, 105)); - final ui.Path union = ui.Path.combine(ui.PathOperation.union, path1, path2); + final ui.Path union = CkPath.combine(ui.PathOperation.union, path1, path2); expect(union, isA()); expect(union.getBounds(), const ui.Rect.fromLTRB(0, 0, 105, 105)); // Smoke-test other operations. for (final ui.PathOperation operation in ui.PathOperation.values) { - final ui.Path combined = ui.Path.combine(operation, path1, path2); + final ui.Path combined = CkPath.combine(operation, path1, path2); expect(combined, isA()); } }); diff --git a/engine/src/flutter/lib/web_ui/test/canvaskit/path_test.dart b/engine/src/flutter/lib/web_ui/test/canvaskit/path_test.dart index ef884a2df54..8bdb7b7eeef 100644 --- a/engine/src/flutter/lib/web_ui/test/canvaskit/path_test.dart +++ b/engine/src/flutter/lib/web_ui/test/canvaskit/path_test.dart @@ -23,8 +23,7 @@ void testMain() { }); test(CkPathMetrics, () { - final ui.Path path = ui.Path(); - expect(path, isA()); + final CkPath path = CkPath(); expect(path.computeMetrics().length, 0); path.addRect(const ui.Rect.fromLTRB(0, 0, 10, 10)); @@ -65,8 +64,7 @@ void testMain() { }); test('CkPath.reset', () { - final ui.Path path = ui.Path(); - expect(path, isA()); + final CkPath path = CkPath(); path.addRect(const ui.Rect.fromLTRB(0, 0, 10, 10)); expect(path.contains(const ui.Offset(5, 5)), isTrue); @@ -95,8 +93,7 @@ void testMain() { }); test('CkContourMeasure iteration', () { - final ui.Path path = ui.Path(); - expect(path, isA()); + final CkPath path = CkPath(); path.addRect(const ui.Rect.fromLTRB(0, 0, 10, 10)); path.addRect(const ui.Rect.fromLTRB(20, 20, 30, 30)); path.addRect(const ui.Rect.fromLTRB(40, 40, 50, 50)); @@ -111,22 +108,21 @@ void testMain() { }); test('CkContourMeasure index', () { - final ui.Path path = ui.Path(); - expect(path, isA()); + final CkPath path = CkPath(); path.addRect(const ui.Rect.fromLTRB(0, 0, 10, 10)); path.addRect(const ui.Rect.fromLTRB(20, 20, 30, 30)); path.addRect(const ui.Rect.fromLTRB(40, 40, 50, 50)); - final ui.PathMetrics metrics = path.computeMetrics(); + final CkPathMetrics metrics = path.computeMetrics(); final CkContourMeasureIter iterator = metrics.iterator as CkContourMeasureIter; expect(iterator.moveNext(), isTrue); - final CkContourMeasure measure0 = iterator.current as CkContourMeasure; + final CkContourMeasure measure0 = iterator.current; expect(measure0.contourIndex, 0); expect(measure0.extractPath(0, 15).getBounds(), const ui.Rect.fromLTRB(0, 0, 10, 5)); expect(iterator.moveNext(), isTrue); - final CkContourMeasure measure1 = iterator.current as CkContourMeasure; + final CkContourMeasure measure1 = iterator.current; expect(measure1.contourIndex, 1); expect(measure1.extractPath(0, 15).getBounds(), const ui.Rect.fromLTRB(20, 20, 30, 25)); }); @@ -135,12 +131,11 @@ void testMain() { const ui.Rect rect1 = ui.Rect.fromLTRB(0, 0, 10, 10); const ui.Rect rect2 = ui.Rect.fromLTRB(10, 10, 20, 20); - final ui.Path original = ui.Path(); + final CkPath original = CkPath(); original.addRect(rect1); - expect(original, isA()); expect(original.getBounds(), rect1); - final ui.Path copy = ui.Path.from(original); + final ui.Path copy = CkPath.from(original); expect(copy, isA()); expect(copy.getBounds(), rect1); diff --git a/engine/src/flutter/lib/web_ui/test/engine/lazy_path_test.dart b/engine/src/flutter/lib/web_ui/test/engine/lazy_path_test.dart new file mode 100644 index 00000000000..5289c2d15f7 --- /dev/null +++ b/engine/src/flutter/lib/web_ui/test/engine/lazy_path_test.dart @@ -0,0 +1,297 @@ +// 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:collection'; +import 'dart:typed_data'; +import 'package:test/bootstrap/browser.dart'; +import 'package:test/test.dart'; + +import 'package:ui/src/engine.dart'; +import 'package:ui/ui.dart'; + +void main() { + internalBootstrapBrowserTest(() => testMain); +} + +class FakePath implements DisposablePath { + int _apiCallCount = 0; + + int get apiCallCount => _apiCallCount; + + bool isDisposed = false; + + final List computedMetrics = []; + + @override + void dispose() { + isDisposed = true; + } + + @override + PathFillType fillType = PathFillType.nonZero; + + @override + void moveTo(double x, double y) { + _apiCallCount++; + } + + @override + void relativeMoveTo(double dx, double dy) { + _apiCallCount++; + } + + @override + void lineTo(double x, double y) { + _apiCallCount++; + } + + @override + void relativeLineTo(double dx, double dy) { + _apiCallCount++; + } + + @override + void quadraticBezierTo(double x1, double y1, double x2, double y2) { + _apiCallCount++; + } + + @override + void relativeQuadraticBezierTo(double x1, double y1, double x2, double y2) { + _apiCallCount++; + } + + @override + void cubicTo(double x1, double y1, double x2, double y2, double x3, double y3) { + _apiCallCount++; + } + + @override + void relativeCubicTo(double x1, double y1, double x2, double y2, double x3, double y3) { + _apiCallCount++; + } + + @override + void conicTo(double x1, double y1, double x2, double y2, double w) { + _apiCallCount++; + } + + @override + void relativeConicTo(double x1, double y1, double x2, double y2, double w) { + _apiCallCount++; + } + + @override + void arcTo(Rect rect, double startAngle, double sweepAngle, bool forceMoveTo) { + _apiCallCount++; + } + + @override + void arcToPoint( + Offset arcEnd, { + Radius radius = Radius.zero, + double rotation = 0.0, + bool largeArc = false, + bool clockwise = true, + }) { + _apiCallCount++; + } + + @override + void relativeArcToPoint( + Offset arcEndDelta, { + Radius radius = Radius.zero, + double rotation = 0.0, + bool largeArc = false, + bool clockwise = true, + }) { + _apiCallCount++; + } + + @override + void addRect(Rect rect) { + _apiCallCount++; + } + + @override + void addOval(Rect oval) { + _apiCallCount++; + } + + @override + void addArc(Rect oval, double startAngle, double sweepAngle) { + _apiCallCount++; + } + + @override + void addPolygon(List points, bool close) { + _apiCallCount++; + } + + @override + void addRRect(RRect rrect) { + _apiCallCount++; + } + + @override + void addRSuperellipse(RSuperellipse rsuperellipse) { + _apiCallCount++; + } + + @override + void addPath(Path path, Offset offset, {Float64List? matrix4}) { + _apiCallCount++; + } + + @override + void extendWithPath(Path path, Offset offset, {Float64List? matrix4}) { + _apiCallCount++; + } + + @override + void close() { + _apiCallCount++; + } + + @override + void reset() { + _apiCallCount++; + } + + @override + bool contains(Offset point) { + return false; + } + + @override + Path shift(Offset offset) { + return this; + } + + @override + Path transform(Float64List matrix4) { + return this; + } + + @override + Rect getBounds() { + return Rect.zero; + } + + @override + FakePathMetrics computeMetrics({bool forceClosed = false}) { + final metrics = FakePathMetrics(); + computedMetrics.add(metrics); + return metrics; + } + + @override + String toSvgString() { + return ''; + } +} + +class FakePathMetrics extends IterableBase implements DisposablePathMetrics { + @override + final FakePathMetricsIterator iterator = FakePathMetricsIterator(); +} + +class FakePathMetricsIterator implements DisposablePathMetricIterator { + bool isDisposed = false; + + @override + void dispose() { + isDisposed = true; + } + + @override + bool moveNext() { + return false; + } + + @override + Never get current { + throw RangeError('nope'); + } +} + +class FakePathConstructors implements DisposablePathConstructors { + final List createdPaths = []; + + @override + FakePath createNew() { + final path = FakePath(); + createdPaths.add(path); + return path; + } + + @override + FakePath combinePaths(PathOperation operation, DisposablePath path1, DisposablePath path2) { + final path = FakePath(); + createdPaths.add(path); + return path; + } +} + +void testMain() { + test('LazyPath lifecycle', () { + final constructors = FakePathConstructors(); + final LazyPath path = LazyPath(constructors); + expect(constructors.createdPaths, isEmpty); + + path.moveTo(0, 0); + path.relativeMoveTo(0, 0); + path.lineTo(0, 0); + path.relativeLineTo(0, 0); + path.quadraticBezierTo(0, 0, 0, 0); + path.relativeQuadraticBezierTo(0, 0, 0, 0); + path.cubicTo(0, 0, 0, 0, 0, 0); + path.relativeCubicTo(0, 0, 0, 0, 0, 0); + path.conicTo(0, 0, 0, 0, 0); + path.relativeConicTo(0, 0, 0, 0, 0); + path.arcTo(Rect.zero, 0, 0, false); + path.arcToPoint(Offset.zero); + path.relativeArcToPoint(Offset.zero); + path.addRect(Rect.zero); + path.addOval(Rect.zero); + path.addArc(Rect.zero, 0, 0); + path.addPolygon([], false); + path.addRRect(RRect.zero); + path.addRSuperellipse(RSuperellipse.zero); + path.close(); + + expect(constructors.createdPaths, isEmpty); + + path.getBounds(); + + expect(constructors.createdPaths.length, 1); + final disposablePath = constructors.createdPaths.first; + + expect(disposablePath.isDisposed, false); + expect(disposablePath.apiCallCount, 20); + + final metrics = path.computeMetrics(); + expect(metrics.iterator.moveNext(), false); + expect(disposablePath.computedMetrics.length, 1); + final disposableMetrics = disposablePath.computedMetrics.first; + expect(disposableMetrics.iterator.isDisposed, false); + + EnginePlatformDispatcher.instance.frameArena.collect(); + + expect(constructors.createdPaths.length, 1); + expect(disposablePath.isDisposed, true); + expect(disposablePath.computedMetrics.length, 1); + expect(disposableMetrics.iterator.isDisposed, true); + + path.getBounds(); + + expect(constructors.createdPaths.length, 2); + final resurrectedPath = constructors.createdPaths.last; + + expect(resurrectedPath.isDisposed, false); + expect(resurrectedPath.apiCallCount, 20); + expect(metrics.iterator.moveNext(), false); + + EnginePlatformDispatcher.instance.frameArena.collect(); + }); +}