mirror of
https://github.com/sharkdp/hyperfine.git
synced 2026-10-12 02:22:35 +00:00
Alternative performance metrics in the terminal (#986)
This commit is contained in:
@@ -1,5 +1,11 @@
|
||||
# Unreleased
|
||||
|
||||
## Features
|
||||
|
||||
- Add support for alternative performance metrics (peak memory usage, CPU cycles, instructions, cache misses, branch misses, ...). By default, hyperfine will now display wall-clock time and peak memory usage,
|
||||
but users can use the new `--metrics` option to select different performance metrics.
|
||||
- The terminal output now shows an overview of all measured performance metrics, including relative changes.
|
||||
|
||||
## Breaking changes
|
||||
|
||||
- Benchmarked commands are now always executed directly, without an intermediate shell (`--shell=none`). Use `-S` (an alias for `--shell=default`) to restore the previous behavior (`sh` on Unix, `cmd.exe` on Windows), or select a shell with `--shell <SHELL>`.
|
||||
@@ -46,6 +52,10 @@
|
||||
```
|
||||
|
||||
Times are always exported in seconds and memory in bytes.
|
||||
- `--time-unit`/`-u` has been removed. Units can now be selected using the `--metrics METRIC[:UNIT],…` option, e.g. `--metrics time_wall_clock:ms,memory_peak_resident:MiB`.
|
||||
- `--sort` has been removed. It complicated hyperfine significantly and doesn't make too much sense with the new output format.
|
||||
- `--reference` and `--reference-name` have been removed (for now). The first command is always considered as the reference command.
|
||||
- The format of Markdown, AsciiDoc, org-mode and CSV exports has also been changed. CSV now includes columns with the name and the unit of the primary metric.
|
||||
|
||||
# v1.21.0
|
||||
|
||||
|
||||
@@ -16,7 +16,7 @@ A command-line benchmarking tool.
|
||||
* Support for arbitrary shell commands.
|
||||
* Constant feedback about the benchmark progress and current estimates.
|
||||
* Warmup runs can be executed before the actual benchmark.
|
||||
* Cache-clearing commands can be set up before each timing run.
|
||||
* Cache-clearing commands can be set up before each benchmark run.
|
||||
* Statistical outlier detection to detect interference from other programs and caching effects.
|
||||
* Export results to various formats: CSV, JSON, Markdown, AsciiDoc.
|
||||
* Parameterized benchmarks (e.g. vary the number of threads).
|
||||
@@ -41,11 +41,14 @@ number of runs, you can use the `-r`/`--runs` option:
|
||||
hyperfine --runs 5 'sleep 0.3'
|
||||
```
|
||||
|
||||
If you want to compare the runtimes of different programs, you can pass multiple commands:
|
||||
If you want to compare different programs, you can pass multiple commands:
|
||||
```sh
|
||||
hyperfine 'hexdump file' 'xxd file'
|
||||
```
|
||||
|
||||
Commands run and appear in input order. The first command is the reference for all comparisons;
|
||||
each subsequent result shows its change from that reference.
|
||||
|
||||
### Warmup runs and preparation commands
|
||||
|
||||
For programs that perform a lot of disk I/O, the benchmarking results can be heavily influenced
|
||||
@@ -58,7 +61,7 @@ hyperfine -S --warmup 3 'grep -R TODO *'
|
||||
```
|
||||
|
||||
Conversely, if you want to run the benchmark for a cold cache, you can use the `-p`/`--prepare`
|
||||
option to run a special command before *each* timing run. For example, to clear Linux filesystem caches,
|
||||
option to run a special command before *each* benchmark run. For example, to clear Linux filesystem caches,
|
||||
you can run
|
||||
```sh
|
||||
sync; echo 3 | sudo tee /proc/sys/vm/drop_caches
|
||||
@@ -90,7 +93,7 @@ hyperfine -L compiler g++,clang++ '{compiler} -O2 main.cpp'
|
||||
```
|
||||
|
||||
A common use case is comparing the same command across multiple Git branches. Use `--setup`
|
||||
to switch branches once before each set of timing runs, so the branch switch is not part of
|
||||
to switch branches once before each set of benchmark runs, so the branch switch is not part of
|
||||
the measured command:
|
||||
```sh
|
||||
hyperfine \
|
||||
@@ -129,7 +132,7 @@ If any of these commands need shell syntax, enable a shell explicitly.
|
||||
|
||||
When a shell is enabled, hyperfine *corrects for the shell spawning time*. It runs the shell with an
|
||||
empty command multiple times to measure its startup time, then subtracts this time from each
|
||||
measurement.
|
||||
time measurement.
|
||||
|
||||
|
||||
### Shell functions
|
||||
@@ -148,38 +151,40 @@ Otherwise, inline the function into the benchmarked command:
|
||||
hyperfine -S 'my_function() { sleep 1; }; my_function'
|
||||
```
|
||||
|
||||
### Exporting results
|
||||
### Choosing metrics and units
|
||||
|
||||
Hyperfine has multiple options for exporting benchmark results to CSV, JSON, Markdown and other
|
||||
formats (see `--help` text for details).
|
||||
By default, hyperfine displays wall-clock time and memory usage
|
||||
(`time_wall_clock,memory_peak_resident`). Use `--metrics` with a
|
||||
comma-separated list to select a different set of metrics. Each
|
||||
metric can have an optional unit after it:
|
||||
|
||||
#### Markdown
|
||||
```sh
|
||||
hyperfine \
|
||||
--metrics memory_peak_resident:MiB,time_wall_clock:ms,instructions \
|
||||
'./baseline' './candidate'
|
||||
```
|
||||
|
||||
You can use the `--export-markdown <file>` option to create tables like the following:
|
||||
|
||||
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|
||||
|:---|---:|---:|---:|---:|
|
||||
| `find . -iregex '.*[0-9]\.jpg$'` | 2.275 ± 0.046 | 2.243 | 2.397 | 9.79 ± 0.22 |
|
||||
| `find . -iname '*[0-9].jpg'` | 1.427 ± 0.026 | 1.405 | 1.468 | 6.14 ± 0.13 |
|
||||
| `fd -HI '.*[0-9]\.jpg$'` | 0.232 ± 0.002 | 0.230 | 0.236 | 1.00 |
|
||||
|
||||
#### JSON
|
||||
|
||||
The JSON export includes the following metrics for each measured run (excluding warmup runs):
|
||||
The following metrics are available:
|
||||
|
||||
- **`time_wall_clock`**: Time from start to finish, including time spent waiting, in seconds.
|
||||
|
||||
- **`time_user`**: CPU time spent running the program's code, summed across threads, in seconds.
|
||||
- **`time_user`**: CPU time spent executing application and library code in user mode,
|
||||
summed across threads, in seconds.
|
||||
|
||||
- Linux/macOS: Includes child-process time when parents wait for their children to finish.
|
||||
- Windows: Includes the command and its child processes.
|
||||
|
||||
- **`time_system`**: CPU time spent running operating-system code for the program, for example
|
||||
- **`time_system`**: CPU time spent executing kernel code on the program's behalf, for example
|
||||
to read files, summed across threads, in seconds.
|
||||
|
||||
- Linux/macOS: Includes child-process time when parents wait for their children to finish.
|
||||
- Windows: Includes the command and its child processes.
|
||||
|
||||
- **`time_cpu`**: Total CPU time, calculated as `time_user + time_system` for each run, in seconds.
|
||||
It excludes time spent sleeping or waiting without executing. CPU time is summed across
|
||||
threads, so it can exceed wall-clock time: four threads running on four cores for one second
|
||||
can consume roughly four CPU-seconds.
|
||||
|
||||
- **`memory_peak_resident`**: Peak memory held in physical RAM, in bytes.
|
||||
|
||||
- Linux/macOS: Peak resident set size (RSS). This is the largest per-process peak among the
|
||||
@@ -215,7 +220,39 @@ The JSON export includes the following metrics for each measured run (excluding
|
||||
- Linux: Same scope as `cpu_cycles`.
|
||||
- macOS/Windows: Currently not supported.
|
||||
|
||||
Hardware counters are not available if a `--shell` is used.
|
||||
Note that hardware counters are not available if a `--shell` is used.
|
||||
|
||||
Time measurements support units `ns`, `us`, `ms`, `s`, `min`, and `h`. Memory measurements
|
||||
support `B`, `kB`, `MB`, `GB`, `TB`, `KiB`, `MiB`, `GiB`, and `TiB`. Hardware counters
|
||||
support `count` (1), `k` (thousand), `M` (million), and `B` (billion).
|
||||
|
||||
You can also use a preset to select a group of metrics:
|
||||
|
||||
| Preset | Metrics |
|
||||
|:---|:---|
|
||||
| `--metrics=default` | `time_wall_clock,memory_peak_resident` |
|
||||
| `--metrics=time` | `time_wall_clock,time_cpu,time_user,time_system` |
|
||||
| `--metrics=all` | All available metrics |
|
||||
|
||||
### Exporting results
|
||||
|
||||
Hyperfine can export results to CSV, JSON, Markdown, AsciiDoc, and org-mode. Non-JSON formats
|
||||
contain only the primary metric.
|
||||
|
||||
#### Markdown
|
||||
|
||||
You can use the `--export-markdown <file>` option to create tables like the following:
|
||||
|
||||
| Command | Mean Wall Time [s] | Change | Factor |
|
||||
|:---|---:|---:|:---|
|
||||
| `find . -iregex '.*[0-9]\.jpg$'` | 2.275 ± 0.046 | | |
|
||||
| `find . -iname '*[0-9].jpg'` | 1.427 ± 0.026 | -37.3% | (1.6x faster) |
|
||||
| `fd -HI '.*[0-9]\.jpg$'` | 0.232 ± 0.002 | -89.8% | (9.8x faster) |
|
||||
|
||||
#### JSON
|
||||
|
||||
The JSON export includes all available metrics listed in [Choosing metrics and units](#choosing-metrics-and-units)
|
||||
for each measured run (excluding warmup runs).
|
||||
|
||||
The JSON output is useful if you want to analyze the benchmark results in more detail. The
|
||||
[`scripts/`](https://github.com/sharkdp/hyperfine/tree/master/scripts) folder includes a lot
|
||||
@@ -229,7 +266,7 @@ multiple benchmarks:
|
||||
|
||||
### Detailed benchmark flowchart
|
||||
|
||||
The following chart explains the execution order of various timing runs when using options
|
||||
The following chart explains the execution order of various benchmark runs when using options
|
||||
like `--warmup`, `--prepare <cmd>`, `--setup <cmd>` or `--cleanup <cmd>`:
|
||||
|
||||

|
||||
|
||||
+43
-66
@@ -15,10 +15,6 @@ hyperfine \- command\-line benchmarking tool
|
||||
.IR NUM ]
|
||||
.RB [ \-\-setup
|
||||
.IR CMD ]
|
||||
.RB [ \-\-reference
|
||||
.IR CMD ]
|
||||
.RB [ \-\-reference\-name
|
||||
.IR NAME ]
|
||||
.RB [ \-\-prepare
|
||||
.IR CMD ]
|
||||
.RB [ \-\-conclude
|
||||
@@ -38,10 +34,8 @@ hyperfine \- command\-line benchmarking tool
|
||||
.IR SHELL ]
|
||||
.RB [ \-\-style
|
||||
.IR TYPE ]
|
||||
.RB [ \-\-sort
|
||||
.IR METHOD ]
|
||||
.RB [ \-\-time-unit
|
||||
.IR UNIT ]
|
||||
.RB [ \-\-metrics
|
||||
.IR METRIC[:UNIT],...|default|time|all ]
|
||||
.RB [ \-\-export\-asciidoc
|
||||
.IR FILE ]
|
||||
.RB [ \-\-export\-csv
|
||||
@@ -75,7 +69,7 @@ A command\-line benchmarking tool which includes:
|
||||
* Warmup runs can be executed before the actual benchmark
|
||||
.RE
|
||||
.RS
|
||||
* Cache-clearing commands can be set up before each timing run
|
||||
* Cache-clearing commands can be set up before each benchmark run
|
||||
.RE
|
||||
.RS
|
||||
* Statistical outlier detection to detect interference from other programs and caching effects
|
||||
@@ -109,28 +103,14 @@ Perform exactly \fINUM\fP runs for each command. If this option is not specified
|
||||
.HP
|
||||
\fB\-s\fR, \fB\-\-setup\fR \fICMD...\fP
|
||||
.IP
|
||||
Execute \fICMD\fP once before each set of timing runs. This is useful
|
||||
Execute \fICMD\fP once before each set of benchmark runs. This is useful
|
||||
for compiling your software with the provided parameters, or to do any
|
||||
other work that should happen once before a series of benchmark runs,
|
||||
not every time as would happen with the \fB\-\-prepare\fR option.
|
||||
.HP
|
||||
\fB\-\-reference\fR \fICMD\fP
|
||||
.IP
|
||||
Use \fICMD\fP as the reference for the relative comparison of results. Without
|
||||
parameters, benchmark \fICMD\fP as a separate reference command. If this option
|
||||
is not specified, the fastest command is used as the reference.
|
||||
With \fB\-\-parameter\-scan\fR or \fB\-\-parameter\-list\fR, select an existing
|
||||
benchmark by its exact printed name. The name must match exactly one benchmark.
|
||||
.HP
|
||||
\fB\-\-reference\-name\fR \fINAME\fP
|
||||
.IP
|
||||
Give a meaningful \fINAME\fP to the reference command.
|
||||
Requires \fB\-\-reference\fR.
|
||||
Cannot be used with parameterized benchmarks; use \fB\-\-command\-name\fR instead.
|
||||
.HP
|
||||
\fB\-p\fR, \fB\-\-prepare\fR \fICMD...\fP
|
||||
.IP
|
||||
Execute \fICMD\fP before each timing run. This is useful for clearing disk caches,
|
||||
Execute \fICMD\fP before each benchmark run. This is useful for clearing disk caches,
|
||||
for example.
|
||||
The \fB\-\-prepare\fR option can be specified once for all commands or multiple times,
|
||||
once for each command. In the latter case, each preparation command will be
|
||||
@@ -138,7 +118,7 @@ run prior to the corresponding benchmark command.
|
||||
.HP
|
||||
\fB\-C\fR, \fB\-\-conclude\fR \fICMD...\fP
|
||||
.IP
|
||||
Execute \fICMD\fP after each timing run. This is useful for clearing disk caches,
|
||||
Execute \fICMD\fP after each benchmark run. This is useful for clearing disk caches,
|
||||
for example.
|
||||
The \fB\-\-conclude\fR option can be specified once for all commands or multiple times,
|
||||
once for each command. In the latter case, each conclusion command will be
|
||||
@@ -248,65 +228,62 @@ interactive output without any colors. Set this to 'color' to keep the colors
|
||||
without any interactive output. Set this to 'none' to disable all the output
|
||||
of the tool.
|
||||
.HP
|
||||
\fB\-\-sort\fR \fIMETHOD\fP
|
||||
\fB\-\-metrics\fR \fIMETRIC[:UNIT],...|default|time|all\fP
|
||||
.IP
|
||||
Specify the sort order of the speed comparison summary and the
|
||||
exported tables for markup formats (Markdown, AsciiDoc, org\-mode):
|
||||
Select the metrics to display, in order, with an optional unit for each.
|
||||
Default: \fBtime_wall_clock,memory_peak_resident\fR, or \fBtime_wall_clock\fR
|
||||
on Windows, where peak resident memory is not yet supported.
|
||||
.IP
|
||||
Use \fBdefault\fR for the platform defaults, \fBtime\fR for
|
||||
\fBtime_wall_clock,time_cpu,time_user,time_system\fR, or \fBall\fR for all available
|
||||
metrics in the order listed below.
|
||||
.IP
|
||||
Time metrics \fBtime_wall_clock\fR, \fBtime_cpu\fR, \fBtime_user\fR, and \fBtime_system\fR
|
||||
accept units \fBns\fR, \fBus\fR, \fBms\fR, \fBs\fR, \fBmin\fR, and \fBh\fR.
|
||||
Peak memory, \fBmemory_peak_resident\fR, accepts \fBB\fR, \fBkB\fR,
|
||||
\fBMB\fR, \fBGB\fR, \fBTB\fR, \fBKiB\fR, \fBMiB\fR, \fBGiB\fR,
|
||||
and \fBTiB\fR.
|
||||
.IP
|
||||
Counter metrics \fBcpu_cycles\fR, \fBinstructions\fR, \fBcache_references\fR,
|
||||
\fBcache_misses\fR, and \fBbranch_misses\fR accept \fBcount\fR (unscaled),
|
||||
\fBk\fR (thousand), \fBM\fR (million), and \fBB\fR (billion).
|
||||
.IP
|
||||
If a unit is omitted, terminal and markup output automatically select a suitable
|
||||
unit.
|
||||
.RS
|
||||
.IP "auto (default)"
|
||||
the speed comparison will be ordered by time and
|
||||
the markup tables will be ordered by command (input order).
|
||||
.IP "command"
|
||||
order benchmarks in the way they were specified
|
||||
.IP "mean\-time"
|
||||
order benchmarks by mean runtime
|
||||
.RE
|
||||
.HP
|
||||
\fB\-u\fR, \fB\-\-time\-unit\fR \fIUNIT\fP
|
||||
.IP
|
||||
Set the time unit to be used. If the option is not given, the time unit is
|
||||
determined automatically. Possible values:
|
||||
Example:
|
||||
.RS
|
||||
.IP "microsecond"
|
||||
Aliases: µs, us, microseconds.
|
||||
.IP "millisecond"
|
||||
Aliases: ms, milliseconds.
|
||||
.IP "second"
|
||||
Aliases: s, seconds.
|
||||
.IP "minute"
|
||||
Aliases: min, minutes.
|
||||
.IP "hour"
|
||||
Aliases: h, hours.
|
||||
\fBhyperfine\fR \fB\-\-metrics\fR memory_peak_resident:MiB,time_wall_clock:ms,instructions:B './baseline' './candidate'
|
||||
.RE
|
||||
.RE
|
||||
.IP
|
||||
This option affects the standard output as well as all export formats
|
||||
except for CSV and JSON.
|
||||
.HP
|
||||
\fB\-\-export\-asciidoc\fR \fIFILE\fP
|
||||
.IP
|
||||
Export the timing summary statistics as an AsciiDoc table to the given \fIFILE\fP.
|
||||
The output time unit can be changed using the \fB\-\-time\-unit\fR option.
|
||||
Export the primary metric's summary statistics as an AsciiDoc table to the given
|
||||
\fIFILE\fP. Use its selected unit, or choose a unit automatically when omitted.
|
||||
.HP
|
||||
\fB\-\-export\-csv\fR \fIFILE\fP
|
||||
.IP
|
||||
Export the timing summary statistics as CSV to the given \fIFILE\fP. If you need the
|
||||
timing results for each individual run, use the JSON export format.
|
||||
The output time unit is always seconds.
|
||||
Export the primary metric's summary statistics as CSV to the given \fIFILE\fP,
|
||||
identifying the metric and unit explicitly. Use the explicitly selected unit,
|
||||
or the base unit (seconds, bytes, or unscaled counts) when omitted.
|
||||
.HP
|
||||
\fB\-\-export\-json\fR \fIFILE\fP
|
||||
.IP
|
||||
Export the timing summary statistics and timings of individual runs as JSON to
|
||||
the given \fIFILE\fP. The output time unit is always seconds.
|
||||
Export summary statistics and individual samples for all collected metrics as
|
||||
JSON to the given \fIFILE\fP. Include the primary metric, and always use base
|
||||
units (seconds, bytes, or unscaled counts), regardless of display settings.
|
||||
.HP
|
||||
\fB\-\-export\-markdown\fR \fIFILE\fP
|
||||
.IP
|
||||
Export the timing summary statistics as a Markdown table to the given \fIFILE\fP.
|
||||
The output time unit can be changed using the \fB\-\-time\-unit\fR option.
|
||||
Export the primary metric's summary statistics as a Markdown table to the given
|
||||
\fIFILE\fP. Use its selected unit, or choose a unit automatically when omitted.
|
||||
.HP
|
||||
\fB\-\-export\-orgmode\fR \fIFILE\fP
|
||||
.IP
|
||||
Export the timing summary statistics as an Emacs org\-mode table to the
|
||||
given \fIFILE\fP. The output time unit can be changed using the \fB\-\-time\-unit\fR option.
|
||||
Export the primary metric's summary statistics as an Emacs org\-mode table to
|
||||
the given \fIFILE\fP. Use its selected unit, or choose a unit automatically
|
||||
when omitted.
|
||||
.HP
|
||||
\fB\-\-show\-output\fR
|
||||
.IP
|
||||
|
||||
+1
-3
@@ -7,9 +7,7 @@ hyperfine 'sleep 0.020' 'sleep 0.021' 'sleep 0.022' --export-json sleep.json
|
||||
uv run plot_whisker.py sleep.json
|
||||
```
|
||||
|
||||
All plotting scripts accept `--metric` to select a metric. Hardware counters
|
||||
(`cpu_cycles`, `instructions`, `cache_references`, `cache_misses`, and `branch_misses`)
|
||||
are plotted as unscaled counts when available in the JSON export. For example:
|
||||
All plotting and analysis scripts use the JSON export's `primary_metric` by default.
|
||||
|
||||
```bash
|
||||
uv run plot_histogram.py benchmark.json --metric instructions
|
||||
|
||||
@@ -7,74 +7,56 @@
|
||||
# ///
|
||||
|
||||
import argparse
|
||||
import json
|
||||
from enum import Enum
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
class Unit(Enum):
|
||||
SECOND = 1
|
||||
MILLISECOND = 2
|
||||
|
||||
def factor(self):
|
||||
match self:
|
||||
case Unit.SECOND:
|
||||
return 1
|
||||
case Unit.MILLISECOND:
|
||||
return 1e3
|
||||
|
||||
def __str__(self):
|
||||
match self:
|
||||
case Unit.SECOND:
|
||||
return "s"
|
||||
case Unit.MILLISECOND:
|
||||
return "ms"
|
||||
|
||||
from plot_utils import METRICS, add_metric_argument, load_results
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("file", help="JSON file with benchmark results")
|
||||
add_metric_argument(parser)
|
||||
parser.add_argument(
|
||||
"--time-unit",
|
||||
help="The unit of time.",
|
||||
default="second",
|
||||
action="store",
|
||||
help="Display unit for time metrics (default: second).",
|
||||
choices=["second", "millisecond"],
|
||||
dest="unit",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
unit = Unit.MILLISECOND if args.unit == "millisecond" else Unit.SECOND
|
||||
unit_str = str(unit)
|
||||
metric, [results] = load_results(parser, [args.file], args.metric)
|
||||
description = METRICS[metric]
|
||||
unit, scale = description.unit, description.scale
|
||||
if args.time_unit is not None:
|
||||
if not metric.startswith("time_"):
|
||||
parser.error("--time-unit can only be used with a time metric")
|
||||
if args.time_unit == "millisecond":
|
||||
unit, scale = "ms", 0.001
|
||||
|
||||
with open(args.file) as f:
|
||||
results = json.load(f)["results"]
|
||||
|
||||
def format_value(value):
|
||||
suffix = f" {unit}" if unit else ""
|
||||
return f"{value:.3f}{suffix}"
|
||||
|
||||
|
||||
print(f"Metric: {description.label}\n")
|
||||
for result in results:
|
||||
summary = result["summary"]["time_wall_clock"]
|
||||
ts = [m["time_wall_clock"]["value"] * unit.factor() for m in result["measurements"]]
|
||||
summary = result["summary"][metric]
|
||||
values = [m[metric]["value"] / scale for m in result["measurements"]]
|
||||
|
||||
p05 = np.percentile(ts, 5)
|
||||
p25 = np.percentile(ts, 25)
|
||||
p75 = np.percentile(ts, 75)
|
||||
p95 = np.percentile(ts, 95)
|
||||
p05, p25, p75, p95 = np.percentile(values, [5, 25, 75, 95])
|
||||
|
||||
iqr = p75 - p25
|
||||
|
||||
print(f"Command '{result.get('name', result['command'])}'")
|
||||
print(f" runs: {summary['count']:8d}")
|
||||
print(f" mean: {summary['mean'] * unit.factor():8.3f} {unit_str}")
|
||||
if summary["stddev"] is None:
|
||||
print(f" stddev: {'N/A':>8}")
|
||||
else:
|
||||
print(f" stddev: {summary['stddev'] * unit.factor():8.3f} {unit_str}")
|
||||
print(f" median: {summary['median'] * unit.factor():8.3f} {unit_str}")
|
||||
print(f" min: {summary['min'] * unit.factor():8.3f} {unit_str}")
|
||||
print(f" max: {summary['max'] * unit.factor():8.3f} {unit_str}")
|
||||
for statistic in ["mean", "stddev", "median", "min", "max"]:
|
||||
value = summary[statistic]
|
||||
formatted = "N/A" if value is None else format_value(value / scale)
|
||||
print(f" {statistic + ':':<8}{formatted:>8}")
|
||||
print()
|
||||
print(" percentiles:")
|
||||
print(f" P_05 .. P_95: {p05:.3f} {unit_str} .. {p95:.3f} {unit_str}")
|
||||
print(f" P_05 .. P_95: {format_value(p05)} .. {format_value(p95)}")
|
||||
print(
|
||||
f" P_25 .. P_75: {p25:.3f} {unit_str} .. {p75:.3f} {unit_str} (IQR = {iqr:.3f} {unit_str})"
|
||||
f" P_25 .. P_75: {format_value(p25)} .. {format_value(p75)}"
|
||||
f" (IQR = {format_value(iqr)})"
|
||||
)
|
||||
print()
|
||||
|
||||
@@ -14,13 +14,12 @@ Note all the input files must contain results for all commands.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import pathlib
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
from plot_utils import METRICS, add_metric_argument, validate_metric
|
||||
from plot_utils import METRICS, add_metric_argument, load_results
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
add_metric_argument(parser)
|
||||
@@ -34,7 +33,8 @@ parser.add_argument(
|
||||
parser.add_argument("-o", "--output", help="Save image to the given filename")
|
||||
|
||||
args = parser.parse_args()
|
||||
metric_label, metric_scale = METRICS[args.metric]
|
||||
metric, datasets = load_results(parser, args.files, args.metric)
|
||||
metric_info = METRICS[metric]
|
||||
|
||||
commands = None
|
||||
data = []
|
||||
@@ -45,10 +45,7 @@ if args.benchmark_names:
|
||||
"Number of benchmark names must match the number of input files."
|
||||
)
|
||||
|
||||
for i, filename in enumerate(args.files):
|
||||
with open(filename) as f:
|
||||
results = json.load(f)["results"]
|
||||
validate_metric(parser, results, args.metric)
|
||||
for i, (filename, results) in enumerate(zip(args.files, datasets)):
|
||||
benchmark_commands = [b.get("name", b["command"]) for b in results]
|
||||
if commands is None:
|
||||
commands = benchmark_commands
|
||||
@@ -56,7 +53,7 @@ for i, filename in enumerate(args.files):
|
||||
assert commands == benchmark_commands, (
|
||||
f"Unexpected commands in {filename}: {benchmark_commands}, expected: {commands}"
|
||||
)
|
||||
data.append([b["summary"][args.metric]["mean"] / metric_scale for b in results])
|
||||
data.append([b["summary"][metric]["mean"] / metric_info.scale for b in results])
|
||||
if args.benchmark_names:
|
||||
inputs.append(args.benchmark_names[i])
|
||||
else:
|
||||
@@ -79,7 +76,7 @@ ax.grid(visible=True, axis="y")
|
||||
if args.title:
|
||||
plt.title(args.title)
|
||||
plt.xlabel("Benchmark")
|
||||
plt.ylabel(metric_label)
|
||||
plt.ylabel(metric_info.axis_label)
|
||||
plt.legend(title="Command")
|
||||
|
||||
if args.output:
|
||||
|
||||
@@ -11,12 +11,11 @@
|
||||
"""This program shows `hyperfine` benchmark results as a histogram."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
from plot_utils import METRICS, add_metric_argument, validate_metric
|
||||
from plot_utils import METRICS, add_metric_argument, load_results
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
add_metric_argument(parser)
|
||||
@@ -70,18 +69,15 @@ parser.add_argument(
|
||||
)
|
||||
|
||||
args = parser.parse_args()
|
||||
metric_label, metric_scale = METRICS[args.metric]
|
||||
|
||||
with open(args.file) as f:
|
||||
results = json.load(f)["results"]
|
||||
validate_metric(parser, results, args.metric)
|
||||
metric, [results] = load_results(parser, [args.file], args.metric)
|
||||
metric_info = METRICS[metric]
|
||||
|
||||
if args.labels:
|
||||
labels = args.labels.split(",")
|
||||
else:
|
||||
labels = [b.get("name", b["command"]) for b in results]
|
||||
all_values = [
|
||||
[m[args.metric]["value"] / metric_scale for m in b["measurements"]] for b in results
|
||||
[m[metric]["value"] / metric_info.scale for m in b["measurements"]] for b in results
|
||||
]
|
||||
|
||||
value_min = (
|
||||
@@ -113,7 +109,7 @@ plt.legend(
|
||||
prop={"size": 10, "family": ["Source Code Pro", "Fira Mono", "Courier New"]},
|
||||
)
|
||||
|
||||
plt.xlabel(metric_label)
|
||||
plt.xlabel(metric_info.axis_label)
|
||||
if args.title:
|
||||
plt.title(args.title)
|
||||
|
||||
|
||||
@@ -11,12 +11,11 @@
|
||||
errorbar plot."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from plot_utils import METRICS, add_metric_argument, validate_metric
|
||||
from plot_utils import METRICS, add_metric_argument, load_results
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
add_metric_argument(parser)
|
||||
@@ -43,7 +42,8 @@ parser.add_argument(
|
||||
parser.add_argument("-o", "--output", help="Save image to the given filename.")
|
||||
|
||||
args = parser.parse_args()
|
||||
metric_label, metric_scale = METRICS[args.metric]
|
||||
metric, datasets = load_results(parser, args.file, args.metric)
|
||||
metric_info = METRICS[metric]
|
||||
if args.parameter_name is not None:
|
||||
sys.stderr.write(
|
||||
"warning: --parameter-name is deprecated; names are inferred from "
|
||||
@@ -84,11 +84,7 @@ def unique_parameter(benchmark):
|
||||
|
||||
parameter_name = None
|
||||
|
||||
for filename in args.file:
|
||||
with open(filename) as f:
|
||||
results = json.load(f)["results"]
|
||||
validate_metric(parser, results, args.metric)
|
||||
|
||||
for results in datasets:
|
||||
(this_parameter_name, parameter_values) = extract_parameters(results)
|
||||
if parameter_name is not None and this_parameter_name != parameter_name:
|
||||
die(
|
||||
@@ -96,17 +92,17 @@ for filename in args.file:
|
||||
)
|
||||
parameter_name = this_parameter_name
|
||||
|
||||
summaries = [b["summary"][args.metric] for b in results]
|
||||
means = [s["mean"] / metric_scale for s in summaries]
|
||||
summaries = [b["summary"][metric] for b in results]
|
||||
means = [s["mean"] / metric_info.scale for s in summaries]
|
||||
stddevs = [
|
||||
s["stddev"] / metric_scale if s["stddev"] is not None else float("nan")
|
||||
s["stddev"] / metric_info.scale if s["stddev"] is not None else float("nan")
|
||||
for s in summaries
|
||||
]
|
||||
|
||||
plt.errorbar(x=parameter_values, y=means, yerr=stddevs, capsize=2)
|
||||
|
||||
plt.xlabel(parameter_name)
|
||||
plt.ylabel(metric_label)
|
||||
plt.ylabel(metric_info.axis_label)
|
||||
|
||||
if args.log_y:
|
||||
plt.yscale("log")
|
||||
|
||||
@@ -14,12 +14,11 @@ thermal throttling and similar effects.
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import numpy as np
|
||||
|
||||
from plot_utils import METRICS, add_metric_argument, validate_metric
|
||||
from plot_utils import METRICS, add_metric_argument, load_results
|
||||
|
||||
|
||||
def moving_average(values, num_runs):
|
||||
@@ -50,15 +49,12 @@ parser.add_argument(
|
||||
|
||||
|
||||
args = parser.parse_args()
|
||||
metric_label, metric_scale = METRICS[args.metric]
|
||||
|
||||
with open(args.file) as f:
|
||||
results = json.load(f)["results"]
|
||||
validate_metric(parser, results, args.metric)
|
||||
metric, [results] = load_results(parser, [args.file], args.metric)
|
||||
metric_info = METRICS[metric]
|
||||
|
||||
for result in results:
|
||||
label = result.get("name", result["command"])
|
||||
values = [m[args.metric]["value"] / metric_scale for m in result["measurements"]]
|
||||
values = [m[metric]["value"] / metric_info.scale for m in result["measurements"]]
|
||||
num = len(values)
|
||||
nums = range(num)
|
||||
|
||||
@@ -86,7 +82,7 @@ for result in results:
|
||||
legend.append("moving average")
|
||||
plt.legend(legend)
|
||||
|
||||
plt.ylabel(metric_label)
|
||||
plt.ylabel(metric_info.axis_label)
|
||||
|
||||
if args.output:
|
||||
plt.savefig(args.output)
|
||||
|
||||
+54
-12
@@ -1,15 +1,30 @@
|
||||
"""Shared metric options for the plotting scripts."""
|
||||
"""Shared metric selection and units for analysis and plotting scripts."""
|
||||
|
||||
import json
|
||||
from typing import NamedTuple
|
||||
|
||||
|
||||
class Metric(NamedTuple):
|
||||
label: str
|
||||
unit: str = ""
|
||||
scale: float = 1
|
||||
|
||||
@property
|
||||
def axis_label(self):
|
||||
return f"{self.label} [{self.unit}]" if self.unit else self.label
|
||||
|
||||
|
||||
METRICS = {
|
||||
"time_wall_clock": ("Time [s]", 1),
|
||||
"time_user": ("User CPU time [s]", 1),
|
||||
"time_system": ("System CPU time [s]", 1),
|
||||
"memory_peak_resident": ("Peak resident memory [MiB]", 1024**2),
|
||||
"cpu_cycles": ("CPU cycles", 1),
|
||||
"instructions": ("Completed CPU instructions", 1),
|
||||
"cache_references": ("Cache references", 1),
|
||||
"cache_misses": ("Cache misses", 1),
|
||||
"branch_misses": ("Mispredicted branches", 1),
|
||||
"time_wall_clock": Metric("Wall-clock time", "s"),
|
||||
"time_cpu": Metric("Total CPU time", "s"),
|
||||
"time_user": Metric("User CPU time", "s"),
|
||||
"time_system": Metric("System CPU time", "s"),
|
||||
"memory_peak_resident": Metric("Peak resident memory", "MiB", 1024**2),
|
||||
"cpu_cycles": Metric("CPU cycles"),
|
||||
"instructions": Metric("Completed CPU instructions"),
|
||||
"cache_references": Metric("Cache references"),
|
||||
"cache_misses": Metric("Cache misses"),
|
||||
"branch_misses": Metric("Mispredicted branches"),
|
||||
}
|
||||
|
||||
|
||||
@@ -17,8 +32,8 @@ def add_metric_argument(parser):
|
||||
parser.add_argument(
|
||||
"--metric",
|
||||
choices=METRICS,
|
||||
default="time_wall_clock",
|
||||
help="Metric to plot (default: time_wall_clock). "
|
||||
help="Metric to analyze (default: primary_metric from the JSON export, "
|
||||
"or time_wall_clock if unspecified). "
|
||||
"Times are in seconds; memory is in MiB; hardware counters are unscaled counts.",
|
||||
)
|
||||
|
||||
@@ -30,3 +45,30 @@ def validate_metric(parser, results, metric):
|
||||
f"metric {metric!r} is unavailable for "
|
||||
f"{result.get('name', result['command'])!r}"
|
||||
)
|
||||
|
||||
|
||||
def load_results(parser, filenames, metric=None):
|
||||
exports = []
|
||||
for filename in filenames:
|
||||
with open(filename, encoding="utf-8") as f:
|
||||
export = json.load(f)
|
||||
if not export["results"]:
|
||||
parser.error(f"no benchmark results in {filename}")
|
||||
exports.append(export)
|
||||
|
||||
if metric is None:
|
||||
primary_metrics = {
|
||||
export.get("primary_metric", "time_wall_clock") for export in exports
|
||||
}
|
||||
if len(primary_metrics) != 1:
|
||||
parser.error(
|
||||
"input files use different primary metrics; select one with --metric"
|
||||
)
|
||||
metric = primary_metrics.pop()
|
||||
if metric not in METRICS:
|
||||
parser.error(f"unknown metric {metric!r}; select one with --metric")
|
||||
|
||||
results = [export["results"] for export in exports]
|
||||
for benchmarks in results:
|
||||
validate_metric(parser, benchmarks, metric)
|
||||
return metric, results
|
||||
|
||||
+6
-10
@@ -16,11 +16,10 @@ Quoting from the matplotlib documentation:
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from plot_utils import METRICS, add_metric_argument, validate_metric
|
||||
from plot_utils import METRICS, add_metric_argument, load_results
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
add_metric_argument(parser)
|
||||
@@ -33,22 +32,19 @@ parser.add_argument(
|
||||
parser.add_argument("-o", "--output", help="Save image to the given filename.")
|
||||
|
||||
args = parser.parse_args()
|
||||
metric_label, metric_scale = METRICS[args.metric]
|
||||
|
||||
with open(args.file, encoding="utf-8") as f:
|
||||
results = json.load(f)["results"]
|
||||
validate_metric(parser, results, args.metric)
|
||||
metric, [results] = load_results(parser, [args.file], args.metric)
|
||||
metric_info = METRICS[metric]
|
||||
|
||||
if args.labels:
|
||||
labels = args.labels.split(",")
|
||||
else:
|
||||
labels = [b.get("name", b["command"]) for b in results]
|
||||
values = [
|
||||
[m[args.metric]["value"] / metric_scale for m in b["measurements"]] for b in results
|
||||
[m[metric]["value"] / metric_info.scale for m in b["measurements"]] for b in results
|
||||
]
|
||||
|
||||
if args.sort_by == "median":
|
||||
medians = [b["summary"][args.metric]["median"] for b in results]
|
||||
medians = [b["summary"][metric]["median"] for b in results]
|
||||
indices = sorted(range(len(labels)), key=lambda k: medians[k])
|
||||
labels = [labels[i] for i in indices]
|
||||
values = [values[i] for i in indices]
|
||||
@@ -64,7 +60,7 @@ for patch, color in zip(boxplot["boxes"], colors):
|
||||
if args.title:
|
||||
plt.title(args.title)
|
||||
plt.legend(handles=boxplot["boxes"], labels=labels, loc="best", fontsize="medium")
|
||||
plt.ylabel(metric_label)
|
||||
plt.ylabel(metric_info.axis_label)
|
||||
plt.ylim(0, None)
|
||||
plt.xticks(list(range(1, len(labels) + 1)), labels, rotation=45)
|
||||
if args.output:
|
||||
|
||||
+15
-10
@@ -7,32 +7,37 @@
|
||||
# ///
|
||||
|
||||
"""This script performs Welch's t-test on a JSON export file with two
|
||||
benchmark results to test for a difference in their population mean runtimes."""
|
||||
benchmark results to test for a difference in their population means."""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import sys
|
||||
import math
|
||||
|
||||
from scipy import stats
|
||||
|
||||
from plot_utils import METRICS, add_metric_argument, load_results
|
||||
|
||||
parser = argparse.ArgumentParser(description=__doc__)
|
||||
parser.add_argument("file", help="JSON file with two benchmark results")
|
||||
add_metric_argument(parser)
|
||||
args = parser.parse_args()
|
||||
|
||||
with open(args.file) as f:
|
||||
results = json.load(f)["results"]
|
||||
metric, [results] = load_results(parser, [args.file], args.metric)
|
||||
|
||||
if len(results) != 2:
|
||||
print("The input file has to contain exactly two benchmarks")
|
||||
sys.exit(1)
|
||||
parser.error("the input file must contain exactly two benchmarks")
|
||||
|
||||
a, b = (x.get("name", x["command"]) for x in results[:2])
|
||||
X, Y = ([m["time_wall_clock"]["value"] for m in x["measurements"]] for x in results[:2])
|
||||
X, Y = ([m[metric]["value"] for m in x["measurements"]] for x in results[:2])
|
||||
if min(len(X), len(Y)) < 2:
|
||||
parser.error("Welch's t-test requires at least two measurements per benchmark")
|
||||
|
||||
print(f"Metric: {METRICS[metric].label}")
|
||||
print(f"Command 1: {a}")
|
||||
print(f"Command 2: {b}\n")
|
||||
|
||||
t, p = stats.ttest_ind(X, Y, equal_var=False)
|
||||
if not math.isfinite(p):
|
||||
parser.error("Welch's t-test is undefined for these measurements")
|
||||
th = 0.05
|
||||
dispose = p < th
|
||||
print(f"t = {t:.3}, p = {p:.3}")
|
||||
@@ -40,9 +45,9 @@ print()
|
||||
|
||||
if dispose:
|
||||
print(
|
||||
f"A statistically significant difference in mean runtime was detected (p < {th})."
|
||||
f"A statistically significant difference in mean value was detected (p < {th})."
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"No statistically significant difference in mean runtime was detected (p >= {th})."
|
||||
f"No statistically significant difference in mean value was detected (p >= {th})."
|
||||
)
|
||||
|
||||
@@ -3,7 +3,6 @@ use std::collections::BTreeMap;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::benchmark::measurement::Measurements;
|
||||
use crate::quantity::Time;
|
||||
|
||||
/// Parameter value
|
||||
#[derive(Debug, Default, Clone, Serialize, PartialEq)]
|
||||
@@ -47,9 +46,4 @@ impl BenchmarkResult {
|
||||
pub fn get_name(&self) -> &str {
|
||||
self.name.as_deref().unwrap_or(&self.command)
|
||||
}
|
||||
|
||||
/// The average wall clock time
|
||||
pub fn mean_wall_clock_time(&self) -> Time {
|
||||
self.measurements.time_wall_clock_mean()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -220,6 +220,7 @@ impl Executor for ShellExecutor<'_> {
|
||||
ensure_non_negative(measurement.time_user - spawning_time.time_user);
|
||||
measurement.time_system =
|
||||
ensure_non_negative(measurement.time_system - spawning_time.time_system);
|
||||
measurement.time_cpu = measurement.time_user + measurement.time_system;
|
||||
}
|
||||
|
||||
Ok(measurement)
|
||||
@@ -278,6 +279,7 @@ impl Executor for ShellExecutor<'_> {
|
||||
|
||||
self.shell_spawning_time = Some(Measurement {
|
||||
time_wall_clock: measurements.time_wall_clock_mean(),
|
||||
time_cpu: measurements.time_user_mean() + measurements.time_system_mean(),
|
||||
time_user: measurements.time_user_mean(),
|
||||
time_system: measurements.time_system_mean(),
|
||||
memory_peak_resident: None,
|
||||
@@ -337,6 +339,7 @@ impl Executor for MockExecutor {
|
||||
|
||||
Ok(Measurement {
|
||||
time_wall_clock: Self::extract_time(command.get_command_line()),
|
||||
time_cpu: Time::zero(),
|
||||
time_user: Time::zero(),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::zero()),
|
||||
|
||||
@@ -2,7 +2,8 @@ use std::process::ExitStatus;
|
||||
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::quantity::statistics::{max, mean, median, min, modified_zscores, standard_deviation};
|
||||
use crate::metric::Metric;
|
||||
use crate::quantity::statistics::{mean, modified_zscores_f64};
|
||||
use crate::quantity::{serialize_information, serialize_time, Information, Time};
|
||||
use crate::util::exit_code::extract_exit_code;
|
||||
|
||||
@@ -77,6 +78,10 @@ pub struct Measurement {
|
||||
#[serde(serialize_with = "serialize_time")]
|
||||
pub time_wall_clock: Time,
|
||||
|
||||
/// Total CPU time (user and kernel mode)
|
||||
#[serde(serialize_with = "serialize_time")]
|
||||
pub time_cpu: Time,
|
||||
|
||||
/// Time spent in user mode
|
||||
#[serde(serialize_with = "serialize_time")]
|
||||
pub time_user: Time,
|
||||
@@ -131,35 +136,14 @@ impl Measurements {
|
||||
mean(self.wall_clock_times())
|
||||
}
|
||||
|
||||
/// The standard deviation of all wall clock times. Not available if only one run has been performed
|
||||
pub fn stddev(&self) -> Option<Time> {
|
||||
let times: Vec<_> = self.wall_clock_times().collect(); // TODO: Avoid collecting
|
||||
|
||||
if times.len() < 2 {
|
||||
None
|
||||
} else {
|
||||
Some(standard_deviation(times))
|
||||
}
|
||||
}
|
||||
|
||||
/// The median wall clock time
|
||||
pub fn median(&self) -> Time {
|
||||
median(self.wall_clock_times())
|
||||
}
|
||||
|
||||
/// The minimum wall clock time
|
||||
pub fn min(&self) -> Time {
|
||||
min(self.wall_clock_times())
|
||||
}
|
||||
|
||||
/// The maximum wall clock time
|
||||
pub fn max(&self) -> Time {
|
||||
max(self.wall_clock_times())
|
||||
}
|
||||
|
||||
/// Compute modified Z-scores for the wall clock times
|
||||
pub fn modified_zscores(&self) -> Vec<f64> {
|
||||
modified_zscores(&self.wall_clock_times().collect::<Vec<_>>())
|
||||
/// Compute modified Z-scores for a metric validated to be available in every run.
|
||||
pub fn modified_zscores(&self, metric: Metric) -> Vec<f64> {
|
||||
let values: Vec<_> = self
|
||||
.measurements
|
||||
.iter()
|
||||
.map(|m| metric.value(m).expect("Validated metric is available"))
|
||||
.collect();
|
||||
modified_zscores_f64(&values)
|
||||
}
|
||||
|
||||
/// The average user time
|
||||
@@ -172,3 +156,28 @@ impl Measurements {
|
||||
mean(self.measurements.iter().map(|m| m.time_system))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::outlier_detection::OUTLIER_THRESHOLD;
|
||||
use crate::quantity::{byte, second};
|
||||
|
||||
#[test]
|
||||
fn outliers_follow_the_selected_metric() {
|
||||
let measurements = Measurements::new(
|
||||
[(1.0, 100.0), (1.0, 1.0), (1.0, 1.0), (100.0, 1.0)]
|
||||
.iter()
|
||||
.copied()
|
||||
.map(|(time, memory)| Measurement {
|
||||
time_wall_clock: Time::new::<second>(time),
|
||||
memory_peak_resident: Some(Information::new::<byte>(memory)),
|
||||
..Measurement::default()
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
|
||||
assert!(measurements.modified_zscores(Metric::TimeWallClock)[0] < OUTLIER_THRESHOLD);
|
||||
assert!(measurements.modified_zscores(Metric::MemoryPeakResident)[0] > OUTLIER_THRESHOLD);
|
||||
}
|
||||
}
|
||||
|
||||
+66
-93
@@ -1,7 +1,6 @@
|
||||
pub mod benchmark_result;
|
||||
pub mod executor;
|
||||
pub mod measurement;
|
||||
pub mod relative_speed;
|
||||
pub mod scheduler;
|
||||
|
||||
use std::cmp;
|
||||
@@ -18,14 +17,15 @@ use crate::options::{
|
||||
use crate::outlier_detection::OUTLIER_THRESHOLD;
|
||||
use crate::output::console_writeln;
|
||||
use crate::output::progress_bar::{
|
||||
finish_initial_measurement, get_progress_bar, start_initial_measurement,
|
||||
finish_initial_measurement, get_progress_bar, set_benchmark_header, start_initial_measurement,
|
||||
};
|
||||
use crate::output::report;
|
||||
use crate::output::warnings::{OutlierWarningOptions, Warnings};
|
||||
use crate::parameter::ParameterNameAndValue;
|
||||
use crate::quantity::{self, const_time_from_seconds, FormatQuantity, Time, Zero};
|
||||
use crate::quantity::{self, const_time_from_seconds, Time, Zero};
|
||||
use benchmark_result::BenchmarkResult;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use anyhow::{anyhow, ensure, Result};
|
||||
use colored::*;
|
||||
|
||||
use self::executor::Executor;
|
||||
@@ -155,20 +155,21 @@ impl<'a> Benchmark<'a> {
|
||||
self.run_intermediate_command(command, error_output, output_policy, iteration)
|
||||
}
|
||||
|
||||
/// Run the benchmark for a single command
|
||||
pub fn run(&self) -> Result<BenchmarkResult> {
|
||||
if self.options.output_style != OutputStyleOption::Disabled {
|
||||
console_writeln!(
|
||||
io::stdout(),
|
||||
"{}{}: {}",
|
||||
"Benchmark ".bold(),
|
||||
(self.number + 1).to_string().bold(),
|
||||
self.command.get_name_with_unused_parameters(),
|
||||
)?;
|
||||
fn validate_measurement(&self, measurement: &Measurement) -> Result<()> {
|
||||
if self.options.skip_unavailable_metrics {
|
||||
return Ok(());
|
||||
}
|
||||
for selection in &self.options.metrics {
|
||||
ensure!(selection.metric.value(measurement).is_some(),
|
||||
"Metric '{}' is unavailable for '{}'. Check platform support and hardware-counter permissions.",
|
||||
selection.metric.name(), self.command.get_command_line());
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Run the benchmark for a single command
|
||||
pub fn run(&self, reference: Option<&BenchmarkResult>) -> Result<BenchmarkResult> {
|
||||
let mut measurements = Measurements::default();
|
||||
let mut all_succeeded = true;
|
||||
|
||||
let output_policy = &self.options.command_output_policies[self.number];
|
||||
|
||||
@@ -213,6 +214,15 @@ impl<'a> Benchmark<'a> {
|
||||
|
||||
self.run_setup_command(self.command.get_parameters().iter().cloned(), output_policy)?;
|
||||
|
||||
let progress_header = format!(
|
||||
"{}: {}",
|
||||
format!("Benchmark {}", self.number + 1).bold(),
|
||||
self.command
|
||||
.get_name_with_unused_parameters()
|
||||
.white()
|
||||
.bold()
|
||||
);
|
||||
|
||||
// Warmup phase
|
||||
if self.options.warmup_count > 0 {
|
||||
let progress_bar = if self.options.output_style != OutputStyleOption::Disabled {
|
||||
@@ -225,6 +235,10 @@ impl<'a> Benchmark<'a> {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(bar) = &progress_bar {
|
||||
set_benchmark_header(bar, progress_header.clone());
|
||||
}
|
||||
|
||||
for i in 0..self.options.warmup_count {
|
||||
let warmup_iteration = BenchmarkIteration::Warmup(i);
|
||||
let _ = run_preparation_command(warmup_iteration)?;
|
||||
@@ -259,6 +273,10 @@ impl<'a> Benchmark<'a> {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(bar) = &progress_bar {
|
||||
set_benchmark_header(bar, progress_header);
|
||||
}
|
||||
|
||||
let benchmark_iteration = BenchmarkIteration::Benchmark(0);
|
||||
let preparation_result = run_preparation_command(benchmark_iteration)?;
|
||||
let preparation_overhead = preparation_result.map_or(Time::zero(), |res| {
|
||||
@@ -275,18 +293,14 @@ impl<'a> Benchmark<'a> {
|
||||
None,
|
||||
output_policy,
|
||||
)?;
|
||||
let success = measurement.exit_status.success();
|
||||
self.validate_measurement(&measurement)?;
|
||||
let mut all_succeeded = measurement.exit_status.success();
|
||||
|
||||
if let Some(bar) = progress_bar.as_ref() {
|
||||
let time_unit = self
|
||||
.options
|
||||
.time_unit
|
||||
.unwrap_or(measurement.time_wall_clock.suitable_unit());
|
||||
let estimate = measurement.time_wall_clock.format(time_unit);
|
||||
finish_initial_measurement(
|
||||
bar,
|
||||
format!("Current estimate: {}", estimate.to_string().green()),
|
||||
);
|
||||
let primary = self.options.metrics[0];
|
||||
let value = primary.metric.value(&measurement).unwrap();
|
||||
let estimate = primary.display_unit(value).format(value);
|
||||
finish_initial_measurement(bar, format!("Current estimate: {}", estimate.green()));
|
||||
}
|
||||
|
||||
let conclusion_result = run_conclusion_command(benchmark_iteration)?;
|
||||
@@ -316,10 +330,10 @@ impl<'a> Benchmark<'a> {
|
||||
let count_remaining = count - 1;
|
||||
|
||||
// Save the first result
|
||||
let primary = self.options.metrics[0];
|
||||
let mut primary_total = primary.metric.value(&measurement).unwrap();
|
||||
measurements.push(measurement);
|
||||
|
||||
all_succeeded = all_succeeded && success;
|
||||
|
||||
// Re-configure the progress bar
|
||||
if let Some(bar) = progress_bar.as_ref() {
|
||||
bar.set_length(count);
|
||||
@@ -330,18 +344,12 @@ impl<'a> Benchmark<'a> {
|
||||
for i in 0..count_remaining {
|
||||
let benchmark_iteration = BenchmarkIteration::Benchmark(i + 1);
|
||||
|
||||
let msg = {
|
||||
let t_wall_clock_mean = measurements.time_wall_clock_mean();
|
||||
let time_unit = self
|
||||
.options
|
||||
.time_unit
|
||||
.unwrap_or(t_wall_clock_mean.suitable_unit());
|
||||
let mean = t_wall_clock_mean.format(time_unit);
|
||||
format!("Current estimate: {}", mean.to_string().green())
|
||||
};
|
||||
|
||||
if let Some(bar) = progress_bar.as_ref() {
|
||||
bar.set_message(msg.to_owned())
|
||||
let mean = primary_total / measurements.len() as f64;
|
||||
bar.set_message(format!(
|
||||
"Current estimate: {}",
|
||||
primary.display_unit(mean).format(mean).green()
|
||||
));
|
||||
}
|
||||
|
||||
run_preparation_command(benchmark_iteration)?;
|
||||
@@ -352,7 +360,9 @@ impl<'a> Benchmark<'a> {
|
||||
None,
|
||||
output_policy,
|
||||
)?;
|
||||
self.validate_measurement(&measurement)?;
|
||||
let success = measurement.exit_status.success();
|
||||
primary_total += primary.metric.value(&measurement).unwrap();
|
||||
measurements.push(measurement);
|
||||
|
||||
all_succeeded = all_succeeded && success;
|
||||
@@ -368,63 +378,27 @@ impl<'a> Benchmark<'a> {
|
||||
bar.finish_and_clear()
|
||||
}
|
||||
|
||||
// Formatting and console output
|
||||
let t_wall_clock_mean = measurements.time_wall_clock_mean();
|
||||
let time_unit = self
|
||||
.options
|
||||
.time_unit
|
||||
.unwrap_or(t_wall_clock_mean.suitable_unit());
|
||||
let mean_str = t_wall_clock_mean.format(time_unit);
|
||||
let min_str = measurements.min().format(time_unit);
|
||||
let max_str = measurements.max().format(time_unit);
|
||||
let num_str = format!("{num_runs} runs", num_runs = measurements.len());
|
||||
|
||||
let user_str = measurements.time_user_mean().format(time_unit);
|
||||
let system_str = measurements.time_system_mean().format(time_unit);
|
||||
|
||||
if self.options.output_style != OutputStyleOption::Disabled {
|
||||
let mut stdout = io::stdout().lock();
|
||||
if measurements.len() == 1 {
|
||||
console_writeln!(
|
||||
stdout,
|
||||
" Time ({} ≡): {:>8} {:>8} [User: {}, System: {}]",
|
||||
"abs".green().bold(),
|
||||
mean_str.green().bold(),
|
||||
" ", // alignment
|
||||
user_str.blue(),
|
||||
system_str.blue()
|
||||
)?;
|
||||
} else {
|
||||
let stddev_str = measurements.stddev().unwrap().format(time_unit);
|
||||
|
||||
console_writeln!(
|
||||
stdout,
|
||||
" Time ({} ± {}): {:>8} ± {:>8} [User: {}, System: {}]",
|
||||
"mean".green().bold(),
|
||||
"σ".green(),
|
||||
mean_str.green().bold(),
|
||||
stddev_str.green(),
|
||||
user_str.blue(),
|
||||
system_str.blue()
|
||||
)?;
|
||||
|
||||
console_writeln!(
|
||||
stdout,
|
||||
" Range ({} … {}): {:>8} … {:>8} {}",
|
||||
"min".cyan(),
|
||||
"max".purple(),
|
||||
min_str.cyan(),
|
||||
max_str.purple(),
|
||||
num_str.dimmed()
|
||||
)?;
|
||||
}
|
||||
report::print(
|
||||
self.number,
|
||||
&self.command.get_name_with_unused_parameters(),
|
||||
&measurements,
|
||||
&self.options.metrics,
|
||||
self.options.skip_unavailable_metrics,
|
||||
reference,
|
||||
)?;
|
||||
}
|
||||
|
||||
// Warnings
|
||||
let mut warnings = vec![];
|
||||
|
||||
// Check execution time
|
||||
if matches!(self.options.executor_kind, ExecutorKind::Shell(_))
|
||||
if self
|
||||
.options
|
||||
.metrics
|
||||
.iter()
|
||||
.any(|m| m.metric == crate::metric::Metric::TimeWallClock)
|
||||
&& matches!(self.options.executor_kind, ExecutorKind::Shell(_))
|
||||
&& measurements
|
||||
.wall_clock_times()
|
||||
.any(|t| t < MIN_EXECUTION_TIME)
|
||||
@@ -438,7 +412,7 @@ impl<'a> Benchmark<'a> {
|
||||
}
|
||||
|
||||
// Run outlier detection
|
||||
let scores = measurements.modified_zscores();
|
||||
let scores = measurements.modified_zscores(primary.metric);
|
||||
|
||||
let outlier_warning_options = OutlierWarningOptions {
|
||||
warmup_in_use: self.options.warmup_count > 0,
|
||||
@@ -452,12 +426,11 @@ impl<'a> Benchmark<'a> {
|
||||
};
|
||||
|
||||
if scores[0] > OUTLIER_THRESHOLD {
|
||||
warnings.push(Warnings::SlowInitialRun(
|
||||
measurements.wall_clock_times().next().unwrap(),
|
||||
warnings.push(Warnings::InitialRunOutlier(
|
||||
primary,
|
||||
primary.metric.value(&measurements.measurements[0]).unwrap(),
|
||||
outlier_warning_options,
|
||||
));
|
||||
} else if scores.iter().any(|&s| s.abs() > OUTLIER_THRESHOLD) {
|
||||
warnings.push(Warnings::OutliersDetected(outlier_warning_options));
|
||||
}
|
||||
|
||||
if !warnings.is_empty() {
|
||||
|
||||
@@ -1,237 +0,0 @@
|
||||
use std::cmp::Ordering;
|
||||
|
||||
use super::benchmark_result::BenchmarkResult;
|
||||
use crate::{
|
||||
options::SortOrder,
|
||||
quantity::{self, Ratio, Time, Zero},
|
||||
};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BenchmarkResultWithRelativeSpeed<'a> {
|
||||
pub result: &'a BenchmarkResult,
|
||||
pub relative_speed: f64,
|
||||
pub relative_speed_stddev: Option<f64>,
|
||||
pub is_reference: bool,
|
||||
// Less means faster
|
||||
pub relative_ordering: Ordering,
|
||||
}
|
||||
|
||||
impl BenchmarkResultWithRelativeSpeed<'_> {
|
||||
pub fn reference_label(&self) -> &'static str {
|
||||
if self.is_reference {
|
||||
" (reference)"
|
||||
} else {
|
||||
match self.relative_ordering {
|
||||
Ordering::Less => " (faster)",
|
||||
Ordering::Equal => " (same speed)",
|
||||
Ordering::Greater => " (slower)",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compare_mean_time(l: &BenchmarkResult, r: &BenchmarkResult) -> Ordering {
|
||||
l.mean_wall_clock_time()
|
||||
.partial_cmp(&r.mean_wall_clock_time())
|
||||
.unwrap_or(Ordering::Equal)
|
||||
}
|
||||
|
||||
pub fn fastest_of(results: &[BenchmarkResult]) -> &BenchmarkResult {
|
||||
results
|
||||
.iter()
|
||||
.min_by(|&l, &r| compare_mean_time(l, r))
|
||||
.expect("at least one benchmark result")
|
||||
}
|
||||
|
||||
fn compute_relative_speeds<'a>(
|
||||
results: &'a [BenchmarkResult],
|
||||
reference: &'a BenchmarkResult,
|
||||
sort_order: SortOrder,
|
||||
) -> Vec<BenchmarkResultWithRelativeSpeed<'a>> {
|
||||
let mut results: Vec<_> = results
|
||||
.iter()
|
||||
.map(|result| {
|
||||
// Separate benchmarks can have identical names and measurements.
|
||||
let is_reference = std::ptr::eq(result, reference);
|
||||
let relative_ordering = compare_mean_time(result, reference);
|
||||
|
||||
if result.mean_wall_clock_time() == Time::zero()
|
||||
|| reference.mean_wall_clock_time() == Time::zero()
|
||||
{
|
||||
return BenchmarkResultWithRelativeSpeed {
|
||||
result,
|
||||
relative_speed: if is_reference { 1.0 } else { f64::INFINITY },
|
||||
relative_speed_stddev: None,
|
||||
is_reference,
|
||||
relative_ordering,
|
||||
};
|
||||
}
|
||||
|
||||
let ratio = match relative_ordering {
|
||||
Ordering::Less => reference.mean_wall_clock_time() / result.mean_wall_clock_time(),
|
||||
Ordering::Equal => Ratio::new::<quantity::ratio>(1.0),
|
||||
Ordering::Greater => {
|
||||
result.mean_wall_clock_time() / reference.mean_wall_clock_time()
|
||||
}
|
||||
};
|
||||
|
||||
// https://en.wikipedia.org/wiki/Propagation_of_uncertainty#Example_formulas
|
||||
// Covariance assumed to be 0, i.e. variables are assumed to be independent
|
||||
let ratio_stddev = match (
|
||||
result.measurements.stddev(),
|
||||
reference.measurements.stddev(),
|
||||
) {
|
||||
(Some(result_stddev), Some(reference_stddev)) => Some(
|
||||
ratio
|
||||
* ((result_stddev / result.mean_wall_clock_time())
|
||||
.powi(uom::typenum::P2::new())
|
||||
+ (reference_stddev / reference.mean_wall_clock_time())
|
||||
.powi(uom::typenum::P2::new()))
|
||||
.sqrt(),
|
||||
),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
BenchmarkResultWithRelativeSpeed {
|
||||
result,
|
||||
relative_speed: ratio.get::<quantity::ratio>(),
|
||||
relative_speed_stddev: ratio_stddev.map(|r| r.get::<quantity::ratio>()),
|
||||
is_reference,
|
||||
relative_ordering,
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
match sort_order {
|
||||
SortOrder::Command => {}
|
||||
SortOrder::MeanTime => {
|
||||
results.sort_unstable_by(|r1, r2| compare_mean_time(r1.result, r2.result));
|
||||
}
|
||||
}
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
pub fn compute_with_check_from_reference<'a>(
|
||||
results: &'a [BenchmarkResult],
|
||||
reference: &'a BenchmarkResult,
|
||||
sort_order: SortOrder,
|
||||
) -> Option<Vec<BenchmarkResultWithRelativeSpeed<'a>>> {
|
||||
if fastest_of(results).mean_wall_clock_time() == Time::zero()
|
||||
|| reference.mean_wall_clock_time() == Time::zero()
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(compute_relative_speeds(results, reference, sort_order))
|
||||
}
|
||||
|
||||
pub fn compute_with_check(
|
||||
results: &[BenchmarkResult],
|
||||
sort_order: SortOrder,
|
||||
) -> Option<Vec<BenchmarkResultWithRelativeSpeed<'_>>> {
|
||||
let fastest = fastest_of(results);
|
||||
|
||||
if fastest.mean_wall_clock_time() == Time::zero() {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(compute_relative_speeds(results, fastest, sort_order))
|
||||
}
|
||||
|
||||
/// Compute relative speeds against the given reference, or the fastest result.
|
||||
pub fn compute<'a>(
|
||||
results: &'a [BenchmarkResult],
|
||||
sort_order: SortOrder,
|
||||
reference: Option<&'a BenchmarkResult>,
|
||||
) -> Vec<BenchmarkResultWithRelativeSpeed<'a>> {
|
||||
let reference = reference.unwrap_or_else(|| fastest_of(results));
|
||||
compute_relative_speeds(results, reference, sort_order)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn create_result(name: &str, mean: f64) -> BenchmarkResult {
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use crate::benchmark::measurement::{Measurement, Measurements};
|
||||
use crate::quantity::second;
|
||||
|
||||
BenchmarkResult {
|
||||
command: name.into(),
|
||||
display_name: name.into(),
|
||||
name: None,
|
||||
measurements: Measurements {
|
||||
measurements: vec![Measurement {
|
||||
time_wall_clock: Time::new::<second>(mean),
|
||||
time_user: Time::new::<second>(mean),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
parameters: BTreeMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_relative_speed() {
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
let results = vec![
|
||||
create_result("cmd1", 3.0),
|
||||
create_result("cmd2", 2.0),
|
||||
create_result("cmd3", 5.0),
|
||||
];
|
||||
|
||||
let annotated_results = compute_with_check(&results, SortOrder::Command).unwrap();
|
||||
|
||||
assert_relative_eq!(1.5, annotated_results[0].relative_speed);
|
||||
assert_relative_eq!(1.0, annotated_results[1].relative_speed);
|
||||
assert_relative_eq!(2.5, annotated_results[2].relative_speed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_relative_speed_with_reference() {
|
||||
use approx::assert_relative_eq;
|
||||
|
||||
let results = vec![create_result("cmd2", 2.0), create_result("cmd3", 5.0)];
|
||||
let reference = create_result("cmd2", 4.0);
|
||||
|
||||
let annotated_results =
|
||||
compute_with_check_from_reference(&results, &reference, SortOrder::Command).unwrap();
|
||||
|
||||
assert_relative_eq!(2.0, annotated_results[0].relative_speed);
|
||||
assert_relative_eq!(1.25, annotated_results[1].relative_speed);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_compute_relative_speed_for_zero_times() {
|
||||
let results = vec![create_result("cmd1", 1.0), create_result("cmd2", 0.0)];
|
||||
|
||||
let annotated_results = compute_with_check(&results, SortOrder::Command);
|
||||
|
||||
assert!(annotated_results.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_identity_distinguishes_equal_results() {
|
||||
let results = vec![create_result("same", 1.0), create_result("same", 1.0)];
|
||||
let entries = compute_relative_speeds(&results, &results[0], SortOrder::Command);
|
||||
|
||||
assert!(entries[0].is_reference);
|
||||
assert!(!entries[1].is_reference);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_ratios_handle_zero_times() {
|
||||
let mut results = vec![create_result("reference", 2.0), create_result("zero", 0.0)];
|
||||
for result in &mut results {
|
||||
let measurement = result.measurements.measurements[0].clone();
|
||||
result.measurements.measurements.push(measurement);
|
||||
}
|
||||
let entries = compute_relative_speeds(&results, &results[0], SortOrder::Command);
|
||||
assert_eq!(entries[1].relative_speed, f64::INFINITY);
|
||||
|
||||
let entries = compute_relative_speeds(&results, &results[1], SortOrder::Command);
|
||||
assert_eq!(entries[0].relative_speed, f64::INFINITY);
|
||||
assert_eq!(entries[0].relative_speed_stddev, None);
|
||||
assert_eq!(entries[1].relative_speed, 1.0);
|
||||
}
|
||||
+24
-130
@@ -1,14 +1,10 @@
|
||||
use super::benchmark_result::BenchmarkResult;
|
||||
use super::executor::{Executor, MockExecutor, RawExecutor, ShellExecutor};
|
||||
use super::{relative_speed, Benchmark};
|
||||
use colored::*;
|
||||
use std::cmp::Ordering;
|
||||
use std::io::{self, Write};
|
||||
use super::Benchmark;
|
||||
|
||||
use crate::command::{Command, Commands};
|
||||
use crate::command::Commands;
|
||||
use crate::export::ExportManager;
|
||||
use crate::options::{ExecutorKind, Options, OutputStyleOption, SortOrder};
|
||||
use crate::output::console_writeln;
|
||||
use crate::options::{ExecutorKind, Options};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -40,133 +36,23 @@ impl<'a> Scheduler<'a> {
|
||||
ExecutorKind::Shell(ref shell) => Box::new(ShellExecutor::new(shell, self.options)),
|
||||
};
|
||||
|
||||
let reference = self
|
||||
.options
|
||||
.reference_command
|
||||
.as_ref()
|
||||
.map(|cmd| Command::new(self.options.reference_name.as_deref(), cmd));
|
||||
|
||||
executor.calibrate()?;
|
||||
|
||||
for (number, cmd) in reference.iter().chain(self.commands.iter()).enumerate() {
|
||||
self.results
|
||||
.push(Benchmark::new(number, cmd, self.options, &*executor).run()?);
|
||||
for (number, cmd) in self.commands.iter().enumerate() {
|
||||
self.results.push(
|
||||
Benchmark::new(number, cmd, self.options, &*executor).run(self.results.first())?,
|
||||
);
|
||||
|
||||
// We export results after each individual benchmark, because
|
||||
// we would risk losing them if a later benchmark fails.
|
||||
self.export_manager
|
||||
.write_results(&self.results, true, self.options.reference_index)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn print_relative_speed_comparison(&self) -> Result<()> {
|
||||
if self.options.output_style == OutputStyleOption::Disabled {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if self.results.len() < 2 {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let reference = self
|
||||
.options
|
||||
.reference_index
|
||||
.map(|index| &self.results[index])
|
||||
.unwrap_or_else(|| relative_speed::fastest_of(&self.results));
|
||||
|
||||
if let Some(annotated_results) = relative_speed::compute_with_check_from_reference(
|
||||
&self.results,
|
||||
reference,
|
||||
self.options.sort_order_speed_comparison,
|
||||
) {
|
||||
let mut stdout = io::stdout().lock();
|
||||
match self.options.sort_order_speed_comparison {
|
||||
SortOrder::MeanTime => {
|
||||
console_writeln!(stdout, "{}", "Summary".bold())?;
|
||||
|
||||
let reference = annotated_results.iter().find(|r| r.is_reference).unwrap();
|
||||
let others = annotated_results.iter().filter(|r| !r.is_reference);
|
||||
|
||||
console_writeln!(stdout, " {} ran", reference.result.display_name.cyan())?;
|
||||
|
||||
for item in others {
|
||||
let stddev = if let Some(stddev) = item.relative_speed_stddev {
|
||||
format!(" ± {}", format!("{:.2}", stddev).green())
|
||||
} else {
|
||||
"".into()
|
||||
};
|
||||
let comparator = match item.relative_ordering {
|
||||
Ordering::Less => format!(
|
||||
"{}{} times slower than",
|
||||
format!("{:8.2}", item.relative_speed).bold().green(),
|
||||
stddev
|
||||
),
|
||||
Ordering::Greater => format!(
|
||||
"{}{} times faster than",
|
||||
format!("{:8.2}", item.relative_speed).bold().green(),
|
||||
stddev
|
||||
),
|
||||
Ordering::Equal => format!(
|
||||
" As fast ({}{}) as",
|
||||
format!("{:.2}", item.relative_speed).bold().green(),
|
||||
stddev
|
||||
),
|
||||
};
|
||||
console_writeln!(
|
||||
stdout,
|
||||
"{} {}",
|
||||
comparator,
|
||||
&item.result.display_name.magenta()
|
||||
)?;
|
||||
}
|
||||
}
|
||||
SortOrder::Command => {
|
||||
console_writeln!(stdout, "{}", "Relative speed comparison".bold())?;
|
||||
|
||||
for item in annotated_results {
|
||||
let relationship = if self.options.reference_index.is_some() {
|
||||
item.reference_label()
|
||||
} else {
|
||||
""
|
||||
};
|
||||
console_writeln!(
|
||||
stdout,
|
||||
" {}{} {}{}",
|
||||
format!("{:10.2}", item.relative_speed).bold().green(),
|
||||
if item.is_reference {
|
||||
" ".into()
|
||||
} else if let Some(stddev) = item.relative_speed_stddev {
|
||||
format!(" ± {}", format!("{stddev:5.2}").green())
|
||||
} else {
|
||||
" ".into()
|
||||
},
|
||||
item.result.display_name,
|
||||
relationship,
|
||||
)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
console_writeln!(
|
||||
io::stderr(),
|
||||
"{}: The benchmark comparison could not be computed as some benchmark times are zero. \
|
||||
This could be caused by background interference during the initial calibration phase \
|
||||
of hyperfine, in combination with very fast commands (faster than a few milliseconds). \
|
||||
Try to re-run the benchmark on a quiet system. If you did not do so already, try the \
|
||||
--shell=none/-N option. If it does not help either, you command is most likely too fast \
|
||||
to be accurately benchmarked by hyperfine.",
|
||||
"Note".bold().red()
|
||||
)?;
|
||||
self.export_manager.write_results(&self.results, true)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn final_export(&self) -> Result<()> {
|
||||
self.export_manager
|
||||
.write_results(&self.results, false, self.options.reference_index)
|
||||
self.export_manager.write_results(&self.results, false)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -183,11 +69,7 @@ fn generate_results(args: &[&'static str]) -> Result<Vec<BenchmarkResult>> {
|
||||
assert_eq!(options.executor_kind, ExecutorKind::Mock(None));
|
||||
|
||||
let commands = Commands::from_cli_arguments(&cli_arguments)?;
|
||||
let export_manager = ExportManager::from_cli_arguments(
|
||||
&cli_arguments,
|
||||
options.time_unit,
|
||||
options.sort_order_exports,
|
||||
)?;
|
||||
let export_manager = ExportManager::from_cli_arguments(&cli_arguments, options.metrics[0])?;
|
||||
|
||||
options.validate_against_command_list(&commands)?;
|
||||
|
||||
@@ -199,12 +81,15 @@ fn generate_results(args: &[&'static str]) -> Result<Vec<BenchmarkResult>> {
|
||||
|
||||
#[test]
|
||||
fn scheduler_basic() -> Result<()> {
|
||||
insta::assert_yaml_snapshot!(generate_results(&["--runs=2", "sleep 0.123", "sleep 0.456"])?, @r#"
|
||||
insta::assert_yaml_snapshot!(generate_results(&["--runs=2", "sleep 0.123", "sleep 0.456"])?, @"
|
||||
- command: sleep 0.123
|
||||
measurements:
|
||||
- time_wall_clock:
|
||||
value: 0.123
|
||||
unit: second
|
||||
time_cpu:
|
||||
value: 0
|
||||
unit: second
|
||||
time_user:
|
||||
value: 0
|
||||
unit: second
|
||||
@@ -218,6 +103,9 @@ fn scheduler_basic() -> Result<()> {
|
||||
- time_wall_clock:
|
||||
value: 0.123
|
||||
unit: second
|
||||
time_cpu:
|
||||
value: 0
|
||||
unit: second
|
||||
time_user:
|
||||
value: 0
|
||||
unit: second
|
||||
@@ -233,6 +121,9 @@ fn scheduler_basic() -> Result<()> {
|
||||
- time_wall_clock:
|
||||
value: 0.456
|
||||
unit: second
|
||||
time_cpu:
|
||||
value: 0
|
||||
unit: second
|
||||
time_user:
|
||||
value: 0
|
||||
unit: second
|
||||
@@ -246,6 +137,9 @@ fn scheduler_basic() -> Result<()> {
|
||||
- time_wall_clock:
|
||||
value: 0.456
|
||||
unit: second
|
||||
time_cpu:
|
||||
value: 0
|
||||
unit: second
|
||||
time_user:
|
||||
value: 0
|
||||
unit: second
|
||||
@@ -256,7 +150,7 @@ fn scheduler_basic() -> Result<()> {
|
||||
value: 0
|
||||
unit: byte
|
||||
exit_code: 0
|
||||
"#);
|
||||
");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
+49
-70
@@ -7,6 +7,12 @@ use clap::{
|
||||
crate_version, Arg, ArgAction, ArgMatches, Command, ValueHint,
|
||||
};
|
||||
|
||||
pub const DEFAULT_METRICS: &str = if cfg!(windows) {
|
||||
"time_wall_clock"
|
||||
} else {
|
||||
"time_wall_clock,memory_peak_resident"
|
||||
};
|
||||
|
||||
pub fn get_cli_arguments<'a, I, T>(args: I) -> ArgMatches
|
||||
where
|
||||
I: IntoIterator<Item = T>,
|
||||
@@ -38,12 +44,40 @@ fn build_command() -> Command {
|
||||
line like \"grep -i todo\" or a shell command like \"sleep 0.5 && echo test\". \
|
||||
The latter requires a shell, which can be enabled via '-S' or '--shell=...'. \
|
||||
If multiple commands are given, hyperfine will show a \
|
||||
comparison of the respective runtimes.")
|
||||
comparison with the first command as the reference.")
|
||||
.required(true)
|
||||
.action(ArgAction::Append)
|
||||
.value_hint(ValueHint::CommandString)
|
||||
.value_parser(NonEmptyStringValueParser::new()),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("metrics")
|
||||
.long("metrics")
|
||||
.value_name("PRESET|METRIC[:UNIT],…")
|
||||
.default_value("default")
|
||||
.hide_default_value(true)
|
||||
.help("Performance metrics to measure, in order.\n\n\
|
||||
This is either a preset name, or a comma-separated list of metrics with optional units. \
|
||||
The first metric is used for non-JSON exports, \
|
||||
JSON always includes all collected metrics.\n\n\
|
||||
Presets (use one on its own):\n \
|
||||
default Wall-clock time and peak RSS\n \
|
||||
time Wall-clock, CPU, user, and system time\n \
|
||||
all All available metrics\n\n\
|
||||
Presets use automatic units.\n\n\
|
||||
Metrics and optional units:\n \
|
||||
Time: time_wall_clock, time_cpu, time_user, time_system\n \
|
||||
Units: ns, us, ms, s, min, h\n \
|
||||
Memory: memory_peak_resident\n \
|
||||
Units: B, kB, MB, GB, TB, KiB, MiB, GiB, TiB\n \
|
||||
Counters: cpu_cycles, instructions, cache_references,\n \
|
||||
cache_misses, branch_misses\n \
|
||||
Units: count, k (thousand), M (million), B (billion)\n\n\
|
||||
Examples:\n \
|
||||
--metrics time_cpu,instructions\n \
|
||||
--metrics memory_peak_resident:MiB,time_wall_clock:ms\n \
|
||||
--metrics time"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("warmup")
|
||||
.long("warmup")
|
||||
@@ -93,34 +127,12 @@ fn build_command() -> Command {
|
||||
.value_name("CMD")
|
||||
.value_hint(ValueHint::CommandString)
|
||||
.help(
|
||||
"Execute CMD before each set of timing runs. This is useful for \
|
||||
"Execute CMD before each set of benchmark runs. This is useful for \
|
||||
compiling your software with the provided parameters, or to do any \
|
||||
other work that should happen once before a series of benchmark runs, \
|
||||
not every time as would happen with the --prepare option."
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("reference")
|
||||
.long("reference")
|
||||
.action(ArgAction::Set)
|
||||
.value_name("CMD")
|
||||
.help(
|
||||
"The reference for the relative comparison of results. Without parameters, \
|
||||
CMD is run as a separate reference command. With --parameter-scan or \
|
||||
--parameter-list, CMD must exactly match one unique benchmark name as \
|
||||
shown in the output. If this is unset, results are compared with the \
|
||||
fastest command as reference."
|
||||
)
|
||||
)
|
||||
.arg(
|
||||
Arg::new("reference-name")
|
||||
.long("reference-name")
|
||||
.action(ArgAction::Set)
|
||||
.value_name("CMD")
|
||||
.help("Give a meaningful name to the reference command. This cannot be used \
|
||||
with parameterized benchmarks; use --command-name instead.")
|
||||
.requires("reference")
|
||||
)
|
||||
.arg(
|
||||
Arg::new("prepare")
|
||||
.long("prepare")
|
||||
@@ -130,7 +142,7 @@ fn build_command() -> Command {
|
||||
.value_name("CMD")
|
||||
.value_hint(ValueHint::CommandString)
|
||||
.help(
|
||||
"Execute CMD before each timing run. This is useful for \
|
||||
"Execute CMD before each benchmark run. This is useful for \
|
||||
clearing disk caches, for example.\nThe --prepare option can \
|
||||
be specified once for all commands or multiple times, once for \
|
||||
each command. In the latter case, each preparation command will \
|
||||
@@ -146,7 +158,7 @@ fn build_command() -> Command {
|
||||
.value_name("CMD")
|
||||
.value_hint(ValueHint::CommandString)
|
||||
.help(
|
||||
"Execute CMD after each timing run. This is useful for killing \
|
||||
"Execute CMD after each benchmark run. This is useful for killing \
|
||||
long-running processes started (e.g. a web server started in --prepare), \
|
||||
for example.\nThe --conclude option can be specified once for all \
|
||||
commands or multiple times, once for each command. In the latter case, \
|
||||
@@ -275,47 +287,14 @@ fn build_command() -> Command {
|
||||
the output of the tool.",
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("sort")
|
||||
.long("sort")
|
||||
.action(ArgAction::Set)
|
||||
.value_name("METHOD")
|
||||
.value_parser(["auto", "command", "mean-time"])
|
||||
.default_value("auto")
|
||||
.hide_default_value(true)
|
||||
.help(
|
||||
"Specify the sort order of the speed comparison summary and the exported tables for \
|
||||
markup formats (Markdown, AsciiDoc, org-mode):\n \
|
||||
* 'auto' (default): the speed comparison will be ordered by time and\n \
|
||||
the markup tables will be ordered by command (input order).\n \
|
||||
* 'command': order benchmarks in the way they were specified\n \
|
||||
* 'mean-time': order benchmarks by mean runtime\n"
|
||||
),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("time-unit")
|
||||
.long("time-unit")
|
||||
.short('u')
|
||||
.action(ArgAction::Set)
|
||||
.value_name("UNIT")
|
||||
.value_parser(["µs", "us", "microsecond", "microseconds", "ms", "millisecond", "milliseconds", "s", "second", "seconds", "min", "minute", "minutes", "h", "hour", "hours"])
|
||||
.help("Set the time unit to be used. If the option is not given, the time unit is determined automatically. \
|
||||
This option affects the standard output as well as all export formats except for CSV and JSON.\n\
|
||||
Possible values:\n \
|
||||
* 'µs', 'us', 'microsecond', 'microseconds'\n \
|
||||
* 'ms', 'millisecond', 'milliseconds'\n \
|
||||
* 's', 'second', 'seconds'\n \
|
||||
* 'min', 'minute', 'minutes'\n \
|
||||
* 'h', 'hour', 'hours'"),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("export-asciidoc")
|
||||
.long("export-asciidoc")
|
||||
.action(ArgAction::Set)
|
||||
.value_name("FILE")
|
||||
.value_hint(ValueHint::FilePath)
|
||||
.help("Export the timing summary statistics as an AsciiDoc table to the given FILE. \
|
||||
The output time unit can be changed using the --time-unit option."),
|
||||
.help("Export the primary metric summary statistics as an AsciiDoc table to the given FILE. \
|
||||
The unit can be set with --metrics METRIC:UNIT."),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("export-csv")
|
||||
@@ -323,9 +302,9 @@ fn build_command() -> Command {
|
||||
.action(ArgAction::Set)
|
||||
.value_name("FILE")
|
||||
.value_hint(ValueHint::FilePath)
|
||||
.help("Export the timing summary statistics as CSV to the given FILE. If you need \
|
||||
the timing results for each individual run, use the JSON export format. \
|
||||
The output time unit is always seconds."),
|
||||
.help("Export the primary metric summary statistics as CSV to the given FILE. If you need \
|
||||
all metrics for each individual run, use the JSON export format. \
|
||||
Use the explicitly selected unit, or the base unit when unspecified."),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("export-json")
|
||||
@@ -333,8 +312,8 @@ fn build_command() -> Command {
|
||||
.action(ArgAction::Set)
|
||||
.value_name("FILE")
|
||||
.value_hint(ValueHint::FilePath)
|
||||
.help("Export the timing summary statistics and timings of individual runs as JSON to the given FILE. \
|
||||
The output time unit is always seconds"),
|
||||
.help("Export all collected metrics and individual runs as JSON to the given FILE. \
|
||||
Values always use base units (seconds, bytes, counts)."),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("export-markdown")
|
||||
@@ -342,8 +321,8 @@ fn build_command() -> Command {
|
||||
.action(ArgAction::Set)
|
||||
.value_name("FILE")
|
||||
.value_hint(ValueHint::FilePath)
|
||||
.help("Export the timing summary statistics as a Markdown table to the given FILE. \
|
||||
The output time unit can be changed using the --time-unit option."),
|
||||
.help("Export the primary metric summary statistics as a Markdown table to the given FILE. \
|
||||
The unit can be set with --metrics METRIC:UNIT."),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("export-orgmode")
|
||||
@@ -351,8 +330,8 @@ fn build_command() -> Command {
|
||||
.action(ArgAction::Set)
|
||||
.value_name("FILE")
|
||||
.value_hint(ValueHint::FilePath)
|
||||
.help("Export the timing summary statistics as an Emacs org-mode table to the given FILE. \
|
||||
The output time unit can be changed using the --time-unit option."),
|
||||
.help("Export the primary metric summary statistics as an Emacs org-mode table to the given FILE. \
|
||||
The unit can be set with --metrics METRIC:UNIT."),
|
||||
)
|
||||
.arg(
|
||||
Arg::new("show-output")
|
||||
|
||||
+2
-2
@@ -257,8 +257,8 @@ impl<'a> Commands<'a> {
|
||||
self.0.iter()
|
||||
}
|
||||
|
||||
pub fn num_commands(&self, has_reference_command: bool) -> usize {
|
||||
self.0.len() + if has_reference_command { 1 } else { 0 }
|
||||
pub fn num_commands(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
/// Finds all the strings that appear multiple times in the input iterator, returning them in
|
||||
|
||||
@@ -41,6 +41,8 @@ impl From<DecimalError> for ParameterScanError {
|
||||
|
||||
#[derive(Debug, Error)]
|
||||
pub enum OptionsError<'a> {
|
||||
#[error("{0}")]
|
||||
InvalidMetrics(String),
|
||||
#[error(
|
||||
"Conflicting requirements for the number of runs (empty range, min is larger than max)"
|
||||
)]
|
||||
|
||||
+11
-2
@@ -24,7 +24,16 @@ impl MarkupExporter for AsciidocExporter {
|
||||
}
|
||||
|
||||
fn table_row(&self, cells: &[&str]) -> String {
|
||||
format!("\n| {} \n", cells.join(" \n| "))
|
||||
let mut row = String::from("\n");
|
||||
for cell in cells {
|
||||
row.push('|');
|
||||
if !cell.is_empty() {
|
||||
row.push(' ');
|
||||
row.push_str(cell);
|
||||
}
|
||||
row.push('\n');
|
||||
}
|
||||
row
|
||||
}
|
||||
|
||||
fn table_divider(&self, _cell_aligmnents: &[Alignment]) -> String {
|
||||
@@ -43,7 +52,7 @@ fn test_asciidoc_exporter_table_data() {
|
||||
let data = vec!["a", "b", "c"];
|
||||
|
||||
let actual = exporter.table_row(&data);
|
||||
let expect = "\n| a \n| b \n| c \n";
|
||||
let expect = "\n| a\n| b\n| c\n";
|
||||
|
||||
assert_eq!(expect, actual);
|
||||
}
|
||||
|
||||
+81
-53
@@ -1,65 +1,73 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use csv::WriterBuilder;
|
||||
|
||||
use super::Exporter;
|
||||
use crate::benchmark::benchmark_result::BenchmarkResult;
|
||||
use crate::options::SortOrder;
|
||||
use crate::quantity::{FormatQuantity, TimeUnit};
|
||||
use crate::metric::MetricSelection;
|
||||
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct CsvExporter {}
|
||||
|
||||
impl Exporter for CsvExporter {
|
||||
fn serialize(
|
||||
&self,
|
||||
results: &[BenchmarkResult],
|
||||
_time_unit: Option<TimeUnit>,
|
||||
_sort_order: SortOrder,
|
||||
_reference_index: Option<usize>,
|
||||
) -> Result<Vec<u8>> {
|
||||
const CSV_UNIT: TimeUnit = TimeUnit::Second;
|
||||
const CSV_PRECISION: usize = 6;
|
||||
|
||||
fn serialize(&self, results: &[BenchmarkResult], primary: MetricSelection) -> Result<Vec<u8>> {
|
||||
// CSV never chooses units automatically: an omitted suffix means base units.
|
||||
let unit = primary.csv_unit();
|
||||
let mut writer = WriterBuilder::new().from_writer(vec![]);
|
||||
|
||||
{
|
||||
let mut headers: Vec<Cow<[u8]>> = [
|
||||
// The list of times and exit codes cannot be exported to the CSV file - omit them.
|
||||
"command", "mean", "stddev", "median", "user", "system", "min", "max",
|
||||
]
|
||||
.iter()
|
||||
.map(|x| Cow::Borrowed(x.as_bytes()))
|
||||
.collect();
|
||||
if let Some(res) = results.first() {
|
||||
for param_name in res.parameters.keys() {
|
||||
headers.push(Cow::Owned(format!("parameter_{param_name}").into_bytes()));
|
||||
}
|
||||
}
|
||||
writer.write_record(headers)?;
|
||||
let mut headers: Vec<String> = [
|
||||
"command", "metric", "unit", "mean", "stddev", "median", "min", "max",
|
||||
]
|
||||
.iter()
|
||||
.copied()
|
||||
.map(str::to_owned)
|
||||
.collect();
|
||||
if let Some(result) = results.first() {
|
||||
headers.extend(
|
||||
result
|
||||
.parameters
|
||||
.keys()
|
||||
.map(|name| format!("parameter_{name}")),
|
||||
);
|
||||
}
|
||||
writer.write_record(headers)?;
|
||||
|
||||
for res in results {
|
||||
let mut fields = vec![Cow::Borrowed(res.get_name().as_bytes())];
|
||||
for f in &[
|
||||
res.mean_wall_clock_time(),
|
||||
res.measurements.stddev().unwrap_or_default(),
|
||||
res.measurements.median(),
|
||||
res.measurements.time_user_mean(),
|
||||
res.measurements.time_system_mean(),
|
||||
res.measurements.min(),
|
||||
res.measurements.max(),
|
||||
] {
|
||||
fields.push(Cow::Owned(
|
||||
f.format_with_precision(CSV_UNIT, CSV_PRECISION)
|
||||
.into_bytes(),
|
||||
))
|
||||
}
|
||||
for v in res.parameters.values() {
|
||||
fields.push(Cow::Borrowed(v.value.as_bytes()))
|
||||
}
|
||||
for result in results {
|
||||
let stats = primary
|
||||
.metric
|
||||
.summarize(&result.measurements)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Metric '{}' is unavailable for '{}'",
|
||||
primary.metric.name(),
|
||||
result.get_name()
|
||||
)
|
||||
})?;
|
||||
let mut fields = vec![
|
||||
result.get_name().to_owned(),
|
||||
primary.metric.name().to_owned(),
|
||||
unit.symbol.to_owned(),
|
||||
];
|
||||
// Preserve numeric precision rather than using rounded display values.
|
||||
fields.extend(
|
||||
[
|
||||
Some(stats.mean),
|
||||
stats.stddev,
|
||||
Some(stats.median),
|
||||
Some(stats.min),
|
||||
Some(stats.max),
|
||||
]
|
||||
.map(|value| {
|
||||
value
|
||||
.map(|value| (value / unit.scale).to_string())
|
||||
.unwrap_or_default()
|
||||
}),
|
||||
);
|
||||
fields.extend(
|
||||
result
|
||||
.parameters
|
||||
.values()
|
||||
.map(|parameter| parameter.value.clone()),
|
||||
);
|
||||
writer.write_record(fields)?;
|
||||
}
|
||||
|
||||
@@ -86,6 +94,7 @@ fn test_csv() {
|
||||
measurements: Measurements::new(vec![
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(7.0),
|
||||
time_cpu: Time::new::<second>(7.0),
|
||||
time_user: Time::new::<second>(7.0),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -94,6 +103,7 @@ fn test_csv() {
|
||||
},
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(8.0),
|
||||
time_cpu: Time::new::<second>(8.0),
|
||||
time_user: Time::new::<second>(8.0),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -102,6 +112,7 @@ fn test_csv() {
|
||||
},
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(12.0),
|
||||
time_cpu: Time::new::<second>(12.0),
|
||||
time_user: Time::new::<second>(12.0),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -133,6 +144,7 @@ fn test_csv() {
|
||||
measurements: Measurements::new(vec![
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(17.0),
|
||||
time_cpu: Time::new::<second>(17.0),
|
||||
time_user: Time::new::<second>(17.0),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -141,6 +153,7 @@ fn test_csv() {
|
||||
},
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(18.0),
|
||||
time_cpu: Time::new::<second>(18.0),
|
||||
time_user: Time::new::<second>(18.0),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -149,6 +162,7 @@ fn test_csv() {
|
||||
},
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(19.0),
|
||||
time_cpu: Time::new::<second>(19.0),
|
||||
time_user: Time::new::<second>(19.0),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -177,14 +191,28 @@ fn test_csv() {
|
||||
|
||||
let actual = String::from_utf8(
|
||||
exporter
|
||||
.serialize(&results, Some(TimeUnit::Second), SortOrder::Command, None)
|
||||
.serialize(&results, MetricSelection::default())
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
insta::assert_snapshot!(actual, @r#"
|
||||
command,mean,stddev,median,user,system,min,max,parameter_bar,parameter_foo
|
||||
command_a,9.000000,2.645751,8.000000,9.000000,0.000000,7.000000,12.000000,two,one
|
||||
command_b,18.000000,1.000000,18.000000,18.000000,0.000000,17.000000,19.000000,seven,one
|
||||
command,metric,unit,mean,stddev,median,min,max,parameter_bar,parameter_foo
|
||||
command_a,time_wall_clock,s,9,2.6457513110645907,8,7,12,two,one
|
||||
command_b,time_wall_clock,s,18,1,18,17,19,seven,one
|
||||
"#);
|
||||
|
||||
for (selection, unit, mean) in [
|
||||
("memory_peak_resident", "B", "1024"),
|
||||
("memory_peak_resident:KiB", "KiB", "1"),
|
||||
] {
|
||||
let output = exporter
|
||||
.serialize(&results, MetricSelection::parse_list(selection).unwrap()[0])
|
||||
.unwrap();
|
||||
let mut reader = csv::Reader::from_reader(output.as_slice());
|
||||
let row = reader.records().next().unwrap().unwrap();
|
||||
assert_eq!(&row[1], "memory_peak_resident");
|
||||
assert_eq!(&row[2], unit);
|
||||
assert_eq!(&row[3], mean);
|
||||
}
|
||||
}
|
||||
|
||||
+33
-178
@@ -1,14 +1,11 @@
|
||||
use std::ops::{Add, AddAssign, Div};
|
||||
use std::collections::BTreeMap;
|
||||
|
||||
use serde::*;
|
||||
use serde::Serialize;
|
||||
use serde_json::to_vec_pretty;
|
||||
|
||||
use super::Exporter;
|
||||
use crate::benchmark::benchmark_result::BenchmarkResult;
|
||||
use crate::benchmark::measurement::Measurements;
|
||||
use crate::options::SortOrder;
|
||||
use crate::quantity::statistics::{max, mean, median, min, standard_deviation, UnsafeRawValue};
|
||||
use crate::quantity::{byte, ratio, second, QuantityInUnit, Ratio, TimeUnit, Zero};
|
||||
use crate::metric::{Metric, MetricSelection, Stats};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -21,6 +18,7 @@ const JSON_SCHEMA_VERSION: u32 = 2;
|
||||
struct HyperfineSummary<'a> {
|
||||
schema_version: u32,
|
||||
metadata: &'a Metadata,
|
||||
primary_metric: &'static str,
|
||||
results: Vec<JsonBenchmarkResult<'a>>,
|
||||
}
|
||||
|
||||
@@ -28,98 +26,7 @@ struct HyperfineSummary<'a> {
|
||||
struct JsonBenchmarkResult<'a> {
|
||||
#[serde(flatten)]
|
||||
result: &'a BenchmarkResult,
|
||||
#[serde(skip_serializing_if = "std::ops::Not::not")]
|
||||
is_reference: bool,
|
||||
summary: BenchmarkSummary,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
struct BenchmarkSummary {
|
||||
time_wall_clock: StatisticalSummary,
|
||||
time_user: StatisticalSummary,
|
||||
time_system: StatisticalSummary,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
memory_peak_resident: Option<StatisticalSummary>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
cpu_cycles: Option<StatisticalSummary>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
instructions: Option<StatisticalSummary>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
cache_references: Option<StatisticalSummary>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
cache_misses: Option<StatisticalSummary>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
branch_misses: Option<StatisticalSummary>,
|
||||
}
|
||||
|
||||
impl BenchmarkSummary {
|
||||
fn from_measurements(measurements: &Measurements) -> Self {
|
||||
let values = &measurements.measurements;
|
||||
Self {
|
||||
time_wall_clock: StatisticalSummary::from_values(
|
||||
values.iter().map(|m| Some(m.time_wall_clock)),
|
||||
Some(second),
|
||||
)
|
||||
.expect("Benchmarks have at least one measurement"),
|
||||
time_user: StatisticalSummary::from_values(
|
||||
values.iter().map(|m| Some(m.time_user)),
|
||||
Some(second),
|
||||
)
|
||||
.expect("Benchmarks have at least one measurement"),
|
||||
time_system: StatisticalSummary::from_values(
|
||||
values.iter().map(|m| Some(m.time_system)),
|
||||
Some(second),
|
||||
)
|
||||
.expect("Benchmarks have at least one measurement"),
|
||||
memory_peak_resident: StatisticalSummary::from_values(
|
||||
values
|
||||
.iter()
|
||||
.filter_map(|m| m.memory_peak_resident)
|
||||
.map(Some),
|
||||
Some(byte),
|
||||
),
|
||||
cpu_cycles: StatisticalSummary::from_values(
|
||||
values.iter().map(|m| {
|
||||
m.hardware_counters
|
||||
.cpu_cycles
|
||||
.map(|v| Ratio::new::<ratio>(v as f64))
|
||||
}),
|
||||
None::<ratio>,
|
||||
),
|
||||
instructions: StatisticalSummary::from_values(
|
||||
values.iter().map(|m| {
|
||||
m.hardware_counters
|
||||
.instructions
|
||||
.map(|v| Ratio::new::<ratio>(v as f64))
|
||||
}),
|
||||
None::<ratio>,
|
||||
),
|
||||
cache_references: StatisticalSummary::from_values(
|
||||
values.iter().map(|m| {
|
||||
m.hardware_counters
|
||||
.cache_references
|
||||
.map(|v| Ratio::new::<ratio>(v as f64))
|
||||
}),
|
||||
None::<ratio>,
|
||||
),
|
||||
cache_misses: StatisticalSummary::from_values(
|
||||
values.iter().map(|m| {
|
||||
m.hardware_counters
|
||||
.cache_misses
|
||||
.map(|v| Ratio::new::<ratio>(v as f64))
|
||||
}),
|
||||
None::<ratio>,
|
||||
),
|
||||
branch_misses: StatisticalSummary::from_values(
|
||||
values.iter().map(|m| {
|
||||
m.hardware_counters
|
||||
.branch_misses
|
||||
.map(|v| Ratio::new::<ratio>(v as f64))
|
||||
}),
|
||||
None::<ratio>,
|
||||
),
|
||||
}
|
||||
}
|
||||
summary: BTreeMap<&'static str, StatisticalSummary>,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -135,37 +42,20 @@ struct StatisticalSummary {
|
||||
}
|
||||
|
||||
impl StatisticalSummary {
|
||||
fn from_values<Q, P, U>(
|
||||
values: impl IntoIterator<Item = Option<Q>>,
|
||||
unit: Option<U>,
|
||||
) -> Option<Self>
|
||||
where
|
||||
Q: Copy
|
||||
+ PartialOrd
|
||||
+ Add<Output = Q>
|
||||
+ AddAssign
|
||||
+ Zero
|
||||
+ Div<Ratio, Output = P>
|
||||
+ UnsafeRawValue
|
||||
+ QuantityInUnit<U>,
|
||||
P: Into<Q>,
|
||||
U: uom::si::Unit,
|
||||
{
|
||||
// Only summarize a metric when it is available for every supplied measurement.
|
||||
let values: Vec<_> = values.into_iter().collect::<Option<_>>()?;
|
||||
if values.is_empty() {
|
||||
return None;
|
||||
fn new(metric: Metric, stats: Stats) -> Self {
|
||||
Self {
|
||||
unit: match metric.base_unit().symbol {
|
||||
"s" => Some("second"),
|
||||
"B" => Some("byte"),
|
||||
_ => None,
|
||||
},
|
||||
count: stats.count,
|
||||
mean: stats.mean,
|
||||
stddev: stats.stddev,
|
||||
median: stats.median,
|
||||
min: stats.min,
|
||||
max: stats.max,
|
||||
}
|
||||
Some(Self {
|
||||
unit: unit.map(|_| U::singular()),
|
||||
count: values.len(),
|
||||
mean: mean(values.iter().copied()).value_in_unit(),
|
||||
stddev: (values.len() > 1)
|
||||
.then(|| standard_deviation(values.iter().copied()).value_in_unit()),
|
||||
median: median(values.iter().copied()).value_in_unit(),
|
||||
min: min(values.iter().copied()).value_in_unit(),
|
||||
max: max(values.iter().copied()).value_in_unit(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,63 +65,28 @@ pub struct JsonExporter {
|
||||
}
|
||||
|
||||
impl Exporter for JsonExporter {
|
||||
fn serialize(
|
||||
&self,
|
||||
results: &[BenchmarkResult],
|
||||
_time_unit: Option<TimeUnit>,
|
||||
_sort_order: SortOrder,
|
||||
reference_index: Option<usize>,
|
||||
) -> Result<Vec<u8>> {
|
||||
fn serialize(&self, results: &[BenchmarkResult], primary: MetricSelection) -> Result<Vec<u8>> {
|
||||
let mut output = to_vec_pretty(&HyperfineSummary {
|
||||
schema_version: JSON_SCHEMA_VERSION,
|
||||
metadata: &self.metadata,
|
||||
primary_metric: primary.metric.name(),
|
||||
results: results
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, result)| JsonBenchmarkResult {
|
||||
.map(|result| JsonBenchmarkResult {
|
||||
result,
|
||||
is_reference: reference_index == Some(index),
|
||||
summary: BenchmarkSummary::from_measurements(&result.measurements),
|
||||
summary: Metric::ALL
|
||||
.iter()
|
||||
.copied()
|
||||
.filter_map(|metric| {
|
||||
metric.summarize(&result.measurements).map(|stats| {
|
||||
(metric.name(), StatisticalSummary::new(metric, stats))
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
})
|
||||
.collect(),
|
||||
});
|
||||
if let Ok(ref mut content) = output {
|
||||
content.push(b'\n');
|
||||
}
|
||||
|
||||
Ok(output?)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_statistical_summary_unit_conversion() {
|
||||
use crate::quantity::{kibibyte, millisecond, Information, Time};
|
||||
|
||||
let time = StatisticalSummary::from_values(
|
||||
[Time::new::<second>(1.0), Time::new::<second>(3.0)].map(Some),
|
||||
Some(millisecond),
|
||||
)
|
||||
.unwrap();
|
||||
let information = StatisticalSummary::from_values(
|
||||
[
|
||||
Information::new::<byte>(1024.0),
|
||||
Information::new::<byte>(3072.0),
|
||||
]
|
||||
.map(Some),
|
||||
Some(kibibyte),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
for (summary, unit, scale) in [
|
||||
(time, "millisecond", 1000.0),
|
||||
(information, "kibibyte", 1.0),
|
||||
] {
|
||||
assert_eq!(summary.unit, Some(unit));
|
||||
assert_eq!(summary.count, 2);
|
||||
approx::assert_relative_eq!(summary.mean, 2.0 * scale);
|
||||
approx::assert_relative_eq!(summary.median, 2.0 * scale);
|
||||
approx::assert_relative_eq!(summary.min, scale);
|
||||
approx::assert_relative_eq!(summary.max, 3.0 * scale);
|
||||
approx::assert_relative_eq!(summary.stddev.unwrap(), 2.0_f64.sqrt() * scale);
|
||||
})?;
|
||||
output.push(b'\n');
|
||||
Ok(output)
|
||||
}
|
||||
}
|
||||
|
||||
+61
-101
@@ -1,11 +1,8 @@
|
||||
use crate::benchmark::relative_speed::BenchmarkResultWithRelativeSpeed;
|
||||
use crate::benchmark::{benchmark_result::BenchmarkResult, relative_speed};
|
||||
use crate::options::SortOrder;
|
||||
use crate::quantity::{FormatQuantity, IsUnit, Time, TimeUnit, Zero};
|
||||
use crate::benchmark::benchmark_result::BenchmarkResult;
|
||||
use crate::metric::{MetricSelection, Stats, Unit};
|
||||
|
||||
use super::Exporter;
|
||||
use anyhow::Result;
|
||||
use std::cmp::Ordering;
|
||||
use anyhow::{Context, Result};
|
||||
|
||||
pub enum Alignment {
|
||||
Left,
|
||||
@@ -15,132 +12,95 @@ pub enum Alignment {
|
||||
pub trait MarkupExporter {
|
||||
fn table_results(
|
||||
&self,
|
||||
entries: &[BenchmarkResultWithRelativeSpeed],
|
||||
time_unit: TimeUnit,
|
||||
reference_pending: bool,
|
||||
explicit_reference: bool,
|
||||
results: &[BenchmarkResult],
|
||||
summaries: &[Stats],
|
||||
primary: MetricSelection,
|
||||
unit: Unit,
|
||||
) -> String {
|
||||
// prepare table header strings
|
||||
let notation = format!("[{}]", time_unit.short_name());
|
||||
|
||||
// prepare table cells alignment
|
||||
let notation = format!("[{}]", unit.symbol);
|
||||
let cells_alignment = [
|
||||
Alignment::Left,
|
||||
Alignment::Right,
|
||||
Alignment::Right,
|
||||
Alignment::Right,
|
||||
Alignment::Right,
|
||||
Alignment::Left,
|
||||
];
|
||||
|
||||
// emit table header format
|
||||
let mut table = self.table_header(&cells_alignment);
|
||||
|
||||
// emit table header data
|
||||
table.push_str(&self.table_row(&[
|
||||
"Command",
|
||||
&format!("Mean {notation}"),
|
||||
&format!("Min {notation}"),
|
||||
&format!("Max {notation}"),
|
||||
"Relative",
|
||||
&format!("Mean {} {notation}", primary.metric.label()),
|
||||
"Change",
|
||||
"Factor",
|
||||
]));
|
||||
|
||||
// emit horizontal line
|
||||
table.push_str(&self.table_divider(&cells_alignment));
|
||||
|
||||
for entry in entries {
|
||||
let result = &entry.result;
|
||||
// prepare data row strings
|
||||
let cmd_str = result.display_name.replace('|', "\\|");
|
||||
let mean_str = result.mean_wall_clock_time().format_value(time_unit);
|
||||
let stddev_str = if let Some(stddev) = result.measurements.stddev() {
|
||||
format!(" ± {}", stddev.format_value(time_unit))
|
||||
for (index, (result, stats)) in results.iter().zip(summaries).enumerate() {
|
||||
let command = result.display_name.replace('|', "\\|");
|
||||
let mean = unit.format_value(stats.mean);
|
||||
let stddev = stats
|
||||
.stddev
|
||||
.map(|value| format!(" ± {}", unit.format_value(value)))
|
||||
.unwrap_or_default();
|
||||
let change = if index == 0 {
|
||||
String::new()
|
||||
} else {
|
||||
"".into()
|
||||
stats.format_change_from(&summaries[0])
|
||||
};
|
||||
let min_str = result.measurements.min().format_value(time_unit);
|
||||
let max_str = result.measurements.max().format_value(time_unit);
|
||||
// The ratio of two zero times is undefined, even if they compare equal.
|
||||
let relative_unavailable = reference_pending
|
||||
|| (explicit_reference
|
||||
&& !entry.is_reference
|
||||
&& entry.relative_ordering == Ordering::Equal
|
||||
&& result.mean_wall_clock_time() == Time::zero());
|
||||
let relative = if relative_unavailable {
|
||||
"N/A".to_string()
|
||||
let factor = if index == 0 {
|
||||
String::new()
|
||||
} else {
|
||||
let stddev = match (entry.is_reference, entry.relative_speed_stddev) {
|
||||
(false, Some(stddev)) => format!(" ± {stddev:.2}"),
|
||||
_ => String::new(),
|
||||
};
|
||||
let label = if explicit_reference {
|
||||
entry.reference_label()
|
||||
} else {
|
||||
""
|
||||
};
|
||||
format!("{:.2}{stddev}{label}", entry.relative_speed)
|
||||
stats.format_factor_from(&summaries[0], primary.metric)
|
||||
};
|
||||
|
||||
// prepare table row entries
|
||||
table.push_str(&self.table_row(&[
|
||||
&self.command(&cmd_str),
|
||||
&format!("{mean_str}{stddev_str}"),
|
||||
&min_str,
|
||||
&max_str,
|
||||
&relative,
|
||||
]))
|
||||
&self.command(&command),
|
||||
&format!("{mean}{stddev}"),
|
||||
&change,
|
||||
&factor,
|
||||
]));
|
||||
}
|
||||
|
||||
// emit table footer format
|
||||
table.push_str(&self.table_footer(&cells_alignment));
|
||||
|
||||
table
|
||||
}
|
||||
|
||||
fn table_row(&self, cells: &[&str]) -> String;
|
||||
|
||||
fn table_divider(&self, cell_aligmnents: &[Alignment]) -> String;
|
||||
fn table_divider(&self, cell_alignments: &[Alignment]) -> String;
|
||||
|
||||
fn table_header(&self, _cell_aligmnents: &[Alignment]) -> String {
|
||||
"".to_string()
|
||||
fn table_header(&self, _cell_alignments: &[Alignment]) -> String {
|
||||
String::new()
|
||||
}
|
||||
|
||||
fn table_footer(&self, _cell_aligmnents: &[Alignment]) -> String {
|
||||
"".to_string()
|
||||
fn table_footer(&self, _cell_alignments: &[Alignment]) -> String {
|
||||
String::new()
|
||||
}
|
||||
|
||||
fn command(&self, size: &str) -> String;
|
||||
}
|
||||
|
||||
fn determine_unit_from_results(results: &[BenchmarkResult]) -> TimeUnit {
|
||||
if let Some(first_result) = results.first() {
|
||||
// Use the first BenchmarkResult entry to determine the unit for all entries.
|
||||
first_result.mean_wall_clock_time().suitable_unit()
|
||||
} else {
|
||||
// Default to `Second`.
|
||||
TimeUnit::Second
|
||||
}
|
||||
fn command(&self, command: &str) -> String;
|
||||
}
|
||||
|
||||
impl<T: MarkupExporter> Exporter for T {
|
||||
fn serialize(
|
||||
&self,
|
||||
results: &[BenchmarkResult],
|
||||
time_unit: Option<TimeUnit>,
|
||||
sort_order: SortOrder,
|
||||
reference_index: Option<usize>,
|
||||
) -> Result<Vec<u8>> {
|
||||
let unit = time_unit.unwrap_or_else(|| determine_unit_from_results(results));
|
||||
// Do not report ratios against another benchmark while the selected
|
||||
// reference is still pending in an intermediate export.
|
||||
let reference_pending = reference_index.is_some_and(|i| i >= results.len());
|
||||
let entries = relative_speed::compute(
|
||||
results,
|
||||
sort_order,
|
||||
reference_index.and_then(|i| results.get(i)),
|
||||
);
|
||||
|
||||
let table =
|
||||
self.table_results(&entries, unit, reference_pending, reference_index.is_some());
|
||||
Ok(table.as_bytes().to_vec())
|
||||
fn serialize(&self, results: &[BenchmarkResult], primary: MetricSelection) -> Result<Vec<u8>> {
|
||||
let stats: Vec<_> = results
|
||||
.iter()
|
||||
.map(|result| {
|
||||
primary
|
||||
.metric
|
||||
.summarize(&result.measurements)
|
||||
.with_context(|| {
|
||||
format!(
|
||||
"Metric '{}' is unavailable for '{}'",
|
||||
primary.metric.name(),
|
||||
result.get_name()
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect::<Result<_>>()?;
|
||||
// Use one common unit, avoiding an unscaled column if the reference is zero.
|
||||
let mean = stats
|
||||
.iter()
|
||||
.find(|stats| stats.mean != 0.0)
|
||||
.map_or(0.0, |stats| stats.mean);
|
||||
let unit = primary.display_unit(mean);
|
||||
Ok(self
|
||||
.table_results(results, &stats, primary, unit)
|
||||
.into_bytes())
|
||||
}
|
||||
}
|
||||
|
||||
+7
-30
@@ -17,9 +17,8 @@ use self::markdown::MarkdownExporter;
|
||||
use self::orgmode::OrgmodeExporter;
|
||||
|
||||
use crate::benchmark::benchmark_result::BenchmarkResult;
|
||||
use crate::options::SortOrder;
|
||||
use crate::metric::MetricSelection;
|
||||
use crate::output::console_writeln;
|
||||
use crate::quantity::TimeUnit;
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use clap::ArgMatches;
|
||||
@@ -46,13 +45,7 @@ pub enum ExportType {
|
||||
/// Interface for different exporters.
|
||||
trait Exporter {
|
||||
/// Export the given entries in the serialized form.
|
||||
fn serialize(
|
||||
&self,
|
||||
results: &[BenchmarkResult],
|
||||
time_unit: Option<TimeUnit>,
|
||||
sort_order: SortOrder,
|
||||
reference_index: Option<usize>,
|
||||
) -> Result<Vec<u8>>;
|
||||
fn serialize(&self, results: &[BenchmarkResult], primary: MetricSelection) -> Result<Vec<u8>>;
|
||||
}
|
||||
|
||||
pub enum ExportTarget {
|
||||
@@ -68,22 +61,16 @@ struct ExporterWithTarget {
|
||||
/// Handles the management of multiple file exporters.
|
||||
pub struct ExportManager {
|
||||
exporters: Vec<ExporterWithTarget>,
|
||||
time_unit: Option<TimeUnit>,
|
||||
sort_order: SortOrder,
|
||||
primary: MetricSelection,
|
||||
}
|
||||
|
||||
impl ExportManager {
|
||||
/// Build the ExportManager that will export the results specified
|
||||
/// in the given ArgMatches
|
||||
pub fn from_cli_arguments(
|
||||
matches: &ArgMatches,
|
||||
time_unit: Option<TimeUnit>,
|
||||
sort_order: SortOrder,
|
||||
) -> Result<Self> {
|
||||
pub fn from_cli_arguments(matches: &ArgMatches, primary: MetricSelection) -> Result<Self> {
|
||||
let mut export_manager = Self {
|
||||
exporters: vec![],
|
||||
time_unit,
|
||||
sort_order,
|
||||
primary,
|
||||
};
|
||||
{
|
||||
let mut add_exporter = |flag, exporttype| -> Result<()> {
|
||||
@@ -131,19 +118,9 @@ impl ExportManager {
|
||||
/// results are written to all file targets (to always have them up to date, even
|
||||
/// if a benchmark fails). In the latter case, we only print to stdout targets (in
|
||||
/// order not to clutter the output of hyperfine with intermediate results).
|
||||
/// `reference_index` refers to the complete benchmark sequence, so it can
|
||||
/// point past `results` while that benchmark is still pending.
|
||||
pub fn write_results(
|
||||
&self,
|
||||
results: &[BenchmarkResult],
|
||||
intermediate: bool,
|
||||
reference_index: Option<usize>,
|
||||
) -> Result<()> {
|
||||
pub fn write_results(&self, results: &[BenchmarkResult], intermediate: bool) -> Result<()> {
|
||||
for e in &self.exporters {
|
||||
let content = || {
|
||||
e.exporter
|
||||
.serialize(results, self.time_unit, self.sort_order, reference_index)
|
||||
};
|
||||
let content = || e.exporter.serialize(results, self.primary);
|
||||
|
||||
match e.target {
|
||||
ExportTarget::File(ref filename) => {
|
||||
|
||||
+138
-125
@@ -2,19 +2,27 @@ use super::Exporter;
|
||||
use crate::benchmark::benchmark_result::BenchmarkResult;
|
||||
use crate::benchmark::measurement::{Measurement, Measurements};
|
||||
use crate::export::asciidoc::AsciidocExporter;
|
||||
use crate::export::markdown::MarkdownExporter;
|
||||
use crate::export::orgmode::OrgmodeExporter;
|
||||
use crate::quantity::{byte, second, Information, Time, TimeUnit, Zero};
|
||||
use crate::{export::markdown::MarkdownExporter, options::SortOrder};
|
||||
use crate::metric::{MetricSelection, Unit};
|
||||
use crate::quantity::{byte, second, Information, Time, Zero};
|
||||
use std::collections::BTreeMap;
|
||||
use std::process::ExitStatus;
|
||||
|
||||
fn get_output<E: Exporter + Default>(
|
||||
results: &[BenchmarkResult],
|
||||
unit: Option<TimeUnit>,
|
||||
sort_order: SortOrder,
|
||||
) -> String {
|
||||
fn get_output<E: Exporter + Default>(results: &[BenchmarkResult], unit: Option<Unit>) -> String {
|
||||
let exporter = E::default();
|
||||
String::from_utf8(exporter.serialize(results, unit, sort_order, None).unwrap()).unwrap()
|
||||
String::from_utf8(
|
||||
exporter
|
||||
.serialize(
|
||||
results,
|
||||
MetricSelection {
|
||||
unit,
|
||||
..MetricSelection::default()
|
||||
},
|
||||
)
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// Ensure the makrup output includes the table header and the multiple
|
||||
@@ -33,6 +41,7 @@ fn test_markup_export_auto_ms() {
|
||||
measurements: Measurements::new(vec![
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(0.09),
|
||||
time_cpu: Time::new::<second>(0.09),
|
||||
time_user: Time::new::<second>(0.09),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -41,6 +50,7 @@ fn test_markup_export_auto_ms() {
|
||||
},
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(0.10),
|
||||
time_cpu: Time::new::<second>(0.10),
|
||||
time_user: Time::new::<second>(0.10),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -49,6 +59,7 @@ fn test_markup_export_auto_ms() {
|
||||
},
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(0.14),
|
||||
time_cpu: Time::new::<second>(0.14),
|
||||
time_user: Time::new::<second>(0.14),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -65,6 +76,7 @@ fn test_markup_export_auto_ms() {
|
||||
measurements: Measurements::new(vec![
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(2.0),
|
||||
time_cpu: Time::new::<second>(2.0),
|
||||
time_user: Time::new::<second>(2.0),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -73,6 +85,7 @@ fn test_markup_export_auto_ms() {
|
||||
},
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(3.0),
|
||||
time_cpu: Time::new::<second>(3.0),
|
||||
time_user: Time::new::<second>(3.0),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -81,6 +94,7 @@ fn test_markup_export_auto_ms() {
|
||||
},
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(4.0),
|
||||
time_cpu: Time::new::<second>(4.0),
|
||||
time_user: Time::new::<second>(4.0),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -92,42 +106,39 @@ fn test_markup_export_auto_ms() {
|
||||
},
|
||||
];
|
||||
|
||||
insta::assert_snapshot!(get_output::<MarkdownExporter>(&results, None, SortOrder::Command), @r#"
|
||||
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|
||||
|:---|---:|---:|---:|---:|
|
||||
| `sleep 0.1` | 110.0 ± 26.5 | 90.0 | 140.0 | 1.00 |
|
||||
| `sleep 2` | 3000.0 ± 1000.0 | 2000.0 | 4000.0 | 27.27 ± 11.21 |
|
||||
"#);
|
||||
insta::assert_snapshot!(get_output::<MarkdownExporter>(&results, None), @"
|
||||
| Command | Mean Wall Time [ms] | Change | Factor |
|
||||
|:---|---:|---:|:---|
|
||||
| `sleep 0.1` | 110.0 ± 26.5 | | |
|
||||
| `sleep 2` | 3000.0 ± 1000.0 | +2627.3% | (27.3x slower) |
|
||||
");
|
||||
|
||||
insta::assert_snapshot!(get_output::<AsciidocExporter>(&results, None, SortOrder::Command), @r#"
|
||||
[cols="<,>,>,>,>"]
|
||||
insta::assert_snapshot!(get_output::<AsciidocExporter>(&results, None), @r#"
|
||||
[cols="<,>,>,<"]
|
||||
|===
|
||||
| Command
|
||||
| Mean [ms]
|
||||
| Min [ms]
|
||||
| Max [ms]
|
||||
| Relative
|
||||
| Command
|
||||
| Mean Wall Time [ms]
|
||||
| Change
|
||||
| Factor
|
||||
|
||||
| `sleep 0.1`
|
||||
| 110.0 ± 26.5
|
||||
| 90.0
|
||||
| 140.0
|
||||
| 1.00
|
||||
| `sleep 0.1`
|
||||
| 110.0 ± 26.5
|
||||
|
|
||||
|
|
||||
|
||||
| `sleep 2`
|
||||
| 3000.0 ± 1000.0
|
||||
| 2000.0
|
||||
| 4000.0
|
||||
| 27.27 ± 11.21
|
||||
| `sleep 2`
|
||||
| 3000.0 ± 1000.0
|
||||
| +2627.3%
|
||||
| (27.3x slower)
|
||||
|===
|
||||
"#);
|
||||
|
||||
insta::assert_snapshot!(get_output::<OrgmodeExporter>(&results, None, SortOrder::Command), @r#"
|
||||
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|
||||
|--+--+--+--+--|
|
||||
| =sleep 0.1= | 110.0 ± 26.5 | 90.0 | 140.0 | 1.00 |
|
||||
| =sleep 2= | 3000.0 ± 1000.0 | 2000.0 | 4000.0 | 27.27 ± 11.21 |
|
||||
"#);
|
||||
insta::assert_snapshot!(get_output::<OrgmodeExporter>(&results, None), @"
|
||||
| Command | Mean Wall Time [ms] | Change | Factor |
|
||||
|--+--+--+--|
|
||||
| =sleep 0.1= | 110.0 ± 26.5 | | |
|
||||
| =sleep 2= | 3000.0 ± 1000.0 | +2627.3% | (27.3x slower) |
|
||||
");
|
||||
}
|
||||
|
||||
/// This (again) demonstrates that the first entry's units (s) are used to set
|
||||
@@ -142,6 +153,7 @@ fn test_markup_export_auto_s() {
|
||||
measurements: Measurements::new(vec![
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(2.1),
|
||||
time_cpu: Time::new::<second>(2.1),
|
||||
time_user: Time::new::<second>(2.1),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -150,6 +162,7 @@ fn test_markup_export_auto_s() {
|
||||
},
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(2.2),
|
||||
time_cpu: Time::new::<second>(2.2),
|
||||
time_user: Time::new::<second>(2.2),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -158,6 +171,7 @@ fn test_markup_export_auto_s() {
|
||||
},
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(2.3),
|
||||
time_cpu: Time::new::<second>(2.3),
|
||||
time_user: Time::new::<second>(2.3),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -174,6 +188,7 @@ fn test_markup_export_auto_s() {
|
||||
measurements: Measurements::new(vec![
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(0.1),
|
||||
time_cpu: Time::new::<second>(0.1),
|
||||
time_user: Time::new::<second>(0.1),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -182,6 +197,7 @@ fn test_markup_export_auto_s() {
|
||||
},
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(0.2),
|
||||
time_cpu: Time::new::<second>(0.2),
|
||||
time_user: Time::new::<second>(0.2),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -190,6 +206,7 @@ fn test_markup_export_auto_s() {
|
||||
},
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(0.3),
|
||||
time_cpu: Time::new::<second>(0.3),
|
||||
time_user: Time::new::<second>(0.3),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -201,42 +218,39 @@ fn test_markup_export_auto_s() {
|
||||
},
|
||||
];
|
||||
|
||||
insta::assert_snapshot!(get_output::<MarkdownExporter>(&results, None, SortOrder::Command), @r#"
|
||||
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|
||||
|:---|---:|---:|---:|---:|
|
||||
| `sleep 2` | 2.200 ± 0.100 | 2.100 | 2.300 | 11.00 ± 5.52 |
|
||||
| `sleep 0.1` | 0.200 ± 0.100 | 0.100 | 0.300 | 1.00 |
|
||||
"#);
|
||||
insta::assert_snapshot!(get_output::<MarkdownExporter>(&results, None), @"
|
||||
| Command | Mean Wall Time [s] | Change | Factor |
|
||||
|:---|---:|---:|:---|
|
||||
| `sleep 2` | 2.200 ± 0.100 | | |
|
||||
| `sleep 0.1` | 0.200 ± 0.100 | -90.9% | (11.0x faster) |
|
||||
");
|
||||
|
||||
insta::assert_snapshot!(get_output::<AsciidocExporter>(&results, None, SortOrder::Command), @r#"
|
||||
[cols="<,>,>,>,>"]
|
||||
insta::assert_snapshot!(get_output::<AsciidocExporter>(&results, None), @r#"
|
||||
[cols="<,>,>,<"]
|
||||
|===
|
||||
| Command
|
||||
| Mean [s]
|
||||
| Min [s]
|
||||
| Max [s]
|
||||
| Relative
|
||||
| Command
|
||||
| Mean Wall Time [s]
|
||||
| Change
|
||||
| Factor
|
||||
|
||||
| `sleep 2`
|
||||
| 2.200 ± 0.100
|
||||
| 2.100
|
||||
| 2.300
|
||||
| 11.00 ± 5.52
|
||||
| `sleep 2`
|
||||
| 2.200 ± 0.100
|
||||
|
|
||||
|
|
||||
|
||||
| `sleep 0.1`
|
||||
| 0.200 ± 0.100
|
||||
| 0.100
|
||||
| 0.300
|
||||
| 1.00
|
||||
| `sleep 0.1`
|
||||
| 0.200 ± 0.100
|
||||
| -90.9%
|
||||
| (11.0x faster)
|
||||
|===
|
||||
"#);
|
||||
|
||||
insta::assert_snapshot!(get_output::<OrgmodeExporter>(&results, None, SortOrder::Command), @r#"
|
||||
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|
||||
|--+--+--+--+--|
|
||||
| =sleep 2= | 2.200 ± 0.100 | 2.100 | 2.300 | 11.00 ± 5.52 |
|
||||
| =sleep 0.1= | 0.200 ± 0.100 | 0.100 | 0.300 | 1.00 |
|
||||
"#);
|
||||
insta::assert_snapshot!(get_output::<OrgmodeExporter>(&results, None), @"
|
||||
| Command | Mean Wall Time [s] | Change | Factor |
|
||||
|--+--+--+--|
|
||||
| =sleep 2= | 2.200 ± 0.100 | | |
|
||||
| =sleep 0.1= | 0.200 ± 0.100 | -90.9% | (11.0x faster) |
|
||||
");
|
||||
}
|
||||
|
||||
/// This (again) demonstrates that the given time unit (ms) is used to set
|
||||
@@ -251,6 +265,7 @@ fn test_markup_export_manual_ms() {
|
||||
measurements: Measurements::new(vec![
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(2.1),
|
||||
time_cpu: Time::new::<second>(2.1),
|
||||
time_user: Time::new::<second>(2.1),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -259,6 +274,7 @@ fn test_markup_export_manual_ms() {
|
||||
},
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(2.2),
|
||||
time_cpu: Time::new::<second>(2.2),
|
||||
time_user: Time::new::<second>(2.2),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -267,6 +283,7 @@ fn test_markup_export_manual_ms() {
|
||||
},
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(2.3),
|
||||
time_cpu: Time::new::<second>(2.3),
|
||||
time_user: Time::new::<second>(2.3),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -283,6 +300,7 @@ fn test_markup_export_manual_ms() {
|
||||
measurements: Measurements::new(vec![
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(0.1),
|
||||
time_cpu: Time::new::<second>(0.1),
|
||||
time_user: Time::new::<second>(0.1),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -291,6 +309,7 @@ fn test_markup_export_manual_ms() {
|
||||
},
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(0.2),
|
||||
time_cpu: Time::new::<second>(0.2),
|
||||
time_user: Time::new::<second>(0.2),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -299,6 +318,7 @@ fn test_markup_export_manual_ms() {
|
||||
},
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(0.3),
|
||||
time_cpu: Time::new::<second>(0.3),
|
||||
time_user: Time::new::<second>(0.3),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -310,42 +330,39 @@ fn test_markup_export_manual_ms() {
|
||||
},
|
||||
];
|
||||
|
||||
insta::assert_snapshot!(get_output::<MarkdownExporter>(&timing_results, Some(TimeUnit::MilliSecond), SortOrder::Command), @r#"
|
||||
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|
||||
|:---|---:|---:|---:|---:|
|
||||
| `sleep 2` | 2200.0 ± 100.0 | 2100.0 | 2300.0 | 11.00 ± 5.52 |
|
||||
| `sleep 0.1` | 200.0 ± 100.0 | 100.0 | 300.0 | 1.00 |
|
||||
"#);
|
||||
insta::assert_snapshot!(get_output::<MarkdownExporter>(&timing_results, Some(Unit { symbol: "ms", scale: 0.001 })), @"
|
||||
| Command | Mean Wall Time [ms] | Change | Factor |
|
||||
|:---|---:|---:|:---|
|
||||
| `sleep 2` | 2200.0 ± 100.0 | | |
|
||||
| `sleep 0.1` | 200.0 ± 100.0 | -90.9% | (11.0x faster) |
|
||||
");
|
||||
|
||||
insta::assert_snapshot!(get_output::<AsciidocExporter>(&timing_results, Some(TimeUnit::MilliSecond), SortOrder::Command), @r#"
|
||||
[cols="<,>,>,>,>"]
|
||||
insta::assert_snapshot!(get_output::<AsciidocExporter>(&timing_results, Some(Unit { symbol: "ms", scale: 0.001 })), @r#"
|
||||
[cols="<,>,>,<"]
|
||||
|===
|
||||
| Command
|
||||
| Mean [ms]
|
||||
| Min [ms]
|
||||
| Max [ms]
|
||||
| Relative
|
||||
| Command
|
||||
| Mean Wall Time [ms]
|
||||
| Change
|
||||
| Factor
|
||||
|
||||
| `sleep 2`
|
||||
| 2200.0 ± 100.0
|
||||
| 2100.0
|
||||
| 2300.0
|
||||
| 11.00 ± 5.52
|
||||
| `sleep 2`
|
||||
| 2200.0 ± 100.0
|
||||
|
|
||||
|
|
||||
|
||||
| `sleep 0.1`
|
||||
| 200.0 ± 100.0
|
||||
| 100.0
|
||||
| 300.0
|
||||
| 1.00
|
||||
| `sleep 0.1`
|
||||
| 200.0 ± 100.0
|
||||
| -90.9%
|
||||
| (11.0x faster)
|
||||
|===
|
||||
"#);
|
||||
|
||||
insta::assert_snapshot!(get_output::<OrgmodeExporter>(&timing_results, Some(TimeUnit::MilliSecond), SortOrder::Command), @r#"
|
||||
| Command | Mean [ms] | Min [ms] | Max [ms] | Relative |
|
||||
|--+--+--+--+--|
|
||||
| =sleep 2= | 2200.0 ± 100.0 | 2100.0 | 2300.0 | 11.00 ± 5.52 |
|
||||
| =sleep 0.1= | 200.0 ± 100.0 | 100.0 | 300.0 | 1.00 |
|
||||
"#);
|
||||
insta::assert_snapshot!(get_output::<OrgmodeExporter>(&timing_results, Some(Unit { symbol: "ms", scale: 0.001 })), @"
|
||||
| Command | Mean Wall Time [ms] | Change | Factor |
|
||||
|--+--+--+--|
|
||||
| =sleep 2= | 2200.0 ± 100.0 | | |
|
||||
| =sleep 0.1= | 200.0 ± 100.0 | -90.9% | (11.0x faster) |
|
||||
");
|
||||
}
|
||||
|
||||
/// The given time unit (s) is used to set the units for all entries.
|
||||
@@ -359,6 +376,7 @@ fn test_markup_export_manual_s() {
|
||||
measurements: Measurements::new(vec![
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(2.01),
|
||||
time_cpu: Time::new::<second>(2.01),
|
||||
time_user: Time::new::<second>(2.01),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -367,6 +385,7 @@ fn test_markup_export_manual_s() {
|
||||
},
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(2.02),
|
||||
time_cpu: Time::new::<second>(2.02),
|
||||
time_user: Time::new::<second>(2.02),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -375,6 +394,7 @@ fn test_markup_export_manual_s() {
|
||||
},
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(2.03),
|
||||
time_cpu: Time::new::<second>(2.03),
|
||||
time_user: Time::new::<second>(2.03),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -391,6 +411,7 @@ fn test_markup_export_manual_s() {
|
||||
measurements: Measurements::new(vec![
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(0.11),
|
||||
time_cpu: Time::new::<second>(0.11),
|
||||
time_user: Time::new::<second>(0.11),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -399,6 +420,7 @@ fn test_markup_export_manual_s() {
|
||||
},
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(0.12),
|
||||
time_cpu: Time::new::<second>(0.12),
|
||||
time_user: Time::new::<second>(0.12),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -407,6 +429,7 @@ fn test_markup_export_manual_s() {
|
||||
},
|
||||
Measurement {
|
||||
time_wall_clock: Time::new::<second>(0.13),
|
||||
time_cpu: Time::new::<second>(0.13),
|
||||
time_user: Time::new::<second>(0.13),
|
||||
time_system: Time::zero(),
|
||||
memory_peak_resident: Some(Information::new::<byte>(1024.)),
|
||||
@@ -418,40 +441,30 @@ fn test_markup_export_manual_s() {
|
||||
},
|
||||
];
|
||||
|
||||
insta::assert_snapshot!(get_output::<MarkdownExporter>(&results, Some(TimeUnit::Second), SortOrder::Command), @r#"
|
||||
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|
||||
|:---|---:|---:|---:|---:|
|
||||
| `sleep 2` | 2.020 ± 0.010 | 2.010 | 2.030 | 16.83 ± 1.41 |
|
||||
| `sleep 0.1` | 0.120 ± 0.010 | 0.110 | 0.130 | 1.00 |
|
||||
"#);
|
||||
insta::assert_snapshot!(get_output::<MarkdownExporter>(&results, Some(Unit { symbol: "s", scale: 1.0 })), @"
|
||||
| Command | Mean Wall Time [s] | Change | Factor |
|
||||
|:---|---:|---:|:---|
|
||||
| `sleep 2` | 2.020 ± 0.010 | | |
|
||||
| `sleep 0.1` | 0.120 ± 0.010 | -94.1% | (16.8x faster) |
|
||||
");
|
||||
|
||||
insta::assert_snapshot!(get_output::<MarkdownExporter>(&results, Some(TimeUnit::Second), SortOrder::MeanTime), @r#"
|
||||
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|
||||
|:---|---:|---:|---:|---:|
|
||||
| `sleep 0.1` | 0.120 ± 0.010 | 0.110 | 0.130 | 1.00 |
|
||||
| `sleep 2` | 2.020 ± 0.010 | 2.010 | 2.030 | 16.83 ± 1.41 |
|
||||
"#);
|
||||
|
||||
insta::assert_snapshot!(get_output::<AsciidocExporter>(&results, Some(TimeUnit::Second), SortOrder::Command), @r#"
|
||||
[cols="<,>,>,>,>"]
|
||||
insta::assert_snapshot!(get_output::<AsciidocExporter>(&results, Some(Unit { symbol: "s", scale: 1.0 })), @r#"
|
||||
[cols="<,>,>,<"]
|
||||
|===
|
||||
| Command
|
||||
| Mean [s]
|
||||
| Min [s]
|
||||
| Max [s]
|
||||
| Relative
|
||||
| Command
|
||||
| Mean Wall Time [s]
|
||||
| Change
|
||||
| Factor
|
||||
|
||||
| `sleep 2`
|
||||
| 2.020 ± 0.010
|
||||
| 2.010
|
||||
| 2.030
|
||||
| 16.83 ± 1.41
|
||||
| `sleep 2`
|
||||
| 2.020 ± 0.010
|
||||
|
|
||||
|
|
||||
|
||||
| `sleep 0.1`
|
||||
| 0.120 ± 0.010
|
||||
| 0.110
|
||||
| 0.130
|
||||
| 1.00
|
||||
| `sleep 0.1`
|
||||
| 0.120 ± 0.010
|
||||
| -94.1%
|
||||
| (16.8x faster)
|
||||
|===
|
||||
"#);
|
||||
}
|
||||
|
||||
+2
-6
@@ -21,6 +21,7 @@ pub mod cli;
|
||||
pub mod command;
|
||||
pub mod error;
|
||||
pub mod export;
|
||||
pub mod metric;
|
||||
pub mod options;
|
||||
pub mod outlier_detection;
|
||||
pub mod output;
|
||||
@@ -39,15 +40,10 @@ fn run() -> Result<()> {
|
||||
let commands = Commands::from_cli_arguments(&cli_arguments)?;
|
||||
options.validate_against_command_list(&commands)?;
|
||||
|
||||
let export_manager = ExportManager::from_cli_arguments(
|
||||
&cli_arguments,
|
||||
options.time_unit,
|
||||
options.sort_order_exports,
|
||||
)?;
|
||||
let export_manager = ExportManager::from_cli_arguments(&cli_arguments, options.metrics[0])?;
|
||||
|
||||
let mut scheduler = Scheduler::new(&commands, &options, &export_manager);
|
||||
scheduler.run_benchmarks()?;
|
||||
scheduler.print_relative_speed_comparison()?;
|
||||
scheduler.final_export()?;
|
||||
|
||||
Ok(())
|
||||
|
||||
+505
@@ -0,0 +1,505 @@
|
||||
//! Metric selection, descriptive statistics, and display units.
|
||||
|
||||
use anyhow::{bail, ensure, Result};
|
||||
|
||||
use crate::benchmark::measurement::{Measurement, Measurements};
|
||||
use crate::quantity::statistics::{max, mean, median, min, standard_deviation};
|
||||
use crate::quantity::{byte, ratio, second, Ratio};
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Metric {
|
||||
TimeWallClock,
|
||||
TimeCpu,
|
||||
TimeUser,
|
||||
TimeSystem,
|
||||
MemoryPeakResident,
|
||||
CpuCycles,
|
||||
Instructions,
|
||||
CacheReferences,
|
||||
CacheMisses,
|
||||
BranchMisses,
|
||||
}
|
||||
|
||||
impl Metric {
|
||||
pub const ALL: [Self; 10] = [
|
||||
Self::TimeWallClock,
|
||||
Self::TimeCpu,
|
||||
Self::TimeUser,
|
||||
Self::TimeSystem,
|
||||
Self::MemoryPeakResident,
|
||||
Self::CpuCycles,
|
||||
Self::Instructions,
|
||||
Self::CacheReferences,
|
||||
Self::CacheMisses,
|
||||
Self::BranchMisses,
|
||||
];
|
||||
|
||||
pub fn name(self) -> &'static str {
|
||||
match self {
|
||||
Self::TimeWallClock => "time_wall_clock",
|
||||
Self::TimeCpu => "time_cpu",
|
||||
Self::TimeUser => "time_user",
|
||||
Self::TimeSystem => "time_system",
|
||||
Self::MemoryPeakResident => "memory_peak_resident",
|
||||
Self::CpuCycles => "cpu_cycles",
|
||||
Self::Instructions => "instructions",
|
||||
Self::CacheReferences => "cache_references",
|
||||
Self::CacheMisses => "cache_misses",
|
||||
Self::BranchMisses => "branch_misses",
|
||||
}
|
||||
}
|
||||
|
||||
pub fn label(self) -> &'static str {
|
||||
match self {
|
||||
Self::TimeWallClock => "Wall Time",
|
||||
Self::TimeCpu => "CPU time",
|
||||
Self::TimeUser => "User time",
|
||||
Self::TimeSystem => "System time",
|
||||
Self::MemoryPeakResident => "Memory",
|
||||
Self::CpuCycles => "CPU cycles",
|
||||
Self::Instructions => "Instructions",
|
||||
Self::CacheReferences => "Cache references",
|
||||
Self::CacheMisses => "Cache misses",
|
||||
Self::BranchMisses => "Branch misses",
|
||||
}
|
||||
}
|
||||
|
||||
/// Extract a value in canonical units: seconds, bytes, or counts.
|
||||
pub fn value(self, measurement: &Measurement) -> Option<f64> {
|
||||
let counters = &measurement.hardware_counters;
|
||||
match self {
|
||||
Self::TimeWallClock => Some(measurement.time_wall_clock.get::<second>()),
|
||||
Self::TimeCpu => Some(measurement.time_cpu.get::<second>()),
|
||||
Self::TimeUser => Some(measurement.time_user.get::<second>()),
|
||||
Self::TimeSystem => Some(measurement.time_system.get::<second>()),
|
||||
Self::MemoryPeakResident => measurement.memory_peak_resident.map(|v| v.get::<byte>()),
|
||||
Self::CpuCycles => counters.cpu_cycles.map(|v| v as f64),
|
||||
Self::Instructions => counters.instructions.map(|v| v as f64),
|
||||
Self::CacheReferences => counters.cache_references.map(|v| v as f64),
|
||||
Self::CacheMisses => counters.cache_misses.map(|v| v as f64),
|
||||
Self::BranchMisses => counters.branch_misses.map(|v| v as f64),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn is_time(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::TimeWallClock | Self::TimeCpu | Self::TimeUser | Self::TimeSystem
|
||||
)
|
||||
}
|
||||
|
||||
pub fn is_counter(self) -> bool {
|
||||
!self.is_time() && self != Self::MemoryPeakResident
|
||||
}
|
||||
|
||||
/// Reject known unsupported configurations before any commands execute.
|
||||
/// Availability that depends on permissions or hardware is checked after collection.
|
||||
pub fn ensure_available(self, shell: bool) -> Result<()> {
|
||||
if self.is_counter() {
|
||||
ensure!(
|
||||
!shell,
|
||||
"Metric '{}' is unavailable when a shell is enabled",
|
||||
self.name()
|
||||
);
|
||||
ensure!(
|
||||
cfg!(any(
|
||||
target_os = "macos",
|
||||
all(
|
||||
target_os = "linux",
|
||||
target_pointer_width = "64",
|
||||
any(
|
||||
target_arch = "aarch64",
|
||||
target_arch = "x86_64",
|
||||
target_arch = "riscv64",
|
||||
target_arch = "powerpc64",
|
||||
target_arch = "loongarch64"
|
||||
)
|
||||
)
|
||||
)),
|
||||
"Metric '{}' is unavailable on this platform",
|
||||
self.name()
|
||||
);
|
||||
ensure!(
|
||||
!cfg!(target_os = "macos") || matches!(self, Self::CpuCycles | Self::Instructions),
|
||||
"Metric '{}' is unavailable on macOS",
|
||||
self.name()
|
||||
);
|
||||
}
|
||||
ensure!(
|
||||
!cfg!(windows) || self != Self::MemoryPeakResident,
|
||||
"Metric '{}' is unavailable on Windows",
|
||||
self.name()
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn base_unit(self) -> Unit {
|
||||
if self.is_time() {
|
||||
Unit::new("s", 1.0)
|
||||
} else if self.is_counter() {
|
||||
Unit::new("count", 1.0)
|
||||
} else {
|
||||
Unit::new("B", 1.0)
|
||||
}
|
||||
}
|
||||
|
||||
fn units(self) -> &'static [Unit] {
|
||||
if self.is_time() {
|
||||
TIME_UNITS
|
||||
} else if self.is_counter() {
|
||||
COUNT_UNITS
|
||||
} else {
|
||||
MEMORY_UNITS
|
||||
}
|
||||
}
|
||||
|
||||
/// Require complete coverage so different metrics describe the same runs.
|
||||
pub fn summarize(self, measurements: &Measurements) -> Option<Stats> {
|
||||
let values = measurements
|
||||
.measurements
|
||||
.iter()
|
||||
.map(|measurement| self.value(measurement))
|
||||
.collect::<Option<Vec<_>>>()?;
|
||||
if values.is_empty() {
|
||||
return None;
|
||||
}
|
||||
Some(Stats {
|
||||
count: values.len(),
|
||||
mean: mean(values.iter().copied()),
|
||||
stddev: (values.len() > 1).then(|| {
|
||||
standard_deviation(values.iter().map(|&v| Ratio::new::<ratio>(v))).get::<ratio>()
|
||||
}),
|
||||
median: median(values.iter().copied()),
|
||||
min: min(values.iter().copied()),
|
||||
max: max(values.iter().copied()),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct MetricSelection {
|
||||
pub metric: Metric,
|
||||
pub unit: Option<Unit>,
|
||||
}
|
||||
|
||||
impl Default for MetricSelection {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
metric: Metric::TimeWallClock,
|
||||
unit: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MetricSelection {
|
||||
/// Parse an ordered, nonempty list of `METRIC[:UNIT]` selections.
|
||||
pub fn parse_list(input: &str) -> Result<Vec<Self>> {
|
||||
let mut selections = Vec::new();
|
||||
for item in input.split(',') {
|
||||
let (name, unit_name) = match item.split_once(':') {
|
||||
Some((name, unit)) => (name, Some(unit)),
|
||||
None => (item, None),
|
||||
};
|
||||
let metric = match Metric::ALL.iter().copied().find(|m| m.name() == name) {
|
||||
Some(metric) => metric,
|
||||
None => bail!(
|
||||
"Unknown metric '{name}'. Expected one of: {}",
|
||||
Metric::ALL.map(Metric::name).join(", ")
|
||||
),
|
||||
};
|
||||
if selections
|
||||
.iter()
|
||||
.any(|selection: &Self| selection.metric == metric)
|
||||
{
|
||||
bail!("Metric '{name}' was selected more than once");
|
||||
}
|
||||
let unit = match unit_name {
|
||||
None => None,
|
||||
Some(unit_name) => {
|
||||
// Accept an ASCII spelling for microseconds as well.
|
||||
let unit_name = if unit_name == "us" { "µs" } else { unit_name };
|
||||
match metric.units().iter().find(|unit| unit.symbol == unit_name) {
|
||||
Some(unit) => Some(*unit),
|
||||
None => bail!(
|
||||
"Unit '{unit_name}' is not valid for metric '{name}'. Expected one of: {}",
|
||||
metric.units().iter().map(|u| u.symbol).collect::<Vec<_>>().join(", ")
|
||||
),
|
||||
}
|
||||
}
|
||||
};
|
||||
selections.push(Self { metric, unit });
|
||||
}
|
||||
Ok(selections)
|
||||
}
|
||||
|
||||
pub fn display_unit(self, mean: f64) -> Unit {
|
||||
if let Some(unit) = self.unit {
|
||||
return unit;
|
||||
}
|
||||
if mean == 0.0 {
|
||||
return self.metric.base_unit();
|
||||
}
|
||||
let units = if self.metric == Metric::MemoryPeakResident {
|
||||
BINARY_MEMORY_UNITS
|
||||
} else {
|
||||
self.metric.units()
|
||||
};
|
||||
units
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|unit| mean.abs() >= unit.scale)
|
||||
.copied()
|
||||
.unwrap_or(units[0])
|
||||
}
|
||||
|
||||
/// CSV never automatically scales values.
|
||||
pub fn csv_unit(self) -> Unit {
|
||||
self.unit.unwrap_or_else(|| self.metric.base_unit())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub struct Unit {
|
||||
pub symbol: &'static str,
|
||||
/// The number of canonical units in this unit (e.g. 1e9 for a billion).
|
||||
pub scale: f64,
|
||||
}
|
||||
|
||||
impl Unit {
|
||||
const fn new(symbol: &'static str, scale: f64) -> Self {
|
||||
Self { symbol, scale }
|
||||
}
|
||||
|
||||
pub fn format(self, value: f64) -> String {
|
||||
let value = self.format_value(value);
|
||||
if self.symbol == "count" {
|
||||
value
|
||||
} else {
|
||||
format!("{value} {}", self.symbol)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn format_value(self, value: f64) -> String {
|
||||
let precision = if self.symbol == "s" { 3 } else { 1 };
|
||||
format!("{:.precision$}", value / self.scale)
|
||||
}
|
||||
}
|
||||
|
||||
const TIME_UNITS: &[Unit] = &[
|
||||
Unit::new("ns", 1e-9),
|
||||
Unit::new("µs", 1e-6),
|
||||
Unit::new("ms", 1e-3),
|
||||
Unit::new("s", 1.0),
|
||||
Unit::new("min", 60.0),
|
||||
Unit::new("h", 3600.0),
|
||||
];
|
||||
|
||||
const BINARY_MEMORY_UNITS: &[Unit] = &[
|
||||
Unit::new("B", 1.0),
|
||||
Unit::new("KiB", 1024.0),
|
||||
Unit::new("MiB", 1_048_576.0),
|
||||
Unit::new("GiB", 1_073_741_824.0),
|
||||
Unit::new("TiB", 1_099_511_627_776.0),
|
||||
];
|
||||
|
||||
const MEMORY_UNITS: &[Unit] = &[
|
||||
Unit::new("B", 1.0),
|
||||
Unit::new("kB", 1e3),
|
||||
Unit::new("MB", 1e6),
|
||||
Unit::new("GB", 1e9),
|
||||
Unit::new("TB", 1e12),
|
||||
Unit::new("KiB", 1024.0),
|
||||
Unit::new("MiB", 1_048_576.0),
|
||||
Unit::new("GiB", 1_073_741_824.0),
|
||||
Unit::new("TiB", 1_099_511_627_776.0),
|
||||
];
|
||||
|
||||
const COUNT_UNITS: &[Unit] = &[
|
||||
Unit::new("count", 1.0),
|
||||
Unit::new("k", 1e3),
|
||||
Unit::new("M", 1e6),
|
||||
Unit::new("B", 1e9),
|
||||
];
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct Stats {
|
||||
pub count: usize,
|
||||
pub mean: f64,
|
||||
pub stddev: Option<f64>,
|
||||
pub median: f64,
|
||||
pub min: f64,
|
||||
pub max: f64,
|
||||
}
|
||||
|
||||
impl Stats {
|
||||
pub fn change_from(&self, reference: &Self) -> Option<f64> {
|
||||
(reference.mean != 0.0).then(|| 100.0 * (self.mean / reference.mean - 1.0))
|
||||
}
|
||||
|
||||
pub fn format_change_from(&self, reference: &Self) -> String {
|
||||
let Some(change) = self.change_from(reference) else {
|
||||
return "N/A".to_owned();
|
||||
};
|
||||
let mut formatted = format!("{change:+.1}%");
|
||||
if formatted == "+0.0%" || formatted == "-0.0%" {
|
||||
formatted = "0.0%".to_owned();
|
||||
}
|
||||
formatted
|
||||
}
|
||||
|
||||
pub fn format_factor_from(&self, reference: &Self, metric: Metric) -> String {
|
||||
if reference.mean > 0.0
|
||||
&& (self.mean >= 1.3 * reference.mean || self.mean <= 0.7 * reference.mean)
|
||||
{
|
||||
let decrease = self.mean < reference.mean;
|
||||
let factor = if decrease {
|
||||
reference.mean / self.mean
|
||||
} else {
|
||||
self.mean / reference.mean
|
||||
};
|
||||
let direction = match (metric.is_time(), decrease) {
|
||||
(true, true) => "faster",
|
||||
(true, false) => "slower",
|
||||
(false, true) => "less",
|
||||
(false, false) => "more",
|
||||
};
|
||||
let factor = if factor.is_infinite() {
|
||||
"∞".to_owned()
|
||||
} else {
|
||||
format!("{factor:.1}")
|
||||
};
|
||||
format!("({factor}x {direction})")
|
||||
} else {
|
||||
String::new()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::quantity::{Information, Time};
|
||||
|
||||
#[test]
|
||||
fn large_changes_include_factors() {
|
||||
let reference = Stats {
|
||||
count: 1,
|
||||
mean: 100.0,
|
||||
stddev: None,
|
||||
median: 100.0,
|
||||
min: 100.0,
|
||||
max: 100.0,
|
||||
};
|
||||
for (mean, metric, expected) in [
|
||||
(130.0, Metric::TimeWallClock, "+30.0% (1.3x slower)"),
|
||||
(70.0, Metric::TimeCpu, "-30.0% (1.4x faster)"),
|
||||
(10.0, Metric::Instructions, "-90.0% (10.0x less)"),
|
||||
(1000.0, Metric::MemoryPeakResident, "+900.0% (10.0x more)"),
|
||||
(129.99, Metric::TimeWallClock, "+30.0%"),
|
||||
(70.01, Metric::Instructions, "-30.0%"),
|
||||
(100.0, Metric::Instructions, "0.0%"),
|
||||
(0.0, Metric::Instructions, "-100.0% (∞x less)"),
|
||||
] {
|
||||
let stats = Stats { mean, ..reference };
|
||||
let (percentage, factor) = expected
|
||||
.split_once(" (")
|
||||
.map_or((expected, String::new()), |(p, f)| (p, format!("({f}")));
|
||||
assert_eq!(stats.format_change_from(&reference), percentage);
|
||||
assert_eq!(stats.format_factor_from(&reference, metric), factor);
|
||||
}
|
||||
let zero = Stats {
|
||||
mean: 0.0,
|
||||
..reference
|
||||
};
|
||||
assert_eq!(reference.format_change_from(&zero), "N/A");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cpu_time_statistics_use_per_run_totals() {
|
||||
let measurements = Measurements::new(
|
||||
[(1.0, 3.0), (3.0, 1.0)]
|
||||
.iter()
|
||||
.map(|&(user, system)| Measurement {
|
||||
time_cpu: Time::new::<second>(user + system),
|
||||
time_user: Time::new::<second>(user),
|
||||
time_system: Time::new::<second>(system),
|
||||
..Measurement::default()
|
||||
})
|
||||
.collect(),
|
||||
);
|
||||
let selection = MetricSelection::parse_list("time_cpu:ms").unwrap()[0];
|
||||
let stats = selection.metric.summarize(&measurements).unwrap();
|
||||
assert_eq!(selection.csv_unit().format(stats.mean), "4000.0 ms");
|
||||
// User and system time vary, but their per-run total is constant.
|
||||
assert_eq!(stats.stddev, Some(0.0));
|
||||
assert_eq!(stats.min, 4.0);
|
||||
assert_eq!(stats.max, 4.0);
|
||||
let json = serde_json::to_value(&measurements).unwrap();
|
||||
for sample in json["measurements"].as_array().unwrap() {
|
||||
assert_eq!(sample["time_cpu"]["value"], 4.0);
|
||||
assert_eq!(sample["time_cpu"]["unit"], "second");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_units_and_validation() {
|
||||
let selections = MetricSelection::parse_list(
|
||||
"instructions:B,memory_peak_resident:B,time_wall_clock:ms,time_user:us",
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(selections[0].csv_unit().scale, 1e9);
|
||||
assert_eq!(selections[1].csv_unit().scale, 1.0);
|
||||
assert_eq!(selections[2].csv_unit().format(0.125), "125.0 ms");
|
||||
assert_eq!(selections[3].csv_unit().symbol, "µs");
|
||||
|
||||
let instructions = MetricSelection::parse_list("instructions").unwrap()[0];
|
||||
assert_eq!(instructions.csv_unit().symbol, "count");
|
||||
assert_eq!(
|
||||
instructions.display_unit(143.2e9).format(143.2e9),
|
||||
"143.2 B"
|
||||
);
|
||||
for invalid in [
|
||||
"",
|
||||
"unknown",
|
||||
"time_wall_clock,",
|
||||
"instructions,instructions:M",
|
||||
"instructions:MiB",
|
||||
"memory_peak_resident:ms",
|
||||
"time_wall_clock:",
|
||||
] {
|
||||
assert!(MetricSelection::parse_list(invalid).is_err(), "{}", invalid);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn summaries_require_complete_samples() {
|
||||
let mut measurements = Measurements::new(vec![Measurement {
|
||||
memory_peak_resident: Some(Information::new::<byte>(10.0)),
|
||||
..Measurement::default()
|
||||
}]);
|
||||
let single = Metric::MemoryPeakResident.summarize(&measurements).unwrap();
|
||||
assert_eq!(single.stddev, None);
|
||||
measurements.push(Measurement::default());
|
||||
assert!(Metric::MemoryPeakResident
|
||||
.summarize(&measurements)
|
||||
.is_none());
|
||||
measurements.measurements[1].memory_peak_resident = Some(Information::new::<byte>(30.0));
|
||||
let stats = Metric::MemoryPeakResident.summarize(&measurements).unwrap();
|
||||
assert_eq!(stats.mean, 20.0);
|
||||
assert_eq!(stats.change_from(&single), Some(100.0));
|
||||
assert_eq!(stats.format_change_from(&single), "+100.0%");
|
||||
assert_eq!(single.format_change_from(&stats), "-50.0%");
|
||||
assert_eq!(
|
||||
Stats {
|
||||
mean: 9.999,
|
||||
..single
|
||||
}
|
||||
.format_change_from(&single),
|
||||
"0.0%"
|
||||
);
|
||||
approx::assert_relative_eq!(stats.stddev.unwrap(), 200.0_f64.sqrt());
|
||||
let zero = Metric::TimeWallClock.summarize(&measurements).unwrap();
|
||||
assert_eq!(zero.change_from(&zero), None);
|
||||
assert_eq!(zero.format_change_from(&zero), "N/A");
|
||||
}
|
||||
}
|
||||
+54
-90
@@ -4,12 +4,13 @@ use std::path::PathBuf;
|
||||
use std::process::{Command, Stdio};
|
||||
use std::{cmp, env, fmt, io};
|
||||
|
||||
use anyhow::{bail, ensure};
|
||||
use anyhow::ensure;
|
||||
use clap::ArgMatches;
|
||||
|
||||
use crate::command::Commands;
|
||||
use crate::error::OptionsError;
|
||||
use crate::quantity::{second, Time, TimeUnit};
|
||||
use crate::metric::{Metric, MetricSelection};
|
||||
use crate::quantity::{second, Time};
|
||||
|
||||
use anyhow::Result;
|
||||
|
||||
@@ -97,12 +98,6 @@ pub enum OutputStyleOption {
|
||||
Disabled,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SortOrder {
|
||||
Command,
|
||||
MeanTime,
|
||||
}
|
||||
|
||||
/// Bounds for the number of benchmark runs
|
||||
pub struct RunBounds {
|
||||
/// Minimum number of benchmark runs
|
||||
@@ -202,36 +197,21 @@ pub struct Options {
|
||||
/// Whether or not to ignore non-zero exit codes
|
||||
pub command_failure_action: CmdFailureAction,
|
||||
|
||||
// Standalone reference command, cleared when an existing benchmark is selected.
|
||||
pub reference_command: Option<String>,
|
||||
|
||||
// Name of the reference command
|
||||
pub reference_name: Option<String>,
|
||||
|
||||
// Index of the reference in the benchmark sequence, if any.
|
||||
pub reference_index: Option<usize>,
|
||||
|
||||
/// Command(s) to run before each timing run
|
||||
/// Command(s) to run before each benchmark run
|
||||
pub preparation_command: Option<Vec<String>>,
|
||||
|
||||
/// Command(s) to run after each timing run
|
||||
/// Command(s) to run after each benchmark run
|
||||
pub conclusion_command: Option<Vec<String>>,
|
||||
|
||||
/// Command to run before each *batch* of timing runs, i.e. before each individual benchmark
|
||||
/// Command to run before each *batch* of benchmark runs, i.e. before each individual benchmark
|
||||
pub setup_command: Option<String>,
|
||||
|
||||
/// Command to run after each *batch* of timing runs, i.e. after each individual benchmark
|
||||
/// Command to run after each *batch* of benchmark runs, i.e. after each individual benchmark
|
||||
pub cleanup_command: Option<String>,
|
||||
|
||||
/// What color mode to use for the terminal output
|
||||
pub output_style: OutputStyleOption,
|
||||
|
||||
/// How to order benchmarks in the relative speed comparison
|
||||
pub sort_order_speed_comparison: SortOrder,
|
||||
|
||||
/// How to order benchmarks in the markup format exports
|
||||
pub sort_order_exports: SortOrder,
|
||||
|
||||
/// Determines how we run commands
|
||||
pub executor_kind: ExecutorKind,
|
||||
|
||||
@@ -241,8 +221,11 @@ pub struct Options {
|
||||
/// What to do with the output of the benchmarked commands
|
||||
pub command_output_policies: Vec<CommandOutputPolicy>,
|
||||
|
||||
/// Which time unit to use when displaying results
|
||||
pub time_unit: Option<TimeUnit>,
|
||||
/// Displayed metrics, with the primary metric first.
|
||||
pub metrics: Vec<MetricSelection>,
|
||||
|
||||
/// The `all` preset displays only metrics with complete measurements.
|
||||
pub skip_unavailable_metrics: bool,
|
||||
}
|
||||
|
||||
impl Default for Options {
|
||||
@@ -252,19 +235,16 @@ impl Default for Options {
|
||||
warmup_count: 0,
|
||||
min_benchmarking_time: Time::new::<second>(3.0),
|
||||
command_failure_action: CmdFailureAction::RaiseError,
|
||||
reference_command: None,
|
||||
reference_name: None,
|
||||
reference_index: None,
|
||||
preparation_command: None,
|
||||
conclusion_command: None,
|
||||
setup_command: None,
|
||||
cleanup_command: None,
|
||||
output_style: OutputStyleOption::Full,
|
||||
sort_order_speed_comparison: SortOrder::MeanTime,
|
||||
sort_order_exports: SortOrder::Command,
|
||||
executor_kind: ExecutorKind::default(),
|
||||
command_output_policies: vec![CommandOutputPolicy::Null],
|
||||
time_unit: None,
|
||||
metrics: MetricSelection::parse_list(crate::cli::DEFAULT_METRICS)
|
||||
.expect("valid default metrics"),
|
||||
skip_unavailable_metrics: false,
|
||||
command_input_policy: CommandInputPolicy::Null,
|
||||
}
|
||||
}
|
||||
@@ -307,11 +287,6 @@ impl Options {
|
||||
|
||||
options.setup_command = matches.get_one::<String>("setup").map(String::from);
|
||||
|
||||
options.reference_command = matches.get_one::<String>("reference").map(String::from);
|
||||
options.reference_name = matches
|
||||
.get_one::<String>("reference-name")
|
||||
.map(String::from);
|
||||
|
||||
options.preparation_command = matches
|
||||
.get_many::<String>("prepare")
|
||||
.map(|values| values.map(String::from).collect::<Vec<String>>());
|
||||
@@ -383,16 +358,6 @@ impl Options {
|
||||
OutputStyleOption::Disabled => {}
|
||||
};
|
||||
|
||||
(
|
||||
options.sort_order_speed_comparison,
|
||||
options.sort_order_exports,
|
||||
) = match matches.get_one::<String>("sort").map(|s| s.as_str()) {
|
||||
None | Some("auto") => (SortOrder::MeanTime, SortOrder::Command),
|
||||
Some("command") => (SortOrder::Command, SortOrder::Command),
|
||||
Some("mean-time") => (SortOrder::MeanTime, SortOrder::MeanTime),
|
||||
Some(_) => unreachable!("Unknown sort order"),
|
||||
};
|
||||
|
||||
options.executor_kind = if matches.get_flag("no-shell") {
|
||||
ExecutorKind::Raw
|
||||
} else if matches.get_flag("default-shell") {
|
||||
@@ -430,14 +395,36 @@ impl Options {
|
||||
};
|
||||
}
|
||||
|
||||
options.time_unit = match matches.get_one::<String>("time-unit").map(|s| s.as_str()) {
|
||||
Some("µs" | "us" | "microsecond" | "microseconds") => Some(TimeUnit::MicroSecond),
|
||||
Some("ms" | "millisecond" | "milliseconds") => Some(TimeUnit::MilliSecond),
|
||||
Some("s" | "second" | "seconds") => Some(TimeUnit::Second),
|
||||
Some("min" | "minute" | "minutes") => Some(TimeUnit::Minute),
|
||||
Some("h" | "hour" | "hours") => Some(TimeUnit::Hour),
|
||||
_ => None,
|
||||
};
|
||||
match matches.get_one::<String>("metrics").unwrap().as_str() {
|
||||
"default" => {}
|
||||
"time" => {
|
||||
options.metrics = [
|
||||
Metric::TimeWallClock,
|
||||
Metric::TimeCpu,
|
||||
Metric::TimeUser,
|
||||
Metric::TimeSystem,
|
||||
]
|
||||
.map(|metric| MetricSelection { metric, unit: None })
|
||||
.to_vec();
|
||||
}
|
||||
"all" => {
|
||||
let shell = matches!(
|
||||
options.executor_kind,
|
||||
ExecutorKind::Shell(_) | ExecutorKind::Mock(Some(_))
|
||||
);
|
||||
options.metrics = Metric::ALL
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|metric| metric.ensure_available(shell).is_ok())
|
||||
.map(|metric| MetricSelection { metric, unit: None })
|
||||
.collect();
|
||||
options.skip_unavailable_metrics = true;
|
||||
}
|
||||
list => {
|
||||
options.metrics = MetricSelection::parse_list(list)
|
||||
.map_err(|error| OptionsError::InvalidMetrics(error.to_string()))?;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(time) = matches.get_one::<String>("min-benchmarking-time") {
|
||||
options.min_benchmarking_time = Time::new::<second>(
|
||||
@@ -466,43 +453,20 @@ impl Options {
|
||||
}
|
||||
|
||||
pub fn validate_against_command_list(&mut self, commands: &Commands) -> Result<()> {
|
||||
if let Some(reference) = &self.reference_command {
|
||||
if commands
|
||||
.iter()
|
||||
.next()
|
||||
.is_some_and(|command| !command.get_parameters().is_empty())
|
||||
{
|
||||
ensure!(
|
||||
self.reference_name.is_none(),
|
||||
"--reference-name cannot be used with a parameterized reference; use --command-name to name the benchmark"
|
||||
);
|
||||
let mut matches = commands
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(_, command)| command.get_name_with_unused_parameters() == *reference);
|
||||
let Some((index, _)) = matches.next() else {
|
||||
bail!(
|
||||
"Reference '{reference}' does not match any parameterized benchmark. Use its full displayed benchmark name."
|
||||
);
|
||||
};
|
||||
ensure!(
|
||||
matches.next().is_none(),
|
||||
"Reference '{reference}' matches multiple parameterized benchmarks. Use --command-name to give them unique names."
|
||||
);
|
||||
self.reference_index = Some(index);
|
||||
self.reference_command = None;
|
||||
} else {
|
||||
self.reference_index = Some(0);
|
||||
if !matches!(self.executor_kind, ExecutorKind::Mock(_)) {
|
||||
for selection in &self.metrics {
|
||||
selection
|
||||
.metric
|
||||
.ensure_available(matches!(self.executor_kind, ExecutorKind::Shell(_)))?;
|
||||
}
|
||||
}
|
||||
let has_reference_command = self.reference_command.is_some();
|
||||
let num_commands = commands.num_commands(has_reference_command);
|
||||
let num_commands = commands.num_commands();
|
||||
|
||||
if let Some(preparation_command) = &self.preparation_command {
|
||||
ensure!(
|
||||
preparation_command.len() <= 1 || num_commands == preparation_command.len(),
|
||||
"The '--prepare' option has to be provided just once or N times, where N={num_commands} is the \
|
||||
number of benchmark commands (including a potential reference)."
|
||||
number of benchmark commands."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -510,7 +474,7 @@ impl Options {
|
||||
ensure!(
|
||||
conclusion_command.len() <= 1 || num_commands == conclusion_command.len(),
|
||||
"The '--conclude' option has to be provided just once or N times, where N={num_commands} is the \
|
||||
number of benchmark commands (including a potential reference)."
|
||||
number of benchmark commands."
|
||||
);
|
||||
}
|
||||
|
||||
@@ -521,7 +485,7 @@ impl Options {
|
||||
ensure!(
|
||||
self.command_output_policies.len() == num_commands,
|
||||
"The '--output' option has to be provided just once or N times, where N={num_commands} is the \
|
||||
number of benchmark commands (including a potential reference)."
|
||||
number of benchmark commands."
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
pub mod progress_bar;
|
||||
pub mod report;
|
||||
pub mod warnings;
|
||||
|
||||
/// Mark console write errors so broken pipes can be handled separately from file errors.
|
||||
|
||||
+22
-15
@@ -16,29 +16,36 @@ const DEFAULT_TEMPLATE: &str = " {spinner} {msg:<30} {wide_bar} ETA {eta_precise
|
||||
const INITIAL_TEMPLATE: &str =
|
||||
" {spinner} Initial run: {initial_elapsed:<17} {wide_bar} ETA {eta_precise} ";
|
||||
|
||||
fn style_with_template(bar: &ProgressBar, template: &str) -> ProgressStyle {
|
||||
let template = if bar.prefix().is_empty() {
|
||||
template.to_owned()
|
||||
} else {
|
||||
format!("{{prefix}}\n{template}")
|
||||
};
|
||||
bar.style().template(&template).expect("no template error")
|
||||
}
|
||||
|
||||
/// Show the benchmark heading above the progress bar, clearing both on completion.
|
||||
pub fn set_benchmark_header(bar: &ProgressBar, header: String) {
|
||||
bar.set_prefix(header);
|
||||
bar.set_style(style_with_template(bar, DEFAULT_TEMPLATE));
|
||||
}
|
||||
|
||||
/// Show the time elapsed since the initial benchmark command started.
|
||||
pub fn start_initial_measurement(bar: &ProgressBar, started: Instant) {
|
||||
let style = bar
|
||||
.style()
|
||||
.template(INITIAL_TEMPLATE)
|
||||
.expect("no template error")
|
||||
.with_key(
|
||||
"initial_elapsed",
|
||||
move |_: &ProgressState, w: &mut dyn fmt::Write| {
|
||||
write!(w, "{}", FormattedDuration(started.elapsed())).unwrap();
|
||||
},
|
||||
);
|
||||
let style = style_with_template(bar, INITIAL_TEMPLATE).with_key(
|
||||
"initial_elapsed",
|
||||
move |_: &ProgressState, w: &mut dyn fmt::Write| {
|
||||
write!(w, "{}", FormattedDuration(started.elapsed())).unwrap();
|
||||
},
|
||||
);
|
||||
bar.set_style(style);
|
||||
}
|
||||
|
||||
/// Stop showing the initial timer and display the next phase's message.
|
||||
pub fn finish_initial_measurement(bar: &ProgressBar, message: String) {
|
||||
bar.set_message(message);
|
||||
bar.set_style(
|
||||
bar.style()
|
||||
.template(DEFAULT_TEMPLATE)
|
||||
.expect("no template error"),
|
||||
);
|
||||
bar.set_style(style_with_template(bar, DEFAULT_TEMPLATE));
|
||||
}
|
||||
|
||||
/// Return a pre-configured progress bar
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
use std::io::{self, Write};
|
||||
|
||||
use anyhow::{ensure, Result};
|
||||
use colored::Colorize;
|
||||
|
||||
use crate::benchmark::{benchmark_result::BenchmarkResult, measurement::Measurements};
|
||||
use crate::metric::MetricSelection;
|
||||
use crate::output::console_writeln;
|
||||
|
||||
// Reserve enough space for typical values and every supported unit suffix so
|
||||
// independently printed benchmarks keep the same layout.
|
||||
const MIN_VALUE_WIDTH: usize = 7;
|
||||
const MIN_UNIT_WIDTH: usize = 3;
|
||||
const MIN_CHANGE_WIDTH: usize = 14;
|
||||
|
||||
/// Print a completed benchmark immediately, comparing every metric with benchmark 1.
|
||||
pub fn print(
|
||||
number: usize,
|
||||
name: &str,
|
||||
measurements: &Measurements,
|
||||
metrics: &[MetricSelection],
|
||||
skip_unavailable_metrics: bool,
|
||||
reference: Option<&BenchmarkResult>,
|
||||
) -> Result<()> {
|
||||
let comparison = reference.is_some();
|
||||
let multiple_runs = measurements.len() > 1;
|
||||
let value_columns = 1..if multiple_runs { 5 } else { 2 };
|
||||
let mut header = vec![
|
||||
String::new(),
|
||||
"mean".to_owned(),
|
||||
"σ".to_owned(),
|
||||
"min".to_owned(),
|
||||
"max".to_owned(),
|
||||
];
|
||||
if !multiple_runs {
|
||||
header.truncate(2);
|
||||
header[1] = "Value".to_owned();
|
||||
}
|
||||
if comparison {
|
||||
header.extend([String::new(), String::new()]);
|
||||
}
|
||||
let mut rows = vec![(header, "")];
|
||||
for selection in metrics {
|
||||
let Some(summary) = selection.metric.summarize(measurements) else {
|
||||
ensure!(
|
||||
skip_unavailable_metrics,
|
||||
"Metric '{}' is unavailable for '{name}'",
|
||||
selection.metric.name()
|
||||
);
|
||||
continue;
|
||||
};
|
||||
let baseline = reference.and_then(|r| selection.metric.summarize(&r.measurements));
|
||||
// Keep auto units consistent with the first command for easier comparison.
|
||||
let scale_mean = baseline
|
||||
.as_ref()
|
||||
.filter(|stats| stats.mean != 0.0)
|
||||
.unwrap_or(&summary)
|
||||
.mean;
|
||||
let unit = selection.display_unit(scale_mean);
|
||||
let mut row = vec![
|
||||
selection.metric.label().to_owned(),
|
||||
unit.format_value(summary.mean),
|
||||
];
|
||||
if let Some(stddev) = summary.stddev {
|
||||
row.push(unit.format_value(stddev));
|
||||
row.push(unit.format_value(summary.min));
|
||||
row.push(unit.format_value(summary.max));
|
||||
}
|
||||
if comparison {
|
||||
row.push(baseline.as_ref().map_or_else(
|
||||
|| "N/A".to_owned(),
|
||||
|baseline| summary.format_change_from(baseline),
|
||||
));
|
||||
row.push(baseline.as_ref().map_or_else(String::new, |baseline| {
|
||||
summary.format_factor_from(baseline, selection.metric)
|
||||
}));
|
||||
}
|
||||
rows.push((
|
||||
row,
|
||||
if unit.symbol == "count" {
|
||||
""
|
||||
} else {
|
||||
unit.symbol
|
||||
},
|
||||
));
|
||||
}
|
||||
// Align numbers and unit suffixes independently so mixed units do not shift
|
||||
// the values or the ± and … separators.
|
||||
let unit_width = rows
|
||||
.iter()
|
||||
.map(|(_, unit)| unit.chars().count())
|
||||
.max()
|
||||
.unwrap()
|
||||
.max(MIN_UNIT_WIDTH);
|
||||
// Keep labels aligned across benchmarks even when some metrics are skipped.
|
||||
let metric_width = metrics
|
||||
.iter()
|
||||
.map(|selection| selection.metric.label().chars().count())
|
||||
.max()
|
||||
.unwrap_or(0);
|
||||
let widths: Vec<_> = (0..rows[0].0.len())
|
||||
.map(|column| {
|
||||
let width = rows
|
||||
.iter()
|
||||
.map(|(row, _)| row[column].chars().count())
|
||||
.max()
|
||||
.unwrap();
|
||||
if column == 0 {
|
||||
width.max(metric_width)
|
||||
} else if value_columns.contains(&column) {
|
||||
width.max(MIN_VALUE_WIDTH)
|
||||
} else {
|
||||
width.max(MIN_CHANGE_WIDTH)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let mut stdout = io::stdout().lock();
|
||||
console_writeln!(
|
||||
stdout,
|
||||
"{}: {} {}",
|
||||
format!("Benchmark {}", number + 1).bold(),
|
||||
name.white().bold(),
|
||||
format!(
|
||||
"({} {})",
|
||||
measurements.len(),
|
||||
if measurements.len() == 1 {
|
||||
"run"
|
||||
} else {
|
||||
"runs"
|
||||
}
|
||||
)
|
||||
.dimmed()
|
||||
)?;
|
||||
for (index, (row, unit)) in rows.iter().enumerate() {
|
||||
let mut line = String::new();
|
||||
for (column, value) in row.iter().enumerate() {
|
||||
let is_factor = comparison && column == row.len() - 1;
|
||||
let is_change = comparison && column >= row.len() - 2;
|
||||
if index == 0 && is_change {
|
||||
break;
|
||||
}
|
||||
if column > 0 {
|
||||
line.push_str(match (multiple_runs, column) {
|
||||
(true, 2) => " ± ",
|
||||
(true, 4) => " … ",
|
||||
_ => " ",
|
||||
});
|
||||
}
|
||||
let padding = " ".repeat(widths[column] - value.chars().count());
|
||||
let has_unit = value_columns.contains(&column);
|
||||
let text = if has_unit && !unit.is_empty() {
|
||||
format!("{value} {unit}")
|
||||
} else {
|
||||
value.clone()
|
||||
};
|
||||
let styled = if is_change {
|
||||
if row[row.len() - 2].starts_with('-') {
|
||||
text.green().to_string()
|
||||
} else if row[row.len() - 2].starts_with('+') {
|
||||
text.red().to_string()
|
||||
} else {
|
||||
text.normal().to_string()
|
||||
}
|
||||
} else if column == 1 || column == 2 {
|
||||
text.green().to_string()
|
||||
} else if column == 3 {
|
||||
text.cyan().to_string()
|
||||
} else if column == 4 {
|
||||
text.purple().to_string()
|
||||
} else {
|
||||
text.normal().to_string()
|
||||
};
|
||||
if column == 0 || is_factor {
|
||||
line.push_str(&format!("{styled}{padding}"));
|
||||
} else {
|
||||
line.push_str(&format!("{padding}{styled}"));
|
||||
}
|
||||
if has_unit && unit_width > 0 {
|
||||
// Empty suffixes (headers and unscaled counters) also need the
|
||||
// space normally separating a number from its unit.
|
||||
let unit_padding = unit_width - unit.chars().count() + usize::from(unit.is_empty());
|
||||
line.push_str(&" ".repeat(unit_padding));
|
||||
}
|
||||
}
|
||||
let line = line.trim_end();
|
||||
console_writeln!(
|
||||
stdout,
|
||||
" {}",
|
||||
if index == 1 {
|
||||
line.bold()
|
||||
} else {
|
||||
line.normal()
|
||||
}
|
||||
)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
+14
-17
@@ -1,7 +1,8 @@
|
||||
use std::fmt;
|
||||
|
||||
use crate::benchmark::MIN_EXECUTION_TIME;
|
||||
use crate::quantity::{FormatQuantity, Time, TimeUnit};
|
||||
use crate::metric::{Metric, MetricSelection};
|
||||
use crate::quantity::millisecond;
|
||||
|
||||
pub struct OutlierWarningOptions {
|
||||
pub warmup_in_use: bool,
|
||||
@@ -12,8 +13,7 @@ pub struct OutlierWarningOptions {
|
||||
pub enum Warnings {
|
||||
FastExecutionTime,
|
||||
NonZeroExitCode,
|
||||
SlowInitialRun(Time, OutlierWarningOptions),
|
||||
OutliersDetected(OutlierWarningOptions),
|
||||
InitialRunOutlier(MetricSelection, f64, OutlierWarningOptions),
|
||||
}
|
||||
|
||||
impl fmt::Display for Warnings {
|
||||
@@ -21,19 +21,25 @@ impl fmt::Display for Warnings {
|
||||
match *self {
|
||||
Warnings::FastExecutionTime => write!(
|
||||
f,
|
||||
"Command took less than {} ms to complete. Note that the results might be \
|
||||
"Command took less than {} ms to complete. Note that the wall-clock measurements might be \
|
||||
inaccurate because hyperfine can not calibrate the shell startup time much \
|
||||
more precise than this limit. You can try to use the `-N`/`--shell=none` \
|
||||
option to disable the shell completely.",
|
||||
MIN_EXECUTION_TIME.format_with_precision(TimeUnit::MilliSecond, 0)
|
||||
MIN_EXECUTION_TIME.get::<millisecond>()
|
||||
),
|
||||
Warnings::NonZeroExitCode => write!(f, "Ignoring non-zero exit code."),
|
||||
Warnings::SlowInitialRun(time_first_run, ref options) => write!(
|
||||
Warnings::InitialRunOutlier(selection, value, _) if selection.metric != Metric::TimeWallClock => write!(
|
||||
f,
|
||||
"The first benchmarking run for this command had an unusually high value for the primary '{}' metric ({}) compared with the rest.",
|
||||
selection.metric.name(),
|
||||
selection.display_unit(value).format(value),
|
||||
),
|
||||
Warnings::InitialRunOutlier(selection, value, ref options) => write!(
|
||||
f,
|
||||
"The first benchmarking run for this command was significantly slower than the \
|
||||
rest ({time}). This could be caused by (filesystem) caches that were not filled until \
|
||||
after the first run. {hints}",
|
||||
time=time_first_run.format_auto(),
|
||||
time=selection.display_unit(value).format(value),
|
||||
hints=match (options.warmup_in_use, options.prepare_in_use) {
|
||||
(true, true) => "You are already using both the '--warmup' option as well \
|
||||
as the '--prepare' option. Consider re-running the benchmark on a quiet system. \
|
||||
@@ -53,16 +59,7 @@ impl fmt::Display for Warnings {
|
||||
option to clear the caches before each timing run."
|
||||
}
|
||||
),
|
||||
Warnings::OutliersDetected(ref options) => write!(
|
||||
f,
|
||||
"Statistical outliers were detected. Consider re-running this benchmark on a quiet \
|
||||
system without any interferences from other programs.{hint}",
|
||||
hint=if options.warmup_in_use && options.prepare_in_use {
|
||||
""
|
||||
} else {
|
||||
" It might help to use the '--warmup' or '--prepare' options."
|
||||
}
|
||||
),
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,80 +11,7 @@ pub use si::ratio::ratio;
|
||||
pub use si::time::{hour, microsecond, millisecond, minute, nanosecond, second};
|
||||
pub use uom::num_traits::Zero;
|
||||
|
||||
pub use units::{InformationUnit, IsUnit, TimeUnit};
|
||||
|
||||
pub mod statistics;
|
||||
mod units;
|
||||
|
||||
pub trait QuantityInUnit<U> {
|
||||
fn value_in_unit(self) -> f64;
|
||||
}
|
||||
|
||||
impl<U: uom::si::time::Unit + uom::Conversion<f64, T = f64>> QuantityInUnit<U> for Time {
|
||||
fn value_in_unit(self) -> f64 {
|
||||
self.get::<U>()
|
||||
}
|
||||
}
|
||||
|
||||
impl<U: uom::si::information::Unit + uom::Conversion<f64, T = f64>> QuantityInUnit<U>
|
||||
for Information
|
||||
{
|
||||
fn value_in_unit(self) -> f64 {
|
||||
self.get::<U>()
|
||||
}
|
||||
}
|
||||
|
||||
impl<U: uom::si::ratio::Unit + uom::Conversion<f64, T = f64>> QuantityInUnit<U> for Ratio {
|
||||
fn value_in_unit(self) -> f64 {
|
||||
self.get::<U>()
|
||||
}
|
||||
}
|
||||
|
||||
pub trait FormatQuantity {
|
||||
type Unit;
|
||||
|
||||
fn suitable_unit(&self) -> Self::Unit;
|
||||
|
||||
fn format_with_precision(&self, unit: Self::Unit, precision: usize) -> String;
|
||||
fn format(&self, unit: Self::Unit) -> String;
|
||||
fn format_auto(&self) -> String;
|
||||
fn format_value(&self, unit: Self::Unit) -> String;
|
||||
}
|
||||
|
||||
impl FormatQuantity for Time {
|
||||
type Unit = TimeUnit;
|
||||
|
||||
fn suitable_unit(&self) -> TimeUnit {
|
||||
if *self < Time::new::<millisecond>(1.0) {
|
||||
TimeUnit::MicroSecond
|
||||
} else if *self < Time::new::<second>(1.0) {
|
||||
TimeUnit::MilliSecond
|
||||
} else {
|
||||
TimeUnit::Second
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the time duration in the given unit with the given precision.
|
||||
fn format_with_precision(&self, u: TimeUnit, precision: usize) -> String {
|
||||
u.format(*self, precision)
|
||||
}
|
||||
|
||||
/// Format the time duration in the given unit.
|
||||
fn format(&self, unit: TimeUnit) -> String {
|
||||
let value = self.format_with_precision(unit, unit.preferred_precision());
|
||||
format!("{} {}", value, unit.short_name())
|
||||
}
|
||||
|
||||
/// Format the given time duration. The unit will be determined automatically.
|
||||
fn format_auto(&self) -> String {
|
||||
self.format(self.suitable_unit())
|
||||
}
|
||||
|
||||
/// Like `format`, but without displaying the unit.
|
||||
fn format_value(&self, unit: TimeUnit) -> String {
|
||||
self.format_with_precision(unit, unit.preferred_precision())
|
||||
}
|
||||
}
|
||||
|
||||
pub const fn const_time_from_seconds(value: f64) -> Time {
|
||||
// Quantity::new in uom is not yet const: https://docs.rs/uom/0.36.0/uom/si/struct.Quantity.html
|
||||
@@ -95,43 +22,6 @@ pub const fn const_time_from_seconds(value: f64) -> Time {
|
||||
}
|
||||
}
|
||||
|
||||
impl FormatQuantity for Information {
|
||||
type Unit = InformationUnit;
|
||||
|
||||
fn suitable_unit(&self) -> InformationUnit {
|
||||
if *self < Information::new::<kibibyte>(1.0) {
|
||||
InformationUnit::Byte
|
||||
} else if *self < Information::new::<mebibyte>(1.0) {
|
||||
InformationUnit::KibiByte
|
||||
} else if *self < Information::new::<gibibyte>(1.0) {
|
||||
InformationUnit::MebiByte
|
||||
} else {
|
||||
InformationUnit::GibiByte
|
||||
}
|
||||
}
|
||||
|
||||
/// Format the information in the given unit with the given precision.
|
||||
fn format_with_precision(&self, u: InformationUnit, precision: usize) -> String {
|
||||
u.format(*self, precision)
|
||||
}
|
||||
|
||||
/// Format the information in the given unit.
|
||||
fn format(&self, unit: InformationUnit) -> String {
|
||||
let value = self.format_with_precision(unit, unit.preferred_precision());
|
||||
format!("{} {}", value, unit.short_name())
|
||||
}
|
||||
|
||||
/// Format the given information. The unit will be determined automatically.
|
||||
fn format_auto(&self) -> String {
|
||||
self.format(self.suitable_unit())
|
||||
}
|
||||
|
||||
/// Like `format`, but without displaying the unit.
|
||||
fn format_value(&self, unit: InformationUnit) -> String {
|
||||
self.format_with_precision(unit, unit.preferred_precision())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn serialize_time<S>(t: &Time, s: S) -> Result<S::Ok, S::Error>
|
||||
where
|
||||
S: Serializer,
|
||||
@@ -177,74 +67,3 @@ fn test_information() {
|
||||
let largest_exactly_representable = Information::new::<byte>(9_007_199_254_740_992.);
|
||||
assert_eq!(largest_exactly_representable.get::<pebibyte>(), 8.);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_suiteable_unit_time() {
|
||||
assert_eq!(Time::new::<second>(1.3).suitable_unit(), TimeUnit::Second);
|
||||
assert_eq!(Time::new::<second>(1.0).suitable_unit(), TimeUnit::Second);
|
||||
assert_eq!(
|
||||
Time::new::<second>(0.999).suitable_unit(),
|
||||
TimeUnit::MilliSecond
|
||||
);
|
||||
assert_eq!(
|
||||
Time::new::<second>(0.0005).suitable_unit(),
|
||||
TimeUnit::MicroSecond
|
||||
);
|
||||
assert_eq!(
|
||||
Time::new::<second>(0.).suitable_unit(),
|
||||
TimeUnit::MicroSecond
|
||||
);
|
||||
assert_eq!(
|
||||
Time::new::<second>(1000.0).suitable_unit(),
|
||||
TimeUnit::Second
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_suitable_unit_information() {
|
||||
assert_eq!(
|
||||
Information::new::<byte>(512.0).suitable_unit(),
|
||||
InformationUnit::Byte
|
||||
);
|
||||
assert_eq!(
|
||||
Information::new::<byte>(2048.0).suitable_unit(),
|
||||
InformationUnit::KibiByte
|
||||
);
|
||||
assert_eq!(
|
||||
Information::new::<byte>(2_097_152.0).suitable_unit(),
|
||||
InformationUnit::MebiByte
|
||||
);
|
||||
assert_eq!(
|
||||
Information::new::<byte>(2_147_483_648.0).suitable_unit(),
|
||||
InformationUnit::GibiByte
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format() {
|
||||
let time = Time::new::<millisecond>(123.4);
|
||||
assert_eq!(time.format(TimeUnit::Second), "0.123 s");
|
||||
assert_eq!(time.format(TimeUnit::MilliSecond), "123.4 ms");
|
||||
assert_eq!(time.format_auto(), "123.4 ms");
|
||||
assert_eq!(time.format(TimeUnit::MicroSecond), "123400.0 µs");
|
||||
|
||||
let memory_peak_resident = Information::new::<kibibyte>(8.);
|
||||
assert_eq!(memory_peak_resident.format_auto(), "8.0 KiB");
|
||||
assert_eq!(memory_peak_resident.format(InformationUnit::Byte), "8192 B");
|
||||
assert_eq!(
|
||||
memory_peak_resident.format(InformationUnit::KibiByte),
|
||||
"8.0 KiB"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_format_duration_unit_with_unit() {
|
||||
let out = Time::new::<second>(1.3).format(TimeUnit::Second);
|
||||
assert_eq!("1.300 s", out);
|
||||
|
||||
let out = Time::new::<second>(1.3).format(TimeUnit::MilliSecond);
|
||||
assert_eq!("1300.0 ms", out);
|
||||
|
||||
let out = Time::new::<second>(1.3).format(TimeUnit::MicroSecond);
|
||||
assert_eq!("1300000.0 µs", out);
|
||||
}
|
||||
|
||||
@@ -146,11 +146,6 @@ pub fn modified_zscores_f64(xs: &[f64]) -> Vec<f64> {
|
||||
xs.iter().map(|&x| (x - x_median) / mad).collect()
|
||||
}
|
||||
|
||||
pub fn modified_zscores<Q: UnsafeRawValue>(values: &[Q]) -> Vec<f64> {
|
||||
let values: Vec<_> = values.iter().map(|q| q.unsafe_raw_value()).collect();
|
||||
modified_zscores_f64(&values)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
@@ -1,166 +0,0 @@
|
||||
//! This module contains common units for time and information quantities.
|
||||
|
||||
use std::marker::PhantomData;
|
||||
|
||||
use crate::quantity::{
|
||||
byte, gibibyte, hour, kibibyte, mebibyte, microsecond, millisecond, minute, second, tebibyte,
|
||||
Information, Time,
|
||||
};
|
||||
|
||||
pub trait IsUnit {
|
||||
type Quantity;
|
||||
|
||||
fn dispatch(&self) -> Box<dyn UnitImpl<Quantity = Self::Quantity>>;
|
||||
|
||||
fn preferred_precision(&self) -> usize;
|
||||
|
||||
fn short_name(&self) -> &'static str {
|
||||
self.dispatch().short_name()
|
||||
}
|
||||
|
||||
fn format(&self, value: Self::Quantity, precision: usize) -> String {
|
||||
self.dispatch().format_value(value, precision)
|
||||
}
|
||||
}
|
||||
|
||||
/// Supported time units
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum TimeUnit {
|
||||
MicroSecond,
|
||||
MilliSecond,
|
||||
Second,
|
||||
Minute,
|
||||
Hour,
|
||||
}
|
||||
|
||||
impl IsUnit for TimeUnit {
|
||||
type Quantity = Time;
|
||||
|
||||
fn dispatch(&self) -> Box<dyn UnitImpl<Quantity = Time>> {
|
||||
match self {
|
||||
TimeUnit::MicroSecond => Box::new(TimeUnitDispatcher::<microsecond>::new()),
|
||||
TimeUnit::MilliSecond => Box::new(TimeUnitDispatcher::<millisecond>::new()),
|
||||
TimeUnit::Second => Box::new(TimeUnitDispatcher::<second>::new()),
|
||||
TimeUnit::Minute => Box::new(TimeUnitDispatcher::<minute>::new()),
|
||||
TimeUnit::Hour => Box::new(TimeUnitDispatcher::<hour>::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn preferred_precision(&self) -> usize {
|
||||
match self {
|
||||
TimeUnit::Second => 3,
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Supported information units
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
|
||||
pub enum InformationUnit {
|
||||
Byte,
|
||||
KibiByte,
|
||||
MebiByte,
|
||||
GibiByte,
|
||||
TebiByte,
|
||||
}
|
||||
|
||||
impl IsUnit for InformationUnit {
|
||||
type Quantity = Information;
|
||||
|
||||
fn dispatch(&self) -> Box<dyn UnitImpl<Quantity = Information>> {
|
||||
match self {
|
||||
InformationUnit::Byte => Box::new(InformationUnitDispatcher::<byte>::new()),
|
||||
InformationUnit::KibiByte => Box::new(InformationUnitDispatcher::<kibibyte>::new()),
|
||||
InformationUnit::MebiByte => Box::new(InformationUnitDispatcher::<mebibyte>::new()),
|
||||
InformationUnit::GibiByte => Box::new(InformationUnitDispatcher::<gibibyte>::new()),
|
||||
InformationUnit::TebiByte => Box::new(InformationUnitDispatcher::<tebibyte>::new()),
|
||||
}
|
||||
}
|
||||
|
||||
fn preferred_precision(&self) -> usize {
|
||||
match self {
|
||||
InformationUnit::Byte => 0,
|
||||
_ => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait UnitImpl {
|
||||
type Quantity;
|
||||
|
||||
fn short_name(&self) -> &'static str;
|
||||
fn format_value(&self, value: Self::Quantity, precision: usize) -> String;
|
||||
}
|
||||
|
||||
struct TimeUnitDispatcher<U: uom::si::time::Unit + uom::Conversion<f64, T = f64>> {
|
||||
u: PhantomData<U>,
|
||||
}
|
||||
|
||||
impl<U: uom::si::time::Unit + uom::Conversion<f64, T = f64>> TimeUnitDispatcher<U> {
|
||||
fn new() -> Self {
|
||||
TimeUnitDispatcher { u: PhantomData }
|
||||
}
|
||||
}
|
||||
|
||||
impl<U: uom::si::time::Unit + uom::Conversion<f64, T = f64>> UnitImpl for TimeUnitDispatcher<U> {
|
||||
type Quantity = Time;
|
||||
|
||||
fn short_name(&self) -> &'static str {
|
||||
U::abbreviation()
|
||||
}
|
||||
|
||||
fn format_value(&self, value: Time, precision: usize) -> String {
|
||||
format!("{value:.precision$}", value = value.get::<U>())
|
||||
}
|
||||
}
|
||||
|
||||
struct InformationUnitDispatcher<U: uom::si::information::Unit + uom::Conversion<f64, T = f64>> {
|
||||
u: PhantomData<U>,
|
||||
}
|
||||
|
||||
impl<U: uom::si::information::Unit + uom::Conversion<f64, T = f64>> InformationUnitDispatcher<U> {
|
||||
fn new() -> Self {
|
||||
InformationUnitDispatcher { u: PhantomData }
|
||||
}
|
||||
}
|
||||
|
||||
impl<U: uom::si::information::Unit + uom::Conversion<f64, T = f64>> UnitImpl
|
||||
for InformationUnitDispatcher<U>
|
||||
{
|
||||
type Quantity = Information;
|
||||
|
||||
fn short_name(&self) -> &'static str {
|
||||
U::abbreviation()
|
||||
}
|
||||
|
||||
fn format_value(&self, value: Information, precision: usize) -> String {
|
||||
format!("{value:.precision$}", value = value.get::<U>())
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_time_unit_short_name() {
|
||||
assert_eq!("s", TimeUnit::Second.short_name());
|
||||
assert_eq!("ms", TimeUnit::MilliSecond.short_name());
|
||||
assert_eq!("µs", TimeUnit::MicroSecond.short_name());
|
||||
assert_eq!("min", TimeUnit::Minute.short_name());
|
||||
assert_eq!("h", TimeUnit::Hour.short_name());
|
||||
}
|
||||
|
||||
// Note - the values are rounded when formatted.
|
||||
#[test]
|
||||
fn test_unit_format() {
|
||||
use crate::quantity::FormatQuantity;
|
||||
|
||||
let value = Time::new::<second>(123.456789);
|
||||
assert_eq!("123.457", value.format_value(TimeUnit::Second));
|
||||
assert_eq!("123456.8", value.format_value(TimeUnit::MilliSecond));
|
||||
|
||||
assert_eq!(
|
||||
"1234.6",
|
||||
Time::new::<second>(0.00123456).format_value(TimeUnit::MicroSecond)
|
||||
);
|
||||
assert_eq!("2.1", value.format_value(TimeUnit::Minute));
|
||||
assert_eq!("0.0", value.format_value(TimeUnit::Hour));
|
||||
}
|
||||
@@ -118,6 +118,7 @@ pub fn execute_and_measure(
|
||||
|
||||
Ok(Measurement {
|
||||
time_wall_clock,
|
||||
time_cpu: time_user + time_system,
|
||||
time_user,
|
||||
time_system,
|
||||
memory_peak_resident,
|
||||
|
||||
@@ -56,11 +56,6 @@ impl ExecutionOrderTest {
|
||||
self.command(output)
|
||||
}
|
||||
|
||||
fn reference(&mut self, output: &str) -> &mut Self {
|
||||
self.arg("--reference");
|
||||
self.command(output)
|
||||
}
|
||||
|
||||
fn conclude(&mut self, output: &str) -> &mut Self {
|
||||
self.arg("--conclude");
|
||||
self.command(output)
|
||||
@@ -373,10 +368,10 @@ fn multiple_parameter_values() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_is_executed_first() {
|
||||
fn commands_execute_in_input_order() {
|
||||
ExecutionOrderTest::new()
|
||||
.arg("--runs=1")
|
||||
.reference("reference")
|
||||
.command("reference")
|
||||
.command("command 1")
|
||||
.command("command 2")
|
||||
.expect_output("reference")
|
||||
@@ -386,10 +381,10 @@ fn reference_is_executed_first() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn reference_is_executed_separately_from_commands() {
|
||||
fn repeated_commands_execute_separately() {
|
||||
ExecutionOrderTest::new()
|
||||
.arg("--runs=1")
|
||||
.reference("command 1")
|
||||
.command("command 1")
|
||||
.command("command 1")
|
||||
.command("command 2")
|
||||
.expect_output("command 1")
|
||||
@@ -405,7 +400,7 @@ fn setup_prepare_reference_conclude_cleanup_combined() {
|
||||
.arg("--runs=2")
|
||||
.setup("setup")
|
||||
.prepare("prepare")
|
||||
.reference("reference")
|
||||
.command("reference")
|
||||
.command("command1")
|
||||
.command("command2")
|
||||
.conclude("conclude")
|
||||
@@ -497,7 +492,7 @@ fn setup_separate_prepare_reference_separate_conclude_cleanup_combined() {
|
||||
.setup("setup")
|
||||
.cleanup("cleanup")
|
||||
.prepare("prepareref")
|
||||
.reference("reference")
|
||||
.command("reference")
|
||||
.conclude("concluderef")
|
||||
.prepare("prepare1")
|
||||
.command("command1")
|
||||
|
||||
+179
-249
@@ -111,13 +111,23 @@ fn fails_with_zero_runs() {
|
||||
|
||||
#[test]
|
||||
fn min_runs_of_one_still_performs_one_run() {
|
||||
hyperfine_debug()
|
||||
let _settings = snapshot_settings().bind_to_scope();
|
||||
assert_cmd_snapshot!(hyperfine_debug()
|
||||
.arg("--style=basic")
|
||||
.arg("--metrics=time_wall_clock")
|
||||
.arg("--min-runs=1")
|
||||
.arg("--warmup=0")
|
||||
.arg("sleep 4")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("Time (abs ≡)"));
|
||||
.arg("sleep 4"), @"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Benchmark 1: sleep 4 (1 run)
|
||||
Value
|
||||
Wall Time 4.000 s
|
||||
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -159,7 +169,7 @@ fn fails_with_wrong_number_of_prepare_options() {
|
||||
.arg("--prepare=echo ref")
|
||||
.arg("--prepare=echo a")
|
||||
.arg("--prepare=echo b")
|
||||
.arg("--reference=echo ref")
|
||||
.arg("echo ref")
|
||||
.arg("echo a")
|
||||
.arg("echo b")
|
||||
.assert()
|
||||
@@ -182,7 +192,7 @@ fn fails_with_wrong_number_of_prepare_options() {
|
||||
.arg("--runs=1")
|
||||
.arg("--prepare=echo a")
|
||||
.arg("--prepare=echo b")
|
||||
.arg("--reference=echo ref")
|
||||
.arg("echo ref")
|
||||
.arg("echo a")
|
||||
.arg("echo b")
|
||||
.assert()
|
||||
@@ -208,7 +218,7 @@ fn fails_with_wrong_number_of_conclude_options() {
|
||||
.arg("--conclude=echo ref")
|
||||
.arg("--conclude=echo a")
|
||||
.arg("--conclude=echo b")
|
||||
.arg("--reference=echo ref")
|
||||
.arg("echo ref")
|
||||
.arg("echo a")
|
||||
.arg("echo b")
|
||||
.assert()
|
||||
@@ -231,7 +241,7 @@ fn fails_with_wrong_number_of_conclude_options() {
|
||||
.arg("--runs=1")
|
||||
.arg("--conclude=echo a")
|
||||
.arg("--conclude=echo b")
|
||||
.arg("--reference=echo ref")
|
||||
.arg("echo ref")
|
||||
.arg("echo a")
|
||||
.arg("echo b")
|
||||
.assert()
|
||||
@@ -509,29 +519,27 @@ fn returns_mean_time_in_correct_unit() {
|
||||
.arg("sleep 1.234")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("Time (mean ± σ): 1.234 s ±"));
|
||||
.stdout(predicate::str::is_match(r"Wall Time\s+1\.234 s\s+±").unwrap());
|
||||
|
||||
hyperfine_debug()
|
||||
.arg("sleep 0.123")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("Time (mean ± σ): 123.0 ms ±"));
|
||||
.stdout(predicate::str::is_match(r"Wall Time\s+123\.0 ms\s+±").unwrap());
|
||||
|
||||
hyperfine_debug()
|
||||
.arg("--time-unit=millisecond")
|
||||
.arg("--metrics=time_wall_clock:ms")
|
||||
.arg("sleep 1.234")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains("Time (mean ± σ): 1234.0 ms ±"));
|
||||
.stdout(predicate::str::is_match(r"Wall Time\s+1234\.0 ms\s+±").unwrap());
|
||||
|
||||
hyperfine_debug()
|
||||
.arg("--time-unit=microsecond")
|
||||
.arg("--metrics=time_wall_clock:us")
|
||||
.arg("sleep 1.234")
|
||||
.assert()
|
||||
.success()
|
||||
.stdout(predicate::str::contains(
|
||||
"Time (mean ± σ): 1234000.0 µs ±",
|
||||
));
|
||||
.stdout(predicate::str::is_match(r"Wall Time\s+1234000\.0 µs\s+±").unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -625,32 +633,57 @@ fn takes_both_preparation_and_conclusion_command_into_account_for_computing_numb
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shows_benchmark_comparison_with_relative_times() {
|
||||
fn secondary_metrics_include_factors_for_large_changes() {
|
||||
// Mock CPU time is zero; secondary wall time still gets ratio annotations.
|
||||
let output = hyperfine_debug()
|
||||
.args([
|
||||
"--metrics=time_cpu,time_wall_clock",
|
||||
"--style=basic",
|
||||
"--runs=2",
|
||||
"sleep 1",
|
||||
"sleep 10",
|
||||
"sleep 0.1",
|
||||
])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
let stdout = String::from_utf8(output.stdout).unwrap();
|
||||
assert!(stdout.contains("+900.0%"), "{}", stdout);
|
||||
assert!(stdout.contains("-90.0%"), "{}", stdout);
|
||||
assert!(stdout.contains("+900.0% (10.0x slower)"), "{}", stdout);
|
||||
assert!(stdout.contains("-90.0% (10.0x faster)"), "{}", stdout);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shows_multiple_metrics_relative_to_first_command() {
|
||||
let _settings = snapshot_settings().bind_to_scope();
|
||||
assert_cmd_snapshot!(hyperfine_debug()
|
||||
.arg("--style=basic")
|
||||
.arg("sleep 1.0")
|
||||
.arg("sleep 2.0")
|
||||
.arg("sleep 3.0"), @r"
|
||||
.arg("--metrics=time_wall_clock:ms,time_user:us,memory_peak_resident:MiB")
|
||||
.arg("sleep 0.0817")
|
||||
.arg("sleep 0.6155")
|
||||
.arg("sleep 0.1707"), @"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Benchmark 1: sleep 1.0
|
||||
Time (mean ± σ): 1.000 s ± 0.000 s [User: 0.000 s, System: 0.000 s]
|
||||
Range (min … max): 1.000 s … 1.000 s 10 runs
|
||||
Benchmark 1: sleep 0.0817 (36 runs)
|
||||
mean ± σ min … max
|
||||
Wall Time 81.7 ms ± 0.0 ms 81.7 ms … 81.7 ms
|
||||
User time 0.0 µs ± 0.0 µs 0.0 µs … 0.0 µs
|
||||
Memory 0.0 MiB ± 0.0 MiB 0.0 MiB … 0.0 MiB
|
||||
|
||||
Benchmark 2: sleep 2.0
|
||||
Time (mean ± σ): 2.000 s ± 0.000 s [User: 0.000 s, System: 0.000 s]
|
||||
Range (min … max): 2.000 s … 2.000 s 10 runs
|
||||
Benchmark 2: sleep 0.6155 (10 runs)
|
||||
mean ± σ min … max
|
||||
Wall Time 615.5 ms ± 0.0 ms 615.5 ms … 615.5 ms +653.4% (7.5x slower)
|
||||
User time 0.0 µs ± 0.0 µs 0.0 µs … 0.0 µs N/A
|
||||
Memory 0.0 MiB ± 0.0 MiB 0.0 MiB … 0.0 MiB N/A
|
||||
|
||||
Benchmark 3: sleep 3.0
|
||||
Time (mean ± σ): 3.000 s ± 0.000 s [User: 0.000 s, System: 0.000 s]
|
||||
Range (min … max): 3.000 s … 3.000 s 10 runs
|
||||
Benchmark 3: sleep 0.1707 (17 runs)
|
||||
mean ± σ min … max
|
||||
Wall Time 170.7 ms ± 0.0 ms 170.7 ms … 170.7 ms +108.9% (2.1x slower)
|
||||
User time 0.0 µs ± 0.0 µs 0.0 µs … 0.0 µs N/A
|
||||
Memory 0.0 MiB ± 0.0 MiB 0.0 MiB … 0.0 MiB N/A
|
||||
|
||||
Summary
|
||||
sleep 1.0 ran
|
||||
2.00 ± 0.00 times faster than sleep 2.0
|
||||
3.00 ± 0.00 times faster than sleep 3.0
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
@@ -660,37 +693,37 @@ fn shows_benchmark_comparison_with_relative_times() {
|
||||
fn shows_benchmark_comparison_with_same_time() {
|
||||
let _settings = snapshot_settings().bind_to_scope();
|
||||
assert_cmd_snapshot!(hyperfine_debug()
|
||||
.arg("--metrics=time_wall_clock,memory_peak_resident")
|
||||
.arg("--style=basic")
|
||||
.arg("--command-name=A")
|
||||
.arg("--command-name=B")
|
||||
.arg("sleep 1.0")
|
||||
.arg("sleep 1.0")
|
||||
.arg("sleep 2.0")
|
||||
.arg("sleep 1000.0"), @r"
|
||||
.arg("sleep 1000.0"), @"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Benchmark 1: A
|
||||
Time (mean ± σ): 1.000 s ± 0.000 s [User: 0.000 s, System: 0.000 s]
|
||||
Range (min … max): 1.000 s … 1.000 s 10 runs
|
||||
Benchmark 1: A (10 runs)
|
||||
mean ± σ min … max
|
||||
Wall Time 1.000 s ± 0.000 s 1.000 s … 1.000 s
|
||||
Memory 0.0 B ± 0.0 B 0.0 B … 0.0 B
|
||||
|
||||
Benchmark 2: B
|
||||
Time (mean ± σ): 1.000 s ± 0.000 s [User: 0.000 s, System: 0.000 s]
|
||||
Range (min … max): 1.000 s … 1.000 s 10 runs
|
||||
Benchmark 2: B (10 runs)
|
||||
mean ± σ min … max
|
||||
Wall Time 1.000 s ± 0.000 s 1.000 s … 1.000 s 0.0%
|
||||
Memory 0.0 B ± 0.0 B 0.0 B … 0.0 B N/A
|
||||
|
||||
Benchmark 3: sleep 2.0
|
||||
Time (mean ± σ): 2.000 s ± 0.000 s [User: 0.000 s, System: 0.000 s]
|
||||
Range (min … max): 2.000 s … 2.000 s 10 runs
|
||||
Benchmark 3: sleep 2.0 (10 runs)
|
||||
mean ± σ min … max
|
||||
Wall Time 2.000 s ± 0.000 s 2.000 s … 2.000 s +100.0% (2.0x slower)
|
||||
Memory 0.0 B ± 0.0 B 0.0 B … 0.0 B N/A
|
||||
|
||||
Benchmark 4: sleep 1000.0
|
||||
Time (mean ± σ): 1000.000 s ± 0.000 s [User: 0.000 s, System: 0.000 s]
|
||||
Range (min … max): 1000.000 s … 1000.000 s 10 runs
|
||||
Benchmark 4: sleep 1000.0 (10 runs)
|
||||
mean ± σ min … max
|
||||
Wall Time 1000.000 s ± 0.000 s 1000.000 s … 1000.000 s +99900.0% (1000.0x slower)
|
||||
Memory 0.0 B ± 0.0 B 0.0 B … 0.0 B N/A
|
||||
|
||||
Summary
|
||||
A ran
|
||||
As fast (1.00 ± 0.00) as B
|
||||
2.00 ± 0.00 times faster than sleep 2.0
|
||||
1000.00 ± 0.00 times faster than sleep 1000.0
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
@@ -700,36 +733,36 @@ fn shows_benchmark_comparison_with_same_time() {
|
||||
fn shows_benchmark_comparison_relative_to_reference() {
|
||||
let _settings = snapshot_settings().bind_to_scope();
|
||||
assert_cmd_snapshot!(hyperfine_debug()
|
||||
.arg("--metrics=time_wall_clock,memory_peak_resident")
|
||||
.arg("--style=basic")
|
||||
.arg("--reference=sleep 2.0")
|
||||
.arg("sleep 2.0")
|
||||
.arg("sleep 1.0")
|
||||
.arg("sleep 3.0"), @r"
|
||||
.arg("sleep 3.0"), @"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Benchmark 1: sleep 2.0
|
||||
Time (mean ± σ): 2.000 s ± 0.000 s [User: 0.000 s, System: 0.000 s]
|
||||
Range (min … max): 2.000 s … 2.000 s 10 runs
|
||||
Benchmark 1: sleep 2.0 (10 runs)
|
||||
mean ± σ min … max
|
||||
Wall Time 2.000 s ± 0.000 s 2.000 s … 2.000 s
|
||||
Memory 0.0 B ± 0.0 B 0.0 B … 0.0 B
|
||||
|
||||
Benchmark 2: sleep 1.0
|
||||
Time (mean ± σ): 1.000 s ± 0.000 s [User: 0.000 s, System: 0.000 s]
|
||||
Range (min … max): 1.000 s … 1.000 s 10 runs
|
||||
Benchmark 2: sleep 1.0 (10 runs)
|
||||
mean ± σ min … max
|
||||
Wall Time 1.000 s ± 0.000 s 1.000 s … 1.000 s -50.0% (2.0x faster)
|
||||
Memory 0.0 B ± 0.0 B 0.0 B … 0.0 B N/A
|
||||
|
||||
Benchmark 3: sleep 3.0
|
||||
Time (mean ± σ): 3.000 s ± 0.000 s [User: 0.000 s, System: 0.000 s]
|
||||
Range (min … max): 3.000 s … 3.000 s 10 runs
|
||||
Benchmark 3: sleep 3.0 (10 runs)
|
||||
mean ± σ min … max
|
||||
Wall Time 3.000 s ± 0.000 s 3.000 s … 3.000 s +50.0% (1.5x slower)
|
||||
Memory 0.0 B ± 0.0 B 0.0 B … 0.0 B N/A
|
||||
|
||||
Summary
|
||||
sleep 2.0 ran
|
||||
2.00 ± 0.00 times slower than sleep 1.0
|
||||
1.50 ± 0.00 times faster than sleep 3.0
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn command_sorted_comparison_and_markup_identify_reference() {
|
||||
fn comparison_and_markup_identify_first_command_as_reference() {
|
||||
let _settings = snapshot_settings().bind_to_scope();
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let export_path = directory.path().join("results.md");
|
||||
@@ -737,34 +770,31 @@ fn command_sorted_comparison_and_markup_identify_reference() {
|
||||
.args([
|
||||
"--style=basic",
|
||||
"--runs=1",
|
||||
"--sort=command",
|
||||
"--reference",
|
||||
"sleep 2",
|
||||
"--reference-name",
|
||||
"baseline",
|
||||
"--command-name=baseline",
|
||||
"--export-markdown",
|
||||
])
|
||||
.arg(&export_path)
|
||||
.args(["sleep 1", "sleep 2", "sleep 3"])
|
||||
.args(["sleep 2", "sleep 1", "sleep 2", "sleep 3"])
|
||||
.output()
|
||||
.unwrap();
|
||||
assert!(output.status.success(), "{:?}", output);
|
||||
let stdout = String::from_utf8(output.stdout).unwrap();
|
||||
let comparison = stdout.split_once("Relative speed comparison\n").unwrap().1;
|
||||
insta::assert_snapshot!(comparison, @r"
|
||||
1.00 baseline (reference)
|
||||
2.00 sleep 1 (faster)
|
||||
1.00 sleep 2 (same speed)
|
||||
1.50 sleep 3 (slower)
|
||||
");
|
||||
assert!(
|
||||
stdout.contains("Benchmark 1: baseline (1 run)"),
|
||||
"{}",
|
||||
stdout
|
||||
);
|
||||
assert!(stdout.contains("-50.0%"), "{}", stdout);
|
||||
assert!(stdout.contains("0.0%"), "{}", stdout);
|
||||
assert!(stdout.contains("+50.0%"), "{}", stdout);
|
||||
let markdown = std::fs::read_to_string(export_path).unwrap();
|
||||
insta::assert_snapshot!(markdown, @r"
|
||||
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|
||||
|:---|---:|---:|---:|---:|
|
||||
| `baseline` | 2.000 | 2.000 | 2.000 | 1.00 (reference) |
|
||||
| `sleep 1` | 1.000 | 1.000 | 1.000 | 2.00 (faster) |
|
||||
| `sleep 2` | 2.000 | 2.000 | 2.000 | 1.00 (same speed) |
|
||||
| `sleep 3` | 3.000 | 3.000 | 3.000 | 1.50 (slower) |
|
||||
insta::assert_snapshot!(markdown, @"
|
||||
| Command | Mean Wall Time [s] | Change | Factor |
|
||||
|:---|---:|---:|:---|
|
||||
| `baseline` | 2.000 | | |
|
||||
| `sleep 1` | 1.000 | -50.0% | (2.0x faster) |
|
||||
| `sleep 2` | 2.000 | 0.0% | |
|
||||
| `sleep 3` | 3.000 | +50.0% | (1.5x slower) |
|
||||
");
|
||||
|
||||
// Equal zero times have no meaningful relative factor.
|
||||
@@ -773,10 +803,8 @@ fn command_sorted_comparison_and_markup_identify_reference() {
|
||||
"--style=none",
|
||||
"--runs=1",
|
||||
"--export-markdown=-",
|
||||
"--reference",
|
||||
"--command-name=baseline",
|
||||
"sleep 0",
|
||||
"--reference-name",
|
||||
"baseline",
|
||||
"sleep 0",
|
||||
])
|
||||
.output()
|
||||
@@ -787,46 +815,14 @@ fn command_sorted_comparison_and_markup_identify_reference() {
|
||||
.lines()
|
||||
.find(|line| line.contains("sleep 0"))
|
||||
.unwrap();
|
||||
assert!(row.ends_with("| N/A |"), "{}", row);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shows_reference_name() {
|
||||
let _settings = snapshot_settings().bind_to_scope();
|
||||
assert_cmd_snapshot!(hyperfine_debug()
|
||||
.arg("--style=basic")
|
||||
.arg("--reference=sleep 2.0")
|
||||
.arg("--reference-name=refabc123")
|
||||
.arg("sleep 1.0")
|
||||
.arg("sleep 3.0"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Benchmark 1: refabc123
|
||||
Time (mean ± σ): 2.000 s ± 0.000 s [User: 0.000 s, System: 0.000 s]
|
||||
Range (min … max): 2.000 s … 2.000 s 10 runs
|
||||
|
||||
Benchmark 2: sleep 1.0
|
||||
Time (mean ± σ): 1.000 s ± 0.000 s [User: 0.000 s, System: 0.000 s]
|
||||
Range (min … max): 1.000 s … 1.000 s 10 runs
|
||||
|
||||
Benchmark 3: sleep 3.0
|
||||
Time (mean ± σ): 3.000 s ± 0.000 s [User: 0.000 s, System: 0.000 s]
|
||||
Range (min … max): 3.000 s … 3.000 s 10 runs
|
||||
|
||||
Summary
|
||||
refabc123 ran
|
||||
2.00 ± 0.00 times slower than sleep 1.0
|
||||
1.50 ± 0.00 times faster than sleep 3.0
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
assert!(row.ends_with("| N/A | |"), "{}", row);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn performs_all_benchmarks_in_parameter_scan() {
|
||||
let _settings = snapshot_settings().bind_to_scope();
|
||||
assert_cmd_snapshot!(hyperfine_debug()
|
||||
.arg("--metrics=time_wall_clock,memory_peak_resident")
|
||||
.arg("--style=basic")
|
||||
.arg("--parameter-scan")
|
||||
.arg("time")
|
||||
@@ -834,31 +830,30 @@ fn performs_all_benchmarks_in_parameter_scan() {
|
||||
.arg("45")
|
||||
.arg("--parameter-step-size")
|
||||
.arg("5")
|
||||
.arg("sleep {time}"), @r"
|
||||
.arg("sleep {time}"), @"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Benchmark 1: sleep 30
|
||||
Time (mean ± σ): 30.000 s ± 0.000 s [User: 0.000 s, System: 0.000 s]
|
||||
Range (min … max): 30.000 s … 30.000 s 10 runs
|
||||
Benchmark 1: sleep 30 (10 runs)
|
||||
mean ± σ min … max
|
||||
Wall Time 30.000 s ± 0.000 s 30.000 s … 30.000 s
|
||||
Memory 0.0 B ± 0.0 B 0.0 B … 0.0 B
|
||||
|
||||
Benchmark 2: sleep 35
|
||||
Time (mean ± σ): 35.000 s ± 0.000 s [User: 0.000 s, System: 0.000 s]
|
||||
Range (min … max): 35.000 s … 35.000 s 10 runs
|
||||
Benchmark 2: sleep 35 (10 runs)
|
||||
mean ± σ min … max
|
||||
Wall Time 35.000 s ± 0.000 s 35.000 s … 35.000 s +16.7%
|
||||
Memory 0.0 B ± 0.0 B 0.0 B … 0.0 B N/A
|
||||
|
||||
Benchmark 3: sleep 40
|
||||
Time (mean ± σ): 40.000 s ± 0.000 s [User: 0.000 s, System: 0.000 s]
|
||||
Range (min … max): 40.000 s … 40.000 s 10 runs
|
||||
Benchmark 3: sleep 40 (10 runs)
|
||||
mean ± σ min … max
|
||||
Wall Time 40.000 s ± 0.000 s 40.000 s … 40.000 s +33.3% (1.3x slower)
|
||||
Memory 0.0 B ± 0.0 B 0.0 B … 0.0 B N/A
|
||||
|
||||
Benchmark 4: sleep 45
|
||||
Time (mean ± σ): 45.000 s ± 0.000 s [User: 0.000 s, System: 0.000 s]
|
||||
Range (min … max): 45.000 s … 45.000 s 10 runs
|
||||
Benchmark 4: sleep 45 (10 runs)
|
||||
mean ± σ min … max
|
||||
Wall Time 45.000 s ± 0.000 s 45.000 s … 45.000 s +50.0% (1.5x slower)
|
||||
Memory 0.0 B ± 0.0 B 0.0 B … 0.0 B N/A
|
||||
|
||||
Summary
|
||||
sleep 30 ran
|
||||
1.17 ± 0.00 times faster than sleep 35
|
||||
1.33 ± 0.00 times faster than sleep 40
|
||||
1.50 ± 0.00 times faster than sleep 45
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
@@ -882,7 +877,7 @@ fn rejects_negative_parameter_steps() {
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn selects_reference_with_parameterized_prepare() {
|
||||
fn preserves_input_order_with_parameterized_prepare() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let csv_path = directory.path().join("results.csv");
|
||||
let json_path = directory.path().join("results.json");
|
||||
@@ -896,8 +891,6 @@ fn selects_reference_with_parameterized_prepare() {
|
||||
"0.2,0.4,0.6",
|
||||
"--prepare",
|
||||
"sleep {delay}",
|
||||
"--reference",
|
||||
"echo a (delay = 0.4)",
|
||||
"--export-csv",
|
||||
])
|
||||
.arg(&csv_path)
|
||||
@@ -912,7 +905,7 @@ fn selects_reference_with_parameterized_prepare() {
|
||||
for (number, delay) in ["0.2", "0.4", "0.6"].iter().enumerate() {
|
||||
assert!(
|
||||
stdout.contains(&format!(
|
||||
"Benchmark {}: echo a (delay = {})",
|
||||
"Benchmark {}: echo a (delay = {}) (1 run)",
|
||||
number + 1,
|
||||
delay
|
||||
)),
|
||||
@@ -920,7 +913,16 @@ fn selects_reference_with_parameterized_prepare() {
|
||||
stdout
|
||||
);
|
||||
}
|
||||
assert!(stdout.contains("echo a (delay = 0.4) ran"), "{}", stdout);
|
||||
assert!(
|
||||
!stdout
|
||||
.split("Benchmark 2")
|
||||
.next()
|
||||
.unwrap()
|
||||
.contains("Change"),
|
||||
"{}",
|
||||
stdout
|
||||
);
|
||||
assert!(!stdout.contains("Change vs #1"));
|
||||
|
||||
let mut csv = csv::Reader::from_path(csv_path).unwrap();
|
||||
assert!(csv
|
||||
@@ -946,14 +948,21 @@ fn selects_reference_with_parameterized_prepare() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_unmatched_reference_before_export_or_setup() {
|
||||
fn rejects_invalid_metric_before_export_or_setup() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let export_path = directory.path().join("results.csv");
|
||||
let marker_path = directory.path().join("setup-ran");
|
||||
std::fs::write(&export_path, "previous contents").unwrap();
|
||||
|
||||
hyperfine()
|
||||
.args(["--reference", "sleep 1", "-P", "secs", "2", "3", "--setup"])
|
||||
.args([
|
||||
"--metrics=memory_peak_resident:ms",
|
||||
"-P",
|
||||
"secs",
|
||||
"2",
|
||||
"3",
|
||||
"--setup",
|
||||
])
|
||||
.arg(format!("echo touched > \"{}\"", marker_path.display()))
|
||||
.arg("--export-csv")
|
||||
.arg(&export_path)
|
||||
@@ -962,7 +971,7 @@ fn rejects_unmatched_reference_before_export_or_setup() {
|
||||
.failure()
|
||||
.stdout(predicate::str::is_empty())
|
||||
.stderr(predicate::str::contains(
|
||||
"does not match any parameterized benchmark",
|
||||
"Unit 'ms' is not valid for metric 'memory_peak_resident'",
|
||||
));
|
||||
assert_eq!(
|
||||
std::fs::read_to_string(export_path).unwrap(),
|
||||
@@ -971,29 +980,9 @@ fn rejects_unmatched_reference_before_export_or_setup() {
|
||||
assert!(!marker_path.exists());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rejects_ambiguous_parameterized_reference() {
|
||||
hyperfine_debug()
|
||||
.args([
|
||||
"-L",
|
||||
"x",
|
||||
"1,2",
|
||||
"--command-name",
|
||||
"case",
|
||||
"--reference",
|
||||
"case",
|
||||
"sleep {x}",
|
||||
])
|
||||
.assert()
|
||||
.failure()
|
||||
.stderr(predicate::str::contains(
|
||||
"matches multiple parameterized benchmarks",
|
||||
));
|
||||
}
|
||||
|
||||
#[cfg(unix)]
|
||||
#[test]
|
||||
fn intermediate_markdown_waits_for_selected_reference() {
|
||||
fn intermediate_markdown_retains_completed_reference() {
|
||||
let directory = tempfile::tempdir().unwrap();
|
||||
let export_path = directory.path().join("results.md");
|
||||
hyperfine()
|
||||
@@ -1004,8 +993,6 @@ fn intermediate_markdown_waits_for_selected_reference() {
|
||||
"-L",
|
||||
"delay",
|
||||
"0.01,0.02,0.03",
|
||||
"--reference",
|
||||
"sleep 0.02",
|
||||
"--prepare",
|
||||
"true",
|
||||
"--prepare",
|
||||
@@ -1023,11 +1010,11 @@ fn intermediate_markdown_waits_for_selected_reference() {
|
||||
.lines()
|
||||
.find(|line| line.contains("sleep 0.01"))
|
||||
.unwrap();
|
||||
assert!(row.ends_with("| N/A |"), "{}", row);
|
||||
assert!(row.ends_with("| | |"), "{}", row);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn markdown_export_uses_selected_parameterized_reference() {
|
||||
fn markdown_export_uses_first_parameterized_benchmark_as_reference() {
|
||||
let _settings = snapshot_settings().bind_to_scope();
|
||||
assert_cmd_snapshot!(hyperfine_debug()
|
||||
.args([
|
||||
@@ -1040,19 +1027,17 @@ fn markdown_export_uses_selected_parameterized_reference() {
|
||||
"3",
|
||||
"--command-name",
|
||||
"case-{x}",
|
||||
"--reference",
|
||||
"case-2",
|
||||
"sleep {x}",
|
||||
]), @r"
|
||||
]), @"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|
||||
|:---|---:|---:|---:|---:|
|
||||
| `case-1` | 1.000 | 1.000 | 1.000 | 2.00 (faster) |
|
||||
| `case-2` | 2.000 | 2.000 | 2.000 | 1.00 (reference) |
|
||||
| `case-3` | 3.000 | 3.000 | 3.000 | 1.50 (slower) |
|
||||
| Command | Mean Wall Time [s] | Change | Factor |
|
||||
|:---|---:|---:|:---|
|
||||
| `case-1` | 1.000 | | |
|
||||
| `case-2` | 2.000 | +100.0% | (2.0x slower) |
|
||||
| `case-3` | 3.000 | +200.0% | (3.0x slower) |
|
||||
|
||||
|
||||
----- stderr -----
|
||||
@@ -1067,15 +1052,15 @@ fn intermediate_results_are_not_exported_to_stdout() {
|
||||
.arg("--export-markdown")
|
||||
.arg("-")
|
||||
.arg("sleep 1")
|
||||
.arg("sleep 2"), @r"
|
||||
.arg("sleep 2"), @"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|
||||
|:---|---:|---:|---:|---:|
|
||||
| `sleep 1` | 1.000 ± 0.000 | 1.000 | 1.000 | 1.00 |
|
||||
| `sleep 2` | 2.000 ± 0.000 | 2.000 | 2.000 | 2.00 ± 0.00 |
|
||||
| Command | Mean Wall Time [s] | Change | Factor |
|
||||
|:---|---:|---:|:---|
|
||||
| `sleep 1` | 1.000 ± 0.000 | | |
|
||||
| `sleep 2` | 2.000 ± 0.000 | +100.0% | (2.0x slower) |
|
||||
|
||||
|
||||
----- stderr -----
|
||||
@@ -1158,9 +1143,9 @@ fn markdown_export_preserves_backticks_and_pipes_in_command_names() {
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
|
||||
| Command | Mean [s] | Min [s] | Max [s] | Relative |
|
||||
|:---|---:|---:|---:|---:|
|
||||
| `` echo `uname` \| cat `` | 1.000 ± 0.000 | 1.000 | 1.000 | 1.00 |
|
||||
| Command | Mean Wall Time [s] | Change | Factor |
|
||||
|:---|---:|---:|:---|
|
||||
| `` echo `uname` \| cat `` | 1.000 ± 0.000 | | |
|
||||
|
||||
|
||||
----- stderr -----
|
||||
@@ -1183,60 +1168,6 @@ fn unused_parameters_are_shown_in_benchmark_name() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn speed_comparison_sort_order() {
|
||||
let _settings = snapshot_settings().bind_to_scope();
|
||||
insta::allow_duplicates! {
|
||||
for sort_order in ["auto", "mean-time"] {
|
||||
assert_cmd_snapshot!(hyperfine_debug()
|
||||
.arg("--style=basic")
|
||||
.arg("sleep 2")
|
||||
.arg("sleep 1")
|
||||
.arg(format!("--sort={sort_order}")), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Benchmark 1: sleep 2
|
||||
Time (mean ± σ): 2.000 s ± 0.000 s [User: 0.000 s, System: 0.000 s]
|
||||
Range (min … max): 2.000 s … 2.000 s 10 runs
|
||||
|
||||
Benchmark 2: sleep 1
|
||||
Time (mean ± σ): 1.000 s ± 0.000 s [User: 0.000 s, System: 0.000 s]
|
||||
Range (min … max): 1.000 s … 1.000 s 10 runs
|
||||
|
||||
Summary
|
||||
sleep 1 ran
|
||||
2.00 ± 0.00 times faster than sleep 2
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
}
|
||||
}
|
||||
|
||||
assert_cmd_snapshot!(hyperfine_debug()
|
||||
.arg("--style=basic")
|
||||
.arg("sleep 2")
|
||||
.arg("sleep 1")
|
||||
.arg("--sort=command"), @r"
|
||||
success: true
|
||||
exit_code: 0
|
||||
----- stdout -----
|
||||
Benchmark 1: sleep 2
|
||||
Time (mean ± σ): 2.000 s ± 0.000 s [User: 0.000 s, System: 0.000 s]
|
||||
Range (min … max): 2.000 s … 2.000 s 10 runs
|
||||
|
||||
Benchmark 2: sleep 1
|
||||
Time (mean ± σ): 1.000 s ± 0.000 s [User: 0.000 s, System: 0.000 s]
|
||||
Range (min … max): 1.000 s … 1.000 s 10 runs
|
||||
|
||||
Relative speed comparison
|
||||
2.00 ± 0.00 sleep 2
|
||||
1.00 sleep 1
|
||||
|
||||
----- stderr -----
|
||||
");
|
||||
}
|
||||
|
||||
#[cfg(windows)]
|
||||
#[test]
|
||||
fn windows_quote_args() {
|
||||
@@ -1308,8 +1239,8 @@ fn json_export_basic() {
|
||||
"--export-json=-",
|
||||
"--runs=2",
|
||||
"--warmup=1",
|
||||
"--reference-name=one second",
|
||||
"--reference=sleep 1",
|
||||
"--command-name=one second",
|
||||
"sleep 1",
|
||||
"--command-name=sleep 2",
|
||||
"sleep 2",
|
||||
]));
|
||||
@@ -1338,7 +1269,6 @@ fn json_export_parameterized_with_reference() {
|
||||
"duration",
|
||||
"1,2",
|
||||
"--command-name=sleep for {duration} seconds",
|
||||
"--reference=sleep for 2 seconds",
|
||||
"sleep {duration}",
|
||||
]));
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: tests/integration_tests.rs
|
||||
assertion_line: 1278
|
||||
info:
|
||||
program: hyperfine
|
||||
args:
|
||||
@@ -8,8 +9,8 @@ info:
|
||||
- "--export-json=-"
|
||||
- "--runs=2"
|
||||
- "--warmup=1"
|
||||
- "--reference-name=one second"
|
||||
- "--reference=sleep 1"
|
||||
- "--command-name=one second"
|
||||
- sleep 1
|
||||
- "--command-name=sleep 2"
|
||||
- sleep 2
|
||||
---
|
||||
@@ -27,6 +28,7 @@ exit_code: 0
|
||||
"architecture": "[architecture]"
|
||||
}
|
||||
},
|
||||
"primary_metric": "time_wall_clock",
|
||||
"results": [
|
||||
{
|
||||
"command": "sleep 1",
|
||||
@@ -37,6 +39,10 @@ exit_code: 0
|
||||
"value": 1.0,
|
||||
"unit": "second"
|
||||
},
|
||||
"time_cpu": {
|
||||
"value": 0.0,
|
||||
"unit": "second"
|
||||
},
|
||||
"time_user": {
|
||||
"value": 0.0,
|
||||
"unit": "second"
|
||||
@@ -56,6 +62,10 @@ exit_code: 0
|
||||
"value": 1.0,
|
||||
"unit": "second"
|
||||
},
|
||||
"time_cpu": {
|
||||
"value": 0.0,
|
||||
"unit": "second"
|
||||
},
|
||||
"time_user": {
|
||||
"value": 0.0,
|
||||
"unit": "second"
|
||||
@@ -71,18 +81,17 @@ exit_code: 0
|
||||
"exit_code": 0
|
||||
}
|
||||
],
|
||||
"is_reference": true,
|
||||
"summary": {
|
||||
"time_wall_clock": {
|
||||
"unit": "second",
|
||||
"memory_peak_resident": {
|
||||
"unit": "byte",
|
||||
"count": 2,
|
||||
"mean": 1.0,
|
||||
"mean": 0.0,
|
||||
"stddev": 0.0,
|
||||
"median": 1.0,
|
||||
"min": 1.0,
|
||||
"max": 1.0
|
||||
"median": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 0.0
|
||||
},
|
||||
"time_user": {
|
||||
"time_cpu": {
|
||||
"unit": "second",
|
||||
"count": 2,
|
||||
"mean": 0.0,
|
||||
@@ -100,14 +109,23 @@ exit_code: 0
|
||||
"min": 0.0,
|
||||
"max": 0.0
|
||||
},
|
||||
"memory_peak_resident": {
|
||||
"unit": "byte",
|
||||
"time_user": {
|
||||
"unit": "second",
|
||||
"count": 2,
|
||||
"mean": 0.0,
|
||||
"stddev": 0.0,
|
||||
"median": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 0.0
|
||||
},
|
||||
"time_wall_clock": {
|
||||
"unit": "second",
|
||||
"count": 2,
|
||||
"mean": 1.0,
|
||||
"stddev": 0.0,
|
||||
"median": 1.0,
|
||||
"min": 1.0,
|
||||
"max": 1.0
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -119,6 +137,10 @@ exit_code: 0
|
||||
"value": 2.0,
|
||||
"unit": "second"
|
||||
},
|
||||
"time_cpu": {
|
||||
"value": 0.0,
|
||||
"unit": "second"
|
||||
},
|
||||
"time_user": {
|
||||
"value": 0.0,
|
||||
"unit": "second"
|
||||
@@ -138,6 +160,10 @@ exit_code: 0
|
||||
"value": 2.0,
|
||||
"unit": "second"
|
||||
},
|
||||
"time_cpu": {
|
||||
"value": 0.0,
|
||||
"unit": "second"
|
||||
},
|
||||
"time_user": {
|
||||
"value": 0.0,
|
||||
"unit": "second"
|
||||
@@ -154,16 +180,16 @@ exit_code: 0
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"time_wall_clock": {
|
||||
"unit": "second",
|
||||
"memory_peak_resident": {
|
||||
"unit": "byte",
|
||||
"count": 2,
|
||||
"mean": 2.0,
|
||||
"mean": 0.0,
|
||||
"stddev": 0.0,
|
||||
"median": 2.0,
|
||||
"min": 2.0,
|
||||
"max": 2.0
|
||||
"median": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 0.0
|
||||
},
|
||||
"time_user": {
|
||||
"time_cpu": {
|
||||
"unit": "second",
|
||||
"count": 2,
|
||||
"mean": 0.0,
|
||||
@@ -181,14 +207,23 @@ exit_code: 0
|
||||
"min": 0.0,
|
||||
"max": 0.0
|
||||
},
|
||||
"memory_peak_resident": {
|
||||
"unit": "byte",
|
||||
"time_user": {
|
||||
"unit": "second",
|
||||
"count": 2,
|
||||
"mean": 0.0,
|
||||
"stddev": 0.0,
|
||||
"median": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 0.0
|
||||
},
|
||||
"time_wall_clock": {
|
||||
"unit": "second",
|
||||
"count": 2,
|
||||
"mean": 2.0,
|
||||
"stddev": 0.0,
|
||||
"median": 2.0,
|
||||
"min": 2.0,
|
||||
"max": 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: tests/integration_tests.rs
|
||||
assertion_line: 1305
|
||||
info:
|
||||
program: hyperfine
|
||||
args:
|
||||
@@ -11,7 +12,6 @@ info:
|
||||
- duration
|
||||
- "1,2"
|
||||
- "--command-name=sleep for {duration} seconds"
|
||||
- "--reference=sleep for 2 seconds"
|
||||
- "sleep {duration}"
|
||||
---
|
||||
success: true
|
||||
@@ -28,6 +28,7 @@ exit_code: 0
|
||||
"architecture": "[architecture]"
|
||||
}
|
||||
},
|
||||
"primary_metric": "time_wall_clock",
|
||||
"results": [
|
||||
{
|
||||
"command": "sleep 1",
|
||||
@@ -38,6 +39,10 @@ exit_code: 0
|
||||
"value": 1.0,
|
||||
"unit": "second"
|
||||
},
|
||||
"time_cpu": {
|
||||
"value": 0.0,
|
||||
"unit": "second"
|
||||
},
|
||||
"time_user": {
|
||||
"value": 0.0,
|
||||
"unit": "second"
|
||||
@@ -57,6 +62,10 @@ exit_code: 0
|
||||
"value": 1.0,
|
||||
"unit": "second"
|
||||
},
|
||||
"time_cpu": {
|
||||
"value": 0.0,
|
||||
"unit": "second"
|
||||
},
|
||||
"time_user": {
|
||||
"value": 0.0,
|
||||
"unit": "second"
|
||||
@@ -78,16 +87,16 @@ exit_code: 0
|
||||
}
|
||||
},
|
||||
"summary": {
|
||||
"time_wall_clock": {
|
||||
"unit": "second",
|
||||
"memory_peak_resident": {
|
||||
"unit": "byte",
|
||||
"count": 2,
|
||||
"mean": 1.0,
|
||||
"mean": 0.0,
|
||||
"stddev": 0.0,
|
||||
"median": 1.0,
|
||||
"min": 1.0,
|
||||
"max": 1.0
|
||||
"median": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 0.0
|
||||
},
|
||||
"time_user": {
|
||||
"time_cpu": {
|
||||
"unit": "second",
|
||||
"count": 2,
|
||||
"mean": 0.0,
|
||||
@@ -105,14 +114,23 @@ exit_code: 0
|
||||
"min": 0.0,
|
||||
"max": 0.0
|
||||
},
|
||||
"memory_peak_resident": {
|
||||
"unit": "byte",
|
||||
"time_user": {
|
||||
"unit": "second",
|
||||
"count": 2,
|
||||
"mean": 0.0,
|
||||
"stddev": 0.0,
|
||||
"median": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 0.0
|
||||
},
|
||||
"time_wall_clock": {
|
||||
"unit": "second",
|
||||
"count": 2,
|
||||
"mean": 1.0,
|
||||
"stddev": 0.0,
|
||||
"median": 1.0,
|
||||
"min": 1.0,
|
||||
"max": 1.0
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -125,6 +143,10 @@ exit_code: 0
|
||||
"value": 2.0,
|
||||
"unit": "second"
|
||||
},
|
||||
"time_cpu": {
|
||||
"value": 0.0,
|
||||
"unit": "second"
|
||||
},
|
||||
"time_user": {
|
||||
"value": 0.0,
|
||||
"unit": "second"
|
||||
@@ -144,6 +166,10 @@ exit_code: 0
|
||||
"value": 2.0,
|
||||
"unit": "second"
|
||||
},
|
||||
"time_cpu": {
|
||||
"value": 0.0,
|
||||
"unit": "second"
|
||||
},
|
||||
"time_user": {
|
||||
"value": 0.0,
|
||||
"unit": "second"
|
||||
@@ -164,18 +190,17 @@ exit_code: 0
|
||||
"value": "2"
|
||||
}
|
||||
},
|
||||
"is_reference": true,
|
||||
"summary": {
|
||||
"time_wall_clock": {
|
||||
"unit": "second",
|
||||
"memory_peak_resident": {
|
||||
"unit": "byte",
|
||||
"count": 2,
|
||||
"mean": 2.0,
|
||||
"mean": 0.0,
|
||||
"stddev": 0.0,
|
||||
"median": 2.0,
|
||||
"min": 2.0,
|
||||
"max": 2.0
|
||||
"median": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 0.0
|
||||
},
|
||||
"time_user": {
|
||||
"time_cpu": {
|
||||
"unit": "second",
|
||||
"count": 2,
|
||||
"mean": 0.0,
|
||||
@@ -193,14 +218,23 @@ exit_code: 0
|
||||
"min": 0.0,
|
||||
"max": 0.0
|
||||
},
|
||||
"memory_peak_resident": {
|
||||
"unit": "byte",
|
||||
"time_user": {
|
||||
"unit": "second",
|
||||
"count": 2,
|
||||
"mean": 0.0,
|
||||
"stddev": 0.0,
|
||||
"median": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 0.0
|
||||
},
|
||||
"time_wall_clock": {
|
||||
"unit": "second",
|
||||
"count": 2,
|
||||
"mean": 2.0,
|
||||
"stddev": 0.0,
|
||||
"median": 2.0,
|
||||
"min": 2.0,
|
||||
"max": 2.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
---
|
||||
source: tests/integration_tests.rs
|
||||
assertion_line: 1294
|
||||
info:
|
||||
program: hyperfine
|
||||
args:
|
||||
@@ -23,6 +24,7 @@ exit_code: 0
|
||||
"architecture": "[architecture]"
|
||||
}
|
||||
},
|
||||
"primary_metric": "time_wall_clock",
|
||||
"results": [
|
||||
{
|
||||
"command": "sleep 1",
|
||||
@@ -32,6 +34,10 @@ exit_code: 0
|
||||
"value": 1.0,
|
||||
"unit": "second"
|
||||
},
|
||||
"time_cpu": {
|
||||
"value": 0.0,
|
||||
"unit": "second"
|
||||
},
|
||||
"time_user": {
|
||||
"value": 0.0,
|
||||
"unit": "second"
|
||||
@@ -48,16 +54,16 @@ exit_code: 0
|
||||
}
|
||||
],
|
||||
"summary": {
|
||||
"time_wall_clock": {
|
||||
"unit": "second",
|
||||
"memory_peak_resident": {
|
||||
"unit": "byte",
|
||||
"count": 1,
|
||||
"mean": 1.0,
|
||||
"mean": 0.0,
|
||||
"stddev": null,
|
||||
"median": 1.0,
|
||||
"min": 1.0,
|
||||
"max": 1.0
|
||||
"median": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 0.0
|
||||
},
|
||||
"time_user": {
|
||||
"time_cpu": {
|
||||
"unit": "second",
|
||||
"count": 1,
|
||||
"mean": 0.0,
|
||||
@@ -75,14 +81,23 @@ exit_code: 0
|
||||
"min": 0.0,
|
||||
"max": 0.0
|
||||
},
|
||||
"memory_peak_resident": {
|
||||
"unit": "byte",
|
||||
"time_user": {
|
||||
"unit": "second",
|
||||
"count": 1,
|
||||
"mean": 0.0,
|
||||
"stddev": null,
|
||||
"median": 0.0,
|
||||
"min": 0.0,
|
||||
"max": 0.0
|
||||
},
|
||||
"time_wall_clock": {
|
||||
"unit": "second",
|
||||
"count": 1,
|
||||
"mean": 1.0,
|
||||
"stddev": null,
|
||||
"median": 1.0,
|
||||
"min": 1.0,
|
||||
"max": 1.0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user