1use 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, ¶llel.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: ¶llel,
868 }),
869 )?;
870 let markdown = arguments.output.with_extension("md");
871 fs::write(&markdown, markdown_report(&cases, repeated_cold, ¶llel))?;
872 println!("{}", markdown.display());
873 Ok(())
874}