1use crate::bpf::{BpfSkel, BpfSkelBuilder};
7use crate::bpf_intf::hints_event;
8use crate::Context;
9use anyhow::{bail, Context as _, Result};
10use clap::Parser;
11use libbpf_rs::skel::{OpenSkel, SkelBuilder};
12use libbpf_rs::{MapCore, MapHandle, OpenObject, RingBufferBuilder};
13use serde::Serialize;
14use std::cell::RefCell;
15use std::fs::{self, File};
16use std::io::{BufWriter, Write};
17use std::mem::MaybeUninit;
18use std::os::fd::{AsRawFd, FromRawFd, OwnedFd, RawFd};
19use std::os::unix::process::ExitStatusExt;
20use std::path::{Path, PathBuf};
21use std::process::{Child, Command, Stdio};
22use std::rc::Rc;
23use std::time::{Duration, Instant};
24
25pub fn perf_binary() -> String {
26 std::env::var("SCX_CHARACTERIZE_PERF").unwrap_or_else(|_| "perf".to_string())
27}
28
29const SCHED_TRACE_EVENTS: &[&str] = &[
30 "sched:sched_switch",
31 "sched:sched_wakeup",
32 "sched:sched_wakeup_new",
33 "sched:sched_waking",
34 "sched:sched_stat_runtime",
35 "irq:irq_handler_entry",
36 "irq:irq_handler_exit",
37 "irq:softirq_entry",
38 "irq:softirq_exit",
39 "irq:softirq_raise",
40 "irq_vectors:local_timer_entry",
41 "irq_vectors:local_timer_exit",
42 "irq_vectors:reschedule_entry",
43 "irq_vectors:reschedule_exit",
44 "nmi:nmi_handler",
45];
46
47pub const PERF_MEM_DATA_FILE: &str = "perf.mem.data";
48pub const PERF_MEM_SCRIPT_FILE: &str = "perf.mem.script";
49pub const PERF_MEM_JSONL_FILE: &str = "perf.mem.jsonl";
50pub const PERF_SCHED_DATA_FILE: &str = "perf.sched.data";
51pub const PERF_SCHED_SCRIPT_FILE: &str = "perf.sched.script";
52pub const PERF_SCHED_JSONL_FILE: &str = "perf.sched.jsonl";
53const DEFAULT_PERF_MMAP_SIZE: &str = "8M";
54const PERF_SCHED_CLOCKID: &str = "CLOCK_MONOTONIC";
55
56#[derive(Debug, Parser)]
57#[command(
58 after_help = "Trace cost notes:\n hints trace < mem trace < sched trace\n\n sched trace produces a much higher volume of data than hints or mem trace and can be quite costly. On some workloads it can materially perturb the host, especially for longer recordings or when writing to slower / write-amplifying filesystems. Prefer shorter durations when sched trace is enabled."
59)]
60pub struct RecordOpts {
61 #[clap(short, long, default_value = "scx_characterize.out")]
63 pub output: PathBuf,
64
65 #[clap(short = 'f', long)]
67 pub file: Option<PathBuf>,
68
69 #[clap(short = 't', long, default_value = "30")]
71 pub timeout: u64,
72
73 #[clap(short = 'l', long, default_value = "10")]
75 pub ldlat: u32,
76
77 #[clap(long)]
79 pub hints_map: Option<PathBuf>,
80
81 #[clap(long, default_value = "8")]
83 pub hints_map_ring_sz: u32,
84
85 #[clap(long)]
87 pub disable_archive: bool,
88
89 #[clap(long)]
91 pub enable_perf_script: bool,
92
93 #[clap(long)]
95 pub disable_sched_trace: bool,
96
97 #[clap(long)]
99 pub disable_mem_trace: bool,
100}
101
102struct SpawnedProcess {
103 child: Child,
104 pidfd: OwnedFd,
105 waited: bool,
106}
107
108impl SpawnedProcess {
109 fn spawn(cmd: &[String]) -> Result<Self> {
110 let child = Command::new(&cmd[0])
111 .args(&cmd[1..])
112 .stdin(Stdio::inherit())
113 .stdout(Stdio::inherit())
114 .stderr(Stdio::inherit())
115 .spawn()
116 .context("failed to spawn command")?;
117
118 let pid = child.id() as i32;
119 let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0) } as RawFd;
120 if fd < 0 {
121 bail!("pidfd_open failed: {}", std::io::Error::last_os_error());
122 }
123
124 Ok(Self {
125 child,
126 pidfd: unsafe { OwnedFd::from_raw_fd(fd) },
127 waited: false,
128 })
129 }
130
131 fn pid(&self) -> i32 {
132 self.child.id() as i32
133 }
134
135 fn pidfd(&self) -> RawFd {
136 self.pidfd.as_raw_fd()
137 }
138
139 fn signal(&self, sig: i32) {
140 unsafe {
141 libc::kill(self.pid(), sig);
142 }
143 }
144
145 fn wait(&mut self) -> Result<()> {
146 if self.waited {
147 return Ok(());
148 }
149 self.waited = true;
150 let status = self.child.wait().context("failed to wait for child")?;
151 if !status.success() {
152 if matches!(status.signal(), Some(libc::SIGINT) | Some(libc::SIGKILL)) {
153 return Ok(());
154 }
155 bail!("perf exited with status: {}", status);
156 }
157 Ok(())
158 }
159}
160
161impl Drop for SpawnedProcess {
162 fn drop(&mut self) {
163 if !self.waited {
164 self.signal(libc::SIGINT);
165 let _ = self.child.wait();
166 }
167 }
168}
169
170#[derive(Debug, Clone, Serialize)]
171struct HintsEventRecord {
172 pid: i32,
173 tgid: i32,
174 hints: u64,
175 timestamp: u64,
176}
177
178struct HintsRecorder<'a> {
179 link: Option<libbpf_rs::Link>,
180 ringbuf: libbpf_rs::RingBuffer<'a>,
181 events: Rc<RefCell<Vec<HintsEventRecord>>>,
182 writer: BufWriter<File>,
183 skel: BpfSkel<'a>,
184 _open_object: Box<MaybeUninit<OpenObject>>,
185}
186
187impl HintsRecorder<'static> {
188 fn new(hints_path: PathBuf, hints_map_path: PathBuf, ring_sz: u32) -> Result<Self> {
189 let open_object = Box::new(MaybeUninit::uninit());
190 let open_object_ptr = Box::into_raw(open_object);
191
192 let open_object_ref: &'static mut MaybeUninit<OpenObject> =
193 unsafe { &mut *open_object_ptr };
194
195 let builder = BpfSkelBuilder::default();
196 let mut open_skel = builder
197 .open(open_object_ref)
198 .context("failed to open BPF skeleton")?;
199
200 open_skel
201 .maps
202 .ringbuf
203 .set_max_entries(ring_sz * 1024 * 1024)
204 .context("failed to set ringbuf size")?;
205
206 let hints_map = MapHandle::from_pinned_path(&hints_map_path).with_context(|| {
207 format!(
208 "failed to open pinned hints map '{}'",
209 hints_map_path.display()
210 )
211 })?;
212 let hints_map_id = hints_map
213 .info()
214 .context("failed to query hints map info")?
215 .info
216 .id;
217 open_skel
218 .maps
219 .bss_data
220 .as_mut()
221 .context("missing BPF bss data")?
222 .hints_bss
223 .target_map_id = hints_map_id;
224
225 let skel = open_skel.load().context("failed to load BPF skeleton")?;
226 let link = skel
227 .progs
228 .trace_map_update
229 .attach()
230 .context("failed to attach BPF program")?;
231
232 let events: Rc<RefCell<Vec<HintsEventRecord>>> = Rc::new(RefCell::new(Vec::new()));
233 let events_clone = events.clone();
234
235 let mut ringbuf_builder = RingBufferBuilder::new();
236 ringbuf_builder
237 .add(&skel.maps.ringbuf, move |data: &[u8]| {
238 if data.len() >= std::mem::size_of::<hints_event>() {
239 let ev: hints_event =
240 unsafe { std::ptr::read_unaligned(data.as_ptr() as *const hints_event) };
241 events_clone.borrow_mut().push(HintsEventRecord {
242 pid: ev.pid,
243 tgid: ev.tgid,
244 hints: ev.hints,
245 timestamp: ev.timestamp,
246 });
247 }
248 0
249 })
250 .context("failed to add ringbuf callback")?;
251
252 let ringbuf = ringbuf_builder
253 .build()
254 .context("failed to build ring buffer")?;
255
256 let file = File::create(&hints_path).context("failed to create hints output file")?;
257 let writer = BufWriter::new(file);
258
259 Ok(Self {
260 link: Some(link),
261 ringbuf,
262 events,
263 writer,
264 skel,
265 _open_object: unsafe { Box::from_raw(open_object_ptr) },
266 })
267 }
268
269 fn ringbuf_fd(&self) -> RawFd {
270 self.ringbuf.epoll_fd()
271 }
272
273 fn consume_and_write(&mut self) -> Result<()> {
274 self.ringbuf
275 .consume()
276 .context("failed to consume ringbuf")?;
277 let events: Vec<_> = self.events.borrow_mut().drain(..).collect();
278 for ev in &events {
279 let json = serde_json::to_string(ev).context("failed to serialize event")?;
280 writeln!(self.writer, "{}", json)?;
281 }
282 self.writer.flush()?;
283 Ok(())
284 }
285}
286
287impl Drop for HintsRecorder<'_> {
288 fn drop(&mut self) {
289 self.link.take();
290 let _ = self.ringbuf.consume();
291 let events: Vec<_> = self.events.borrow_mut().drain(..).collect();
292 for ev in &events {
293 if let Ok(json) = serde_json::to_string(ev) {
294 let _ = writeln!(self.writer, "{}", json);
295 }
296 }
297 let _ = self.writer.flush();
298
299 let dropped = self
300 .skel
301 .maps
302 .bss_data
303 .as_ref()
304 .map(|bss| bss.hints_bss.dropped_events)
305 .unwrap_or(0);
306 if dropped > 0 {
307 eprintln!(
308 "warning: {} hints events were dropped due to full ring buffer, \
309 consider increasing --hints-map-ring-sz",
310 dropped
311 );
312 }
313 }
314}
315
316#[derive(Debug, Clone, Copy, PartialEq)]
317enum PollResult {
318 Shutdown,
319 ProcessExited(usize),
320 RingbufReady,
321 Timeout,
322}
323
324fn poll_fds(
325 shutdown_fd: RawFd,
326 process_fds: &[RawFd],
327 ringbuf_fd: Option<RawFd>,
328 timeout_ms: i32,
329) -> Result<PollResult> {
330 let mut pollfds = Vec::with_capacity(1 + process_fds.len() + usize::from(ringbuf_fd.is_some()));
331 pollfds.push(libc::pollfd {
332 fd: shutdown_fd,
333 events: libc::POLLIN,
334 revents: 0,
335 });
336 pollfds.extend(process_fds.iter().map(|&fd| libc::pollfd {
337 fd,
338 events: libc::POLLIN,
339 revents: 0,
340 }));
341 if let Some(fd) = ringbuf_fd {
342 pollfds.push(libc::pollfd {
343 fd,
344 events: libc::POLLIN,
345 revents: 0,
346 });
347 }
348
349 let ret = unsafe {
350 libc::poll(
351 pollfds.as_mut_ptr(),
352 pollfds.len() as libc::nfds_t,
353 timeout_ms,
354 )
355 };
356
357 if ret < 0 {
358 let err = std::io::Error::last_os_error();
359 if err.raw_os_error() == Some(libc::EINTR) {
360 return Ok(PollResult::Shutdown);
361 }
362 bail!("poll failed: {}", err);
363 }
364
365 if ret == 0 {
366 return Ok(PollResult::Timeout);
367 }
368
369 if pollfds[0].revents & libc::POLLIN != 0 {
370 return Ok(PollResult::Shutdown);
371 }
372 for (idx, pollfd) in pollfds[1..=process_fds.len()].iter().enumerate() {
373 if pollfd.revents & libc::POLLIN != 0 {
374 return Ok(PollResult::ProcessExited(idx));
375 }
376 }
377 if ringbuf_fd.is_some() && pollfds.last().unwrap().revents & libc::POLLIN != 0 {
378 return Ok(PollResult::RingbufReady);
379 }
380
381 Ok(PollResult::Timeout)
382}
383
384fn build_sched_perf_args(sched_data_path: &Path) -> Vec<String> {
385 let mut sched_perf_args = vec![
386 perf_binary(),
387 "record".to_string(),
388 "-a".to_string(),
389 "-k".to_string(),
390 PERF_SCHED_CLOCKID.to_string(),
391 "-m".to_string(),
392 DEFAULT_PERF_MMAP_SIZE.to_string(),
393 "-o".to_string(),
394 sched_data_path.to_string_lossy().to_string(),
395 ];
396 for event in SCHED_TRACE_EVENTS {
397 sched_perf_args.push("-e".to_string());
398 sched_perf_args.push((*event).to_string());
399 }
400 sched_perf_args
401}
402
403fn stop_hints_recorder(hints_recorder: &mut Option<HintsRecorder<'static>>) {
404 if let Some(recorder) = hints_recorder.take() {
405 drop(recorder);
406 }
407}
408
409pub fn cmd_record(ctx: &Context, opts: RecordOpts) -> Result<()> {
410 if opts.output.exists() {
411 bail!(
412 "output directory '{}' already exists",
413 opts.output.display()
414 );
415 }
416
417 if opts.disable_archive && opts.file.is_some() {
418 bail!("--file cannot be used with --disable-archive");
419 }
420
421 fs::create_dir_all(&opts.output).context("failed to create output directory")?;
422
423 save_perf_version(&opts.output)?;
424
425 let completed = match run_recording(ctx, &opts) {
426 Ok(completed) => completed,
427 Err(e) => {
428 let _ = fs::remove_dir_all(&opts.output);
429 return Err(e);
430 }
431 };
432
433 if !completed {
434 let _ = fs::remove_dir_all(&opts.output);
435 return Ok(());
436 }
437
438 if opts.enable_perf_script {
439 println!("Generating perf.mem.script...");
440 if let Err(e) = generate_perf_script(
441 ctx,
442 &opts.output.join(PERF_MEM_DATA_FILE),
443 &opts.output.join(PERF_MEM_SCRIPT_FILE),
444 PERF_MEM_SCRIPT_FIELDS,
445 ) {
446 eprintln!("warning: failed to generate perf.mem.script: {}", e);
447 }
448
449 if !opts.disable_sched_trace && opts.output.join(PERF_SCHED_DATA_FILE).exists() {
450 println!("Generating perf.sched.script...");
451 if let Err(e) = generate_perf_script(
452 ctx,
453 &opts.output.join(PERF_SCHED_DATA_FILE),
454 &opts.output.join(PERF_SCHED_SCRIPT_FILE),
455 PERF_SCHED_SCRIPT_FIELDS,
456 ) {
457 eprintln!("warning: failed to generate perf.sched.script: {}", e);
458 }
459 }
460 }
461
462 if !opts.disable_archive {
463 let archive_path = opts
464 .file
465 .clone()
466 .unwrap_or_else(|| PathBuf::from(format!("{}.tar.gz", opts.output.display())));
467 create_archive(&opts.output, &archive_path)?;
468 fs::remove_dir_all(&opts.output).context("failed to remove output directory")?;
469 }
470
471 Ok(())
472}
473
474fn run_recording(ctx: &Context, opts: &RecordOpts) -> Result<bool> {
475 let hints_trace_enabled = opts.hints_map.is_some();
476 if opts.disable_mem_trace && opts.disable_sched_trace && !hints_trace_enabled {
477 bail!("at least one of mem trace, sched trace, or hints trace must be enabled");
478 }
479
480 let mut processes = Vec::new();
481
482 if !opts.disable_mem_trace {
483 let perf_data_path = opts.output.join(PERF_MEM_DATA_FILE);
484 let mem_perf_args = vec![
485 perf_binary(),
486 "mem".to_string(),
487 "record".to_string(),
488 "--all-cgroups".to_string(),
489 "-p".to_string(),
490 "--data-page-size".to_string(),
491 "--ldlat".to_string(),
492 opts.ldlat.to_string(),
493 "-m".to_string(),
494 format!("{0},{0}", DEFAULT_PERF_MMAP_SIZE),
495 "-o".to_string(),
496 perf_data_path.to_string_lossy().to_string(),
497 ];
498 processes.push(SpawnedProcess::spawn(&mem_perf_args)?);
499 }
500
501 if !opts.disable_sched_trace {
502 let sched_data_path = opts.output.join(PERF_SCHED_DATA_FILE);
503 let sched_perf_args = build_sched_perf_args(&sched_data_path);
504 processes.push(SpawnedProcess::spawn(&sched_perf_args)?);
505 }
506
507 let hints_recorder = if opts.hints_map.is_some() {
508 let hints_path = opts.output.join("hints.jsonl");
509 Some(HintsRecorder::new(
510 hints_path,
511 opts.hints_map.clone().unwrap(),
512 opts.hints_map_ring_sz,
513 )?)
514 } else {
515 None
516 };
517
518 let mut hints_recorder = hints_recorder;
519 let timeout = Duration::from_secs(opts.timeout);
520 let start = Instant::now();
521 let mut completed = true;
522
523 loop {
524 let process_fds: Vec<_> = processes.iter().map(SpawnedProcess::pidfd).collect();
525 let ringbuf_fd = hints_recorder.as_ref().map(HintsRecorder::ringbuf_fd);
526 let elapsed = start.elapsed();
527 if elapsed >= timeout {
528 stop_hints_recorder(&mut hints_recorder);
534 for process in &processes {
535 process.signal(libc::SIGINT);
536 }
537 for process in &mut processes {
538 process.wait()?;
539 }
540 break;
541 }
542
543 let remaining_ms = (timeout - elapsed).as_millis().min(100) as i32;
544
545 match poll_fds(ctx.shutdown_fd(), &process_fds, ringbuf_fd, remaining_ms)? {
546 PollResult::Shutdown => {
547 stop_hints_recorder(&mut hints_recorder);
548 for process in &processes {
549 process.signal(libc::SIGKILL);
550 }
551 for process in &mut processes {
552 process.wait()?;
553 }
554 completed = false;
555 break;
556 }
557 PollResult::ProcessExited(exited_idx) => {
558 stop_hints_recorder(&mut hints_recorder);
559 let exited_result = processes[exited_idx].wait();
560 for (idx, process) in processes.iter().enumerate() {
561 if idx != exited_idx {
562 process.signal(libc::SIGINT);
563 }
564 }
565 for (idx, process) in processes.iter_mut().enumerate() {
566 if idx != exited_idx {
567 process.wait()?;
568 }
569 }
570 exited_result?;
571 break;
572 }
573 PollResult::RingbufReady | PollResult::Timeout => {
574 if let Some(ref mut recorder) = hints_recorder {
575 recorder.consume_and_write()?;
576 }
577 }
578 }
579 }
580
581 drop(hints_recorder);
582
583 Ok(completed)
584}
585
586fn save_perf_version(output_dir: &Path) -> Result<()> {
587 let version_path = output_dir.join("perf.version");
588
589 let output = Command::new(perf_binary())
590 .arg("version")
591 .output()
592 .context("failed to run perf version")?;
593
594 if !output.status.success() {
595 bail!("perf version failed");
596 }
597
598 fs::write(&version_path, &output.stdout).context("failed to write perf.version")?;
599
600 Ok(())
601}
602
603fn create_archive(output_dir: &Path, archive_path: &Path) -> Result<()> {
604 let archive_root = archive_root_name(archive_path)?;
605 let dir_name = output_dir
606 .file_name()
607 .context("invalid output directory name")?;
608
609 let parent = output_dir.parent().unwrap_or(std::path::Path::new("."));
610 let parent = if parent.as_os_str().is_empty() {
611 std::path::Path::new(".")
612 } else {
613 parent
614 };
615
616 let status = Command::new("tar")
617 .args([
618 "-czf",
619 archive_path
620 .to_str()
621 .context("invalid archive output path")?,
622 "-C",
623 parent.to_str().context("invalid parent path")?,
624 "--transform",
625 &format!("s,^{},{},", dir_name.to_string_lossy(), archive_root),
626 dir_name.to_str().context("invalid directory name")?,
627 ])
628 .status()
629 .context("failed to run tar")?;
630
631 if !status.success() {
632 bail!("tar failed with status: {}", status);
633 }
634
635 Ok(())
636}
637
638fn archive_root_name(archive_path: &Path) -> Result<String> {
639 let file_name = archive_path
640 .file_name()
641 .context("invalid archive output path")?
642 .to_string_lossy();
643
644 let archive_root = file_name
645 .strip_suffix(".tar.gz")
646 .context("archive path must end with .tar.gz")?;
647
648 if archive_root.is_empty() {
649 bail!("archive path must include a basename before .tar.gz");
650 }
651
652 Ok(archive_root.to_string())
653}
654
655pub(crate) fn perf_script_output_exists(output_len: u64) -> bool {
656 output_len > 0
657}
658
659pub(crate) fn report_perf_script_stderr(context: &str, stderr: &str) {
660 let stderr = stderr.trim();
661 if !stderr.is_empty() {
662 eprintln!("warning: perf script reported diagnostics for {context}:\n{stderr}");
663 }
664}
665
666#[cfg(test)]
667mod tests {
668 use super::*;
669
670 #[test]
671 fn sched_perf_args_use_explicit_monotonic_clock() {
672 let args = build_sched_perf_args(Path::new("/tmp/perf.sched.data"));
673
674 assert!(
675 args.windows(2)
676 .any(|window| window == ["-k", PERF_SCHED_CLOCKID]),
677 "expected perf record args to include an explicit clockid"
678 );
679 assert!(
680 args.windows(2)
681 .any(|window| window == ["-o", "/tmp/perf.sched.data"]),
682 "expected perf record args to include the output path"
683 );
684 for event in SCHED_TRACE_EVENTS {
685 assert!(
686 args.windows(2).any(|window| window == ["-e", *event]),
687 "missing sched trace event {event}"
688 );
689 }
690 }
691
692 #[test]
693 fn perf_script_output_exists_requires_nonempty_file() {
694 assert!(perf_script_output_exists(1));
695 assert!(perf_script_output_exists(4096));
696 assert!(!perf_script_output_exists(0));
697 }
698}
699
700pub const PERF_MEM_SCRIPT_FIELDS: &str =
702 "comm,tid,pid,time,cgroup,ip,addr,phys_addr,data_page_size,dso,sym";
703
704pub const PERF_SCHED_SCRIPT_FIELDS: &str = "comm,pid,tid,cpu,time,event,trace";
706
707fn generate_perf_script(
708 ctx: &Context,
709 perf_data_path: &Path,
710 perf_script_path: &Path,
711 fields: &str,
712) -> Result<()> {
713 if !perf_data_path.exists() {
714 bail!("perf data file '{}' not found", perf_data_path.display());
715 }
716
717 let output_file = File::create(&perf_script_path)
718 .with_context(|| format!("failed to create {}", perf_script_path.display()))?;
719
720 let child = Command::new(perf_binary())
721 .args([
722 "script",
723 "-F",
724 fields,
725 "-i",
726 perf_data_path.to_str().context("invalid perf.data path")?,
727 ])
728 .stdout(output_file)
729 .stderr(Stdio::piped())
730 .spawn()
731 .context("failed to spawn perf script")?;
732
733 let pid = child.id() as i32;
734 let fd = unsafe { libc::syscall(libc::SYS_pidfd_open, pid, 0) } as RawFd;
735 if fd < 0 {
736 bail!("pidfd_open failed: {}", std::io::Error::last_os_error());
737 }
738 let pidfd = unsafe { OwnedFd::from_raw_fd(fd) };
739
740 let mut child = child;
741
742 loop {
743 match poll_fds(ctx.shutdown_fd(), &[pidfd.as_raw_fd()], None, 100)? {
744 PollResult::Shutdown => {
745 unsafe { libc::kill(pid, libc::SIGKILL) };
746 let _ = child.wait();
747 let _ = fs::remove_file(&perf_script_path);
748 bail!("perf script interrupted");
749 }
750 PollResult::ProcessExited(_) => {
751 let output = child
752 .wait_with_output()
753 .context("failed to wait for perf script")?;
754 let stderr = String::from_utf8_lossy(&output.stderr);
755 let output_len = fs::metadata(perf_script_path)
756 .map(|meta| meta.len())
757 .unwrap_or(0);
758 if !output.status.success() {
759 if perf_script_output_exists(output_len) {
760 eprintln!(
761 "warning: perf script exited with status {} after writing {} bytes to {}. Continuing with the generated output because the output file is non-empty.",
762 output.status,
763 output_len,
764 perf_script_path.display()
765 );
766 report_perf_script_stderr(&perf_script_path.display().to_string(), &stderr);
767 break;
768 }
769 let _ = fs::remove_file(&perf_script_path);
770 let stderr = stderr.trim();
771 if stderr.is_empty() {
772 bail!("perf script failed with status: {}", output.status);
773 }
774 bail!(
775 "perf script failed with status: {}: {}",
776 output.status,
777 stderr
778 );
779 }
780 report_perf_script_stderr(&perf_script_path.display().to_string(), &stderr);
781 break;
782 }
783 PollResult::RingbufReady | PollResult::Timeout => {}
784 }
785 }
786
787 Ok(())
788}