Skip to content

Commit

Permalink
Use weak references instead of resurrection, if available (flutter#20283
Browse files Browse the repository at this point in the history
)

* Use weak references instead of resurrection, if available
  • Loading branch information
yjbanov committed Aug 7, 2020
1 parent cfd8528 commit e020907
Show file tree
Hide file tree
Showing 9 changed files with 142 additions and 36 deletions.
71 changes: 71 additions & 0 deletions lib/web_ui/lib/src/engine/compositor/canvaskit_api.dart
Expand Up @@ -1558,3 +1558,74 @@ class SkFontMgrNamespace {
class TypefaceFontProviderNamespace {
external TypefaceFontProvider Make();
}

Timer? _skObjectCollector;
List<SkDeletable> _skObjectDeleteQueue = <SkDeletable>[];

final SkObjectFinalizationRegistry skObjectFinalizationRegistry = SkObjectFinalizationRegistry(js.allowInterop((SkDeletable deletable) {
_skObjectDeleteQueue.add(deletable);
_skObjectCollector ??= _scheduleSkObjectCollection();
}));

/// Schedules an asap timer to delete garbage-collected Skia objects.
///
/// We use a timer for the following reasons:
///
/// - Deleting the object immediately may lead to dangling pointer as the Skia
/// object may still be used by a function in the current frame. For example,
/// a `CkPaint` + `SkPaint` pair may be created by the framework, passed to
/// the engine, and the `CkPaint` dropped immediately. Because GC can kick in
/// any time, including in the middle of the event, we may delete `SkPaint`
/// prematurely.
/// - A microtask, while solves the problem above, would prevent the event from
/// yielding to the graphics system to render the frame on the screen if there
/// is a large number of objects to delete, causing jank.
Timer _scheduleSkObjectCollection() => Timer(Duration.zero, () {
html.window.performance.mark('SkObject collection-start');
final int length = _skObjectDeleteQueue.length;
for (int i = 0; i < length; i++) {
_skObjectDeleteQueue[i].delete();
}
_skObjectDeleteQueue = <SkDeletable>[];

// Null out the timer so we can schedule a new one next time objects are
// scheduled for deletion.
_skObjectCollector = null;
html.window.performance.mark('SkObject collection-end');
html.window.performance.measure('SkObject collection', 'SkObject collection-start', 'SkObject collection-end');
});

typedef SkObjectFinalizer = void Function(SkDeletable deletable);

/// Any Skia object that has a `delete` method.
@JS()
class SkDeletable {
/// Deletes the C++ side object.
external void delete();
}

/// Attaches a weakly referenced object to another object and calls a finalizer
/// with the latter when weakly referenced object is garbage collected.
///
/// We use this to delete Skia objects when their "Ck" wrapper is garbage
/// collected.
///
/// Example sequence of events:
///
/// 1. A (CkPaint, SkPaint) pair created.
/// 2. The paint is used to paint some picture.
/// 3. CkPaint is dropped by the app.
/// 4. GC decides to perform a GC cycle and collects CkPaint.
/// 5. The finalizer function is called with the SkPaint as the sole argument.
/// 6. We call `delete` on SkPaint.
@JS('window.FinalizationRegistry')
class SkObjectFinalizationRegistry {
external SkObjectFinalizationRegistry(SkObjectFinalizer finalizer);
external void register(Object ckObject, Object skObject);
}

@JS('window.FinalizationRegistry')
external Object? get _finalizationRegistryConstructor;

/// Whether the current browser supports `FinalizationRegistry`.
bool browserSupportsFinalizationRegistry = _finalizationRegistryConstructor != null;
2 changes: 1 addition & 1 deletion lib/web_ui/lib/src/engine/compositor/color_filter.dart
Expand Up @@ -6,7 +6,7 @@
part of engine;

/// A [ui.ColorFilter] backed by Skia's [CkColorFilter].
class CkColorFilter extends ResurrectableSkiaObject<SkColorFilter> {
class CkColorFilter extends ManagedSkiaObject<SkColorFilter> {
final EngineColorFilter _engineFilter;

CkColorFilter.mode(EngineColorFilter filter) : _engineFilter = filter;
Expand Down
2 changes: 1 addition & 1 deletion lib/web_ui/lib/src/engine/compositor/image_filter.dart
Expand Up @@ -8,7 +8,7 @@ part of engine;
/// The CanvasKit implementation of [ui.ImageFilter].
///
/// Currently only supports `blur`.
class CkImageFilter extends ResurrectableSkiaObject<SkImageFilter> implements ui.ImageFilter {
class CkImageFilter extends ManagedSkiaObject<SkImageFilter> implements ui.ImageFilter {
CkImageFilter.blur({double sigmaX = 0.0, double sigmaY = 0.0})
: _sigmaX = sigmaX,
_sigmaY = sigmaY;
Expand Down
2 changes: 1 addition & 1 deletion lib/web_ui/lib/src/engine/compositor/mask_filter.dart
Expand Up @@ -6,7 +6,7 @@
part of engine;

/// The CanvasKit implementation of [ui.MaskFilter].
class CkMaskFilter extends ResurrectableSkiaObject<SkMaskFilter> {
class CkMaskFilter extends ManagedSkiaObject<SkMaskFilter> {
CkMaskFilter.blur(ui.BlurStyle blurStyle, double sigma)
: _blurStyle = blurStyle,
_sigma = sigma;
Expand Down
2 changes: 1 addition & 1 deletion lib/web_ui/lib/src/engine/compositor/painting.dart
Expand Up @@ -9,7 +9,7 @@ part of engine;
///
/// This class is backed by a Skia object that must be explicitly
/// deleted to avoid a memory leak. This is done by extending [SkiaObject].
class CkPaint extends ResurrectableSkiaObject<SkPaint> implements ui.Paint {
class CkPaint extends ManagedSkiaObject<SkPaint> implements ui.Paint {
CkPaint();

static const ui.Color _defaultPaintColor = ui.Color(0xFF000000);
Expand Down
2 changes: 1 addition & 1 deletion lib/web_ui/lib/src/engine/compositor/picture.dart
Expand Up @@ -32,6 +32,6 @@ class SkPictureSkiaObject extends OneShotSkiaObject<SkPicture> {

@override
void delete() {
rawSkiaObject?.delete();
rawSkiaObject.delete();
}
}
81 changes: 53 additions & 28 deletions lib/web_ui/lib/src/engine/compositor/skia_object_cache.dart
Expand Up @@ -86,7 +86,7 @@ class SkiaObjectCache {
/// WebAssembly heap.
///
/// These objects are automatically deleted when no longer used.
abstract class SkiaObject<T> {
abstract class SkiaObject<T extends Object> {
/// The JavaScript object that's mapped onto a Skia C++ object in the WebAssembly heap.
T get skiaObject;

Expand All @@ -99,16 +99,19 @@ abstract class SkiaObject<T> {
void didDelete();
}

/// A [SkiaObject] that can resurrect its C++ counterpart.
/// A [SkiaObject] that manages the lifecycle of its C++ counterpart.
///
/// Because there is no feedback from JavaScript's GC (no destructors or
/// finalizers), we pessimistically delete the underlying C++ object before
/// the Dart object is garbage-collected. The current algorithm deletes objects
/// at the end of every frame. This allows reusing the C++ objects within the
/// frame. In the future we may add smarter strategies that will allow us to
/// reuse C++ objects across frames.
/// In browsers that support weak references we use feedback from the garbage
/// collector to determine when it is safe to release the C++ object.
///
/// The lifecycle of a C++ object is as follows:
/// In browsers that do not support weak references we pessimistically delete
/// the underlying C++ object before the Dart object is garbage-collected. The
/// current algorithm deletes objects at the end of every frame. This allows
/// reusing the C++ objects within the frame. If the object is used again after
/// is was deleted, we [resurrect] it based on the data available on the
/// JavaScript side.
///
/// The lifecycle of a resurrectable C++ object is as follows:
///
/// - Create default: when instantiating a C++ object for a Dart object for the
/// first time, the C++ object is populated with default data (the defaults are
Expand All @@ -120,20 +123,30 @@ abstract class SkiaObject<T> {
/// [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 ResurrectableSkiaObject<T> extends SkiaObject<T> {
ResurrectableSkiaObject() {
rawSkiaObject = createDefault();
if (isResurrectionExpensive) {
SkiaObjects.manageExpensive(this);
abstract class ManagedSkiaObject<T extends Object> extends SkiaObject<T> {
ManagedSkiaObject() {
final T defaultObject = 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 {
SkiaObjects.manageResurrectable(this);
// 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) {
Expand All @@ -146,6 +159,7 @@ abstract class ResurrectableSkiaObject<T> extends SkiaObject<T> {

@override
void didDelete() {
assert(!browserSupportsFinalizationRegistry);
rawSkiaObject = null;
}

Expand Down Expand Up @@ -182,7 +196,11 @@ abstract class ResurrectableSkiaObject<T> extends SkiaObject<T> {
// 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.
abstract class OneShotSkiaObject<T> extends SkiaObject<T> {
///
/// 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.
///
Expand All @@ -191,34 +209,41 @@ abstract class OneShotSkiaObject<T> extends SkiaObject<T> {
///
/// Use this field instead of the [skiaObject] getter when implementing
/// the [delete] method.
T? rawSkiaObject;
T rawSkiaObject;

OneShotSkiaObject(this.rawSkiaObject) {
SkiaObjects.manageOneShot(this);
bool _isDeleted = false;

OneShotSkiaObject(T skObject) : this.rawSkiaObject = skObject {
if (browserSupportsFinalizationRegistry) {
skObjectFinalizationRegistry.register(this, skObject);
} else {
SkiaObjects.manageOneShot(this);
}
}

@override
T get skiaObject {
if (rawSkiaObject == null) {
if (browserSupportsFinalizationRegistry) {
return rawSkiaObject;
}
if (_isDeleted) {
throw StateError('Attempting to use a Skia object that has been freed.');
}
SkiaObjects.oneShotCache.markUsed(this);
return rawSkiaObject!;
return rawSkiaObject;
}

@override
void didDelete() {
rawSkiaObject = null;
_isDeleted = true;
}
}

/// Singleton that manages the lifecycles of [SkiaObject] instances.
class SkiaObjects {
// TODO(yjbanov): some sort of LRU strategy would allow us to reuse objects
// beyond a single frame.
@visibleForTesting
static final List<ResurrectableSkiaObject> resurrectableObjects =
<ResurrectableSkiaObject>[];
static final List<ManagedSkiaObject> resurrectableObjects =
<ManagedSkiaObject>[];

@visibleForTesting
static int maximumCacheSize = 8192;
Expand Down Expand Up @@ -247,7 +272,7 @@ class SkiaObjects {
/// Starts managing the lifecycle of resurrectable [object].
///
/// These can safely be deleted at any time.
static void manageResurrectable(ResurrectableSkiaObject object) {
static void manageResurrectable(ManagedSkiaObject object) {
registerCleanupCallback();
resurrectableObjects.add(object);
}
Expand All @@ -265,7 +290,7 @@ class SkiaObjects {
///
/// 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(ResurrectableSkiaObject object) {
static void manageExpensive(ManagedSkiaObject object) {
registerCleanupCallback();
expensiveCache.add(object);
}
Expand Down
2 changes: 1 addition & 1 deletion lib/web_ui/lib/src/engine/compositor/text.dart
Expand Up @@ -216,7 +216,7 @@ SkFontStyle toSkFontStyle(ui.FontWeight? fontWeight, ui.FontStyle? fontStyle) {
return style;
}

class CkParagraph extends ResurrectableSkiaObject<SkParagraph>
class CkParagraph extends ManagedSkiaObject<SkParagraph>
implements ui.Paragraph {
CkParagraph(
this._initialParagraph, this._paragraphStyle, this._paragraphCommands);
Expand Down
14 changes: 12 additions & 2 deletions lib/web_ui/test/canvaskit/skia_objects_cache_test.dart
Expand Up @@ -21,12 +21,22 @@ void main() {

void _tests() {
SkiaObjects.maximumCacheSize = 4;
bool originalBrowserSupportsFinalizationRegistry;

setUpAll(() async {
await ui.webOnlyInitializePlatform();

// Pretend the browser does not support FinalizationRegistry so we can test the
// resurrection logic.
originalBrowserSupportsFinalizationRegistry = browserSupportsFinalizationRegistry;
browserSupportsFinalizationRegistry = false;
});

tearDownAll(() {
browserSupportsFinalizationRegistry = originalBrowserSupportsFinalizationRegistry;
});

group(ResurrectableSkiaObject, () {
group(ManagedSkiaObject, () {
test('implements create, cache, delete, resurrect, delete lifecycle', () {
int addPostFrameCallbackCount = 0;

Expand Down Expand Up @@ -152,7 +162,7 @@ class TestOneShotSkiaObject extends OneShotSkiaObject<SkPaint> {
}
}

class TestSkiaObject extends ResurrectableSkiaObject<SkPaint> {
class TestSkiaObject extends ManagedSkiaObject<SkPaint> {
int createDefaultCount = 0;
int resurrectCount = 0;
int deleteCount = 0;
Expand Down

0 comments on commit e020907

Please sign in to comment.