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#[derive(Parser)]
30#[command(name = "hugetop", version, about, max_term_width = MAX_TERM_WIDTH)]
31pub struct Args {
32 #[arg(short, long, default_value = "2")]
34 delay: f64,
35
36 #[arg(short, long)]
38 batch: bool,
39
40 #[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 let hugetlb_kb = status.hugetlbpages.unwrap_or(0);
156
157 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 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 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}