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());
+ }
+}