The FreeRADIUS server $Id: f3670dba8951ca10eb4948feb3dc3db9423a334f $
Loading...
Searching...
No Matches
xlat_profiling.c
Go to the documentation of this file.
1/*
2 * This program is free software; you can redistribute it and/or modify
3 * it under the terms of the GNU General Public License as published by
4 * the Free Software Foundation; either version 2 of the License, or
5 * (at your option) any later version.
6 *
7 * This program is distributed in the hope that it will be useful,
8 * but WITHOUT ANY WARRANTY; without even the implied warranty of
9 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 * GNU General Public License for more details.
11 *
12 * You should have received a copy of the GNU General Public License
13 * along with this program; if not, write to the Free Software
14 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301, USA
15 */
16
17/**
18 * $Id: 0b3059a7b886fb58ea5c574d46c689652874faf5 $
19 *
20 * @file xlat_profiling.c
21 * @brief Functions for controlling profilers attached to the running server
22 *
23 * The comments in this file use the terms of each profiler. Callgrind
24 * counts costs (instruction fetches, cache misses, branch mispredictions)
25 * into one cost centre per function, and writes the cost centres as a
26 * profile dump. gperftools records samples of the program counter on a
27 * timer, and writes the samples to a profile file.
28 *
29 * @copyright 2026 Arran Cudbard-Bell (a.cudbardb@freeradius.org)
30 */
31RCSID("$Id: 0b3059a7b886fb58ea5c574d46c689652874faf5 $")
32
33#include <freeradius-devel/server/base.h>
34#include <freeradius-devel/unlang/xlat_priv.h>
35#include <fcntl.h>
36
37#ifdef HAVE_VALGRIND_CALLGRIND_H
38# include <valgrind/callgrind.h>
39
40/** Log and return false if the server is not running under valgrind
41 *
42 * A natively running process ignores callgrind client requests. Every
43 * callgrind function therefore fails when valgrind is not attached, rather
44 * than silently succeeding. A misconfigured profiling run then shows in
45 * the log instead of producing an empty profile.
46 */
47static bool callgrind_attached(request_t *request)
48{
49 if (RUNNING_ON_VALGRIND) return true;
50
51 RERROR("Refusing callgrind request, the server is not running under valgrind");
52 return false;
53}
54
55/** Switch callgrind instrumentation on
56 *
57 * Start valgrind with `valgrind --tool=callgrind --instr-atstart=no`, then
58 * call `%callgrind.start()` from the `server.start` trigger to keep server
59 * startup out of the profile. Instrumentation is process-wide, so the
60 * thread that calls the function does not matter.
61 *
62 * Use this function when the profile should begin later than process
63 * start. Configuration parsing and module instantiation have finished
64 * when the `server.start` trigger fires.
65 *
66@verbatim
67%callgrind.start()
68@endverbatim
69 *
70 * @ingroup xlat_functions
71 */
72static xlat_action_t xlat_func_callgrind_start(TALLOC_CTX *ctx, fr_dcursor_t *out,
73 UNUSED xlat_ctx_t const *xctx,
74 request_t *request, UNUSED fr_value_box_list_t *args)
75{
76 fr_value_box_t *dst;
77
78 if (!callgrind_attached(request)) return XLAT_ACTION_FAIL;
79
80 CALLGRIND_START_INSTRUMENTATION;
81
82 MEM(dst = fr_value_box_alloc(ctx, FR_TYPE_BOOL, NULL));
83 dst->vb_bool = true;
85
86 return XLAT_ACTION_DONE;
87}
88
89/** Switch callgrind instrumentation off
90 *
91 * Use this function when the profile should end before process exit. A
92 * call from the `server.stop` trigger keeps thread teardown out of the
93 * profile. A call from a policy ends the profile after a chosen section
94 * of unlang. If no trigger or policy calls this function, callgrind
95 * keeps counting costs until process exit and writes the profile dump at
96 * exit.
97 *
98@verbatim
99%callgrind.stop()
100@endverbatim
101 *
102 * @ingroup xlat_functions
103 */
104static xlat_action_t xlat_func_callgrind_stop(TALLOC_CTX *ctx, fr_dcursor_t *out,
105 UNUSED xlat_ctx_t const *xctx,
106 request_t *request, UNUSED fr_value_box_list_t *args)
107{
108 fr_value_box_t *dst;
109
110 if (!callgrind_attached(request)) return XLAT_ACTION_FAIL;
111
112 CALLGRIND_STOP_INSTRUMENTATION;
113
114 MEM(dst = fr_value_box_alloc(ctx, FR_TYPE_BOOL, NULL));
115 dst->vb_bool = true;
117
118 return XLAT_ACTION_DONE;
119}
120
121static xlat_arg_parser_t const xlat_func_callgrind_dump_args[] = {
122 { .required = false, .concat = true, .type = FR_TYPE_STRING },
124};
125
126/** Write the cost data collected so far to a profile dump, then reset cost data
127 *
128 * Callgrind records the optional label in the description field of the dump.
129 * The label lets the reader distinguish the dumps when one run produces several.
130 *
131 * Use this function when one run should produce several profile dumps,
132 * such as one dump per load step, or one dump for the load phase and one
133 * dump for shutdown. Instrumentation stays on, and each dump holds only
134 * the costs counted since the previous dump. `callgrind_annotate`
135 * accepts several dumps at once.
136 *
137@verbatim
138%callgrind.dump([<label>])
139@endverbatim
140 *
141 * @ingroup xlat_functions
142 */
143static xlat_action_t xlat_func_callgrind_dump(TALLOC_CTX *ctx, fr_dcursor_t *out,
144 UNUSED xlat_ctx_t const *xctx,
145 request_t *request, fr_value_box_list_t *args)
146{
147 fr_value_box_t *dst, *label;
148
149 XLAT_ARGS(args, &label);
150
151 if (!callgrind_attached(request)) return XLAT_ACTION_FAIL;
152
153 if (label) {
154 CALLGRIND_DUMP_STATS_AT(label->vb_strvalue);
155 } else {
156 CALLGRIND_DUMP_STATS;
157 }
158
159 MEM(dst = fr_value_box_alloc(ctx, FR_TYPE_BOOL, NULL));
160 dst->vb_bool = true;
162
163 return XLAT_ACTION_DONE;
164}
165
166/** Zero the costs collected so far without writing a dump
167 *
168 * Use this function to discard the costs of a period that the profile
169 * should not include. Examples are the cache warmup after
170 * `%callgrind.start()`, and a settling period after a load step changes
171 * the request rate. Call this function when the period ends. The next
172 * profile dump, or the dump at process exit, then holds only the costs
173 * counted after the call.
174 *
175@verbatim
176%callgrind.zero()
177@endverbatim
178 *
179 * @ingroup xlat_functions
180 */
181static xlat_action_t xlat_func_callgrind_zero(TALLOC_CTX *ctx, fr_dcursor_t *out,
182 UNUSED xlat_ctx_t const *xctx,
183 request_t *request, UNUSED fr_value_box_list_t *args)
184{
185 fr_value_box_t *dst;
186
187 if (!callgrind_attached(request)) return XLAT_ACTION_FAIL;
188
189 CALLGRIND_ZERO_STATS;
190
191 MEM(dst = fr_value_box_alloc(ctx, FR_TYPE_BOOL, NULL));
192 dst->vb_bool = true;
194
195 return XLAT_ACTION_DONE;
196}
197#endif /* HAVE_VALGRIND_CALLGRIND_H */
198
199#ifdef HAVE_GPERFTOOLS_PROFILER_H
200# include <gperftools/profiler.h>
201/** Start the gperftools CPU profiler, writing samples to a file
202 *
203 * The function fails if the profiler is already running, so a second
204 * start never silently discards a running profile. The `limit files { ... }`
205 * section restricts the filename in the same way as for the `%file.*`
206 * functions.
207 *
208 * Use this function to measure where the server spends wall-clock time.
209 * The profiler samples the program counter on a timer at close to native
210 * speed, so the server handles a realistic load while the profiler runs.
211 * Callgrind measures instruction and cache behaviour instead, and slows
212 * the server by a large factor. The `server.start` trigger is the usual
213 * place to call this function.
214 *
215@verbatim
216%gperftools.start(<filename>)
217@endverbatim
218 *
219 * @ingroup xlat_functions
220 */
221static xlat_action_t xlat_func_gperftools_start(TALLOC_CTX *ctx, fr_dcursor_t *out,
222 UNUSED xlat_ctx_t const *xctx,
223 request_t *request, fr_value_box_list_t *args)
224{
225 fr_value_box_t *dst, *vb;
226 struct ProfilerState state;
227
228 XLAT_ARGS(args, &vb);
229 fr_assert(vb->type == FR_TYPE_STRING);
230
231 if (!xlat_file_allowed(request, vb, O_RDWR)) return XLAT_ACTION_FAIL;
232
233 ProfilerGetCurrentState(&state);
234 if (state.enabled) {
235 RERROR("Profiler already running, writing to %s", state.profile_name);
236 return XLAT_ACTION_FAIL;
237 }
238
239 if (ProfilerStart(vb->vb_strvalue) == 0) {
240 RERROR("Failed starting profiler with output file %s", vb->vb_strvalue);
241 return XLAT_ACTION_FAIL;
242 }
243
244 MEM(dst = fr_value_box_alloc(ctx, FR_TYPE_BOOL, NULL));
245 dst->vb_bool = true;
247
248 return XLAT_ACTION_DONE;
249}
250
251/** Log and return false if the gperftools CPU profiler is not running
252 */
253static bool gperftools_running(request_t *request)
254{
255 struct ProfilerState state;
256
257 ProfilerGetCurrentState(&state);
258 if (state.enabled) return true;
259
260 RERROR("Profiler not running");
261 return false;
262}
263
264/** Write the buffered gperftools samples to the profile file, then stop the profiler
265 *
266 * Use this function to end the profile at a known point, usually from
267 * the `server.stop` trigger. `%gperftools.start()` starts a stopped
268 * profiler again with a new profile file.
269 *
270@verbatim
271%gperftools.stop()
272@endverbatim
273 *
274 * @ingroup xlat_functions
275 */
276static xlat_action_t xlat_func_gperftools_stop(TALLOC_CTX *ctx, fr_dcursor_t *out,
277 UNUSED xlat_ctx_t const *xctx,
278 request_t *request, UNUSED fr_value_box_list_t *args)
279{
280 fr_value_box_t *dst;
281
282 if (!gperftools_running(request)) return XLAT_ACTION_FAIL;
283
284 ProfilerFlush();
285 ProfilerStop();
286
287 MEM(dst = fr_value_box_alloc(ctx, FR_TYPE_BOOL, NULL));
288 dst->vb_bool = true;
290
291 return XLAT_ACTION_DONE;
292}
293
294/** Write the buffered gperftools samples to the profile file and keep the profiler running
295 *
296 * Use this function during a long run, so that `pprof` can read a partial
297 * profile while the server keeps running. Also use this function before
298 * a step that might crash the server. The buffered samples then reach
299 * the profile file before the crash.
300 *
301@verbatim
302%gperftools.flush()
303@endverbatim
304 *
305 * @ingroup xlat_functions
306 */
307static xlat_action_t xlat_func_gperftools_flush(TALLOC_CTX *ctx, fr_dcursor_t *out,
308 UNUSED xlat_ctx_t const *xctx,
309 request_t *request, UNUSED fr_value_box_list_t *args)
310{
311 fr_value_box_t *dst;
312
313 if (!gperftools_running(request)) return XLAT_ACTION_FAIL;
314
315 ProfilerFlush();
316
317 MEM(dst = fr_value_box_alloc(ctx, FR_TYPE_BOOL, NULL));
318 dst->vb_bool = true;
320
321 return XLAT_ACTION_DONE;
322}
323#endif /* HAVE_GPERFTOOLS_PROFILER_H */
324
325/** Register the control functions for every profiler that the build compiled in
326 *
327 * @return
328 * - 0 on success.
329 * - -1 on failure.
330 */
332{
333#if defined(HAVE_VALGRIND_CALLGRIND_H) || defined(HAVE_GPERFTOOLS_PROFILER_H)
334 xlat_t *xlat;
335
336#define XLAT_REGISTER_PROFILING(_xlat, _func, _args) \
337do { \
338 if (unlikely((xlat = xlat_func_register(NULL, _xlat, _func, FR_TYPE_BOOL)) == NULL)) return -1; \
339 xlat_func_args_set(xlat, _args); \
340 xlat_func_flags_set(xlat, XLAT_FUNC_FLAG_INTERNAL); \
341} while (0)
342
343#define XLAT_REGISTER_PROFILING_VOID(_xlat, _func) \
344do { \
345 if (unlikely((xlat = xlat_func_register(NULL, _xlat, _func, FR_TYPE_BOOL)) == NULL)) return -1; \
346 xlat_func_flags_set(xlat, XLAT_FUNC_FLAG_INTERNAL); \
347} while (0)
348
349# ifdef HAVE_VALGRIND_CALLGRIND_H
350 XLAT_REGISTER_PROFILING_VOID("callgrind.start", xlat_func_callgrind_start);
351 XLAT_REGISTER_PROFILING_VOID("callgrind.stop", xlat_func_callgrind_stop);
352 XLAT_REGISTER_PROFILING("callgrind.dump", xlat_func_callgrind_dump, xlat_func_callgrind_dump_args);
353 XLAT_REGISTER_PROFILING_VOID("callgrind.zero", xlat_func_callgrind_zero);
354# endif
355
356# ifdef HAVE_GPERFTOOLS_PROFILER_H
357 XLAT_REGISTER_PROFILING("gperftools.start", xlat_func_gperftools_start, xlat_func_file_name_args);
358 XLAT_REGISTER_PROFILING_VOID("gperftools.stop", xlat_func_gperftools_stop);
359 XLAT_REGISTER_PROFILING_VOID("gperftools.flush", xlat_func_gperftools_flush);
360# endif
361
362#undef XLAT_REGISTER_PROFILING
363#undef XLAT_REGISTER_PROFILING_VOID
364#endif
365
366 return 0;
367}
va_list args
Definition acutest.h:770
#define RCSID(id)
Definition build.h:560
#define UNUSED
Definition build.h:384
static int fr_dcursor_append(fr_dcursor_t *cursor, void *v)
Insert a single item at the end of the list.
Definition dcursor.h:406
#define MEM(x)
Definition debug.h:38
#define RUNNING_ON_VALGRIND
Definition dl.c:40
#define RERROR(fmt,...)
Definition log.h:315
@ FR_TYPE_STRING
String of printable characters.
@ FR_TYPE_BOOL
A truth value.
#define fr_assert(_expr)
Definition rad_assert.h:37
#define XLAT_ARGS(_list,...)
Populate local variables with value boxes from the input list.
Definition xlat.h:373
unsigned int required
Argument must be present, and non-empty.
Definition xlat.h:136
#define XLAT_ARG_PARSER_TERMINATOR
Definition xlat.h:160
xlat_action_t
Definition xlat.h:37
@ XLAT_ACTION_FAIL
An xlat function failed.
Definition xlat.h:44
@ XLAT_ACTION_DONE
We're done evaluating this level of nesting.
Definition xlat.h:43
Definition for a single argument consumed by an xlat function.
Definition xlat.h:135
#define fr_value_box_alloc(_ctx, _type, _enumv)
Allocate a value box of a specific type.
Definition value.h:669
static fr_sbuff_err_t char ** out
Definition value.h:1062
bool xlat_file_allowed(request_t *request, fr_value_box_t const *vb, int oflags)
xlat_arg_parser_t const xlat_func_file_name_args[]
An xlat calling ctx.
Definition xlat_ctx.h:49
int xlat_profiling_init(void)
Register the control functions for every profiler that the build compiled in.