Skip to content

Commit 5ddd749

Browse files
authored
Support running benchmarks in AOT mode. (#1486)
Support running benchmarks in AOT mode. Many of the optimizations I'm trying have a relatively small improvement. That can make it hard to tell if they are actually an improvement if there is enough variance in the runs that they are lost in the noise. AOT builds don't need warm-up time and seem to have generally lower variance than JIT runs. Also, they are how the shipped formatter is run in `dart format`. So this adds support to the two benchmark scripts for running themselves in AOT snapshot mode. I added support for this directly to the scripts instead of just manually compiling and running them on the command line (which works fine) because this way it's less error-prone. I don't have to remember to re-compile the AOT snapshot and risk using an old build which is an easy mistake to make when the only behavioral difference is (possibly) performance. Also, tweaked the profiler output a little.
1 parent 25dcdbb commit 5ddd749

File tree

5 files changed

+125
-21
lines changed

5 files changed

+125
-21
lines changed

benchmark/directory.dart

Lines changed: 52 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,24 +4,29 @@
44

55
import 'dart:io';
66

7+
import 'package:args/args.dart';
8+
import 'package:collection/collection.dart';
79
import 'package:dart_style/dart_style.dart';
810
import 'package:dart_style/src/constants.dart';
911
import 'package:dart_style/src/profile.dart';
12+
import 'package:dart_style/src/testing/benchmark.dart';
1013

1114
/// Reads a given directory of Dart files and repeatedly formats their contents.
1215
///
1316
/// This allows getting profile information on a large real-world codebase
1417
/// while also factoring out the file IO time from the process.
15-
void main(List<String> args) {
16-
if (args.length != 1) {
17-
stderr.writeln('Usage: dart run directory.dart <directory to format>');
18-
exit(65);
19-
}
18+
void main(List<String> arguments) async {
19+
var directory = await _parseArguments(arguments);
2020

21-
var directory = args[0];
2221
print('Listing entries in $directory...');
2322
var entries = Directory(directory).listSync(recursive: true);
2423

24+
// Make sure the benchmark is deterministic. The order shouldn't really
25+
// matter for performance, but since the JIT is warming up as it goes through
26+
// the files, different orders could potentially affect how it chooses to
27+
// optimize.
28+
entries.sortBy((entry) => entry.path);
29+
2530
print('Reading sources...');
2631
var sources = <String>[];
2732
for (var entry in entries) {
@@ -65,3 +70,44 @@ void _runFormatter(String source) {
6570
print(error.message());
6671
}
6772
}
73+
74+
/// Parses the command line arguments and options.
75+
///
76+
/// Returns the path to the directory that should be formatted.
77+
Future<String> _parseArguments(List<String> arguments) async {
78+
var argParser = ArgParser();
79+
argParser.addFlag('help', negatable: false, help: 'Show usage information.');
80+
argParser.addFlag('aot',
81+
negatable: false,
82+
help: 'Whether the benchmark should run in AOT mode versus JIT.');
83+
84+
var argResults = argParser.parse(arguments);
85+
if (argResults['help'] as bool) {
86+
_usage(argParser, exitCode: 0);
87+
}
88+
89+
if (argResults.rest.length != 1) {
90+
stderr.writeln('Missing directory path to format.');
91+
stderr.writeln();
92+
_usage(argParser, exitCode: 0);
93+
}
94+
95+
if (argResults['aot'] as bool) {
96+
await rerunAsAot([argResults.rest.single]);
97+
}
98+
99+
return argResults.rest.single;
100+
}
101+
102+
/// Prints usage information.
103+
///
104+
/// If [exitCode] is non-zero, prints to stderr.
105+
Never _usage(ArgParser argParser, {required int exitCode}) {
106+
var stream = exitCode == 0 ? stdout : stderr;
107+
108+
stream.writeln('dart benchmark/directory.dart [--aot] <directory>');
109+
stream.writeln('');
110+
stream.writeln(argParser.usage);
111+
112+
exit(exitCode);
113+
}

benchmark/run.dart

Lines changed: 21 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,9 @@ const _baselinePath = 'benchmark/baseline.json';
3838

3939
final _benchmarkDirectory = p.dirname(p.fromUri(Platform.script));
4040

41+
/// Whether to run a number of trials before measuring to warm up the JIT.
42+
bool _runWarmupTrials = false;
43+
4144
/// Whether to use the short or tall style formatter.
4245
bool _isShort = false;
4346

@@ -68,7 +71,9 @@ Future<void> main(List<String> arguments) async {
6871
}
6972
}
7073

71-
await _warmUp();
74+
if (_runWarmupTrials) {
75+
await _warmUp();
76+
}
7277

7378
var results = <(Benchmark, List<double>)>[];
7479
for (var benchmark in benchmarks) {
@@ -175,10 +180,15 @@ double _runTrial(DartFormatter formatter, ParseStringResult parseResult,
175180
Future<List<Benchmark>> _parseArguments(List<String> arguments) async {
176181
var argParser = ArgParser();
177182
argParser.addFlag('help', negatable: false, help: 'Show usage information.');
183+
argParser.addFlag('aot',
184+
negatable: false,
185+
help: 'Whether the benchmark should run in AOT mode versus JIT.');
178186
argParser.addFlag('short',
179187
abbr: 's',
180188
negatable: false,
181189
help: 'Whether the formatter should use short or tall style.');
190+
argParser.addFlag('no-warmup',
191+
negatable: false, help: 'Skip the JIT warmup runs.');
182192
argParser.addFlag('write-baseline',
183193
abbr: 'w',
184194
negatable: false,
@@ -189,6 +199,14 @@ Future<List<Benchmark>> _parseArguments(List<String> arguments) async {
189199
_usage(argParser, exitCode: 0);
190200
}
191201

202+
if (argResults['aot'] as bool) {
203+
await rerunAsAot([
204+
for (var argument in arguments)
205+
if (argument != '--aot') argument,
206+
'--no-warmup',
207+
]);
208+
}
209+
192210
var benchmarks = switch (argResults.rest) {
193211
// Find all the benchmarks.
194212
['all'] => await Benchmark.findAll(),
@@ -205,6 +223,7 @@ Future<List<Benchmark>> _parseArguments(List<String> arguments) async {
205223
_ => _usage(argParser, exitCode: 64),
206224
};
207225

226+
_runWarmupTrials = !(argResults['no-warmup'] as bool);
208227
_isShort = argResults['short'] as bool;
209228
_writeBaseline = argResults['write-baseline'] as bool;
210229

@@ -250,7 +269,7 @@ Never _usage(ArgParser argParser, {required int exitCode}) {
250269
var stream = exitCode == 0 ? stdout : stderr;
251270

252271
stream.writeln('dart benchmark/run.dart [benchmark/case/<benchmark>.unit] '
253-
'[--short] [--baseline=n]');
272+
'[--aot] [--short] [--baseline=n]');
254273
stream.writeln('');
255274
stream.writeln(argParser.usage);
256275

lib/src/profile.dart

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -58,20 +58,22 @@ class Profile {
5858
static void begin(String label) {
5959
if (!enabled) return;
6060

61+
// Indent to show nesting of profiled regions.
62+
label = '${' ' * _running.length}$label';
63+
6164
_running.add((label, Timeline.now));
65+
66+
// Add the label eagerly to the map so that labels are printed in the
67+
// order they were began.
68+
_accumulatedTimes.putIfAbsent(label, () => 0);
6269
}
6370

64-
static void end(String label) {
71+
static void end(String _) {
6572
if (!enabled) return;
6673

67-
var (startLabel, start) = _running.removeLast();
68-
69-
// Must push and pop in stack order.
70-
assert(label == startLabel);
74+
var (label, start) = _running.removeLast();
7175
var elapsed = Timeline.now - start;
72-
73-
_accumulatedTimes.update(label, (accumulated) => accumulated + elapsed,
74-
ifAbsent: () => elapsed);
76+
_accumulatedTimes.update(label, (accumulated) => accumulated + elapsed);
7577
}
7678

7779
/// Discards all recorded profiling data.
@@ -105,8 +107,6 @@ class Profile {
105107
print(header);
106108

107109
var labels = data.keys.toList();
108-
labels.sort();
109-
110110
var longestLabel =
111111
labels.fold(0, (length, label) => max(length, label.length));
112112

lib/src/testing/benchmark.dart

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,3 +81,35 @@ class Benchmark {
8181
required this.shortOutput,
8282
required this.tallOutput});
8383
}
84+
85+
/// Compiles the currently running script to an AOT snapshot and then executes
86+
/// it.
87+
///
88+
/// This function never returns. When the AOT snapshot ends, this exits the
89+
/// process.
90+
Future<Never> rerunAsAot(List<String> arguments) async {
91+
var script = Platform.script.toFilePath();
92+
var snapshotPath = p.join(
93+
Directory.systemTemp.path, p.setExtension(p.basename(script), '.aot'));
94+
95+
print('Creating AOT snapshot for $script...');
96+
var result = await Process.run(
97+
'dart', ['compile', 'aot-snapshot', '-o', snapshotPath, script]);
98+
stdout.write(result.stdout);
99+
stderr.write(result.stderr);
100+
if (result.exitCode != 0) {
101+
stderr.writeln('Failed to create AOT snapshot.');
102+
exit(result.exitCode);
103+
}
104+
105+
print('Running AOT snapshot...');
106+
var process =
107+
await Process.start('dartaotruntime', [snapshotPath, ...arguments]);
108+
await stdout.addStream(process.stdout);
109+
await stderr.addStream(process.stderr);
110+
111+
var exitCode = await process.exitCode;
112+
113+
await File(snapshotPath).delete();
114+
exit(exitCode);
115+
}

lib/src/testing/test_file.dart

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,9 +15,16 @@ final _unicodeEscapePattern = RegExp('[\x0a\x0c\x0d]');
1515

1616
/// Get the absolute local file path to the dart_style package's root directory.
1717
Future<String> findPackageDirectory() async {
18-
var libraryUri = await Isolate.resolvePackageUri(
19-
Uri.parse('package:dart_style/src/testing/test_file.dart'));
20-
return p.normalize(p.join(p.dirname(libraryUri!.toFilePath()), '../../..'));
18+
var libraryPath = (await Isolate.resolvePackageUri(
19+
Uri.parse('package:dart_style/src/testing/test_file.dart')))
20+
?.toFilePath();
21+
22+
// Fallback, if we can't resolve the package URI because we're running in an
23+
// AOT snapshot, just assume we're running from the root directory of the
24+
// package.
25+
libraryPath ??= 'lib/src/testing/test_file.dart';
26+
27+
return p.normalize(p.join(p.dirname(libraryPath), '../../..'));
2128
}
2229

2330
/// Get the absolute local file path to the package's "test" directory.

0 commit comments

Comments
 (0)