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 glob_ten_thousand_with_vsh_program() -> &'static str {
297    "len(vsh_glob('**/*7.txt', path='/workspace/large-tree', max_results=1000))\n"
298}
299
300fn rename_subtree_program() -> &'static str {
301    "import os\nos.rename('/workspace/large-tree/dir-000', '/workspace/moved-tree')\nNone\n"
302}
303
304fn delete_subtree_program() -> &'static str {
305    concat!(
306        "import os\n",
307        "root = '/workspace/large-tree/dir-000'\n",
308        "for name in os.listdir(root):\n",
309        "    os.unlink(root + '/' + name)\n",
310        "os.rmdir(root)\n",
311        "None\n",
312    )
313}
314
315fn delete_subtree_with_vsh_program() -> &'static str {
316    "vsh_remove('/workspace/large-tree/dir-000', recursive=True)\nNone\n"
317}
318
319fn massive_delete_program() -> &'static str {
320    concat!(
321        "import os\n",
322        "root = '/workspace/large-tree'\n",
323        "for directory in sorted(os.listdir(root))[:50]:\n",
324        "    subtree = root + '/' + directory\n",
325        "    for name in os.listdir(subtree):\n",
326        "        os.unlink(subtree + '/' + name)\n",
327        "    os.rmdir(subtree)\n",
328        "None\n",
329    )
330}
331
332fn repeated_cold_case(
333    iterations: usize,
334    worker: &Path,
335) -> Result<(Distribution, Distribution), Box<dyn Error>> {
336    let mut opens = Vec::with_capacity(iterations);
337    let mut first_calls = Vec::with_capacity(iterations);
338    for index in 0..iterations {
339        let workspace = TempWorkspace::create(&format!("cold-{index}"))?;
340        let open_started = Instant::now();
341        let runtime = open_runtime(&workspace.path, worker)?;
342        opens.push(elapsed_ns(open_started));
343        let first = sample(&runtime, "None", &format!("cold-{index}"))?;
344        if first.state != TransactionState::AutoApproved {
345            return Err(
346                io::Error::other(format!("cold first call returned {:?}", first.state)).into(),
347            );
348        }
349        first_calls.push(first.wall_ns);
350    }
351    Ok((distribution(opens), distribution(first_calls)))
352}
353
354fn open_runtime(root: &Path, worker: &Path) -> Result<Runtime, Box<dyn Error>> {
355    Ok(Runtime::open(
356        RuntimeConfig::new(root).with_worker_path(worker),
357    )?)
358}
359
360fn parallel_case(worker_count: usize, worker: &Path) -> Result<ParallelSummary, Box<dyn Error>> {
361    let workspaces = (0..worker_count)
362        .map(|index| TempWorkspace::create(&format!("parallel-{index}")))
363        .collect::<Result<Vec<_>, _>>()?;
364    let runtimes = workspaces
365        .iter()
366        .map(|workspace| open_runtime(&workspace.path, worker).map(Arc::new))
367        .collect::<Result<Vec<_>, _>>()?;
368
369    for (index, runtime) in runtimes.iter().enumerate() {
370        sample(runtime, "None", &format!("parallel-warmup-{index}"))?;
371    }
372
373    let sequential_started = Instant::now();
374    for (index, runtime) in runtimes.iter().enumerate() {
375        for transaction in 0..PARALLEL_TRANSACTIONS_PER_RUNTIME {
376            sample(
377                runtime,
378                "sum(range(10000))",
379                &format!("sequential-{index}-{transaction}"),
380            )?;
381        }
382    }
383    let sequential_wall_ns = elapsed_ns(sequential_started);
384
385    let parallel_started = Instant::now();
386    let handles = runtimes
387        .iter()
388        .enumerate()
389        .map(|(index, runtime)| {
390            let runtime = Arc::clone(runtime);
391            thread::spawn(move || {
392                (0..PARALLEL_TRANSACTIONS_PER_RUNTIME)
393                    .map(|transaction| {
394                        sample(
395                            &runtime,
396                            "sum(range(10000))",
397                            &format!("parallel-{index}-{transaction}"),
398                        )
399                        .map_err(|error| error.to_string())
400                    })
401                    .collect::<Result<Vec<_>, _>>()
402            })
403        })
404        .collect::<Vec<_>>();
405    let sample_groups = handles
406        .into_iter()
407        .map(|handle| {
408            handle
409                .join()
410                .map_err(|_| io::Error::other("parallel benchmark thread panicked"))?
411                .map_err(io::Error::other)
412        })
413        .collect::<Result<Vec<_>, _>>()?;
414    let samples = sample_groups.into_iter().flatten().collect();
415    let parallel_wall_ns = elapsed_ns(parallel_started);
416
417    Ok(ParallelSummary {
418        workers: worker_count,
419        sequential_wall_ns,
420        parallel_wall_ns,
421        samples,
422    })
423}
424
425fn required_value(
426    arguments: &mut impl Iterator<Item = std::ffi::OsString>,
427    flag: &str,
428) -> io::Result<std::ffi::OsString> {
429    arguments.next().ok_or_else(|| {
430        io::Error::new(
431            io::ErrorKind::InvalidInput,
432            format!("{flag} requires a value"),
433        )
434    })
435}
436
437fn parse_usize(value: &std::ffi::OsStr, flag: &str) -> io::Result<usize> {
438    value.to_string_lossy().parse().map_err(|error| {
439        io::Error::new(
440            io::ErrorKind::InvalidInput,
441            format!("invalid {flag} value: {error}"),
442        )
443    })
444}
445
446fn parse_arguments() -> io::Result<Arguments> {
447    let mut parsed = Arguments {
448        iterations: 100,
449        cold_iterations: 30,
450        parallel_workers: 4,
451        worker: PathBuf::from("vsh-monty-worker"),
452        output: PathBuf::from("benchmarks/results/local/native-rust.json"),
453    };
454    let mut arguments = std::env::args_os().skip(1);
455    while let Some(argument) = arguments.next() {
456        match argument.to_string_lossy().as_ref() {
457            "--iterations" => {
458                let value = required_value(&mut arguments, "--iterations")?;
459                parsed.iterations = parse_usize(&value, "--iterations")?;
460            }
461            "--cold-iterations" => {
462                let value = required_value(&mut arguments, "--cold-iterations")?;
463                parsed.cold_iterations = parse_usize(&value, "--cold-iterations")?;
464            }
465            "--parallel-workers" => {
466                let value = required_value(&mut arguments, "--parallel-workers")?;
467                parsed.parallel_workers = parse_usize(&value, "--parallel-workers")?;
468            }
469            "--worker" => {
470                parsed.worker = PathBuf::from(required_value(&mut arguments, "--worker")?);
471            }
472            "--output" => {
473                parsed.output = PathBuf::from(required_value(&mut arguments, "--output")?);
474            }
475            flag => {
476                return Err(io::Error::new(
477                    io::ErrorKind::InvalidInput,
478                    format!("unknown argument: {flag}"),
479                ));
480            }
481        }
482    }
483    if parsed.iterations < 3 {
484        return Err(io::Error::new(
485            io::ErrorKind::InvalidInput,
486            "--iterations must be at least 3",
487        ));
488    }
489    if parsed.parallel_workers < 2 {
490        return Err(io::Error::new(
491            io::ErrorKind::InvalidInput,
492            "--parallel-workers must be at least 2",
493        ));
494    }
495    if parsed.cold_iterations < 3 {
496        return Err(io::Error::new(
497            io::ErrorKind::InvalidInput,
498            "--cold-iterations must be at least 3",
499        ));
500    }
501    Ok(parsed)
502}
503
504fn push_json_string(output: &mut String, value: &str) {
505    output.push('"');
506    for character in value.chars() {
507        match character {
508            '"' => output.push_str("\\\""),
509            '\\' => output.push_str("\\\\"),
510            '\n' => output.push_str("\\n"),
511            '\r' => output.push_str("\\r"),
512            '\t' => output.push_str("\\t"),
513            character if character.is_control() => {
514                write!(output, "\\u{:04x}", u32::from(character))
515                    .expect("writing to String cannot fail");
516            }
517            character => output.push(character),
518        }
519    }
520    output.push('"');
521}
522
523fn push_distribution(output: &mut String, value: Distribution) {
524    write!(
525        output,
526        "{{\"min\":{},\"p50\":{},\"p95\":{},\"p99\":{},\"max\":{}}}",
527        value.min, value.p50, value.p95, value.p99, value.max
528    )
529    .expect("writing to String cannot fail");
530}
531
532fn push_stage_distributions(output: &mut String, value: StageDistributions) {
533    output.push_str("{\"snapshot\":");
534    push_distribution(output, value.snapshot);
535    output.push_str(",\"execute\":");
536    push_distribution(output, value.execute);
537    output.push_str(",\"diff\":");
538    push_distribution(output, value.diff);
539    output.push_str(",\"policy\":");
540    push_distribution(output, value.policy);
541    output.push_str(",\"bind_and_store\":");
542    push_distribution(output, value.bind_and_store);
543    output.push_str(",\"commit\":");
544    push_distribution(output, value.commit);
545    output.push('}');
546}
547
548fn push_samples(output: &mut String, samples: &[Sample]) {
549    output.push('[');
550    for (index, sample) in samples.iter().enumerate() {
551        if index != 0 {
552            output.push(',');
553        }
554        write!(
555            output,
556            "{{\"wall_ns\":{},\"internal_ns\":{},\"api_envelope_ns\":{},\"state\":",
557            sample.wall_ns,
558            sample.internal_ns,
559            sample.api_envelope_ns()
560        )
561        .expect("writing to String cannot fail");
562        push_json_string(output, transaction_state_name(sample.state));
563        write!(output, ",\"changed_paths\":{}}}", sample.changed_paths)
564            .expect("writing to String cannot fail");
565    }
566    output.push(']');
567}
568
569fn push_case(output: &mut String, value: &CaseSummary) {
570    output.push_str("{\"samples\":");
571    push_samples(output, &value.samples);
572    output.push_str(",\"wall_ns\":");
573    push_distribution(output, value.wall);
574    output.push_str(",\"internal_ns\":");
575    push_distribution(output, value.internal);
576    output.push_str(",\"api_envelope_ns\":");
577    push_distribution(output, value.api_envelope);
578    output.push_str(",\"changed_paths\":");
579    push_distribution(output, value.changed_paths);
580    output.push_str(",\"state\":");
581    push_json_string(output, transaction_state_name(value.samples[0].state));
582    output.push_str(",\"stages_ns\":");
583    push_stage_distributions(output, value.stages);
584    output.push('}');
585}
586
587const fn transaction_state_name(state: TransactionState) -> &'static str {
588    match state {
589        TransactionState::Created => "created",
590        TransactionState::Running => "running",
591        TransactionState::VirtualComplete => "virtual_complete",
592        TransactionState::Denied => "denied",
593        TransactionState::AutoApproved => "auto_approved",
594        TransactionState::PendingApproval => "pending_approval",
595        TransactionState::Approved => "approved",
596        TransactionState::Reserved => "reserved",
597        TransactionState::Revalidating => "revalidating",
598        TransactionState::Committing => "committing",
599        TransactionState::Committed => "committed",
600        TransactionState::Stale => "stale",
601        TransactionState::Expired => "expired",
602        TransactionState::RecoveryRequired => "recovery_required",
603        TransactionState::Failed => "failed",
604        _ => "unknown",
605    }
606}
607
608fn json_report(report: ReportInput<'_>) -> String {
609    let ReportInput {
610        arguments,
611        runtime_open_ns,
612        cold,
613        repeated_cold,
614        large_fixture_ns,
615        large_runtime_open_ns,
616        cases,
617        parallel,
618    } = report;
619    let mut output = String::with_capacity(32 * 1024);
620    output.push_str("{\n  \"schema\":\"vsh-native-rust-benchmark-v2\",\n");
621    write!(
622        output,
623        "  \"captured_at_unix_ms\":{},\n  \"environment\":{{\"vsh_version\":",
624        unix_duration().as_millis()
625    )
626    .expect("writing to String cannot fail");
627    push_json_string(&mut output, VERSION);
628    output.push_str(",\"os\":");
629    push_json_string(&mut output, std::env::consts::OS);
630    output.push_str(",\"arch\":");
631    push_json_string(&mut output, std::env::consts::ARCH);
632    output.push_str(",\"worker\":");
633    push_json_string(&mut output, &arguments.worker.to_string_lossy());
634    write!(
635        output,
636        ",\"available_parallelism\":{}}},\n  \"iterations\":{},\n",
637        thread::available_parallelism().map_or(1, std::num::NonZero::get),
638        arguments.iterations
639    )
640    .expect("writing to String cannot fail");
641    write!(
642        output,
643        "  \"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\":",
644        cold.wall_ns,
645        cold.internal_ns,
646        cold.api_envelope_ns(),
647        arguments.cold_iterations
648    )
649    .expect("writing to String cannot fail");
650    push_distribution(&mut output, repeated_cold.0);
651    output.push_str(",\"repeated_first_call_wall_ns\":");
652    push_distribution(&mut output, repeated_cold.1);
653    write!(
654        output,
655        "}},\n  \"large_tree\":{{\"directories\":100,\"files\":10000,\"fixture_ns\":{large_fixture_ns},\"runtime_open_ns\":{large_runtime_open_ns}}},\n  \"cases\":{{"
656    )
657    .expect("writing to String cannot fail");
658    for (index, (name, summary)) in cases.iter().enumerate() {
659        if index != 0 {
660            output.push(',');
661        }
662        push_json_string(&mut output, name);
663        output.push(':');
664        push_case(&mut output, summary);
665    }
666    write!(
667        output,
668        "}},\n  \"parallel_independent_runtimes\":{{\"workers\":{},\"transactions_per_runtime\":{},\"total_transactions\":{},\"sequential_wall_ns\":{},\"parallel_wall_ns\":{},\"speedup\":{:.6},\"samples\":",
669        parallel.workers,
670        PARALLEL_TRANSACTIONS_PER_RUNTIME,
671        parallel.workers * PARALLEL_TRANSACTIONS_PER_RUNTIME,
672        parallel.sequential_wall_ns,
673        parallel.parallel_wall_ns,
674        parallel.speedup()
675    )
676    .expect("writing to String cannot fail");
677    push_samples(&mut output, &parallel.samples);
678    output.push_str("}\n}\n");
679    output
680}
681
682fn markdown_report(
683    cases: &[(&str, CaseSummary)],
684    repeated_cold: (Distribution, Distribution),
685    parallel: &ParallelSummary,
686) -> String {
687    let mut output = String::from(
688        "# 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",
689    );
690    for (name, summary) in cases {
691        #[allow(clippy::cast_precision_loss)]
692        let wall_p50_ms = summary.wall.p50 as f64 / 1_000_000.0;
693        #[allow(clippy::cast_precision_loss)]
694        let wall_p99_ms = summary.wall.p99 as f64 / 1_000_000.0;
695        #[allow(clippy::cast_precision_loss)]
696        let envelope_p50_us = summary.api_envelope.p50 as f64 / 1_000.0;
697        #[allow(clippy::cast_precision_loss)]
698        let snapshot_p50_ms = summary.stages.snapshot.p50 as f64 / 1_000_000.0;
699        #[allow(clippy::cast_precision_loss)]
700        let execute_p50_ms = summary.stages.execute.p50 as f64 / 1_000_000.0;
701        writeln!(
702            output,
703            "| {name} | {} | {} | {wall_p50_ms:.3} | {wall_p99_ms:.3} | {snapshot_p50_ms:.3} | {execute_p50_ms:.3} | {envelope_p50_us:.1} |",
704            transaction_state_name(summary.samples[0].state),
705            summary.changed_paths.p50
706        )
707        .expect("writing to String cannot fail");
708    }
709    write!(
710        output,
711        "\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",
712        ns_as_ms(repeated_cold.0.p50),
713        ns_as_ms(repeated_cold.0.p99),
714        ns_as_ms(repeated_cold.1.p50),
715        ns_as_ms(repeated_cold.1.p99),
716        parallel.speedup(),
717        parallel.workers
718    )
719    .expect("writing to String cannot fail");
720    output
721}
722
723#[allow(clippy::cast_precision_loss)]
724fn ns_as_ms(value: u64) -> f64 {
725    value as f64 / 1_000_000.0
726}
727
728fn benchmark_small(arguments: &Arguments) -> Result<(u64, Sample, Vec<NamedCase>), Box<dyn Error>> {
729    let workspace = TempWorkspace::create("primary")?;
730    small_fixture(&workspace.path)?;
731    let open_started = Instant::now();
732    let runtime = open_runtime(&workspace.path, &arguments.worker)?;
733    let runtime_open_ns = elapsed_ns(open_started);
734    let cold = sample(&runtime, "None", "cold-first-worker")?;
735    let read_ten = read_ten_program();
736    let edit_twenty = edit_twenty_program();
737    let cases = vec![
738        (
739            "noop",
740            run_case(
741                &runtime,
742                "None",
743                "noop",
744                arguments.iterations,
745                TransactionState::AutoApproved,
746            )?,
747        ),
748        (
749            "read_10",
750            run_case(
751                &runtime,
752                &read_ten,
753                "read-10",
754                arguments.iterations,
755                TransactionState::AutoApproved,
756            )?,
757        ),
758        (
759            "edit_20",
760            run_case(
761                &runtime,
762                &edit_twenty,
763                "edit-20",
764                arguments.iterations,
765                TransactionState::AutoApproved,
766            )?,
767        ),
768    ];
769    Ok((runtime_open_ns, cold, cases))
770}
771
772fn benchmark_large(arguments: &Arguments) -> Result<(u64, u64, Vec<NamedCase>), Box<dyn Error>> {
773    let large_workspace = TempWorkspace::create("large")?;
774    let fixture_started = Instant::now();
775    large_tree_fixture(&large_workspace.path)?;
776    let large_fixture_ns = elapsed_ns(fixture_started);
777    let large_open_started = Instant::now();
778    let large_runtime = open_runtime(&large_workspace.path, &arguments.worker)?;
779    let large_runtime_open_ns = elapsed_ns(large_open_started);
780    let cases = vec![
781        (
782            "search_10k",
783            run_case(
784                &large_runtime,
785                search_ten_thousand_program(),
786                "search-10k",
787                arguments.iterations,
788                TransactionState::AutoApproved,
789            )?,
790        ),
791        (
792            "vsh_glob_10k",
793            run_case(
794                &large_runtime,
795                glob_ten_thousand_with_vsh_program(),
796                "vsh-glob-10k",
797                arguments.iterations,
798                TransactionState::AutoApproved,
799            )?,
800        ),
801        (
802            "rename_subtree_100",
803            run_case(
804                &large_runtime,
805                rename_subtree_program(),
806                "rename-subtree-100",
807                arguments.iterations,
808                TransactionState::PendingApproval,
809            )?,
810        ),
811        (
812            "delete_subtree_100",
813            run_case(
814                &large_runtime,
815                delete_subtree_program(),
816                "delete-subtree-100",
817                arguments.iterations,
818                TransactionState::PendingApproval,
819            )?,
820        ),
821        (
822            "vsh_remove_subtree_100",
823            run_case(
824                &large_runtime,
825                delete_subtree_with_vsh_program(),
826                "vsh-remove-subtree-100",
827                arguments.iterations,
828                TransactionState::PendingApproval,
829            )?,
830        ),
831        (
832            "massive_delete_5k",
833            run_case(
834                &large_runtime,
835                massive_delete_program(),
836                "massive-delete-5k",
837                arguments.iterations,
838                TransactionState::PendingApproval,
839            )?,
840        ),
841    ];
842    Ok((large_fixture_ns, large_runtime_open_ns, cases))
843}
844
845fn main() -> Result<(), Box<dyn Error>> {
846    let arguments = parse_arguments()?;
847    let (runtime_open_ns, cold, mut cases) = benchmark_small(&arguments)?;
848    let repeated_cold = repeated_cold_case(arguments.cold_iterations, &arguments.worker)?;
849    let (large_fixture_ns, large_runtime_open_ns, large_cases) = benchmark_large(&arguments)?;
850    cases.extend(large_cases);
851
852    let parallel = parallel_case(arguments.parallel_workers, &arguments.worker)?;
853
854    if let Some(parent) = arguments.output.parent() {
855        fs::create_dir_all(parent)?;
856    }
857    fs::write(
858        &arguments.output,
859        json_report(ReportInput {
860            arguments: &arguments,
861            runtime_open_ns,
862            cold,
863            repeated_cold,
864            large_fixture_ns,
865            large_runtime_open_ns,
866            cases: &cases,
867            parallel: &parallel,
868        }),
869    )?;
870    let markdown = arguments.output.with_extension("md");
871    fs::write(&markdown, markdown_report(&cases, repeated_cold, &parallel))?;
872    println!("{}", markdown.display());
873    Ok(())
874}