未验证 提交 b502fcb9 编写于 作者: Y Yegor 提交者: GitHub

Implement image resurrection (#22575)

上级 97cacfbf
......@@ -99,6 +99,23 @@ class CanvasKit {
/// Creates an [SkPath] using commands obtained from [SkPath.toCmds].
// TODO(yjbanov): switch to CanvasKit.Path.MakeFromCmds when it's available.
external SkPath MakePathFromCmds(List<dynamic> pathCommands);
/// Creates an image from decoded pixels represented as a list of bytes.
///
/// The pixel data must match the [width], [height], [alphaType], [colorType],
/// and [colorSpace].
///
/// Typically pixel data is obtained using [SkImage.readPixels]. The
/// parameters specified in [SkImageInfo] passed [SkImage.readPixels] much
/// match the arguments passed to this method.
external SkImage MakeImage(
Uint8List imageData,
int width,
int height,
SkAlphaType alphaType,
SkColorType colorType,
ColorSpace colorSpace,
);
}
@JS('window.CanvasKitInit')
......@@ -1726,48 +1743,159 @@ class TypefaceFontProviderNamespace {
external TypefaceFontProvider Make();
}
Timer? _skObjectCollector;
List<SkDeletable> _skObjectDeleteQueue = <SkDeletable>[];
/// Collects Skia objects that are no longer necessary.
abstract class Collector {
/// The production collector implementation.
static final Collector _productionInstance = ProductionCollector();
final SkObjectFinalizationRegistry skObjectFinalizationRegistry =
SkObjectFinalizationRegistry(js.allowInterop((SkDeletable deletable) {
_scheduleSkObjectCollection(deletable);
}));
/// The collector implementation currently in use.
static Collector get instance => _instance;
static Collector _instance = _productionInstance;
/// Schedules a Skia object for deletion in an asap timer.
///
/// A timer is used for the following reasons:
///
/// - Deleting the object immediately may lead to dangling pointer as the Skia
/// object may still be used by a function in the current frame. For example,
/// a `CkPaint` + `SkPaint` pair may be created by the framework, passed to
/// the engine, and the `CkPaint` dropped immediately. Because GC can kick in
/// any time, including in the middle of the event, we may delete `SkPaint`
/// prematurely.
/// - A microtask, while solves the problem above, would prevent the event from
/// yielding to the graphics system to render the frame on the screen if there
/// is a large number of objects to delete, causing jank.
/// In tests overrides the collector implementation.
static void debugOverrideCollector(Collector override) {
_instance = override;
}
/// In tests restores the collector to the production implementation.
static void debugRestoreCollector() {
_instance = _productionInstance;
}
/// Registers a [deletable] for collection when the [wrapper] object is
/// garbage collected.
///
/// The [debugLabel] is used to track the origin of the deletable.
void register(Object wrapper, SkDeletable deletable);
/// Deletes the [deletable].
///
/// The exact timing of the deletion is implementation-specific. For example,
/// a production implementation may want to batch deletables and schedule a
/// timer to collect them instead of deleting right away.
///
/// A test implementation may want a collection strategy that's less efficient
/// but more predictable.
void collect(SkDeletable deletable);
}
/// Uses the browser's real `FinalizationRegistry` to collect objects.
///
/// Because scheduling a timer is expensive, the timer is shared by all objects
/// deleted this frame. No timer is created if no objects were scheduled for
/// deletion.
void _scheduleSkObjectCollection(SkDeletable deletable) {
_skObjectDeleteQueue.add(deletable);
_skObjectCollector ??= Timer(Duration.zero, () {
/// Uses timers to delete objects in batches and outside the animation frame.
class ProductionCollector implements Collector {
ProductionCollector() {
_skObjectFinalizationRegistry = SkObjectFinalizationRegistry(js.allowInterop((SkDeletable deletable) {
// This is called when GC decides to collect the wrapper object and
// notify us, which may happen after the object is already deleted
// explicitly, e.g. when its ref count drops to zero. When that happens
// skip collection of this object.
if (!deletable.isDeleted()) {
collect(deletable);
}
}));
}
late final SkObjectFinalizationRegistry _skObjectFinalizationRegistry;
List<SkDeletable> _skiaObjectCollectionQueue = <SkDeletable>[];
Timer? _skiaObjectCollectionTimer;
@override
void register(Object wrapper, SkDeletable deletable) {
_skObjectFinalizationRegistry.register(wrapper, deletable);
}
/// Schedules a Skia object for deletion in an asap timer.
///
/// A timer is used for the following reasons:
///
/// - Deleting the object immediately may lead to dangling pointer as the Skia
/// object may still be used by a function in the current frame. For example,
/// a `CkPaint` + `SkPaint` pair may be created by the framework, passed to
/// the engine, and the `CkPaint` dropped immediately. Because GC can kick in
/// any time, including in the middle of the event, we may delete `SkPaint`
/// prematurely.
/// - A microtask, while solves the problem above, would prevent the event from
/// yielding to the graphics system to render the frame on the screen if there
/// is a large number of objects to delete, causing jank.
///
/// Because scheduling a timer is expensive, the timer is shared by all objects
/// deleted this frame. No timer is created if no objects were scheduled for
/// deletion.
@override
void collect(SkDeletable deletable) {
assert(
!deletable.isDeleted(),
'Attempted to delete an already deleted Skia object.',
);
_skiaObjectCollectionQueue.add(deletable);
_skiaObjectCollectionTimer ??= Timer(Duration.zero, () {
// Null out the timer so we can schedule a new one next time objects are
// scheduled for deletion.
_skiaObjectCollectionTimer = null;
collectSkiaObjectsNow();
});
}
/// Deletes all Skia objects pending deletion synchronously.
///
/// After calling this method [_skiaObjectCollectionQueue] is empty.
///
/// Throws a [SkiaObjectCollectionError] if CanvasKit fails to delete at least
/// one object. The error is populated with information about the first failed
/// object. Upon an error the collection continues and the collection queue is
/// emptied out to prevent memory leaks. This may happen, for example, when the
/// same object is deleted more than once.
void collectSkiaObjectsNow() {
html.window.performance.mark('SkObject collection-start');
final int length = _skObjectDeleteQueue.length;
final int length = _skiaObjectCollectionQueue.length;
dynamic firstError;
StackTrace? firstStackTrace;
for (int i = 0; i < length; i++) {
_skObjectDeleteQueue[i].delete();
final SkDeletable deletable = _skiaObjectCollectionQueue[i];
if (deletable.isDeleted()) {
// Some Skia objects are ref counted and are deleted before GC and/or
// the collection timer begins collecting them. So we have to check
// again if the objects is worth collecting.
continue;
}
try {
deletable.delete();
} catch (error, stackTrace) {
// Remember the error, but keep going. If for some reason CanvasKit fails
// to delete an object we still want to delete other objects and empty
// out the queue. Otherwise, the queue will never be flushed and keep
// accumulating objects, a.k.a. memory leak.
if (firstError == null) {
firstError = error;
firstStackTrace = stackTrace;
}
}
}
_skObjectDeleteQueue = <SkDeletable>[];
_skiaObjectCollectionQueue = <SkDeletable>[];
// Null out the timer so we can schedule a new one next time objects are
// scheduled for deletion.
_skObjectCollector = null;
html.window.performance.mark('SkObject collection-end');
html.window.performance.measure('SkObject collection',
'SkObject collection-start', 'SkObject collection-end');
});
// It's safe to throw the error here, now that we've processed the queue.
if (firstError != null) {
throw SkiaObjectCollectionError(firstError, firstStackTrace);
}
}
}
/// Thrown by [ProductionCollector] when Skia object collection fails.
class SkiaObjectCollectionError implements Error {
SkiaObjectCollectionError(this.error, this.stackTrace);
final dynamic error;
@override
final StackTrace? stackTrace;
@override
String toString() => 'SkiaObjectCollectionError: $error\n$stackTrace';
}
/// Any Skia object that has a `delete` method.
......@@ -1776,6 +1904,9 @@ void _scheduleSkObjectCollection(SkDeletable deletable) {
class SkDeletable {
/// Deletes the C++ side object.
external void delete();
/// Returns whether the correcponding C++ object has been deleted.
external bool isDeleted();
}
/// Attaches a weakly referenced object to another object and calls a finalizer
......
......@@ -41,24 +41,24 @@ Future<ui.Codec> skiaInstantiateWebImageCodec(
/// The CanvasKit implementation of [ui.Codec].
///
/// Wraps `SkAnimatedImage`.
class CkAnimatedImage implements ui.Codec, StackTraceDebugger {
class CkAnimatedImage extends ManagedSkiaObject<SkAnimatedImage> implements ui.Codec {
/// 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.decodeFromBytes(this._bytes);
final Uint8List _bytes;
@override
SkAnimatedImage createDefault() {
return canvasKit.MakeAnimatedImageFromEncoded(_bytes);
}
// 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
SkAnimatedImage resurrect() => createDefault();
@override
StackTrace get debugStackTrace => _debugStackTrace!;
StackTrace? _debugStackTrace;
void delete() {
rawSkiaObject?.delete();
}
bool _disposed = false;
bool get debugDisposed => _disposed;
......@@ -75,29 +75,27 @@ class CkAnimatedImage implements ui.Codec, StackTraceDebugger {
'Cannot dispose a codec that has already been disposed.',
);
_disposed = true;
// This image is no longer usable. Bump the ref count.
box.unref(this);
delete();
}
@override
int get frameCount {
assert(_debugCheckIsNotDisposed());
return box.skiaObject.getFrameCount();
return skiaObject.getFrameCount();
}
@override
int get repetitionCount {
assert(_debugCheckIsNotDisposed());
return box.skiaObject.getRepetitionCount();
return skiaObject.getRepetitionCount();
}
@override
Future<ui.FrameInfo> getNextFrame() {
assert(_debugCheckIsNotDisposed());
final int durationMillis = box.skiaObject.decodeNextFrame();
final int durationMillis = skiaObject.decodeNextFrame();
final Duration duration = Duration(milliseconds: durationMillis);
final CkImage image = CkImage(box.skiaObject.getCurrentFrame());
final CkImage image = CkImage(skiaObject.getCurrentFrame());
return Future<ui.FrameInfo>.value(AnimatedImageFrameInfo(duration, image));
}
}
......@@ -108,7 +106,35 @@ class CkImage implements ui.Image, StackTraceDebugger {
if (assertionsEnabled) {
_debugStackTrace = StackTrace.current;
}
box = SkiaObjectBox<CkImage, SkImage>(this, skImage);
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,
);
box = SkiaObjectBox<CkImage, SkImage>.resurrectable(this, skImage, () {
return canvasKit.MakeImage(
originalBytes.buffer.asUint8List(),
width,
height,
canvasKit.AlphaType.Premul,
canvasKit.ColorType.RGBA_8888,
SkColorSpaceSRGB,
);
});
}
}
CkImage.cloneOf(this.box) {
......@@ -187,18 +213,35 @@ class CkImage implements ui.Image, StackTraceDebugger {
}
@override
Future<ByteData> toByteData(
{ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba}) {
Future<ByteData> toByteData({
ui.ImageByteFormat format = ui.ImageByteFormat.rawRgba,
}) {
assert(_debugCheckIsNotDisposed());
return Future<ByteData>.value(_encodeImage(
skImage: skImage,
format: format,
alphaType: canvasKit.AlphaType.Premul,
colorType: canvasKit.ColorType.RGBA_8888,
colorSpace: SkColorSpaceSRGB,
));
}
static ByteData _encodeImage({
required SkImage skImage,
required ui.ImageByteFormat format,
required SkAlphaType alphaType,
required SkColorType colorType,
required ColorSpace colorSpace,
}) {
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,
alphaType: alphaType,
colorType: colorType,
colorSpace: colorSpace,
width: skImage.width(),
height: skImage.height(),
);
bytes = skImage.readPixels(imageInfo, 0, 0);
} else {
......@@ -208,8 +251,7 @@ class CkImage implements ui.Image, StackTraceDebugger {
skData.delete();
}
final ByteData data = bytes.buffer.asByteData(0, bytes.length);
return Future<ByteData>.value(data);
return bytes.buffer.asByteData(0, bytes.length);
}
@override
......
......@@ -143,7 +143,7 @@ abstract class ManagedSkiaObject<T extends Object> extends SkiaObject<T> {
if (browserSupportsFinalizationRegistry) {
// If FinalizationRegistry is supported we will only ever need the
// default object, as we know precisely when to delete it.
skObjectFinalizationRegistry.register(this, defaultObject);
Collector.instance.register(this, defaultObject as SkDeletable);
} else {
// If FinalizationRegistry is _not_ supported we may need to delete
// and resurrect the object multiple times before deleting it forever.
......@@ -228,7 +228,7 @@ abstract class OneShotSkiaObject<T extends Object> extends SkiaObject<T> {
OneShotSkiaObject(T skObject) : this.rawSkiaObject = skObject {
if (browserSupportsFinalizationRegistry) {
skObjectFinalizationRegistry.register(this, skObject);
Collector.instance.register(this, skObject as SkDeletable);
} else {
SkiaObjects.manageOneShot(this);
}
......@@ -261,25 +261,51 @@ abstract class StackTraceDebugger {
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.
///
/// When the wrapper is garbage collected, decrements the refcount (only in
/// browsers that support weak references).
/// The [ref] method can be used to increment the refcount to tell this box to
/// keep the underlying Skia object alive.
///
/// The [delete] method can be used to eagerly decrement the refcount before the
/// wrapper is garbage collected.
/// 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]).
///
/// The [delete] method may be called any number of times. The box
/// will only delete the object once.
class SkiaObjectBox<R extends StackTraceDebugger, T> {
SkiaObjectBox(R debugReferrer, this.skiaObject) : _skDeletable = skiaObject as SkDeletable {
/// 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) : _resurrector = null {
assert(browserSupportsFinalizationRegistry);
_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);
SkiaObjects.manageExpensive(this);
}
void _initialize(R debugReferrer, T initialValue) {
_update(initialValue);
if (assertionsEnabled) {
debugReferrers.add(debugReferrer);
}
if (browserSupportsFinalizationRegistry) {
boxRegistry.register(this, _skDeletable);
}
assert(refCount == debugReferrers.length);
}
......@@ -312,25 +338,57 @@ class SkiaObjectBox<R extends StackTraceDebugger, T> {
///
/// 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;
T? rawSkiaObject;
SkDeletable? _skDeletable;
Resurrector<T>? _resurrector;
/// Whether this object has been deleted.
bool get isDeleted => _isDeleted;
bool _isDeleted = false;
void _update(T? newSkiaObject) {
rawSkiaObject = newSkiaObject;
_skDeletable = newSkiaObject as SkDeletable?;
}
/// Deletes Skia objects when their wrappers are garbage collected.
static final SkObjectFinalizationRegistry boxRegistry =
SkObjectFinalizationRegistry(js.allowInterop((SkDeletable deletable) {
deletable.delete();
}));
@override
T get skiaObject => rawSkiaObject ?? _doResurrect();
T _doResurrect() {
assert(!browserSupportsFinalizationRegistry);
assert(_resurrector != null);
assert(!_isDeletedPermanently, 'Cannot use deleted object.');
_update(_resurrector!());
SkiaObjects.manageExpensive(this);
return skiaObject;
}
@override
void delete() {
_skDeletable?.delete();
}
@override
void didDelete() {
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(!_isDeleted, 'Cannot increment ref count on a deleted handle.');
assert(!_isDeletedPermanently, 'Cannot increment ref count on a deleted handle.');
assert(_refCount > 0);
assert(
debugReferrers.add(debugReferrer),
......@@ -347,7 +405,7 @@ class SkiaObjectBox<R extends StackTraceDebugger, T> {
/// If this causes the reference count to drop to zero, deletes the
/// [skObject].
void unref(R debugReferrer) {
assert(!_isDeleted, 'Attempted to unref an already deleted Skia object.');
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.',
......@@ -355,8 +413,19 @@ class SkiaObjectBox<R extends StackTraceDebugger, T> {
_refCount -= 1;
assert(refCount == debugReferrers.length);
if (_refCount == 0) {
_isDeleted = true;
_scheduleSkObjectCollection(_skDeletable);
// 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 {
_skDeletable!.delete();
}
}
rawSkiaObject = null;
_skDeletable = null;
_resurrector = null;
_isDeletedPermanently = true;
}
}
}
......
......@@ -20,13 +20,7 @@ void main() {
void testMain() {
group('CanvasKit API', () {
setUpAll(() async {
await ui.webOnlyInitializePlatform();
});
test('Using CanvasKit', () {
expect(useCanvasKit, true);
});
setUpCanvasKitTest();
_blendModeTests();
_paintStyleTests();
......
......@@ -2,10 +2,161 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// @dart = 2.6
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
/// Whether we are running on iOS Safari.
// TODO: https://github.com/flutter/flutter/issues/60040
bool get isIosSafari => browserEngine == BrowserEngine.webkit &&
operatingSystem == OperatingSystem.iOs;
/// Used in tests instead of [ProductionCollector] to control Skia object
/// collection explicitly, and to prevent leaks across tests.
///
/// See [TestCollector] for usage.
late TestCollector testCollector;
/// Common test setup for all CanvasKit unit-tests.
void setUpCanvasKitTest() {
setUpAll(() async {
expect(useCanvasKit, true,
reason: 'This test must run in CanvasKit mode.');
debugResetBrowserSupportsFinalizationRegistry();
await ui.webOnlyInitializePlatform();
});
setUp(() async {
testCollector = TestCollector();
Collector.debugOverrideCollector(testCollector);
});
tearDown(() {
testCollector.cleanUpAfterTest();
debugResetBrowserSupportsFinalizationRegistry();
});
tearDownAll(() {
debugResetBrowserSupportsFinalizationRegistry();
});
}
class _TestFinalizerRegistration {
_TestFinalizerRegistration(this.wrapper, this.deletable, this.stackTrace);
final Object wrapper;
final SkDeletable deletable;
final StackTrace stackTrace;
}
class _TestCollection {
_TestCollection(this.deletable, this.stackTrace);
final SkDeletable deletable;
final StackTrace stackTrace;
}
/// Provides explicit synchronous API for collecting Skia objects in tests.
///
/// [ProductionCollector] relies on `FinalizationRegistry` and timers to
/// delete Skia objects, which makes it more precise and efficient. However,
/// it also makes it unpredictable. For example, an object created in one
/// test may be collected while running another test because the timing is
/// subject to browser-specific GC scheduling.
///
/// Tests should use [collectNow] and [collectAfterTest] to trigger collections.
class TestCollector implements Collector {
final List<_TestFinalizerRegistration> _activeRegistrations = <_TestFinalizerRegistration>[];
final List<_TestFinalizerRegistration> _collectedRegistrations = <_TestFinalizerRegistration>[];
final List<_TestCollection> _pendingCollections = <_TestCollection>[];
final List<_TestCollection> _completedCollections = <_TestCollection>[];
@override
void register(Object wrapper, SkDeletable deletable) {
_activeRegistrations.add(
_TestFinalizerRegistration(wrapper, deletable, StackTrace.current),
);
}
@override
void collect(SkDeletable deletable) {
_pendingCollections.add(
_TestCollection(deletable, StackTrace.current),
);
}
/// Deletes all Skia objects scheduled for collection.
void collectNow() {
for (_TestCollection collection in _pendingCollections) {
late final _TestFinalizerRegistration? activeRegistration;
for (_TestFinalizerRegistration registration in _activeRegistrations) {
if (identical(registration.deletable, collection.deletable)) {
activeRegistration = registration;
break;
}
}
if (activeRegistration == null) {
late final _TestFinalizerRegistration? collectedRegistration;
for (_TestFinalizerRegistration registration in _collectedRegistrations) {
if (identical(registration.deletable, collection.deletable)) {
collectedRegistration = registration;
break;
}
}
if (collectedRegistration == null) {
fail(
'Attempted to collect an object that was never registered for finalization.\n'
'The collection was requested here:\n'
'${collection.stackTrace}'
);
} else {
final _TestCollection firstCollection = _completedCollections.firstWhere(
(_TestCollection completedCollection) {
return identical(completedCollection.deletable, collection.deletable);
}
);
fail(
'Attempted to collect an object that was previously collected.\n'
'The object was registered for finalization here:\n'
'${collection.stackTrace}\n\n'
'The first collection was requested here:\n'
'${firstCollection.stackTrace}\n\n'
'The second collection was requested here:\n'
'${collection.stackTrace}'
);
}
} else {
_collectedRegistrations.add(activeRegistration);
_activeRegistrations.remove(activeRegistration);
_completedCollections.add(collection);
if (!collection.deletable.isDeleted()) {
collection.deletable.delete();
}
}
}
_pendingCollections.clear();
}
/// Deletes all Skia objects with registered finalizers.
///
/// This also deletes active objects that have not been scheduled for
/// collection, to prevent objects leaking across tests.
void cleanUpAfterTest() {
for (_TestCollection collection in _pendingCollections) {
if (!collection.deletable.isDeleted()) {
collection.deletable.delete();
}
}
for (_TestFinalizerRegistration registration in _activeRegistrations) {
if (!registration.deletable.isDeleted()) {
registration.deletable.delete();
}
}
_activeRegistrations.clear();
_collectedRegistrations.clear();
_pendingCollections.clear();
_completedCollections.clear();
}
}
......@@ -47,9 +47,7 @@ void testMain() {
}
group('ImageFilters', () {
setUpAll(() async {
await ui.webOnlyInitializePlatform();
});
setUpCanvasKitTest();
test('can be constructed', () {
final CkImageFilter imageFilter = CkImageFilter.blur(sigmaX: 5, sigmaY: 10);
......
......@@ -5,8 +5,6 @@
// @dart = 2.6
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import 'common.dart';
import '../frame_timings_common.dart';
......@@ -17,13 +15,7 @@ void main() {
void testMain() {
group('frame timings', () {
setUpAll(() async {
await ui.webOnlyInitializePlatform();
});
test('Using CanvasKit', () {
expect(useCanvasKit, true);
});
setUpCanvasKitTest();
test('collects frame timings', () async {
await runFrameTimingsTest();
......
......@@ -20,34 +20,22 @@ void main() {
void testMain() {
group('CanvasKit image', () {
setUpAll(() async {
await ui.webOnlyInitializePlatform();
});
setUpCanvasKitTest();
test('CkAnimatedImage can be explicitly disposed of', () {
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);
// Disallow usage after disposal
expect(() => image.frameCount, throwsAssertionError);
expect(() => image.repetitionCount, throwsAssertionError);
expect(() => image.getNextFrame(), throwsAssertionError);
// Disallow double-dispose.
expect(() => image.dispose(), throwsAssertionError);
});
test('CkAnimatedImage can be cloned and explicitly disposed of', () async {
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);
image.dispose();
expect(box.isDeleted, true);
await Future<void>.delayed(Duration.zero);
expect(skAnimatedImage.isDeleted(), true);
expect(box.debugGetStackTraces().length, 0);
testCollector.collectNow();
});
test('CkImage toString', () {
......@@ -57,6 +45,7 @@ void testMain() {
final CkImage image = CkImage(skImage);
expect(image.toString(), '[1×1]');
image.dispose();
testCollector.collectNow();
});
test('CkImage can be explicitly disposed of', () {
......@@ -65,13 +54,14 @@ void testMain() {
.getCurrentFrame();
final CkImage image = CkImage(skImage);
expect(image.debugDisposed, false);
expect(image.box.isDeleted, false);
expect(image.box.isDeletedPermanently, false);
image.dispose();
expect(image.debugDisposed, true);
expect(image.box.isDeleted, true);
expect(image.box.isDeletedPermanently, true);
// Disallow double-dispose.
expect(() => image.dispose(), throwsAssertionError);
testCollector.collectNow();
});
test('CkImage can be explicitly disposed of when cloned', () async {
......@@ -83,29 +73,36 @@ void testMain() {
expect(box.refCount, 1);
expect(box.debugGetStackTraces().length, 1);
final CkImage imageClone = image.clone();
final CkImage clone = image.clone();
expect(box.refCount, 2);
expect(box.debugGetStackTraces().length, 2);
expect(image.isCloneOf(imageClone), true);
expect(box.isDeleted, false);
await Future<void>.delayed(Duration.zero);
expect(image.isCloneOf(clone), true);
expect(box.isDeletedPermanently, false);
testCollector.collectNow();
expect(skImage.isDeleted(), false);
image.dispose();
expect(box.isDeleted, false);
await Future<void>.delayed(Duration.zero);
expect(box.refCount, 1);
expect(box.isDeletedPermanently, false);
testCollector.collectNow();
expect(skImage.isDeleted(), false);
imageClone.dispose();
expect(box.isDeleted, true);
await Future<void>.delayed(Duration.zero);
clone.dispose();
expect(box.refCount, 0);
expect(box.isDeletedPermanently, true);
testCollector.collectNow();
expect(skImage.isDeleted(), true);
expect(box.debugGetStackTraces().length, 0);
testCollector.collectNow();
});
test('skiaInstantiateWebImageCodec throws exception if given invalid URL',
() async {
expect(skiaInstantiateWebImageCodec('invalid-url', null),
throwsA(isA<ProgressEvent>()));
testCollector.collectNow();
});
test('CkImage toByteData', () async {
......@@ -115,6 +112,7 @@ void testMain() {
final CkImage image = CkImage(skImage);
expect((await image.toByteData()).lengthInBytes, greaterThan(0));
expect((await image.toByteData(format: ui.ImageByteFormat.png)).lengthInBytes, greaterThan(0));
testCollector.collectNow();
});
// TODO: https://github.com/flutter/flutter/issues/60040
}, skip: isIosSafari);
......
......@@ -17,14 +17,7 @@ void main() {
void testMain() {
group('CkPath', () {
setUpAll(() async {
debugResetBrowserSupportsFinalizationRegistry();
await ui.webOnlyInitializePlatform();
});
tearDown(() {
debugResetBrowserSupportsFinalizationRegistry();
});
setUpCanvasKitTest();
test('Using CanvasKit', () {
expect(useCanvasKit, true);
......
......@@ -19,9 +19,7 @@ void main() {
void testMain() {
group('CanvasKit shaders', () {
setUpAll(() async {
await ui.webOnlyInitializePlatform();
});
setUpCanvasKitTest();
test('Sweep gradient', () {
final CkGradientSweep gradient = ui.Gradient.sweep(
......
......@@ -8,7 +8,6 @@ import 'package:mockito/mockito.dart';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/ui.dart' as ui;
import 'package:ui/src/engine.dart';
import '../matchers.dart';
......@@ -27,21 +26,15 @@ void testMain() {
void _tests() {
SkiaObjects.maximumCacheSize = 4;
bool originalBrowserSupportsFinalizationRegistry;
setUpAll(() async {
await ui.webOnlyInitializePlatform();
setUpCanvasKitTest();
setUp(() async {
// Pretend the browser does not support FinalizationRegistry so we can test the
// resurrection logic.
originalBrowserSupportsFinalizationRegistry = browserSupportsFinalizationRegistry;
browserSupportsFinalizationRegistry = false;
});
tearDownAll(() {
browserSupportsFinalizationRegistry = originalBrowserSupportsFinalizationRegistry;
});
group(ManagedSkiaObject, () {
test('implements create, cache, delete, resurrect, delete lifecycle', () {
int addPostFrameCallbackCount = 0;
......@@ -158,11 +151,12 @@ void _tests() {
group(SkiaObjectBox, () {
test('Records stack traces and respects refcounts', () async {
TestSkDeletable.deleteCount = 0;
TestBoxWrapper.resurrectCount = 0;
final TestBoxWrapper original = TestBoxWrapper();
expect(original.box.debugGetStackTraces().length, 1);
expect(original.box.refCount, 1);
expect(original.box.isDeleted, false);
expect(original.box.isDeletedPermanently, false);
final TestBoxWrapper clone = original.clone();
expect(clone.box, same(original.box));
......@@ -170,12 +164,11 @@ void _tests() {
expect(clone.box.refCount, 2);
expect(original.box.debugGetStackTraces().length, 2);
expect(original.box.refCount, 2);
expect(original.box.isDeleted, false);
expect(original.box.isDeletedPermanently, false);
original.dispose();
// Let Skia object delete queue run.
await Future<void>.delayed(Duration.zero);
testCollector.collectNow();
expect(TestSkDeletable.deleteCount, 0);
expect(clone.box.debugGetStackTraces().length, 1);
......@@ -184,19 +177,63 @@ void _tests() {
expect(original.box.refCount, 1);
clone.dispose();
// Let Skia object delete queue run.
await Future<void>.delayed(Duration.zero);
expect(TestSkDeletable.deleteCount, 1);
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(original.box.isDeletedPermanently, true);
testCollector.collectNow();
expect(TestSkDeletable.deleteCount, 1);
expect(TestBoxWrapper.resurrectCount, 0);
expect(() => clone.box.unref(clone), throwsAssertionError);
});
test('Can resurrect Skia objects', () async {
TestSkDeletable.deleteCount = 0;
TestBoxWrapper.resurrectCount = 0;
final TestBoxWrapper object = TestBoxWrapper();
expect(TestSkDeletable.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(TestSkDeletable.deleteCount, i + 1);
expect(TestBoxWrapper.resurrectCount, i);
expect(object.box.isDeletedTemporarily, true);
expect(object.box.isDeletedPermanently, false);
expect(object.box.skiaObject, isNotNull);
expect(TestSkDeletable.deleteCount, i + 1);
expect(TestBoxWrapper.resurrectCount, i + 1);
expect(object.box.isDeletedTemporarily, false);
expect(object.box.isDeletedPermanently, false);
}
object.dispose();
expect(object.box.isDeletedPermanently, true);
});
test('Can dispose temporarily deleted object', () async {
TestSkDeletable.deleteCount = 0;
TestBoxWrapper.resurrectCount = 0;
final TestBoxWrapper object = TestBoxWrapper();
expect(TestSkDeletable.deleteCount, 0);
expect(TestBoxWrapper.resurrectCount, 0);
object.box.delete();
object.box.didDelete();
expect(TestSkDeletable.deleteCount, 1);
expect(TestBoxWrapper.resurrectCount, 0);
expect(object.box.isDeletedTemporarily, true);
expect(object.box.isDeletedPermanently, false);
object.dispose();
expect(object.box.isDeletedPermanently, true);
});
});
}
......@@ -204,11 +241,20 @@ void _tests() {
///
/// Can be [clone]d such that the clones share the same ref counted box.
class TestBoxWrapper implements StackTraceDebugger {
static int resurrectCount = 0;
TestBoxWrapper() {
if (assertionsEnabled) {
_debugStackTrace = StackTrace.current;
}
box = SkiaObjectBox<TestBoxWrapper, TestSkDeletable>(this, TestSkDeletable());
box = SkiaObjectBox<TestBoxWrapper, TestSkDeletable>.resurrectable(
this,
TestSkDeletable(),
() {
resurrectCount += 1;
return TestSkDeletable();
}
);
}
TestBoxWrapper.cloneOf(this.box) {
......@@ -235,8 +281,15 @@ class TestBoxWrapper implements StackTraceDebugger {
class TestSkDeletable implements SkDeletable {
static int deleteCount = 0;
@override
bool isDeleted() => _isDeleted;
bool _isDeleted = false;
@override
void delete() {
expect(_isDeleted, isFalse,
reason: 'CanvasKit does not allow deleting the same object more than once.');
_isDeleted = true;
deleteCount++;
}
}
......@@ -246,8 +299,14 @@ class TestOneShotSkiaObject extends OneShotSkiaObject<SkPaint> implements SkDele
TestOneShotSkiaObject() : super(SkPaint());
@override
bool isDeleted() => _isDeleted;
bool _isDeleted = false;
@override
void delete() {
expect(_isDeleted, isFalse,
reason: 'CanvasKit does not allow deleting the same object more than once.');
rawSkiaObject?.delete();
deleteCount++;
}
......
......@@ -16,9 +16,7 @@ void main() {
void testMain() {
group('Vertices', () {
setUpAll(() async {
await ui.webOnlyInitializePlatform();
});
setUpCanvasKitTest();
test('can be constructed, drawn, and deleted', () {
final CkVertices vertices = _testVertices();
......
Markdown is supported
0% .
You are about to add 0 people to the discussion. Proceed with caution.
先完成此消息的编辑!
想要评论请 注册