| // 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); |
| } |