Refactor CanvasKit image ref counting; fix a minor memory leak (#22549)
* Refactor SkiaObjectBox ref counting
* make CkAnimatedImage a Codec
* disallow double dispose; better assertion messages
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 a3458ae..94b04f5 100644
--- a/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart
+++ b/lib/web_ui/lib/src/engine/canvaskit/canvaskit_api.dart
@@ -685,14 +685,11 @@
external SkImage getCurrentFrame();
external int width();
external int height();
- external Uint8List readPixels(SkImageInfo imageInfo, int srcX, int srcY);
- external SkData encodeToData();
/// Deletes the C++ object.
///
/// This object is no longer usable after calling this method.
external void delete();
- external bool isAliasOf(SkAnimatedImage other);
external bool isDeleted();
}
@@ -1820,6 +1817,7 @@
external int size();
external bool isEmpty();
external Uint8List bytes();
+ external void delete();
}
@JS()
diff --git a/lib/web_ui/lib/src/engine/canvaskit/image.dart b/lib/web_ui/lib/src/engine/canvaskit/image.dart
index 388af1f..5c558d5 100644
--- a/lib/web_ui/lib/src/engine/canvaskit/image.dart
+++ b/lib/web_ui/lib/src/engine/canvaskit/image.dart
@@ -8,20 +8,17 @@
/// Instantiates a [ui.Codec] backed by an `SkAnimatedImage` from Skia.
void skiaInstantiateImageCodec(Uint8List list, Callback<ui.Codec> callback,
[int? width, int? height, int? format, int? rowBytes]) {
- final SkAnimatedImage skAnimatedImage =
- canvasKit.MakeAnimatedImageFromEncoded(list);
- final CkAnimatedImage animatedImage = CkAnimatedImage(skAnimatedImage);
- final CkAnimatedImageCodec codec = CkAnimatedImageCodec(animatedImage);
+ final CkAnimatedImage codec = CkAnimatedImage.decodeFromBytes(list);
callback(codec);
}
/// Instantiates a [ui.Codec] backed by an `SkAnimatedImage` from Skia after
/// requesting from URI.
Future<ui.Codec> skiaInstantiateWebImageCodec(
- String src, WebOnlyImageCodecChunkCallback? chunkCallback) {
+ String uri, WebOnlyImageCodecChunkCallback? chunkCallback) {
Completer<ui.Codec> completer = Completer<ui.Codec>();
//TODO: Switch to using MakeImageFromCanvasImageSource when animated images are supported.
- html.HttpRequest.request(src, responseType: "arraybuffer",
+ html.HttpRequest.request(uri, responseType: "arraybuffer",
onProgress: (html.ProgressEvent event) {
if (event.lengthComputable) {
chunkCallback?.call(event.loaded!, event.total!);
@@ -33,10 +30,7 @@
}
final Uint8List list =
new Uint8List.view((response.response as ByteBuffer));
- final SkAnimatedImage skAnimatedImage =
- canvasKit.MakeAnimatedImageFromEncoded(list);
- final CkAnimatedImage animatedImage = CkAnimatedImage(skAnimatedImage);
- final CkAnimatedImageCodec codec = CkAnimatedImageCodec(animatedImage);
+ final CkAnimatedImage codec = CkAnimatedImage.decodeFromBytes(list);
completer.complete(codec);
}, onError: (dynamic error) {
completer.completeError(error);
@@ -44,123 +38,115 @@
return completer.future;
}
-/// A wrapper for `SkAnimatedImage`.
-class CkAnimatedImage implements ui.Image {
- // Use a box because `SkImage` may be deleted either due to this object
- // being garbage-collected, or by an explicit call to [delete].
- late final SkiaObjectBox<SkAnimatedImage> box;
-
- SkAnimatedImage get _skAnimatedImage => box.skObject;
-
- CkAnimatedImage(SkAnimatedImage skAnimatedImage) {
- box = SkiaObjectBox<SkAnimatedImage>(this, skAnimatedImage);
+/// The CanvasKit implementation of [ui.Codec].
+///
+/// Wraps `SkAnimatedImage`.
+class CkAnimatedImage implements ui.Codec, StackTraceDebugger {
+ /// Decodes an image from a list of encoded bytes.
+ CkAnimatedImage.decodeFromBytes(Uint8List bytes) {
+ if (assertionsEnabled) {
+ _debugStackTrace = StackTrace.current;
+ }
+ final SkAnimatedImage skAnimatedImage =
+ canvasKit.MakeAnimatedImageFromEncoded(bytes);
+ box = SkiaObjectBox<CkAnimatedImage, SkAnimatedImage>(this, skAnimatedImage);
}
- CkAnimatedImage.cloneOf(SkiaObjectBox<SkAnimatedImage> boxToClone) {
- box = boxToClone.clone(this);
- }
+ // Use a box because `CkAnimatedImage` may be deleted either due to this
+ // object being garbage-collected, or by an explicit call to [dispose].
+ late final SkiaObjectBox<CkAnimatedImage, SkAnimatedImage> box;
+
+ @override
+ StackTrace get debugStackTrace => _debugStackTrace!;
+ StackTrace? _debugStackTrace;
bool _disposed = false;
+ bool get debugDisposed => _disposed;
+
+ bool _debugCheckIsNotDisposed() {
+ assert(!_disposed, 'This image has been disposed.');
+ return true;
+ }
+
@override
void dispose() {
- box.delete();
+ assert(
+ !_disposed,
+ 'Cannot dispose a codec that has already been disposed.',
+ );
_disposed = true;
+
+ // This image is no longer usable. Bump the ref count.
+ box.unref(this);
}
@override
- bool get debugDisposed {
- if (assertionsEnabled) {
- return _disposed;
- }
- throw StateError(
- 'Image.debugDisposed is only available when asserts are enabled.');
- }
-
- ui.Image clone() => CkAnimatedImage.cloneOf(box);
-
- @override
- bool isCloneOf(ui.Image other) {
- return other is CkAnimatedImage &&
- other._skAnimatedImage.isAliasOf(_skAnimatedImage);
+ int get frameCount {
+ assert(_debugCheckIsNotDisposed());
+ return box.skiaObject.getFrameCount();
}
@override
- List<StackTrace>? debugGetOpenHandleStackTraces() =>
- box.debugGetStackTraces();
-
- int get frameCount => _skAnimatedImage.getFrameCount();
-
- /// Decodes the next frame and returns the frame duration.
- Duration decodeNextFrame() {
- final int durationMillis = _skAnimatedImage.decodeNextFrame();
- return Duration(milliseconds: durationMillis);
- }
-
- int get repetitionCount => _skAnimatedImage.getRepetitionCount();
-
- CkImage get currentFrameAsImage {
- return CkImage(_skAnimatedImage.getCurrentFrame());
+ int get repetitionCount {
+ assert(_debugCheckIsNotDisposed());
+ return box.skiaObject.getRepetitionCount();
}
@override
- int get width => _skAnimatedImage.width();
-
- @override
- int get height => _skAnimatedImage.height();
-
- @override
- Future<ByteData> toByteData(
- {ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) {
- Uint8List bytes;
-
- if (format == ui.ImageByteFormat.rawRgba) {
- final SkImageInfo imageInfo = SkImageInfo(
- alphaType: canvasKit.AlphaType.Premul,
- colorType: canvasKit.ColorType.RGBA_8888,
- colorSpace: SkColorSpaceSRGB,
- width: width,
- height: height,
- );
- bytes = _skAnimatedImage.readPixels(imageInfo, 0, 0);
- } else {
- // Defaults to PNG 100%.
- final SkData skData = _skAnimatedImage.encodeToData();
- // Make a copy that we can return.
- bytes = Uint8List.fromList(canvasKit.getDataBytes(skData));
- }
-
- final ByteData data = bytes.buffer.asByteData(0, bytes.length);
- return Future<ByteData>.value(data);
+ Future<ui.FrameInfo> getNextFrame() {
+ assert(_debugCheckIsNotDisposed());
+ final int durationMillis = box.skiaObject.decodeNextFrame();
+ final Duration duration = Duration(milliseconds: durationMillis);
+ final CkImage image = CkImage(box.skiaObject.getCurrentFrame());
+ return Future<ui.FrameInfo>.value(AnimatedImageFrameInfo(duration, image));
}
-
- @override
- String toString() => '[$width\u00D7$height]';
}
/// A [ui.Image] backed by an `SkImage` from Skia.
-class CkImage implements ui.Image {
+class CkImage implements ui.Image, StackTraceDebugger {
+ CkImage(SkImage skImage) {
+ if (assertionsEnabled) {
+ _debugStackTrace = StackTrace.current;
+ }
+ box = SkiaObjectBox<CkImage, SkImage>(this, skImage);
+ }
+
+ CkImage.cloneOf(this.box) {
+ if (assertionsEnabled) {
+ _debugStackTrace = StackTrace.current;
+ }
+ box.ref(this);
+ }
+
+ @override
+ StackTrace get debugStackTrace => _debugStackTrace!;
+ StackTrace? _debugStackTrace;
+
// Use a box because `SkImage` may be deleted either due to this object
// being garbage-collected, or by an explicit call to [delete].
- late final SkiaObjectBox<SkImage> box;
+ late final SkiaObjectBox<CkImage, SkImage> box;
- SkImage get skImage => box.skObject;
-
- CkImage(SkImage skImage) {
- box = SkiaObjectBox<SkImage>(this, skImage);
- }
-
- CkImage.cloneOf(SkiaObjectBox<SkImage> boxToClone) {
- box = boxToClone.clone(this);
- }
+ /// The underlying Skia image object.
+ ///
+ /// Do not store the returned value. It is memory-managed by [SkiaObjectBox].
+ /// Storing it may result in use-after-free bugs.
+ SkImage get skImage => box.skiaObject;
bool _disposed = false;
+
+ bool _debugCheckIsNotDisposed() {
+ assert(!_disposed, 'This image has been disposed.');
+ return true;
+ }
+
@override
void dispose() {
- box.delete();
- assert(() {
- _disposed = true;
- return true;
- }());
+ assert(
+ !_disposed,
+ 'Cannot dispose an image that has already been disposed.',
+ );
+ _disposed = true;
+ box.unref(this);
}
@override
@@ -173,10 +159,14 @@
}
@override
- ui.Image clone() => CkImage.cloneOf(box);
+ ui.Image clone() {
+ assert(_debugCheckIsNotDisposed());
+ return CkImage.cloneOf(box);
+ }
@override
bool isCloneOf(ui.Image other) {
+ assert(_debugCheckIsNotDisposed());
return other is CkImage && other.skImage.isAliasOf(skImage);
}
@@ -185,14 +175,21 @@
box.debugGetStackTraces();
@override
- int get width => skImage.width();
+ int get width {
+ assert(_debugCheckIsNotDisposed());
+ return skImage.width();
+ }
@override
- int get height => skImage.height();
+ int get height {
+ assert(_debugCheckIsNotDisposed());
+ return skImage.height();
+ }
@override
Future<ByteData> toByteData(
{ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) {
+ assert(_debugCheckIsNotDisposed());
Uint8List bytes;
if (format == ui.ImageByteFormat.rawRgba) {
@@ -208,6 +205,7 @@
final SkData skData = skImage.encodeToData(); //defaults to PNG 100%
// make a copy that we can return
bytes = Uint8List.fromList(canvasKit.getDataBytes(skData));
+ skData.delete();
}
final ByteData data = bytes.buffer.asByteData(0, bytes.length);
@@ -215,31 +213,9 @@
}
@override
- String toString() => '[$width\u00D7$height]';
-}
-
-/// A [Codec] that wraps an `SkAnimatedImage`.
-class CkAnimatedImageCodec implements ui.Codec {
- CkAnimatedImage animatedImage;
-
- CkAnimatedImageCodec(this.animatedImage);
-
- @override
- void dispose() {
- animatedImage.dispose();
- }
-
- @override
- int get frameCount => animatedImage.frameCount;
-
- @override
- int get repetitionCount => animatedImage.repetitionCount;
-
- @override
- Future<ui.FrameInfo> getNextFrame() {
- final Duration duration = animatedImage.decodeNextFrame();
- final CkImage image = animatedImage.currentFrameAsImage;
- return Future<ui.FrameInfo>.value(AnimatedImageFrameInfo(duration, image));
+ String toString() {
+ assert(_debugCheckIsNotDisposed());
+ return '[$width\u00D7$height]';
}
}
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 3ce432d..17ef953 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
@@ -252,6 +252,15 @@
}
}
+/// 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;
+}
+
/// Uses reference counting to manage the lifecycle of a Skia object owned by a
/// wrapper object.
///
@@ -263,37 +272,47 @@
///
/// The [delete] method may be called any number of times. The box
/// will only delete the object once.
-class SkiaObjectBox<T> {
- SkiaObjectBox(Object wrapper, T skObject)
- : this._(wrapper, skObject, skObject as SkDeletable, <SkiaObjectBox>{});
-
- SkiaObjectBox._(Object wrapper, this.skObject, this._skDeletable, this._refs) {
+class SkiaObjectBox<R extends StackTraceDebugger, T> {
+ SkiaObjectBox(R debugReferrer, this.skiaObject) : _skDeletable = skiaObject as SkDeletable {
if (assertionsEnabled) {
- _debugStackTrace = StackTrace.current;
+ debugReferrers.add(debugReferrer);
}
- _refs.add(this);
if (browserSupportsFinalizationRegistry) {
- boxRegistry.register(wrapper, this);
+ boxRegistry.register(this, _skDeletable);
}
+ assert(refCount == debugReferrers.length);
}
- /// Reference handles to the same underlying [skObject].
- final Set<SkiaObjectBox> _refs;
+ /// 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;
- late final StackTrace? _debugStackTrace;
+ /// 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 _refs
- .map<StackTrace>((SkiaObjectBox box) => box._debugStackTrace!)
+ return debugReferrers
+ .map<StackTrace>((R referrer) => referrer.debugStackTrace)
.toList();
}
return null;
}
/// The Skia object whose lifecycle is being managed.
- final T skObject;
+ ///
+ /// Do not store this value outside this box. It is memory-managed by
+ /// [SkiaObjectBox]. Storing it may result in use-after-free bugs.
+ final T skiaObject;
final SkDeletable _skDeletable;
/// Whether this object has been deleted.
@@ -302,17 +321,23 @@
/// Deletes Skia objects when their wrappers are garbage collected.
static final SkObjectFinalizationRegistry boxRegistry =
- SkObjectFinalizationRegistry(js.allowInterop((SkiaObjectBox box) {
- box.delete();
+ SkObjectFinalizationRegistry(js.allowInterop((SkDeletable deletable) {
+ deletable.delete();
}));
- /// Returns a clone of this object, which increases its reference count.
+ /// 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.
- SkiaObjectBox<T> clone(Object wrapper) {
- assert(!_isDeleted, 'Cannot clone from a deleted handle.');
- assert(_refs.isNotEmpty);
- return SkiaObjectBox<T>._(wrapper, skObject, _skDeletable, _refs);
+ void ref(R debugReferrer) {
+ assert(!_isDeleted, '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].
@@ -321,15 +346,16 @@
///
/// If this causes the reference count to drop to zero, deletes the
/// [skObject].
- void delete() {
- if (_isDeleted) {
- assert(!_refs.contains(this));
- return;
- }
- final bool removed = _refs.remove(this);
- assert(removed);
- _isDeleted = true;
- if (_refs.isEmpty) {
+ void unref(R debugReferrer) {
+ assert(!_isDeleted, '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) {
+ _isDeleted = true;
_scheduleSkObjectCollection(_skDeletable);
}
}
@@ -386,7 +412,7 @@
///
/// Since it's expensive to resurrect, we shouldn't just delete it after every
/// frame. Instead, add it to a cache and only delete it when the cache fills.
- static void manageExpensive(ManagedSkiaObject object) {
+ static void manageExpensive(SkiaObject object) {
registerCleanupCallback();
expensiveCache.add(object);
}
diff --git a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart
index 452cffd..2498193 100644
--- a/lib/web_ui/test/canvaskit/canvaskit_api_test.dart
+++ b/lib/web_ui/test/canvaskit/canvaskit_api_test.dart
@@ -1187,10 +1187,10 @@
final CkImage image = await picture.toImage(1, 1);
final ByteData rawData =
await image.toByteData(format: ui.ImageByteFormat.rawRgba);
- expect(rawData, isNotNull);
+ expect(rawData.lengthInBytes, greaterThan(0));
final ByteData pngData =
await image.toByteData(format: ui.ImageByteFormat.png);
- expect(pngData, isNotNull);
+ expect(pngData.lengthInBytes, greaterThan(0));
});
}
diff --git a/lib/web_ui/test/canvaskit/image_test.dart b/lib/web_ui/test/canvaskit/image_test.dart
index 2487efa..5141464 100644
--- a/lib/web_ui/test/canvaskit/image_test.dart
+++ b/lib/web_ui/test/canvaskit/image_test.dart
@@ -10,6 +10,7 @@
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
+import '../matchers.dart';
import 'common.dart';
import 'test_data.dart';
@@ -23,48 +24,30 @@
await ui.webOnlyInitializePlatform();
});
- test('CkAnimatedImage toString', () {
- final SkAnimatedImage skAnimatedImage =
- canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage);
- final CkAnimatedImage image = CkAnimatedImage(skAnimatedImage);
- expect(image.toString(), '[1×1]');
- image.dispose();
- });
-
test('CkAnimatedImage can be explicitly disposed of', () {
- final SkAnimatedImage skAnimatedImage =
- canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage);
- final CkAnimatedImage image = CkAnimatedImage(skAnimatedImage);
+ final CkAnimatedImage image = CkAnimatedImage.decodeFromBytes(kTransparentImage);
expect(image.box.isDeleted, false);
expect(image.debugDisposed, false);
image.dispose();
expect(image.box.isDeleted, true);
expect(image.debugDisposed, true);
- image.dispose();
- expect(image.box.isDeleted, true);
- expect(image.debugDisposed, true);
+
+ // Disallow double-dispose.
+ expect(() => image.dispose(), throwsAssertionError);
});
test('CkAnimatedImage can be cloned and explicitly disposed of', () async {
- final SkAnimatedImage skAnimatedImage =
- canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage);
- final CkAnimatedImage image = CkAnimatedImage(skAnimatedImage);
- final CkAnimatedImage imageClone = image.clone();
+ final CkAnimatedImage image = CkAnimatedImage.decodeFromBytes(kTransparentImage);
+ final SkAnimatedImage skAnimatedImage = image.box.skiaObject;
+ final SkiaObjectBox<CkAnimatedImage, SkAnimatedImage> box = image.box;
+ expect(box.refCount, 1);
+ expect(box.debugGetStackTraces().length, 1);
- expect(image.isCloneOf(imageClone), true);
- expect(image.box.isDeleted, false);
- await Future<void>.delayed(Duration.zero);
- expect(skAnimatedImage.isDeleted(), false);
image.dispose();
- expect(image.box.isDeleted, true);
- expect(imageClone.box.isDeleted, false);
- await Future<void>.delayed(Duration.zero);
- expect(skAnimatedImage.isDeleted(), false);
- imageClone.dispose();
- expect(image.box.isDeleted, true);
- expect(imageClone.box.isDeleted, true);
+ expect(box.isDeleted, true);
await Future<void>.delayed(Duration.zero);
expect(skAnimatedImage.isDeleted(), true);
+ expect(box.debugGetStackTraces().length, 0);
});
test('CkImage toString', () {
@@ -86,9 +69,9 @@
image.dispose();
expect(image.debugDisposed, true);
expect(image.box.isDeleted, true);
- image.dispose();
- expect(image.debugDisposed, true);
- expect(image.box.isDeleted, true);
+
+ // Disallow double-dispose.
+ expect(() => image.dispose(), throwsAssertionError);
});
test('CkImage can be explicitly disposed of when cloned', () async {
@@ -96,22 +79,27 @@
canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage)
.getCurrentFrame();
final CkImage image = CkImage(skImage);
+ final SkiaObjectBox<CkImage, SkImage> box = image.box;
+ expect(box.refCount, 1);
+ expect(box.debugGetStackTraces().length, 1);
+
final CkImage imageClone = image.clone();
+ expect(box.refCount, 2);
+ expect(box.debugGetStackTraces().length, 2);
expect(image.isCloneOf(imageClone), true);
- expect(image.box.isDeleted, false);
+ expect(box.isDeleted, false);
await Future<void>.delayed(Duration.zero);
expect(skImage.isDeleted(), false);
image.dispose();
- expect(image.box.isDeleted, true);
- expect(imageClone.box.isDeleted, false);
+ expect(box.isDeleted, false);
await Future<void>.delayed(Duration.zero);
expect(skImage.isDeleted(), false);
imageClone.dispose();
- expect(image.box.isDeleted, true);
- expect(imageClone.box.isDeleted, true);
+ expect(box.isDeleted, true);
await Future<void>.delayed(Duration.zero);
expect(skImage.isDeleted(), true);
+ expect(box.debugGetStackTraces().length, 0);
});
test('skiaInstantiateWebImageCodec throws exception if given invalid URL',
@@ -119,6 +107,15 @@
expect(skiaInstantiateWebImageCodec('invalid-url', null),
throwsA(isA<ProgressEvent>()));
});
+
+ test('CkImage toByteData', () async {
+ final SkImage skImage =
+ canvasKit.MakeAnimatedImageFromEncoded(kTransparentImage)
+ .getCurrentFrame();
+ final CkImage image = CkImage(skImage);
+ expect((await image.toByteData()).lengthInBytes, greaterThan(0));
+ expect((await image.toByteData(format: ui.ImageByteFormat.png)).lengthInBytes, greaterThan(0));
+ });
// TODO: https://github.com/flutter/flutter/issues/60040
}, skip: isIosSafari);
}
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 7de655b..aa1b808 100644
--- a/lib/web_ui/test/canvaskit/skia_objects_cache_test.dart
+++ b/lib/web_ui/test/canvaskit/skia_objects_cache_test.dart
@@ -11,8 +11,8 @@
import 'package:ui/ui.dart' as ui;
import 'package:ui/src/engine.dart';
-import 'common.dart';
import '../matchers.dart';
+import 'common.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
@@ -158,42 +158,80 @@
group(SkiaObjectBox, () {
test('Records stack traces and respects refcounts', () async {
TestSkDeletable.deleteCount = 0;
- final Object wrapper = Object();
- final SkiaObjectBox<TestSkDeletable> box = SkiaObjectBox<TestSkDeletable>(wrapper, TestSkDeletable());
+ final TestBoxWrapper original = TestBoxWrapper();
- expect(box.debugGetStackTraces().length, 1);
+ expect(original.box.debugGetStackTraces().length, 1);
+ expect(original.box.refCount, 1);
+ expect(original.box.isDeleted, false);
- final SkiaObjectBox clone = box.clone(wrapper);
- expect(clone, isNot(same(box)));
- expect(clone.debugGetStackTraces().length, 2);
- expect(box.debugGetStackTraces().length, 2);
+ 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.isDeleted, false);
- box.delete();
+ original.dispose();
- expect(() => box.clone(wrapper), throwsAssertionError);
-
- expect(box.isDeleted, true);
-
- // Let any timers elapse.
+ // Let Skia object delete queue run.
await Future<void>.delayed(Duration.zero);
expect(TestSkDeletable.deleteCount, 0);
- expect(clone.debugGetStackTraces().length, 1);
- expect(box.debugGetStackTraces().length, 1);
+ expect(clone.box.debugGetStackTraces().length, 1);
+ expect(clone.box.refCount, 1);
+ expect(original.box.debugGetStackTraces().length, 1);
+ expect(original.box.refCount, 1);
- clone.delete();
- expect(() => clone.clone(wrapper), throwsAssertionError);
+ clone.dispose();
- // Let any timers elapse.
+ // Let Skia object delete queue run.
await Future<void>.delayed(Duration.zero);
expect(TestSkDeletable.deleteCount, 1);
- expect(clone.debugGetStackTraces().length, 0);
- expect(box.debugGetStackTraces().length, 0);
+ 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.isDeleted, true);
+
+ expect(() => clone.box.unref(clone), throwsAssertionError);
});
});
}
+/// 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>(this, TestSkDeletable());
+ }
+
+ TestBoxWrapper.cloneOf(this.box) {
+ if (assertionsEnabled) {
+ _debugStackTrace = StackTrace.current;
+ }
+ box.ref(this);
+ }
+
+ @override
+ StackTrace get debugStackTrace => _debugStackTrace;
+ StackTrace _debugStackTrace;
+
+ SkiaObjectBox<TestBoxWrapper, TestSkDeletable> box;
+
+ void dispose() {
+ box.unref(this);
+ }
+
+ TestBoxWrapper clone() => TestBoxWrapper.cloneOf(box);
+}
+
+
class TestSkDeletable implements SkDeletable {
static int deleteCount = 0;