@cryptotaxi247 / netdata-1 / commits / d46928ad5

Optimize health DB lookups (#22246)

* optimize health DB lookups by reusing onewayalloc arena and adding reset functionality * Enhance onewayalloc reset functionality to improve memory management and clarify usage in comments

Stelios Fragkakis committed Apr 22, 2026 at 18:42 UTC d46928ad5ecbde931bda0ef7066fd889fd19548e
5 files changed +162 -6
src/health/health_event_loop.c
+29 -3
@@ -317,7 +317,13 @@ static void do_eval_expression(
317 }
318
319 // returns the number of runnable alerts
320 -static void health_event_loop_for_host(RRDHOST *host, bool apply_hibernation_delay, time_t now, time_t *next_run) {
320 +//
321 +// The caller owns `owa` and provides it so a single arena can be reused
322 +// across all hosts of one iteration (and, in the multi-threaded variant,
323 +// across all hosts a single worker processes). The arena is reset between
324 +// alerts inside this function, so peak memory stays bounded by one alert's
325 +// scratch no matter how many hosts flow through it.
326 +static void health_event_loop_for_host(RRDHOST *host, bool apply_hibernation_delay, time_t now, time_t *next_run, ONEWAYALLOC *owa) {
327 size_t runnable = 0;
328 struct health_alert_status_counts status_counts = { 0 };
329 bool snapshot_complete = true;
@@ -369,9 +375,22 @@ static void health_event_loop_for_host(RRDHOST *host, bool apply_hibernation_del
375 return;
376 }
377
378 + // Reuse the caller-provided arena across every alert's DB lookup. Each
379 + // alert's scratch (RRDR + query state) is released via onewayalloc_reset
380 + // at the top of the next iteration — trims the page list back to a
381 + // single head page. Net effect: one mmap/munmap for the whole health
382 + // iteration, regardless of host count or alert count.
383 +
384 // the first loop is to lookup values from the db
385 RRDCALC *rc;
386 foreach_rrdcalc_in_rrdhost_read(host, rc) {
387 + // Reclaim the previous alert's query scratch before starting the
388 + // next one. The arena is reused across every alert of every host in
389 + // this iteration, so this reset trims trailing pages left by the
390 + // previous alert (same host or prior host). No-op only on the very
391 + // first alert after the arena was created.
392 + onewayalloc_reset(owa);
393 +
394 if(unlikely(!service_running(SERVICE_HEALTH) || !rrdhost_should_run_health(host))) {
395 snapshot_complete = false;
396 break;
@@ -464,7 +483,8 @@ static void health_event_loop_for_host(RRDHOST *host, bool apply_hibernation_del
483 break;
484 }
485
467 - int ret = rrdset2value_api_v1(rc->rrdset, NULL, &rc->value, rrdcalc_dimensions(rc), 1,
486 + int ret = rrdset2value_api_v1_with_owa(owa,
487 + rc->rrdset, NULL, &rc->value, rrdcalc_dimensions(rc), 1,
488 rc->config.after, rc->config.before, rc->config.time_group, group_options,
489 0, rc->config.options | RRDR_OPTION_SELECTED_TIER,
490 &rc->db_after,&rc->db_before,
@@ -781,15 +801,21 @@ static void health_event_loop(void) {
801 worker_is_busy(WORKER_HEALTH_JOB_RRD_LOCK);
802 uint64_t loop = __atomic_add_fetch(&health_evloop_iteration, 1, __ATOMIC_RELAXED);
803
804 + // Single onewayalloc arena reused across every host in this iteration —
805 + // one mmap/munmap pair for the whole cycle instead of per host.
806 + ONEWAYALLOC *iter_owa = onewayalloc_create(0);
807 +
808 RRDHOST *host;
809 dfe_start_reentrant(rrdhost_root_index, host) {
810 if(unlikely(!service_running(SERVICE_HEALTH)))
811 break;
812
789 - health_event_loop_for_host(host, apply_hibernation_delay, now, &next_run);
813 + health_event_loop_for_host(host, apply_hibernation_delay, now, &next_run, iter_owa);
814 }
815 dfe_done(host);
816
817 + onewayalloc_destroy(iter_owa);
818 +
819 if(unlikely(!service_running(SERVICE_HEALTH)))
820 break;
821
src/libnetdata/onewayalloc/onewayalloc.c
+45
@@ -184,6 +184,51 @@ void *onewayalloc_doublesize(ONEWAYALLOC *owa, const void *src, size_t oldsize)
184 return dst;
185 }
186
187 +void onewayalloc_reset(ONEWAYALLOC *owa) {
188 + if (!owa) return;
189 +
190 +#ifdef FSANITIZE_ADDRESS
191 + // Under the sanitizer path, onewayalloc_mallocz goes straight to the
192 + // system allocator and nothing is tracked in the owa page list — there
193 + // is nothing to reset. Individual allocations are released by callers
194 + // via onewayalloc_freez() (which calls freez() under the sanitizer).
195 + return;
196 +#endif
197 +
198 + OWA_PAGE *head = (OWA_PAGE *)owa;
199 +
200 + // Free every page except the head; we keep the head so the caller can
201 + // reuse the arena without another mmap.
202 + size_t freed_size = 0;
203 + OWA_PAGE *page = head->next;
204 + while (page) {
205 + OWA_PAGE *p = page;
206 + page = page->next;
207 + freed_size += p->size;
208 + if (p->mmap)
209 + nd_munmap(p, p->size);
210 + else
211 + freez(p);
212 + }
213 +
214 + if (freed_size)
215 + __atomic_sub_fetch(&onewayalloc_total_memory, freed_size, __ATOMIC_RELAXED);
216 +
217 + // Roll the head page's bump cursor back to the position right after the
218 + // OWA_PAGE header, and rewire the single-page list so head == last.
219 + head->next = NULL;
220 + head->last = head;
221 + head->offset = natural_alignment(sizeof(OWA_PAGE));
222 +
223 + // stats_pages / stats_pages_size describe the arena's *current* footprint
224 + // (what is mapped right now), so they reflect the single-page post-reset
225 + // state. stats_mallocs_made / stats_mallocs_size are lifetime counters
226 + // (total allocations ever served by this arena) and are intentionally
227 + // preserved across resets, to stay useful for diagnostics.
228 + head->stats_pages = 1;
229 + head->stats_pages_size = head->size;
230 +}
231 +
232 void onewayalloc_destroy(ONEWAYALLOC *owa) {
233 if(!owa) return;
234
src/libnetdata/onewayalloc/onewayalloc.h
+15
@@ -8,6 +8,21 @@ typedef void ONEWAYALLOC;
8 ONEWAYALLOC *onewayalloc_create(size_t size_hint);
9 void onewayalloc_destroy(ONEWAYALLOC *owa);
10
11 +// Reset the arena to an empty state without destroying it. Frees every
12 +// page except the head, and rolls the head page's bump pointer back to
13 +// its initial offset so the arena is reusable. Intended for callers that
14 +// do many short bursts of allocations back-to-back (e.g. evaluating all
15 +// the alerts of one host) and want to amortise the mmap/munmap cost of
16 +// create/destroy across the whole burst.
17 +//
18 +// Important: reset does NOT zero the retained head page. Subsequent
19 +// onewayalloc_mallocz() calls may return memory that still holds bytes
20 +// from the previous burst — this matches the mallocz() contract (the "z"
21 +// means "fatal on failure", not "zeroed"). Callers that need zeroed
22 +// memory must use onewayalloc_callocz() just as they would against a
23 +// freshly-created arena; the reset path does not relax that contract.
24 +void onewayalloc_reset(ONEWAYALLOC *owa);
25 +
26 void *onewayalloc_mallocz(ONEWAYALLOC *owa, size_t size);
27 void *onewayalloc_callocz(ONEWAYALLOC *owa, size_t nmemb, size_t size);
28 char *onewayalloc_strdupz(ONEWAYALLOC *owa, const char *s);
src/web/api/formatters/rrd2json.c
+41 -3
@@ -10,8 +10,9 @@ void rrd_stats_api_v1_chart(RRDSET *st, BUFFER *wb)
10 buffer_json_finalize(wb);
11 }
12
13 -int rrdset2value_api_v1(
14 - RRDSET *st
13 +int rrdset2value_api_v1_with_owa(
14 + ONEWAYALLOC *owa
15 + , RRDSET *st
16 , BUFFER *wb
17 , NETDATA_DOUBLE *n
18 , const char *dimensions
@@ -34,9 +35,10 @@ int rrdset2value_api_v1(
35 , QUERY_SOURCE query_source
36 , STORAGE_PRIORITY priority
37 ) {
38 + internal_fatal(!owa, "rrdset2value_api_v1_with_owa(): owa must be non-NULL");
39 +
40 int ret = HTTP_RESP_INTERNAL_SERVER_ERROR;
41
39 - ONEWAYALLOC *owa = onewayalloc_create(0);
42 RRDR *r = rrd2rrdr_legacy(
43 owa,
44 st,
@@ -95,6 +97,42 @@ int rrdset2value_api_v1(
97
98 cleanup:
99 rrdr_free(owa, r);
100 + return ret;
101 +}
102 +
103 +int rrdset2value_api_v1(
104 + RRDSET *st
105 + , BUFFER *wb
106 + , NETDATA_DOUBLE *n
107 + , const char *dimensions
108 + , size_t points
109 + , time_t after
110 + , time_t before
111 + , RRDR_TIME_GROUPING group_method
112 + , const char *group_options
113 + , time_t resampling_time
114 + , uint32_t options
115 + , time_t *db_after
116 + , time_t *db_before
117 + , size_t *db_points_read
118 + , size_t *db_points_per_tier
119 + , size_t *result_points_generated
120 + , int *value_is_null
121 + , NETDATA_DOUBLE *anomaly_rate
122 + , time_t timeout
123 + , size_t tier
124 + , QUERY_SOURCE query_source
125 + , STORAGE_PRIORITY priority
126 +) {
127 + // Back-compat wrapper for callers that don't need to reuse the arena
128 + // across calls (e.g. the badge code path). Creates a throwaway owa.
129 + ONEWAYALLOC *owa = onewayalloc_create(0);
130 + int ret = rrdset2value_api_v1_with_owa(
131 + owa, st, wb, n, dimensions, points, after, before,
132 + group_method, group_options, resampling_time, options,
133 + db_after, db_before, db_points_read, db_points_per_tier,
134 + result_points_generated, value_is_null, anomaly_rate,
135 + timeout, tier, query_source, priority);
136 onewayalloc_destroy(owa);
137 return ret;
138 }
src/web/api/formatters/rrd2json.h
+32
@@ -49,6 +49,38 @@ int rrdset2value_api_v1(
49 , STORAGE_PRIORITY priority
50 );
51
52 +// Same contract as rrdset2value_api_v1 but uses a caller-provided
53 +// onewayalloc arena. Lets a caller that issues many back-to-back queries
54 +// (e.g. evaluating all alerts on one host) amortise the mmap/munmap cost
55 +// of owa create/destroy across the whole burst. The caller owns the owa's
56 +// lifetime; between calls they should invoke onewayalloc_reset() to reclaim
57 +// trailing pages and keep peak memory bounded. `owa` must be non-NULL.
58 +int rrdset2value_api_v1_with_owa(
59 + ONEWAYALLOC *owa
60 + , RRDSET *st
61 + , BUFFER *wb
62 + , NETDATA_DOUBLE *n
63 + , const char *dimensions
64 + , size_t points
65 + , time_t after
66 + , time_t before
67 + , RRDR_TIME_GROUPING group_method
68 + , const char *group_options
69 + , time_t resampling_time
70 + , uint32_t options
71 + , time_t *db_after
72 + , time_t *db_before
73 + , size_t *db_points_read
74 + , size_t *db_points_per_tier
75 + , size_t *result_points_generated
76 + , int *value_is_null
77 + , NETDATA_DOUBLE *anomaly_rate
78 + , time_t timeout
79 + , size_t tier
80 + , QUERY_SOURCE query_source
81 + , STORAGE_PRIORITY priority
82 +);
83 +
84 static inline bool rrdr_dimension_should_be_exposed(RRDR_DIMENSION_FLAGS rrdr_dim_flags, RRDR_OPTIONS options) {
85 if(unlikely((options & RRDR_OPTION_RETURN_RAW) && (rrdr_dim_flags & RRDR_DIMENSION_QUERIED)))
86 return true;