benchset 0.0.2__tar.gz → 0.0.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. {benchset-0.0.2 → benchset-0.0.3}/Cargo.lock +1 -1
  2. {benchset-0.0.2 → benchset-0.0.3}/Cargo.toml +2 -1
  3. {benchset-0.0.2 → benchset-0.0.3}/MANIFEST.in +2 -1
  4. {benchset-0.0.2 → benchset-0.0.3}/PKG-INFO +245 -32
  5. {benchset-0.0.2 → benchset-0.0.3}/Package.swift +2 -2
  6. {benchset-0.0.2 → benchset-0.0.3}/README.md +244 -31
  7. {benchset-0.0.2 → benchset-0.0.3}/Sources/Benchset/Benchset.swift +102 -8
  8. {benchset-0.0.2 → benchset-0.0.3}/benchset/__init__.py +7 -0
  9. {benchset-0.0.2 → benchset-0.0.3}/benchset/_native.py +70 -10
  10. {benchset-0.0.2 → benchset-0.0.3}/benchset/api.py +69 -39
  11. benchset-0.0.3/benchset/cli.py +270 -0
  12. {benchset-0.0.2 → benchset-0.0.3}/benchset/models.py +18 -0
  13. benchset-0.0.3/benchset/reference.py +74 -0
  14. {benchset-0.0.2 → benchset-0.0.3}/benchset.egg-info/PKG-INFO +245 -32
  15. {benchset-0.0.2 → benchset-0.0.3}/benchset.egg-info/SOURCES.txt +20 -1
  16. benchset-0.0.3/examples/native/c.c +40 -0
  17. benchset-0.0.3/examples/native/rust.rs +53 -0
  18. benchset-0.0.3/examples/native/swift.swift +12 -0
  19. benchset-0.0.3/include/benchset.h +109 -0
  20. benchset-0.0.3/native/reference-calibration.json +47 -0
  21. {benchset-0.0.2 → benchset-0.0.3}/native/src/ffi.rs +212 -12
  22. benchset-0.0.3/native/src/ffi_reference_tests.rs +872 -0
  23. {benchset-0.0.2 → benchset-0.0.3}/native/src/ffi_tests.rs +167 -57
  24. {benchset-0.0.2 → benchset-0.0.3}/native/src/lib.rs +36 -21
  25. benchset-0.0.3/native/src/reference.rs +77 -0
  26. benchset-0.0.3/native/src/reference_tests.rs +312 -0
  27. {benchset-0.0.2 → benchset-0.0.3}/native/src/runtime_tests.rs +199 -28
  28. {benchset-0.0.2 → benchset-0.0.3}/pyproject.toml +2 -2
  29. {benchset-0.0.2 → benchset-0.0.3}/scripts/build_native.py +6 -0
  30. {benchset-0.0.2 → benchset-0.0.3}/scripts/check_native_coverage.py +7 -0
  31. benchset-0.0.3/scripts/check_source_install.py +173 -0
  32. {benchset-0.0.2 → benchset-0.0.3}/scripts/package_native.py +12 -1
  33. {benchset-0.0.2 → benchset-0.0.3}/scripts/release.py +33 -24
  34. benchset-0.0.3/scripts/version.py +109 -0
  35. {benchset-0.0.2 → benchset-0.0.3}/setup.py +19 -6
  36. benchset-0.0.3/tests/__init__.py +0 -0
  37. benchset-0.0.3/tests/helpers.py +35 -0
  38. {benchset-0.0.2 → benchset-0.0.3}/tests/swift/BenchsetTests.swift +289 -30
  39. benchset-0.0.3/tests/swift/ReferenceTests.swift +311 -0
  40. {benchset-0.0.2 → benchset-0.0.3}/tests/test_benchset.py +52 -12
  41. benchset-0.0.3/tests/test_cli.py +753 -0
  42. {benchset-0.0.2 → benchset-0.0.3}/tests/test_errors.py +19 -1
  43. benchset-0.0.3/tests/test_reference.py +646 -0
  44. benchset-0.0.3/tests/test_reference_artifacts.py +777 -0
  45. benchset-0.0.3/tests/test_reference_native.py +139 -0
  46. {benchset-0.0.2 → benchset-0.0.3}/tests/test_release.py +128 -7
  47. {benchset-0.0.2 → benchset-0.0.3}/tests/test_resources.py +6 -2
  48. {benchset-0.0.2 → benchset-0.0.3}/tests/test_semantics.py +185 -26
  49. benchset-0.0.3/tests/test_signals.py +310 -0
  50. benchset-0.0.3/tests/test_version.py +178 -0
  51. benchset-0.0.3/tests/typing/__init__.py +1 -0
  52. benchset-0.0.3/tests/typing/reference_negative.py +15 -0
  53. benchset-0.0.3/tests/typing/reference_positive.py +46 -0
  54. benchset-0.0.3/tests/typing/test_reference_types.py +46 -0
  55. benchset-0.0.2/benchset/cli.py +0 -168
  56. benchset-0.0.2/examples/native/c.c +0 -26
  57. benchset-0.0.2/examples/native/rust.rs +0 -15
  58. benchset-0.0.2/examples/native/swift.swift +0 -5
  59. benchset-0.0.2/include/benchset.h +0 -64
  60. benchset-0.0.2/scripts/check_source_install.py +0 -44
  61. {benchset-0.0.2 → benchset-0.0.3}/LICENSE +0 -0
  62. {benchset-0.0.2 → benchset-0.0.3}/benchset/__main__.py +0 -0
  63. {benchset-0.0.2 → benchset-0.0.3}/benchset/py.typed +0 -0
  64. {benchset-0.0.2 → benchset-0.0.3}/benchset.egg-info/dependency_links.txt +0 -0
  65. {benchset-0.0.2 → benchset-0.0.3}/benchset.egg-info/entry_points.txt +0 -0
  66. {benchset-0.0.2 → benchset-0.0.3}/benchset.egg-info/requires.txt +0 -0
  67. {benchset-0.0.2 → benchset-0.0.3}/benchset.egg-info/top_level.txt +0 -0
  68. {benchset-0.0.2 → benchset-0.0.3}/include/module.modulemap +0 -0
  69. {benchset-0.0.2 → benchset-0.0.3}/native/src/kernel.rs +0 -0
  70. {benchset-0.0.2 → benchset-0.0.3}/native/src/kernel_tests.rs +0 -0
  71. {benchset-0.0.2 → benchset-0.0.3}/rust-toolchain.toml +0 -0
  72. {benchset-0.0.2 → benchset-0.0.3}/scripts/__init__.py +0 -0
  73. {benchset-0.0.2 → benchset-0.0.3}/scripts/build_backend.py +0 -0
  74. {benchset-0.0.2 → benchset-0.0.3}/setup.cfg +0 -0
  75. {benchset-0.0.2 → benchset-0.0.3}/tests/test_build.py +0 -0
@@ -4,4 +4,4 @@ version = 3
4
4
 
5
5
  [[package]]
6
6
  name = "benchset"
7
- version = "0.0.2"
7
+ version = "0.0.3"
@@ -1,6 +1,7 @@
1
1
  [package]
2
2
  name = "benchset"
3
- version = "0.0.2"
3
+ # Generated from [project].version in pyproject.toml by scripts/version.py.
4
+ version = "0.0.3"
4
5
  edition = "2021"
5
6
  rust-version = "1.74"
6
7
  description = "Embeddable sustained scalar CPU benchmarks with a C ABI"
@@ -1,12 +1,13 @@
1
1
  include setup.py
2
2
  include Cargo.toml Cargo.lock rust-toolchain.toml LICENSE README.md
3
3
  recursive-include native *.rs
4
+ include native/reference-calibration.json
4
5
  recursive-include include *.h *.modulemap
5
6
  recursive-include scripts *.py
6
7
  recursive-include examples *.c *.rs *.swift
7
8
  include Package.swift
8
9
  recursive-include Sources *.swift
9
- recursive-include tests *.swift
10
+ recursive-include tests *.py *.swift
10
11
  prune target
11
12
  prune Artifacts
12
13
  prune benchset/_native
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: benchset
3
- Version: 0.0.2
3
+ Version: 0.0.3
4
4
  Summary: Sustained scalar CPU benchmarks with an embeddable Rust core
5
5
  Author-email: Evgeniy Blinov <zheni-b@yandex.ru>
6
6
  Project-URL: Source, https://github.com/mutating/benchset
@@ -41,7 +41,13 @@ needed by applications embedding the native library.
41
41
  The initial suite contains **`single_core`** (one computational worker) and
42
42
  **`multi_core`** (all available computational workers). Both execute the same
43
43
  fixed mixed workload and return a score in **completed iterations per second**.
44
- Higher is faster. There is no reference machine or score normalization.
44
+ Higher is faster. Each completed result also includes a **Pi equivalent**:
45
+ its throughput divided by the same benchmark's sustained Raspberry Pi 5 score.
46
+ `0.5` means half the Pi's throughput, `1.0` equal throughput, and `2.0` twice
47
+ the Pi's throughput. The CLI prints both iterations/s and `Pi equivalent`.
48
+
49
+ This README describes the current source tree. For changes not yet published,
50
+ use `pip install .` or the local Rust/Swift dependencies described below.
45
51
 
46
52
  ## Install and run
47
53
 
@@ -67,14 +73,17 @@ these build tools.
67
73
 
68
74
  | CLI option | Default | Meaning |
69
75
  |---|---:|---|
70
- | `--bench` | all | `single_core` or `multi_core`; omitted runs both in this order |
76
+ | `--bench` | all | `all`, `single_core` or `multi_core`; `all` runs both in this order |
71
77
  | `--warmup` | 120 | Seconds of continuous workload excluded from the score; zero is allowed |
72
78
  | `--duration` | 60 | Seconds of measurement after warmup; must be positive |
73
- | `--help` | | Show syntax |
79
+ | `-h`, `--help` | | Show options, defaults, examples and Pi equivalent interpretation |
74
80
  | `--version` | | Show the library/benchmark version |
75
81
 
76
82
  `--bench` is the public spelling of the selector. CLI settings are read through
77
83
  `skelet`; configuration files and environment settings are not used.
84
+ Options accept both `--duration 0.5` and `--duration=0.5`, in any order;
85
+ durations accept decimal and scientific notation. A supplied option requires
86
+ a value. Repeating an option uses its last value.
78
87
 
79
88
  A default full suite takes approximately six minutes. Each benchmark has its
80
89
  own warmup. Warmup transitions directly into measurement without stopping the
@@ -83,6 +92,25 @@ Completed result fields, including all measurement samples, are printed to
83
92
  stdout. Failed/cancelled results go to stderr. Exit codes: 0 completed, 1
84
93
  execution failure, 2 invalid CLI arguments, 130 interrupted/cancelled. The
85
94
  suite stops after a failed or cancelled benchmark.
95
+ Each result is flushed immediately, including when stdout is redirected.
96
+ Progress fits the terminal width and is cleared before the result. Output
97
+ uses `× pi` and `—` when the stream supports them, with `x pi` and `-` as
98
+ fallbacks for restrictive encodings. A closed stdout pipe exits with code 1
99
+ without a traceback.
100
+
101
+ For example, a completed result at half the reference throughput includes
102
+ these lines (illustrative values; other fields and samples are omitted):
103
+
104
+ ```text
105
+ Benchmark : single_core
106
+ Score : 23,615.614
107
+ Unit : iterations/s
108
+ Reference machine : Raspberry Pi 5 Model B Rev 1.0 (pi)
109
+ Pi equivalent : 0.5 × pi
110
+ ```
111
+
112
+ The multiplier uses the full-precision native score, before display rounding.
113
+ An unavailable multiplier is displayed as `—` (`-` in the fallback format).
86
114
 
87
115
  ## Python API
88
116
 
@@ -94,11 +122,12 @@ from benchset import run, run_all
94
122
 
95
123
  result = run('single_core', warmup_seconds=120, measurement_seconds=60)
96
124
  if result.success:
97
- print(result.score, result.unit)
125
+ print(f'{result.score} {result.unit}, {result.pi_equivalent} x {result.reference_machine.short_name}')
98
126
  else:
99
127
  print(result.status, result.reason)
100
128
 
101
129
  results = run_all(warmup_seconds=120, measurement_seconds=60)
130
+ print(result.reference_run.started_at, result.reference_run.results)
102
131
  ```
103
132
 
104
133
  For a GUI or cancellable task, run a reusable runner on a background thread:
@@ -126,12 +155,17 @@ iterations in that phase. Dispatch UI updates to the UI thread yourself and
126
155
  return promptly. A Python callback exception cancels the workload and produces
127
156
  a failed result; it does not propagate through C.
128
157
 
129
- `BenchmarkRunner.cancel()` is thread-safe while a call is active. After it
130
- returns, all native workers are joined and the runner can be reused. Concurrent
158
+ `BenchmarkRunner.cancel()` is thread-safe and requests cancellation without
159
+ waiting for the active call to finish. After `run()` returns, all native workers
160
+ are joined and the runner can be reused. Concurrent
131
161
  runs are rejected. The native core enforces one active run per loaded library
132
162
  instance, including calls from separate Python runners. Separate OS processes
133
163
  or separately linked copies of the library can still compete for CPU time.
134
164
 
165
+ With Python's default SIGINT handler, Ctrl+C during a main-thread call cancels
166
+ the native work and raises `KeyboardInterrupt` after cleanup. Custom signal
167
+ handlers are preserved.
168
+
135
169
  ## What one iteration does
136
170
 
137
171
  This is a fixed scalar integer workload, not a prediction of arbitrary
@@ -193,6 +227,8 @@ The immutable Python result contains:
193
227
  | `benchmark`, `version` | Name and benchmark/library version |
194
228
  | `status`, `success`, `reason` | `completed`, `cancelled` or `failed`; explanation on failure |
195
229
  | `score`, `unit` | Iterations/s; `score=None` unless completed |
230
+ | `pi_equivalent` | Double-precision `score / reference_score` for this benchmark; `None` unless completed |
231
+ | `reference_machine`, `reference_run` | Calibration machine and series, present for every result status |
196
232
  | `workers` | Number of computational workers |
197
233
  | `warmup_seconds`, `measurement_seconds` | Requested windows |
198
234
  | `elapsed_seconds`, `iterations` | Actual measured wall time and completed work |
@@ -205,21 +241,124 @@ interval iterations and iterations/s. Variation is `None` with fewer than two
205
241
  samples. It reports within-run variability, not repeat-to-repeat accuracy.
206
242
  An unstable run retains its score and completed status; the consumer decides
207
243
  whether to use it. Cancellation/failure can retain partial diagnostics but
208
- never produces a usable score.
209
-
210
- The Python package version is defined in `pyproject.toml`. `benchset.__version__`
211
- and result objects read the installed package metadata through
244
+ never produces a usable score or Pi equivalent. An unstable completed run
245
+ retains its Pi equivalent. Multi-core compares total measured throughput with
246
+ the multi-core reference; it does not normalize either score by worker count.
247
+
248
+ ## The Pi reference
249
+
250
+ The immutable global machine record is `PI_REFERENCE_MACHINE` in Rust/Python
251
+ and `piReferenceMachine` in Swift. It describes **Raspberry Pi 5 Model B Rev
252
+ 1.0**, abbreviated **`pi`**, with **Ubuntu Server 26.04 LTS**, **aarch64**, four
253
+ CPU cores and **8 GiB (8,589,934,592 bytes) of installed RAM**. Installed
254
+ capacity differs from the OS's usable `MemTotal` (8,317,272,064 bytes during
255
+ calibration). The metadata describes the reference machine, independently of
256
+ the device running the library.
257
+
258
+ The public names are:
259
+
260
+ | Interface | Result multiplier | Reference machine | Reference series |
261
+ |---|---|---|---|
262
+ | Python | `result.pi_equivalent: Optional[float]` | `result.reference_machine: ReferenceMachine` | `result.reference_run: ReferenceRun` |
263
+ | Rust | `result.pi_equivalent: Option<f64>` | `result.reference_machine: &'static ReferenceMachine` | `result.reference_run: &'static ReferenceRun` |
264
+ | Swift | `result.piEquivalent: Double?` | `result.referenceMachine: ReferenceMachine` | `result.referenceRun: ReferenceRun` |
265
+ | C / C++ V3 | `result.base.pi_equivalent`, when `result.base.has_pi_equivalent != 0` | `result.reference_machine` | `result.reference_run` + `benchset_reference_result` |
266
+
267
+ Every returned result carries these calibration records, including completed,
268
+ unstable, failed, cancelled, busy and validation-error results. They describe
269
+ the same fixed Pi reference for both workloads. Python fields default to the
270
+ existing globals (`result.reference_machine is PI_REFERENCE_MACHINE` and
271
+ `result.reference_run is PI_REFERENCE_RUN`); the new fields follow all previous
272
+ constructor arguments. `dataclasses.asdict(result)` includes both records as
273
+ nested data with numeric `started_at_unix_seconds`. Rust fields borrow the
274
+ public statics. Swift computed properties return the global structs with ordinary
275
+ value semantics. The globals
276
+ remain available without running a benchmark; C also retains its handle-free
277
+ `benchset_reference_machine` and `benchset_reference_run` getters.
278
+
279
+ Rust callers that construct `BenchmarkResult` with a struct literal must add
280
+ the new `pi_equivalent`, `reference_machine` and `reference_run` fields. This
281
+ source change follows from extending the public result struct; callers that
282
+ obtain results from benchmark functions keep their existing call signatures.
283
+
284
+ Every multiplier is a double-precision value. It is available for completed
285
+ results, including unstable ones. Failed/cancelled results have no multiplier:
286
+ `None` in Python/Rust, `nil` in Swift, or a zero presence flag in C.
287
+
288
+ `PI_REFERENCE_RUN` / Swift `piReferenceRun` describes the final six-run
289
+ calibration series, starting at its first warmup and ending with the final
290
+ measurement, before final cooling. It began on **2026-10-23 at
291
+ 14:59:50.517391 UTC** (Unix seconds `1792767590.517391`); CPU temperature was
292
+ **41.35 °C** at the beginning and **66.65 °C** at the end. Preparation, plateau
293
+ profiling and the later cooling period are outside these boundaries. The
294
+ reference series ended on 23 October, including in Moscow; the larger session
295
+ with preparation and cooling crossed midnight.
296
+
297
+ Each benchmark had three independent measurements of 300 seconds, with cooling
298
+ verified by temperature readings between experiments. The stored score pools
299
+ all three measurements as **total iterations / total actual elapsed seconds**;
300
+ it is neither the best repeat nor a rounded display value:
301
+
302
+ | Benchmark | Iterations | Actual measurement seconds | Reference iterations/s |
303
+ |---|---:|---:|---:|
304
+ | `single_core` | 42,508,112 | 900.000162475 | 47231.227029007096 |
305
+ | `multi_core` | 163,757,952 | 900.000523364 | 181953.17419139884 |
306
+
307
+ The historical calibration used **1248 seconds (20 minutes 48 seconds) of
308
+ warmup before every measurement**. This is the conservative 960-second plateau
309
+ boundary plus 30%, based on both throughput and thermal establishment. There
310
+ was no substantial prolonged throughput decline during profiling. The
311
+ library's runtime defaults remain **120 seconds warmup / 60 seconds
312
+ measurement**; callers can choose their own windows. The reference warmup is
313
+ recorded provenance, not an automatic runtime setting.
314
+
315
+ The metadata types are `ReferenceMachine`, `ReferenceRun` and
316
+ `ReferenceBenchmarkResult` in Rust, Python and Swift. In Rust/Python their
317
+ fields are:
318
+
319
+ - `ReferenceMachine`: `name`, `short_name`, `os_name`, `os_version`,
320
+ `cpu_architecture`, `memory_bytes` and `cpu_cores`.
321
+ - `ReferenceRun`: `started_at_unix_seconds`, `start_temperature_c`,
322
+ `end_temperature_c`, `warmup_seconds` and `results`. Temperatures are in
323
+ Celsius; the warmup is in seconds before **each** reference measurement.
324
+ - `ReferenceBenchmarkResult`: `benchmark` and `score` (iterations/s).
325
+ `results` contains two pooled entries, one per benchmark, rather than six
326
+ separate repeats.
327
+
328
+ Swift uses camelCase fields, such as `shortName`, `startedAtUnixSeconds` and
329
+ `startTemperatureC`. Python additionally offers the timezone-aware UTC
330
+ `started_at` datetime; Swift offers `startedAt: Date` for the same instant.
331
+ The frozen Python records use a tuple for results; Swift returns immutable
332
+ value snapshots. Reading the Python globals does not load the native library.
333
+
334
+ [Calibration values and provenance](native/reference-calibration.json) include
335
+ the original kernel commit, binary SHA-256, raw pooled totals and series
336
+ boundaries. The calibration used the unchanged scalar kernel at commit
337
+ `1b6771c1fe47514edfe72384b76a1d595cb2ff10`. Ambient conditions were not
338
+ controlled; the measured idle temperature shifted, so later cooling also
339
+ accepted a documented stable idle plateau. Ratios describe these workloads
340
+ and this reference configuration. The library reads these fixed values
341
+ locally; it does not connect to the Pi, query sensors or require the original
342
+ experiment directory.
343
+
344
+ The only editable package version is `[project].version` in `pyproject.toml`.
345
+ The version values in `Cargo.toml`, the package's `Cargo.lock` entry and the
346
+ Rust/Swift dependency pins below are generated copies. `benchset.__version__`
347
+ and result objects read installed Python package metadata through
212
348
  `importlib.metadata`; no Python source version constant needs updating.
213
- The native library shares this version; builds check its Cargo metadata for
214
- consistency. Consumers decide which versions and measurement settings are comparable.
349
+ Rust, C and Swift read the version compiled from the generated Cargo metadata.
350
+ Python builds (including metadata, sdist, wheel and editable hooks),
351
+ `scripts/build_native.py` and `scripts/package_native.py` automatically refresh
352
+ these copies before building. Consumers decide which versions and measurement
353
+ settings are comparable.
215
354
 
216
355
  ## Embed in Rust
217
356
 
218
357
  ```toml
219
358
  [dependencies]
220
- benchset = "0.0.1"
221
- # Before registry publication, use:
222
- # benchset = { git = "https://github.com/mutating/benchset.git" }
359
+ benchset = "0.0.3"
360
+ # For changes from a local source checkout, use:
361
+ # benchset = { path = "../benchset" }
223
362
  ```
224
363
 
225
364
  ```rust
@@ -227,7 +366,12 @@ use benchset::{run, Benchmark, Cancellation, Options};
227
366
  let cancellation = Cancellation::default();
228
367
  let result = run(Benchmark::MultiCore, Options::default(), &cancellation,
229
368
  |progress| println!("{:?}: {}", progress.phase, progress.fraction));
230
- println!("{:?}", result.score);
369
+ if let (Some(score), Some(equivalent)) = (result.score, result.pi_equivalent) {
370
+ println!("{score:.3} iterations/s, {equivalent:.3} x {}", result.reference_machine.short_name);
371
+ } else {
372
+ println!("{:?}: {}", result.status, result.reason);
373
+ }
374
+ println!("Reference warmup: {} s", result.reference_run.warmup_seconds);
231
375
  ```
232
376
 
233
377
  The cancellation token can be cloned and cancelled from another thread. It is
@@ -248,25 +392,66 @@ dynamic loading. `include/benchset.h` documents structures, status codes and
248
392
  lifetime rules. `examples/native/c.c` demonstrates configuration and progress.
249
393
 
250
394
  ```c
395
+ /* Inside main(); callback/context are supplied by the host application. */
396
+ if (benchset_abi_version() != BENCHSET_ABI_VERSION) return 1;
251
397
  BenchsetConfig config;
252
- benchset_default_config(&config);
398
+ if (!benchset_default_config(&config)) return 1;
253
399
  config.benchmark = BENCHSET_MULTI_CORE;
254
400
  BenchsetHandle *handle = benchset_create();
255
401
  if (handle) {
256
402
  benchset_run(handle, &config, callback, context);
257
- BenchsetResult result;
258
- if (benchset_result(handle, &result) && result.status == BENCHSET_COMPLETED) {
259
- /* result.score is iterations/s */
403
+ BenchsetResultV3 result;
404
+ if (benchset_result_v3(handle, &result, sizeof(result))) {
405
+ const BenchsetReferenceMachine *machine = result.reference_machine;
406
+ const BenchsetReferenceRun *reference = result.reference_run;
407
+ /* machine and reference are available even after failure/cancellation. */
408
+ printf("Reference: %s, warmup %.0f s\n", machine->name, reference->warmup_seconds);
409
+ if (result.base.base.status == BENCHSET_COMPLETED && result.base.has_pi_equivalent) {
410
+ /* result.base.base.score is iterations/s; result.base.pi_equivalent is a multiplier. */
411
+ }
412
+ /* The metadata pointers remain valid after benchset_free(handle). */
260
413
  }
261
414
  benchset_free(handle);
262
415
  }
263
416
  ```
264
417
 
265
418
  Call `benchset_cancel(handle)` from another thread or a progress callback.
266
- Handles are single-use. Getters are used after `benchset_run` returns. Strings
267
- are borrowed until the handle is freed; progress pointers are borrowed only
268
- during the callback. Do not free a handle while it is in use or throw/unwind
269
- through a C callback. Invalid configuration produces a failed result.
419
+ Handles are single-use. Result/sample getters are used after `benchset_run`
420
+ returns. The reason string is borrowed until the handle is freed; the version
421
+ string is valid for the loaded library's lifetime. Progress pointers are
422
+ borrowed only during the callback. Do not free a handle while it is in use or
423
+ throw/unwind through a C callback. Invalid configuration produces a failed result.
424
+
425
+ The C ABI version remains 1. The additive, size-checked
426
+ `benchset_reference_machine`, `benchset_reference_run` and indexed
427
+ `benchset_reference_result` getters need no handle. Their
428
+ immutable strings remain valid for the loaded library's lifetime. Pass the
429
+ exact output structure size to these getters, `benchset_result_v2` and
430
+ `benchset_result_v3`;
431
+ failure returns zero and leaves the output unchanged. Any nonzero
432
+ `has_pi_equivalent` flag means present, including a multiplier of `0.0`.
433
+ Existing ABI-1 configurations and the original 88-byte `BenchsetResult`
434
+ layout/getter retain their behavior, so existing consumers continue to run.
435
+ New consumers check `benchset_abi_version()` and use the extended result getter
436
+ to access the multiplier. V2 and V3 name additive result formats; they use the
437
+ same ABI-1 configuration. Older libraries can report ABI 1 while lacking the
438
+ new getters, so the Python wrapper checks its required symbols before allocating
439
+ a handle.
440
+
441
+ `BenchsetResultV3` adds a complete `BenchsetResultV2 base` followed by const
442
+ `reference_machine` and `reference_run` pointers. Both pointers are non-null
443
+ for every available result. They and their strings remain immutable and valid
444
+ after `benchset_free(handle)`, until the library is unloaded. Do not use the
445
+ freed handle or its reason string. The ABI version remains 1, and the V1
446
+ (88-byte) and V2 (104-byte) structures and getters retain their layouts and
447
+ behavior. V3 is 120 bytes on
448
+ the supported 64-bit platforms.
449
+
450
+ The C metadata types are `BenchsetReferenceMachine`, `BenchsetReferenceRun`
451
+ and `BenchsetReferenceBenchmarkResult`. `BenchsetReferenceRun.result_count`
452
+ replaces the Rust/Python/Swift `results` collection: retrieve each entry with
453
+ `benchset_reference_result(index, &entry, sizeof(entry))`, using zero-based
454
+ indices smaller than `result_count`.
270
455
 
271
456
  When statically linking Rust into a foreign application, system libraries may
272
457
  also be required. Obtain their list with
@@ -286,11 +471,13 @@ the same repository. Rust is not required by the consumer.
286
471
  For a Swift manifest, add:
287
472
 
288
473
  ```swift
289
- .package(url: "https://github.com/mutating/benchset.git", .exact("0.0.1"))
474
+ .package(url: "https://github.com/mutating/benchset.git", .exact("0.0.3"))
290
475
  ```
291
476
 
292
477
  Add `.product(name: "Benchset", package: "benchset")` to your target's
293
478
  dependencies. Pinning a version also pins the benchmark implementation.
479
+ For unreleased changes, build the Apple package from source as shown below
480
+ and use `.package(path: "../benchset")` instead.
294
481
 
295
482
  For offline use, download and extract **`Benchset-Swift.zip`** from the release
296
483
  and add the extracted directory as a local package. It contains the wrapper,
@@ -304,6 +491,12 @@ let runner = BenchmarkRunner()
304
491
  let result = try runner.run(.multiCore, warmupSeconds: 120, measurementSeconds: 60) {
305
492
  progress in print(progress.fraction)
306
493
  }
494
+ if let score = result.score, let equivalent = result.piEquivalent {
495
+ print("\(score) \(result.unit), \(equivalent) x \(result.referenceMachine.shortName)")
496
+ } else {
497
+ print(result.status, result.reason as Any)
498
+ }
499
+ print(result.referenceRun.startedAt, result.referenceRun.warmupSeconds)
307
500
  // Another thread may call runner.cancel() while run is active.
308
501
  ```
309
502
 
@@ -348,6 +541,14 @@ installation. Release wheels are platform-specific but interpreter-independent
348
541
  correctness, result accounting, progress, cancellation and API/CLI behavior;
349
542
  they do not enforce speed thresholds in CI.
350
543
 
544
+ `python -m pytest tests/test_reference_artifacts.py` also builds and runs
545
+ consumers of unpacked crates, source distributions, wheels and native ZIPs
546
+ outside the checkout. Build the Apple package first to include its unpacked
547
+ Swift consumer and symbol checks for both XCFramework architectures. All smoke
548
+ measurements use short explicit windows, never the historical calibration
549
+ warmup. The [Pi equivalent test plan](tests/pi-equivalent-plan.md) records both
550
+ rounds of independent test reviews and their deduplicated scenarios.
551
+
351
552
  ## Test coverage
352
553
 
353
554
  The [test matrix](tests/coverage-plan.md) combines five independent reviews and
@@ -380,10 +581,21 @@ All distributions are released from this repository by
380
581
  `.github/workflows/release.yml`. A push to `main` checks the common package
381
582
  version. An unreleased version is built and published automatically; a version
382
583
  whose managed GitHub Release is already published is skipped. To release a new
383
- version, update `Cargo.toml` and `pyproject.toml` together,
384
- run `cargo generate-lockfile` to update `Cargo.lock`, commit the changes and
385
- push to `main`. Use an `X.Y.Z` version. Do not create or move release tags
386
- manually.
584
+ version, change only `[project].version` in `pyproject.toml`, commit it and push
585
+ to `main`. Use an `X.Y.Z` version. Builds automatically synchronize the Cargo
586
+ manifest, lockfile entry and README dependency pins; no separate version command
587
+ is needed. The prepared release commit freezes those generated values for all
588
+ published source archives and tags. `python scripts/version.py --check` remains
589
+ an optional diagnostic; it does not have to pass on an unbuilt checkout after a
590
+ version edit.
591
+
592
+ For a local native build after editing the version, use
593
+ `python scripts/build_native.py`; for native/Swift archives use
594
+ `python scripts/package_native.py --target <target>` (add `--apple` for the
595
+ universal Swift package). Cargo reads its package version before `build.rs`
596
+ runs, so direct `cargo build`/`cargo package` use the already-generated manifest.
597
+ Published crates contain that manifest and still build without Python. Do not
598
+ create or move release tags manually.
387
599
 
388
600
  The workflow performs these steps:
389
601
 
@@ -394,8 +606,9 @@ The workflow performs these steps:
394
606
  the Linux containers; update the pin and package version together when
395
607
  changing the release toolchain.
396
608
  3. Compute the XCFramework archive checksum and create a detached release
397
- commit that changes only `Package.swift`. Its parent is the tested source
398
- commit. `main` keeps its local development manifest.
609
+ commit containing generated version values and `Package.swift`. Its parent
610
+ is the tested canonical source commit; unrelated source changes are rejected.
611
+ `main` keeps its local development manifest.
399
612
  4. Build the sdist and crate from that exact release commit, then retain the
400
613
  complete, checksummed release bundle as a GitHub Actions artifact.
401
614
  5. Push `v<version>` at the prepared commit and populate a draft GitHub Release
@@ -9,8 +9,8 @@ let package = Package(
9
9
  products: [.library(name: "Benchset", targets: ["Benchset"])],
10
10
  targets: [
11
11
  .binaryTarget(name: "CBenchset",
12
- url: "https://github.com/mutating/benchset/releases/download/v0.0.2/Benchset.xcframework.zip",
13
- checksum: "29af959b4b1604abb0365f197cfceb73b7c54dc1713e24541b82b04c1956268f"),
12
+ url: "https://github.com/mutating/benchset/releases/download/v0.0.3/Benchset.xcframework.zip",
13
+ checksum: "f50ee204c5a636497bdae1f0a2bb8a7987042f58bd84c5153cae6201e1e35aea"),
14
14
  .target(name: "Benchset", dependencies: ["CBenchset"]),
15
15
  .testTarget(name: "BenchsetTests", dependencies: ["Benchset"], path: "tests/swift")
16
16
  ]