blob: 86b7fe27ad3373a514d0ab6127292bbe3c452eee [file] [log] [blame] [edit]
// 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.
import "dart:io";
import '../test/simple_stats.dart';
import '../test/utils/io_utils.dart' show computeRepoDirUri;
late final Uri repoDir = computeRepoDirUri();
void main(List<String> args) {
if (args.contains("--help")) return _help();
_checkEnvironment();
bool doCacheBenchmarkingToo = false;
bool silent = false;
int iterations = 5;
int core = 7;
String? aotRuntime;
String? checkFileSize;
List<String> snapshots = [];
List<String> arguments = [];
for (String arg in args) {
if (arg.startsWith("--iterations=")) {
iterations = int.parse(arg.substring("--iterations=".length));
} else if (arg.startsWith("--core=")) {
core = int.parse(arg.substring("--core=".length));
} else if (arg.startsWith("--aotruntime")) {
aotRuntime = arg.substring("--aotruntime".length);
} else if (arg.startsWith("--snapshot=")) {
snapshots.add(arg.substring("--snapshot=".length));
} else if (arg.startsWith("--arguments=")) {
arguments.add(arg.substring("--arguments=".length));
} else if (arg.startsWith("--filesize=")) {
checkFileSize = arg.substring("--filesize=".length);
} else if (arg == "--cache") {
doCacheBenchmarkingToo = true;
} else if (arg == "--silent") {
silent = true;
} else {
throw "Don't know argument '$arg'";
}
}
aotRuntime ??= _computeAotRuntime();
if (snapshots.length < 2) {
throw "Can't compare less than two snapshots. Specify using '--snapshot='";
}
if (arguments.isEmpty) {
print("Note: Running without any arguments to the snapshots.");
}
_doRun(iterations, snapshots, aotRuntime, core, arguments, checkFileSize,
cacheBenchmarking: false, silent: silent);
if (doCacheBenchmarkingToo) {
_doRun(iterations, snapshots, aotRuntime, core, arguments, checkFileSize,
cacheBenchmarking: true, silent: silent);
}
}
void _doRun(int iterations, List<String> snapshots, String aotRuntime, int core,
List<String> arguments, String? checkFileSize,
{required bool cacheBenchmarking, required bool silent}) {
print("Will now run $iterations iterations with "
"${snapshots.length} snapshots.");
List<List<Map<String, num>>> runResults = [];
List<GCInfo> gcInfos = [];
Warnings warnings = new Warnings();
for (String snapshot in snapshots) {
List<Map<String, num>> snapshotResults = [];
runResults.add(snapshotResults);
for (int iteration = 0; iteration < iterations; iteration++) {
// We want this silent to mean no stdout print, but still want progress
// info which is what the dot provides.
if (silent) stdout.write(".");
Map<String, num> benchmarkRun = _benchmark(
aotRuntime, core, snapshot, [], arguments,
warnings: warnings,
cacheBenchmarking: cacheBenchmarking,
silent: silent);
if (checkFileSize != null) {
File f = new File(checkFileSize);
if (f.existsSync()) {
benchmarkRun["filesize"] = f.lengthSync();
}
}
snapshotResults.add(benchmarkRun);
}
// Do a single GC run too.
if (silent) stdout.write(".");
gcInfos
.add(_verboseGcRun(aotRuntime, snapshot, [], arguments, silent: true));
}
stdout.write("\n\n");
List<Map<String, num>> firstSnapshotResults = runResults.first;
for (int i = 1; i < runResults.length; i++) {
if (i > 1) print("");
print("Comparing snapshot #1 (${_getName(snapshots[0])}) with "
"snapshot #${i + 1} (${_getName(snapshots[i])})");
List<Map<String, num>> compareToResults = runResults[i];
if (!_compare(firstSnapshotResults, compareToResults)) {
print("No change.");
}
printGcDiff(gcInfos.first, gcInfos[i]);
}
if (warnings.scalingInEffect) {
print("Be aware that the above was with scaling in effect.");
print("As such the results are likely useless.");
print("Possibly some other process is using the hardware counters.");
print("Running this tool");
print("sudo out/ReleaseX64/dart pkg/front_end/tool/perf_event_tool.dart");
print("will attempt to give you such information.");
}
}
String _getName(String urlIsh) {
return Uri.parse(urlIsh).pathSegments.last;
}
void _help() {
print("CFE benchmarker tool");
print("");
print("First create 2 or more aot snapshots of the code");
print("you want to benchmark. E.g. by running:");
print("");
print(r"out/ReleaseX64/dart-sdk/bin/dart \");
print(r" compile aot-snapshot \");
print(r" pkg/front_end/tool/compile.dart");
print("");
print("then moving with e.g.");
print("");
print(r"mv pkg/front_end/tool/compile.aot \");
print(r" pkg/front_end/tool/compile.aot.1");
print("");
print("Then applying your code-change and compiling again,");
print("this time moving to somewhere else (e.g. .2)");
print("");
print("Then run this tool via for instance");
print("");
print(r"out/ReleaseX64/dart pkg/front_end/tool/benchmarker.dart \");
print(r" --iterations=3 \");
print(r" --snapshot=pkg/front_end/tool/compile.aot.1 \");
print(r" --snapshot=pkg/front_end/tool/compile.aot.2 \");
print(r" --arguments=pkg/front_end/tool/compile.dart");
print("");
print("This will run the 2 snapshots 3 times each, each time asking it");
print("to compile compile.dart, then do statistics on the data returned");
print("by `perf stat` where especially `instructions:u` and `branches:u`");
print("has been observed to be stable.");
}
bool compare(List<Map<String, num>> from, List<Map<String, num>> to) {
return _compare(from, to);
}
bool _compare(List<Map<String, num>> from, List<Map<String, num>> to) {
bool somethingWasSignificant = false;
Set<String> allCaptions = {};
for (Map<String, num> entry in [...from, ...to]) {
allCaptions.addAll(entry.keys);
}
for (String caption in allCaptions) {
List<num> fromForCaption = _extractDataForCaption(caption, from);
List<num> toForCaption = _extractDataForCaption(caption, to);
if (caption.startsWith("context-switches") ||
caption.startsWith("cpu-migrations")) {
// These are seemingly always 0 --- if they're not we'll print a warning.
for (num value in [...fromForCaption, ...toForCaption]) {
if (value != 0) {
print("Warning: "
"$caption has values $fromForCaption and $toForCaption");
break;
}
}
}
if (fromForCaption.isEmpty || toForCaption.isEmpty) continue;
TTestResult stats = SimpleTTestStat.ttest(toForCaption, fromForCaption);
if (stats.significant) {
somethingWasSignificant = true;
print("$caption: ${stats.percentChangeIfSignificant(fractionDigits: 4)} "
"(${stats.valueChangeIfSignificant(fractionDigits: 2)}) "
"(${stats.meanChangeStringIfSignificant(fractionDigits: 2)})");
}
}
return somethingWasSignificant;
}
List<num> _extractDataForCaption(String caption, List<Map<String, num>> data) {
List<num> result = [];
for (Map<String, num> entry in data) {
num? value = entry[caption];
if (value != null) result.add(value);
}
return result;
}
Map<String, num> benchmark(
String snapshot, List<String> extraVmArguments, List<String> arguments,
{String? aotRuntime, int? core, bool cacheBenchmarking = false}) {
return _benchmark(aotRuntime ?? _computeAotRuntime(), core ?? 7, snapshot,
extraVmArguments, arguments,
silent: true, cacheBenchmarking: cacheBenchmarking);
}
late final RegExp _extractNumbers =
new RegExp(r"([\d+\,\.]+)\s+(.+)\s*", caseSensitive: false);
Map<String, num> _benchmark(String aotRuntime, int core, String snapshot,
List<String> extraVmArguments, List<String> arguments,
{bool silent = false, Warnings? warnings, bool cacheBenchmarking = false}) {
if (!silent) stdout.write(".");
// These influence scaling, so only pick 3 (apparently that's now the
// magic limit)
String scalingEvents = "cycles:u,"
"instructions:u,"
"branch-misses:u";
if (cacheBenchmarking) {
scalingEvents = "L1-icache-load-misses:u,"
"LLC-loads:u,"
"LLC-load-misses:u";
}
ProcessResult processResult = Process.runSync("perf", [
"stat",
"-B",
"-e",
// These doesn't influence scaling
"task-clock:u,context-switches:u,cpu-migrations:u,page-faults:u,"
// These influence scaling
"$scalingEvents",
"taskset",
"-c",
"$core",
aotRuntime,
"--deterministic",
...extraVmArguments,
snapshot,
...arguments
]);
if (processResult.exitCode != 0) {
throw "Run failed with exit code ${processResult.exitCode}.\n"
"stdout:\n${processResult.stdout}\n\n"
"stderr:\n${processResult.stderr}\n\n";
}
if (processResult.stdout != "" && !silent) {
print(processResult.stdout);
}
String stderr = processResult.stderr;
List<String> lines = stderr.split("\n");
Map<String, num> result = new Map<String, num>();
for (String line in lines) {
int pos = line.indexOf("#");
String? scaling;
if (pos >= 0) {
// Check for scaling e.g.
// ```
// 974,702,464 cycles:u (74.32%)
// 932,606,794 cycles:u (76.01%)
// 922,272,003 cycles:u (75.84%)
// 942,191,386 cycles:u (74.01%)
// ```
String comment = line.substring(pos);
if (comment.trim().endsWith("%)")) {
int lastStartParen = comment.lastIndexOf("(");
if (lastStartParen < 0) {
throw "Thought it found scaling for '$comment' "
"but it didn't look as expected.";
}
scaling = comment.substring(lastStartParen + 1, comment.length - 1);
}
line = line.substring(0, pos);
}
for (RegExpMatch match in _extractNumbers.allMatches(line)) {
String stringValue = match.group(1)!.trim();
String caption = match.group(2)!.trim();
stringValue = stringValue.replaceAll(",", "");
num value;
if (stringValue.contains(".")) {
value = double.parse(stringValue);
} else {
value = int.parse(stringValue);
}
result[caption] = value;
if (scaling != null) {
print("WARNING: $caption is scaled at $scaling!");
warnings?.scalingInEffect = true;
}
}
}
return result;
}
GCInfo _verboseGcRun(String aotRuntime, String snapshot,
List<String> extraVmArguments, List<String> arguments,
{bool silent = false}) {
if (!silent) stdout.write(".");
ProcessResult processResult = Process.runSync(aotRuntime, [
"--deterministic",
"--verbose-gc",
...extraVmArguments,
snapshot,
...arguments
]);
if (processResult.exitCode != 0) {
throw "Run failed with exit code ${processResult.exitCode}.\n"
"stdout:\n${processResult.stdout}\n\n"
"stderr:\n${processResult.stderr}\n\n";
}
if (processResult.stdout != "" && !silent) {
print(processResult.stdout);
}
return parseVerboseGcOutput(processResult);
}
String _computeAotRuntime() {
File f = new File.fromUri(
repoDir.resolve("out/ReleaseX64/dart-sdk/bin/dartaotruntime"));
if (f.existsSync()) {
return f.path;
} else {
throw "Couldn't find the aot runtime. Have you compiled everything?";
}
}
void _checkEnvironment() {
if (!Platform.isLinux) {
throw "This (probably) only works in Linux";
}
if (!_whichOk("taskset")) {
throw "Couldn't find 'taskset'. Please install that.";
}
if (!_whichOk("perf")) {
throw "Couldn't find 'perf'. Please install that.";
}
}
bool _whichOk(String what) {
ProcessResult result = Process.runSync("which", [what]);
return result.exitCode == 0;
}
GCInfo parseVerboseGcOutput(ProcessResult processResult) {
List<String> stderrLines = processResult.stderr.split("\n");
return parseVerboseGcText(stderrLines);
}
GCInfo parseVerboseGcText(List<String> stderrLines) {
double combinedTime = 0;
Map<String, int> countWhat = {};
for (String line in stderrLines) {
if (!line.trim().startsWith("[")) continue;
if (line.indexOf(",") < 0) continue;
// Hardcoding this might not be the best solution, but works for now.
// The data is space and comma delimited like this (cut off in both
// directions):
//
// ```
// [ GC isolate | space (reason) | GC# | start | time | [...]
// [ | | | (s) | (ms) | [...]
// [ main , StartCMark( external), 1, 0.02, 0.7, [...]
// [...]
// ```
//
// and (currently) contains this information:
// * [0]: GC isolate
// * [1]: space (reason)
// * [2]: GC#
// * [3]: start (s)
// * [4]: time (ms)
// * [5]: new gen used (MB) before
// * [6]: new gen used (MB) after
// * [7]: new gen capacity (MB) before
// * [8]: new gen capacity (MB) after
// * [9]: new gen external (MB) before
// * [10]: new gen external (MB) after
// * [11]: old gen used (MB) before
// * [12]: old gen used (MB) after
// * [13]: old gen capacity (MB) before
// * [14]: old gen capacity (MB) after
// * [15]: old gen external (MB) before
// * [16]: old gen external (MB) after
// * [17]: store buffer before
// * [18]: store buffer after
// * [19]: delta used new (MB)
// * [20]: delta used old (MB)
// * [21]: (nothing, but the cell before ends in a comma)
List<String> cells = line.split(",");
String spaceReason = cells[1].trim();
double time = double.parse(cells[4].trim());
combinedTime += time;
countWhat[spaceReason] = (countWhat[spaceReason] ?? 0) + 1;
}
return new GCInfo(combinedTime, countWhat);
}
void printGcDiff(GCInfo prev, GCInfo current) {
Set<String> allKeys = {...prev.countWhat.keys, ...current.countWhat.keys};
bool printedAnything = false;
for (String key in allKeys) {
int prevValue = prev.countWhat[key] ?? 0;
int currentValue = current.countWhat[key] ?? 0;
if (prevValue == currentValue) continue;
printedAnything = true;
print("$key goes from $prevValue to $currentValue");
}
if (printedAnything) {
print("Notice combined GC time goes "
"from ${prev.combinedTime.toStringAsFixed(0)} ms "
"to ${current.combinedTime.toStringAsFixed(0)} ms "
"(notice only 1 run each).");
}
}
class GCInfo {
final double combinedTime;
final Map<String, int> countWhat;
GCInfo(this.combinedTime, this.countWhat);
}
class Warnings {
bool scalingInEffect = false;
}