[benchmark_harness] add support for detailed measurement with CV% - #2393
[benchmark_harness] add support for detailed measurement with CV%#2393modulovalue wants to merge 3 commits into
Conversation
Adds BenchmarkBase.measureDetailed / reportDetailed that time each run() call individually and surface a coefficient of variation alongside the mean. Intended for benchmarks where one run() is at or above the timer noise floor (~100 us, typically ms-scale). The detailed path bypasses any exercise override (always calls run() directly), so the reported mean is per run() rather than per batch of 10. Existing APIs (measure, report, Measurement, PrintEmitter, PrintEmitterV2, ScoreEmitter, ScoreEmitterV2) are unchanged.
There was a problem hiding this comment.
Code Review
This pull request introduces a detailed measurement path for benchmarks, enabling per-run timing and statistical analysis such as median and coefficient of variation. It adds the DetailedMeasurement class and measureDetailed and reportDetailed methods to BenchmarkBase. The reviewer suggests improving implementation robustness with try...finally for teardown, respecting the emitter abstraction, and aligning documentation with the sample collection logic.
Addresses gemini-code-assist review on the PR. The previous text claimed the loop never extends past minimumMillis, but the 2-sample floor for stddev means it can. Reword to describe the actual behavior. Also remove dashes from a few unrelated dartdoc lines and the README section added in the previous commit, replacing them with periods or commas for consistency.
|
#2394 😆😆😆😆😆 I have NOT done a thorough review of this PR. Mostly vibes. But hilarious we are thinking about the same stuff! |
Package publishing
Documentation at https://github.com/dart-lang/ecosystem/wiki/Publishing-automation. |
PR HealthLicense Headers ✔️
All source files should start with a license header. Unrelated files missing license headers
This check can be disabled by tagging the PR with
Unused Dependencies
|
| Package | Status |
|---|---|
| benchmark_harness | ❗ Show IssuesThese packages may be unused, or you may be using assets from these packages: |
For details on how to fix these, see dependency_validator.
This check can be disabled by tagging the PR with skip-unused-dependencies-check.
Changelog Entry ✔️
| Package | Changed Files |
|---|
Changes to files need to be accounted for in their respective changelogs.
This check can be disabled by tagging the PR with skip-changelog-check.
Coverage ⚠️
| File | Coverage |
|---|---|
| pkgs/benchmark_harness/example/cv_example.dart | 💔 Not covered |
| pkgs/benchmark_harness/lib/benchmark_harness.dart | 💔 Not covered |
| pkgs/benchmark_harness/lib/src/benchmark_base.dart | 💔 57 % ⬇️ 32 % |
| pkgs/benchmark_harness/lib/src/measurement.dart | 💚 96 % ⬆️ 5 % |
| pkgs/benchmark_harness/lib/src/score_emitter.dart | 💔 8 % ⬇️ 54 % |
This check for test coverage is informational (issues shown here will not fail the PR).
This check can be disabled by tagging the PR with skip-coverage-check.
Breaking changes ✔️
| Package | Change | Current Version | New Version | Needed Version | Looking good? |
|---|---|---|---|---|---|
| benchmark_harness | Non-Breaking | 2.4.0 | 2.5.0 | 2.5.0 | ✔️ |
This check can be disabled by tagging the PR with skip-breaking-check.
API leaks ✔️
The following packages contain symbols visible in the public API, but not exported by the library. Export these symbols or remove them from your publicly visible API.
| Package | Leaked API symbol | Leaking sources |
|---|
This check can be disabled by tagging the PR with skip-leaking-check.
| ## Detailed measurement (CV%) | ||
|
|
||
| For benchmarks where one `run()` is comfortably above the timer noise | ||
| floor (≥ ~100 µs, typically ms-scale), `BenchmarkBase` exposes |
There was a problem hiding this comment.
>= 100 us is inconsistent with the code below which asks for more than 5 ms of work.
I think >= 5 ms is more realistic here.
| const SlowSineSum() : super('SlowSineSum'); | ||
|
|
||
| @override | ||
| void run() { |
There was a problem hiding this comment.
If we are trying to make things better, I suggest we redesign this API even futher, how about:
base class BenchmarkBase {
// Override this if you want to use measureDetailed.
void runFor(int iterations) => throw UnsupportedError();
DetailedMeasurement measureDetailed() {
// First run trials to estimate required number of iterations for `runFor`, then collect samples using estimated iterations count.
}
}The best would be if we could generate runFor automatically, but alas that requires codegen.
| } | ||
|
|
||
| void main() { | ||
| const JsonParse().reportDetailed(); |
There was a problem hiding this comment.
Side-note (re changing in-tree benchmarks to use reportDetailed): we have automatic tooling running benchmarks on every-commit and parsing the output, so we can't just change the output. It would have to be under some sort of a flag.
|
I instrumented our Dart VM benchmarks and:
This PR uses elapsedMicroseconds, which is unreliable at small scales. Since I intentionally didn't add support for batching, this PR is unfit for the VM. ~5% of VM benchmarks are under 100ns. We can increase our timer resolution by looking at ticks, but those benchmarks are no longer hermetic and only valuable in relation to other benchmarks that ran on a similar machine. Once we start using batching, the CV becomes less reliable because we are calculating it from a moving average. It looks to me like we should consider three different buckets as the major use cases we want to optimize for:
Ideally we have the following properties:
Here's how I think those properties apply to the buckets:
I'll run some experiments with benchmark harness plus and revisit this in the future. Here's the raw report from the VM benchmarks for future reference: report.html |
Summary
Adds an opt-in detailed measurement path to
BenchmarkBasefor benchmarks where onerun()is comfortably above the timer noise floor (~100 µs, typically ms-scale). Surfaces a coefficient of variation alongside the mean, median, and min so users can gauge stability of their measurements.This moves the metrics I wrote about here from benchmark_harness_plus into the OG benchmark harness!
BenchmarkBase.measureDetailed: returns a newDetailedMeasurementwith per-call samples, mean, median, min, stddev, and CV.BenchmarkBase.reportDetailed: printsname: avg=... ms · median=... ms · CV=...% · samples=N · min=... msvia the new top-levelprintDetailedMeasurement(name, m)helper.exerciseand times eachrun()directly, so the printed mean is per-run(), not per batch of 10.Existing APIs are unchanged.
Output example
What's not included (deliberately)
AsyncBenchmarkBaseparity. Left as a follow-up to keep this PR focused on the sync path.msfor now.The existing benchmark methodology (i.e. the exercise part and not tracking sample results) is not compatible with a CV% measurement since taking the CV% of averages skews the result, so this does not use the exercise path (which is suboptimal but backward compatible) and runs iterations only without exercises. Should we perhaps have a new BenchmarkBase, a DetailedBenchmarkBase? Open to API feedback.
/cc @kevmoo what do you think?
/cc @munificent FYI added min from your feedback on benchmark harness plus!
/cc @mraleph the primary motivation here is to add support for more meaningful metrics in https://github.com/dart-lang/sdk/tree/main/benchmarks do you see any blindspots?