blob: 43fb8f6f2b6f829ba2c32d7439e2229a39c6d5f6 [file]
// 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/source_visitor.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));
void main(List<String> arguments) {
var (:isShort, :baseline, :benchmarkPath) = _parseArguments(arguments);
var sourceLines = File(benchmarkPath).readAsLinesSync();
// The first line may have a "|" to indicate the page width.
var pageWidth = 80;
if (sourceLines[0].endsWith('|')) {
pageWidth = sourceLines[0].indexOf('|');
sourceLines.removeAt(0);
}
var sourceText = sourceLines.join('\n');
var source = SourceCode(sourceText);
var expected =
File(p.setExtension(benchmarkPath, isShort ? '.expect_short' : '.expect'))
.readAsStringSync();
var benchmarkName = p.basenameWithoutExtension(benchmarkPath);
print('Benchmarking "$benchmarkName" '
'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: 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);
}
({bool isShort, double? baseline, String benchmarkPath}) _parseArguments(
List<String> arguments) {
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) {
print('dart benchmark/run.dart benchmark/case/<benchmark>.unit '
'[--short] [--baseline=n]');
print('');
print(argParser.usage);
exit(0);
}
var benchmarkPath = '';
switch (argResults.rest) {
case []:
// Default to the large benchmark.
benchmarkPath = p.join(_benchmarkDirectory, 'case/large.unit');
case [var path]:
benchmarkPath = path;
default:
stderr.writeln('Usage: benchmark/run.dart [--short] <path to benchmark>');
exit(64);
}
double? baseline;
if (argResults.wasParsed('baseline')) {
baseline = double.parse(argResults['baseline'] as String);
}
return (
isShort: argResults['short'] as bool,
baseline: baseline,
benchmarkPath: benchmarkPath
);
}
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)}");
}
}