blob: d7bf8d0f1eec10fcef6f93410bd3cfbcbaf00e4d [file] [log] [blame]
// Copyright (c) 2020, 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.
// @dart = 2.9
import "vm_service_helper.dart" as vmService;
class VMServiceHeapHelperSpecificExactLeakFinder
extends vmService.LaunchingVMServiceHelper {
final Set _interestsClassNames = {};
final Map<Uri, Map<String, List<String>>> _interests =
new Map<Uri, Map<String, List<String>>>();
final Map<Uri, Map<String, List<String>>> _prettyPrints =
new Map<Uri, Map<String, List<String>>>();
final bool throwOnPossibleLeak;
VMServiceHeapHelperSpecificExactLeakFinder({
List<Interest> interests: const [],
List<Interest> prettyPrints: const [],
this.throwOnPossibleLeak: false,
}) {
if (interests.isEmpty) throw "Empty list of interests given";
for (Interest interest in interests) {
Map<String, List<String>> classToFields = _interests[interest.uri];
if (classToFields == null) {
classToFields = Map<String, List<String>>();
_interests[interest.uri] = classToFields;
}
_interestsClassNames.add(interest.className);
List<String> fields = classToFields[interest.className];
if (fields == null) {
fields = <String>[];
classToFields[interest.className] = fields;
}
fields.addAll(interest.fieldNames);
}
for (Interest interest in prettyPrints) {
Map<String, List<String>> classToFields = _prettyPrints[interest.uri];
if (classToFields == null) {
classToFields = Map<String, List<String>>();
_prettyPrints[interest.uri] = classToFields;
}
List<String> fields = classToFields[interest.className];
if (fields == null) {
fields = <String>[];
classToFields[interest.className] = fields;
}
fields.addAll(interest.fieldNames);
}
}
void pause() async {
await serviceClient.pause(_isolateRef.id);
}
vmService.VM _vm;
vmService.IsolateRef _isolateRef;
int _iterationNumber;
int get iterationNumber => _iterationNumber;
/// Best effort check if the isolate is idle.
Future<bool> isIdle() async {
dynamic tmp = await serviceClient.getIsolate(_isolateRef.id);
if (tmp is vmService.Isolate) {
vmService.Isolate isolate = tmp;
return isolate.pauseEvent.topFrame == null;
}
return false;
}
@override
Future<void> run() async {
_vm = await serviceClient.getVM();
if (_vm.isolates.length == 0) {
print("Didn't get any isolates. Will wait 1 second and retry.");
await Future.delayed(new Duration(seconds: 1));
_vm = await serviceClient.getVM();
}
if (_vm.isolates.length != 1) {
throw "Expected 1 isolate, got ${_vm.isolates.length}";
}
_isolateRef = _vm.isolates.single;
await forceGC(_isolateRef.id);
assert(await isPausedAtStart(_isolateRef.id));
await serviceClient.resume(_isolateRef.id);
_iterationNumber = 1;
while (true) {
if (!shouldDoAnotherIteration(_iterationNumber)) break;
await waitUntilPaused(_isolateRef.id);
print("Iteration: #$_iterationNumber");
Stopwatch stopwatch = new Stopwatch()..start();
vmService.AllocationProfile allocationProfile =
await forceGC(_isolateRef.id);
print("Forced GC in ${stopwatch.elapsedMilliseconds} ms");
stopwatch.reset();
List<Leak> leaks = [];
for (vmService.ClassHeapStats member in allocationProfile.members) {
if (_interestsClassNames.contains(member.classRef.name)) {
vmService.Class c =
await serviceClient.getObject(_isolateRef.id, member.classRef.id);
String uriString = c.location?.script?.uri;
if (uriString == null) continue;
Uri uri = Uri.parse(uriString);
Map<String, List<String>> uriInterest = _interests[uri];
if (uriInterest == null) continue;
List<String> fieldsForClass = uriInterest[c.name];
if (fieldsForClass == null) continue;
List<String> fieldsForClassPrettyPrint = fieldsForClass;
uriInterest = _prettyPrints[uri];
if (uriInterest != null) {
if (uriInterest[c.name] != null) {
fieldsForClassPrettyPrint = uriInterest[c.name];
}
}
leaks.addAll(await _findLeaks(_isolateRef, member.classRef,
fieldsForClass, fieldsForClassPrettyPrint));
}
}
if (leaks.isNotEmpty) {
for (Leak leak in leaks) {
leakDetected(leak.duplicate, leak.count, leak.prettyPrints);
}
if (throwOnPossibleLeak) {
throw "Leaks found";
}
} else {
noLeakDetected();
}
print("Looked for leaks in ${stopwatch.elapsedMilliseconds} ms");
await serviceClient.resume(_isolateRef.id);
_iterationNumber++;
}
}
Future<List<Leak>> _findLeaks(
vmService.IsolateRef isolateRef,
vmService.ClassRef classRef,
List<String> fieldsForClass,
List<String> fieldsForClassPrettyPrint) async {
// Use undocumented (/ private?) method to get all instances of this class.
vmService.InstanceRef instancesAsList = await serviceClient.callMethod(
"_getInstancesAsArray",
isolateId: isolateRef.id,
args: {
"objectId": classRef.id,
"includeSubclasses": false,
"includeImplementors": false,
},
);
// Create dart code that `toString`s a class instance according to
// the fields given as wanting printed. Both for finding duplicates (1) and
// for pretty printing entries (for instance to be able to differentiate
// them) (2).
// 1:
String fieldsToStringCode = classRef.name +
"[" +
fieldsForClass
.map((value) => "$value: \"\${element.$value}\"")
.join(", ") +
"]";
// 2:
String fieldsToStringPrettyPrintCode = classRef.name +
"[" +
fieldsForClassPrettyPrint
.map((value) => "$value: \"\${element.$value}\"")
.join(", ") +
"]";
// Expression evaluation to find duplicates: Put all entries into a map
// indexed by the `toString` code created above, mapping to list of that
// data.
vmService.InstanceRef mappedData = await serviceClient.evaluate(
isolateRef.id,
instancesAsList.id,
"""
this
.fold({}, (dynamic index, dynamic element) {
String key = '$fieldsToStringCode';
var list = index[key] ??= [];
list.add(element);
return index;
})
""",
);
// Expression calculation to find if any of the lists created as values
// above contains more than one entry (i.e. there's a duplicate).
vmService.InstanceRef duplicatesLengthRef = await serviceClient.evaluate(
isolateRef.id,
mappedData.id,
"""
this
.values
.where((dynamic element) => (element.length > 1) as bool)
.length
""",
);
vmService.Instance duplicatesLength =
await serviceClient.getObject(isolateRef.id, duplicatesLengthRef.id);
int duplicates = int.tryParse(duplicatesLength.valueAsString);
if (duplicates != 0) {
// There are duplicates. Expression calculation to encode the duplication
// data (both the string that caused it to be a duplicate and the pretty
// prints) as a string (to be able to easily get a hold of it here).
// It filters out the duplicates and then encodes it with a simple scheme
// of length-prefixed strings (and with everything separated by colons),
// e.g. encode the string "string" as "6:string" (length 6, string),
// and the list ["foo", "bar"] as "2:3:foo:3:bar" (2 entries, length 3,
// foo, length 3, bar).
vmService.ObjRef duplicatesDataRef = await serviceClient.evaluate(
isolateRef.id,
mappedData.id,
"""
this
.entries
.where((element) => (element.value as List).length > 1)
.map((dynamic e) {
var keyPart = "\${e.key.length}:\${e.key}";
List value = e.value as List;
var valuePart1 = "\${value.length}";
var valuePart2 = value
.map((element) => '$fieldsToStringPrettyPrintCode')
.map((element) => "\${element.length}:\$element")
.join(":");
return "\${keyPart}:\${valuePart1}:\${valuePart2}";
}).join(":")
""",
);
if (duplicatesDataRef is! vmService.InstanceRef) {
if (duplicatesDataRef is vmService.ErrorRef) {
vmService.Error error = await serviceClient.getObject(
isolateRef.id, duplicatesDataRef.id);
throw "Leak found, but trying to evaluate pretty printing "
"didn't go as planned.\n"
"Got error with message "
"'${error.message}'";
} else {
throw "Leak found, but trying to evaluate pretty printing "
"didn't go as planned.\n"
"Got type '${duplicatesDataRef.runtimeType}':"
"$duplicatesDataRef";
}
}
vmService.Instance duplicatesData =
await serviceClient.getObject(isolateRef.id, duplicatesDataRef.id);
String encodedData = duplicatesData.valueAsString;
try {
return parseEncodedLeakString(encodedData);
} catch (e) {
print("Failure on decoding '$encodedData'");
rethrow;
}
} else {
// No leaks.
return [];
}
}
static List<Leak> parseEncodedLeakString(String leakString) {
int index = 0;
int parseInt() {
int endPartIndex = leakString.indexOf(":", index);
String part = leakString.substring(index, endPartIndex);
int value = int.parse(part);
index = endPartIndex + 1;
return value;
}
String parseString() {
int value = parseInt();
String string = leakString.substring(index, index + value);
index = index + value + 1;
return string;
}
List<Leak> result = [];
while (index < leakString.length) {
String duplicate = parseString();
int count = parseInt();
List<String> prettyPrints = [];
for (int i = 0; i < count; i++) {
String data = parseString();
prettyPrints.add(data);
}
result.add(new Leak(duplicate, count, prettyPrints));
}
return result;
}
int _latestLeakIteration = -1;
void leakDetected(String duplicate, int count, List<String> prettyPrints) {
if (_iterationNumber != _latestLeakIteration) {
print("======================================");
print("WARNING: Duplicated pretty prints of objects.");
print("This might be a memory leak!");
print("");
}
_latestLeakIteration = _iterationNumber;
print("$duplicate ($count)");
for (String prettyPrint in prettyPrints) {
print(" => ${prettyPrint}");
}
print("");
}
void noLeakDetected() {}
bool shouldDoAnotherIteration(int iterationNumber) {
return true;
}
}
class Interest {
final Uri uri;
final String className;
final List<String> fieldNames;
Interest(this.uri, this.className, this.fieldNames);
}
class Leak {
final String duplicate;
final int count;
final List<String> prettyPrints;
Leak(this.duplicate, this.count, this.prettyPrints);
}