forked from firka/flutter
[web] remove obsolete object caches; simplify native object management (flutter/engine#40894)
(this is attempt 3; details below) Remove obsolete object caches and introduce a simpler way to manage native objects: * Remove the unused `SynchronousSkiaObjectCache`. * Introduce new library `native_memory.dart` that's smaller and simpler than `skia_object_cache.dart`. * Introduce two types of native object references: * `UniqueRef` a reference with a unique Dart object owner. * `CountedRef` a ref-counted reference with multiple Dart object owners. * All native references use GC (via `FinalizationRegistry`) as a back-up. * The new library removes everything related to object resurrection that was needed only in browsers that didn't support `FinalizationRegistry`. All browsers support it now. * Remove the ad hoc `SkParagraph` cache that predates the introduction of `Paragraph.dispose`. * Rewrite `CkParagraph` in terms of `UniqueRef`. * Rewrite `CkImage` in terms of `CountedRef`; delete `SkiaObjectBox`. This PR does not migrate all objects from the old `skia_object_cache.dart` to `native_memory.dart`. That would be too big of a change. The migration can be done in multiple smaller PRs. This also removes a few unnecessary relayouts observed in https://github.com/flutter/flutter/issues/120921, but not all of them (more details in https://github.com/flutter/flutter/issues/120921#issuecomment-1481958762) ## About attempt 3 More about [attempt 2 here](https://github.com/flutter/engine/pull/40862). In this attempt 3 I'm replacing the `factory` with a top-level function.
This commit is contained in:
@@ -1876,6 +1876,7 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder
|
||||
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
|
||||
@@ -4460,6 +4461,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/layer_scene_builder.d
|
||||
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
|
||||
|
||||
@@ -39,6 +39,7 @@ export 'engine/canvaskit/layer_scene_builder.dart';
|
||||
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';
|
||||
|
||||
@@ -215,7 +215,6 @@ class CkCanvas {
|
||||
offset.dx,
|
||||
offset.dy,
|
||||
);
|
||||
paragraph.markUsed();
|
||||
}
|
||||
|
||||
void drawPath(CkPath path, CkPaint paint) {
|
||||
@@ -1112,7 +1111,6 @@ class CkDrawParagraphCommand extends CkPaintCommand {
|
||||
offset.dx,
|
||||
offset.dy,
|
||||
);
|
||||
paragraph.markUsed();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3392,7 +3392,7 @@ abstract class Collector {
|
||||
class ProductionCollector implements Collector {
|
||||
ProductionCollector() {
|
||||
_skObjectFinalizationRegistry =
|
||||
SkObjectFinalizationRegistry((SkDeletable deletable) {
|
||||
createSkObjectFinalizationRegistry((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
|
||||
@@ -3568,10 +3568,13 @@ extension JsConstructorExtension on JsConstructor {
|
||||
/// 6. We call `delete` on SkPaint.
|
||||
@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);
|
||||
class SkObjectFinalizationRegistry {}
|
||||
|
||||
SkObjectFinalizationRegistry createSkObjectFinalizationRegistry(JSFunction cleanup) {
|
||||
return js_util.callConstructor(
|
||||
_finalizationRegistryConstructor!.toObjectShallow,
|
||||
<Object>[cleanup],
|
||||
);
|
||||
}
|
||||
|
||||
extension SkObjectFinalizationRegistryExtension on SkObjectFinalizationRegistry {
|
||||
|
||||
@@ -15,10 +15,10 @@ import 'canvas.dart';
|
||||
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 @@ Future<Uint8List> readChunked(HttpFetchPayload payload, int contentLength, WebOn
|
||||
/// 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 @@ class CkImage implements ui.Image, StackTraceDebugger {
|
||||
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 @@ class CkImage implements ui.Image, StackTraceDebugger {
|
||||
|
||||
/// 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;
|
||||
|
||||
|
||||
@@ -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 = createSkObjectFinalizationRegistry(
|
||||
(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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -46,8 +46,7 @@ class CkPicture extends ManagedSkiaObject<SkPicture> implements ui.Picture {
|
||||
/// 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.
|
||||
|
||||
@@ -7,7 +7,6 @@ import 'dart:collection';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
import '../../engine.dart' show Instrumentation;
|
||||
import '../util.dart';
|
||||
import 'canvaskit_api.dart';
|
||||
import 'renderer.dart';
|
||||
|
||||
@@ -88,85 +87,6 @@ class SkiaObjectCache {
|
||||
}
|
||||
}
|
||||
|
||||
/// 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 @@ abstract class ManagedSkiaObject<T extends Object> extends SkiaObject<T> {
|
||||
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 {
|
||||
|
||||
@@ -10,6 +10,7 @@ import 'package:ui/ui.dart' as ui;
|
||||
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 @@ SkFontStyle toSkFontStyle(ui.FontWeight? fontWeight, ui.FontStyle? fontStyle) {
|
||||
/// 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 @@ class CkParagraph extends SkiaObject<SkParagraph> implements ui.Paragraph {
|
||||
/// 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 @@ class CkParagraph extends SkiaObject<SkParagraph> implements ui.Paragraph {
|
||||
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 @@ class CkParagraph extends SkiaObject<SkParagraph> implements ui.Paragraph {
|
||||
}
|
||||
|
||||
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 @@ class CkParagraph extends SkiaObject<SkParagraph> implements ui.Paragraph {
|
||||
|
||||
@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 @@ class CkParagraph extends SkiaObject<SkParagraph> implements ui.Paragraph {
|
||||
|
||||
@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 @@ class CkParagraph extends SkiaObject<SkParagraph> implements ui.Paragraph {
|
||||
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 @@ class CkParagraph extends SkiaObject<SkParagraph> implements ui.Paragraph {
|
||||
|
||||
@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 @@ class CkParagraph extends SkiaObject<SkParagraph> implements ui.Paragraph {
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
delete();
|
||||
didDelete();
|
||||
assert(!_disposed, 'Paragraph has been disposed.');
|
||||
_ref.dispose();
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
@@ -877,8 +797,7 @@ class CkLineMetrics implements ui.LineMetrics {
|
||||
|
||||
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 @@ class CkParagraphBuilder implements ui.ParagraphBuilder {
|
||||
|
||||
final SkParagraphBuilder _paragraphBuilder;
|
||||
final CkParagraphStyle _style;
|
||||
final List<_ParagraphCommand> _commands;
|
||||
int _placeholderCount;
|
||||
final List<double> _placeholderScales;
|
||||
final List<CkTextStyle> _styleStack;
|
||||
@@ -923,7 +841,6 @@ class CkParagraphBuilder implements ui.ParagraphBuilder {
|
||||
}
|
||||
|
||||
void _addPlaceholder(_CkParagraphPlaceholder placeholderStyle) {
|
||||
_commands.add(_ParagraphCommand.addPlaceholder(placeholderStyle));
|
||||
_paragraphBuilder.addPlaceholder(
|
||||
placeholderStyle.width,
|
||||
placeholderStyle.height,
|
||||
@@ -961,14 +878,13 @@ class CkParagraphBuilder implements ui.ParagraphBuilder {
|
||||
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 @@ class CkParagraphBuilder implements ui.ParagraphBuilder {
|
||||
}
|
||||
return;
|
||||
}
|
||||
_commands.add(const _ParagraphCommand.pop());
|
||||
_styleStack.removeLast();
|
||||
_paragraphBuilder.pop();
|
||||
}
|
||||
@@ -1026,7 +941,6 @@ class CkParagraphBuilder implements ui.ParagraphBuilder {
|
||||
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 @@ class _CkParagraphPlaceholder {
|
||||
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>[];
|
||||
|
||||
@@ -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 @@ void _canvasTests() {
|
||||
builder.addText('Hello');
|
||||
final CkParagraph paragraph = builder.build();
|
||||
|
||||
paragraph.delete();
|
||||
paragraph.dispose();
|
||||
expect(paragraph.debugDisposed, true);
|
||||
});
|
||||
@@ -1894,6 +1894,15 @@ void _paragraphTests() {
|
||||
'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 = createSkObjectFinalizationRegistry((String arg) {}.toJS);
|
||||
registry.register(Object(), Object());
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -137,10 +137,10 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) {
|
||||
.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 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) {
|
||||
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 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) {
|
||||
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 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) {
|
||||
// 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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -12,8 +12,6 @@ import 'package:test/test.dart';
|
||||
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 @@ void _tests() {
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user