| // 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:test/bootstrap/browser.dart'; |
| import 'package:test/test.dart'; |
| |
| import 'package:ui/src/engine.dart'; |
| |
| import 'common.dart'; |
| |
| void main() { |
| internalBootstrapBrowserTest(() => testMain); |
| } |
| |
| void testMain() { |
| setUpCanvasKitTest(); |
| |
| late _MockNativeMemoryFinalizationRegistry mockFinalizationRegistry; |
| |
| setUp(() { |
| TestSkDeletableMock.deleteCount = 0; |
| nativeMemoryFinalizationRegistry = mockFinalizationRegistry = _MockNativeMemoryFinalizationRegistry(); |
| }); |
| |
| tearDown(() { |
| nativeMemoryFinalizationRegistry = NativeMemoryFinalizationRegistry(); |
| }); |
| |
| group(UniqueRef, () { |
| test('create-dispose-collect cycle', () { |
| expect(mockFinalizationRegistry.registeredPairs, hasLength(0)); |
| final Object owner = Object(); |
| final TestSkDeletable nativeObject = TestSkDeletable(); |
| final UniqueRef<TestSkDeletable> ref = UniqueRef<TestSkDeletable>(owner, nativeObject, 'TestSkDeletable'); |
| expect(ref.isDisposed, isFalse); |
| expect(ref.nativeObject, same(nativeObject)); |
| expect(TestSkDeletableMock.deleteCount, 0); |
| expect(mockFinalizationRegistry.registeredPairs, hasLength(1)); |
| expect(mockFinalizationRegistry.registeredPairs.single.owner, same(owner)); |
| expect(mockFinalizationRegistry.registeredPairs.single.ref, same(ref)); |
| |
| ref.dispose(); |
| expect(TestSkDeletableMock.deleteCount, 1); |
| expect(ref.isDisposed, isTrue); |
| expect( |
| reason: 'Cannot access object that was disposed', |
| () => ref.nativeObject, throwsA(isA<AssertionError>()), |
| ); |
| expect( |
| reason: 'Cannot dispose object more than once', |
| () => ref.dispose(), throwsA(isA<AssertionError>()), |
| ); |
| expect(TestSkDeletableMock.deleteCount, 1); |
| |
| // Simulate a GC |
| mockFinalizationRegistry.registeredPairs.single.ref.collect(); |
| expect( |
| reason: 'Manually disposed object should not be deleted again by GC.', |
| TestSkDeletableMock.deleteCount, 1, |
| ); |
| }); |
| |
| test('create-collect cycle', () { |
| expect(mockFinalizationRegistry.registeredPairs, hasLength(0)); |
| final Object owner = Object(); |
| final TestSkDeletable nativeObject = TestSkDeletable(); |
| final UniqueRef<TestSkDeletable> ref = UniqueRef<TestSkDeletable>(owner, nativeObject, 'TestSkDeletable'); |
| expect(ref.isDisposed, isFalse); |
| expect(ref.nativeObject, same(nativeObject)); |
| expect(TestSkDeletableMock.deleteCount, 0); |
| expect(mockFinalizationRegistry.registeredPairs, hasLength(1)); |
| |
| ref.collect(); |
| expect(TestSkDeletableMock.deleteCount, 1); |
| // There's nothing else to test for any practical gain. UniqueRef.collect |
| // is called when GC decided that the owner is no longer reachable. So |
| // there must not be anything else calling into this object for anything |
| // useful. |
| }); |
| |
| test('dispose instrumentation', () { |
| Instrumentation.enabled = true; |
| Instrumentation.instance.debugCounters.clear(); |
| |
| final Object owner = Object(); |
| final TestSkDeletable nativeObject = TestSkDeletable(); |
| |
| expect(Instrumentation.instance.debugCounters, <String, int>{}); |
| final UniqueRef<TestSkDeletable> ref = UniqueRef<TestSkDeletable>(owner, nativeObject, 'TestSkDeletable'); |
| expect(Instrumentation.instance.debugCounters, <String, int>{ |
| 'TestSkDeletable Created': 1, |
| }); |
| ref.dispose(); |
| expect(Instrumentation.instance.debugCounters, <String, int>{ |
| 'TestSkDeletable Created': 1, |
| 'TestSkDeletable Deleted': 1, |
| }); |
| }); |
| |
| test('collect instrumentation', () { |
| Instrumentation.enabled = true; |
| Instrumentation.instance.debugCounters.clear(); |
| |
| final Object owner = Object(); |
| final TestSkDeletable nativeObject = TestSkDeletable(); |
| |
| expect(Instrumentation.instance.debugCounters, <String, int>{}); |
| final UniqueRef<TestSkDeletable> ref = UniqueRef<TestSkDeletable>(owner, nativeObject, 'TestSkDeletable'); |
| expect(Instrumentation.instance.debugCounters, <String, int>{ |
| 'TestSkDeletable Created': 1, |
| }); |
| ref.collect(); |
| expect(Instrumentation.instance.debugCounters, <String, int>{ |
| 'TestSkDeletable Created': 1, |
| 'TestSkDeletable Leaked': 1, |
| 'TestSkDeletable Deleted': 1, |
| }); |
| }); |
| }); |
| |
| group(CountedRef, () { |
| test('single owner', () { |
| expect(mockFinalizationRegistry.registeredPairs, hasLength(0)); |
| final TestSkDeletable nativeObject = TestSkDeletable(); |
| final TestCountedRefOwner owner = TestCountedRefOwner(nativeObject); |
| expect(owner.ref.debugReferrers, hasLength(1)); |
| expect(owner.ref.debugReferrers.single, owner); |
| expect(owner.ref.refCount, 1); |
| expect(owner.ref.nativeObject, nativeObject); |
| expect(TestSkDeletableMock.deleteCount, 0); |
| expect(mockFinalizationRegistry.registeredPairs, hasLength(1)); |
| |
| owner.dispose(); |
| expect(owner.ref.debugReferrers, isEmpty); |
| expect(owner.ref.refCount, 0); |
| expect( |
| reason: 'Cannot access object that was disposed', |
| () => owner.ref.nativeObject, throwsA(isA<AssertionError>()), |
| ); |
| expect(TestSkDeletableMock.deleteCount, 1); |
| |
| expect( |
| reason: 'Cannot dispose object more than once', |
| () => owner.dispose(), throwsA(isA<AssertionError>()), |
| ); |
| }); |
| |
| test('multiple owners', () { |
| expect(mockFinalizationRegistry.registeredPairs, hasLength(0)); |
| final TestSkDeletable nativeObject = TestSkDeletable(); |
| final TestCountedRefOwner owner1 = TestCountedRefOwner(nativeObject); |
| expect(owner1.ref.debugReferrers, hasLength(1)); |
| expect(owner1.ref.debugReferrers.single, owner1); |
| expect(owner1.ref.refCount, 1); |
| expect(owner1.ref.nativeObject, nativeObject); |
| expect(TestSkDeletableMock.deleteCount, 0); |
| expect(mockFinalizationRegistry.registeredPairs, hasLength(1)); |
| |
| final TestCountedRefOwner owner2 = owner1.clone(); |
| expect(owner2.ref, same(owner1.ref)); |
| expect(owner2.ref.debugReferrers, hasLength(2)); |
| expect(owner2.ref.debugReferrers.first, owner1); |
| expect(owner2.ref.debugReferrers.last, owner2); |
| expect(owner2.ref.refCount, 2); |
| expect(owner2.ref.nativeObject, nativeObject); |
| expect(TestSkDeletableMock.deleteCount, 0); |
| expect( |
| reason: 'Second owner does not add more native object owners. ' |
| 'The underlying shared UniqueRef is the only one.', |
| mockFinalizationRegistry.registeredPairs, hasLength(1), |
| ); |
| |
| owner1.dispose(); |
| expect(owner2.ref.debugReferrers, hasLength(1)); |
| expect(owner2.ref.debugReferrers.single, owner2); |
| expect(owner2.ref.refCount, 1); |
| expect(owner2.ref.nativeObject, nativeObject); |
| expect(TestSkDeletableMock.deleteCount, 0); |
| expect( |
| reason: 'The same owner cannot dispose its CountedRef more than once, even when CountedRef is still alive.', |
| () => owner1.dispose(), throwsA(isA<AssertionError>()), |
| ); |
| |
| owner2.dispose(); |
| expect(owner2.ref.debugReferrers, isEmpty); |
| expect(owner2.ref.refCount, 0); |
| expect( |
| reason: 'Cannot access object that was disposed', |
| () => owner2.ref.nativeObject, throwsA(isA<AssertionError>()), |
| ); |
| expect(TestSkDeletableMock.deleteCount, 1); |
| |
| expect( |
| reason: 'The same owner cannot dispose its CountedRef more than once.', |
| () => owner2.dispose(), throwsA(isA<AssertionError>()), |
| ); |
| |
| // Simulate a GC |
| mockFinalizationRegistry.registeredPairs.single.ref.collect(); |
| expect( |
| reason: 'Manually disposed object should not be deleted again by GC.', |
| TestSkDeletableMock.deleteCount, 1, |
| ); |
| }); |
| }); |
| } |
| |
| 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'); |
| } |
| |
| @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({String name}); |
| } |
| |
| class TestCountedRefOwner implements StackTraceDebugger { |
| TestCountedRefOwner(TestSkDeletable nativeObject) { |
| assert(() { |
| _debugStackTrace = StackTrace.current; |
| return true; |
| }()); |
| ref = CountedRef<TestCountedRefOwner, TestSkDeletable>( |
| nativeObject, this, 'TestCountedRefOwner'); |
| } |
| |
| TestCountedRefOwner.cloneOf(this.ref) { |
| assert(() { |
| _debugStackTrace = StackTrace.current; |
| return true; |
| }()); |
| ref.ref(this); |
| } |
| |
| @override |
| StackTrace get debugStackTrace => _debugStackTrace; |
| late StackTrace _debugStackTrace; |
| |
| late final CountedRef<TestCountedRefOwner, TestSkDeletable> ref; |
| |
| void dispose() { |
| ref.unref(this); |
| } |
| |
| TestCountedRefOwner clone() => TestCountedRefOwner.cloneOf(ref); |
| } |
| |
| class _MockNativeMemoryFinalizationRegistry implements NativeMemoryFinalizationRegistry { |
| final List<_MockPair> registeredPairs = <_MockPair>[]; |
| |
| @override |
| void register(Object owner, UniqueRef<Object> ref) { |
| registeredPairs.add(_MockPair(owner, ref)); |
| } |
| } |
| |
| class _MockPair { |
| _MockPair(this.owner, this.ref); |
| |
| Object owner; |
| UniqueRef<Object> ref; |
| } |