# `#prep_uplc` benchmarks This suite measures the time and memory of the `#prep_uplc` command (`PlutusCore/UPLC/PreProcess.lean`) on every script of the repository, the conformance suite included. It also times the compiled CEK machine on the same programs. It compares runs, e.g. before and after a change to Blaster or to the CEK machine. ```sh make bench_prep_uplc # all 885 cases, one at a time: ~1.5 h make bench_prep_uplc JOBS=8 # ~15 min, noisier make bench_prep_uplc BENCH_ARGS="--suite repo,symbolic" lake exe bench_prep_uplc list --filter example/ # which cases a selection runs lake exe bench_prep_uplc run --filter example/factorial --runs 5 make bench_prep_uplc_compare BASE=Benchmark/results/ NEW=Benchmark/results/ ``` `lake exe bench_prep_uplc help` lists every option. Those durations are from a 32-core machine. Most of that time goes to Lean's startup (about 1.2 s per case) and to the 5 cases that reach the 10-minute timeout: 4 `countSetBits` conformance programs and one symbolic validator. Requirements: - The conformance cases need the `.plutus-conformance` symlink, as for `make gen_conformance_tests`. Without it, pass `--suite repo,symbolic`. - Memory is measured through `/proc`, so memory figures need Linux. Elsewhere, the peak is the whole process's and the other memory columns stay empty. ## What is measured Each case is a generated Lean file that runs one harness command, `#bench_prep_uplc` or `#bench_prep_uplc_file` (`PrepUplc/Harness.lean`). The harness first imports or wraps the script, then times one `#prep_uplc` command. The timed region is the whole command: - the Blaster optimization; - `inferType` of the optimized term; - the kernel checks of `….prop` and of the `PrepUPLC.…` structure instance, which embeds the optimized term a second time; - the compilation of `….exec`. It does not include Lean's startup, the imports (about 1.1 s and 1.2 GB resident), decoding the script, or the harness's other work. For closed programs, meaning every case except the symbolic ones, the harness also runs the compiled `cekExecuteProgram` on the program with the same step budget. This run happens before and apart from `#prep_uplc`. The harness times it, taking the best of repeated runs: at least one, then more until 50 ms or 1000 runs have passed. It also records how the run ends and how many machine steps it takes. The measured command runs with `Elab.async` off. Otherwise `lean` runs the kernel checks and the compilation in background tasks, outside any timer. It also runs with `maxHeartbeats 0`. Heartbeats are not reported: the Blaster optimizer resets the counter while it runs. | column | meaning | | --- | --- | | `status` | `ok`; `import_error` (the script does not decode); `prep_error` (`#prep_uplc` failed; see `error`); `timeout`, `mem_limit` (stopped by the driver); `crash` (the process died before reporting); `no_result` | | `wall_ms`, `wall_ms_median` | wall time of `#prep_uplc`: best and median over `--runs` | | `cpu_ms` | user + system CPU time of the process during the best run | | `rss_before_kb`, `peak_rss_kb`, `rss_after_kb` | resident set size before, at the peak of, and after `#prep_uplc`. The peak is `VmHWM`, reset through `/proc/self/clear_refs` just before the run. | | `peak_delta_kb` | `peak_rss_kb − rss_before_kb`, the memory `#prep_uplc` needed. It includes the pages of the memory-mapped `.olean` files it touched. | | `native_result` | how `cekExecuteProgram` ends on a closed program: `halt`, `error`, or `exhausted` when its `Error` comes from running out of fuel (so `#prep_uplc` sees an error too) | | `cek_steps` | the machine steps of that run, counted by iterating `step`. Empty when that count ends differently from `cekExecuteProgram`, e.g. after a change to `runSteps`. | | `native_ms`, `native_runs` | best time of `cekExecuteProgram` on the program, and how many runs it is the best of | | `prep_steps_per_s`, `native_steps_per_s` | `cek_steps` per second of `#prep_uplc` and of the native run. The `#prep_uplc` rate is given only for the closed programs it fully evaluates, since otherwise it did not take all these steps. It includes the fixed cost of about 0.2 s, so it means something only for programs of thousands of steps. | | `prop_head`, `prop_objs` | head constant of the optimized term, under its binders, and its size in distinct objects (`Expr.numObjs`). A closed program is fully evaluated when this head is the state it natively ends in. `summary.md` lists the cases where it isn't. | | `prop_hash` | `Expr.hash` of the optimized term (32 bits). It changes whenever the term does, barring collisions. | | `process_s`, `exit_code`, `max_rss_observed_kb` | the whole `lean` process, as the driver saw it (it samples memory every 200 ms) | ## Cases | suite | cases | what | | --- | --- | --- | | `conformance` | 844 | Every program the conformance tests under `Tests/Conformance/Generated` import (`#import_uplc`, not the `…_expected` one). This is 662 expected successes and 182 expected evaluation failures. Parse-error tests import nothing and are skipped, and so are the categories the suite was generated without (`--exclude-not-implemented`). Run with no inputs and `--max-steps` (default 100000). | | `repo` | 25 | `repoScripts` in `PrepUplc/Driver/Cases.lean`, run with no inputs, plus 6 concrete calls such as `saturate(100)`. | | `symbolic` | 16 | The 13 validators and functions among `repoScripts`, applied to free `Data` or `Integer` arguments (`PrepUplc/Inputs.lean`), once per `--symbolic-steps` budget (default 2000), and also at the step budget of the proof a function comes from. | The `repo` scripts: - the script files of `Tests/UPLC/ScriptEncoding/Tests{Text,Flat}`. `factorial.uplc`, `fibonacci.uplc` and `dataMap.flat` are also conformance programs, and `dataMap.flat` is really single-CBOR hex; - the `Program`s `test1` and `test3`–`test10` of `Tests/UPLC/ScriptEncoding/Tests.lean`. test3–test9 are real validators; test7 is identical to test6; test1, test10 and `testUplc.flat` are the same toy program; - three validators copied into `PrepUplc/Scripts/` from the unnamed `example`s of `Tests/UPLC/ScriptEncoding/Tests.lean`, which a benchmark cannot refer to: | file | origin | UPLC | size | | --- | --- | --- | --- | | `aiken-validator-v1.cbor.hex` | line 89 (also `FlatEncoding/Tests.lean:103`) | 1.0.0 | 3.5 KB | | `nonce-list-v3.cbor.hex` | line 94 | 1.1.0 | 254 B | | `minting-policy-v3.cbor.hex` | line 99, trace messages renamed | 1.1.0 | 483 B | - two functions on integers, with concrete calls and symbolic `Integer` cases: | file | origin | computes | concrete calls | proof budget | | --- | --- | --- | --- | --- | | `saturate.uplc` | the hand-written program of Lean-blaster's `Tests/Smt/Benchmarks/UPLC/Examples/Integer/Saturate.lean` (commit `49a4886`), ported to textual UPLC | x·(x+1), by a loop adding 2n for n from x down to 1 | 100, 256 (7260 and 18492 steps) | 7100 steps | | `fibonacci-naive.cbor.hex`, `fibonacci-size.cbor.hex` | CardanoLedgerApiBlaster's `Tests/Functions/Fibonacci`, the files this repository removed in `ddaf2d2` | Fibonacci numbers, naively and optimized for size | 10, 15 (about 8k and 90k steps) | 4000 steps | The driver warns about script files (`*.uplc`, `*.flat`, `*.cbor`, `*.hex`) that `repoScripts` does not list, and benchmarks them with format `auto`. A symbolic case applies a script to the free arguments given by its `symbolic?` in `repoScripts`. The functions take one `Integer`. For a validator, the number of `Data` arguments is the least number of `Data.I 0` arguments with which the script, evaluated natively, no longer returns a lambda: - 3 for test3–test7, test9 and `aiken-validator-v1`; - 2 for test8, `nonce-list-v3` and `minting-policy-v3` (parameterized V3 scripts). The functions' symbolic cases also run at the `proofSteps` budget of the proof they come from: the Lean-blaster `saturate` proof (7100 steps) and CardanoLedgerApiBlaster's Fibonacci proofs (4000 steps). The cost varies widely. At 2000 steps: - nonce-list-v3 takes 0.2 s, test9 0.3 s, test4 1.4 s, test8 4.8 s and test3 20 s; - test6 takes 144 s and 2.3 GB; - aiken-validator-v1 takes 574 s and 6.3 GB, only to reduce to `State.Error`; - minting-policy-v3 exceeds the 600 s timeout; - saturate takes 0.3 s, and the Fibonacci functions 1.4 s. At their proof budgets, saturate takes 0.6 s and unrolls to a 20k-object term, and the Fibonacci functions take 5.6 s. The two Fibonacci implementations optimize to the same term (same `prop_hash`), which is what CardanoLedgerApiBlaster's equivalence proof of them relies on. A budget too small to get past a validator's prelude reduces to a mere `State.Error`: test8 does so at up to 1000 steps. ## How a run works 1. The driver writes one case file per case under `PrepUplc/Generated/`. This directory is rewritten on every run and not committed. A single case can be rerun by hand with `lake lean `, which prints its measurement. 2. It runs `lake build Benchmark`. The library root imports everything the cases import, so the cases never build anything themselves. 3. It gets each case's module setup from `lake setup-file` and starts `lean --setup … ` directly. This is the setup `lake lean` would use, so the precompiled PlutusCore and Blaster libraries are loaded and `#prep_uplc` runs native code, as in `lake build`. The driver keeps the process id to watch memory and to stop the case after `--timeout` or above `--max-rss-mb`. 4. Each case runs in its own process, so a crash stays contained and each peak is its own. A run directory, by default `Benchmark/results/