[web] remove obsolete object caches; simplify native object management (#40862)
[web] remove obsolete object caches; simplify native object management
diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter
index 387f2de..5fad7bc 100644
--- a/ci/licenses_golden/licenses_flutter
+++ b/ci/licenses_golden/licenses_flutter
@@ -1876,6 +1876,7 @@
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/mask_filter.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/n_way_canvas.dart + ../../../flutter/LICENSE
+ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/native_memory.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/noto_font.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/painting.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/path.dart + ../../../flutter/LICENSE
@@ -4458,6 +4459,7 @@
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_tree.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/mask_filter.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/n_way_canvas.dart
+FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/native_memory.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/noto_font.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/painting.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/path.dart
diff --git a/lib/web_ui/lib/src/engine.dart b/lib/web_ui/lib/src/engine.dart
index a82b9d9..07e54ca 100644
--- a/lib/web_ui/lib/src/engine.dart
+++ b/lib/web_ui/lib/src/engine.dart
@@ -39,6 +39,7 @@
export 'engine/canvaskit/layer_tree.dart';
export 'engine/canvaskit/mask_filter.dart';
export 'engine/canvaskit/n_way_canvas.dart';
+export 'engine/canvaskit/native_memory.dart';
export 'engine/canvaskit/noto_font.dart';
export 'engine/canvaskit/painting.dart';
export 'engine/canvaskit/path.dart';
diff --git a/lib/web_ui/lib/src/engine/canvaskit/canvas.dart b/lib/web_ui/lib/src/engine/canvaskit/canvas.dart
index 3d35df9..9a5dd90 100644
--- a/lib/web_ui/lib/src/engine/canvaskit/canvas.dart
+++ b/lib/web_ui/lib/src/engine/canvaskit/canvas.dart
@@ -215,7 +215,6 @@
offset.dx,
offset.dy,
);
- paragraph.markUsed();
}
void drawPath(CkPath path, CkPaint paint) {
@@ -1112,7 +1111,6 @@
offset.dx,
offset.dy,
);
- paragraph.markUsed();
}
}
diff --git a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart
index 6d70ee2..de79aa6 100644
--- a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart
+++ b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart
@@ -3569,9 +3569,12 @@
@JS('window.FinalizationRegistry')
@staticInterop
class SkObjectFinalizationRegistry {
- // TODO(hterkelsen): Add a type for the `cleanup` function when
- // native constructors support type parameters.
- external factory SkObjectFinalizationRegistry(JSFunction cleanup);
+ factory SkObjectFinalizationRegistry(JSFunction cleanup) {
+ return js_util.callConstructor(
+ _finalizationRegistryConstructor!.toObjectShallow,
+ <Object>[cleanup],
+ );
+ }
}
extension SkObjectFinalizationRegistryExtension on SkObjectFinalizationRegistry {
diff --git a/lib/web_ui/lib/src/engine/canvaskit/image.dart b/lib/web_ui/lib/src/engine/canvaskit/image.dart
index e43157d..3e16742 100644
--- a/lib/web_ui/lib/src/engine/canvaskit/image.dart
+++ b/lib/web_ui/lib/src/engine/canvaskit/image.dart
@@ -15,10 +15,10 @@
import 'canvaskit_api.dart';
import 'image_wasm_codecs.dart';
import 'image_web_codecs.dart';
+import 'native_memory.dart';
import 'painting.dart';
import 'picture.dart';
import 'picture_recorder.dart';
-import 'skia_object_cache.dart';
/// Instantiates a [ui.Codec] backed by an `SkAnimatedImage` from Skia.
FutureOr<ui.Codec> skiaInstantiateImageCodec(Uint8List list,
@@ -227,52 +227,8 @@
/// A [ui.Image] backed by an `SkImage` from Skia.
class CkImage implements ui.Image, StackTraceDebugger {
CkImage(SkImage skImage, { this.videoFrame }) {
+ box = CountedRef<CkImage, SkImage>(skImage, this, 'SkImage');
_init();
- if (browserSupportsFinalizationRegistry) {
- box = SkiaObjectBox<CkImage, SkImage>(this, skImage);
- } else {
- // If finalizers are not supported we need to be able to resurrect the
- // image if it was temporarily deleted. To do that, we keep the original
- // pixels and ask the SkiaObjectBox to make an image from them when
- // resurrecting.
- //
- // IMPORTANT: the alphaType, colorType, and colorSpace passed to
- // _encodeImage and to canvasKit.MakeImage must be the same. Otherwise
- // Skia will misinterpret the pixels and corrupt the image.
- final ByteData? originalBytes = _encodeImage(
- skImage: skImage,
- format: ui.ImageByteFormat.rawRgba,
- alphaType: canvasKit.AlphaType.Premul,
- colorType: canvasKit.ColorType.RGBA_8888,
- colorSpace: SkColorSpaceSRGB,
- );
- if (originalBytes == null) {
- printWarning('Unable to encode image to bytes. We will not '
- 'be able to resurrect it once it has been garbage collected.');
- return;
- }
- final int originalWidth = skImage.width().toInt();
- final int originalHeight = skImage.height().toInt();
- box = SkiaObjectBox<CkImage, SkImage>.resurrectable(this, skImage, () {
- final SkImage? skImage = canvasKit.MakeImage(
- SkImageInfo(
- alphaType: canvasKit.AlphaType.Premul,
- colorType: canvasKit.ColorType.RGBA_8888,
- colorSpace: SkColorSpaceSRGB,
- width: originalWidth.toDouble(),
- height: originalHeight.toDouble(),
- ),
- originalBytes.buffer.asUint8List(),
- (4 * originalWidth).toDouble(),
- );
- if (skImage == null) {
- throw ImageCodecException(
- 'Failed to resurrect image from pixels.'
- );
- }
- return skImage;
- });
- }
}
CkImage.cloneOf(this.box, {this.videoFrame}) {
@@ -291,9 +247,9 @@
StackTrace get debugStackTrace => _debugStackTrace;
late StackTrace _debugStackTrace;
- // Use a box because `SkImage` may be deleted either due to this object
+ // Use ref counting because `SkImage` may be deleted either due to this object
// being garbage-collected, or by an explicit call to [delete].
- late final SkiaObjectBox<CkImage, SkImage> box;
+ late final CountedRef<CkImage, SkImage> box;
/// For browsers that support `ImageDecoder` this field holds the video frame
/// from which this image was created.
@@ -305,9 +261,9 @@
/// The underlying Skia image object.
///
- /// Do not store the returned value. It is memory-managed by [SkiaObjectBox].
+ /// Do not store the returned value. It is memory-managed by [CountedRef].
/// Storing it may result in use-after-free bugs.
- SkImage get skImage => box.skiaObject;
+ SkImage get skImage => box.nativeObject;
bool _disposed = false;
diff --git a/lib/web_ui/lib/src/engine/canvaskit/native_memory.dart b/lib/web_ui/lib/src/engine/canvaskit/native_memory.dart
new file mode 100644
index 0000000..45b051c
--- /dev/null
+++ b/lib/web_ui/lib/src/engine/canvaskit/native_memory.dart
@@ -0,0 +1,216 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:js_interop';
+import 'package:meta/meta.dart';
+
+import '../../engine.dart' show Instrumentation;
+import '../util.dart';
+import 'canvaskit_api.dart';
+
+/// Collects native objects that weren't explicitly disposed of using
+/// [UniqueRef.dispose] or [CountedRef.unref].
+SkObjectFinalizationRegistry _finalizationRegistry = SkObjectFinalizationRegistry(
+ (UniqueRef<Object> uniq) {
+ uniq.collect();
+ }.toJS
+);
+
+NativeMemoryFinalizationRegistry nativeMemoryFinalizationRegistry = NativeMemoryFinalizationRegistry();
+
+/// An indirection to [SkObjectFinalizationRegistry] to enable tests provide a
+/// mock implementation of a finalization registry.
+class NativeMemoryFinalizationRegistry {
+ void register(Object owner, UniqueRef<Object> ref) {
+ _finalizationRegistry.register(owner, ref);
+ }
+}
+
+/// Manages the lifecycle of a C++ object referenced by a single Dart object.
+///
+/// It is expected that when the C++ object is no longer needed [dispose] is
+/// called.
+///
+/// To prevent memory leaks, the underlying C++ object is deleted by the GC if
+/// it wasn't previously disposed of explicitly.
+class UniqueRef<T extends Object> {
+ UniqueRef(Object owner, T nativeObject, this._debugOwnerLabel) {
+ _nativeObject = nativeObject;
+ if (Instrumentation.enabled) {
+ Instrumentation.instance.incrementCounter('$_debugOwnerLabel Created');
+ }
+ nativeMemoryFinalizationRegistry.register(owner, this);
+ }
+
+ T? _nativeObject;
+ final String _debugOwnerLabel;
+
+ /// Returns the underlying native object reference, if it has not been
+ /// disposed of yet.
+ ///
+ /// The returned reference must not be stored. I should only be borrowed
+ /// temporarily. Storing this reference may result in dangling pointer errors.
+ T get nativeObject {
+ assert(!isDisposed, 'Native object was disposed.');
+ return _nativeObject!;
+ }
+
+ /// Returns whether the underlying native object has been disposed and
+ /// therefore can no longer be used.
+ bool get isDisposed => _nativeObject == null;
+
+ /// Disposes the underlying native object.
+ ///
+ /// The underlying object may be deleted or its ref count may be bumped down.
+ /// The exact action taken depends on the sharing model of that particular
+ /// object. For example, an [SkImage] may not be immediately deleted if a
+ /// [SkPicture] exists that still references it. On the other hand, [SkPaint]
+ /// is deleted eagerly.
+ void dispose() {
+ assert(!isDisposed, 'A native object reference cannot be disposed more than once.');
+ if (Instrumentation.enabled) {
+ Instrumentation.instance.incrementCounter('$_debugOwnerLabel Deleted');
+ }
+ final SkDeletable object = nativeObject as SkDeletable;
+ if (!object.isDeleted()) {
+ object.delete();
+ }
+ _nativeObject = null;
+ }
+
+ /// Called by the garbage [Collector] when the owner of this handle is
+ /// collected.
+ ///
+ /// Garbage collection is used as a back-up for the cases when the handle
+ /// isn't disposed of explicitly by calling [dispose]. It most likely
+ /// indicates a memory leak or inefficiency in the framework or application
+ /// code.
+ @visibleForTesting
+ void collect() {
+ if (!isDisposed) {
+ if (Instrumentation.enabled) {
+ Instrumentation.instance.incrementCounter('$_debugOwnerLabel Leaked');
+ }
+ dispose();
+ }
+ }
+}
+
+/// Interface that classes wrapping [UniqueRef] must implement.
+///
+/// Used to collect stack traces in debug mode.
+abstract class StackTraceDebugger {
+ /// The stack trace pointing to code location that created or upreffed a
+ /// [CountedRef].
+ StackTrace get debugStackTrace;
+}
+
+/// Manages the lifecycle of a C++ object referenced by multiple Dart objects.
+///
+/// Uses reference counting to manage the lifecycle of the C++ object.
+///
+/// If the C++ object has a unique owner, use [UniqueRef] instead.
+///
+/// The [ref] method can be used to increment the refcount to tell this box to
+/// keep the underlying C++ object alive.
+///
+/// The [unref] method can be used to decrement the refcount indicating that a
+/// referring object no longer needs it. When the refcount drops to zero the
+/// underlying C++ object is deleted.
+///
+/// In addition to ref counting, this object is also managed by GC. When this
+/// reference is garbage collected, the underlying C++ object is automatically
+/// deleted. This is mostly done to prevent memory leaks in production. Well
+/// behaving framework and app code are expected to rely on [ref] and [unref]
+/// for timely collection of resources.
+class CountedRef<R extends StackTraceDebugger, T extends Object> {
+ /// Creates a counted reference.
+ CountedRef(T nativeObject, R debugReferrer, String debugLabel) {
+ _ref = UniqueRef<T>(this, nativeObject, debugLabel);
+ if (assertionsEnabled) {
+ debugReferrers.add(debugReferrer);
+ }
+ assert(refCount == debugReferrers.length);
+ }
+
+ /// The native object reference whose lifecycle is being managed by this ref
+ /// count.
+ ///
+ /// Do not store this value outside this class.
+ late final UniqueRef<T> _ref;
+
+ /// Returns the underlying native object reference, if it has not been
+ /// disposed of yet.
+ ///
+ /// The returned reference must not be stored. I should only be borrowed
+ /// temporarily. Storing this reference may result in dangling pointer errors.
+ T get nativeObject => _ref.nativeObject;
+
+ /// The number of objects sharing references to this box.
+ ///
+ /// When this count reaches zero, the underlying [nativeObject] is scheduled
+ /// for deletion.
+ int get refCount => _refCount;
+ int _refCount = 1;
+
+ /// Whether the underlying [nativeObject] has been disposed and is no longer
+ /// accessible.
+ bool get isDisposed => _ref.isDisposed;
+
+ /// When assertions are enabled, stores all objects that share this box.
+ ///
+ /// The length of this list is always identical to [refCount].
+ ///
+ /// This list can be used for debugging ref counting issues.
+ final Set<R> debugReferrers = <R>{};
+
+ /// If asserts are enabled, the [StackTrace]s representing when a reference
+ /// was created.
+ List<StackTrace> debugGetStackTraces() {
+ if (assertionsEnabled) {
+ return debugReferrers
+ .map<StackTrace>((R referrer) => referrer.debugStackTrace)
+ .toList();
+ }
+ throw UnsupportedError('');
+ }
+
+ /// Increases the reference count of this box because a new object began
+ /// sharing ownership of the underlying [nativeObject].
+ void ref(R debugReferrer) {
+ assert(
+ !_ref.isDisposed,
+ 'Cannot increment ref count on a deleted handle.',
+ );
+ assert(_refCount > 0);
+ assert(
+ debugReferrers.add(debugReferrer),
+ 'Attempted to increment ref count by the same referrer more than once.',
+ );
+ _refCount += 1;
+ assert(refCount == debugReferrers.length);
+ }
+
+ /// Decrements the reference count for the [nativeObject].
+ ///
+ /// Does nothing if the object has already been deleted.
+ ///
+ /// If this causes the reference count to drop to zero, deletes the
+ /// [nativeObject].
+ void unref(R debugReferrer) {
+ assert(
+ !_ref.isDisposed,
+ 'Attempted to unref an already deleted native object.',
+ );
+ assert(
+ debugReferrers.remove(debugReferrer),
+ 'Attempted to decrement ref count by the same referrer more than once.',
+ );
+ _refCount -= 1;
+ assert(refCount == debugReferrers.length);
+ if (_refCount == 0) {
+ _ref.dispose();
+ }
+ }
+}
diff --git a/lib/web_ui/lib/src/engine/canvaskit/picture.dart b/lib/web_ui/lib/src/engine/canvaskit/picture.dart
index 28511bb..936d589 100644
--- a/lib/web_ui/lib/src/engine/canvaskit/picture.dart
+++ b/lib/web_ui/lib/src/engine/canvaskit/picture.dart
@@ -46,8 +46,7 @@
/// false.
///
/// This extra flag is necessary on top of [rawSkiaObject] because
- /// [rawSkiaObject] being null does not indicate permanent deletion. See
- /// similar flag [SkiaObjectBox.isDeletedPermanently].
+ /// [rawSkiaObject] being null does not indicate permanent deletion.
bool _isDisposed = false;
/// The stack trace taken when [dispose] was called.
diff --git a/lib/web_ui/lib/src/engine/canvaskit/skia_object_cache.dart b/lib/web_ui/lib/src/engine/canvaskit/skia_object_cache.dart
index 21debea..677397b 100644
--- a/lib/web_ui/lib/src/engine/canvaskit/skia_object_cache.dart
+++ b/lib/web_ui/lib/src/engine/canvaskit/skia_object_cache.dart
@@ -7,7 +7,6 @@
import 'package:meta/meta.dart';
import '../../engine.dart' show Instrumentation;
-import '../util.dart';
import 'canvaskit_api.dart';
import 'renderer.dart';
@@ -88,85 +87,6 @@
}
}
-/// Like [SkiaObjectCache] but enforces the [maximumSize] of the cache
-/// synchronously instead of waiting until a post-frame callback.
-class SynchronousSkiaObjectCache {
- SynchronousSkiaObjectCache(this.maximumSize)
- : _itemQueue = DoubleLinkedQueue<SkiaObject<Object>>(),
- _itemMap = <SkiaObject<Object>, DoubleLinkedQueueEntry<SkiaObject<Object>>>{};
-
- /// This cache will never exceed this limit, even temporarily.
- final int maximumSize;
-
- /// A doubly linked list of the objects in the cache.
- ///
- /// This makes it fast to move a recently used object to the front.
- final DoubleLinkedQueue<SkiaObject<Object>> _itemQueue;
-
- /// A map of objects to their associated node in the [_itemQueue].
- ///
- /// This makes it fast to find the node in the queue when we need to
- /// move the object to the front of the queue.
- final Map<SkiaObject<Object>, DoubleLinkedQueueEntry<SkiaObject<Object>>> _itemMap;
-
- /// The number of objects in the cache.
- int get length => _itemQueue.length;
-
- /// Whether or not [object] is in the cache.
- ///
- /// This is only for testing.
- @visibleForTesting
- bool debugContains(SkiaObject<Object> object) {
- return _itemMap.containsKey(object);
- }
-
- /// Adds [object] to the cache.
- ///
- /// If adding [object] causes the total size of the cache to exceed
- /// [maximumSize], then the least recently used objects are evicted and
- /// deleted.
- void add(SkiaObject<Object> object) {
- assert(
- !_itemMap.containsKey(object),
- 'Cannot add object. Object is already in the cache: $object',
- );
- _itemQueue.addFirst(object);
- _itemMap[object] = _itemQueue.firstEntry()!;
- _enforceCacheLimit();
- }
-
- /// Marks the [object] as most recently used.
- ///
- /// If [object] is in the cache returns true. If the object is not in
- /// the cache, for example, because it was never added or because it was
- /// evicted as a result of the app reaching the cache limit, returns false.
- bool markUsed(SkiaObject<Object> object) {
- final DoubleLinkedQueueEntry<SkiaObject<Object>>? item = _itemMap[object];
-
- if (item == null) {
- return false;
- }
-
- item.remove();
- _itemQueue.addFirst(object);
- _itemMap[object] = _itemQueue.firstEntry()!;
- return true;
- }
-
- /// Ensures the cache does not exceed [maximumSize], evicting objects if
- /// necessary.
- ///
- /// Calls `delete` and `didDelete` on objects evicted from the cache.
- void _enforceCacheLimit() {
- while (_itemQueue.length > maximumSize) {
- final SkiaObject<Object> oldObject = _itemQueue.removeLast();
- _itemMap.remove(oldObject);
- oldObject.delete();
- oldObject.didDelete();
- }
- }
-}
-
/// An object backed by a JavaScript object mapped onto a Skia C++ object in the
/// WebAssembly heap.
///
@@ -311,205 +231,9 @@
bool get isResurrectionExpensive => false;
}
-/// Interface that classes wrapping [SkiaObjectBox] must implement.
-///
-/// Used to collect stack traces in debug mode.
-abstract class StackTraceDebugger {
- /// The stack trace pointing to code location that created or upreffed a
- /// [SkiaObjectBox].
- StackTrace get debugStackTrace;
-}
-
/// A function that restores a Skia object that was temporarily deleted.
typedef Resurrector<T> = T Function();
-/// Uses reference counting to manage the lifecycle of a Skia object owned by a
-/// wrapper object.
-///
-/// The [ref] method can be used to increment the refcount to tell this box to
-/// keep the underlying Skia object alive.
-///
-/// The [unref] method can be used to decrement the refcount to tell this box
-/// that a wrapper object no longer needs it. When the refcount drops to zero
-/// the underlying Skia object is deleted permanently (see [isDeletedPermanently]).
-///
-/// In addition to ref counting, this object is also managed by GC. In browsers
-/// that support [SkFinalizationRegistry] the underlying Skia object is deleted
-/// permanently when no JavaScript objects have references to this box. In
-/// browsers that do not support [SkFinalizationRegistry] the underlying Skia
-/// object may undergo several cycles of temporary deletions and resurrections
-/// prior to being deleted permanently. A temporary deletion may effectively
-/// be permanent if this object is garbage collected. This is safe because a
-/// temporarily deleted object has no C++ resources to collect.
-class SkiaObjectBox<R extends StackTraceDebugger, T extends Object>
- extends SkiaObject<T> {
- /// Creates an object box that's memory-managed using [SkFinalizationRegistry].
- ///
- /// This constructor must only be used if [browserSupportsFinalizationRegistry] is true.
- SkiaObjectBox(R debugReferrer, T initialValue) :
- assert(browserSupportsFinalizationRegistry), _resurrector = null {
- _initialize(debugReferrer, initialValue);
- Collector.instance.register(this, _skDeletable!);
- }
-
- /// Creates an object box that's memory-managed using a [Resurrector].
- ///
- /// This constructor must only be used if [browserSupportsFinalizationRegistry] is false.
- SkiaObjectBox.resurrectable(
- R debugReferrer, T initialValue, this._resurrector) :
- assert(!browserSupportsFinalizationRegistry) {
- _initialize(debugReferrer, initialValue);
- if (Instrumentation.enabled) {
- Instrumentation.instance.incrementCounter(
- '${_skDeletable?.constructor.name} created',
- );
- }
- SkiaObjects.manageExpensive(this);
- }
-
- void _initialize(R debugReferrer, T initialValue) {
- _update(initialValue);
- if (assertionsEnabled) {
- debugReferrers.add(debugReferrer);
- }
- assert(refCount == debugReferrers.length);
- }
-
- /// The number of objects sharing references to this box.
- ///
- /// When this count reaches zero, the underlying [skiaObject] is scheduled
- /// for deletion.
- int get refCount => _refCount;
- int _refCount = 1;
-
- /// When assertions are enabled, stores all objects that share this box.
- ///
- /// The length of this list is always identical to [refCount].
- ///
- /// This list can be used for debugging ref counting issues.
- final Set<R> debugReferrers = <R>{};
-
- /// If asserts are enabled, the [StackTrace]s representing when a reference
- /// was created.
- List<StackTrace> debugGetStackTraces() {
- if (assertionsEnabled) {
- return debugReferrers
- .map<StackTrace>((R referrer) => referrer.debugStackTrace)
- .toList();
- }
- throw UnsupportedError('');
- }
-
- /// The Skia object whose lifecycle is being managed.
- ///
- /// Do not store this value outside this box. It is memory-managed by
- /// [SkiaObjectBox]. Storing it may result in use-after-free bugs.
- T? rawSkiaObject;
- SkDeletable? _skDeletable;
- Resurrector<T>? _resurrector;
-
- void _update(T? newSkiaObject) {
- rawSkiaObject = newSkiaObject;
- _skDeletable = newSkiaObject as SkDeletable?;
- }
-
- @override
- T get skiaObject => rawSkiaObject ?? _doResurrect();
-
- T _doResurrect() {
- assert(!browserSupportsFinalizationRegistry);
- assert(_resurrector != null);
- assert(!_isDeletedPermanently, 'Cannot use deleted object.');
- _update(_resurrector!());
- if (Instrumentation.enabled) {
- Instrumentation.instance.incrementCounter(
- '${_skDeletable?.constructor.name} resurrected',
- );
- }
- SkiaObjects.manageExpensive(this);
- return skiaObject;
- }
-
- @override
- void delete() {
- _skDeletable?.delete();
- }
-
- @override
- void didDelete() {
- if (Instrumentation.enabled) {
- Instrumentation.instance.incrementCounter(
- '${_skDeletable?.constructor.name} deleted',
- );
- }
- assert(!browserSupportsFinalizationRegistry);
- _update(null);
- }
-
- /// Whether this object has been deleted permanently.
- ///
- /// If this is true it will remain true forever, and the Skia object is no
- /// longer resurrectable.
- ///
- /// See also [isDeletedTemporarily].
- bool get isDeletedPermanently => _isDeletedPermanently;
- bool _isDeletedPermanently = false;
-
- /// Whether the underlying [rawSkiaObject] has been deleted, but it may still
- /// be resurrected (see [SkiaObjectBox.resurrectable]).
- bool get isDeletedTemporarily =>
- rawSkiaObject == null && !_isDeletedPermanently;
-
- /// Increases the reference count of this box because a new object began
- /// sharing ownership of the underlying [skiaObject].
- ///
- /// Clones must be [dispose]d when finished.
- void ref(R debugReferrer) {
- assert(!_isDeletedPermanently,
- 'Cannot increment ref count on a deleted handle.');
- assert(_refCount > 0);
- assert(
- debugReferrers.add(debugReferrer),
- 'Attempted to increment ref count by the same referrer more than once.',
- );
- _refCount += 1;
- assert(refCount == debugReferrers.length);
- }
-
- /// Decrements the reference count for the [skObject].
- ///
- /// Does nothing if the object has already been deleted.
- ///
- /// If this causes the reference count to drop to zero, deletes the
- /// [skObject].
- void unref(R debugReferrer) {
- assert(!_isDeletedPermanently,
- 'Attempted to unref an already deleted Skia object.');
- assert(
- debugReferrers.remove(debugReferrer),
- 'Attempted to decrement ref count by the same referrer more than once.',
- );
- _refCount -= 1;
- assert(refCount == debugReferrers.length);
- if (_refCount == 0) {
- // The object may be null because it was deleted temporarily, i.e. it was
- // expecting the possibility of resurrection.
- if (_skDeletable != null) {
- if (browserSupportsFinalizationRegistry) {
- Collector.instance.collect(_skDeletable!);
- } else {
- delete();
- didDelete();
- }
- }
- rawSkiaObject = null;
- _skDeletable = null;
- _resurrector = null;
- _isDeletedPermanently = true;
- }
- }
-}
-
// ignore: avoid_classes_with_only_static_members
/// Singleton that manages the lifecycles of [SkiaObject] instances.
class SkiaObjects {
diff --git a/lib/web_ui/lib/src/engine/canvaskit/text.dart b/lib/web_ui/lib/src/engine/canvaskit/text.dart
index 2b4ab31..ed14a91 100644
--- a/lib/web_ui/lib/src/engine/canvaskit/text.dart
+++ b/lib/web_ui/lib/src/engine/canvaskit/text.dart
@@ -10,6 +10,7 @@
import '../util.dart';
import 'canvaskit_api.dart';
import 'font_fallbacks.dart';
+import 'native_memory.dart';
import 'painting.dart';
import 'renderer.dart';
import 'skia_object_cache.dart';
@@ -561,13 +562,20 @@
/// while painting a small subset of it. To achieve this a
/// [SynchronousSkiaObjectCache] is used that limits the number of live laid out
/// paragraphs at any point in time within or outside the frame.
-class CkParagraph extends SkiaObject<SkParagraph> implements ui.Paragraph {
- CkParagraph(this._skParagraph, this._paragraphStyle, this._paragraphCommands);
+class CkParagraph implements ui.Paragraph {
+ CkParagraph(SkParagraph skParagraph, this._paragraphStyle) {
+ _ref = UniqueRef<SkParagraph>(this, skParagraph, 'Paragraph');
+ }
- /// The result of calling `build()` on the JS CkParagraphBuilder.
+ late final UniqueRef<SkParagraph> _ref;
+
+ SkParagraph get skiaObject => _ref.nativeObject;
+
+ /// The constraints from the last time we laid the paragraph out.
///
- /// This may be invalidated later.
- SkParagraph? _skParagraph;
+ /// This is used to resurrect the paragraph if the initial paragraph
+ /// is deleted.
+ double _lastLayoutConstraints = double.negativeInfinity;
/// The paragraph style used to build this paragraph.
///
@@ -575,114 +583,6 @@
/// is deleted.
final CkParagraphStyle _paragraphStyle;
- /// The paragraph builder commands used to build this paragraph.
- ///
- /// This is used to resurrect the paragraph if the initial paragraph
- /// is deleted.
- final List<_ParagraphCommand> _paragraphCommands;
-
- /// The constraints from the last time we laid the paragraph out.
- ///
- /// This is used to resurrect the paragraph if the initial paragraph
- /// is deleted.
- ui.ParagraphConstraints? _lastLayoutConstraints;
-
- @override
- SkParagraph get skiaObject => _ensureInitialized(_lastLayoutConstraints!);
-
- SkParagraph _ensureInitialized(ui.ParagraphConstraints constraints) {
- SkParagraph? paragraph = _skParagraph;
-
- // Paragraph objects are immutable. It's OK to skip initialization and reuse
- // existing object.
- bool didRebuildSkiaObject = false;
- if (paragraph == null) {
- final CkParagraphBuilder builder = CkParagraphBuilder(_paragraphStyle);
- for (final _ParagraphCommand command in _paragraphCommands) {
- switch (command.type) {
- case _ParagraphCommandType.addText:
- builder.addText(command.text!);
- case _ParagraphCommandType.pop:
- builder.pop();
- case _ParagraphCommandType.pushStyle:
- builder.pushStyle(command.style!);
- case _ParagraphCommandType.addPlaceholder:
- builder._addPlaceholder(command.placeholderStyle!);
- }
- }
- paragraph = builder._buildSkParagraph();
- _skParagraph = paragraph;
- didRebuildSkiaObject = true;
- }
-
- final bool constraintsChanged = _lastLayoutConstraints != constraints;
- if (didRebuildSkiaObject || constraintsChanged) {
- _lastLayoutConstraints = constraints;
- // TODO(het): CanvasKit throws an exception when laid out with
- // a font that wasn't registered.
- try {
- paragraph.layout(constraints.width);
- _alphabeticBaseline = paragraph.getAlphabeticBaseline();
- _didExceedMaxLines = paragraph.didExceedMaxLines();
- _height = paragraph.getHeight();
- _ideographicBaseline = paragraph.getIdeographicBaseline();
- _longestLine = paragraph.getLongestLine();
- _maxIntrinsicWidth = paragraph.getMaxIntrinsicWidth();
- _minIntrinsicWidth = paragraph.getMinIntrinsicWidth();
- _width = paragraph.getMaxWidth();
- _boxesForPlaceholders =
- skRectsToTextBoxes(paragraph.getRectsForPlaceholders());
- } catch (e) {
- printWarning('CanvasKit threw an exception while laying '
- 'out the paragraph. The font was "${_paragraphStyle._fontFamily}". '
- 'Exception:\n$e');
- rethrow;
- }
- }
-
- return paragraph;
- }
-
- // Caches laid out paragraphs and synchronously reclaims memory if there's
- // memory pressure.
- //
- // On May 26, 2021, 500 seemed like a reasonable number to pick for the cache
- // size. At the time a single laid out SkParagraph used 100KB of memory. So,
- // 500 items in the cache is roughly 50MB of memory, which is not too high,
- // while at the same time enough for most use-cases.
- //
- // TODO(yjbanov): this strategy is not sufficient for the use-case where a
- // lot of paragraphs are laid out _and_ rendered. To support
- // this use-case without blowing up memory usage we need this:
- // https://github.com/flutter/flutter/issues/81224
- static final SynchronousSkiaObjectCache _paragraphCache =
- SynchronousSkiaObjectCache(500);
-
- /// Marks this paragraph as having been used this frame.
- ///
- /// Puts this paragraph in a [SynchronousSkiaObjectCache], which will delete it
- /// if there's memory pressure to do so. This protects our memory usage from
- /// blowing up if within a single frame the framework needs to layout a lot of
- /// paragraphs. One common use-case is `ListView.builder`, which needs to layout
- /// more of its content than it actually renders to compute the scroll position.
- void markUsed() {
- // If the paragraph is already in the cache, just mark it as most recently
- // used. Otherwise, add to cache.
- if (!_paragraphCache.markUsed(this)) {
- _paragraphCache.add(this);
- }
- }
-
- @override
- void delete() {
- _skParagraph?.delete();
- _skParagraph = null;
- }
-
- @override
- void didDelete() {
- _skParagraph = null;
- }
@override
double get alphabeticBaseline => _alphabeticBaseline;
@@ -727,12 +627,12 @@
ui.BoxHeightStyle boxHeightStyle = ui.BoxHeightStyle.tight,
ui.BoxWidthStyle boxWidthStyle = ui.BoxWidthStyle.tight,
}) {
+ assert(!_disposed, 'Paragraph has been disposed.');
if (start < 0 || end < 0) {
return const <ui.TextBox>[];
}
- final SkParagraph paragraph = _ensureInitialized(_lastLayoutConstraints!);
- final List<SkRectWithDirection> skRects = paragraph.getRectsForRange(
+ final List<SkRectWithDirection> skRects = skiaObject.getRectsForRange(
start.toDouble(),
end.toDouble(),
toSkRectHeightStyle(boxHeightStyle),
@@ -743,6 +643,7 @@
}
List<ui.TextBox> skRectsToTextBoxes(List<SkRectWithDirection> skRects) {
+ assert(!_disposed, 'Paragraph has been disposed.');
final List<ui.TextBox> result = <ui.TextBox>[];
for (int i = 0; i < skRects.length; i++) {
@@ -763,9 +664,8 @@
@override
ui.TextPosition getPositionForOffset(ui.Offset offset) {
- final SkParagraph paragraph = _ensureInitialized(_lastLayoutConstraints!);
- final SkTextPosition positionWithAffinity =
- paragraph.getGlyphPositionAtCoordinate(
+ assert(!_disposed, 'Paragraph has been disposed.');
+ final SkTextPosition positionWithAffinity = skiaObject.getGlyphPositionAtCoordinate(
offset.dx,
offset.dy,
);
@@ -774,7 +674,7 @@
@override
ui.TextRange getWordBoundary(ui.TextPosition position) {
- final SkParagraph paragraph = _ensureInitialized(_lastLayoutConstraints!);
+ assert(!_disposed, 'Paragraph has been disposed.');
final int characterPosition;
switch (position.affinity) {
case ui.TextAffinity.upstream:
@@ -782,26 +682,46 @@
case ui.TextAffinity.downstream:
characterPosition = position.offset;
}
- final SkTextRange skRange = paragraph.getWordBoundary(characterPosition.toDouble());
+ final SkTextRange skRange = skiaObject.getWordBoundary(characterPosition.toDouble());
return ui.TextRange(start: skRange.start.toInt(), end: skRange.end.toInt());
}
@override
void layout(ui.ParagraphConstraints constraints) {
- if (_lastLayoutConstraints == constraints) {
+ assert(!_disposed, 'Paragraph has been disposed.');
+ if (_lastLayoutConstraints == constraints.width) {
return;
}
- _ensureInitialized(constraints);
- // See class-level and _paragraphCache doc comments for why we're releasing
- // the paragraph immediately after layout.
- markUsed();
+ _lastLayoutConstraints = constraints.width;
+
+ // TODO(het): CanvasKit throws an exception when laid out with
+ // a font that wasn't registered.
+ try {
+ final SkParagraph paragraph = skiaObject;
+ paragraph.layout(constraints.width);
+ _alphabeticBaseline = paragraph.getAlphabeticBaseline();
+ _didExceedMaxLines = paragraph.didExceedMaxLines();
+ _height = paragraph.getHeight();
+ _ideographicBaseline = paragraph.getIdeographicBaseline();
+ _longestLine = paragraph.getLongestLine();
+ _maxIntrinsicWidth = paragraph.getMaxIntrinsicWidth();
+ _minIntrinsicWidth = paragraph.getMinIntrinsicWidth();
+ _width = paragraph.getMaxWidth();
+ _boxesForPlaceholders =
+ skRectsToTextBoxes(paragraph.getRectsForPlaceholders());
+ } catch (e) {
+ printWarning('CanvasKit threw an exception while laying '
+ 'out the paragraph. The font was "${_paragraphStyle._fontFamily}". '
+ 'Exception:\n$e');
+ rethrow;
+ }
}
@override
ui.TextRange getLineBoundary(ui.TextPosition position) {
- final SkParagraph paragraph = _ensureInitialized(_lastLayoutConstraints!);
- final List<SkLineMetrics> metrics = paragraph.getLineMetrics();
+ assert(!_disposed, 'Paragraph has been disposed.');
+ final List<SkLineMetrics> metrics = skiaObject.getLineMetrics();
final int offset = position.offset;
for (final SkLineMetrics metric in metrics) {
if (offset >= metric.startIndex && offset <= metric.endIndex) {
@@ -813,8 +733,8 @@
@override
List<ui.LineMetrics> computeLineMetrics() {
- final SkParagraph paragraph = _ensureInitialized(_lastLayoutConstraints!);
- final List<SkLineMetrics> skLineMetrics = paragraph.getLineMetrics();
+ assert(!_disposed, 'Paragraph has been disposed.');
+ final List<SkLineMetrics> skLineMetrics = skiaObject.getLineMetrics();
final List<ui.LineMetrics> result = <ui.LineMetrics>[];
for (final SkLineMetrics metric in skLineMetrics) {
result.add(CkLineMetrics._(metric));
@@ -826,8 +746,8 @@
@override
void dispose() {
- delete();
- didDelete();
+ assert(!_disposed, 'Paragraph has been disposed.');
+ _ref.dispose();
_disposed = true;
}
@@ -877,8 +797,7 @@
class CkParagraphBuilder implements ui.ParagraphBuilder {
CkParagraphBuilder(ui.ParagraphStyle style)
- : _commands = <_ParagraphCommand>[],
- _style = style as CkParagraphStyle,
+ : _style = style as CkParagraphStyle,
_placeholderCount = 0,
_placeholderScales = <double>[],
_styleStack = <CkTextStyle>[],
@@ -891,7 +810,6 @@
final SkParagraphBuilder _paragraphBuilder;
final CkParagraphStyle _style;
- final List<_ParagraphCommand> _commands;
int _placeholderCount;
final List<double> _placeholderScales;
final List<CkTextStyle> _styleStack;
@@ -923,7 +841,6 @@
}
void _addPlaceholder(_CkParagraphPlaceholder placeholderStyle) {
- _commands.add(_ParagraphCommand.addPlaceholder(placeholderStyle));
_paragraphBuilder.addPlaceholder(
placeholderStyle.width,
placeholderStyle.height,
@@ -961,14 +878,13 @@
fontFamilies.addAll(style.fontFamilyFallback!);
}
FontFallbackData.instance.ensureFontsSupportText(text, fontFamilies);
- _commands.add(_ParagraphCommand.addText(text));
_paragraphBuilder.addText(text);
}
@override
CkParagraph build() {
final SkParagraph builtParagraph = _buildSkParagraph();
- return CkParagraph(builtParagraph, _style, _commands);
+ return CkParagraph(builtParagraph, _style);
}
/// Builds the CkParagraph with the builder and deletes the builder.
@@ -999,7 +915,6 @@
}
return;
}
- _commands.add(const _ParagraphCommand.pop());
_styleStack.removeLast();
_paragraphBuilder.pop();
}
@@ -1026,7 +941,6 @@
final CkTextStyle ckStyle = style as CkTextStyle;
final CkTextStyle skStyle = baseStyle.mergeWith(ckStyle);
_styleStack.add(skStyle);
- _commands.add(_ParagraphCommand.pushStyle(ckStyle));
if (skStyle.foreground != null || skStyle.background != null) {
SkPaint? foreground = skStyle.foreground?.skiaObject;
if (foreground == null) {
@@ -1062,41 +976,6 @@
final double offset;
}
-class _ParagraphCommand {
- const _ParagraphCommand._(
- this.type,
- this.text,
- this.style,
- this.placeholderStyle,
- );
-
- const _ParagraphCommand.addText(String text)
- : this._(_ParagraphCommandType.addText, text, null, null);
-
- const _ParagraphCommand.pop()
- : this._(_ParagraphCommandType.pop, null, null, null);
-
- const _ParagraphCommand.pushStyle(CkTextStyle style)
- : this._(_ParagraphCommandType.pushStyle, null, style, null);
-
- const _ParagraphCommand.addPlaceholder(
- _CkParagraphPlaceholder placeholderStyle)
- : this._(
- _ParagraphCommandType.addPlaceholder, null, null, placeholderStyle);
-
- final _ParagraphCommandType type;
- final String? text;
- final CkTextStyle? style;
- final _CkParagraphPlaceholder? placeholderStyle;
-}
-
-enum _ParagraphCommandType {
- addText,
- pop,
- pushStyle,
- addPlaceholder,
-}
-
List<String> _getEffectiveFontFamilies(String? fontFamily,
[List<String>? fontFamilyFallback]) {
final List<String> fontFamilies = <String>[];
diff --git a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart
index 6bdd2b3..16260d4 100644
--- a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart
+++ b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart
@@ -2,6 +2,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
+import 'dart:js_interop';
import 'dart:math';
import 'dart:typed_data';
@@ -1432,7 +1433,6 @@
builder.addText('Hello');
final CkParagraph paragraph = builder.build();
- paragraph.delete();
paragraph.dispose();
expect(paragraph.debugDisposed, true);
});
@@ -1894,6 +1894,15 @@
'http://localhost:1234/foo/canvaskit.wasm',
);
});
+
+ test('SkObjectFinalizationRegistry', () {
+ // There's no reliable way to test the actual functionality of
+ // FinalizationRegistry because it depends on GC, which cannot be controlled,
+ // So the test simply tests that a FinalizationRegistry can be constructed
+ // and its `register` method can be called.
+ final SkObjectFinalizationRegistry registry = SkObjectFinalizationRegistry((String arg) {}.toJS);
+ registry.register(Object(), Object());
+ });
}
diff --git a/lib/web_ui/test/canvaskit/image_golden_test.dart b/lib/web_ui/test/canvaskit/image_golden_test.dart
index 3fae53f..ffc9507 100644
--- a/lib/web_ui/test/canvaskit/image_golden_test.dart
+++ b/lib/web_ui/test/canvaskit/image_golden_test.dart
@@ -137,10 +137,10 @@
.makeImageAtCurrentFrame();
final CkImage image = CkImage(skImage);
expect(image.debugDisposed, isFalse);
- expect(image.box.isDeletedPermanently, isFalse);
+ expect(image.box.isDisposed, isFalse);
image.dispose();
expect(image.debugDisposed, isTrue);
- expect(image.box.isDeletedPermanently, isTrue);
+ expect(image.box.isDisposed, isTrue);
// Disallow double-dispose.
expect(() => image.dispose(), throwsAssertionError);
@@ -152,7 +152,7 @@
canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage)!
.makeImageAtCurrentFrame();
final CkImage image = CkImage(skImage);
- final SkiaObjectBox<CkImage, SkImage> box = image.box;
+ final CountedRef<CkImage, SkImage> box = image.box;
expect(box.refCount, 1);
expect(box.debugGetStackTraces().length, 1);
@@ -161,19 +161,19 @@
expect(box.debugGetStackTraces().length, 2);
expect(image.isCloneOf(clone), isTrue);
- expect(box.isDeletedPermanently, isFalse);
+ expect(box.isDisposed, isFalse);
testCollector.collectNow();
expect(skImage.isDeleted(), isFalse);
image.dispose();
expect(box.refCount, 1);
- expect(box.isDeletedPermanently, isFalse);
+ expect(box.isDisposed, isFalse);
testCollector.collectNow();
expect(skImage.isDeleted(), isFalse);
clone.dispose();
expect(box.refCount, 0);
- expect(box.isDeletedPermanently, isTrue);
+ expect(box.isDisposed, isTrue);
testCollector.collectNow();
expect(skImage.isDeleted(), isTrue);
@@ -266,26 +266,6 @@
// TODO(hterkelsen): Firefox and Safari do not currently support ImageDecoder.
}, skip: isFirefox || isSafari);
- // Regression test for https://github.com/flutter/flutter/issues/72469
- test('CkImage can be resurrected', () {
- browserSupportsFinalizationRegistry = false;
- final SkImage skImage =
- canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage)!
- .makeImageAtCurrentFrame();
- final CkImage image = CkImage(skImage);
- expect(image.box.rawSkiaObject, isNotNull);
-
- // Pretend that the image is temporarily deleted.
- image.box.delete();
- image.box.didDelete();
- expect(image.box.rawSkiaObject, isNull);
-
- // Attempting to access the skia object here would previously throw
- // "Stack Overflow" in Safari.
- expect(image.box.skiaObject, isNotNull);
- testCollector.collectNow();
- });
-
test('skiaInstantiateWebImageCodec loads an image from the network',
() async {
mockHttpFetchResponseFactory = (String url) async {
diff --git a/lib/web_ui/test/canvaskit/native_memory_test.dart b/lib/web_ui/test/canvaskit/native_memory_test.dart
new file mode 100644
index 0000000..42a99d2
--- /dev/null
+++ b/lib/web_ui/test/canvaskit/native_memory_test.dart
@@ -0,0 +1,258 @@
+// Copyright 2013 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+import 'dart:js_interop';
+import 'package:js/js.dart';
+
+import 'package:test/bootstrap/browser.dart';
+import 'package:test/test.dart';
+
+import 'package:ui/src/engine.dart';
+
+import 'common.dart';
+
+void main() {
+ internalBootstrapBrowserTest(() => testMain);
+}
+
+void testMain() {
+ setUpCanvasKitTest();
+
+ late _MockNativeMemoryFinalizationRegistry mockFinalizationRegistry;
+
+ setUp(() {
+ TestSkDeletableMock.deleteCount = 0;
+ nativeMemoryFinalizationRegistry = mockFinalizationRegistry = _MockNativeMemoryFinalizationRegistry();
+ });
+
+ tearDown(() {
+ nativeMemoryFinalizationRegistry = NativeMemoryFinalizationRegistry();
+ });
+
+ group(UniqueRef, () {
+ test('create-dispose-collect cycle', () {
+ expect(mockFinalizationRegistry.registeredPairs, hasLength(0));
+ final Object owner = Object();
+ final TestSkDeletable nativeObject = TestSkDeletable();
+ final UniqueRef<TestSkDeletable> ref = UniqueRef<TestSkDeletable>(owner, nativeObject, 'TestSkDeletable');
+ expect(ref.isDisposed, isFalse);
+ expect(ref.nativeObject, same(nativeObject));
+ expect(TestSkDeletableMock.deleteCount, 0);
+ expect(mockFinalizationRegistry.registeredPairs, hasLength(1));
+ expect(mockFinalizationRegistry.registeredPairs.single.owner, same(owner));
+ expect(mockFinalizationRegistry.registeredPairs.single.ref, same(ref));
+
+ ref.dispose();
+ expect(TestSkDeletableMock.deleteCount, 1);
+ expect(ref.isDisposed, isTrue);
+ expect(
+ reason: 'Cannot access object that was disposed',
+ () => ref.nativeObject, throwsA(isA<AssertionError>()),
+ );
+ expect(
+ reason: 'Cannot dispose object more than once',
+ () => ref.dispose(), throwsA(isA<AssertionError>()),
+ );
+ expect(TestSkDeletableMock.deleteCount, 1);
+
+ // Simulate a GC
+ mockFinalizationRegistry.registeredPairs.single.ref.collect();
+ expect(
+ reason: 'Manually disposed object should not be deleted again by GC.',
+ TestSkDeletableMock.deleteCount, 1,
+ );
+ });
+
+ test('create-collect cycle', () {
+ expect(mockFinalizationRegistry.registeredPairs, hasLength(0));
+ final Object owner = Object();
+ final TestSkDeletable nativeObject = TestSkDeletable();
+ final UniqueRef<TestSkDeletable> ref = UniqueRef<TestSkDeletable>(owner, nativeObject, 'TestSkDeletable');
+ expect(ref.isDisposed, isFalse);
+ expect(ref.nativeObject, same(nativeObject));
+ expect(TestSkDeletableMock.deleteCount, 0);
+ expect(mockFinalizationRegistry.registeredPairs, hasLength(1));
+
+ ref.collect();
+ expect(TestSkDeletableMock.deleteCount, 1);
+ // There's nothing else to test for any practical gain. UniqueRef.collect
+ // is called when GC decided that the owner is no longer reachable. So
+ // there must not be anything else calling into this object for anything
+ // useful.
+ });
+ });
+
+ group(CountedRef, () {
+ test('single owner', () {
+ expect(mockFinalizationRegistry.registeredPairs, hasLength(0));
+ final TestSkDeletable nativeObject = TestSkDeletable();
+ final TestCountedRefOwner owner = TestCountedRefOwner(nativeObject);
+ expect(owner.ref.debugReferrers, hasLength(1));
+ expect(owner.ref.debugReferrers.single, owner);
+ expect(owner.ref.refCount, 1);
+ expect(owner.ref.nativeObject, nativeObject);
+ expect(TestSkDeletableMock.deleteCount, 0);
+ expect(mockFinalizationRegistry.registeredPairs, hasLength(1));
+
+ owner.dispose();
+ expect(owner.ref.debugReferrers, isEmpty);
+ expect(owner.ref.refCount, 0);
+ expect(
+ reason: 'Cannot access object that was disposed',
+ () => owner.ref.nativeObject, throwsA(isA<AssertionError>()),
+ );
+ expect(TestSkDeletableMock.deleteCount, 1);
+
+ expect(
+ reason: 'Cannot dispose object more than once',
+ () => owner.dispose(), throwsA(isA<AssertionError>()),
+ );
+ });
+
+ test('multiple owners', () {
+ expect(mockFinalizationRegistry.registeredPairs, hasLength(0));
+ final TestSkDeletable nativeObject = TestSkDeletable();
+ final TestCountedRefOwner owner1 = TestCountedRefOwner(nativeObject);
+ expect(owner1.ref.debugReferrers, hasLength(1));
+ expect(owner1.ref.debugReferrers.single, owner1);
+ expect(owner1.ref.refCount, 1);
+ expect(owner1.ref.nativeObject, nativeObject);
+ expect(TestSkDeletableMock.deleteCount, 0);
+ expect(mockFinalizationRegistry.registeredPairs, hasLength(1));
+
+ final TestCountedRefOwner owner2 = owner1.clone();
+ expect(owner2.ref, same(owner1.ref));
+ expect(owner2.ref.debugReferrers, hasLength(2));
+ expect(owner2.ref.debugReferrers.first, owner1);
+ expect(owner2.ref.debugReferrers.last, owner2);
+ expect(owner2.ref.refCount, 2);
+ expect(owner2.ref.nativeObject, nativeObject);
+ expect(TestSkDeletableMock.deleteCount, 0);
+ expect(
+ reason: 'Second owner does not add more native object owners. '
+ 'The underlying shared UniqueRef is the only one.',
+ mockFinalizationRegistry.registeredPairs, hasLength(1),
+ );
+
+ owner1.dispose();
+ expect(owner2.ref.debugReferrers, hasLength(1));
+ expect(owner2.ref.debugReferrers.single, owner2);
+ expect(owner2.ref.refCount, 1);
+ expect(owner2.ref.nativeObject, nativeObject);
+ expect(TestSkDeletableMock.deleteCount, 0);
+ expect(
+ reason: 'The same owner cannot dispose its CountedRef more than once, even when CountedRef is still alive.',
+ () => owner1.dispose(), throwsA(isA<AssertionError>()),
+ );
+
+ owner2.dispose();
+ expect(owner2.ref.debugReferrers, isEmpty);
+ expect(owner2.ref.refCount, 0);
+ expect(
+ reason: 'Cannot access object that was disposed',
+ () => owner2.ref.nativeObject, throwsA(isA<AssertionError>()),
+ );
+ expect(TestSkDeletableMock.deleteCount, 1);
+
+ expect(
+ reason: 'The same owner cannot dispose its CountedRef more than once.',
+ () => owner2.dispose(), throwsA(isA<AssertionError>()),
+ );
+
+ // Simulate a GC
+ mockFinalizationRegistry.registeredPairs.single.ref.collect();
+ expect(
+ reason: 'Manually disposed object should not be deleted again by GC.',
+ TestSkDeletableMock.deleteCount, 1,
+ );
+ });
+ });
+}
+
+class TestSkDeletableMock {
+ static int deleteCount = 0;
+
+ bool isDeleted() => _isDeleted;
+ bool _isDeleted = false;
+
+ void delete() {
+ expect(_isDeleted, isFalse,
+ reason:
+ 'CanvasKit does not allow deleting the same object more than once.');
+ _isDeleted = true;
+ deleteCount++;
+ }
+
+ JsConstructor get constructor => TestJsConstructor(name: 'TestSkDeletable');
+}
+
+@JS()
+@anonymous
+@staticInterop
+class TestSkDeletable implements SkDeletable {
+ factory TestSkDeletable() {
+ final TestSkDeletableMock mock = TestSkDeletableMock();
+ return TestSkDeletable._(
+ isDeleted: () { return mock.isDeleted(); }.toJS,
+ delete: () { return mock.delete(); }.toJS,
+ constructor: mock.constructor);
+ }
+
+ external factory TestSkDeletable._({
+ JSFunction isDeleted,
+ JSFunction delete,
+ JsConstructor constructor});
+}
+
+@JS()
+@anonymous
+@staticInterop
+class TestJsConstructor implements JsConstructor {
+ external factory TestJsConstructor({String name});
+}
+
+class TestCountedRefOwner implements StackTraceDebugger {
+ TestCountedRefOwner(TestSkDeletable nativeObject) {
+ if (assertionsEnabled) {
+ _debugStackTrace = StackTrace.current;
+ }
+ ref = CountedRef<TestCountedRefOwner, TestSkDeletable>(
+ nativeObject, this, 'TestCountedRefOwner');
+ }
+
+ TestCountedRefOwner.cloneOf(this.ref) {
+ if (assertionsEnabled) {
+ _debugStackTrace = StackTrace.current;
+ }
+ ref.ref(this);
+ }
+
+ @override
+ StackTrace get debugStackTrace => _debugStackTrace;
+ late StackTrace _debugStackTrace;
+
+ late final CountedRef<TestCountedRefOwner, TestSkDeletable> ref;
+
+ void dispose() {
+ ref.unref(this);
+ }
+
+ TestCountedRefOwner clone() => TestCountedRefOwner.cloneOf(ref);
+}
+
+class _MockNativeMemoryFinalizationRegistry implements NativeMemoryFinalizationRegistry {
+ final List<_MockPair> registeredPairs = <_MockPair>[];
+
+ @override
+ void register(Object owner, UniqueRef<Object> ref) {
+ registeredPairs.add(_MockPair(owner, ref));
+ }
+}
+
+class _MockPair {
+ _MockPair(this.owner, this.ref);
+
+ Object owner;
+ UniqueRef<Object> ref;
+}
diff --git a/lib/web_ui/test/canvaskit/skia_objects_cache_test.dart b/lib/web_ui/test/canvaskit/skia_objects_cache_test.dart
index 9e7def1..14df75d 100644
--- a/lib/web_ui/test/canvaskit/skia_objects_cache_test.dart
+++ b/lib/web_ui/test/canvaskit/skia_objects_cache_test.dart
@@ -12,8 +12,6 @@
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
-import '../common/matchers.dart';
-import '../common/spy.dart';
import 'common.dart';
void main() {
@@ -113,242 +111,6 @@
expect(SkiaObjects.expensiveCache.debugContains(object2), isFalse);
});
});
-
- group(SkiaObjectBox, () {
- test('Records stack traces and respects refcounts', () async {
- final ZoneSpy spy = ZoneSpy();
- spy.run(() {
- Instrumentation.enabled = true;
- TestSkDeletableMock.deleteCount = 0;
- TestBoxWrapper.resurrectCount = 0;
- final TestBoxWrapper original = TestBoxWrapper();
-
- expect(original.box.debugGetStackTraces().length, 1);
- expect(original.box.refCount, 1);
- expect(original.box.isDeletedPermanently, isFalse);
-
- final TestBoxWrapper clone = original.clone();
- expect(clone.box, same(original.box));
- expect(clone.box.debugGetStackTraces().length, 2);
- expect(clone.box.refCount, 2);
- expect(original.box.debugGetStackTraces().length, 2);
- expect(original.box.refCount, 2);
- expect(original.box.isDeletedPermanently, isFalse);
-
- original.dispose();
-
- testCollector.collectNow();
- expect(TestSkDeletableMock.deleteCount, 0);
-
- spy.fakeAsync.elapse(const Duration(seconds: 2));
- expect(
- spy.printLog,
- <String>[
- 'Engine counters:\n TestSkDeletable created: 1\n'
- ],
- );
-
- expect(clone.box.debugGetStackTraces().length, 1);
- expect(clone.box.refCount, 1);
- expect(original.box.debugGetStackTraces().length, 1);
- expect(original.box.refCount, 1);
-
- clone.dispose();
- expect(clone.box.debugGetStackTraces().length, 0);
- expect(clone.box.refCount, 0);
- expect(original.box.debugGetStackTraces().length, 0);
- expect(original.box.refCount, 0);
- expect(original.box.isDeletedPermanently, isTrue);
-
- testCollector.collectNow();
- expect(TestSkDeletableMock.deleteCount, 1);
- expect(TestBoxWrapper.resurrectCount, 0);
-
- expect(() => clone.box.unref(clone), throwsAssertionError);
- spy.printLog.clear();
- spy.fakeAsync.elapse(const Duration(seconds: 2));
- expect(
- spy.printLog,
- <String>[
- 'Engine counters:\n TestSkDeletable created: 1\n TestSkDeletable deleted: 1\n'
- ],
- );
- Instrumentation.enabled = false;
- });
- });
-
- test('Can resurrect Skia objects', () async {
- TestSkDeletableMock.deleteCount = 0;
- TestBoxWrapper.resurrectCount = 0;
- final TestBoxWrapper object = TestBoxWrapper();
- expect(TestSkDeletableMock.deleteCount, 0);
- expect(TestBoxWrapper.resurrectCount, 0);
-
- // Test 3 cycles of delete/resurrect.
- for (int i = 0; i < 3; i++) {
- object.box.delete();
- object.box.didDelete();
- expect(TestSkDeletableMock.deleteCount, i + 1);
- expect(TestBoxWrapper.resurrectCount, i);
- expect(object.box.isDeletedTemporarily, isTrue);
- expect(object.box.isDeletedPermanently, isFalse);
-
- expect(object.box.skiaObject, isNotNull);
- expect(TestSkDeletableMock.deleteCount, i + 1);
- expect(TestBoxWrapper.resurrectCount, i + 1);
- expect(object.box.isDeletedTemporarily, isFalse);
- expect(object.box.isDeletedPermanently, isFalse);
- }
-
- object.dispose();
- expect(object.box.isDeletedPermanently, isTrue);
- });
-
- test('Can dispose temporarily deleted object', () async {
- TestSkDeletableMock.deleteCount = 0;
- TestBoxWrapper.resurrectCount = 0;
- final TestBoxWrapper object = TestBoxWrapper();
- expect(TestSkDeletableMock.deleteCount, 0);
- expect(TestBoxWrapper.resurrectCount, 0);
-
- object.box.delete();
- object.box.didDelete();
- expect(TestSkDeletableMock.deleteCount, 1);
- expect(TestBoxWrapper.resurrectCount, 0);
- expect(object.box.isDeletedTemporarily, isTrue);
- expect(object.box.isDeletedPermanently, isFalse);
-
- object.dispose();
- expect(object.box.isDeletedPermanently, isTrue);
- });
- });
-
- group('$SynchronousSkiaObjectCache', () {
- test('is initialized empty', () {
- expect(SynchronousSkiaObjectCache(10), hasLength(0));
- });
-
- test('adds objects', () {
- final SynchronousSkiaObjectCache cache = SynchronousSkiaObjectCache(2);
- cache.add(TestSelfManagedObject());
- expect(cache, hasLength(1));
- cache.add(TestSelfManagedObject());
- expect(cache, hasLength(2));
- });
-
- test('forbids adding the same object twice', () {
- final SynchronousSkiaObjectCache cache = SynchronousSkiaObjectCache(2);
- final TestSelfManagedObject object = TestSelfManagedObject();
- cache.add(object);
- expect(cache, hasLength(1));
- expect(() => cache.add(object), throwsAssertionError);
- });
-
- void expectObjectInCache(
- SynchronousSkiaObjectCache cache,
- TestSelfManagedObject object,
- ) {
- expect(cache.debugContains(object), isTrue);
- expect(object._skiaObject, isNotNull);
- }
-
- void expectObjectNotInCache(
- SynchronousSkiaObjectCache cache,
- TestSelfManagedObject object,
- ) {
- expect(cache.debugContains(object), isFalse);
- expect(object._skiaObject, isNull);
- }
-
- test('respects maximumSize', () {
- final SynchronousSkiaObjectCache cache = SynchronousSkiaObjectCache(2);
- final TestSelfManagedObject object1 = TestSelfManagedObject();
- final TestSelfManagedObject object2 = TestSelfManagedObject();
- final TestSelfManagedObject object3 = TestSelfManagedObject();
- final TestSelfManagedObject object4 = TestSelfManagedObject();
-
- cache.add(object1);
- expect(cache, hasLength(1));
- expectObjectInCache(cache, object1);
-
- cache.add(object2);
- expect(cache, hasLength(2));
- expectObjectInCache(cache, object1);
- expectObjectInCache(cache, object2);
-
- cache.add(object3);
- expect(cache, hasLength(2));
- expectObjectNotInCache(cache, object1);
- expectObjectInCache(cache, object2);
- expectObjectInCache(cache, object3);
-
- cache.add(object4);
- expect(cache, hasLength(2));
- expectObjectNotInCache(cache, object1);
- expectObjectNotInCache(cache, object2);
- expectObjectInCache(cache, object3);
- expectObjectInCache(cache, object4);
- });
-
- test('uses RLU strategy', () {
- final SynchronousSkiaObjectCache cache = SynchronousSkiaObjectCache(2);
- final TestSelfManagedObject object1 = TestSelfManagedObject();
- final TestSelfManagedObject object2 = TestSelfManagedObject();
- final TestSelfManagedObject object3 = TestSelfManagedObject();
- final TestSelfManagedObject object4 = TestSelfManagedObject();
-
- cache.add(object1);
- expectObjectInCache(cache, object1);
- cache.add(object2);
- expectObjectInCache(cache, object2);
- cache.add(object3);
- expectObjectInCache(cache, object3);
- expectObjectNotInCache(cache, object1);
-
- cache.markUsed(object2);
- cache.add(object4);
- expectObjectInCache(cache, object2);
- expectObjectNotInCache(cache, object3);
- expectObjectInCache(cache, object4);
- });
- });
-}
-
-/// A simple class that wraps a [SkiaObjectBox].
-///
-/// Can be [clone]d such that the clones share the same ref counted box.
-class TestBoxWrapper implements StackTraceDebugger {
- TestBoxWrapper() {
- if (assertionsEnabled) {
- _debugStackTrace = StackTrace.current;
- }
- box = SkiaObjectBox<TestBoxWrapper, TestSkDeletable>.resurrectable(
- this, TestSkDeletable(), () {
- resurrectCount += 1;
- return TestSkDeletable();
- });
- }
-
- TestBoxWrapper.cloneOf(this.box) {
- if (assertionsEnabled) {
- _debugStackTrace = StackTrace.current;
- }
- box.ref(this);
- }
-
- static int resurrectCount = 0;
-
- @override
- StackTrace get debugStackTrace => _debugStackTrace;
- late StackTrace _debugStackTrace;
-
- late SkiaObjectBox<TestBoxWrapper, TestSkDeletable> box;
-
- void dispose() {
- box.unref(this);
- }
-
- TestBoxWrapper clone() => TestBoxWrapper.cloneOf(box);
}
class TestSkDeletableMock {