Skip to content

Commit

Permalink
Create troubleshooting helpers. (#89)
Browse files Browse the repository at this point in the history
  • Loading branch information
polina-c committed Jul 11, 2023
1 parent 85bd7fb commit 8e3aa07
Show file tree
Hide file tree
Showing 18 changed files with 294 additions and 55 deletions.
6 changes: 6 additions & 0 deletions .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
35 changes: 32 additions & 3 deletions doc/TROUBLESHOOT.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -44,7 +44,15 @@ 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:

```
setUpAll(() {
collectDebugInformationForLeaks = true;
});
```

**Applications**
Expand All @@ -56,6 +64,27 @@ 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,
but not sure, you can validate it by temporaryly adding assertion.

```
final ref = WeakReference(myObject);
myObject = null;
await forceGC();
if (ref.target == null) {
throw StateError('Validated that myObject is not held from garbage collection.');
} else {
print(await formattedRetainingPath(ref));
throw StateError('myObject is reachable from root. See console output for the retaining path.');
}
```

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
Expand Down
5 changes: 5 additions & 0 deletions pkgs/leak_tracker/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,8 @@
# 7.0.6

* Add helpers for troubleshooting.
* Handle generic arguments for retaining path detection.

# 7.0.5

* Convert to multi-package.
Expand Down
4 changes: 2 additions & 2 deletions pkgs/leak_tracker/lib/src/leak_tracking/_formatting.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
};
}
Expand Down Expand Up @@ -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.',
Expand Down
2 changes: 1 addition & 1 deletion pkgs/leak_tracker/lib/src/leak_tracking/_gc_counter.dart
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand Down
61 changes: 48 additions & 13 deletions pkgs/leak_tracker/lib/src/leak_tracking/orchestration.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,12 @@
import 'dart:async';
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.
///
Expand Down Expand Up @@ -100,8 +100,8 @@ Future<Leaks> withLeakTracking(
await checkNonGCed();
}

await _forceGC(
gcCycles: gcCountBuffer,
await forceGC(
fullGcCycles: gcCountBuffer,
timeout: timeoutForFinalGarbageCollection,
);

Expand All @@ -124,22 +124,57 @@ Future<Leaks> withLeakTracking(
}

/// Forces garbage collection by aggressive memory allocation.
Future<void> _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 waiting time.
/// Use [fullGcCycles] to force multiple garbage collections.
///
/// 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
/// https://github.com/dart-lang/leak_tracker/blob/main/doc/TROUBLESHOOT.md
Future<void> forceGC({
Duration? timeout,
int fullGcCycles = 1,
}) async {
final Stopwatch? stopwatch = timeout == null ? null : (Stopwatch()..start());
final int barrier = reachabilityBarrier;

final storage = <List<DateTime>>[];
final List<List<DateTime>> storage = <List<DateTime>>[];

void allocateMemory() {
storage.add(Iterable.generate(10000, (_) => DateTime.now()).toList());
if (storage.length > 100) storage.removeAt(0);
storage.add(
Iterable<DateTime>.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<void>.delayed(Duration.zero);
allocateMemory();
}
}

/// Returns nicely formatted retaining path for the [ref.target].
///
/// If the object is garbage collected or not retained, returns null.
///
/// Does not work in web and in release mode.
Future<String?> formattedRetainingPath(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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ final _log = Logger('_connection.dart');
class Connection {
Connection(this.service, this.isolates);

final List<String> isolates;
final List<IsolateRef> isolates;
final VmService service;
}

Expand Down Expand Up @@ -45,7 +45,7 @@ Future<Connection> 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);
Expand All @@ -57,10 +57,10 @@ Future<Connection> 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<List<String>> _getIdForTwoIsolates(VmService service) async {
Future<List<IsolateRef>> _getTwoIsolates(VmService service) async {
_log.info('Started loading isolates...');

final result = <String>[];
final result = <IsolateRef>[];

const isolatesToGet = 2;
const watingTime = Duration(seconds: 2);
Expand All @@ -69,7 +69,7 @@ Future<List<String>> _getIdForTwoIsolates(VmService service) async {
result.clear();
await _forEachIsolate(
service,
(IsolateRef r) async => result.add(r.id!),
(IsolateRef r) async => result.add(r),
);
if (result.length < isolatesToGet) {
await Future.delayed(const Duration(milliseconds: 100));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ Future<RetainingPath?> obtainRetainingPath(Type type, int code) async {
if (theObject == null) return null;

final result = await connection.service.getRetainingPath(
theObject.isolateId,
theObject.isolateRef.id!,
theObject.itemId,
100000,
);
Expand All @@ -30,20 +30,30 @@ 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;

final instances = (await connection.service.getInstances(
theClass.isolateId,
theClass.isolateRef.id!,
theClass.itemId,
pathLengthLimit,
1000000000,
))
.instances ??
<ObjRef>[];
Expand All @@ -53,7 +63,10 @@ Future<_ItemInIsolate?> _objectInIsolate(
objRef is InstanceRef && objRef.identityHashCode == object.code,
);
if (result != null) {
return _ItemInIsolate(isolateId: theClass.isolateId, itemId: result.id!);
return _ItemInIsolate(
isolateRef: theClass.isolateRef,
itemId: result.id!,
);
}
}

Expand All @@ -64,13 +77,16 @@ Future<_ItemInIsolate?> _objectInIsolate(
///
/// It can be class or object.
class _ItemInIsolate {
_ItemInIsolate({required this.isolateId, required this.itemId});
_ItemInIsolate({required this.isolateRef, required this.itemId, this.name});

/// Id of the isolate.
final String isolateId;
/// The isolate.
final IsolateRef isolateRef;

/// Id of the item in the isolate.
final String itemId;

/// Name of the item, for debugging purposes.
final String? name;
}

Future<List<_ItemInIsolate>> _findClasses(
Expand All @@ -79,27 +95,31 @@ Future<List<_ItemInIsolate>> _findClasses(
) async {
final result = <_ItemInIsolate>[];

for (final isolateId in connection.isolates) {
var classes = await connection.service.getClassList(isolateId);
for (final isolate in connection.isolates) {
var classes = await connection.service.getClassList(isolate.id!);

const watingTime = Duration(seconds: 2);
final stopwatch = Stopwatch()..start();

// 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(isolate.id!);
}
if (classes.classes?.isEmpty ?? true) {
throw StateError('Could not get list of classes.');
}

final filtered =
classes.classes?.where((ref) => runtimeClassName == ref.name) ?? [];

result.addAll(
filtered.map(
(classRef) =>
_ItemInIsolate(itemId: classRef.id!, isolateId: isolateId),
(classRef) => _ItemInIsolate(
itemId: classRef.id!,
isolateRef: isolate,
name: classRef.name,
),
),
);
}
Expand Down
4 changes: 2 additions & 2 deletions pkgs/leak_tracker/pubspec.yaml
Original file line number Diff line number Diff line change
@@ -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

Expand All @@ -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:
Expand Down
2 changes: 2 additions & 0 deletions pkgs/leak_tracker/test/debug/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,5 @@ To run them locally:
```
dart test --debug test/dart_debug
```

Or click 'Debug' near the test name in your IDE.
Loading

0 comments on commit 8e3aa07

Please sign in to comment.