1use alloy_provider::{Provider, ProviderBuilder};
4use clap::Parser;
5use eyre::{eyre, Result, WrapErr};
6use reth_chainspec::Chain;
7use reth_cli_runner::CliContext;
8use reth_node_core::args::{DatadirArgs, LogArgs, TraceArgs};
9use reth_tracing::FileWorkerGuard;
10use std::{net::TcpListener, path::PathBuf, str::FromStr};
11use tokio::process::Command;
12use tracing::{debug, info, warn};
13
14use crate::{
15 benchmark::BenchmarkRunner, comparison::ComparisonGenerator, compilation::CompilationManager,
16 git::GitManager, node::NodeManager,
17};
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
21pub(crate) enum DisableStartupSyncStateIdle {
22 Baseline,
24 Feature,
26 All,
28}
29
30impl FromStr for DisableStartupSyncStateIdle {
31 type Err = String;
32
33 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
34 match s.to_lowercase().as_str() {
35 "baseline" => Ok(Self::Baseline),
36 "feature" => Ok(Self::Feature),
37 "all" => Ok(Self::All),
38 _ => Err(format!("Invalid value '{}'. Expected 'baseline', 'feature', or 'all'", s)),
39 }
40 }
41}
42
43impl std::fmt::Display for DisableStartupSyncStateIdle {
44 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
45 match self {
46 Self::Baseline => write!(f, "baseline"),
47 Self::Feature => write!(f, "feature"),
48 Self::All => write!(f, "all"),
49 }
50 }
51}
52
53#[derive(Debug, Parser)]
55#[command(
56 name = "reth-bench-compare",
57 about = "Compare reth performance between two git references (branches or tags)",
58 version
59)]
60pub(crate) struct Args {
61 #[arg(long, value_name = "REF")]
63 pub baseline_ref: String,
64
65 #[arg(long, value_name = "REF")]
67 pub feature_ref: String,
68
69 #[command(flatten)]
70 pub datadir: DatadirArgs,
71
72 #[arg(long, value_name = "N", default_value = "100")]
74 pub blocks: u64,
75
76 #[arg(long, value_name = "URL")]
78 pub rpc_url: Option<String>,
79
80 #[arg(long, value_name = "PATH")]
85 pub jwt_secret: Option<PathBuf>,
86
87 #[arg(long, value_name = "PATH", default_value = "./reth-bench-compare")]
89 pub output_dir: String,
90
91 #[arg(long)]
93 pub skip_git_validation: bool,
94
95 #[arg(long, value_name = "PORT", default_value = "5005")]
97 pub metrics_port: u16,
98
99 #[arg(long, value_name = "CHAIN", default_value = "mainnet", required = false)]
103 pub chain: Chain,
104
105 #[arg(long)]
107 pub sudo: bool,
108
109 #[arg(long)]
111 pub draw: bool,
112
113 #[arg(long)]
115 pub profile: bool,
116
117 #[arg(long, value_name = "DURATION")]
122 pub wait_time: Option<String>,
123
124 #[arg(long)]
133 pub wait_for_persistence: bool,
134
135 #[arg(long, value_name = "PERSISTENCE_THRESHOLD")]
141 pub persistence_threshold: Option<u64>,
142
143 #[arg(long, value_name = "N")]
146 pub warmup_blocks: Option<u64>,
147
148 #[arg(long)]
151 pub no_clear_cache: bool,
152
153 #[arg(long)]
156 pub skip_wait_syncing: bool,
157
158 #[command(flatten)]
159 pub logs: LogArgs,
160
161 #[command(flatten)]
162 pub traces: TraceArgs,
163
164 #[arg(
167 long,
168 value_name = "OTLP_BUFFER_SIZE",
169 default_value = "32768",
170 help_heading = "Tracing"
171 )]
172 pub otlp_max_queue_size: usize,
173
174 #[arg(long, value_name = "ARGS")]
178 pub baseline_args: Option<String>,
179
180 #[arg(long, value_name = "ARGS")]
184 pub feature_args: Option<String>,
185
186 #[arg(trailing_var_arg = true, allow_hyphen_values = true)]
192 pub reth_args: Vec<String>,
193
194 #[arg(long, value_name = "FEATURES", default_value = "")]
197 pub features: String,
198
199 #[arg(long, value_name = "FEATURES")]
203 pub baseline_features: Option<String>,
204
205 #[arg(long, value_name = "FEATURES")]
209 pub feature_features: Option<String>,
210
211 #[arg(long, value_name = "FLAGS", default_value = "-C target-cpu=native")]
215 pub rustflags: String,
216
217 #[arg(long, value_name = "FLAGS")]
221 pub baseline_rustflags: Option<String>,
222
223 #[arg(long, value_name = "FLAGS")]
227 pub feature_rustflags: Option<String>,
228
229 #[arg(long, value_name = "TARGET")]
236 pub disable_startup_sync_state_idle: Option<DisableStartupSyncStateIdle>,
237}
238
239impl Args {
240 pub(crate) fn init_tracing(&self) -> Result<Option<FileWorkerGuard>> {
242 let guard = self.logs.init_tracing()?;
243 Ok(guard)
244 }
245
246 pub(crate) fn build_additional_args(
249 &self,
250 ref_type: &str,
251 base_args_str: Option<&String>,
252 ) -> Vec<String> {
253 let mut args = base_args_str.map(|s| parse_args_string(s)).unwrap_or_default();
255
256 let should_add_flag = match self.disable_startup_sync_state_idle {
258 None => true, Some(DisableStartupSyncStateIdle::All) => false,
260 Some(DisableStartupSyncStateIdle::Baseline) => {
261 ref_type != "baseline" && ref_type != "warmup"
262 }
263 Some(DisableStartupSyncStateIdle::Feature) => ref_type != "feature",
264 };
265
266 if should_add_flag {
267 args.push("--debug.startup-sync-state-idle".to_string());
268 debug!("Adding --debug.startup-sync-state-idle flag for ref_type: {}", ref_type);
269 } else {
270 debug!("Skipping --debug.startup-sync-state-idle flag for ref_type: {}", ref_type);
271 }
272
273 args
274 }
275
276 const fn get_default_rpc_url(chain: &Chain) -> &'static str {
278 match chain.id() {
279 27082 => "https://rpc.hoodi.ethpandaops.io", _ => "https://ethereum.reth.rs/rpc", }
282 }
283
284 pub(crate) fn get_rpc_url(&self) -> String {
286 self.rpc_url.clone().unwrap_or_else(|| Self::get_default_rpc_url(&self.chain).to_string())
287 }
288
289 pub(crate) fn jwt_secret_path(&self) -> PathBuf {
291 match &self.jwt_secret {
292 Some(path) => {
293 let jwt_secret_str = path.to_string_lossy();
294 let expanded = shellexpand::tilde(&jwt_secret_str);
295 PathBuf::from(expanded.as_ref())
296 }
297 None => {
298 let chain_path = self.datadir.clone().resolve_datadir(self.chain);
300 chain_path.jwt()
301 }
302 }
303 }
304
305 pub(crate) fn datadir_path(&self) -> PathBuf {
307 let chain_path = self.datadir.clone().resolve_datadir(self.chain);
308 chain_path.data_dir().to_path_buf()
309 }
310
311 pub(crate) fn output_dir_path(&self) -> PathBuf {
313 let expanded = shellexpand::tilde(&self.output_dir);
314 PathBuf::from(expanded.as_ref())
315 }
316
317 pub(crate) fn get_warmup_blocks(&self) -> u64 {
319 self.warmup_blocks.unwrap_or(self.blocks)
320 }
321}
322
323async fn validate_rpc_chain_id(rpc_url: &str, expected_chain: &Chain) -> Result<()> {
325 let url = rpc_url.parse().map_err(|e| eyre!("Invalid RPC URL '{}': {}", rpc_url, e))?;
327 let provider = ProviderBuilder::new().connect_http(url);
328
329 let rpc_chain_id = provider
331 .get_chain_id()
332 .await
333 .map_err(|e| eyre!("Failed to get chain ID from RPC endpoint {}: {:?}", rpc_url, e))?;
334
335 let expected_chain_id = expected_chain.id();
336
337 if rpc_chain_id != expected_chain_id {
338 return Err(eyre!(
339 "RPC endpoint chain ID mismatch!\n\
340 Expected: {} (chain: {})\n\
341 Found: {} at RPC endpoint: {}\n\n\
342 Please use an RPC endpoint for the correct network or change the --chain argument.",
343 expected_chain_id,
344 expected_chain,
345 rpc_chain_id,
346 rpc_url
347 ));
348 }
349
350 info!("Validated RPC endpoint chain ID");
351 Ok(())
352}
353
354pub(crate) async fn run_comparison(args: Args, _ctx: CliContext) -> Result<()> {
356 #[cfg(unix)]
358 {
359 use nix::unistd::{getpid, setpgid};
360 if let Err(e) = setpgid(getpid(), getpid()) {
361 warn!("Failed to create process group: {e}");
362 }
363 }
364
365 info!(
366 "Starting benchmark comparison between '{}' and '{}'",
367 args.baseline_ref, args.feature_ref
368 );
369
370 if args.sudo {
371 info!("Running in sudo mode - reth commands will use elevated privileges");
372 }
373
374 let git_manager = GitManager::new()?;
376 git_manager.fetch_all()?;
378
379 let output_dir = args.output_dir_path();
381 let compilation_manager = CompilationManager::new(
382 git_manager.repo_root().to_string(),
383 output_dir.clone(),
384 git_manager.clone(),
385 )?;
386 let mut node_manager = NodeManager::new(&args);
388
389 let benchmark_runner = BenchmarkRunner::new(&args);
390 let mut comparison_generator = ComparisonGenerator::new(&args);
391
392 node_manager.set_comparison_dir(comparison_generator.get_output_dir());
394
395 let original_ref = git_manager.get_current_ref()?;
397 info!("Current git reference: {}", original_ref);
398
399 if !args.skip_git_validation {
401 git_manager.validate_clean_state()?;
402 git_manager.validate_refs(&[&args.baseline_ref, &args.feature_ref])?;
403 }
404
405 let rpc_url = args.get_rpc_url();
407 validate_rpc_chain_id(&rpc_url, &args.chain).await?;
408
409 let git_manager_cleanup = git_manager.clone();
411 let original_ref_cleanup = original_ref.clone();
412 ctrlc::set_handler(move || {
413 eprintln!("Received interrupt signal, cleaning up...");
414
415 #[cfg(unix)]
417 {
418 use nix::{
419 sys::signal::{kill, Signal},
420 unistd::Pid,
421 };
422
423 let current_pid = std::process::id() as i32;
425 let pgid = Pid::from_raw(-current_pid);
426 if let Err(e) = kill(pgid, Signal::SIGTERM) {
427 eprintln!("Failed to send SIGTERM to process group: {e}");
428 }
429 }
430
431 std::thread::sleep(std::time::Duration::from_millis(200));
433
434 if let Err(e) = git_manager_cleanup.switch_ref(&original_ref_cleanup) {
435 eprintln!("Failed to restore original git reference: {e}");
436 eprintln!("You may need to manually run: git checkout {original_ref_cleanup}");
437 }
438 std::process::exit(1);
439 })?;
440
441 let result = run_benchmark_workflow(
442 &git_manager,
443 &compilation_manager,
444 &mut node_manager,
445 &benchmark_runner,
446 &mut comparison_generator,
447 &args,
448 )
449 .await;
450
451 info!("Restoring original git reference: {}", original_ref);
453 git_manager.switch_ref(&original_ref)?;
454
455 result?;
457
458 Ok(())
459}
460
461fn parse_args_string(args_str: &str) -> Vec<String> {
463 shlex::split(args_str).unwrap_or_else(|| {
464 args_str.split_whitespace().map(|s| s.to_string()).collect()
466 })
467}
468
469async fn run_compilation_phase(
471 git_manager: &GitManager,
472 compilation_manager: &CompilationManager,
473 args: &Args,
474) -> Result<(String, String)> {
475 info!("=== Running compilation phase ===");
476
477 compilation_manager.ensure_reth_bench_available()?;
479 if args.profile {
480 compilation_manager.ensure_samply_available()?;
481 }
482
483 let refs = [&args.baseline_ref, &args.feature_ref];
484 let ref_types = ["baseline", "feature"];
485
486 let mut ref_commits = std::collections::HashMap::new();
489 for &git_ref in &refs {
490 if !ref_commits.contains_key(git_ref) {
491 git_manager.switch_ref(git_ref)?;
492 let commit = git_manager.get_current_commit()?;
493 ref_commits.insert(git_ref.clone(), commit);
494 info!("Reference {} resolves to commit: {}", git_ref, &ref_commits[git_ref][..8]);
495 }
496 }
497
498 for (i, &git_ref) in refs.iter().enumerate() {
500 let ref_type = ref_types[i];
501 let commit = &ref_commits[git_ref];
502
503 let features = match ref_type {
505 "baseline" => args.baseline_features.as_ref().unwrap_or(&args.features),
506 "feature" => args.feature_features.as_ref().unwrap_or(&args.features),
507 _ => &args.features,
508 };
509 let rustflags = match ref_type {
510 "baseline" => args.baseline_rustflags.as_ref().unwrap_or(&args.rustflags),
511 "feature" => args.feature_rustflags.as_ref().unwrap_or(&args.rustflags),
512 _ => &args.rustflags,
513 };
514
515 info!(
516 "Compiling {} binary for reference: {} (commit: {})",
517 ref_type,
518 git_ref,
519 &commit[..8]
520 );
521
522 git_manager.switch_ref(git_ref)?;
524
525 compilation_manager.compile_reth(commit, features, rustflags)?;
527
528 info!("Completed compilation for {} reference", ref_type);
529 }
530
531 let baseline_commit = ref_commits[&args.baseline_ref].clone();
532 let feature_commit = ref_commits[&args.feature_ref].clone();
533
534 info!("Compilation phase completed");
535 Ok((baseline_commit, feature_commit))
536}
537
538#[allow(clippy::too_many_arguments)]
539async fn run_warmup_phase(
541 git_manager: &GitManager,
542 compilation_manager: &CompilationManager,
543 node_manager: &mut NodeManager,
544 benchmark_runner: &BenchmarkRunner,
545 args: &Args,
546 baseline_commit: &str,
547 starting_tip: u64,
548) -> Result<()> {
549 info!("=== Running warmup phase ===");
550
551 let warmup_blocks = args.get_warmup_blocks();
553 let unwind_target = starting_tip.saturating_sub(warmup_blocks);
554 node_manager.unwind_to_block(unwind_target).await?;
555
556 let warmup_ref = &args.baseline_ref;
558
559 git_manager.switch_ref(warmup_ref)?;
561
562 let binary_path = compilation_manager.get_cached_binary_path_for_commit(baseline_commit);
564
565 if !binary_path.exists() {
567 return Err(eyre!(
568 "Cached baseline binary not found at {:?}. Compilation phase should have created it.",
569 binary_path
570 ));
571 }
572
573 info!("Using cached baseline binary for warmup (commit: {})", &baseline_commit[..8]);
574
575 let additional_args = args.build_additional_args("warmup", args.baseline_args.as_ref());
577
578 let (mut node_process, _warmup_command) =
580 node_manager.start_node(&binary_path, warmup_ref, "warmup", &additional_args).await?;
581
582 let current_tip = if args.skip_wait_syncing {
584 node_manager.wait_for_rpc_and_get_tip(&mut node_process).await?
585 } else {
586 node_manager.wait_for_node_ready_and_get_tip(&mut node_process).await?
587 };
588 info!("Warmup node is ready at tip: {}", current_tip);
589
590 if args.no_clear_cache {
592 info!("Skipping filesystem cache clearing (--no-clear-cache flag set)");
593 } else {
594 BenchmarkRunner::clear_fs_caches().await?;
595 }
596
597 benchmark_runner.run_warmup(current_tip).await?;
599
600 node_manager.stop_node(&mut node_process).await?;
602
603 info!("Warmup phase completed");
604 Ok(())
605}
606
607async fn run_benchmark_workflow(
609 git_manager: &GitManager,
610 compilation_manager: &CompilationManager,
611 node_manager: &mut NodeManager,
612 benchmark_runner: &BenchmarkRunner,
613 comparison_generator: &mut ComparisonGenerator,
614 args: &Args,
615) -> Result<()> {
616 let (baseline_commit, feature_commit) =
618 run_compilation_phase(git_manager, compilation_manager, args).await?;
619
620 git_manager.switch_ref(&args.baseline_ref)?;
622 let binary_path = compilation_manager.get_cached_binary_path_for_commit(&baseline_commit);
623 if !binary_path.exists() {
624 return Err(eyre!(
625 "Cached baseline binary not found at {:?}. Compilation phase should have created it.",
626 binary_path
627 ));
628 }
629
630 info!("=== Determining initial block height ===");
632 let additional_args = args.build_additional_args("baseline", args.baseline_args.as_ref());
633 let (mut node_process, _) = node_manager
634 .start_node(&binary_path, &args.baseline_ref, "baseline", &additional_args)
635 .await?;
636 let starting_tip = if args.skip_wait_syncing {
637 node_manager.wait_for_rpc_and_get_tip(&mut node_process).await?
638 } else {
639 node_manager.wait_for_node_ready_and_get_tip(&mut node_process).await?
640 };
641 info!("Node starting tip: {}", starting_tip);
642 node_manager.stop_node(&mut node_process).await?;
643
644 if args.get_warmup_blocks() > 0 {
646 run_warmup_phase(
647 git_manager,
648 compilation_manager,
649 node_manager,
650 benchmark_runner,
651 args,
652 &baseline_commit,
653 starting_tip,
654 )
655 .await?;
656 } else {
657 info!("Skipping warmup phase (warmup_blocks is 0)");
658 }
659
660 let refs = [&args.baseline_ref, &args.feature_ref];
661 let ref_types = ["baseline", "feature"];
662 let commits = [&baseline_commit, &feature_commit];
663
664 for (i, &git_ref) in refs.iter().enumerate() {
665 let ref_type = ref_types[i];
666 let commit = commits[i];
667 info!("=== Processing {} reference: {} ===", ref_type, git_ref);
668
669 let unwind_target = starting_tip.saturating_sub(args.blocks);
671 node_manager.unwind_to_block(unwind_target).await?;
672
673 git_manager.switch_ref(git_ref)?;
675
676 let binary_path = compilation_manager.get_cached_binary_path_for_commit(commit);
678
679 if !binary_path.exists() {
681 return Err(eyre!(
682 "Cached {} binary not found at {:?}. Compilation phase should have created it.",
683 ref_type,
684 binary_path
685 ));
686 }
687
688 info!("Using cached {} binary (commit: {})", ref_type, &commit[..8]);
689
690 let base_args_str = match ref_type {
692 "baseline" => args.baseline_args.as_ref(),
693 "feature" => args.feature_args.as_ref(),
694 _ => None,
695 };
696
697 let additional_args = args.build_additional_args(ref_type, base_args_str);
699
700 let (mut node_process, reth_command) =
702 node_manager.start_node(&binary_path, git_ref, ref_type, &additional_args).await?;
703
704 let current_tip = if args.skip_wait_syncing {
706 node_manager.wait_for_rpc_and_get_tip(&mut node_process).await?
707 } else {
708 node_manager.wait_for_node_ready_and_get_tip(&mut node_process).await?
709 };
710 info!("Node is ready at tip: {}", current_tip);
711
712 let from_block = current_tip;
716 let to_block = current_tip + args.blocks;
717
718 let output_dir = comparison_generator.get_ref_output_dir(ref_type);
720
721 let benchmark_start = chrono::Utc::now();
723
724 benchmark_runner.run_benchmark(from_block, to_block, &output_dir).await?;
726
727 let benchmark_end = chrono::Utc::now();
729
730 node_manager.stop_node(&mut node_process).await?;
732
733 comparison_generator.add_ref_results(ref_type, &output_dir)?;
735
736 comparison_generator.set_ref_timestamps(ref_type, benchmark_start, benchmark_end)?;
738 comparison_generator.set_ref_command(ref_type, reth_command)?;
739
740 info!("Completed {} reference benchmark", ref_type);
741 }
742
743 comparison_generator.generate_comparison_report().await?;
745
746 if args.draw {
748 generate_comparison_charts(comparison_generator).await?;
749 }
750
751 if args.profile {
753 start_samply_servers(args).await?;
754 }
755
756 Ok(())
757}
758
759async fn generate_comparison_charts(comparison_generator: &ComparisonGenerator) -> Result<()> {
761 info!("Generating comparison charts with Python script...");
762
763 let baseline_output_dir = comparison_generator.get_ref_output_dir("baseline");
764 let feature_output_dir = comparison_generator.get_ref_output_dir("feature");
765
766 let baseline_csv = baseline_output_dir.join("combined_latency.csv");
767 let feature_csv = feature_output_dir.join("combined_latency.csv");
768
769 if !baseline_csv.exists() {
771 return Err(eyre!("Baseline CSV not found: {:?}", baseline_csv));
772 }
773 if !feature_csv.exists() {
774 return Err(eyre!("Feature CSV not found: {:?}", feature_csv));
775 }
776
777 let output_dir = comparison_generator.get_output_dir();
778 let chart_output = output_dir.join("latency_comparison.png");
779
780 let script_path = "bin/reth-bench/scripts/compare_newpayload_latency.py";
781
782 info!("Running Python comparison script with uv...");
783 let mut cmd = Command::new("uv");
784 cmd.args([
785 "run",
786 script_path,
787 &baseline_csv.to_string_lossy(),
788 &feature_csv.to_string_lossy(),
789 "-o",
790 &chart_output.to_string_lossy(),
791 ]);
792
793 #[cfg(unix)]
795 {
796 cmd.process_group(0);
797 }
798
799 let output = cmd.output().await.map_err(|e| {
800 eyre!("Failed to execute Python script with uv: {}. Make sure uv is installed.", e)
801 })?;
802
803 if !output.status.success() {
804 let stderr = String::from_utf8_lossy(&output.stderr);
805 let stdout = String::from_utf8_lossy(&output.stdout);
806 return Err(eyre!(
807 "Python script failed with exit code {:?}:\nstdout: {}\nstderr: {}",
808 output.status.code(),
809 stdout,
810 stderr
811 ));
812 }
813
814 let stdout = String::from_utf8_lossy(&output.stdout);
815 if !stdout.trim().is_empty() {
816 info!("Python script output:\n{}", stdout);
817 }
818
819 info!("Comparison chart generated: {:?}", chart_output);
820 Ok(())
821}
822
823async fn start_samply_servers(args: &Args) -> Result<()> {
825 info!("Starting samply servers for profile viewing...");
826
827 let output_dir = args.output_dir_path();
828 let profiles_dir = output_dir.join("profiles");
829
830 let baseline_profile = profiles_dir.join("baseline.json.gz");
832 let feature_profile = profiles_dir.join("feature.json.gz");
833
834 if !baseline_profile.exists() {
836 warn!("Baseline profile not found: {:?}", baseline_profile);
837 return Ok(());
838 }
839 if !feature_profile.exists() {
840 warn!("Feature profile not found: {:?}", feature_profile);
841 return Ok(());
842 }
843
844 let (baseline_port, feature_port) = find_consecutive_ports(3000)?;
846 info!("Found available ports: {} and {}", baseline_port, feature_port);
847
848 let samply_path = get_samply_path().await?;
850
851 info!("Starting samply server for baseline '{}' on port {}", args.baseline_ref, baseline_port);
853 let mut baseline_cmd = Command::new(&samply_path);
854 baseline_cmd
855 .args(["load", "--port", &baseline_port.to_string(), &baseline_profile.to_string_lossy()])
856 .kill_on_drop(true);
857
858 #[cfg(unix)]
860 {
861 baseline_cmd.process_group(0);
862 }
863
864 if tracing::enabled!(tracing::Level::DEBUG) {
866 baseline_cmd.stdout(std::process::Stdio::piped()).stderr(std::process::Stdio::piped());
867 } else {
868 baseline_cmd.stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null());
869 }
870
871 debug!("Executing samply load command: {:?}", baseline_cmd);
873
874 let mut baseline_child =
875 baseline_cmd.spawn().wrap_err("Failed to start samply server for baseline")?;
876
877 if tracing::enabled!(tracing::Level::DEBUG) {
879 if let Some(stdout) = baseline_child.stdout.take() {
880 tokio::spawn(async move {
881 use tokio::io::{AsyncBufReadExt, BufReader};
882 let reader = BufReader::new(stdout);
883 let mut lines = reader.lines();
884 while let Ok(Some(line)) = lines.next_line().await {
885 debug!("[SAMPLY-BASELINE] {}", line);
886 }
887 });
888 }
889
890 if let Some(stderr) = baseline_child.stderr.take() {
891 tokio::spawn(async move {
892 use tokio::io::{AsyncBufReadExt, BufReader};
893 let reader = BufReader::new(stderr);
894 let mut lines = reader.lines();
895 while let Ok(Some(line)) = lines.next_line().await {
896 debug!("[SAMPLY-BASELINE] {}", line);
897 }
898 });
899 }
900 }
901
902 info!("Starting samply server for feature '{}' on port {}", args.feature_ref, feature_port);
904 let mut feature_cmd = Command::new(&samply_path);
905 feature_cmd
906 .args(["load", "--port", &feature_port.to_string(), &feature_profile.to_string_lossy()])
907 .kill_on_drop(true);
908
909 #[cfg(unix)]
911 {
912 feature_cmd.process_group(0);
913 }
914
915 if tracing::enabled!(tracing::Level::DEBUG) {
917 feature_cmd.stdout(std::process::Stdio::piped()).stderr(std::process::Stdio::piped());
918 } else {
919 feature_cmd.stdout(std::process::Stdio::null()).stderr(std::process::Stdio::null());
920 }
921
922 debug!("Executing samply load command: {:?}", feature_cmd);
924
925 let mut feature_child =
926 feature_cmd.spawn().wrap_err("Failed to start samply server for feature")?;
927
928 if tracing::enabled!(tracing::Level::DEBUG) {
930 if let Some(stdout) = feature_child.stdout.take() {
931 tokio::spawn(async move {
932 use tokio::io::{AsyncBufReadExt, BufReader};
933 let reader = BufReader::new(stdout);
934 let mut lines = reader.lines();
935 while let Ok(Some(line)) = lines.next_line().await {
936 debug!("[SAMPLY-FEATURE] {}", line);
937 }
938 });
939 }
940
941 if let Some(stderr) = feature_child.stderr.take() {
942 tokio::spawn(async move {
943 use tokio::io::{AsyncBufReadExt, BufReader};
944 let reader = BufReader::new(stderr);
945 let mut lines = reader.lines();
946 while let Ok(Some(line)) = lines.next_line().await {
947 debug!("[SAMPLY-FEATURE] {}", line);
948 }
949 });
950 }
951 }
952
953 tokio::time::sleep(std::time::Duration::from_secs(2)).await;
955
956 println!("\n=== SAMPLY PROFILE SERVERS STARTED ===");
958 println!("Baseline '{}': http://127.0.0.1:{}", args.baseline_ref, baseline_port);
959 println!("Feature '{}': http://127.0.0.1:{}", args.feature_ref, feature_port);
960 println!("\nOpen the URLs in your browser to view the profiles.");
961 println!("Press Ctrl+C to stop the servers and exit.");
962 println!("=========================================\n");
963
964 let ctrl_c = tokio::signal::ctrl_c();
966 let baseline_wait = baseline_child.wait();
967 let feature_wait = feature_child.wait();
968
969 tokio::select! {
970 _ = ctrl_c => {
971 info!("Received Ctrl+C, shutting down samply servers...");
972 }
973 result = baseline_wait => {
974 match result {
975 Ok(status) => info!("Baseline samply server exited with status: {}", status),
976 Err(e) => warn!("Baseline samply server error: {}", e),
977 }
978 }
979 result = feature_wait => {
980 match result {
981 Ok(status) => info!("Feature samply server exited with status: {}", status),
982 Err(e) => warn!("Feature samply server error: {}", e),
983 }
984 }
985 }
986
987 let _ = baseline_child.kill().await;
989 let _ = feature_child.kill().await;
990
991 info!("Samply servers stopped.");
992 Ok(())
993}
994
995fn find_consecutive_ports(start_port: u16) -> Result<(u16, u16)> {
997 for port in start_port..=65533 {
998 if is_port_available(port) && is_port_available(port + 1) {
1000 return Ok((port, port + 1));
1001 }
1002 }
1003 Err(eyre!("Could not find two consecutive available ports starting from {}", start_port))
1004}
1005
1006fn is_port_available(port: u16) -> bool {
1008 TcpListener::bind(("127.0.0.1", port)).is_ok()
1009}
1010
1011async fn get_samply_path() -> Result<String> {
1013 let output = Command::new("which")
1014 .arg("samply")
1015 .output()
1016 .await
1017 .wrap_err("Failed to execute 'which samply' command")?;
1018
1019 if !output.status.success() {
1020 return Err(eyre!("samply not found in PATH"));
1021 }
1022
1023 let samply_path = String::from_utf8(output.stdout)
1024 .wrap_err("samply path is not valid UTF-8")?
1025 .trim()
1026 .to_string();
1027
1028 if samply_path.is_empty() {
1029 return Err(eyre!("which samply returned empty path"));
1030 }
1031
1032 Ok(samply_path)
1033}