Skip to main content

procutils_hugetop/
lib.rs

1use clap::Parser;
2use crossterm::event::{self, Event, KeyCode, KeyEventKind};
3use procfs::{prelude::*, process};
4use procutils_common::{MAX_TERM_WIDTH, man::ManContent};
5use ratatui::{
6    DefaultTerminal, Frame,
7    layout::{Constraint, Layout},
8    style::{Color, Modifier, Style},
9    text::{Line, Span},
10    widgets::{Block, Borders, Cell, Paragraph, Row, Table},
11};
12use std::{process::ExitCode, time::Duration};
13
14pub const MAN: ManContent = ManContent {
15    description: Some(include_str!("../man/description.man")),
16    extra_sections: &[
17        ("KEY COMMANDS", include_str!("../man/key_commands.man")),
18        (
19            "FIELD DESCRIPTIONS",
20            include_str!("../man/field_descriptions.man"),
21        ),
22        ("EXAMPLES", include_str!("../man/examples.man")),
23        ("NOTES", include_str!("../man/notes.man")),
24        ("SEE ALSO", include_str!("../man/see_also.man")),
25    ],
26};
27
28/// Display hugepage usage by process.
29#[derive(Parser)]
30#[command(name = "hugetop", version, about, max_term_width = MAX_TERM_WIDTH)]
31pub struct Args {
32    /// Delay between updates in seconds.
33    #[arg(short, long, default_value = "2")]
34    delay: f64,
35
36    /// Batch mode (non-interactive, for piping).
37    #[arg(short, long)]
38    batch: bool,
39
40    /// Exit after this many display updates (only honoured with --batch).
41    #[arg(short = 'n', long)]
42    iterations: Option<u64>,
43}
44
45struct ProcessHuge {
46    pid: i32,
47    user: String,
48    comm: String,
49    hugetlb_kb: u64,
50    anon_huge_kb: u64,
51    total_huge_kb: u64,
52}
53
54struct SystemHuge {
55    anon_hugepages_kb: u64,
56    shmem_hugepages_kb: u64,
57    hugepages_total: u64,
58    hugepages_free: u64,
59    hugepages_rsvd: u64,
60    hugepages_surp: u64,
61    hugepagesize_kb: u64,
62    hugetlb_kb: u64,
63}
64
65impl SystemHuge {
66    fn read() -> Self {
67        let m = procfs::Meminfo::current().ok();
68        Self {
69            anon_hugepages_kb: m
70                .as_ref()
71                .and_then(|m| m.anon_hugepages)
72                .unwrap_or(0)
73                / 1024,
74            shmem_hugepages_kb: m
75                .as_ref()
76                .and_then(|m| m.shmem_hugepages)
77                .unwrap_or(0)
78                / 1024,
79            hugepages_total: m
80                .as_ref()
81                .and_then(|m| m.hugepages_total)
82                .unwrap_or(0),
83            hugepages_free: m
84                .as_ref()
85                .and_then(|m| m.hugepages_free)
86                .unwrap_or(0),
87            hugepages_rsvd: m
88                .as_ref()
89                .and_then(|m| m.hugepages_rsvd)
90                .unwrap_or(0),
91            hugepages_surp: m
92                .as_ref()
93                .and_then(|m| m.hugepages_surp)
94                .unwrap_or(0),
95            hugepagesize_kb: m
96                .as_ref()
97                .and_then(|m| m.hugepagesize)
98                .unwrap_or(0)
99                / 1024,
100            hugetlb_kb: m.as_ref().and_then(|m| m.hugetlb).unwrap_or(0) / 1024,
101        }
102    }
103}
104
105fn format_kb(kb: u64) -> String {
106    if kb >= 1_048_576 {
107        format!("{:.1}G", kb as f64 / 1_048_576.0)
108    } else if kb >= 1024 {
109        format!("{:.1}M", kb as f64 / 1024.0)
110    } else {
111        format!("{kb}K")
112    }
113}
114
115struct App {
116    processes: Vec<ProcessHuge>,
117    system: SystemHuge,
118    delay: Duration,
119    uid_cache: procutils_common::uid::UidCache,
120}
121
122impl App {
123    fn new(args: &Args) -> Self {
124        Self {
125            processes: Vec::new(),
126            system: SystemHuge::read(),
127            delay: Duration::from_secs_f64(args.delay.max(0.5)),
128            uid_cache: procutils_common::uid::UidCache::new(),
129        }
130    }
131
132    fn refresh(&mut self) {
133        self.system = SystemHuge::read();
134        self.processes.clear();
135
136        let all_procs = match process::all_processes() {
137            Ok(iter) => iter,
138            Err(_) => return,
139        };
140
141        for proc_result in all_procs {
142            let proc = match proc_result {
143                Ok(p) => p,
144                Err(_) => continue,
145            };
146
147            let status = match proc.status() {
148                Ok(s) => s,
149                Err(_) => continue,
150            };
151
152            // /proc/[pid]/status reports HugetlbPages in kB; procfs strips
153            // " kB" and returns the integer as-is (no /1024 needed). Other
154            // procfs fields (Meminfo, smaps) return bytes — those need /1024.
155            let hugetlb_kb = status.hugetlbpages.unwrap_or(0);
156
157            // Read AnonHugePages from smaps_rollup if available, fall back to status
158            let anon_huge_kb = proc
159                .smaps_rollup()
160                .ok()
161                .and_then(|r| {
162                    r.memory_map_rollup.0.first().and_then(|m| {
163                        m.extension.map.get("AnonHugePages").copied()
164                    })
165                })
166                .unwrap_or(0)
167                / 1024;
168
169            let total = hugetlb_kb + anon_huge_kb;
170            if total == 0 {
171                continue;
172            }
173
174            let stat = match proc.stat() {
175                Ok(s) => s,
176                Err(_) => continue,
177            };
178
179            self.processes.push(ProcessHuge {
180                pid: stat.pid,
181                user: self.uid_cache.get(status.euid).to_string(),
182                comm: stat.comm.clone(),
183                hugetlb_kb,
184                anon_huge_kb,
185                total_huge_kb: total,
186            });
187        }
188
189        self.processes
190            .sort_by(|a, b| b.total_huge_kb.cmp(&a.total_huge_kb));
191    }
192
193    fn draw(&self, frame: &mut Frame) {
194        let area = frame.area();
195
196        let chunks =
197            Layout::vertical([Constraint::Length(4), Constraint::Min(5)])
198                .split(area);
199
200        // System summary
201        let s = &self.system;
202        let summary = vec![
203            Line::from(vec![
204                Span::styled(" HugePages: ", Style::default().fg(Color::Cyan)),
205                Span::raw(format!(
206                    "total={}, free={}, rsvd={}, surp={}, size={}K",
207                    s.hugepages_total,
208                    s.hugepages_free,
209                    s.hugepages_rsvd,
210                    s.hugepages_surp,
211                    s.hugepagesize_kb,
212                )),
213            ]),
214            Line::from(vec![
215                Span::styled(
216                    " Transparent: ",
217                    Style::default().fg(Color::Cyan),
218                ),
219                Span::raw(format!(
220                    "anon={}, shmem={}",
221                    format_kb(s.anon_hugepages_kb),
222                    format_kb(s.shmem_hugepages_kb),
223                )),
224                Span::raw(format!("  HugeTLB: {}", format_kb(s.hugetlb_kb))),
225            ]),
226            Line::from(vec![Span::styled(
227                format!(" {} processes using hugepages", self.processes.len()),
228                Style::default().fg(Color::White),
229            )]),
230        ];
231
232        frame.render_widget(
233            Paragraph::new(summary)
234                .block(Block::default().borders(Borders::BOTTOM)),
235            chunks[0],
236        );
237
238        // Process table
239        let header = Row::new(vec![
240            Cell::from("PID")
241                .style(Style::default().add_modifier(Modifier::BOLD)),
242            Cell::from("USER")
243                .style(Style::default().add_modifier(Modifier::BOLD)),
244            Cell::from("HUGETLB")
245                .style(Style::default().add_modifier(Modifier::BOLD)),
246            Cell::from("ANON_HUGE")
247                .style(Style::default().add_modifier(Modifier::BOLD)),
248            Cell::from("TOTAL")
249                .style(Style::default().add_modifier(Modifier::BOLD)),
250            Cell::from("COMMAND")
251                .style(Style::default().add_modifier(Modifier::BOLD)),
252        ]);
253
254        let rows: Vec<Row> = self
255            .processes
256            .iter()
257            .map(|p| {
258                let color = if p.total_huge_kb >= 1_048_576 {
259                    Color::Red
260                } else if p.total_huge_kb >= 1024 {
261                    Color::Yellow
262                } else {
263                    Color::default()
264                };
265
266                Row::new(vec![
267                    Cell::from(p.pid.to_string()),
268                    Cell::from(p.user.clone()),
269                    Cell::from(format_kb(p.hugetlb_kb))
270                        .style(Style::default().fg(color)),
271                    Cell::from(format_kb(p.anon_huge_kb))
272                        .style(Style::default().fg(color)),
273                    Cell::from(format_kb(p.total_huge_kb)).style(
274                        Style::default().fg(color).add_modifier(Modifier::BOLD),
275                    ),
276                    Cell::from(p.comm.clone()),
277                ])
278            })
279            .collect();
280
281        let widths = [
282            Constraint::Length(8),
283            Constraint::Length(12),
284            Constraint::Length(10),
285            Constraint::Length(10),
286            Constraint::Length(10),
287            Constraint::Fill(1),
288        ];
289
290        let table = Table::new(rows, widths)
291            .header(header)
292            .block(Block::default().borders(Borders::NONE));
293
294        frame.render_widget(table, chunks[1]);
295    }
296}
297
298pub fn run(args: Args) -> ExitCode {
299    if args.batch {
300        return run_batch(&args);
301    }
302
303    let mut terminal = match ratatui::try_init() {
304        Ok(t) => t,
305        Err(e) => {
306            eprintln!("hugetop: failed to initialize terminal: {e}");
307            return ExitCode::FAILURE;
308        }
309    };
310
311    let result = run_app(&mut terminal, &args);
312
313    ratatui::restore();
314
315    match result {
316        Ok(()) => ExitCode::SUCCESS,
317        Err(e) => {
318            eprintln!("hugetop: {e}");
319            ExitCode::FAILURE
320        }
321    }
322}
323
324fn run_batch(args: &Args) -> ExitCode {
325    let mut app = App::new(args);
326    let mut iteration = 0u64;
327    let max = args.iterations.unwrap_or(1);
328
329    loop {
330        if iteration >= max {
331            break;
332        }
333        if iteration > 0 {
334            std::thread::sleep(app.delay);
335        }
336
337        app.refresh();
338        print_batch(&app);
339        iteration += 1;
340    }
341
342    ExitCode::SUCCESS
343}
344
345fn print_batch(app: &App) {
346    let s = &app.system;
347    println!(
348        "HugePages: total={}, free={}, rsvd={}, surp={}, size={}K",
349        s.hugepages_total,
350        s.hugepages_free,
351        s.hugepages_rsvd,
352        s.hugepages_surp,
353        s.hugepagesize_kb,
354    );
355    println!(
356        "Transparent: anon={}, shmem={}",
357        format_kb(s.anon_hugepages_kb),
358        format_kb(s.shmem_hugepages_kb),
359    );
360    println!("HugeTLB: {}", format_kb(s.hugetlb_kb));
361    println!("{} processes using hugepages", app.processes.len());
362    println!();
363
364    println!(
365        "{:>7} {:<10} {:>10} {:>10} {:>10} COMMAND",
366        "PID", "USER", "HUGETLB", "ANON_HUGE", "TOTAL",
367    );
368    for p in &app.processes {
369        println!(
370            "{:>7} {:<10} {:>10} {:>10} {:>10} {}",
371            p.pid,
372            p.user,
373            format_kb(p.hugetlb_kb),
374            format_kb(p.anon_huge_kb),
375            format_kb(p.total_huge_kb),
376            p.comm,
377        );
378    }
379}
380
381fn run_app(
382    terminal: &mut DefaultTerminal,
383    args: &Args,
384) -> Result<(), Box<dyn std::error::Error>> {
385    let mut app = App::new(args);
386
387    loop {
388        app.refresh();
389
390        terminal.draw(|frame| app.draw(frame))?;
391
392        if event::poll(app.delay)?
393            && let Event::Key(key) = event::read()?
394            && key.kind == KeyEventKind::Press
395            && key.code == KeyCode::Char('q')
396        {
397            return Ok(());
398        }
399    }
400}