Flow-IPC 3.0.0
Flow-IPC project: Full implementation reference.
memory_manager.hpp
Go to the documentation of this file.
1/* Flow-IPC: SHM-jemalloc
2 * Copyright (c) 2023 Akamai Technologies, Inc.; and other contributors.
3 * Each commit is copyright by its respective author or author's employer.
4 *
5 * Licensed under the MIT License:
6 *
7 * Permission is hereby granted, free of charge, to any person obtaining a copy
8 * of this software and associated documentation files (the "Software"), to deal
9 * in the Software without restriction, including without limitation the rights
10 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11 * copies of the Software, and to permit persons to whom the Software is
12 * furnished to do so, subject to the following conditions:
13 *
14 * The above copyright notice and this permission notice shall be included in
15 * all copies or substantial portions of the Software.
16 *
17 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23 * THE SOFTWARE. */
24
25/// @file
26#pragma once
27
30#include "ipc/util/util_fwd.hpp"
31#include <flow/error/error.hpp>
32#include <flow/log/log.hpp>
33#include <flow/util/string_view.hpp>
34#include <jemalloc/jemalloc.h>
35#include <cstddef>
36#include <iostream>
37#include <string>
38
40{
41
42// Types.
43
44/**
45 * Wrapper around jemalloc. This is a thin wrapper around the jemalloc API: used by SHM-jemalloc for its internal
46 * impl purposes but available for direct end user calls as well.
47 *
48 * ### Purpose ###
49 * It is usable directly as a small jemalloc wrangler. That is, it can make easier some of the more advanced
50 * activities, like working with custom-made arenas, including the surprisingly tricky business of destroying
51 * one safely. The stats-dump helpers like stats_dump_to_string() and stats_dump_to_ostream() are potentially
52 * nice too. (However they're also available through Ipc_arena::info_dump().) Last but not least, using this
53 * wrangler instead of direct jemalloc-API calls can avoid certain jemalloc landmines not yet fixed as of
54 * jemalloc-5.4.0 (see below).
55 *
56 * It won't block access to the actual jemalloc API.
57 *
58 * That said, the main purpose of its existence is for the SHM-provider SHM-jemalloc (see ipc::shm for overall
59 * intro to Flow-IPC's SHM-providers) to implement its services. For example Ipc_arena::construct() and
60 * Ipc_arena::allocate() will use Memory_manager::allocate(), while Ipc_arena::create() will create_arena()
61 * while hooking up its SHM-wrangling extent hooks that would execute as part of allocate() at times.
62 *
63 * The public access to these goodies seemed worthwhile as well, so we made it user-facing.
64 *
65 * @see also Thread_cache which provides a related, and fairly sophisticated, wrangler of the jemalloc tcache
66 * system. How to properly use the jemalloc tcache feature is not necessarily obvious, especially so if
67 * custom arenas with arbitrarily overlapping finite lifetimes are involved. Things are prone to crash --
68 * and not necessarily right when one does the wrong thing but, in some scenarios, later.
69 *
70 * ### SHM-jemalloc versus direct use of jemalloc ###
71 * Using jemalloc directly ("directly" in this case includes calls through Memory_manager) is possible with or
72 * without using SHM-jemalloc (centered on Ipc_arena) in parallel. As far as conflicts of using jemalloc
73 * directly versus SHM-jemalloc in parallel:
74 * - SHM-jemalloc [de]allocates() only through jemalloc-arenas it creates/destroys/manages by itself; never
75 * through the default arenas (i.e., it would not do a regular jemalloc-`malloc()` but rather always specifies
76 * some arena ID it itself maintains). This holds whether or not jemalloc is also your default
77 * `malloc()`/`free()`/`new`/`delete` for regular heap purposes.
78 * - Certain *global* (as opposed to per-arena) jemalloc config options are required for SHM-jemalloc to work
79 * properly. In some cases this is something we enforce programmatically: by emitting an error
80 * if something is set wrong (best-effort). Otherwise we assume the default/common setup is what
81 * is in effect and proceed under that assumption (see below `todo`). As of this writing the checking/emission
82 * occurs in Ipc_arena::create().
83 *
84 * @todo While the current SHM-jemalloc impl is thought to play nicely with other parallel jemalloc use (such
85 * as for vanilla heap allocation) -- with all allocation segregated into separately maintained jemalloc-arenas and
86 * with only very few enforced global option-setting requirements -- we should methodically explore
87 * and document what exactly SHM-jemalloc requires of the jemalloc config (both compiled-in and dynamically
88 * settable options). Things are OK given a standard jemalloc build, within a fairly wide (calendar-wise)
89 * version range, and we check some (a small # of) global settings we know we require, but a methodical analysis
90 * in the guts of SHM-jemalloc might yield more and save users some pain.
91 *
92 * ### Stats-dump versus arena creation/destruction ###
93 * We offer a thin wrapper for jemalloc's stats-dump-to-string dump feature (`malloc_stats_print()`):
94 * stats_dump_to_ostream(), stats_dump_to_string(), log_stats_dump(). It turns out that `malloc_stats_print()`
95 * is likely to crash (abort with a message like `<jemalloc>: Failure in xmallctlbymib()` and variations), if
96 * concurrently an arena is being destroyed or (conservatively speaking) created.
97 * (This is not fixed, or counted as a bug, as of jemalloc-5.4.0 (9/2026).)
98 *
99 * SHM-jemalloc works around it (internally by using a process-wide mutex), so as long as one uses our APIs
100 * (Flow-IPC generally for SHM-jemalloc arena control as needed, naturally; Ipc_arena::info_dump() or
101 * `Memory_manager::*stats_dump*()` for stats-dumping), one is safe from this problem. Just be warned that using
102 * these jemalloc APIs directly (for whatever reason) can lead to trouble, as direct calls will not know to
103 * collaborate with Flow-IPC's use of the internal mutex; and Flow-IPC may itself create/destroy arenas and
104 * dump stats at various times.
105 */
107{
108public:
109 // Types.
110
111 /**
112 * Wrapper around extent hooks, which are callbacks that can be instituted in jemalloc per arena. The
113 * prescribed usage is to implement desired callbacks that execute the implementation within the creating
114 * object (arg `owner` to ctor). The `extent_hooks*` callback parameter can be cast to a pointer of this type
115 * to retrieve the owner of the callback for execution in that class object's context.
116 *
117 * So to jemalloc, which is provided `this` as cast to `extent_hooks_t*`: It sees a table of free/`static`
118 * functions of yours and calls the appropriate one depending on the event (e.g.: new extent needed). (Next to
119 * that is stored the value `owner` given to ctor, but jemalloc does not know or care.) Each
120 * of those functions is given the `extent_hooks_t*` plus the hook-specific args; so the function can then
121 * -# take `extent_hooks_t*` and cast it to `Extent_hooks_wrapper<T>*`;
122 * -# call `->get_owner()` on that which gets the aforementioned stored `T*`;
123 * -# on that, call the appropriate method (e.g., one that handles "new extent needed") of T, passing it
124 * the rest of the args following the `extent_hooks_t*` arg.
125 *
126 * @tparam T
127 * Any type; see get_owner().
128 */
129 template<typename T>
131 public extent_hooks_t
132 {
133 public:
134 // Constructors/destructor.
135
136 /**
137 * Constructor.
138 *
139 * @param extent_hooks
140 * The set of callback functions.
141 * @param owner
142 * See get_owner().
143 */
144 Extent_hooks_wrapper(extent_hooks_t&& extent_hooks, T* owner);
145
146 // Methods.
147
148 /**
149 * Returns the value `owner` as given to ctor earlier.
150 * @return See above.
151 */
152 T* get_owner() const;
153
154 private:
155 // Data.
156
157 /// The holder of this object.
159 }; // class Extent_hooks_wrapper
160
161 /// Output format for stats_dump_to_ostream() and the sibling stats-dump methods.
162 enum class Stat_format
163 {
164 /// Human-readable text (jemalloc `malloc_stats_print()` default formatting).
165 S_TEXT,
166 /// JSON (jemalloc `malloc_stats_print()` `"J"` option).
167 S_JSON
168 };
169
170 // Methods.
171
172 /**
173 * Allocates uninitialized memory by using jemalloc's vanilla `malloc()` implementation.
174 *
175 * This may use an automatically determined thread cache, transparently.
176 *
177 * A jemalloc-arena is internally chosen from jemalloc's own arena set, also transparently.
178 *
179 * @note If used in an STL-compliant allocator, proper behavior on null return from this is to throw an exception
180 * (such as `std::bad_alloc`).
181 *
182 * @param size
183 * The allocation size which must be greater than zero.
184 * @return Upon success, a non-null pointer to the base address of the allocation; otherwise, null.
185 */
186 void* allocate(size_t size) const;
187
188 /**
189 * Allocates uninitialized memory, in the specified jemalloc-arena, without the use of a thread cache.
190 *
191 * @note If used in an STL-compliant allocator, proper behavior on null return from this is to throw an exception
192 * (such as `std::bad_alloc`).
193 * @see Thread_cache class doc header for a description of how jemalloc tcache works.
194 *
195 * @param size
196 * The allocation size which must be greater than zero.
197 * @param arena_id
198 * The jemalloc-arena ID.
199 * @return Upon success, a non-null pointer to the base address of the allocation; otherwise, null.
200 */
201 void* allocate_no_tcache(size_t size, arena_id_t arena_id) const;
202
203 /**
204 * Allocates uninitialized memory, in the specified jemalloc-arena, with the (potential) use of the specified
205 * thread cache.
206 *
207 * @note If used in an STL-compliant allocator, proper behavior on null return from this is to throw an exception
208 * (such as `std::bad_alloc`).
209 * @see Thread_cache class doc header for a description of how jemalloc tcache works. Using this API form,
210 * without knowing that information or equivalent, may lead to confusing and undefined behavior.
211 *
212 * @param size
213 * The allocation size which must be greater than zero.
214 * @param arena_id
215 * The jemalloc-arena ID.
216 * @param thread_cache_id
217 * The thread cache ID with which to associate the allocation.
218 * jemalloc requires that a given tcache is not used by more than one thread concurrently; a stronger (not
219 * strictly required but, informally speaking, convenient) rule is that the mention of a given tcache be
220 * restricted to the thread in which it was created.
221 * All in all: suggest using class Thread_cache to manage these.
222 * @return Upon success, a non-null pointer to the base address of the allocation; otherwise, null.
223 */
224 void* allocate(size_t size, arena_id_t arena_id, tcache_id_t thread_cache_id) const;
225
226 /**
227 * Deallocates memory by using jemalloc's vanilla `free()` implementation.
228 *
229 * This may use an automatically determined thread cache, transparently.
230 *
231 * The jemalloc-arena is (here and elsewhere) always internally determined by jemalloc based on `address`.
232 *
233 * @param address
234 * The address to be deallocated; must be non-null.
235 */
236 void deallocate(void* address) const noexcept;
237
238 /**
239 * Deallocates memory similarly to `deallocate(address)`, except that this is done without the use of a thread
240 * cache. That is: `address` is not fed to any tcache for later reuse by some `allocate()` or equivalent.
241 *
242 * The jemalloc-arena is (here and elsewhere) always internally determined by jemalloc based on `address`.
243 *
244 * @see Thread_cache class doc header for a description of how jemalloc tcache works.
245 *
246 * @param address
247 * The address to be deallocated; must be non-null.
248 */
249 void deallocate_no_tcache(void* address) const noexcept;
250
251 /**
252 * Deallocates memory similarly to `deallocate(address)`, except that this is done with the (potential) use of
253 * the *specified* thread cache.
254 *
255 * The jemalloc-arena is (here and elsewhere) always internally determined by jemalloc based on `address`.
256 *
257 * @see Thread_cache class doc header for a description of how jemalloc tcache works. Using this API form,
258 * without knowing that information or equivalent, may lead to confusing and undefined behavior.
259 *
260 * @param address
261 * The address to be deallocated; must be non-null.
262 * @param thread_cache_id
263 * The thread cache ID with which to associate the deallocation.
264 * jemalloc requires that a given tcache is not used by more than one thread concurrently; a stronger (not
265 * strictly required but, informally speaking, convenient) rule is that the mention of a given tcache be
266 * restricted to the thread in which it was created.
267 * All in all: suggest using class Thread_cache to manage these.
268 */
269 void deallocate(void* address, tcache_id_t thread_cache_id) const noexcept;
270
271 /**
272 * Creates a new segregated memory area (jemalloc-arena), wired to the given extent hooks (if any).
273 * Throws `flow::error::Runtime_error` on jemalloc error.
274 *
275 * @param extent_hooks
276 * A handle (which is memorized) to a set of callbacks; or null to use the default set.
277 * Note that any non-null value must be kept valid for the arena's lifetime.
278 * @return jemalloc-arena ID. (It is not unique; in that destroying arena X may mean create_arena() will
279 * subsequently return X; jemalloc tends to reuse arena IDs.)
280 */
281 arena_id_t create_arena(extent_hooks_t* extent_hooks) const;
282
283 /**
284 * Forwards to Thread_cache::destroy_arena_safely(). Please see that method and in particular note that
285 * depending on the tcache situation the *actual* arena destruction may occur synchronously or asynchronously.
286 *
287 * @param arena_id
288 * See above.
289 * @param log_ctx
290 * See above.
291 * @param on_done_func
292 * See above. Note `on_done_func` is potentially consumed.
293 */
294 template<typename On_done_func>
295 void destroy_arena(arena_id_t arena_id, const flow::log::Log_context_mt* log_ctx, On_done_func&& on_done_func) const;
296
297 /**
298 * Equivalent of the other overload but lacks the `on_done_func` argument and therefore offers no opportunity
299 * to execute code of caller's choice when the arena destruction actually occurs (which may be asynchronous).
300 *
301 * @param arena_id
302 * See above.
303 * @param log_ctx
304 * See above.
305 */
306 void destroy_arena(arena_id_t arena_id, const flow::log::Log_context_mt* log_ctx) const;
307
308 /**
309 * Writes a broad, raw, process-wide jemalloc statistics dump (all arenas) to `os`. Thin wrapper around
310 * jemalloc `malloc_stats_print()`. For curated, typed, per-Flow-IPC-arena stats use
311 * jemalloc::Ipc_arena::memory_manager_stats() instead; this dump is the complementary "everything jemalloc
312 * knows" firehose of information.
313 *
314 * The output ends in a newline. No bracketing text is added by us: it all comes from jemalloc. (OK, fine,
315 * for Stat_format::S_JSON we add a newline at the end.)
316 *
317 * If the linked jemalloc was built without statistics support (a configure-time option, `--disable-stats`,
318 * reported at runtime by the read-only `config.stats` option), the output is still well-formed and non-empty.
319 * Roughly, it'll include at least things like jemalloc version + build config + settings but will
320 * omit per-arena runtime statistics (alloc counts/bytes, bins/large/extents/mutex).
321 *
322 * @note Relatedly: our curated stats-accessor, Ipc_arena::memory_manager_stats(), will also produce
323 * nominally non-empty output, if jemalloc not built with stats support, but essentially it'll be
324 * zeroes all-over.
325 *
326 * @param os
327 * Stream to which to write. Defaults to standard output.
328 * @param format
329 * Format of the output. Defaults to human-readable text.
330 * @param extra_opts
331 * If not empty: appended verbatim to jemalloc's `malloc_stats_print()` options string (its flag letters
332 * toggle output sections -- merged-vs-per-arena, bins/large/extents/mutex; see jemalloc docs).
333 */
334 void stats_dump_to_ostream(std::ostream& os = std::cout, Stat_format format = Stat_format::S_TEXT,
335 util::String_view extra_opts = {}) const;
336
337 /**
338 * Like stats_dump_to_ostream() but returns the dump as a string.
339 *
340 * @param format
341 * See stats_dump_to_ostream().
342 * @param extra_opts
343 * See stats_dump_to_ostream().
344 * @return The dump.
345 */
347 util::String_view extra_opts = {}) const;
348
349 /**
350 * Like stats_dump_to_ostream() but logs the dump -- as a single (multi-line) message -- via `logger_ptr` at
351 * severity `sev`. The message will bracket stats_dump_to_ostream() with header/footer.
352 *
353 * @param logger_ptr
354 * Logger to use (null is allowed as usual).
355 * @param format
356 * See stats_dump_to_ostream().
357 * @param sev
358 * Severity at which to log (INFO-level by default); normal severity/component-filter must pass for
359 * logging to happen (as usual).
360 * @param extra_opts
361 * See stats_dump_to_ostream().
362 */
363 void log_stats_dump(flow::log::Logger* logger_ptr, Stat_format format = Stat_format::S_TEXT,
364 flow::log::Sev sev = flow::log::Sev::S_INFO, util::String_view extra_opts = {}) const;
365
366private:
367 // Methods.
368
369 /**
370 * Boring helper for allocate_no_tcache() and `allocate(size, arena_id, tcache_id)`.
371 *
372 * @param size
373 * See allocate_no_tcache().
374 * @param arena_id
375 * See allocate_no_tcache().
376 * @param thread_cache_flags
377 * A `MALLOCX_TCACHE_*` expression.
378 * @return Upon success, a non-null pointer to the base address of the allocation; otherwise, null.
379 */
380 void* allocate_helper(size_t size, arena_id_t arena_id, int thread_cache_flags) const;
381
382 /**
383 * Boring helper for deallocate_no_tcache() and `deallocate(address, tcache_id)`.
384 *
385 * @param address
386 * See any `deallocate*()`.
387 * @param thread_cache_flags
388 * See allocate_helper().
389 */
390 void deallocate_helper(void* address, int thread_cache_flags) const noexcept;
391}; // class Memory_manager
392
393// Template implementations.
394
395template<typename T>
397 extent_hooks_t(std::move(extent_hooks)),
398 m_owner(owner)
399{
400 // Nothing.
401}
402
403template<typename T>
405{
406 return m_owner;
407}
408
409template<typename On_done_func>
411 On_done_func&& on_done_func) const
412{
413 Thread_cache::destroy_arena_safely(arena_id, log_ctx, std::move(on_done_func));
414 // Incidentally: Thread_cache is aware of the detail::jemalloc_arena_list_mutex() issue and will work with us.
415}
416
417} // namespace ipc::shm::arena_lend::jemalloc
Wrapper around extent hooks, which are callbacks that can be instituted in jemalloc per arena.
T * get_owner() const
Returns the value owner as given to ctor earlier.
Extent_hooks_wrapper(extent_hooks_t &&extent_hooks, T *owner)
Constructor.
arena_id_t create_arena(extent_hooks_t *extent_hooks) const
Creates a new segregated memory area (jemalloc-arena), wired to the given extent hooks (if any).
void deallocate_helper(void *address, int thread_cache_flags) const noexcept
Boring helper for deallocate_no_tcache() and deallocate(address, tcache_id).
void stats_dump_to_ostream(std::ostream &os=std::cout, Stat_format format=Stat_format::S_TEXT, util::String_view extra_opts={}) const
Writes a broad, raw, process-wide jemalloc statistics dump (all arenas) to os.
std::string stats_dump_to_string(Stat_format format=Stat_format::S_TEXT, util::String_view extra_opts={}) const
Like stats_dump_to_ostream() but returns the dump as a string.
void deallocate(void *address) const noexcept
Deallocates memory by using jemalloc's vanilla free() implementation.
void * allocate_no_tcache(size_t size, arena_id_t arena_id) const
Allocates uninitialized memory, in the specified jemalloc-arena, without the use of a thread cache.
void destroy_arena(arena_id_t arena_id, const flow::log::Log_context_mt *log_ctx, On_done_func &&on_done_func) const
Forwards to Thread_cache::destroy_arena_safely().
void deallocate_no_tcache(void *address) const noexcept
Deallocates memory similarly to deallocate(address), except that this is done without the use of a th...
void * allocate(size_t size) const
Allocates uninitialized memory by using jemalloc's vanilla malloc() implementation.
Stat_format
Output format for stats_dump_to_ostream() and the sibling stats-dump methods.
@ S_TEXT
Human-readable text (jemalloc malloc_stats_print() default formatting).
void * allocate_helper(size_t size, arena_id_t arena_id, int thread_cache_flags) const
Boring helper for allocate_no_tcache() and allocate(size, arena_id, tcache_id).
void log_stats_dump(flow::log::Logger *logger_ptr, Stat_format format=Stat_format::S_TEXT, flow::log::Sev sev=flow::log::Sev::S_INFO, util::String_view extra_opts={}) const
Like stats_dump_to_ostream() but logs the dump – as a single (multi-line) message – via logger_ptr at...
static void destroy_arena_safely(arena_id_t arena_id, const flow::log::Log_context_mt *log_ctx, On_done_func &&on_done_func)
Destroys (as by mallctl("arena.X.destroy")) the given jemalloc arena, as soon as it is safe without c...
ipc::shm sub-module with the SHM-jemalloc SHM-provider. See ipc::shm doc header for introduction.
unsigned int arena_id_t
Arena ID as dictated by jemalloc.
unsigned int tcache_id_t
Thread cache ID type as dictated by jemalloc.
flow::util::String_view String_view
Short-hand for Flow's String_view.
Definition: util_fwd.hpp:112