Track quicklist memory incrementally via zmalloc_usable capture#2
Track quicklist memory incrementally via zmalloc_usable capture#2
Conversation
Signed-off-by: Lior Sventitzky <liorsve@amazon.com>
Signed-off-by: Lior Sventitzky <liorsve@amazon.com>
Signed-off-by: Lior Sventitzky <liorsve@amazon.com>
Signed-off-by: Lior Sventitzky <liorsve@amazon.com>
Signed-off-by: Lior Sventitzky <liorsve@amazon.com>
Signed-off-by: Lior Sventitzky <liorsve@amazon.com>
|
I've been thinking about a simpler alternative that avoids modifying the listpack allocator, avoids adding Key insightEvery place
And every node enters/leaves the quicklist through:
We can track the logical data size (sum of The change1. Add one field to typedef struct quicklist {
quicklistNode *head;
quicklistNode *tail;
unsigned long count;
unsigned long len;
signed int fill : QL_FILL_BITS;
unsigned int compress : QL_COMP_BITS;
unsigned int bookmark_count : QL_BM_BITS;
size_t tracked_data_bytes; /* NEW: running sum of node->sz for all nodes */
quicklistBookmark bookmarks[];
} quicklist;2. Track on node link/unlink: static void __quicklistInsertNode(quicklist *quicklist, ..., quicklistNode *new_node, ...) {
/* ... existing linking code ... */
quicklist->len++;
quicklist->tracked_data_bytes += new_node->sz; /* NEW */
}
static void __quicklistDelNode(quicklist *quicklist, quicklistNode *node) {
/* ... existing unlinking code ... */
quicklist->tracked_data_bytes -= node->sz; /* NEW */
quicklist->len--;
/* ... rest of function ... */
}3. Extend the macro to track deltas on in-place mutations: #define quicklistNodeUpdateSz(ql, node) \
do { \
size_t _old_sz = (node)->sz; \
(node)->sz = lpBytes((node)->entry); \
(ql)->tracked_data_bytes += (node)->sz - _old_sz; \
} while (0)All 14 call sites of 4. One companion macro for the single direct-assignment case on a linked node: #define quicklistNodeSetSz(ql, node, new_sz) \
do { \
(ql)->tracked_data_bytes += (new_sz) - (node)->sz; \
(node)->sz = (new_sz); \
} while (0)This is only needed in Why this works for every mutation site
Comparison with the current PR approach
What we track and what we don'tWe track the logical uncompressed data size — the sum of
For per-slot memory reporting, the metadata portion ( Note on accuracyThe logical size is arguably more useful than the allocator size for per-slot memory balancing. It represents the actual user data volume and is deterministic — it doesn't vary based on jemalloc version, size classes, or compression settings. Two nodes with the same data will report the same size regardless of whether they're compressed or which allocator is in use. |
Summary
Add incremental memory tracking to quicklist by capturing allocation sizes already computed internally by$O(1)$ instead of iterating all nodes.
zmalloc_usable/zrealloc_usable, trading 8 bytes perquicklistNodeand 8 bytes perquicklistto avoid callingzmalloc_size()on mutation hot paths (LPUSH,RPUSH,LPOP,RPOP,LINSERT,LREM,LTRIM,LSET). AddedobjectComputeSizeWithTrackedSize()as a parallel toobjectComputeSize()that uses the tracked size for quicklists, computingLISTmemory inStruct changes
quicklistNode — added
size_t entry_alloc_szStores the usable allocation size of the node's entry buffer (listpack or plain data).
quicklist — added
size_t tracked_sizeRunning total of all memory allocated for the quicklist:
sizeof(quicklist) + sum of sizeof(quicklistNode) + entry_alloc_szfor every node. Maintained incrementally on insert, delete, compress, decompress, merge, split, and replace.How allocation size capture works
zmalloc_usable()andzrealloc_usable()already callzmalloc_size()internally to update jemalloc memory stats — the result was previously discarded. We now capture it instead of throwing it away.There are three different capture paths depending on the operation:
Listpack mutations (push, pop, delete, insert, replace, merge, split, delete-range):
The listpack allocator macros
lp_malloc/lp_reallocinlistpack_malloc.hnow pass&lp_last_alloc_sizeinstead ofNULLtozmalloc_usable/zrealloc_usable. After any listpack operation,quicklist.creads the captured size vialpLastAllocSize()and updates the node'sentry_alloc_szand the quicklist'stracked_sizethrough thequicklistTrackEntryResize()macro.Compress/decompress:
__quicklistCompressNodecallszrealloc_usable()directly with&new_entry_alloc_szwhen reallocating the LZF buffer.__quicklistDecompressNodecallszmalloc_usable()directly with&new_entry_alloc_szwhen allocating the decompressed buffer. Both compute the delta against the oldentry_alloc_szand updatetracked_size.Note: We cannot reuse the existing
szfield inquicklistNodebecauseszstores the logical uncompressed data size, whileentry_alloc_szreflects the actual jemalloc allocation (which differs due to size-class rounding, and changes entirely when the entry is compressed into an LZF struct).Plain node creation (
__quicklistCreateNodewithQUICKLIST_NODE_CONTAINER_PLAIN):Calls
zmalloc_usable()directly with&new_node->entry_alloc_sz.Remaining
zmalloc_size()call sitesTwo RDB load paths still call
zmalloc_size()because they receive pre-allocated buffers from the RDB loader (not through the listpack allocator):quicklistAppendListpack— receives a listpack allocated during RDB deserializationquicklistAppendPlainNode— receives a plain data buffer allocated during RDB deserializationListpack no-realloc paths
When a listpack operation skips reallocation (same-size replacement, growth within jemalloc slack, no-op delete/range),
lp_last_alloc_sizeis explicitly set tolp_malloc_size(lp)so thatlpLastAllocSize()returns the current allocation size. This is done in:lpInsert— growth fits within jemalloc slack, or same-size replacementlpShrinkToFit— allocation already at minimumlpDeleteRange/lpDeleteRangeWithEntry— early returns onnum == 0or seek failureTradeoff
This approach adds 8 bytes per
quicklistNode(entry_alloc_sz) and 8 bytes perquicklist(tracked_size) to avoid callingzmalloc_size()on mutation hot paths. Instead of twozmalloc_size()calls per operation (before/after), allocation sizes are captured for free fromzmalloc_usable/zrealloc_usablewhich already compute them internally.The implementation is a bit more complicated than a simple
zmalloc_sizeapproach—the listpack layer requires updates to setlp_last_alloc_sizein all no-realloc code paths (same-size replacement, jemalloc slack, no-op deletes) to prevent stale values, and every new allocation path (compress, decompress, plain nodes) must explicitly capture its size.Files changed
entry_alloc_sztoquicklistNode,tracked_sizetoquicklistquicklistTrackEntryResize()macro; updated all mutation sites to maintaintracked_size; threadedquicklist*through compress/decompress for tracking deltaslp_malloc/lp_reallocto capture usable size intolp_last_alloc_sizelpLastAllocSize()getter; updated no-realloc paths to setlp_last_alloc_sizelpLastAllocSize()objectComputeSizeWithTrackedSize()that usestracked_sizefor quickliststracked_sizematchesobjectComputeSize()across all mutation types