Skip to main content

scx_characterize/
record.rs

1// Copyright (c) 2026 Meta Platforms, Inc. and affiliates.
2//
3// This software may be used and distributed according to the terms of the
4// GNU General Public License version 2.
5
6use 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    /// Output directory for recording
62    #[clap(short, long, default_value = "scx_characterize.out")]
63    pub output: PathBuf,
64
65    /// Output archive path for the recording
66    #[clap(short = 'f', long)]
67    pub file: Option<PathBuf>,
68
69    /// Recording duration in seconds
70    #[clap(short = 't', long, default_value = "30")]
71    pub timeout: u64,
72
73    /// Load latency threshold in cycles
74    #[clap(short = 'l', long, default_value = "10")]
75    pub ldlat: u32,
76
77    /// Path to the SCX scheduler's task hint map
78    #[clap(long)]
79    pub hints_map: Option<PathBuf>,
80
81    /// Size of the hints ring buffer in MB
82    #[clap(long, default_value = "8")]
83    pub hints_map_ring_sz: u32,
84
85    /// Disable creating a tar.gz archive after recording
86    #[clap(long)]
87    pub disable_archive: bool,
88
89    /// Generate perf.mem.script and perf.sched.script during recording
90    #[clap(long)]
91    pub enable_perf_script: bool,
92
93    /// Disable recording sched/irq trace events into perf.sched.data
94    #[clap(long)]
95    pub disable_sched_trace: bool,
96
97    /// Disable recording perf mem trace into perf.mem.data
98    #[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            /*
529             * Stop tracing hint updates before waiting for perf to flush and
530             * exit. Otherwise hints.jsonl keeps accumulating updates during
531             * perf teardown and no longer matches the perf capture window.
532             */
533            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
700/// Fields to extract from perf mem script output
701pub const PERF_MEM_SCRIPT_FIELDS: &str =
702    "comm,tid,pid,time,cgroup,ip,addr,phys_addr,data_page_size,dso,sym";
703
704/// Fields to extract from sched trace perf script output
705pub 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}