Flow-IPC 3.0.0
Flow-IPC project: Full implementation reference.
ipc_arena.cpp
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
27#include "ipc/shm/arena_lend/jemalloc/jemalloc_pages.hpp"
32#include "ipc/shm/arena_lend/owner_shm_pool_listener.hpp"
35#include "ipc/shm/arena_lend/shm_pool.hpp"
38#include "ipc/common.hpp"
39#include <flow/util/util.hpp>
40#include <flow/util/stat/stat_set.hpp>
41#include <flow/log/log.hpp>
42#include <flow/error/error.hpp>
43#include <boost/range/algorithm/sort.hpp>
44#include <boost/range/adaptor/transformed.hpp>
45#include <boost/algorithm/string.hpp>
46#include <boost/system/errc.hpp>
47#include <string>
48#include <vector>
49#include <utility>
50#include <new>
51#include <cassert>
52#include <cstdlib>
53
55{
56
57// Static initializers.
58
59std::atomic<collection_id_t> Ipc_arena::m_collection_id_counter{0};
60
61// Implementations.
62
63std::shared_ptr<Ipc_arena> Ipc_arena::create(flow::log::Logger* logger, // Static.
64 const std::shared_ptr<Memory_manager>& memory_manager,
65 Shared_name&& pool_name_base,
66 const util::Permissions& permissions)
67{
68 using std::shared_ptr;
69 using std::unique_ptr;
71
72 Thread_lcl_obj_db_admin<Ipc_arena>::this_thread_piggy_scan(); // Opportunistic!
73
74 // unique_ptr rather than raw pointer: start() can throw (jemalloc-arena creation failure); do not leak then.
75 unique_ptr<Ipc_arena> arena{new Ipc_arena{logger, memory_manager, std::move(pool_name_base), permissions}};
76 arena->start();
77
78 /* We need to set up a special disposer for the returned shared_ptr. Unfortunately this is a subtle situation.
79 * Explanation is in destroy(). For now note merely that this disposer calls this_arena->destroy() -- does not
80 * simply `delete this_arena` here. destroy() might... but see its insides. */
81 return shared_ptr<Ipc_arena>{arena.release(),
82 [](auto* this_arena) { this_arena->destroy(); }};
83} // Ipc_arena::create()
84
86{
90 using flow::util::ostream_op_string;
91
92 using std::shared_ptr;
93 namespace str_algo = boost::algorithm;
94 using boost::adaptors::transformed;
95
96 /* Reminder: This is not some normal method executed normally. Explanation:
97 *
98 * When is a *this destroyed? Answer: Once the shared_ptr ref-count for the group returned
99 * by create() factory reaches 0; and that occurs not merely when the user's outer shared_ptr copies/etc.
100 * all disappear; but also (possibly among more things) all the shared_ptr<T>s returned by e.g.
101 * construct<T>() also disappear: we intentionally capture a copy of this->shared_from_this() in the
102 * disposer for each such shared_ptr<T>. So they have to drop `*this` *and* all the objects
103 * this->construct<T>()ed from it. That is when, by default, the default deleter runs which simply
104 * performs `delete this`. In our case, though, this may need to be deferred for a few different activities
105 * to complete first.
106 *
107 * At the current stage. X=shared_ptr<Ipc_arena> handle reaching ref-count 0 implies 3 key things
108 * (indeed that ref-count is 0 now):
109 * - `Shm_session`s to which X was added have all been destroyed (which by the way itself implies that
110 * all SHM-object-shared_ptr-handles borrow_object()ed through them have also reached their ref-count=0).
111 * - User has dropped X and any copies.
112 * - User has dropped any SHM-object-shared_ptr-handles construct()ed through *this. <-- ATTN
113 * A copy of X was placed into the shared_ptr disposer for each one; but now they are all gone.
114 *
115 * The last one (ATTN) is somewhat subtle: It means in this process there are no more references to every
116 * object Y acquired as `Y = this->construct<T>(...)`. It however does not mean every such object Y has
117 * actually been deallocated (from SHM!). There are at least two reasons for this, although one really
118 * subsumes the other; but we mention both in the interests of education/explication. The lesser reason is
119 * that even if deallocation always occurred the moment it was warranted (no more users in either the lender
120 * or the borrower remain), we might have lost contact with an opposing Shm_session prematurely -- e.g.,
121 * just simply an opposing-process crash -- so the use-count decrements w/r/t objects borrow_object()ed by
122 * that side never happened and might never happen; so a deallocation hasn't been warranted for them
123 * (use-count must reach 0).
124 *
125 * But the greater/subsuming reason is that, assuming no crashing or anything weird, deallocations do *not*
126 * occur instantly upon a given object Y's use-count reaching 0. In fact only the Thread_lcl_obj_db_admin
127 * per-thread object belonging to the thread that originally allocated Y -- or its degraded-mode thread
128 * (see Thread_lcl_obj_db_admin; but we digress) -- is the one, exclusively, performing scans of
129 * Y's use-count. It can only do that opportunistically -- when a user calls into any of our APIs, and we
130 * opportunistically perform Thread_lcl_obj_db_admin::piggy_scan(). We don't control the event loop of
131 * that thread -- it's the user's thread! -- so this will only happen when it will happen; we can't, like,
132 * .post() a thing that will do it.
133 *
134 * Naturally, deleting Y cannot occur once ~Ipc_arena() dtor executes; the jemalloc-arenas would be
135 * potentially eliminated immediately (or a bit later depending on Thread_cache situation). So then we
136 * have a few options ostensibly. 1, we could just... not delete Y. After all if the arena holding it
137 * will be destroyed wholesale, who cares? Well, no: User did nothing wrong whatsoever, and there haven't
138 * been any IPC-errors necessarily; they at least expect the dtor of type *Y to run. Of course we could
139 * just execute dtor of the type *Y, but that brings us back essentially to the original problem; if the
140 * type is an STL-compliant thing that was using our Stateless_allocator, then invoking that dtor will still
141 * deallocate stuff, just not the outer object pointed-at by Y itself.
142 *
143 * Well then, 2, why *don't* we just delete Y right now, from *this* thread, while we have the instruction
144 * pointer pointing into us?! Answer: Ignoring the perf issues with that (it foregoes thread caching),
145 * consider that now it's possible for another thread to be completely validly right now detecting that
146 * the use-count for the Y pointee *is* zero -- it might be getting around to deleting Y right now itself.
147 * So we'd have to synchronize, using a mutex or something both in here and in
148 * Thread_lcl_obj_db_admin::piggy_scan(). This blows up the simplicity and lock-freeness of the existing
149 * design; though given that only 2 threads can ever contend for this mutex, it isn't the most awful idea
150 * actually. Still, I (ygoldfel) am not a fan at least.
151 *
152 * So that leaves 3: Since we must wait for the proper thread to delete Y, and this will happen at the
153 * earlier of either the user calling into *a* SHM-jemalloc API from that thread, or that thread exiting
154 * normally, why not delay the actual destruction of *this until then? So that's why we'll chain the
155 * "retirement" of a *this as follows:
156 * - create()-returned shared_ptr<Ipc_arena> reaches ref-count 0 =>
157 * - our custom disposer runs... but doesn't delete the Ipc_arena; instead =>
158 * - it creates a new shared_ptr<Ipc_arena> Z with a different custom disposer; and
159 * - calls static Thread_lcl_obj_db_admin::forgetting_shm_arena(Z) which memorizes moved()d Z
160 * inside itself; and
161 * - sets up a cross-thread assignment to per-thread Thread_lcl_obj_db_admin thread-local objects,
162 * specifically to (1) opportunistically forget all objects originated-from arena Z; and (2) once done:
163 * - forget shared_ptr Z itself; which causes Z=shared_ptr<Ipc_arena> to reach ref-count 0 =>
164 * - our 2nd custom disposer runs... but still doesn't delete the Ipc_arena; instead =>
165 * - ...continue procedure. Namely execute helper destroy_on_obj_db_forgot_us(); see further commentary there.
166 *
167 * Worth noting, even if it's not strictly within our purview here:
168 * - Due to the "lesser reason" a few paragraphs back,
169 * Thread_lcl_obj_db_admin::forgetting_shm_arena() shouldn't be looking for use-count=0 objects
170 * like a normal this_thread_piggy_scan(): due to e.g. an opposing-side crash, that might never be reached --
171 * but an Ipc_arena going away by contract means we consider that moot. So it should get rid of
172 * everything to do with our `m_arena` ID(s), whether or not use-count 0 is in effect for a given thing.
173 * - Specifically, take object Y this->construct()ed by thread T1; all such objects must be deleted
174 * before Thread_lcl_obj_db_admin will be able to move on-to Ipc_arena::destroy_on_obj_db_forgot_us().
175 * When will this occur?
176 * - If thread T1 is still live: The earlier of:
177 * - T1 exits (~Thread_lcl_obj_db_admin() dtor runs for that thread-local obj-DB);
178 * - In T1, user calls into any SHM-jemalloc API.
179 * - If thread T1 has exited (then it would have launched degraded-mode thread T1' of its own):
180 * - Next time T1' periodically wakes up (as of this writing every 100msec). */
181
182 Thread_lcl_obj_db_admin<Ipc_arena>::this_thread_piggy_scan(); // Opportunistic!
183
184 // Log ~final info/stats.
185 FLOW_LOG_INFO("Ipc_arena [" << this << "] handle has reached ref-count=0. "
186 "Before totally destroying it, we issue order to the cross-thread obj-DB admin/client-modules "
187 "to forget this arena; admin-part shall occur potentially asynchronously, piggybacked onto "
188 "user API calls and/or relevant threads exiting; once all have finished this "
189 "forget-arena op, the last one to do so shall continue to destroy the Ipc_arena.");
190 FLOW_LOG_INFO("Ipc_arena [" << this << "] shutdown: jemalloc-arena-ID list: " // Very useful diagnostic.
191 "[" << str_algo::join(m_arenas | transformed([](auto arena_id) -> auto
192 { return ostream_op_string(arena_id); }),
193 ", ") << "]."); // BTW: reminder: they're in sorted order in m_arenas.
194 {
195 /* Don't need the many-pages jemalloc-dump (verbose=false). multiline=true is fine however.
196 * Avoid unneeded info_dump() by placing it inside log-macro. */
197 Info_dump dump;
198 dump.m_fmt.m_verbose = false;
199 FLOW_LOG_INFO("Ipc_arena [" << this << "] shutdown: "
200 "~Final state (includes ~final this-arena + ~current global):\n"
201 << (info_dump(&dump), dump) << '.'); // Note: no newline at end of info_dump.
202 }
203
204 /* This is also required but is orthogonal to the much-discussed _admin-oriented stuff and lacks its complexities.
205 * Here it's entirely about cleanliness. At any rate: it's documented as being required, so do it synchronously. */
206 Thread_lcl_obj_db_client<Ipc_arena>::forgetting_shm_arena(get_id());
207 // Now back to complicated _admin stuff.
208
209 /* Logging possibilities are about to get async-weird; and the first thing to potentially trigger asyncness
210 * if the following _admin::forgetting_shm_arena() call. So it's time to lock things down.
211 *
212 * Firstly save this; get_logger() is safe until the current method destroy() returns (to reiterate: it's on
213 * the user to ensure resources given to Ipc_arena stay alive while Ipc_arena's `shared_ptr` group is alive --
214 * which is still the case at the moment; we are in the disposer for that smart-pointer group). So just make a
215 * silly Log_contex_mt whose contents are valid and cannot change until the present method returns.
216 *
217 * We can give it to forgetting_shm_arena() et al; and for sync logging we can use it ourselves. */
218 const Log_context_mt log_ctx_sync{get_logger(), get_log_component()};
219 FLOW_LOG_SET_LOCKED_CONTEXT(&log_ctx_sync);
220 /* Secondly, before asyncness might begin, ensure that any "regular" *this logging code (that uses get_logger()
221 * such as FLOW_LOG_INFO/TRACE/WARNING()) sees null get_logger(). At this stage it is thread-safe: user cannot
222 * be messing with *this while its original handle has ref-count 0; no construct()ed objects remain, so no
223 * concurrent allocs/pool creations/etc. can occur; and we haven't kicked off anything async/concurrent yet.
224 * The one exception: obj-db-deferred deallocations may run -- via an obj-db scan in some thread -- until the
225 * _admin::forgetting_shm_arena() barrier below; hence deallocate() specifically must not (and does not) touch
226 * get_logger() (see comment there). */
227 set_logger(nullptr);
228 // Now, any "regular" logging => no-op; but _LOCKED() logging shall work synchronously.
229
230 // Kick off the (potential) async-craziness.
231 const bool obj_db_forgot_us_synchronously
232 = Thread_lcl_obj_db_admin<Ipc_arena>
233 ::forgetting_shm_arena(get_id(),
234 get_logger(), // For synchronous logging only (get_logger() is safe while in destroy()).
235 [this_arena_on_death_row_ptr = this]
236 (const Log_context_mt* log_ctx) mutable
237 {
238 Thread_lcl_obj_db_admin<Ipc_arena>::this_thread_piggy_scan(); // Opportunistic!
239
240 /* Careful. If this is executing, then forgetting_shm_arena() did *not* complete its stuff synchronously,
241 * and we're not executing within Ipc_arena::destroy() but rather form another thread.
242 * User is allowed to let that *get_logger() be destroyed: they don't know the Ipc_arena
243 * is still hanging around due to Thread_lcl_obj_db_admin (TLODBA) technicalities. To help us with that
244 * TLODBA::forgetting_shm_arena() gives us its Log_context_mt* (Logger*
245 * therein could still be null, but the point is it's something
246 * the user is at least indirectly in charge of ensuring exists while TLODBA is in action). So
247 * we log the following message using that Logger*, and we set the Ipc_arena's Logger to that.
248 * Then inside the .reset() below this->destroy_on_obj_db_forgot_us() will be executing -- synchronously --
249 * with a valid Logger*. At least, the synchronous part of it (but not our problem here). */
250
251 FLOW_LOG_SET_LOCKED_CONTEXT(log_ctx);
252 FLOW_LOG_INFO_LOCKED("Ipc_arena death-row disposer executing for one of the per-thread admins, in thread "
253 "different from the one in which the shared_ptr<Ipc_arena> disposer initiated shutdown; "
254 "according to that last obj-DB admin: arena is safe to continue to destroy.");
255
256 this_arena_on_death_row_ptr->destroy_on_obj_db_forgot_us(log_ctx);
257 }); // Thread_lcl_obj_db_admin::forgetting_shm_arena()
258
259 if (!obj_db_forgot_us_synchronously)
260 {
261 return; // The above lambda is memorized by _admin and will execute from another thread. Done here.
262 }
263 /* else: The above lambda is forgotten: it is safe for us to proceed synchronously.
264 *
265 * We do basically the same thing, but as for logging: we are still synchronously in destroy(); get_logger()
266 * is still safe; use the Log_context_mt we've set-up for that purpose. (Our own log-call here uses it too.) */
267
268 FLOW_LOG_INFO_LOCKED("shared_ptr<Ipc_arena> disposer initiated shutdown; obj-DB reports that no async "
269 "arena-forgetting work remains, so we can immediately/synchronously continue to "
270 "destroy arena.");
271 destroy_on_obj_db_forgot_us(&log_ctx_sync);
272
273 /* Careful! It's very much possible this->destroy() *also* synchronously is able to finish
274 * what it must -- and *right now here* *this is gone. So no more this-> statements! */
275} // Ipc_arena::destroy()
276
278{
281 using std::shared_ptr;
282
283 assert((!get_logger()) && "Regular get_logger()-based logging should have been disabled earlier by "
284 "destroy(); it's *log_ctx or bust from now on.");
285
286 /* Per contract, any synchronous logging must go through *log_ctx. (In practice, it is either
287 * the regular get_logger() in-effect when destroy() was called, and we are still in destroy(); or this
288 * is happening asynchronously/past destroy(), and *log_ctx is some Thread_lcl_obj_db_admin's.) */
289 FLOW_LOG_SET_LOCKED_CONTEXT(log_ctx);
290
291 /* What exactly happens from this point is in my (ygoldfel) opinion notoriously difficult to grasp from
292 * reading the code.
293 *
294 * At risk of redundancy (versus destroy*() doc header(s) and further comments below) here is an
295 * annotated recap to help navigate this touchy area.
296 *
297 * -# m_destroy_started = true. (Effect below.)
298 * -# For each of m_shm_pools, for each of m_listeners, .notify_removed_shm_pool().
299 * - If N = # of `Shm_session`s we are lent-through: There are N listeners through which this does:
300 * Shm_session::remove_lender_shm_pool().
301 * - As of this writing: no-op. Might change to inform opposing process of pool-removal.
302 * -# For each of m_shm_pools: Owner_shm_pool_repository::get_instance().erase(pool_id).
303 * -# For each jemalloc-arena A (as of this writing: 1 of them):
304 * jemalloc::Memory_manager::destroy_arena(A)
305 * -# (The following occurs [a]synchronously in 1+ threads including this one.
306 * It is potentially async, because via jemalloc::Thread_cache::destroy_arena_safely(), this must
307 * occur via piggy-backing in 0+ other threads with jemalloc tcaches. If only this thread has tcache(s),
308 * then it is synchronous.)
309 * For each arena A:
310 * - At least some (presumably all?) jemalloc-extents (vaddr-areas) are deinitialized/let-go-of.
311 * This triggers, via still-registered jemalloc-extent-hooks:
312 * pool-removal method(s) execute, cleaning up book-keeping; closing SHM-pool handles; deleting
313 * SHM-pool names from file-system (SHM-RAM returned to OS once all of that, plus SHM-pool handle freeing,
314 * also occurs in borrower(s), at latest on Shm_session destruction that
315 * might follow soon/already happened/happening now (but not necessarily, depending on user behavior; might
316 * be later).
317 * - Ipc_arena::on_shm_pool_removed() is called. No-op: m_destroy_started is true.
318 * We already did what this would do, in step 3 above. (Trying it again is not harmless; in particular
319 * various pools' base vaddrs are no longer in Owner_shm_pool_repository -- step 3 did it -- so it'd
320 * try to remove non-existent things and crash.)
321 * -# (This [a]sync-follows the last step of the destruction of the last arena A to be destroyed.)
322 * `delete this`. `*this` Ipc_arena is destroyed. Destructor is essentially no-op other than freeing RAM;
323 * The per-pool SHM-pool-handles should have been closed via arena destruction.
324 * Note: This last step is deferred, because the registered jemalloc-extents (their required bookkeeping) are
325 * in *this.
326 *
327 * We will explain hopefully each step again, but without the above tactical recap it has proven difficult
328 * to see the forest for the trees in the past. */
329
330 /* Prevents at least Owner_shm_pool_repository erase and listener notification from happening
331 * below in the arena destruction (on_shm_pool_removed() early-returns when m_destroy_started). */
332 m_destroy_started.store(true);
333
334 /* Run listeners that would presumably otherwise run in arena destruction below as triggered
335 * via jemalloc extent-hooks. We've prevented that now and choose to do so all in one group from this one thread
336 * and with no possibility of skipping any SHM-pools (if jemalloc for whatever reason doesn't trigger that).
337 * We feel this involves less entropy overall.
338 *
339 * Then deregister from Owner_shm_pool_repository (after listeners, for consistency with on_shm_pool_removed()). */
340 {
341 Lock lock{m_shm_pools_and_listeners_mutex}; // Probably not necessary by now but just in case....
342
343 FLOW_LOG_TRACE_LOCKED("Notifying listeners of [" << m_shm_pools.size() << "] shared memory pool removals; these "
344 "will not run via jemalloc extent-hooks anymore (though extent-hooks will execute soon; but "
345 "listeners will be skipped).");
346 for (const auto& cur_shm_pool : m_shm_pools)
347 {
348 for (auto cur_listener : m_listeners)
349 {
350 cur_listener->notify_removed_shm_pool(cur_shm_pool);
351 /* Logging safety mental check: Formally speaking the logging by whatever that triggers is that thing's
352 * responsibility; we do not log there -- just invoke their virtual handler.
353 *
354 * Just in case let's think what "that thing" might be: As of this writing `Shm_session`s register
355 * themselves for these updates; that's it. Each Shm_session we are lent-through (.lend_arena(this)
356 * essentially) does so. That said, as of this writing, the Ipc_arena would not be destroyed until
357 * those `Shm_session`s have all dropped their shared_ptr refs to *this; and that (again... as of this
358 * writing) only happens when the `Shm_session`s go away which would unregister from m_listeners
359 * (but so would a hypothetical .unlend_arena() which does not exist as of this writing). So really
360 * this should not even be executing.
361 *
362 * If other registering listeners exist, it might be a different story, but even then "their" logging is
363 * "their" problem. */
364 }
365 } // for (cur_shm_pool : m_shm_pools)
366
367 for (const auto& cur_shm_pool : m_shm_pools) // Pool-repository deregistration.
368 {
369 Owner_shm_pool_repository::get_instance().erase(cur_shm_pool->get_id());
370 /* Logging safety mental check: Owner_shm_pool_repository has its own logging setup as a singleton; it
371 * (and subordinate things like pool-lookup thread-local machinery) are responsible for maintaining
372 * safe Logger*s and all. */
373 }
374 } // Lock lock{m_shm_pools_and_listeners_mutex}
375
376 /* The rest: destroy jemalloc arenas. See the big comment recap above for context.
377 *
378 * Please see destroy() for key background; then come back here and keep reading.
379 * What does our dtor need to do? Just this: For each ID in m_arenas, just:
380 * get_jemalloc_memory_manager()->destroy_arena(arena_id);
381 *
382 * It's actually important to know what it does. Consider one arena
383 * (it's the same for each one; plus realistically we just have one in practice as of this writing, per *this).
384 * It won't just blow-up the vaddr areas a/k/a extents in the arena, and that's it. That'd be nice arguably but...
385 * actually it'll in civilized fashion, like, unmap/remove extents and things of that nature -- which in fact
386 * will trigger the relevant extent hooks (see ctor), most notably remove_shm_pool_handler() which calls
387 * remove_shm_pool() which calls super-class Owner_shm_pool_collection::remove_range_and_pool_if_empty()
388 * which unmaps the extent and closes the SHM-pool (SHM-object) handle and even unlinks the pool name
389 * (it disappears from file system, and once all handles across the system are also closed, the RAM is returned
390 * fully).
391 *
392 * No problem though, right? Just call ->destroy_arena(). Sadly -- see its doc header -- that doesn't
393 * simply do the mallctl("...destroy") call; and that's because it potentially cannot. You can read
394 * its doc header and/or that for Thread_cache::destroy_arena_safely() (which is what it calls really), but
395 * long story short: It is mandatory to destroy (really merely flush, but at this point why not destroy too)
396 * every tcache that has ever been used in 1+ allocations against arena_id.
397 *
398 * Well, who cares? Fine then, let ->destroy_arena() do it, however long it takes asynchronously; why should we
399 * care? Sigh: nope. By that time, if we let our dtor run *now*, the extent hooks we'd registered
400 * will refer to a non-existent *this; so that'll crash probably; and anyway the extents (pools) might be around
401 * and mapped, but our Shm_pool objects, etc., are gone.
402 *
403 * Once that becomes clear the solution isn't that hard; we "just" can't let *this die yet. ->destroy_arena()
404 * takes an optional functor to execute right after it's actually safe to destroy the arena. So we can capture
405 * `this` and delete it in the functor. destroy(), which led to us, used a similar technique, but it couldn't
406 * delete `this` once done with is charge; so it called us. We are the last thing though and can delete `this`
407 * once done. */
408
409 shared_ptr<Ipc_arena> this_arena_on_death_row{this};
410
411 const auto disposer_func = [this_arena_on_death_row]
412 (const Log_context_mt* log_ctx, auto&& destroy_arena_func) mutable
413 {
414 /* Careful. This might be running synchronously from destroy_on_obj_db_forgot_us() (which itself might
415 * be, or might not be, running synchronously from destroy()), or it might be running from another thread
416 * due to straggling Thread_cache-owning thread(s) needing to flush stuff first. log_ctx is valid either
417 * way; if synchronously from destroy_on_obj_db_forgot_us() then it is that guy's passed-in one; if async
418 * then it is some Thread_cache's Log_context_mt instead. (Meanwhile we reiterate: Ipc_arena's get_logger()
419 * is null. Again: log_ctx or bust from now on.)
420 *
421 * If `m_arenas.size() == 1`, then there is one disposer_func copy and one this_arena_on_death_row copy.
422 * So it'll reach ref-count 0 below, causing finally the destruction of the Ipc_arena *this.
423 *
424 * If `m_arenas.size() > 1`, then there are a couple possibilities. There are then N disposer_func()
425 * (and this_arena_on_death_row) copies out there, and they might execute serially or (at least partially)
426 * concurrently. If serially: nothing special to say. If concurrency occurs:
427 * Ipc_arena and its super-classes can handle concurrent extent hooks executing; after all
428 * an allocate() might coincide with another in another thread and a deallocate() in another, etc.
429 * And that's for the *same* jemalloca-arena (e.g., m_arena0); if they're separate (the case here)
430 * then definitely fine. / Regardless: this_arena_on_death_row is 1 of N copies, so if it's the last
431 * of the N to be handled, then ref-count 0 belos is reached + *this dies. Otherwise not. */
432
433 FLOW_LOG_SET_LOCKED_CONTEXT(log_ctx);
434 FLOW_LOG_INFO_LOCKED("Ipc_arena death-row disposer executing for one of the jemalloc-arenas; it is safe to "
435 "destroy. For internal reasons logging from resulting extent-hooks firing during this "
436 "arena-destruction has been disabled. Destroying jemalloc arena now.");
437
438 /* A bunch of SHM-pool removals (might) occur synchronously in here, triggering hooks => calling
439 * stuff in *this Ipc_arena which may well log... but we've set get_logger() to null in destroy(); so
440 * sadly that stuff will not be visible.
441 *
442 * @todo It would be better if it were visible: if those hooks could log. It would merely require us to
443 * hook up log_ctx to those log call sites. Tactically this is a bit difficult: Consider at least two parts
444 * of *this hierarchy: Ipc_arena itself (extent hooks logging) and Shm_pool_collection super-super-class
445 * (pool-ops, like closing of pool, logging). At the moment they just use Log_context super-class's stuff
446 * for regular FLOW_LOG_INFO()/etc. calls. We'd need to set something somewhere that would cause such
447 * log-call-sites to use `Log_context_mt* log_ctx` (which we've spent some serious effort in obtaining across
448 * dangerous territory) instead of the normal thing. (Having Shm_pool_collection/Ipc_arena/et-al derive
449 * from Log_context_mt does not do it: *log_ctx might be some Thread_cache's log-context; loading the get_logger()
450 * from there into a hypothetical *this Log_context_mt defeats the point: If user does set_logger(), it will
451 * be against Thread_cache/its Log_context_mt -- not whatever we copied out of it.) It is definitely doable
452 * but requires auditing all of the potentially-affected code and having it do the proper hoop-jumping to
453 * select the proper Log_context_mt (without forgetting about perf). Worth looking into: the arena-end
454 * logging could be helpful in practice. We do at least log here ourselves (but the hooks won't). */
455 destroy_arena_func();
456
457 /* If `m_arenas.size() == 1` then: The following *will* trigger the default deleter, namely,
458 * delete this_arena_on_death_row.get()
459 * If `m_arenas.size() > 1` then: The following *might* do that, but across the disposer_func() copies
460 * it'll do that exactly once.
461 *
462 * (We could omit the line, but it'd happen momentarily anyway. This way at least it's nice for
463 * debugger sessions.) */
464 this_arena_on_death_row.reset();
465 }; // const auto disposer_func =
466 using Disposer_func = decltype(disposer_func);
467
468 /* So now, for each arena_id in series, it will either actually destroy the arena right now or defer it
469 * until thread(s) get a chance to do it. Since we still have this_arena_on_death_row on the stack, `*this` won't be
470 * destroyed within the loop, even if all of the arenas are such that they got destroyed synchronously. */
471 for (const auto arena_id : m_arenas)
472 {
473 FLOW_LOG_INFO_LOCKED("Destroying arena (possibly deferred due to tcache dependency), "
474 "jemalloc-ID [" << arena_id << "].");
475 get_jemalloc_memory_manager()->destroy_arena(arena_id, log_ctx,
476 // Make a copy, as it's a destructive && arg.
477 Disposer_func{disposer_func});
478 }
479
480 assert(this_arena_on_death_row);
481 /* Now this_arena_on_death_row will go out of scope, so it's possible `delete this` happens right here.
482 * E.g.: `m_arenas.size() == 1`; Thread_cache was only ever used (allocations only ever happened) right in this
483 * thread; so the single arena just got destroyed. */
484} // Ipc_arena::destroy_on_obj_db_forgot_us()
485
486Ipc_arena::Ipc_arena(flow::log::Logger* logger,
487 const std::shared_ptr<Memory_manager>& memory_manager,
488 Shared_name&& pool_name_base,
489 const util::Permissions& permissions) :
491 ++m_collection_id_counter,
492 std::move(pool_name_base),
493 permissions),
494 m_owner_id(uint64_t(util::Process_credentials::own_process_id())),
495 m_memory_manager(memory_manager),
496 m_arena0(0), // Not valid.
497 /* (gcc and clang accept this init style for C++17 even with warnings turned up (non-pedantically), though it is not
498 * allowed by standard. In C++20 this is officially allowed. It's nice so leaving it, as long as it doesn't blow
499 * up one of our build targets; so far, so good.) */
500 m_extent_hooks_wrapper({ .alloc = &create_shm_pool_handler,
502 .destroy = &remove_shm_pool_handler,
505 .purge_lazy = nullptr,
506 .purge_forced = &purge_forced_memory_pages_handler,
508 .merge = &merge_memory_pages_handler },
509 this),
510 m_destroy_started(false)
511{
512 // Yep.
513}
514
516{
517 start_impl(1); // This probably looks silly, and it is, but see the 1 doc header(s).
518}
519
520void Ipc_arena::start_impl(unsigned int n_arenas)
521{
522 assert((n_arenas != 0) && "At least 1 arena please.");
523 assert(m_arenas.empty() && "Do not call start() twice.");
524 for (unsigned int i = 0; i != n_arenas; ++i)
525 {
526 FLOW_LOG_INFO("Creating arena (idx [" << i << "]/0-based of [" << n_arenas << "]/1-based).");
527
528 const arena_id_t arena_id = create_jemalloc_arena();
529
530 /* Informationally:
531 *
532 * That call itself just created our first SHM-pool for this arena (via the extent-alloc hook,
533 * synchronously inside): jemalloc's *base* (metadata) block, wherein the arena's internal metadata lives --
534 * including the native-arena structure itself. Sibling blocks (pools) can appear later if metadata grows enough.
535 * User data never live in these (nothing we know of relies on this fact per se, but it's good to know).
536 * The block extents (<=> SHM-pools for us) are also destroyed (during arena shutdown; see destroy())
537 * differently from data-storing extents (that will appear due to subsequent allocate()s triggering that same
538 * extent-alloc hook); that fact is actually salient for us -- see optional_remove_shm_pool().
539 *
540 * Orthogonally: See notes and to-dos in our caller create()'s doc header. 1: Emit error Flow-style (Error_code*
541 * arg, default null, throw if null and problem). 2: Find some way to detect case where create_jemalloc_arena()
542 * above throws exception with .code() = resource-unavailable (Linux: EAGAIN) and emit a nicer Error_code of
543 * our own (which would be added to arena_lend::error table). Easy way, which might catch some other problem
544 * too -- but error could include that possibility -- is just to detect that particular error and emit something
545 * else in that case. Pinpointy way would have our extent hook for pool creation record this outcome. */
546
547#ifndef NDEBUG
548 const auto result =
549#endif
550 m_arenas.emplace(arena_id);
551 assert(result.second);
552
553 m_mem_mgr_base_stats.try_emplace(arena_id); // Per-arena reset-state baseline (zero-init).
554
555 FLOW_LOG_INFO("Created arena, jemalloc-ID [" << arena_id << "].");
556 }
557
558 m_arena0 = *(m_arenas.begin());
559
560 /* Establish the per-arena reset baselines from the freshly-created (zero-count) arenas. Beyond zeroing the
561 * scalar baselines, this importantly gives the base histograms their bucket structure -- required for the
562 * per-bucket since-reset deltas (`-=`) computed in memory_manager_stats(). */
564} // Ipc_arena::start_impl()
565
567{
570 using flow::util::ostream_op_string;
571 using flow::error::Runtime_error;
572 using boost::system::system_category;
573 namespace errc = boost::system::errc;
574
575 /* Check opt.retain up-front, before creating the arena. We need it: without it jemalloc refuses to set
576 * retain_grow_limit (ENOENT) -- see below for why we must set that. Once the arena exists -- wired to our
577 * extent hooks -- there is no clean way to back out of a failure; so detect this likeliest cause of such a
578 * failure first. (opt.retain is on by default in 64-bit Linux; it can be turned off via jemalloc's run-time
579 * configuration, e.g., the MALLOC_CONF environment variable.) */
580 bool retain = false;
581 if (const int ec = mallctl_read("opt.retain", &retain); ec != 0)
582 {
583 throw Runtime_error{Error_code{ec, system_category()}, "je_mallctl() error reading opt.retain."};
584 }
585 if (!retain)
586 {
587 throw Runtime_error{errc::make_error_code(errc::not_supported),
588 "jemalloc opt.retain is disabled (perhaps via run-time configuration such as "
589 "MALLOC_CONF); SHM-jemalloc requires it."};
590 }
591
592 const arena_id_t arena_id = get_jemalloc_memory_manager()->create_arena(&m_extent_hooks_wrapper); // Throws.
593
594#ifndef FLOW_OS_LINUX
595 static_assert(false, "The logic below might be Linux-specific and has only been tested in Linux as of this "
596 "writing.");
597#endif
598 /* Set the arena's retain_grow_limit to Shm_pool_offset_ptr_data_base::max_pool_size().
599 *
600 * jemalloc grows an arena's mapped vaddr footprint via a series of extent-alloc requests (for us: each a new
601 * SHM-pool), each geometrically larger than the last; retain_grow_limit caps the size of each such growth step.
602 * (This geometric growth happens only in jemalloc's retain mode: hence the name; hence jemalloc's refusal to set
603 * it without opt.retain; hence our check above.) The default is effectively no limit.
604 *
605 * Why we set it:
606 * - Hard correctness bound. Our fancy-pointer representation cannot encode addresses within a pool larger
607 * than max_pool_size() (~2Gi bytes as of this writing). This caps only the growth steps, though: a single
608 * allocation request too large for any step makes jemalloc request an extent sized for that request
609 * regardless. create_shm_pool() (the extent-alloc hook) declines any such too-large extent, which completes
610 * the guarantee.
611 * - Defensive bound. SHM objects are subject to OS-imposed size/count limits that ordinary heap mappings are
612 * not. Without a cap, an arena can grow a pool to many GBs of vaddr even when RAM-resident usage is much
613 * smaller -- observed in the field, in fact, prompting the original choice of this value.
614 *
615 * Despite the name, this is unrelated to the `stats.*.retained` stat, which counts vaddr jemalloc keeps mapped for
616 * later reuse (for us: pools, or ranges thereof, whose removal optional_remove_shm_pool() declined, their physical
617 * pages having been released). */
618 size_t input_param = Shm_pool_offset_ptr_data_base::max_pool_size();
619 const int ec
620 = IPC_SHM_ARENA_LEND_JEMALLOC_API(mallctl)(ostream_op_string("arena.", arena_id, ".retain_grow_limit").c_str(),
621 nullptr, nullptr, &input_param, sizeof(input_param));
622 if (ec != 0)
623 {
624 /* The arena exists and is wired to our extent hooks (see up-front opt.retain check above for why that makes a
625 * clean back-out impractical). With opt.retain checked, a failure here means a bad arena ID or limit value:
626 * a bug. */
627 const Error_code sys_err_code{ec, system_category()};
628 FLOW_LOG_FATAL("Setting retain_grow_limit to [" << input_param << "] for new jemalloc-arena [" << arena_id << "] "
629 "failed despite opt.retain check: system error [" << sys_err_code << "] "
630 "[" << sys_err_code.message() << "]. Bug?");
631 assert(false && "Setting retain_grow_limit failed despite opt.retain check: bug?");
632 std::abort();
633 }
634
635 return arena_id;
636} // Ipc_arena::create_jemalloc_arena()
637
638void* Ipc_arena::allocate(size_t size)
639{
640 using std::bad_alloc;
641
642 const auto ret = allocate_impl(size);
643#if IPC_SHM_ARENA_LEND_JEMALLOC_NO_TCACHE
644 constexpr bool TCACHE_ON = false;
645#else
646 constexpr bool TCACHE_ON = true;
647#endif
648
649 if (!ret)
650 {
651 /* We could behave consistently with classic::Pool_arena::allocate() in avoiding a WARNING here, but
652 * jemalloc -- or our extent hook that does SHM-pool-opening/mapping, say -- failing to at some allocate
653 * step (~impossible to know which one) is more exceptional than Pool_arena running out of user-configured,
654 * potentially quite tight, space. Users would likely want to know about it in the logs (assuming the
655 * environment isn't so messed up that that too would fail). */
656 FLOW_LOG_WARNING("Tried to allocate size [" << size << "], arena [" << m_arena0 << "], tcache on? = "
657 "[" << TCACHE_ON << "]; but it failed (jemalloc-allocate function returned null); "
658 "preceding logs may or may not provide more detail. Note both jemalloc-proper code and "
659 "(potentially) Flow-IPC code, via extent hooks, are executed as part of jemalloc-allocate "
660 "function. Throwing std::bad_alloc.");
661 throw bad_alloc{};
662 }
663 // else
664
665 return ret;
666}
667
668void* Ipc_arena::allocate_impl(size_t size)
669{
670 assert(!m_arenas.empty() && "start() must have been called by now.");
671 const auto& arena_id = m_arena0;
672
673#if IPC_SHM_ARENA_LEND_JEMALLOC_NO_TCACHE
674 // ^-- Tcache support is stable but turning it off can simplify specific profiling/debugging.
675 if (skip_fast_path_verbose_logging())
676 {
677 return get_jemalloc_memory_manager()->allocate_no_tcache(size, arena_id);
678 }
679 // else
680
681 FLOW_LOG_DATA("Allocating size [" << size << "], arena [" << arena_id << "], no tcache.");
682 void* const buf = get_jemalloc_memory_manager()->allocate_no_tcache(size, arena_id);
683 FLOW_LOG_DATA("Allocated size [" << size << "], arena [" << arena_id << "], no tcache, "
684 "resulting in [" << buf << "].");
685 return buf;
686#else // #if !IPC_SHM_ARENA_LEND_JEMALLOC_NO_TCACHE
687 // Suggest reading Thread_cache class doc header for background on jemalloc-tcache.
688
689 /* Get (and first create if needed) cache object (Thread_cache) for this thread.
690 * Get (and first create if needed) jemalloc tcache for this thread for this arena.
691 *
692 * Reminder: jemalloc docs don't make this clear, and it doesn't appear to be widely known, but both
693 * empirical evidence and links like https://github.com/jemalloc/jemalloc/issues/460 show that, while
694 * a given tcache (and tcache ID X) can be used to jemalloc-mallocx() against 2+ arenas A and B, this leads
695 * to potential trouble: such an mallocx(B) shall absolutely sometimes yield an address in an A-owned pool.
696 * For us this is totally unacceptable. (Perhaps when allocating is always in heap, without arenas ever dying
697 * throughout, it would have been fine; not fine for us at all.)
698 *
699 * It does make sense, if one has the properly simple conception of how jemalloc-tcache
700 * works; namely that it is essentially nothing more than a thread-local intermediary store of
701 * what would normally be mallocx()-returned pointers, sitting between (1) us with our actual-mallocx() calls
702 * (get_j_m_m()->allocate() above) and actual-dallocx() calls (in this->deallocate()) and (2) jemalloc and its
703 * jemalloc-arenas (this arena_id but also all the other ones). So if I do an initial mallocx() here,
704 * it might seed this thread's TL-cache with (across a single mutex-lock) hundreds of
705 * what-would-normally-be-mallocx()es' returned pointers from arena_id, yes. But if I do another initial mallocx()
706 * here, for another arena -- arena_id2 -- it may well do the same thing; now this TL-cache of allocated-buffer
707 * pointers has pointers into arena_id and arena_id2; jemalloc's tcache does not auto-segregate by source-arena.
708 * Now a subsequent mallocx(arena_id) might see the cache has lots of juicy pointers to give out -- no need to
709 * lock any mutex to do it -- and will hand out one of the ones in the TL-cache... which may well have come
710 * from arena_id2. As stated before, we can't have that: arena_id2's Ipc_arena might go away, while arena_id's
711 * Ipc_arena is still around; the (one would expect) perfectly-alive mallocx()-result pointer from (one would
712 * expect) arena_id (which is indeed perfectly alive) now points at destroyed-arena-backed garbage. */
713 const auto tcache_id_for_arena = Thread_cache::this_thread_cache()->id(arena_id);
714
715 if (skip_fast_path_verbose_logging())
716 {
717 return get_jemalloc_memory_manager()->allocate(size, arena_id, tcache_id_for_arena);
718 }
719 // else
720
721 FLOW_LOG_DATA("Allocating size [" << size << "], arena [" << arena_id << "], "
722 "tcache [" << tcache_id_for_arena << "].");
723 void* const buf = get_jemalloc_memory_manager()->allocate(size, arena_id, tcache_id_for_arena);
724 FLOW_LOG_DATA("Allocated size [" << size << "], arena [" << arena_id << "], tcache "
725 "[" << tcache_id_for_arena << "], resulting in [" << buf << "].");
726 return buf;
727#endif // #elif !IPC_SHM_ARENA_LEND_JEMALLOC_NO_TCACHE
728} // Ipc_arena::allocate()
729
730void Ipc_arena::deallocate(void* address) noexcept
731{
732 assert(!m_arenas.empty() && "start() must have been called by now.");
733 const auto& arena_id = m_arena0;
734
735 /* Careful: no logging -- nor any other get_logger() access -- in this method, unlike in allocate() and others.
736 * Reason: obj-db-deferred deallocations can make this method run, via an obj-db scan in some thread,
737 * concurrently with destroy()'s set_logger(nullptr) (see notes there): destroy() can begin while such a scan
738 * is in flight, and scans stop only at the _admin::forgetting_shm_arena() barrier -- which is necessarily
739 * after the set_logger(nullptr). So a get_logger() here would race that mute. allocate() is immune: it cannot run
740 * once the handle group hits ref-count 0 (no construct()ed objects remain to allocate() things). */
741
742#if IPC_SHM_ARENA_LEND_JEMALLOC_NO_TCACHE
743 get_jemalloc_memory_manager()->deallocate_no_tcache(address);
744#else // #if !IPC_SHM_ARENA_LEND_JEMALLOC_NO_TCACHE
745 // Suggest reading Thread_cache class doc header for background on jemalloc-tcache.
746
747 /* It would seem we could, much as in allocate(), just do
748 * tcache_id_for_arena = Thread_cache::this_thread_cache()->id(arena_id);
749 * so that the Thread_cache and inside it the per-arena jemalloc-tcache are created if not currently in existence.
750 * We intentionally avoid this, treating this thread's Thread_cache read-only (our data structure, not
751 * the jemalloc-internal tcache: it is certainly potentially written-to -- if it exists, but the point is
752 * we don't make it exist, if it doesn't already). Reason:
753 *
754 * In many cases, in and of itself, it would be fine and good. Say allocate(arena_id) returned `address` in
755 * thread T1, and we are deallocate()ing in thread T2, and one has never allocate(arena_id)ed in T2 so far.
756 * Creating the tcache here would push `address` into a new tcache, and further deallocate(<thing from arena_id>)s
757 * would keep pushing onto it sans mutex-locking -- better than the normal mutex-locking no-tcache dallocx().
758 *
759 * The danger area is during, generally speaking, "things" going down -- the user would not be triggering
760 * deallocate() themselves, but our obj-DB tracking system might. 1, at thread exit
761 * detail::Thread_lcl_obj_db_admin can (much as during thread's lifetime) delete construct()ed objects that
762 * lack live user handles (a/k/a zombie objects; note these do not represent an error or anomaly, despite the
763 * name). 2, when *this Ipc_arena is going down (destroy() path), it unconditionally similarly deletes
764 * any still-live construct()ed objects. For (1), that thread's Thread_cache might have been cleaned-up
765 * already, so at least the outer deallocate() during said deletion would re-create Thread_cache. It should
766 * still be cleaned-up again quite soon by Thread_local_state_registry/Thread_local_ptr/thread_specific_ptr
767 * stubborn-cleanup machinery, but this is hard to reason about and harder to maintain over time. For (2),
768 * as of this writing it is probably safe actually: first
769 * Thread_lcl_obj_db_admin::forgetting_shm_arena() does the deletes, then triggers the relevant Thread_cache
770 * cleaning, so it should be still around when needed (during the deletes). Again, though, it is hard to
771 * reason about and maintain. We just don't want that trouble.
772 *
773 * There is a cost to this, all else being equal: Until the first arena_id allocate() in thread T,
774 * any deallocate()s (of things allocate()d in other threads: totally possible and common enough) will
775 * do the full-mutex-locking tcache-bypassing operation that returns the buffer to jemalloc's arena-set.
776 * With an active tcache, it could skip that and return to the tcache intermediary store instead. This is a
777 * real cost, but it is well mitigated.
778 * - Consider a given deallocate(). Often (not always), it is part of an outer-object's
779 * deletion by Thread_lcl_obj_db_admin (TLODBA): at least the deallocate() of the object's raw sizeof().
780 * If not (e.g., an in-SHM `vector` being move-assigned-to => deletion of overwritten buffer), the same
781 * thread T probably (not always) would've TLODBA-deleted *some* outer object at least once. So what?
782 * They're still DEallocations; doesn't help. True: but TLODBA-deletion of object X in thread T
783 * means X *must* have been construct()ed in thread T. If so, then the construct() would first-thing
784 * have allocate(sizeof())d... and that first-thing would have create the tcache after all.
785 * - Supposing the "often" and "probably" in the prev bullet are accurate, there is the possible corner case
786 * (totall allowed and not an error though) that TLODBA-deletion in thread T is actually handling object X
787 * that was created in another thread T2, not T: but this specifically only happens in the
788 * degraded-admin drain-thread TLODBAs. For that reason
789 * Thread_lcl_obj_db_admin::degraded_admin_thread_body(), in controlled fashion, pre-creates the tcache
790 * ahead of any TLODBA-deletes it would then do.
791 *
792 * @todo This does still leave a hole as noted: It is possible -- hard to say how common but "feels" not-too-common --
793 * that some thread might *just* deallocate() lots and lots, without allocate()ing once throughout. It is OK:
794 * it just won't get the tcache perf-bonus for those deallocate()s. Nevertheless it'd be nice to plug the hole.
795 * All it would take is either some heuristic... but let's aim higher in this to-do... or knowing to be
796 * read-only (like now) only in the danger-zone -- we think, as explained above, around thread exit and
797 * arena destruction. Any mechanism for determining this is TBD; it is unlikely to be trivial but feels doable.
798 * The degraded_admin_thread_body() heuristic can then also be removed. */
799
800 const auto this_thread_cache_or_null = Thread_cache::this_thread_cache_or_null();
801 const auto tcache_id_for_arena = this_thread_cache_or_null ? this_thread_cache_or_null->id_or_none(arena_id)
803
804 if (tcache_id_for_arena != Thread_cache::S_NO_TCACHE_ID)
805 {
806 /* In this thread, tcache (for relevant arena) exists. Use it -- regardless of which thread allocated
807 * the buffer: a tcache is merely a thread-local store of pointers to this arena's regions awaiting
808 * reuse; a region carries no memory of the tcache (if any) that handed it out; so any this-arena region
809 * may be sitting in the calling thread's tcache. (See discussion at end of function for how it comes to
810 * be -- and how commonly -- that the allocating thread differs from ours.) */
811
812 get_jemalloc_memory_manager()->deallocate(address, tcache_id_for_arena);
813 return;
814 } // if (tcache_id_for_arena != Thread_cache::S_NO_TCACHE_ID)
815 // else: No tcache available in this thread (yet), for this arena.
816
817 /* Do a similar thing to the NO_TCACHE snippet above. (Slight perf loss results versus the tcache path
818 * above -- the alloc was tcache-enabled, but this thread has not requested tcache creation for the arena,
819 * presumably having never allocated in it at this layer.) */
820 get_jemalloc_memory_manager()->deallocate_no_tcache(address);
821
822 /* Discussion for context: Suppose end user's actual code *outside any SHM-aware allocators* refrains from making
823 * direct [de]allocate() calls (which isn't disallowed either incidentally). That represents not-guaranteed
824 * but definitely recommended behavior. So in that case, can deallocate() even occur in a thread other than
825 * the buffer's allocate() thread? (Not that anything above requires a particular answer; but it affects
826 * tcache-usage patterns, so it is worth understanding.)
827 *
828 * At first it probably seems like it's not, and that would be good; various reasoning becomes much simpler.
829 * It seems like it's not possible: user must do construct<T>() which triggers allocate(sizeof(T).
830 * Thread_lcl_obj_db* machinery is built on the idea that *once it is determined the T goes out of scope, in
831 * whichever thread or process*, only the original constructing thread will (opportunistically) destroy the T,
832 * calling deallocate(). As of this writing you can see that in construct() where it passes a really-destroy-T
833 * functor to Thread_lcl_obj_db_admin->constructing_obj(). Awesome!
834 *
835 * Indeed, when T is simple, without allocating/deallocating buffers on its behalf, the above holds, and that's
836 * great. It might not hold, though, if T allocates/deallocates on its behalf. We're assuming things are done
837 * in recommended fashion (assumption at top of this comment); in which case T is/contains container(s).
838 * Take 1 container, a vector<char>. In the simplest case one might construct<T>(10): so the outer [de]allocate()
839 * of sizeof(T) occurs as explained above, all good. In addition though when T{10} executes it'll
840 * ask the SHM-aware allocator (our Stateless_allocator recommended) to allocate 10. Allocator calls
841 * allocate(10), that same thread: cool. Eventually handle to the T go away, and _admin deallocate(sizeof(T))s
842 * as explained, then calls ~T(); that asks Stateless_allocator to deallocate the 10; allocator
843 * thus calls deallocate(). Awesome! All still in the proper thread.
844 *
845 * However, one could resize the vector in some other thread in-between, requiring a bigger buffer. That'll
846 * deallocate() (<-- "wrong" thread), allocate() ("wrong" thread). At that point the possibilities are endless;
847 * they could let it live until ~T(); then the 2nd allocate() is matched with original-thread deallocate()
848 * (mismatch). Or they could do more allocs/deallocs, in various threads that may or may not match each other.
849 * Bottom line: If the user does allocs/deallocs via T=vector<char>, and happens to do so in other threads than
850 * where they chose to originally construct() => mismatch. Totally possible.
851 *
852 * For completeness (?) consider also that one need not even use construct(). One can declare a
853 * `vector<char, Stateless_allocator<...>> v{100, 'A'};` on the stack, or similarly on the heap. The SHM-allocating
854 * allocator will do allocate(100) there. Granted, why do this, if this `v` cannot be lent/borrowerd (shared
855 * with different process -- the point of using SHM)? Well, by itself, true; but it's allowed and normal
856 * to have such temp guys sitting around; one can then construct() -- in some other thread -- another vector
857 * and move v's buffer into it: `construct<vector<...>>(std::move(v))`. Or maybe it'd be more realistically into
858 * a construct()ed struct with a vector<char, Stateless_allocator<...>> member. Either way: might be different
859 * thread than the construct()ing one -- in this case the buffer in question even predates the construct()!
860 *
861 * So that's how. */
862#endif // #elif !IPC_SHM_ARENA_LEND_JEMALLOC_NO_TCACHE
863} // Ipc_arena::deallocate()
864
866{
867#if !IPC_SHM_ARENA_LEND_JEMALLOC_NO_TCACHE
869#endif
870}
871
872void* Ipc_arena::create_shm_pool_handler(extent_hooks_t* extent_hooks, // Static.
873 void* address, size_t size, size_t alignment, bool* zero,
874 bool* commit, unsigned arena_id)
875{
876 assert(extent_hooks);
877 return static_cast<Extent_hooks*>(extent_hooks)->get_owner()
878 ->create_shm_pool(address, size, alignment, zero, commit, arena_id);
879}
880
881bool Ipc_arena::optional_remove_shm_pool_handler(extent_hooks_t* extent_hooks, // Static.
882 void* address, size_t size,
883 bool committed, unsigned arena_id)
884{
885 assert(extent_hooks);
886 return !static_cast<Extent_hooks*>(extent_hooks)->get_owner()
887 ->optional_remove_shm_pool(address, size, committed, arena_id);
888}
889
890void Ipc_arena::remove_shm_pool_handler(extent_hooks_t* extent_hooks, // Static.
891 void* address, size_t size, bool committed, unsigned arena_id)
892{
893 assert(extent_hooks);
894 static_cast<Extent_hooks*>(extent_hooks)->get_owner()
895 ->remove_shm_pool(address, size, committed, arena_id);
896}
897
898bool Ipc_arena::commit_memory_pages_handler(extent_hooks_t* extent_hooks, // Static.
899 void* address, size_t size, size_t offset,
900 size_t length, unsigned arena_id)
901{
902 assert(extent_hooks);
903 return !static_cast<Extent_hooks*>(extent_hooks)->get_owner()
904 ->commit_memory_pages(address, size, offset, length, arena_id);
905}
906
907bool Ipc_arena::decommit_memory_pages_handler(extent_hooks_t* extent_hooks, // Static.
908 void* address, size_t size, size_t offset, size_t length,
909 unsigned arena_id)
910{
911 assert(extent_hooks);
912 return !static_cast<Extent_hooks*>(extent_hooks)->get_owner()
913 ->decommit_memory_pages(address, size, offset, length, arena_id);
914}
915
916bool Ipc_arena::purge_forced_memory_pages_handler(extent_hooks_t* extent_hooks, // Static.
917 void* address, size_t size, size_t offset, size_t length,
918 unsigned arena_id)
919{
920 assert(extent_hooks);
921 return !static_cast<Extent_hooks*>(extent_hooks)->get_owner()
922 ->purge_forced_memory_pages(address, size, offset, length, arena_id);
923}
924
925bool Ipc_arena::split_memory_pages_handler(extent_hooks_t* extent_hooks, // Static.
926 void* address, size_t size, size_t size_a, size_t size_b,
927 bool committed, unsigned arena_id)
928{
929 assert(extent_hooks);
930 return !static_cast<Extent_hooks*>(extent_hooks)->get_owner()
931 ->split_memory_pages(address, size, size_a, size_b, committed, arena_id);
932}
933
934bool Ipc_arena::merge_memory_pages_handler(extent_hooks_t* extent_hooks, // Static.
935 void* address_a, size_t size_a, void* address_b, size_t size_b,
936 bool committed, unsigned arena_id)
937{
938 assert(extent_hooks);
939 return !static_cast<Extent_hooks*>(extent_hooks)->get_owner()
940 ->merge_memory_pages(address_a, size_a, address_b, size_b, committed, arena_id);
941}
942
943void* Ipc_arena::create_shm_pool(void* address, size_t size, size_t alignment, bool* zero,
944 bool* commit, arena_id_t arena_id)
945{
947 using flow::util::stat::fetch_add;
948 using std::string;
949 using std::shared_ptr;
950
951 assert(zero);
952 assert(commit);
953
954 if (size > Shm_pool_offset_ptr_data_base::max_pool_size())
955 {
956 /* jemalloc's retain_grow_limit (see create_jemalloc_arena()) keeps its routine growth steps within this
957 * limit; but a single allocation request too large for that makes jemalloc ask for an extent sized for it
958 * regardless. We cannot encode pointers into such a pool (offsets past the limit), so decline; jemalloc then
959 * fails the allocation (=> allocate() throws bad_alloc). */
960 FLOW_LOG_WARNING("Asked to create SHM pool of size [" << size << "], arena [" << arena_id << "], exceeding the "
961 "max pool size [" << Shm_pool_offset_ptr_data_base::max_pool_size() << "] addressable by "
962 "our fancy-pointer type. Probably a single allocation of (nearly) that size or more was "
963 "attempted. Declining; the allocation shall fail.");
964 return nullptr;
965 }
966 // else
967
968 /* Ultra-unique ID generated here. It's used as a key in various maps including borrower-side global maps.
969 *
970 * For context: at least Lend_tracker_pool uses the same pool-ID namespace (same pool name prefix for a given
971 * Ipc_arena generating these "real" SHM-pools and the Lend_tracker_pool "aux" SHM-pools that we use to track
972 * objects within the "real" ones; both use this generator function). This at least allows wholesale cleanup
973 * (via file-system) of both types of things when needed. If we had some other type of aux pool, we could
974 * have it use the same technique for the same reason(s). */
975 const auto id = Shm_pool_offset_ptr_data_base::generate_pool_id();
976 // It may also be encoded in the name, to provide uniqueness and for convenience in debugging and such.
977 const string name = generate_shm_object_name(id).str(); // @todo Should be using Shared_name ~throughout.
978
979 shared_ptr<Shm_pool> pool
980 = Owner_shm_pool_collection::create_shm_pool(id, name, size, address,
981 [&](int fd, size_t size, void* address) -> void*
982 {
983 void* const actual_address = Jemalloc_pages::map(address, size, alignment, *commit, fd);
984 if (!actual_address)
985 {
986 return nullptr; // Our caller cleans up (closes `fd`, removes SHM object); we then log a WARNING.
987 }
988
989 // Extracted (somewhat) from extent_mmap.c
990 *zero = *commit;
991
992 FLOW_LOG_TRACE("Mapped SHM pool at address [" << actual_address << "].");
993
994 return actual_address;
995 });
996
997 void* pool_address;
998 if (pool)
999 {
1000 pool_address = pool->get_address();
1001 FLOW_LOG_TRACE("Created SHM pool at address [" << pool_address << "], name [" << name << "], "
1002 "size [" << size << "], arena [" << arena_id << "].");
1003
1004 { // Stats. (See also to-do in the `else` branch.)
1006 fetch_add(&m_pool_stats.m_owner_pool.m_pool_create_sz, size);
1007 }
1008 }
1009 else
1010 {
1011 // Failed to create pool.
1012 pool_address = nullptr;
1013 FLOW_LOG_WARNING("Failed to create SHM pool of size [" << size << "].");
1014
1015 /* @todo Stat-member doc header says pool-create request is always satisfied. This WARNING/code-path looks to me
1016 * (ygoldfel) like it indicates pretty much a catastrophic/unexpected situation that is hardly recoverable.
1017 * We are somewhere in the middle of a jemalloc alloc itself in the middle of a user SHM-alloc, too, so
1018 * it's not like reporting the problem (whatever it even might be) to the user is straightforward -- how *would*
1019 * we even do it, and what % of users would even try to handle this tiny possibility every time?
1020 * The point: Probably we should FLOW_LOG_FATAL() and std::abort() here and in places like it.
1021 * There's probably a bigger-scope to-do to that effect elsewhere (as SHM-jemalloc is littered with attempts
1022 * at civilized handling of errors that doesn't really reach the user in an intended way and is therefore
1023 * arguably more trouble than a crash-out); I nevertheless wanted to emphasize this particular one.
1024 *
1025 * Historical note: Code originally written by another developer (echan), and while we have refactored much of
1026 * *this class since then, this extent-hook area is quite deep/low-level, and generally works well, so beyond
1027 * cosmetic tweaks we have not touched it in similar ways. This to-do is a way in which there is still some
1028 * refactoring (at least arguably) still left. */
1029 }
1030
1031 const auto logger = get_logger();
1032 if (logger && logger->should_log(flow::log::Sev::S_TRACE, get_log_component()))
1033 {
1034 print_shm_pool_map();
1035 }
1036
1037 return pool_address;
1038} // Ipc_arena::create_shm_pool()
1039
1040bool Ipc_arena::optional_remove_shm_pool([[maybe_unused]] void* address,
1041 [[maybe_unused]] size_t size,
1042 [[maybe_unused]] bool committed,
1043 [[maybe_unused]] arena_id_t arena_id)
1044{
1045 using flow::util::stat::fetch_add;
1046
1047 /* @todo MGCOGS-385 - Create decision algorithm
1048 * @todo Track down preceding ticket (echan-filed)/file in project ticket database/possibly update comment. */
1049
1050 /* Always retain for now.
1051 *
1052 * Notes by ygoldfel (original author: echan):
1053 *
1054 * jemalloc is saying, "you can unmap this vaddr area [a range of a SHM-pool in our
1055 * case; possibly a whole one], if you want; or you can refuse, and I'll memorize it -- still mapped -- in my
1056 * *retained* set and reuse it for allocations later." (On our declining, jemalloc shall also -- via our
1057 * decommit/purge hooks -- release the range's backing physical pages by SHM-object hole-punching; so
1058 * retention costs vaddr and SHM-pool-count footprint, not RAM. See also `stats.*.retained` note in
1059 * create_jemalloc_arena().) At some point
1060 * users reported pools (note: vaddr areas; does not mean actual RAM use until allocation in there happens)
1061 * growing to huge sizes, so echan did a couple of basic things w/r/t keeping existing areas for reuse. This was
1062 * one of them, I think. Another was capping the size of any single such vaddr area (SHM-pool):
1063 * `retain_grow_limit`, set at arena creation (again see create_jemalloc_arena()).
1064 *
1065 * The above concerns mid-arena-life requests. This hook also fires during native-arena destruction (see
1066 * destroy()); there:
1067 * - For data extents declining is moot: they proceed to the mandatory-removal (extent-destroy) hook, which
1068 * removes their pools properly.
1069 * - The arena's *base* (metadata) block extents (see start_impl()), though, are offered *only* through
1070 * this hook (jemalloc's internal base_unmap() never uses the destroy hook as of jemalloc-5.4.0 at least) --
1071 * so declining here leaves their pool(s) registered, un-removed.
1072 * That is fine: ~Owner_shm_pool_collection() eliminates any such straggler pools all the same. Intentionally
1073 * we don't complicate our logic here for the sake of the destroy-time cleanup of those pools; the
1074 * aforementioned dtor does so. By definition nothing by that point is allowed to require any pools,
1075 * base-block or otherwise, to persist.
1076 * @todo *Possibly* consider extending that philosophy to *all* destroy-time pool removal. That is,
1077 * one could argue that once destroy() begins we can stop really participating in jemalloc's extent-dance
1078 * too much; do the minimal stuff so jemalloc-arena-destroy API can proceed, but don't remove anything through
1079 * those hooks anymore; and then simply let ~Owner_shm_pool_collection() dtor remove them all.
1080 * All that said: No need to fix what isn't broken. It's best to pursue this only if it makes something
1081 * else simpler. */
1082
1083 { // Stats.
1085 }
1086 return false;
1087
1088 // Reminder: Try to keep various Memory_manager_stats doc headers accurate if changing logic around here.
1089}
1090
1091bool Ipc_arena::remove_shm_pool(void* address, size_t size, bool committed, arena_id_t arena_id)
1092{
1093 using flow::util::stat::fetch_add;
1094 using std::shared_ptr;
1095 size_t pool_size;
1096
1097 Memory_decommit_functor decommit_functor
1098 = [](const shared_ptr<Shm_pool>& shm_pool, size_t offset, size_t length) -> bool
1099 {
1100 return Jemalloc_pages::decommit(reinterpret_cast<void*>(reinterpret_cast<uint8_t*>(shm_pool->get_address())
1101 + offset),
1102 shm_pool->get_fd(), offset, length, true);
1103 };
1104
1105 bool removed_range;
1106 bool unmapped_pool;
1107 const bool result
1108 = remove_range_and_pool_if_empty
1109 (address,
1110 size,
1111 committed ? &decommit_functor : nullptr,
1112 removed_range,
1113 [&pool_size](const shared_ptr<Shm_pool>& shm_pool) -> bool
1114 { Jemalloc_pages::unmap(shm_pool->get_address(), pool_size = shm_pool->get_size()); return true; },
1115 unmapped_pool);
1116 if (!result)
1117 {
1118 FLOW_LOG_WARNING("Failure when performing range removal at address [" << address << "], size "
1119 "[" << size << "], arena [" << arena_id << "], removed-range [" << removed_range << "], "
1120 "unmapped-pool [" << unmapped_pool << "].");
1121 return removed_range;
1122 }
1123
1124 if (unmapped_pool)
1125 {
1126 FLOW_LOG_TRACE("Unmapped SHM pool when removing range at address [" << address << "], "
1127 "size [" << size << "], arena [" << arena_id << "].");
1128 print_shm_pool_map();
1129
1130 { // Stats.
1132 /* Technically (maybe even for realz) `size` merely = sz of last range to be removed from pool, if
1133 * for whatever reason (maybe upon splitting original extent/pool into 2+ extents?) jemalloc then
1134 * (during arena shutdown, we are pretty sure) told us to remove the whole thing piecemeal by specifying
1135 * those sub-extents one-by-one. So obtain the entire pool's size opportunistically above. */
1136 fetch_add(&m_pool_stats.m_owner_pool.m_pool_destroy_sz, pool_size);
1137 }
1138 }
1139
1140 // If we removed the range, no physical memory would persist, although virtual memory leak may occur
1141 return removed_range;
1142
1143 // Reminder: Try to keep various Memory_manager_stats doc headers accurate if changing logic around here.
1144} // Ipc_arena::remove_shm_pool()
1145
1146bool Ipc_arena::commit_memory_pages(void* address, size_t size, size_t offset, size_t length,
1147 [[maybe_unused]] arena_id_t arena_id)
1148{
1149 std::shared_ptr<Shm_pool> pool;
1150 // The offset from the start of the originally created pool (i.e., file offset).
1151 size_t pool_offset;
1152 if (!compute_pool_and_offset(address, size, offset, length, "committing", pool, pool_offset))
1153 {
1154 return false;
1155 }
1156
1157 void* const page_address = reinterpret_cast<void*>(reinterpret_cast<uint8_t*>(address) + offset);
1158 const bool result = Jemalloc_pages::commit(page_address, length);
1159
1160 if (!skip_fast_path_verbose_logging())
1161 {
1162 FLOW_LOG_TRACE("Committing with success [" << result << "], page address "
1163 "[" << page_address << "], length [" << length << "] in pool [" << *pool << "].");
1164 }
1165
1166 return result;
1167
1168 // Reminder: Try to keep various Memory_manager_stats doc headers accurate if changing logic around here.
1169}
1170
1171bool Ipc_arena::decommit_memory_pages(void* address, size_t size, size_t offset, size_t length,
1172 [[maybe_unused]] arena_id_t arena_id)
1173{
1174 std::shared_ptr<Shm_pool> pool;
1175 // The offset from the start of the originally created pool (i.e., file offset).
1176 size_t pool_offset;
1177 if (!compute_pool_and_offset(address, size, offset, length, "decommitting", pool, pool_offset))
1178 {
1179 return false;
1180 }
1181
1182 void* const page_address = reinterpret_cast<void*>(reinterpret_cast<uint8_t*>(address) + offset);
1183 const bool result = Jemalloc_pages::decommit(page_address, pool->get_fd(), pool_offset, length);
1184
1185 if (!skip_fast_path_verbose_logging())
1186 {
1187 FLOW_LOG_TRACE("Decommitting with success [" << result << "], page address [" << page_address <<
1188 "], pool offset [" << pool_offset << "], length [" << length <<
1189 "] in pool [" << *pool << "].");
1190 }
1191
1192 return result;
1193
1194 // Reminder: Try to keep various Memory_manager_stats doc headers accurate if changing logic around here.
1195}
1196
1197bool Ipc_arena::purge_forced_memory_pages(void* address, size_t size, size_t offset, size_t length,
1198 [[maybe_unused]] arena_id_t arena_id)
1199{
1200 std::shared_ptr<Shm_pool> pool;
1201 // The offset from the start of the originally created pool (i.e., file offset).
1202 size_t pool_offset;
1203 if (!compute_pool_and_offset(address,
1204 size,
1205 offset,
1206 length,
1207 "force purging",
1208 pool,
1209 pool_offset))
1210 {
1211 return false;
1212 }
1213
1214 const bool result = Jemalloc_pages::purge_forced(pool->get_fd(), pool_offset, length);
1215
1216 if (!skip_fast_path_verbose_logging())
1217 {
1218 FLOW_LOG_TRACE("Force-purge with success [" << result << "], pool offset [" << pool_offset << "], "
1219 "length [" << length << "] in pool [" << *pool << "].");
1220 }
1221
1222 return result;
1223
1224 // Reminder: Try to keep various Memory_manager_stats doc headers accurate if changing logic around here.
1225}
1226
1227bool Ipc_arena::split_memory_pages(const void* address, size_t size, size_t size_a, size_t size_b,
1228 [[maybe_unused]] bool committed, arena_id_t arena_id)
1229{
1230 // There should not be an existing region that spans across segment boundaries, so it should always be okay to split
1231 if (!skip_fast_path_verbose_logging())
1232 {
1233 FLOW_LOG_TRACE("Allowed split of memory pages at [" << address << "], size [" << size << "] "
1234 "to sizes [" << size_a << ", " << size_b << "], arena [" << arena_id << "].");
1235 }
1236
1237 return true;
1238}
1239
1240bool Ipc_arena::merge_memory_pages(const void* address_a, size_t size_a, const void* address_b, size_t size_b,
1241 [[maybe_unused]] bool committed,
1242 arena_id_t arena_id)
1243{
1244 using std::shared_ptr;
1245
1246 shared_ptr<Shm_pool> pool_a = lookup_shm_pool(address_a);
1247 if (!pool_a)
1248 {
1249 FLOW_LOG_WARNING("Could not find SHM pool for address [" << address_a << "] in merge request.");
1250 return false;
1251 }
1252
1253 shared_ptr<Shm_pool> pool_b = lookup_shm_pool(address_b);
1254 if (!pool_b)
1255 {
1256 FLOW_LOG_WARNING("Could not find SHM pool for address [" << address_b << "] in merge request.");
1257 return false;
1258 }
1259
1260 if (pool_a != pool_b)
1261 {
1262 /* Pools don't match, so we cannot merge. This situation can arise, if the pools were originally created
1263 * adjacent to each other. */
1264 if (!skip_fast_path_verbose_logging())
1265 {
1266 FLOW_LOG_TRACE("Could not merge distinct pools from address A [" << address_a << "] and "
1267 "B [" << address_b << "].");
1268 }
1269 return false;
1270 }
1271
1272 if (!Shm_pool::is_adjacent(address_a, size_a, address_b, size_b))
1273 {
1274 /* Address is not adjacent, which should not occur.
1275 * @todo So then assert()? Check other extent-hook error paths too. */
1276 FLOW_LOG_WARNING("Merge request for non-adjacent regions (address, size) [" << address_a << ", " << size_a << "] "
1277 "and [" << address_b << ", " << size_b << "], arena [" << arena_id << "].");
1278 return false;
1279 }
1280
1281 // Ensure both ranges are within the pool
1282 if ((!pool_a->is_subset(address_a, size_a)) || (!pool_a->is_subset(address_b, size_b)))
1283 {
1284 // One of the ranges is not a subset, which should not occur.
1285 FLOW_LOG_WARNING("Merge request for a region (address, size) [" << address_a << ", " << size_a << "] or "
1286 "[" << address_b << ", " << size_b << "] that is not wholly within pool [" << *pool_a << "].");
1287 return false;
1288 }
1289
1290 if (!skip_fast_path_verbose_logging())
1291 {
1292 FLOW_LOG_TRACE("Allowed merge of memory pages at [" << address_a << "], size [" << size_a << "] "
1293 "with [" << address_b << "], size [" << size_b << "], arena [" << arena_id << "].");
1294 }
1295
1296 return true;
1297} // Ipc_arena::merge_memory_pages()
1298
1299bool Ipc_arena::compute_pool_and_offset(void* address, size_t size, size_t offset,
1300 size_t length, util::String_view use_case,
1301 std::shared_ptr<Shm_pool>& pool, size_t& pool_offset) const
1302{
1303 // A bit confusing what the mission is; but doc header should make it clear. See that.
1304
1305 assert(length > 0);
1306
1307 pool = lookup_shm_pool(address);
1308 if (!pool)
1309 {
1310 // Failed
1311 FLOW_LOG_WARNING("When " << use_case << " pages, could not find pool with address [" << address << "].");
1312 return false;
1313 }
1314
1315 Shm_pool::size_t address_offset;
1316 if (!pool->is_subset(address, size, &address_offset))
1317 {
1318 // Alert - specified range does not reside completely in pool
1319 FLOW_LOG_WARNING("Requested range [" << address << "], size [" << size << "] does not completely "
1320 "reside in pool.");
1321 return false;
1322 }
1323
1324 // Check `offset` and `length` individually first, so that `offset + length` cannot overflow.
1325 if ((offset > size) || (length > size) || (offset + length) > size)
1326 {
1327 FLOW_LOG_WARNING("Requested offset [" << offset << "] and length [" << length << "] is > size "
1328 "[" << size << "].");
1329 return false;
1330 }
1331
1332 pool_offset = address_offset + offset;
1333 return true;
1334} // Ipc_arena::compute_pool_and_offset()
1335
1337{
1338 {
1340
1341 const auto result_pair = m_listeners.emplace(listener);
1342 if (!result_pair.second) // @todo assert() and/or abort() would probably be OK and make things simpler.
1343 {
1344 FLOW_LOG_WARNING("Could not add already existing SHM pool listener [" << listener << "] to "
1345 "collection [" << get_id() << "].");
1346 return false;
1347 }
1348
1349 listener->notify_initial_shm_pools(m_shm_pools);
1350 }
1351
1352 FLOW_LOG_TRACE("Successfully added SHM pool listener [" << listener << "] to collection [" << get_id() << "].");
1353
1354 return true;
1355}
1356
1358{
1359 {
1361
1362 if (m_listeners.erase(listener) == 0) // @todo assert() and/or abort() would probably be OK and make things simpler.
1363 {
1364 FLOW_LOG_WARNING("Could not remove non-existent SHM pool listener [" << listener << "] from "
1365 "collection [" << get_id() << "].");
1366 return false;
1367 }
1368 }
1369
1370 FLOW_LOG_TRACE("Successfully removed SHM pool listener [" << listener << "] from collection [" << get_id() << "].");
1371
1372 return true;
1373}
1374
1375void Ipc_arena::on_shm_pool_created(const std::shared_ptr<Shm_pool>& shm_pool)
1376{
1378 using std::shared_ptr;
1379
1380 if (m_destroy_started.load())
1381 // See destroy_on_obj_db_forgot_us(). Perf: full-synchronized flag access, but it is rare.
1382 {
1383 return;
1384 }
1385 // else:
1386
1387 const auto shm_pool_id = shm_pool->get_id();
1388
1389 {
1390 // Register SHM pool
1392
1393 auto result_pair = m_shm_pools.emplace(shm_pool);
1394 if (!result_pair.second)
1395 {
1396 // This will eventually lead to inconsistency and issues, so abort.
1397 FLOW_LOG_FATAL("Could not insert SHM pool [" << *shm_pool << "], existing registration "
1398 "[" << *(result_pair.first) << "] in collection [" << get_id() << "]. Bug?");
1399 assert(false && "Could not insert SHM pool; existing registration; bug?");
1400 std::abort();
1401 return;
1402 }
1403 FLOW_LOG_TRACE("Registered SHM pool [" << shm_pool_id << "] in collection [" << get_id() << "].");
1404
1405 // Register in the global owner-side pool repository (enables Shm_pool_offset_ptr resolution).
1406 Owner_shm_pool_repository::get_instance().insert(shared_ptr<Shm_pool>{shm_pool});
1407
1408 // Notify listeners.
1409 for (auto cur_listener : m_listeners)
1410 {
1411 cur_listener->notify_created_shm_pool(shm_pool);
1412 }
1413 }
1414
1415 FLOW_LOG_TRACE("Successfully handled SHM pool creation notification of pool [" << shm_pool_id << "] in collection "
1416 "[" << get_id() << "].");
1417} // Ipc_arena::on_shm_pool_created()
1418
1419void Ipc_arena::on_shm_pool_removed(const std::shared_ptr<Shm_pool>& shm_pool, bool)
1420{
1422
1423 if (m_destroy_started.load())
1424 // See destroy_on_obj_db_forgot_us(). Perf: full-synchronized flag access, but it is rare.
1425 {
1426 return;
1427 }
1428 // else:
1429
1430 const auto shm_pool_id = shm_pool->get_id();
1431
1432 {
1433 // Remove SHM pool
1435
1436 if (m_shm_pools.erase(shm_pool) == 0) // @todo assert() and/or abort()?
1437 {
1438 FLOW_LOG_WARNING("SHM pool [" << shm_pool_id << "] not found in collection [" << get_id() << "].");
1439 return;
1440 }
1441
1442 FLOW_LOG_TRACE("Deregistered SHM pool [" << shm_pool_id << "] from collection [" << get_id() << "].");
1443
1444 // Notify listeners.
1445 for (auto cur_listener : m_listeners)
1446 {
1447 cur_listener->notify_removed_shm_pool(shm_pool);
1448 }
1449
1450 // Deregister from the global owner-side pool repository (undoes on_shm_pool_created() registration).
1451 Owner_shm_pool_repository::get_instance().erase(shm_pool_id);
1452 }
1453
1454 FLOW_LOG_TRACE("Successfully handled SHM pool removal notification of pool [" << shm_pool_id << "] in "
1455 "collection [" << get_id() << "].");
1456} // Ipc_arena::on_shm_pool_removed()
1457
1459{
1460 return m_arena0;
1461}
1462
1463const std::shared_ptr<Memory_manager>& Ipc_arena::get_jemalloc_memory_manager() const
1464{
1465 return m_memory_manager;
1466}
1467
1469{
1470 /* @todo The owner-PID half is captured at construction (#m_owner_id); we haven't really looked into whether
1471 * PID could change over time in exotic situations. So -- kinda sorta maybe? -- this might not match the owner_id the
1472 * way an opposing Shm_session records it (its `m_remote_process_id`) when IDing opposing arenas; which would
1473 * matter only if one cross-referenced this owner-side output against borrower-side reporting. This is mere
1474 * reporting regardless, so it is OK until proven otherwise; but maybe revisit sometime. */
1475 return { m_owner_id, get_id() };
1476}
1477
1478void Ipc_arena::info_dump(Info_dump* target_info_dump)
1479{
1480 using flow::util::stat::stats_assign;
1481 using std::string;
1482
1483 assert(target_info_dump);
1484 target_info_dump->m_mem_mgr_stats = memory_manager_stats();
1485 // Info_dump_format contract allows us to not even collect this now.
1486 target_info_dump->m_mem_mgr_stats_dump = target_info_dump->m_fmt.m_verbose
1487 ? get_jemalloc_memory_manager()->stats_dump_to_string()
1488 : string{};
1489
1490 sharded_stats(&target_info_dump->m_sharded_stats);
1491 stats_assign(&target_info_dump->m_pool_stats, pool_stats());
1492 stats_assign(&target_info_dump->m_obj_db_aux_pool_global_stats, obj_db_aux_pool_global_stats());
1493 stats_assign(&target_info_dump->m_owner_pool_lookup_global_stats, owner_pool_lookup_global_stats());
1494 target_info_dump->m_shm_pool_live_info = shm_pool_live_info();
1495} // Ipc_arena::info_dump()
1496
1497std::vector<Ipc_arena::Shm_pool_info> Ipc_arena::shm_pool_live_info() const
1498{
1499 using std::vector;
1500
1501 const auto uniq_id = uniq_arena_id();
1502
1503 vector<Shm_pool_info> shm_pools_id_sorted;
1504 for_each_shm_pool([&](auto&& shm_pool_ptr)
1505 {
1506 shm_pools_id_sorted.push_back({ shm_pool_ptr->get_id(), static_cast<size_t>(shm_pool_ptr->get_size()),
1507 uniq_id, 1 });
1508 });
1509
1510 return shm_pools_id_sorted;
1511}
1512
1514{
1515 namespace stat = ipc::shm::arena_lend::detail::stat;
1516 using flow::util::stat::stats_assign;
1517
1520 if (target_stats) // See sample_hi_wmarks();
1521 {
1522 stats_assign(target_stats, m_sharded_stats);
1523 }
1524}
1525
1527{
1530}
1531
1533{
1534 return m_pool_stats;
1535}
1536
1538{
1539 flow::util::stat::stats_reset(&m_pool_stats, {});
1540}
1541
1543{
1545
1546 return Pool_lookup_global_stats<Ipc_arena, true>::stats();
1547}
1548
1550{
1552
1553 return Thread_lcl_obj_db_client<Ipc_arena>::obj_db_aux_pool_global_stats();
1554}
1555
1557{
1560
1561 Pool_lookup_global_stats<Ipc_arena, true>::stats_reset();
1562 Thread_lcl_obj_db_client<Ipc_arena>::obj_db_aux_pool_global_stats_reset();
1563}
1564
1566{
1568
1569 Thread_lcl_obj_db_admin<Ipc_arena>::this_thread_piggy_scan();
1570}
1571
1572std::vector<Ipc_arena::Memory_manager_stats> Ipc_arena::memory_manager_stats()
1573{
1575 using flow::util::stat::stats_since_reset_state;
1576 using std::vector;
1577
1578 /* m_arenas is a std::set => iteration is ascending by native (jemalloc) arena-ID; hence the result is sorted
1579 * that way, as promised by our contract. The arena-identifying IDs are ours to know regardless of jemalloc's
1580 * build flags; set them on every row. */
1581 vector<Memory_manager_stats> result_id_sorted(m_arenas.size());
1582 const auto uniq_id = uniq_arena_id();
1583 {
1584 size_t idx = 0;
1585 for (const auto arena_id : m_arenas)
1586 {
1587 auto& row = result_id_sorted[idx++];
1588 row.m_uniq_arena_id = uniq_id;
1589 row.m_native_arena_id = uint64_t(arena_id);
1590 }
1591 }
1592
1593 /* The jemalloc-sourced gauges require a stats-enabled jemalloc build. If absent: still emit a row per arena
1594 * (with IDs set), but leave the real stat-members at defaults (zeroes probably) -- as advertised. */
1595 if (!config_stats_enabled())
1596 {
1597 return result_id_sorted;
1598 }
1599 // else: populate the raw stat-members, then fold them into the since-reset view (including HI_WMARKs).
1600
1601 // Lock throughout, including the raw reads; see detail::jemalloc_stats_mutex() and m_mem_mgr_base_stats doc headers.
1603
1604 memory_manager_snapshot_read(&result_id_sorted);
1605
1606 /* Formally speaking we do here what flow::util::stat::stats_since_reset_state() doc header prescribes:
1607 * read the raw values from data-source (jemalloc-stats), then do the following against -- and potentially
1608 * updating -- m_mem_mgr_base_stats[arena_id]. As a reminder though: ACCUMULATORs are -=ed against the base values;
1609 * GAUGEs are left alone; HI_WMARKs are chosen as the higher of the new GAUGE value and the one in
1610 * m_mem_mgr_base_stats; and lastly if the former then the HI_WMARK in base struct is updated to the new max too
1611 * (for next time we do this if any). */
1612 size_t idx = 0;
1613 for (const auto arena_id : m_arenas)
1614 {
1615 stats_since_reset_state(&result_id_sorted[idx++], &(m_mem_mgr_base_stats[arena_id]));
1616 }
1617
1618 return result_id_sorted;
1619} // Ipc_arena::memory_manager_stats()
1620
1622{
1624 using flow::util::stat::stats_mark_reset_state;
1625 using std::vector;
1626
1627 /* Formally speaking we do here what flow::util::stat::stats_since_reset_state() doc header prescribes.
1628 * Read that guy please for details of how it all works. Basically, though, we are to grab the raw
1629 * values for the data-source, which results in the following items used in subsequent stat-consumptions
1630 * (memory_manager_stats()): ACCUMULATOR saved for a delta computation; HI_WMARK saved to current GAUGEd
1631 * value, since by definition the max since right-now = value right-now. That last part is achieved
1632 * via stats_mark_reset_state().
1633 *
1634 * Only meaningful with a stats-enabled jemalloc; otherwise there is nothing to read (bases stay zero). */
1635 if (!config_stats_enabled())
1636 {
1637 return;
1638 }
1639 // else:
1640
1641 vector<Memory_manager_stats> rows(m_arenas.size());
1642
1643 // Lock throughout, including the raw reads; see detail::jemalloc_stats_mutex() and m_mem_mgr_base_stats doc headers.
1645
1647
1648 /* Install current raw reading as the new baseline, then mark (sets each HI_WMARK = its gauge => running
1649 * max restarts at "now"). */
1650 size_t idx = 0;
1651 for (const auto arena_id : m_arenas)
1652 {
1653 auto& base = m_mem_mgr_base_stats[arena_id];
1654 auto& row = rows[idx++];
1655 base.m_vaddr = std::move(row.m_vaddr);
1656 base.m_alloc = std::move(row.m_alloc);
1657 stats_mark_reset_state(&base);
1658 }
1659} // Ipc_arena::memory_manager_stats_reset()
1660
1661void Ipc_arena::memory_manager_snapshot_read(std::vector<Memory_manager_stats>* rows) const
1662{
1664 using detail::stat::epoch;
1665
1666 assert(rows && (rows->size() == m_arenas.size()));
1667
1668 // See our doc header for the reasoning behind all this. (Pre-condition: detail::jemalloc_stats_mutex() locked.)
1669 auto snapshot_epoch = epoch_refresh();
1670 while (true)
1671 {
1672 size_t idx = 0;
1673 for (const auto arena_id : m_arenas)
1674 {
1675 auto& row = (*rows)[idx++];
1676 memory_manager_vaddr_stats_read(arena_id, &row.m_vaddr);
1677 memory_manager_alloc_stats_read(arena_id, &row.m_alloc);
1678 }
1679
1680 const auto epoch_now = epoch();
1681 if (epoch_now == snapshot_epoch)
1682 {
1683 return; // Nobody refreshed while we were reading: all from 1 snapshot.
1684 }
1685 // else
1686
1687 FLOW_LOG_INFO("Ipc_arena [" << this << "]: jemalloc stats were refreshed elsewhere (epoch "
1688 "[" << snapshot_epoch << "] => [" << epoch_now << "]) while we were reading them; reading them "
1689 "again. This is normal but should be infrequent.");
1690 snapshot_epoch = epoch_now;
1691 } // while (true)
1692} // Ipc_arena::memory_manager_snapshot_read()
1693
1695{
1696 using flow::util::ostream_op_string;
1699
1700 assert(vaddr);
1701
1702 const auto page_sz = page_size();
1703 const auto read_sz = [&](const char* leaf, size_t* target)
1704 {
1705 mallctl_read(ostream_op_string("stats.arenas.", arena_id, '.', leaf).c_str(), target);
1706 };
1707 const auto read_pages_sz = [&](const char* leaf) -> size_t
1708 {
1709 size_t n_pages = 0;
1710 mallctl_read(ostream_op_string("stats.arenas.", arena_id, '.', leaf).c_str(), &n_pages);
1711 return n_pages * page_sz;
1712 };
1713
1714 read_sz("mapped", &vaddr->m_mapped_sz);
1715 read_sz("retained", &vaddr->m_retained_sz);
1716 read_sz("resident", &vaddr->m_resident_sz);
1717 vaddr->m_active_sz = read_pages_sz("pactive");
1718 vaddr->m_inactive_resident_sz = read_pages_sz("pdirty");
1719 vaddr->m_inactive_muzzy_sz = read_pages_sz("pmuzzy");
1720} // Ipc_arena::memory_manager_vaddr_stats_read()
1721
1723{
1724 using flow::util::ostream_op_string;
1726
1727 assert(alloc);
1728
1729 const auto read_u64 = [&](const char* leaf) -> uint64_t
1730 {
1731 uint64_t val = 0;
1732 mallctl_read(ostream_op_string("stats.arenas.", arena_id, '.', leaf).c_str(), &val);
1733 return val;
1734 };
1735 const auto read_sz = [&](const char* leaf) -> size_t
1736 {
1737 size_t val = 0;
1738 mallctl_read(ostream_op_string("stats.arenas.", arena_id, '.', leaf).c_str(), &val);
1739 return val;
1740 };
1741
1742 alloc->m_alloc_count = read_u64("small.nmalloc") + read_u64("large.nmalloc");
1743 alloc->m_dealloc_count = read_u64("small.ndalloc") + read_u64("large.ndalloc");
1744 alloc->m_live_count = alloc->m_alloc_count - alloc->m_dealloc_count;
1745 alloc->m_live_sz = read_sz("small.allocated") + read_sz("large.allocated");
1746
1747 /* Cumulative bytes alloc-ed/dealloc-ed + the by-size-class alloc-count histogram: all derived via the
1748 * per-size-class (bins + lextents) walk. */
1750 &alloc->m_alloc_sz, &alloc->m_dealloc_sz,
1752} // Ipc_arena::memory_manager_alloc_stats_read()
1753
1755{
1756 FLOW_LOG_TRACE("Ipc_arena [" << this << "]: Stat-consuming a couple things: high-water-mark refresh requested.");
1757
1758 sharded_stats(nullptr);
1760}
1761
1762std::ostream& operator<<(std::ostream& os, const Ipc_arena& val)
1763{
1764 // @todo Something more useful than just this?
1765 return os << '@' << &val;
1766}
1767
1768} // namespace ipc::shm::arena_lend::jemalloc
Singleton repository for SHM-pool data pertaining to all SHM-arenas owned by this process.
Global stats singleton for either Owner_pool_lookup_global_stats or Borrower_pool_lookup_global_stats...
Parameterization-independent aspects of Shm_pool_offset_ptr_data segregated in non-template base.
(Internal-use) Thread_lcl_obj_db_admin<A>::this_thread_obj_db() is the current thread's module that t...
(Internal-use) Where a Thread_lcl_obj_db_admin cannot handle lend-object and dispose-object tracking,...
Represents/manages a segregated collection of in-SHM (shared memory) objects and buffers allocated us...
Definition: ipc_arena.hpp:177
arena_id_t m_arena0
The first (and for now only, in practice) arena-ID in m_arenas. Used for perf between start() and des...
Definition: ipc_arena.hpp:1391
bool compute_pool_and_offset(void *address, size_t size, size_t offset, size_t length, util::String_view use_case, std::shared_ptr< Shm_pool > &pool, size_t &pool_offset) const
Extent hook impl helper for certain extent hooks: given a jemalloc extent at address of size bytes,...
Definition: ipc_arena.cpp:1299
bool add_shm_pool_listener(Owner_shm_pool_listener *listener)
(Internal-use) Adds a listener to get updates on shared memory pools.
Definition: ipc_arena.cpp:1336
void destroy()
Important helper synchronously invoked, in lieu of delete arena, when the create()-returned shared_pt...
Definition: ipc_arena.cpp:85
arena_id_t create_jemalloc_arena()
start_impl() helper: creates one jemalloc-arena, wired to our extent hooks, and applies the per-arena...
Definition: ipc_arena.cpp:566
void sample_hi_wmarks()
Internally samples certain stats so as to update _hi_wmarks that are computed at stat-consume time as...
Definition: ipc_arena.cpp:1754
void start_impl(unsigned int n_arenas)
start() helper.
Definition: ipc_arena.cpp:520
void pool_stats_reset()
Resets pool_stats(). The formal meaning of a reset is discussed in flow::util::stat doc header.
Definition: ipc_arena.cpp:1537
static void this_thread_gc()
Synchronously garbage-collects, in the calling thread, any objects x = A.construct()ed by this thread...
Definition: ipc_arena.cpp:1565
Uniq_arena_id uniq_arena_id() const
Returns the stat::Uniq_arena_id (owner-process PID + collection-ID) identifying *this arena; used by ...
Definition: ipc_arena.cpp:1468
Extent_hooks m_extent_hooks_wrapper
The callback functions for jemalloc. Contains the jemalloc extent-hook table + a pointer back to *thi...
Definition: ipc_arena.hpp:1394
std::map< arena_id_t, Memory_manager_stats > m_mem_mgr_base_stats
Per-native-arena reset-state ("base") for memory_manager_stats() / memory_manager_stats_reset(): for ...
Definition: ipc_arena.hpp:1499
bool decommit_memory_pages(void *address, size_t size, size_t offset, size_t length, arena_id_t arena_id)
jemalloc extent hook impl.
Definition: ipc_arena.cpp:1171
static bool merge_memory_pages_handler(extent_hooks_t *extent_hooks, void *address_a, size_t size_a, void *address_b, size_t size_b, bool committed, unsigned arena_id)
jemalloc extent hook impl: forwards to similarly named member function of the proper this (which is s...
Definition: ipc_arena.cpp:934
std::set< std::shared_ptr< Shm_pool > > m_shm_pools
Collection of created SHM-pools.
Definition: ipc_arena.hpp:1423
std::vector< Shm_pool_info > shm_pool_live_info() const
For informational/stats-adjacent purposes, returns information on currently live SHM-pools within *th...
Definition: ipc_arena.cpp:1497
bool remove_shm_pool(void *address, size_t size, bool committed, arena_id_t arena_id)
jemalloc extent hook impl.
Definition: ipc_arena.cpp:1091
void * allocate(size_t size)
Performs a non-garbage-collected allocation of a buffer in SHM.
Definition: ipc_arena.cpp:638
Mutex m_shm_pools_and_listeners_mutex
Protects access to m_shm_pools and m_listeners.
Definition: ipc_arena.hpp:1412
void on_shm_pool_removed(const std::shared_ptr< Shm_pool > &shm_pool, bool removed_shared_memory) override
Implements super-class API: forwards to handle_created_shm_pool().
Definition: ipc_arena.cpp:1419
static const Obj_db_aux_pool_global_stats & obj_db_aux_pool_global_stats()
Like owner_pool_lookup_global_stats() but returns the process-global stat-set stat::Obj_db_aux_pool_g...
Definition: ipc_arena.cpp:1549
bool purge_forced_memory_pages(void *address, size_t size, size_t offset, size_t length, arena_id_t arena_id)
jemalloc extent hook impl.
Definition: ipc_arena.cpp:1197
static std::shared_ptr< Ipc_arena > create(flow::log::Logger *logger, const std::shared_ptr< Memory_manager > &memory_manager, Shared_name &&pool_name_base, const util::Permissions &permissions)
Creates an instance of this class, factory-style.
Definition: ipc_arena.cpp:63
void sharded_stats_reset()
Resets sharded_stats(). The formal meaning of a reset is discussed in flow::util::stat doc header.
Definition: ipc_arena.cpp:1526
static void * create_shm_pool_handler(extent_hooks_t *extent_hooks, void *address, size_t size, size_t alignment, bool *zero, bool *commit, unsigned arena_id)
jemalloc extent hook impl: forwards to similarly named member function of the proper this (which is s...
Definition: ipc_arena.cpp:872
void * allocate_impl(size_t size)
Helper core of allocate() that, in reaction to jemalloc-allocate failure, simply returns null (no fur...
Definition: ipc_arena.cpp:668
static std::atomic< collection_id_t > m_collection_id_counter
The monotonically increasing collection (arena ID).
Definition: ipc_arena.hpp:1379
static bool split_memory_pages_handler(extent_hooks_t *extent_hooks, void *address, size_t size, size_t size_a, size_t size_b, bool committed, unsigned arena_id)
jemalloc extent hook impl: forwards to similarly named member function of the proper this (which is s...
Definition: ipc_arena.cpp:925
Mutex m_sharded_stats_mutex
Protects m_sharded_stats, so that sharded_stats() and sharded_stats_reset() can be thread-safe as adv...
Definition: ipc_arena.hpp:1444
void on_shm_pool_created(const std::shared_ptr< Shm_pool > &shm_pool) override
Implements super-class API.
Definition: ipc_arena.cpp:1375
void memory_manager_snapshot_read(std::vector< Memory_manager_stats > *rows) const
Reads the raw jemalloc-sourced stat-members – the m_vaddr and m_alloc sub-groups – of each (*rows)[id...
Definition: ipc_arena.cpp:1661
bool merge_memory_pages(const void *address_a, size_t size_a, const void *address_b, size_t size_b, bool committed, arena_id_t arena_id)
jemalloc extent hook impl.
Definition: ipc_arena.cpp:1240
uint64_t m_owner_id
Owner (this) process's PID, captured at construction; the first half of uniq_arena_id().
Definition: ipc_arena.hpp:1382
void memory_manager_stats_reset()
Resets memory_manager_stats().
Definition: ipc_arena.cpp:1621
std::set< arena_id_t > m_arenas
The segregated memory areas. Empty until start(); non-empty and unchanging thereafter.
Definition: ipc_arena.hpp:1388
void start()
Creates the jemalloc arena; create() calls it, once, immediately upon construction; and nothing else ...
Definition: ipc_arena.cpp:515
void memory_manager_vaddr_stats_read(arena_id_t arena_id, Memory_manager_stats::Vaddr *vaddr) const
Helper that for the given jemalloc-arena arena_id reads the values into stat::Memory_manager_stats::V...
Definition: ipc_arena.cpp:1694
std::atomic< bool > m_destroy_started
Start at false; at some point in the destroy() flow becomes true to prevent event listeners from firi...
Definition: ipc_arena.hpp:1409
void this_thread_ensure_tcache_exists() const
Ensures the calling thread has a jemalloc tcache for *this arena, creating it if needed – making subs...
Definition: ipc_arena.cpp:865
void memory_manager_alloc_stats_read(arena_id_t arena_id, Memory_manager_stats::Alloc *alloc) const
Similar to memory_manager_vaddr_stats_read() but applied to stat::Memory_manager_stats::Alloc group.
Definition: ipc_arena.cpp:1722
const std::shared_ptr< Memory_manager > m_memory_manager
The jemalloc memory manager given to create().
Definition: ipc_arena.hpp:1385
static void global_stats_reset()
Resets owner_pool_lookup_global_stats() and obj_db_aux_pool_global_stats().
Definition: ipc_arena.cpp:1556
static bool commit_memory_pages_handler(extent_hooks_t *extent_hooks, void *address, size_t size, size_t offset, size_t length, unsigned arena_id)
jemalloc extent hook impl: forwards to similarly named member function of the proper this (which is s...
Definition: ipc_arena.cpp:898
std::set< Owner_shm_pool_listener * > m_listeners
The SHM pool listeners, each to be informed of relevant events (basically: pool(s) added,...
Definition: ipc_arena.hpp:1426
static void remove_shm_pool_handler(extent_hooks_t *extent_hooks, void *address, size_t size, bool committed, unsigned arena_id)
jemalloc extent hook impl: forwards to similarly named member function of the proper this (which is s...
Definition: ipc_arena.cpp:890
static bool purge_forced_memory_pages_handler(extent_hooks_t *extent_hooks, void *address, size_t size, size_t offset, size_t length, unsigned arena_id)
jemalloc extent hook impl: forwards to similarly named member function of the proper this (which is s...
Definition: ipc_arena.cpp:916
Sharded_stats m_sharded_stats
The stats for *this arena that are in the stat-collection purview of detail::Thread_lcl_obj_db_admin ...
Definition: ipc_arena.hpp:1462
Ipc_arena(flow::log::Logger *logger, const std::shared_ptr< Memory_manager > &memory_manager, Shared_name &&pool_name_base, const util::Permissions &permissions)
Constructor.
Definition: ipc_arena.cpp:486
std::vector< Memory_manager_stats > memory_manager_stats()
Returns the memory-manager-level statistics – one stat::Memory_manager_stats per native (jemalloc) ar...
Definition: ipc_arena.cpp:1572
flow::util::Lock_guard< Mutex > Lock
Exclusive lock for the mutex.
Definition: ipc_arena.hpp:1049
arena_id_t get_jemalloc_arena_id() const
Returns the jemalloc-ID for the (only, as of this writing) jemalloc-arena maintained by *this; this i...
Definition: ipc_arena.cpp:1458
bool remove_shm_pool_listener(Owner_shm_pool_listener *listener)
(Internal-use) Removes a listener from getting further updates on shared memory pools.
Definition: ipc_arena.cpp:1357
static const Owner_pool_lookup_global_stats & owner_pool_lookup_global_stats()
Returns the process-global owner-side stat-set stat::Owner_pool_lookup_global_stats,...
Definition: ipc_arena.cpp:1542
static bool optional_remove_shm_pool_handler(extent_hooks_t *extent_hooks, void *address, size_t size, bool committed, unsigned arena_id)
jemalloc extent hook impl: forwards to similarly named member function of the proper this (which is s...
Definition: ipc_arena.cpp:881
bool split_memory_pages(const void *address, size_t size, size_t size_a, size_t size_b, bool committed, arena_id_t arena_id)
jemalloc extent hook impl.
Definition: ipc_arena.cpp:1227
Pool_stats m_pool_stats
For *this arena, the non-sharded, non-jemalloc-sourced stats, bundled into stat::Pool_stats.
Definition: ipc_arena.hpp:1441
const std::shared_ptr< Memory_manager > & get_jemalloc_memory_manager() const
Returns the memory manager.
Definition: ipc_arena.cpp:1463
void info_dump(Info_dump *target_info_dump)
Fills-out the stats/info contents of the given stat::Arena_info_dump: a printable bundling of stats/i...
Definition: ipc_arena.cpp:1478
void * create_shm_pool(void *address, size_t size, size_t alignment, bool *zero, bool *commit, arena_id_t arena_id)
Creates a shared memory pool (jemalloc extent hook).
Definition: ipc_arena.cpp:943
void destroy_on_obj_db_forgot_us(const flow::log::Log_context_mt *log_ctx)
destroy() helper; handles the steps that remain once the possibly-async forgetting_shm_arena() finish...
Definition: ipc_arena.cpp:277
static bool decommit_memory_pages_handler(extent_hooks_t *extent_hooks, void *address, size_t size, size_t offset, size_t length, unsigned arena_id)
jemalloc extent hook impl: forwards to similarly named member function of the proper this (which is s...
Definition: ipc_arena.cpp:907
const Pool_stats & pool_stats() const
Returns the stat-set stat::Pool_stats for *this arena – the arena's non-sharded SHM-pool stats (obj-D...
Definition: ipc_arena.cpp:1532
void deallocate(void *address) noexcept
Undoes allocate(): deallocates a previously allocated, by this arena, buffer in SHM.
Definition: ipc_arena.cpp:730
void sharded_stats(Sharded_stats *target_stats)
Outputs the stat-set stat::Sharded_stats for *this arena.
Definition: ipc_arena.cpp:1513
bool commit_memory_pages(void *address, size_t size, size_t offset, size_t length, arena_id_t arena_id)
jemalloc extent hook impl.
Definition: ipc_arena.cpp:1146
bool optional_remove_shm_pool(void *address, size_t size, bool committed, arena_id_t arena_id)
jemalloc extent hook impl.
Definition: ipc_arena.cpp:1040
static constexpr tcache_id_t S_NO_TCACHE_ID
Special value returned by id_or_none() indicating no tcache yet exists for the given arena_id_t.
static Thread_cache * this_thread_cache_or_null()
Returns pointer to the one cache-object in the calling thread, or null if none exists (this_thread_ca...
static Thread_cache * this_thread_cache()
Returns pointer to the one cache-object in the calling thread, first creating it if it is the first s...
tcache_id_t id(arena_id_t arena_id)
Returns tcache ID of the tcache in this thread with a 1-1 relationship to the given arena,...
String-wrapping abstraction representing a name uniquely distinguishing a kernel-persistent entity fr...
Stats-related sub-namespace, for ADL segregation and general organization.
void sharded_stats(const Shm_arena &shm_arena, Sharded_stats *target_stats)
Assembles and outputs the current stats tracked by the obj-DB module (as anchored by Thread_lcl_obj_d...
Definition: stats.hpp:254
void sharded_stats_reset(const Shm_arena &shm_arena, Sharded_stats *target_stats)
Resets Sharded_stats; companion to sharded_stats().
Definition: stats.hpp:260
size_t page_size()
Returns the jemalloc page size in bytes (arenas.page), read once and cached (constant for the process...
bool config_stats_enabled()
Returns whether the linked jemalloc was built with statistics support (its config....
uint64_t epoch_refresh()
Refreshes jemalloc's statistics snapshot (writes mallctl("epoch")), so that subsequently-read "stats....
uint64_t epoch()
Returns jemalloc's current statistics epoch (reads mallctl("epoch")), without refreshing anything.
void cumulative_alloc_dealloc_sz(arena_id_t arena_id, uint64_t *alloc_sz, uint64_t *dealloc_sz, flow::util::stat::Histogram_counter *alloc_count_by_size)
Computes, for the given jemalloc-arena, the cumulative bytes-ever-allocated and bytes-ever-deallocate...
int mallctl_read(const char *name, T *val)
Reads a single scalar mallctl value, by name, into *val (using sizeof(T) as the value size).
flow::util::Mutex_non_recursive & jemalloc_stats_mutex()
Returns the process-global mutex held throughout each of Flow-IPC's jemalloc-statistics operations – ...
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.
std::ostream & operator<<(std::ostream &os, const Ipc_arena &val)
Prints string representation of the given Ipc_arena to the given ostream.
Definition: ipc_arena.cpp:1762
void set_logger(flow::log::Logger *logger_ptr)
Convenience function that sets (or clears) the Logger used by all SHM-arena-lend singleton subsystems...
Definition: util.cpp:32
Error_code make_error_code(Code err_code)
Given a Code enum value, creates a lightweight Error_code (a/k/a boost.system error_code) representin...
Definition: error.cpp:96
bipc::permissions Permissions
Short-hand for Unix (POSIX) permissions class.
Definition: util_fwd.hpp:158
flow::util::String_view String_view
Short-hand for Flow's String_view.
Definition: util_fwd.hpp:112
flow::Error_code Error_code
Short-hand for flow::Error_code which is very common.
Definition: common.hpp:313
#define IPC_SHM_ARENA_LEND_JEMALLOC_API(ARG_api_name)
To be used when invoking jemalloc public API functions, this evaluates to the given argument (which s...
Definition: jemalloc.hpp:87
Printable (with knobs in contained util::stat::Info_dump_format) bundling of stats/info relevant to (...
std::vector< arena_lend::stat::Shm_pool_info > m_shm_pool_live_info
Target Ipc_arena's info from: Ipc_arena::shm_pool_live_info().
arena_lend::stat::Obj_db_aux_pool_global_stats m_obj_db_aux_pool_global_stats
Global stats from: static Ipc_arena::obj_db_aux_pool_global_stats().
util::stat::Info_dump_format m_fmt
Print-format knobs in effect at ostream << *this time.
arena_lend::stat::Owner_pool_lookup_global_stats m_owner_pool_lookup_global_stats
Global stats from: static Ipc_arena::owner_pool_lookup_global_stats().
std::vector< arena_lend::stat::Memory_manager_stats > m_mem_mgr_stats
Target Ipc_arena's stats from: Ipc_arena::memory_manager_stats().
arena_lend::stat::Sharded_stats m_sharded_stats
Target Ipc_arena's stats from: Ipc_arena::sharded_stats().
std::string m_mem_mgr_stats_dump
If unavailable then empty; otherwise the newline-terminated output of Memory_manager::stats_dump_to_o...
arena_lend::stat::Pool_stats m_pool_stats
Target Ipc_arena's stats from: Ipc_arena::pool_stats().
Sub-group: raw allocation activity – cumulative counts of memory-manager allocate/deallocate operatio...
uint64_t m_live_count
(Gauge) Currently-live allocation count (= m_alloc_count minus m_dealloc_count).
flow::util::stat::Histogram_counter m_histo_alloc_count_by_sz
Cumulative allocation count broken down by allocation size-class, as a histogram: each bucket counts ...
uint64_t m_dealloc_count
Cumulative count of deallocate ops (for SHM-jemalloc: jemalloc small + large ndalloc).
size_t m_live_sz
(Gauge) Currently-live allocated bytes (for SHM-jemalloc: jemalloc small + large allocated).
uint64_t m_alloc_count
Cumulative count of allocate ops (for SHM-jemalloc: jemalloc small + large nmalloc).
uint64_t m_dealloc_sz
Cumulative bytes deallocated (for SHM-jemalloc: sum over size-classes of ndalloc x size-class-byte-si...
uint64_t m_alloc_sz
Cumulative bytes allocated (for SHM-jemalloc: sum over size-classes of nmalloc x size-class-byte-size...
Sub-group: the arena's virtual-address-space / page-state footprint – how much memory it currently ho...
size_t m_active_sz
(Gauge) Bytes in pages backing live (active) allocations: ~= live allocated bytes + unused space on t...
size_t m_retained_sz
(Gauge) Bytes of once-used vaddr space the memory-manager has excluded from m_mapped_sz book-keeping ...
size_t m_inactive_resident_sz
(Gauge) Not-active (so freed from/not yet used for allocated user-buffers; contrast with m_active_sz)...
size_t m_resident_sz
(Gauge) Roughly: the peak, since arena creation, of concurrently-allocated bytes (rounded up to jemal...
size_t m_mapped_sz
(Gauge) Total bytes currently mapped from the OS into the arena's address space.
size_t m_inactive_muzzy_sz
(Gauge) Not-active (see also m_inactive_resident_sz) bytes that the memory manager has returned to th...
Similar mission to Obj_db_aux_pool_stats but covers information that, by nature, is not per-arena but...
Similar mission to Owner_pool_stats but covers information that, by nature, is not per-arena but rath...
std::atomic< uint64_t > m_pool_destroy_sz
Across the pool-destructions counted in m_pool_destroy_count, the sum of the actual sizes of those po...
std::atomic< uint64_t > m_pool_destroy_count
Number of times the memory manager (e.g., jemalloc for SHM-jemalloc) requested something,...
std::atomic< uint64_t > m_pool_optional_destroy_request_count
Number of times the memory manager (e.g., jemalloc for SHM-jemalloc) requested the optional removal (...
std::atomic< uint64_t > m_pool_create_count
Number of times the memory manager (e.g., jemalloc for SHM-jemalloc) requested the creation of a new ...
std::atomic< uint64_t > m_pool_create_sz
Across the pool-creations counted in m_pool_create_count, the sum of the requested/actual sizes of th...
Bundles together per-arena Owner_pool_stats and Obj_db_aux_pool_stats, so they can be accessed via on...
Owner_pool_stats m_owner_pool
Sub-struct with individual stats; see type's doc header.
A bundle of several individually-documented stat-sets – object construct/dispose/live and zombie-reap...
IDs an arena, as a GAUGE in contexts like Shm_pool_info and per-arena Borrower_pool_stats,...
bool m_verbose
If true: Print everything you've got; if false: refrain from producing multi-page output if possible.
Definition: util.hpp:56