blob: 47ed66379203814e080b8abc774f41a960be2cbb [file] [log] [blame]
// Copyright 2013 The Flutter Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import 'package:test/bootstrap/browser.dart';
import 'package:test/test.dart';
import 'package:ui/src/engine.dart';
import 'package:ui/ui.dart';
import '../matchers.dart';
import '../spy.dart';
import 'common.dart';
void main() {
internalBootstrapBrowserTest(() => testMain);
}
void testMain() {
group('skia_objects_cache', () {
_tests();
// TODO: https://github.com/flutter/flutter/issues/60040
}, skip: isIosSafari);
}
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();
EnginePlatformDispatcher.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);
});
});
group(SkiaObjectBox, () {
test('Records stack traces and respects refcounts', () async {
final ZoneSpy spy = ZoneSpy();
spy.run(() {
Instrumentation.enabled = true;
TestSkDeletable.deleteCount = 0;
TestBoxWrapper.resurrectCount = 0;
final TestBoxWrapper original = TestBoxWrapper();
expect(original.box.debugGetStackTraces().length, 1);
expect(original.box.refCount, 1);
expect(original.box.isDeletedPermanently, isFalse);
final TestBoxWrapper clone = original.clone();
expect(clone.box, same(original.box));
expect(clone.box.debugGetStackTraces().length, 2);
expect(clone.box.refCount, 2);
expect(original.box.debugGetStackTraces().length, 2);
expect(original.box.refCount, 2);
expect(original.box.isDeletedPermanently, isFalse);
original.dispose();
testCollector.collectNow();
expect(TestSkDeletable.deleteCount, 0);
spy.fakeAsync.elapse(const Duration(seconds: 2));
expect(
spy.printLog,
<String>[
'Engine counters:\n TestSkDeletable created: 1\n'
],
);
expect(clone.box.debugGetStackTraces().length, 1);
expect(clone.box.refCount, 1);
expect(original.box.debugGetStackTraces().length, 1);
expect(original.box.refCount, 1);
clone.dispose();
expect(clone.box.debugGetStackTraces().length, 0);
expect(clone.box.refCount, 0);
expect(original.box.debugGetStackTraces().length, 0);
expect(original.box.refCount, 0);
expect(original.box.isDeletedPermanently, isTrue);
testCollector.collectNow();
expect(TestSkDeletable.deleteCount, 1);
expect(TestBoxWrapper.resurrectCount, 0);
expect(() => clone.box.unref(clone), throwsAssertionError);
spy.printLog.clear();
spy.fakeAsync.elapse(const Duration(seconds: 2));
expect(
spy.printLog,
<String>[
'Engine counters:\n TestSkDeletable created: 1\n TestSkDeletable deleted: 1\n'
],
);
Instrumentation.enabled = false;
});
});
test('Can resurrect Skia objects', () async {
TestSkDeletable.deleteCount = 0;
TestBoxWrapper.resurrectCount = 0;
final TestBoxWrapper object = TestBoxWrapper();
expect(TestSkDeletable.deleteCount, 0);
expect(TestBoxWrapper.resurrectCount, 0);
// Test 3 cycles of delete/resurrect.
for (int i = 0; i < 3; i++) {
object.box.delete();
object.box.didDelete();
expect(TestSkDeletable.deleteCount, i + 1);
expect(TestBoxWrapper.resurrectCount, i);
expect(object.box.isDeletedTemporarily, isTrue);
expect(object.box.isDeletedPermanently, isFalse);
expect(object.box.skiaObject, isNotNull);
expect(TestSkDeletable.deleteCount, i + 1);
expect(TestBoxWrapper.resurrectCount, i + 1);
expect(object.box.isDeletedTemporarily, isFalse);
expect(object.box.isDeletedPermanently, isFalse);
}
object.dispose();
expect(object.box.isDeletedPermanently, isTrue);
});
test('Can dispose temporarily deleted object', () async {
TestSkDeletable.deleteCount = 0;
TestBoxWrapper.resurrectCount = 0;
final TestBoxWrapper object = TestBoxWrapper();
expect(TestSkDeletable.deleteCount, 0);
expect(TestBoxWrapper.resurrectCount, 0);
object.box.delete();
object.box.didDelete();
expect(TestSkDeletable.deleteCount, 1);
expect(TestBoxWrapper.resurrectCount, 0);
expect(object.box.isDeletedTemporarily, isTrue);
expect(object.box.isDeletedPermanently, isFalse);
object.dispose();
expect(object.box.isDeletedPermanently, isTrue);
});
});
group('$SynchronousSkiaObjectCache', () {
test('is initialized empty', () {
expect(SynchronousSkiaObjectCache(10), hasLength(0));
});
test('adds objects', () {
final SynchronousSkiaObjectCache cache = SynchronousSkiaObjectCache(2);
cache.add(TestSelfManagedObject());
expect(cache, hasLength(1));
cache.add(TestSelfManagedObject());
expect(cache, hasLength(2));
});
test('forbids adding the same object twice', () {
final SynchronousSkiaObjectCache cache = SynchronousSkiaObjectCache(2);
final TestSelfManagedObject object = TestSelfManagedObject();
cache.add(object);
expect(cache, hasLength(1));
expect(() => cache.add(object), throwsAssertionError);
});
void expectObjectInCache(
SynchronousSkiaObjectCache cache,
TestSelfManagedObject object,
) {
expect(cache.debugContains(object), isTrue);
expect(object._skiaObject, isNotNull);
}
void expectObjectNotInCache(
SynchronousSkiaObjectCache cache,
TestSelfManagedObject object,
) {
expect(cache.debugContains(object), isFalse);
expect(object._skiaObject, isNull);
}
test('respects maximumSize', () {
final SynchronousSkiaObjectCache cache = SynchronousSkiaObjectCache(2);
final TestSelfManagedObject object1 = TestSelfManagedObject();
final TestSelfManagedObject object2 = TestSelfManagedObject();
final TestSelfManagedObject object3 = TestSelfManagedObject();
final TestSelfManagedObject object4 = TestSelfManagedObject();
cache.add(object1);
expect(cache, hasLength(1));
expectObjectInCache(cache, object1);
cache.add(object2);
expect(cache, hasLength(2));
expectObjectInCache(cache, object1);
expectObjectInCache(cache, object2);
cache.add(object3);
expect(cache, hasLength(2));
expectObjectNotInCache(cache, object1);
expectObjectInCache(cache, object2);
expectObjectInCache(cache, object3);
cache.add(object4);
expect(cache, hasLength(2));
expectObjectNotInCache(cache, object1);
expectObjectNotInCache(cache, object2);
expectObjectInCache(cache, object3);
expectObjectInCache(cache, object4);
});
test('uses RLU strategy', () {
final SynchronousSkiaObjectCache cache = SynchronousSkiaObjectCache(2);
final TestSelfManagedObject object1 = TestSelfManagedObject();
final TestSelfManagedObject object2 = TestSelfManagedObject();
final TestSelfManagedObject object3 = TestSelfManagedObject();
final TestSelfManagedObject object4 = TestSelfManagedObject();
cache.add(object1);
expectObjectInCache(cache, object1);
cache.add(object2);
expectObjectInCache(cache, object2);
cache.add(object3);
expectObjectInCache(cache, object3);
expectObjectNotInCache(cache, object1);
cache.markUsed(object2);
cache.add(object4);
expectObjectInCache(cache, object2);
expectObjectNotInCache(cache, object3);
expectObjectInCache(cache, object4);
});
});
}
/// A simple class that wraps a [SkiaObjectBox].
///
/// Can be [clone]d such that the clones share the same ref counted box.
class TestBoxWrapper implements StackTraceDebugger {
static int resurrectCount = 0;
TestBoxWrapper() {
if (assertionsEnabled) {
_debugStackTrace = StackTrace.current;
}
box = SkiaObjectBox<TestBoxWrapper, TestSkDeletable>.resurrectable(
this, TestSkDeletable(), () {
resurrectCount += 1;
return TestSkDeletable();
});
}
TestBoxWrapper.cloneOf(this.box) {
if (assertionsEnabled) {
_debugStackTrace = StackTrace.current;
}
box.ref(this);
}
@override
StackTrace get debugStackTrace => _debugStackTrace;
late StackTrace _debugStackTrace;
late SkiaObjectBox<TestBoxWrapper, TestSkDeletable> box;
void dispose() {
box.unref(this);
}
TestBoxWrapper clone() => TestBoxWrapper.cloneOf(box);
}
class TestSkDeletable implements SkDeletable {
static int deleteCount = 0;
@override
bool isDeleted() => _isDeleted;
bool _isDeleted = false;
@override
void delete() {
expect(_isDeleted, isFalse,
reason:
'CanvasKit does not allow deleting the same object more than once.');
_isDeleted = true;
deleteCount++;
}
@override
JsConstructor get constructor => TestJsConstructor('TestSkDeletable');
}
class TestJsConstructor implements JsConstructor {
TestJsConstructor(this.name);
@override
final String name;
}
class TestSkiaObject extends ManagedSkiaObject<SkPaint> {
int createDefaultCount = 0;
int resurrectCount = 0;
int deleteCount = 0;
final bool isExpensive;
TestSkiaObject({this.isExpensive = false});
@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();
}