Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Create troubleshooting helpers. #89

Merged
merged 23 commits into from
Jul 11, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
31 changes: 28 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,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,
but tot sure, you can validate it by temporary adding assertion:

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
but tot sure, you can validate it by temporary adding assertion:
but not sure, you can validate it by temporarily 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.');
}
```

## 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 waitning time.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: waiting

/// Use [fullGcCycles] to force multiple garbage collections.
///
/// The methot is useable for testing in combination with [WeakReference] to ensure
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: "method is used"

/// 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.
Original file line number Diff line number Diff line change
@@ -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 = <int>[1, 2, 3, 4, 5];
final path = await formattedRetainingPath(WeakReference(myObject));

print(path);
});
}
Loading
Loading