| // 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:collection'; |
| |
| import 'package:meta/meta.dart'; |
| |
| import '../../engine.dart' show EnginePlatformDispatcher, Instrumentation; |
| import '../util.dart'; |
| import 'canvaskit_api.dart'; |
| |
| /// 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<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; |
| |
| SkiaObjectCache(this.maximumSize) |
| : _itemQueue = DoubleLinkedQueue<SkiaObject<Object>>(), |
| _itemMap = <SkiaObject<Object>, DoubleLinkedQueueEntry<SkiaObject<Object>>>{}; |
| |
| /// 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 half of the cache |
| /// will be deleted. |
| void add(SkiaObject<Object> 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> object) { |
| final DoubleLinkedQueueEntry<SkiaObject<Object>> 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<Object> oldObject = _itemQueue.removeLast(); |
| _itemMap.remove(oldObject); |
| oldObject.delete(); |
| oldObject.didDelete(); |
| } |
| } |
| } |
| |
| /// Like [SkiaObjectCache] but enforces the [maximumSize] of the cache |
| /// synchronously instead of waiting until a post-frame callback. |
| class SynchronousSkiaObjectCache { |
| /// 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; |
| |
| SynchronousSkiaObjectCache(this.maximumSize) |
| : _itemQueue = DoubleLinkedQueue<SkiaObject<Object>>(), |
| _itemMap = <SkiaObject<Object>, DoubleLinkedQueueEntry<SkiaObject<Object>>>{}; |
| |
| /// 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. |
| /// |
| /// 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. |
| 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. |
| if (Instrumentation.enabled) { |
| Instrumentation.instance.incrementCounter( |
| '${(defaultObject as SkDeletable).constructor.name} created', |
| ); |
| } |
| if (isResurrectionExpensive) { |
| SkiaObjects.manageExpensive(this); |
| } else { |
| SkiaObjects.manageResurrectable(this); |
| } |
| } |
| } |
| |
| @override |
| T get skiaObject => rawSkiaObject ?? _doResurrect(); |
| |
| T _doResurrect() { |
| assert(!browserSupportsFinalizationRegistry); |
| final T skiaObject = resurrect(); |
| if (Instrumentation.enabled) { |
| Instrumentation.instance.incrementCounter( |
| '${(skiaObject as SkDeletable).constructor.name} resurrected', |
| ); |
| } |
| rawSkiaObject = skiaObject; |
| if (isResurrectionExpensive) { |
| SkiaObjects.manageExpensive(this); |
| } else { |
| SkiaObjects.manageResurrectable(this); |
| } |
| return skiaObject; |
| } |
| |
| @override |
| void didDelete() { |
| assert(!browserSupportsFinalizationRegistry); |
| |
| // Null indicates that the object has been manually disposed of. This |
| // happens for objects with manual lifecycles, such as Picture. |
| if (rawSkiaObject == null) { |
| return; |
| } |
| |
| if (Instrumentation.enabled) { |
| Instrumentation.instance.incrementCounter( |
| '${(rawSkiaObject! as SkDeletable).constructor.name} deleted', |
| ); |
| } |
| 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; |
| } |
| |
| /// 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 { |
| @visibleForTesting |
| static final List<ManagedSkiaObject<Object>> resurrectableObjects = |
| <ManagedSkiaObject<Object>>[]; |
| |
| @visibleForTesting |
| static int maximumCacheSize = 1024; |
| |
| @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; |
| } |
| // This method is @visibleForTesting but we're getting a warning about |
| // using a @visibleForTesting member. |
| // ignore: invalid_use_of_visible_for_testing_member |
| 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> object) { |
| registerCleanupCallback(); |
| resurrectableObjects.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> 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> 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(); |
| } |
| } |