]> granicus.if.org Git - postgresql/commitdiff
Add transparent block-level memory accounting
authorTomas Vondra <tomas.vondra@postgresql.org>
Tue, 1 Oct 2019 01:13:39 +0000 (03:13 +0200)
committerTomas Vondra <tomas.vondra@postgresql.org>
Tue, 1 Oct 2019 01:13:39 +0000 (03:13 +0200)
Adds accounting of memory allocated in a memory context. Compared to
various ad hoc solutions, the main advantage is that the accounting is
transparent and does not require direct control over allocations (this
matters for use cases where the allocations happen in user code, like
for example aggregate states allocated in a transition functions).

To reduce overhead, the accounting happens at the block level (not for
individual chunks) and only the context immediately owning the block is
updated. When inquiring about amount of memory allocated in a context,
we have to recursively walk all children contexts.

This "lazy" accounting works well for cases with relatively small number
of contexts in the relevant subtree and/or with infrequent inquiries.

Author: Jeff Davis
Reivewed-by: Tomas Vondra, Melanie Plageman, Soumyadeep Chakraborty
Discussion: https://www.postgresql.org/message-id/flat/027a129b8525601c6a680d27ce3a7172dab61aab.camel@j-davis.com

src/backend/utils/mmgr/README
src/backend/utils/mmgr/aset.c
src/backend/utils/mmgr/generation.c
src/backend/utils/mmgr/mcxt.c
src/backend/utils/mmgr/slab.c
src/include/nodes/memnodes.h
src/include/utils/memutils.h

index 7e6541d0dee2783065b66cd6482478c298dec7a9..b9ba3fc7730437cfdddc30b2a07cac5cdd80bfe2 100644 (file)
@@ -23,6 +23,10 @@ The basic operations on a memory context are:
 * reset a context (free all memory allocated in the context, but not the
   context object itself)
 
+* inquire about the total amount of memory allocated to the context
+  (the raw memory from which the context allocates chunks; not the
+  chunks themselves)
+
 Given a chunk of memory previously allocated from a context, one can
 free it or reallocate it larger or smaller (corresponding to standard C
 library's free() and realloc() routines).  These operations return memory
@@ -452,3 +456,33 @@ returns the memory when reset/deleted).
 
 These memory contexts were initially developed for ReorderBuffer, but
 may be useful elsewhere as long as the allocation patterns match.
+
+
+Memory Accounting
+-----------------
+
+One of the basic memory context operations is determining the amount of
+memory used in the context (and it's children). We have multiple places
+that implement their own ad hoc memory accounting, and this is meant to
+provide a unified approach. Ad hoc accounting solutions work for places
+with tight control over the allocations or when it's easy to determine
+sizes of allocated chunks (e.g. places that only work with tuples).
+
+The accounting built into the memory contexts is transparent and works
+transparently for all allocations as long as they end up in the right
+memory context subtree.
+
+Consider for example aggregate functions - the aggregate state is often
+represented by an arbitrary structure, allocated from the transition
+function, so the ad hoc accounting is unlikely to work. The built-in
+accounting will however handle such cases just fine.
+
+To minimize overhead, the accounting is done at the block level, not for
+individual allocation chunks.
+
+The accounting is lazy - after a block is allocated (or freed), only the
+context owning that block is updated. This means that when inquiring
+about the memory usage in a given context, we have to walk all children
+contexts recursively. This means the memory accounting is not intended
+for cases with too many memory contexts (in the relevant subtree).
+
index 6b63d6f85d042b073d3cd5c504366e25aa217978..90f370570fe37176d3a87100b5734f0e7176e5c3 100644 (file)
@@ -458,6 +458,9 @@ AllocSetContextCreateInternal(MemoryContext parent,
                                                                parent,
                                                                name);
 
+                       ((MemoryContext) set)->mem_allocated =
+                               set->keeper->endptr - ((char *) set);
+
                        return (MemoryContext) set;
                }
        }
@@ -546,6 +549,8 @@ AllocSetContextCreateInternal(MemoryContext parent,
                                                parent,
                                                name);
 
+       ((MemoryContext) set)->mem_allocated = firstBlockSize;
+
        return (MemoryContext) set;
 }
 
@@ -566,6 +571,7 @@ AllocSetReset(MemoryContext context)
 {
        AllocSet        set = (AllocSet) context;
        AllocBlock      block;
+       Size            keepersize = set->keeper->endptr - ((char *) set);
 
        AssertArg(AllocSetIsValid(set));
 
@@ -604,6 +610,8 @@ AllocSetReset(MemoryContext context)
                else
                {
                        /* Normal case, release the block */
+                       context->mem_allocated -= block->endptr - ((char*) block);
+
 #ifdef CLOBBER_FREED_MEMORY
                        wipe_mem(block, block->freeptr - ((char *) block));
 #endif
@@ -612,6 +620,8 @@ AllocSetReset(MemoryContext context)
                block = next;
        }
 
+       Assert(context->mem_allocated == keepersize);
+
        /* Reset block size allocation sequence, too */
        set->nextBlockSize = set->initBlockSize;
 }
@@ -628,6 +638,7 @@ AllocSetDelete(MemoryContext context)
 {
        AllocSet        set = (AllocSet) context;
        AllocBlock      block = set->blocks;
+       Size            keepersize = set->keeper->endptr - ((char *) set);
 
        AssertArg(AllocSetIsValid(set));
 
@@ -683,6 +694,9 @@ AllocSetDelete(MemoryContext context)
        {
                AllocBlock      next = block->next;
 
+               if (block != set->keeper)
+                       context->mem_allocated -= block->endptr - ((char *) block);
+
 #ifdef CLOBBER_FREED_MEMORY
                wipe_mem(block, block->freeptr - ((char *) block));
 #endif
@@ -693,6 +707,8 @@ AllocSetDelete(MemoryContext context)
                block = next;
        }
 
+       Assert(context->mem_allocated == keepersize);
+
        /* Finally, free the context header, including the keeper block */
        free(set);
 }
@@ -733,6 +749,9 @@ AllocSetAlloc(MemoryContext context, Size size)
                block = (AllocBlock) malloc(blksize);
                if (block == NULL)
                        return NULL;
+
+               context->mem_allocated += blksize;
+
                block->aset = set;
                block->freeptr = block->endptr = ((char *) block) + blksize;
 
@@ -928,6 +947,8 @@ AllocSetAlloc(MemoryContext context, Size size)
                if (block == NULL)
                        return NULL;
 
+               context->mem_allocated += blksize;
+
                block->aset = set;
                block->freeptr = ((char *) block) + ALLOC_BLOCKHDRSZ;
                block->endptr = ((char *) block) + blksize;
@@ -1028,6 +1049,9 @@ AllocSetFree(MemoryContext context, void *pointer)
                        set->blocks = block->next;
                if (block->next)
                        block->next->prev = block->prev;
+
+               context->mem_allocated -= block->endptr - ((char*) block);
+
 #ifdef CLOBBER_FREED_MEMORY
                wipe_mem(block, block->freeptr - ((char *) block));
 #endif
@@ -1144,6 +1168,7 @@ AllocSetRealloc(MemoryContext context, void *pointer, Size size)
                AllocBlock      block = (AllocBlock) (((char *) chunk) - ALLOC_BLOCKHDRSZ);
                Size            chksize;
                Size            blksize;
+               Size            oldblksize;
 
                /*
                 * Try to verify that we have a sane block pointer: it should
@@ -1159,6 +1184,8 @@ AllocSetRealloc(MemoryContext context, void *pointer, Size size)
                /* Do the realloc */
                chksize = MAXALIGN(size);
                blksize = chksize + ALLOC_BLOCKHDRSZ + ALLOC_CHUNKHDRSZ;
+               oldblksize = block->endptr - ((char *)block);
+
                block = (AllocBlock) realloc(block, blksize);
                if (block == NULL)
                {
@@ -1166,6 +1193,9 @@ AllocSetRealloc(MemoryContext context, void *pointer, Size size)
                        VALGRIND_MAKE_MEM_NOACCESS(chunk, ALLOCCHUNK_PRIVATE_LEN);
                        return NULL;
                }
+
+               context->mem_allocated += blksize - oldblksize;
+
                block->freeptr = block->endptr = ((char *) block) + blksize;
 
                /* Update pointers since block has likely been moved */
@@ -1383,6 +1413,7 @@ AllocSetCheck(MemoryContext context)
        const char *name = set->header.name;
        AllocBlock      prevblock;
        AllocBlock      block;
+       int64           total_allocated = 0;
 
        for (prevblock = NULL, block = set->blocks;
                 block != NULL;
@@ -1393,6 +1424,11 @@ AllocSetCheck(MemoryContext context)
                long            blk_data = 0;
                long            nchunks = 0;
 
+               if (set->keeper == block)
+                       total_allocated += block->endptr - ((char *) set);
+               else
+                       total_allocated += block->endptr - ((char *) block);
+
                /*
                 * Empty block - empty can be keeper-block only
                 */
@@ -1479,6 +1515,8 @@ AllocSetCheck(MemoryContext context)
                        elog(WARNING, "problem in alloc set %s: found inconsistent memory block %p",
                                 name, block);
        }
+
+       Assert(total_allocated == context->mem_allocated);
 }
 
 #endif                                                 /* MEMORY_CONTEXT_CHECKING */
index eaacafb7be598514e466f1e67b4828ca755e18b1..2d24ab68bc037fc7af8c72fd0247e835ad50141c 100644 (file)
@@ -297,6 +297,8 @@ GenerationReset(MemoryContext context)
 
                dlist_delete(miter.cur);
 
+               context->mem_allocated -= block->blksize;
+
 #ifdef CLOBBER_FREED_MEMORY
                wipe_mem(block, block->blksize);
 #endif
@@ -352,6 +354,8 @@ GenerationAlloc(MemoryContext context, Size size)
                if (block == NULL)
                        return NULL;
 
+               context->mem_allocated += blksize;
+
                /* block with a single (used) chunk */
                block->blksize = blksize;
                block->nchunks = 1;
@@ -407,6 +411,8 @@ GenerationAlloc(MemoryContext context, Size size)
                if (block == NULL)
                        return NULL;
 
+               context->mem_allocated += blksize;
+
                block->blksize = blksize;
                block->nchunks = 0;
                block->nfree = 0;
@@ -522,6 +528,7 @@ GenerationFree(MemoryContext context, void *pointer)
        if (set->block == block)
                set->block = NULL;
 
+       context->mem_allocated -= block->blksize;
        free(block);
 }
 
@@ -746,6 +753,7 @@ GenerationCheck(MemoryContext context)
        GenerationContext *gen = (GenerationContext *) context;
        const char *name = context->name;
        dlist_iter      iter;
+       int64           total_allocated = 0;
 
        /* walk all blocks in this context */
        dlist_foreach(iter, &gen->blocks)
@@ -755,6 +763,8 @@ GenerationCheck(MemoryContext context)
                                        nchunks;
                char       *ptr;
 
+               total_allocated += block->blksize;
+
                /*
                 * nfree > nchunks is surely wrong, and we don't expect to see
                 * equality either, because such a block should have gotten freed.
@@ -833,6 +843,8 @@ GenerationCheck(MemoryContext context)
                        elog(WARNING, "problem in Generation %s: number of free chunks %d in block %p does not match header %d",
                                 name, nfree, block, block->nfree);
        }
+
+       Assert(total_allocated == context->mem_allocated);
 }
 
 #endif                                                 /* MEMORY_CONTEXT_CHECKING */
index b07be1223698a094f1b3a41ac59370d2b3d1cdb7..7bbfabe0eab7c338942ae999633f621b710a9b90 100644 (file)
@@ -462,6 +462,30 @@ MemoryContextIsEmpty(MemoryContext context)
        return context->methods->is_empty(context);
 }
 
+/*
+ * Find the memory allocated to blocks for this memory context. If recurse is
+ * true, also include children.
+ */
+int64
+MemoryContextMemAllocated(MemoryContext context, bool recurse)
+{
+       int64 total = context->mem_allocated;
+
+       AssertArg(MemoryContextIsValid(context));
+
+       if (recurse)
+       {
+               MemoryContext child = context->firstchild;
+
+               for (child = context->firstchild;
+                        child != NULL;
+                        child = child->nextchild)
+                       total += MemoryContextMemAllocated(child, true);
+       }
+
+       return total;
+}
+
 /*
  * MemoryContextStats
  *             Print statistics about the named context and all its descendants.
@@ -736,6 +760,7 @@ MemoryContextCreate(MemoryContext node,
        node->methods = methods;
        node->parent = parent;
        node->firstchild = NULL;
+       node->mem_allocated = 0;
        node->prevchild = NULL;
        node->name = name;
        node->ident = NULL;
index 700a91a2a374c4098f158ef743143dafa47acb1c..50deb354c284e0a0b2a969501662ee902409b081 100644 (file)
@@ -305,12 +305,14 @@ SlabReset(MemoryContext context)
 #endif
                        free(block);
                        slab->nblocks--;
+                       context->mem_allocated -= slab->blockSize;
                }
        }
 
        slab->minFreeChunks = 0;
 
        Assert(slab->nblocks == 0);
+       Assert(context->mem_allocated == 0);
 }
 
 /*
@@ -388,6 +390,7 @@ SlabAlloc(MemoryContext context, Size size)
 
                slab->minFreeChunks = slab->chunksPerBlock;
                slab->nblocks += 1;
+               context->mem_allocated += slab->blockSize;
        }
 
        /* grab the block from the freelist (even the new block is there) */
@@ -480,6 +483,9 @@ SlabAlloc(MemoryContext context, Size size)
 #endif
 
        SlabAllocInfo(slab, chunk);
+
+       Assert(slab->nblocks * slab->blockSize == context->mem_allocated);
+
        return SlabChunkGetPointer(chunk);
 }
 
@@ -555,11 +561,13 @@ SlabFree(MemoryContext context, void *pointer)
        {
                free(block);
                slab->nblocks--;
+               context->mem_allocated -= slab->blockSize;
        }
        else
                dlist_push_head(&slab->freelist[block->nfree], &block->node);
 
        Assert(slab->nblocks >= 0);
+       Assert(slab->nblocks * slab->blockSize == context->mem_allocated);
 }
 
 /*
@@ -782,6 +790,8 @@ SlabCheck(MemoryContext context)
                                         name, block->nfree, block, nfree);
                }
        }
+
+       Assert(slab->nblocks * slab->blockSize == context->mem_allocated);
 }
 
 #endif                                                 /* MEMORY_CONTEXT_CHECKING */
index dbae98d3d9f28522a203fe81d21d7dd9810c6330..df0ae3625cb7696252cce3c8d8aaf11c2516232e 100644 (file)
@@ -79,6 +79,7 @@ typedef struct MemoryContextData
        /* these two fields are placed here to minimize alignment wastage: */
        bool            isReset;                /* T = no space alloced since last reset */
        bool            allowInCritSection; /* allow palloc in critical section */
+       int64           mem_allocated;  /* track memory allocated for this context */
        const MemoryContextMethods *methods;    /* virtual function table */
        MemoryContext parent;           /* NULL if no parent (toplevel context) */
        MemoryContext firstchild;       /* head of linked list of children */
index ffe6de536e2001cf2ee8e1b3e378793b4103ba2d..6a837bc99020fbb2c9e231099ece6f95f9fe8954 100644 (file)
@@ -82,6 +82,7 @@ extern void MemoryContextSetParent(MemoryContext context,
 extern Size GetMemoryChunkSpace(void *pointer);
 extern MemoryContext MemoryContextGetParent(MemoryContext context);
 extern bool MemoryContextIsEmpty(MemoryContext context);
+extern int64 MemoryContextMemAllocated(MemoryContext context, bool recurse);
 extern void MemoryContextStats(MemoryContext context);
 extern void MemoryContextStatsDetail(MemoryContext context, int max_children);
 extern void MemoryContextAllowInCriticalSection(MemoryContext context,