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.
This commit is contained in:
Jackson Gardner 2025-06-06 09:27:08 -07:00 committed by GitHub
parent ed275039e1
commit a11524896e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 1168 additions and 80 deletions

View File

@ -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

View File

@ -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';

View File

@ -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<Collectable> _collectables = <Collectable>[];
void add(Collectable collectable) {
_collectables.add(collectable);
}
void collect() {
final List<Collectable> collectables = _collectables;
_collectables = <Collectable>[];
for (final collect in collectables) {
collect.collect();
}
}
}

View File

@ -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,
);
}
}

View File

@ -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>(ClipPathEngineLayer(path as CkPath, clipBehavior));
return pushLayer<ClipPathEngineLayer>(
ClipPathEngineLayer((path as LazyPath).builtPath as CkPath, clipBehavior),
);
}
@override

View File

@ -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);
}
}

View File

@ -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<ui.PathMetric> implements ui.PathMetrics {
class CkPathMetrics extends IterableBase<ui.PathMetric> implements DisposablePathMetrics {
CkPathMetrics(this._path, this._forceClosed);
final CkPath _path;
@ -19,11 +16,11 @@ class CkPathMetrics extends IterableBase<ui.PathMetric> 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<ui.PathMetric> iterator =
late final DisposablePathMetricIterator iterator =
_path.isEmpty ? const CkPathMetricIteratorEmpty._() : CkContourMeasureIter(this);
}
class CkContourMeasureIter implements Iterator<ui.PathMetric> {
class CkContourMeasureIter implements DisposablePathMetricIterator {
CkContourMeasureIter(this._metrics) {
_ref = UniqueRef<SkContourMeasureIter>(
this,
@ -32,6 +29,11 @@ class CkContourMeasureIter implements Iterator<ui.PathMetric> {
);
}
@override
void dispose() {
_ref.dispose();
}
final CkPathMetrics _metrics;
late final UniqueRef<SkContourMeasureIter> _ref;
@ -43,8 +45,8 @@ class CkContourMeasureIter implements Iterator<ui.PathMetric> {
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<ui.PathMetric> {
}
}
class CkContourMeasure implements ui.PathMetric {
class CkContourMeasure implements DisposablePathMetric {
CkContourMeasure(this._metrics, SkContourMeasure skiaObject, this.contourIndex) {
_ref = UniqueRef<SkContourMeasure>(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<ui.PathMetric> {
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<ui.PathMetric> {
bool moveNext() {
return false;
}
@override
void dispose() {}
}

View File

@ -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({

View File

@ -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<ui.PathMetric> {
@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<ui.Offset> 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<PathCommand> _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<ui.Offset> 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<ui.PathMetric> implements ui.PathMetrics {
LazyPathMetrics({required LazyPath path, required bool forceClosed})
: iterator = LazyPathMetricIterator(path, forceClosed);
@override
final LazyPathMetricIterator iterator;
}
class LazyPathMetricIterator implements Iterator<ui.PathMetric>, Collectable {
LazyPathMetricIterator(this.path, this.forceClosed);
final LazyPath path;
final bool forceClosed;
int _nextIndex = 0;
DisposablePathMetricIterator? _cachedIterator;
final List<DisposablePathMetric> _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;
}

View File

@ -44,6 +44,8 @@ class EnginePlatformDispatcher extends ui.PlatformDispatcher {
late StreamSubscription<int> _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 = <ui.FlutterView>{};
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);
}
}

View File

@ -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,

View File

@ -14,7 +14,7 @@ enum PathDirection { clockwise, counterClockwise }
enum PathArcSize { small, large }
class SkwasmPath extends SkwasmObjectWrapper<RawPath> implements ScenePath {
class SkwasmPath extends SkwasmObjectWrapper<RawPath> implements ScenePath, DisposablePath {
factory SkwasmPath() {
return SkwasmPath.fromHandle(pathCreate());
}
@ -234,7 +234,7 @@ class SkwasmPath extends SkwasmObjectWrapper<RawPath> 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<RawPath> 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);
}
}

View File

@ -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<ui.PathMetric> implements ui.PathMetrics {
class SkwasmPathMetrics extends IterableBase<ui.PathMetric> implements DisposablePathMetrics {
SkwasmPathMetrics({required this.path, required this.forceClosed});
SkwasmPath path;
bool forceClosed;
@override
late Iterator<ui.PathMetric> iterator = SkwasmPathMetricIterator(path, forceClosed);
late DisposablePathMetricIterator iterator = SkwasmPathMetricIterator(path, forceClosed);
}
class SkwasmPathMetricIterator extends SkwasmObjectWrapper<RawContourMeasureIter>
implements Iterator<ui.PathMetric> {
implements DisposablePathMetricIterator {
SkwasmPathMetricIterator(SkwasmPath path, bool forceClosed)
: super(contourMeasureIterCreate(path.handle, forceClosed, 1.0), _registry);
@ -32,7 +33,7 @@ class SkwasmPathMetricIterator extends SkwasmObjectWrapper<RawContourMeasureIter
int _nextIndex = 0;
@override
ui.PathMetric get current {
DisposablePathMetric get current {
if (_current == null) {
throw RangeError(
'PathMetricIterator is not pointing to a PathMetric. This can happen in two situations:\n'
@ -57,7 +58,8 @@ class SkwasmPathMetricIterator extends SkwasmObjectWrapper<RawContourMeasureIter
}
}
class SkwasmPathMetric extends SkwasmObjectWrapper<RawContourMeasure> implements ui.PathMetric {
class SkwasmPathMetric extends SkwasmObjectWrapper<RawContourMeasure>
implements DisposablePathMetric {
SkwasmPathMetric(ContourMeasureHandle handle, this.contourIndex) : super(handle, _registry);
static final SkwasmFinalizationRegistry<RawContourMeasure> _registry =
@ -69,7 +71,7 @@ class SkwasmPathMetric extends SkwasmObjectWrapper<RawContourMeasure> 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));
}

View File

@ -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();

View File

@ -206,23 +206,21 @@ void _pathOpTests() {
});
test('Path.combine test', () {
final ui.Path path1 = ui.Path();
expect(path1, isA<CkPath>());
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<CkPath>());
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<CkPath>());
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<CkPath>());
}
});

View File

@ -23,8 +23,7 @@ void testMain() {
});
test(CkPathMetrics, () {
final ui.Path path = ui.Path();
expect(path, isA<CkPath>());
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<CkPath>());
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<CkPath>());
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<CkPath>());
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<CkPath>());
expect(original.getBounds(), rect1);
final ui.Path copy = ui.Path.from(original);
final ui.Path copy = CkPath.from(original);
expect(copy, isA<CkPath>());
expect(copy.getBounds(), rect1);

View File

@ -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<FakePathMetrics> 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<Offset> 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<PathMetric> 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<FakePath> 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();
});
}