blob: 17ef95318ba136fed8af1feb7c1946128bc8e286 [file] [log] [blame]
// 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.
// @dart = 2.12
part of engine;
/// A cache of Skia objects whose memory Flutter manages.
///
/// When using Skia, Flutter creates Skia objects which are allocated in
/// WASM memory and which must be explicitly deleted. In the case of Flutter
/// mobile, the Skia objects are wrapped by a C++ class which is destroyed
/// when the associated Dart object is garbage collected.
///
/// On the web, we cannot tell when a Dart object is garbage collected, so
/// we must use other strategies to know when to delete a Skia object. Some
/// objects, like [ui.Paint], can safely delete their associated Skia object
/// because they can always recreate the Skia object from data stored in the
/// Dart object. Other objects, like [ui.Picture], can be serialized to a
/// JS-managed data structure when they are deleted so that when the associated
/// object is garbage collected, so is the serialized data.
class SkiaObjectCache {
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> _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, DoubleLinkedQueueEntry<SkiaObject>> _itemMap;
SkiaObjectCache(this.maximumSize)
: _itemQueue = DoubleLinkedQueue<SkiaObject>(),
_itemMap = <SkiaObject, DoubleLinkedQueueEntry<SkiaObject>>{};
/// 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) {
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 half of the cache
/// will be deleted.
void add(SkiaObject object) {
_itemQueue.addFirst(object);
_itemMap[object] = _itemQueue.firstEntry()!;
if (_itemQueue.length > maximumSize) {
SkiaObjects.markCacheForResize(this);
}
}
/// Records that [object] was used in the most recent frame.
void markUsed(SkiaObject object) {
DoubleLinkedQueueEntry<SkiaObject> item = _itemMap[object]!;
item.remove();
_itemQueue.addFirst(object);
_itemMap[object] = _itemQueue.firstEntry()!;
}
/// Deletes the least recently used half of this cache.
void resize() {
final int itemsToDelete = maximumSize ~/ 2;
for (int i = 0; i < itemsToDelete; i++) {
final SkiaObject 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.
///
/// These objects are automatically deleted when no longer used.
abstract class SkiaObject<T extends Object> {
/// The JavaScript object that's mapped onto a Skia C++ object in the WebAssembly heap.
T get skiaObject;
/// Deletes the associated C++ object from the WebAssembly heap.
void delete();
/// Lifecycle method called immediately after calling [delete].
///
/// This method is used to
void didDelete();
}
/// A [SkiaObject] that manages the lifecycle of its C++ counterpart.
///
/// In browsers that support weak references we use feedback from the garbage
/// collector to determine when it is safe to release the C++ object.
///
/// In browsers that do not support weak references we pessimistically delete
/// the underlying C++ object before the Dart object is garbage-collected.
///
/// If [isResurrectionExpensive] is false the object is deleted at the end of
/// the frame. If a deleted object is reused in a subsequent frame it is
/// resurrected by calling [resurrect]. This allows reusing the C++ objects
/// within the frame.
///
/// If [isResurrectionExpensive] is true the object is put in a LRU cache.
/// Objects that are used least frequently are deleted from the cache when
/// the cache limit is reached.
///
/// The lifecycle of a resurrectable C++ object is as follows:
///
/// - Create: a managed object is created using a default instance that's
/// either supplied as a constructor argument, or obtained by calling
/// [createDefault]. The data in the new object is expected to contain
/// data matching Flutter's defaults (sometimes Skia defaults need to be
/// adjusted).
/// - Zero or more cycles of delete + resurrect: when a Dart object is reused
/// after its C++ object is deleted we create a new C++ object populated with
/// data from the current state of the Dart object. This is done using the
/// [resurrect] method.
/// - Final delete: if a Dart object is never reused, it is GC'd after its
/// underlying C++ object is deleted. This is implemented by [SkiaObjects].
abstract class ManagedSkiaObject<T extends Object> extends SkiaObject<T> {
/// Creates a managed Skia object.
///
/// If `instance` is null calls [createDefault] to create a Skia object to
/// manage. Otherwise, uses the provided instance.
///
/// The provided instance must not be managed by another [ManagedSkiaObject],
/// as it may lead to undefined behavior.
ManagedSkiaObject([T? instance]) {
final T defaultObject = instance ?? createDefault();
rawSkiaObject = defaultObject;
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);
} else {
// If FinalizationRegistry is _not_ supported we may need to delete
// and resurrect the object multiple times before deleting it forever.
if (isResurrectionExpensive) {
SkiaObjects.manageExpensive(this);
} else {
SkiaObjects.manageResurrectable(this);
}
}
}
@override
T get skiaObject => rawSkiaObject ?? _doResurrect();
T _doResurrect() {
assert(!browserSupportsFinalizationRegistry);
final T skiaObject = resurrect();
rawSkiaObject = skiaObject;
if (isResurrectionExpensive) {
SkiaObjects.manageExpensive(this);
} else {
SkiaObjects.manageResurrectable(this);
}
return skiaObject;
}
@override
void didDelete() {
assert(!browserSupportsFinalizationRegistry);
rawSkiaObject = null;
}
/// Returns the current skia object as is without attempting to
/// resurrect it.
///
/// If the returned value is `null`, the corresponding C++ object has
/// been deleted.
///
/// Use this field instead of the [skiaObject] getter when implementing
/// the [delete] method.
T? rawSkiaObject;
/// Instantiates a new Skia-backed JavaScript object containing default
/// values.
///
/// The object is expected to represent Flutter's defaults. If Skia uses
/// different defaults from those used by Flutter, this method is expected
/// initialize the object to Flutter's defaults.
T createDefault();
/// Creates a new Skia-backed JavaScript object containing data representing
/// the current state of the Dart object.
T resurrect();
/// Whether or not it is expensive to resurrect this object.
///
/// Defaults to false.
bool get isResurrectionExpensive => false;
}
// TODO(hterkelsen): [OneShotSkiaObject] is dangerous because it might delete
// the underlying Skia object while the associated Dart object is still in
// use. This issue discusses ways to address this:
// https://github.com/flutter/flutter/issues/60401
/// A [SkiaObject] which is deleted once and cannot be used again.
///
/// In browsers that support weak references we use feedback from the garbage
/// collector to determine when it is safe to release the C++ object. Otherwise,
/// we use an LRU cache (see [SkiaObjects.manageOneShot]).
abstract class OneShotSkiaObject<T extends Object> extends SkiaObject<T> {
/// Returns the current skia object as is without attempting to
/// resurrect it.
///
/// If the returned value is `null`, the corresponding C++ object has
/// been deleted.
///
/// Use this field instead of the [skiaObject] getter when implementing
/// the [delete] method.
T rawSkiaObject;
bool _isDeleted = false;
OneShotSkiaObject(T skObject) : this.rawSkiaObject = skObject {
if (browserSupportsFinalizationRegistry) {
skObjectFinalizationRegistry.register(this, skObject);
} else {
SkiaObjects.manageOneShot(this);
}
}
@override
T get skiaObject {
if (browserSupportsFinalizationRegistry) {
return rawSkiaObject;
}
if (_isDeleted) {
throw StateError('Attempting to use a Skia object that has been freed.');
}
SkiaObjects.oneShotCache.markUsed(this);
return rawSkiaObject;
}
@override
void didDelete() {
_isDeleted = true;
}
}
/// Interface that classes wrapping [SkiaObjectBox] must implement.
///
/// Used to collect stack traces in debug mode.
abstract class StackTraceDebugger {
/// The stack trace pointing to code location that created or upreffed a
/// [SkiaObjectBox].
StackTrace get debugStackTrace;
}
/// Uses reference counting to manage the lifecycle of a Skia object owned by a
/// wrapper object.
///
/// When the wrapper is garbage collected, decrements the refcount (only in
/// browsers that support weak references).
///
/// The [delete] method can be used to eagerly decrement the refcount before the
/// wrapper is garbage collected.
///
/// 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 {
if (assertionsEnabled) {
debugReferrers.add(debugReferrer);
}
if (browserSupportsFinalizationRegistry) {
boxRegistry.register(this, _skDeletable);
}
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();
}
return null;
}
/// 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.
final T skiaObject;
final SkDeletable _skDeletable;
/// Whether this object has been deleted.
bool get isDeleted => _isDeleted;
bool _isDeleted = false;
/// Deletes Skia objects when their wrappers are garbage collected.
static final SkObjectFinalizationRegistry boxRegistry =
SkObjectFinalizationRegistry(js.allowInterop((SkDeletable deletable) {
deletable.delete();
}));
/// 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(_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(!_isDeleted, 'Attempted to unref an already deleted Skia object.');
assert(
debugReferrers.remove(debugReferrer),
'Attempted to decrement ref count by the same referrer more than once.',
);
_refCount -= 1;
assert(refCount == debugReferrers.length);
if (_refCount == 0) {
_isDeleted = true;
_scheduleSkObjectCollection(_skDeletable);
}
}
}
/// Singleton that manages the lifecycles of [SkiaObject] instances.
class SkiaObjects {
@visibleForTesting
static final List<ManagedSkiaObject> resurrectableObjects =
<ManagedSkiaObject>[];
@visibleForTesting
static int maximumCacheSize = 8192;
@visibleForTesting
static final SkiaObjectCache oneShotCache = SkiaObjectCache(maximumCacheSize);
@visibleForTesting
static final SkiaObjectCache expensiveCache =
SkiaObjectCache(maximumCacheSize);
@visibleForTesting
static final List<SkiaObjectCache> cachesToResize = <SkiaObjectCache>[];
static bool _addedCleanupCallback = false;
@visibleForTesting
static void registerCleanupCallback() {
if (_addedCleanupCallback) {
return;
}
EnginePlatformDispatcher.instance.rasterizer!.addPostFrameCallback(postFrameCleanUp);
_addedCleanupCallback = true;
}
/// Starts managing the lifecycle of resurrectable [object].
///
/// These can safely be deleted at any time.
static void manageResurrectable(ManagedSkiaObject object) {
registerCleanupCallback();
resurrectableObjects.add(object);
}
/// Starts managing the lifecycle of a one-shot [object].
///
/// We should avoid deleting these whenever we can, since we won't
/// be able to resurrect them.
static void manageOneShot(OneShotSkiaObject object) {
registerCleanupCallback();
oneShotCache.add(object);
}
/// Starts managing the lifecycle of a resurrectable object that is expensive.
///
/// Since it's expensive to resurrect, we shouldn't just delete it after every
/// frame. Instead, add it to a cache and only delete it when the cache fills.
static void manageExpensive(SkiaObject object) {
registerCleanupCallback();
expensiveCache.add(object);
}
/// Marks that [cache] has overflown its maximum size and show be resized.
static void markCacheForResize(SkiaObjectCache cache) {
registerCleanupCallback();
if (cachesToResize.contains(cache)) {
return;
}
cachesToResize.add(cache);
}
/// Cleans up managed Skia memory.
static void postFrameCleanUp() {
if (resurrectableObjects.isEmpty && cachesToResize.isEmpty) {
return;
}
for (int i = 0; i < resurrectableObjects.length; i++) {
final SkiaObject object = resurrectableObjects[i];
object.delete();
object.didDelete();
}
resurrectableObjects.clear();
for (int i = 0; i < cachesToResize.length; i++) {
final SkiaObjectCache cache = cachesToResize[i];
cache.resize();
}
cachesToResize.clear();
}
}