Optimize EBPF memory handling and PID indexing. (#22050)
- Replace heap allocations with inline Judy array storage for efficiency. - Add macros for value conversion, ensuring safer and more compact logic. - Reduce fragmentation and improve memory reuse in allocator settings. - Trim unused memory after BPF initialization to minimize memory footprint.
Stelios Fragkakis committed
Mar 29, 2026 at 22:52 UTC
fe4eec7199f88322cff0d26f33574c1b1ffe8da3
2 files changed
+44
-27
src/collectors/collectors-ipc/ebpf-ipc.c
+25
-27
@@ -9,21 +9,29 @@ sem_t *shm_mutex_ebpf_integration = SEM_FAILED;
9
static Pvoid_t ebpf_ipc_JudyL = NULL;
10
ebpf_user_mem_stat_t ebpf_stat_values;
11
12
-static uint32_t *ebpf_shm_find_index_unsafe(uint32_t pid)
12
+// Judy stores index+1 directly in the pointer slot to avoid heap allocation.
13
+// 0 means "not found" (Judy returns NULL for missing keys),
14
+// so we offset by 1: stored = index+1, retrieved = stored-1.
15
+#define IDX_TO_JVALUE(idx) ((Pvoid_t)((Word_t)(idx) + 1))
16
+#define JVALUE_TO_IDX(pv) ((uint32_t)((Word_t)(pv) - 1))
17
+#define JVALUE_IS_VALID(pv) ((pv) != NULL)
18
+
19
+static bool ebpf_shm_find_index_unsafe(uint32_t pid, uint32_t *result)
20
{
21
Pvoid_t *Pvalue = JudyLGet(ebpf_ipc_JudyL, (Word_t)pid, PJE0);
15
- if (Pvalue)
16
- return *Pvalue;
17
- return NULL;
22
+ if (Pvalue && JVALUE_IS_VALID(*Pvalue)) {
23
+ *result = JVALUE_TO_IDX(*Pvalue);
24
+ return true;
25
+ }
26
+ return false;
27
}
28
29
static bool ebpf_find_pid_shm_del_unsafe(uint32_t pid, enum ebpf_pids_index shm_idx)
30
{
22
- uint32_t *lpid = ebpf_shm_find_index_unsafe(pid);
23
- if (!lpid)
31
+ uint32_t idx;
32
+ if (!ebpf_shm_find_index_unsafe(pid, &idx))
33
return false;
34
26
- uint32_t idx = *lpid;
35
if (idx >= ebpf_stat_values.current)
36
return false;
37
@@ -35,17 +43,17 @@ static bool ebpf_find_pid_shm_del_unsafe(uint32_t pid, enum ebpf_pids_index shm_
43
if (ptr->threads)
44
return true;
45
38
- freez(lpid);
46
(void)JudyLDel(&ebpf_ipc_JudyL, (Word_t)pid, PJE0);
47
ebpf_stat_values.current--;
48
49
if (idx == ebpf_stat_values.current)
50
return false;
51
52
+ // Compact: move last entry into the freed slot
53
uint32_t last_pid = integration_shm[ebpf_stat_values.current].pid;
46
- uint32_t *last_lpid = ebpf_shm_find_index_unsafe(last_pid);
47
- if (last_lpid) {
48
- *last_lpid = idx;
54
+ Pvoid_t *Pvalue = JudyLGet(ebpf_ipc_JudyL, (Word_t)last_pid, PJE0);
55
+ if (Pvalue && JVALUE_IS_VALID(*Pvalue)) {
56
+ *Pvalue = IDX_TO_JVALUE(idx);
57
memcpy(ptr, &integration_shm[ebpf_stat_values.current], sizeof(*ptr));
58
}
59
@@ -54,9 +62,9 @@ static bool ebpf_find_pid_shm_del_unsafe(uint32_t pid, enum ebpf_pids_index shm_
62
63
static uint32_t ebpf_find_or_create_index_pid(uint32_t pid)
64
{
57
- uint32_t *idx = ebpf_shm_find_index_unsafe(pid);
58
- if (idx)
59
- return *idx;
65
+ uint32_t idx;
66
+ if (ebpf_shm_find_index_unsafe(pid, &idx))
67
+ return idx;
68
69
if (ebpf_stat_values.current >= ebpf_stat_values.total)
70
return UINT32_MAX;
@@ -65,9 +73,7 @@ static uint32_t ebpf_find_or_create_index_pid(uint32_t pid)
73
internal_fatal(!Pvalue || Pvalue == PJERR, "EBPF: pid judy index");
74
75
uint32_t new_idx = ebpf_stat_values.current++;
68
- uint32_t *stored_idx = callocz(1, sizeof(uint32_t));
69
- *stored_idx = new_idx;
70
- *Pvalue = stored_idx;
76
+ *Pvalue = IDX_TO_JVALUE(new_idx);
77
78
return new_idx;
79
}
@@ -108,16 +114,8 @@ void netdata_integration_cleanup_shm()
114
integration_shm = NULL;
115
}
116
111
- Word_t index = 0;
112
- Word_t next_index;
113
- PPvoid_t pid_ptr;
114
- while ((pid_ptr = JudyLFirst(ebpf_ipc_JudyL, &index, PJE0)) != NULL) {
115
- uint32_t *pid = *(uint32_t **)pid_ptr;
116
- next_index = index;
117
- freez(pid);
118
- JudyLDel(&ebpf_ipc_JudyL, next_index, PJE0);
119
- index = next_index;
120
- }
117
+ // Values are stored inline (no heap allocation), just free the Judy array
118
+ (void)JudyLFreeArray(&ebpf_ipc_JudyL, PJE0);
119
ebpf_ipc_JudyL = NULL;
120
121
if (shm_fd_ebpf_integration > 0) {
src/collectors/ebpf.plugin/ebpf.c
+19
@@ -2299,6 +2299,19 @@ static void ebpf_signal_stop_handler(int sig)
2299
*/
2300
int main(int argc, char **argv)
2301
{
2302
+ // Reduce memory footprint:
2303
+ // - Single malloc arena avoids fragmentation across 24+ threads
2304
+ // - Allocations >1MB use mmap so they're returned to OS on free,
2305
+ // preventing 30MB+ of brk heap holes from libbpf's temp buffers
2306
+ // - THP disabled prevents 2MB huge page waste in sparse allocations
2307
+#if defined(HAVE_C_MALLOPT)
2308
+ mallopt(M_ARENA_MAX, 1);
2309
+ mallopt(M_MMAP_THRESHOLD, 1024 * 1024);
2310
+#endif
2311
+#if defined(HAVE_SYS_PRCTL_H) && defined(PR_SET_THP_DISABLE)
2312
+ prctl(PR_SET_THP_DISABLE, 1, 0, 0, 0);
2313
+#endif
2314
+
2315
nd_log_initialize_for_external_plugins(NETDATA_EBPF_PLUGIN_NAME);
2316
netdata_threads_init_for_external_plugins(0);
2317
@@ -2373,6 +2386,12 @@ int main(int argc, char **argv)
2386
}
2387
}
2388
2389
+ // BPF loading allocated ~50MB then freed most of it.
2390
+ // glibc keeps freed pages in its free list; trim returns them to the OS.
2391
+#if defined(HAVE_C_MALLOC_TRIM)
2392
+ malloc_trim(0);
2393
+#endif
2394
+
2395
heartbeat_t hb;
2396
heartbeat_init(&hb, USEC_PER_SEC);
2397
int update_apps_every = (int)EBPF_CFG_UPDATE_APPS_EVERY_DEFAULT;