Skip to main content

scx_arena/
lib.rs

1// Copyright (c) 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
6//! # SCX Arena library setup utilities
7//!
8//! Crate for setting up the BPF arena library for sched-ext schedulers.
9
10mod bpf_skel;
11
12mod arenalib;
13pub use arenalib::ArenaLib;
14mod scx_libarena;
15pub use scx_libarena::ScxLibArena;
16
17use std::fs::File;
18use std::io::BufRead;
19use std::io::BufReader;
20use std::io::ErrorKind;
21
22#[cfg(feature = "build-support")]
23pub mod build_support;
24use std::os::fd::AsFd;
25use std::os::fd::AsRawFd;
26use std::os::fd::BorrowedFd;
27use std::os::fd::FromRawFd;
28use std::os::fd::OwnedFd;
29use std::time::Duration;
30use std::time::Instant;
31
32use anyhow::Context;
33use anyhow::Result;
34use anyhow::bail;
35use libbpf_rs::AsRawLibbpf as _;
36use libbpf_rs::MapCore as _;
37use libbpf_rs::libbpf_sys;
38
39/// Cacheline size assumed by the arena allocator's alignment parameter.
40/// Mirrors scheds/include/scx/const-defs.h, keep in sync.
41#[cfg(target_arch = "s390x")]
42pub const CACHELINE_SIZE: usize = 256;
43#[cfg(target_arch = "powerpc64")]
44pub const CACHELINE_SIZE: usize = 128;
45#[cfg(not(any(target_arch = "s390x", target_arch = "powerpc64")))]
46pub const CACHELINE_SIZE: usize = 64;
47
48const MEMBARRIER_CMD_GLOBAL: libc::c_long = 1;
49const URCU_DOORBELL: &str = "scx_urcu_doorbell";
50const URCU_MIN_INTERVAL: Duration = Duration::from_millis(1);
51/* the fallback grace period is expedited and IPIs every CPU in the kernel */
52const URCU_FALLBACK_INTERVAL: Duration = Duration::from_millis(100);
53/* membarrier retries under a hotplug storm before the fallback takes over */
54const URCU_MEMBARRIER_RETRIES: u32 = 4;
55
56/// One background thread and the eventfd that stops it. Dropping writes the
57/// eventfd and joins the thread.
58#[derive(Debug)]
59pub(crate) struct Daemon {
60    stop: OwnedFd,
61    thread: Option<std::thread::JoinHandle<()>>,
62}
63
64impl Drop for Daemon {
65    fn drop(&mut self) {
66        let one: u64 = 1;
67        let ret = unsafe {
68            libc::write(
69                self.stop.as_raw_fd(),
70                &one as *const u64 as *const libc::c_void,
71                std::mem::size_of::<u64>(),
72            )
73        };
74        /* on a failed wakeup, leak the thread rather than hang the join */
75        if ret != std::mem::size_of::<u64>() as isize {
76            return;
77        }
78        if let Some(thread) = self.thread.take() {
79            let _ = thread.join();
80        }
81    }
82}
83
84/// Create the eventfd a Daemon is stopped through.
85fn stop_eventfd() -> Result<OwnedFd> {
86    let fd = unsafe { libc::eventfd(0, libc::EFD_CLOEXEC) };
87    if fd < 0 {
88        bail!(
89            "creating daemon stop eventfd failed: {}",
90            std::io::Error::last_os_error()
91        );
92    }
93    Ok(unsafe { OwnedFd::from_raw_fd(fd) })
94}
95
96/// Duplicate @prog's fd, None for programs that were not loaded and thus have
97/// no fd, prog.as_fd() on those would construct a BorrowedFd from an invalid
98/// fd. Errors only when a loaded program's fd cannot be duplicated.
99fn prog_fd_clone(prog: &libbpf_rs::Program<'_>) -> Result<Option<OwnedFd>> {
100    let raw_fd = unsafe { libbpf_sys::bpf_program__fd(prog.as_libbpf_object().as_ptr()) };
101    if raw_fd < 0 {
102        return Ok(None);
103    }
104    let fd = unsafe { BorrowedFd::borrow_raw(raw_fd) }
105        .try_clone_to_owned()
106        .with_context(|| format!("cloning the fd of BPF prog {:?}", prog.name()))?;
107    Ok(Some(fd))
108}
109
110/// Userspace half of the scx_urcu machinery, see the BPF side in
111/// lib/sdt_alloc.bpf.c. ArenaLib::setup() spawns the daemon when the object
112/// carries the scx_urcu doorbell. The returned ArenaLib owns it and stops and
113/// joins it on drop, so nothing else is visible to the scheduler: its whole
114/// runtime surface is calling the scx_*_free_rcu() variants from its free
115/// path hooks.
116///
117/// The daemon sleeps on the doorbell and, when woken, waits an RCU grace
118/// period, membarrier(MEMBARRIER_CMD_GLOBAL) is synchronize_rcu(), and runs
119/// the lib-provided scx_urcu_<storage>_pending/reclaim driver programs until
120/// nothing is awaiting reclaim, one grace period per cycle shared across the
121/// storages and at least URCU_MIN_INTERVAL between the side flips. See
122/// UrcuGracePeriod for the cases membarrier cannot cover.
123fn urcu_run_prog(fd: &OwnedFd) -> Result<u32> {
124    let mut opts: libbpf_sys::bpf_test_run_opts = unsafe { std::mem::zeroed() };
125
126    opts.sz = std::mem::size_of::<libbpf_sys::bpf_test_run_opts>() as _;
127    let ret = unsafe { libbpf_sys::bpf_prog_test_run_opts(fd.as_raw_fd(), &mut opts) };
128    if ret != 0 {
129        bail!("urcu driver program run failed: {}", ret);
130    }
131    Ok(opts.retval)
132}
133
134/// membarrier(MEMBARRIER_CMD_GLOBAL) is synchronize_rcu() except in two cases:
135/// it fails with EINVAL when nohz_full is active and it returns without
136/// waiting when only one CPU is online. Both fall back to updating a private
137/// map-in-map slot, which the kernel documents as waiting for the running
138/// non-sleepable programs. That wait is an expedited grace period and IPIs
139/// every CPU in the kernel, so the daemon spaces those rounds out.
140struct UrcuGracePeriod {
141    membarrier: bool,
142    outer: libbpf_rs::MapHandle,
143    inner: libbpf_rs::MapHandle,
144    warned: bool,
145}
146
147fn membarrier_global() -> std::io::Result<()> {
148    let ret = unsafe { libc::syscall(libc::SYS_membarrier, MEMBARRIER_CMD_GLOBAL, 0, 0) };
149    if ret != 0 {
150        return Err(std::io::Error::last_os_error());
151    }
152    Ok(())
153}
154
155/* bumped by the kernel for every CPU online and offline transition */
156fn hotplug_seq() -> Result<u64> {
157    let seq = std::fs::read_to_string("/sys/kernel/sched_ext/hotplug_seq")
158        .context("reading /sys/kernel/sched_ext/hotplug_seq")?;
159    seq.trim()
160        .parse()
161        .with_context(|| format!("parsing hotplug_seq {:?}", seq))
162}
163
164fn online_cpus() -> Result<libc::c_long> {
165    let ret = unsafe { libc::sysconf(libc::_SC_NPROCESSORS_ONLN) };
166    if ret < 0 {
167        bail!(
168            "reading the online CPU count failed: {}",
169            std::io::Error::last_os_error()
170        );
171    }
172    Ok(ret)
173}
174
175impl UrcuGracePeriod {
176    fn new() -> Result<Self> {
177        let membarrier = match membarrier_global() {
178            Ok(()) => true,
179            Err(e) if e.raw_os_error() == Some(libc::EINVAL) => false,
180            Err(e) => bail!("membarrier(GLOBAL) failed: {}", e),
181        };
182
183        let opts = libbpf_sys::bpf_map_create_opts {
184            sz: std::mem::size_of::<libbpf_sys::bpf_map_create_opts>() as _,
185            ..Default::default()
186        };
187        let inner = libbpf_rs::MapHandle::create(
188            libbpf_rs::MapType::Array,
189            Some("scx_urcu_inner"),
190            4,
191            4,
192            1,
193            &opts,
194        )
195        .context("creating urcu inner map")?;
196        let opts = libbpf_sys::bpf_map_create_opts {
197            inner_map_fd: inner.as_fd().as_raw_fd() as u32,
198            ..opts
199        };
200        let outer = libbpf_rs::MapHandle::create(
201            libbpf_rs::MapType::ArrayOfMaps,
202            Some("scx_urcu_outer"),
203            4,
204            4,
205            1,
206            &opts,
207        )
208        .context("creating urcu outer map")?;
209        let gp = Self {
210            membarrier,
211            outer,
212            inner,
213            warned: false,
214        };
215        gp.map_update()
216            .context("probing urcu map-update grace period")?;
217        Ok(gp)
218    }
219
220    fn map_update(&self) -> Result<()> {
221        /*
222         * A successful map-in-map update waits for the running non-sleepable
223         * programs, see maybe_wait_bpf_programs(), even when the slot keeps the
224         * same inner map.
225         */
226        self.outer
227            .update(
228                &0u32.to_ne_bytes(),
229                &self.inner.as_fd().as_raw_fd().to_ne_bytes(),
230                libbpf_rs::MapFlags::ANY,
231            )
232            .context("waiting for urcu map-update grace period")
233    }
234
235    /// Wait for a grace period. Returns whether the expedited fallback was used.
236    fn synchronize(&mut self) -> Result<bool> {
237        if self.membarrier {
238            /*
239             * membarrier(GLOBAL) does not wait when only one CPU is online, so
240             * the count is checked before and after the call and the hotplug
241             * counter is compared across it. That still has a hole because the
242             * counter moves at the active transitions, not when the online mask
243             * changes: a CPU going offline bumps it before leaving the mask, a
244             * CPU coming online after entering. Example with A and B online
245             * while B goes offline and C comes online:
246             *
247             *  1. B bumps the counter, still in the mask.
248             *  2. Read count 2 and the counter.
249             *  3. B leaves the mask, one CPU online.
250             *  4. membarrier() sees one CPU and returns without waiting.
251             *  5. Descheduled. B finishes, C starts and enters the mask.
252             *  6. Read count 2 and the counter, unchanged. Trusted.
253             *  7. C bumps the counter.
254             *
255             * That needs this thread descheduled mid-call during two
256             * back-to-back hotplugs. A moved counter with CPUs left means a
257             * hotplug raced the call, so retry. This is best effort: the hole
258             * stays open for a scheduler that runs through hotplug, and for a
259             * restarting one until its asynchronous disable completes.
260             */
261            for _ in 0..URCU_MEMBARRIER_RETRIES {
262                if online_cpus()? <= 1 {
263                    break;
264                }
265                let seq = hotplug_seq()?;
266
267                membarrier_global().context("membarrier(GLOBAL) failed")?;
268                if online_cpus()? <= 1 {
269                    break;
270                }
271                if hotplug_seq()? == seq {
272                    return Ok(false);
273                }
274            }
275        }
276
277        if !self.warned {
278            self.warned = true;
279            eprintln!(
280                "scx-urcu: {}, waiting for grace periods through expedited map-in-map updates at least {:?} apart",
281                if self.membarrier {
282                    "a single CPU is online"
283                } else {
284                    "membarrier is unavailable with nohz_full"
285                },
286                URCU_FALLBACK_INTERVAL
287            );
288        }
289        self.map_update()?;
290        Ok(true)
291    }
292}
293
294fn urcu_daemon(
295    stop: OwnedFd,
296    doorbell: libbpf_rs::MapHandle,
297    pairs: Vec<(OwnedFd, OwnedFd)>,
298    mut gp: UrcuGracePeriod,
299) -> Result<()> {
300    let mut builder = libbpf_rs::RingBufferBuilder::new();
301    builder
302        .add(&doorbell, |_| 0)
303        .context("adding urcu doorbell to ring buffer")?;
304    let rb = builder
305        .build()
306        .context("building urcu doorbell ring buffer")?;
307
308    let mut last: Option<Instant> = None;
309    let mut interval = URCU_MIN_INTERVAL;
310    loop {
311        let mut fds = [
312            libc::pollfd {
313                fd: doorbell.as_fd().as_raw_fd(),
314                events: libc::POLLIN,
315                revents: 0,
316            },
317            libc::pollfd {
318                fd: stop.as_raw_fd(),
319                events: libc::POLLIN,
320                revents: 0,
321            },
322        ];
323
324        let ret = unsafe { libc::poll(fds.as_mut_ptr(), 2, -1) };
325        if ret < 0 {
326            let err = std::io::Error::last_os_error();
327            if err.raw_os_error() == Some(libc::EINTR) {
328                continue;
329            }
330            bail!("urcu doorbell poll failed: {}", err);
331        }
332
333        /* run one final drain below before honoring a stop request */
334        let stopping = fds[1].revents != 0;
335
336        /* drain until nothing is awaiting reclaim, unpaced once stopping */
337        loop {
338            let pace = if stopping { Duration::ZERO } else { interval };
339            if let Some(last) = last {
340                let elapsed = last.elapsed();
341                if elapsed < pace {
342                    std::thread::sleep(pace - elapsed);
343                }
344            }
345
346            /*
347             * Consume before the pending checks. A free landing afterwards is
348             * either seen by the checks or leaves its ring behind and the poll
349             * returns immediately. The reverse order can consume the ring of a
350             * free the checks missed and sleep on it.
351             */
352            rb.consume().context("consuming urcu doorbell")?;
353
354            let mut reclaims = Vec::new();
355            for (pending, reclaim) in &pairs {
356                if urcu_run_prog(pending)? != 0 {
357                    reclaims.push(reclaim);
358                }
359            }
360            if reclaims.is_empty() {
361                break;
362            }
363            last = Some(Instant::now());
364
365            interval = if gp.synchronize()? {
366                URCU_FALLBACK_INTERVAL
367            } else {
368                URCU_MIN_INTERVAL
369            };
370
371            for reclaim in reclaims {
372                while urcu_run_prog(reclaim)? != 0 {}
373            }
374        }
375
376        if stopping {
377            return Ok(());
378        }
379    }
380}
381
382/// Spawn the urcu reclaim daemon if @obj carries the scx_urcu doorbell and
383/// driver programs. Called from ArenaLib::setup(), the daemon is owned by
384/// the returned ArenaLib.
385pub(crate) fn urcu_spawn(obj: &libbpf_rs::Object) -> Result<Option<Daemon>> {
386    let Some(doorbell) = obj.maps().find(|m| m.name() == URCU_DOORBELL) else {
387        return Ok(None);
388    };
389    let doorbell =
390        libbpf_rs::MapHandle::try_from(&doorbell).context("cloning urcu doorbell handle")?;
391
392    let mut pairs = Vec::new();
393    for prog in obj.progs() {
394        let Some(name) = prog.name().to_str() else {
395            continue;
396        };
397        let Some(base) = name.strip_suffix("_pending") else {
398            continue;
399        };
400        if !name.starts_with("scx_urcu_") {
401            continue;
402        }
403
404        let reclaim_name = format!("{}_reclaim", base);
405        let reclaim = obj
406            .progs()
407            .find(|p| p.name() == reclaim_name.as_str())
408            .with_context(|| format!("urcu driver program {} not found", reclaim_name))?;
409
410        let Some(pending_fd) = prog_fd_clone(&prog)? else {
411            bail!("urcu driver program {} is not loaded", name);
412        };
413        let Some(reclaim_fd) = prog_fd_clone(&reclaim)? else {
414            bail!("urcu driver program {} is not loaded", reclaim_name);
415        };
416        pairs.push((pending_fd, reclaim_fd));
417    }
418    if pairs.is_empty() {
419        return Ok(None);
420    }
421
422    let gp = UrcuGracePeriod::new().context("setting up urcu grace periods")?;
423    let stop = stop_eventfd()?;
424    let daemon_stop = stop.try_clone().context("cloning urcu stop eventfd")?;
425    let thread = std::thread::Builder::new()
426        .name("scx-urcu".into())
427        .spawn(move || {
428            if let Err(e) = urcu_daemon(daemon_stop, doorbell, pairs, gp) {
429                let _ = std::io::Write::write_fmt(
430                    &mut std::io::stderr(),
431                    format_args!("FATAL: scx-urcu daemon failed: {:#}\n", e),
432                );
433                std::process::exit(1);
434            }
435        })
436        .context("spawning urcu daemon thread")?;
437
438    Ok(Some(Daemon {
439        stop,
440        thread: Some(thread),
441    }))
442}
443
444const BPF_STDOUT: u32 = 1;
445const BPF_STDERR: u32 = 2;
446const BPF_STREAMS: [(u32, &str); 2] = [(BPF_STDOUT, "stdout"), (BPF_STDERR, "stderr")];
447const STREAM_POLL_INTERVAL: Duration = Duration::from_secs(1);
448
449fn stream_read(fd: &OwnedFd, stream_id: u32, buf: &mut [u8]) -> i32 {
450    unsafe {
451        libbpf_sys::bpf_prog_stream_read(
452            fd.as_raw_fd(),
453            stream_id,
454            buf.as_mut_ptr() as *mut _,
455            buf.len() as u32,
456            std::ptr::null_mut(),
457        )
458    }
459}
460
461/// The kernel prefixes the errors it reports on a program's BPF stderr
462/// stream with "ERROR: ".
463fn stream_is_fatal(lines: &[String]) -> bool {
464    lines.iter().any(|l| l.starts_with("ERROR: "))
465}
466
467/// Split the complete lines out of @carry, keeping a trailing partial for the
468/// next read. Stream elements are concatenated without separators and reads
469/// split lines arbitrarily, so prefixes may only be matched on complete lines.
470/// @new_data says whether this poll read anything: a partial with no
471/// continuation after a full poll interval is flushed as a line of its own.
472fn stream_extract_lines(carry: &mut String, new_data: bool) -> Vec<String> {
473    let mut lines: Vec<String> = Vec::new();
474
475    if let Some(pos) = carry.rfind('\n') {
476        lines = carry[..pos].split('\n').map(str::to_string).collect();
477        carry.drain(..=pos);
478    }
479    if !new_data && !carry.is_empty() {
480        lines.push(std::mem::take(carry));
481    }
482
483    lines
484}
485
486/// Forward a batch of complete lines from one stream to the matching standard
487/// stream of the scheduler, and abort when the kernel reports an error on a
488/// stderr stream. Lines prefixed "IGN: " are dropped instead of
489/// forwarded, for prints with side effects that nobody needs to see, the arena
490/// association print in scx_arena_subprog_init() for example.
491fn stream_forward(name: &str, stream_id: u32, label: &str, lines: &[String]) {
492    let kept: Vec<&str> = lines
493        .iter()
494        .map(String::as_str)
495        .filter(|l| !l.starts_with("IGN: "))
496        .collect();
497    if !kept.is_empty() {
498        let msg = kept.join("\n");
499        let out = format!("BPF {} of prog {}:\n{}\n", label, name, msg);
500        if stream_id == BPF_STDOUT {
501            print!("{}", out);
502            let _ = std::io::Write::flush(&mut std::io::stdout());
503        } else {
504            eprint!("{}", out);
505        }
506    }
507    if stream_id == BPF_STDERR && stream_is_fatal(lines) {
508        eprintln!("FATAL: aborting on BPF error report");
509        std::process::exit(1);
510    }
511}
512
513/// A stream opened as a descriptor. `reader` splits the lines and `pending`
514/// carries the partial line a read ends in over to the next read.
515struct StreamFd {
516    name: String,
517    stream_id: u32,
518    label: &'static str,
519    reader: BufReader<File>,
520    pending: Vec<u8>,
521    eof: bool,
522}
523
524impl StreamFd {
525    /// Forward what the descriptor holds. EOF comes once the program is gone
526    /// and the buffered data is out. A partial line is flushed at EOF and on
527    /// `last`, the final read before the watcher stops.
528    fn read_lines(&mut self, last: bool) {
529        let mut lines = Vec::new();
530
531        loop {
532            match self.reader.read_until(b'\n', &mut self.pending) {
533                Ok(0) => self.eof = true,
534                Ok(_) if self.pending.ends_with(b"\n") => {
535                    self.pending.pop();
536                    lines.push(String::from_utf8_lossy(&self.pending).into_owned());
537                    self.pending.clear();
538                    continue;
539                }
540                /* only EOF returns without the delimiter */
541                Ok(_) => self.eof = true,
542                Err(e) if e.kind() == ErrorKind::WouldBlock => {}
543                Err(e) => {
544                    eprintln!("reading BPF {} of prog {}: {}", self.label, self.name, e);
545                    self.eof = true;
546                }
547            }
548            break;
549        }
550        if (self.eof || last) && !self.pending.is_empty() {
551            lines.push(String::from_utf8_lossy(&self.pending).into_owned());
552            self.pending.clear();
553        }
554        if !lines.is_empty() {
555            stream_forward(&self.name, self.stream_id, self.label, &lines);
556        }
557    }
558}
559
560/// poll(2) the stream descriptors and the stop eventfd. A stream is dropped at
561/// EOF and a stop request gets one last read of every stream.
562fn stream_watch_fds(mut streams: Vec<StreamFd>, stop: OwnedFd) {
563    while !streams.is_empty() {
564        let mut fds: Vec<libc::pollfd> = std::iter::once(stop.as_raw_fd())
565            .chain(streams.iter().map(|s| s.reader.get_ref().as_raw_fd()))
566            .map(|fd| libc::pollfd {
567                fd,
568                events: libc::POLLIN,
569                revents: 0,
570            })
571            .collect();
572        let ret = unsafe { libc::poll(fds.as_mut_ptr(), fds.len() as libc::nfds_t, -1) };
573        if ret < 0 {
574            let err = std::io::Error::last_os_error();
575            if err.kind() == ErrorKind::Interrupted {
576                continue;
577            }
578            eprintln!("BPF stream watcher stopping, poll failed: {}", err);
579            break;
580        }
581
582        let stopping = fds[0].revents != 0;
583        for (i, stream) in streams.iter_mut().enumerate() {
584            if stopping || fds[i + 1].revents != 0 {
585                stream.read_lines(stopping);
586            }
587        }
588        streams.retain(|s| !s.eof);
589        if stopping {
590            break;
591        }
592    }
593}
594
595/// Read every stream once per STREAM_POLL_INTERVAL through the read command
596/// and assemble the lines here, for kernels without stream descriptors.
597fn stream_watch_periodic(progs: Vec<(String, OwnedFd)>, stop: OwnedFd) {
598    let mut buf = vec![0u8; 65536];
599    let mut carries: Vec<[String; 2]> = (0..progs.len())
600        .map(|_| [String::new(), String::new()])
601        .collect();
602
603    loop {
604        let mut fds = [libc::pollfd {
605            fd: stop.as_raw_fd(),
606            events: libc::POLLIN,
607            revents: 0,
608        }];
609        let ret =
610            unsafe { libc::poll(fds.as_mut_ptr(), 1, STREAM_POLL_INTERVAL.as_millis() as i32) };
611        /* run one final scan below before honoring a stop request */
612        let stopping = if ret < 0 {
613            std::io::Error::last_os_error().raw_os_error() != Some(libc::EINTR)
614        } else {
615            ret > 0
616        };
617
618        for (pidx, (name, fd)) in progs.iter().enumerate() {
619            for (sidx, (stream_id, label)) in BPF_STREAMS.iter().enumerate() {
620                let carry = &mut carries[pidx][sidx];
621                let mut new_data = false;
622
623                /* drain fully, a backlog can exceed the buffer */
624                loop {
625                    let n = stream_read(fd, *stream_id, &mut buf);
626                    if n <= 0 {
627                        break;
628                    }
629                    carry.push_str(&String::from_utf8_lossy(&buf[..n as usize]));
630                    new_data = true;
631                    if (n as usize) < buf.len() {
632                        break;
633                    }
634                }
635
636                /* the final scan flushes a trailing partial like the fd path */
637                let lines = stream_extract_lines(carry, new_data && !stopping);
638                if !lines.is_empty() {
639                    stream_forward(name, *stream_id, label, &lines);
640                }
641            }
642        }
643
644        if stopping {
645            break;
646        }
647    }
648}
649
650/// Forward the BPF stdout/stderr streams of every program in the object to
651/// the scheduler's stdout and stderr. Nothing reads these streams otherwise,
652/// so messages would be silently dropped. On a kernel with stream descriptors
653/// the watcher poll(2)s them and forwards each line as it is written.
654/// Elsewhere it reads the streams periodically. Called from ArenaLib::setup(),
655/// the watcher is owned by the returned ArenaLib.
656pub(crate) fn stream_watcher_spawn(obj: &libbpf_rs::Object) -> Result<Daemon> {
657    let with_fds = *scx_utils::compat::PROG_STREAM_OPEN_SUPPORTED;
658    let mut progs = Vec::new();
659    let mut streams = Vec::new();
660
661    for prog in obj.progs() {
662        let Some(name) = prog.name().to_str() else {
663            continue;
664        };
665        /* feature-gated programs may not be loaded and then have no fd */
666        let Some(fd) = prog_fd_clone(&prog)? else {
667            continue;
668        };
669        if !with_fds {
670            progs.push((name.to_string(), fd));
671            continue;
672        }
673        for (stream_id, label) in BPF_STREAMS {
674            let sfd = scx_utils::compat::prog_stream_open(fd.as_fd(), stream_id, true)
675                .with_context(|| format!("opening BPF {} of prog {}", label, name))?;
676            streams.push(StreamFd {
677                name: name.to_string(),
678                stream_id,
679                label,
680                reader: BufReader::new(File::from(sfd)),
681                pending: Vec::new(),
682                eof: false,
683            });
684        }
685    }
686
687    let stop = stop_eventfd()?;
688    let daemon_stop = stop
689        .try_clone()
690        .context("cloning stream watcher stop eventfd")?;
691    let thread = std::thread::Builder::new()
692        .name("scx-bpf-stream".into())
693        .spawn(move || {
694            if with_fds {
695                stream_watch_fds(streams, daemon_stop);
696            } else {
697                stream_watch_periodic(progs, daemon_stop);
698            }
699        })
700        .context("spawning BPF stream watcher")?;
701
702    Ok(Daemon {
703        stop,
704        thread: Some(thread),
705    })
706}
707
708#[cfg(test)]
709mod tests {
710    use super::stream_extract_lines;
711    use super::stream_is_fatal;
712
713    #[test]
714    fn test_stream_extract_lines() {
715        let mut carry = String::new();
716
717        /* empty carry, quiet tick */
718        assert!(stream_extract_lines(&mut carry, false).is_empty());
719
720        /* one complete line plus a partial */
721        carry.push_str("ERROR: whole line\npartial");
722        assert_eq!(
723            stream_extract_lines(&mut carry, true),
724            ["ERROR: whole line"]
725        );
726        assert_eq!(carry, "partial");
727
728        /* the partial is not flushed while data keeps arriving */
729        assert!(stream_extract_lines(&mut carry, true).is_empty());
730        assert_eq!(carry, "partial");
731
732        /* a line split across reads is reassembled */
733        carry.push_str(" continued\nnext");
734        assert_eq!(
735            stream_extract_lines(&mut carry, true),
736            ["partial continued"]
737        );
738        assert_eq!(carry, "next");
739
740        /* a stale partial flushes on a quiet tick, exactly once */
741        assert_eq!(stream_extract_lines(&mut carry, false), ["next"]);
742        assert!(carry.is_empty());
743        assert!(stream_extract_lines(&mut carry, false).is_empty());
744
745        /* multiple lines per chunk, blank lines forwarded */
746        carry.push_str("a\n\nb\ntail");
747        assert_eq!(stream_extract_lines(&mut carry, true), ["a", "", "b"]);
748        assert_eq!(carry, "tail");
749        carry.clear();
750
751        /* multi-byte UTF-8 split across reads stays on char boundaries */
752        carry.push_str("caf");
753        assert!(stream_extract_lines(&mut carry, true).is_empty());
754        carry.push_str("\u{e9}\n");
755        assert_eq!(stream_extract_lines(&mut carry, true), ["caf\u{e9}"]);
756        assert!(carry.is_empty());
757    }
758
759    #[test]
760    fn test_stream_is_fatal() {
761        let fatal = ["noise".to_string(), "ERROR: Arena READ access".to_string()];
762        assert!(stream_is_fatal(&fatal));
763
764        /* the prefix only counts at the start of a line */
765        let glued = ["no task dataERROR: Arena READ access".to_string()];
766        assert!(!stream_is_fatal(&glued));
767        assert!(!stream_is_fatal(&[]));
768    }
769}