[web:canvaskit] clean up the rest of skia_object_cache usages (flutter/engine#41259)

This should go after https://github.com/flutter/engine/pull/41230.

Removes the remaining usages of `skia_object_cache.dart` and deletes it.

Also fixes https://github.com/flutter/flutter/issues/86632
This commit is contained in:
Yegor
2023-04-18 14:34:01 -07:00
committed by GitHub
parent 60494f6f45
commit 1289bfa247
18 changed files with 49 additions and 1941 deletions

View File

@@ -1891,7 +1891,6 @@ ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/raster_cache.dart +
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/shader.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/skia_object_cache.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/surface.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/surface_factory.dart + ../../../flutter/LICENSE
ORIGIN: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/text.dart + ../../../flutter/LICENSE
@@ -4487,7 +4486,6 @@ FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/raster_cache.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/rasterizer.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/renderer.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/shader.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/skia_object_cache.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/surface.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/surface_factory.dart
FILE: ../../../flutter/lib/web_ui/lib/src/engine/canvaskit/text.dart

View File

@@ -50,7 +50,6 @@ export 'engine/canvaskit/raster_cache.dart';
export 'engine/canvaskit/rasterizer.dart';
export 'engine/canvaskit/renderer.dart';
export 'engine/canvaskit/shader.dart';
export 'engine/canvaskit/skia_object_cache.dart';
export 'engine/canvaskit/surface.dart';
export 'engine/canvaskit/surface_factory.dart';
export 'engine/canvaskit/text.dart';

View File

@@ -333,855 +333,4 @@ class CkCanvas {
}
return matrix4;
}
CkPictureSnapshot? get pictureSnapshot => null;
}
class RecordingCkCanvas extends CkCanvas {
RecordingCkCanvas(super.skCanvas, ui.Rect bounds)
: pictureSnapshot = CkPictureSnapshot(bounds);
@override
final CkPictureSnapshot pictureSnapshot;
void _addCommand(CkPaintCommand command) {
pictureSnapshot._commands.add(command);
}
@override
void clear(ui.Color color) {
super.clear(color);
_addCommand(CkClearCommand(color));
}
@override
void clipPath(CkPath path, bool doAntiAlias) {
super.clipPath(path, doAntiAlias);
_addCommand(CkClipPathCommand(path, doAntiAlias));
}
@override
void clipRRect(ui.RRect rrect, bool doAntiAlias) {
super.clipRRect(rrect, doAntiAlias);
_addCommand(CkClipRRectCommand(rrect, doAntiAlias));
}
@override
void clipRect(ui.Rect rect, ui.ClipOp clipOp, bool doAntiAlias) {
super.clipRect(rect, clipOp, doAntiAlias);
_addCommand(CkClipRectCommand(rect, clipOp, doAntiAlias));
}
@override
void drawArc(
ui.Rect oval,
double startAngle,
double sweepAngle,
bool useCenter,
CkPaint paint,
) {
super.drawArc(oval, startAngle, sweepAngle, useCenter, paint);
_addCommand(
CkDrawArcCommand(oval, startAngle, sweepAngle, useCenter, paint));
}
@override
void drawAtlasRaw(
CkPaint paint,
CkImage atlas,
Float32List rstTransforms,
Float32List rects,
Uint32List? colors,
ui.BlendMode blendMode,
) {
super.drawAtlasRaw(paint, atlas, rstTransforms, rects, colors, blendMode);
_addCommand(CkDrawAtlasCommand(
paint, atlas, rstTransforms, rects, colors, blendMode));
}
@override
void drawCircle(ui.Offset c, double radius, CkPaint paint) {
super.drawCircle(c, radius, paint);
_addCommand(CkDrawCircleCommand(c, radius, paint));
}
@override
void drawColor(ui.Color color, ui.BlendMode blendMode) {
super.drawColor(color, blendMode);
_addCommand(CkDrawColorCommand(color, blendMode));
}
@override
void drawDRRect(ui.RRect outer, ui.RRect inner, CkPaint paint) {
super.drawDRRect(outer, inner, paint);
_addCommand(CkDrawDRRectCommand(outer, inner, paint));
}
@override
void drawImage(CkImage image, ui.Offset offset, CkPaint paint) {
super.drawImage(image, offset, paint);
_addCommand(CkDrawImageCommand(image, offset, paint));
}
@override
void drawImageRect(CkImage image, ui.Rect src, ui.Rect dst, CkPaint paint) {
super.drawImageRect(image, src, dst, paint);
_addCommand(CkDrawImageRectCommand(image, src, dst, paint));
}
@override
void drawImageNine(
CkImage image, ui.Rect center, ui.Rect dst, CkPaint paint) {
super.drawImageNine(image, center, dst, paint);
_addCommand(CkDrawImageNineCommand(image, center, dst, paint));
}
@override
void drawLine(ui.Offset p1, ui.Offset p2, CkPaint paint) {
super.drawLine(p1, p2, paint);
_addCommand(CkDrawLineCommand(p1, p2, paint));
}
@override
void drawOval(ui.Rect rect, CkPaint paint) {
super.drawOval(rect, paint);
_addCommand(CkDrawOvalCommand(rect, paint));
}
@override
void drawPaint(CkPaint paint) {
super.drawPaint(paint);
_addCommand(CkDrawPaintCommand(paint));
}
@override
void drawParagraph(CkParagraph paragraph, ui.Offset offset) {
super.drawParagraph(paragraph, offset);
_addCommand(CkDrawParagraphCommand(paragraph, offset));
}
@override
void drawPath(CkPath path, CkPaint paint) {
super.drawPath(path, paint);
_addCommand(CkDrawPathCommand(path, paint));
}
@override
void drawPicture(CkPicture picture) {
super.drawPicture(picture);
_addCommand(CkDrawPictureCommand(picture));
}
@override
void drawPoints(CkPaint paint, ui.PointMode pointMode, Float32List points) {
super.drawPoints(paint, pointMode, points);
_addCommand(CkDrawPointsCommand(pointMode, points, paint));
}
@override
void drawRRect(ui.RRect rrect, CkPaint paint) {
super.drawRRect(rrect, paint);
_addCommand(CkDrawRRectCommand(rrect, paint));
}
@override
void drawRect(ui.Rect rect, CkPaint paint) {
super.drawRect(rect, paint);
_addCommand(CkDrawRectCommand(rect, paint));
}
@override
void drawShadow(
CkPath path, ui.Color color, double elevation, bool transparentOccluder) {
super.drawShadow(path, color, elevation, transparentOccluder);
_addCommand(
CkDrawShadowCommand(path, color, elevation, transparentOccluder));
}
@override
void drawVertices(
CkVertices vertices, ui.BlendMode blendMode, CkPaint paint) {
super.drawVertices(vertices, blendMode, paint);
_addCommand(CkDrawVerticesCommand(vertices, blendMode, paint));
}
@override
void restore() {
super.restore();
_addCommand(const CkRestoreCommand());
}
@override
void restoreToCount(int count) {
super.restoreToCount(count);
_addCommand(CkRestoreToCountCommand(count));
}
@override
void rotate(double radians) {
super.rotate(radians);
_addCommand(CkRotateCommand(radians));
}
@override
int save() {
_addCommand(const CkSaveCommand());
return super.save();
}
@override
void saveLayer(ui.Rect bounds, CkPaint? paint) {
super.saveLayer(bounds, paint);
_addCommand(CkSaveLayerCommand(bounds, paint));
}
@override
void saveLayerWithoutBounds(CkPaint? paint) {
super.saveLayerWithoutBounds(paint);
_addCommand(CkSaveLayerWithoutBoundsCommand(paint));
}
@override
void saveLayerWithFilter(ui.Rect bounds, ui.ImageFilter filter,
[CkPaint? paint]) {
super.saveLayerWithFilter(bounds, filter, paint);
_addCommand(CkSaveLayerWithFilterCommand(bounds, filter, paint));
}
@override
void scale(double sx, double sy) {
super.scale(sx, sy);
_addCommand(CkScaleCommand(sx, sy));
}
@override
void skew(double sx, double sy) {
super.skew(sx, sy);
_addCommand(CkSkewCommand(sx, sy));
}
@override
void transform(Float32List matrix4) {
super.transform(matrix4);
_addCommand(CkTransformCommand(matrix4));
}
@override
void translate(double dx, double dy) {
super.translate(dx, dy);
_addCommand(CkTranslateCommand(dx, dy));
}
}
class CkPictureSnapshot {
CkPictureSnapshot(this._bounds);
final ui.Rect _bounds;
final List<CkPaintCommand> _commands = <CkPaintCommand>[];
SkPicture toPicture() {
final SkPictureRecorder recorder = SkPictureRecorder();
final Float32List skRect = toSkRect(_bounds);
final SkCanvas skCanvas = recorder.beginRecording(skRect);
for (final CkPaintCommand command in _commands) {
command.apply(skCanvas);
}
final SkPicture skPicture = recorder.finishRecordingAsPicture();
recorder.delete();
return skPicture;
}
void dispose() {
for (final CkPaintCommand command in _commands) {
command.dispose();
}
}
}
/// A paint command recorded by [RecordingCkCanvas].
///
/// # Special rules when drawing images
///
/// A command painting an image must clone the original image to bump the ref
/// count. Otherwise when the framework decides it doesn't need the image any
/// more it will bump the ref count down and delete the underlying Skia object,
/// leaving the picture that recorded this paint command with a dangling
/// pointer. If we attempt to resurrect the picture we'll hit a use-after-free
/// error. The command must call [CkImage.dispose] in its [dispose]
/// implementation.
abstract class CkPaintCommand {
const CkPaintCommand();
/// Applies the command onto the [canvas].
void apply(SkCanvas canvas);
/// Frees resources associated with the command.
void dispose() {}
}
class CkClearCommand extends CkPaintCommand {
const CkClearCommand(this.color);
final ui.Color color;
@override
void apply(SkCanvas canvas) {
canvas.clear(toSharedSkColor1(color));
}
}
class CkSaveCommand extends CkPaintCommand {
const CkSaveCommand();
@override
void apply(SkCanvas canvas) {
canvas.save();
}
}
class CkRestoreCommand extends CkPaintCommand {
const CkRestoreCommand();
@override
void apply(SkCanvas canvas) {
canvas.restore();
}
}
class CkRestoreToCountCommand extends CkPaintCommand {
const CkRestoreToCountCommand(this.count);
final int count;
@override
void apply(SkCanvas canvas) {
canvas.restoreToCount(count.toDouble());
}
}
class CkTranslateCommand extends CkPaintCommand {
CkTranslateCommand(this.dx, this.dy);
final double dx;
final double dy;
@override
void apply(SkCanvas canvas) {
canvas.translate(dx, dy);
}
}
class CkScaleCommand extends CkPaintCommand {
CkScaleCommand(this.sx, this.sy);
final double sx;
final double sy;
@override
void apply(SkCanvas canvas) {
canvas.scale(sx, sy);
}
}
class CkRotateCommand extends CkPaintCommand {
CkRotateCommand(this.radians);
final double radians;
@override
void apply(SkCanvas canvas) {
canvas.rotate(radians * 180.0 / math.pi, 0.0, 0.0);
}
}
class CkTransformCommand extends CkPaintCommand {
CkTransformCommand(this.matrix4);
final Float32List matrix4;
@override
void apply(SkCanvas canvas) {
canvas.concat(toSkM44FromFloat32(matrix4));
}
}
class CkSkewCommand extends CkPaintCommand {
CkSkewCommand(this.sx, this.sy);
final double sx;
final double sy;
@override
void apply(SkCanvas canvas) {
canvas.skew(sx, sy);
}
}
class CkClipRectCommand extends CkPaintCommand {
CkClipRectCommand(this.rect, this.clipOp, this.doAntiAlias);
final ui.Rect rect;
final ui.ClipOp clipOp;
final bool doAntiAlias;
@override
void apply(SkCanvas canvas) {
canvas.clipRect(
toSkRect(rect),
toSkClipOp(clipOp),
doAntiAlias,
);
}
}
class CkDrawArcCommand extends CkPaintCommand {
CkDrawArcCommand(
this.oval, this.startAngle, this.sweepAngle, this.useCenter, this.paint);
final ui.Rect oval;
final double startAngle;
final double sweepAngle;
final bool useCenter;
final CkPaint paint;
@override
void apply(SkCanvas canvas) {
const double toDegrees = 180 / math.pi;
canvas.drawArc(
toSkRect(oval),
startAngle * toDegrees,
sweepAngle * toDegrees,
useCenter,
paint.skiaObject,
);
}
}
class CkDrawAtlasCommand extends CkPaintCommand {
CkDrawAtlasCommand(this.paint, this.atlas, this.rstTransforms, this.rects,
this.colors, this.blendMode);
final CkPaint paint;
final CkImage atlas;
final Float32List rstTransforms;
final Float32List rects;
final Uint32List? colors;
final ui.BlendMode blendMode;
@override
void apply(SkCanvas canvas) {
canvas.drawAtlas(
atlas.skImage,
rects,
rstTransforms,
paint.skiaObject,
toSkBlendMode(blendMode),
colors,
);
}
}
class CkClipRRectCommand extends CkPaintCommand {
CkClipRRectCommand(this.rrect, this.doAntiAlias);
final ui.RRect rrect;
final bool doAntiAlias;
@override
void apply(SkCanvas canvas) {
canvas.clipRRect(
toSkRRect(rrect),
_clipOpIntersect,
doAntiAlias,
);
}
}
class CkClipPathCommand extends CkPaintCommand {
CkClipPathCommand(this.path, this.doAntiAlias);
final CkPath path;
final bool doAntiAlias;
@override
void apply(SkCanvas canvas) {
canvas.clipPath(
path.skiaObject,
_clipOpIntersect,
doAntiAlias,
);
}
}
class CkDrawColorCommand extends CkPaintCommand {
CkDrawColorCommand(this.color, this.blendMode);
final ui.Color color;
final ui.BlendMode blendMode;
@override
void apply(SkCanvas canvas) {
canvas.drawColorInt(
color.value.toDouble(),
toSkBlendMode(blendMode),
);
}
}
class CkDrawLineCommand extends CkPaintCommand {
CkDrawLineCommand(this.p1, this.p2, this.paint);
final ui.Offset p1;
final ui.Offset p2;
final CkPaint paint;
@override
void apply(SkCanvas canvas) {
canvas.drawLine(
p1.dx,
p1.dy,
p2.dx,
p2.dy,
paint.skiaObject,
);
}
}
class CkDrawPaintCommand extends CkPaintCommand {
CkDrawPaintCommand(this.paint);
final CkPaint paint;
@override
void apply(SkCanvas canvas) {
canvas.drawPaint(paint.skiaObject);
}
}
class CkDrawVerticesCommand extends CkPaintCommand {
CkDrawVerticesCommand(this.vertices, this.blendMode, this.paint);
final CkVertices vertices;
final ui.BlendMode blendMode;
final CkPaint paint;
@override
void apply(SkCanvas canvas) {
canvas.drawVertices(
vertices.skiaObject,
toSkBlendMode(blendMode),
paint.skiaObject,
);
}
}
class CkDrawPointsCommand extends CkPaintCommand {
CkDrawPointsCommand(this.pointMode, this.points, this.paint);
final Float32List points;
final ui.PointMode pointMode;
final CkPaint paint;
@override
void apply(SkCanvas canvas) {
canvas.drawPoints(
toSkPointMode(pointMode),
points,
paint.skiaObject,
);
}
}
class CkDrawRectCommand extends CkPaintCommand {
CkDrawRectCommand(this.rect, this.paint);
final ui.Rect rect;
final CkPaint paint;
@override
void apply(SkCanvas canvas) {
canvas.drawRect(toSkRect(rect), paint.skiaObject);
}
}
class CkDrawRRectCommand extends CkPaintCommand {
CkDrawRRectCommand(this.rrect, this.paint);
final ui.RRect rrect;
final CkPaint paint;
@override
void apply(SkCanvas canvas) {
canvas.drawRRect(
toSkRRect(rrect),
paint.skiaObject,
);
}
}
class CkDrawDRRectCommand extends CkPaintCommand {
CkDrawDRRectCommand(this.outer, this.inner, this.paint);
final ui.RRect outer;
final ui.RRect inner;
final CkPaint paint;
@override
void apply(SkCanvas canvas) {
canvas.drawDRRect(
toSkRRect(outer),
toSkRRect(inner),
paint.skiaObject,
);
}
}
class CkDrawOvalCommand extends CkPaintCommand {
CkDrawOvalCommand(this.rect, this.paint);
final ui.Rect rect;
final CkPaint paint;
@override
void apply(SkCanvas canvas) {
canvas.drawOval(
toSkRect(rect),
paint.skiaObject,
);
}
}
class CkDrawCircleCommand extends CkPaintCommand {
CkDrawCircleCommand(this.c, this.radius, this.paint);
final ui.Offset c;
final double radius;
final CkPaint paint;
@override
void apply(SkCanvas canvas) {
canvas.drawCircle(
c.dx,
c.dy,
radius,
paint.skiaObject,
);
}
}
class CkDrawPathCommand extends CkPaintCommand {
CkDrawPathCommand(this.path, this.paint);
final CkPath path;
final CkPaint paint;
@override
void apply(SkCanvas canvas) {
canvas.drawPath(path.skiaObject, paint.skiaObject);
}
}
class CkDrawShadowCommand extends CkPaintCommand {
CkDrawShadowCommand(
this.path, this.color, this.elevation, this.transparentOccluder);
final CkPath path;
final ui.Color color;
final double elevation;
final bool transparentOccluder;
@override
void apply(SkCanvas canvas) {
drawSkShadow(canvas, path, color, elevation, transparentOccluder,
ui.window.devicePixelRatio);
}
}
class CkDrawImageCommand extends CkPaintCommand {
CkDrawImageCommand(CkImage ckImage, this.offset, this.paint)
: image = ckImage.clone();
final CkImage image;
final ui.Offset offset;
final CkPaint paint;
@override
void apply(SkCanvas canvas) {
final ui.FilterQuality filterQuality = paint.filterQuality;
if (filterQuality == ui.FilterQuality.high) {
canvas.drawImageCubic(
image.skImage,
offset.dx,
offset.dy,
CkCanvas._kMitchellNetravali_B,
CkCanvas._kMitchellNetravali_C,
paint.skiaObject,
);
} else {
canvas.drawImageOptions(
image.skImage,
offset.dx,
offset.dy,
toSkFilterMode(filterQuality),
toSkMipmapMode(filterQuality),
paint.skiaObject,
);
}
}
@override
void dispose() {
image.dispose();
}
}
class CkDrawImageRectCommand extends CkPaintCommand {
CkDrawImageRectCommand(CkImage ckImage, this.src, this.dst, this.paint)
: image = ckImage.clone();
final CkImage image;
final ui.Rect src;
final ui.Rect dst;
final CkPaint paint;
@override
void apply(SkCanvas canvas) {
final ui.FilterQuality filterQuality = paint.filterQuality;
if (filterQuality == ui.FilterQuality.high) {
canvas.drawImageRectCubic(
image.skImage,
toSkRect(src),
toSkRect(dst),
CkCanvas._kMitchellNetravali_B,
CkCanvas._kMitchellNetravali_C,
paint.skiaObject,
);
} else {
canvas.drawImageRectOptions(
image.skImage,
toSkRect(src),
toSkRect(dst),
toSkFilterMode(filterQuality),
toSkMipmapMode(filterQuality),
paint.skiaObject,
);
}
}
@override
void dispose() {
image.dispose();
}
}
class CkDrawImageNineCommand extends CkPaintCommand {
CkDrawImageNineCommand(CkImage ckImage, this.center, this.dst, this.paint)
: image = ckImage.clone();
final CkImage image;
final ui.Rect center;
final ui.Rect dst;
final CkPaint paint;
@override
void apply(SkCanvas canvas) {
canvas.drawImageNine(
image.skImage,
toSkRect(center),
toSkRect(dst),
toSkFilterMode(paint.filterQuality),
paint.skiaObject,
);
}
@override
void dispose() {
image.dispose();
}
}
class CkDrawParagraphCommand extends CkPaintCommand {
CkDrawParagraphCommand(this.paragraph, this.offset);
final CkParagraph paragraph;
final ui.Offset offset;
@override
void apply(SkCanvas canvas) {
canvas.drawParagraph(
paragraph.skiaObject,
offset.dx,
offset.dy,
);
}
}
class CkDrawPictureCommand extends CkPaintCommand {
CkDrawPictureCommand(this.picture);
final CkPicture picture;
@override
void apply(SkCanvas canvas) {
canvas.drawPicture(picture.skiaObject);
}
}
class CkSaveLayerCommand extends CkPaintCommand {
CkSaveLayerCommand(this.bounds, this.paint);
final ui.Rect bounds;
final CkPaint? paint;
@override
void apply(SkCanvas canvas) {
canvas.saveLayer(
paint?.skiaObject,
toSkRect(bounds),
null,
null,
);
}
}
class CkSaveLayerWithoutBoundsCommand extends CkPaintCommand {
CkSaveLayerWithoutBoundsCommand(this.paint);
final CkPaint? paint;
@override
void apply(SkCanvas canvas) {
canvas.saveLayer(
paint?.skiaObject,
null,
null,
null,
);
}
}
class CkSaveLayerWithFilterCommand extends CkPaintCommand {
CkSaveLayerWithFilterCommand(this.bounds, this.filter, this.paint);
final ui.Rect bounds;
final ui.ImageFilter filter;
final CkPaint? paint;
@override
void apply(SkCanvas canvas) {
final CkManagedSkImageFilterConvertible convertible;
if (filter is ui.ColorFilter) {
convertible = createCkColorFilter(filter as EngineColorFilter)!;
} else {
convertible = filter as CkManagedSkImageFilterConvertible;
}
convertible.imageFilter((SkImageFilter filter) {
canvas.saveLayer(
paint?.skiaObject,
toSkRect(bounds),
filter,
0,
);
});
}
}

View File

@@ -23,7 +23,6 @@ import 'package:ui/ui.dart' as ui;
import '../browser_detection.dart';
import '../configuration.dart';
import '../dom.dart';
import '../profiler.dart';
import 'renderer.dart';
/// Entrypoint into the CanvasKit API.
@@ -3350,172 +3349,6 @@ extension SkTypefaceFactoryExtension on SkTypefaceFactory {
_MakeFreeTypeFaceFromData(fontData.toJS);
}
/// Collects Skia objects that are no longer necessary.
abstract class Collector {
/// The production collector implementation.
static final Collector _productionInstance = ProductionCollector();
/// The collector implementation currently in use.
static Collector get instance => _instance;
static Collector _instance = _productionInstance;
/// In tests overrides the collector implementation.
static void debugOverrideCollector(Collector override) {
_instance = override;
}
/// In tests restores the collector to the production implementation.
static void debugRestoreCollector() {
_instance = _productionInstance;
}
/// Registers a [deletable] for collection when the [wrapper] object is
/// garbage collected.
///
/// The [debugLabel] is used to track the origin of the deletable.
void register(Object wrapper, SkDeletable deletable);
/// Deletes the [deletable].
///
/// The exact timing of the deletion is implementation-specific. For example,
/// a production implementation may want to batch deletables and schedule a
/// timer to collect them instead of deleting right away.
///
/// A test implementation may want a collection strategy that's less efficient
/// but more predictable.
void collect(SkDeletable deletable);
}
/// Uses the browser's real `FinalizationRegistry` to collect objects.
///
/// Uses timers to delete objects in batches and outside the animation frame.
class ProductionCollector implements Collector {
ProductionCollector() {
_skObjectFinalizationRegistry =
createSkObjectFinalizationRegistry((SkDeletable deletable) {
// This is called when GC decides to collect the wrapper object and
// notify us, which may happen after the object is already deleted
// explicitly, e.g. when its ref count drops to zero. When that happens
// skip collection of this object.
if (!deletable.isDeleted()) {
collect(deletable);
}
}.toJS);
}
late final SkObjectFinalizationRegistry _skObjectFinalizationRegistry;
List<SkDeletable> _skiaObjectCollectionQueue = <SkDeletable>[];
Timer? _skiaObjectCollectionTimer;
@override
void register(Object wrapper, SkDeletable deletable) {
if (Instrumentation.enabled) {
Instrumentation.instance.incrementCounter(
'${deletable.constructor.name} registered',
);
}
_skObjectFinalizationRegistry.register(wrapper, deletable);
}
/// Schedules a Skia object for deletion in an asap timer.
///
/// A timer is used 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.
///
/// Because scheduling a timer is expensive, the timer is shared by all objects
/// deleted this frame. No timer is created if no objects were scheduled for
/// deletion.
@override
void collect(SkDeletable deletable) {
assert(
!deletable.isDeleted(),
'Attempted to delete an already deleted Skia object.',
);
_skiaObjectCollectionQueue.add(deletable);
_skiaObjectCollectionTimer ??= Timer(Duration.zero, () {
// Null out the timer so we can schedule a new one next time objects are
// scheduled for deletion.
_skiaObjectCollectionTimer = null;
collectSkiaObjectsNow();
});
}
/// Deletes all Skia objects pending deletion synchronously.
///
/// After calling this method [_skiaObjectCollectionQueue] is empty.
///
/// Throws a [SkiaObjectCollectionError] if CanvasKit fails to delete at least
/// one object. The error is populated with information about the first failed
/// object. Upon an error the collection continues and the collection queue is
/// emptied out to prevent memory leaks. This may happen, for example, when the
/// same object is deleted more than once.
void collectSkiaObjectsNow() {
domWindow.performance.mark('SkObject collection-start');
final int length = _skiaObjectCollectionQueue.length;
dynamic firstError;
StackTrace? firstStackTrace;
for (int i = 0; i < length; i++) {
final SkDeletable deletable = _skiaObjectCollectionQueue[i];
if (deletable.isDeleted()) {
// Some Skia objects are ref counted and are deleted before GC and/or
// the collection timer begins collecting them. So we have to check
// again if the objects is worth collecting.
continue;
}
if (Instrumentation.enabled) {
Instrumentation.instance.incrementCounter(
'${deletable.constructor.name} deleted',
);
}
try {
deletable.delete();
} catch (error, stackTrace) {
// Remember the error, but keep going. If for some reason CanvasKit fails
// to delete an object we still want to delete other objects and empty
// out the queue. Otherwise, the queue will never be flushed and keep
// accumulating objects, a.k.a. memory leak.
if (firstError == null) {
firstError = error;
firstStackTrace = stackTrace;
}
}
}
_skiaObjectCollectionQueue = <SkDeletable>[];
domWindow.performance.mark('SkObject collection-end');
domWindow.performance.measure('SkObject collection',
'SkObject collection-start', 'SkObject collection-end');
// It's safe to throw the error here, now that we've processed the queue.
if (firstError != null) {
throw SkiaObjectCollectionError(firstError, firstStackTrace);
}
}
}
/// Thrown by [ProductionCollector] when Skia object collection fails.
class SkiaObjectCollectionError implements Error {
SkiaObjectCollectionError(this.error, this.stackTrace);
final dynamic error;
@override
final StackTrace? stackTrace;
@override
String toString() => 'SkiaObjectCollectionError: $error\n$stackTrace';
}
/// Any Skia object that has a `delete` method.
@JS()
@anonymous
@@ -3591,12 +3424,6 @@ external JSAny? get _finalizationRegistryConstructor;
bool browserSupportsFinalizationRegistry =
_finalizationRegistryConstructor != null;
/// Sets the value of [browserSupportsFinalizationRegistry] to its true value.
void debugResetBrowserSupportsFinalizationRegistry() {
browserSupportsFinalizationRegistry =
_finalizationRegistryConstructor != null;
}
@JS()
@staticInterop
class SkData {}

View File

@@ -17,29 +17,28 @@ import 'package:ui/ui.dart' as ui;
import '../util.dart';
import 'canvaskit_api.dart';
import 'image.dart';
import 'skia_object_cache.dart';
import 'native_memory.dart';
/// The CanvasKit implementation of [ui.Codec].
///
/// Wraps `SkAnimatedImage`.
class CkAnimatedImage extends ManagedSkiaObject<SkAnimatedImage>
implements ui.Codec {
class CkAnimatedImage implements ui.Codec {
/// Decodes an image from a list of encoded bytes.
CkAnimatedImage.decodeFromBytes(this._bytes, this.src, {this.targetWidth, this.targetHeight});
CkAnimatedImage.decodeFromBytes(this._bytes, this.src, {this.targetWidth, this.targetHeight}) {
final SkAnimatedImage skAnimatedImage = createSkAnimatedImage();
_ref = UniqueRef<SkAnimatedImage>(this, skAnimatedImage, 'Codec');
}
late final UniqueRef<SkAnimatedImage> _ref;
final String src;
final Uint8List _bytes;
int _frameCount = 0;
int _repetitionCount = -1;
/// Current frame index.
int _currentFrameIndex = 0;
final int? targetWidth;
final int? targetHeight;
@override
SkAnimatedImage createDefault() {
SkAnimatedImage createSkAnimatedImage() {
SkAnimatedImage? animatedImage =
canvasKit.MakeAnimatedImageFromEncoded(_bytes);
if (animatedImage == null) {
@@ -66,16 +65,6 @@ class CkAnimatedImage extends ManagedSkiaObject<SkAnimatedImage>
_frameCount = animatedImage.getFrameCount().toInt();
_repetitionCount = animatedImage.getRepetitionCount().toInt();
// Normally CanvasKit initializes `SkAnimatedImage` to point to the first
// frame in the animation. However, if the Skia object has been deleted then
// resurrected, the framework/app may already have advanced to one of the
// subsequent frames. When that happens the value of _currentFrameIndex will
// be something other than zero, and we need to tell the decoder to skip
// over the previous frames to point to the current one.
for (int i = 0; i < _currentFrameIndex; i++) {
animatedImage.decodeNextFrame();
}
return animatedImage;
}
@@ -92,17 +81,6 @@ class CkAnimatedImage extends ManagedSkiaObject<SkAnimatedImage>
return resizedAnimatedImage;
}
@override
SkAnimatedImage resurrect() => createDefault();
@override
bool get isResurrectionExpensive => true;
@override
void delete() {
rawSkiaObject?.delete();
}
bool _disposed = false;
bool get debugDisposed => _disposed;
@@ -118,7 +96,7 @@ class CkAnimatedImage extends ManagedSkiaObject<SkAnimatedImage>
'Cannot dispose a codec that has already been disposed.',
);
_disposed = true;
delete();
_ref.dispose();
}
@override
@@ -136,7 +114,7 @@ class CkAnimatedImage extends ManagedSkiaObject<SkAnimatedImage>
@override
Future<ui.FrameInfo> getNextFrame() {
assert(_debugCheckIsNotDisposed());
final SkAnimatedImage animatedImage = skiaObject;
final SkAnimatedImage animatedImage = _ref.nativeObject;
// SkAnimatedImage comes pre-initialized to point to the current frame (by
// default the first frame, and, with some special resurrection logic in
@@ -152,7 +130,6 @@ class CkAnimatedImage extends ManagedSkiaObject<SkAnimatedImage>
);
animatedImage.decodeNextFrame();
_currentFrameIndex = (_currentFrameIndex + 1) % _frameCount;
return Future<ui.FrameInfo>.value(currentFrame);
}
}

View File

@@ -16,7 +16,6 @@ import 'image_filter.dart';
import 'mask_filter.dart';
import 'native_memory.dart';
import 'shader.dart';
import 'skia_object_cache.dart';
/// The implementation of [ui.Paint] used by the CanvasKit backend.
///

View File

@@ -11,25 +11,20 @@ import '../util.dart';
import 'canvas.dart';
import 'canvaskit_api.dart';
import 'image.dart';
import 'skia_object_cache.dart';
import 'native_memory.dart';
import 'surface.dart';
import 'surface_factory.dart';
/// Implements [ui.Picture] on top of [SkPicture].
///
/// Unlike most other [ManagedSkiaObject] implementations, instances of this
/// class may have their Skia counterparts deleted before finalization registry
/// or [SkiaObjectCache] decide to delete it.
class CkPicture extends ManagedSkiaObject<SkPicture> implements ui.Picture {
CkPicture(SkPicture super.picture, this.cullRect, this._snapshot) :
assert(
browserSupportsFinalizationRegistry && _snapshot == null ||
_snapshot != null,
'If the browser does not support FinalizationRegistry (WeakRef), then we must have a picture snapshot to be able to resurrect it.',
);
class CkPicture implements ui.Picture {
CkPicture(SkPicture skPicture, this.cullRect) {
_ref = UniqueRef<SkPicture>(this, skPicture, 'Picture');
}
late final UniqueRef<SkPicture> _ref;
final ui.Rect? cullRect;
final CkPictureSnapshot? _snapshot;
SkPicture get skiaObject => _ref.nativeObject;
@override
int get approximateBytesUsed => 0;
@@ -86,11 +81,7 @@ class CkPicture extends ManagedSkiaObject<SkPicture> implements ui.Picture {
Instrumentation.instance.incrementCounter('Picture disposed');
}
_isDisposed = true;
_snapshot?.dispose();
// Emulate what SkiaObjectCache does.
rawSkiaObject?.delete();
rawSkiaObject = null;
_ref.dispose();
}
@override
@@ -123,31 +114,4 @@ class CkPicture extends ManagedSkiaObject<SkPicture> implements ui.Picture {
}
return CkImage(rasterImage);
}
@override
bool get isResurrectionExpensive => true;
@override
SkPicture createDefault() {
// The default object is supplied in the constructor.
throw StateError('Unreachable code');
}
@override
SkPicture resurrect() {
// If a picture has been explicitly disposed of, it can no longer be
// resurrected. An attempt to resurrect after the framework told the
// engine to dispose of the picture likely indicates a bug in the engine.
assert(debugCheckNotDisposed('Cannot resurrect picture.'));
return _snapshot!.toPicture();
}
@override
void delete() {
// This method may be called after [dispose], in which case there's nothing
// left to do. The Skia object is deleted permanently.
if (!_isDisposed) {
rawSkiaObject?.delete();
}
}
}

View File

@@ -20,9 +20,7 @@ class CkPictureRecorder implements ui.PictureRecorder {
final SkPictureRecorder recorder = _skRecorder = SkPictureRecorder();
final Float32List skRect = toSkRect(bounds);
final SkCanvas skCanvas = recorder.beginRecording(skRect);
return _recordingCanvas = browserSupportsFinalizationRegistry
? CkCanvas(skCanvas)
: RecordingCkCanvas(skCanvas, bounds);
return _recordingCanvas = CkCanvas(skCanvas);
}
CkCanvas? get recordingCanvas => _recordingCanvas;
@@ -38,8 +36,7 @@ class CkPictureRecorder implements ui.PictureRecorder {
final SkPicture skPicture = recorder.finishRecordingAsPicture();
recorder.delete();
_skRecorder = null;
final CkPicture result =
CkPicture(skPicture, _cullRect, _recordingCanvas!.pictureSnapshot);
final CkPicture result = CkPicture(skPicture, _cullRect);
// We invoke the handler here, not in the picture constructor, because we want
// [result.approximateBytesUsed] to be available for the handler.
ui.Picture.onCreate?.call(result);

View File

@@ -1,310 +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.
import 'dart:collection';
import 'package:meta/meta.dart';
import '../../engine.dart' show Instrumentation;
import 'canvaskit_api.dart';
import 'renderer.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 {
SkiaObjectCache(this.maximumSize)
: _itemQueue = DoubleLinkedQueue<SkiaObject<Object>>(),
_itemMap = <SkiaObject<Object>, DoubleLinkedQueueEntry<SkiaObject<Object>>>{};
final int maximumSize;
/// A doubly linked list of the objects in the cache.
///
/// This makes it fast to move a recently used object to the front.
final DoubleLinkedQueue<SkiaObject<Object>> _itemQueue;
/// A map of objects to their associated node in the [_itemQueue].
///
/// This makes it fast to find the node in the queue when we need to
/// move the object to the front of the queue.
final Map<SkiaObject<Object>, DoubleLinkedQueueEntry<SkiaObject<Object>>> _itemMap;
/// The number of objects in the cache.
int get length => _itemQueue.length;
/// Whether or not [object] is in the cache.
///
/// This is only for testing.
@visibleForTesting
bool debugContains(SkiaObject<Object> object) {
return _itemMap.containsKey(object);
}
/// Adds [object] to the cache.
///
/// If adding [object] causes the total size of the cache to exceed
/// [maximumSize], then the least recently used 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();
}
}
}
/// 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;
}
/// A function that restores a Skia object that was temporarily deleted.
typedef Resurrector<T> = T Function();
// 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;
}
CanvasKitRenderer.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();
}
}

View File

@@ -6,11 +6,10 @@ import 'dart:typed_data';
import 'package:ui/ui.dart' as ui;
import '../util.dart';
import 'canvaskit_api.dart';
import 'skia_object_cache.dart';
import 'native_memory.dart';
class CkVertices extends ManagedSkiaObject<SkVertices> implements ui.Vertices {
class CkVertices implements ui.Vertices {
factory CkVertices(
ui.VertexMode mode,
List<ui.Offset> positions, {
@@ -82,48 +81,31 @@ class CkVertices extends ManagedSkiaObject<SkVertices> implements ui.Vertices {
this._textureCoordinates,
this._colors,
this._indices,
);
final SkVertexMode _mode;
final Float32List _positions;
final Float32List? _textureCoordinates;
final Uint32List? _colors;
final Uint16List? _indices;
@override
SkVertices createDefault() {
return canvasKit.MakeVertices(
) {
final SkVertices skVertices = canvasKit.MakeVertices(
_mode,
_positions,
_textureCoordinates,
_colors,
_indices,
);
_ref = UniqueRef<SkVertices>(this, skVertices, 'Vertices');
}
@override
SkVertices resurrect() {
return createDefault();
}
final SkVertexMode _mode;
final Float32List _positions;
final Float32List? _textureCoordinates;
final Uint32List? _colors;
final Uint16List? _indices;
late final UniqueRef<SkVertices> _ref;
@override
void delete() {
rawSkiaObject?.delete();
}
bool _disposed = false;
SkVertices get skiaObject => _ref.nativeObject;
@override
void dispose() {
delete();
_disposed = true;
_ref.dispose();
}
@override
bool get debugDisposed {
if (assertionsEnabled) {
return _disposed;
}
throw StateError('Vertices.debugDisposed is only avialalbe when asserts are enabled.');
}
bool get debugDisposed => _ref.isDisposed;
}

View File

@@ -43,9 +43,6 @@ void testMain() {
test('renders using non-recording canvas if weak refs are supported',
() async {
expect(browserSupportsFinalizationRegistry, isTrue,
reason: 'This test specifically tests non-recording canvas, which '
'only works if FinalizationRegistry is available.');
final CkPictureRecorder recorder = CkPictureRecorder();
final CkCanvas canvas = recorder.beginRecording(kDefaultRegion);
expect(canvas.runtimeType, CkCanvas);
@@ -55,38 +52,6 @@ void testMain() {
recorder.endRecording(),
region: kDefaultRegion,
);
// Safari does not support weak refs (FinalizationRegistry).
// This test should be revisited when Safari ships weak refs.
// TODO(yjbanov): skip Firefox due to a crash: https://github.com/flutter/flutter/issues/86632
}, skip: isSafari || isFirefox);
test('renders using a recording canvas if weak refs are not supported',
() async {
browserSupportsFinalizationRegistry = false;
final CkPictureRecorder recorder = CkPictureRecorder();
final CkCanvas canvas = recorder.beginRecording(kDefaultRegion);
expect(canvas, isA<RecordingCkCanvas>());
drawTestPicture(canvas);
final CkPicture originalPicture = recorder.endRecording();
await matchPictureGolden('canvaskit_picture.png', originalPicture, region: kDefaultRegion);
final ByteData originalPixels =
(await (await originalPicture.toImage(50, 50)).toByteData())!;
// Test that a picture restored from a snapshot looks the same.
final CkPictureSnapshot? snapshot = canvas.pictureSnapshot;
expect(snapshot, isNotNull);
final SkPicture restoredSkPicture = snapshot!.toPicture();
expect(restoredSkPicture, isNotNull);
final CkPicture restoredPicture = CkPicture(
restoredSkPicture, const ui.Rect.fromLTRB(0, 0, 50, 50), snapshot);
final ByteData restoredPixels =
(await (await restoredPicture.toImage(50, 50)).toByteData())!;
await matchPictureGolden('canvaskit_picture.png', restoredPicture, region: kDefaultRegion);
expect(restoredPixels.buffer.asUint8List(),
originalPixels.buffer.asUint8List());
});
// Regression test for https://github.com/flutter/flutter/issues/51237
@@ -885,9 +850,7 @@ void testMain() {
await matchGoldenFile('cross_overlay_resources.png', region: const ui.Rect.fromLTRB(0, 0, 100, 100));
});
// TODO(hterkelsen): https://github.com/flutter/flutter/issues/71520
}, skip: isSafari || isFirefox);
});
}
Future<void> testSampleText(String language, String text,

View File

@@ -1438,10 +1438,6 @@ void _canvasTests() {
});
test('toImage.toByteData', () async {
// Pretend that FinalizationRegistry is supported, so we can run this
// test in older browsers (the test will use a TestCollector instead of
// ProductionCollector)
browserSupportsFinalizationRegistry = true;
final SkPictureRecorder otherRecorder = SkPictureRecorder();
final SkCanvas otherCanvas = otherRecorder
.beginRecording(Float32List.fromList(<double>[0, 0, 1, 1]));
@@ -1450,7 +1446,7 @@ void _canvasTests() {
SkPaint()..setColorInt(0xAAFFFFFF),
);
final CkPicture picture =
CkPicture(otherRecorder.finishRecordingAsPicture(), null, null);
CkPicture(otherRecorder.finishRecordingAsPicture(), null);
final CkImage image = await picture.toImage(1, 1) as CkImage;
final ByteData rawData =
await image.toByteData();

View File

@@ -11,38 +11,20 @@ import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import 'package:web_engine_tester/golden_tester.dart';
/// Used in tests instead of [ProductionCollector] to control Skia object
/// collection explicitly, and to prevent leaks across tests.
///
/// See [TestCollector] for usage.
late TestCollector testCollector;
const MethodCodec codec = StandardMethodCodec();
/// Common test setup for all CanvasKit unit-tests.
void setUpCanvasKitTest() {
setUpAll(() async {
expect(renderer, isA<CanvasKitRenderer>(), reason: 'This test must run in CanvasKit mode.');
debugResetBrowserSupportsFinalizationRegistry();
debugDisableFontFallbacks = false;
await initializeEngine(assetManager: WebOnlyMockAssetManager());
});
setUp(() async {
testCollector = TestCollector();
Collector.debugOverrideCollector(testCollector);
});
tearDown(() {
testCollector.cleanUpAfterTest();
debugResetBrowserSupportsFinalizationRegistry();
HtmlViewEmbedder.instance.debugClear();
SurfaceFactory.instance.debugClear();
});
tearDownAll(() {
debugResetBrowserSupportsFinalizationRegistry();
});
}
/// Utility function for CanvasKit tests to draw pictures without
@@ -55,127 +37,6 @@ CkPicture paintPicture(
return recorder.endRecording();
}
class _TestFinalizerRegistration {
_TestFinalizerRegistration(this.wrapper, this.deletable, this.stackTrace);
final Object wrapper;
final SkDeletable deletable;
final StackTrace stackTrace;
}
class _TestCollection {
_TestCollection(this.deletable, this.stackTrace);
final SkDeletable deletable;
final StackTrace stackTrace;
}
/// Provides explicit synchronous API for collecting Skia objects in tests.
///
/// [ProductionCollector] relies on `FinalizationRegistry` and timers to
/// delete Skia objects, which makes it more precise and efficient. However,
/// it also makes it unpredictable. For example, an object created in one
/// test may be collected while running another test because the timing is
/// subject to browser-specific GC scheduling.
///
/// Tests should use [collectNow] and [collectAfterTest] to trigger collections.
class TestCollector implements Collector {
final List<_TestFinalizerRegistration> _activeRegistrations =
<_TestFinalizerRegistration>[];
final List<_TestFinalizerRegistration> _collectedRegistrations =
<_TestFinalizerRegistration>[];
final List<_TestCollection> _pendingCollections = <_TestCollection>[];
final List<_TestCollection> _completedCollections = <_TestCollection>[];
@override
void register(Object wrapper, SkDeletable deletable) {
_activeRegistrations.add(
_TestFinalizerRegistration(wrapper, deletable, StackTrace.current),
);
}
@override
void collect(SkDeletable deletable) {
_pendingCollections.add(
_TestCollection(deletable, StackTrace.current),
);
}
/// Deletes all Skia objects scheduled for collection.
void collectNow() {
for (final _TestCollection collection in _pendingCollections) {
late final _TestFinalizerRegistration? activeRegistration;
for (final _TestFinalizerRegistration registration in _activeRegistrations) {
if (identical(registration.deletable, collection.deletable)) {
activeRegistration = registration;
break;
}
}
if (activeRegistration == null) {
late final _TestFinalizerRegistration? collectedRegistration;
for (final _TestFinalizerRegistration registration
in _collectedRegistrations) {
if (identical(registration.deletable, collection.deletable)) {
collectedRegistration = registration;
break;
}
}
if (collectedRegistration == null) {
fail(
'Attempted to collect an object that was never registered for finalization.\n'
'The collection was requested here:\n'
'${collection.stackTrace}');
} else {
final _TestCollection firstCollection = _completedCollections
.firstWhere((_TestCollection completedCollection) {
return identical(
completedCollection.deletable, collection.deletable);
});
fail(
'Attempted to collect an object that was previously collected.\n'
'The object was registered for finalization here:\n'
'${collection.stackTrace}\n\n'
'The first collection was requested here:\n'
'${firstCollection.stackTrace}\n\n'
'The second collection was requested here:\n'
'${collection.stackTrace}',
);
}
} else {
_collectedRegistrations.add(activeRegistration);
_activeRegistrations.remove(activeRegistration);
_completedCollections.add(collection);
if (!collection.deletable.isDeleted()) {
collection.deletable.delete();
}
}
}
_pendingCollections.clear();
}
/// Deletes all Skia objects with registered finalizers.
///
/// This also deletes active objects that have not been scheduled for
/// collection, to prevent objects leaking across tests.
void cleanUpAfterTest() {
for (final _TestCollection collection in _pendingCollections) {
if (!collection.deletable.isDeleted()) {
collection.deletable.delete();
}
}
for (final _TestFinalizerRegistration registration in _activeRegistrations) {
if (!registration.deletable.isDeleted()) {
registration.deletable.delete();
}
}
_activeRegistrations.clear();
_collectedRegistrations.clear();
_pendingCollections.clear();
_completedCollections.clear();
}
}
Future<void> matchSceneGolden(String goldenFile, LayerScene scene, {
required ui.Rect region,
}) async {

View File

@@ -95,12 +95,9 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) {
// Disallow double-dispose.
expect(() => image.dispose(), throwsAssertionError);
testCollector.collectNow();
});
test('CkAnimatedImage remembers last animation position after resurrection', () async {
browserSupportsFinalizationRegistry = false;
test('CkAnimatedImage iterates frames correctly', () async {
final CkAnimatedImage image = CkAnimatedImage.decodeFromBytes(kAnimatedGif, 'test');
expect(image.frameCount, 3);
expect(image.repetitionCount, -1);
@@ -109,16 +106,8 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) {
await expectFrameData(frame1, <int>[255, 0, 0, 255]);
final ui.FrameInfo frame2 = await image.getNextFrame();
await expectFrameData(frame2, <int>[0, 255, 0, 255]);
// Pretend that the image is temporarily deleted.
image.delete();
image.didDelete();
// Check that we got the 3rd frame after resurrection.
final ui.FrameInfo frame3 = await image.getNextFrame();
await expectFrameData(frame3, <int>[0, 0, 255, 255]);
testCollector.collectNow();
});
test('CkImage toString', () {
@@ -128,7 +117,6 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) {
final CkImage image = CkImage(skImage);
expect(image.toString(), '[1×1]');
image.dispose();
testCollector.collectNow();
});
test('CkImage can be explicitly disposed of', () {
@@ -144,7 +132,6 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) {
// Disallow double-dispose.
expect(() => image.dispose(), throwsAssertionError);
testCollector.collectNow();
});
test('CkImage can be explicitly disposed of when cloned', () async {
@@ -163,22 +150,18 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) {
expect(image.isCloneOf(clone), isTrue);
expect(box.isDisposed, isFalse);
testCollector.collectNow();
expect(skImage.isDeleted(), isFalse);
image.dispose();
expect(box.refCount, 1);
expect(box.isDisposed, isFalse);
testCollector.collectNow();
expect(skImage.isDeleted(), isFalse);
clone.dispose();
expect(box.refCount, 0);
expect(box.isDisposed, isTrue);
testCollector.collectNow();
expect(skImage.isDeleted(), isTrue);
expect(box.debugGetStackTraces().length, 0);
testCollector.collectNow();
});
test('CkImage toByteData', () async {
@@ -188,7 +171,6 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) {
final CkImage image = CkImage(skImage);
expect((await image.toByteData()).lengthInBytes, greaterThan(0));
expect((await image.toByteData(format: ui.ImageByteFormat.png)).lengthInBytes, greaterThan(0));
testCollector.collectNow();
});
test('toByteData with decodeImageFromPixels on videoFrame formats', () async {
@@ -262,7 +244,6 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) {
// The precise PNG encoding is browser-specific, but we can check the file
// signature.
expect(detectContentType(png.buffer.asUint8List()), 'image/png');
testCollector.collectNow();
// TODO(hterkelsen): Firefox and Safari do not currently support ImageDecoder.
}, skip: isFirefox || isSafari);
@@ -282,7 +263,6 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) {
final ui.Image image = (await codec.getNextFrame()).image;
expect(image.height, 1);
expect(image.width, 1);
testCollector.collectNow();
});
test('instantiateImageCodec respects target image size',
@@ -312,8 +292,6 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) {
image.dispose();
codec.dispose();
}
testCollector.collectNow();
});
test('instantiateImageCodec with multi-frame image does not support targetWidth/targetHeight',
@@ -339,8 +317,6 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) {
expect(image.height, 1);
image.dispose();
codec.dispose();
testCollector.collectNow();
});
test('skiaInstantiateWebImageCodec throws exception on request error',
@@ -361,7 +337,6 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) {
'https://flutter.dev/docs/development/platform-integration/web-images',
);
}
testCollector.collectNow();
});
test('skiaInstantiateWebImageCodec throws exception on HTTP error',
@@ -377,7 +352,6 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) {
'Server response code: 404',
);
}
testCollector.collectNow();
});
test('skiaInstantiateWebImageCodec includes URL in the error for malformed image',
@@ -409,7 +383,6 @@ void _testForImageCodecs({required bool useBrowserImageDecoder}) {
);
}
}
testCollector.collectNow();
});
test('Reports error when failing to decode empty image data', () async {
@@ -831,7 +804,6 @@ void _testCkAnimatedImage() {
// The precise PNG encoding is browser-specific, but we can check the file
// signature.
expect(detectContentType(png!.buffer.asUint8List()), 'image/png');
testCollector.collectNow();
});
test('CkAnimatedImage toByteData(RGBA)', () async {
@@ -847,7 +819,6 @@ void _testCkAnimatedImage() {
expect(rgba, isNotNull);
expect(rgba!.buffer.asUint8List(), expectedColors[i]);
}
testCollector.collectNow();
});
}
@@ -867,7 +838,6 @@ void _testCkBrowserImageDecoder() {
// The precise PNG encoding is browser-specific, but we can check the file
// signature.
expect(detectContentType(png!.buffer.asUint8List()), 'image/png');
testCollector.collectNow();
});
test('ImageDecoder toByteData(RGBA)', () async {
@@ -886,7 +856,6 @@ void _testCkBrowserImageDecoder() {
expect(rgba, isNotNull);
expect(rgba!.buffer.asUint8List(), expectedColors[i]);
}
testCollector.collectNow();
});
test('ImageDecoder expires after inactivity', () async {
@@ -930,7 +899,6 @@ void _testCkBrowserImageDecoder() {
final ui.FrameInfo frame3 = await image.getNextFrame();
await expectFrameData(frame3, <int>[0, 0, 255, 255]);
testCollector.collectNow();
debugRestoreWebDecoderExpireDuration();
});
}

View File

@@ -95,7 +95,6 @@ void testMain() {
});
test('CkContourMeasure iteration', () {
browserSupportsFinalizationRegistry = false;
final ui.Path path = ui.Path();
expect(path, isA<CkPath>());
path.addRect(const ui.Rect.fromLTRB(0, 0, 10, 10));
@@ -112,7 +111,6 @@ void testMain() {
});
test('CkContourMeasure index', () {
browserSupportsFinalizationRegistry = false;
final ui.Path path = ui.Path();
expect(path, isA<CkPath>());
path.addRect(const ui.Rect.fromLTRB(0, 0, 10, 10));

View File

@@ -10,6 +10,7 @@ import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import '../common/matchers.dart';
import 'common.dart';
void main() {
@@ -20,19 +21,17 @@ void testMain() {
group('CkPicture', () {
setUpCanvasKitTest();
group('in browsers that do not support FinalizationRegistry', () {
group('lifecycle', () {
test('can be disposed of manually', () {
browserSupportsFinalizationRegistry = false;
final ui.PictureRecorder recorder = ui.PictureRecorder();
final ui.Canvas canvas = ui.Canvas(recorder);
canvas.drawPaint(ui.Paint());
final CkPicture picture = recorder.endRecording() as CkPicture;
expect(picture.rawSkiaObject, isNotNull);
expect(picture.skiaObject, isNotNull);
expect(picture.debugDisposed, isFalse);
picture.debugCheckNotDisposed('Test.'); // must not throw
picture.dispose();
expect(picture.rawSkiaObject, isNull);
expect(() => picture.skiaObject, throwsA(isAssertionError));
expect(picture.debugDisposed, isTrue);
StateError? actualError;
@@ -50,36 +49,6 @@ void testMain() {
'The picture has been disposed. '
'When the picture was disposed the stack trace was:\n'
));
// Emulate SkiaObjectCache deleting the picture
picture.delete();
picture.didDelete();
expect(picture.rawSkiaObject, isNull);
// A Picture that's been disposed of can no longer be resurrected
expect(() => picture.resurrect(), throwsStateError);
expect(() => picture.toImage(10, 10), throwsStateError);
expect(() => picture.dispose(), throwsStateError);
});
test('can be deleted by SkiaObjectCache', () {
browserSupportsFinalizationRegistry = false;
final ui.PictureRecorder recorder = ui.PictureRecorder();
final ui.Canvas canvas = ui.Canvas(recorder);
canvas.drawPaint(ui.Paint());
final CkPicture picture = recorder.endRecording() as CkPicture;
expect(picture.rawSkiaObject, isNotNull);
// Emulate SkiaObjectCache deleting the picture
picture.delete();
picture.didDelete();
expect(picture.rawSkiaObject, isNull);
// Deletion is softer than disposal. An object may still be resurrected
// if it was deleted prematurely.
expect(picture.debugDisposed, isFalse);
expect(picture.resurrect(), isNotNull);
});
});

View File

@@ -1,232 +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.
import 'dart:js_interop';
import 'package:js/js.dart';
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import 'common.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
group('skia_objects_cache', () {
_tests();
});
}
void _tests() {
SkiaObjects.maximumCacheSize = 4;
setUpCanvasKitTest();
setUp(() async {
// Pretend the browser does not support FinalizationRegistry so we can test the
// resurrection logic.
browserSupportsFinalizationRegistry = false;
});
group(ManagedSkiaObject, () {
test('implements create, cache, delete, resurrect, delete lifecycle', () {
final FakeRasterizer fakeRasterizer = FakeRasterizer();
CanvasKitRenderer.instance.rasterizer = fakeRasterizer;
// 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 SkPaint 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(fakeRasterizer.addPostFrameCallbackCount, 1);
expect(testObject.createDefaultCount, 1);
expect(testObject.resurrectCount, 0);
expect(testObject.deleteCount, 1);
// Trigger resurrect
final SkPaint skiaObject2 = testObject.skiaObject;
expect(skiaObject2, isNotNull);
expect(skiaObject2, isNot(same(skiaObject1)));
expect(SkiaObjects.resurrectableObjects.single, testObject);
expect(fakeRasterizer.addPostFrameCallbackCount, 1);
expect(testObject.createDefaultCount, 1);
expect(testObject.resurrectCount, 1);
expect(testObject.deleteCount, 1);
// Trigger final delete
SkiaObjects.postFrameCleanUp();
expect(SkiaObjects.resurrectableObjects, isEmpty);
expect(fakeRasterizer.addPostFrameCallbackCount, 1);
expect(testObject.createDefaultCount, 1);
expect(testObject.resurrectCount, 1);
expect(testObject.deleteCount, 2);
});
test('is added to SkiaObjects cache if expensive', () {
final TestSkiaObject object1 = TestSkiaObject(isExpensive: true);
expect(SkiaObjects.expensiveCache.length, 1);
expect(SkiaObjects.expensiveCache.debugContains(object1), isTrue);
final 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);
});
});
}
class TestSkDeletableMock {
static int deleteCount = 0;
bool isDeleted() => _isDeleted;
bool _isDeleted = false;
void delete() {
expect(_isDeleted, isFalse,
reason:
'CanvasKit does not allow deleting the same object more than once.');
_isDeleted = true;
deleteCount++;
}
JsConstructor get constructor => TestJsConstructor(name:
'TestSkDeletable'.toJS);
}
@JS()
@anonymous
@staticInterop
class TestSkDeletable implements SkDeletable {
factory TestSkDeletable() {
final TestSkDeletableMock mock = TestSkDeletableMock();
return TestSkDeletable._(
isDeleted: () { return mock.isDeleted(); }.toJS,
delete: () { return mock.delete(); }.toJS,
constructor: mock.constructor);
}
external factory TestSkDeletable._({
JSFunction isDeleted,
JSFunction delete,
JsConstructor constructor});
}
@JS()
@anonymous
@staticInterop
class TestJsConstructor implements JsConstructor {
external factory TestJsConstructor({JSString name});
}
class TestSkiaObject extends ManagedSkiaObject<SkPaint> {
TestSkiaObject({this.isExpensive = false});
int createDefaultCount = 0;
int resurrectCount = 0;
int deleteCount = 0;
final bool isExpensive;
@override
SkPaint createDefault() {
createDefaultCount++;
return SkPaint();
}
@override
SkPaint resurrect() {
resurrectCount++;
return SkPaint();
}
@override
void delete() {
rawSkiaObject?.delete();
deleteCount++;
}
@override
bool get isResurrectionExpensive => isExpensive;
}
class FakeRasterizer implements Rasterizer {
int addPostFrameCallbackCount = 0;
@override
void addPostFrameCallback(VoidCallback callback) {
addPostFrameCallbackCount++;
}
@override
CompositorContext get context => throw UnimplementedError();
@override
void draw(LayerTree layerTree) {
throw UnimplementedError();
}
@override
void setSkiaResourceCacheMaxBytes(int bytes) {
throw UnimplementedError();
}
@override
void debugRunPostFrameCallbacks() {
throw UnimplementedError();
}
}
class TestSelfManagedObject extends SkiaObject<TestSkDeletable> {
TestSkDeletable? _skiaObject = TestSkDeletable();
@override
void delete() {
_skiaObject!.delete();
}
@override
void didDelete() {
_skiaObject = null;
}
@override
TestSkDeletable get skiaObject => throw UnimplementedError();
}

View File

@@ -10,6 +10,7 @@ import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart' as ui;
import 'package:web_engine_tester/golden_tester.dart';
import '../common/matchers.dart';
import 'common.dart';
void main() {
@@ -20,11 +21,11 @@ void testMain() {
group('Vertices', () {
setUpCanvasKitTest();
test('can be constructed, drawn, and deleted', () {
test('can be constructed, drawn, and disposed of', () {
final CkVertices vertices = _testVertices();
expect(vertices, isA<CkVertices>());
expect(vertices.createDefault(), isNotNull);
expect(vertices.resurrect(), isNotNull);
expect(vertices.skiaObject, isNotNull);
expect(vertices.debugDisposed, isFalse);
final CkPictureRecorder recorder = CkPictureRecorder();
final CkCanvas canvas =
@@ -34,7 +35,9 @@ void testMain() {
ui.BlendMode.srcOver,
CkPaint(),
);
vertices.delete();
vertices.dispose();
expect(vertices.debugDisposed, isTrue);
expect(() => vertices.skiaObject, throwsA(isAssertionError));
});
});