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 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, ¶llel.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: ¶llel,
840 }),
841 )?;
842 let markdown = arguments.output.with_extension("md");
843 fs::write(&markdown, markdown_report(&cases, repeated_cold, ¶llel))?;
844 println!("{}", markdown.display());
845 Ok(())
846}