| // 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. |
| |
| import 'dart:async'; |
| import 'dart:convert'; |
| import 'dart:io'; |
| import 'dart:math'; |
| |
| import "vm_service_heap_helper.dart" as helper; |
| |
| late Completer completer; |
| |
| Set<String> files = {}; |
| |
| // General idea: Do the once-a-week leak testing script |
| // => In this file: |
| // * Start the frontend_server |
| // * Do a compilation |
| // * Pause the VM and do a "leak iteration" |
| // * Once the VM has been unpaused, do an invalidation etc and repeat. |
| // |
| // This script also makes sure to clone flutter gallery, |
| // but assumes that flutter has been setup as by the script |
| // `tools/bots/flutter/compile_flutter.sh`. |
| |
| Future<void> main(List<String> args) async { |
| if (Platform.isWindows) { |
| throw "This script cannot run on Windows as it uses non-Windows " |
| "assumptions both for the placement of pub packages and the presence " |
| "of 'ln' for symbolic links. It has only been tested on Linux but will " |
| "probably also work on Mac."; |
| } |
| |
| bool quicker = false; |
| bool alternativeInvalidation = false; |
| String? rootPath; |
| |
| for (String arg in args) { |
| if (arg == "--quicker") { |
| quicker = true; |
| } else if (arg == "--alternativeInvalidation") { |
| alternativeInvalidation = true; |
| } else if (arg.startsWith("--path=")) { |
| rootPath = arg.substring("--path=".length); |
| } else { |
| throw "Unknown argument '$arg'"; |
| } |
| } |
| |
| if (rootPath == null) { |
| throw "No path given. Pass with --path=<path>"; |
| } |
| |
| Directory patchedSdk = new Directory("$rootPath/flutter_patched_sdk/"); |
| if (!patchedSdk.existsSync()) { |
| throw "Directory $patchedSdk doesn't exist."; |
| } |
| Uri frontendServerStarter = Platform.script |
| .resolve("../../frontend_server/bin/frontend_server_starter.dart"); |
| if (!new File.fromUri(frontendServerStarter).existsSync()) { |
| throw "File not found: $frontendServerStarter"; |
| } |
| |
| Directory gallery = new Directory("$rootPath/gallery"); |
| if (!gallery.existsSync()) { |
| print("Gallery not found... Attempting to clone via git."); |
| // git clone https://github.com/flutter/gallery.git |
| Process process = await Process.start("git", [ |
| "clone", |
| "https://github.com/flutter/gallery.git", |
| "$rootPath/gallery" |
| ]); |
| process.stdout |
| .transform(utf8.decoder) |
| .transform(new LineSplitter()) |
| .listen((line) { |
| print("git stdout> $line"); |
| }); |
| process.stderr |
| .transform(utf8.decoder) |
| .transform(new LineSplitter()) |
| .listen((line) { |
| print("git stderr> $line"); |
| }); |
| int processExitCode = await process.exitCode; |
| print("Exit code from git: $processExitCode"); |
| |
| process = await Process.start("../flutter/bin/flutter", ["pub", "get"], |
| workingDirectory: "$rootPath/gallery/"); |
| process.stdout |
| .transform(utf8.decoder) |
| .transform(new LineSplitter()) |
| .listen((line) { |
| print("flutter stdout> $line"); |
| }); |
| process.stderr |
| .transform(utf8.decoder) |
| .transform(new LineSplitter()) |
| .listen((line) { |
| print("flutter stderr> $line"); |
| }); |
| processExitCode = await process.exitCode; |
| print("Exit code from flutter: $processExitCode"); |
| |
| // Attempt to hack around strings being truncated to 128 bytes in heap dumps |
| // https://github.com/dart-lang/sdk/blob/c59cdee365b94ce066344840f9e3412d642019b5/runtime/vm/object_graph.cc#L809 |
| // (pub paths can become too long, so two distinct files will look to have |
| // the same url and thus give a false positive). |
| Uri pubDirUri = Uri.parse("file://${Platform.environment['HOME']}/" |
| ".pub-cache/hosted/pub.dartlang.org/"); |
| Directory pubDir = new Directory.fromUri(pubDirUri); |
| if (!pubDir.existsSync()) throw "Expected to find $pubDir"; |
| |
| File galleryPackageConfig = |
| new File("$rootPath/gallery/.dart_tool/package_config.json"); |
| if (!galleryPackageConfig.existsSync()) { |
| throw "Didn't find $galleryPackageConfig"; |
| } |
| String data = galleryPackageConfig.readAsStringSync(); |
| data = data.replaceAll(pubDirUri.toString(), "../pub/"); |
| galleryPackageConfig.writeAsStringSync(data); |
| |
| process = await Process.start("ln", ["-s", pubDir.path, "pub"], |
| workingDirectory: "$rootPath/gallery/"); |
| process.stdout |
| .transform(utf8.decoder) |
| .transform(new LineSplitter()) |
| .listen((line) { |
| print("ln stdout> $line"); |
| }); |
| process.stderr |
| .transform(utf8.decoder) |
| .transform(new LineSplitter()) |
| .listen((line) { |
| print("ln stderr> $line"); |
| }); |
| processExitCode = await process.exitCode; |
| print("Exit code from ln: $processExitCode"); |
| } |
| |
| File packageConfig = |
| new File("$rootPath/gallery/.dart_tool/package_config.json"); |
| if (!packageConfig.existsSync()) { |
| throw "Didn't find $packageConfig"; |
| } |
| |
| List<helper.Interest> interests = <helper.Interest>[]; |
| interests.add(new helper.Interest( |
| Uri.parse("package:kernel/ast.dart"), |
| "Library", |
| ["fileUri"], |
| )); |
| helper.VMServiceHeapHelperSpecificExactLeakFinder heapHelper = |
| new helper.VMServiceHeapHelperSpecificExactLeakFinder( |
| interests: interests, |
| prettyPrints: [ |
| new helper.Interest( |
| Uri.parse("package:kernel/ast.dart"), |
| "Library", |
| ["fileUri", "libraryIdForTesting"], |
| ), |
| ], |
| throwOnPossibleLeak: true, |
| ); |
| |
| print("About to run with " |
| "quicker = $quicker; " |
| "alternativeInvalidation = $alternativeInvalidation; " |
| "path = $rootPath; " |
| "..."); |
| |
| List<String> processArgs = [ |
| "--disable_dart_dev", |
| "--disable-service-auth-codes", |
| frontendServerStarter.toString(), |
| "--sdk-root", |
| "$rootPath/flutter_patched_sdk/", |
| "--incremental", |
| "--target=flutter", |
| "--debugger-module-names", |
| "--output-dill", |
| "$rootPath/flutter_server_tmp.dill", |
| "--packages", |
| "$rootPath/gallery/.dart_tool/package_config.json", |
| "-Ddart.vm.profile=false", |
| "-Ddart.vm.product=false", |
| "--enable-asserts", |
| "--track-widget-creation", |
| "--initialize-from-dill", |
| "$rootPath/cache.dill", |
| ]; |
| if (alternativeInvalidation) { |
| processArgs.add("--enable-experiment=alternative-invalidation-strategy"); |
| } |
| |
| await heapHelper.start(processArgs, |
| stdoutReceiver: (s) { |
| if (s.startsWith("+")) { |
| files.add(s.substring(1)); |
| } else if (s.startsWith("-")) { |
| files.remove(s.substring(1)); |
| } else { |
| List<String> split = s.split(" "); |
| if (int.tryParse(split.last) != null && |
| split[split.length - 2].endsWith(".dill")) { |
| // e.g. something like "filename.dill 0" for output file and 0 |
| // errors. |
| completer.complete(); |
| } else { |
| print("out> $s"); |
| } |
| } |
| }, |
| stderrReceiver: (s) => print("err> $s")); |
| |
| Stopwatch stopwatch = new Stopwatch()..start(); |
| await sendAndWait(heapHelper.process, ['compile package:gallery/main.dart']); |
| print("First compile took ${stopwatch.elapsedMilliseconds} ms"); |
| await pauseAndWait(heapHelper); |
| |
| await recompileAndWait(heapHelper.process, "package:gallery/main.dart", []); |
| await accept(heapHelper); |
| await sendAndWaitSetSelection(heapHelper.process); |
| await sendAndWaitToObjectForSourceLocation(heapHelper.process); |
| await sendAndWaitToObject(heapHelper.process); |
| await pauseAndWait(heapHelper); |
| |
| print("Knows about ${files.length} files..."); |
| List<String> listFiles = new List<String>.from(files); |
| int iteration = 0; |
| for (String s in listFiles) { |
| print("On iteration ${iteration++} / ${listFiles.length}"); |
| print(" => Invalidating $s"); |
| stopwatch.reset(); |
| await recompileAndWait( |
| heapHelper.process, "package:gallery/main.dart", [s]); |
| await accept(heapHelper); |
| print("Recompile took ${stopwatch.elapsedMilliseconds} ms"); |
| await sendAndWaitSetSelection(heapHelper.process); |
| await sendAndWaitToObjectForSourceLocation(heapHelper.process); |
| await sendAndWaitToObject(heapHelper.process); |
| if (quicker) { |
| if (iteration % 10 == 0) { |
| await pauseAndWait(heapHelper); |
| } |
| } else { |
| await pauseAndWait(heapHelper); |
| } |
| } |
| if (quicker) { |
| await pauseAndWait(heapHelper); |
| } |
| |
| // We should now be done. |
| print("Done!"); |
| heapHelper.process.kill(); |
| } |
| |
| Future accept( |
| helper.VMServiceHeapHelperSpecificExactLeakFinder heapHelper) async { |
| heapHelper.process.stdin.writeln('accept'); |
| int waits = 0; |
| while (!await heapHelper.isIdle()) { |
| if (waits > 100) { |
| // Waited for at least 10 seconds --- assume there's something wrong. |
| throw "Timed out waiting to become idle!"; |
| } |
| await new Future.delayed(new Duration(milliseconds: 100)); |
| waits++; |
| } |
| } |
| |
| class Uuid { |
| final Random _random = new Random(); |
| |
| /// Generate a version 4 (random) uuid. This is a uuid scheme that only uses |
| /// random numbers as the source of the generated uuid. |
| String generateV4() { |
| // Generate xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx / 8-4-4-4-12. |
| int special = 8 + _random.nextInt(4); |
| |
| return '${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}-' |
| '${_bitsDigits(16, 4)}-' |
| '4${_bitsDigits(12, 3)}-' |
| '${_printDigits(special, 1)}${_bitsDigits(12, 3)}-' |
| '${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}${_bitsDigits(16, 4)}'; |
| } |
| |
| String _bitsDigits(int bitCount, int digitCount) => |
| _printDigits(_generateBits(bitCount), digitCount); |
| |
| int _generateBits(int bitCount) => _random.nextInt(1 << bitCount); |
| |
| String _printDigits(int value, int count) => |
| value.toRadixString(16).padLeft(count, '0'); |
| } |
| |
| Future pauseAndWait( |
| helper.VMServiceHeapHelperSpecificExactLeakFinder heapHelper) async { |
| int prevIterationNumber = heapHelper.iterationNumber; |
| await heapHelper.pause(); |
| |
| int waits = 0; |
| while (heapHelper.iterationNumber == prevIterationNumber) { |
| if (waits > 10000) { |
| // Waited for at least 1000 seconds --- assume there's something wrong. |
| throw "Timed out waiting for the helper iteration number to increase!"; |
| } |
| await new Future.delayed(new Duration(milliseconds: 100)); |
| waits++; |
| } |
| } |
| |
| Future recompileAndWait( |
| Process _server, String what, List<String> invalidates) async { |
| String inputKey = Uuid().generateV4(); |
| List<String> data = ['recompile $what $inputKey']; |
| invalidates.forEach(data.add); |
| data.add('$inputKey'); |
| await sendAndWait(_server, data); |
| } |
| |
| Future sendAndWait(Process _server, List<String> data) async { |
| completer = new Completer(); |
| data.forEach(_server.stdin.writeln); |
| await completer.future; |
| } |
| |
| Future sendAndWaitDebugDidSendFirstFrameEvent(Process _server) async { |
| String inputKey = Uuid().generateV4(); |
| await sendAndWait(_server, [ |
| /* 'compile-expression' <boundarykey> */ 'compile-expression $inputKey', |
| /* expression */ 'WidgetsBinding.instance.debugDidSendFirstFrameEvent', |
| /* no definitions */ |
| /* <boundarykey> */ inputKey, |
| /* no type-definitions */ |
| /* <boundarykey> */ inputKey, |
| /* libraryUri */ 'package:flutter/src/widgets/binding.dart', |
| /* class */ '', |
| /* isStatic */ 'true' |
| ]); |
| } |
| |
| Future sendAndWaitSetSelection(Process _server) async { |
| String inputKey = Uuid().generateV4(); |
| await sendAndWait(_server, [ |
| /* 'compile-expression' <boundarykey> */ 'compile-expression $inputKey', |
| /* expression */ 'WidgetInspectorService.instance.setSelection(' |
| 'arg1, "dummy_68")', |
| /* definition #1 */ 'arg1', |
| /* <boundarykey> */ inputKey, |
| /* no type-definitions */ |
| /* <boundarykey> */ inputKey, |
| /* libraryUri */ 'package:flutter/src/widgets/widget_inspector.dart', |
| /* class */ '', |
| /* isStatic */ 'true' |
| ]); |
| } |
| |
| Future sendAndWaitToObject(Process _server) async { |
| String inputKey = Uuid().generateV4(); |
| await sendAndWait(_server, [ |
| /* 'compile-expression' <boundarykey> */ 'compile-expression $inputKey', |
| /* expression */ 'WidgetInspectorService.instance.toObject(' |
| '"inspector-836", "tree_112")', |
| /* no definitions */ |
| /* <boundarykey> */ inputKey, |
| /* no type-definitions */ |
| /* <boundarykey> */ inputKey, |
| /* libraryUri */ 'package:flutter/src/widgets/widget_inspector.dart', |
| /* class */ '', |
| /* isStatic */ 'true' |
| ]); |
| } |
| |
| Future sendAndWaitToObjectForSourceLocation(Process _server) async { |
| String inputKey = Uuid().generateV4(); |
| await sendAndWait(_server, [ |
| /* 'compile-expression' <boundarykey> */ 'compile-expression $inputKey', |
| /* expression */ 'WidgetInspectorService.instance.' |
| 'toObjectForSourceLocation("inspector-607", "tree_112")', |
| /* no definitions */ |
| /* <boundarykey> */ inputKey, |
| /* no type-definitions */ |
| /* <boundarykey> */ inputKey, |
| /* libraryUri */ 'package:flutter/src/widgets/widget_inspector.dart', |
| /* class */ '', |
| /* isStatic */ 'true' |
| ]); |
| } |