Skip to main content

native_benchmark/
native_benchmark.rs

1//! Dependency-free release benchmark for the public native Rust API.
2
3use std::error::Error;
4use std::fmt::Write as _;
5use std::fs;
6use std::io;
7use std::path::{Path, PathBuf};
8use std::sync::Arc;
9use std::sync::atomic::{AtomicU64, Ordering};
10use std::thread;
11use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
12
13use vsh::{RunRequest, Runtime, RuntimeConfig, StageTimings, TransactionState, VERSION};
14
15static TEMP_SEQUENCE: AtomicU64 = AtomicU64::new(0);
16const PARALLEL_TRANSACTIONS_PER_RUNTIME: usize = 20;
17
18#[derive(Clone, Copy, Debug)]
19struct Sample {
20    wall_ns: u64,
21    internal_ns: u64,
22    stages: StageSample,
23    state: TransactionState,
24    changed_paths: u64,
25}
26
27impl Sample {
28    const fn api_envelope_ns(self) -> u64 {
29        self.wall_ns.saturating_sub(self.internal_ns)
30    }
31}
32
33#[derive(Clone, Copy, Debug)]
34struct StageSample {
35    snapshot: u64,
36    execute: u64,
37    diff: u64,
38    policy: u64,
39    bind_and_store: u64,
40    commit: u64,
41}
42
43impl From<StageTimings> for StageSample {
44    fn from(value: StageTimings) -> Self {
45        Self {
46            snapshot: value.snapshot_ns,
47            execute: value.execute_ns,
48            diff: value.diff_ns,
49            policy: value.policy_ns,
50            bind_and_store: value.bind_and_store_ns,
51            commit: value.commit_ns,
52        }
53    }
54}
55
56#[derive(Clone, Copy, Debug)]
57struct Distribution {
58    min: u64,
59    p50: u64,
60    p95: u64,
61    p99: u64,
62    max: u64,
63}
64
65#[derive(Debug)]
66struct CaseSummary {
67    samples: Vec<Sample>,
68    wall: Distribution,
69    internal: Distribution,
70    api_envelope: Distribution,
71    changed_paths: Distribution,
72    stages: StageDistributions,
73}
74
75#[derive(Clone, Copy, Debug)]
76struct StageDistributions {
77    snapshot: Distribution,
78    execute: Distribution,
79    diff: Distribution,
80    policy: Distribution,
81    bind_and_store: Distribution,
82    commit: Distribution,
83}
84
85#[derive(Debug)]
86struct ParallelSummary {
87    workers: usize,
88    sequential_wall_ns: u64,
89    parallel_wall_ns: u64,
90    samples: Vec<Sample>,
91}
92
93impl ParallelSummary {
94    #[allow(clippy::cast_precision_loss)]
95    fn speedup(&self) -> f64 {
96        self.sequential_wall_ns as f64 / self.parallel_wall_ns as f64
97    }
98}
99
100#[derive(Debug)]
101struct Arguments {
102    iterations: usize,
103    cold_iterations: usize,
104    parallel_workers: usize,
105    worker: PathBuf,
106    output: PathBuf,
107}
108
109type NamedCase = (&'static str, CaseSummary);
110
111#[derive(Clone, Copy)]
112struct ReportInput<'a> {
113    arguments: &'a Arguments,
114    runtime_open_ns: u64,
115    cold: Sample,
116    repeated_cold: (Distribution, Distribution),
117    large_fixture_ns: u64,
118    large_runtime_open_ns: u64,
119    cases: &'a [NamedCase],
120    parallel: &'a ParallelSummary,
121}
122
123struct TempWorkspace {
124    path: PathBuf,
125}
126
127impl TempWorkspace {
128    fn create(label: &str) -> io::Result<Self> {
129        let sequence = TEMP_SEQUENCE.fetch_add(1, Ordering::Relaxed);
130        let timestamp = unix_duration().as_nanos();
131        let path = std::env::temp_dir().join(format!(
132            "vsh-native-benchmark-{label}-{}-{timestamp}-{sequence}",
133            std::process::id()
134        ));
135        fs::create_dir(&path)?;
136        Ok(Self { path })
137    }
138}
139
140impl Drop for TempWorkspace {
141    fn drop(&mut self) {
142        let _ = fs::remove_dir_all(&self.path);
143    }
144}
145
146fn unix_duration() -> Duration {
147    SystemTime::now()
148        .duration_since(UNIX_EPOCH)
149        .unwrap_or(Duration::ZERO)
150}
151
152fn elapsed_ns(started: Instant) -> u64 {
153    u64::try_from(started.elapsed().as_nanos()).unwrap_or(u64::MAX)
154}
155
156fn percentile(ordered: &[u64], numerator: usize) -> u64 {
157    let index = ((ordered.len() - 1) * numerator + 50) / 100;
158    ordered[index]
159}
160
161fn distribution(values: impl IntoIterator<Item = u64>) -> Distribution {
162    let mut ordered: Vec<u64> = values.into_iter().collect();
163    ordered.sort_unstable();
164    Distribution {
165        min: ordered[0],
166        p50: percentile(&ordered, 50),
167        p95: percentile(&ordered, 95),
168        p99: percentile(&ordered, 99),
169        max: ordered[ordered.len() - 1],
170    }
171}
172
173fn summarize(samples: Vec<Sample>) -> CaseSummary {
174    let wall = distribution(samples.iter().map(|sample| sample.wall_ns));
175    let internal = distribution(samples.iter().map(|sample| sample.internal_ns));
176    let api_envelope = distribution(samples.iter().map(|sample| sample.api_envelope_ns()));
177    let changed_paths = distribution(samples.iter().map(|sample| sample.changed_paths));
178    let stages = StageDistributions {
179        snapshot: distribution(samples.iter().map(|sample| sample.stages.snapshot)),
180        execute: distribution(samples.iter().map(|sample| sample.stages.execute)),
181        diff: distribution(samples.iter().map(|sample| sample.stages.diff)),
182        policy: distribution(samples.iter().map(|sample| sample.stages.policy)),
183        bind_and_store: distribution(samples.iter().map(|sample| sample.stages.bind_and_store)),
184        commit: distribution(samples.iter().map(|sample| sample.stages.commit)),
185    };
186    CaseSummary {
187        samples,
188        wall,
189        internal,
190        api_envelope,
191        changed_paths,
192        stages,
193    }
194}
195
196fn sample(runtime: &Runtime, code: &str, intent: &str) -> Result<Sample, Box<dyn Error>> {
197    let started = Instant::now();
198    let receipt = runtime.preview(RunRequest::new(code).with_intent(intent))?;
199    let wall_ns = elapsed_ns(started);
200    if receipt.state == TransactionState::AutoApproved
201        && !runtime.discard_preview(receipt.transaction)?
202    {
203        return Err(io::Error::other("runtime did not retain its auto-approved preview").into());
204    }
205    Ok(Sample {
206        wall_ns,
207        internal_ns: receipt.timings.total_ns,
208        stages: receipt.timings.into(),
209        state: receipt.state,
210        changed_paths: u64::try_from(receipt.changed_paths).unwrap_or(u64::MAX),
211    })
212}
213
214fn run_case(
215    runtime: &Runtime,
216    code: &str,
217    name: &str,
218    iterations: usize,
219    expected_state: TransactionState,
220) -> Result<CaseSummary, Box<dyn Error>> {
221    let warmup = sample(runtime, code, &format!("{name}-warmup"))?;
222    if warmup.state != expected_state {
223        return Err(io::Error::other(format!(
224            "{name} warmup returned {:?}, expected {expected_state:?}",
225            warmup.state
226        ))
227        .into());
228    }
229    let samples = (0..iterations)
230        .map(|index| sample(runtime, code, &format!("{name}-{index}")))
231        .collect::<Result<Vec<_>, _>>()?;
232    if let Some(sample) = samples.iter().find(|sample| sample.state != expected_state) {
233        return Err(io::Error::other(format!(
234            "{name} returned {:?}, expected {expected_state:?}",
235            sample.state
236        ))
237        .into());
238    }
239    Ok(summarize(samples))
240}
241
242fn small_fixture(root: &Path) -> io::Result<()> {
243    for index in 0..20 {
244        fs::write(
245            root.join(format!("input-{index:02}.txt")),
246            format!("line-{index}\n").repeat(32),
247        )?;
248    }
249    Ok(())
250}
251
252fn large_tree_fixture(root: &Path) -> io::Result<()> {
253    let large_tree = root.join("large-tree");
254    fs::create_dir(&large_tree)?;
255    for directory_index in 0..100 {
256        let directory = large_tree.join(format!("dir-{directory_index:03}"));
257        fs::create_dir(&directory)?;
258        for file_index in 0..100 {
259            fs::File::create(directory.join(format!("file-{file_index:03}.txt")))?;
260        }
261    }
262    Ok(())
263}
264
265fn read_ten_program() -> String {
266    let reads = (0..10)
267        .map(|index| format!("Path('/workspace/input-{index:02}.txt').read_bytes()"))
268        .collect::<Vec<_>>()
269        .join(",\n    ");
270    format!("from pathlib import Path\nlen(b''.join([\n    {reads}\n]))")
271}
272
273fn edit_twenty_program() -> String {
274    let writes = (0..20)
275        .map(|index| {
276            format!("Path('/workspace/output-{index:02}.txt').write_text('value-{index}')")
277        })
278        .collect::<Vec<_>>()
279        .join("\n");
280    format!("from pathlib import Path\n{writes}\n20")
281}
282
283fn search_ten_thousand_program() -> &'static str {
284    concat!(
285        "import os\n",
286        "matches = 0\n",
287        "root = '/workspace/large-tree'\n",
288        "for directory in os.listdir(root):\n",
289        "    for name in os.listdir(root + '/' + directory):\n",
290        "        if name.endswith('7.txt'):\n",
291        "            matches += 1\n",
292        "matches\n",
293    )
294}
295
296fn rename_subtree_program() -> &'static str {
297    "import os\nos.rename('/workspace/large-tree/dir-000', '/workspace/moved-tree')\nNone\n"
298}
299
300fn delete_subtree_program() -> &'static str {
301    concat!(
302        "import os\n",
303        "root = '/workspace/large-tree/dir-000'\n",
304        "for name in os.listdir(root):\n",
305        "    os.unlink(root + '/' + name)\n",
306        "os.rmdir(root)\n",
307        "None\n",
308    )
309}
310
311fn massive_delete_program() -> &'static str {
312    concat!(
313        "import os\n",
314        "root = '/workspace/large-tree'\n",
315        "for directory in sorted(os.listdir(root))[:50]:\n",
316        "    subtree = root + '/' + directory\n",
317        "    for name in os.listdir(subtree):\n",
318        "        os.unlink(subtree + '/' + name)\n",
319        "    os.rmdir(subtree)\n",
320        "None\n",
321    )
322}
323
324fn repeated_cold_case(
325    iterations: usize,
326    worker: &Path,
327) -> Result<(Distribution, Distribution), Box<dyn Error>> {
328    let mut opens = Vec::with_capacity(iterations);
329    let mut first_calls = Vec::with_capacity(iterations);
330    for index in 0..iterations {
331        let workspace = TempWorkspace::create(&format!("cold-{index}"))?;
332        let open_started = Instant::now();
333        let runtime = open_runtime(&workspace.path, worker)?;
334        opens.push(elapsed_ns(open_started));
335        let first = sample(&runtime, "None", &format!("cold-{index}"))?;
336        if first.state != TransactionState::AutoApproved {
337            return Err(
338                io::Error::other(format!("cold first call returned {:?}", first.state)).into(),
339            );
340        }
341        first_calls.push(first.wall_ns);
342    }
343    Ok((distribution(opens), distribution(first_calls)))
344}
345
346fn open_runtime(root: &Path, worker: &Path) -> Result<Runtime, Box<dyn Error>> {
347    Ok(Runtime::open(
348        RuntimeConfig::new(root).with_worker_path(worker),
349    )?)
350}
351
352fn parallel_case(worker_count: usize, worker: &Path) -> Result<ParallelSummary, Box<dyn Error>> {
353    let workspaces = (0..worker_count)
354        .map(|index| TempWorkspace::create(&format!("parallel-{index}")))
355        .collect::<Result<Vec<_>, _>>()?;
356    let runtimes = workspaces
357        .iter()
358        .map(|workspace| open_runtime(&workspace.path, worker).map(Arc::new))
359        .collect::<Result<Vec<_>, _>>()?;
360
361    for (index, runtime) in runtimes.iter().enumerate() {
362        sample(runtime, "None", &format!("parallel-warmup-{index}"))?;
363    }
364
365    let sequential_started = Instant::now();
366    for (index, runtime) in runtimes.iter().enumerate() {
367        for transaction in 0..PARALLEL_TRANSACTIONS_PER_RUNTIME {
368            sample(
369                runtime,
370                "sum(range(10000))",
371                &format!("sequential-{index}-{transaction}"),
372            )?;
373        }
374    }
375    let sequential_wall_ns = elapsed_ns(sequential_started);
376
377    let parallel_started = Instant::now();
378    let handles = runtimes
379        .iter()
380        .enumerate()
381        .map(|(index, runtime)| {
382            let runtime = Arc::clone(runtime);
383            thread::spawn(move || {
384                (0..PARALLEL_TRANSACTIONS_PER_RUNTIME)
385                    .map(|transaction| {
386                        sample(
387                            &runtime,
388                            "sum(range(10000))",
389                            &format!("parallel-{index}-{transaction}"),
390                        )
391                        .map_err(|error| error.to_string())
392                    })
393                    .collect::<Result<Vec<_>, _>>()
394            })
395        })
396        .collect::<Vec<_>>();
397    let sample_groups = handles
398        .into_iter()
399        .map(|handle| {
400            handle
401                .join()
402                .map_err(|_| io::Error::other("parallel benchmark thread panicked"))?
403                .map_err(io::Error::other)
404        })
405        .collect::<Result<Vec<_>, _>>()?;
406    let samples = sample_groups.into_iter().flatten().collect();
407    let parallel_wall_ns = elapsed_ns(parallel_started);
408
409    Ok(ParallelSummary {
410        workers: worker_count,
411        sequential_wall_ns,
412        parallel_wall_ns,
413        samples,
414    })
415}
416
417fn required_value(
418    arguments: &mut impl Iterator<Item = std::ffi::OsString>,
419    flag: &str,
420) -> io::Result<std::ffi::OsString> {
421    arguments.next().ok_or_else(|| {
422        io::Error::new(
423            io::ErrorKind::InvalidInput,
424            format!("{flag} requires a value"),
425        )
426    })
427}
428
429fn parse_usize(value: &std::ffi::OsStr, flag: &str) -> io::Result<usize> {
430    value.to_string_lossy().parse().map_err(|error| {
431        io::Error::new(
432            io::ErrorKind::InvalidInput,
433            format!("invalid {flag} value: {error}"),
434        )
435    })
436}
437
438fn parse_arguments() -> io::Result<Arguments> {
439    let mut parsed = Arguments {
440        iterations: 100,
441        cold_iterations: 30,
442        parallel_workers: 4,
443        worker: PathBuf::from("vsh-monty-worker"),
444        output: PathBuf::from("reports/rust-rewrite/native-rust.json"),
445    };
446    let mut arguments = std::env::args_os().skip(1);
447    while let Some(argument) = arguments.next() {
448        match argument.to_string_lossy().as_ref() {
449            "--iterations" => {
450                let value = required_value(&mut arguments, "--iterations")?;
451                parsed.iterations = parse_usize(&value, "--iterations")?;
452            }
453            "--cold-iterations" => {
454                let value = required_value(&mut arguments, "--cold-iterations")?;
455                parsed.cold_iterations = parse_usize(&value, "--cold-iterations")?;
456            }
457            "--parallel-workers" => {
458                let value = required_value(&mut arguments, "--parallel-workers")?;
459                parsed.parallel_workers = parse_usize(&value, "--parallel-workers")?;
460            }
461            "--worker" => {
462                parsed.worker = PathBuf::from(required_value(&mut arguments, "--worker")?);
463            }
464            "--output" => {
465                parsed.output = PathBuf::from(required_value(&mut arguments, "--output")?);
466            }
467            flag => {
468                return Err(io::Error::new(
469                    io::ErrorKind::InvalidInput,
470                    format!("unknown argument: {flag}"),
471                ));
472            }
473        }
474    }
475    if parsed.iterations < 3 {
476        return Err(io::Error::new(
477            io::ErrorKind::InvalidInput,
478            "--iterations must be at least 3",
479        ));
480    }
481    if parsed.parallel_workers < 2 {
482        return Err(io::Error::new(
483            io::ErrorKind::InvalidInput,
484            "--parallel-workers must be at least 2",
485        ));
486    }
487    if parsed.cold_iterations < 3 {
488        return Err(io::Error::new(
489            io::ErrorKind::InvalidInput,
490            "--cold-iterations must be at least 3",
491        ));
492    }
493    Ok(parsed)
494}
495
496fn push_json_string(output: &mut String, value: &str) {
497    output.push('"');
498    for character in value.chars() {
499        match character {
500            '"' => output.push_str("\\\""),
501            '\\' => output.push_str("\\\\"),
502            '\n' => output.push_str("\\n"),
503            '\r' => output.push_str("\\r"),
504            '\t' => output.push_str("\\t"),
505            character if character.is_control() => {
506                write!(output, "\\u{:04x}", u32::from(character))
507                    .expect("writing to String cannot fail");
508            }
509            character => output.push(character),
510        }
511    }
512    output.push('"');
513}
514
515fn push_distribution(output: &mut String, value: Distribution) {
516    write!(
517        output,
518        "{{\"min\":{},\"p50\":{},\"p95\":{},\"p99\":{},\"max\":{}}}",
519        value.min, value.p50, value.p95, value.p99, value.max
520    )
521    .expect("writing to String cannot fail");
522}
523
524fn push_stage_distributions(output: &mut String, value: StageDistributions) {
525    output.push_str("{\"snapshot\":");
526    push_distribution(output, value.snapshot);
527    output.push_str(",\"execute\":");
528    push_distribution(output, value.execute);
529    output.push_str(",\"diff\":");
530    push_distribution(output, value.diff);
531    output.push_str(",\"policy\":");
532    push_distribution(output, value.policy);
533    output.push_str(",\"bind_and_store\":");
534    push_distribution(output, value.bind_and_store);
535    output.push_str(",\"commit\":");
536    push_distribution(output, value.commit);
537    output.push('}');
538}
539
540fn push_samples(output: &mut String, samples: &[Sample]) {
541    output.push('[');
542    for (index, sample) in samples.iter().enumerate() {
543        if index != 0 {
544            output.push(',');
545        }
546        write!(
547            output,
548            "{{\"wall_ns\":{},\"internal_ns\":{},\"api_envelope_ns\":{},\"state\":",
549            sample.wall_ns,
550            sample.internal_ns,
551            sample.api_envelope_ns()
552        )
553        .expect("writing to String cannot fail");
554        push_json_string(output, transaction_state_name(sample.state));
555        write!(output, ",\"changed_paths\":{}}}", sample.changed_paths)
556            .expect("writing to String cannot fail");
557    }
558    output.push(']');
559}
560
561fn push_case(output: &mut String, value: &CaseSummary) {
562    output.push_str("{\"samples\":");
563    push_samples(output, &value.samples);
564    output.push_str(",\"wall_ns\":");
565    push_distribution(output, value.wall);
566    output.push_str(",\"internal_ns\":");
567    push_distribution(output, value.internal);
568    output.push_str(",\"api_envelope_ns\":");
569    push_distribution(output, value.api_envelope);
570    output.push_str(",\"changed_paths\":");
571    push_distribution(output, value.changed_paths);
572    output.push_str(",\"state\":");
573    push_json_string(output, transaction_state_name(value.samples[0].state));
574    output.push_str(",\"stages_ns\":");
575    push_stage_distributions(output, value.stages);
576    output.push('}');
577}
578
579const fn transaction_state_name(state: TransactionState) -> &'static str {
580    match state {
581        TransactionState::Created => "created",
582        TransactionState::Running => "running",
583        TransactionState::VirtualComplete => "virtual_complete",
584        TransactionState::Denied => "denied",
585        TransactionState::AutoApproved => "auto_approved",
586        TransactionState::PendingApproval => "pending_approval",
587        TransactionState::Approved => "approved",
588        TransactionState::Reserved => "reserved",
589        TransactionState::Revalidating => "revalidating",
590        TransactionState::Committing => "committing",
591        TransactionState::Committed => "committed",
592        TransactionState::Stale => "stale",
593        TransactionState::Expired => "expired",
594        TransactionState::RecoveryRequired => "recovery_required",
595        TransactionState::Failed => "failed",
596        _ => "unknown",
597    }
598}
599
600fn json_report(report: ReportInput<'_>) -> String {
601    let ReportInput {
602        arguments,
603        runtime_open_ns,
604        cold,
605        repeated_cold,
606        large_fixture_ns,
607        large_runtime_open_ns,
608        cases,
609        parallel,
610    } = report;
611    let mut output = String::with_capacity(32 * 1024);
612    output.push_str("{\n  \"schema\":\"vsh-native-rust-benchmark-v2\",\n");
613    write!(
614        output,
615        "  \"captured_at_unix_ms\":{},\n  \"environment\":{{\"vsh_version\":",
616        unix_duration().as_millis()
617    )
618    .expect("writing to String cannot fail");
619    push_json_string(&mut output, VERSION);
620    output.push_str(",\"os\":");
621    push_json_string(&mut output, std::env::consts::OS);
622    output.push_str(",\"arch\":");
623    push_json_string(&mut output, std::env::consts::ARCH);
624    output.push_str(",\"worker\":");
625    push_json_string(&mut output, &arguments.worker.to_string_lossy());
626    write!(
627        output,
628        ",\"available_parallelism\":{}}},\n  \"iterations\":{},\n",
629        thread::available_parallelism().map_or(1, std::num::NonZero::get),
630        arguments.iterations
631    )
632    .expect("writing to String cannot fail");
633    write!(
634        output,
635        "  \"cold\":{{\"runtime_open_ns\":{runtime_open_ns},\"first_call_wall_ns\":{},\"first_call_internal_ns\":{},\"first_call_api_envelope_ns\":{},\"repeated_samples\":{},\"repeated_runtime_open_ns\":",
636        cold.wall_ns,
637        cold.internal_ns,
638        cold.api_envelope_ns(),
639        arguments.cold_iterations
640    )
641    .expect("writing to String cannot fail");
642    push_distribution(&mut output, repeated_cold.0);
643    output.push_str(",\"repeated_first_call_wall_ns\":");
644    push_distribution(&mut output, repeated_cold.1);
645    write!(
646        output,
647        "}},\n  \"large_tree\":{{\"directories\":100,\"files\":10000,\"fixture_ns\":{large_fixture_ns},\"runtime_open_ns\":{large_runtime_open_ns}}},\n  \"cases\":{{"
648    )
649    .expect("writing to String cannot fail");
650    for (index, (name, summary)) in cases.iter().enumerate() {
651        if index != 0 {
652            output.push(',');
653        }
654        push_json_string(&mut output, name);
655        output.push(':');
656        push_case(&mut output, summary);
657    }
658    write!(
659        output,
660        "}},\n  \"parallel_independent_runtimes\":{{\"workers\":{},\"transactions_per_runtime\":{},\"total_transactions\":{},\"sequential_wall_ns\":{},\"parallel_wall_ns\":{},\"speedup\":{:.6},\"samples\":",
661        parallel.workers,
662        PARALLEL_TRANSACTIONS_PER_RUNTIME,
663        parallel.workers * PARALLEL_TRANSACTIONS_PER_RUNTIME,
664        parallel.sequential_wall_ns,
665        parallel.parallel_wall_ns,
666        parallel.speedup()
667    )
668    .expect("writing to String cannot fail");
669    push_samples(&mut output, &parallel.samples);
670    output.push_str("}\n}\n");
671    output
672}
673
674fn markdown_report(
675    cases: &[(&str, CaseSummary)],
676    repeated_cold: (Distribution, Distribution),
677    parallel: &ParallelSummary,
678) -> String {
679    let mut output = String::from(
680        "# VSH native Rust benchmark\n\n| Case | state | changed paths | wall p50 ms | wall p99 ms | snapshot p50 ms | execute p50 ms | Rust API envelope p50 µs |\n|---|---|---:|---:|---:|---:|---:|---:|\n",
681    );
682    for (name, summary) in cases {
683        #[allow(clippy::cast_precision_loss)]
684        let wall_p50_ms = summary.wall.p50 as f64 / 1_000_000.0;
685        #[allow(clippy::cast_precision_loss)]
686        let wall_p99_ms = summary.wall.p99 as f64 / 1_000_000.0;
687        #[allow(clippy::cast_precision_loss)]
688        let envelope_p50_us = summary.api_envelope.p50 as f64 / 1_000.0;
689        #[allow(clippy::cast_precision_loss)]
690        let snapshot_p50_ms = summary.stages.snapshot.p50 as f64 / 1_000_000.0;
691        #[allow(clippy::cast_precision_loss)]
692        let execute_p50_ms = summary.stages.execute.p50 as f64 / 1_000_000.0;
693        writeln!(
694            output,
695            "| {name} | {} | {} | {wall_p50_ms:.3} | {wall_p99_ms:.3} | {snapshot_p50_ms:.3} | {execute_p50_ms:.3} | {envelope_p50_us:.1} |",
696            transaction_state_name(summary.samples[0].state),
697            summary.changed_paths.p50
698        )
699        .expect("writing to String cannot fail");
700    }
701    write!(
702        output,
703        "\nRepeated cold runtime-open p50/p99: {:.3}/{:.3} ms; first-call p50/p99: {:.3}/{:.3} ms.\n\nIndependent-runtime parallel speedup: {:.2}x across {} workers.\n",
704        ns_as_ms(repeated_cold.0.p50),
705        ns_as_ms(repeated_cold.0.p99),
706        ns_as_ms(repeated_cold.1.p50),
707        ns_as_ms(repeated_cold.1.p99),
708        parallel.speedup(),
709        parallel.workers
710    )
711    .expect("writing to String cannot fail");
712    output
713}
714
715#[allow(clippy::cast_precision_loss)]
716fn ns_as_ms(value: u64) -> f64 {
717    value as f64 / 1_000_000.0
718}
719
720fn benchmark_small(arguments: &Arguments) -> Result<(u64, Sample, Vec<NamedCase>), Box<dyn Error>> {
721    let workspace = TempWorkspace::create("primary")?;
722    small_fixture(&workspace.path)?;
723    let open_started = Instant::now();
724    let runtime = open_runtime(&workspace.path, &arguments.worker)?;
725    let runtime_open_ns = elapsed_ns(open_started);
726    let cold = sample(&runtime, "None", "cold-first-worker")?;
727    let read_ten = read_ten_program();
728    let edit_twenty = edit_twenty_program();
729    let cases = vec![
730        (
731            "noop",
732            run_case(
733                &runtime,
734                "None",
735                "noop",
736                arguments.iterations,
737                TransactionState::AutoApproved,
738            )?,
739        ),
740        (
741            "read_10",
742            run_case(
743                &runtime,
744                &read_ten,
745                "read-10",
746                arguments.iterations,
747                TransactionState::AutoApproved,
748            )?,
749        ),
750        (
751            "edit_20",
752            run_case(
753                &runtime,
754                &edit_twenty,
755                "edit-20",
756                arguments.iterations,
757                TransactionState::AutoApproved,
758            )?,
759        ),
760    ];
761    Ok((runtime_open_ns, cold, cases))
762}
763
764fn benchmark_large(arguments: &Arguments) -> Result<(u64, u64, Vec<NamedCase>), Box<dyn Error>> {
765    let large_workspace = TempWorkspace::create("large")?;
766    let fixture_started = Instant::now();
767    large_tree_fixture(&large_workspace.path)?;
768    let large_fixture_ns = elapsed_ns(fixture_started);
769    let large_open_started = Instant::now();
770    let large_runtime = open_runtime(&large_workspace.path, &arguments.worker)?;
771    let large_runtime_open_ns = elapsed_ns(large_open_started);
772    let cases = vec![
773        (
774            "search_10k",
775            run_case(
776                &large_runtime,
777                search_ten_thousand_program(),
778                "search-10k",
779                arguments.iterations,
780                TransactionState::AutoApproved,
781            )?,
782        ),
783        (
784            "rename_subtree_100",
785            run_case(
786                &large_runtime,
787                rename_subtree_program(),
788                "rename-subtree-100",
789                arguments.iterations,
790                TransactionState::PendingApproval,
791            )?,
792        ),
793        (
794            "delete_subtree_100",
795            run_case(
796                &large_runtime,
797                delete_subtree_program(),
798                "delete-subtree-100",
799                arguments.iterations,
800                TransactionState::PendingApproval,
801            )?,
802        ),
803        (
804            "massive_delete_5k",
805            run_case(
806                &large_runtime,
807                massive_delete_program(),
808                "massive-delete-5k",
809                arguments.iterations,
810                TransactionState::PendingApproval,
811            )?,
812        ),
813    ];
814    Ok((large_fixture_ns, large_runtime_open_ns, cases))
815}
816
817fn main() -> Result<(), Box<dyn Error>> {
818    let arguments = parse_arguments()?;
819    let (runtime_open_ns, cold, mut cases) = benchmark_small(&arguments)?;
820    let repeated_cold = repeated_cold_case(arguments.cold_iterations, &arguments.worker)?;
821    let (large_fixture_ns, large_runtime_open_ns, large_cases) = benchmark_large(&arguments)?;
822    cases.extend(large_cases);
823
824    let parallel = parallel_case(arguments.parallel_workers, &arguments.worker)?;
825
826    if let Some(parent) = arguments.output.parent() {
827        fs::create_dir_all(parent)?;
828    }
829    fs::write(
830        &arguments.output,
831        json_report(ReportInput {
832            arguments: &arguments,
833            runtime_open_ns,
834            cold,
835            repeated_cold,
836            large_fixture_ns,
837            large_runtime_open_ns,
838            cases: &cases,
839            parallel: &parallel,
840        }),
841    )?;
842    let markdown = arguments.output.with_extension("md");
843    fs::write(&markdown, markdown_report(&cases, repeated_cold, &parallel))?;
844    println!("{}", markdown.display());
845    Ok(())
846}