Skip to main content

profile_bee/
ebpf.rs

1use anyhow::anyhow;
2use aya::maps::{Array, HashMap, MapData, RingBuf, StackTraceMap};
3use aya::programs::{
4    perf_event::{PerfEventScope, PerfTypeId, SamplePolicy},
5    KProbe, PerfEvent,
6};
7use aya::programs::{RawTracePoint, TracePoint, UProbe};
8use aya::{include_bytes_aligned, util::online_cpus};
9use aya::{Btf, Ebpf, EbpfLoader};
10
11use aya::Pod;
12use profile_bee_common::{ProcInfo, ProcInfoKey, UnwindEntry};
13
14// Create a newtype wrapper around StackInfo
15#[repr(transparent)]
16#[derive(Debug, Clone, Copy)]
17pub struct StackInfoPod(pub profile_bee_common::StackInfo);
18unsafe impl Pod for StackInfoPod {}
19
20#[repr(transparent)]
21#[derive(Debug, Clone, Copy)]
22pub struct FramePointersPod(pub profile_bee_common::FramePointers);
23unsafe impl Pod for FramePointersPod {}
24
25#[repr(transparent)]
26#[derive(Debug, Clone, Copy)]
27pub struct UnwindEntryPod(pub UnwindEntry);
28unsafe impl Pod for UnwindEntryPod {}
29
30#[repr(transparent)]
31#[derive(Debug, Clone, Copy)]
32pub struct ProcInfoKeyPod(pub ProcInfoKey);
33unsafe impl Pod for ProcInfoKeyPod {}
34
35#[repr(transparent)]
36#[derive(Clone, Copy)]
37pub struct ProcInfoPod(pub ProcInfo);
38unsafe impl Pod for ProcInfoPod {}
39
40/// Wrapper for eBPF stuff
41#[derive(Debug)]
42pub struct EbpfProfiler {
43    pub bpf: Ebpf,
44    /// eBPF stacktrace map
45    pub stack_traces: StackTraceMap<MapData>,
46    /// Count stacktraces
47    pub counts: HashMap<MapData, StackInfoPod, u64>,
48    /// Custom storage of StackTrace IPs
49    pub stacked_pointers: HashMap<MapData, StackInfoPod, FramePointersPod>,
50}
51/// Legacy configuration for uprobe attachment (single target).
52/// Kept for backward compatibility; prefer `ResolvedProbe` for new code.
53#[derive(Debug, Clone)]
54pub struct UProbeConfig {
55    /// Function name (with optional +offset)
56    pub function: String,
57    /// Path to binary/library (absolute or library name)
58    pub path: String,
59    /// Whether this is a return probe
60    pub is_retprobe: bool,
61    /// Optional PID to attach to (None = all processes)
62    pub pid: Option<i32>,
63}
64
65/// Configuration for the new smart uprobe system.
66/// Holds one or more resolved probe targets for multi-attach.
67#[derive(Debug, Clone)]
68pub struct SmartUProbeConfig {
69    /// Resolved probe targets to attach to.
70    pub probes: Vec<crate::probe_resolver::ResolvedProbe>,
71    /// Optional PID to attach to (None = all processes).
72    pub pid: Option<i32>,
73}
74
75pub struct ProfilerConfig {
76    pub skip_idle: bool,
77    pub stream_mode: u8,
78    pub frequency: u64,
79    pub kprobe: Option<String>,
80    /// Legacy single-target uprobe config (backward compat).
81    pub uprobe: Option<UProbeConfig>,
82    /// New smart uprobe config with multi-attach support.
83    pub smart_uprobe: Option<SmartUProbeConfig>,
84    pub tracepoint: Option<String>,
85    /// Raw tracepoint name for syscall raw_tp (e.g., "sys_enter").
86    /// Uses collect_trace_raw_syscall which reads pt_regs from args[0].
87    pub raw_tracepoint: Option<String>,
88    /// Raw tracepoint name for generic raw_tp with task pt_regs (e.g., "sched_switch").
89    /// Uses bpf_get_current_task_btf() + bpf_task_pt_regs() for full FP/DWARF unwinding.
90    /// Requires kernel >= 5.15; falls back to raw_tracepoint_generic on older kernels.
91    pub raw_tracepoint_task_regs: Option<String>,
92    /// Raw tracepoint name for generic raw_tp (e.g., "sched_switch").
93    /// Uses collect_trace_raw_tp_generic which relies on bpf_get_stackid only.
94    pub raw_tracepoint_generic: Option<String>,
95    /// Target syscall number for raw tracepoint filtering (-1 = all).
96    pub target_syscall_nr: i64,
97    pub pid: Option<u32>,
98    pub cpu: Option<u32>,
99    pub self_profile: bool,
100    pub dwarf: bool,
101}
102
103/// Creates an aya Ebpf object
104pub fn load_ebpf(config: &ProfilerConfig) -> Result<Ebpf, anyhow::Error> {
105    // The eBPF object file is selected by build.rs: it uses a freshly-built
106    // binary from `cargo xtask build-ebpf` if available, otherwise the prebuilt
107    // binary shipped in ebpf-bin/. Either way it ends up in OUT_DIR.
108    let data = include_bytes_aligned!(concat!(env!("OUT_DIR"), "/profile-bee.bpf.o"));
109
110    let skip_idle = if config.skip_idle { 1u8 } else { 0u8 };
111    let dwarf_enabled = if config.dwarf { 1u8 } else { 0u8 };
112    let target_syscall_nr: i64 = config.target_syscall_nr;
113
114    let bpf = EbpfLoader::new()
115        .set_global("SKIP_IDLE", &skip_idle, true)
116        .set_global("NOTIFY_TYPE", &config.stream_mode, true)
117        .set_global("DWARF_ENABLED", &dwarf_enabled, true)
118        .set_global("TARGET_SYSCALL_NR", &target_syscall_nr, true)
119        .btf(Btf::from_sys_fs().ok().as_ref())
120        .load(data)
121        .map_err(|e| {
122            println!("{:?}", e);
123            e
124        })?;
125
126    // // this might be useful for debugging, but definitely disable bpf logging for performance purposes
127    // aya_log::EbpfLogger::init(&mut bpf)?;
128
129    Ok(bpf)
130}
131
132pub fn setup_ebpf_profiler(config: &ProfilerConfig) -> Result<EbpfProfiler, anyhow::Error> {
133    let mut bpf = load_ebpf(config)?;
134
135    if let Some(kprobe) = &config.kprobe {
136        let program: &mut KProbe = bpf.program_mut("kprobe_profile").unwrap().try_into()?;
137        program.load()?;
138        program.attach(kprobe, 0)?;
139    } else if let Some(smart) = &config.smart_uprobe {
140        // New smart uprobe path: attach to all resolved probe targets
141        if smart.probes.is_empty() {
142            return Err(anyhow!("no probe targets resolved — nothing to attach"));
143        }
144
145        // Separate probes by type (uprobe vs uretprobe)
146        let has_uprobe = smart.probes.iter().any(|p| !p.is_ret);
147        let has_uretprobe = smart.probes.iter().any(|p| p.is_ret);
148
149        if has_uprobe {
150            let program: &mut UProbe = bpf.program_mut("uprobe_profile").unwrap().try_into()?;
151            program.load()?;
152
153            for probe in smart.probes.iter().filter(|p| !p.is_ret) {
154                let display_name = probe.demangled.as_deref().unwrap_or(&probe.symbol_name);
155                eprintln!(
156                    "  attaching uprobe: {}:{} (0x{:x})",
157                    probe.library_path.display(),
158                    display_name,
159                    probe.address,
160                );
161                program.attach(
162                    Some(probe.symbol_name.as_str()),
163                    probe.offset,
164                    probe.library_path.to_str().ok_or_else(|| {
165                        anyhow!("non-UTF8 library path: {}", probe.library_path.display())
166                    })?,
167                    smart.pid,
168                )?;
169            }
170        }
171
172        if has_uretprobe {
173            let program: &mut UProbe = bpf.program_mut("uretprobe_profile").unwrap().try_into()?;
174            program.load()?;
175
176            for probe in smart.probes.iter().filter(|p| p.is_ret) {
177                let display_name = probe.demangled.as_deref().unwrap_or(&probe.symbol_name);
178                eprintln!(
179                    "  attaching uretprobe: {}:{} (0x{:x})",
180                    probe.library_path.display(),
181                    display_name,
182                    probe.address,
183                );
184                program.attach(
185                    Some(probe.symbol_name.as_str()),
186                    probe.offset,
187                    probe.library_path.to_str().ok_or_else(|| {
188                        anyhow!("non-UTF8 library path: {}", probe.library_path.display())
189                    })?,
190                    smart.pid,
191                )?;
192            }
193        }
194    } else if let Some(uprobe_config) = &config.uprobe {
195        // Choose the right program based on is_retprobe flag
196        let program_name = if uprobe_config.is_retprobe {
197            "uretprobe_profile"
198        } else {
199            "uprobe_profile"
200        };
201
202        let program: &mut UProbe = bpf.program_mut(program_name).unwrap().try_into()?;
203        program.load()?;
204
205        // Parse function name and offset (format: "function_name" or "function_name+offset")
206        let (fn_name, offset) = if let Some(plus_pos) = uprobe_config.function.find('+') {
207            let (name, offset_str) = uprobe_config.function.split_at(plus_pos);
208            let offset = offset_str[1..]
209                .parse::<u64>()
210                .map_err(|e| anyhow::anyhow!("Invalid offset in uprobe function: {}", e))?;
211            (Some(name), offset)
212        } else {
213            (Some(uprobe_config.function.as_str()), 0)
214        };
215
216        program.attach(
217            fn_name,
218            offset,
219            uprobe_config.path.as_str(),
220            uprobe_config.pid,
221        )?;
222    } else if let Some(raw_tp) = &config.raw_tracepoint {
223        // Pick the correct syscall raw_tp program based on enter vs exit
224        let prog_name = if raw_tp == "sys_exit" {
225            "raw_tp_sys_exit"
226        } else {
227            "raw_tp_sys_enter"
228        };
229        let program: &mut RawTracePoint = bpf.program_mut(prog_name).unwrap().try_into()?;
230        program.load()?;
231        program.attach(raw_tp)?;
232    } else if let Some(raw_tp) = &config.raw_tracepoint_task_regs {
233        let program: &mut RawTracePoint =
234            bpf.program_mut("raw_tp_with_regs").unwrap().try_into()?;
235        program.load()?;
236        program.attach(raw_tp)?;
237    } else if let Some(raw_tp) = &config.raw_tracepoint_generic {
238        let program: &mut RawTracePoint = bpf.program_mut("raw_tp_generic").unwrap().try_into()?;
239        program.load()?;
240        program.attach(raw_tp)?;
241    } else if let Some(tracepoint) = &config.tracepoint {
242        let program: &mut TracePoint = bpf.program_mut("tracepoint_profile").unwrap().try_into()?;
243        program.load()?;
244
245        let mut split = tracepoint.split(':');
246        let category = split.next().expect("category");
247        let name = split.next().expect("name");
248
249        program.attach(category, name)?;
250    } else {
251        let program: &mut PerfEvent = bpf.program_mut("profile_cpu").unwrap().try_into()?;
252
253        program.load()?;
254
255        // https://elixir.bootlin.com/linux/v4.2/source/include/uapi/linux/perf_event.h#L103
256        const PERF_COUNT_SW_CPU_CLOCK: u64 = 0;
257
258        // could change this to Hardware if your system supports
259        // `lscpu | grep -i pmu`
260        let perf_type = PerfTypeId::Software;
261
262        if config.self_profile {
263            program.attach(
264                perf_type,
265                PERF_COUNT_SW_CPU_CLOCK,
266                PerfEventScope::CallingProcessAnyCpu,
267                SamplePolicy::Frequency(config.frequency),
268                true,
269            )?;
270        } else if config.pid.is_some() || config.cpu.is_some() {
271            // When filtering by PID or CPU, attach to all/specific CPUs
272            // and let eBPF filter by tgid. This allows profiling child processes.
273            let cpus = if let Some(cpu) = config.cpu {
274                vec![cpu]
275            } else {
276                online_cpus().map_err(|(_, error)| error)?
277            };
278
279            let nprocs = cpus.len();
280            if let Some(pid) = config.pid {
281                eprintln!(
282                    "Profiling PID {} and child processes across {} CPUs",
283                    pid, nprocs
284                );
285            } else if let Some(cpu) = config.cpu {
286                eprintln!("Profiling CPU {}", cpu);
287            }
288
289            for cpu in cpus {
290                program.attach(
291                    perf_type.clone(),
292                    PERF_COUNT_SW_CPU_CLOCK,
293                    PerfEventScope::AllProcessesOneCpu { cpu },
294                    SamplePolicy::Frequency(config.frequency),
295                    true,
296                )?;
297            }
298        } else {
299            let cpus = online_cpus().map_err(|(_, error)| error)?;
300            let nprocs = cpus.len();
301            eprintln!("CPUs: {}", nprocs);
302
303            for cpu in cpus {
304                program.attach(
305                    perf_type.clone(),
306                    PERF_COUNT_SW_CPU_CLOCK,
307                    PerfEventScope::AllProcessesOneCpu { cpu },
308                    SamplePolicy::Frequency(config.frequency),
309                    true,
310                )?;
311            }
312        }
313    }
314
315    let stack_traces = StackTraceMap::try_from(
316        bpf.take_map("stack_traces")
317            .ok_or(anyhow!("stack_traces not found"))?,
318    )?;
319
320    let counts = bpf
321        .take_map("counts")
322        .ok_or(anyhow!("counts not found"))?
323        .try_into()?;
324
325    let stacked_pointers = bpf
326        .take_map("stacked_pointers")
327        .ok_or(anyhow!("stacked_pointers not found"))?
328        .try_into()?;
329
330    Ok(EbpfProfiler {
331        bpf,
332        stack_traces,
333        counts,
334        stacked_pointers,
335    })
336}
337
338pub fn setup_ring_buffer(bpf: &mut Ebpf) -> Result<RingBuf<MapData>, anyhow::Error> {
339    let ring_buf = RingBuf::try_from(
340        bpf.take_map("RING_BUF_STACKS")
341            .ok_or(anyhow!("RING_BUF_STACKS not found"))?,
342    )?;
343
344    Ok(ring_buf)
345}
346
347impl EbpfProfiler {
348    /// Set the target PID for eBPF filtering (0 = profile all processes)
349    pub fn set_target_pid(&mut self, pid: u32) -> Result<(), anyhow::Error> {
350        let mut target_pid_map: Array<&mut MapData, u32> = Array::try_from(
351            self.bpf
352                .map_mut("target_pid_map")
353                .ok_or(anyhow!("target_pid_map not found"))?,
354        )?;
355        target_pid_map.set(0, pid, 0)?;
356        Ok(())
357    }
358
359    /// Load DWARF unwind tables into eBPF maps for a process
360    pub fn load_dwarf_unwind_tables(
361        &mut self,
362        manager: &crate::dwarf_unwind::DwarfUnwindManager,
363    ) -> Result<(), anyhow::Error> {
364        // Load all shards
365        let all_shard_ids: Vec<u8> = manager.binary_tables.keys().copied().collect();
366        self.update_dwarf_tables(manager, &all_shard_ids)
367    }
368
369    /// Load a single shard's unwind entries into its eBPF Array map.
370    fn load_shard(&mut self, shard_id: u8, entries: &[UnwindEntry]) -> Result<(), anyhow::Error> {
371        let map_name = format!("shard_{}", shard_id);
372        let mut arr: Array<&mut MapData, UnwindEntryPod> = Array::try_from(
373            self.bpf
374                .map_mut(&map_name)
375                .ok_or_else(|| anyhow!("{} map not found", map_name))?,
376        )?;
377        for (idx, entry) in entries.iter().enumerate() {
378            arr.set(idx as u32, UnwindEntryPod(*entry), 0)?;
379        }
380        Ok(())
381    }
382
383    /// Incrementally update eBPF maps with new unwind shard entries,
384    /// and refresh all proc_info entries.
385    pub fn update_dwarf_tables(
386        &mut self,
387        manager: &crate::dwarf_unwind::DwarfUnwindManager,
388        new_shard_ids: &[u8],
389    ) -> Result<(), anyhow::Error> {
390        if !new_shard_ids.is_empty() {
391            let mut total_entries = 0usize;
392            for &shard_id in new_shard_ids {
393                if let Some(entries) = manager.binary_tables.get(&shard_id) {
394                    self.load_shard(shard_id, entries)?;
395                    total_entries += entries.len();
396                    tracing::info!("Loaded shard {} with {} entries", shard_id, entries.len());
397                }
398            }
399
400            tracing::info!(
401                "Loaded {} total unwind entries across {} shards",
402                total_entries,
403                new_shard_ids.len()
404            );
405        }
406
407        let mut proc_info_map: HashMap<&mut MapData, ProcInfoKeyPod, ProcInfoPod> =
408            HashMap::try_from(
409                self.bpf
410                    .map_mut("proc_info")
411                    .ok_or(anyhow!("proc_info map not found"))?,
412            )?;
413
414        for (&tgid, proc_info) in &manager.proc_info {
415            let key = ProcInfoKeyPod(ProcInfoKey { tgid, _pad: 0 });
416            let value = ProcInfoPod(*proc_info);
417            proc_info_map.insert(key, value, 0)?;
418
419            tracing::info!(
420                "Loaded process info for tgid {} ({} mappings)",
421                tgid,
422                proc_info.mapping_count,
423            );
424        }
425
426        Ok(())
427    }
428}