Improve getting retaining path. (#69)

diff --git a/CHANGELOG.md b/CHANGELOG.md
index a07ec4b..7862c6e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,3 +1,7 @@
+# 7.0.2
+
+* Improve retaining path formatting.
+
 # 7.0.1
 
 * Format retaining path nicely.
diff --git a/doc/TROUBLESHOOT.md b/doc/TROUBLESHOOT.md
index ab42ee2..b46bc0a 100644
--- a/doc/TROUBLESHOOT.md
+++ b/doc/TROUBLESHOOT.md
@@ -15,7 +15,7 @@
 
 TODO: add steps.
 
-## More complicated cases
+## Collect additional information
 
 To understand the root cause of a memory leak, you may want to gather additional information.
 
@@ -33,41 +33,38 @@
 
     - **Retaining path**: shows which objects hold the leaked one from garbage collection.
 
-## Collect stacktrace
 
-By default, the leak tracker does not collect stacktraces, because the collection may
+By default, the leak tracker does not gather the information, because the collection may
 impact performance and memory footprint.
 
-### In tests
+**Tests**
 
-Temporarily setup stacktrace collection for your test:
+For collecting debugging information in tests, temporarily pass an instance of `LeakTrackingTestConfig` to the test:
 
 ```
   testWidgets('My test', (WidgetTester tester) async {
     ...
-  },
-  leakTrackingConfig: LeakTrackingTestConfig(
-    stackTraceCollectionConfig: StackTraceCollectionConfig(
-      classesToCollectStackTraceOnStart: {'MyClass'},
-    )
-  ));
+  }, leakTrackingConfig: LeakTrackingTestConfig.debug());
 ```
 
-### In applications
+**Applications**
 
-There are options to enable stacktrace collection in applications:
+For collecting debugging information in your running application, the options are:
 
-1. By passing `stackTraceCollectionConfig` to `enableLeakTracking`.
-
-https://user-images.githubusercontent.com/12115586/208321882-ecb96152-3aa7-4671-800e-f2eb8c18149e.mov
-
-2. Using interactive UI in DevTools > Memory > Leaks.
+1. Pass `LeakTrackingConfiguration` to `enableLeakTracking`
+2. Use the interactive UI in DevTools > Memory > Leaks
 
 TODO: link DevTools documentation with explanation
 
-## Check retaining pathes
+## Known complicated cases
 
-Open DevTools > Memory > Leaks, wait for not-GCed leaks to be caught,
-and click 'Analyze and Download'.
+### 1. More than one closure context
 
-TODO: add details
+If a method contains more than one closures, they share the context and thus all
+instances of the context will be alive while at least one of the closures is alive.
+
+TODO: add example
+
+Such cases are hard to troubleshoot. One way to fix them is to convert all closures,
+which reference the leaked type, to named methods.
+
diff --git a/lib/src/leak_tracking/_formatting.dart b/lib/src/leak_tracking/_formatting.dart
index 9bc8def..6116267 100644
--- a/lib/src/leak_tracking/_formatting.dart
+++ b/lib/src/leak_tracking/_formatting.dart
@@ -41,6 +41,15 @@
     ['value', 'class', 'name'],
     ['value', 'declaredType', 'class', 'name'],
     ['value', 'type'],
+  ]),
+  closureOwner([
+    ['value', 'closureFunction', 'owner', 'name'],
+  ]),
+  globalVarUri([
+    ['value', 'location', 'script', 'uri'],
+  ]),
+  globalVarName([
+    ['value', 'name'],
   ]);
 
   const RetainingObjectProperty(this.paths);
@@ -54,6 +63,13 @@
 
   var result = property(RetainingObjectProperty.type, json) ?? '';
 
+  if (result == '_Closure') {
+    final func = property(RetainingObjectProperty.closureOwner, json);
+    if (func != null) {
+      result = '$result (in $func)';
+    }
+  }
+
   final lib = property(RetainingObjectProperty.lib, json);
   if (lib != null) {
     result = '$lib/$result';
@@ -66,6 +82,12 @@
     result = '$result:$location';
   }
 
+  if (result == 'dart.core/_Type') {
+    final globalVarUri = property(RetainingObjectProperty.globalVarUri, json);
+    final globalVarName = property(RetainingObjectProperty.globalVarName, json);
+    result = '$globalVarUri/$globalVarName';
+  }
+
   return result;
 }
 
diff --git a/lib/src/leak_tracking/orchestration.dart b/lib/src/leak_tracking/orchestration.dart
index 0ef3226..cdecc48 100644
--- a/lib/src/leak_tracking/orchestration.dart
+++ b/lib/src/leak_tracking/orchestration.dart
@@ -70,12 +70,14 @@
 /// });
 /// ```
 Future<Leaks> withLeakTracking(
-  DartAsyncCallback callback, {
+  DartAsyncCallback? callback, {
   bool shouldThrowOnLeaks = true,
   Duration? timeoutForFinalGarbageCollection,
   LeakDiagnosticConfig leakDiagnosticConfig = const LeakDiagnosticConfig(),
   AsyncCodeRunner? asyncCodeRunner,
 }) async {
+  if (callback == null) return Leaks({});
+
   enableLeakTracking(
     resetIfAlreadyEnabled: true,
     config: LeakTrackingConfiguration.passive(
@@ -85,6 +87,7 @@
 
   try {
     await callback();
+    callback = null;
 
     asyncCodeRunner ??= (action) => action();
     late Leaks leaks;
diff --git a/lib/src/leak_tracking/retaining_path/_retaining_path.dart b/lib/src/leak_tracking/retaining_path/_retaining_path.dart
index 0efa57d..8037912 100644
--- a/lib/src/leak_tracking/retaining_path/_retaining_path.dart
+++ b/lib/src/leak_tracking/retaining_path/_retaining_path.dart
@@ -22,17 +22,19 @@
   }
 
   _log.info('Requesting retaining path.');
-  final result = await _service.getRetainingPath(
+
+  final result = await _theService.getRetainingPath(
     theObject.isolateId,
     theObject.itemId,
     100000,
   );
+
   _log.info('Recieved retaining path.');
   return result;
 }
 
 final List<String> _isolateIds = [];
-late VmService _service;
+late VmService _theService;
 bool _connected = false;
 
 Future<void> _connect() async {
@@ -46,10 +48,10 @@
     );
   }
 
-  _service = await connectWithWebSocket(info.serverWebSocketUri!, (error) {
+  _theService = await connectWithWebSocket(info.serverWebSocketUri!, (error) {
     throw error ?? Exception('Error connecting to service protocol');
   });
-  await _service.getVersion();
+  await _theService.getVersion();
   await _getIdForTwoIsolates();
 
   _connected = true;
@@ -68,7 +70,7 @@
   while (_isolateIds.length < isolatesToGet && stopwatch.elapsed < watingTime) {
     _isolateIds.clear();
     await forEachIsolate(
-      _service,
+      _theService,
       (IsolateRef r) async => _isolateIds.add(r.id!),
     );
     if (_isolateIds.length < isolatesToGet) {
@@ -93,7 +95,7 @@
 
   for (final theClass in classes) {
     const pathLengthLimit = 10000000;
-    final instances = (await _service.getInstances(
+    final instances = (await _theService.getInstances(
           theClass.isolateId,
           theClass.itemId,
           pathLengthLimit,
@@ -130,7 +132,7 @@
   final result = <_ItemInIsolate>[];
 
   for (final isolateId in _isolateIds) {
-    var classes = await _service.getClassList(isolateId);
+    var classes = await _theService.getClassList(isolateId);
 
     const watingTime = Duration(seconds: 2);
     final stopwatch = Stopwatch()..start();
@@ -138,7 +140,7 @@
     // 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 _service.getClassList(isolateId);
+      classes = await _theService.getClassList(isolateId);
     }
     if (classes.classes?.isEmpty ?? true) {
       throw StateError('Could not get list of classes.');
diff --git a/pubspec.yaml b/pubspec.yaml
index df50e26..1f8dd45 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -1,5 +1,5 @@
 name: leak_tracker
-version: 7.0.1
+version: 7.0.2
 description: A framework for memory leak tracking for Dart and Flutter applications.
 repository: https://github.com/dart-lang/leak_tracker
 
diff --git a/test/dart_debug/leak_tracking/end_to_end_test.dart b/test/dart_debug/leak_tracking/end_to_end_test.dart
index 5287aa9..60c6cbe 100644
--- a/test/dart_debug/leak_tracking/end_to_end_test.dart
+++ b/test/dart_debug/leak_tracking/end_to_end_test.dart
@@ -5,7 +5,6 @@
 import 'package:leak_tracker/leak_tracker.dart';
 import 'package:leak_tracker/testing.dart';
 import 'package:test/test.dart';
-import 'package:vm_service/vm_service.dart';
 
 import '../../dart_test_infra/data/dart_classes.dart';
 
@@ -14,12 +13,9 @@
   tearDown(() => disableLeakTracking());
 
   test('Retaining path for not GCed object is reported.', () async {
-    late LeakTrackedClass notGCedObject;
     final leaks = await withLeakTracking(
       () async {
-        notGCedObject = LeakTrackedClass();
-        // Dispose reachable instance.
-        notGCedObject.dispose();
+        LeakingClass();
       },
       shouldThrowOnLeaks: false,
       leakDiagnosticConfig: const LeakDiagnosticConfig(
@@ -27,16 +23,23 @@
       ),
     );
 
+    const expectedRetainingPathTails = [
+      '/leak_tracker/test/dart_test_infra/data/dart_classes.dart/_notGCedObjects',
+      'dart.core/_GrowableList:0',
+      '/leak_tracker/test/dart_test_infra/data/dart_classes.dart/LeakTrackedClass',
+    ];
+
     expect(leaks.total, 1);
     expect(
       () => expect(leaks, isLeakFree),
       throwsA(
         predicate(
           (e) {
-            return e is TestFailure &&
-                e.toString().contains(
-                      'leak_tracker/test/dart_test_infra/data/dart_classes.dart/LeakTrackedClass',
-                    );
+            if (e is! TestFailure) {
+              throw 'Unexpected exception type: ${e.runtimeType}';
+            }
+            _verifyRetainingPath(expectedRetainingPathTails, e.message!);
+            return true;
           },
         ),
       ),
@@ -45,9 +48,29 @@
     final theLeak = leaks.notGCed.first;
     expect(theLeak.trackedClass, contains(LeakTrackedClass.library));
     expect(theLeak.trackedClass, contains('$LeakTrackedClass'));
-    expect(
-      theLeak.context![ContextKeys.retainingPath].runtimeType,
-      RetainingPath,
-    );
   });
 }
+
+void _verifyRetainingPath(
+  List<String> expectedRetainingPathTails,
+  String actualMessage,
+) {
+  int? previousIndex;
+  for (var item in expectedRetainingPathTails) {
+    final index = actualMessage.indexOf('$item\n');
+    if (previousIndex == null) {
+      previousIndex = index;
+      continue;
+    }
+
+    expect(index > previousIndex, true);
+    final stringBetweenItems = actualMessage.substring(previousIndex, index);
+    expect(
+      RegExp('^').allMatches(stringBetweenItems).length,
+      1,
+      reason:
+          'There should be only one line break between items in retaining path.',
+    );
+    previousIndex = index;
+  }
+}
diff --git a/test/dart_test_infra/data/dart_classes.dart b/test/dart_test_infra/data/dart_classes.dart
index e436d33..7e97b0c 100644
--- a/test/dart_test_infra/data/dart_classes.dart
+++ b/test/dart_test_infra/data/dart_classes.dart
@@ -19,3 +19,11 @@
     dispatchObjectDisposed(object: this);
   }
 }
+
+final _notGCedObjects = <LeakTrackedClass>[];
+
+class LeakingClass {
+  LeakingClass() {
+    _notGCedObjects.add(LeakTrackedClass()..dispose());
+  }
+}