1use crate::{
2 CreateJavascriptContextRequest, JavascriptExecutionEngine, JavascriptExecutionError,
3 StartJavascriptExecutionRequest,
4};
5use agentos_runtime::RuntimeContext;
6use serde::{Deserialize, Serialize};
7use std::collections::BTreeMap;
8use std::env;
9use std::fmt;
10use std::fmt::Write as _;
11use std::fs;
12use std::path::{Path, PathBuf};
13use std::process::Command;
14use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
15
16const BENCHMARK_MARKER_PREFIX: &str = "__AGENTOS_BENCH__:";
17const LOCAL_GRAPH_MODULE_COUNT: usize = 24;
18const BENCHMARK_ARTIFACT_VERSION: u32 = 5;
19const BENCHMARK_ARTIFACT_DIR: &str = "target/benchmark-reports/node-import-bench";
20const BENCHMARK_RUN_STATE_FILE: &str = "run-state.json";
21const TRANSPORT_RTT_CHANNEL: &str = "execution-stdio-echo";
22const TRANSPORT_RTT_PAYLOAD_BYTES: [usize; 3] = [32, 4 * 1024, 64 * 1024];
23const TRANSPORT_POLL_TIMEOUT: Duration = Duration::from_secs(5);
24const MAX_BENCHMARK_ITERATIONS: usize = 1_000;
25const MAX_BENCHMARK_WARMUP_ITERATIONS: usize = 1_000;
26
27#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
28pub struct JavascriptBenchmarkConfig {
29 pub iterations: usize,
30 pub warmup_iterations: usize,
31}
32
33impl Default for JavascriptBenchmarkConfig {
34 fn default() -> Self {
35 Self {
36 iterations: 5,
37 warmup_iterations: 1,
38 }
39 }
40}
41
42#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
43pub struct BenchmarkHost {
44 pub node_binary: String,
45 pub node_version: String,
46 pub os: &'static str,
47 pub arch: &'static str,
48 pub logical_cpus: usize,
49}
50
51#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
52pub struct BenchmarkScenarioPhases<T> {
53 pub context_setup_ms: T,
54 pub startup_ms: T,
55 #[serde(skip_serializing_if = "Option::is_none", default)]
56 pub guest_execution_ms: Option<T>,
57 pub completion_ms: T,
58}
59
60#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
61pub struct BenchmarkStats {
62 pub mean_ms: f64,
63 pub p50_ms: f64,
64 pub p95_ms: f64,
65 pub min_ms: f64,
66 pub max_ms: f64,
67 pub stddev_ms: f64,
68}
69
70#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
71pub struct BenchmarkDistributionStats {
72 pub mean: f64,
73 pub p50: f64,
74 pub p95: f64,
75 pub min: f64,
76 pub max: f64,
77 pub stddev: f64,
78}
79
80#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
81pub struct BenchmarkResourceUsage<T> {
82 #[serde(skip_serializing_if = "Option::is_none", default)]
83 pub rss_bytes: Option<T>,
84 #[serde(skip_serializing_if = "Option::is_none", default)]
85 pub heap_used_bytes: Option<T>,
86 #[serde(skip_serializing_if = "Option::is_none", default)]
87 pub cpu_user_us: Option<T>,
88 #[serde(skip_serializing_if = "Option::is_none", default)]
89 pub cpu_system_us: Option<T>,
90 #[serde(skip_serializing_if = "Option::is_none", default)]
91 pub cpu_total_us: Option<T>,
92}
93
94#[derive(Debug, Clone, PartialEq, Serialize)]
95pub struct BenchmarkTransportRttReport {
96 pub channel: &'static str,
97 pub payload_bytes: usize,
98 pub samples_ms: Vec<f64>,
99 pub stats: BenchmarkStats,
100}
101
102#[derive(Debug, Clone, PartialEq, Serialize)]
103pub struct BenchmarkScenarioReport {
104 pub id: &'static str,
105 pub workload: &'static str,
106 pub runtime: &'static str,
107 pub mode: &'static str,
108 pub description: &'static str,
109 pub fixture: &'static str,
110 pub compile_cache: &'static str,
111 pub wall_samples_ms: Vec<f64>,
112 pub wall_stats: BenchmarkStats,
113 pub guest_import_samples_ms: Option<Vec<f64>>,
114 pub guest_import_stats: Option<BenchmarkStats>,
115 pub startup_overhead_samples_ms: Option<Vec<f64>>,
116 pub startup_overhead_stats: Option<BenchmarkStats>,
117 pub phase_samples_ms: BenchmarkScenarioPhases<Vec<f64>>,
118 pub phase_stats: BenchmarkScenarioPhases<BenchmarkStats>,
119 #[serde(skip_serializing_if = "Option::is_none", default)]
120 pub resource_usage_samples: Option<BenchmarkResourceUsage<Vec<f64>>>,
121 #[serde(skip_serializing_if = "Option::is_none", default)]
122 pub resource_usage_stats: Option<BenchmarkResourceUsage<BenchmarkDistributionStats>>,
123}
124
125#[derive(Debug, Clone, PartialEq, Serialize)]
126pub struct JavascriptBenchmarkReport {
127 pub generated_at_unix_ms: u128,
128 pub config: JavascriptBenchmarkConfig,
129 pub host: BenchmarkHost,
130 pub repo_root: PathBuf,
131 pub transport_rtt: Vec<BenchmarkTransportRttReport>,
132 pub scenarios: Vec<BenchmarkScenarioReport>,
133}
134
135#[derive(Debug, Clone, PartialEq, Serialize)]
136pub struct BenchmarkComparison {
137 pub baseline: BenchmarkComparisonBaseline,
138 pub summary: BenchmarkComparisonSummary,
139 pub scenario_deltas: Vec<BenchmarkScenarioDelta>,
140 pub scenarios_missing_from_baseline: Vec<String>,
141 pub baseline_only_scenarios: Vec<String>,
142}
143
144#[derive(Debug, Clone, PartialEq, Serialize)]
145pub struct BenchmarkComparisonBaseline {
146 pub artifact_version: u32,
147 pub generated_at_unix_ms: u128,
148 pub path: PathBuf,
149}
150
151#[derive(Debug, Clone, PartialEq, Serialize)]
152pub struct BenchmarkComparisonSummary {
153 pub compared_scenario_count: usize,
154 #[serde(skip_serializing_if = "Option::is_none")]
155 pub largest_wall_improvement: Option<BenchmarkDeltaHighlight>,
156 #[serde(skip_serializing_if = "Option::is_none")]
157 pub largest_wall_regression: Option<BenchmarkDeltaHighlight>,
158}
159
160#[derive(Debug, Clone, PartialEq, Serialize)]
161pub struct BenchmarkDeltaHighlight {
162 pub id: String,
163 pub delta_ms: f64,
164 pub delta_pct: f64,
165}
166
167#[derive(Debug, Clone, PartialEq, Serialize)]
168pub struct BenchmarkScenarioDelta {
169 pub id: String,
170 pub description: String,
171 pub wall_mean_ms: BenchmarkMetricDelta,
172 #[serde(skip_serializing_if = "Option::is_none")]
173 pub guest_import_mean_ms: Option<BenchmarkMetricDelta>,
174 #[serde(skip_serializing_if = "Option::is_none")]
175 pub startup_overhead_mean_ms: Option<BenchmarkMetricDelta>,
176 #[serde(skip_serializing_if = "Option::is_none")]
177 pub phase_mean_ms: Option<BenchmarkScenarioPhases<BenchmarkMetricDelta>>,
178}
179
180#[derive(Debug, Clone, PartialEq, Serialize)]
181pub struct BenchmarkMetricDelta {
182 pub baseline_ms: f64,
183 pub current_ms: f64,
184 pub delta_ms: f64,
185 pub delta_pct: f64,
186}
187
188impl JavascriptBenchmarkReport {
189 pub fn render_markdown(&self) -> String {
190 self.render_markdown_with_comparison(None)
191 }
192
193 pub fn render_markdown_with_comparison(
194 &self,
195 comparison: Option<&BenchmarkComparison>,
196 ) -> String {
197 let mut markdown = String::new();
198 let _ = writeln!(&mut markdown, "# secure-exec Node Import Benchmark");
199 let _ = writeln!(&mut markdown);
200 let _ = writeln!(
201 &mut markdown,
202 "- Generated at unix ms: `{}`",
203 self.generated_at_unix_ms
204 );
205 let _ = writeln!(&mut markdown, "- Node binary: `{}`", self.host.node_binary);
206 let _ = writeln!(
207 &mut markdown,
208 "- Node version: `{}`",
209 self.host.node_version.trim()
210 );
211 let _ = writeln!(
212 &mut markdown,
213 "- Host: `{}` / `{}` / `{}` logical CPUs",
214 self.host.os, self.host.arch, self.host.logical_cpus
215 );
216 let _ = writeln!(&mut markdown, "- Repo root: `{}`", self.repo_root.display());
217 let _ = writeln!(
218 &mut markdown,
219 "- Iterations: `{}` recorded, `{}` warmup",
220 self.config.iterations, self.config.warmup_iterations
221 );
222 let _ = writeln!(
223 &mut markdown,
224 "- Reproduce: `cargo run -p agentos-execution --bin node-import-bench -- --iterations {} --warmup-iterations {}`",
225 self.config.iterations, self.config.warmup_iterations
226 );
227 let _ = writeln!(&mut markdown);
228 let _ = writeln!(&mut markdown, "## Transport RTT");
229 let _ = writeln!(&mut markdown);
230 let _ = writeln!(
231 &mut markdown,
232 "| Channel | Payload (bytes) | Mean RTT (ms) | P50 | P95 |"
233 );
234 let _ = writeln!(&mut markdown, "| --- | ---: | ---: | ---: | ---: |");
235
236 for transport in &self.transport_rtt {
237 let _ = writeln!(
238 &mut markdown,
239 "| `{}` | {} | {} | {} | {} |",
240 transport.channel,
241 transport.payload_bytes,
242 format_ms(transport.stats.mean_ms),
243 format_ms(transport.stats.p50_ms),
244 format_ms(transport.stats.p95_ms),
245 );
246 }
247
248 let _ = writeln!(&mut markdown, "## Control Matrix");
249 let _ = writeln!(&mut markdown);
250
251 for row in self.control_matrix() {
252 let _ = writeln!(
253 &mut markdown,
254 "- Workload `{}`: runtimes {}, modes {}, scenarios {}",
255 row.workload,
256 format_label_list(&row.runtimes),
257 format_label_list(&row.modes),
258 format_label_list(&row.scenario_ids),
259 );
260 }
261
262 let _ = writeln!(&mut markdown);
263 let _ = writeln!(&mut markdown, "## Scenario Summary");
264 let _ = writeln!(&mut markdown);
265 let _ = writeln!(
266 &mut markdown,
267 "| Scenario | Workload | Runtime | Mode | Fixture | Cache | Mean wall (ms) | Mean context (ms) | Mean startup (ms) | Mean guest exec (ms) | Mean completion (ms) | Mean startup overhead (ms) |"
268 );
269 let _ = writeln!(
270 &mut markdown,
271 "| --- | --- | --- | --- | --- | --- | ---: | ---: | ---: | ---: | ---: | ---: |"
272 );
273
274 for scenario in &self.scenarios {
275 let guest_execution_mean = scenario
276 .phase_stats
277 .guest_execution_ms
278 .as_ref()
279 .map(|stats| format_ms(stats.mean_ms))
280 .unwrap_or_else(|| String::from("n/a"));
281 let startup_overhead_mean = scenario
282 .startup_overhead_stats
283 .as_ref()
284 .map(|stats| format_ms(stats.mean_ms))
285 .unwrap_or_else(|| String::from("n/a"));
286
287 let _ = writeln!(
288 &mut markdown,
289 "| `{}` | `{}` | `{}` | `{}` | {} | {} | {} | {} | {} | {} | {} | {} |",
290 scenario.id,
291 scenario.workload,
292 scenario.runtime,
293 scenario.mode,
294 scenario.fixture,
295 scenario.compile_cache,
296 format_ms(scenario.wall_stats.mean_ms),
297 format_ms(scenario.phase_stats.context_setup_ms.mean_ms),
298 format_ms(scenario.phase_stats.startup_ms.mean_ms),
299 guest_execution_mean,
300 format_ms(scenario.phase_stats.completion_ms.mean_ms),
301 startup_overhead_mean,
302 );
303 }
304
305 let _ = writeln!(&mut markdown);
306 let _ = writeln!(&mut markdown, "## Stability And Resource Summary");
307 let _ = writeln!(&mut markdown);
308 let _ = writeln!(
309 &mut markdown,
310 "| Scenario | Wall P50 (ms) | Wall min-max (ms) | Wall stddev (ms) | Mean RSS (MiB) | Mean heap (MiB) | Mean total CPU (ms) |"
311 );
312 let _ = writeln!(
313 &mut markdown,
314 "| --- | ---: | --- | ---: | ---: | ---: | ---: |"
315 );
316
317 for scenario in &self.scenarios {
318 let _ = writeln!(
319 &mut markdown,
320 "| `{}` | {} | {}-{} | {} | {} | {} | {} |",
321 scenario.id,
322 format_ms(scenario.wall_stats.p50_ms),
323 format_ms(scenario.wall_stats.min_ms),
324 format_ms(scenario.wall_stats.max_ms),
325 format_ms(scenario.wall_stats.stddev_ms),
326 scenario
327 .resource_usage_stats
328 .as_ref()
329 .and_then(|stats| stats.rss_bytes.as_ref())
330 .map(|stats| format_mib(bytes_to_mib(stats.mean)))
331 .unwrap_or_else(|| String::from("n/a")),
332 scenario
333 .resource_usage_stats
334 .as_ref()
335 .and_then(|stats| stats.heap_used_bytes.as_ref())
336 .map(|stats| format_mib(bytes_to_mib(stats.mean)))
337 .unwrap_or_else(|| String::from("n/a")),
338 scenario
339 .resource_usage_stats
340 .as_ref()
341 .and_then(|stats| stats.cpu_total_us.as_ref())
342 .map(|stats| format_ms(micros_to_ms(stats.mean)))
343 .unwrap_or_else(|| String::from("n/a")),
344 );
345 }
346
347 let _ = writeln!(&mut markdown);
348 let _ = writeln!(&mut markdown, "## Ranked Hotspots");
349 let _ = writeln!(&mut markdown);
350
351 for ranking in self.hotspot_rankings() {
352 let _ = writeln!(
353 &mut markdown,
354 "### {} (`{}`, `{}`)",
355 ranking.label, ranking.dimension, ranking.unit
356 );
357 let _ = writeln!(&mut markdown);
358 let _ = writeln!(
359 &mut markdown,
360 "| Rank | Scenario | Workload | Runtime | Mode | Value |"
361 );
362 let _ = writeln!(&mut markdown, "| ---: | --- | --- | --- | --- | ---: |");
363
364 for scenario in &ranking.ranked_scenarios {
365 let _ = writeln!(
366 &mut markdown,
367 "| {} | `{}` | `{}` | `{}` | `{}` | {} |",
368 scenario.rank,
369 scenario.id,
370 scenario.workload,
371 scenario.runtime,
372 scenario.mode,
373 format_hotspot_value(ranking.unit, scenario.value),
374 );
375 }
376
377 if !ranking.scenarios_without_metric.is_empty() {
378 let _ = writeln!(&mut markdown);
379 let _ = writeln!(
380 &mut markdown,
381 "Missing metric for: {}",
382 format_string_label_list(&ranking.scenarios_without_metric),
383 );
384 }
385
386 let _ = writeln!(&mut markdown);
387 }
388
389 let _ = writeln!(&mut markdown, "## Hotspot Guidance");
390 let _ = writeln!(&mut markdown);
391
392 for line in self.guidance_lines() {
393 let _ = writeln!(&mut markdown, "- {line}");
394 }
395
396 if let Some(comparison) = comparison {
397 let _ = writeln!(&mut markdown);
398 let _ = writeln!(&mut markdown, "## Baseline Comparison");
399 let _ = writeln!(&mut markdown);
400 let _ = writeln!(
401 &mut markdown,
402 "- Baseline artifact: `{}`",
403 comparison.baseline.path.display()
404 );
405 let _ = writeln!(
406 &mut markdown,
407 "- Baseline generated at unix ms: `{}`",
408 comparison.baseline.generated_at_unix_ms
409 );
410 let _ = writeln!(
411 &mut markdown,
412 "- Compared scenarios: `{}`",
413 comparison.summary.compared_scenario_count
414 );
415 if let Some(improvement) = &comparison.summary.largest_wall_improvement {
416 let _ = writeln!(
417 &mut markdown,
418 "- Largest wall-time improvement: `{}` at {} ({})",
419 improvement.id,
420 format_delta_ms(improvement.delta_ms),
421 format_delta_pct(improvement.delta_pct),
422 );
423 }
424 if let Some(regression) = &comparison.summary.largest_wall_regression {
425 let _ = writeln!(
426 &mut markdown,
427 "- Largest wall-time regression: `{}` at {} ({})",
428 regression.id,
429 format_delta_ms(regression.delta_ms),
430 format_delta_pct(regression.delta_pct),
431 );
432 }
433 if !comparison.scenarios_missing_from_baseline.is_empty() {
434 let _ = writeln!(
435 &mut markdown,
436 "- Scenarios missing from baseline: {}",
437 comparison.scenarios_missing_from_baseline.join(", ")
438 );
439 }
440 if !comparison.baseline_only_scenarios.is_empty() {
441 let _ = writeln!(
442 &mut markdown,
443 "- Baseline-only scenarios: {}",
444 comparison.baseline_only_scenarios.join(", ")
445 );
446 }
447 let _ = writeln!(&mut markdown);
448 let _ = writeln!(
449 &mut markdown,
450 "| Scenario | Wall delta (ms) | Wall delta % | Import delta (ms) | Startup delta (ms) | Context delta (ms) | Completion delta (ms) |"
451 );
452 let _ = writeln!(
453 &mut markdown,
454 "| --- | ---: | ---: | ---: | ---: | ---: | ---: |"
455 );
456
457 for scenario in &comparison.scenario_deltas {
458 let import_delta = scenario
459 .guest_import_mean_ms
460 .as_ref()
461 .map(|delta| format_delta_ms(delta.delta_ms))
462 .unwrap_or_else(|| String::from("n/a"));
463 let startup_delta = scenario
464 .startup_overhead_mean_ms
465 .as_ref()
466 .map(|delta| format_delta_ms(delta.delta_ms))
467 .unwrap_or_else(|| String::from("n/a"));
468 let context_delta = scenario
469 .phase_mean_ms
470 .as_ref()
471 .map(|delta| format_delta_ms(delta.context_setup_ms.delta_ms))
472 .unwrap_or_else(|| String::from("n/a"));
473 let completion_delta = scenario
474 .phase_mean_ms
475 .as_ref()
476 .map(|delta| format_delta_ms(delta.completion_ms.delta_ms))
477 .unwrap_or_else(|| String::from("n/a"));
478
479 let _ = writeln!(
480 &mut markdown,
481 "| `{}` | {} | {} | {} | {} | {} | {} |",
482 scenario.id,
483 format_delta_ms(scenario.wall_mean_ms.delta_ms),
484 format_delta_pct(scenario.wall_mean_ms.delta_pct),
485 import_delta,
486 startup_delta,
487 context_delta,
488 completion_delta,
489 );
490 }
491 }
492
493 let _ = writeln!(&mut markdown);
494 let _ = writeln!(&mut markdown, "## Raw Samples");
495 let _ = writeln!(&mut markdown);
496
497 for scenario in &self.scenarios {
498 let _ = writeln!(&mut markdown, "### `{}`", scenario.id);
499 let _ = writeln!(&mut markdown, "- Workload: `{}`", scenario.workload);
500 let _ = writeln!(&mut markdown, "- Runtime: `{}`", scenario.runtime);
501 let _ = writeln!(&mut markdown, "- Mode: `{}`", scenario.mode);
502 let _ = writeln!(&mut markdown, "- Description: {}", scenario.description);
503 let _ = writeln!(
504 &mut markdown,
505 "- Wall samples (ms): {}",
506 format_sample_list(&scenario.wall_samples_ms)
507 );
508 if let Some(samples) = &scenario.guest_import_samples_ms {
509 let _ = writeln!(
510 &mut markdown,
511 "- Guest import samples (ms): {}",
512 format_sample_list(samples)
513 );
514 }
515 if let Some(samples) = &scenario.startup_overhead_samples_ms {
516 let _ = writeln!(
517 &mut markdown,
518 "- Startup overhead samples (ms): {}",
519 format_sample_list(samples)
520 );
521 }
522 let _ = writeln!(
523 &mut markdown,
524 "- Context setup samples (ms): {}",
525 format_sample_list(&scenario.phase_samples_ms.context_setup_ms)
526 );
527 let _ = writeln!(
528 &mut markdown,
529 "- Startup samples (ms): {}",
530 format_sample_list(&scenario.phase_samples_ms.startup_ms)
531 );
532 if let Some(samples) = &scenario.phase_samples_ms.guest_execution_ms {
533 let _ = writeln!(
534 &mut markdown,
535 "- Guest execution samples (ms): {}",
536 format_sample_list(samples)
537 );
538 }
539 let _ = writeln!(
540 &mut markdown,
541 "- Completion samples (ms): {}",
542 format_sample_list(&scenario.phase_samples_ms.completion_ms)
543 );
544 if let Some(samples) = &scenario.resource_usage_samples {
545 if let Some(rss_samples) = &samples.rss_bytes {
546 let _ = writeln!(
547 &mut markdown,
548 "- RSS samples (MiB): {}",
549 format_scaled_sample_list(rss_samples, bytes_to_mib)
550 );
551 }
552 if let Some(heap_samples) = &samples.heap_used_bytes {
553 let _ = writeln!(
554 &mut markdown,
555 "- Heap samples (MiB): {}",
556 format_scaled_sample_list(heap_samples, bytes_to_mib)
557 );
558 }
559 if let Some(cpu_samples) = &samples.cpu_total_us {
560 let _ = writeln!(
561 &mut markdown,
562 "- Total CPU samples (ms): {}",
563 format_scaled_sample_list(cpu_samples, micros_to_ms)
564 );
565 }
566 }
567 let _ = writeln!(&mut markdown);
568 }
569
570 markdown
571 }
572
573 pub fn render_json(&self) -> Result<String, serde_json::Error> {
574 self.render_json_with_comparison(None)
575 }
576
577 pub fn render_json_with_comparison(
578 &self,
579 comparison: Option<&BenchmarkComparison>,
580 ) -> Result<String, serde_json::Error> {
581 serde_json::to_string_pretty(&self.json_artifact(comparison))
582 }
583
584 pub fn write_artifacts(
585 &self,
586 output_dir: &Path,
587 ) -> Result<JavascriptBenchmarkArtifactPaths, JavascriptBenchmarkError> {
588 self.write_artifacts_with_comparison(output_dir, None)
589 }
590
591 pub fn write_artifacts_with_comparison(
592 &self,
593 output_dir: &Path,
594 comparison: Option<&BenchmarkComparison>,
595 ) -> Result<JavascriptBenchmarkArtifactPaths, JavascriptBenchmarkError> {
596 fs::create_dir_all(output_dir)?;
597
598 let markdown_path = output_dir.join("report.md");
599 let json_path = output_dir.join("report.json");
600 write_string_atomic(
601 &markdown_path,
602 &self.render_markdown_with_comparison(comparison),
603 )?;
604 write_string_atomic(&json_path, &self.render_json_with_comparison(comparison)?)?;
605
606 Ok(JavascriptBenchmarkArtifactPaths {
607 markdown_path,
608 json_path,
609 })
610 }
611
612 pub fn compare_to_baseline_path(
613 &self,
614 baseline_path: &Path,
615 ) -> Result<BenchmarkComparison, JavascriptBenchmarkError> {
616 let baseline = load_benchmark_artifact(baseline_path)?;
617 Ok(BenchmarkComparison::from_reports(
618 self,
619 baseline_path,
620 &baseline,
621 ))
622 }
623
624 fn guidance_lines(&self) -> Vec<String> {
625 let isolate = self.scenario("isolate-startup");
626 let cold_local = self.scenario("cold-local-import");
627 let warm_local = self.scenario("warm-local-import");
628 let prewarmed_local = self.scenario("prewarmed-local-import");
629 let builtin = self.scenario("builtin-import");
630 let large = self.scenario("large-package-import");
631
632 let mut guidance = Vec::new();
633
634 if let (
635 Some(cold_import),
636 Some(warm_import),
637 Some(warm_context),
638 Some(warm_startup_phase),
639 Some(warm_completion),
640 Some(warm_startup_overhead),
641 Some(warm_wall),
642 Some(isolate_wall),
643 ) = (
644 cold_local
645 .and_then(|scenario| scenario.guest_import_stats.as_ref())
646 .map(|stats| stats.mean_ms),
647 warm_local
648 .and_then(|scenario| scenario.guest_import_stats.as_ref())
649 .map(|stats| stats.mean_ms),
650 warm_local.map(|scenario| scenario.phase_stats.context_setup_ms.mean_ms),
651 warm_local.map(|scenario| scenario.phase_stats.startup_ms.mean_ms),
652 warm_local.map(|scenario| scenario.phase_stats.completion_ms.mean_ms),
653 warm_local
654 .and_then(|scenario| scenario.startup_overhead_stats.as_ref())
655 .map(|stats| stats.mean_ms),
656 warm_local.map(|scenario| scenario.wall_stats.mean_ms),
657 isolate.map(|scenario| scenario.wall_stats.mean_ms),
658 ) {
659 guidance.push(format!(
660 "Compile-cache reuse cuts the local import graph from {} to {} on average ({:.1}% faster), but the warm path still spends {} outside guest module evaluation. That keeps startup prewarm work in `ARC-021D` and sidecar warm-pool/snapshot work in `ARC-022` on the critical path above the `{}` empty-isolate floor.",
661 format_ms(cold_import),
662 format_ms(warm_import),
663 percentage_reduction(cold_import, warm_import),
664 format_ms(warm_startup_overhead),
665 format_ms(isolate_wall),
666 ));
667 if warm_wall > 0.0 {
668 guidance.push(format!(
669 "Warm local imports still spend {:.1}% of wall time in process startup, wrapper evaluation, and stdio handling instead of guest import work. Optimizations that only touch module compilation will not remove that floor.",
670 percentage_share(warm_startup_overhead, warm_wall),
671 ));
672 }
673 let warm_guest = warm_local
674 .and_then(|scenario| scenario.phase_stats.guest_execution_ms.as_ref())
675 .map(|stats| stats.mean_ms)
676 .unwrap_or(0.0);
677 guidance.push(format!(
678 "The warm path phase split is {} context setup, {} runtime startup, {} guest execution, and {} completion/stdio work. Future attribution can now separate bootstrap wins from pure transport/collection wins instead of treating them as one startup bucket.",
679 format_ms(warm_context),
680 format_ms(warm_startup_phase),
681 format_ms(warm_guest),
682 format_ms(warm_completion),
683 ));
684 }
685
686 if let (Some(warm_startup_overhead), Some(prewarmed_startup_overhead), Some(isolate_wall)) = (
687 warm_local
688 .and_then(|scenario| scenario.startup_overhead_stats.as_ref())
689 .map(|stats| stats.mean_ms),
690 prewarmed_local
691 .and_then(|scenario| scenario.startup_overhead_stats.as_ref())
692 .map(|stats| stats.mean_ms),
693 isolate.map(|scenario| scenario.wall_stats.mean_ms),
694 ) {
695 guidance.push(format!(
696 "Keeping the current import-cache materialization and builtin/polyfill prewarm alive inside one execution engine cuts warm local startup overhead from {} to {} ({:.1}% faster). The remaining {} of non-import work is the post-prewarm floor that broader warm-pool/snapshot work would still need to attack above the `{}` empty-isolate baseline.",
697 format_ms(warm_startup_overhead),
698 format_ms(prewarmed_startup_overhead),
699 percentage_reduction(warm_startup_overhead, prewarmed_startup_overhead),
700 format_ms(prewarmed_startup_overhead),
701 format_ms(isolate_wall),
702 ));
703 }
704
705 if let (Some(builtin_import), Some(large_import)) = (
706 builtin
707 .and_then(|scenario| scenario.guest_import_stats.as_ref())
708 .map(|stats| stats.mean_ms),
709 large
710 .and_then(|scenario| scenario.guest_import_stats.as_ref())
711 .map(|stats| stats.mean_ms),
712 ) {
713 guidance.push(format!(
714 "The large real-world package import (`typescript`) is {:.1}x the builtin path ({} versus {}). That makes `ARC-021C` the right next import-path optimization story: cache sidecar-scoped resolution results, package-type lookups, and module-format classification before attempting deeper structural rewrites.",
715 safe_ratio(large_import, builtin_import),
716 format_ms(large_import),
717 format_ms(builtin_import),
718 ));
719 }
720
721 if let (Some(smallest), Some(largest)) =
722 (self.transport_rtt.first(), self.transport_rtt.last())
723 {
724 guidance.push(format!(
725 "Execution-transport RTT over the stdio bridge rises from {} at {} bytes to {} at {} bytes. That gives later work a direct transport floor to compare against the larger startup and import phases.",
726 format_ms(smallest.stats.mean_ms),
727 smallest.payload_bytes,
728 format_ms(largest.stats.mean_ms),
729 largest.payload_bytes,
730 ));
731 }
732
733 if let Some(noisiest) = self.scenarios.iter().max_by(|lhs, rhs| {
734 lhs.wall_stats
735 .stddev_ms
736 .total_cmp(&rhs.wall_stats.stddev_ms)
737 }) {
738 guidance.push(format!(
739 "Wall-time noise is now surfaced directly in the same artifact set: `{}` currently shows the largest spread at {} stddev over a {}-{} wall range, so future deltas on that path should be judged against stability as well as mean time.",
740 noisiest.id,
741 format_ms(noisiest.wall_stats.stddev_ms),
742 format_ms(noisiest.wall_stats.min_ms),
743 format_ms(noisiest.wall_stats.max_ms),
744 ));
745 }
746
747 if let Some(heaviest) = self.scenarios.iter().max_by(|lhs, rhs| {
748 lhs.resource_usage_stats
749 .as_ref()
750 .and_then(|stats| stats.rss_bytes.as_ref())
751 .map(|stats| stats.mean)
752 .unwrap_or(f64::NEG_INFINITY)
753 .total_cmp(
754 &rhs.resource_usage_stats
755 .as_ref()
756 .and_then(|stats| stats.rss_bytes.as_ref())
757 .map(|stats| stats.mean)
758 .unwrap_or(f64::NEG_INFINITY),
759 )
760 }) {
761 if let Some(rss_mean) = heaviest
762 .resource_usage_stats
763 .as_ref()
764 .and_then(|stats| stats.rss_bytes.as_ref())
765 {
766 guidance.push(format!(
767 "Per-scenario resource reporting is now attached to the benchmark rows themselves: `{}` currently has the highest mean RSS at {} MiB, so import-path changes can now be judged for memory regressions without a separate memory-only pass.",
768 heaviest.id,
769 format_mib(bytes_to_mib(rss_mean.mean)),
770 ));
771 }
772 }
773
774 guidance.push(String::from(
775 "No new PRD stories were added from this run. The measured hotspots already map cleanly onto existing follow-ons: `ARC-021C` for safe resolution and metadata caches, `ARC-021D` for builtin/polyfill prewarm, and `ARC-022` for broader warm-pool and timing-mitigation execution work.",
776 ));
777
778 guidance
779 }
780
781 fn scenario(&self, id: &str) -> Option<&BenchmarkScenarioReport> {
782 self.scenarios.iter().find(|scenario| scenario.id == id)
783 }
784
785 fn json_artifact<'a>(
786 &'a self,
787 comparison: Option<&'a BenchmarkComparison>,
788 ) -> JavascriptBenchmarkArtifact<'a> {
789 JavascriptBenchmarkArtifact {
790 artifact_version: BENCHMARK_ARTIFACT_VERSION,
791 generated_at_unix_ms: self.generated_at_unix_ms,
792 command: format!(
793 "cargo run -p agentos-execution --bin node-import-bench -- --iterations {} --warmup-iterations {}",
794 self.config.iterations, self.config.warmup_iterations
795 ),
796 config: &self.config,
797 host: &self.host,
798 repo_root: &self.repo_root,
799 summary: self.summary(),
800 comparison,
801 transport_rtt: self
802 .transport_rtt
803 .iter()
804 .map(|transport| BenchmarkTransportRttArtifact {
805 channel: transport.channel,
806 payload_bytes: transport.payload_bytes,
807 samples_ms: &transport.samples_ms,
808 stats: &transport.stats,
809 })
810 .collect(),
811 scenarios: self
812 .scenarios
813 .iter()
814 .map(|scenario| BenchmarkScenarioArtifact {
815 id: scenario.id,
816 workload: scenario.workload,
817 runtime: scenario.runtime,
818 mode: scenario.mode,
819 description: scenario.description,
820 fixture: scenario.fixture,
821 compile_cache: scenario.compile_cache,
822 wall_samples_ms: &scenario.wall_samples_ms,
823 wall_stats: &scenario.wall_stats,
824 guest_import_samples_ms: scenario.guest_import_samples_ms.as_deref(),
825 guest_import_stats: scenario.guest_import_stats.as_ref(),
826 startup_overhead_samples_ms: scenario.startup_overhead_samples_ms.as_deref(),
827 startup_overhead_stats: scenario.startup_overhead_stats.as_ref(),
828 mean_startup_share_pct: scenario.mean_startup_share_pct(),
829 phase_samples_ms: &scenario.phase_samples_ms,
830 phase_stats: &scenario.phase_stats,
831 resource_usage_samples: scenario.resource_usage_samples.as_ref(),
832 resource_usage_stats: scenario.resource_usage_stats.as_ref(),
833 })
834 .collect(),
835 }
836 }
837
838 fn summary(&self) -> BenchmarkSummaryArtifact<'_> {
839 BenchmarkSummaryArtifact {
840 scenario_count: self.scenarios.len(),
841 recorded_samples_per_scenario: self.config.iterations,
842 warmup_iterations: self.config.warmup_iterations,
843 control_matrix: self.control_matrix(),
844 slowest_wall_scenario: self.slowest_scenario_by(|scenario| scenario.wall_stats.mean_ms),
845 slowest_guest_import_scenario: self.slowest_scenario_by(|scenario| {
846 scenario
847 .guest_import_stats
848 .as_ref()
849 .map(|stats| stats.mean_ms)
850 .unwrap_or(f64::NEG_INFINITY)
851 }),
852 highest_startup_share_scenario: self.scenarios.iter().max_by(|lhs, rhs| {
853 lhs.mean_startup_share_pct()
854 .unwrap_or(f64::NEG_INFINITY)
855 .total_cmp(&rhs.mean_startup_share_pct().unwrap_or(f64::NEG_INFINITY))
856 }),
857 hotspot_rankings: self.hotspot_rankings(),
858 guidance_lines: self.guidance_lines(),
859 }
860 }
861
862 fn control_matrix(&self) -> Vec<BenchmarkControlMatrixArtifact<'_>> {
863 let mut rows = Vec::new();
864 let mut row_indexes = BTreeMap::new();
865
866 for scenario in &self.scenarios {
867 let row_index = *row_indexes.entry(scenario.workload).or_insert_with(|| {
868 rows.push(BenchmarkControlMatrixArtifact {
869 workload: scenario.workload,
870 runtimes: Vec::new(),
871 modes: Vec::new(),
872 scenario_ids: Vec::new(),
873 });
874 rows.len() - 1
875 });
876 let row = &mut rows[row_index];
877 push_unique_label(&mut row.runtimes, scenario.runtime);
878 push_unique_label(&mut row.modes, scenario.mode);
879 row.scenario_ids.push(scenario.id);
880 }
881
882 rows
883 }
884
885 fn slowest_scenario_by(
886 &self,
887 value: impl Fn(&BenchmarkScenarioReport) -> f64,
888 ) -> Option<&BenchmarkScenarioReport> {
889 self.scenarios
890 .iter()
891 .max_by(|lhs, rhs| value(lhs).total_cmp(&value(rhs)))
892 }
893
894 fn hotspot_rankings(&self) -> Vec<BenchmarkHotspotRankingArtifact<'_>> {
895 HOTSPOT_METRICS
896 .iter()
897 .map(|metric| {
898 let mut ranked_scenarios = self
899 .scenarios
900 .iter()
901 .filter_map(|scenario| {
902 (metric.value)(scenario).map(|value| BenchmarkHotspotScenarioArtifact {
903 rank: 0,
904 id: scenario.id,
905 workload: scenario.workload,
906 runtime: scenario.runtime,
907 mode: scenario.mode,
908 value,
909 })
910 })
911 .collect::<Vec<_>>();
912 ranked_scenarios.sort_by(|lhs, rhs| {
913 rhs.value
914 .total_cmp(&lhs.value)
915 .then_with(|| lhs.id.cmp(rhs.id))
916 });
917 for (index, scenario) in ranked_scenarios.iter_mut().enumerate() {
918 scenario.rank = index + 1;
919 }
920
921 BenchmarkHotspotRankingArtifact {
922 metric: metric.metric,
923 label: metric.label,
924 dimension: metric.dimension,
925 unit: metric.unit,
926 ranked_scenarios,
927 scenarios_without_metric: self
928 .scenarios
929 .iter()
930 .filter(|scenario| (metric.value)(scenario).is_none())
931 .map(|scenario| scenario.id)
932 .collect(),
933 }
934 })
935 .collect()
936 }
937}
938
939impl BenchmarkScenarioReport {
940 fn mean_startup_share_pct(&self) -> Option<f64> {
941 let startup_mean = self.startup_overhead_stats.as_ref()?.mean_ms;
942 let wall_mean = self.wall_stats.mean_ms;
943 if wall_mean <= 0.0 {
944 Some(0.0)
945 } else {
946 Some((startup_mean / wall_mean) * 100.0)
947 }
948 }
949
950 fn wall_range_ms(&self) -> f64 {
951 self.wall_stats.max_ms - self.wall_stats.min_ms
952 }
953}
954
955impl BenchmarkResourceUsage<Vec<f64>> {
956 fn push_sample(&mut self, sample: &BenchmarkResourceUsage<f64>) {
957 push_optional_sample(&mut self.rss_bytes, sample.rss_bytes);
958 push_optional_sample(&mut self.heap_used_bytes, sample.heap_used_bytes);
959 push_optional_sample(&mut self.cpu_user_us, sample.cpu_user_us);
960 push_optional_sample(&mut self.cpu_system_us, sample.cpu_system_us);
961 push_optional_sample(&mut self.cpu_total_us, sample.cpu_total_us);
962 }
963
964 fn into_populated(self) -> Option<Self> {
965 (!self.is_empty()).then_some(self)
966 }
967}
968
969impl<T> BenchmarkResourceUsage<T> {
970 fn is_empty(&self) -> bool {
971 self.rss_bytes.is_none()
972 && self.heap_used_bytes.is_none()
973 && self.cpu_user_us.is_none()
974 && self.cpu_system_us.is_none()
975 && self.cpu_total_us.is_none()
976 }
977}
978
979impl BenchmarkComparison {
980 fn from_reports(
981 current: &JavascriptBenchmarkReport,
982 baseline_path: &Path,
983 baseline: &StoredBenchmarkArtifact,
984 ) -> Self {
985 let baseline_path =
986 fs::canonicalize(baseline_path).unwrap_or_else(|_| baseline_path.to_path_buf());
987 let baseline_by_id = baseline
988 .scenarios
989 .iter()
990 .map(|scenario| (scenario.id.as_str(), scenario))
991 .collect::<BTreeMap<_, _>>();
992
993 let mut scenario_deltas = Vec::new();
994 let mut scenarios_missing_from_baseline = Vec::new();
995
996 for scenario in ¤t.scenarios {
997 if let Some(baseline_scenario) = baseline_by_id.get(scenario.id) {
998 scenario_deltas.push(BenchmarkScenarioDelta {
999 id: scenario.id.to_owned(),
1000 description: scenario.description.to_owned(),
1001 wall_mean_ms: BenchmarkMetricDelta::from_means(
1002 baseline_scenario.wall_stats.mean_ms,
1003 scenario.wall_stats.mean_ms,
1004 ),
1005 guest_import_mean_ms: match (
1006 baseline_scenario.guest_import_stats.as_ref(),
1007 scenario.guest_import_stats.as_ref(),
1008 ) {
1009 (Some(baseline_stats), Some(current_stats)) => {
1010 Some(BenchmarkMetricDelta::from_means(
1011 baseline_stats.mean_ms,
1012 current_stats.mean_ms,
1013 ))
1014 }
1015 _ => None,
1016 },
1017 startup_overhead_mean_ms: match (
1018 baseline_scenario.startup_overhead_stats.as_ref(),
1019 scenario.startup_overhead_stats.as_ref(),
1020 ) {
1021 (Some(baseline_stats), Some(current_stats)) => {
1022 Some(BenchmarkMetricDelta::from_means(
1023 baseline_stats.mean_ms,
1024 current_stats.mean_ms,
1025 ))
1026 }
1027 _ => None,
1028 },
1029 phase_mean_ms: match (
1030 baseline_scenario.phase_stats.as_ref(),
1031 Some(&scenario.phase_stats),
1032 ) {
1033 (Some(baseline_phase), Some(current_phase)) => {
1034 Some(BenchmarkScenarioPhases {
1035 context_setup_ms: BenchmarkMetricDelta::from_means(
1036 baseline_phase.context_setup_ms.mean_ms,
1037 current_phase.context_setup_ms.mean_ms,
1038 ),
1039 startup_ms: BenchmarkMetricDelta::from_means(
1040 baseline_phase.startup_ms.mean_ms,
1041 current_phase.startup_ms.mean_ms,
1042 ),
1043 guest_execution_ms: match (
1044 baseline_phase.guest_execution_ms.as_ref(),
1045 current_phase.guest_execution_ms.as_ref(),
1046 ) {
1047 (Some(baseline_stats), Some(current_stats)) => {
1048 Some(BenchmarkMetricDelta::from_means(
1049 baseline_stats.mean_ms,
1050 current_stats.mean_ms,
1051 ))
1052 }
1053 _ => None,
1054 },
1055 completion_ms: BenchmarkMetricDelta::from_means(
1056 baseline_phase.completion_ms.mean_ms,
1057 current_phase.completion_ms.mean_ms,
1058 ),
1059 })
1060 }
1061 _ => None,
1062 },
1063 });
1064 } else {
1065 scenarios_missing_from_baseline.push(scenario.id.to_owned());
1066 }
1067 }
1068
1069 let current_ids = current
1070 .scenarios
1071 .iter()
1072 .map(|scenario| (scenario.id, ()))
1073 .collect::<BTreeMap<_, _>>();
1074 let baseline_only_scenarios = baseline
1075 .scenarios
1076 .iter()
1077 .filter(|scenario| !current_ids.contains_key(scenario.id.as_str()))
1078 .map(|scenario| scenario.id.clone())
1079 .collect::<Vec<_>>();
1080
1081 let largest_wall_improvement = scenario_deltas
1082 .iter()
1083 .filter(|scenario| scenario.wall_mean_ms.delta_ms < 0.0)
1084 .min_by(|lhs, rhs| {
1085 lhs.wall_mean_ms
1086 .delta_ms
1087 .total_cmp(&rhs.wall_mean_ms.delta_ms)
1088 })
1089 .map(BenchmarkDeltaHighlight::from_wall_delta);
1090 let largest_wall_regression = scenario_deltas
1091 .iter()
1092 .filter(|scenario| scenario.wall_mean_ms.delta_ms > 0.0)
1093 .max_by(|lhs, rhs| {
1094 lhs.wall_mean_ms
1095 .delta_ms
1096 .total_cmp(&rhs.wall_mean_ms.delta_ms)
1097 })
1098 .map(BenchmarkDeltaHighlight::from_wall_delta);
1099
1100 Self {
1101 baseline: BenchmarkComparisonBaseline {
1102 artifact_version: baseline.artifact_version,
1103 generated_at_unix_ms: baseline.generated_at_unix_ms,
1104 path: baseline_path,
1105 },
1106 summary: BenchmarkComparisonSummary {
1107 compared_scenario_count: scenario_deltas.len(),
1108 largest_wall_improvement,
1109 largest_wall_regression,
1110 },
1111 scenario_deltas,
1112 scenarios_missing_from_baseline,
1113 baseline_only_scenarios,
1114 }
1115 }
1116}
1117
1118impl BenchmarkDeltaHighlight {
1119 fn from_wall_delta(delta: &BenchmarkScenarioDelta) -> Self {
1120 Self {
1121 id: delta.id.clone(),
1122 delta_ms: delta.wall_mean_ms.delta_ms,
1123 delta_pct: delta.wall_mean_ms.delta_pct,
1124 }
1125 }
1126}
1127
1128impl BenchmarkMetricDelta {
1129 fn from_means(baseline_ms: f64, current_ms: f64) -> Self {
1130 let delta_ms = current_ms - baseline_ms;
1131 let delta_pct = if baseline_ms <= 0.0 {
1132 0.0
1133 } else {
1134 (delta_ms / baseline_ms) * 100.0
1135 };
1136
1137 Self {
1138 baseline_ms,
1139 current_ms,
1140 delta_ms,
1141 delta_pct,
1142 }
1143 }
1144}
1145
1146#[derive(Debug, Clone, PartialEq, Eq)]
1147pub struct JavascriptBenchmarkArtifactPaths {
1148 pub markdown_path: PathBuf,
1149 pub json_path: PathBuf,
1150}
1151
1152#[derive(Debug, Clone, PartialEq, Eq)]
1153pub struct JavascriptBenchmarkRunOutput {
1154 pub artifact_paths: JavascriptBenchmarkArtifactPaths,
1155 pub resumed_stage_count: usize,
1156}
1157
1158#[derive(Debug, Serialize)]
1159struct JavascriptBenchmarkArtifact<'a> {
1160 artifact_version: u32,
1161 generated_at_unix_ms: u128,
1162 command: String,
1163 config: &'a JavascriptBenchmarkConfig,
1164 host: &'a BenchmarkHost,
1165 repo_root: &'a Path,
1166 summary: BenchmarkSummaryArtifact<'a>,
1167 #[serde(skip_serializing_if = "Option::is_none")]
1168 comparison: Option<&'a BenchmarkComparison>,
1169 transport_rtt: Vec<BenchmarkTransportRttArtifact<'a>>,
1170 scenarios: Vec<BenchmarkScenarioArtifact<'a>>,
1171}
1172
1173#[derive(Debug, Serialize)]
1174struct BenchmarkSummaryArtifact<'a> {
1175 scenario_count: usize,
1176 recorded_samples_per_scenario: usize,
1177 warmup_iterations: usize,
1178 control_matrix: Vec<BenchmarkControlMatrixArtifact<'a>>,
1179 #[serde(skip_serializing_if = "Option::is_none")]
1180 slowest_wall_scenario: Option<&'a BenchmarkScenarioReport>,
1181 #[serde(skip_serializing_if = "Option::is_none")]
1182 slowest_guest_import_scenario: Option<&'a BenchmarkScenarioReport>,
1183 #[serde(skip_serializing_if = "Option::is_none")]
1184 highest_startup_share_scenario: Option<&'a BenchmarkScenarioReport>,
1185 hotspot_rankings: Vec<BenchmarkHotspotRankingArtifact<'a>>,
1186 guidance_lines: Vec<String>,
1187}
1188
1189#[derive(Debug, Serialize)]
1190struct BenchmarkScenarioArtifact<'a> {
1191 id: &'static str,
1192 workload: &'static str,
1193 runtime: &'static str,
1194 mode: &'static str,
1195 description: &'static str,
1196 fixture: &'static str,
1197 compile_cache: &'static str,
1198 wall_samples_ms: &'a [f64],
1199 wall_stats: &'a BenchmarkStats,
1200 #[serde(skip_serializing_if = "Option::is_none")]
1201 guest_import_samples_ms: Option<&'a [f64]>,
1202 #[serde(skip_serializing_if = "Option::is_none")]
1203 guest_import_stats: Option<&'a BenchmarkStats>,
1204 #[serde(skip_serializing_if = "Option::is_none")]
1205 startup_overhead_samples_ms: Option<&'a [f64]>,
1206 #[serde(skip_serializing_if = "Option::is_none")]
1207 startup_overhead_stats: Option<&'a BenchmarkStats>,
1208 #[serde(skip_serializing_if = "Option::is_none")]
1209 mean_startup_share_pct: Option<f64>,
1210 phase_samples_ms: &'a BenchmarkScenarioPhases<Vec<f64>>,
1211 phase_stats: &'a BenchmarkScenarioPhases<BenchmarkStats>,
1212 #[serde(skip_serializing_if = "Option::is_none")]
1213 resource_usage_samples: Option<&'a BenchmarkResourceUsage<Vec<f64>>>,
1214 #[serde(skip_serializing_if = "Option::is_none")]
1215 resource_usage_stats: Option<&'a BenchmarkResourceUsage<BenchmarkDistributionStats>>,
1216}
1217
1218#[derive(Debug, Serialize)]
1219struct BenchmarkControlMatrixArtifact<'a> {
1220 workload: &'a str,
1221 runtimes: Vec<&'a str>,
1222 modes: Vec<&'a str>,
1223 scenario_ids: Vec<&'a str>,
1224}
1225
1226#[derive(Debug, Serialize)]
1227struct BenchmarkTransportRttArtifact<'a> {
1228 channel: &'static str,
1229 payload_bytes: usize,
1230 samples_ms: &'a [f64],
1231 stats: &'a BenchmarkStats,
1232}
1233
1234#[derive(Debug, Serialize)]
1235struct BenchmarkHotspotRankingArtifact<'a> {
1236 metric: &'static str,
1237 label: &'static str,
1238 dimension: &'static str,
1239 unit: &'static str,
1240 ranked_scenarios: Vec<BenchmarkHotspotScenarioArtifact<'a>>,
1241 #[serde(skip_serializing_if = "Vec::is_empty")]
1242 scenarios_without_metric: Vec<&'a str>,
1243}
1244
1245#[derive(Debug, Serialize)]
1246struct BenchmarkHotspotScenarioArtifact<'a> {
1247 rank: usize,
1248 id: &'a str,
1249 workload: &'a str,
1250 runtime: &'a str,
1251 mode: &'a str,
1252 value: f64,
1253}
1254
1255struct HotspotMetricDefinition {
1256 metric: &'static str,
1257 label: &'static str,
1258 dimension: &'static str,
1259 unit: &'static str,
1260 value: fn(&BenchmarkScenarioReport) -> Option<f64>,
1261}
1262
1263const HOTSPOT_METRICS: [HotspotMetricDefinition; 13] = [
1264 HotspotMetricDefinition {
1265 metric: "wall_mean_ms",
1266 label: "Wall Time",
1267 dimension: "time",
1268 unit: "ms",
1269 value: hotspot_wall_mean_ms,
1270 },
1271 HotspotMetricDefinition {
1272 metric: "wall_stddev_ms",
1273 label: "Wall Time Stddev",
1274 dimension: "stability",
1275 unit: "ms",
1276 value: hotspot_wall_stddev_ms,
1277 },
1278 HotspotMetricDefinition {
1279 metric: "wall_range_ms",
1280 label: "Wall Time Range",
1281 dimension: "stability",
1282 unit: "ms",
1283 value: hotspot_wall_range_ms,
1284 },
1285 HotspotMetricDefinition {
1286 metric: "guest_import_mean_ms",
1287 label: "Guest Import Time",
1288 dimension: "time",
1289 unit: "ms",
1290 value: hotspot_guest_import_mean_ms,
1291 },
1292 HotspotMetricDefinition {
1293 metric: "startup_overhead_mean_ms",
1294 label: "Startup Overhead",
1295 dimension: "time",
1296 unit: "ms",
1297 value: hotspot_startup_overhead_mean_ms,
1298 },
1299 HotspotMetricDefinition {
1300 metric: "context_setup_mean_ms",
1301 label: "Context Setup Phase",
1302 dimension: "time",
1303 unit: "ms",
1304 value: hotspot_context_setup_mean_ms,
1305 },
1306 HotspotMetricDefinition {
1307 metric: "startup_phase_mean_ms",
1308 label: "Runtime Startup Phase",
1309 dimension: "time",
1310 unit: "ms",
1311 value: hotspot_startup_phase_mean_ms,
1312 },
1313 HotspotMetricDefinition {
1314 metric: "guest_execution_mean_ms",
1315 label: "Guest Execution Phase",
1316 dimension: "time",
1317 unit: "ms",
1318 value: hotspot_guest_execution_mean_ms,
1319 },
1320 HotspotMetricDefinition {
1321 metric: "completion_mean_ms",
1322 label: "Completion/Stdio Phase",
1323 dimension: "time",
1324 unit: "ms",
1325 value: hotspot_completion_mean_ms,
1326 },
1327 HotspotMetricDefinition {
1328 metric: "startup_share_pct",
1329 label: "Startup Share Of Wall",
1330 dimension: "share",
1331 unit: "pct",
1332 value: hotspot_startup_share_pct,
1333 },
1334 HotspotMetricDefinition {
1335 metric: "rss_mean_mib",
1336 label: "RSS",
1337 dimension: "memory",
1338 unit: "MiB",
1339 value: hotspot_rss_mean_mib,
1340 },
1341 HotspotMetricDefinition {
1342 metric: "heap_mean_mib",
1343 label: "Heap Used",
1344 dimension: "memory",
1345 unit: "MiB",
1346 value: hotspot_heap_mean_mib,
1347 },
1348 HotspotMetricDefinition {
1349 metric: "cpu_total_mean_ms",
1350 label: "Total CPU",
1351 dimension: "cpu",
1352 unit: "ms",
1353 value: hotspot_total_cpu_mean_ms,
1354 },
1355];
1356
1357#[derive(Debug)]
1358pub enum JavascriptBenchmarkError {
1359 InvalidConfig(&'static str),
1360 InvalidWorkspaceRoot(PathBuf),
1361 InvalidBaselineReport {
1362 path: PathBuf,
1363 message: String,
1364 },
1365 Io(std::io::Error),
1366 Utf8(std::string::FromUtf8Error),
1367 Execution(JavascriptExecutionError),
1368 NodeVersion(std::io::Error),
1369 MissingBenchmarkMetric(&'static str),
1370 InvalidBenchmarkMetric {
1371 scenario: &'static str,
1372 raw_value: String,
1373 },
1374 TransportProbeTimeout {
1375 payload_bytes: usize,
1376 },
1377 TransportProbeExited {
1378 exit_code: i32,
1379 stderr: String,
1380 },
1381 InvalidTransportProbeResponse {
1382 payload_bytes: usize,
1383 expected: String,
1384 actual: String,
1385 },
1386 NonZeroExit {
1387 scenario: &'static str,
1388 exit_code: i32,
1389 stderr: String,
1390 },
1391}
1392
1393impl fmt::Display for JavascriptBenchmarkError {
1394 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
1395 match self {
1396 Self::InvalidConfig(message) => write!(f, "invalid benchmark config: {message}"),
1397 Self::InvalidWorkspaceRoot(path) => {
1398 write!(
1399 f,
1400 "failed to resolve workspace root from execution crate path: {}",
1401 path.display()
1402 )
1403 }
1404 Self::InvalidBaselineReport { path, message } => {
1405 write!(
1406 f,
1407 "failed to parse benchmark baseline artifact {}: {message}",
1408 path.display()
1409 )
1410 }
1411 Self::Io(err) => write!(f, "benchmark I/O failure: {err}"),
1412 Self::Utf8(err) => write!(f, "benchmark output was not valid UTF-8: {err}"),
1413 Self::Execution(err) => write!(f, "benchmark execution failed: {err}"),
1414 Self::NodeVersion(err) => write!(f, "failed to query node version: {err}"),
1415 Self::MissingBenchmarkMetric(scenario) => {
1416 write!(
1417 f,
1418 "benchmark scenario `{scenario}` did not emit a metric marker"
1419 )
1420 }
1421 Self::InvalidBenchmarkMetric {
1422 scenario,
1423 raw_value,
1424 } => write!(
1425 f,
1426 "benchmark scenario `{scenario}` emitted an invalid metric: {raw_value}"
1427 ),
1428 Self::TransportProbeTimeout { payload_bytes } => {
1429 write!(
1430 f,
1431 "transport probe timed out waiting for {payload_bytes}-byte round-trip"
1432 )
1433 }
1434 Self::TransportProbeExited { exit_code, stderr } => {
1435 write!(f, "transport probe exited with code {exit_code}: {stderr}")
1436 }
1437 Self::InvalidTransportProbeResponse {
1438 payload_bytes,
1439 expected,
1440 actual,
1441 } => write!(
1442 f,
1443 "transport probe returned unexpected payload for {payload_bytes}-byte round-trip: expected {expected:?}, got {actual:?}"
1444 ),
1445 Self::NonZeroExit {
1446 scenario,
1447 exit_code,
1448 stderr,
1449 } => write!(
1450 f,
1451 "benchmark scenario `{scenario}` exited with code {exit_code}: {stderr}"
1452 ),
1453 }
1454 }
1455}
1456
1457impl std::error::Error for JavascriptBenchmarkError {}
1458
1459impl From<std::io::Error> for JavascriptBenchmarkError {
1460 fn from(err: std::io::Error) -> Self {
1461 Self::Io(err)
1462 }
1463}
1464
1465impl From<std::string::FromUtf8Error> for JavascriptBenchmarkError {
1466 fn from(err: std::string::FromUtf8Error) -> Self {
1467 Self::Utf8(err)
1468 }
1469}
1470
1471impl From<serde_json::Error> for JavascriptBenchmarkError {
1472 fn from(err: serde_json::Error) -> Self {
1473 Self::Io(std::io::Error::new(std::io::ErrorKind::InvalidData, err))
1474 }
1475}
1476
1477impl From<JavascriptExecutionError> for JavascriptBenchmarkError {
1478 fn from(err: JavascriptExecutionError) -> Self {
1479 Self::Execution(err)
1480 }
1481}
1482
1483pub fn run_javascript_benchmarks(
1484 runtime: &RuntimeContext,
1485 config: &JavascriptBenchmarkConfig,
1486) -> Result<JavascriptBenchmarkReport, JavascriptBenchmarkError> {
1487 validate_benchmark_config(config)?;
1488
1489 let repo_root = workspace_root()?;
1490 let host = benchmark_host()?;
1491 let workspace = BenchmarkWorkspace::create(&repo_root)?;
1492 let transport_rtt = measure_transport_rtt(runtime, &workspace, config)?;
1493
1494 let mut scenarios = Vec::new();
1495
1496 for scenario in benchmark_scenarios() {
1497 scenarios.push(run_scenario(runtime, &workspace, config, scenario)?);
1498 }
1499
1500 Ok(JavascriptBenchmarkReport {
1501 generated_at_unix_ms: SystemTime::now()
1502 .duration_since(UNIX_EPOCH)
1503 .unwrap_or_default()
1504 .as_millis(),
1505 config: config.clone(),
1506 host,
1507 repo_root,
1508 transport_rtt,
1509 scenarios,
1510 })
1511}
1512
1513fn benchmark_artifact_dir(repo_root: &Path) -> PathBuf {
1514 repo_root.join(BENCHMARK_ARTIFACT_DIR)
1515}
1516
1517fn benchmark_run_state_path(artifact_dir: &Path) -> PathBuf {
1518 artifact_dir.join(BENCHMARK_RUN_STATE_FILE)
1519}
1520
1521fn load_benchmark_run_state(
1522 state_path: &Path,
1523 config: &JavascriptBenchmarkConfig,
1524 host: &BenchmarkHost,
1525 repo_root: &Path,
1526 definitions: &[ScenarioDefinition],
1527) -> Result<StoredBenchmarkRunState, JavascriptBenchmarkError> {
1528 match fs::read_to_string(state_path) {
1529 Ok(raw) => match serde_json::from_str::<StoredBenchmarkRunState>(&raw) {
1530 Ok(state) if state.is_compatible(config, host, repo_root) => {
1531 Ok(state.sanitized(definitions))
1532 }
1533 Ok(_) | Err(_) => Ok(StoredBenchmarkRunState::new(config, host, repo_root)),
1534 },
1535 Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
1536 Ok(StoredBenchmarkRunState::new(config, host, repo_root))
1537 }
1538 Err(err) => Err(JavascriptBenchmarkError::Io(err)),
1539 }
1540}
1541
1542fn persist_benchmark_run_state(
1543 state_path: &Path,
1544 state: &StoredBenchmarkRunState,
1545) -> Result<(), JavascriptBenchmarkError> {
1546 write_string_atomic(state_path, &serde_json::to_string_pretty(state)?)
1547}
1548
1549fn write_string_atomic(path: &Path, contents: &str) -> Result<(), JavascriptBenchmarkError> {
1550 if let Some(parent) = path.parent() {
1551 fs::create_dir_all(parent)?;
1552 }
1553
1554 let temp_path = path.with_file_name(format!(
1555 ".{}.tmp-{}-{}",
1556 path.file_name()
1557 .and_then(|name| name.to_str())
1558 .unwrap_or("artifact"),
1559 std::process::id(),
1560 SystemTime::now()
1561 .duration_since(UNIX_EPOCH)
1562 .unwrap_or_default()
1563 .as_nanos()
1564 ));
1565 fs::write(&temp_path, contents)?;
1566 if let Err(err) = fs::rename(&temp_path, path) {
1567 let _ = fs::remove_file(&temp_path);
1568 return Err(JavascriptBenchmarkError::Io(err));
1569 }
1570
1571 Ok(())
1572}
1573
1574fn remove_file_if_exists(path: &Path) -> Result<(), JavascriptBenchmarkError> {
1575 match fs::remove_file(path) {
1576 Ok(()) => Ok(()),
1577 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
1578 Err(err) => Err(JavascriptBenchmarkError::Io(err)),
1579 }
1580}
1581
1582fn current_unix_ms() -> u128 {
1583 SystemTime::now()
1584 .duration_since(UNIX_EPOCH)
1585 .unwrap_or_default()
1586 .as_millis()
1587}
1588
1589#[derive(Debug, Clone, Copy)]
1590struct ScenarioDefinition {
1591 id: &'static str,
1592 workload: &'static str,
1593 runtime: ScenarioRuntime,
1594 mode: ScenarioMode,
1595 description: &'static str,
1596 fixture: &'static str,
1597 entrypoint: &'static str,
1598 compile_cache: CompileCacheStrategy,
1599 engine_reuse: EngineReuseStrategy,
1600 expect_import_metric: bool,
1601 env: ScenarioEnvironment,
1602}
1603
1604#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1605enum CompileCacheStrategy {
1606 Disabled,
1607 Primed,
1608}
1609
1610impl CompileCacheStrategy {
1611 fn label(self) -> &'static str {
1612 match self {
1613 Self::Disabled => "disabled",
1614 Self::Primed => "primed",
1615 }
1616 }
1617}
1618
1619#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1620enum EngineReuseStrategy {
1621 FreshPerSample,
1622 SharedAcrossScenario,
1623 SharedContextAcrossScenario,
1624}
1625
1626#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1627enum ScenarioEnvironment {
1628 None,
1629 ProjectedWorkspaceNodeModules,
1630}
1631
1632#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1633enum ScenarioRuntime {
1634 NativeExecution,
1635 HostNode,
1636}
1637
1638impl ScenarioRuntime {
1639 fn label(self) -> &'static str {
1640 match self {
1641 Self::NativeExecution => "native-execution",
1642 Self::HostNode => "host-node",
1643 }
1644 }
1645}
1646
1647#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1648enum ScenarioMode {
1649 BaselineControl,
1650 TrueColdStart,
1651 NewSessionReplay,
1652 SameSessionReplay,
1653 SameEngineReplay,
1654 HostControl,
1655}
1656
1657impl ScenarioMode {
1658 fn label(self) -> &'static str {
1659 match self {
1660 Self::BaselineControl => "baseline-control",
1661 Self::TrueColdStart => "true-cold-start",
1662 Self::NewSessionReplay => "new-session-replay",
1663 Self::SameSessionReplay => "same-session-replay",
1664 Self::SameEngineReplay => "same-engine-replay",
1665 Self::HostControl => "host-control",
1666 }
1667 }
1668}
1669
1670#[derive(Debug)]
1671struct SampleMeasurement {
1672 wall_ms: f64,
1673 guest_import_ms: Option<f64>,
1674 context_setup_ms: f64,
1675 startup_ms: f64,
1676 completion_ms: f64,
1677 resource_usage: Option<BenchmarkResourceUsage<f64>>,
1678}
1679
1680#[derive(Debug)]
1681struct BenchmarkWorkspace {
1682 root: PathBuf,
1683 repo_root: PathBuf,
1684}
1685
1686#[derive(Debug, Deserialize)]
1687struct StoredBenchmarkArtifact {
1688 artifact_version: u32,
1689 generated_at_unix_ms: u128,
1690 scenarios: Vec<StoredBenchmarkScenario>,
1691}
1692
1693#[derive(Debug, Deserialize)]
1694struct StoredBenchmarkScenario {
1695 id: String,
1696 wall_stats: BenchmarkStats,
1697 #[serde(default)]
1698 guest_import_stats: Option<BenchmarkStats>,
1699 #[serde(default)]
1700 startup_overhead_stats: Option<BenchmarkStats>,
1701 #[serde(default)]
1702 phase_stats: Option<BenchmarkScenarioPhases<BenchmarkStats>>,
1703}
1704
1705#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
1706struct StoredBenchmarkRunHost {
1707 node_binary: String,
1708 node_version: String,
1709 os: String,
1710 arch: String,
1711 logical_cpus: usize,
1712}
1713
1714#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1715struct StoredBenchmarkRunState {
1716 artifact_version: u32,
1717 config: JavascriptBenchmarkConfig,
1718 host: StoredBenchmarkRunHost,
1719 repo_root: PathBuf,
1720 #[serde(default)]
1721 transport_rtt: Option<Vec<StoredBenchmarkTransportRttReport>>,
1722 #[serde(default)]
1723 scenarios: Vec<StoredBenchmarkScenarioReport>,
1724}
1725
1726#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1727struct StoredBenchmarkTransportRttReport {
1728 payload_bytes: usize,
1729 samples_ms: Vec<f64>,
1730 stats: BenchmarkStats,
1731}
1732
1733#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
1734struct StoredBenchmarkScenarioReport {
1735 id: String,
1736 wall_samples_ms: Vec<f64>,
1737 wall_stats: BenchmarkStats,
1738 #[serde(default)]
1739 guest_import_samples_ms: Option<Vec<f64>>,
1740 #[serde(default)]
1741 guest_import_stats: Option<BenchmarkStats>,
1742 #[serde(default)]
1743 startup_overhead_samples_ms: Option<Vec<f64>>,
1744 #[serde(default)]
1745 startup_overhead_stats: Option<BenchmarkStats>,
1746 phase_samples_ms: BenchmarkScenarioPhases<Vec<f64>>,
1747 phase_stats: BenchmarkScenarioPhases<BenchmarkStats>,
1748 #[serde(default)]
1749 resource_usage_samples: Option<BenchmarkResourceUsage<Vec<f64>>>,
1750 #[serde(default)]
1751 resource_usage_stats: Option<BenchmarkResourceUsage<BenchmarkDistributionStats>>,
1752}
1753
1754impl BenchmarkWorkspace {
1755 fn create(repo_root: &Path) -> Result<Self, JavascriptBenchmarkError> {
1756 let root = repo_root.join(format!(
1757 ".tmp-agentos-execution-bench-{}-{}",
1758 std::process::id(),
1759 SystemTime::now()
1760 .duration_since(UNIX_EPOCH)
1761 .unwrap_or_default()
1762 .as_nanos()
1763 ));
1764 fs::create_dir_all(&root)?;
1765 write_benchmark_workspace(&root, repo_root)?;
1766 Ok(Self {
1767 root,
1768 repo_root: repo_root.to_path_buf(),
1769 })
1770 }
1771}
1772
1773impl Drop for BenchmarkWorkspace {
1774 fn drop(&mut self) {
1775 let _ = fs::remove_dir_all(&self.root);
1776 }
1777}
1778
1779impl StoredBenchmarkRunHost {
1780 fn from_host(host: &BenchmarkHost) -> Self {
1781 Self {
1782 node_binary: host.node_binary.clone(),
1783 node_version: host.node_version.clone(),
1784 os: host.os.to_owned(),
1785 arch: host.arch.to_owned(),
1786 logical_cpus: host.logical_cpus,
1787 }
1788 }
1789
1790 fn matches_host(&self, host: &BenchmarkHost) -> bool {
1791 self.node_binary == host.node_binary
1792 && self.node_version == host.node_version
1793 && self.os == host.os
1794 && self.arch == host.arch
1795 && self.logical_cpus == host.logical_cpus
1796 }
1797}
1798
1799impl StoredBenchmarkRunState {
1800 fn new(config: &JavascriptBenchmarkConfig, host: &BenchmarkHost, repo_root: &Path) -> Self {
1801 Self {
1802 artifact_version: BENCHMARK_ARTIFACT_VERSION,
1803 config: config.clone(),
1804 host: StoredBenchmarkRunHost::from_host(host),
1805 repo_root: repo_root.to_path_buf(),
1806 transport_rtt: None,
1807 scenarios: Vec::new(),
1808 }
1809 }
1810
1811 fn is_compatible(
1812 &self,
1813 config: &JavascriptBenchmarkConfig,
1814 host: &BenchmarkHost,
1815 repo_root: &Path,
1816 ) -> bool {
1817 self.artifact_version == BENCHMARK_ARTIFACT_VERSION
1818 && self.config == *config
1819 && self.host.matches_host(host)
1820 && self.repo_root == repo_root
1821 }
1822
1823 fn sanitized(mut self, definitions: &[ScenarioDefinition]) -> Self {
1824 if let Some(transport_rtt) = &self.transport_rtt {
1825 let payloads = transport_rtt
1826 .iter()
1827 .map(|report| report.payload_bytes)
1828 .collect::<Vec<_>>();
1829 if payloads != TRANSPORT_RTT_PAYLOAD_BYTES {
1830 self.transport_rtt = None;
1831 }
1832 }
1833
1834 let mut scenarios_by_id = self
1835 .scenarios
1836 .into_iter()
1837 .map(|scenario| (scenario.id.clone(), scenario))
1838 .collect::<BTreeMap<_, _>>();
1839 self.scenarios = definitions
1840 .iter()
1841 .filter_map(|definition| scenarios_by_id.remove(definition.id))
1842 .collect();
1843 self
1844 }
1845
1846 fn resumed_stage_count(&self, definitions: &[ScenarioDefinition]) -> usize {
1847 usize::from(self.transport_rtt.is_some())
1848 + definitions
1849 .iter()
1850 .filter(|definition| self.has_scenario(definition.id))
1851 .count()
1852 }
1853
1854 fn has_scenario(&self, id: &str) -> bool {
1855 self.scenarios.iter().any(|scenario| scenario.id == id)
1856 }
1857
1858 fn record_transport_rtt(&mut self, transport_rtt: &[BenchmarkTransportRttReport]) {
1859 self.transport_rtt = Some(
1860 transport_rtt
1861 .iter()
1862 .map(StoredBenchmarkTransportRttReport::from_report)
1863 .collect(),
1864 );
1865 }
1866
1867 fn record_scenario(&mut self, scenario: &BenchmarkScenarioReport) {
1868 self.scenarios.retain(|stored| stored.id != scenario.id);
1869 self.scenarios
1870 .push(StoredBenchmarkScenarioReport::from_report(scenario));
1871 }
1872
1873 fn to_report(
1874 &self,
1875 config: &JavascriptBenchmarkConfig,
1876 host: &BenchmarkHost,
1877 repo_root: &Path,
1878 definitions: &[ScenarioDefinition],
1879 ) -> JavascriptBenchmarkReport {
1880 let scenarios_by_id = self
1881 .scenarios
1882 .iter()
1883 .map(|scenario| (scenario.id.as_str(), scenario))
1884 .collect::<BTreeMap<_, _>>();
1885
1886 JavascriptBenchmarkReport {
1887 generated_at_unix_ms: current_unix_ms(),
1888 config: config.clone(),
1889 host: host.clone(),
1890 repo_root: repo_root.to_path_buf(),
1891 transport_rtt: self
1892 .transport_rtt
1893 .clone()
1894 .unwrap_or_default()
1895 .into_iter()
1896 .map(StoredBenchmarkTransportRttReport::into_report)
1897 .collect(),
1898 scenarios: definitions
1899 .iter()
1900 .filter_map(|definition| {
1901 scenarios_by_id
1902 .get(definition.id)
1903 .map(|scenario| scenario.to_report(*definition))
1904 })
1905 .collect(),
1906 }
1907 }
1908}
1909
1910impl StoredBenchmarkTransportRttReport {
1911 fn from_report(report: &BenchmarkTransportRttReport) -> Self {
1912 Self {
1913 payload_bytes: report.payload_bytes,
1914 samples_ms: report.samples_ms.clone(),
1915 stats: report.stats.clone(),
1916 }
1917 }
1918
1919 fn into_report(self) -> BenchmarkTransportRttReport {
1920 BenchmarkTransportRttReport {
1921 channel: TRANSPORT_RTT_CHANNEL,
1922 payload_bytes: self.payload_bytes,
1923 samples_ms: self.samples_ms,
1924 stats: self.stats,
1925 }
1926 }
1927}
1928
1929impl StoredBenchmarkScenarioReport {
1930 fn from_report(report: &BenchmarkScenarioReport) -> Self {
1931 Self {
1932 id: report.id.to_owned(),
1933 wall_samples_ms: report.wall_samples_ms.clone(),
1934 wall_stats: report.wall_stats.clone(),
1935 guest_import_samples_ms: report.guest_import_samples_ms.clone(),
1936 guest_import_stats: report.guest_import_stats.clone(),
1937 startup_overhead_samples_ms: report.startup_overhead_samples_ms.clone(),
1938 startup_overhead_stats: report.startup_overhead_stats.clone(),
1939 phase_samples_ms: report.phase_samples_ms.clone(),
1940 phase_stats: report.phase_stats.clone(),
1941 resource_usage_samples: report.resource_usage_samples.clone(),
1942 resource_usage_stats: report.resource_usage_stats.clone(),
1943 }
1944 }
1945
1946 fn to_report(&self, definition: ScenarioDefinition) -> BenchmarkScenarioReport {
1947 BenchmarkScenarioReport {
1948 id: definition.id,
1949 workload: definition.workload,
1950 runtime: definition.runtime.label(),
1951 mode: definition.mode.label(),
1952 description: definition.description,
1953 fixture: definition.fixture,
1954 compile_cache: definition.compile_cache.label(),
1955 wall_samples_ms: self.wall_samples_ms.clone(),
1956 wall_stats: self.wall_stats.clone(),
1957 guest_import_samples_ms: self.guest_import_samples_ms.clone(),
1958 guest_import_stats: self.guest_import_stats.clone(),
1959 startup_overhead_samples_ms: self.startup_overhead_samples_ms.clone(),
1960 startup_overhead_stats: self.startup_overhead_stats.clone(),
1961 phase_samples_ms: self.phase_samples_ms.clone(),
1962 phase_stats: self.phase_stats.clone(),
1963 resource_usage_samples: self.resource_usage_samples.clone(),
1964 resource_usage_stats: self.resource_usage_stats.clone(),
1965 }
1966 }
1967}
1968
1969pub fn run_javascript_benchmarks_with_recovery(
1970 runtime: &RuntimeContext,
1971 config: &JavascriptBenchmarkConfig,
1972 baseline_path: Option<&Path>,
1973) -> Result<JavascriptBenchmarkRunOutput, JavascriptBenchmarkError> {
1974 validate_benchmark_config(config)?;
1975
1976 let repo_root = workspace_root()?;
1977 let host = benchmark_host()?;
1978 let artifact_dir = benchmark_artifact_dir(&repo_root);
1979 let workspace = BenchmarkWorkspace::create(&repo_root)?;
1980 let (report, resumed_stage_count, state_path) = orchestrate_javascript_benchmark_report(
1981 config,
1982 &repo_root,
1983 &host,
1984 &artifact_dir,
1985 || measure_transport_rtt(runtime, &workspace, config),
1986 |scenario| run_scenario(runtime, &workspace, config, scenario),
1987 )?;
1988 let comparison = baseline_path
1989 .map(|path| report.compare_to_baseline_path(path))
1990 .transpose()?;
1991 let artifact_paths =
1992 report.write_artifacts_with_comparison(&artifact_dir, comparison.as_ref())?;
1993 remove_file_if_exists(&state_path)?;
1994
1995 Ok(JavascriptBenchmarkRunOutput {
1996 artifact_paths,
1997 resumed_stage_count,
1998 })
1999}
2000
2001fn orchestrate_javascript_benchmark_report<MeasureTransport, RunScenario>(
2002 config: &JavascriptBenchmarkConfig,
2003 repo_root: &Path,
2004 host: &BenchmarkHost,
2005 artifact_dir: &Path,
2006 mut measure_transport: MeasureTransport,
2007 mut run_scenario: RunScenario,
2008) -> Result<(JavascriptBenchmarkReport, usize, PathBuf), JavascriptBenchmarkError>
2009where
2010 MeasureTransport: FnMut() -> Result<Vec<BenchmarkTransportRttReport>, JavascriptBenchmarkError>,
2011 RunScenario:
2012 FnMut(ScenarioDefinition) -> Result<BenchmarkScenarioReport, JavascriptBenchmarkError>,
2013{
2014 validate_benchmark_config(config)?;
2015
2016 fs::create_dir_all(artifact_dir)?;
2017
2018 let definitions = benchmark_scenarios();
2019 let state_path = benchmark_run_state_path(artifact_dir);
2020 let mut state = load_benchmark_run_state(&state_path, config, host, repo_root, &definitions)?;
2021 let resumed_stage_count = state.resumed_stage_count(&definitions);
2022
2023 if state.transport_rtt.is_none() {
2024 let transport_rtt = measure_transport()?;
2025 state.record_transport_rtt(&transport_rtt);
2026 persist_benchmark_run_state(&state_path, &state)?;
2027 }
2028
2029 for definition in definitions {
2030 if state.has_scenario(definition.id) {
2031 continue;
2032 }
2033
2034 let scenario = run_scenario(definition)?;
2035 state.record_scenario(&scenario);
2036 persist_benchmark_run_state(&state_path, &state)?;
2037 }
2038
2039 Ok((
2040 state.to_report(config, host, repo_root, &benchmark_scenarios()),
2041 resumed_stage_count,
2042 state_path,
2043 ))
2044}
2045
2046fn validate_benchmark_config(
2047 config: &JavascriptBenchmarkConfig,
2048) -> Result<(), JavascriptBenchmarkError> {
2049 if config.iterations == 0 {
2050 return Err(JavascriptBenchmarkError::InvalidConfig(
2051 "iterations must be greater than zero",
2052 ));
2053 }
2054 if config.iterations > MAX_BENCHMARK_ITERATIONS {
2055 return Err(JavascriptBenchmarkError::InvalidConfig(
2056 "iterations must be less than or equal to 1000",
2057 ));
2058 }
2059 if config.warmup_iterations > MAX_BENCHMARK_WARMUP_ITERATIONS {
2060 return Err(JavascriptBenchmarkError::InvalidConfig(
2061 "warmup iterations must be less than or equal to 1000",
2062 ));
2063 }
2064
2065 Ok(())
2066}
2067
2068fn benchmark_scenarios() -> [ScenarioDefinition; 21] {
2069 [
2070 ScenarioDefinition {
2071 id: "isolate-startup",
2072 workload: "startup-floor",
2073 runtime: ScenarioRuntime::NativeExecution,
2074 mode: ScenarioMode::BaselineControl,
2075 description: "Minimal guest with no extra imports. Measures the current startup floor for create-context plus node process bootstrap.",
2076 fixture: "empty entrypoint",
2077 entrypoint: "./bench/isolate-startup.mjs",
2078 compile_cache: CompileCacheStrategy::Disabled,
2079 engine_reuse: EngineReuseStrategy::FreshPerSample,
2080 expect_import_metric: false,
2081 env: ScenarioEnvironment::None,
2082 },
2083 ScenarioDefinition {
2084 id: "prewarmed-isolate-startup",
2085 workload: "startup-floor",
2086 runtime: ScenarioRuntime::NativeExecution,
2087 mode: ScenarioMode::SameEngineReplay,
2088 description: "Minimal guest after a priming pass while one execution engine keeps materialized assets and builtin/polyfill prewarm state alive, isolating the hot startup floor from import work.",
2089 fixture: "empty entrypoint",
2090 entrypoint: "./bench/isolate-startup.mjs",
2091 compile_cache: CompileCacheStrategy::Primed,
2092 engine_reuse: EngineReuseStrategy::SharedAcrossScenario,
2093 expect_import_metric: false,
2094 env: ScenarioEnvironment::None,
2095 },
2096 ScenarioDefinition {
2097 id: "cold-local-import",
2098 workload: "local-import",
2099 runtime: ScenarioRuntime::NativeExecution,
2100 mode: ScenarioMode::TrueColdStart,
2101 description: "Cold import of a repo-local ESM graph that simulates layered application modules without compile-cache reuse.",
2102 fixture: "24-module local ESM graph",
2103 entrypoint: "./bench/cold-local-import.mjs",
2104 compile_cache: CompileCacheStrategy::Disabled,
2105 engine_reuse: EngineReuseStrategy::FreshPerSample,
2106 expect_import_metric: true,
2107 env: ScenarioEnvironment::None,
2108 },
2109 ScenarioDefinition {
2110 id: "warm-local-import",
2111 workload: "local-import",
2112 runtime: ScenarioRuntime::NativeExecution,
2113 mode: ScenarioMode::NewSessionReplay,
2114 description: "Warm import of the same local ESM graph after a compile-cache priming pass in an earlier isolate.",
2115 fixture: "24-module local ESM graph",
2116 entrypoint: "./bench/warm-local-import.mjs",
2117 compile_cache: CompileCacheStrategy::Primed,
2118 engine_reuse: EngineReuseStrategy::FreshPerSample,
2119 expect_import_metric: true,
2120 env: ScenarioEnvironment::None,
2121 },
2122 ScenarioDefinition {
2123 id: "same-context-local-import",
2124 workload: "local-import",
2125 runtime: ScenarioRuntime::NativeExecution,
2126 mode: ScenarioMode::SameSessionReplay,
2127 description: "Warm import of the same local ESM graph by replaying executions against one reused JavaScript context after a compile-cache priming pass.",
2128 fixture: "24-module local ESM graph",
2129 entrypoint: "./bench/warm-local-import.mjs",
2130 compile_cache: CompileCacheStrategy::Primed,
2131 engine_reuse: EngineReuseStrategy::SharedContextAcrossScenario,
2132 expect_import_metric: true,
2133 env: ScenarioEnvironment::None,
2134 },
2135 ScenarioDefinition {
2136 id: "prewarmed-local-import",
2137 workload: "local-import",
2138 runtime: ScenarioRuntime::NativeExecution,
2139 mode: ScenarioMode::SameEngineReplay,
2140 description: "Warm import of the same local ESM graph after compile-cache priming while one execution engine keeps materialized assets and builtin/polyfill prewarm state alive.",
2141 fixture: "24-module local ESM graph",
2142 entrypoint: "./bench/warm-local-import.mjs",
2143 compile_cache: CompileCacheStrategy::Primed,
2144 engine_reuse: EngineReuseStrategy::SharedAcrossScenario,
2145 expect_import_metric: true,
2146 env: ScenarioEnvironment::None,
2147 },
2148 ScenarioDefinition {
2149 id: "host-local-import",
2150 workload: "local-import",
2151 runtime: ScenarioRuntime::HostNode,
2152 mode: ScenarioMode::HostControl,
2153 description: "Direct host-Node control for the same local ESM graph so later runs can separate native executor overhead from guest import work.",
2154 fixture: "24-module local ESM graph",
2155 entrypoint: "./bench/cold-local-import.mjs",
2156 compile_cache: CompileCacheStrategy::Disabled,
2157 engine_reuse: EngineReuseStrategy::FreshPerSample,
2158 expect_import_metric: true,
2159 env: ScenarioEnvironment::None,
2160 },
2161 ScenarioDefinition {
2162 id: "builtin-import",
2163 workload: "builtin-import",
2164 runtime: ScenarioRuntime::NativeExecution,
2165 mode: ScenarioMode::TrueColdStart,
2166 description: "Import of the common builtin path used by the wrappers and polyfill-adjacent bootstrap code.",
2167 fixture: "node:path + node:url + node:fs/promises",
2168 entrypoint: "./bench/builtin-import.mjs",
2169 compile_cache: CompileCacheStrategy::Disabled,
2170 engine_reuse: EngineReuseStrategy::FreshPerSample,
2171 expect_import_metric: true,
2172 env: ScenarioEnvironment::None,
2173 },
2174 ScenarioDefinition {
2175 id: "hot-builtin-stream-import",
2176 workload: "builtin-hot-import",
2177 runtime: ScenarioRuntime::NativeExecution,
2178 mode: ScenarioMode::SameEngineReplay,
2179 description: "Hot single-import microbench for `node:stream` after a priming pass inside one reused execution engine.",
2180 fixture: "node:stream",
2181 entrypoint: "./bench/hot-builtin-stream-import.mjs",
2182 compile_cache: CompileCacheStrategy::Primed,
2183 engine_reuse: EngineReuseStrategy::SharedAcrossScenario,
2184 expect_import_metric: true,
2185 env: ScenarioEnvironment::None,
2186 },
2187 ScenarioDefinition {
2188 id: "hot-builtin-stream-web-import",
2189 workload: "builtin-hot-import",
2190 runtime: ScenarioRuntime::NativeExecution,
2191 mode: ScenarioMode::SameEngineReplay,
2192 description: "Hot single-import microbench for `node:stream/web` after a priming pass inside one reused execution engine.",
2193 fixture: "node:stream/web",
2194 entrypoint: "./bench/hot-builtin-stream-web-import.mjs",
2195 compile_cache: CompileCacheStrategy::Primed,
2196 engine_reuse: EngineReuseStrategy::SharedAcrossScenario,
2197 expect_import_metric: true,
2198 env: ScenarioEnvironment::None,
2199 },
2200 ScenarioDefinition {
2201 id: "hot-builtin-crypto-import",
2202 workload: "builtin-hot-import",
2203 runtime: ScenarioRuntime::NativeExecution,
2204 mode: ScenarioMode::SameEngineReplay,
2205 description: "Hot single-import microbench for `node:crypto` after a priming pass inside one reused execution engine.",
2206 fixture: "node:crypto",
2207 entrypoint: "./bench/hot-builtin-crypto-import.mjs",
2208 compile_cache: CompileCacheStrategy::Primed,
2209 engine_reuse: EngineReuseStrategy::SharedAcrossScenario,
2210 expect_import_metric: true,
2211 env: ScenarioEnvironment::None,
2212 },
2213 ScenarioDefinition {
2214 id: "hot-builtin-zlib-import",
2215 workload: "builtin-hot-import",
2216 runtime: ScenarioRuntime::NativeExecution,
2217 mode: ScenarioMode::SameEngineReplay,
2218 description: "Hot single-import microbench for `node:zlib` after a priming pass inside one reused execution engine.",
2219 fixture: "node:zlib",
2220 entrypoint: "./bench/hot-builtin-zlib-import.mjs",
2221 compile_cache: CompileCacheStrategy::Primed,
2222 engine_reuse: EngineReuseStrategy::SharedAcrossScenario,
2223 expect_import_metric: true,
2224 env: ScenarioEnvironment::None,
2225 },
2226 ScenarioDefinition {
2227 id: "hot-builtin-assert-import",
2228 workload: "builtin-hot-import",
2229 runtime: ScenarioRuntime::NativeExecution,
2230 mode: ScenarioMode::SameEngineReplay,
2231 description: "Hot single-import microbench for `node:assert/strict` after a priming pass inside one reused execution engine.",
2232 fixture: "node:assert/strict",
2233 entrypoint: "./bench/hot-builtin-assert-import.mjs",
2234 compile_cache: CompileCacheStrategy::Primed,
2235 engine_reuse: EngineReuseStrategy::SharedAcrossScenario,
2236 expect_import_metric: true,
2237 env: ScenarioEnvironment::None,
2238 },
2239 ScenarioDefinition {
2240 id: "hot-builtin-url-import",
2241 workload: "builtin-hot-import",
2242 runtime: ScenarioRuntime::NativeExecution,
2243 mode: ScenarioMode::SameEngineReplay,
2244 description: "Hot single-import microbench for `node:url` after a priming pass inside one reused execution engine.",
2245 fixture: "node:url",
2246 entrypoint: "./bench/hot-builtin-url-import.mjs",
2247 compile_cache: CompileCacheStrategy::Primed,
2248 engine_reuse: EngineReuseStrategy::SharedAcrossScenario,
2249 expect_import_metric: true,
2250 env: ScenarioEnvironment::None,
2251 },
2252 ScenarioDefinition {
2253 id: "hot-projected-package-file-import",
2254 workload: "projected-package-hot-import",
2255 runtime: ScenarioRuntime::HostNode,
2256 mode: ScenarioMode::SameEngineReplay,
2257 description: "Hot projected-package single-import microbench for the TypeScript compiler file with compile cache and projected-source manifest reuse enabled across repeated contexts.",
2258 fixture: "projected TypeScript compiler file",
2259 entrypoint: "./bench/hot-projected-package-file-import.mjs",
2260 compile_cache: CompileCacheStrategy::Primed,
2261 engine_reuse: EngineReuseStrategy::FreshPerSample,
2262 expect_import_metric: true,
2263 env: ScenarioEnvironment::ProjectedWorkspaceNodeModules,
2264 },
2265 ScenarioDefinition {
2266 id: "large-package-import",
2267 workload: "large-package-import",
2268 runtime: ScenarioRuntime::HostNode,
2269 mode: ScenarioMode::TrueColdStart,
2270 description: "Cold import of the real-world `typescript` package from the workspace root `node_modules` tree.",
2271 fixture: "typescript",
2272 entrypoint: "./bench/large-package-import.mjs",
2273 compile_cache: CompileCacheStrategy::Disabled,
2274 engine_reuse: EngineReuseStrategy::FreshPerSample,
2275 expect_import_metric: true,
2276 env: ScenarioEnvironment::None,
2277 },
2278 ScenarioDefinition {
2279 id: "projected-package-import",
2280 workload: "projected-package-import",
2281 runtime: ScenarioRuntime::HostNode,
2282 mode: ScenarioMode::HostControl,
2283 description: "Projected-package guest-path import of TypeScript with compile cache and projected-source manifest reuse enabled across repeated contexts.",
2284 fixture: "projected TypeScript guest-path import",
2285 entrypoint: "./bench/projected-package-import.mjs",
2286 compile_cache: CompileCacheStrategy::Primed,
2287 engine_reuse: EngineReuseStrategy::FreshPerSample,
2288 expect_import_metric: true,
2289 env: ScenarioEnvironment::ProjectedWorkspaceNodeModules,
2290 },
2291 ScenarioDefinition {
2292 id: "pdf-lib-startup",
2293 workload: "pdf-lib-startup",
2294 runtime: ScenarioRuntime::HostNode,
2295 mode: ScenarioMode::HostControl,
2296 description: "Cold import of `pdf-lib` plus representative document setup that creates a PDF page and embeds a standard font.",
2297 fixture: "pdf-lib document creation",
2298 entrypoint: "./bench/pdf-lib-startup.mjs",
2299 compile_cache: CompileCacheStrategy::Disabled,
2300 engine_reuse: EngineReuseStrategy::FreshPerSample,
2301 expect_import_metric: true,
2302 env: ScenarioEnvironment::None,
2303 },
2304 ScenarioDefinition {
2305 id: "jszip-startup",
2306 workload: "jszip-startup",
2307 runtime: ScenarioRuntime::HostNode,
2308 mode: ScenarioMode::HostControl,
2309 description: "Cold import of `jszip` plus representative archive staging that builds a nested archive structure.",
2310 fixture: "jszip archive staging",
2311 entrypoint: "./bench/jszip-startup.mjs",
2312 compile_cache: CompileCacheStrategy::Disabled,
2313 engine_reuse: EngineReuseStrategy::FreshPerSample,
2314 expect_import_metric: true,
2315 env: ScenarioEnvironment::None,
2316 },
2317 ScenarioDefinition {
2318 id: "jszip-end-to-end",
2319 workload: "jszip-end-to-end",
2320 runtime: ScenarioRuntime::HostNode,
2321 mode: ScenarioMode::HostControl,
2322 description: "Cold import of `jszip` plus a full compressed archive roundtrip that writes, compresses, reloads, and validates nested archive contents.",
2323 fixture: "jszip end-to-end archive roundtrip",
2324 entrypoint: "./bench/jszip-end-to-end.mjs",
2325 compile_cache: CompileCacheStrategy::Disabled,
2326 engine_reuse: EngineReuseStrategy::FreshPerSample,
2327 expect_import_metric: true,
2328 env: ScenarioEnvironment::None,
2329 },
2330 ScenarioDefinition {
2331 id: "jszip-repeated-session-compressed",
2332 workload: "jszip-repeated-session-compressed",
2333 runtime: ScenarioRuntime::HostNode,
2334 mode: ScenarioMode::HostControl,
2335 description: "Repeated-session `jszip` workload after a compile-cache priming pass that compresses and reloads a nested archive in each fresh isolate.",
2336 fixture: "jszip compressed archive roundtrip",
2337 entrypoint: "./bench/jszip-repeated-session-compressed.mjs",
2338 compile_cache: CompileCacheStrategy::Primed,
2339 engine_reuse: EngineReuseStrategy::FreshPerSample,
2340 expect_import_metric: true,
2341 env: ScenarioEnvironment::None,
2342 },
2343 ]
2344}
2345
2346fn run_scenario(
2347 runtime: &RuntimeContext,
2348 workspace: &BenchmarkWorkspace,
2349 config: &JavascriptBenchmarkConfig,
2350 scenario: ScenarioDefinition,
2351) -> Result<BenchmarkScenarioReport, JavascriptBenchmarkError> {
2352 let compile_cache_root = workspace
2353 .root
2354 .join("compile-cache")
2355 .join(scenario.id.replace('-', "_"));
2356 let mut shared_engine = match scenario.engine_reuse {
2357 EngineReuseStrategy::FreshPerSample => None,
2358 EngineReuseStrategy::SharedAcrossScenario
2359 | EngineReuseStrategy::SharedContextAcrossScenario => {
2360 Some(JavascriptExecutionEngine::new(runtime.clone()))
2361 }
2362 };
2363 let mut shared_context = None;
2364
2365 if scenario.compile_cache == CompileCacheStrategy::Primed {
2366 run_sample(
2367 runtime,
2368 workspace,
2369 &scenario,
2370 Some(compile_cache_root.clone()),
2371 shared_engine.as_mut(),
2372 &mut shared_context,
2373 )?;
2374 }
2375
2376 for _ in 0..config.warmup_iterations {
2377 run_sample(
2378 runtime,
2379 workspace,
2380 &scenario,
2381 compile_cache_root_for_strategy(scenario.compile_cache, &compile_cache_root),
2382 shared_engine.as_mut(),
2383 &mut shared_context,
2384 )?;
2385 }
2386
2387 let mut wall_samples_ms = Vec::with_capacity(config.iterations);
2388 let mut guest_import_samples_ms = if scenario.expect_import_metric {
2389 Some(Vec::with_capacity(config.iterations))
2390 } else {
2391 None
2392 };
2393 let mut context_setup_samples_ms = Vec::with_capacity(config.iterations);
2394 let mut startup_samples_ms = Vec::with_capacity(config.iterations);
2395 let mut completion_samples_ms = Vec::with_capacity(config.iterations);
2396 let mut resource_usage_samples = BenchmarkResourceUsage::<Vec<f64>>::default();
2397
2398 for _ in 0..config.iterations {
2399 let sample = run_sample(
2400 runtime,
2401 workspace,
2402 &scenario,
2403 compile_cache_root_for_strategy(scenario.compile_cache, &compile_cache_root),
2404 shared_engine.as_mut(),
2405 &mut shared_context,
2406 )?;
2407 wall_samples_ms.push(sample.wall_ms);
2408 context_setup_samples_ms.push(sample.context_setup_ms);
2409 startup_samples_ms.push(sample.startup_ms);
2410 completion_samples_ms.push(sample.completion_ms);
2411
2412 if let (Some(import_ms), Some(samples)) =
2413 (sample.guest_import_ms, guest_import_samples_ms.as_mut())
2414 {
2415 samples.push(import_ms);
2416 }
2417 if let Some(resource_usage) = sample.resource_usage.as_ref() {
2418 resource_usage_samples.push_sample(resource_usage);
2419 }
2420 }
2421
2422 let startup_overhead_samples_ms = guest_import_samples_ms.as_ref().map(|guest_samples| {
2423 context_setup_samples_ms
2424 .iter()
2425 .zip(startup_samples_ms.iter())
2426 .zip(completion_samples_ms.iter())
2427 .zip(guest_samples.iter())
2428 .map(|(((context_ms, startup_ms), completion_ms), _guest_ms)| {
2429 context_ms + startup_ms + completion_ms
2430 })
2431 .collect::<Vec<_>>()
2432 });
2433
2434 let phase_samples_ms = BenchmarkScenarioPhases {
2435 context_setup_ms: context_setup_samples_ms,
2436 startup_ms: startup_samples_ms,
2437 guest_execution_ms: guest_import_samples_ms.clone(),
2438 completion_ms: completion_samples_ms,
2439 };
2440 let resource_usage_samples = resource_usage_samples.into_populated();
2441
2442 Ok(BenchmarkScenarioReport {
2443 id: scenario.id,
2444 workload: scenario.workload,
2445 runtime: scenario.runtime.label(),
2446 mode: scenario.mode.label(),
2447 description: scenario.description,
2448 fixture: scenario.fixture,
2449 compile_cache: scenario.compile_cache.label(),
2450 wall_stats: compute_stats(&wall_samples_ms),
2451 guest_import_stats: guest_import_samples_ms
2452 .as_ref()
2453 .map(|samples| compute_stats(samples)),
2454 startup_overhead_stats: startup_overhead_samples_ms
2455 .as_ref()
2456 .map(|samples| compute_stats(samples)),
2457 phase_stats: BenchmarkScenarioPhases {
2458 context_setup_ms: compute_stats(&phase_samples_ms.context_setup_ms),
2459 startup_ms: compute_stats(&phase_samples_ms.startup_ms),
2460 guest_execution_ms: phase_samples_ms
2461 .guest_execution_ms
2462 .as_ref()
2463 .map(|samples| compute_stats(samples)),
2464 completion_ms: compute_stats(&phase_samples_ms.completion_ms),
2465 },
2466 resource_usage_stats: resource_usage_samples
2467 .as_ref()
2468 .and_then(compute_resource_usage_stats),
2469 wall_samples_ms,
2470 guest_import_samples_ms,
2471 startup_overhead_samples_ms,
2472 phase_samples_ms,
2473 resource_usage_samples,
2474 })
2475}
2476
2477fn compile_cache_root_for_strategy(strategy: CompileCacheStrategy, root: &Path) -> Option<PathBuf> {
2478 match strategy {
2479 CompileCacheStrategy::Disabled => None,
2480 CompileCacheStrategy::Primed => Some(root.to_path_buf()),
2481 }
2482}
2483
2484fn run_sample(
2485 runtime: &RuntimeContext,
2486 workspace: &BenchmarkWorkspace,
2487 scenario: &ScenarioDefinition,
2488 compile_cache_root: Option<PathBuf>,
2489 shared_engine: Option<&mut JavascriptExecutionEngine>,
2490 shared_context: &mut Option<crate::JavascriptContext>,
2491) -> Result<SampleMeasurement, JavascriptBenchmarkError> {
2492 match scenario.runtime {
2493 ScenarioRuntime::NativeExecution => run_native_sample(
2494 runtime,
2495 workspace,
2496 scenario,
2497 compile_cache_root,
2498 shared_engine,
2499 shared_context,
2500 ),
2501 ScenarioRuntime::HostNode => run_host_node_sample(workspace, scenario),
2502 }
2503}
2504
2505fn run_native_sample(
2506 runtime: &RuntimeContext,
2507 workspace: &BenchmarkWorkspace,
2508 scenario: &ScenarioDefinition,
2509 compile_cache_root: Option<PathBuf>,
2510 shared_engine: Option<&mut JavascriptExecutionEngine>,
2511 shared_context: &mut Option<crate::JavascriptContext>,
2512) -> Result<SampleMeasurement, JavascriptBenchmarkError> {
2513 let mut fresh_engine = JavascriptExecutionEngine::new(runtime.clone());
2514 let engine = shared_engine.unwrap_or(&mut fresh_engine);
2515 let context_started_at = Instant::now();
2516 let (context, context_setup_ms) = match scenario.engine_reuse {
2517 EngineReuseStrategy::SharedContextAcrossScenario => {
2518 if let Some(context) = shared_context.as_ref() {
2519 (context.clone(), 0.0)
2520 } else {
2521 let context = engine.create_context(CreateJavascriptContextRequest {
2522 vm_id: String::from("vm-bench"),
2523 bootstrap_module: None,
2524 compile_cache_root,
2525 });
2526 let context_setup_ms = context_started_at.elapsed().as_secs_f64() * 1000.0;
2527 *shared_context = Some(context.clone());
2528 (context, context_setup_ms)
2529 }
2530 }
2531 _ => {
2532 let context = engine.create_context(CreateJavascriptContextRequest {
2533 vm_id: String::from("vm-bench"),
2534 bootstrap_module: None,
2535 compile_cache_root,
2536 });
2537 let context_setup_ms = context_started_at.elapsed().as_secs_f64() * 1000.0;
2538 (context, context_setup_ms)
2539 }
2540 };
2541
2542 let startup_started_at = Instant::now();
2543 let execution = engine.start_execution(StartJavascriptExecutionRequest {
2544 limits: Default::default(),
2545 argv0: None,
2546 guest_runtime: Default::default(),
2547 vm_id: String::from("vm-bench"),
2548 context_id: context.context_id,
2549 argv: vec![String::from(scenario.entrypoint)],
2550 env: scenario_env(workspace, scenario),
2551 cwd: workspace.root.clone(),
2552 wasm_module_bytes: None,
2553 inline_code: None,
2554 })?;
2555 let startup_ms = startup_started_at.elapsed().as_secs_f64() * 1000.0;
2556
2557 let completion_started_at = Instant::now();
2558 let result = execution.wait()?;
2559 let completion_total_ms = completion_started_at.elapsed().as_secs_f64() * 1000.0;
2560 let stdout = String::from_utf8(result.stdout)?;
2561 let stderr = String::from_utf8(result.stderr)?;
2562
2563 if result.exit_code != 0 {
2564 return Err(JavascriptBenchmarkError::NonZeroExit {
2565 scenario: scenario.id,
2566 exit_code: result.exit_code,
2567 stderr,
2568 });
2569 }
2570
2571 let parsed_metrics =
2572 parse_benchmark_metrics(scenario.id, &stdout, scenario.expect_import_metric)?;
2573 let guest_import_ms = parsed_metrics.import_ms;
2574 let completion_ms = guest_import_ms
2575 .map(|guest_ms| saturating_delta_ms(completion_total_ms, guest_ms))
2576 .unwrap_or(completion_total_ms);
2577 let wall_ms = context_setup_ms + startup_ms + completion_total_ms;
2578
2579 Ok(SampleMeasurement {
2580 wall_ms,
2581 guest_import_ms,
2582 context_setup_ms,
2583 startup_ms,
2584 completion_ms,
2585 resource_usage: parsed_metrics.resource_usage,
2586 })
2587}
2588
2589fn run_host_node_sample(
2590 workspace: &BenchmarkWorkspace,
2591 scenario: &ScenarioDefinition,
2592) -> Result<SampleMeasurement, JavascriptBenchmarkError> {
2593 let started_at = Instant::now();
2594 let output = Command::new(crate::host_node::node_binary())
2595 .arg(scenario.entrypoint)
2596 .current_dir(&workspace.root)
2597 .envs(scenario_env(workspace, scenario))
2598 .output()?;
2599 let wall_ms = started_at.elapsed().as_secs_f64() * 1000.0;
2600 let stdout = String::from_utf8(output.stdout)?;
2601 let stderr = String::from_utf8(output.stderr)?;
2602
2603 if !output.status.success() {
2604 return Err(JavascriptBenchmarkError::NonZeroExit {
2605 scenario: scenario.id,
2606 exit_code: output.status.code().unwrap_or(-1),
2607 stderr,
2608 });
2609 }
2610
2611 let parsed_metrics =
2612 parse_benchmark_metrics(scenario.id, &stdout, scenario.expect_import_metric)?;
2613 let guest_import_ms = parsed_metrics.import_ms;
2614 let startup_ms = guest_import_ms
2615 .map(|guest_ms| saturating_delta_ms(wall_ms, guest_ms))
2616 .unwrap_or(wall_ms);
2617
2618 Ok(SampleMeasurement {
2619 wall_ms,
2620 guest_import_ms,
2621 context_setup_ms: 0.0,
2622 startup_ms,
2623 completion_ms: 0.0,
2624 resource_usage: parsed_metrics.resource_usage,
2625 })
2626}
2627
2628fn scenario_env(
2629 workspace: &BenchmarkWorkspace,
2630 scenario: &ScenarioDefinition,
2631) -> BTreeMap<String, String> {
2632 match scenario.env {
2633 ScenarioEnvironment::None => BTreeMap::new(),
2634 ScenarioEnvironment::ProjectedWorkspaceNodeModules => {
2635 let projected_node_modules = workspace.repo_root.join("node_modules");
2636 let projected_node_modules_json =
2637 serde_json::to_string(&vec![projected_node_modules.display().to_string()])
2638 .expect("serialize projected node_modules read path");
2639 let guest_path_mappings = serde_json::json!([{
2640 "guestPath": "/root/node_modules",
2641 "hostPath": projected_node_modules.display().to_string(),
2642 }])
2643 .to_string();
2644
2645 BTreeMap::from([
2646 (
2647 String::from("AGENTOS_EXTRA_FS_READ_PATHS"),
2648 projected_node_modules_json,
2649 ),
2650 (
2651 String::from("AGENTOS_GUEST_PATH_MAPPINGS"),
2652 guest_path_mappings,
2653 ),
2654 ])
2655 }
2656 }
2657}
2658
2659fn measure_transport_rtt(
2660 runtime: &RuntimeContext,
2661 workspace: &BenchmarkWorkspace,
2662 config: &JavascriptBenchmarkConfig,
2663) -> Result<Vec<BenchmarkTransportRttReport>, JavascriptBenchmarkError> {
2664 let mut engine = JavascriptExecutionEngine::new(runtime.clone());
2665 let context = engine.create_context(CreateJavascriptContextRequest {
2666 vm_id: String::from("vm-transport"),
2667 bootstrap_module: None,
2668 compile_cache_root: None,
2669 });
2670 let mut execution = engine.start_execution(StartJavascriptExecutionRequest {
2671 limits: Default::default(),
2672 argv0: None,
2673 guest_runtime: Default::default(),
2674 vm_id: String::from("vm-transport"),
2675 context_id: context.context_id,
2676 argv: vec![String::from("./bench/transport-echo.mjs")],
2677 env: BTreeMap::from([(String::from("AGENTOS_KEEP_STDIN_OPEN"), String::from("1"))]),
2678 cwd: workspace.root.clone(),
2679 wasm_module_bytes: None,
2680 inline_code: None,
2681 })?;
2682
2683 let mut stdout_buffer = String::new();
2684 let mut stderr_buffer = String::new();
2685 let mut reports = Vec::with_capacity(TRANSPORT_RTT_PAYLOAD_BYTES.len());
2686
2687 for payload_bytes in TRANSPORT_RTT_PAYLOAD_BYTES {
2688 for warmup_index in 0..config.warmup_iterations {
2689 let label = format!("warmup-{}-{warmup_index}", payload_bytes);
2690 measure_transport_roundtrip(
2691 &mut execution,
2692 payload_bytes,
2693 &label,
2694 &mut stdout_buffer,
2695 &mut stderr_buffer,
2696 )?;
2697 }
2698
2699 let mut samples_ms = Vec::with_capacity(config.iterations);
2700 for iteration in 0..config.iterations {
2701 let label = format!("measure-{}-{iteration}", payload_bytes);
2702 samples_ms.push(measure_transport_roundtrip(
2703 &mut execution,
2704 payload_bytes,
2705 &label,
2706 &mut stdout_buffer,
2707 &mut stderr_buffer,
2708 )?);
2709 }
2710
2711 reports.push(BenchmarkTransportRttReport {
2712 channel: TRANSPORT_RTT_CHANNEL,
2713 payload_bytes,
2714 stats: compute_stats(&samples_ms),
2715 samples_ms,
2716 });
2717 }
2718
2719 execution.close_stdin()?;
2720 let result = execution.wait()?;
2721 if result.exit_code != 0 {
2722 stderr_buffer.push_str(&String::from_utf8(result.stderr)?);
2723 return Err(JavascriptBenchmarkError::TransportProbeExited {
2724 exit_code: result.exit_code,
2725 stderr: stderr_buffer,
2726 });
2727 }
2728
2729 Ok(reports)
2730}
2731
2732fn measure_transport_roundtrip(
2733 execution: &mut crate::JavascriptExecution,
2734 payload_bytes: usize,
2735 label: &str,
2736 stdout_buffer: &mut String,
2737 stderr_buffer: &mut String,
2738) -> Result<f64, JavascriptBenchmarkError> {
2739 let payload = transport_probe_payload(payload_bytes, label);
2740 let expected_line = format!("{payload}\n");
2741 let started_at = Instant::now();
2742 execution.write_stdin(expected_line.as_bytes())?;
2743
2744 loop {
2745 if let Some(line) = take_complete_line(stdout_buffer) {
2746 if line == payload {
2747 return Ok(started_at.elapsed().as_secs_f64() * 1000.0);
2748 }
2749 return Err(JavascriptBenchmarkError::InvalidTransportProbeResponse {
2750 payload_bytes,
2751 expected: payload,
2752 actual: line,
2753 });
2754 }
2755
2756 match execution.poll_event_blocking(TRANSPORT_POLL_TIMEOUT)? {
2757 Some(crate::JavascriptExecutionEvent::Stdout(chunk)) => {
2758 stdout_buffer.push_str(&String::from_utf8(chunk)?);
2759 }
2760 Some(crate::JavascriptExecutionEvent::Stderr(chunk)) => {
2761 stderr_buffer.push_str(&String::from_utf8(chunk)?);
2762 }
2763 Some(crate::JavascriptExecutionEvent::SyncRpcRequest(request)) => {
2764 return Err(JavascriptBenchmarkError::Execution(
2765 JavascriptExecutionError::PendingSyncRpcRequest(request.id),
2766 ));
2767 }
2768 Some(crate::JavascriptExecutionEvent::SignalState { .. }) => {}
2769 Some(crate::JavascriptExecutionEvent::Exited(exit_code)) => {
2770 return Err(JavascriptBenchmarkError::TransportProbeExited {
2771 exit_code,
2772 stderr: stderr_buffer.clone(),
2773 });
2774 }
2775 None => {
2776 return Err(JavascriptBenchmarkError::TransportProbeTimeout { payload_bytes });
2777 }
2778 }
2779 }
2780}
2781
2782fn transport_probe_payload(payload_bytes: usize, label: &str) -> String {
2783 if payload_bytes == 0 {
2784 return format!("transport:{label}:");
2785 }
2786
2787 let header = format!("transport:{label}:");
2788 let fill_len = payload_bytes.saturating_sub(header.len());
2789 format!("{header}{}", "x".repeat(fill_len))
2790}
2791
2792fn take_complete_line(buffer: &mut String) -> Option<String> {
2793 let newline_index = buffer.find('\n')?;
2794 let line = buffer[..newline_index].trim_end_matches('\r').to_owned();
2795 buffer.drain(..=newline_index);
2796 Some(line)
2797}
2798
2799#[derive(Debug, Default, Deserialize)]
2800struct ParsedBenchmarkMetrics {
2801 #[serde(default)]
2802 import_ms: Option<f64>,
2803 #[serde(default)]
2804 resource_usage: Option<BenchmarkResourceUsage<f64>>,
2805}
2806
2807fn parse_benchmark_metrics(
2808 scenario_id: &'static str,
2809 stdout: &str,
2810 expect_import_metric: bool,
2811) -> Result<ParsedBenchmarkMetrics, JavascriptBenchmarkError> {
2812 let raw_value = stdout
2813 .lines()
2814 .rev()
2815 .find_map(|line| line.strip_prefix(BENCHMARK_MARKER_PREFIX))
2816 .ok_or(JavascriptBenchmarkError::MissingBenchmarkMetric(
2817 scenario_id,
2818 ))?
2819 .trim();
2820
2821 if let Ok(parsed) = serde_json::from_str::<ParsedBenchmarkMetrics>(raw_value) {
2822 let has_resource_usage = match parsed.resource_usage.as_ref() {
2823 Some(resource_usage) => !resource_usage.is_empty(),
2824 None => false,
2825 };
2826 if parsed.import_ms.is_some() || has_resource_usage {
2827 if expect_import_metric && parsed.import_ms.is_none() {
2828 return Err(JavascriptBenchmarkError::MissingBenchmarkMetric(
2829 scenario_id,
2830 ));
2831 }
2832 return Ok(parsed);
2833 }
2834 }
2835
2836 raw_value
2837 .parse::<f64>()
2838 .map(|import_ms| ParsedBenchmarkMetrics {
2839 import_ms: Some(import_ms),
2840 resource_usage: None,
2841 })
2842 .map_err(|_| JavascriptBenchmarkError::InvalidBenchmarkMetric {
2843 scenario: scenario_id,
2844 raw_value: raw_value.to_owned(),
2845 })
2846}
2847
2848fn workspace_root() -> Result<PathBuf, JavascriptBenchmarkError> {
2849 let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
2850 manifest_dir
2851 .parent()
2852 .and_then(Path::parent)
2853 .map(Path::to_path_buf)
2854 .ok_or(JavascriptBenchmarkError::InvalidWorkspaceRoot(manifest_dir))
2855}
2856
2857fn load_benchmark_artifact(
2858 baseline_path: &Path,
2859) -> Result<StoredBenchmarkArtifact, JavascriptBenchmarkError> {
2860 let raw = fs::read_to_string(baseline_path)?;
2861 serde_json::from_str(&raw).map_err(|err| JavascriptBenchmarkError::InvalidBaselineReport {
2862 path: baseline_path.to_path_buf(),
2863 message: err.to_string(),
2864 })
2865}
2866
2867fn benchmark_host() -> Result<BenchmarkHost, JavascriptBenchmarkError> {
2868 let node_binary = crate::host_node::node_binary();
2869 let output = Command::new(&node_binary)
2870 .arg("--version")
2871 .output()
2872 .map_err(JavascriptBenchmarkError::NodeVersion)?;
2873 let node_version = String::from_utf8(output.stdout)?;
2874
2875 Ok(BenchmarkHost {
2876 node_binary,
2877 node_version,
2878 os: env::consts::OS,
2879 arch: env::consts::ARCH,
2880 logical_cpus: std::thread::available_parallelism()
2881 .map(usize::from)
2882 .unwrap_or(1),
2883 })
2884}
2885
2886fn write_benchmark_workspace(
2887 root: &Path,
2888 repo_root: &Path,
2889) -> Result<(), JavascriptBenchmarkError> {
2890 fs::create_dir_all(root.join("bench"))?;
2891 fs::create_dir_all(root.join("bench/local-graph"))?;
2892 let host_node_modules = repo_root.join("node_modules");
2893 let workspace_node_modules = root.join("node_modules");
2894 if host_node_modules.exists() && !workspace_node_modules.exists() {
2895 std::os::unix::fs::symlink(&host_node_modules, &workspace_node_modules)?;
2896 }
2897 fs::write(
2898 root.join("package.json"),
2899 "{\n \"name\": \"agentos-execution-bench\",\n \"private\": true,\n \"type\": \"module\"\n}\n",
2900 )?;
2901
2902 for index in 0..LOCAL_GRAPH_MODULE_COUNT {
2903 let path = root
2904 .join("bench/local-graph")
2905 .join(format!("mod-{index:02}.mjs"));
2906 let source = if index == 0 {
2907 String::from("export const value = 1;\n")
2908 } else {
2909 format!(
2910 "import {{ value as previous }} from './mod-{previous:02}.mjs';\nexport const value = previous + {index};\n",
2911 previous = index - 1
2912 )
2913 };
2914 fs::write(path, source)?;
2915 }
2916
2917 let final_value = local_graph_terminal_value();
2918 fs::write(
2919 root.join("bench/local-graph/root.mjs"),
2920 format!(
2921 "import {{ value }} from './mod-{last:02}.mjs';\nexport {{ value }};\nexport const expected = {final_value};\n",
2922 last = LOCAL_GRAPH_MODULE_COUNT - 1
2923 ),
2924 )?;
2925 fs::write(
2926 root.join("bench/benchmark-metrics.mjs"),
2927 benchmark_metrics_module_source(),
2928 )?;
2929
2930 fs::write(
2931 root.join("bench/isolate-startup.mjs"),
2932 resource_only_entrypoint_source("console.log('isolate-ready');"),
2933 )?;
2934 fs::write(
2935 root.join("bench/cold-local-import.mjs"),
2936 local_import_entrypoint_source(final_value),
2937 )?;
2938 fs::write(
2939 root.join("bench/warm-local-import.mjs"),
2940 local_import_entrypoint_source(final_value),
2941 )?;
2942 fs::write(
2943 root.join("bench/builtin-import.mjs"),
2944 timed_entrypoint_source(
2945 "const [pathMod, fsMod, urlMod] = await Promise.all([\n import('node:path'),\n import('node:fs/promises'),\n import('node:url'),\n]);\nif (typeof pathMod.basename !== 'function' || typeof fsMod.readFile !== 'function' || typeof urlMod.pathToFileURL !== 'function') {\n throw new Error('builtin import fixture did not load expected exports');\n}",
2946 ),
2947 )?;
2948 fs::write(
2949 root.join("bench/hot-builtin-stream-import.mjs"),
2950 single_import_entrypoint_source(
2951 "node:stream",
2952 "typeof imported.Readable === 'function'",
2953 "node:stream import did not expose Readable",
2954 ),
2955 )?;
2956 fs::write(
2957 root.join("bench/hot-builtin-stream-web-import.mjs"),
2958 single_import_entrypoint_source(
2959 "node:stream/web",
2960 "typeof imported.ReadableStream === 'function'",
2961 "node:stream/web import did not expose ReadableStream",
2962 ),
2963 )?;
2964 fs::write(
2965 root.join("bench/hot-builtin-crypto-import.mjs"),
2966 single_import_entrypoint_source(
2967 "node:crypto",
2968 "typeof imported.createHash === 'function'",
2969 "node:crypto import did not expose createHash",
2970 ),
2971 )?;
2972 fs::write(
2973 root.join("bench/hot-builtin-zlib-import.mjs"),
2974 single_import_entrypoint_source(
2975 "node:zlib",
2976 "typeof imported.gzipSync === 'function'",
2977 "node:zlib import did not expose gzipSync",
2978 ),
2979 )?;
2980 fs::write(
2981 root.join("bench/hot-builtin-assert-import.mjs"),
2982 single_import_entrypoint_source(
2983 "node:assert/strict",
2984 "typeof imported.strictEqual === 'function'",
2985 "node:assert/strict import did not expose strictEqual",
2986 ),
2987 )?;
2988 fs::write(
2989 root.join("bench/hot-builtin-url-import.mjs"),
2990 single_import_entrypoint_source(
2991 "node:url",
2992 "typeof imported.pathToFileURL === 'function'",
2993 "node:url import did not expose pathToFileURL",
2994 ),
2995 )?;
2996 fs::write(
2997 root.join("bench/large-package-import.mjs"),
2998 timed_entrypoint_source(
2999 "const typescript = await import('typescript');\nif (typeof typescript.transpileModule !== 'function') {\n throw new Error('typescript import did not expose transpileModule');\n}",
3000 ),
3001 )?;
3002 fs::write(
3003 root.join("bench/hot-projected-package-file-import.mjs"),
3004 projected_package_file_import_entrypoint_source(),
3005 )?;
3006 fs::write(
3007 root.join("bench/projected-package-import.mjs"),
3008 projected_package_import_entrypoint_source(),
3009 )?;
3010 fs::write(
3011 root.join("bench/pdf-lib-startup.mjs"),
3012 pdf_lib_startup_entrypoint_source(),
3013 )?;
3014 fs::write(
3015 root.join("bench/jszip-startup.mjs"),
3016 jszip_startup_entrypoint_source(),
3017 )?;
3018 fs::write(
3019 root.join("bench/jszip-end-to-end.mjs"),
3020 jszip_end_to_end_entrypoint_source(),
3021 )?;
3022 fs::write(
3023 root.join("bench/jszip-repeated-session-compressed.mjs"),
3024 jszip_repeated_session_compressed_entrypoint_source(),
3025 )?;
3026 fs::write(
3027 root.join("bench/transport-echo.mjs"),
3028 "process.stdin.setEncoding('utf8');\nlet buffered = '';\nconst flushLines = () => {\n let newlineIndex = buffered.indexOf('\\n');\n while (newlineIndex >= 0) {\n const line = buffered.slice(0, newlineIndex).replace(/\\r$/, '');\n buffered = buffered.slice(newlineIndex + 1);\n process.stdout.write(line);\n newlineIndex = buffered.indexOf('\\n');\n }\n};\nprocess.stdin.on('data', (chunk) => {\n buffered += chunk;\n flushLines();\n});\nprocess.stdin.on('end', () => {\n if (buffered.length > 0) {\n process.stdout.write(buffered.replace(/\\r$/, ''));\n }\n});\n",
3029 )?;
3030
3031 Ok(())
3032}
3033
3034fn local_import_entrypoint_source(final_value: usize) -> String {
3035 timed_entrypoint_source(&format!(
3036 "const graph = await import('./local-graph/root.mjs');\nif (graph.value !== {final_value} || graph.expected !== {final_value}) {{\n throw new Error(`local graph import returned ${{\n graph.value\n }} instead of {final_value}`);\n}}"
3037 ))
3038}
3039
3040fn single_import_entrypoint_source(
3041 specifier: &str,
3042 validation_expression: &str,
3043 error_message: &str,
3044) -> String {
3045 timed_entrypoint_source(&format!(
3046 "const imported = await import('{specifier}');\nif (!({validation_expression})) {{\n throw new Error('{error_message}');\n}}"
3047 ))
3048}
3049
3050fn projected_package_file_import_entrypoint_source() -> String {
3051 timed_entrypoint_source(
3052 "const typescriptModule = await import('../node_modules/typescript/lib/typescript.js');\nconst typescript = typescriptModule.default ?? typescriptModule;\nif (typeof typescript.transpileModule !== 'function') {\n throw new Error('projected package file import did not expose transpileModule');\n}",
3053 )
3054}
3055
3056fn projected_package_import_entrypoint_source() -> String {
3057 timed_entrypoint_source(
3058 "const typescriptModule = await import('../node_modules/typescript/lib/typescript.js');\nconst typescript = typescriptModule.default ?? typescriptModule;\nconst sourceFile = typescript.createSourceFile(\n 'bench.ts',\n 'const answer: number = 42;',\n typescript.ScriptTarget.ES2022,\n true,\n);\nif (\n typeof typescript.transpileModule !== 'function' ||\n typeof typescript.createSourceFile !== 'function' ||\n !sourceFile ||\n sourceFile.statements.length !== 1\n) {\n throw new Error('projected package import did not expose TypeScript compiler APIs');\n}",
3059 )
3060}
3061
3062fn pdf_lib_startup_entrypoint_source() -> String {
3063 timed_entrypoint_source(
3064 "const pdfLib = await import('pdf-lib');\nconst pdfDoc = await pdfLib.PDFDocument.create();\nconst page = pdfDoc.addPage([612, 792]);\nconst font = await pdfDoc.embedFont(pdfLib.StandardFonts.Helvetica);\npage.drawText('secure-exec pdf-lib benchmark', {\n x: 50,\n y: 750,\n font,\n size: 18,\n});\nif (pdfDoc.getPageCount() !== 1 || page.getSize().width !== 612) {\n throw new Error('pdf-lib fixture did not create the expected document');\n}",
3065 )
3066}
3067
3068fn jszip_startup_entrypoint_source() -> String {
3069 timed_entrypoint_source(
3070 "const jszipModule = await import('jszip');\nconst JSZip = jszipModule.default ?? jszipModule;\nconst zip = new JSZip();\nzip.file('README.txt', 'secure-exec benchmark archive');\nconst notes = zip.folder('notes');\nif (!notes) {\n throw new Error('jszip fixture failed to create nested folder');\n}\nnotes.file('todo.txt', 'benchmark staging payload');\nconst fileCount = Object.values(zip.files).filter((entry) => !entry.dir).length;\nif (typeof zip.generateAsync !== 'function' || fileCount !== 2) {\n throw new Error('jszip fixture did not stage the expected archive');\n}",
3071 )
3072}
3073
3074fn jszip_end_to_end_entrypoint_source() -> String {
3075 timed_entrypoint_source(
3076 "const jszipModule = await import('jszip');\nconst JSZip = jszipModule.default ?? jszipModule;\nconst zip = new JSZip();\nconst repeatedPayload = 'secure-exec benchmark payload '.repeat(512);\nzip.file('README.txt', repeatedPayload);\nconst notes = zip.folder('notes');\nif (!notes) {\n throw new Error('jszip end-to-end fixture failed to create notes folder');\n}\nnotes.file('todo.txt', 'complete the archive roundtrip');\nconst data = zip.folder('data');\nif (!data) {\n throw new Error('jszip end-to-end fixture failed to create data folder');\n}\ndata.file('payload.json', JSON.stringify({\n repeatedPayloadLength: repeatedPayload.length,\n mode: 'cold-end-to-end',\n}));\nconst archiveBytes = await zip.generateAsync({\n type: 'uint8array',\n compression: 'DEFLATE',\n compressionOptions: { level: 6 },\n});\nconst restored = await JSZip.loadAsync(archiveBytes);\nconst restoredFileCount = Object.values(restored.files).filter((entry) => !entry.dir).length;\nconst restoredReadme = await restored.file('README.txt')?.async('string');\nconst restoredTodo = await restored.file('notes/todo.txt')?.async('string');\nconst restoredPayload = await restored.file('data/payload.json')?.async('string');\nif (\n archiveBytes.byteLength >= repeatedPayload.length ||\n restoredFileCount !== 3 ||\n restoredReadme !== repeatedPayload ||\n restoredTodo !== 'complete the archive roundtrip' ||\n !restoredPayload?.includes('cold-end-to-end')\n) {\n throw new Error('jszip end-to-end fixture did not complete the compressed archive roundtrip');\n}",
3077 )
3078}
3079
3080fn jszip_repeated_session_compressed_entrypoint_source() -> String {
3081 timed_entrypoint_source(
3082 "const jszipModule = await import('jszip');\nconst JSZip = jszipModule.default ?? jszipModule;\nconst zip = new JSZip();\nconst repeatedPayload = 'secure-exec benchmark payload '.repeat(512);\nzip.file('README.txt', repeatedPayload);\nconst notes = zip.folder('notes');\nif (!notes) {\n throw new Error('jszip repeated-session fixture failed to create notes folder');\n}\nnotes.file('todo.txt', 'repeat this session workload');\nconst data = zip.folder('data');\nif (!data) {\n throw new Error('jszip repeated-session fixture failed to create data folder');\n}\ndata.file('payload.json', JSON.stringify({\n repeatedPayloadLength: repeatedPayload.length,\n repeatedSessions: true,\n}));\nconst archiveBytes = await zip.generateAsync({\n type: 'uint8array',\n compression: 'DEFLATE',\n compressionOptions: { level: 6 },\n});\nconst restored = await JSZip.loadAsync(archiveBytes);\nconst restoredFileCount = Object.values(restored.files).filter((entry) => !entry.dir).length;\nconst restoredReadme = await restored.file('README.txt')?.async('string');\nconst restoredTodo = await restored.file('notes/todo.txt')?.async('string');\nif (\n archiveBytes.byteLength >= repeatedPayload.length ||\n restoredFileCount !== 3 ||\n restoredReadme !== repeatedPayload ||\n restoredTodo !== 'repeat this session workload'\n) {\n throw new Error('jszip repeated-session fixture did not complete the compressed archive roundtrip');\n}",
3083 )
3084}
3085
3086fn benchmark_metrics_module_source() -> String {
3087 format!(
3088 "const BENCHMARK_MARKER_PREFIX = '{BENCHMARK_MARKER_PREFIX}';\n\nexport function emitBenchmarkMetrics(importMs) {{\n const memoryUsage = process.memoryUsage();\n const resourceUsage = typeof process.resourceUsage === 'function'\n ? process.resourceUsage()\n : null;\n const payload = {{\n resource_usage: {{\n rss_bytes: memoryUsage.rss,\n heap_used_bytes: memoryUsage.heapUsed,\n ...(resourceUsage\n ? {{\n cpu_user_us: resourceUsage.userCPUTime,\n cpu_system_us: resourceUsage.systemCPUTime,\n cpu_total_us: resourceUsage.userCPUTime + resourceUsage.systemCPUTime,\n }}\n : {{}}),\n }},\n }};\n\n if (typeof importMs === 'number') {{\n payload.import_ms = importMs;\n }}\n\n console.log(BENCHMARK_MARKER_PREFIX + JSON.stringify(payload));\n}}\n"
3089 )
3090}
3091
3092fn resource_only_entrypoint_source(body: &str) -> String {
3093 format!(
3094 "import {{ emitBenchmarkMetrics }} from './benchmark-metrics.mjs';\n{body}\nemitBenchmarkMetrics();\n"
3095 )
3096}
3097
3098fn timed_entrypoint_source(body: &str) -> String {
3099 format!(
3100 "import {{ performance }} from 'node:perf_hooks';\nimport {{ emitBenchmarkMetrics }} from './benchmark-metrics.mjs';\nconst started = performance.now();\n{body}\nemitBenchmarkMetrics(performance.now() - started);\n"
3101 )
3102}
3103
3104fn local_graph_terminal_value() -> usize {
3105 let mut value = 1;
3106
3107 for index in 1..LOCAL_GRAPH_MODULE_COUNT {
3108 value += index;
3109 }
3110
3111 value
3112}
3113
3114fn compute_distribution_stats(samples: &[f64]) -> BenchmarkDistributionStats {
3115 let mut sorted = samples.to_vec();
3116 sorted.sort_by(|a, b| a.total_cmp(b));
3117 let mean = sorted.iter().sum::<f64>() / sorted.len() as f64;
3118
3119 BenchmarkDistributionStats {
3120 mean,
3121 p50: percentile(&sorted, 50.0),
3122 p95: percentile(&sorted, 95.0),
3123 min: *sorted.first().unwrap_or(&0.0),
3124 max: *sorted.last().unwrap_or(&0.0),
3125 stddev: standard_deviation(&sorted, mean),
3126 }
3127}
3128
3129fn compute_stats(samples: &[f64]) -> BenchmarkStats {
3130 let stats = compute_distribution_stats(samples);
3131
3132 BenchmarkStats {
3133 mean_ms: stats.mean,
3134 p50_ms: stats.p50,
3135 p95_ms: stats.p95,
3136 min_ms: stats.min,
3137 max_ms: stats.max,
3138 stddev_ms: stats.stddev,
3139 }
3140}
3141
3142fn compute_resource_usage_stats(
3143 samples: &BenchmarkResourceUsage<Vec<f64>>,
3144) -> Option<BenchmarkResourceUsage<BenchmarkDistributionStats>> {
3145 let stats = BenchmarkResourceUsage {
3146 rss_bytes: samples
3147 .rss_bytes
3148 .as_ref()
3149 .map(|samples| compute_distribution_stats(samples)),
3150 heap_used_bytes: samples
3151 .heap_used_bytes
3152 .as_ref()
3153 .map(|samples| compute_distribution_stats(samples)),
3154 cpu_user_us: samples
3155 .cpu_user_us
3156 .as_ref()
3157 .map(|samples| compute_distribution_stats(samples)),
3158 cpu_system_us: samples
3159 .cpu_system_us
3160 .as_ref()
3161 .map(|samples| compute_distribution_stats(samples)),
3162 cpu_total_us: samples
3163 .cpu_total_us
3164 .as_ref()
3165 .map(|samples| compute_distribution_stats(samples)),
3166 };
3167
3168 (!stats.is_empty()).then_some(stats)
3169}
3170
3171fn standard_deviation(samples: &[f64], mean: f64) -> f64 {
3172 if samples.is_empty() {
3173 return 0.0;
3174 }
3175
3176 let variance = samples
3177 .iter()
3178 .map(|sample| {
3179 let delta = sample - mean;
3180 delta * delta
3181 })
3182 .sum::<f64>()
3183 / samples.len() as f64;
3184
3185 variance.sqrt()
3186}
3187
3188fn percentile(sorted: &[f64], p: f64) -> f64 {
3189 if sorted.is_empty() {
3190 return 0.0;
3191 }
3192
3193 let rank = ((p / 100.0) * sorted.len() as f64).ceil() as usize;
3194 let index = rank.saturating_sub(1).min(sorted.len() - 1);
3195 sorted[index]
3196}
3197
3198fn percentage_reduction(original: f64, current: f64) -> f64 {
3199 if original <= 0.0 {
3200 0.0
3201 } else {
3202 ((original - current) / original) * 100.0
3203 }
3204}
3205
3206fn percentage_share(part: f64, total: f64) -> f64 {
3207 if total <= 0.0 {
3208 0.0
3209 } else {
3210 (part / total) * 100.0
3211 }
3212}
3213
3214fn safe_ratio(lhs: f64, rhs: f64) -> f64 {
3215 if rhs <= 0.0 {
3216 0.0
3217 } else {
3218 lhs / rhs
3219 }
3220}
3221
3222fn saturating_delta_ms(total_ms: f64, subtracted_ms: f64) -> f64 {
3223 (total_ms - subtracted_ms).max(0.0)
3224}
3225
3226fn format_ms(value: f64) -> String {
3227 format!("{value:.2}")
3228}
3229
3230fn format_hotspot_value(unit: &str, value: f64) -> String {
3231 match unit {
3232 "pct" => format!("{value:.1}%"),
3233 "MiB" => format_mib(value),
3234 _ => format_ms(value),
3235 }
3236}
3237
3238fn format_sample_list(samples: &[f64]) -> String {
3239 format_scaled_sample_list(samples, std::convert::identity)
3240}
3241
3242fn format_scaled_sample_list(samples: &[f64], scale: impl Fn(f64) -> f64) -> String {
3243 let mut formatted = String::from("[");
3244
3245 for (index, sample) in samples.iter().enumerate() {
3246 if index > 0 {
3247 formatted.push_str(", ");
3248 }
3249 let _ = write!(&mut formatted, "{:.2}", scale(*sample));
3250 }
3251
3252 formatted.push(']');
3253 formatted
3254}
3255
3256fn format_mib(value: f64) -> String {
3257 format!("{value:.2}")
3258}
3259
3260fn format_label_list(labels: &[&str]) -> String {
3261 labels
3262 .iter()
3263 .map(|label| format!("`{label}`"))
3264 .collect::<Vec<_>>()
3265 .join(", ")
3266}
3267
3268fn format_string_label_list(labels: &[&str]) -> String {
3269 labels
3270 .iter()
3271 .map(|label| format!("`{label}`"))
3272 .collect::<Vec<_>>()
3273 .join(", ")
3274}
3275
3276fn push_unique_label<'a>(labels: &mut Vec<&'a str>, value: &'a str) {
3277 if !labels.contains(&value) {
3278 labels.push(value);
3279 }
3280}
3281
3282fn format_delta_ms(value: f64) -> String {
3283 format!("{value:+.2}")
3284}
3285
3286fn format_delta_pct(value: f64) -> String {
3287 format!("{value:+.1}%")
3288}
3289
3290fn push_optional_sample(samples: &mut Option<Vec<f64>>, value: Option<f64>) {
3291 if let Some(value) = value {
3292 samples.get_or_insert_with(Vec::new).push(value);
3293 }
3294}
3295
3296fn bytes_to_mib(value: f64) -> f64 {
3297 value / (1024.0 * 1024.0)
3298}
3299
3300fn micros_to_ms(value: f64) -> f64 {
3301 value / 1000.0
3302}
3303
3304fn hotspot_wall_mean_ms(scenario: &BenchmarkScenarioReport) -> Option<f64> {
3305 Some(scenario.wall_stats.mean_ms)
3306}
3307
3308fn hotspot_wall_stddev_ms(scenario: &BenchmarkScenarioReport) -> Option<f64> {
3309 Some(scenario.wall_stats.stddev_ms)
3310}
3311
3312fn hotspot_wall_range_ms(scenario: &BenchmarkScenarioReport) -> Option<f64> {
3313 Some(scenario.wall_range_ms())
3314}
3315
3316fn hotspot_guest_import_mean_ms(scenario: &BenchmarkScenarioReport) -> Option<f64> {
3317 scenario
3318 .guest_import_stats
3319 .as_ref()
3320 .map(|stats| stats.mean_ms)
3321}
3322
3323fn hotspot_startup_overhead_mean_ms(scenario: &BenchmarkScenarioReport) -> Option<f64> {
3324 scenario
3325 .startup_overhead_stats
3326 .as_ref()
3327 .map(|stats| stats.mean_ms)
3328}
3329
3330fn hotspot_context_setup_mean_ms(scenario: &BenchmarkScenarioReport) -> Option<f64> {
3331 Some(scenario.phase_stats.context_setup_ms.mean_ms)
3332}
3333
3334fn hotspot_startup_phase_mean_ms(scenario: &BenchmarkScenarioReport) -> Option<f64> {
3335 Some(scenario.phase_stats.startup_ms.mean_ms)
3336}
3337
3338fn hotspot_guest_execution_mean_ms(scenario: &BenchmarkScenarioReport) -> Option<f64> {
3339 scenario
3340 .phase_stats
3341 .guest_execution_ms
3342 .as_ref()
3343 .map(|stats| stats.mean_ms)
3344}
3345
3346fn hotspot_completion_mean_ms(scenario: &BenchmarkScenarioReport) -> Option<f64> {
3347 Some(scenario.phase_stats.completion_ms.mean_ms)
3348}
3349
3350fn hotspot_startup_share_pct(scenario: &BenchmarkScenarioReport) -> Option<f64> {
3351 scenario.mean_startup_share_pct()
3352}
3353
3354fn hotspot_rss_mean_mib(scenario: &BenchmarkScenarioReport) -> Option<f64> {
3355 scenario
3356 .resource_usage_stats
3357 .as_ref()?
3358 .rss_bytes
3359 .as_ref()
3360 .map(|stats| bytes_to_mib(stats.mean))
3361}
3362
3363fn hotspot_heap_mean_mib(scenario: &BenchmarkScenarioReport) -> Option<f64> {
3364 scenario
3365 .resource_usage_stats
3366 .as_ref()?
3367 .heap_used_bytes
3368 .as_ref()
3369 .map(|stats| bytes_to_mib(stats.mean))
3370}
3371
3372fn hotspot_total_cpu_mean_ms(scenario: &BenchmarkScenarioReport) -> Option<f64> {
3373 scenario
3374 .resource_usage_stats
3375 .as_ref()?
3376 .cpu_total_us
3377 .as_ref()
3378 .map(|stats| micros_to_ms(stats.mean))
3379}
3380
3381#[cfg(test)]
3382mod tests {
3383 use super::*;
3384 use std::cell::RefCell;
3385 use tempfile::tempdir;
3386
3387 fn synthetic_transport_reports() -> Vec<BenchmarkTransportRttReport> {
3388 TRANSPORT_RTT_PAYLOAD_BYTES
3389 .iter()
3390 .enumerate()
3391 .map(|(index, payload_bytes)| {
3392 let sample = index as f64 + 1.0;
3393 BenchmarkTransportRttReport {
3394 channel: TRANSPORT_RTT_CHANNEL,
3395 payload_bytes: *payload_bytes,
3396 samples_ms: vec![sample],
3397 stats: compute_stats(&[sample]),
3398 }
3399 })
3400 .collect()
3401 }
3402
3403 fn synthetic_scenario_report(
3404 definition: ScenarioDefinition,
3405 wall_sample_ms: f64,
3406 ) -> BenchmarkScenarioReport {
3407 let context_setup_ms = wall_sample_ms / 5.0;
3408 let startup_ms = wall_sample_ms / 4.0;
3409 let guest_execution_ms = definition
3410 .expect_import_metric
3411 .then_some(wall_sample_ms / 3.0);
3412 let completion_ms =
3413 wall_sample_ms - context_setup_ms - startup_ms - guest_execution_ms.unwrap_or(0.0);
3414 let startup_overhead_ms = definition
3415 .expect_import_metric
3416 .then_some(context_setup_ms + startup_ms + completion_ms);
3417 let resource_usage_samples = BenchmarkResourceUsage {
3418 rss_bytes: Some(vec![64.0 * 1024.0 * 1024.0]),
3419 heap_used_bytes: Some(vec![12.0 * 1024.0 * 1024.0]),
3420 cpu_user_us: None,
3421 cpu_system_us: None,
3422 cpu_total_us: Some(vec![wall_sample_ms * 1000.0]),
3423 };
3424
3425 BenchmarkScenarioReport {
3426 id: definition.id,
3427 workload: definition.workload,
3428 runtime: definition.runtime.label(),
3429 mode: definition.mode.label(),
3430 description: definition.description,
3431 fixture: definition.fixture,
3432 compile_cache: definition.compile_cache.label(),
3433 wall_samples_ms: vec![wall_sample_ms],
3434 wall_stats: compute_stats(&[wall_sample_ms]),
3435 guest_import_samples_ms: guest_execution_ms.map(|sample| vec![sample]),
3436 guest_import_stats: guest_execution_ms.map(|sample| compute_stats(&[sample])),
3437 startup_overhead_samples_ms: startup_overhead_ms.map(|sample| vec![sample]),
3438 startup_overhead_stats: startup_overhead_ms.map(|sample| compute_stats(&[sample])),
3439 phase_samples_ms: BenchmarkScenarioPhases {
3440 context_setup_ms: vec![context_setup_ms],
3441 startup_ms: vec![startup_ms],
3442 guest_execution_ms: guest_execution_ms.map(|sample| vec![sample]),
3443 completion_ms: vec![completion_ms],
3444 },
3445 phase_stats: BenchmarkScenarioPhases {
3446 context_setup_ms: compute_stats(&[context_setup_ms]),
3447 startup_ms: compute_stats(&[startup_ms]),
3448 guest_execution_ms: guest_execution_ms.map(|sample| compute_stats(&[sample])),
3449 completion_ms: compute_stats(&[completion_ms]),
3450 },
3451 resource_usage_stats: compute_resource_usage_stats(&resource_usage_samples),
3452 resource_usage_samples: Some(resource_usage_samples),
3453 }
3454 }
3455
3456 fn synthetic_host() -> BenchmarkHost {
3457 BenchmarkHost {
3458 node_binary: String::from("node"),
3459 node_version: String::from("v22.0.0"),
3460 os: "linux",
3461 arch: "x86_64",
3462 logical_cpus: 8,
3463 }
3464 }
3465
3466 #[test]
3467 fn javascript_benchmark_config_rejects_unbounded_iteration_counts() {
3468 assert!(matches!(
3469 validate_benchmark_config(&JavascriptBenchmarkConfig {
3470 iterations: 0,
3471 warmup_iterations: 0,
3472 }),
3473 Err(JavascriptBenchmarkError::InvalidConfig(
3474 "iterations must be greater than zero"
3475 ))
3476 ));
3477 assert!(matches!(
3478 validate_benchmark_config(&JavascriptBenchmarkConfig {
3479 iterations: MAX_BENCHMARK_ITERATIONS + 1,
3480 warmup_iterations: 0,
3481 }),
3482 Err(JavascriptBenchmarkError::InvalidConfig(
3483 "iterations must be less than or equal to 1000"
3484 ))
3485 ));
3486 assert!(matches!(
3487 validate_benchmark_config(&JavascriptBenchmarkConfig {
3488 iterations: 1,
3489 warmup_iterations: MAX_BENCHMARK_WARMUP_ITERATIONS + 1,
3490 }),
3491 Err(JavascriptBenchmarkError::InvalidConfig(
3492 "warmup iterations must be less than or equal to 1000"
3493 ))
3494 ));
3495 }
3496
3497 #[test]
3498 fn javascript_benchmark_orchestration_resumes_completed_stages_from_run_state() {
3499 let tempdir = tempdir().expect("create tempdir");
3500 let repo_root = tempdir.path().join("repo");
3501 let artifact_dir = tempdir.path().join("artifacts");
3502 fs::create_dir_all(&repo_root).expect("create repo root");
3503
3504 let config = JavascriptBenchmarkConfig {
3505 iterations: 1,
3506 warmup_iterations: 0,
3507 };
3508 let host = synthetic_host();
3509 let definitions = benchmark_scenarios();
3510 let mut state = StoredBenchmarkRunState::new(&config, &host, &repo_root);
3511 state.record_transport_rtt(&synthetic_transport_reports());
3512 state.record_scenario(&synthetic_scenario_report(definitions[0], 10.0));
3513 persist_benchmark_run_state(&benchmark_run_state_path(&artifact_dir), &state)
3514 .expect("persist initial run state");
3515
3516 let transport_calls = RefCell::new(0usize);
3517 let scenario_calls = RefCell::new(Vec::new());
3518 let (report, resumed_stage_count, _) = orchestrate_javascript_benchmark_report(
3519 &config,
3520 &repo_root,
3521 &host,
3522 &artifact_dir,
3523 || {
3524 *transport_calls.borrow_mut() += 1;
3525 Ok(synthetic_transport_reports())
3526 },
3527 |definition| {
3528 scenario_calls.borrow_mut().push(definition.id.to_owned());
3529 Ok(synthetic_scenario_report(definition, 20.0))
3530 },
3531 )
3532 .expect("resume benchmark orchestration");
3533
3534 assert_eq!(resumed_stage_count, 2);
3535 assert_eq!(*transport_calls.borrow(), 0);
3536 assert_eq!(
3537 scenario_calls.borrow().as_slice(),
3538 &definitions[1..]
3539 .iter()
3540 .map(|definition| definition.id.to_owned())
3541 .collect::<Vec<_>>()
3542 );
3543 assert_eq!(
3544 report.transport_rtt.len(),
3545 TRANSPORT_RTT_PAYLOAD_BYTES.len()
3546 );
3547 assert_eq!(report.scenarios.len(), definitions.len());
3548 assert_eq!(report.scenarios[0].id, definitions[0].id);
3549 assert_eq!(report.scenarios[1].id, definitions[1].id);
3550 }
3551
3552 #[test]
3553 fn javascript_benchmark_orchestration_persists_completed_stages_before_failure() {
3554 let tempdir = tempdir().expect("create tempdir");
3555 let repo_root = tempdir.path().join("repo");
3556 let artifact_dir = tempdir.path().join("artifacts");
3557 fs::create_dir_all(&repo_root).expect("create repo root");
3558
3559 let config = JavascriptBenchmarkConfig {
3560 iterations: 1,
3561 warmup_iterations: 0,
3562 };
3563 let host = synthetic_host();
3564 let state_path = benchmark_run_state_path(&artifact_dir);
3565 let failure = orchestrate_javascript_benchmark_report(
3566 &config,
3567 &repo_root,
3568 &host,
3569 &artifact_dir,
3570 || Ok(synthetic_transport_reports()),
3571 |definition| {
3572 if definition.id == "cold-local-import" {
3573 Err(JavascriptBenchmarkError::InvalidConfig("synthetic failure"))
3574 } else {
3575 Ok(synthetic_scenario_report(definition, 15.0))
3576 }
3577 },
3578 )
3579 .expect_err("expected synthetic orchestration failure");
3580
3581 assert!(matches!(
3582 failure,
3583 JavascriptBenchmarkError::InvalidConfig("synthetic failure")
3584 ));
3585
3586 let stored_state = serde_json::from_str::<StoredBenchmarkRunState>(
3587 &fs::read_to_string(&state_path).expect("read persisted run state"),
3588 )
3589 .expect("parse persisted run state");
3590 assert!(stored_state.transport_rtt.is_some());
3591 assert_eq!(
3592 stored_state
3593 .scenarios
3594 .iter()
3595 .map(|scenario| scenario.id.as_str())
3596 .collect::<Vec<_>>(),
3597 vec!["isolate-startup", "prewarmed-isolate-startup"]
3598 );
3599 }
3600}