-
Couldn't load subscription status.
- Fork 26
Add support for running perftools to use hardware performance counters when benchmarking. #98
Changes from all commits
f212528
8a2c57f
3010760
c7a2166
bb86037
0176c8a
73f42b0
a9858d9
11d8eab
221124a
166a5b8
a687636
be1a2c4
931e4e7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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(); | ||
| }); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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'; |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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 { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. And you need to write a CHANGELOG entry about this new feature and bump the minor version number. |
||
| 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<String> perfProcessArgs; | ||
|
|
||
| PerfBenchmarkBase(super.name, {super.emitter = const PrintEmitter()}); | ||
|
|
||
| Future<void> _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<void> _startPerfStat() async { | ||
| await _createFifos(); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Any reason this is its own method? Perhaps it might be nicer with a utility function to make a singular fifo |
||
| 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<void> _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; | ||
whesse marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| // 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<double> measurePerf() async { | ||
whesse marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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<void> 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; | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do we want to check if this message is actually written and throw if not? |
||
| var ack = <int>[...openedAck.readSync(ackLength)]; | ||
| while (ack.length < ackLength) { | ||
| ack.addAll(openedAck.readSync(ackLength - ack.length)); | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<double> measurePerf() async { | ||
| return super.measure(); | ||
sortie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
|
|
||
| Future<void> reportPerf() async { | ||
| super.report(); | ||
sortie marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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}); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's good to note that this is a breaking change to anyone who https://github.com/search?q=%22implements+ScoreEmitter%22&type=code Is the breaking change worth it? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It is worth it, but it should be made in a major release of benchmark_harness. I'm reverting this interface change, using a new final subclass instead, and filing an issue to make the change in a major release. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Thank you |
||
| } | ||
|
|
||
| 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(' ')); | ||
| } | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.