| // 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. |
| |
| // ignore_for_file: lines_longer_than_80_chars |
| |
| import 'dart:io'; |
| import 'dart:typed_data'; |
| |
| import 'package:_fe_analyzer_shared/src/util/options.dart'; |
| import 'package:front_end/src/base/file_system_dependency_tracker.dart'; |
| import 'package:front_end/src/base/processed_options.dart'; |
| import 'package:kernel/binary/ast_from_binary.dart'; |
| import 'package:kernel/kernel.dart'; |
| |
| import '../additional_targets.dart'; |
| import '../command_line.dart'; |
| import '../compile.dart' as cfe_compile; |
| |
| /// Instrumenter that can produce flame graphs, count invocations, |
| /// perform time tracking etc. |
| /// |
| /// ### Example of using this to produce data for a flame graph |
| /// |
| /// ``` |
| /// out/ReleaseX64/dart pkg/front_end/tool/flame/instrumenter.dart pkg/front_end/tool/compile.dart |
| /// out/ReleaseX64/dart pkg/front_end/tool/compile.dart.dill.instrumented.dill --omit-platform pkg/front_end/tool/compile.dart |
| /// out/ReleaseX64/dart pkg/front_end/tool/flame/instrumenter.dart pkg/front_end/tool/compile.dart --candidates=cfe_compile_trace_candidates.txt |
| /// out/ReleaseX64/dart pkg/front_end/tool/compile.dart.dill.instrumented.dill --omit-platform pkg/front_end/tool/compile.dart |
| /// out/ReleaseX64/dart pkg/front_end/tool/flame/instrumenter.dart pkg/front_end/tool/compile.dart --candidates=cfe_compile_trace_candidates_subsequent.txt |
| /// out/ReleaseX64/dart pkg/front_end/tool/compile.dart.dill.instrumented.dill --omit-platform pkg/front_end/tool/compile.dart |
| /// ``` |
| /// |
| /// Where it's instrumented in several passes to automatically find the |
| /// "interesting" procedures to instrument which gives a good overview without |
| /// costing too much (and thereby still display ~correct timings). |
| /// |
| /// This produces a file "cfe_compile_trace.txt" that can be displayed via |
| /// Chromes about://tracing tool. |
| /// |
| /// |
| /// ### Example of using this to count method calls |
| /// |
| /// ``` |
| /// out/ReleaseX64/dart pkg/front_end/tool/flame/instrumenter.dart pkg/front_end/tool/compile.dart --count |
| /// out/ReleaseX64/dart pkg/front_end/tool/compile.dart.dill.instrumented.dill pkg/front_end/tool/compile.dart |
| /// ``` |
| /// |
| /// It will produce an output like this: |
| /// ``` |
| /// [...] |
| /// 4,597,852: utf8_bytes_scanner.dart|Utf8BytesScanner.stringOffset |
| /// 4,775,443: ast_to_binary.dart|BinaryPrinter.writeUInt30 |
| /// 5,213,581: token.dart|SimpleToken.kind |
| /// 5,299,735: ast_to_binary.dart|BufferedSink.addByte |
| /// 8,253,178: abstract_scanner.dart|_isIdentifierChar |
| /// 11,853,919: util.dart|optional |
| /// 12,889,502: token.dart|SimpleToken.stringValue |
| /// 20,468,609: token.dart|SimpleToken.type |
| /// 20,749,114: utf8_bytes_scanner.dart|Utf8BytesScanner.advance |
| /// ``` |
| /// |
| /// ### Example of using this to get combined time on stack |
| /// |
| /// ``` |
| /// out/ReleaseX64/dart pkg/front_end/tool/flame/instrumenter.dart -Diterations=10 pkg/front_end/tool/compile.dart --single-timer "--candidates-raw=flow_analysis.dart|*" |
| /// out/ReleaseX64/dart pkg/front_end/tool/compile.dart.dill.instrumented.dill pkg/front_end/tool/compile.dart |
| /// ``` |
| /// |
| /// This will give a combined runtime of when any of the instrumented procedures |
| /// was on the stack. In the example note how `-Diterations=10` will be passed |
| /// to the compilation, but that the "candidates" (i.e. the data to instrument) |
| /// is given directly via `"--candidates-raw=flow_analysis.dart|*"` and uses the |
| /// `*` as a wildcard meaning everything in this file. |
| /// |
| /// It will produce an output like this: |
| /// ``` |
| /// Runtime: 3834491044 |
| /// Runtime in seconds: 3.834491044 |
| /// Visits: 52643690 |
| /// Active: 0 |
| /// Stopwatch frequency: 1000000000 |
| /// ``` |
| /// |
| /// ### Example of using this to get timings for when on stack: |
| /// |
| /// ``` |
| /// out/ReleaseX64/dart --enable-asserts pkg/front_end/tool/flame/instrumenter.dart -Diterations=10 pkg/front_end/tool/compile.dart --timer "--candidates-raw=flow_analysis.dart|*" |
| /// out/ReleaseX64/dart pkg/front_end/tool/compile.dart.dill.instrumented.dill pkg/front_end/tool/compile.dart |
| /// ``` |
| /// |
| /// This will give runtime info for all instrumented procedures, timing when |
| /// they're on the stack. |
| /// Note in the example output below for instance `_FlowAnalysisImpl._merge` |
| /// just passes to `FlowModel.merge`, so while the "self time" of the first is |
| /// almost nothing it's actually on the stack (slightly) longer. |
| /// |
| /// This will produce output like this: |
| /// ``` |
| /// [...] |
| /// flow_analysis.dart|_FlowAnalysisImpl.propertyGet: runtime: 818095151 (0.818095151 s), visits: 1328320, active: 0 |
| /// flow_analysis.dart|FlowModel._updateVariableInfo: runtime: 827669322 (0.827669322 s), visits: 968180, active: 0 |
| /// flow_analysis.dart|_FlowAnalysisImpl.variableRead: runtime: 1012755488 (1.012755488 s), visits: 1100140, active: 0 |
| /// flow_analysis.dart|FlowModel.joinVariableInfo: runtime: 1118758076 (1.118758076 s), visits: 320810, active: 0 |
| /// flow_analysis.dart|FlowModel.merge: runtime: 1185477853 (1.185477853 s), visits: 334100, active: 0 |
| /// flow_analysis.dart|_FlowAnalysisImpl._merge: runtime: 1238735352 (1.238735352 s), visits: 334100, active: 0 |
| /// ``` |
| Future<void> main(List<String> arguments) async { |
| Directory tmpDir = Directory.systemTemp.createTempSync("cfe_instrumenter"); |
| try { |
| await _main(arguments, tmpDir); |
| } finally { |
| tmpDir.deleteSync(recursive: true); |
| } |
| } |
| |
| Future<void> _main(List<String> inputArguments, Directory tmpDir) async { |
| List<String> candidates = []; |
| List<String> candidatesRaw = []; |
| List<String> arguments = []; |
| bool doCount = false; |
| bool doTimer = false; |
| bool doSingleTimer = false; |
| for (String arg in inputArguments) { |
| if (arg == "--count") { |
| doCount = true; |
| } else if (arg == "--timer") { |
| doTimer = true; |
| } else if (arg == "--single-timer") { |
| doSingleTimer = true; |
| } else if (arg.startsWith("--candidates=")) { |
| candidates.add(arg.substring("--candidates=".length)); |
| } else if (arg.startsWith("--candidates-raw=")) { |
| candidatesRaw.add(arg.substring("--candidates-raw=".length)); |
| } else { |
| arguments.add(arg); |
| } |
| } |
| bool reportCandidates = candidates.isEmpty && candidatesRaw.isEmpty; |
| |
| installAdditionalTargets(); |
| |
| Uri output = parseCompilerArguments(arguments); |
| |
| Map<String, Set<String>> wanted = setupWantedMap(candidates, candidatesRaw); |
| |
| String libFilename = "instrumenter_lib.dart"; |
| if (doCount) { |
| libFilename = "instrumenter_lib_counter.dart"; |
| } else if (doTimer) { |
| libFilename = "instrumenter_lib_timer.dart"; |
| } else if (doSingleTimer) { |
| libFilename = "instrumenter_lib_single_timer.dart"; |
| } |
| |
| await compileInstrumentationLibrary( |
| tmpDir, |
| new TimerCounterInstrumenterConfig( |
| libFilename: libFilename, |
| reportCandidates: reportCandidates, |
| wanted: wanted, |
| includeAll: reportCandidates, |
| includeConstructors: |
| !reportCandidates || doCount || doTimer || doSingleTimer), |
| arguments, |
| output); |
| } |
| |
| Uri parseCompilerArguments(List<String> arguments) { |
| installAdditionalTargets(); |
| FileSystemDependencyTracker tracker = new FileSystemDependencyTracker(); |
| ParsedOptions parsedOptions = |
| ParsedOptions.parse(arguments, optionSpecification); |
| ProcessedOptions options = |
| analyzeCommandLine(tracker, "compile", parsedOptions, true); |
| Uri? output = options.output; |
| if (output == null) throw "No output"; |
| if (!output.isScheme("file")) throw "Output won't be saved"; |
| return output; |
| } |
| |
| abstract class InstrumenterConfig { |
| String get libFilename; |
| String get beforeName; |
| String get enterName; |
| String get exitName; |
| String get afterName; |
| |
| bool includeProcedure(Procedure procedure); |
| bool includeConstructor(Constructor constructor); |
| |
| Arguments createBeforeArguments( |
| List<Procedure> procedures, List<Constructor> constructors); |
| |
| Arguments createAfterArguments( |
| List<Procedure> procedures, List<Constructor> constructors); |
| |
| Arguments createEnterArguments(int id, Member member); |
| |
| Arguments createExitArguments(int id, Member member); |
| } |
| |
| class TimerCounterInstrumenterConfig implements InstrumenterConfig { |
| @override |
| final String libFilename; |
| final bool reportCandidates; |
| final bool includeAll; |
| final bool includeConstructors; |
| final Map<String, Set<String>> wanted; |
| |
| TimerCounterInstrumenterConfig( |
| {required this.libFilename, |
| required this.reportCandidates, |
| required this.includeAll, |
| required this.includeConstructors, |
| required this.wanted}); |
| |
| @override |
| String get beforeName => 'initialize'; |
| |
| @override |
| String get enterName => 'enter'; |
| |
| @override |
| String get exitName => 'exit'; |
| |
| @override |
| String get afterName => 'report'; |
| |
| @override |
| bool includeProcedure(Procedure p) { |
| if (includeAll) return true; |
| String name = getProcedureName(p); |
| Set<String> procedureNamesWantedInFile = |
| wanted[p.fileUri.pathSegments.last] ?? const {}; |
| return procedureNamesWantedInFile.contains(name) || |
| !procedureNamesWantedInFile.contains("*"); |
| } |
| |
| @override |
| bool includeConstructor(Constructor c) { |
| if (!includeConstructors) return false; |
| if (includeAll) return true; |
| String name = getConstructorName(c); |
| Set<String> constructorNamesWantedInFile = |
| wanted[c.fileUri.pathSegments.last] ?? const {}; |
| return constructorNamesWantedInFile.contains(name) || |
| constructorNamesWantedInFile.contains("*"); |
| } |
| |
| @override |
| Arguments createBeforeArguments( |
| List<Procedure> procedures, List<Constructor> constructors) { |
| return new Arguments([ |
| new IntLiteral(procedures.length + constructors.length), |
| new BoolLiteral(reportCandidates), |
| ]); |
| } |
| |
| @override |
| Arguments createAfterArguments( |
| List<Procedure> procedures, List<Constructor> constructors) { |
| return new Arguments([ |
| new ListLiteral([ |
| ...procedures |
| .map((p) => new StringLiteral("${p.fileUri.pathSegments.last}|" |
| "${getProcedureName(p)}")), |
| ...constructors |
| .map((c) => new StringLiteral("${c.fileUri.pathSegments.last}|" |
| "${getConstructorName(c)}")), |
| ]), |
| ]); |
| } |
| |
| @override |
| Arguments createEnterArguments(int id, Member member) { |
| return new Arguments([new IntLiteral(id)]); |
| } |
| |
| @override |
| Arguments createExitArguments(int id, Member member) { |
| return new Arguments([new IntLiteral(id)]); |
| } |
| } |
| |
| Future<void> compileInstrumentationLibrary(Directory tmpDir, |
| InstrumenterConfig config, List<String> arguments, Uri output) async { |
| print("Compiling the instrumentation library."); |
| Uri instrumentationLibDill = tmpDir.uri.resolve("instrumenter.dill"); |
| await cfe_compile.main([ |
| "--omit-platform", |
| "-o=${instrumentationLibDill.toFilePath()}", |
| Platform.script.resolve(config.libFilename).toFilePath() |
| ]); |
| if (!File.fromUri(instrumentationLibDill).existsSync()) { |
| throw "Instrumentation library didn't compile as expected."; |
| } |
| |
| print("Compiling the given input."); |
| await cfe_compile.main(arguments); |
| |
| print("Reading the compiled dill."); |
| Component component = new Component(); |
| Uint8List bytes = new File.fromUri(output).readAsBytesSync(); |
| new BinaryBuilder(bytes).readComponent(component); |
| |
| int librariesBefore = component.libraries.length; |
| |
| bytes = File.fromUri(instrumentationLibDill).readAsBytesSync(); |
| new BinaryBuilder(bytes).readComponent(component); |
| |
| assert(librariesBefore + 1 == component.libraries.length); |
| |
| List<Procedure> procedures = []; |
| List<Constructor> constructors = []; |
| for (int i = 0; i < librariesBefore; i++) { |
| Library lib = component.libraries[i]; |
| if (lib.importUri.scheme == "dart") continue; |
| for (Class c in lib.classes) { |
| addIfWantedProcedures(config, procedures, c.procedures); |
| addIfWantedConstructors(config, constructors, c.constructors); |
| } |
| addIfWantedProcedures(config, procedures, lib.procedures); |
| } |
| print("Procedures: ${procedures.length}"); |
| print("Constructors: ${constructors.length}"); |
| |
| // TODO: Check that this is true. |
| Library instrumenterLib = component.libraries |
| .singleWhere((lib) => lib.fileUri.path.endsWith(config.libFilename)); |
| Procedure instrumenterInitialize = instrumenterLib.procedures |
| .singleWhere((p) => p.name.text == config.beforeName); |
| Procedure instrumenterEnter = instrumenterLib.procedures |
| .singleWhere((p) => p.name.text == config.enterName); |
| Procedure instrumenterExit = instrumenterLib.procedures |
| .singleWhere((p) => p.name.text == config.exitName); |
| Procedure instrumenterReport = instrumenterLib.procedures |
| .singleWhere((p) => p.name.text == config.afterName); |
| |
| int id = 0; |
| for (Procedure p in procedures) { |
| int thisId = id++; |
| wrapProcedure(config, p, thisId, instrumenterEnter, instrumenterExit); |
| } |
| for (Constructor c in constructors) { |
| int thisId = id++; |
| wrapConstructor(config, c, thisId, instrumenterEnter, instrumenterExit); |
| } |
| |
| initializeAndReport(config, component.mainMethod!, instrumenterInitialize, |
| procedures, constructors, instrumenterReport); |
| |
| print("Writing output."); |
| String outString = output.toFilePath() + ".instrumented.dill"; |
| await writeComponentToBinary(component, outString); |
| print("Wrote to $outString"); |
| } |
| |
| void addIfWantedProcedures( |
| InstrumenterConfig config, List<Procedure> output, List<Procedure> input) { |
| for (Procedure p in input) { |
| if (p.function.body == null) continue; |
| // Yielding functions doesn't work well with the begin/end scheme. |
| if (p.function.dartAsyncMarker == AsyncMarker.SyncStar) continue; |
| if (p.function.dartAsyncMarker == AsyncMarker.AsyncStar) continue; |
| if (config.includeProcedure(p)) { |
| output.add(p); |
| } |
| } |
| } |
| |
| void addIfWantedConstructors(InstrumenterConfig config, |
| List<Constructor> output, List<Constructor> input) { |
| for (Constructor c in input) { |
| if (c.isExternal) continue; |
| if (config.includeConstructor(c)) { |
| output.add(c); |
| } |
| } |
| } |
| |
| String getProcedureName(Procedure p) { |
| String name = p.name.text; |
| if (p.isSetter) { |
| name = "set:$name"; |
| } |
| if (p.parent is Class) { |
| return "${(p.parent as Class).name}.$name"; |
| } else { |
| return name; |
| } |
| } |
| |
| String getConstructorName(Constructor c) { |
| String name = "constructor:${c.name.text}"; |
| Class parent = c.parent as Class; |
| return "${parent.name}.$name"; |
| } |
| |
| Map<String, Set<String>> setupWantedMap( |
| List<String> candidates, List<String> candidatesRaw) { |
| Map<String, Set<String>> wanted = {}; |
| for (String filename in candidates) { |
| File f = new File(filename); |
| if (!f.existsSync()) throw "$filename doesn't exist."; |
| for (String line in f.readAsLinesSync()) { |
| int index = line.indexOf("|"); |
| if (index < 0) throw "Not correctly formatted: $line (from $filename)"; |
| String file = line.substring(0, index); |
| String displayName = line.substring(index + 1); |
| Set<String> existingInFile = wanted[file] ??= {}; |
| existingInFile.add(displayName); |
| } |
| } |
| for (String raw in candidatesRaw) { |
| for (String line in raw.split(",")) { |
| int index = line.indexOf("|"); |
| if (index < 0) throw "Not correctly formatted: $line ($raw)"; |
| String file = line.substring(0, index); |
| String displayName = line.substring(index + 1); |
| Set<String> existingInFile = wanted[file] ??= {}; |
| existingInFile.add(displayName); |
| } |
| } |
| return wanted; |
| } |
| |
| void initializeAndReport( |
| InstrumenterConfig config, |
| Procedure mainProcedure, |
| Procedure initializeProcedure, |
| List<Procedure> procedures, |
| List<Constructor> constructors, |
| Procedure instrumenterReport) { |
| Block block = new Block([ |
| new ExpressionStatement(new StaticInvocation(initializeProcedure, |
| config.createBeforeArguments(procedures, constructors))), |
| new TryFinally( |
| mainProcedure.function.body as Statement, |
| new ExpressionStatement(new StaticInvocation(instrumenterReport, |
| config.createAfterArguments(procedures, constructors)))), |
| ]); |
| mainProcedure.function.body = block; |
| block.parent = mainProcedure.function; |
| } |
| |
| void wrapProcedure(InstrumenterConfig config, Procedure p, int id, |
| Procedure instrumenterEnter, Procedure instrumenterExit) { |
| Block block = new Block([ |
| new ExpressionStatement(new StaticInvocation( |
| instrumenterEnter, config.createEnterArguments(id, p))), |
| p.function.body as Statement |
| ]); |
| TryFinally tryFinally = new TryFinally( |
| block, |
| new ExpressionStatement(new StaticInvocation( |
| instrumenterExit, config.createExitArguments(id, p)))); |
| p.function.body = tryFinally; |
| tryFinally.parent = p.function; |
| } |
| |
| void wrapConstructor(InstrumenterConfig config, Constructor c, int id, |
| Procedure instrumenterEnter, Procedure instrumenterExit) { |
| Arguments enterArguments = config.createEnterArguments(id, c); |
| Arguments exitArguments = config.createExitArguments(id, c); |
| if (c.function.body == null || c.function.body is EmptyStatement) { |
| // We just completely replace the body. |
| Block block = new Block([ |
| new ExpressionStatement( |
| new StaticInvocation(instrumenterEnter, enterArguments)), |
| new ExpressionStatement( |
| new StaticInvocation(instrumenterExit, exitArguments)), |
| ]); |
| c.function.body = block; |
| block.parent = c.function; |
| return; |
| } |
| |
| // We retain the original body as with procedures. |
| Block block = new Block([ |
| new ExpressionStatement( |
| new StaticInvocation(instrumenterEnter, enterArguments)), |
| c.function.body as Statement, |
| ]); |
| TryFinally tryFinally = new TryFinally( |
| block, |
| new ExpressionStatement(new StaticInvocation( |
| instrumenterExit, new Arguments([new IntLiteral(id)])))); |
| c.function.body = tryFinally; |
| tryFinally.parent = c.function; |
| } |