diff --git a/CHANGELOG.md b/CHANGELOG.md index ce0c0eb..ec69baf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,10 @@ -## 2.2.3-wip +## 2.2.3 - Require Dart 3.2. +- Add `PerfBenchmarkBase` class which runs the 'perf stat' command from +linux-tools on a benchmark and reports metrics from the hardware +performance counters and the iteration count, as well as the run time +measurement reported by `BenchmarkBase`. ## 2.2.2 diff --git a/integration_test/perf_benchmark_test.dart b/integration_test/perf_benchmark_test.dart new file mode 100644 index 0000000..339777f --- /dev/null +++ b/integration_test/perf_benchmark_test.dart @@ -0,0 +1,26 @@ +// Copyright 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 'package:benchmark_harness/perf_benchmark_harness.dart'; +import 'package:test/test.dart'; + +class PerfBenchmark extends PerfBenchmarkBase { + PerfBenchmark(super.name); + int runCount = 0; + + @override + void run() { + runCount++; + for (final i in List.filled(1000, 7)) { + runCount += i - i; + } + } +} + +void main() { + test('run is called', () async { + final benchmark = PerfBenchmark('ForLoop'); + await benchmark.reportPerf(); + }); +} diff --git a/lib/benchmark_harness.dart b/lib/benchmark_harness.dart index ee1563c..b46a36f 100644 --- a/lib/benchmark_harness.dart +++ b/lib/benchmark_harness.dart @@ -3,5 +3,5 @@ // BSD-style license that can be found in the LICENSE file. export 'src/async_benchmark_base.dart'; -export 'src/benchmark_base.dart'; +export 'src/benchmark_base.dart' show BenchmarkBase; export 'src/score_emitter.dart'; diff --git a/lib/perf_benchmark_harness.dart b/lib/perf_benchmark_harness.dart new file mode 100644 index 0000000..3de8329 --- /dev/null +++ b/lib/perf_benchmark_harness.dart @@ -0,0 +1,7 @@ +// 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. + +export 'src/perf_benchmark_base_stub.dart' + if (dart.library.io) 'src/perf_benchmark_base.dart'; +export 'src/score_emitter.dart'; diff --git a/lib/src/async_benchmark_base.dart b/lib/src/async_benchmark_base.dart index 1472ee7..b342a3d 100644 --- a/lib/src/async_benchmark_base.dart +++ b/lib/src/async_benchmark_base.dart @@ -64,6 +64,6 @@ class AsyncBenchmarkBase { /// Run the benchmark and report results on the [emitter]. Future report() async { - emitter.emit(name, await measure()); + emitter.emit(name, await measure(), unit: 'us.'); } } diff --git a/lib/src/benchmark_base.dart b/lib/src/benchmark_base.dart index ad1bb91..51a89bb 100644 --- a/lib/src/benchmark_base.dart +++ b/lib/src/benchmark_base.dart @@ -6,7 +6,7 @@ import 'dart:math' as math; import 'score_emitter.dart'; -const int _minimumMeasureDurationMillis = 2000; +const int minimumMeasureDurationMillis = 2000; class BenchmarkBase { final String name; @@ -40,56 +40,62 @@ class BenchmarkBase { /// Measures the score for this benchmark by executing it enough times /// to reach [minimumMillis]. - static _Measurement _measureForImpl(void Function() f, int minimumMillis) { - final minimumMicros = minimumMillis * 1000; - // If running a long measurement permit some amount of measurement jitter - // to avoid discarding results that are almost good, but not quite there. - final allowedJitter = - minimumMillis < 1000 ? 0 : (minimumMicros * 0.1).floor(); - var iter = 2; - final watch = Stopwatch()..start(); - while (true) { - watch.reset(); - for (var i = 0; i < iter; i++) { - f(); - } - final elapsed = watch.elapsedMicroseconds; - final measurement = _Measurement(elapsed, iter); - if (measurement.elapsedMicros >= (minimumMicros - allowedJitter)) { - return measurement; - } - - iter = measurement.estimateIterationsNeededToReach( - minimumMicros: minimumMicros); - } - } /// Measures the score for this benchmark by executing it repeatedly until /// time minimum has been reached. static double measureFor(void Function() f, int minimumMillis) => - _measureForImpl(f, minimumMillis).score; + measureForImpl(f, minimumMillis).score; /// Measures the score for the benchmark and returns it. double measure() { setup(); // Warmup for at least 100ms. Discard result. - _measureForImpl(warmup, 100); + measureForImpl(warmup, 100); // Run the benchmark for at least 2000ms. - var result = _measureForImpl(exercise, _minimumMeasureDurationMillis); + var result = measureForImpl(exercise, minimumMeasureDurationMillis); teardown(); return result.score; } void report() { - emitter.emit(name, measure()); + emitter.emit(name, measure(), unit: 'us.'); + } +} + +/// Measures the score for this benchmark by executing it enough times +/// to reach [minimumMillis]. +Measurement measureForImpl(void Function() f, int minimumMillis) { + final minimumMicros = minimumMillis * 1000; + // If running a long measurement permit some amount of measurement jitter + // to avoid discarding results that are almost good, but not quite there. + final allowedJitter = + minimumMillis < 1000 ? 0 : (minimumMicros * 0.1).floor(); + var iter = 2; + var totalIterations = iter; + final watch = Stopwatch()..start(); + while (true) { + watch.reset(); + for (var i = 0; i < iter; i++) { + f(); + } + final elapsed = watch.elapsedMicroseconds; + final measurement = Measurement(elapsed, iter, totalIterations); + if (measurement.elapsedMicros >= (minimumMicros - allowedJitter)) { + return measurement; + } + + iter = measurement.estimateIterationsNeededToReach( + minimumMicros: minimumMicros); + totalIterations += iter; } } -class _Measurement { +class Measurement { final int elapsedMicros; final int iterations; + final int totalIterations; - _Measurement(this.elapsedMicros, this.iterations); + Measurement(this.elapsedMicros, this.iterations, this.totalIterations); double get score => elapsedMicros / iterations; diff --git a/lib/src/perf_benchmark_base.dart b/lib/src/perf_benchmark_base.dart new file mode 100644 index 0000000..3c4a5a1 --- /dev/null +++ b/lib/src/perf_benchmark_base.dart @@ -0,0 +1,132 @@ +// 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:async'; +import 'dart:convert'; +import 'dart:io'; + +import 'benchmark_base.dart'; +import 'score_emitter.dart'; + +class PerfBenchmarkBase extends BenchmarkBase { + late final Directory fifoDir; + late final String perfControlFifo; + late final RandomAccessFile openedFifo; + late final String perfControlAck; + late final RandomAccessFile openedAck; + late final Process perfProcess; + late final List perfProcessArgs; + + PerfBenchmarkBase(super.name, {super.emitter = const PrintEmitter()}); + + Future _createFifos() async { + perfControlFifo = '${fifoDir.path}/perf_control_fifo'; + perfControlAck = '${fifoDir.path}/perf_control_ack'; + for (final path in [perfControlFifo, perfControlAck]) { + final fifoResult = await Process.run('mkfifo', [path]); + if (fifoResult.exitCode != 0) { + throw ProcessException('mkfifo', [path], + 'Cannot create fifo: ${fifoResult.stderr}', fifoResult.exitCode); + } + } + } + + Future _startPerfStat() async { + await _createFifos(); + perfProcessArgs = [ + 'stat', + '--delay=-1', + '--control=fifo:$perfControlFifo,$perfControlAck', + '-x\\t', + '--pid=$pid', + ]; + perfProcess = await Process.start('perf', perfProcessArgs); + } + + void _enablePerf() { + openedFifo = File(perfControlFifo).openSync(mode: FileMode.writeOnly); + openedAck = File(perfControlAck).openSync(); + openedFifo.writeStringSync('enable\n'); + _waitForAck(); + } + + Future _stopPerfStat(int totalIterations) async { + openedFifo.writeStringSync('disable\n'); + openedFifo.closeSync(); + _waitForAck(); + openedAck.closeSync(); + perfProcess.kill(ProcessSignal.sigint); + unawaited(perfProcess.stdout.drain()); + final lines = await perfProcess.stderr + .transform(utf8.decoder) + .transform(const LineSplitter()) + .toList(); + final exitCode = await perfProcess.exitCode; + // Exit code from perf is -SIGINT when terminated with SIGINT. + if (exitCode != 0 && exitCode != -ProcessSignal.sigint.signalNumber) { + throw ProcessException( + 'perf', perfProcessArgs, lines.join('\n'), exitCode); + } + + const metrics = { + 'cycles': 'CpuCycles', + 'page-faults': 'MajorPageFaults', + }; + for (final line in lines) { + if (line.split('\t') + case [ + String counter, + _, + String event && ('cycles' || 'page-faults'), + ... + ]) { + emitter.emit(name, double.parse(counter) / totalIterations, + metric: metrics[event]!); + } + } + emitter.emit('$name.totalIterations', totalIterations.toDouble(), + metric: 'Count'); + } + + /// Measures the score for the benchmark and returns it. + Future measurePerf() async { + Measurement result; + setup(); + try { + fifoDir = await Directory.systemTemp.createTemp('fifo'); + try { + // Warmup for at least 100ms. Discard result. + measureForImpl(warmup, 100); + await _startPerfStat(); + try { + _enablePerf(); + // Run the benchmark for at least 2000ms. + result = measureForImpl(exercise, minimumMeasureDurationMillis); + await _stopPerfStat(result.totalIterations); + } catch (_) { + perfProcess.kill(ProcessSignal.sigkill); + rethrow; + } + } finally { + await fifoDir.delete(recursive: true); + } + } finally { + teardown(); + } + return result.score; + } + + Future reportPerf() async { + emitter.emit(name, await measurePerf(), unit: 'us.'); + } + + void _waitForAck() { + // Perf writes 'ack\n\x00' to the acknowledgement fifo. + const ackLength = 'ack\n\x00'.length; + var ack = [...openedAck.readSync(ackLength)]; + while (ack.length < ackLength) { + ack.addAll(openedAck.readSync(ackLength - ack.length)); + } + } +} diff --git a/lib/src/perf_benchmark_base_stub.dart b/lib/src/perf_benchmark_base_stub.dart new file mode 100644 index 0000000..81aa0ea --- /dev/null +++ b/lib/src/perf_benchmark_base_stub.dart @@ -0,0 +1,18 @@ +// 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 'benchmark_base.dart'; +import 'score_emitter.dart'; + +class PerfBenchmarkBase extends BenchmarkBase { + PerfBenchmarkBase(super.name, {super.emitter = const PrintEmitter()}); + + Future measurePerf() async { + return super.measure(); + } + + Future reportPerf() async { + super.report(); + } +} diff --git a/lib/src/score_emitter.dart b/lib/src/score_emitter.dart index f7138d3..43ea7aa 100644 --- a/lib/src/score_emitter.dart +++ b/lib/src/score_emitter.dart @@ -3,14 +3,16 @@ // BSD-style license that can be found in the LICENSE file. abstract class ScoreEmitter { - void emit(String testName, double value); + void emit(String testName, double value, + {String metric = 'RunTime', String unit}); } class PrintEmitter implements ScoreEmitter { const PrintEmitter(); @override - void emit(String testName, double value) { - print('$testName(RunTime): $value us.'); + void emit(String testName, double value, + {String metric = 'RunTime', String unit = ''}) { + print(['$testName($metric):', value, if (unit.isNotEmpty) unit].join(' ')); } } diff --git a/pubspec.yaml b/pubspec.yaml index adc5f6d..465d274 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,5 +1,5 @@ name: benchmark_harness -version: 2.2.3-wip +version: 2.2.3 description: The official Dart project benchmark harness. repository: https://github.com/dart-lang/benchmark_harness diff --git a/test/result_emitter_test.dart b/test/result_emitter_test.dart index e2cd1ea..bfbce4e 100644 --- a/test/result_emitter_test.dart +++ b/test/result_emitter_test.dart @@ -13,7 +13,8 @@ class MockResultEmitter extends ScoreEmitter { int emitCount = 0; @override - void emit(String name, double value) { + void emit(String name, double value, + {String metric = 'RunTime', String unit = ''}) { emitCount++; } }