blob: 922866fbee3c8f0425ad6f7762ec128fc2f3b4c9 [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.md file.
// @dart = 2.9
import 'dart:async';
import 'dart:io';
import "simple_stats.dart";
import "vm_service_helper.dart" as vmService;
const int limit = 10;
main(List<String> args) async {
LeakFinder heapHelper = new LeakFinder();
heapHelper.start([
"--disable-dart-dev",
"--enable-asserts",
Platform.script.resolve("incremental_dart2js_tester.dart").toString(),
"--addDebugBreaks",
"--fast",
"--limit=$limit",
]);
}
class LeakFinder extends vmService.LaunchingVMServiceHelper {
@override
Future<void> run() async {
vmService.VM vm = await serviceClient.getVM();
if (vm.isolates.length != 1) {
throw "Expected 1 isolate, got ${vm.isolates.length}";
}
vmService.IsolateRef isolateRef = vm.isolates.single;
await waitUntilIsolateIsRunnable(isolateRef.id);
await serviceClient.resume(isolateRef.id);
Map<vmService.ClassRef, List<int>> instanceCounts =
new Map<vmService.ClassRef, List<int>>();
Map<vmService.ClassRef, vmService.Class> classInfo =
new Map<vmService.ClassRef, vmService.Class>();
Completer<String> cTimeout = new Completer();
Timer timer = new Timer(new Duration(minutes: 6), () {
cTimeout.complete("Timeout");
killProcess();
});
Completer<String> cRunDone = new Completer();
// ignore: unawaited_futures
runInternal(
isolateRef,
classInfo,
instanceCounts,
(int iteration) =>
// Subtract 2 as it's logically one ahead and asks _before_ the run.
(iteration - 2) > limit ||
cTimeout.isCompleted ||
cProcessExited.isCompleted).then((value) {
cRunDone.complete("Done");
});
await Future.any([cRunDone.future, cTimeout.future, cProcessExited.future]);
timer.cancel();
print("\n\n======================\n\n");
findPossibleLeaks(instanceCounts, classInfo);
// Make sure the process doesn't hang.
killProcess();
}
void findPossibleLeaks(Map<vmService.ClassRef, List<int>> instanceCounts,
Map<vmService.ClassRef, vmService.Class> classInfo) {
bool foundLeak = false;
for (vmService.ClassRef c in instanceCounts.keys) {
List<int> listOfInstanceCounts = instanceCounts[c];
// Ignore VM internal stuff like "PatchClass", "PcDescriptors" etc.
// (they don't have a url).
vmService.Class classDetails = classInfo[c];
String uriString = classDetails.location?.script?.uri;
if (uriString == null) continue;
// For now ignore anything not in package:kernel or package:front_end.
if (ignoredClass(classDetails)) continue;
// If they're all equal there's nothing to talk about.
bool same = true;
for (int i = 1; i < listOfInstanceCounts.length; i++) {
if (listOfInstanceCounts[i] != listOfInstanceCounts[0]) {
same = false;
break;
}
}
if (same) continue;
int midPoint = listOfInstanceCounts.length ~/ 2;
List<int> firstHalf = listOfInstanceCounts.sublist(0, midPoint);
List<int> secondHalf = listOfInstanceCounts.sublist(midPoint);
TTestResult ttestResult = SimpleTTestStat.ttest(secondHalf, firstHalf);
if (!strictClass(classDetails)) {
if (!ttestResult.significant) continue;
// TODO(jensj): We could possibly also ignore if it's less (i.e. a
// negative change), or if the change is < 1%, or the change minus the
// confidence is < 1% etc.
}
print("Differences on ${c.name} (${uriString}): "
"$listOfInstanceCounts ($ttestResult)");
foundLeak = true;
}
print("\n\n");
if (foundLeak) {
print("Possible leak(s) found.");
print("(Note that this doesn't guarantee that there are any!)");
exitCode = 1;
} else {
print("Didn't identify any leaks.");
print("(Note that this doesn't guarantee that there are none!)");
exitCode = 0;
}
}
Future<void> runInternal(
vmService.IsolateRef isolateRef,
Map<vmService.ClassRef, vmService.Class> classInfo,
Map<vmService.ClassRef, List<int>> instanceCounts,
bool Function(int iteration) shouldBail) async {
int iterationNumber = 1;
try {
while (true) {
if (shouldBail(iterationNumber)) break;
if (!await waitUntilPaused(isolateRef.id)) break;
print("\n\n====================\n\nIteration #$iterationNumber");
iterationNumber++;
vmService.AllocationProfile allocationProfile =
await forceGC(isolateRef.id);
for (vmService.ClassHeapStats member in allocationProfile.members) {
if (!classInfo.containsKey(member.classRef)) {
vmService.Class c = await serviceClient.getObject(
isolateRef.id, member.classRef.id);
classInfo[member.classRef] = c;
}
List<int> listOfInstanceCounts = instanceCounts[member.classRef];
if (listOfInstanceCounts == null) {
listOfInstanceCounts = instanceCounts[member.classRef] = <int>[];
}
while (listOfInstanceCounts.length < iterationNumber - 2) {
listOfInstanceCounts.add(0);
}
listOfInstanceCounts.add(member.instancesCurrent);
if (listOfInstanceCounts.length != iterationNumber - 1) {
throw "Unexpected length";
}
}
await serviceClient.resume(isolateRef.id);
}
} catch (e) {
print("Got error: $e");
}
}
Completer<String> cProcessExited = new Completer();
void processExited(int exitCode) {
cProcessExited.complete("Exit");
}
bool ignoredClass(vmService.Class classDetails) {
String uriString = classDetails.location?.script?.uri;
if (uriString == null) return true;
if (uriString.startsWith("package:front_end/")) {
// Classes used for lazy initialization will naturally fluctuate.
if (classDetails.name == "DillClassBuilder") return true;
if (classDetails.name == "DillExtensionBuilder") return true;
if (classDetails.name == "DillExtensionMemberBuilder") return true;
if (classDetails.name == "DillMemberBuilder") return true;
if (classDetails.name == "DillTypeAliasBuilder") return true;
// These classes have proved to fluctuate, although the reason is less
// clear.
if (classDetails.name == "InheritedImplementationInterfaceConflict") {
return true;
}
if (classDetails.name == "AbstractMemberOverridingImplementation") {
return true;
}
if (classDetails.name == "VoidTypeBuilder") return true;
if (classDetails.name == "NamedTypeBuilder") return true;
if (classDetails.name == "DillClassMember") return true;
if (classDetails.name == "Scope") return true;
if (classDetails.name == "ConstructorScope") return true;
if (classDetails.name == "ScopeBuilder") return true;
if (classDetails.name == "ConstructorScopeBuilder") return true;
if (classDetails.name == "NullTypeDeclarationBuilder") return true;
if (classDetails.name == "NullabilityBuilder") return true;
return false;
} else if (uriString.startsWith("package:kernel/")) {
// DirtifyingList is used for lazy stuff and naturally change in numbers.
if (classDetails.name == "DirtifyingList") return true;
// Constants are canonicalized in their compilation run and will thus
// naturally increase, e.g. we can get 2 more booleans every time (up to
// a maximum of 2 per library or however many would have been there if we
// didn't canonicalize at all).
if (classDetails.name.endsWith("Constant")) return true;
// These classes have proved to fluctuate, although the reason is less
// clear.
if (classDetails.name == "InterfaceType") return true;
return false;
}
return true;
}
// I have commented out the lazy ones below.
Set<String> frontEndStrictClasses = {
// "DillClassBuilder",
// "DillExtensionBuilder",
// "DillExtensionMemberBuilder",
"DillLibraryBuilder",
"DillLoader",
// "DillMemberBuilder",
"DillTarget",
// "DillTypeAliasBuilder",
"SourceClassBuilder",
"SourceExtensionBuilder",
"SourceLibraryBuilder",
"SourceLoader",
};
Set<String> kernelAstStrictClasses = {
"Class",
"Constructor",
"Extension",
"Field",
"Library",
"Procedure",
"RedirectingFactory",
"Typedef",
};
bool strictClass(vmService.Class classDetails) {
if (!kernelAstStrictClasses.contains(classDetails.name) &&
!frontEndStrictClasses.contains(classDetails.name)) return false;
if (kernelAstStrictClasses.contains(classDetails.name) &&
classDetails.location?.script?.uri == "package:kernel/ast.dart") {
return true;
}
if (frontEndStrictClasses.contains(classDetails.name) &&
classDetails.location?.script?.uri?.startsWith("package:front_end/") ==
true) {
return true;
}
throw "$classDetails: ${classDetails.name} --- ${classDetails.location}";
}
}