Skip to content

Commit ac2e75a

Browse files
committed
mem: add an hg_stats MI command, and document carving
The shared shmem: statistics have six slots, designed for an allocator where freeing returns memory to one general pool. HG_MALLOC carves the arena into fixed size-class chunks that are never handed back, so committed, live and available stop being the same question and the six slots cannot express the difference. Rather than overload the shared names further, report the allocator's own view: which tier the arena got, how much has ever been carved, how much of that is live versus sitting recycled on a free stack, and the per-size-class chunk breakdown. pkg arenas are per-process, so that half reports the arena of whichever process answered. Document what carving means alongside it, including the two things that surprise: free_size + real_used_size does not sum to total_size (the difference is carved-but-idle capacity), and recycled capacity is only reusable within its own size class.
1 parent 01d980d commit ac2e75a

2 files changed

Lines changed: 232 additions & 0 deletions

File tree

‎mem/README.hg_malloc‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,86 @@ while the other allocators on the same machine and workload ranged from
117117
still worth using.
118118

119119

120+
Carving, and what the memory statistics mean
121+
--------------------------------------------
122+
123+
HG_MALLOC does not treat the arena as one pool of interchangeable bytes.
124+
It CARVES it: the first time a process needs a cell of some size class, a
125+
whole chunk is cut out of the arena, divided into equal cells of that
126+
class, and given to that process. A chunk is never handed back. Freeing a
127+
cell returns it to a per-process free stack, ready to be reused - but only
128+
for the same size class, and preferentially by the same process.
129+
130+
That single property is why the memory statistics read differently here
131+
than under q_malloc or f_malloc, where a freed fragment returns to a
132+
general pool and can serve a request of any size.
133+
134+
used_size payload bytes currently handed to callers. Rises and
135+
falls with load; same meaning as everywhere else.
136+
137+
real_used_size live commitment - the cell slots currently handed
138+
out, including each cell's header and the round-up to
139+
its size class. Rises and falls with load.
140+
141+
max_used_size high-water mark of real_used_size. Climbs to the
142+
working set and then holds, as for the other
143+
allocators.
144+
145+
free_size arena not yet carved. This is the figure that
146+
predicts "no more HG_MALLOC arena memory": it counts
147+
down as chunks are cut, and never goes back up.
148+
149+
fragments live cells.
150+
151+
Two consequences are worth stating plainly.
152+
153+
First, free_size + real_used_size does not add up to total_size. Under
154+
q_malloc it does. Here the difference is the carved-but-idle capacity -
155+
cells sitting on a free stack. They are genuinely reusable, so calling
156+
them "used" would overstate demand; they are also already committed to one
157+
size class, so calling them "free" would overstate what is available. The
158+
identity that does hold is:
159+
160+
free_size + carved = total_size
161+
162+
Second, recycled capacity is not interchangeable. An arena can report a
163+
large free_size and still fail an allocation, if what has been carved does
164+
not match the size being asked for. In practice a given workload settles
165+
into a stable mix of sizes and this does not arise, but it is the reason
166+
free_size is defined as "not yet carved" rather than "not in use".
167+
168+
None of this has to be inferred: hg_stats below reports each quantity
169+
separately.
170+
171+
172+
hg_stats
173+
--------
174+
175+
opensips-cli -x mi hg_stats
176+
177+
Reports the allocator's own view of each arena it manages: shm, and the
178+
pkg arena of whichever process answered the command. pkg arenas are
179+
per-process, so that half is one process's arena, not a server-wide total.
180+
181+
tier which of the four huge-page tiers the arena got
182+
total_size arena size, as given by -m / -M
183+
pinned_mb MB protected from swapping
184+
carved bytes ever cut out of the arena into chunks
185+
carved_peak high-water mark of the above
186+
chunks how many chunks that is
187+
free_to_carve total_size - carved, i.e. shmem:free_size
188+
live_cell_bytes cell slots currently handed out
189+
live_payload of which, requested payload, i.e. shmem:used_size
190+
live_cells number of live cells, i.e. shmem:fragments
191+
live_peak high-water mark, i.e. shmem:max_used_size
192+
recycled carved but idle: on a free stack or in the global
193+
pool
194+
classes per size class: cell size, chunk count, total cells
195+
196+
The per-class breakdown is the quickest way to tell whether an arena is
197+
being carved into the sizes the workload actually asks for.
198+
199+
120200
Per-module memory accounting
121201
----------------------------
122202

‎mi/mi_core.c‎

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@
4444
#include "../net/net_tcp.h"
4545
#include "../mem/mem.h"
4646
#include "../mem/rpm_mem.h"
47+
#ifdef HG_MALLOC
48+
#include "../mem/shm_mem.h"
49+
#include "../mem/hg_malloc.h"
50+
#endif
4751
#include "../cachedb/cachedb.h"
4852
#include "../evi/event_interface.h"
4953
#include "../ipc.h"
@@ -982,6 +986,146 @@ static mi_response_t *w_reload_routes(const mi_params_t *params,
982986

983987

984988

989+
#ifdef HG_MALLOC
990+
/*
991+
* HG_MALLOC keeps state the shared shmem:/pkgmem: statistics cannot express.
992+
* Those six figures were designed for a free-list allocator, where freed
993+
* memory returns to one general pool; HG_MALLOC instead CARVES the arena into
994+
* fixed size-class chunks that are never given back, so "how much is
995+
* committed", "how much is live" and "how much can still be handed out" stop
996+
* being the same question. Rather than overload the shared names further,
997+
* report the allocator's own view here.
998+
*/
999+
static int hg_stats_one(mi_item_t *parent, char *name, struct hg_block *hb)
1000+
{
1001+
mi_item_t *o, *cls_arr, *cls_item;
1002+
struct hg_chunk *ch;
1003+
unsigned int chunks_of[HG_NCLASSES], cell_size_of[HG_NCLASSES];
1004+
unsigned long cells_of[HG_NCLASSES];
1005+
unsigned long carved;
1006+
const char *tier;
1007+
int c;
1008+
1009+
if (!hb)
1010+
return 0;
1011+
1012+
o = add_mi_object(parent, name, strlen(name));
1013+
if (!o)
1014+
return -1;
1015+
1016+
carved = hb->real_used;
1017+
tier = hg_mem_tier_str(hb->tier);
1018+
1019+
if (add_mi_string(o, MI_SSTR("tier"), (char *)tier, strlen(tier)) < 0)
1020+
return -1;
1021+
if (add_mi_number(o, MI_SSTR("total_size"), hb->size) < 0)
1022+
return -1;
1023+
if (add_mi_number(o, MI_SSTR("pinned_mb"), hb->locked_mb) < 0)
1024+
return -1;
1025+
1026+
/* carved: bytes taken from the arena and cut into size-class chunks.
1027+
* Never returned - this is the figure that only ever grows, and the one
1028+
* that free_size counts down from. */
1029+
if (add_mi_number(o, MI_SSTR("carved"), carved) < 0)
1030+
return -1;
1031+
if (add_mi_number(o, MI_SSTR("carved_peak"), hb->max_real_used) < 0)
1032+
return -1;
1033+
if (add_mi_number(o, MI_SSTR("chunks"), hb->nchunks) < 0)
1034+
return -1;
1035+
/* what shmem:free_size reports: arena never yet carved */
1036+
if (add_mi_number(o, MI_SSTR("free_to_carve"), hb->size - carved) < 0)
1037+
return -1;
1038+
1039+
/* live: what is actually handed out right now */
1040+
if (add_mi_number(o, MI_SSTR("live_cell_bytes"), hg_cell_live(hb)) < 0)
1041+
return -1;
1042+
if (add_mi_number(o, MI_SSTR("live_payload"), hg_used(hb)) < 0)
1043+
return -1;
1044+
if (add_mi_number(o, MI_SSTR("live_cells"), hg_fragments(hb)) < 0)
1045+
return -1;
1046+
if (add_mi_number(o, MI_SSTR("live_peak"), hb->max_live_used) < 0)
1047+
return -1;
1048+
1049+
/* carved but idle: on a private free stack or in the global pool.
1050+
* Reusable, but ONLY for its own size class - which is why it is not
1051+
* counted as free_to_carve. */
1052+
if (add_mi_number(o, MI_SSTR("recycled"), hg_slab_recycled(hb)) < 0)
1053+
return -1;
1054+
1055+
memset(chunks_of, 0, sizeof chunks_of);
1056+
memset(cell_size_of, 0, sizeof cell_size_of);
1057+
memset(cells_of, 0, sizeof cells_of);
1058+
for (ch = hb->chunks; ch; ch = ch->next) {
1059+
if (ch->cls >= HG_NCLASSES)
1060+
continue;
1061+
chunks_of[ch->cls]++;
1062+
cell_size_of[ch->cls] = ch->cell_size;
1063+
cells_of[ch->cls] += ch->cells;
1064+
}
1065+
1066+
cls_arr = add_mi_array(o, MI_SSTR("classes"));
1067+
if (!cls_arr)
1068+
return -1;
1069+
for (c = 0; c < HG_NCLASSES; c++) {
1070+
if (!chunks_of[c])
1071+
continue;
1072+
cls_item = add_mi_object(cls_arr, 0, 0);
1073+
if (!cls_item)
1074+
return -1;
1075+
if (add_mi_number(cls_item, MI_SSTR("cell_size"),
1076+
cell_size_of[c]) < 0)
1077+
return -1;
1078+
if (add_mi_number(cls_item, MI_SSTR("chunks"), chunks_of[c]) < 0)
1079+
return -1;
1080+
if (add_mi_number(cls_item, MI_SSTR("cells"), cells_of[c]) < 0)
1081+
return -1;
1082+
}
1083+
1084+
return 0;
1085+
}
1086+
1087+
static mi_response_t *mi_hg_stats(const mi_params_t *params,
1088+
struct mi_handler *async_hdl)
1089+
{
1090+
mi_response_t *resp;
1091+
mi_item_t *resp_obj;
1092+
int reported = 0;
1093+
1094+
resp = init_mi_result_object(&resp_obj);
1095+
if (!resp)
1096+
return 0;
1097+
1098+
if (mem_allocator_shm == MM_HG_MALLOC ||
1099+
mem_allocator_shm == MM_HG_MALLOC_DBG) {
1100+
if (hg_stats_one(resp_obj, "shm", (struct hg_block *)shm_block) < 0)
1101+
goto error;
1102+
reported++;
1103+
}
1104+
1105+
if (mem_allocator_pkg == MM_HG_MALLOC ||
1106+
mem_allocator_pkg == MM_HG_MALLOC_DBG) {
1107+
/* pkg arenas are per-process: this is the arena of whichever
1108+
* process answered the command, not a fleet-wide total */
1109+
if (hg_stats_one(resp_obj, "pkg", (struct hg_block *)mem_block) < 0)
1110+
goto error;
1111+
reported++;
1112+
}
1113+
1114+
if (!reported) {
1115+
free_mi_response(resp);
1116+
return init_mi_error(400,
1117+
MI_SSTR("HG_MALLOC is not the active allocator"));
1118+
}
1119+
1120+
return resp;
1121+
1122+
error:
1123+
LM_ERR("failed to add mi item\n");
1124+
free_mi_response(resp);
1125+
return 0;
1126+
}
1127+
#endif /* HG_MALLOC */
1128+
9851129
static const mi_export_t mi_core_cmds[] = {
9861130
{ "pi_list", "lists the provisioning framework", 0, 0, {
9871131
{w_mi_pi_list, {0}},
@@ -1005,6 +1149,14 @@ static const mi_export_t mi_core_cmds[] = {
10051149
{mi_pwd, {0}},
10061150
{EMPTY_MI_RECIPE}}, {0}
10071151
},
1152+
#ifdef HG_MALLOC
1153+
{ "hg_stats", "HG_MALLOC arena internals: how much of the arena is "
1154+
"carved into size-class chunks, how much of that is live versus "
1155+
"recycled, and the per-class chunk breakdown", 0, 0, {
1156+
{mi_hg_stats, {0}},
1157+
{EMPTY_MI_RECIPE}}, {0}
1158+
},
1159+
#endif
10081160
{ "arg", "returns the full list of arguments used at startup", 0, 0, {
10091161
{mi_arg, {0}},
10101162
{EMPTY_MI_RECIPE}}, {0}

0 commit comments

Comments
 (0)