Skip to content

Commit eadf7bb

Browse files
Евгений БлиновЕвгений Блинов
authored andcommitted
Add full documentation with examples and feature comparison
1 parent a08b7bb commit eadf7bb

File tree

1 file changed

+324
-1
lines changed

1 file changed

+324
-1
lines changed

README.md

Lines changed: 324 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,324 @@
1-
# microbenchmark
1+
# microbenchmark
2+
3+
A minimal Python library for writing and running benchmarks.
4+
5+
`microbenchmark` gives you simple building blocks — `Scenario`, `ScenarioGroup`, and `BenchmarkResult` — that you can embed directly into your project or call from CI. There is no CLI tool to install and no configuration to manage. You write a Python file, call `.run()` or `.cli()`, and you're done.
6+
7+
**Key features:**
8+
9+
- A `Scenario` wraps any callable with a fixed argument list and runs it `n` times, collecting per-run timings.
10+
- A `ScenarioGroup` lets you combine scenarios and run them together.
11+
- `BenchmarkResult` holds every individual duration and gives you mean, best, worst, and percentile views.
12+
- Results can be serialised to and restored from JSON.
13+
- No external dependencies beyond the Python standard library.
14+
15+
---
16+
17+
## Table of contents
18+
19+
- [Installation](#installation)
20+
- [Quick start](#quick-start)
21+
- [Scenario](#scenario)
22+
- [ScenarioGroup](#scenariogroup)
23+
- [BenchmarkResult](#benchmarkresult)
24+
- [Comparison with alternatives](#comparison-with-alternatives)
25+
26+
---
27+
28+
## Installation
29+
30+
```
31+
pip install microbenchmark
32+
```
33+
34+
---
35+
36+
## Quick start
37+
38+
```python
39+
from microbenchmark import Scenario
40+
41+
def build_list():
42+
return list(range(1000))
43+
44+
scenario = Scenario(build_list, name='build_list', number=500)
45+
result = scenario.run()
46+
47+
print(result.mean)
48+
#> 0.000012 (example value, actual result will vary)
49+
print(result.best)
50+
#> 0.000010
51+
print(result.worst)
52+
#> 0.000018
53+
```
54+
55+
---
56+
57+
## Scenario
58+
59+
A `Scenario` describes a single benchmark: the function to call, what arguments to pass, and how many times to run it.
60+
61+
### Constructor
62+
63+
```python
64+
Scenario(
65+
function,
66+
args=None,
67+
*,
68+
name,
69+
doc='',
70+
number=1000,
71+
timer=time.perf_counter,
72+
)
73+
```
74+
75+
- `function` — the callable to benchmark.
76+
- `args` — a list of positional arguments to pass on each call. `None` (the default) means the function is called with no arguments. The list is copied on construction, so mutating it afterwards has no effect.
77+
- `name` — a short label for this scenario (required).
78+
- `doc` — an optional longer description.
79+
- `number` — how many times to call `function` per run. Must be at least `1`.
80+
- `timer` — a callable that returns the current time as a `float`. Defaults to `time.perf_counter`. Useful for injecting a controlled clock in tests.
81+
82+
```python
83+
scenario = Scenario(
84+
sorted,
85+
args=[[3, 1, 2]],
86+
name='sort_three_items',
87+
doc='Sort a list of three integers.',
88+
number=10000,
89+
)
90+
```
91+
92+
### `run(warmup=0)`
93+
94+
Runs the benchmark and returns a `BenchmarkResult`.
95+
96+
The optional `warmup` argument specifies how many calls to make before timing begins. Warm-up calls execute the function and consume timer ticks, but their timings are not included in the result.
97+
98+
```python
99+
result = scenario.run(warmup=100)
100+
print(len(result.durations))
101+
#> 10000
102+
```
103+
104+
### `cli()`
105+
106+
Turns the scenario into a small command-line programme. Call `scenario.cli()` as the entry point of a script and it will parse `sys.argv`, run the benchmark, and print the result.
107+
108+
Supported arguments:
109+
110+
- `--number N` — override the scenario's `number` for this run.
111+
- `--max-mean THRESHOLD` — exit with code `1` if the mean time (in seconds) exceeds `THRESHOLD`. Useful in CI.
112+
113+
```python
114+
# benchmark.py
115+
from microbenchmark import Scenario
116+
117+
def build_list():
118+
return list(range(1000))
119+
120+
scenario = Scenario(build_list, name='build_list', number=500)
121+
122+
if __name__ == '__main__':
123+
scenario.cli()
124+
```
125+
126+
```
127+
$ python benchmark.py --number 1000
128+
benchmark: build_list
129+
mean: 0.000012s
130+
best: 0.000010s
131+
worst: 0.000018s
132+
```
133+
134+
```
135+
$ python benchmark.py --max-mean 0.001
136+
benchmark: build_list
137+
mean: 0.000012s
138+
best: 0.000010s
139+
worst: 0.000018s
140+
```
141+
142+
```
143+
$ python benchmark.py --max-mean 0.000001
144+
benchmark: build_list
145+
mean: 0.000012s
146+
best: 0.000010s
147+
worst: 0.000018s
148+
$ echo $?
149+
#> 1
150+
```
151+
152+
---
153+
154+
## ScenarioGroup
155+
156+
A `ScenarioGroup` holds a flat collection of scenarios and lets you run them together.
157+
158+
### Creating a group
159+
160+
There are four ways to create a group.
161+
162+
**Direct construction** — pass any number of scenarios to the constructor:
163+
164+
```python
165+
from microbenchmark import Scenario, ScenarioGroup
166+
167+
s1 = Scenario(lambda: None, name='s1')
168+
s2 = Scenario(lambda: None, name='s2')
169+
170+
group = ScenarioGroup(s1, s2)
171+
```
172+
173+
**The `+` operator between scenarios** — adding two or more `Scenario` objects produces a `ScenarioGroup`:
174+
175+
```python
176+
group = s1 + s2
177+
```
178+
179+
**Adding a scenario to a group** — the result is always a flat group:
180+
181+
```python
182+
s3 = Scenario(lambda: None, name='s3')
183+
group = s1 + s2 + s3
184+
print(type(group).__name__)
185+
#> ScenarioGroup
186+
```
187+
188+
**Adding two groups together** — the result is a single flat group containing the scenarios from both:
189+
190+
```python
191+
g1 = ScenarioGroup(s1)
192+
g2 = ScenarioGroup(s2, s3)
193+
combined = g1 + g2
194+
print(len(combined.run()))
195+
#> 3
196+
```
197+
198+
### `run(warmup=0)`
199+
200+
Runs every scenario in order and returns a list of `BenchmarkResult` objects. The order in the list matches the order the scenarios were added.
201+
202+
```python
203+
results = group.run(warmup=50)
204+
for result in results:
205+
print(result.scenario.name, result.mean)
206+
#> s1 ...
207+
#> s2 ...
208+
#> s3 ...
209+
```
210+
211+
### `cli()`
212+
213+
Runs all scenarios and prints their results separated by dividers.
214+
215+
Supported arguments:
216+
217+
- `--number N` — passed to every scenario.
218+
- `--max-mean THRESHOLD` — exits with code `1` if any scenario's mean exceeds the threshold.
219+
220+
```python
221+
# benchmarks.py
222+
from microbenchmark import Scenario, ScenarioGroup
223+
224+
s1 = Scenario(lambda: list(range(100)), name='range_100')
225+
s2 = Scenario(lambda: list(range(1000)), name='range_1000')
226+
227+
group = s1 + s2
228+
229+
if __name__ == '__main__':
230+
group.cli()
231+
```
232+
233+
```
234+
$ python benchmarks.py
235+
benchmark: range_100
236+
mean: 0.000003s
237+
best: 0.000002s
238+
worst: 0.000005s
239+
---
240+
benchmark: range_1000
241+
mean: 0.000012s
242+
best: 0.000010s
243+
worst: 0.000018s
244+
```
245+
246+
---
247+
248+
## BenchmarkResult
249+
250+
`BenchmarkResult` is a dataclass that holds the outcome of a single benchmark run.
251+
252+
### Fields
253+
254+
- `scenario` — the `Scenario` that produced this result, or `None` if the result was restored from JSON.
255+
- `durations` — a tuple of per-call timings in seconds, one entry per call.
256+
- `mean` — arithmetic mean of `durations`, computed with `math.fsum` to minimise floating-point error.
257+
- `best` — the shortest individual timing.
258+
- `worst` — the longest individual timing.
259+
- `is_primary``True` for results returned directly by `run()`, `False` for results derived via `percentile()`.
260+
261+
```python
262+
result = Scenario(lambda: None, name='noop', number=100).run()
263+
print(len(result.durations))
264+
#> 100
265+
print(result.is_primary)
266+
#> True
267+
```
268+
269+
### `percentile(p)`
270+
271+
Returns a new `BenchmarkResult` containing only the fastest `ceil(len(durations) * p / 100)` timings. The returned result has `is_primary=False`.
272+
273+
```python
274+
trimmed = result.percentile(95)
275+
print(trimmed.is_primary)
276+
#> False
277+
print(len(trimmed.durations) <= len(result.durations))
278+
#> True
279+
```
280+
281+
`p` must be in the range `(0, 100]`. Passing `0` or a value above `100` raises `ValueError`.
282+
283+
### `p95` and `p99`
284+
285+
Convenient cached properties that return `percentile(95)` and `percentile(99)` respectively. The value is computed once and cached for the lifetime of the result object.
286+
287+
```python
288+
print(result.p95.mean <= result.mean)
289+
#> True
290+
```
291+
292+
### `to_json()` and `from_json()`
293+
294+
`to_json()` serialises the result to a JSON string. It stores all individual `durations`, `is_primary`, and the scenario's `name`, `doc`, and `number`.
295+
296+
`from_json()` restores a `BenchmarkResult` from a JSON string produced by `to_json()`. Because the original callable cannot be serialised, the restored result has `scenario=None`.
297+
298+
```python
299+
json_str = result.to_json()
300+
restored = BenchmarkResult.from_json(json_str)
301+
302+
print(restored.scenario)
303+
#> None
304+
print(restored.mean == result.mean)
305+
#> True
306+
print(restored.durations == result.durations)
307+
#> True
308+
```
309+
310+
---
311+
312+
## Comparison with alternatives
313+
314+
| Feature | `microbenchmark` | `timeit` (stdlib) | `pytest-benchmark` |
315+
|---|---|---|---|
316+
| Per-call timings | yes | no | yes |
317+
| Percentile views | yes | no | yes |
318+
| JSON serialisation | yes | no | no |
319+
| CI integration (`--max-mean`) | yes | no | via plugins |
320+
| `+` operator for grouping | yes | no | no |
321+
| External dependencies | none | none | several |
322+
| Embeddable in your own code | yes | yes | test suite only |
323+
324+
`timeit` from the standard library is great for interactive exploration but gives you only a single aggregate number. `pytest-benchmark` is powerful but is tightly coupled to the `pytest` runner and brings its own dependencies. `microbenchmark` occupies the space between: richer than `timeit`, lighter than `pytest-benchmark`, and not tied to any test framework.

0 commit comments

Comments
 (0)