Cache CanvasKit objects and delete if not used. (flutter/engine#19341)

This commit is contained in:
Harry Terkelsen 2020-06-29 16:40:57 -07:00 committed by GitHub
parent 6fd117d1ae
commit caa3fb9ff3
17 changed files with 705 additions and 309 deletions

View File

@ -431,6 +431,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/initialization.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/layer.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/layer_scene_builder.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/layer_tree.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/mask_filter.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/n_way_canvas.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/painting.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/path.dart
@ -440,6 +441,7 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/picture_recorder.dar
FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/platform_message.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/raster_cache.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/rasterizer.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/skia_object_cache.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/surface.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/text.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/compositor/util.dart

View File

@ -5,7 +5,8 @@
library engine;
import 'dart:async';
import 'dart:collection' show ListBase, IterableBase;
import 'dart:collection'
show ListBase, IterableBase, DoubleLinkedQueue, DoubleLinkedQueueEntry;
import 'dart:convert' hide Codec;
import 'dart:developer' as developer;
import 'dart:html' as html;
@ -37,6 +38,7 @@ part 'engine/compositor/initialization.dart';
part 'engine/compositor/layer.dart';
part 'engine/compositor/layer_scene_builder.dart';
part 'engine/compositor/layer_tree.dart';
part 'engine/compositor/mask_filter.dart';
part 'engine/compositor/n_way_canvas.dart';
part 'engine/compositor/path.dart';
part 'engine/compositor/painting.dart';
@ -46,6 +48,7 @@ part 'engine/compositor/picture_recorder.dart';
part 'engine/compositor/platform_message.dart';
part 'engine/compositor/raster_cache.dart';
part 'engine/compositor/rasterizer.dart';
part 'engine/compositor/skia_object_cache.dart';
part 'engine/compositor/surface.dart';
part 'engine/compositor/text.dart';
part 'engine/compositor/util.dart';

View File

@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
part of engine;
/// A Dart wrapper around Skia's SKCanvas.
@ -168,7 +167,7 @@ class SkCanvas {
void drawParagraph(ui.Paragraph paragraph, ui.Offset offset) {
final SkParagraph skParagraph = paragraph as SkParagraph;
skCanvas.callMethod('drawParagraph', <dynamic>[
skParagraph.skParagraph,
skParagraph.skiaObject,
offset.dx,
offset.dy,
]);
@ -183,7 +182,8 @@ class SkCanvas {
void drawPicture(ui.Picture picture) {
final SkPicture skPicture = picture as SkPicture;
skCanvas.callMethod('drawPicture', <js.JsObject?>[skPicture.skPicture]);
skCanvas.callMethod(
'drawPicture', <js.JsObject?>[skPicture.skPicture.skiaObject]);
}
// TODO(hterkelsen): https://github.com/flutter/flutter/issues/58824
@ -211,8 +211,8 @@ class SkCanvas {
void drawShadow(ui.Path path, ui.Color color, double elevation,
bool transparentOccluder) {
drawSkShadow(skCanvas, path as SkPath, color, elevation, transparentOccluder,
ui.window.devicePixelRatio);
drawSkShadow(skCanvas, path as SkPath, color, elevation,
transparentOccluder, ui.window.devicePixelRatio);
}
void drawVertices(
@ -243,7 +243,6 @@ class SkCanvas {
}
void saveLayer(ui.Rect bounds, SkPaint paint) {
assert(bounds != null, 'Use saveLayerWithoutBounds'); // ignore: unnecessary_null_comparison
skCanvas.callMethod('saveLayer', <js.JsObject?>[
makeSkRect(bounds),
paint.skiaObject,
@ -260,7 +259,7 @@ class SkCanvas {
'saveLayer',
<dynamic>[
null,
skImageFilter.skImageFilter,
skImageFilter.skiaObject,
0,
makeSkRect(bounds),
],

View File

@ -6,36 +6,52 @@
part of engine;
/// A [ui.ColorFilter] backed by Skia's [SkColorFilter].
class SkColorFilter {
js.JsObject? skColorFilter;
class SkColorFilter extends ResurrectableSkiaObject {
final EngineColorFilter _engineFilter;
SkColorFilter.mode(EngineColorFilter filter) {
setSharedSkColor1(filter._color!);
skColorFilter =
canvasKit['SkColorFilter'].callMethod('MakeBlend', <dynamic>[
sharedSkColor1,
makeSkBlendMode(filter._blendMode),
]);
}
SkColorFilter.mode(EngineColorFilter filter) : _engineFilter = filter;
SkColorFilter.matrix(EngineColorFilter filter) {
// TODO(het): Find a way to remove these array conversions.
final js.JsArray<double> colorMatrix = js.JsArray<double>();
colorMatrix.length = 20;
for (int i = 0; i < 20; i++) {
colorMatrix[i] = filter._matrix![i];
SkColorFilter.matrix(EngineColorFilter filter) : _engineFilter = filter;
SkColorFilter.linearToSrgbGamma(EngineColorFilter filter)
: _engineFilter = filter;
SkColorFilter.srgbToLinearGamma(EngineColorFilter filter)
: _engineFilter = filter;
js.JsObject _createSkiaObjectFromFilter() {
switch (_engineFilter._type) {
case EngineColorFilter._TypeMode:
setSharedSkColor1(_engineFilter._color!);
return canvasKit['SkColorFilter'].callMethod('MakeBlend', <dynamic>[
sharedSkColor1,
makeSkBlendMode(_engineFilter._blendMode),
]);
case EngineColorFilter._TypeMatrix:
final js.JsArray<double> colorMatrix = js.JsArray<double>();
colorMatrix.length = 20;
for (int i = 0; i < 20; i++) {
colorMatrix[i] = _engineFilter._matrix![i];
}
return canvasKit['SkColorFilter']
.callMethod('MakeMatrix', <js.JsArray>[colorMatrix]);
case EngineColorFilter._TypeLinearToSrgbGamma:
return canvasKit['SkColorFilter'].callMethod('MakeLinearToSRGBGamma');
case EngineColorFilter._TypeSrgbToLinearGamma:
return canvasKit['SkColorFilter'].callMethod('MakeSRGBToLinearGamma');
default:
throw StateError(
'Unknown mode ${_engineFilter._type} for ColorFilter.');
}
skColorFilter = canvasKit['SkColorFilter']
.callMethod('MakeMatrix', <js.JsArray>[colorMatrix]);
}
SkColorFilter.linearToSrgbGamma(EngineColorFilter filter) {
skColorFilter =
canvasKit['SkColorFilter'].callMethod('MakeLinearToSRGBGamma');
@override
js.JsObject createDefault() {
return _createSkiaObjectFromFilter();
}
SkColorFilter.srgbToLinearGamma(EngineColorFilter filter) {
skColorFilter =
canvasKit['SkColorFilter'].callMethod('MakeSRGBToLinearGamma');
@override
js.JsObject resurrect() {
return _createSkiaObjectFromFilter();
}
}

View File

@ -2,32 +2,35 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
part of engine;
/// The CanvasKit implementation of [ui.ImageFilter].
///
/// Currently only supports `blur`.
class SkImageFilter implements ui.ImageFilter {
js.JsObject? skImageFilter;
class SkImageFilter extends ResurrectableSkiaObject implements ui.ImageFilter {
SkImageFilter.blur({double sigmaX = 0.0, double sigmaY = 0.0})
: _sigmaX = sigmaX,
_sigmaY = sigmaY {
skImageFilter = canvasKit['SkImageFilter'].callMethod(
'MakeBlur',
<dynamic>[
sigmaX,
sigmaY,
canvasKit['TileMode']['Clamp'],
null,
],
);
}
_sigmaY = sigmaY;
final double _sigmaX;
final double _sigmaY;
@override
js.JsObject createDefault() => _initSkiaObject();
@override
js.JsObject resurrect() => _initSkiaObject();
js.JsObject _initSkiaObject() => canvasKit['SkImageFilter'].callMethod(
'MakeBlur',
<dynamic>[
_sigmaX,
_sigmaY,
canvasKit['TileMode']['Clamp'],
null,
],
);
@override
bool operator ==(dynamic other) {
if (other is! SkImageFilter) {

View File

@ -150,7 +150,8 @@ class ClipPathLayer extends ContainerLayer {
assert(needsPainting);
paintContext.internalNodesCanvas.save();
paintContext.internalNodesCanvas.clipPath(_clipPath, _clipBehavior != ui.Clip.hardEdge);
paintContext.internalNodesCanvas
.clipPath(_clipPath, _clipBehavior != ui.Clip.hardEdge);
if (_clipBehavior == ui.Clip.antiAliasWithSaveLayer) {
paintContext.internalNodesCanvas.saveLayer(paintBounds, null);

View File

@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
part of engine;
class LayerScene implements ui.Scene {
@ -49,7 +48,8 @@ class LayerSceneBuilder implements ui.SceneBuilder {
bool isComplexHint = false,
bool willChangeHint = false,
}) {
currentLayer!.add(PictureLayer(picture as SkPicture, offset, isComplexHint, willChangeHint));
currentLayer!.add(PictureLayer(
picture as SkPicture, offset, isComplexHint, willChangeHint));
}
@override

View File

@ -0,0 +1,42 @@
// 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.
part of engine;
/// The CanvasKit implementation of [ui.MaskFilter].
class SkMaskFilter extends ResurrectableSkiaObject {
SkMaskFilter.blur(ui.BlurStyle blurStyle, double sigma)
: _blurStyle = blurStyle,
_sigma = sigma;
final ui.BlurStyle _blurStyle;
final double _sigma;
@override
js.JsObject createDefault() => _initSkiaObject();
@override
js.JsObject resurrect() => _initSkiaObject();
js.JsObject _initSkiaObject() {
js.JsObject skBlurStyle;
switch (_blurStyle) {
case ui.BlurStyle.normal:
skBlurStyle = canvasKit['BlurStyle']['Normal'];
break;
case ui.BlurStyle.solid:
skBlurStyle = canvasKit['BlurStyle']['Solid'];
break;
case ui.BlurStyle.outer:
skBlurStyle = canvasKit['BlurStyle']['Outer'];
break;
case ui.BlurStyle.inner:
skBlurStyle = canvasKit['BlurStyle']['Inner'];
break;
}
return canvasKit
.callMethod('MakeBlurMaskFilter', <dynamic>[skBlurStyle, _sigma, true]);
}
}

View File

@ -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 SkPaint extends SkiaObject implements ui.Paint {
class SkPaint extends ResurrectableSkiaObject implements ui.Paint {
SkPaint();
static const ui.Color _defaultPaintColor = ui.Color(0xFF000000);
@ -22,7 +22,7 @@ class SkPaint extends SkiaObject implements ui.Paint {
@override
set blendMode(ui.BlendMode value) {
_blendMode = value;
_syncBlendMode(skiaObject!);
_syncBlendMode(skiaObject);
}
void _syncBlendMode(js.JsObject object) {
@ -41,7 +41,7 @@ class SkPaint extends SkiaObject implements ui.Paint {
_syncStyle(skiaObject);
}
void _syncStyle(js.JsObject? object) {
void _syncStyle(js.JsObject object) {
js.JsObject? skPaintStyle;
switch (_style) {
case ui.PaintingStyle.stroke:
@ -51,7 +51,7 @@ class SkPaint extends SkiaObject implements ui.Paint {
skPaintStyle = _skPaintStyleFill;
break;
}
object!.callMethod('setStyle', <js.JsObject?>[skPaintStyle]);
object.callMethod('setStyle', <js.JsObject?>[skPaintStyle]);
}
ui.PaintingStyle _style = ui.PaintingStyle.fill;
@ -61,7 +61,7 @@ class SkPaint extends SkiaObject implements ui.Paint {
@override
set strokeWidth(double value) {
_strokeWidth = value;
_syncStrokeWidth(skiaObject!);
_syncStrokeWidth(skiaObject);
}
void _syncStrokeWidth(js.JsObject object) {
@ -95,7 +95,7 @@ class SkPaint extends SkiaObject implements ui.Paint {
@override
set isAntiAlias(bool value) {
_isAntiAlias = value;
_syncAntiAlias(skiaObject!);
_syncAntiAlias(skiaObject);
}
void _syncAntiAlias(js.JsObject object) {
@ -109,7 +109,7 @@ class SkPaint extends SkiaObject implements ui.Paint {
@override
set color(ui.Color value) {
_color = value;
_syncColor(skiaObject!);
_syncColor(skiaObject);
}
void _syncColor(js.JsObject object) {
@ -135,7 +135,7 @@ class SkPaint extends SkiaObject implements ui.Paint {
@override
set shader(ui.Shader? value) {
_shader = value as EngineShader?;
_syncShader(skiaObject!);
_syncShader(skiaObject);
}
void _syncShader(js.JsObject object) {
@ -156,32 +156,15 @@ class SkPaint extends SkiaObject implements ui.Paint {
_syncMaskFilter(skiaObject);
}
void _syncMaskFilter(js.JsObject? object) {
js.JsObject? skMaskFilter;
void _syncMaskFilter(js.JsObject object) {
SkMaskFilter? skMaskFilter;
if (_maskFilter != null) {
final ui.BlurStyle blurStyle = _maskFilter!.webOnlyBlurStyle;
final double sigma = _maskFilter!.webOnlySigma;
js.JsObject? skBlurStyle;
switch (blurStyle) {
case ui.BlurStyle.normal:
skBlurStyle = canvasKit['BlurStyle']['Normal'];
break;
case ui.BlurStyle.solid:
skBlurStyle = canvasKit['BlurStyle']['Solid'];
break;
case ui.BlurStyle.outer:
skBlurStyle = canvasKit['BlurStyle']['Outer'];
break;
case ui.BlurStyle.inner:
skBlurStyle = canvasKit['BlurStyle']['Inner'];
break;
}
skMaskFilter = canvasKit.callMethod(
'MakeBlurMaskFilter', <dynamic>[skBlurStyle, sigma, true]);
skMaskFilter = SkMaskFilter.blur(blurStyle, sigma);
}
object!.callMethod('setMaskFilter', <js.JsObject?>[skMaskFilter]);
object.callMethod('setMaskFilter', <js.JsObject?>[skMaskFilter?.skiaObject]);
}
ui.MaskFilter? _maskFilter;
@ -220,14 +203,14 @@ class SkPaint extends SkiaObject implements ui.Paint {
@override
set colorFilter(ui.ColorFilter? value) {
_colorFilter = value as EngineColorFilter?;
_syncColorFilter(skiaObject!);
_syncColorFilter(skiaObject);
}
void _syncColorFilter(js.JsObject object) {
js.JsObject? skColorFilterJs;
if (_colorFilter != null) {
SkColorFilter skFilter = _colorFilter!._toSkColorFilter()!;
skColorFilterJs = skFilter.skColorFilter;
SkColorFilter? skFilter = _colorFilter!._toSkColorFilter();
skColorFilterJs = skFilter!.skiaObject;
}
object.callMethod('setColorFilter', <js.JsObject?>[skColorFilterJs]);
}
@ -249,13 +232,13 @@ class SkPaint extends SkiaObject implements ui.Paint {
@override
set imageFilter(ui.ImageFilter? value) {
_imageFilter = value as SkImageFilter?;
_syncImageFilter(skiaObject!);
_syncImageFilter(skiaObject);
}
void _syncImageFilter(js.JsObject object) {
js.JsObject? imageFilterJs;
if (_imageFilter != null) {
imageFilterJs = _imageFilter!.skImageFilter;
imageFilterJs = _imageFilter!.skiaObject;
}
object.callMethod('setImageFilter', <js.JsObject?>[imageFilterJs]);
}

View File

@ -5,7 +5,7 @@
part of engine;
class SkPicture implements ui.Picture {
final js.JsObject? skPicture;
final SkiaObject skPicture;
final ui.Rect? cullRect;
SkPicture(this.skPicture, this.cullRect);
@ -15,7 +15,7 @@ class SkPicture implements ui.Picture {
@override
void dispose() {
// TODO: implement dispose
skPicture.delete();
}
@override

View File

@ -2,7 +2,6 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
part of engine;
class SkPictureRecorder implements ui.PictureRecorder {
@ -29,7 +28,8 @@ class SkPictureRecorder implements ui.PictureRecorder {
_recorder!.callMethod('finishRecordingAsPicture');
_recorder!.callMethod('delete');
_recorder = null;
return SkPicture(skPicture, _cullRect);
return SkPicture(OneShotSkiaObject(skPicture), _cullRect);
}
@override

View File

@ -0,0 +1,279 @@
// 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.
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();
}
}
}
/// An object backed by a [js.JsObject] mapped onto a Skia C++ object in the
/// WebAssembly heap.
///
/// These objects are automatically deleted when no longer used.
abstract class SkiaObject {
/// The JavaScript object that's mapped onto a Skia C++ object in the WebAssembly heap.
js.JsObject? get skiaObject;
/// Deletes the associated C++ object from the WebAssembly heap.
void delete();
}
/// A [SkiaObject] that can resurrect 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.
///
/// The lifecycle of a 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
/// defined by Flutter; Skia defaults are corrected if necessary). The
/// default object is created by [createDefault].
/// - 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 ResurrectableSkiaObject extends SkiaObject {
ResurrectableSkiaObject() {
_skiaObject = createDefault();
if (isResurrectionExpensive) {
SkiaObjects.manageExpensive(this);
} else {
SkiaObjects.manageResurrectable(this);
}
}
@override
js.JsObject get skiaObject {
if (_skiaObject == null) {
_skiaObject = resurrect();
if (isResurrectionExpensive) {
SkiaObjects.manageExpensive(this);
} else {
SkiaObjects.manageResurrectable(this);
}
}
return _skiaObject!;
}
/// Do not use this field outside this class. Use [skiaObject] instead.
js.JsObject? _skiaObject;
/// 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.
js.JsObject createDefault();
/// Creates a new Skia-backed JavaScript object containing data representing
/// the current state of the Dart object.
js.JsObject resurrect();
/// Whether or not it is expensive to resurrect this object.
///
/// Defaults to false.
bool get isResurrectionExpensive => false;
@override
void delete() {
_skiaObject!.callMethod('delete');
_skiaObject = null;
}
}
// 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.
class OneShotSkiaObject extends SkiaObject {
js.JsObject? _skiaObject;
OneShotSkiaObject(this._skiaObject) {
SkiaObjects.manageOneShot(this);
}
@override
js.JsObject? get skiaObject {
if (_skiaObject == null) {
throw StateError('Attempting to use a Skia object that has been freed.');
}
SkiaObjects.oneShotCache.markUsed(this);
return _skiaObject;
}
@override
void delete() {
_skiaObject!.callMethod('delete');
_skiaObject = null;
}
}
/// 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>[];
@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;
}
window.rasterizer!.addPostFrameCallback(postFrameCleanUp);
_addedCleanupCallback = true;
}
/// Starts managing the lifecycle of resurrectable [object].
///
/// These can safely be deleted at any time.
static void manageResurrectable(ResurrectableSkiaObject 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(ResurrectableSkiaObject 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();
}
resurrectableObjects.clear();
for (int i = 0; i < cachesToResize.length; i++) {
final SkiaObjectCache cache = cachesToResize[i];
cache.resize();
}
cachesToResize.clear();
}
}

View File

@ -79,9 +79,8 @@ class Surface {
final SkSurface? currentSurface = _surface;
if (currentSurface != null) {
final bool isSameSize =
size.width == currentSurface.width() &&
size.height == currentSurface.height();
final bool isSameSize = size.width == currentSurface.width() &&
size.height == currentSurface.height();
if (isSameSize) {
// The existing surface is still reusable.
return;
@ -131,7 +130,7 @@ class Surface {
}
htmlElement = htmlCanvas;
return SkSurface(skSurface, glContext);
return SkSurface(skSurface, grContext, glContext);
}
bool _presentSurface() {
@ -144,9 +143,10 @@ class Surface {
/// A Dart wrapper around Skia's SkSurface.
class SkSurface {
final js.JsObject _surface;
final js.JsObject _grContext;
final int _glContext;
SkSurface(this._surface, this._glContext);
SkSurface(this._surface, this._grContext, this._glContext);
SkCanvas getCanvas() {
final js.JsObject skCanvas = _surface.callMethod('getCanvas');
@ -160,5 +160,7 @@ class SkSurface {
void dispose() {
_surface.callMethod('dispose');
_grContext.callMethod('releaseResourcesAndAbandonContext');
_grContext.callMethod('delete');
}
}

View File

@ -282,40 +282,90 @@ Map<String, js.JsObject?> toSkFontStyle(
return style;
}
class SkParagraph implements ui.Paragraph {
SkParagraph(this.skParagraph, this._textDirection, this._fontFamily);
class SkParagraph extends ResurrectableSkiaObject implements ui.Paragraph {
SkParagraph(
this._initialParagraph, this._paragraphStyle, this._paragraphCommands);
final js.JsObject? skParagraph;
final ui.TextDirection? _textDirection;
final String? _fontFamily;
/// The result of calling `build()` on the JS SkParagraphBuilder.
///
/// This may be invalidated later.
final js.JsObject _initialParagraph;
/// The paragraph style used to build this paragraph.
///
/// This is used to resurrect the paragraph if the initial paragraph
/// is deleted.
final SkParagraphStyle _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 layed the paragraph out.
///
/// This is used to resurrect the paragraph if the initial paragraph
/// is deleted.
ui.ParagraphConstraints? _lastLayoutConstraints;
@override
js.JsObject createDefault() => _initialParagraph;
@override
js.JsObject resurrect() {
final builder = SkParagraphBuilder(_paragraphStyle);
for (_ParagraphCommand command in _paragraphCommands) {
switch (command.type) {
case _ParagraphCommandType.addText:
builder.addText(command.text!);
break;
case _ParagraphCommandType.pop:
builder.pop();
break;
case _ParagraphCommandType.pushStyle:
builder.pushStyle(command.style!);
break;
}
}
final js.JsObject result = builder._buildSkParagraph();
if (_lastLayoutConstraints != null) {
// We need to set the Skia object early so layout works.
_skiaObject = result;
this.layout(_lastLayoutConstraints!);
}
return result;
}
@override
bool get isResurrectionExpensive => true;
@override
double get alphabeticBaseline =>
skParagraph!.callMethod('getAlphabeticBaseline');
skiaObject.callMethod('getAlphabeticBaseline');
@override
bool get didExceedMaxLines => skParagraph!.callMethod('didExceedMaxLines');
bool get didExceedMaxLines => skiaObject.callMethod('didExceedMaxLines');
@override
double get height => skParagraph!.callMethod('getHeight');
double get height => skiaObject.callMethod('getHeight');
@override
double get ideographicBaseline =>
skParagraph!.callMethod('getIdeographicBaseline');
skiaObject.callMethod('getIdeographicBaseline');
@override
double get longestLine => skParagraph!.callMethod('getLongestLine');
double get longestLine => skiaObject.callMethod('getLongestLine');
@override
double get maxIntrinsicWidth =>
skParagraph!.callMethod('getMaxIntrinsicWidth');
double get maxIntrinsicWidth => skiaObject.callMethod('getMaxIntrinsicWidth');
@override
double get minIntrinsicWidth =>
skParagraph!.callMethod('getMinIntrinsicWidth');
double get minIntrinsicWidth => skiaObject.callMethod('getMinIntrinsicWidth');
@override
double get width => skParagraph!.callMethod('getMaxWidth');
double get width => skiaObject.callMethod('getMaxWidth');
// TODO(hterkelsen): Implement placeholders once it's in CanvasKit
@override
@ -361,7 +411,7 @@ class SkParagraph implements ui.Paragraph {
}
List<js.JsObject> skRects =
skParagraph!.callMethod('getRectsForRange', <dynamic>[
skiaObject.callMethod('getRectsForRange', <dynamic>[
start,
end,
heightStyle,
@ -377,7 +427,7 @@ class SkParagraph implements ui.Paragraph {
rect['fTop'],
rect['fRight'],
rect['fBottom'],
_textDirection!,
_paragraphStyle._textDirection!,
));
}
@ -387,7 +437,7 @@ class SkParagraph implements ui.Paragraph {
@override
ui.TextPosition getPositionForOffset(ui.Offset offset) {
js.JsObject positionWithAffinity =
skParagraph!.callMethod('getGlyphPositionAtCoordinate', <double>[
skiaObject.callMethod('getGlyphPositionAtCoordinate', <double>[
offset.dx,
offset.dy,
]);
@ -397,13 +447,14 @@ class SkParagraph implements ui.Paragraph {
@override
ui.TextRange getWordBoundary(ui.TextPosition position) {
js.JsObject skRange =
skParagraph!.callMethod('getWordBoundary', <int>[position.offset]);
skiaObject.callMethod('getWordBoundary', <int>[position.offset]);
return ui.TextRange(start: skRange['start'], end: skRange['end']);
}
@override
void layout(ui.ParagraphConstraints constraints) {
assert(constraints.width != null); // ignore: unnecessary_null_comparison
_lastLayoutConstraints = constraints;
// Infinite width breaks layout, just use a very large number instead.
// TODO(het): Remove this once https://bugs.chromium.org/p/skia/issues/detail?id=9874
@ -418,10 +469,11 @@ class SkParagraph implements ui.Paragraph {
// TODO(het): CanvasKit throws an exception when laid out with
// a font that wasn't registered.
try {
skParagraph!.callMethod('layout', <double>[width]);
skiaObject.callMethod('layout', <double>[width]);
} catch (e) {
html.window.console.warn('CanvasKit threw an exception while laying '
'out the paragraph. The font was "$_fontFamily". Exception:\n$e');
'out the paragraph. The font was "${_paragraphStyle._fontFamily}". '
'Exception:\n$e');
rethrow;
}
}
@ -441,17 +493,16 @@ class SkParagraph implements ui.Paragraph {
class SkParagraphBuilder implements ui.ParagraphBuilder {
js.JsObject? _paragraphBuilder;
ui.TextDirection? _textDirection;
String? _fontFamily;
final SkParagraphStyle _style;
final List<_ParagraphCommand> _commands;
SkParagraphBuilder(ui.ParagraphStyle style) {
SkParagraphStyle skStyle = style as SkParagraphStyle;
_textDirection = skStyle._textDirection;
_fontFamily = skStyle._fontFamily;
SkParagraphBuilder(ui.ParagraphStyle style)
: _commands = <_ParagraphCommand>[],
_style = style as SkParagraphStyle {
_paragraphBuilder = canvasKit['ParagraphBuilder'].callMethod(
'Make',
<js.JsObject?>[
skStyle.skParagraphStyle,
_style.skParagraphStyle,
skiaFontCollection.skFontMgr,
],
);
@ -472,16 +523,22 @@ class SkParagraphBuilder implements ui.ParagraphBuilder {
@override
void addText(String text) {
_commands.add(_ParagraphCommand.addText(text));
_paragraphBuilder!.callMethod('addText', <String>[text]);
}
@override
ui.Paragraph build() {
final SkParagraph paragraph = SkParagraph(
_paragraphBuilder!.callMethod('build'), _textDirection, _fontFamily);
final builtParagraph = _buildSkParagraph();
return SkParagraph(builtParagraph, _style, _commands);
}
/// Builds the SkParagraph with the builder and deletes the builder.
js.JsObject _buildSkParagraph() {
final js.JsObject result = _paragraphBuilder!.callMethod('build');
_paragraphBuilder!.callMethod('delete');
_paragraphBuilder = null;
return paragraph;
return result;
}
@override
@ -493,13 +550,37 @@ class SkParagraphBuilder implements ui.ParagraphBuilder {
@override
void pop() {
_commands.add(const _ParagraphCommand.pop());
_paragraphBuilder!.callMethod('pop');
}
@override
void pushStyle(ui.TextStyle style) {
final SkTextStyle skStyle = style as SkTextStyle;
_commands.add(_ParagraphCommand.pushStyle(skStyle));
_paragraphBuilder!
.callMethod('pushStyle', <js.JsObject?>[skStyle.skTextStyle]);
}
}
class _ParagraphCommand {
final _ParagraphCommandType type;
final String? text;
final SkTextStyle? style;
const _ParagraphCommand._(this.type, this.text, this.style);
const _ParagraphCommand.addText(String text)
: this._(_ParagraphCommandType.addText, text, null);
const _ParagraphCommand.pop() : this._(_ParagraphCommandType.pop, null, null);
const _ParagraphCommand.pushStyle(SkTextStyle style)
: this._(_ParagraphCommandType.pushStyle, null, style);
}
enum _ParagraphCommandType {
addText,
pop,
pushStyle,
}

View File

@ -15,97 +15,6 @@ class CanvasKitError extends Error {
String toString() => 'CanvasKitError: $message';
}
/// An object backed by a [js.JsObject] mapped onto a Skia C++ object in the
/// WebAssembly heap.
///
/// These objects are automatically deleted when no longer used.
///
/// 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.
///
/// The lifecycle of a 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
/// defined by Flutter; Skia defaults are corrected if necessary). The
/// default object is created by [createDefault].
/// - 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 SkiaObject {
SkiaObject() {
_skiaObject = createDefault();
SkiaObjects.manage(this);
}
/// The JavaScript object that's mapped onto a Skia C++ object in the WebAssembly heap.
js.JsObject? get skiaObject {
if (_skiaObject == null) {
_skiaObject = resurrect();
SkiaObjects.manage(this);
}
return _skiaObject;
}
/// Do not use this field outside this class. Use [skiaObject] instead.
js.JsObject? _skiaObject;
/// 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.
js.JsObject createDefault();
/// Creates a new Skia-backed JavaScript object containing data representing
/// the current state of the Dart object.
js.JsObject resurrect();
}
/// 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<SkiaObject> managedObjects = () {
window.rasterizer!.addPostFrameCallback(postFrameCleanUp);
return <SkiaObject>[];
}();
/// Starts managing the lifecycle of [object].
///
/// The object's underlying WASM object is deleted by calling the
/// "delete" method when it goes out of scope.
///
/// The current implementation deletes objects at the end of every frame.
static void manage(SkiaObject object) {
managedObjects.add(object);
}
/// Deletes all C++ objects created this frame.
static void postFrameCleanUp() {
if (managedObjects.isEmpty) {
return;
}
for (int i = 0; i < managedObjects.length; i++) {
final SkiaObject object = managedObjects[i];
object._skiaObject!.callMethod('delete');
object._skiaObject = null;
}
managedObjects.clear();
}
}
/// Converts a list of [ui.Color] into the 2d array expected by CanvasKit.
js.JsArray<Float32List> makeColorList(List<ui.Color> colors) {
var result = js.JsArray<Float32List>();

View File

@ -0,0 +1,170 @@
// 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.6
import 'dart:js';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
void main() {
SkiaObjects.maximumCacheSize = 4;
group(ResurrectableSkiaObject, () {
test('implements create, cache, delete, resurrect, delete lifecycle', () {
int addPostFrameCallbackCount = 0;
MockRasterizer mockRasterizer = MockRasterizer();
when(mockRasterizer.addPostFrameCallback(any)).thenAnswer((_) {
addPostFrameCallbackCount++;
});
window.rasterizer = mockRasterizer;
// Trigger first create
final TestSkiaObject testObject = TestSkiaObject();
expect(SkiaObjects.resurrectableObjects.single, testObject);
expect(testObject.createDefaultCount, 1);
expect(testObject.resurrectCount, 0);
expect(testObject.deleteCount, 0);
// Check that the getter does not have side-effects
final JsObject skiaObject1 = testObject.skiaObject;
expect(skiaObject1, isNotNull);
expect(SkiaObjects.resurrectableObjects.single, testObject);
expect(testObject.createDefaultCount, 1);
expect(testObject.resurrectCount, 0);
expect(testObject.deleteCount, 0);
// Trigger first delete
SkiaObjects.postFrameCleanUp();
expect(SkiaObjects.resurrectableObjects, isEmpty);
expect(addPostFrameCallbackCount, 1);
expect(testObject.createDefaultCount, 1);
expect(testObject.resurrectCount, 0);
expect(testObject.deleteCount, 1);
// Trigger resurrect
final JsObject skiaObject2 = testObject.skiaObject;
expect(skiaObject2, isNotNull);
expect(skiaObject2, isNot(same(skiaObject1)));
expect(SkiaObjects.resurrectableObjects.single, testObject);
expect(addPostFrameCallbackCount, 1);
expect(testObject.createDefaultCount, 1);
expect(testObject.resurrectCount, 1);
expect(testObject.deleteCount, 1);
// Trigger final delete
SkiaObjects.postFrameCleanUp();
expect(SkiaObjects.resurrectableObjects, isEmpty);
expect(addPostFrameCallbackCount, 1);
expect(testObject.createDefaultCount, 1);
expect(testObject.resurrectCount, 1);
expect(testObject.deleteCount, 2);
});
test('is added to SkiaObjects cache if expensive', () {
TestSkiaObject object1 = TestSkiaObject(isExpensive: true);
expect(SkiaObjects.expensiveCache.length, 1);
expect(SkiaObjects.expensiveCache.debugContains(object1), isTrue);
TestSkiaObject object2 = TestSkiaObject(isExpensive: true);
expect(SkiaObjects.expensiveCache.length, 2);
expect(SkiaObjects.expensiveCache.debugContains(object2), isTrue);
SkiaObjects.postFrameCleanUp();
expect(SkiaObjects.expensiveCache.length, 2);
expect(SkiaObjects.expensiveCache.debugContains(object1), isTrue);
expect(SkiaObjects.expensiveCache.debugContains(object2), isTrue);
/// Add 3 more objects to the cache to overflow it.
TestSkiaObject(isExpensive: true);
TestSkiaObject(isExpensive: true);
TestSkiaObject(isExpensive: true);
expect(SkiaObjects.expensiveCache.length, 5);
expect(SkiaObjects.cachesToResize.length, 1);
SkiaObjects.postFrameCleanUp();
expect(object1.deleteCount, 1);
expect(object2.deleteCount, 1);
expect(SkiaObjects.expensiveCache.length, 3);
expect(SkiaObjects.expensiveCache.debugContains(object1), isFalse);
expect(SkiaObjects.expensiveCache.debugContains(object2), isFalse);
});
});
group(OneShotSkiaObject, () {
test('is added to SkiaObjects cache', () {
int deleteCount = 0;
JsObject _makeJsObject() {
return JsObject.jsify({
'delete': allowInterop(() {
deleteCount++;
}),
});
}
OneShotSkiaObject object1 = OneShotSkiaObject(_makeJsObject());
expect(SkiaObjects.oneShotCache.length, 1);
expect(SkiaObjects.oneShotCache.debugContains(object1), isTrue);
OneShotSkiaObject object2 = OneShotSkiaObject(_makeJsObject());
expect(SkiaObjects.oneShotCache.length, 2);
expect(SkiaObjects.oneShotCache.debugContains(object2), isTrue);
SkiaObjects.postFrameCleanUp();
expect(SkiaObjects.oneShotCache.length, 2);
expect(SkiaObjects.oneShotCache.debugContains(object1), isTrue);
expect(SkiaObjects.oneShotCache.debugContains(object2), isTrue);
// Add 3 more objects to the cache to overflow it.
OneShotSkiaObject(_makeJsObject());
OneShotSkiaObject(_makeJsObject());
OneShotSkiaObject(_makeJsObject());
expect(SkiaObjects.oneShotCache.length, 5);
expect(SkiaObjects.cachesToResize.length, 1);
SkiaObjects.postFrameCleanUp();
expect(deleteCount, 2);
expect(SkiaObjects.oneShotCache.length, 3);
expect(SkiaObjects.oneShotCache.debugContains(object1), isFalse);
expect(SkiaObjects.oneShotCache.debugContains(object2), isFalse);
});
});
}
class TestSkiaObject extends ResurrectableSkiaObject {
int createDefaultCount = 0;
int resurrectCount = 0;
int deleteCount = 0;
final bool isExpensive;
TestSkiaObject({this.isExpensive = false});
JsObject _makeJsObject() {
return JsObject.jsify({
'delete': allowInterop(() {
deleteCount++;
}),
});
}
@override
JsObject createDefault() {
createDefaultCount++;
return _makeJsObject();
}
@override
JsObject resurrect() {
resurrectCount++;
return _makeJsObject();
}
@override
bool get isResurrectionExpensive => isExpensive;
}
class MockRasterizer extends Mock implements Rasterizer {}

View File

@ -1,94 +0,0 @@
// 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.6
import 'dart:js';
import 'package:mockito/mockito.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
void main() {
group(SkiaObject, () {
test('implements create, cache, delete, resurrect, delete lifecycle', () {
int addPostFrameCallbackCount = 0;
MockRasterizer mockRasterizer = MockRasterizer();
when(mockRasterizer.addPostFrameCallback(any)).thenAnswer((_) {
addPostFrameCallbackCount++;
});
window.rasterizer = mockRasterizer;
// Trigger first create
final TestSkiaObject testObject = TestSkiaObject();
expect(SkiaObjects.managedObjects.single, testObject);
expect(testObject.createDefaultCount, 1);
expect(testObject.resurrectCount, 0);
expect(testObject.deleteCount, 0);
// Check that the getter does not have side-effects
final JsObject skiaObject1 = testObject.skiaObject;
expect(skiaObject1, isNotNull);
expect(SkiaObjects.managedObjects.single, testObject);
expect(testObject.createDefaultCount, 1);
expect(testObject.resurrectCount, 0);
expect(testObject.deleteCount, 0);
// Trigger first delete
SkiaObjects.postFrameCleanUp();
expect(SkiaObjects.managedObjects, isEmpty);
expect(addPostFrameCallbackCount, 1);
expect(testObject.createDefaultCount, 1);
expect(testObject.resurrectCount, 0);
expect(testObject.deleteCount, 1);
// Trigger resurrect
final JsObject skiaObject2 = testObject.skiaObject;
expect(skiaObject2, isNotNull);
expect(skiaObject2, isNot(same(skiaObject1)));
expect(SkiaObjects.managedObjects.single, testObject);
expect(addPostFrameCallbackCount, 1);
expect(testObject.createDefaultCount, 1);
expect(testObject.resurrectCount, 1);
expect(testObject.deleteCount, 1);
// Trigger final delete
SkiaObjects.postFrameCleanUp();
expect(SkiaObjects.managedObjects, isEmpty);
expect(addPostFrameCallbackCount, 1);
expect(testObject.createDefaultCount, 1);
expect(testObject.resurrectCount, 1);
expect(testObject.deleteCount, 2);
});
});
}
class TestSkiaObject extends SkiaObject {
int createDefaultCount = 0;
int resurrectCount = 0;
int deleteCount = 0;
JsObject _makeJsObject() {
return JsObject.jsify({
'delete': allowInterop(() {
deleteCount++;
}),
});
}
@override
JsObject createDefault() {
createDefaultCount++;
return _makeJsObject();
}
@override
JsObject resurrect() {
resurrectCount++;
return _makeJsObject();
}
}
class MockRasterizer extends Mock implements Rasterizer {}