From 4d69a14759332eab50d537f22f7d5bcb7dfb5e65 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Sat, 8 Jul 2023 14:40:00 -0700 Subject: [PATCH 01/23] - --- doc/TROUBLESHOOT.md | 8 +++ .../src/leak_tracking/leak_tracker_model.dart | 3 + .../lib/src/leak_tracking/orchestration.dart | 3 + .../test/end_to_end_test.dart | 65 +++++++++++++++++-- .../test/test_infra/helpers.dart | 9 ++- pkgs/leak_tracker_testing/CHANGELOG.md | 4 ++ .../lib/leak_tracker_testing.dart | 1 + pkgs/leak_tracker_testing/lib/src/gc.dart | 40 ++++++++++++ pkgs/leak_tracker_testing/pubspec.yaml | 2 +- pkgs/leak_tracker_testing/test/gc_test.dart | 27 ++++++++ 10 files changed, 153 insertions(+), 9 deletions(-) create mode 100644 pkgs/leak_tracker_testing/lib/src/gc.dart create mode 100644 pkgs/leak_tracker_testing/test/gc_test.dart diff --git a/doc/TROUBLESHOOT.md b/doc/TROUBLESHOOT.md index b46bc0ad..afecf0f7 100644 --- a/doc/TROUBLESHOOT.md +++ b/doc/TROUBLESHOOT.md @@ -47,6 +47,14 @@ For collecting debugging information in tests, temporarily pass an instance of ` }, leakTrackingConfig: LeakTrackingTestConfig.debug()); ``` +Or, you can temporarily set global flag, to make all tests collecting debug information: + +``` +setUpAll(() { + collectDebugInformationForLeaks = true; +}); +``` + **Applications** For collecting debugging information in your running application, the options are: diff --git a/pkgs/leak_tracker/lib/src/leak_tracking/leak_tracker_model.dart b/pkgs/leak_tracker/lib/src/leak_tracking/leak_tracker_model.dart index 39069b5f..61564a50 100644 --- a/pkgs/leak_tracker/lib/src/leak_tracking/leak_tracker_model.dart +++ b/pkgs/leak_tracker/lib/src/leak_tracking/leak_tracker_model.dart @@ -4,6 +4,9 @@ import '../shared/shared_model.dart'; +/// If true, the leak tracker will collect debug information for leaks. +bool collectDebugInformationForLeaks = false; + /// Handler to collect leak summary. typedef LeakSummaryCallback = void Function(LeakSummary); diff --git a/pkgs/leak_tracker/lib/src/leak_tracking/orchestration.dart b/pkgs/leak_tracker/lib/src/leak_tracking/orchestration.dart index 3bd73810..1ce2d42f 100644 --- a/pkgs/leak_tracker/lib/src/leak_tracking/orchestration.dart +++ b/pkgs/leak_tracker/lib/src/leak_tracking/orchestration.dart @@ -124,6 +124,9 @@ Future withLeakTracking( } /// Forces garbage collection by aggressive memory allocation. +/// +/// The package cannot use the methos `forceGC` in the package `leak+tracker-testing`, +/// because of G3 limitations for test only code. Future _forceGC({required int gcCycles, Duration? timeout}) async { final start = clock.now(); final barrier = reachabilityBarrier; diff --git a/pkgs/leak_tracker_flutter_test/test/end_to_end_test.dart b/pkgs/leak_tracker_flutter_test/test/end_to_end_test.dart index 246e98ed..280b69c5 100644 --- a/pkgs/leak_tracker_flutter_test/test/end_to_end_test.dart +++ b/pkgs/leak_tracker_flutter_test/test/end_to_end_test.dart @@ -27,7 +27,7 @@ void main() { (WidgetTester tester) async { await tester.pumpWidget(StatelessLeakingWidget()); }, - leakTrackingConfig: LeakTrackingTestConfig( + leakTrackingTestConfig: LeakTrackingTestConfig( onLeaks: (Leaks theLeaks) { leaks = theLeaks; }, @@ -36,7 +36,42 @@ void main() { ); tearDown( - () => _verifyLeaks(leaks, expectedNotDisposed: 1, expectedNotGCed: 1), + () => _verifyLeaks( + leaks, + expectedNotDisposed: 1, + expectedNotGCed: 1, + shouldContainDebugInfo: false, + ), + ); + }); + + group('Leak tracker respects flag collectDebugInformationForLeaks', () { + late Leaks leaks; + + setUp( + () => collectDebugInformationForLeaks = true, + ); + + testWidgetsWithLeakTracking( + 'for $StatelessLeakingWidget', + (WidgetTester tester) async { + await tester.pumpWidget(StatelessLeakingWidget()); + }, + leakTrackingTestConfig: LeakTrackingTestConfig( + onLeaks: (Leaks theLeaks) { + leaks = theLeaks; + }, + failTestOnLeaks: false, + ), + ); + + tearDown( + () => _verifyLeaks( + leaks, + expectedNotDisposed: 1, + expectedNotGCed: 1, + shouldContainDebugInfo: false, + ), ); }); @@ -48,7 +83,7 @@ void main() { (WidgetTester tester) async { await tester.pumpWidget(StatelessLeakingWidget()); }, - leakTrackingConfig: LeakTrackingTestConfig( + leakTrackingTestConfig: LeakTrackingTestConfig( onLeaks: (Leaks theLeaks) { leaks = theLeaks; }, @@ -92,6 +127,7 @@ void _verifyLeaks( Leaks leaks, { int expectedNotDisposed = 0, int expectedNotGCed = 0, + required bool shouldContainDebugInfo, }) { const String linkToLeakTracker = 'https://github.com/dart-lang/leak_tracker'; @@ -104,14 +140,31 @@ void _verifyLeaks( ), ); - _verifyLeakList(leaks.notDisposed, expectedNotDisposed); - _verifyLeakList(leaks.notGCed, expectedNotGCed); + _verifyLeakList( + leaks.notDisposed, + expectedNotDisposed, + shouldContainDebugInfo, + ); + _verifyLeakList( + leaks.notGCed, + expectedNotGCed, + shouldContainDebugInfo, + ); } -void _verifyLeakList(List list, int expectedCount) { +void _verifyLeakList( + List list, + int expectedCount, + bool shouldContainDebugInfo, +) { expect(list.length, expectedCount); for (final LeakReport leak in list) { + if (shouldContainDebugInfo) { + expect(leak.context, isNotEmpty); + } else { + expect(leak.context ?? [], isEmpty); + } expect(leak.trackedClass, contains(LeakTrackedClass.library)); expect(leak.trackedClass, contains('$LeakTrackedClass')); } diff --git a/pkgs/leak_tracker_flutter_test/test/test_infra/helpers.dart b/pkgs/leak_tracker_flutter_test/test/test_infra/helpers.dart index 5a159c85..6905daef 100644 --- a/pkgs/leak_tracker_flutter_test/test/test_infra/helpers.dart +++ b/pkgs/leak_tracker_flutter_test/test/test_infra/helpers.dart @@ -32,13 +32,18 @@ void testWidgetsWithLeakTracking( bool semanticsEnabled = true, TestVariant variant = const DefaultTestVariant(), dynamic tags, - LeakTrackingTestConfig leakTrackingConfig = const LeakTrackingTestConfig(), + LeakTrackingTestConfig? leakTrackingTestConfig, }) { + final config = leakTrackingTestConfig ?? + (collectDebugInformationForLeaks + ? LeakTrackingTestConfig.debug() + : const LeakTrackingTestConfig()); + Future wrappedCallback(WidgetTester tester) async { await withFlutterLeakTracking( () async => callback(tester), tester, - leakTrackingConfig, + config, ); } diff --git a/pkgs/leak_tracker_testing/CHANGELOG.md b/pkgs/leak_tracker_testing/CHANGELOG.md index e73f77ab..41879adc 100644 --- a/pkgs/leak_tracker_testing/CHANGELOG.md +++ b/pkgs/leak_tracker_testing/CHANGELOG.md @@ -1,3 +1,7 @@ +# 1.0.1 + +* Implement `forceGC`. + # 1.0.0 * Create version. diff --git a/pkgs/leak_tracker_testing/lib/leak_tracker_testing.dart b/pkgs/leak_tracker_testing/lib/leak_tracker_testing.dart index 8dd06554..619e5ebc 100644 --- a/pkgs/leak_tracker_testing/lib/leak_tracker_testing.dart +++ b/pkgs/leak_tracker_testing/lib/leak_tracker_testing.dart @@ -2,4 +2,5 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. +export 'src/gc.dart'; export 'src/matchers.dart'; diff --git a/pkgs/leak_tracker_testing/lib/src/gc.dart b/pkgs/leak_tracker_testing/lib/src/gc.dart new file mode 100644 index 00000000..417445d2 --- /dev/null +++ b/pkgs/leak_tracker_testing/lib/src/gc.dart @@ -0,0 +1,40 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. 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:async'; +import 'dart:developer'; + +/// Forces garbage collection by aggressive memory allocation. +/// +/// Verifies that garbage collection happened using [reachabilityBarrier]. +/// Does not work in web and in release mode. +/// +/// Use [timeout] to limit waitning time. +/// Use [fullGcCycles] to force multiple garbage collections. +Future forceGC({ + Duration? timeout, + int fullGcCycles = 1, +}) async { + final Stopwatch? stopwatch = timeout == null ? null : (Stopwatch()..start()); + final int barrier = reachabilityBarrier; + + final List> storage = >[]; + + void allocateMemory() { + storage.add( + Iterable.generate(10000, (_) => DateTime.now()).toList(), + ); + if (storage.length > 100) { + storage.removeAt(0); + } + } + + while (reachabilityBarrier < barrier + fullGcCycles) { + if ((stopwatch?.elapsed ?? Duration.zero) > (timeout ?? Duration.zero)) { + throw TimeoutException('forceGC timed out', timeout); + } + await Future.delayed(Duration.zero); + allocateMemory(); + } +} diff --git a/pkgs/leak_tracker_testing/pubspec.yaml b/pkgs/leak_tracker_testing/pubspec.yaml index beb55602..2bfa727d 100644 --- a/pkgs/leak_tracker_testing/pubspec.yaml +++ b/pkgs/leak_tracker_testing/pubspec.yaml @@ -1,5 +1,5 @@ name: leak_tracker_testing -version: 1.0.0 +version: 1.0.1 description: Leak tracking code intended for usage in tests. repository: https://github.com/dart-lang/leak_tracker/tree/main/pkgs/leak_tracker_testing diff --git a/pkgs/leak_tracker_testing/test/gc_test.dart b/pkgs/leak_tracker_testing/test/gc_test.dart new file mode 100644 index 00000000..f6b98ec1 --- /dev/null +++ b/pkgs/leak_tracker_testing/test/gc_test.dart @@ -0,0 +1,27 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. 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:async'; + +import 'package:leak_tracker_testing/leak_tracker_testing.dart'; +import 'package:test/test.dart'; + +void main() { + test('forceGC forces gc', () async { + Object? myObject = [1, 2, 3, 4, 5]; + final ref = WeakReference(myObject); + myObject = null; + + await forceGC(); + + expect(ref.target, null); + }); + + test('forceGC times out', () async { + await expectLater( + forceGC(timeout: Duration.zero), + throwsA(isA()), + ); + }); +} From adeb80acfda3eaf612123ce611c29cca50a09dc8 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Sat, 8 Jul 2023 15:06:58 -0700 Subject: [PATCH 02/23] Update end_to_end_test.dart --- pkgs/leak_tracker_flutter_test/test/end_to_end_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/leak_tracker_flutter_test/test/end_to_end_test.dart b/pkgs/leak_tracker_flutter_test/test/end_to_end_test.dart index 280b69c5..855fa623 100644 --- a/pkgs/leak_tracker_flutter_test/test/end_to_end_test.dart +++ b/pkgs/leak_tracker_flutter_test/test/end_to_end_test.dart @@ -163,7 +163,7 @@ void _verifyLeakList( if (shouldContainDebugInfo) { expect(leak.context, isNotEmpty); } else { - expect(leak.context ?? [], isEmpty); + expect(leak.context ?? {}, isEmpty); } expect(leak.trackedClass, contains(LeakTrackedClass.library)); expect(leak.trackedClass, contains('$LeakTrackedClass')); From d7a0eec9041a6247a1242e74b5e9db1fea476a6a Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Sun, 9 Jul 2023 10:49:02 -0700 Subject: [PATCH 03/23] - --- doc/TROUBLESHOOT.md | 23 ++++++++++++++++--- .../lib/src/leak_tracking/_formatting.dart | 4 ++-- .../lib/src/leak_tracking/orchestration.dart | 16 +++++++++++++ pkgs/leak_tracker_testing/lib/src/gc.dart | 6 +++++ 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/doc/TROUBLESHOOT.md b/doc/TROUBLESHOOT.md index afecf0f7..a734fd60 100644 --- a/doc/TROUBLESHOOT.md +++ b/doc/TROUBLESHOOT.md @@ -9,11 +9,11 @@ This page describes how to troubleshoot memory leaks. See other information on m If leak tracker detected a leak in your application or test, first check if the leak matches a [known simple case](#known-simple-cases), and, if no, switch to [more complicated troubleshooting](#more-complicated-cases). -## Known simple cases +## Check known simple cases ### 1. The test holds a disposed object -TODO: add steps. +TODO: add example and steps. ## Collect additional information @@ -44,7 +44,7 @@ For collecting debugging information in tests, temporarily pass an instance of ` ``` testWidgets('My test', (WidgetTester tester) async { ... - }, leakTrackingConfig: LeakTrackingTestConfig.debug()); + }, leakTrackingTestConfig: LeakTrackingTestConfig.debug()); ``` Or, you can temporarily set global flag, to make all tests collecting debug information: @@ -64,6 +64,23 @@ For collecting debugging information in your running application, the options ar TODO: link DevTools documentation with explanation +## Verify object references + +If you expect an object to be not referenced at some point, +you may temporary add assertion for this, and collect retaining path, if the object is held: + +``` +final ref = WeakReference(myObject); +myObject = null; +await forceGC(); +if (ref.target == null) { + throw StateError('Validated that myObject is not held by another object'); +} else { + print(await retainingPath(ref)); + throw StateError('myObject is reachable from root. See output for the retaining path'); +} +``` + ## Known complicated cases ### 1. More than one closure context diff --git a/pkgs/leak_tracker/lib/src/leak_tracking/_formatting.dart b/pkgs/leak_tracker/lib/src/leak_tracking/_formatting.dart index a40bcba1..efb6e4ca 100644 --- a/pkgs/leak_tracker/lib/src/leak_tracking/_formatting.dart +++ b/pkgs/leak_tracker/lib/src/leak_tracking/_formatting.dart @@ -11,7 +11,7 @@ import '../shared/_util.dart'; String contextToString(Object? object) { return switch (object) { StackTrace() => _formatStackTrace(object), - RetainingPath() => _retainingPathToString(object), + RetainingPath() => retainingPathToString(object), _ => object.toString(), }; } @@ -42,7 +42,7 @@ String removeLeakTrackingLines(String stackTrace) { return lines.join('\n'); } -String _retainingPathToString(RetainingPath retainingPath) { +String retainingPathToString(RetainingPath retainingPath) { final StringBuffer buffer = StringBuffer(); buffer.writeln( 'References that retain the object from garbage collection.', diff --git a/pkgs/leak_tracker/lib/src/leak_tracking/orchestration.dart b/pkgs/leak_tracker/lib/src/leak_tracking/orchestration.dart index 1ce2d42f..2b3abcfd 100644 --- a/pkgs/leak_tracker/lib/src/leak_tracking/orchestration.dart +++ b/pkgs/leak_tracker/lib/src/leak_tracking/orchestration.dart @@ -8,9 +8,11 @@ import 'dart:developer'; import 'package:clock/clock.dart'; import '../shared/shared_model.dart'; +import '_formatting.dart'; import '_gc_counter.dart'; import 'leak_tracker.dart'; import 'leak_tracker_model.dart'; +import 'retaining_path/_retaining_path.dart'; /// Asynchronous callback. /// @@ -146,3 +148,17 @@ Future _forceGC({required int gcCycles, Duration? timeout}) async { allocateMemory(); } } + +/// Returns nicely formatted retaining path for the [ref.target]. +/// +/// If the object is garbage collected or not retained, returns null. +Future retainingPath(WeakReference ref) async { + if (ref.target == null) return null; + final path = await obtainRetainingPath( + ref.target.runtimeType, + identityHashCode(ref.target), + ); + + if (path == null) return null; + return retainingPathToString(path); +} diff --git a/pkgs/leak_tracker_testing/lib/src/gc.dart b/pkgs/leak_tracker_testing/lib/src/gc.dart index 417445d2..4f1007c6 100644 --- a/pkgs/leak_tracker_testing/lib/src/gc.dart +++ b/pkgs/leak_tracker_testing/lib/src/gc.dart @@ -12,6 +12,12 @@ import 'dart:developer'; /// /// Use [timeout] to limit waitning time. /// Use [fullGcCycles] to force multiple garbage collections. +/// +/// The methot is useable for testing in combination with [WeakReference] to ensure +/// an object is not held by another object from garbage collection. +/// +/// For code example see ../../test/gc_test.dart. +/// TODO(polina-c): add link to GitHub when this code gets merged. Future forceGC({ Duration? timeout, int fullGcCycles = 1, From c72d8ec537801c8abd7dd3328197e49d8e8849ae Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Sun, 9 Jul 2023 11:47:44 -0700 Subject: [PATCH 04/23] - --- doc/TROUBLESHOOT.md | 8 ++-- .../lib/src/leak_tracking/_gc_counter.dart | 2 +- .../lib/src/leak_tracking/orchestration.dart | 46 +++++++++++++------ .../lib/leak_tracker_testing.dart | 1 - pkgs/leak_tracker_testing/lib/src/gc.dart | 46 ------------------- pkgs/leak_tracker_testing/test/gc_test.dart | 27 ----------- 6 files changed, 37 insertions(+), 93 deletions(-) delete mode 100644 pkgs/leak_tracker_testing/lib/src/gc.dart delete mode 100644 pkgs/leak_tracker_testing/test/gc_test.dart diff --git a/doc/TROUBLESHOOT.md b/doc/TROUBLESHOOT.md index a734fd60..0a4b8e62 100644 --- a/doc/TROUBLESHOOT.md +++ b/doc/TROUBLESHOOT.md @@ -67,17 +67,17 @@ TODO: link DevTools documentation with explanation ## Verify object references If you expect an object to be not referenced at some point, -you may temporary add assertion for this, and collect retaining path, if the object is held: +but tot sure, you can validate it by temporary adding assertion: ``` final ref = WeakReference(myObject); myObject = null; await forceGC(); if (ref.target == null) { - throw StateError('Validated that myObject is not held by another object'); + throw StateError('Validated that myObject is not held from garbage collection.'); } else { - print(await retainingPath(ref)); - throw StateError('myObject is reachable from root. See output for the retaining path'); + print(await formattedRetainingPath(ref)); + throw StateError('myObject is reachable from root. See console output for the retaining path.'); } ``` diff --git a/pkgs/leak_tracker/lib/src/leak_tracking/_gc_counter.dart b/pkgs/leak_tracker/lib/src/leak_tracking/_gc_counter.dart index 6de94e0b..6f10eb55 100644 --- a/pkgs/leak_tracker/lib/src/leak_tracking/_gc_counter.dart +++ b/pkgs/leak_tracker/lib/src/leak_tracking/_gc_counter.dart @@ -10,7 +10,7 @@ class GcCounter { int get gcCount => reachabilityBarrier; } -/// Delta of GC time, enough for a non reachable object to be GCed. +/// Delta of GC cycles, enough for a non reachable object to be GCed. /// /// Theoretically, 2 should be enough, however it gives false positives /// if there is no activity in the application for ~5 minutes. diff --git a/pkgs/leak_tracker/lib/src/leak_tracking/orchestration.dart b/pkgs/leak_tracker/lib/src/leak_tracking/orchestration.dart index 2b3abcfd..22116c31 100644 --- a/pkgs/leak_tracker/lib/src/leak_tracking/orchestration.dart +++ b/pkgs/leak_tracker/lib/src/leak_tracking/orchestration.dart @@ -102,8 +102,8 @@ Future withLeakTracking( await checkNonGCed(); } - await _forceGC( - gcCycles: gcCountBuffer, + await forceGC( + fullGcCycles: gcCountBuffer, timeout: timeoutForFinalGarbageCollection, ); @@ -127,24 +127,40 @@ Future withLeakTracking( /// Forces garbage collection by aggressive memory allocation. /// -/// The package cannot use the methos `forceGC` in the package `leak+tracker-testing`, -/// because of G3 limitations for test only code. -Future _forceGC({required int gcCycles, Duration? timeout}) async { - final start = clock.now(); - final barrier = reachabilityBarrier; +/// Verifies that garbage collection happened using [reachabilityBarrier]. +/// Does not work in web and in release mode. +/// +/// Use [timeout] to limit waitning time. +/// Use [fullGcCycles] to force multiple garbage collections. +/// +/// The methot is useable for testing in combination with [WeakReference] to ensure +/// an object is not held by another object from garbage collection. +/// +/// For code example see +/// https://github.com/dart-lang/leak_tracker/blob/main/doc/TROUBLESHOOT.md +Future forceGC({ + Duration? timeout, + int fullGcCycles = 1, +}) async { + final Stopwatch? stopwatch = timeout == null ? null : (Stopwatch()..start()); + final int barrier = reachabilityBarrier; - final storage = >[]; + final List> storage = >[]; void allocateMemory() { - storage.add(Iterable.generate(10000, (_) => DateTime.now()).toList()); - if (storage.length > 100) storage.removeAt(0); + storage.add( + Iterable.generate(10000, (_) => DateTime.now()).toList(), + ); + if (storage.length > 100) { + storage.removeAt(0); + } } - while (reachabilityBarrier < barrier + gcCycles) { - if (timeout != null && clock.now().difference(start) > timeout) { + while (reachabilityBarrier < barrier + fullGcCycles) { + if ((stopwatch?.elapsed ?? Duration.zero) > (timeout ?? Duration.zero)) { throw TimeoutException('forceGC timed out', timeout); } - await Future.delayed(const Duration()); + await Future.delayed(Duration.zero); allocateMemory(); } } @@ -152,7 +168,9 @@ Future _forceGC({required int gcCycles, Duration? timeout}) async { /// Returns nicely formatted retaining path for the [ref.target]. /// /// If the object is garbage collected or not retained, returns null. -Future retainingPath(WeakReference ref) async { +/// +/// Does not work in web and in release mode. +Future formattedRetainingPath(WeakReference ref) async { if (ref.target == null) return null; final path = await obtainRetainingPath( ref.target.runtimeType, diff --git a/pkgs/leak_tracker_testing/lib/leak_tracker_testing.dart b/pkgs/leak_tracker_testing/lib/leak_tracker_testing.dart index 619e5ebc..8dd06554 100644 --- a/pkgs/leak_tracker_testing/lib/leak_tracker_testing.dart +++ b/pkgs/leak_tracker_testing/lib/leak_tracker_testing.dart @@ -2,5 +2,4 @@ // for details. All rights reserved. Use of this source code is governed by a // BSD-style license that can be found in the LICENSE file. -export 'src/gc.dart'; export 'src/matchers.dart'; diff --git a/pkgs/leak_tracker_testing/lib/src/gc.dart b/pkgs/leak_tracker_testing/lib/src/gc.dart deleted file mode 100644 index 4f1007c6..00000000 --- a/pkgs/leak_tracker_testing/lib/src/gc.dart +++ /dev/null @@ -1,46 +0,0 @@ -// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file -// for details. 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:async'; -import 'dart:developer'; - -/// Forces garbage collection by aggressive memory allocation. -/// -/// Verifies that garbage collection happened using [reachabilityBarrier]. -/// Does not work in web and in release mode. -/// -/// Use [timeout] to limit waitning time. -/// Use [fullGcCycles] to force multiple garbage collections. -/// -/// The methot is useable for testing in combination with [WeakReference] to ensure -/// an object is not held by another object from garbage collection. -/// -/// For code example see ../../test/gc_test.dart. -/// TODO(polina-c): add link to GitHub when this code gets merged. -Future forceGC({ - Duration? timeout, - int fullGcCycles = 1, -}) async { - final Stopwatch? stopwatch = timeout == null ? null : (Stopwatch()..start()); - final int barrier = reachabilityBarrier; - - final List> storage = >[]; - - void allocateMemory() { - storage.add( - Iterable.generate(10000, (_) => DateTime.now()).toList(), - ); - if (storage.length > 100) { - storage.removeAt(0); - } - } - - while (reachabilityBarrier < barrier + fullGcCycles) { - if ((stopwatch?.elapsed ?? Duration.zero) > (timeout ?? Duration.zero)) { - throw TimeoutException('forceGC timed out', timeout); - } - await Future.delayed(Duration.zero); - allocateMemory(); - } -} diff --git a/pkgs/leak_tracker_testing/test/gc_test.dart b/pkgs/leak_tracker_testing/test/gc_test.dart deleted file mode 100644 index f6b98ec1..00000000 --- a/pkgs/leak_tracker_testing/test/gc_test.dart +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file -// for details. 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:async'; - -import 'package:leak_tracker_testing/leak_tracker_testing.dart'; -import 'package:test/test.dart'; - -void main() { - test('forceGC forces gc', () async { - Object? myObject = [1, 2, 3, 4, 5]; - final ref = WeakReference(myObject); - myObject = null; - - await forceGC(); - - expect(ref.target, null); - }); - - test('forceGC times out', () async { - await expectLater( - forceGC(timeout: Duration.zero), - throwsA(isA()), - ); - }); -} From 8822711589052a7cd96a78b3fefb7cddc7c388e6 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Sun, 9 Jul 2023 13:35:25 -0700 Subject: [PATCH 05/23] - --- .vscode/launch.json | 6 ++++ .../retaining_path/_retaining_path.dart | 12 ++++++-- pkgs/leak_tracker/pubspec.yaml | 2 +- .../leak_tracking/orchestration_test.dart | 15 ++++++++++ .../retaining_path/_retaining_path_test.dart | 10 ++++++- .../leak_tracking/orchestration_test.dart | 19 +++++++++++++ pkgs/leak_tracker_testing/CHANGELOG.md | 4 --- pkgs/leak_tracker_testing/pubspec.yaml | 2 +- tool/analyze.sh | 7 ++--- tool/pub_get.sh | 28 +++++++++++++++++++ 10 files changed, 90 insertions(+), 15 deletions(-) create mode 100644 pkgs/leak_tracker/test/debug/leak_tracking/orchestration_test.dart create mode 100644 tool/pub_get.sh diff --git a/.vscode/launch.json b/.vscode/launch.json index a8bfedc4..5ef6ce9e 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -11,6 +11,12 @@ "type": "dart", "program": "main.dart", }, + { + "name": "pub_get", + "request": "launch", + "type": "node-terminal", + "command": "sh tool/pub_get.sh", + }, { "name": "minimal_flutter", "cwd": "examples/minimal_flutter", diff --git a/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_retaining_path.dart b/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_retaining_path.dart index d13bbb07..85cb555a 100644 --- a/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_retaining_path.dart +++ b/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_retaining_path.dart @@ -53,10 +53,12 @@ Future<_ItemInIsolate?> _objectInIsolate( objRef is InstanceRef && objRef.identityHashCode == object.code, ); if (result != null) { + throw ('found!!!!'); + return _ItemInIsolate(isolateId: theClass.isolateId, itemId: result.id!); } } - + throw ('not found!!!!'); return null; } @@ -94,8 +96,12 @@ Future> _findClasses( throw StateError('Could not get list of classes.'); } - final filtered = - classes.classes?.where((ref) => runtimeClassName == ref.name) ?? []; + // final filtered = classes.classes + // ?.where((ref) => (ref.name?.contains('List') ?? false)) ?? + // []; + + final filtered = classes.classes ?? []; + result.addAll( filtered.map( (classRef) => diff --git a/pkgs/leak_tracker/pubspec.yaml b/pkgs/leak_tracker/pubspec.yaml index 94409a99..31a1ab27 100644 --- a/pkgs/leak_tracker/pubspec.yaml +++ b/pkgs/leak_tracker/pubspec.yaml @@ -13,7 +13,7 @@ dependencies: logging: ^1.1.1 meta: ^1.8.0 path: ^1.8.3 - vm_service: '>=11.6.0 <13.0.0' + vm_service: '>=11.7.2 <13.0.0' web_socket_channel: ^2.1.0 dev_dependencies: diff --git a/pkgs/leak_tracker/test/debug/leak_tracking/orchestration_test.dart b/pkgs/leak_tracker/test/debug/leak_tracking/orchestration_test.dart new file mode 100644 index 00000000..a528c069 --- /dev/null +++ b/pkgs/leak_tracker/test/debug/leak_tracking/orchestration_test.dart @@ -0,0 +1,15 @@ +// Copyright (c) 2023, the Dart project authors. Please see the AUTHORS file +// for details. 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:leak_tracker/leak_tracker.dart'; +import 'package:test/test.dart'; + +void main() { + test('formattedRetainingPath returns path', () async { + final Object myObject = [1, 2, 3, 4, 5]; + final path = await formattedRetainingPath(WeakReference(myObject)); + + print(path); + }); +} diff --git a/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart b/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart index d06b9c42..622f388c 100644 --- a/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart +++ b/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart @@ -31,13 +31,21 @@ void main() { await subscription.cancel(); }); - test('$MyClass instance can be found.', () async { + test('Path for $MyClass instance is found.', () async { final instance = MyClass(); final path = await obtainRetainingPath(MyClass, identityHashCode(instance)); expect(path!.elements, isNotEmpty); }); + test('Path type with generic arg is found.', () async { + final instance = [1, 2, 3, 4, 5]; + + final path = await obtainRetainingPath(MyClass, identityHashCode(instance)); + print(instance); + expect(path!.elements, isNotEmpty); + }); + test('Connection is happening just once', () async { final instance1 = MyClass(); final instance2 = MyClass(); diff --git a/pkgs/leak_tracker/test/release/leak_tracking/orchestration_test.dart b/pkgs/leak_tracker/test/release/leak_tracking/orchestration_test.dart index 564e2cc4..78fe6aff 100644 --- a/pkgs/leak_tracker/test/release/leak_tracking/orchestration_test.dart +++ b/pkgs/leak_tracker/test/release/leak_tracking/orchestration_test.dart @@ -2,6 +2,8 @@ // for details. 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:async'; + import 'package:leak_tracker/src/leak_tracking/orchestration.dart'; import 'package:test/test.dart'; @@ -19,4 +21,21 @@ void main() { await withLeakTracking(() async {}); }); + + test('forceGC forces gc', () async { + Object? myObject = [1, 2, 3, 4, 5]; + final ref = WeakReference(myObject); + myObject = null; + + await forceGC(); + + expect(ref.target, null); + }); + + test('forceGC times out', () async { + await expectLater( + forceGC(timeout: Duration.zero), + throwsA(isA()), + ); + }); } diff --git a/pkgs/leak_tracker_testing/CHANGELOG.md b/pkgs/leak_tracker_testing/CHANGELOG.md index 41879adc..e73f77ab 100644 --- a/pkgs/leak_tracker_testing/CHANGELOG.md +++ b/pkgs/leak_tracker_testing/CHANGELOG.md @@ -1,7 +1,3 @@ -# 1.0.1 - -* Implement `forceGC`. - # 1.0.0 * Create version. diff --git a/pkgs/leak_tracker_testing/pubspec.yaml b/pkgs/leak_tracker_testing/pubspec.yaml index 2bfa727d..beb55602 100644 --- a/pkgs/leak_tracker_testing/pubspec.yaml +++ b/pkgs/leak_tracker_testing/pubspec.yaml @@ -1,5 +1,5 @@ name: leak_tracker_testing -version: 1.0.1 +version: 1.0.0 description: Leak tracking code intended for usage in tests. repository: https://github.com/dart-lang/leak_tracker/tree/main/pkgs/leak_tracker_testing diff --git a/tool/analyze.sh b/tool/analyze.sh index f4e26984..74d1ee19 100644 --- a/tool/analyze.sh +++ b/tool/analyze.sh @@ -7,27 +7,24 @@ # Fast fail the script on failures. set -ex +sh pub_get.sh + cd examples/autosnapshotting -flutter pub get flutter analyze --fatal-infos cd - cd examples/minimal_flutter -flutter pub get flutter analyze --fatal-infos cd - cd pkgs/leak_tracker -dart pub get dart analyze --fatal-infos cd - cd pkgs/leak_tracker_flutter_test -flutter pub get flutter analyze --fatal-infos cd - cd pkgs/leak_tracker_testing -flutter pub get dart analyze --fatal-infos cd - diff --git a/tool/pub_get.sh b/tool/pub_get.sh new file mode 100644 index 00000000..5df56ec2 --- /dev/null +++ b/tool/pub_get.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# Copyright 2023 The Chromium Authors. All rights reserved. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# Fast fail the script on failures. +set -ex + +cd examples/autosnapshotting +flutter pub get +cd - + +cd examples/minimal_flutter +flutter pub get +cd - + +cd pkgs/leak_tracker +dart pub get +cd - + +cd pkgs/leak_tracker_flutter_test +flutter pub get +cd - + +cd pkgs/leak_tracker_testing +flutter pub get +cd - From 8344b10627e0fc2da96ebe898f491025c4d1ac93 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Sun, 9 Jul 2023 14:17:25 -0700 Subject: [PATCH 06/23] Update _retaining_path.dart --- .../retaining_path/_retaining_path.dart | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_retaining_path.dart b/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_retaining_path.dart index 85cb555a..54d98ec0 100644 --- a/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_retaining_path.dart +++ b/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_retaining_path.dart @@ -40,6 +40,12 @@ Future<_ItemInIsolate?> _objectInIsolate( for (final theClass in classes) { const pathLengthLimit = 10000000; + + // TODO(polina-c): remove when issue is fixed + // https://github.com/dart-lang/sdk/issues/52893 + if (theClass.name == 'TypeParameters') continue; + + print('requesting getInstances for ${theClass.name}'); final instances = (await connection.service.getInstances( theClass.isolateId, theClass.itemId, @@ -66,13 +72,16 @@ Future<_ItemInIsolate?> _objectInIsolate( /// /// It can be class or object. class _ItemInIsolate { - _ItemInIsolate({required this.isolateId, required this.itemId}); + _ItemInIsolate({required this.isolateId, required this.itemId, this.name}); /// Id of the isolate. final String isolateId; /// Id of the item in the isolate. final String itemId; + + /// Name of the item, for debugging purposes + final String? name; } Future> _findClasses( @@ -104,8 +113,11 @@ Future> _findClasses( result.addAll( filtered.map( - (classRef) => - _ItemInIsolate(itemId: classRef.id!, isolateId: isolateId), + (classRef) => _ItemInIsolate( + itemId: classRef.id!, + isolateId: isolateId, + name: classRef.name, + ), ), ); } From c9562b8c632c6d99edba6f17200f583886e866ac Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Sun, 9 Jul 2023 15:14:54 -0700 Subject: [PATCH 07/23] - --- .../retaining_path/_retaining_path.dart | 12 +++--- .../retaining_path/_retaining_path_test.dart | 37 ++++++++++++++++++- 2 files changed, 40 insertions(+), 9 deletions(-) diff --git a/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_retaining_path.dart b/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_retaining_path.dart index 54d98ec0..266667cd 100644 --- a/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_retaining_path.dart +++ b/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_retaining_path.dart @@ -44,8 +44,8 @@ Future<_ItemInIsolate?> _objectInIsolate( // TODO(polina-c): remove when issue is fixed // https://github.com/dart-lang/sdk/issues/52893 if (theClass.name == 'TypeParameters') continue; + print('looking for ${theClass.name}'); - print('requesting getInstances for ${theClass.name}'); final instances = (await connection.service.getInstances( theClass.isolateId, theClass.itemId, @@ -54,12 +54,10 @@ Future<_ItemInIsolate?> _objectInIsolate( .instances ?? []; - final result = instances.firstWhereOrNull( - (objRef) => - objRef is InstanceRef && objRef.identityHashCode == object.code, - ); + final result = instances.firstWhereOrNull((ObjRef objRef) => + objRef is InstanceRef && objRef.identityHashCode == object.code); if (result != null) { - throw ('found!!!!'); + throw ('found!!!! for ${theClass.name}'); return _ItemInIsolate(isolateId: theClass.isolateId, itemId: result.id!); } @@ -80,7 +78,7 @@ class _ItemInIsolate { /// Id of the item in the isolate. final String itemId; - /// Name of the item, for debugging purposes + /// Name of the item, for debugging purposes. final String? name; } diff --git a/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart b/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart index 622f388c..04b41848 100644 --- a/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart +++ b/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart @@ -40,12 +40,45 @@ void main() { test('Path type with generic arg is found.', () async { final instance = [1, 2, 3, 4, 5]; - final path = await obtainRetainingPath(MyClass, identityHashCode(instance)); - print(instance); expect(path!.elements, isNotEmpty); }); + test( + 'Instance of array is found.', + () async { + final myClass = MyClass(); + final instance = [1, 2, 3, 4, 5]; + + final connection = await connect(); + print(connection.isolates.length); + final isolateId = connection.isolates[0]; + var classList = await connection.service.getClassList(isolateId); + + // In the beginning list of classes may be empty. + while (classList.classes?.isEmpty ?? true) { + await Future.delayed(const Duration(milliseconds: 100)); + classList = await connection.service.getClassList(isolateId); + } + if (classList.classes?.isEmpty ?? true) { + throw StateError('Could not get list of classes.'); + } + + final classes = classList.classes!; + + final path = await obtainRetainingPath( + instance.runtimeType, identityHashCode(instance)); + print(instance); + instance.add(7); + expect(path!.elements, isNotEmpty); + + // To make sure instance is not const. + instance.add(6); + instance.add(7); + }, + timeout: const Timeout(Duration(minutes: 20)), + ); + test('Connection is happening just once', () async { final instance1 = MyClass(); final instance2 = MyClass(); From ac68c5c068b00fb6b989ba4a96c678784d98fd5a Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Sun, 9 Jul 2023 15:49:33 -0700 Subject: [PATCH 08/23] - --- .../retaining_path/_connection.dart | 17 +++-- .../retaining_path/_retaining_path.dart | 21 ++--- .../retaining_path/_retaining_path_test.dart | 76 +++++++++++++------ 3 files changed, 77 insertions(+), 37 deletions(-) diff --git a/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_connection.dart b/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_connection.dart index eab35ce9..a938abc9 100644 --- a/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_connection.dart +++ b/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_connection.dart @@ -14,10 +14,17 @@ final _log = Logger('_connection.dart'); class Connection { Connection(this.service, this.isolates); - final List isolates; + final List isolates; final VmService service; } +class IsolateInfo { + IsolateInfo({required this.id, required this.name}); + + final String id; + final String name; +} + Completer? _completer; void disconnect() => _completer = null; @@ -45,7 +52,7 @@ Future connect() async { throw error ?? Exception('Error connecting to service protocol'); }); await service.getVersion(); // Warming up and validating the connection. - final isolates = await _getIdForTwoIsolates(service); + final isolates = await _getTwoIsolates(service); final result = Connection(service, isolates); completer.complete(result); @@ -57,10 +64,10 @@ Future connect() async { /// Depending on environment (command line / IDE, Flutter / Dart), isolates may have different names, /// and there can be one or two. Sometimes the second one appears with latency. /// And sometimes there are two isolates with name 'main'. -Future> _getIdForTwoIsolates(VmService service) async { +Future> _getTwoIsolates(VmService service) async { _log.info('Started loading isolates...'); - final result = []; + final result = []; const isolatesToGet = 2; const watingTime = Duration(seconds: 2); @@ -69,7 +76,7 @@ Future> _getIdForTwoIsolates(VmService service) async { result.clear(); await _forEachIsolate( service, - (IsolateRef r) async => result.add(r.id!), + (IsolateRef r) async => result.add(IsolateInfo(id: r.id!, name: r.name!)), ); if (result.length < isolatesToGet) { await Future.delayed(const Duration(milliseconds: 100)); diff --git a/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_retaining_path.dart b/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_retaining_path.dart index 266667cd..ba830d4d 100644 --- a/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_retaining_path.dart +++ b/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_retaining_path.dart @@ -17,7 +17,7 @@ Future obtainRetainingPath(Type type, int code) async { if (theObject == null) return null; final result = await connection.service.getRetainingPath( - theObject.isolateId, + theObject.isolateInfo.id, theObject.itemId, 100000, ); @@ -47,7 +47,7 @@ Future<_ItemInIsolate?> _objectInIsolate( print('looking for ${theClass.name}'); final instances = (await connection.service.getInstances( - theClass.isolateId, + theClass.isolateInfo.id, theClass.itemId, pathLengthLimit, )) @@ -59,7 +59,8 @@ Future<_ItemInIsolate?> _objectInIsolate( if (result != null) { throw ('found!!!! for ${theClass.name}'); - return _ItemInIsolate(isolateId: theClass.isolateId, itemId: result.id!); + return _ItemInIsolate( + isolateInfo: theClass.isolateInfo, itemId: result.id!); } } throw ('not found!!!!'); @@ -70,10 +71,10 @@ Future<_ItemInIsolate?> _objectInIsolate( /// /// It can be class or object. class _ItemInIsolate { - _ItemInIsolate({required this.isolateId, required this.itemId, this.name}); + _ItemInIsolate({required this.isolateInfo, required this.itemId, this.name}); - /// Id of the isolate. - final String isolateId; + /// The isolate. + final IsolateInfo isolateInfo; /// Id of the item in the isolate. final String itemId; @@ -88,8 +89,8 @@ Future> _findClasses( ) async { final result = <_ItemInIsolate>[]; - for (final isolateId in connection.isolates) { - var classes = await connection.service.getClassList(isolateId); + for (final isolateInfo in connection.isolates) { + var classes = await connection.service.getClassList(isolateInfo.id); const watingTime = Duration(seconds: 2); final stopwatch = Stopwatch()..start(); @@ -97,7 +98,7 @@ Future> _findClasses( // In the beginning list of classes may be empty. while (classes.classes?.isEmpty ?? true && stopwatch.elapsed < watingTime) { await Future.delayed(const Duration(milliseconds: 100)); - classes = await connection.service.getClassList(isolateId); + classes = await connection.service.getClassList(isolateInfo.id); } if (classes.classes?.isEmpty ?? true) { throw StateError('Could not get list of classes.'); @@ -113,7 +114,7 @@ Future> _findClasses( filtered.map( (classRef) => _ItemInIsolate( itemId: classRef.id!, - isolateId: isolateId, + isolateInfo: isolateInfo, name: classRef.name, ), ), diff --git a/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart b/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart index 04b41848..f2bde5e6 100644 --- a/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart +++ b/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart @@ -4,10 +4,12 @@ import 'dart:async'; +import 'package:collection/collection.dart'; import 'package:leak_tracker/src/leak_tracking/retaining_path/_connection.dart'; import 'package:leak_tracker/src/leak_tracking/retaining_path/_retaining_path.dart'; import 'package:logging/logging.dart'; import 'package:test/test.dart'; +import 'package:vm_service/vm_service.dart' hide LogRecord; class MyClass { MyClass(); @@ -44,37 +46,67 @@ void main() { expect(path!.elements, isNotEmpty); }); + ObjRef? _find(List instances, int code) { + return instances.firstWhereOrNull( + (ObjRef objRef) => + objRef is InstanceRef && objRef.identityHashCode == code, + ); + } + test( 'Instance of array is found.', () async { final myClass = MyClass(); - final instance = [1, 2, 3, 4, 5]; + ObjRef? myClassRef; + + final myList = [1, 2, 3, 4, 5]; + ObjRef? myListRef; final connection = await connect(); - print(connection.isolates.length); - final isolateId = connection.isolates[0]; - var classList = await connection.service.getClassList(isolateId); - - // In the beginning list of classes may be empty. - while (classList.classes?.isEmpty ?? true) { - await Future.delayed(const Duration(milliseconds: 100)); - classList = await connection.service.getClassList(isolateId); + print(connection.isolates.map((i) => '${i.name}-${i.id}')); + + for (final isolate in connection.isolates) { + var classList = await connection.service.getClassList(isolate.id); + // In the beginning list of classes may be empty. + while (classList.classes?.isEmpty ?? true) { + await Future.delayed(const Duration(milliseconds: 100)); + classList = await connection.service.getClassList(isolate.id); + } + if (classList.classes?.isEmpty ?? true) { + throw StateError('Could not get list of classes.'); + } + + final classes = classList.classes!; + + for (final theClass in classes) { + const pathLengthLimit = 10000000; + + // TODO(polina-c): remove when issue is fixed + // https://github.com/dart-lang/sdk/issues/52893 + if (theClass.name == 'TypeParameters') continue; + + final instances = (await connection.service.getInstances( + isolate.id, + theClass.id!, + pathLengthLimit, + )) + .instances ?? + []; + + myClassRef ??= _find(instances, identityHashCode(myClass)); + myListRef ??= _find(instances, identityHashCode(myList)); + + if (myClassRef != null && myListRef != null) { + throw 'Found both instances!!!'; + } + } } - if (classList.classes?.isEmpty ?? true) { - throw StateError('Could not get list of classes.'); - } - - final classes = classList.classes!; - final path = await obtainRetainingPath( - instance.runtimeType, identityHashCode(instance)); - print(instance); - instance.add(7); - expect(path!.elements, isNotEmpty); + print('myClassRef: $myClassRef, myListRef: $myListRef'); - // To make sure instance is not const. - instance.add(6); - instance.add(7); + // To make sure [myList] is not const. + myList.add(6); + myList.add(7); }, timeout: const Timeout(Duration(minutes: 20)), ); From b4bf26e2b4c52c20d9fb631cd7ea97d8ca630e65 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Sun, 9 Jul 2023 16:02:22 -0700 Subject: [PATCH 09/23] Update _retaining_path_test.dart --- .../leak_tracking/retaining_path/_retaining_path_test.dart | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart b/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart index f2bde5e6..7e8db470 100644 --- a/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart +++ b/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart @@ -79,8 +79,6 @@ void main() { final classes = classList.classes!; for (final theClass in classes) { - const pathLengthLimit = 10000000; - // TODO(polina-c): remove when issue is fixed // https://github.com/dart-lang/sdk/issues/52893 if (theClass.name == 'TypeParameters') continue; @@ -88,7 +86,7 @@ void main() { final instances = (await connection.service.getInstances( isolate.id, theClass.id!, - pathLengthLimit, + 1000000000000, )) .instances ?? []; From 0f0765b55064921e9cb62c870f332095f1385a10 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Sun, 9 Jul 2023 16:03:10 -0700 Subject: [PATCH 10/23] Update _retaining_path_test.dart --- .../leak_tracking/retaining_path/_retaining_path_test.dart | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart b/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart index 7e8db470..25ff5ef4 100644 --- a/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart +++ b/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart @@ -100,7 +100,8 @@ void main() { } } - print('myClassRef: $myClassRef, myListRef: $myListRef'); + print('myClassRef: $myClassRef'); + print('myListRef: $myListRef'); // To make sure [myList] is not const. myList.add(6); From 9f3aa00593c4549d38185f1553c8aee4bf1be52b Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Sun, 9 Jul 2023 16:58:46 -0700 Subject: [PATCH 11/23] Update _retaining_path_test.dart --- .../retaining_path/_retaining_path_test.dart | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart b/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart index 25ff5ef4..bcc6f421 100644 --- a/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart +++ b/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart @@ -59,7 +59,7 @@ void main() { final myClass = MyClass(); ObjRef? myClassRef; - final myList = [1, 2, 3, 4, 5]; + final myList = [DateTime.now(), DateTime.now()]; ObjRef? myListRef; final connection = await connect(); @@ -79,6 +79,7 @@ void main() { final classes = classList.classes!; for (final theClass in classes) { + print('Checking class ${theClass.name}...'); // TODO(polina-c): remove when issue is fixed // https://github.com/dart-lang/sdk/issues/52893 if (theClass.name == 'TypeParameters') continue; @@ -92,7 +93,13 @@ void main() { []; myClassRef ??= _find(instances, identityHashCode(myClass)); - myListRef ??= _find(instances, identityHashCode(myList)); + + if (myListRef == null) { + myListRef = _find(instances, identityHashCode(myList)); + if (myListRef != null) { + print('Found myListRef in ${theClass.name}.'); + } + } if (myClassRef != null && myListRef != null) { throw 'Found both instances!!!'; @@ -104,8 +111,7 @@ void main() { print('myListRef: $myListRef'); // To make sure [myList] is not const. - myList.add(6); - myList.add(7); + myList.add(DateTime.now()); }, timeout: const Timeout(Duration(minutes: 20)), ); From 713feec3fc90835baba103d888adff6fe286bb72 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Sun, 9 Jul 2023 17:00:43 -0700 Subject: [PATCH 12/23] - --- pkgs/leak_tracker/CHANGELOG.md | 4 ++++ pkgs/leak_tracker/pubspec.yaml | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/pkgs/leak_tracker/CHANGELOG.md b/pkgs/leak_tracker/CHANGELOG.md index f96d6305..0cda642a 100644 --- a/pkgs/leak_tracker/CHANGELOG.md +++ b/pkgs/leak_tracker/CHANGELOG.md @@ -1,3 +1,7 @@ +# 7.0.6 + +* Add helpers for troubleshooting. + # 7.0.5 * Convert to multi-package. diff --git a/pkgs/leak_tracker/pubspec.yaml b/pkgs/leak_tracker/pubspec.yaml index 31a1ab27..15f975f3 100644 --- a/pkgs/leak_tracker/pubspec.yaml +++ b/pkgs/leak_tracker/pubspec.yaml @@ -1,5 +1,5 @@ name: leak_tracker -version: 7.0.4 +version: 7.0.6 description: A framework for memory leak tracking for Dart and Flutter applications. repository: https://github.com/dart-lang/leak_tracker/tree/main/pkgs/leak_tracker From b5a084d287e47a04713c4c7138bb16e984917ec9 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Sun, 9 Jul 2023 17:05:29 -0700 Subject: [PATCH 13/23] Update _retaining_path_test.dart --- .../leak_tracking/retaining_path/_retaining_path_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart b/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart index bcc6f421..ac0fe3af 100644 --- a/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart +++ b/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart @@ -54,7 +54,7 @@ void main() { } test( - 'Instance of array is found.', + 'Instance of list is found.', () async { final myClass = MyClass(); ObjRef? myClassRef; From f1b84b90a1e0caef2c7646c22b82cd1d9d575cb3 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Sun, 9 Jul 2023 19:43:11 -0700 Subject: [PATCH 14/23] Update _retaining_path_test.dart --- .../leak_tracking/retaining_path/_retaining_path_test.dart | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart b/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart index ac0fe3af..d670e263 100644 --- a/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart +++ b/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart @@ -40,7 +40,7 @@ void main() { expect(path!.elements, isNotEmpty); }); - test('Path type with generic arg is found.', () async { + test('Path for type with generic arg is found.', () async { final instance = [1, 2, 3, 4, 5]; final path = await obtainRetainingPath(MyClass, identityHashCode(instance)); expect(path!.elements, isNotEmpty); From 18be89c8bbbe732d5c0d308d92d00680108019f7 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Mon, 10 Jul 2023 07:50:11 -0700 Subject: [PATCH 15/23] Update _retaining_path_test.dart --- .../leak_tracking/retaining_path/_retaining_path_test.dart | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart b/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart index d670e263..53dd4d00 100644 --- a/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart +++ b/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart @@ -66,6 +66,8 @@ void main() { print(connection.isolates.map((i) => '${i.name}-${i.id}')); for (final isolate in connection.isolates) { + await connection.service.requestHeapSnapshot(isolate.id); + var classList = await connection.service.getClassList(isolate.id); // In the beginning list of classes may be empty. while (classList.classes?.isEmpty ?? true) { From 0c490727dbca4bbc4e4df0f125f9654752498919 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Mon, 10 Jul 2023 08:03:39 -0700 Subject: [PATCH 16/23] - --- .../retaining_path/_connection.dart | 15 ++++-------- .../retaining_path/_retaining_path.dart | 24 ++++++++----------- .../retaining_path/_retaining_path_test.dart | 8 +++---- 3 files changed, 18 insertions(+), 29 deletions(-) diff --git a/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_connection.dart b/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_connection.dart index a938abc9..691a70ae 100644 --- a/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_connection.dart +++ b/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_connection.dart @@ -14,17 +14,10 @@ final _log = Logger('_connection.dart'); class Connection { Connection(this.service, this.isolates); - final List isolates; + final List isolates; final VmService service; } -class IsolateInfo { - IsolateInfo({required this.id, required this.name}); - - final String id; - final String name; -} - Completer? _completer; void disconnect() => _completer = null; @@ -64,10 +57,10 @@ Future connect() async { /// Depending on environment (command line / IDE, Flutter / Dart), isolates may have different names, /// and there can be one or two. Sometimes the second one appears with latency. /// And sometimes there are two isolates with name 'main'. -Future> _getTwoIsolates(VmService service) async { +Future> _getTwoIsolates(VmService service) async { _log.info('Started loading isolates...'); - final result = []; + final result = []; const isolatesToGet = 2; const watingTime = Duration(seconds: 2); @@ -76,7 +69,7 @@ Future> _getTwoIsolates(VmService service) async { result.clear(); await _forEachIsolate( service, - (IsolateRef r) async => result.add(IsolateInfo(id: r.id!, name: r.name!)), + (IsolateRef r) async => result.add(r), ); if (result.length < isolatesToGet) { await Future.delayed(const Duration(milliseconds: 100)); diff --git a/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_retaining_path.dart b/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_retaining_path.dart index ba830d4d..9601f44d 100644 --- a/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_retaining_path.dart +++ b/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_retaining_path.dart @@ -17,7 +17,7 @@ Future obtainRetainingPath(Type type, int code) async { if (theObject == null) return null; final result = await connection.service.getRetainingPath( - theObject.isolateInfo.id, + theObject.isolateRef.id!, theObject.itemId, 100000, ); @@ -47,7 +47,7 @@ Future<_ItemInIsolate?> _objectInIsolate( print('looking for ${theClass.name}'); final instances = (await connection.service.getInstances( - theClass.isolateInfo.id, + theClass.isolateRef.id!, theClass.itemId, pathLengthLimit, )) @@ -57,24 +57,20 @@ Future<_ItemInIsolate?> _objectInIsolate( final result = instances.firstWhereOrNull((ObjRef objRef) => objRef is InstanceRef && objRef.identityHashCode == object.code); if (result != null) { - throw ('found!!!! for ${theClass.name}'); - - return _ItemInIsolate( - isolateInfo: theClass.isolateInfo, itemId: result.id!); + throw 'found!!!! for ${theClass.name}'; } } - throw ('not found!!!!'); - return null; + throw 'not found!!!!'; } /// Represents an item in an isolate. /// /// It can be class or object. class _ItemInIsolate { - _ItemInIsolate({required this.isolateInfo, required this.itemId, this.name}); + _ItemInIsolate({required this.isolateRef, required this.itemId, this.name}); /// The isolate. - final IsolateInfo isolateInfo; + final IsolateRef isolateRef; /// Id of the item in the isolate. final String itemId; @@ -89,8 +85,8 @@ Future> _findClasses( ) async { final result = <_ItemInIsolate>[]; - for (final isolateInfo in connection.isolates) { - var classes = await connection.service.getClassList(isolateInfo.id); + for (final isolate in connection.isolates) { + var classes = await connection.service.getClassList(isolate.id!); const watingTime = Duration(seconds: 2); final stopwatch = Stopwatch()..start(); @@ -98,7 +94,7 @@ Future> _findClasses( // In the beginning list of classes may be empty. while (classes.classes?.isEmpty ?? true && stopwatch.elapsed < watingTime) { await Future.delayed(const Duration(milliseconds: 100)); - classes = await connection.service.getClassList(isolateInfo.id); + classes = await connection.service.getClassList(isolate.id!); } if (classes.classes?.isEmpty ?? true) { throw StateError('Could not get list of classes.'); @@ -114,7 +110,7 @@ Future> _findClasses( filtered.map( (classRef) => _ItemInIsolate( itemId: classRef.id!, - isolateInfo: isolateInfo, + isolateRef: isolate, name: classRef.name, ), ), diff --git a/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart b/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart index 53dd4d00..f26bd478 100644 --- a/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart +++ b/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart @@ -66,13 +66,13 @@ void main() { print(connection.isolates.map((i) => '${i.name}-${i.id}')); for (final isolate in connection.isolates) { - await connection.service.requestHeapSnapshot(isolate.id); + await HeapSnapshotGraph.getSnapshot(connection.service, isolate); - var classList = await connection.service.getClassList(isolate.id); + var classList = await connection.service.getClassList(isolate.id!); // In the beginning list of classes may be empty. while (classList.classes?.isEmpty ?? true) { await Future.delayed(const Duration(milliseconds: 100)); - classList = await connection.service.getClassList(isolate.id); + classList = await connection.service.getClassList(isolate.id!); } if (classList.classes?.isEmpty ?? true) { throw StateError('Could not get list of classes.'); @@ -87,7 +87,7 @@ void main() { if (theClass.name == 'TypeParameters') continue; final instances = (await connection.service.getInstances( - isolate.id, + isolate.id!, theClass.id!, 1000000000000, )) From 9d5620fd7458cf1a3f0aea0741d468a95805db4c Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Mon, 10 Jul 2023 18:45:21 -0700 Subject: [PATCH 17/23] - --- pkgs/leak_tracker/test/debug/README.md | 2 ++ .../retaining_path/_retaining_path_test.dart | 27 +++++++++++++++---- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/pkgs/leak_tracker/test/debug/README.md b/pkgs/leak_tracker/test/debug/README.md index 0112ba57..94941847 100644 --- a/pkgs/leak_tracker/test/debug/README.md +++ b/pkgs/leak_tracker/test/debug/README.md @@ -4,3 +4,5 @@ To run them locally: ``` dart test --debug test/dart_debug ``` + +Or click 'Debug' near the test name in your IDE. diff --git a/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart b/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart index f26bd478..2a9d9e95 100644 --- a/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart +++ b/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart @@ -15,6 +15,10 @@ class MyClass { MyClass(); } +class MyArgClass { + MyArgClass(); +} + final _logs = []; late StreamSubscription subscription; @@ -40,7 +44,7 @@ void main() { expect(path!.elements, isNotEmpty); }); - test('Path for type with generic arg is found.', () async { + test('Path for list is found.', () async { final instance = [1, 2, 3, 4, 5]; final path = await obtainRetainingPath(MyClass, identityHashCode(instance)); expect(path!.elements, isNotEmpty); @@ -59,6 +63,9 @@ void main() { final myClass = MyClass(); ObjRef? myClassRef; + final myArgClass = MyArgClass(); + ObjRef? myArgClassRef; + final myList = [DateTime.now(), DateTime.now()]; ObjRef? myListRef; @@ -103,14 +110,24 @@ void main() { } } - if (myClassRef != null && myListRef != null) { - throw 'Found both instances!!!'; + if (myArgClassRef == null) { + myArgClassRef = _find(instances, identityHashCode(myArgClass)); + if (myArgClassRef != null) { + print('Found myArgClassRef in ${theClass.name}.'); + } + } + + if (myClassRef != null && + myListRef != null && + myArgClassRef != null) { + throw 'Found all instances!!!'; } } } - print('myClassRef: $myClassRef'); - print('myListRef: $myListRef'); + print('myClassRef: $myClassRef\n'); + print('myArgClassRef: $myArgClassRef\n'); + print('myListRef: $myListRef\n'); // To make sure [myList] is not const. myList.add(DateTime.now()); From 982a05e2b5b1ed9d7999a96473fac3cb2e155fcc Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Mon, 10 Jul 2023 19:19:59 -0700 Subject: [PATCH 18/23] - --- pkgs/leak_tracker/CHANGELOG.md | 1 + .../lib/src/leak_tracking/orchestration.dart | 2 - .../retaining_path/_retaining_path.dart | 35 ++++--- .../retaining_path/_retaining_path_test.dart | 99 ++----------------- 4 files changed, 30 insertions(+), 107 deletions(-) diff --git a/pkgs/leak_tracker/CHANGELOG.md b/pkgs/leak_tracker/CHANGELOG.md index 0cda642a..aa4d7e8f 100644 --- a/pkgs/leak_tracker/CHANGELOG.md +++ b/pkgs/leak_tracker/CHANGELOG.md @@ -1,6 +1,7 @@ # 7.0.6 * Add helpers for troubleshooting. +* Handle generic arguments for retaining path detection. # 7.0.5 diff --git a/pkgs/leak_tracker/lib/src/leak_tracking/orchestration.dart b/pkgs/leak_tracker/lib/src/leak_tracking/orchestration.dart index 22116c31..69173e21 100644 --- a/pkgs/leak_tracker/lib/src/leak_tracking/orchestration.dart +++ b/pkgs/leak_tracker/lib/src/leak_tracking/orchestration.dart @@ -5,8 +5,6 @@ import 'dart:async'; import 'dart:developer'; -import 'package:clock/clock.dart'; - import '../shared/shared_model.dart'; import '_formatting.dart'; import '_gc_counter.dart'; diff --git a/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_retaining_path.dart b/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_retaining_path.dart index 9601f44d..48777478 100644 --- a/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_retaining_path.dart +++ b/pkgs/leak_tracker/lib/src/leak_tracking/retaining_path/_retaining_path.dart @@ -30,37 +30,47 @@ class _ObjectFingerprint { final Type type; final int code; + + String get typeNameWithoutArgs { + final name = type.toString(); + final index = name.indexOf('<'); + if (index == -1) return name; + return name.substring(0, index); + } } Future<_ItemInIsolate?> _objectInIsolate( Connection connection, _ObjectFingerprint object, ) async { - final classes = await _findClasses(connection, object.type.toString()); + final classes = await _findClasses(connection, object.typeNameWithoutArgs); for (final theClass in classes) { - const pathLengthLimit = 10000000; - // TODO(polina-c): remove when issue is fixed // https://github.com/dart-lang/sdk/issues/52893 if (theClass.name == 'TypeParameters') continue; - print('looking for ${theClass.name}'); final instances = (await connection.service.getInstances( theClass.isolateRef.id!, theClass.itemId, - pathLengthLimit, + 1000000000, )) .instances ?? []; - final result = instances.firstWhereOrNull((ObjRef objRef) => - objRef is InstanceRef && objRef.identityHashCode == object.code); + final result = instances.firstWhereOrNull( + (objRef) => + objRef is InstanceRef && objRef.identityHashCode == object.code, + ); if (result != null) { - throw 'found!!!! for ${theClass.name}'; + return _ItemInIsolate( + isolateRef: theClass.isolateRef, + itemId: result.id!, + ); } } - throw 'not found!!!!'; + + return null; } /// Represents an item in an isolate. @@ -100,11 +110,8 @@ Future> _findClasses( throw StateError('Could not get list of classes.'); } - // final filtered = classes.classes - // ?.where((ref) => (ref.name?.contains('List') ?? false)) ?? - // []; - - final filtered = classes.classes ?? []; + final filtered = + classes.classes?.where((ref) => runtimeClassName == ref.name) ?? []; result.addAll( filtered.map( diff --git a/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart b/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart index 2a9d9e95..f3d04f5d 100644 --- a/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart +++ b/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart @@ -4,12 +4,10 @@ import 'dart:async'; -import 'package:collection/collection.dart'; import 'package:leak_tracker/src/leak_tracking/retaining_path/_connection.dart'; import 'package:leak_tracker/src/leak_tracking/retaining_path/_retaining_path.dart'; import 'package:logging/logging.dart'; import 'package:test/test.dart'; -import 'package:vm_service/vm_service.dart' hide LogRecord; class MyClass { MyClass(); @@ -40,101 +38,20 @@ void main() { test('Path for $MyClass instance is found.', () async { final instance = MyClass(); - final path = await obtainRetainingPath(MyClass, identityHashCode(instance)); + final path = await obtainRetainingPath( + instance.runtimeType, identityHashCode(instance)); expect(path!.elements, isNotEmpty); }); - test('Path for list is found.', () async { - final instance = [1, 2, 3, 4, 5]; - final path = await obtainRetainingPath(MyClass, identityHashCode(instance)); + test('Path for class with generic arg is found.', () async { + final instance = MyArgClass(); + final path = await obtainRetainingPath( + instance.runtimeType, + identityHashCode(instance), + ); expect(path!.elements, isNotEmpty); }); - ObjRef? _find(List instances, int code) { - return instances.firstWhereOrNull( - (ObjRef objRef) => - objRef is InstanceRef && objRef.identityHashCode == code, - ); - } - - test( - 'Instance of list is found.', - () async { - final myClass = MyClass(); - ObjRef? myClassRef; - - final myArgClass = MyArgClass(); - ObjRef? myArgClassRef; - - final myList = [DateTime.now(), DateTime.now()]; - ObjRef? myListRef; - - final connection = await connect(); - print(connection.isolates.map((i) => '${i.name}-${i.id}')); - - for (final isolate in connection.isolates) { - await HeapSnapshotGraph.getSnapshot(connection.service, isolate); - - var classList = await connection.service.getClassList(isolate.id!); - // In the beginning list of classes may be empty. - while (classList.classes?.isEmpty ?? true) { - await Future.delayed(const Duration(milliseconds: 100)); - classList = await connection.service.getClassList(isolate.id!); - } - if (classList.classes?.isEmpty ?? true) { - throw StateError('Could not get list of classes.'); - } - - final classes = classList.classes!; - - for (final theClass in classes) { - print('Checking class ${theClass.name}...'); - // TODO(polina-c): remove when issue is fixed - // https://github.com/dart-lang/sdk/issues/52893 - if (theClass.name == 'TypeParameters') continue; - - final instances = (await connection.service.getInstances( - isolate.id!, - theClass.id!, - 1000000000000, - )) - .instances ?? - []; - - myClassRef ??= _find(instances, identityHashCode(myClass)); - - if (myListRef == null) { - myListRef = _find(instances, identityHashCode(myList)); - if (myListRef != null) { - print('Found myListRef in ${theClass.name}.'); - } - } - - if (myArgClassRef == null) { - myArgClassRef = _find(instances, identityHashCode(myArgClass)); - if (myArgClassRef != null) { - print('Found myArgClassRef in ${theClass.name}.'); - } - } - - if (myClassRef != null && - myListRef != null && - myArgClassRef != null) { - throw 'Found all instances!!!'; - } - } - } - - print('myClassRef: $myClassRef\n'); - print('myArgClassRef: $myArgClassRef\n'); - print('myListRef: $myListRef\n'); - - // To make sure [myList] is not const. - myList.add(DateTime.now()); - }, - timeout: const Timeout(Duration(minutes: 20)), - ); - test('Connection is happening just once', () async { final instance1 = MyClass(); final instance2 = MyClass(); From f4fe7af9248011067a9319a07885ae6fb445fc65 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Tue, 11 Jul 2023 09:14:06 -0700 Subject: [PATCH 19/23] - --- .../debug/leak_tracking/orchestration_test.dart | 14 ++++++++++---- .../retaining_path/_retaining_path_test.dart | 4 +++- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/pkgs/leak_tracker/test/debug/leak_tracking/orchestration_test.dart b/pkgs/leak_tracker/test/debug/leak_tracking/orchestration_test.dart index a528c069..4197d8fe 100644 --- a/pkgs/leak_tracker/test/debug/leak_tracking/orchestration_test.dart +++ b/pkgs/leak_tracker/test/debug/leak_tracking/orchestration_test.dart @@ -5,11 +5,17 @@ import 'package:leak_tracker/leak_tracker.dart'; import 'package:test/test.dart'; +class MyClass { + MyClass(); + + final Stopwatch stopwatch = Stopwatch(); + WeakReference get ref => WeakReference(stopwatch); +} + void main() { + final myObject = MyClass(); test('formattedRetainingPath returns path', () async { - final Object myObject = [1, 2, 3, 4, 5]; - final path = await formattedRetainingPath(WeakReference(myObject)); - - print(path); + final path = await formattedRetainingPath(myObject.ref); + expect(path, contains('_test.dart/MyClass:stopwatch')); }); } diff --git a/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart b/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart index f3d04f5d..62a30e4d 100644 --- a/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart +++ b/pkgs/leak_tracker/test/debug/leak_tracking/retaining_path/_retaining_path_test.dart @@ -39,7 +39,9 @@ void main() { final instance = MyClass(); final path = await obtainRetainingPath( - instance.runtimeType, identityHashCode(instance)); + instance.runtimeType, + identityHashCode(instance), + ); expect(path!.elements, isNotEmpty); }); From 224e572f9119f0e7400a55ac43669f2a7badce1c Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Tue, 11 Jul 2023 09:23:57 -0700 Subject: [PATCH 20/23] Update TROUBLESHOOT.md --- doc/TROUBLESHOOT.md | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/doc/TROUBLESHOOT.md b/doc/TROUBLESHOOT.md index 0a4b8e62..91b1de57 100644 --- a/doc/TROUBLESHOOT.md +++ b/doc/TROUBLESHOOT.md @@ -67,7 +67,7 @@ TODO: link DevTools documentation with explanation ## Verify object references If you expect an object to be not referenced at some point, -but tot sure, you can validate it by temporary adding assertion: +but not sure, you can validate it by temporary adding assertion. ``` final ref = WeakReference(myObject); @@ -81,6 +81,10 @@ if (ref.target == null) { } ``` +IMPORTANT: this code will not work in release mode, so +you need to run it with flag `--debug` or `--profile`, or, +if it is test, by clicking `Debug` near your test name in IDE. + ## Known complicated cases ### 1. More than one closure context From 37db89a0013ca2250354be15849266d7c2ce6b42 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Tue, 11 Jul 2023 09:25:09 -0700 Subject: [PATCH 21/23] Update orchestration.dart --- pkgs/leak_tracker/lib/src/leak_tracking/orchestration.dart | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkgs/leak_tracker/lib/src/leak_tracking/orchestration.dart b/pkgs/leak_tracker/lib/src/leak_tracking/orchestration.dart index 69173e21..37783ba1 100644 --- a/pkgs/leak_tracker/lib/src/leak_tracking/orchestration.dart +++ b/pkgs/leak_tracker/lib/src/leak_tracking/orchestration.dart @@ -128,10 +128,10 @@ Future withLeakTracking( /// Verifies that garbage collection happened using [reachabilityBarrier]. /// Does not work in web and in release mode. /// -/// Use [timeout] to limit waitning time. +/// Use [timeout] to limit waiting time. /// Use [fullGcCycles] to force multiple garbage collections. /// -/// The methot is useable for testing in combination with [WeakReference] to ensure +/// The method is helpful for testing in combination with [WeakReference] to ensure /// an object is not held by another object from garbage collection. /// /// For code example see From d2decee713fcd60de7de4d7932d052455fdc0d5d Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Tue, 11 Jul 2023 09:25:32 -0700 Subject: [PATCH 22/23] Update TROUBLESHOOT.md --- doc/TROUBLESHOOT.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/TROUBLESHOOT.md b/doc/TROUBLESHOOT.md index 91b1de57..e7eb008f 100644 --- a/doc/TROUBLESHOOT.md +++ b/doc/TROUBLESHOOT.md @@ -67,7 +67,7 @@ TODO: link DevTools documentation with explanation ## Verify object references If you expect an object to be not referenced at some point, -but not sure, you can validate it by temporary adding assertion. +but not sure, you can validate it by temporaryly adding assertion. ``` final ref = WeakReference(myObject); From 735e53fe2cfc038045a25d761363f91a3b0efaa6 Mon Sep 17 00:00:00 2001 From: Polina Cherkasova Date: Tue, 11 Jul 2023 09:27:57 -0700 Subject: [PATCH 23/23] Update analyze.sh --- tool/analyze.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tool/analyze.sh b/tool/analyze.sh index 74d1ee19..2a34db92 100644 --- a/tool/analyze.sh +++ b/tool/analyze.sh @@ -7,7 +7,7 @@ # Fast fail the script on failures. set -ex -sh pub_get.sh +sh ./tool/pub_get.sh cd examples/autosnapshotting flutter analyze --fatal-infos