Skip to main content

gander_core/
lib.rs

1//! gander-core — platform data collection and process model.
2//!
3//! This crate must not depend on any UI crate. That keeps the parsers testable
4//! against fixtures on any host, and makes a `--json` batch mode (and future
5//! non-TUI front ends) nearly free.
6//!
7//! Two backends exist:
8//!  * [`linux`] — reads `/proc` and `/sys`. Parametrised by a root path, so the
9//!    entire read path is testable from fixtures on any unix.
10//!  * [`darwin`] — uses `libproc`, `sysctl` and Mach host statistics.
11//!
12//! Both produce the same [`row::Row`] / [`system::SystemSnapshot`] types, so the
13//! UI and configuration layers are platform-independent.
14//!
15//! See `docs/DESIGN.md` and `docs/PORTABILITY.md`.
16
17pub mod action;
18pub mod affinity;
19pub mod format;
20pub mod row;
21pub mod system;
22pub mod table;
23pub mod tree;
24pub mod users;
25
26/// The `/proc` backend. Compiles on any unix because the IO layer is
27/// parameterised by a root path; only Linux has a live `/proc` to point it at.
28#[cfg(unix)]
29pub mod linux;
30
31/// The macOS backend: `libproc` + `sysctl` + Mach.
32#[cfg(target_os = "macos")]
33pub mod darwin;
34
35/// The FreeBSD backend: `sysctl` + `libkvm` + `devstat`.
36#[cfg(target_os = "freebsd")]
37pub mod freebsd;
38
39/// Process identifier.
40pub type Pid = u32;
41
42/// A unified sampler interface so the front end does not name a backend.
43///
44/// `docs/DESIGN.md` §5: selection happens at compile time, so this trait exists
45/// for genericity in tests and for documentation, not for runtime dispatch in
46/// the hot loop.
47pub trait PlatformSampler: Default {
48    /// Take one sample: host counters plus one row per process.
49    fn sample(&mut self) -> (system::SystemSnapshot, Vec<row::Row>);
50
51    /// Number of cached per-process identity records, for tests and diagnostics.
52    fn cache_len(&self) -> usize;
53
54    /// A user-facing name for the backend.
55    fn backend_name() -> &'static str;
56
57    /// Tell the sampler what the front end currently needs.
58    ///
59    /// Call this before every `sample()`. The default implementation ignores it,
60    /// which is the right behaviour for a backend with no expensive reads.
61    fn set_scan_options(&mut self, _opts: ScanOptions) {}
62
63    /// Whether this backend can actually enumerate threads.
64    fn supports_threads() -> bool {
65        false
66    }
67}
68
69/// What the front end is currently showing, and therefore what the sampler has to
70/// pay for.
71///
72/// # Why this is one struct rather than three setters
73///
74/// The per-process reads named here are not free:
75///
76/// * `threads` — an extra `stat` read per thread (~1700 per tick on a desktop).
77/// * `smaps` — a kernel walk over every VMA of every process (tens of ms).
78/// * `gpu` — a `readdir` of `fdinfo` plus a read per open DRM fd, per process.
79///
80/// Each one used to be its own `set_*` method on the trait. That worked until the
81/// third, at which point a front end had to remember to call every setter on every
82/// change, and a forgotten one meant either a wasted scan or a column of dashes.
83/// Passing the whole decision as one value makes both failures impossible: there
84/// is one call, it carries everything, and a new gated read can only be added by
85/// extending this struct — which breaks every call site until it is answered.
86#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
87pub struct ScanOptions {
88    /// Emit one row per thread as well as one per process.
89    pub threads: bool,
90    /// Read `/proc/<pid>/smaps_rollup` (PSS, EPSS, private, per-process swap).
91    pub smaps: bool,
92    /// Read `/proc/<pid>/fdinfo` for `drm-*` keys (GPU time and percentage).
93    pub gpu: bool,
94    /// Read per-task delay counters over `TASKSTATS` generic netlink.
95    pub delayacct: bool,
96}
97
98impl ScanOptions {
99    /// Everything off: the cheap scan, which is what the shipped default column
100    /// set and the default header need.
101    pub const fn none() -> Self {
102        Self {
103            threads: false,
104            smaps: false,
105            gpu: false,
106            delayacct: false,
107        }
108    }
109
110    pub const fn with_threads(mut self, on: bool) -> Self {
111        self.threads = on;
112        self
113    }
114
115    pub const fn with_smaps(mut self, on: bool) -> Self {
116        self.smaps = on;
117        self
118    }
119
120    pub const fn with_gpu(mut self, on: bool) -> Self {
121        self.gpu = on;
122        self
123    }
124
125    pub const fn with_delayacct(mut self, on: bool) -> Self {
126        self.delayacct = on;
127        self
128    }
129}
130
131/// Whether a live backend exists for this build.
132///
133/// Three backends: Linux reads `/proc`, macOS uses `libproc` and Mach, FreeBSD
134/// uses `sysctl` and `libkvm`. Everything else compiles but has no sampler, and
135/// the front ends refuse to start rather than showing an empty table.
136pub const PLATFORM_SUPPORTED: bool = cfg!(any(
137    target_os = "linux",
138    target_os = "macos",
139    target_os = "freebsd"
140));
141
142/// The sampler for the current target.
143#[cfg(target_os = "linux")]
144pub type Sampler = linux::system::Sampler;
145
146#[cfg(target_os = "macos")]
147pub type Sampler = darwin::system::Sampler;
148
149#[cfg(target_os = "freebsd")]
150pub type Sampler = freebsd::system::Sampler;
151
152/// Re-export of the platform selected at compile time.
153#[cfg(target_os = "linux")]
154pub use linux::LinuxPlatform as CurrentPlatform;
155
156#[cfg(target_os = "macos")]
157pub use darwin::DarwinPlatform as CurrentPlatform;
158
159#[cfg(target_os = "freebsd")]
160pub use freebsd::FreeBsdPlatform as CurrentPlatform;
161
162/// Platform capabilities that vary per OS, resolved at compile time.
163///
164/// Front ends use this to hide panes and columns that cannot work here, rather
165/// than discovering it at runtime. See `docs/PORTABILITY.md`.
166///
167/// **A constant here must be read by something.** This module had ten and four of
168/// them were referenced by nothing but their own doc comments — among them
169/// `PROC_IO`, which claimed per-process I/O counters were available while no such
170/// column existed. A capability nothing consults is worse than no capability: it
171/// reads like an implemented feature and answers a question nobody asked. They
172/// were removed rather than kept "for later"; adding one back is two lines, and
173/// the second line is the call site.
174///
175/// Where the answer is needed by only one module, ask that module instead of
176/// growing this list — `affinity::supported()` returns whether a mask can actually
177/// be read and written, which is a stricter question than "does the panel open".
178pub mod capability {
179    /// The `a` affinity panel has something to say on this platform.
180    ///
181    /// True on macOS as well, where there is no per-process mask: the panel still
182    /// opens, and explains that the OS offers only a scheduler hint rather than
183    /// silently doing nothing. Whether a mask can actually be **read and
184    /// written** is a stricter question, and it is
185    /// [`affinity::supported()`](crate::affinity::supported) that answers it.
186    pub const AFFINITY: bool = cfg!(any(
187        target_os = "linux",
188        target_os = "macos",
189        target_os = "freebsd"
190    ));
191
192    /// cgroup membership is readable (`/proc/<pid>/cgroup`).
193    ///
194    /// Gates the `CGROUP`/`CONTAINER` columns in the setup screen and the `O`
195    /// key, so a platform without cgroups offers neither rather than showing a
196    /// column of dashes.
197    pub const CGROUP: bool = cfg!(target_os = "linux");
198
199    /// Host-wide disk byte counters are available (`/proc/diskstats`,
200    /// `devstat_getdevs`).
201    ///
202    /// macOS is the exception: it exposes per-process `rusage` figures but no
203    /// host-wide byte counter that avoids IOKit, and the design forbids a vendor
204    /// SDK. The `DiskIO` meter says so there instead of drawing a permanent zero.
205    pub const HOST_DISK_IO: bool =
206        cfg!(any(target_os = "linux", target_os = "freebsd"));
207}