Skip to main content

gander_core/
row.rs

1//! Field identifiers and their presentation metadata.
2
3use crate::Pid;
4
5/// A displayable per-process column.
6///
7/// Variants mirror htop's `RowField.h` / `<platform>/ProcessField.h` semantics.
8/// The numeric IDs deliberately do *not* match htop: only the `htoprc` importer
9/// needs name-level compatibility.
10///
11/// Render with `match`, never `dyn` dispatch — thousands of rows times dozens of
12/// columns per frame makes dynamic dispatch measurable.
13#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug, PartialOrd, Ord)]
14#[non_exhaustive]
15pub enum Field {
16    Pid,
17    Comm,
18    State,
19    Ppid,
20    Pgrp,
21    Session,
22    Tty,
23    Tpgid,
24    MinFlt,
25    MajFlt,
26    Priority,
27    Nice,
28    StartTime,
29    Processor,
30    VirtMem,
31    ResMem,
32    User,
33    PercentCpu,
34    PercentMem,
35    Time,
36    Nlwp,
37    Tgid,
38    PercentNormCpu,
39    Elapsed,
40    SchedPolicy,
41    ProcComm,
42    ProcExe,
43    Cwd,
44
45    // Linux-only fields (see docs/DESIGN.md, milestone M3).
46    #[cfg(target_os = "linux")]
47    Pss,
48    #[cfg(target_os = "linux")]
49    Swap,
50    #[cfg(target_os = "linux")]
51    Private,
52    #[cfg(target_os = "linux")]
53    Epss,
54    #[cfg(target_os = "linux")]
55    Cgroup,
56    #[cfg(target_os = "linux")]
57    Container,
58    #[cfg(target_os = "linux")]
59    GpuPercent,
60    #[cfg(target_os = "linux")]
61    GpuTime,
62    #[cfg(target_os = "linux")]
63    CpuDelayPercent,
64    #[cfg(target_os = "linux")]
65    IoDelayPercent,
66    #[cfg(target_os = "linux")]
67    SwapDelayPercent,
68}
69
70impl Field {
71    /// Whether reading this column requires `/proc/<pid>/smaps_rollup`.
72    ///
73    /// The sampler consults this before spending tens of milliseconds per tick on
74    /// a kernel VMA walk: a column that is not on screen must not cost anything.
75    /// Kept next to the `Field` definition so a future PSS-derived column cannot
76    /// be added without answering the question.
77    pub const fn needs_smaps_rollup(self) -> bool {
78        #[cfg(target_os = "linux")]
79        {
80            matches!(self, Field::Pss | Field::Epss | Field::Private | Field::Swap)
81        }
82        #[cfg(not(target_os = "linux"))]
83        {
84            let _ = self;
85            false
86        }
87    }
88
89    /// Whether reading this column requires a `TASKSTATS` netlink query.
90    ///
91    /// Same contract as [`Self::needs_smaps_rollup`], with a higher price: every
92    /// query is a syscall pair and a kernel task lookup, per process, per tick.
93    pub const fn needs_delayacct(self) -> bool {
94        #[cfg(target_os = "linux")]
95        {
96            matches!(
97                self,
98                Field::CpuDelayPercent | Field::IoDelayPercent | Field::SwapDelayPercent
99            )
100        }
101        #[cfg(not(target_os = "linux"))]
102        {
103            let _ = self;
104            false
105        }
106    }
107
108    /// Whether reading this column requires scanning `/proc/<pid>/fdinfo`.
109    ///
110    /// Same contract as [`Self::needs_smaps_rollup`]: a column that is not on
111    /// screen must not cost a directory walk per process per tick.
112    pub const fn needs_gpu_fdinfo(self) -> bool {
113        #[cfg(target_os = "linux")]
114        {
115            matches!(self, Field::GpuPercent | Field::GpuTime)
116        }
117        #[cfg(not(target_os = "linux"))]
118        {
119            let _ = self;
120            false
121        }
122    }
123}
124
125/// Horizontal alignment of a column.
126#[derive(Copy, Clone, PartialEq, Eq, Debug)]
127pub enum Align {
128    Left,
129    Right,
130}
131
132/// Static description of a field, used by the setup screen and by the renderer.
133pub struct FieldMeta {
134    pub field: Field,
135    /// Stable identifier used in config files (`"M_PSS"`).
136    pub name: &'static str,
137    /// Header cell text (`" PSS "`).
138    pub title: &'static str,
139    /// Human description shown in the setup screen.
140    pub description: &'static str,
141    /// Whether this column sorts descending by default.
142    pub default_sort_desc: bool,
143    pub align: Align,
144}
145
146impl Field {
147    pub fn meta(self) -> Option<&'static FieldMeta> {
148        FIELD_META.iter().find(|m| m.field == self)
149    }
150
151    /// Stable config name, e.g. `PercentCpu` -> `"PERCENT_CPU"`.
152    pub fn name(self) -> &'static str {
153        self.meta().map_or("UNKNOWN", |m| m.name)
154    }
155
156    /// Header text, e.g. `PercentCpu` -> `" CPU% "`.
157    pub fn title(self) -> &'static str {
158        self.meta().map_or(" ? ", |m| m.title)
159    }
160
161    pub fn default_sort_desc(self) -> bool {
162        self.meta().is_some_and(|m| m.default_sort_desc)
163    }
164
165    /// All fields available on this platform, in setup-screen order.
166    pub fn all() -> impl Iterator<Item = Field> {
167        FIELD_META.iter().map(|m| m.field)
168    }
169
170    pub fn from_name(name: &str) -> Option<Field> {
171        FIELD_META.iter().find(|m| m.name == name).map(|m| m.field)
172    }
173}
174
175const fn m(
176    field: Field,
177    name: &'static str,
178    title: &'static str,
179    description: &'static str,
180    default_sort_desc: bool,
181    align: Align,
182) -> FieldMeta {
183    FieldMeta {
184        field,
185        name,
186        title,
187        description,
188        default_sort_desc,
189        align,
190    }
191}
192
193const FIELD_META: &[FieldMeta] = &[
194    m(
195        Field::Pid,
196        "PID",
197        "   PID ",
198        "Process ID",
199        false,
200        Align::Right,
201    ),
202    m(
203        Field::Comm,
204        "COMM",
205        " COMMAND ",
206        "Command name from /proc/<pid>/comm",
207        false,
208        Align::Left,
209    ),
210    m(
211        Field::State,
212        "STATE",
213        " S ",
214        "Process state",
215        false,
216        Align::Left,
217    ),
218    m(
219        Field::Ppid,
220        "PPID",
221        "  PPID ",
222        "Parent process ID",
223        false,
224        Align::Right,
225    ),
226    m(
227        Field::Pgrp,
228        "PGRP",
229        "  PGRP ",
230        "Process group ID",
231        false,
232        Align::Right,
233    ),
234    m(
235        Field::User,
236        "USER",
237        " USER ",
238        "Effective user",
239        false,
240        Align::Left,
241    ),
242    m(
243        Field::Priority,
244        "PRIORITY",
245        " PRI ",
246        "Kernel scheduling priority",
247        false,
248        Align::Right,
249    ),
250    m(
251        Field::Nice,
252        "NICE",
253        " NI ",
254        "Nice value",
255        false,
256        Align::Right,
257    ),
258    m(
259        Field::PercentCpu,
260        "PERCENT_CPU",
261        " CPU% ",
262        "CPU usage in percent",
263        true,
264        Align::Right,
265    ),
266    m(
267        Field::PercentMem,
268        "PERCENT_MEM",
269        " MEM% ",
270        "Memory usage in percent",
271        true,
272        Align::Right,
273    ),
274    m(
275        Field::PercentNormCpu,
276        "PERCENT_NORM_CPU",
277        " NCPU% ",
278        "CPU usage normalized by core count",
279        true,
280        Align::Right,
281    ),
282    m(
283        Field::Time,
284        "TIME",
285        "  TIME+ ",
286        "Cumulative CPU time",
287        true,
288        Align::Right,
289    ),
290    m(
291        Field::Elapsed,
292        "ELAPSED",
293        " ELAPSED ",
294        "Time since the process started",
295        false,
296        Align::Right,
297    ),
298    m(
299        Field::ResMem,
300        "M_RESIDENT",
301        "  RES ",
302        "Resident set size",
303        true,
304        Align::Right,
305    ),
306    m(
307        Field::VirtMem,
308        "M_VIRT",
309        "  VIRT ",
310        "Virtual memory size",
311        true,
312        Align::Right,
313    ),
314    m(
315        Field::Nlwp,
316        "NLWP",
317        " NLWP ",
318        "Number of threads",
319        false,
320        Align::Right,
321    ),
322    m(
323        Field::Processor,
324        "PROCESSOR",
325        " CPU ",
326        "Last used CPU",
327        false,
328        Align::Right,
329    ),
330    m(
331        Field::MinFlt,
332        "MINFLT",
333        " MINFLT ",
334        "Minor page faults",
335        false,
336        Align::Right,
337    ),
338    m(
339        Field::MajFlt,
340        "MAJFLT",
341        " MAJFLT ",
342        "Major page faults",
343        false,
344        Align::Right,
345    ),
346    // Session and the controlling terminal. Both are in `/proc/<pid>/stat` (fields
347    // 4 and 5) and in Darwin's `kinfo_proc`, so they are real on every platform;
348    // they were declared as `Field` variants with no metadata, which made them
349    // unreachable: no name, no header, no setup entry, and an `htoprc` that named
350    // them imported to the string `"UNKNOWN"` and was silently dropped.
351    m(
352        Field::Session,
353        "SESSION",
354        " SID ",
355        "Session ID: the login session this process belongs to",
356        false,
357        Align::Right,
358    ),
359    m(
360        Field::Tty,
361        "TTY",
362        " TTY      ",
363        "Controlling terminal, or `?` when there is none",
364        false,
365        Align::Left,
366    ),
367    // The process group a *terminal* is currently sending its signals to. Equal to
368    // PGRP unless one of the jobs is in the foreground.
369    m(
370        Field::Tpgid,
371        "TPGID",
372        "  TPGID ",
373        "Process group of the foreground job on this terminal",
374        false,
375        Align::Right,
376    ),
377    // Wall-clock start, not the `starttime` ticks kept for PID-reuse detection:
378    // ticks since boot mean nothing without the boot time next to them.
379    m(
380        Field::StartTime,
381        "STARTTIME",
382        " START     ",
383        "Wall-clock time this process started",
384        false,
385        Align::Left,
386    ),
387    m(
388        Field::Tgid,
389        "TGID",
390        "  TGID ",
391        "Thread group ID: the pid of the process a thread row belongs to",
392        false,
393        Align::Right,
394    ),
395    m(
396        Field::SchedPolicy,
397        "SCHED_POLICY",
398        " POL ",
399        "Scheduling policy: other, fifo, rr, batch, idle, deadline",
400        false,
401        Align::Left,
402    ),
403    // The two PROC_* fields exist because `COMM` is the *command line* here (what
404    // htop's Command column shows), so the kernel's own short name needs its own
405    // column. htop draws the same distinction: `COMM` vs `PROC_COMM`.
406    m(
407        Field::ProcComm,
408        "PROC_COMM",
409        " COMMAND       ",
410        "Kernel's short command name, always the executable's base name",
411        false,
412        Align::Left,
413    ),
414    m(
415        Field::ProcExe,
416        "PROC_EXE",
417        " EXE                ",
418        "Path to the executable, as the kernel resolved it",
419        false,
420        Align::Left,
421    ),
422    m(
423        Field::Cwd,
424        "CWD",
425        " CWD                ",
426        "Working directory of the process",
427        false,
428        Align::Left,
429    ),
430    // Linux-only: cgroup membership, and the container derived from it.
431    #[cfg(target_os = "linux")]
432    m(
433        Field::Cgroup,
434        "CGROUP",
435        " CGROUP ",
436        "Full cgroup path",
437        false,
438        Align::Left,
439    ),
440    #[cfg(target_os = "linux")]
441    m(
442        Field::Container,
443        "CONTAINER",
444        " CONTAINER ",
445        "Container id, prefixed with its runtime",
446        false,
447        Align::Left,
448    ),
449    // The PSS family. All four come from `/proc/<pid>/smaps_rollup`, which is why
450    // they share `Field::needs_smaps_rollup` — the sampler reads it only when one
451    // of these is on screen.
452    #[cfg(target_os = "linux")]
453    m(
454        Field::Pss,
455        "M_PSS",
456        "  PSS ",
457        "Proportional set size: shared pages divided between sharers",
458        true,
459        Align::Right,
460    ),
461    #[cfg(target_os = "linux")]
462    m(
463        Field::Epss,
464        "M_EPSS",
465        " EPSS ",
466        "Effective PSS: PSS plus the swapped-out equivalent",
467        true,
468        Align::Right,
469    ),
470    #[cfg(target_os = "linux")]
471    m(
472        Field::Private,
473        "M_PRIVATE",
474        " PRIV ",
475        "Private memory: what would be freed if this process exited",
476        true,
477        Align::Right,
478    ),
479    #[cfg(target_os = "linux")]
480    m(
481        Field::Swap,
482        "M_SWAP",
483        "  SWP ",
484        "Memory this process has pushed to swap",
485        true,
486        Align::Right,
487    ),
488    // GPU, from `drm-usage-stats` keys in `fdinfo`. No vendor SDK is involved:
489    // the kernel exports these for every driver that opts in, which today means
490    // amdgpu, i915/xe, nouveau and (partially) nvidia.
491    #[cfg(target_os = "linux")]
492    m(
493        Field::GpuPercent,
494        "GPU%",
495        " GPU% ",
496        "Share of GPU time this process used, from drm-usage-stats",
497        true,
498        Align::Right,
499    ),
500    #[cfg(target_os = "linux")]
501    m(
502        Field::GpuTime,
503        "GPU_TIME",
504        "  GPUT ",
505        "Cumulative GPU time, across every engine class",
506        true,
507        Align::Right,
508    ),
509    // Delay accounting, from `TASKSTATS` over generic netlink. The figures are
510    // shares of the task's lifetime spent *waiting*: runnable but not on a CPU;
511    // blocked on synchronous I/O; waiting for a page to be swapped back in.
512    //
513    // htop names these `PERCENT_CPU_DELAY`, `PERCENT_IO_DELAY` and
514    // `PERCENT_SWAP_DELAY`; the ids match htop's `ProcessField.h` so an htoprc
515    // imports.
516    #[cfg(target_os = "linux")]
517    m(
518        Field::CpuDelayPercent,
519        "CPUD%",
520        "CPUD% ",
521        "Share of this task's life spent runnable but waiting for a CPU",
522        true,
523        Align::Right,
524    ),
525    #[cfg(target_os = "linux")]
526    m(
527        Field::IoDelayPercent,
528        "IOD%",
529        " IOD% ",
530        "Share of this task's life spent waiting for synchronous block I/O",
531        true,
532        Align::Right,
533    ),
534    #[cfg(target_os = "linux")]
535    m(
536        Field::SwapDelayPercent,
537        "SWPD%",
538        "SWPD% ",
539        "Share of this task's life spent waiting for a page to be swapped in",
540        true,
541        Align::Right,
542    ),
543];
544
545/// Names of the fields shown in the default M0 layout.
546pub const DEFAULT_COLUMNS: &[Field] = &[
547    Field::Pid,
548    Field::User,
549    Field::Priority,
550    Field::Nice,
551    Field::PercentCpu,
552    Field::PercentMem,
553    Field::Time,
554    Field::Comm,
555];
556
557/// A process name, bounded to avoid any heap allocation during a scan.
558pub type CommBuf = arrayvec::ArrayString<64>;
559
560/// Raw, monotonically increasing CPU tick counters for one process.
561///
562/// CPU percent can only be derived from two samples; see `docs/DESIGN.md` §3.5.
563#[derive(Copy, Clone, Default, Debug)]
564pub struct CpuTicks {
565    /// `utime + stime` of the process, in clock ticks.
566    pub proc: u64,
567    /// System-wide total ticks across all CPUs at the same instant.
568    pub total: u64,
569}
570
571bitflags_like::flags! {
572    /// Properties of a row that affect rendering and filtering.
573    pub struct RowFlags: u16 {
574        const KERNEL_THREAD = 1 << 0;
575        const USERLAND_THREAD = 1 << 1;
576        const STALE_EXE = 1 << 2;
577        const STALE_LIB = 1 << 3;
578        const NEW = 1 << 4;
579    }
580}
581
582/// Internal shim: a tiny bitflags implementation, to avoid a dependency.
583mod bitflags_like {
584    macro_rules! flags {
585        (
586            $(#[$meta:meta])*
587            pub struct $name:ident: $ty:ty {
588                $(
589                    const $flag:ident = $value:expr;
590                )*
591            }
592        ) => {
593            $(#[$meta])*
594            #[derive(Copy, Clone, Default, PartialEq, Eq, Debug)]
595            pub struct $name(pub $ty);
596
597            impl $name {
598                $(pub const $flag: $name = $name($value);)*
599
600                pub const fn empty() -> Self { $name(0) }
601
602                pub const fn contains(self, other: Self) -> bool {
603                    (self.0 & other.0) == other.0
604                }
605
606                pub const fn intersects(self, other: Self) -> bool {
607                    (self.0 & other.0) != 0
608                }
609
610                pub const fn insert(self, other: Self) -> Self {
611                    $name(self.0 | other.0)
612                }
613
614                pub const fn remove(self, other: Self) -> Self {
615                    $name(self.0 & !other.0)
616                }
617
618                pub const fn set(self, other: Self, on: bool) -> Self {
619                    if on { self.insert(other) } else { self.remove(other) }
620                }
621            }
622        };
623    }
624
625    pub(crate) use flags;
626}
627
628/// One process, as sampled during a single tick.
629///
630/// **Invariant: `Row` stores raw scalars and interned strings only.** Any
631/// `String` produced by `format!` during a scan is a bug — see
632/// `docs/DESIGN.md` §9.4. Human-readable text is produced by the renderer
633/// through `crate::format`.
634#[derive(Clone, Debug)]
635pub struct Row {
636    pub pid: Pid,
637    pub ppid: Pid,
638    pub pgrp: Pid,
639    pub session: Pid,
640    pub tty_nr: i64,
641    /// The process group the controlling terminal is currently signalling.
642    ///
643    /// Equal to `pgrp` unless a job has the terminal in the foreground, which is
644    /// the one distinction this column exists to show.
645    ///
646    /// `-1` is what the kernel reports when the terminal has no foreground job,
647    /// and it is also what a thread row carries. The renderer shows `-` for it
648    /// rather than the number: `-1` is a wire value, not a process group.
649    pub tpgid: i64,
650    /// Field 22 of `/proc/<pid>/stat`: cache key and PID-reuse detector.
651    pub starttime: u64,
652    /// Thread group id: the pid of the process this row belongs to.
653    ///
654    /// For a process this equals `pid`. For a thread it is the leader's pid,
655    /// which is what lets the table nest threads under their process and keep
656    /// the selection stable while thread enumeration is toggled.
657    pub tgid: Pid,
658    /// True when this row is a thread rather than a process.
659    ///
660    /// Kept separate from `RowFlags::USERLAND_THREAD` because that flag answers
661    /// "is this a userland thread" for display purposes, while this answers a
662    /// structural question the table has to get right.
663    pub is_thread: bool,
664    /// Identity of a thread row, unique across the whole table.
665    ///
666    /// For a process row this is `None` and `pid` is the identity. For a thread it
667    /// is `Some`, because on macOS a thread has no pid of its own: `pth` handles
668    /// are 64-bit (95% exceed `u32`) and are *not* globally unique (every process
669    /// reports the same handle for its main thread). Linux threads do have a tid
670    /// that fits in `pid`, but it can coincide with an unrelated process's pid, so
671    /// thread rows use this on both platforms.
672    pub thread_id: Option<u64>,
673
674    /// `ArrayString<64>`: no heap allocation per scanned process.
675    pub comm: CommBuf,
676    pub state: u8,
677
678    pub uid: u32,
679    pub user: Option<u32>, // index into the user-name table
680
681    pub priority: i8,
682    pub nice: i8,
683    pub processor: i32,
684    pub nlwp: i32,
685    pub sched_policy: u8,
686
687    /// Raw tick counters, current and previous sample.
688    pub ticks: CpuTicks,
689    pub prev_ticks: CpuTicks,
690
691    pub min_flt: u64,
692    pub maj_flt: u64,
693
694    /// Virtual and resident size in bytes.
695    pub virt: u64,
696    pub res: u64,
697
698    /// Derived values, computed at sample time (not at render time).
699    pub percent_cpu: f32,
700    pub percent_mem: f32,
701    /// `utime + stime` in ticks, kept for the TIME column.
702    pub time_ticks: u64,
703    /// Seconds since process start.
704    pub elapsed: u64,
705
706    /// Lazily loaded, cached until the process exits.
707    pub cmdline: Option<Box<str>>,
708    pub exe: Option<Box<str>>,
709    /// The process's working directory, read from `/proc/<pid>/cwd` or, on macOS,
710    /// from `proc_pidinfo(PROC_PIDVNODEPATHINFO)`.
711    ///
712    /// Cached with the identity because a `readlink` per row per tick is
713    /// thousands of syscalls for a value that changes rarely — even though, unlike
714    /// `exe`, it *can* change. Accepting a stale value until the process exits is
715    /// the same trade htop makes.
716    pub cwd: Option<Box<str>>,
717    /// Proportional set size, from `/proc/<pid>/smaps_rollup`, in bytes.
718    ///
719    /// `None` means "not read", not "zero": the file is expensive enough that
720    /// it is only opened when a column needing it is visible, and another user's
721    /// process may refuse to be read at all. The renderer shows `-` for `None`
722    /// and a real figure for `Some(0)`.
723    ///
724    /// RSS counts a shared page once per process, so two processes sharing 100 MB
725    /// of libraries report 100 MB each and the machine appears to use 200 MB.
726    /// PSS splits that page between them, which is the only figure that adds up
727    /// across the table.
728    #[cfg(target_os = "linux")]
729    pub pss: Option<u64>,
730    /// `Pss` + `SwapPss`: what the process costs including what it pushed to swap.
731    #[cfg(target_os = "linux")]
732    pub epss: Option<u64>,
733    /// Memory exclusively this process's (`Private_Clean + Private_Dirty`).
734    ///
735    /// The complement of the shared part: PSS minus this is what would be freed
736    /// if the process exited and nothing else changed.
737    #[cfg(target_os = "linux")]
738    pub private: Option<u64>,
739    /// Bytes of this process's memory currently in swap (`Swap` from rollup).
740    #[cfg(target_os = "linux")]
741    pub swap_bytes: Option<u64>,
742    /// GPU utilisation in percent, from `drm-usage-stats` keys in `fdinfo`.
743    ///
744    /// `None` when the process holds no DRM descriptor, or holds one whose driver
745    /// reports no `drm-busy`, or the `fdinfo` files are unreadable. Those are
746    /// three different reasons for the same thing — "no percentage is available"
747    /// — and none of them is zero.
748    #[cfg(target_os = "linux")]
749    pub gpu_percent: Option<f32>,
750    /// Cumulative GPU nanoseconds, across every engine class the driver reports.
751    #[cfg(target_os = "linux")]
752    pub gpu_ns: Option<u64>,
753    /// Share of this task's life spent runnable but waiting for a CPU.
754    ///
755    /// From `TASKSTATS`; see `linux::delayacct`. `None` means unread — no
756    /// `CAP_NET_ADMIN`, no `CONFIG_TASKSTATS`, or no delay column visible — and
757    /// renders `-`, never `0.0`.
758    pub cpu_delay_percent: Option<f32>,
759    /// Share of this task's life spent waiting for synchronous block I/O.
760    pub io_delay_percent: Option<f32>,
761    /// Share of this task's life spent waiting for a page to be swapped in.
762    pub swap_delay_percent: Option<f32>,
763    /// The process's cgroup path, e.g. `/system.slice/docker-1f2e….scope`.
764    ///
765    /// The *path*, not the raw `/proc/<pid>/cgroup` line: the raw line is
766    /// `12:cpuset:/docker/abc` and carries the hierarchy id and controller list,
767    /// which no column wants. The v2 entry (empty controller list) is preferred
768    /// when present, because that is the hierarchy the whole machine shares.
769    pub cgroup: Option<Box<str>>,
770    /// The container this process belongs to, if any.
771    ///
772    /// Derived from `cgroup` once per process and cached with it. Linux-only
773    /// because the detection is cgroup-based; `None` everywhere else.
774    #[cfg(target_os = "linux")]
775    pub container: Option<Box<str>>,
776    /// The runtime that owns the container (`docker`, `kubepods`, …), kept apart
777    /// from `container` so the `CONTAINER` column can show `docker:1f2e…` without
778    /// re-parsing the path at render time.
779    #[cfg(target_os = "linux")]
780    pub container_runtime: Option<Box<str>>,
781
782    /// Tree bookkeeping, filled by `tree::build`.
783    pub depth: u16,
784    pub has_children: bool,
785    pub is_last_child: bool,
786
787    pub flags: RowFlags,
788}
789
790impl Row {
791    /// Construct a minimal row, used by tests and by the tree helpers.
792    pub fn default_for_test(pid: Pid) -> Self {
793        Row {
794            pid,
795            ppid: 0,
796            pgrp: 0,
797            session: 0,
798            tty_nr: 0,
799            tpgid: -1,
800            starttime: 0,
801            tgid: pid,
802            is_thread: false,
803            thread_id: None,
804            comm: CommBuf::new(),
805            state: b'R',
806            uid: 0,
807            user: None,
808            priority: 20,
809            nice: 0,
810            processor: 0,
811            nlwp: 1,
812            sched_policy: 0,
813            ticks: CpuTicks::default(),
814            prev_ticks: CpuTicks::default(),
815            min_flt: 0,
816            maj_flt: 0,
817            virt: 0,
818            res: 0,
819            percent_cpu: 0.0,
820            percent_mem: 0.0,
821            time_ticks: 0,
822            elapsed: 0,
823            cmdline: None,
824            exe: None,
825            cwd: None,
826            #[cfg(target_os = "linux")]
827            pss: None,
828            #[cfg(target_os = "linux")]
829            epss: None,
830            #[cfg(target_os = "linux")]
831            private: None,
832            #[cfg(target_os = "linux")]
833            swap_bytes: None,
834            #[cfg(target_os = "linux")]
835            gpu_percent: None,
836            #[cfg(target_os = "linux")]
837            gpu_ns: None,
838            cpu_delay_percent: None,
839            io_delay_percent: None,
840            swap_delay_percent: None,
841            cgroup: None,
842            #[cfg(target_os = "linux")]
843            container: None,
844            #[cfg(target_os = "linux")]
845            container_runtime: None,
846            depth: 0,
847            has_children: false,
848            is_last_child: false,
849            flags: RowFlags::empty(),
850        }
851    }
852
853    /// Major page faults.
854    ///
855    /// Exposed as a method so sort keys and renderers read symmetrically.
856    pub const fn maj_flt_count(&self) -> u64 {
857        self.maj_flt
858    }
859
860    /// Total CPU ticks consumed by the process.
861    pub const fn time_ticks_total(&self) -> u64 {
862        self.time_ticks
863    }
864
865    pub fn is_kernel_thread(&self) -> bool {
866        self.flags.contains(RowFlags::KERNEL_THREAD)
867    }
868
869    /// The value that identifies this row across samples.
870    ///
871    /// `pid` for a process, a thread-specific id for a thread. Used wherever a
872    /// row must be told apart from another row with the same `pid`, which on
873    /// macOS every thread of a process shares.
874    pub fn identity(&self) -> u64 {
875        match self.thread_id {
876            Some(id) => id,
877            None => self.pid as u64,
878        }
879    }
880
881    /// Text for the command column: `cmdline` if known, else `comm`, with
882    /// kernel threads bracketed per htop's convention.
883    pub fn display_command(&self) -> &str {
884        match &self.cmdline {
885            Some(c) if !c.is_empty() => c,
886            _ => self.comm.as_str(),
887        }
888    }
889
890    /// The executable's base name, and where it sits in [`Self::display_command`].
891    ///
892    /// Returns `(start, end)` byte offsets into `display_command()` so the renderer
893    /// can emphasise those cells without re-deriving the text or allocating.
894    ///
895    /// # Why this is not just "the last path component"
896    ///
897    /// The command line is `argv` joined by spaces, so a naive split on `/` finds
898    /// a path in an *argument* too (`grep /usr/lib/foo`). The base name is taken
899    /// from `exe` when the sampler has it, and located in the command text as a
900    /// prefix match on that path; when `exe` is unavailable (another user's
901    /// process) it falls back to the first word of the command, which is what
902    /// argv[0] is.
903    ///
904    /// Returns `None` when there is nothing worth highlighting — a kernel thread
905    /// in brackets, or a command whose base name is the whole string.
906    pub fn base_name_span(&self) -> Option<(usize, usize)> {
907        self.base_name_span_in(self.display_command())
908    }
909
910    /// [`Self::base_name_span`] against `text` instead of the command column text.
911    ///
912    /// The renderer prepends the tree branch prefix to the command column, so the
913    /// span it needs is offset by that prefix's length. Searching the *finished*
914    /// text is simpler and more robust than adding an offset to the span: the
915    /// prefix can only appear before the command, and the exe path search below
916    /// does not match it.
917    pub fn base_name_span_in(&self, text: &str) -> Option<(usize, usize)> {
918        if text.is_empty() {
919            return None;
920        }
921
922        // Prefer the real exe path: a prefix of the command line, when we have it.
923        if let Some(exe) = self.exe.as_deref() {
924            if !exe.is_empty() {
925                if let Some(pos) = text.find(exe) {
926                    let start = pos + exe.rfind('/').map_or(0, |i| i + 1);
927                    let end = pos + exe.len();
928                    if end > start {
929                        return Some((start, end));
930                    }
931                }
932            }
933        }
934
935        // Fall back to argv[0]: everything up to the first space.
936        let end = text.find(' ').unwrap_or(text.len());
937        let word = &text[..end];
938        // A bracketed name is a kernel thread, and htop does not highlight those.
939        if word.starts_with('[') {
940            return None;
941        }
942        let start = word.rfind('/').map_or(0, |i| i + 1);
943        // Nothing to gain from highlighting the entire string.
944        if start == 0 && end == text.len() {
945            return None;
946        }
947        if end > start {
948            Some((start, end))
949        } else {
950            None
951        }
952    }
953}
954
955#[cfg(test)]
956mod base_name_tests {
957    use super::*;
958
959    fn row_with(cmdline: &str, exe: Option<&str>) -> Row {
960        let mut r = Row::default_for_test(1);
961        r.cmdline = Some(cmdline.into());
962        r.exe = exe.map(Into::into);
963        r
964    }
965
966    #[test]
967    fn finds_the_basename_inside_a_full_path() {
968        let r = row_with(
969            "/Applications/iTerm.app/Contents/MacOS/iTerm2 --server",
970            Some("/Applications/iTerm.app/Contents/MacOS/iTerm2"),
971        );
972        let text = r.display_command();
973        let (s, e) = r.base_name_span().expect("span");
974        assert_eq!(&text[s..e], "iTerm2");
975    }
976
977    /// The exe path is authoritative, so an argument that looks like a path must
978    /// not win. `grep /usr/lib/foo` should highlight `grep`.
979    #[test]
980    fn an_argument_path_does_not_win_over_the_exe() {
981        let r = row_with("grep /usr/lib/foo", Some("/usr/bin/grep"));
982        let text = r.display_command();
983        let (s, e) = r.base_name_span().expect("span");
984        assert_eq!(&text[s..e], "grep");
985    }
986
987    /// Without `exe` (another user's process) fall back to argv[0].
988    #[test]
989    fn falls_back_to_argv0_without_an_exe() {
990        let r = row_with("/usr/bin/python3 script.py", None);
991        let text = r.display_command();
992        let (s, e) = r.base_name_span().expect("span");
993        assert_eq!(&text[s..e], "python3");
994    }
995
996    #[test]
997    fn a_bare_command_name_gets_no_span() {
998        // Nothing to emphasise: the name is the whole string already.
999        let r = row_with("launchd", None);
1000        assert_eq!(r.base_name_span(), None);
1001    }
1002
1003    #[test]
1004    fn a_kernel_thread_gets_no_span() {
1005        let r = row_with("[kworker/0:1]", None);
1006        assert_eq!(r.base_name_span(), None);
1007    }
1008
1009    #[test]
1010    fn an_empty_command_gets_no_span() {
1011        let mut r = Row::default_for_test(1);
1012        r.cmdline = None;
1013        r.comm.try_push_str("").unwrap();
1014        assert_eq!(r.base_name_span(), None);
1015    }
1016
1017    /// The renderer passes the *finished* column text, which in tree mode carries
1018    /// a branch prefix. The span has to move with it.
1019    #[test]
1020    fn the_span_tracks_a_prepended_tree_prefix() {
1021        let r = row_with("/usr/bin/nginx -g daemon", Some("/usr/bin/nginx"));
1022        let plain = r.display_command();
1023        let (s, e) = r.base_name_span().expect("span");
1024        assert_eq!(&plain[s..e], "nginx");
1025
1026        let prefixed = format!("├─ {plain}");
1027        let (ps, pe) = r.base_name_span_in(&prefixed).expect("span");
1028        assert_eq!(&prefixed[ps..pe], "nginx");
1029        assert_eq!(ps, s + "├─ ".len(), "offset by the prefix");
1030    }
1031
1032    /// A base name containing a space would make the argv[0] fallback wrong, but
1033    /// the exe path still locates it correctly.
1034    #[test]
1035    fn an_exe_with_spaces_is_located_by_path_not_by_word() {
1036        let r = row_with(
1037            "/Users/x/My App.app/Contents/MacOS/My App --flag",
1038            Some("/Users/x/My App.app/Contents/MacOS/My App"),
1039        );
1040        let text = r.display_command();
1041        let (s, e) = r.base_name_span().expect("span");
1042        assert_eq!(&text[s..e], "My App");
1043    }
1044}
1045
1046#[cfg(test)]
1047mod field_meta_tests {
1048    use super::*;
1049
1050    /// Every `Field` variant must have a `FieldMeta`.
1051    ///
1052    /// This is the guard for a real bug: `Session`, `Tty`, `Tpgid`, `StartTime`,
1053    /// `Tgid`, `SchedPolicy`, `ProcComm`, `ProcExe` and `Cwd` were declared as
1054    /// variants with no metadata, which made them unreachable in three ways at
1055    /// once — `name()` returned `"UNKNOWN"`, so an `htoprc` that named them
1056    /// imported to a column string that `TabConfig::fields` silently dropped;
1057    /// they never appeared in the setup screen, which iterates `Field::all()`; and
1058    /// the header title was `" ? "`. Nothing failed loudly, so it went unnoticed.
1059    ///
1060    /// The list below is written out rather than derived, because `Field` has no
1061    /// `strum`-style enumeration and adding a derive for one test is not worth the
1062    /// dependency. A new variant therefore requires editing this list too — which
1063    /// is the point: the test is a checklist, not an oracle.
1064    #[test]
1065    fn every_variant_has_metadata() {
1066        let all = [
1067            Field::Pid,
1068            Field::Comm,
1069            Field::State,
1070            Field::Ppid,
1071            Field::Pgrp,
1072            Field::Session,
1073            Field::Tty,
1074            Field::Tpgid,
1075            Field::MinFlt,
1076            Field::MajFlt,
1077            Field::Priority,
1078            Field::Nice,
1079            Field::StartTime,
1080            Field::Processor,
1081            Field::VirtMem,
1082            Field::ResMem,
1083            Field::User,
1084            Field::PercentCpu,
1085            Field::PercentMem,
1086            Field::Time,
1087            Field::Nlwp,
1088            Field::Tgid,
1089            Field::PercentNormCpu,
1090            Field::Elapsed,
1091            Field::SchedPolicy,
1092            Field::ProcComm,
1093            Field::ProcExe,
1094            Field::Cwd,
1095        ];
1096        #[allow(unused_mut)]
1097        let mut all = all.to_vec();
1098        #[cfg(target_os = "linux")]
1099        all.extend([
1100            Field::Pss,
1101            Field::Swap,
1102            Field::Private,
1103            Field::Epss,
1104            Field::Cgroup,
1105            Field::Container,
1106            Field::GpuPercent,
1107            Field::GpuTime,
1108            Field::CpuDelayPercent,
1109            Field::IoDelayPercent,
1110            Field::SwapDelayPercent,
1111        ]);
1112
1113        for field in all {
1114            let meta = field
1115                .meta()
1116                .unwrap_or_else(|| panic!("{field:?} has no FieldMeta"));
1117            assert_ne!(meta.name, "UNKNOWN", "{field:?} has no name");
1118            assert_ne!(meta.title.trim(), "?", "{field:?} has no header title");
1119            assert!(
1120                !meta.description.is_empty(),
1121                "{field:?} has no description for the setup screen"
1122            );
1123            // The name is the config key and the `--sort` argument, so it must
1124            // resolve back to the same field.
1125            assert_eq!(
1126                Field::from_name(meta.name),
1127                Some(field),
1128                "{field:?} does not round-trip through {}",
1129                meta.name
1130            );
1131        }
1132    }
1133
1134    /// The same check for `Field::all()`: a variant with metadata but not in the
1135    /// table is equally invisible, because the setup screen iterates the table.
1136    #[test]
1137    fn every_metadata_entry_is_offered_by_all() {
1138        let offered: Vec<Field> = Field::all().collect();
1139        for field in &offered {
1140            assert!(field.meta().is_some());
1141        }
1142        for wanted in [Field::Session, Field::Tty, Field::Cwd, Field::ProcComm] {
1143            assert!(
1144                offered.contains(&wanted),
1145                "{wanted:?} must be selectable in the setup screen"
1146            );
1147        }
1148    }
1149
1150    /// Names must be unique, or `from_name` would resolve to whichever came first.
1151    #[test]
1152    fn names_are_unique() {
1153        let mut seen = std::collections::HashSet::new();
1154        for field in Field::all() {
1155            assert!(seen.insert(field.name()), "duplicate name {}", field.name());
1156        }
1157    }
1158}