[CFE] Attempt at 'benchmarking' intrumenter
Ultimatley this proved not very useful (for me at this time at least).
As it might be useful in the future, though, I'll land it here.
Change-Id: I7e6c7ad878d290763d2b2c0f3c372bf7f6521ea1
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/184422
Commit-Queue: Jens Johansen <jensj@google.com>
Reviewed-by: Johnni Winther <johnniwinther@google.com>
diff --git a/pkg/front_end/test/compile_benchmark.dart b/pkg/front_end/test/compile_benchmark.dart
new file mode 100644
index 0000000..da61b76
--- /dev/null
+++ b/pkg/front_end/test/compile_benchmark.dart
@@ -0,0 +1,458 @@
+// @dart = 2.9
+
+import 'dart:convert';
+import 'dart:io';
+import 'dart:typed_data';
+
+import 'package:front_end/src/fasta/kernel/utils.dart';
+import 'package:kernel/kernel.dart';
+import 'package:kernel/binary/ast_from_binary.dart';
+
+import "simple_stats.dart";
+
+/// This pass all parameters after "--" to the compiler (via `compileEntryPoint`
+/// via the helper `compile_benchmark_helper.dart`). It is meant to "benchmark"/
+/// instrument the compiler to give insights into specific runs, i.e. specific
+/// compiles (compiling program `a` and program `b` might have different
+/// characteristics). Said another way, it will always instrument and give
+/// insights on *the compiler*, not the program it's asked to compile (not
+/// directly anyway -- if you want to know if that program successfully
+/// exercises a specific part of the compiler it _does_ do that).
+
+final Uri benchmarkHelper =
+ Platform.script.resolve("compile_benchmark_helper.dart");
+
+main(List<String> args) {
+ List<String> arguments;
+ bool tryToAnnotate = false;
+ bool tryToSlowDown = false;
+ bool timeInsteadOfCount = false;
+ for (int i = 0; i < args.length; i++) {
+ if (args[i] == "--tryToAnnotate") {
+ tryToAnnotate = true;
+ } else if (args[i] == "--tryToSlowDown") {
+ tryToSlowDown = true;
+ } else if (args[i] == "--timeInsteadOfCount") {
+ timeInsteadOfCount = true;
+ } else if (args[i] == "--") {
+ arguments = args.sublist(i + 1);
+ break;
+ } else {
+ throw "Unknown argument '${args[i]}'";
+ }
+ }
+ if (arguments == null || arguments.isEmpty) {
+ throw "No arguments given to compiler.\n"
+ "Give arguments as `dart compile_benchmark.dart -- "
+ "argument1 argument2 (etc)`";
+ }
+
+ Directory tmp = Directory.systemTemp.createTempSync("benchmark");
+ try {
+ // Compile the helper to get a dill we can compile with.
+ final Uri helperDill = tmp.uri.resolve("compile.dill");
+
+ print("Compiling $benchmarkHelper into $helperDill");
+ runXTimes(1, [
+ benchmarkHelper.toString(),
+ benchmarkHelper.toString(),
+ "-o",
+ helperDill.toString(),
+ ]);
+ File f = new File.fromUri(helperDill);
+ if (!f.existsSync()) throw "$f doesn't exist!";
+
+ List<int> dillData = new File.fromUri(helperDill).readAsBytesSync();
+ doWork(
+ tmp,
+ dillData,
+ arguments,
+ tryToAnnotate: tryToAnnotate,
+ tryToSlowDown: tryToSlowDown,
+ timeInsteadOfCount: timeInsteadOfCount,
+ );
+ } finally {
+ tmp.deleteSync(recursive: true);
+ }
+}
+
+/// Perform asked operations on the dill data provided.
+///
+/// Will save files into the provided tmp directory, do timing or counting
+/// instructions and run that with the arguments passed, and then do comparative
+/// runs of transformed/non-transformed versions of the dill. The possible
+/// transformations are:
+/// * [tryToAnnotate] which will add a `@pragma("vm:prefer-inline")` annotation
+/// to each procedure one at a time.
+/// * [tryToSlowDown] which will add a busywait for approximately 0.002 ms to
+/// each procedure one at a time. (Unsurprisingly this makes it slower
+/// proportional to the number of times the procedure is called, so the
+/// counting annotation is likely at least as useful).
+///
+void doWork(Directory tmp, List<int> dillData, List<String> arguments,
+ {bool tryToAnnotate: false,
+ bool tryToSlowDown: false,
+ bool timeInsteadOfCount: false}) {
+ File f = new File.fromUri(tmp.uri.resolve("a.dill"));
+ f.writeAsBytesSync(dillData);
+ Uri dillOrgInTmp = f.uri;
+ print("Wrote to $f");
+
+ List<Procedure> sortedProcedures;
+ if (timeInsteadOfCount) {
+ sortedProcedures = doTimingInstrumentation(dillData, tmp, arguments);
+ } else {
+ sortedProcedures = doCountingInstrumentation(dillData, tmp, arguments);
+ }
+ print("\n\n");
+
+ bool didSomething = false;
+
+ if (tryToAnnotate) {
+ didSomething = true;
+ for (Procedure p in sortedProcedures) {
+ print("Prefer inline $p (${p.location})");
+ Uri preferredInlined = preferInlineProcedure(
+ dillData,
+ tmp.uri,
+ (lib) => lib.importUri == p.enclosingLibrary.importUri,
+ p.enclosingClass?.name,
+ p.name.text);
+
+ print("\nOriginal runs:");
+ List<int> runtimesA =
+ runXTimes(5, [dillOrgInTmp.toString(), ...arguments]);
+
+ print("\nModified runs:");
+ List<int> runtimesB =
+ runXTimes(5, [preferredInlined.toString(), ...arguments]);
+
+ print(SimpleTTestStat.ttest(runtimesB, runtimesA));
+ print("\n------------\n");
+ }
+ }
+
+ if (tryToSlowDown) {
+ didSomething = true;
+ for (Procedure p in sortedProcedures) {
+ Uri busyWaiting = busyWaitProcedure(
+ dillData,
+ tmp.uri,
+ (lib) => lib.importUri == p.enclosingLibrary.importUri,
+ p.enclosingClass?.name,
+ p.name.text);
+ if (busyWaiting == null) continue;
+
+ print("Slow down $p (${p.location})");
+
+ print("\nOriginal runs:");
+ List<int> runtimesA =
+ runXTimes(2, [dillOrgInTmp.toString(), ...arguments]);
+
+ print("\nModified runs:");
+ List<int> runtimesB =
+ runXTimes(2, [busyWaiting.toString(), ...arguments]);
+
+ print(SimpleTTestStat.ttest(runtimesB, runtimesA));
+ print("\n------------\n");
+ }
+ }
+
+ if (!didSomething) {
+ runXTimes(10, [dillOrgInTmp.toString(), ...arguments]);
+ }
+}
+
+/// Instrument the [dillData] so that each procedure-call can be registered
+/// (in package:front_end) and we find out how many times each procedure is
+/// called for a specific run, then run it, print the result and return the
+/// procedures in sorted order (most calls first).
+List<Procedure> doCountingInstrumentation(
+ List<int> dillData, Directory tmp, List<String> arguments) {
+ Instrumented instrumented = instrumentCallsCount(dillData, tmp.uri);
+ List<dynamic> stdout = [];
+ runXTimes(1, [instrumented.dill.toString(), ...arguments], stdout);
+ List<int> procedureCountsTmp = new List<int>.from(jsonDecode(stdout.single));
+ List<IntPair> procedureCounts = [];
+ for (int i = 0; i < procedureCountsTmp.length; i += 2) {
+ procedureCounts
+ .add(new IntPair(procedureCountsTmp[i], procedureCountsTmp[i + 1]));
+ }
+ // Sort highest call-count first.
+ procedureCounts.sort((a, b) => b.value - a.value);
+ List<Procedure> sortedProcedures = [];
+ for (IntPair p in procedureCounts) {
+ if (p.value > 1000) {
+ Procedure procedure = instrumented.procedures[p.key];
+ String location = procedure.location.toString();
+ if (location.length > 50) {
+ location = location.substring(location.length - 50);
+ }
+ print("Called $procedure ${p.value} times ($location)");
+ sortedProcedures.add(procedure);
+ }
+ }
+ return sortedProcedures;
+}
+
+/// Instrument the [dillData] so that each (sync) procedure-call can be timed
+/// (time on stack, i.e. not only the procedure itself, but also the
+/// procedure-calls it makes) (in package:front_end) and we find out how long
+/// each procedure is on the stack for a specific run, then run it, print the
+/// result and return the procedures in sorted order (most time on stack first).
+List<Procedure> doTimingInstrumentation(
+ List<int> dillData, Directory tmp, List<String> arguments) {
+ Instrumented instrumented = instrumentCallsTiming(dillData, tmp.uri);
+ List<dynamic> stdout = [];
+ runXTimes(1, [instrumented.dill.toString(), ...arguments], stdout);
+ List<int> procedureTimeTmp = new List<int>.from(jsonDecode(stdout.single));
+ List<IntPair> procedureTime = [];
+ for (int i = 0; i < procedureTimeTmp.length; i += 2) {
+ procedureTime
+ .add(new IntPair(procedureTimeTmp[i], procedureTimeTmp[i + 1]));
+ }
+ // Sort highest time-on-stack first.
+ procedureTime.sort((a, b) => b.value - a.value);
+ List<Procedure> sortedProcedures = [];
+ for (IntPair p in procedureTime) {
+ if (p.value > 1000) {
+ Procedure procedure = instrumented.procedures[p.key];
+ String location = procedure.location.toString();
+ if (location.length > 50) {
+ location = location.substring(location.length - 50);
+ }
+ print("$procedure was on stack for ${p.value} microseconds ($location)");
+ sortedProcedures.add(procedure);
+ }
+ }
+ return sortedProcedures;
+}
+
+class IntPair {
+ final int key;
+ final int value;
+
+ IntPair(this.key, this.value);
+
+ String toString() {
+ return "IntPair[$key: $value]";
+ }
+}
+
+/// Adds the annotation `@pragma("vm:prefer-inline")` to the specified procedure
+/// and serialize the resulting dill into `b.dill` (return uri).
+///
+/// The annotation is copied from the [preferInlineMe] method in the helper.
+Uri preferInlineProcedure(List<int> dillData, Uri tmp,
+ bool libraryMatcher(Library lib), String className, String procedureName) {
+ Component component = new Component();
+ new BinaryBuilder(dillData, disableLazyReading: true)
+ .readComponent(component);
+ Procedure preferInlineMeProcedure = getProcedure(component,
+ (lib) => lib.fileUri == benchmarkHelper, null, "preferInlineMe");
+ ConstantExpression annotation = preferInlineMeProcedure.annotations.single;
+ Procedure markProcedure =
+ getProcedure(component, libraryMatcher, className, procedureName);
+ markProcedure.addAnnotation(
+ new ConstantExpression(annotation.constant, annotation.type));
+
+ Uint8List newDillData = serializeComponent(component);
+ File f = new File.fromUri(tmp.resolve("b.dill"));
+ f.writeAsBytesSync(newDillData);
+ return f.uri;
+}
+
+/// Makes the procedure specified call [busyWait] from the helper and serialize
+/// the resulting dill into `c.dill` (return uri).
+///
+/// This will make the procedure busy-wait approximately 0.002 ms for each
+/// invocation (+ whatever overhead and imprecision).
+Uri busyWaitProcedure(List<int> dillData, Uri tmp,
+ bool libraryMatcher(Library lib), String className, String procedureName) {
+ Component component = new Component();
+ new BinaryBuilder(dillData, disableLazyReading: true)
+ .readComponent(component);
+ Procedure busyWaitProcedure = getProcedure(
+ component, (lib) => lib.fileUri == benchmarkHelper, null, "busyWait");
+
+ Procedure markProcedure =
+ getProcedure(component, libraryMatcher, className, procedureName);
+ if (markProcedure.function.body == null) return null;
+
+ Statement orgBody = markProcedure.function.body;
+ markProcedure.function.body = new Block([
+ new ExpressionStatement(new StaticInvocation(
+ busyWaitProcedure, new Arguments([new IntLiteral(2 /* 0.002 ms */)]))),
+ orgBody
+ ])
+ ..parent = markProcedure.function;
+
+ Uint8List newDillData = serializeComponent(component);
+ File f = new File.fromUri(tmp.resolve("c.dill"));
+ f.writeAsBytesSync(newDillData);
+ return f.uri;
+}
+
+/// Instrument the [dillData] so that each procedure-call can be registered
+/// (in package:front_end) and we find out how many times each procedure is
+/// called for a specific run.
+///
+/// Uses the [registerCall] in the helper.
+/// Numbers each procedure, saves the instrumented dill and returns both the
+/// dill and the list of procedures so that procedure i in the list will be
+/// annotated with a call to `registerCall(i)`.
+Instrumented instrumentCallsCount(List<int> dillData, Uri tmp) {
+ Component component = new Component();
+ new BinaryBuilder(dillData, disableLazyReading: true)
+ .readComponent(component);
+ Procedure registerCallProcedure = getProcedure(
+ component, (lib) => lib.fileUri == benchmarkHelper, null, "registerCall");
+ RegisterCallTransformer registerCallTransformer =
+ new RegisterCallTransformer(registerCallProcedure);
+ component.accept(registerCallTransformer);
+
+ Uint8List newDillData = serializeComponent(component);
+ File f = new File.fromUri(tmp.resolve("counting.dill"));
+ f.writeAsBytesSync(newDillData);
+
+ return new Instrumented(f.uri, registerCallTransformer.procedures);
+}
+
+/// Instrument the [dillData] so that each (sync) procedure-call can be timed
+/// (time on stack, i.e. not only the procedure itself, but also the
+/// procedure-calls it makes) (in package:front_end) and we find out how long
+/// each procedure is on the stack for a specific run.
+///
+/// Uses [registerCallStart] and [registerCallEnd] from the helper.
+/// Numbers each sync procedure, saves the instrumented dill and returns both
+/// the dill and the list of procedures so that procedure i in the list will be
+/// annotated with a call-pair to `registerCallStart(i)` and
+/// `registerCallEnd(i)`.
+Instrumented instrumentCallsTiming(List<int> dillData, Uri tmp) {
+ Component component = new Component();
+ new BinaryBuilder(dillData, disableLazyReading: true)
+ .readComponent(component);
+ Procedure registerCallStartProcedure = getProcedure(component,
+ (lib) => lib.fileUri == benchmarkHelper, null, "registerCallStart");
+ Procedure registerCallEndProcedure = getProcedure(component,
+ (lib) => lib.fileUri == benchmarkHelper, null, "registerCallEnd");
+ RegisterTimeTransformer registerTimeTransformer = new RegisterTimeTransformer(
+ registerCallStartProcedure, registerCallEndProcedure);
+ component.accept(registerTimeTransformer);
+
+ Uint8List newDillData = serializeComponent(component);
+ File f = new File.fromUri(tmp.resolve("timing.dill"));
+ f.writeAsBytesSync(newDillData);
+
+ return new Instrumented(f.uri, registerTimeTransformer.procedures);
+}
+
+/// Class holding both the uri of a saved dill file and a list of procedures (in
+/// order) that has some reference to the annotation added to the dill file.
+class Instrumented {
+ final Uri dill;
+ final List<Procedure> procedures;
+
+ Instrumented(this.dill, this.procedures);
+}
+
+class RegisterCallTransformer extends RecursiveVisitor {
+ final Procedure registerCallProcedure;
+ RegisterCallTransformer(this.registerCallProcedure);
+ List<Procedure> procedures = [];
+
+ visitLibrary(Library node) {
+ if (node.importUri.scheme == "package" &&
+ node.importUri.pathSegments.first == "front_end") {
+ super.visitLibrary(node);
+ }
+ }
+
+ visitProcedure(Procedure node) {
+ if (node.function.body == null) return;
+ int procedureNum = procedures.length;
+ procedures.add(node);
+ Statement orgBody = node.function.body;
+ node.function.body = new Block([
+ new ExpressionStatement(new StaticInvocation(registerCallProcedure,
+ new Arguments([new IntLiteral(procedureNum)]))),
+ orgBody
+ ]);
+ node.function.body.parent = node.function;
+ }
+}
+
+class RegisterTimeTransformer extends RecursiveVisitor {
+ final Procedure registerCallStartProcedure;
+ final Procedure registerCallEndProcedure;
+
+ RegisterTimeTransformer(
+ this.registerCallStartProcedure, this.registerCallEndProcedure);
+
+ List<Procedure> procedures = [];
+
+ visitLibrary(Library node) {
+ if (node.importUri.scheme == "package" &&
+ node.importUri.pathSegments.first == "front_end") {
+ super.visitLibrary(node);
+ }
+ }
+
+ visitProcedure(Procedure node) {
+ if (node.function.body == null) return;
+ if (node.function.dartAsyncMarker != AsyncMarker.Sync) return;
+ int procedureNum = procedures.length;
+ procedures.add(node);
+ Statement orgBody = node.function.body;
+ // Rewrite as
+ // {
+ // registerCallStartProcedure(x);
+ // try {
+ // originalBody
+ // } finally {
+ // registerCallEndProcedure(x);
+ // }
+ // }
+ Block block = new Block([
+ new ExpressionStatement(new StaticInvocation(registerCallStartProcedure,
+ new Arguments([new IntLiteral(procedureNum)]))),
+ new TryFinally(
+ orgBody,
+ new ExpressionStatement(new StaticInvocation(registerCallEndProcedure,
+ new Arguments([new IntLiteral(procedureNum)]))),
+ )
+ ]);
+ node.function.body = block;
+ node.function.body.parent = node.function;
+ }
+}
+
+Procedure getProcedure(Component component, bool libraryMatcher(Library lib),
+ String className, String procedureName) {
+ Library lib = component.libraries.where(libraryMatcher).single;
+ List<Procedure> procedures = lib.procedures;
+ if (className != null) {
+ Class cls = lib.classes.where((c) => c.name == className).single;
+ procedures = cls.procedures;
+ }
+ // TODO: This will fail for getter/setter pairs. Fix that.
+ return procedures.where((p) => p.name.text == procedureName).single;
+}
+
+List<int> runXTimes(int x, List<String> arguments, [List<dynamic> stdout]) {
+ List<int> result = [];
+ Stopwatch stopwatch = new Stopwatch()..start();
+ for (int i = 0; i < x; i++) {
+ stopwatch.reset();
+ ProcessResult run = Process.runSync(Platform.resolvedExecutable, arguments,
+ runInShell: true);
+ int ms = stopwatch.elapsedMilliseconds;
+ result.add(ms);
+ print(ms);
+ if (run.exitCode != 0) throw "Got exit code ${run.exitCode}";
+ if (stdout != null) {
+ stdout.add(run.stdout);
+ }
+ }
+ return result;
+}
diff --git a/pkg/front_end/test/compile_benchmark_helper.dart b/pkg/front_end/test/compile_benchmark_helper.dart
new file mode 100644
index 0000000..1b187e8
--- /dev/null
+++ b/pkg/front_end/test/compile_benchmark_helper.dart
@@ -0,0 +1,100 @@
+// Copyright (c) 2017, 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 '../tool/_fasta/entry_points.dart' show compileEntryPoint;
+
+main(List<String> arguments) async {
+ await compileEntryPoint(arguments);
+ if (numCalls.isNotEmpty) {
+ print("[");
+ bool printed = false;
+ for (int i = 0; i < numCalls.length; i++) {
+ int value = numCalls[i];
+ if (value != null && value > 0) {
+ if (printed) print(",");
+ print("$i, $value");
+ printed = true;
+ }
+ }
+ print("]");
+ } else if (inCall.isNotEmpty) {
+ print("[");
+ bool printed = false;
+ for (int i = 0; i < inCall.length; i++) {
+ int value = inCall[i];
+ if (value == null) continue;
+ if (value != 0) throw "$i has value $value";
+ if (printed) print(",");
+ int time = callTimes[i];
+ print("$i, $time");
+ printed = true;
+ }
+ print("]");
+ }
+}
+
+List<int> numCalls = [];
+
+void registerCall(int procedureNum) {
+ while (numCalls.length <= procedureNum) {
+ if (numCalls.length < 8) {
+ numCalls.length = 8;
+ } else {
+ numCalls.length *= 2;
+ }
+ }
+ numCalls[procedureNum] ??= 0;
+ numCalls[procedureNum]++;
+}
+
+List<int> inCall = [];
+List<int> callTimes = [];
+Stopwatch stopwatch = new Stopwatch()..start();
+
+void registerCallStart(int procedureNum) {
+ while (inCall.length <= procedureNum) {
+ if (inCall.length < 8) {
+ inCall.length = 8;
+ callTimes.length = 8;
+ } else {
+ inCall.length *= 2;
+ callTimes.length *= 2;
+ }
+ }
+ inCall[procedureNum] ??= 0;
+ callTimes[procedureNum] ??= 0;
+ if (inCall[procedureNum]++ == 0) {
+ // First --- start a timer-ish.
+ callTimes[procedureNum] -= stopwatch.elapsedMicroseconds;
+ }
+}
+
+void registerCallEnd(int procedureNum) {
+ if (inCall[procedureNum]-- == 1) {
+ // Last --- stop the timer-ish.
+ callTimes[procedureNum] += stopwatch.elapsedMicroseconds;
+ }
+}
+
+@pragma("vm:prefer-inline")
+void preferInlineMe() {
+ // This makes it easier to get the annotation :).
+}
+
+int busyWait(int micro) {
+ int count = 0;
+ Stopwatch stopwatch = new Stopwatch()..start();
+ while (true) {
+ for (int i = 0; i < 1000; i++) {
+ count++;
+ }
+ int elapsed = stopwatch.elapsedMicroseconds;
+ if (elapsed >= micro) {
+ print("Bye from busywait after $count iterations ($elapsed vs $micro)!");
+ return count;
+ }
+ }
+}
diff --git a/pkg/front_end/test/simple_stats.dart b/pkg/front_end/test/simple_stats.dart
index b163e67..dd69a4e 100644
--- a/pkg/front_end/test/simple_stats.dart
+++ b/pkg/front_end/test/simple_stats.dart
@@ -7,9 +7,9 @@
import 'dart:math' as math;
class SimpleTTestStat {
- static TTestResult ttest(List<int> a, List<int> b) {
- int aSum = a.reduce((value, element) => (value + element));
- int bSum = b.reduce((value, element) => (value + element));
+ static TTestResult ttest<E extends num>(List<E> a, List<E> b) {
+ E aSum = a.reduce((value, element) => (value + element));
+ E bSum = b.reduce((value, element) => (value + element));
int aCount = a.length;
int bCount = b.length;
double aMean = aSum / aCount;
@@ -36,13 +36,13 @@
}
}
- static double variance(List<int> data) {
- int sum = data.reduce((value, element) => (value + element));
+ static double variance<E extends num>(List<E> data) {
+ E sum = data.reduce((value, element) => (value + element));
int count = data.length;
double average = sum / count;
double diffSquareSum = 0;
- for (int value in data) {
+ for (E value in data) {
double diff = value - average;
double squared = diff * diff;
diffSquareSum += squared;
diff --git a/pkg/front_end/test/spell_checking_list_tests.txt b/pkg/front_end/test/spell_checking_list_tests.txt
index 2ec515d..1b92362 100644
--- a/pkg/front_end/test/spell_checking_list_tests.txt
+++ b/pkg/front_end/test/spell_checking_list_tests.txt
@@ -86,6 +86,8 @@
builddir
bulk2
bulkcompile
+busy
+busywait
bye
c's
c59cdee365b94ce066344840f9e3412d642019b
@@ -99,6 +101,7 @@
ccc
cell
cf
+characteristics
charset
checkme
checkout
@@ -127,6 +130,7 @@
commented
commit
companion
+comparative
comparer
compilations
compiler's
@@ -362,6 +366,7 @@
image
images
implementor
+imprecision
in1
in2
inclosure
@@ -375,7 +380,9 @@
inlinable
inlineable
inlines
+insights
instance2
+instrument
insufficient
intdiv
interactive
@@ -527,6 +534,7 @@
out2
outbound
outlined
+overhead
overlay
ox
pack
@@ -566,6 +574,7 @@
property5b
property8a
property8b
+proportional
protected
proved
provider
@@ -627,6 +636,7 @@
rows
runtimes
rv
+saves
scans
scheduler
screen
@@ -750,6 +760,7 @@
unpatched
unpaused
unregistered
+unsurprisingly
untransformed
untrimmed
unusual