blob: 960fbdcec6d7458f0a939eca47f7db38e67ddb0b [file] [log] [blame]
// Copyright (c) 2024, 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 'package:analyzer/dart/analysis/features.dart';
import 'package:analyzer/dart/analysis/utilities.dart';
import 'package:args/args.dart';
import 'package:dart_style/dart_style.dart';
import 'package:dart_style/src/constants.dart';
import 'package:dart_style/src/front_end/ast_node_visitor.dart';
import 'package:dart_style/src/short/source_visitor.dart';
import 'package:dart_style/src/testing/benchmark.dart';
import 'package:path/path.dart' as p;
import 'package:pub_semver/pub_semver.dart';
const _totalTrials = 100;
const _formatsPerTrial = 10;
final _benchmarkDirectory = p.dirname(p.fromUri(Platform.script));
Future<void> main(List<String> arguments) async {
var (:isShort, :baseline, :benchmarks) = await _parseArguments(arguments);
for (var benchmark in benchmarks) {
_runBenchmark(benchmark, baseline, isShort: isShort);
}
}
void _runBenchmark(Benchmark benchmark, double? baseline,
{required bool isShort}) {
var source = SourceCode(benchmark.input);
var expected = isShort ? benchmark.shortOutput : benchmark.tallOutput;
print('Benchmarking "${benchmark.name}" '
'using ${isShort ? 'short' : 'tall'} style...');
if (baseline != null) {
print('Comparing to baseline where 100% = ${baseline.toStringAsFixed(3)}ms'
' (shorter is better)');
}
// Parse the source outside of the main benchmark loop. That way, analyzer
// parse time (which we don't control) isn't part of the benchmark.
var parseResult = parseString(
content: source.text,
featureSet: FeatureSet.fromEnableFlags2(
sdkLanguageVersion: Version(3, 3, 0), flags: const []),
path: source.uri,
throwIfDiagnostics: false,
);
var formatter = DartFormatter(
pageWidth: benchmark.pageWidth,
lineEnding: '\n',
experimentFlags: [if (!isShort) tallStyleExperimentFlag]);
// Run the benchmark several times. This ensures the VM is warmed up and lets
// us see how much variance there is.
var best = 99999999.0;
for (var i = 0; i <= _totalTrials; i++) {
var stopwatch = Stopwatch()..start();
// For a single benchmark, format the source multiple times.
String? result;
for (var j = 0; j < _formatsPerTrial; j++) {
if (isShort) {
var visitor = SourceVisitor(formatter, parseResult.lineInfo, source);
result = visitor.run(parseResult.unit).text;
} else {
var visitor = AstNodeVisitor(formatter, parseResult.lineInfo, source);
result = visitor.run(parseResult.unit).text;
}
}
var elapsed = stopwatch.elapsedMicroseconds / 1000 / _formatsPerTrial;
// Keep track of the best run so far.
if (elapsed >= best) continue;
best = elapsed;
// Sanity check to make sure the output is what we expect and to make sure
// the VM doesn't optimize "dead" code away.
if (result != expected) {
print('Incorrect output:\n$result');
exit(1);
}
// Don't print the first run. It's always terrible since the VM hasn't
// warmed up yet.
if (i == 0) continue;
_printResult("Run ${'#$i'.padLeft(4)}", baseline, elapsed);
}
_printResult('Best ', baseline, best);
}
Future<({bool isShort, double? baseline, List<Benchmark> benchmarks})>
_parseArguments(List<String> arguments) async {
var argParser = ArgParser();
argParser.addFlag('help', negatable: false, help: 'Show usage information.');
argParser.addOption('baseline',
abbr: 'b',
help: 'The millisecond count of the baseline to compare the results to.');
argParser.addFlag('short',
abbr: 's',
negatable: false,
help: 'Whether the formatter should use short or tall style.');
var argResults = argParser.parse(arguments);
if (argResults['help'] as bool) {
_usage(argParser, exitCode: 0);
}
var benchmarks = switch (argResults.rest) {
// Find all the benchmarks.
['all'] => await Benchmark.findAll(),
// Default to the large benchmark.
[] => [
await Benchmark.read(p.join(_benchmarkDirectory, 'case/large.unit'))
],
// The user-specified list of paths.
[...var paths] when paths.isNotEmpty => [
for (var path in paths) await Benchmark.read(path)
],
_ => _usage(argParser, exitCode: 64),
};
double? baseline;
if (argResults.wasParsed('baseline')) {
baseline = double.parse(argResults['baseline'] as String);
}
return (
isShort: argResults['short'] as bool,
baseline: baseline,
benchmarks: benchmarks
);
}
void _printResult(String label, double? baseline, double time) {
if (baseline == null) {
print('$label: ${time.toStringAsFixed(3).padLeft(7)}ms '
"${'=' * ((time * 5).toInt())}");
} else {
var percent = 100 * time / baseline;
print('$label: ${percent.toStringAsFixed(3).padLeft(7)}% '
"${'=' * (percent ~/ 2)}");
}
}
/// Prints usage information.
///
/// If [exitCode] is non-zero, prints to stderr.
Never _usage(ArgParser argParser, {required int exitCode}) {
var stream = exitCode == 0 ? stdout : stderr;
stream.writeln('dart benchmark/run.dart [benchmark/case/<benchmark>.unit] '
'[--short] [--baseline=n]');
stream.writeln('');
stream.writeln(argParser.usage);
exit(exitCode);
}