Add a benchmark (#419)
* Add a benchmark
* Format and fix analysis
* Fix bugs
* Fix analysis
* Install dart
* Run dart pub get
* Reduce the number of benchmarks
* Always comment
* Reduce alert threshold
* Report results in multiples of no-coverage baseline
* Show few decimal places
* Remove function coverage benchmark
* Add timeout/retry logic
* Force exit once the benchmark is done
* Await file write
* Force exit after file write
* Increase the alert threshold
diff --git a/.github/workflows/test-package.yml b/.github/workflows/test-package.yml
index c8cb613..4e2de79 100644
--- a/.github/workflows/test-package.yml
+++ b/.github/workflows/test-package.yml
@@ -77,3 +77,31 @@
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
path-to-lcov: coverage/lcov.info
+
+ benchmark:
+ needs: test
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v2
+ - uses: dart-lang/setup-dart@v1.0
+ with:
+ sdk: dev
+ - name: Install dependencies
+ run: dart pub get
+ - name: Run benchmark
+ run: dart run benchmark/run_benchmarks.dart
+ - name: Download previous benchmark data
+ uses: actions/cache@v1
+ with:
+ path: benchmark/data/cache
+ key: ${{ runner.os }}-benchmark
+ - name: Check benchmark result
+ uses: benchmark-action/github-action-benchmark@v1
+ with:
+ tool: 'customSmallerIsBetter'
+ github-token: ${{ secrets.GITHUB_TOKEN }}
+ output-file-path: benchmark/data/benchmark_result.json
+ external-data-json-path: benchmark/data/cache/benchmark_result.json
+ fail-on-alert: true
+ comment-always: true
+ alert-threshold: 150%
diff --git a/benchmark/.gitignore b/benchmark/.gitignore
new file mode 100644
index 0000000..1269488
--- /dev/null
+++ b/benchmark/.gitignore
@@ -0,0 +1 @@
+data
diff --git a/benchmark/many_isolates.dart b/benchmark/many_isolates.dart
new file mode 100644
index 0000000..f0b3c04
--- /dev/null
+++ b/benchmark/many_isolates.dart
@@ -0,0 +1,27 @@
+// Copyright (c) 2022, 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:isolate';
+import 'dart:math';
+
+Future<void> main(List<String> args, dynamic message) async {
+ if (message == null) {
+ // If there is no message, it means this instance was created by
+ // run_benchmarks.dart. In that case, this is the parent instance that
+ // spawns all the others.
+ int sum = 0;
+ for (int i = 0; i < 10; ++i) {
+ final port = ReceivePort();
+ final isolate =
+ Isolate.spawnUri(Uri.file('many_isolates.dart'), [], port.sendPort);
+ sum += await port.first as int;
+ await isolate;
+ }
+ print('sum = $sum');
+ } else {
+ // If there is a message, it means this instance is one of the child
+ // instances. The message is the port that this instance replies on.
+ (message as SendPort).send(Random().nextInt(1000));
+ }
+}
diff --git a/benchmark/run_benchmarks.dart b/benchmark/run_benchmarks.dart
new file mode 100644
index 0000000..4f5fd1b
--- /dev/null
+++ b/benchmark/run_benchmarks.dart
@@ -0,0 +1,143 @@
+// Copyright (c) 2022, 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:async';
+import 'dart:io';
+
+import 'package:benchmark_harness/benchmark_harness.dart';
+
+import '../bin/collect_coverage.dart' as collect_coverage;
+import '../bin/format_coverage.dart' as format_coverage;
+
+// Runs a test script with various different coverage configurations.
+class CoverageBenchmark extends AsyncBenchmarkBase {
+ CoverageBenchmark(
+ ScoreEmitter emitter,
+ String name,
+ this.script, {
+ this.gatherCoverage = false,
+ this.functionCoverage = false,
+ this.branchCoverage = false,
+ }) : super(name, emitter: emitter);
+
+ final String script;
+ final bool gatherCoverage;
+ final bool functionCoverage;
+ final bool branchCoverage;
+ int iteration = 0;
+
+ @override
+ Future<void> run() async {
+ print('Running $name...');
+ final covFile = 'data/$name $iteration coverage.json';
+ final lcovFile = 'data/$name $iteration lcov.info';
+ ++iteration;
+
+ await Process.start(
+ Platform.executable,
+ [
+ if (branchCoverage) '--branch-coverage',
+ 'run',
+ if (gatherCoverage) ...[
+ '--pause-isolates-on-exit',
+ '--disable-service-auth-codes',
+ '--enable-vm-service=1234',
+ ],
+ script,
+ ],
+ mode: ProcessStartMode.detached,
+ );
+ if (gatherCoverage) {
+ await collect_coverage.main([
+ '--wait-paused',
+ '--resume-isolates',
+ '--uri=http://127.0.0.1:1234/',
+ if (branchCoverage) '--branch-coverage',
+ if (functionCoverage) '--function-coverage',
+ '-o',
+ covFile,
+ ]);
+
+ await format_coverage.main([
+ '--lcov',
+ '--check-ignore',
+ '-i',
+ covFile,
+ '-o',
+ lcovFile,
+ ]);
+ }
+ }
+}
+
+// Emitter that just captures the value.
+class CaptureEmitter implements ScoreEmitter {
+ late double capturedValue;
+
+ @override
+ void emit(String testName, double value) {
+ capturedValue = value;
+ }
+}
+
+// Prints a JSON representation of the benchmark results, in a format compatible
+// with the github benchmark action.
+class JsonEmitter implements ScoreEmitter {
+ JsonEmitter(this._baseline);
+
+ final double _baseline;
+ final _results = <String, double>{};
+
+ @override
+ void emit(String testName, double value) {
+ _results[testName] = value;
+ }
+
+ String write() => '[${_results.entries.map((entry) => """{
+ "name": "${entry.key}",
+ "unit": "times slower",
+ "value": ${(entry.value / _baseline).toStringAsFixed(2)}
+}""").join(',\n')}]';
+}
+
+Future<void> runBenchmark(CoverageBenchmark benchmark) async {
+ for (int i = 0; i < 3; ++i) {
+ try {
+ await benchmark.report().timeout(Duration(minutes: 2));
+ return;
+ } on TimeoutException {
+ print('Timed out');
+ }
+ }
+ print('Timed out too many times. Giving up.');
+ exit(127);
+}
+
+Future<String> runBenchmarkSet(String name, String script) async {
+ final captureEmitter = CaptureEmitter();
+ await runBenchmark(
+ CoverageBenchmark(captureEmitter, '$name - no coverage', script));
+ final benchmarkBaseline = captureEmitter.capturedValue;
+
+ final emitter = JsonEmitter(benchmarkBaseline);
+ await runBenchmark(CoverageBenchmark(
+ emitter, '$name - basic coverage', script,
+ gatherCoverage: true));
+ await runBenchmark(CoverageBenchmark(
+ emitter, '$name - function coverage', script,
+ gatherCoverage: true, functionCoverage: true));
+ await runBenchmark(CoverageBenchmark(
+ emitter, '$name - branch coverage', script,
+ gatherCoverage: true, branchCoverage: true));
+ return emitter.write();
+}
+
+Future<void> main() async {
+ // Assume this script was started from the root coverage directory. Change to
+ // the benchmark directory.
+ Directory.current = 'benchmark';
+ final result = await runBenchmarkSet('Many isolates', 'many_isolates.dart');
+ await File('data/benchmark_result.json').writeAsString(result);
+ exit(0);
+}
diff --git a/pubspec.yaml b/pubspec.yaml
index 429085f..16d2cdb 100644
--- a/pubspec.yaml
+++ b/pubspec.yaml
@@ -16,6 +16,7 @@
vm_service: ^9.2.0
dev_dependencies:
+ benchmark_harness: ^2.2.0
build_runner: ^2.1.10
lints: ^1.0.0
mockito: ^5.1.0