scxtop/
main.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
6use scx_utils::compat;
7use scxtop::bpf_skel::types::bpf_event;
8use scxtop::cli::{generate_completions, Cli, Commands, TraceArgs, TuiArgs};
9use scxtop::config::Config;
10use scxtop::edm::{ActionHandler, BpfEventActionPublisher, BpfEventHandler, EventDispatchManager};
11use scxtop::mangoapp::poll_mangoapp;
12use scxtop::read_file_string;
13use scxtop::tracer::Tracer;
14use scxtop::Action;
15use scxtop::App;
16use scxtop::Event;
17use scxtop::Key;
18use scxtop::KeyMap;
19use scxtop::PerfettoTraceManager;
20use scxtop::Tui;
21use scxtop::SCHED_NAME_PATH;
22use scxtop::{bpf_skel::*, AppState};
23
24use anyhow::anyhow;
25use anyhow::Result;
26use clap::{CommandFactory, Parser};
27use libbpf_rs::skel::OpenSkel;
28use libbpf_rs::skel::SkelBuilder;
29use libbpf_rs::Link;
30use libbpf_rs::ProgramInput;
31use libbpf_rs::RingBufferBuilder;
32use libbpf_rs::UprobeOpts;
33use log::debug;
34use log::info;
35use ratatui::crossterm::event::{KeyCode::Char, KeyEvent};
36use simplelog::{
37    ColorChoice, Config as SimplelogConfig, LevelFilter, TermLogger, TerminalMode, WriteLogger,
38};
39use std::sync::atomic::AtomicBool;
40use tokio::sync::mpsc;
41
42use std::ffi::CString;
43use std::fs::File;
44use std::mem::MaybeUninit;
45use std::str::FromStr;
46use std::sync::atomic::Ordering;
47use std::sync::Arc;
48use std::time::Duration;
49
50fn get_action(app: &App, keymap: &KeyMap, event: Event) -> Action {
51    match event {
52        Event::Error => Action::None,
53        Event::Tick => Action::Tick,
54        Event::TickRateChange(tick_rate_ms) => {
55            Action::TickRateChange(std::time::Duration::from_millis(tick_rate_ms))
56        }
57        Event::Key(key) => handle_key_event(app, keymap, key),
58        Event::Paste(paste) => match app.state() {
59            AppState::Event => Action::InputEntry(paste),
60            _ => Action::None,
61        },
62        _ => Action::None,
63    }
64}
65
66fn handle_key_event(app: &App, keymap: &KeyMap, key: KeyEvent) -> Action {
67    match key.code {
68        Char(c) => match app.state() {
69            AppState::Event => Action::InputEntry(c.to_string()),
70            _ => keymap.action(&Key::Char(c)),
71        },
72        _ => keymap.action(&Key::Code(key.code)),
73    }
74}
75
76/// Attaches BPF programs to the skel.
77fn attach_progs(skel: &mut BpfSkel) -> Result<Vec<Link>> {
78    // Attach probes
79    let mut links = vec![
80        skel.progs.on_sched_cpu_perf.attach()?,
81        skel.progs.scx_sched_reg.attach()?,
82        skel.progs.scx_sched_unreg.attach()?,
83        skel.progs.on_sched_switch.attach()?,
84        skel.progs.on_sched_wakeup.attach()?,
85        skel.progs.on_sched_wakeup_new.attach()?,
86        skel.progs.on_sched_waking.attach()?,
87    ];
88
89    // 6.13 compatibility
90    if compat::ksym_exists("scx_bpf_dsq_insert_vtime")? {
91        if let Ok(link) = skel.progs.scx_insert_vtime.attach() {
92            links.push(link);
93        }
94        if let Ok(link) = skel.progs.scx_insert.attach() {
95            links.push(link);
96        }
97        if let Ok(link) = skel.progs.scx_dsq_move.attach() {
98            links.push(link);
99        }
100        if let Ok(link) = skel.progs.scx_dsq_move_set_vtime.attach() {
101            links.push(link);
102        }
103        if let Ok(link) = skel.progs.scx_dsq_move_set_slice.attach() {
104            links.push(link);
105        }
106    } else {
107        if let Ok(link) = skel.progs.scx_dispatch.attach() {
108            links.push(link);
109        }
110        if let Ok(link) = skel.progs.scx_dispatch_vtime.attach() {
111            links.push(link);
112        }
113        if let Ok(link) = skel.progs.scx_dispatch_from_dsq_set_vtime.attach() {
114            links.push(link);
115        }
116        if let Ok(link) = skel.progs.scx_dispatch_from_dsq_set_slice.attach() {
117            links.push(link);
118        }
119        if let Ok(link) = skel.progs.scx_dispatch_from_dsq.attach() {
120            links.push(link);
121        }
122    }
123    if let Ok(link) = skel.progs.on_cpuhp_enter.attach() {
124        links.push(link);
125    }
126    if let Ok(link) = skel.progs.on_cpuhp_exit.attach() {
127        links.push(link);
128    }
129    if let Ok(link) = skel.progs.on_pstate_sample.attach() {
130        links.push(link);
131    }
132
133    Ok(links)
134}
135
136fn run_trace(trace_args: &TraceArgs) -> Result<()> {
137    TermLogger::init(
138        match trace_args.verbose {
139            0 => simplelog::LevelFilter::Info,
140            1 => simplelog::LevelFilter::Debug,
141            _ => simplelog::LevelFilter::Trace,
142        },
143        SimplelogConfig::default(),
144        TerminalMode::Mixed,
145        ColorChoice::Auto,
146    )?;
147
148    let config = Config::default_config();
149    let worker_threads = config.worker_threads() as usize;
150    tokio::runtime::Builder::new_multi_thread()
151        .enable_all()
152        .worker_threads(if worker_threads > 2 {
153            worker_threads
154        } else {
155            4
156        })
157        .build()
158        .unwrap()
159        .block_on(async {
160            let (action_tx, mut action_rx) = mpsc::unbounded_channel();
161
162            let mut open_object = MaybeUninit::uninit();
163            let mut builder = BpfSkelBuilder::default();
164            if trace_args.verbose > 2 {
165                builder.obj_builder.debug(true);
166            }
167
168            let skel = builder.open(&mut open_object)?;
169            // set sample rate to 1 here to populate the BPF tctxs
170            skel.maps.data_data.sample_rate = 1;
171            compat::cond_kprobe_enable("gpu_memory_total", &skel.progs.on_gpu_memory_total)?;
172            compat::cond_kprobe_enable("hw_pressure_update", &skel.progs.on_hw_pressure_update)?;
173
174            let mut skel = skel.load()?;
175            let mut links = attach_progs(&mut skel)?;
176            links.push(skel.progs.on_sched_fork.attach()?);
177            links.push(skel.progs.on_sched_exec.attach()?);
178            links.push(skel.progs.on_sched_exit.attach()?);
179
180            let trace_dur = std::time::Duration::from_millis(trace_args.trace_ms);
181            let bpf_publisher = BpfEventActionPublisher::new(action_tx.clone());
182
183            let mut event_rbb = RingBufferBuilder::new();
184            let mut edm = EventDispatchManager::new(None, None);
185            let warmup_done = Arc::new(AtomicBool::new(false));
186            edm.register_bpf_handler(Box::new(bpf_publisher));
187            let event_handler = move |data: &[u8]| {
188                let mut event = bpf_event::default();
189                plain::copy_from_bytes(&mut event, data).expect("Event data buffer was too short");
190                let _ = edm.on_event(&event);
191                0
192            };
193
194            event_rbb.add(&skel.maps.events, event_handler)?;
195            let event_rb = event_rbb.build()?;
196            let shutdown = Arc::new(AtomicBool::new(false));
197            let stop_poll = shutdown.clone();
198            let stop_main = shutdown.clone();
199
200            tokio::spawn(async move {
201                loop {
202                    let _ = event_rb.poll(Duration::from_millis(1));
203                    if stop_poll.load(Ordering::Relaxed) {
204                        debug!("polling stopped");
205                        break;
206                    }
207                }
208            });
209
210            let (complete_tx, mut complete_rx) = mpsc::channel(1);
211            let trace_file_prefix = config.trace_file_prefix().to_string();
212            let trace_file = trace_args.output_file.clone();
213            let mut trace_manager = PerfettoTraceManager::new(trace_file_prefix, None);
214            info!("warming up for {}ms", trace_args.warmup_ms);
215            tokio::time::sleep(Duration::from_millis(trace_args.warmup_ms)).await;
216            debug!("starting trace");
217            let thread_warmup = warmup_done.clone();
218            trace_manager.start()?;
219            thread_warmup.store(true, Ordering::Relaxed);
220            tokio::spawn(async move {
221                let mut count = 0;
222                loop {
223                    let action = action_rx.recv().await;
224                    if let Some(a) = action {
225                        if thread_warmup.load(Ordering::Relaxed) {
226                            count += 1;
227                            trace_manager.on_action(&a).unwrap();
228                        }
229                    }
230                    if stop_main.load(Ordering::Relaxed) {
231                        trace_manager.stop(trace_file, None).unwrap();
232                        info!("trace complete, collected {} events", count);
233                        let _ = complete_tx.send(1).await;
234                        break;
235                    }
236                }
237            });
238
239            let mut tracer = Tracer::new(skel);
240            tracer.trace_async(trace_dur).await?;
241
242            // The order is important here:
243            // 1) first drop the links to detach the attached BPF programs
244            // 2) set the shutdown variable to stop background tokio threads
245            // 3) wait for the completion of the trace file generation to complete
246            drop(links);
247            shutdown.store(true, Ordering::Relaxed);
248            let _ = complete_rx.recv().await;
249
250            let stats = tracer.stats()?;
251            info!("{:?}", stats);
252
253            Ok(())
254        })
255}
256
257fn run_tui(tui_args: &TuiArgs) -> Result<()> {
258    if let Ok(log_path) = std::env::var("RUST_LOG_PATH") {
259        let log_level = match std::env::var("RUST_LOG") {
260            Ok(v) => LevelFilter::from_str(&v)?,
261            Err(_) => LevelFilter::Info,
262        };
263
264        WriteLogger::init(
265            log_level,
266            simplelog::Config::default(),
267            File::create(log_path)?,
268        )?;
269
270        log_panics::Config::new()
271            .backtrace_mode(log_panics::BacktraceMode::Resolved)
272            .install_panic_hook();
273    };
274
275    let config = Config::merge([
276        Config::from(tui_args.clone()),
277        Config::load().unwrap_or(Config::default_config()),
278    ]);
279    let keymap = config.active_keymap.clone();
280
281    tokio::runtime::Builder::new_multi_thread()
282        .enable_all()
283        .worker_threads(config.worker_threads() as usize)
284        .build()
285        .unwrap()
286        .block_on(async {
287            let (action_tx, mut action_rx) = mpsc::unbounded_channel();
288
289            let mut open_object = MaybeUninit::uninit();
290            let mut builder = BpfSkelBuilder::default();
291            if config.debug() {
292                builder.obj_builder.debug(true);
293            }
294            let bpf_publisher = BpfEventActionPublisher::new(action_tx.clone());
295            let mut edm = EventDispatchManager::new(None, None);
296            edm.register_bpf_handler(Box::new(bpf_publisher));
297
298            let skel = builder.open(&mut open_object)?;
299            skel.maps.rodata_data.long_tail_tracing_min_latency_ns =
300                tui_args.experimental_long_tail_tracing_min_latency_ns;
301
302            compat::cond_kprobe_enable("gpu_memory_total", &skel.progs.on_gpu_memory_total)?;
303            compat::cond_kprobe_enable("hw_pressure_update", &skel.progs.on_hw_pressure_update)?;
304            let mut skel = skel.load()?;
305            let mut links = attach_progs(&mut skel)?;
306            skel.progs.scxtop_init.test_run(ProgramInput::default())?;
307
308            if tui_args.experimental_long_tail_tracing {
309                skel.maps.data_data.trace_duration_ns = config.trace_duration_ns();
310                skel.maps.data_data.trace_warmup_ns = config.trace_warmup_ns();
311
312                let binary = tui_args
313                    .experimental_long_tail_tracing_binary
314                    .clone()
315                    .unwrap();
316                let symbol = tui_args
317                    .experimental_long_tail_tracing_symbol
318                    .clone()
319                    .unwrap();
320
321                links.extend([
322                    skel.progs.long_tail_tracker_exit.attach_uprobe_with_opts(
323                        -1, /* pid, -1 == all */
324                        binary.clone(),
325                        0,
326                        UprobeOpts {
327                            retprobe: true,
328                            func_name: symbol.clone(),
329                            ..Default::default()
330                        },
331                    )?,
332                    skel.progs.long_tail_tracker_entry.attach_uprobe_with_opts(
333                        -1, /* pid, -1 == all */
334                        binary.clone(),
335                        0,
336                        UprobeOpts {
337                            retprobe: false,
338                            func_name: symbol.clone(),
339                            ..Default::default()
340                        },
341                    )?,
342                ]);
343            };
344
345            let mut tui = Tui::new(keymap.clone(), config.tick_rate_ms())?;
346            let mut event_rbb = RingBufferBuilder::new();
347            let event_handler = move |data: &[u8]| {
348                let mut event = bpf_event::default();
349                plain::copy_from_bytes(&mut event, data).expect("Event data buffer was too short");
350                let _ = edm.on_event(&event);
351                0
352            };
353            event_rbb.add(&skel.maps.events, event_handler)?;
354            let event_rb = event_rbb.build()?;
355            let scheduler = read_file_string(SCHED_NAME_PATH).unwrap_or("".to_string());
356
357            let mut app = App::new(
358                config,
359                scheduler,
360                100,
361                tui_args.process_id,
362                action_tx.clone(),
363                skel,
364            )?;
365
366            tui.enter()?;
367
368            let shutdown = app.should_quit.clone();
369            tokio::spawn(async move {
370                loop {
371                    let _ = event_rb.poll(Duration::from_millis(1));
372                    if shutdown.load(Ordering::Relaxed) {
373                        break;
374                    }
375                }
376            });
377
378            if tui_args.mangoapp_tracing {
379                let stop_mangoapp = app.should_quit.clone();
380                let mangoapp_path = CString::new(tui_args.mangoapp_path.clone()).unwrap();
381                let poll_intvl_ms = tui_args.mangoapp_poll_intvl_ms;
382                let tx = action_tx.clone();
383                tokio::spawn(async move {
384                    poll_mangoapp(
385                        mangoapp_path,
386                        poll_intvl_ms,
387                        tx,
388                        stop_mangoapp,
389                    )
390                    .await
391                });
392            }
393
394
395            loop {
396                tokio::select! {
397                    ev = tui.next() => {
398                        let ev = ev?;
399                        match ev {
400                            Event::Quit => { action_tx.send(Action::Quit)?; },
401                            Event::Tick => action_tx.send(Action::Tick)?,
402                            Event::TickRateChange(tick_rate_ms) => action_tx.send(
403                                Action::TickRateChange(std::time::Duration::from_millis(tick_rate_ms)),
404                            )?,
405                            Event::Render => {
406                                if app.should_quit.load(Ordering::Relaxed) {
407                                    break;
408                                }
409                                tui.draw(|f| app.render(f).expect("Failed to render application"))?;
410                            }
411                            Event::Key(_) => {
412                                let action = get_action(&app, &keymap, ev);
413                                action_tx.send(action)?;
414                            }
415                            _ => {}
416                    }}
417
418                    ac = action_rx.recv() => {
419                        let ac = ac.ok_or(anyhow!("actions channel closed"))?;
420                        app.handle_action(&ac)?;
421                    }
422                }
423            }
424            tui.exit()?;
425            drop(links);
426
427            Ok(())
428        })
429}
430
431fn main() -> Result<()> {
432    let args = Cli::parse();
433
434    match &args.command.unwrap_or(Commands::Tui(args.tui)) {
435        Commands::Tui(tui_args) => {
436            run_tui(tui_args)?;
437        }
438        Commands::Trace(trace_args) => {
439            run_trace(trace_args)?;
440        }
441        Commands::GenerateCompletions { shell, output } => {
442            generate_completions(Cli::command(), *shell, output.clone())
443                .unwrap_or_else(|_| panic!("Failed to generate completions for {}", shell));
444        }
445    }
446    Ok(())
447}