Skip to content

Commit 05c4e1f

Browse files
Евгений БлиновЕвгений Блинов
authored andcommitted
Better readme
1 parent 728d0bb commit 05c4e1f

File tree

1 file changed

+114
-42
lines changed

1 file changed

+114
-42
lines changed

README.md

Lines changed: 114 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -2,14 +2,14 @@
22

33
A minimal Python library for writing and running benchmarks.
44

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.
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 are done.
66

77
**Key features:**
88

99
- 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.
10+
- A `ScenarioGroup` lets you combine scenarios and run them together with a single call.
1111
- `BenchmarkResult` holds every individual duration and gives you mean, best, worst, and percentile views.
12-
- Results can be serialised to and restored from JSON.
12+
- Results can be serialized to and restored from JSON.
1313
- No external dependencies beyond the Python standard library.
1414

1515
---
@@ -44,12 +44,11 @@ def build_list():
4444
scenario = Scenario(build_list, name='build_list', number=500)
4545
result = scenario.run()
4646

47-
print(result.mean)
48-
#> 0.000012 (example value, actual result will vary)
47+
print(result.mean) # example — actual value depends on your hardware
4948
print(result.best)
50-
#> 0.000010
5149
print(result.worst)
52-
#> 0.000018
50+
print(len(result.durations))
51+
#> 500
5352
```
5453

5554
---
@@ -68,18 +67,20 @@ Scenario(
6867
name,
6968
doc='',
7069
number=1000,
71-
timer=time.perf_counter,
70+
timer=..., # defaults to time.perf_counter
7271
)
7372
```
7473

7574
- `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.
75+
- `args` — a list of positional arguments passed to `function` on every call. `None` (the default) and `[]` both mean the function is called with no positional arguments. The list is shallow-copied on construction, so appending to your original list afterward has no effect. Keyword arguments are not supported; wrap your callable in a `functools.partial` or a lambda if you need them.
7776
- `name` — a short label for this scenario (required).
7877
- `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.
78+
- `number` — how many times to call `function` per run. Must be at least `1`; passing `0` or a negative value raises `ValueError`.
79+
- `timer` — a zero-argument callable that returns the current time as a `float`. Defaults to `time.perf_counter`. Useful for injecting a controlled clock in tests.
8180

8281
```python
82+
from microbenchmark import Scenario
83+
8384
scenario = Scenario(
8485
sorted,
8586
args=[[3, 1, 2]],
@@ -89,26 +90,54 @@ scenario = Scenario(
8990
)
9091
```
9192

93+
For keyword arguments, use `functools.partial`:
94+
95+
```python
96+
from functools import partial
97+
from microbenchmark import Scenario
98+
99+
scenario = Scenario(
100+
partial(sorted, key=lambda x: -x),
101+
args=[[3, 1, 2]],
102+
name='sort_descending',
103+
)
104+
```
105+
92106
### `run(warmup=0)`
93107

94108
Runs the benchmark and returns a `BenchmarkResult`.
95109

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.
110+
The optional `warmup` argument specifies how many calls to make before timing begins. Warm-up calls invoke the function and consume timer ticks, but their timings are not included in the result.
97111

98112
```python
113+
from microbenchmark import Scenario
114+
115+
scenario = Scenario(lambda: list(range(100)), name='build', number=1000)
99116
result = scenario.run(warmup=100)
100117
print(len(result.durations))
101-
#> 10000
118+
#> 1000
102119
```
103120

104121
### `cli()`
105122

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.
123+
Turns the scenario into a small command-line program. Call `scenario.cli()` as the entry point of a script and it will parse `sys.argv`, run the benchmark, and print the result.
107124

108125
Supported arguments:
109126

110127
- `--number N` — override the scenario's `number` for this run.
111128
- `--max-mean THRESHOLD` — exit with code `1` if the mean time (in seconds) exceeds `THRESHOLD`. Useful in CI.
129+
- `--help` — print usage information and exit.
130+
131+
Output format:
132+
133+
```
134+
benchmark: <name>
135+
mean: <mean>s
136+
best: <best>s
137+
worst: <worst>s
138+
```
139+
140+
Values are in seconds. The `mean`, `best`, and `worst` labels are padded to the same width. If `--max-mean` is supplied and the actual mean exceeds the threshold, the same output is printed but the process exits with code `1`.
112141

113142
```python
114143
# benchmark.py
@@ -124,7 +153,7 @@ if __name__ == '__main__':
124153
```
125154

126155
```
127-
$ python benchmark.py --number 1000
156+
$ python benchmark.py
128157
benchmark: build_list
129158
mean: 0.000012s
130159
best: 0.000010s
@@ -137,6 +166,8 @@ benchmark: build_list
137166
mean: 0.000012s
138167
best: 0.000010s
139168
worst: 0.000018s
169+
$ echo $?
170+
0
140171
```
141172

142173
```
@@ -146,7 +177,7 @@ mean: 0.000012s
146177
best: 0.000010s
147178
worst: 0.000018s
148179
$ echo $?
149-
#> 1
180+
1
150181
```
151182

152183
---
@@ -170,15 +201,31 @@ s2 = Scenario(lambda: None, name='s2')
170201
group = ScenarioGroup(s1, s2)
171202
```
172203

204+
You can also create an empty group and combine it with others later:
205+
206+
```python
207+
empty = ScenarioGroup()
208+
print(len(empty.run()))
209+
#> 0
210+
```
211+
173212
**The `+` operator between scenarios** — adding two or more `Scenario` objects produces a `ScenarioGroup`:
174213

175214
```python
215+
from microbenchmark import Scenario
216+
217+
s1 = Scenario(lambda: None, name='s1')
218+
s2 = Scenario(lambda: None, name='s2')
176219
group = s1 + s2
177220
```
178221

179-
**Adding a scenario to a group** — the result is always a flat group:
222+
**Adding a scenario to a group** — the result is always a flat group with no nesting:
180223

181224
```python
225+
from microbenchmark import Scenario, ScenarioGroup
226+
227+
s1 = Scenario(lambda: None, name='s1')
228+
s2 = Scenario(lambda: None, name='s2')
182229
s3 = Scenario(lambda: None, name='s3')
183230
group = s1 + s2 + s3
184231
print(type(group).__name__)
@@ -188,6 +235,11 @@ print(type(group).__name__)
188235
**Adding two groups together** — the result is a single flat group containing the scenarios from both:
189236

190237
```python
238+
from microbenchmark import Scenario, ScenarioGroup
239+
240+
s1 = Scenario(lambda: None, name='s1')
241+
s2 = Scenario(lambda: None, name='s2')
242+
s3 = Scenario(lambda: None, name='s3')
191243
g1 = ScenarioGroup(s1)
192244
g2 = ScenarioGroup(s2, s3)
193245
combined = g1 + g2
@@ -197,25 +249,30 @@ print(len(combined.run()))
197249

198250
### `run(warmup=0)`
199251

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.
252+
Runs every scenario in order and returns a list of `BenchmarkResult` objects. The order in the list matches the order the scenarios were added. The `warmup` argument is forwarded to each scenario.
201253

202254
```python
255+
from microbenchmark import Scenario, ScenarioGroup
256+
257+
s1 = Scenario(lambda: None, name='s1')
258+
s2 = Scenario(lambda: None, name='s2')
259+
group = ScenarioGroup(s1, s2)
203260
results = group.run(warmup=50)
204261
for result in results:
205-
print(result.scenario.name, result.mean)
206-
#> s1 ...
207-
#> s2 ...
208-
#> s3 ...
262+
print(result.scenario.name)
263+
#> s1
264+
#> s2
209265
```
210266

211267
### `cli()`
212268

213-
Runs all scenarios and prints their results separated by dividers.
269+
Runs all scenarios and prints their results separated by `---` dividers.
214270

215271
Supported arguments:
216272

217273
- `--number N` — passed to every scenario.
218274
- `--max-mean THRESHOLD` — exits with code `1` if any scenario's mean exceeds the threshold.
275+
- `--help` — print usage information and exit.
219276

220277
```python
221278
# benchmarks.py
@@ -251,14 +308,16 @@ worst: 0.000018s
251308

252309
### Fields
253310

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()`.
311+
- `scenario: Scenario | None` — the `Scenario` that produced this result, or `None` if the result was restored from JSON.
312+
- `durations: tuple[float, ...]` per-call timings in seconds, one entry per call.
313+
- `mean: float` — arithmetic mean of `durations`, computed with `math.fsum` to minimize floating-point error.
314+
- `best: float` — the shortest individual timing.
315+
- `worst: float` — the longest individual timing.
316+
- `is_primary: bool``True` for results returned directly by `run()`, `False` for results derived via `percentile()`.
260317

261318
```python
319+
from microbenchmark import Scenario
320+
262321
result = Scenario(lambda: None, name='noop', number=100).run()
263322
print(len(result.durations))
264323
#> 100
@@ -268,34 +327,45 @@ print(result.is_primary)
268327

269328
### `percentile(p)`
270329

271-
Returns a new `BenchmarkResult` containing only the fastest `ceil(len(durations) * p / 100)` timings. The returned result has `is_primary=False`.
330+
Returns a new `BenchmarkResult` containing only the `ceil(len(durations) * p / 100)` fastest timings, sorted by duration ascending. The returned result has `is_primary=False`. `p` must be in the range `(0, 100]`; passing `0` or a value above `100` raises `ValueError`.
272331

273332
```python
333+
from microbenchmark import Scenario
334+
335+
result = Scenario(lambda: None, name='noop', number=100).run()
274336
trimmed = result.percentile(95)
275337
print(trimmed.is_primary)
276338
#> False
277-
print(len(trimmed.durations) <= len(result.durations))
278-
#> True
339+
print(len(trimmed.durations))
340+
#> 95
279341
```
280342

281-
`p` must be in the range `(0, 100]`. Passing `0` or a value above `100` raises `ValueError`.
282-
283343
### `p95` and `p99`
284344

285345
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.
286346

287347
```python
288-
print(result.p95.mean <= result.mean)
289-
#> True
348+
from microbenchmark import Scenario
349+
350+
result = Scenario(lambda: None, name='noop', number=100).run()
351+
p95 = result.p95
352+
print(len(p95.durations))
353+
#> 95
354+
print(p95.is_primary)
355+
#> False
290356
```
291357

292358
### `to_json()` and `from_json()`
293359

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`.
360+
`to_json()` serializes the result to a JSON string. It stores all individual `durations`, `is_primary`, and the scenario's `name`, `doc`, and `number`.
295361

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`.
362+
`from_json()` is a class method that restores a `BenchmarkResult` from a JSON string produced by `to_json()`. Because the original callable cannot be serialized, the restored result has `scenario=None`. The `mean`, `best`, and `worst` fields are recomputed from `durations` on restoration.
297363

298364
```python
365+
from microbenchmark import Scenario, BenchmarkResult
366+
367+
result = Scenario(lambda: None, name='noop', number=100).run()
368+
299369
json_str = result.to_json()
300370
restored = BenchmarkResult.from_json(json_str)
301371

@@ -315,10 +385,12 @@ print(restored.durations == result.durations)
315385
|---|---|---|---|
316386
| Per-call timings | yes | no | yes |
317387
| Percentile views | yes | no | yes |
318-
| JSON serialisation | yes | no | no |
319-
| CI integration (`--max-mean`) | yes | no | via plugins |
388+
| JSON serialization | yes | no | yes (internal format) |
389+
| Inject custom timer | yes | yes | no |
390+
| Warmup support | yes | no | yes (calibration) |
391+
| CI integration (`--max-mean`) | yes | no | via configuration |
320392
| `+` operator for grouping | yes | no | no |
321393
| External dependencies | none | none | several |
322-
| Embeddable in your own code | yes | yes | test suite only |
394+
| Embeddable in your own code | yes | yes | pytest plugin required |
323395

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.
396+
`timeit` from the standard library is great for interactive exploration but gives you only a single aggregate number and offers no per-call data. `pytest-benchmark` is powerful and well-integrated into the `pytest` ecosystem, but it is tightly coupled to the test runner and brings its own dependencies. `microbenchmark` sits between the two: richer than `timeit`, lighter and more portable than `pytest-benchmark`, and not tied to any test framework.

0 commit comments

Comments
 (0)