On Mon, 2026-04-20 at 16:40 +1200, David Rowley wrote:
> I was maybe wrong about just not bothering to handle
> MemoryContextSetParent(), but I'm not all that sure where the
> complexity is. Shouldn't it just be a matter of:
>
> If the context has a MemoryPool set, check if the parent has one too,
> if not just swap parents out as the pool belongs to the context
> that's changing parent.
> Else, gather memory totals for the swapping context and subtract
> from the MemoryPool, set the context being reparented's pool to NULL
> and change parent.
> else (no pool is set), just swap parent... I think.
>
> I think there might also need to be a check to see if the new parent
> has a pool and ERROR if it does. Maybe that's the messy part?
Patches attached.
I implemented everything, such that we don't need to ERROR.
It feels slightly over-engineered, but I just didn't like the idea of
erroring on what seem to be valid operations. Given the inheritance
behavior, you may not even be trying to use memory pools, and then
SetParent can still fail, and then what do you do?
Notes:
* It adds 3 extra fields to MemoryContextData inline. The out of line
approaches are not very clean: if we allocate in the context itself
reset will throw it away; if we allocate in the parent context then we
would need to move the allocation on SetParent(); allocating in the
caller means the caller needs to track it even though it has the same
lifetime; and I'm not sure it's a good idea to use malloc() directly.
* The "limit" terminology is a bit awkward because it doesn't really
enforce anything it just adjusts the max block size. Maybe there's a
better term for that?
* allocChunkLimit is not recalculated after SetParent(). I don't think
that's a correctness issue, but I might need to add some more comments.
I like the idea that memory contexts can inherit some information about
work_mem. I've wanted that to be possible for a while, and if we think
this is a good approach then we can expand it to other places in the
executor.
Regards,
Jeff Davis
From 93ab0ff7e857acecdefc0a9c01b2d6d805c90559 Mon Sep 17 00:00:00 2001
From: Jeff Davis <[email protected]>
Date: Wed, 29 Apr 2026 11:19:53 -0700
Subject: [PATCH v4 1/3] Refactor memory accounting into inline function.
---
src/backend/utils/mmgr/aset.c | 21 ++++++++++-----------
src/backend/utils/mmgr/bump.c | 8 ++++----
src/backend/utils/mmgr/generation.c | 6 +++---
src/backend/utils/mmgr/slab.c | 8 ++++----
src/include/utils/memutils_internal.h | 12 ++++++++++++
5 files changed, 33 insertions(+), 22 deletions(-)
diff --git a/src/backend/utils/mmgr/aset.c b/src/backend/utils/mmgr/aset.c
index 6a9ea367107..3ddef26a3d3 100644
--- a/src/backend/utils/mmgr/aset.c
+++ b/src/backend/utils/mmgr/aset.c
@@ -422,8 +422,8 @@ AllocSetContextCreateInternal(MemoryContext parent,
parent,
name);
- ((MemoryContext) set)->mem_allocated =
- KeeperBlock(set)->endptr - ((char *) set);
+ MemoryContextUpdateAlloc((MemoryContext) set,
+ KeeperBlock(set)->endptr - ((char *) set));
return (MemoryContext) set;
}
@@ -525,7 +525,7 @@ AllocSetContextCreateInternal(MemoryContext parent,
parent,
name);
- ((MemoryContext) set)->mem_allocated = firstBlockSize;
+ MemoryContextUpdateAlloc((MemoryContext) set, firstBlockSize);
return (MemoryContext) set;
}
@@ -589,7 +589,7 @@ AllocSetReset(MemoryContext context)
else
{
/* Normal case, release the block */
- context->mem_allocated -= block->endptr - ((char *) block);
+ MemoryContextUpdateAlloc(context, -(block->endptr - ((char *) block)));
#ifdef CLOBBER_FREED_MEMORY
wipe_mem(block, block->freeptr - ((char *) block));
@@ -696,7 +696,7 @@ AllocSetDelete(MemoryContext context)
AllocBlock next = block->next;
if (!IsKeeperBlock(set, block))
- context->mem_allocated -= block->endptr - ((char *) block);
+ MemoryContextUpdateAlloc(context, -(block->endptr - ((char *) block)));
#ifdef CLOBBER_FREED_MEMORY
wipe_mem(block, block->freeptr - ((char *) block));
@@ -758,7 +758,7 @@ AllocSetAllocLarge(MemoryContext context, Size size, int flags)
/* Make a vchunk covering the new block's header */
VALGRIND_MEMPOOL_ALLOC(set, block, ALLOC_BLOCKHDRSZ);
- context->mem_allocated += blksize;
+ MemoryContextUpdateAlloc(context, blksize);
block->aset = set;
block->freeptr = block->endptr = ((char *) block) + blksize;
@@ -967,7 +967,7 @@ AllocSetAllocFromNewBlock(MemoryContext context, Size size, int flags,
/* Make a vchunk covering the new block's header */
VALGRIND_MEMPOOL_ALLOC(set, block, ALLOC_BLOCKHDRSZ);
- context->mem_allocated += blksize;
+ MemoryContextUpdateAlloc(context, blksize);
block->aset = set;
block->freeptr = ((char *) block) + ALLOC_BLOCKHDRSZ;
@@ -1144,7 +1144,7 @@ AllocSetFree(void *pointer)
if (block->next)
block->next->prev = block->prev;
- set->header.mem_allocated -= block->endptr - ((char *) block);
+ MemoryContextUpdateAlloc(&set->header, -(block->endptr - ((char *) block)));
#ifdef CLOBBER_FREED_MEMORY
wipe_mem(block, block->freeptr - ((char *) block));
@@ -1307,9 +1307,8 @@ AllocSetRealloc(void *pointer, Size size, int flags)
VALGRIND_MEMPOOL_CHANGE(set, block, newblock, ALLOC_BLOCKHDRSZ);
block = newblock;
- /* updated separately, not to underflow when (oldblksize > blksize) */
- set->header.mem_allocated -= oldblksize;
- set->header.mem_allocated += blksize;
+ MemoryContextUpdateAlloc(&set->header,
+ (ssize_t) blksize - (ssize_t) oldblksize);
block->freeptr = block->endptr = ((char *) block) + blksize;
diff --git a/src/backend/utils/mmgr/bump.c b/src/backend/utils/mmgr/bump.c
index bfb5a114147..1deb73f451a 100644
--- a/src/backend/utils/mmgr/bump.c
+++ b/src/backend/utils/mmgr/bump.c
@@ -235,7 +235,7 @@ BumpContextCreate(MemoryContext parent, const char *name, Size minContextSize,
MemoryContextCreate((MemoryContext) set, T_BumpContext, MCTX_BUMP_ID,
parent, name);
- ((MemoryContext) set)->mem_allocated = allocSize;
+ MemoryContextUpdateAlloc((MemoryContext) set, allocSize);
return (MemoryContext) set;
}
@@ -341,7 +341,7 @@ BumpAllocLarge(MemoryContext context, Size size, int flags)
/* Make a vchunk covering the new block's header */
VALGRIND_MEMPOOL_ALLOC(set, block, Bump_BLOCKHDRSZ);
- context->mem_allocated += blksize;
+ MemoryContextUpdateAlloc(context, blksize);
/* the block is completely full */
block->freeptr = block->endptr = ((char *) block) + blksize;
@@ -481,7 +481,7 @@ BumpAllocFromNewBlock(MemoryContext context, Size size, int flags,
/* Make a vchunk covering the new block's header */
VALGRIND_MEMPOOL_ALLOC(set, block, Bump_BLOCKHDRSZ);
- context->mem_allocated += blksize;
+ MemoryContextUpdateAlloc(context, blksize);
/* initialize the new block */
BumpBlockInit(set, block, blksize);
@@ -626,7 +626,7 @@ BumpBlockFree(BumpContext *set, BumpBlock *block)
/* release the block from the list of blocks */
dlist_delete(&block->node);
- ((MemoryContext) set)->mem_allocated -= ((char *) block->endptr - (char *) block);
+ MemoryContextUpdateAlloc((MemoryContext) set, -((char *) block->endptr - (char *) block));
#ifdef CLOBBER_FREED_MEMORY
wipe_mem(block, ((char *) block->endptr - (char *) block));
diff --git a/src/backend/utils/mmgr/generation.c b/src/backend/utils/mmgr/generation.c
index 609c9bdc9a6..ddcd25c1e9d 100644
--- a/src/backend/utils/mmgr/generation.c
+++ b/src/backend/utils/mmgr/generation.c
@@ -388,7 +388,7 @@ GenerationAllocLarge(MemoryContext context, Size size, int flags)
/* Make a vchunk covering the new block's header */
VALGRIND_MEMPOOL_ALLOC(set, block, Generation_BLOCKHDRSZ);
- context->mem_allocated += blksize;
+ MemoryContextUpdateAlloc(context, blksize);
/* block with a single (used) chunk */
block->context = set;
@@ -513,7 +513,7 @@ GenerationAllocFromNewBlock(MemoryContext context, Size size, int flags,
/* Make a vchunk covering the new block's header */
VALGRIND_MEMPOOL_ALLOC(set, block, Generation_BLOCKHDRSZ);
- context->mem_allocated += blksize;
+ MemoryContextUpdateAlloc(context, blksize);
/* initialize the new block */
GenerationBlockInit(set, block, blksize);
@@ -697,7 +697,7 @@ GenerationBlockFree(GenerationContext *set, GenerationBlock *block)
/* release the block from the list of blocks */
dlist_delete(&block->node);
- ((MemoryContext) set)->mem_allocated -= block->blksize;
+ MemoryContextUpdateAlloc((MemoryContext) set, -((ssize_t) block->blksize));
#ifdef CLOBBER_FREED_MEMORY
wipe_mem(block, block->blksize);
diff --git a/src/backend/utils/mmgr/slab.c b/src/backend/utils/mmgr/slab.c
index dd1db9566d1..f27d9683db6 100644
--- a/src/backend/utils/mmgr/slab.c
+++ b/src/backend/utils/mmgr/slab.c
@@ -461,7 +461,7 @@ SlabReset(MemoryContext context)
VALGRIND_MEMPOOL_FREE(slab, block);
free(block);
- context->mem_allocated -= slab->blockSize;
+ MemoryContextUpdateAlloc(context, -((ssize_t) slab->blockSize));
}
/* walk over blocklist and free the blocks */
@@ -481,7 +481,7 @@ SlabReset(MemoryContext context)
VALGRIND_MEMPOOL_FREE(slab, block);
free(block);
- context->mem_allocated -= slab->blockSize;
+ MemoryContextUpdateAlloc(context, -((ssize_t) slab->blockSize));
}
}
@@ -597,7 +597,7 @@ SlabAllocFromNewBlock(MemoryContext context, Size size, int flags)
VALGRIND_MEMPOOL_ALLOC(slab, block, Slab_BLOCKHDRSZ);
block->slab = slab;
- context->mem_allocated += slab->blockSize;
+ MemoryContextUpdateAlloc(context, slab->blockSize);
/* use the first chunk in the new block */
chunk = SlabBlockGetChunk(slab, block, 0);
@@ -836,7 +836,7 @@ SlabFree(void *pointer)
VALGRIND_MEMPOOL_FREE(slab, block);
free(block);
- slab->header.mem_allocated -= slab->blockSize;
+ MemoryContextUpdateAlloc(&slab->header, -((ssize_t) slab->blockSize));
}
/*
diff --git a/src/include/utils/memutils_internal.h b/src/include/utils/memutils_internal.h
index 475e91b336b..8f793f3c781 100644
--- a/src/include/utils/memutils_internal.h
+++ b/src/include/utils/memutils_internal.h
@@ -157,6 +157,18 @@ extern void MemoryContextCreate(MemoryContext node,
MemoryContext parent,
const char *name);
+/*
+ * MemoryContextUpdateAlloc()
+ *
+ * Update allocation total.
+ */
+static inline void
+MemoryContextUpdateAlloc(MemoryContext context, ssize_t size)
+{
+ Assert(size >= 0 || -size <= context->mem_allocated);
+ context->mem_allocated += size;
+}
+
extern void *MemoryContextAllocationFailure(MemoryContext context, Size size,
int flags);
--
2.43.0
From 55766fc0354e25621d922bb588a248196d124051 Mon Sep 17 00:00:00 2001
From: Jeff Davis <[email protected]>
Date: Mon, 4 May 2026 14:24:40 -0700
Subject: [PATCH v4 2/3] Memory Pools.
A memory context can have a memory pool, which tracks the memory for
that context as well as all subcontexts in a single total, and also
holds an (unenforced) limit. Memory pools are automatically inherited
by child contexts.
Memory Pools solve two problems:
First, the previous implementation in MemoryContextMemAllocated()
recursively walked through subcontexts to total the mem_allocated
fields. For hash aggregates that used a child memory context for each
group, it was too slow. Now, it just reads the single total.
Second, with the block size doubling behavior, it was likely that a
single small chunk allocation could cause a block allocation that
overwhelmed a small value of work_mem. Hash Aggregation protected
against this by constraining maxBlockSize for known contexts, but not
their subcontexts. Now, subcontexts constrain the block size using the
inherited memory pool's limit.
Suggested-by: David Rowley <[email protected]>
Discussion: https://postgr.es/m/[email protected]
---
src/backend/utils/mmgr/aset.c | 8 +-
src/backend/utils/mmgr/bump.c | 7 +-
src/backend/utils/mmgr/generation.c | 7 +-
src/backend/utils/mmgr/mcxt.c | 110 +++++++++++++++++++++++++-
src/include/nodes/memnodes.h | 16 ++++
src/include/utils/memutils.h | 1 +
src/include/utils/memutils_internal.h | 76 +++++++++++++++++-
src/tools/pgindent/typedefs.list | 1 +
8 files changed, 218 insertions(+), 8 deletions(-)
diff --git a/src/backend/utils/mmgr/aset.c b/src/backend/utils/mmgr/aset.c
index 3ddef26a3d3..8981c128672 100644
--- a/src/backend/utils/mmgr/aset.c
+++ b/src/backend/utils/mmgr/aset.c
@@ -867,6 +867,7 @@ AllocSetAllocFromNewBlock(MemoryContext context, Size size, int flags,
Size blksize;
Size required_size;
Size chunk_size;
+ Size blockSizeLimit;
/* due to the keeper block set->blocks should always be valid */
Assert(set->blocks != NULL);
@@ -931,8 +932,11 @@ AllocSetAllocFromNewBlock(MemoryContext context, Size size, int flags,
*/
blksize = set->nextBlockSize;
set->nextBlockSize <<= 1;
- if (set->nextBlockSize > set->maxBlockSize)
- set->nextBlockSize = set->maxBlockSize;
+
+ blockSizeLimit = MemoryContextBlockSizeLimit(context, set->initBlockSize,
+ set->maxBlockSize);
+ if (set->nextBlockSize > blockSizeLimit)
+ set->nextBlockSize = blockSizeLimit;
/* Choose the actual chunk size to allocate */
chunk_size = GetChunkSizeFromFreeListIdx(fidx);
diff --git a/src/backend/utils/mmgr/bump.c b/src/backend/utils/mmgr/bump.c
index 1deb73f451a..154ebdebd12 100644
--- a/src/backend/utils/mmgr/bump.c
+++ b/src/backend/utils/mmgr/bump.c
@@ -457,15 +457,18 @@ BumpAllocFromNewBlock(MemoryContext context, Size size, int flags,
BumpBlock *block;
Size blksize;
Size required_size;
+ Size blockSizeLimit;
/*
* The first such block has size initBlockSize, and we double the space in
* each succeeding block, but not more than maxBlockSize.
*/
blksize = set->nextBlockSize;
+ blockSizeLimit = MemoryContextBlockSizeLimit(context, set->initBlockSize,
+ set->maxBlockSize);
set->nextBlockSize <<= 1;
- if (set->nextBlockSize > set->maxBlockSize)
- set->nextBlockSize = set->maxBlockSize;
+ if (set->nextBlockSize > blockSizeLimit)
+ set->nextBlockSize = blockSizeLimit;
/* we'll need space for the chunk, chunk hdr and block hdr */
required_size = chunk_size + Bump_CHUNKHDRSZ + Bump_BLOCKHDRSZ;
diff --git a/src/backend/utils/mmgr/generation.c b/src/backend/utils/mmgr/generation.c
index ddcd25c1e9d..cade3ae3fe9 100644
--- a/src/backend/utils/mmgr/generation.c
+++ b/src/backend/utils/mmgr/generation.c
@@ -488,15 +488,18 @@ GenerationAllocFromNewBlock(MemoryContext context, Size size, int flags,
GenerationBlock *block;
Size blksize;
Size required_size;
+ Size blockSizeLimit;
/*
* The first such block has size initBlockSize, and we double the space in
* each succeeding block, but not more than maxBlockSize.
*/
blksize = set->nextBlockSize;
+ blockSizeLimit = MemoryContextBlockSizeLimit(context, set->initBlockSize,
+ set->maxBlockSize);
set->nextBlockSize <<= 1;
- if (set->nextBlockSize > set->maxBlockSize)
- set->nextBlockSize = set->maxBlockSize;
+ if (set->nextBlockSize > blockSizeLimit)
+ set->nextBlockSize = blockSizeLimit;
/* we'll need space for the chunk, chunk hdr and block hdr */
required_size = chunk_size + Generation_CHUNKHDRSZ + Generation_BLOCKHDRSZ;
diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c
index 073bdb35d2a..472123612f2 100644
--- a/src/backend/utils/mmgr/mcxt.c
+++ b/src/backend/utils/mmgr/mcxt.c
@@ -664,6 +664,86 @@ MemoryContextSetIdentifier(MemoryContext context, const char *id)
context->ident = id;
}
+/*
+ * Add a memory pool to an existing context. It may already be a member of an
+ * outer pool owned by an ancestor, but it may not already own its memory
+ * pool.
+ *
+ * Currently, the context must have no child contexts, but that restriction
+ * could be lifted if necessary.
+ */
+MemoryPool *
+MemoryContextCreatePool(MemoryContext context, Size limit)
+{
+ MemoryPool *pool = &context->pool;
+
+ Assert(MemoryContextIsValid(context));
+ Assert(limit > 0);
+ Assert(context->firstchild == NULL);
+ Assert(!MemoryContextOwnsPool(context));
+
+ /* outer may already be set */
+
+ pool->limit = limit;
+ pool->allocated = context->mem_allocated;
+
+ return pool;
+}
+
+/* helper for MemoryContextSetParent() */
+static void
+MemoryContextTransferPool(MemoryContext context, MemoryContext new_parent)
+{
+ MemoryPool *old_outer_pool = context->pool.outer;
+ MemoryPool *new_outer_pool = NULL;
+ Size subtree_mem;
+
+ if (new_parent)
+ new_outer_pool = MemoryContextGetPool(new_parent);
+
+ if (!old_outer_pool && !new_outer_pool)
+ return;
+
+ if (old_outer_pool == new_outer_pool)
+ return;
+
+ /*
+ * If the context's pool is inherited from some ancestor context, we need
+ * to calculate the total for this subtree. If it's owned, we can just use
+ * the pool's total.
+ */
+ if (MemoryContextOwnsPool(context))
+ subtree_mem = context->pool.allocated;
+ else
+ subtree_mem = MemoryContextMemAllocated(context, true);
+
+ /* subtract from old subtree */
+ for (MemoryPool *pool = old_outer_pool; pool != NULL; pool = pool->outer)
+ pool->allocated -= subtree_mem;
+
+ /* add to new subtree */
+ for (MemoryPool *pool = new_outer_pool; pool != NULL; pool = pool->outer)
+ pool->allocated += subtree_mem;
+
+ /*
+ * If it's not the owner of its pool, subcontexts could refer to pools
+ * owned by the context's old ancestors. Search subtree for references to
+ * old_outer_pool and replace with new_outer_pool. NB: old_outer_pool or
+ * new_outer_pool might be NULL here.
+ */
+ if (!MemoryContextOwnsPool(context))
+ {
+ for (MemoryContext cxt = context->firstchild; cxt != NULL;
+ cxt = MemoryContextTraverseNext(cxt, context))
+ {
+ if (cxt->pool.outer == old_outer_pool)
+ cxt->pool.outer = new_outer_pool;
+ }
+ }
+
+ context->pool.outer = new_outer_pool;
+}
+
/*
* MemoryContextSetParent
* Change a context to belong to a new parent (or no parent).
@@ -692,6 +772,12 @@ MemoryContextSetParent(MemoryContext context, MemoryContext new_parent)
if (new_parent == context->parent)
return;
+ /*
+ * This must happen here while we still have information about the
+ * existing ancestor.
+ */
+ MemoryContextTransferPool(context, new_parent);
+
/* Delink from existing parent, if any */
if (context->parent)
{
@@ -726,6 +812,10 @@ MemoryContextSetParent(MemoryContext context, MemoryContext new_parent)
context->prevchild = NULL;
context->nextchild = NULL;
}
+
+#ifdef MEMORY_CONTEXT_CHECKING
+ MemoryContextCheck(context);
+#endif
}
/*
@@ -1090,18 +1180,30 @@ MemoryContextStatsPrint(MemoryContext context, void *passthru,
level, name, stats_string, truncated_ident)));
}
+#ifdef MEMORY_CONTEXT_CHECKING
+
+static void
+MemoryContextCheckPool(MemoryContext context)
+{
+ if (MemoryContextOwnsPool(context))
+ Assert(context->pool.allocated == MemoryContextMemAllocated(context, true));
+
+ if (context->parent)
+ Assert(context->pool.outer == MemoryContextGetPool(context->parent));
+}
+
/*
* MemoryContextCheck
* Check all chunks in the named context and its children.
*
* This is just a debugging utility, so it's not fancy.
*/
-#ifdef MEMORY_CONTEXT_CHECKING
void
MemoryContextCheck(MemoryContext context)
{
Assert(MemoryContextIsValid(context));
context->methods->check(context);
+ MemoryContextCheckPool(context);
for (MemoryContext curr = context->firstchild;
curr != NULL;
@@ -1109,8 +1211,10 @@ MemoryContextCheck(MemoryContext context)
{
Assert(MemoryContextIsValid(curr));
curr->methods->check(curr);
+ MemoryContextCheckPool(curr);
}
}
+
#endif
/*
@@ -1166,6 +1270,9 @@ MemoryContextCreate(MemoryContext node,
node->parent = parent;
node->firstchild = NULL;
node->mem_allocated = 0;
+ node->pool.limit = 0;
+ node->pool.allocated = 0;
+ node->pool.outer = NULL;
node->prevchild = NULL;
node->name = name;
node->ident = NULL;
@@ -1180,6 +1287,7 @@ MemoryContextCreate(MemoryContext node,
parent->firstchild = node;
/* inherit allowInCritSection flag from parent */
node->allowInCritSection = parent->allowInCritSection;
+ node->pool.outer = MemoryContextGetPool(parent);
}
else
{
diff --git a/src/include/nodes/memnodes.h b/src/include/nodes/memnodes.h
index 4b24778fe1e..8c3bc7b7d30 100644
--- a/src/include/nodes/memnodes.h
+++ b/src/include/nodes/memnodes.h
@@ -114,6 +114,21 @@ typedef struct MemoryContextMethods
} MemoryContextMethods;
+/*
+ * Memory Pools account for memory usage.
+ *
+ * If limit > 0, then this pool is owned by the containing context, and
+ * 'allocated' is valid. If limit == 0, the pool was inherited from the parent
+ * context, and 'allocated' is invalid.
+ */
+typedef struct MemoryPool
+{
+ Size limit; /* limit (not enforced) */
+ Size allocated; /* allocation total for subtree */
+ struct MemoryPool *outer; /* outer containing pool */
+} MemoryPool;
+
+
typedef struct MemoryContextData
{
pg_node_attr(abstract) /* there are no nodes of this type */
@@ -123,6 +138,7 @@ typedef struct MemoryContextData
bool isReset; /* T = no space allocated since last reset */
bool allowInCritSection; /* allow palloc in critical section */
Size mem_allocated; /* track memory allocated for this context */
+ MemoryPool pool; /* accounting, if enabled */
const MemoryContextMethods *methods; /* virtual function table */
MemoryContext parent; /* NULL if no parent (toplevel context) */
MemoryContext firstchild; /* head of linked list of children */
diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h
index 11ab1717a16..e531876ba66 100644
--- a/src/include/utils/memutils.h
+++ b/src/include/utils/memutils.h
@@ -86,6 +86,7 @@ extern bool MemoryContextIsEmpty(MemoryContext context);
extern Size MemoryContextMemAllocated(MemoryContext context, bool recurse);
extern void MemoryContextMemConsumed(MemoryContext context,
MemoryContextCounters *consumed);
+extern MemoryPool *MemoryContextCreatePool(MemoryContext owner, Size limit);
extern void MemoryContextStats(MemoryContext context);
extern void MemoryContextStatsDetail(MemoryContext context,
int max_level, int max_children,
diff --git a/src/include/utils/memutils_internal.h b/src/include/utils/memutils_internal.h
index 8f793f3c781..8cacd8abb8c 100644
--- a/src/include/utils/memutils_internal.h
+++ b/src/include/utils/memutils_internal.h
@@ -17,6 +17,7 @@
#define MEMUTILS_INTERNAL_H
#include "utils/memutils.h"
+#include "port/pg_bitutils.h"
/* These functions implement the MemoryContext API for AllocSet context. */
extern void *AllocSetAlloc(MemoryContext context, Size size, int flags);
@@ -157,16 +158,89 @@ extern void MemoryContextCreate(MemoryContext node,
MemoryContext parent,
const char *name);
+static inline bool
+MemoryPoolIsValid(MemoryPool *pool)
+{
+ if (pool->limit > 0)
+ return true;
+
+ Assert(pool->allocated == 0);
+ return false;
+}
+
+static inline bool
+MemoryContextOwnsPool(MemoryContext context)
+{
+ return MemoryPoolIsValid(&context->pool);
+}
+
+static inline MemoryPool *
+MemoryPoolOuter(MemoryPool *pool)
+{
+ Assert(pool->outer == NULL || MemoryPoolIsValid(pool->outer));
+ return pool->outer;
+}
+
+/* get owned or inherited pool for memory context, or NULL */
+static inline MemoryPool *
+MemoryContextGetPool(MemoryContext context)
+{
+ MemoryPool *pool;
+
+ if (MemoryContextOwnsPool(context))
+ pool = &context->pool;
+ else
+ pool = context->pool.outer;
+
+ Assert(pool == NULL || MemoryPoolIsValid(pool));
+ return pool;
+}
+
+/*
+ * If the context is part of a memory pool, calculate a reasonable max block
+ * size given the memory pool limit. This avoids a case where a small chunk
+ * request causes a large block allocation that overwhelms the limit.
+ * Calculated dynamically because the limit could change, e.g. during
+ * MemoryContextSetParent().
+ */
+static inline Size
+MemoryContextBlockSizeLimit(MemoryContext context, Size min, Size max)
+{
+ MemoryPool *pool = MemoryContextGetPool(context);
+ Size maxBlockSize;
+
+ if (pool != NULL)
+ {
+ /* reasonable value chosen as 1/16th of the limit */
+ maxBlockSize = pg_prevpower2_size_t(pool->limit / 16);
+
+ maxBlockSize = Min(maxBlockSize, max);
+ maxBlockSize = Max(maxBlockSize, min);
+ }
+ else
+ maxBlockSize = max;
+
+ return maxBlockSize;
+}
+
/*
* MemoryContextUpdateAlloc()
*
- * Update allocation total.
+ * Update allocation total for context and pool(s).
*/
static inline void
MemoryContextUpdateAlloc(MemoryContext context, ssize_t size)
{
Assert(size >= 0 || -size <= context->mem_allocated);
context->mem_allocated += size;
+
+ for (MemoryPool *pool = MemoryContextGetPool(context);
+ pool != NULL;
+ pool = MemoryPoolOuter(pool))
+ {
+ Assert(size >= 0 || -size <= pool->allocated);
+ pool->allocated += size;
+ }
}
extern void *MemoryContextAllocationFailure(MemoryContext context, Size size,
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 0abdb2d37e2..9fe2b799476 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1754,6 +1754,7 @@ MemoryContextData
MemoryContextId
MemoryContextMethodID
MemoryContextMethods
+MemoryPool
MemoryStatsPrintFunc
MergeAction
MergeActionState
--
2.43.0
From baf0e7b28111337c6b9f138ddea7c2ef137fd930 Mon Sep 17 00:00:00 2001
From: Jeff Davis <[email protected]>
Date: Mon, 4 May 2026 14:29:35 -0700
Subject: [PATCH v4 3/3] Update Hash Aggregation to use memory pools.
This reverts commit 50a38f65177ea7858bc97f71ba0757ba04c1c167, which is
no longer needed.
---
src/backend/executor/execUtils.c | 69 +++++++-------------------------
src/backend/executor/nodeAgg.c | 37 +++++------------
src/include/executor/executor.h | 1 -
3 files changed, 24 insertions(+), 83 deletions(-)
diff --git a/src/backend/executor/execUtils.c b/src/backend/executor/execUtils.c
index 1eb6b9f1f40..4b8d1c900b3 100644
--- a/src/backend/executor/execUtils.c
+++ b/src/backend/executor/execUtils.c
@@ -234,13 +234,21 @@ FreeExecutorState(EState *estate)
MemoryContextDelete(estate->es_query_cxt);
}
-/*
- * Internal implementation for CreateExprContext() and CreateWorkExprContext()
- * that allows control over the AllocSet parameters.
+/* ----------------
+ * CreateExprContext
+ *
+ * Create a context for expression evaluation within an EState.
+ *
+ * An executor run may require multiple ExprContexts (we usually make one
+ * for each Plan node, and a separate one for per-output-tuple processing
+ * such as constraint checking). Each ExprContext has its own "per-tuple"
+ * memory context.
+ *
+ * Note we make no assumption about the caller's memory context.
+ * ----------------
*/
-static ExprContext *
-CreateExprContextInternal(EState *estate, Size minContextSize,
- Size initBlockSize, Size maxBlockSize)
+ExprContext *
+CreateExprContext(EState *estate)
{
ExprContext *econtext;
MemoryContext oldcontext;
@@ -263,9 +271,7 @@ CreateExprContextInternal(EState *estate, Size minContextSize,
econtext->ecxt_per_tuple_memory =
AllocSetContextCreate(estate->es_query_cxt,
"ExprContext",
- minContextSize,
- initBlockSize,
- maxBlockSize);
+ ALLOCSET_DEFAULT_SIZES);
econtext->ecxt_param_exec_vals = estate->es_param_exec_vals;
econtext->ecxt_param_list_info = estate->es_param_list_info;
@@ -295,51 +301,6 @@ CreateExprContextInternal(EState *estate, Size minContextSize,
return econtext;
}
-/* ----------------
- * CreateExprContext
- *
- * Create a context for expression evaluation within an EState.
- *
- * An executor run may require multiple ExprContexts (we usually make one
- * for each Plan node, and a separate one for per-output-tuple processing
- * such as constraint checking). Each ExprContext has its own "per-tuple"
- * memory context.
- *
- * Note we make no assumption about the caller's memory context.
- * ----------------
- */
-ExprContext *
-CreateExprContext(EState *estate)
-{
- return CreateExprContextInternal(estate, ALLOCSET_DEFAULT_SIZES);
-}
-
-
-/* ----------------
- * CreateWorkExprContext
- *
- * Like CreateExprContext, but specifies the AllocSet sizes to be reasonable
- * in proportion to work_mem. If the maximum block allocation size is too
- * large, it's easy to skip right past work_mem with a single allocation.
- * ----------------
- */
-ExprContext *
-CreateWorkExprContext(EState *estate)
-{
- Size maxBlockSize;
-
- maxBlockSize = pg_prevpower2_size_t(work_mem * (Size) 1024 / 16);
-
- /* But no bigger than ALLOCSET_DEFAULT_MAXSIZE */
- maxBlockSize = Min(maxBlockSize, ALLOCSET_DEFAULT_MAXSIZE);
-
- /* and no smaller than ALLOCSET_DEFAULT_INITSIZE */
- maxBlockSize = Max(maxBlockSize, ALLOCSET_DEFAULT_INITSIZE);
-
- return CreateExprContextInternal(estate, ALLOCSET_DEFAULT_MINSIZE,
- ALLOCSET_DEFAULT_INITSIZE, maxBlockSize);
-}
-
/* ----------------
* CreateStandaloneExprContext
*
diff --git a/src/backend/executor/nodeAgg.c b/src/backend/executor/nodeAgg.c
index 925caadd2ce..01334d6012e 100644
--- a/src/backend/executor/nodeAgg.c
+++ b/src/backend/executor/nodeAgg.c
@@ -1866,12 +1866,9 @@ static void
hash_agg_check_limits(AggState *aggstate)
{
uint64 ngroups = aggstate->hash_ngroups_current;
- Size meta_mem = MemoryContextMemAllocated(aggstate->hash_metacxt,
- true);
- Size entry_mem = MemoryContextMemAllocated(aggstate->hash_tuplescxt,
- true);
- Size tval_mem = MemoryContextMemAllocated(aggstate->hashcontext->ecxt_per_tuple_memory,
- true);
+ Size meta_mem = MemoryContextGetPool(aggstate->hash_metacxt)->allocated;
+ Size entry_mem = MemoryContextGetPool(aggstate->hash_tuplescxt)->allocated;
+ Size tval_mem = MemoryContextGetPool(aggstate->hashcontext->ecxt_per_tuple_memory)->allocated;
Size total_mem = meta_mem + entry_mem + tval_mem;
bool do_spill = false;
@@ -1998,13 +1995,13 @@ hash_agg_update_metrics(AggState *aggstate, bool from_tape, int npartitions)
static void
hash_create_memory(AggState *aggstate)
{
- Size maxBlockSize = ALLOCSET_DEFAULT_MAXSIZE;
-
/*
* The hashcontext's per-tuple memory will be used for byref transition
* values and returned by AggCheckCallContext().
*/
- aggstate->hashcontext = CreateWorkExprContext(aggstate->ss.ps.state);
+ aggstate->hashcontext = CreateExprContext(aggstate->ss.ps.state);
+ MemoryContextCreatePool(aggstate->hashcontext->ecxt_per_tuple_memory,
+ work_mem * (Size) 1024);
/*
* The meta context will be used for the bucket array of
@@ -2017,6 +2014,7 @@ hash_create_memory(AggState *aggstate)
aggstate->hash_metacxt = AllocSetContextCreate(aggstate->ss.ps.state->es_query_cxt,
"HashAgg meta context",
ALLOCSET_DEFAULT_SIZES);
+ MemoryContextCreatePool(aggstate->hash_metacxt, work_mem * (Size) 1024);
/*
* The hash entries themselves, which include the grouping key
@@ -2025,29 +2023,12 @@ hash_create_memory(AggState *aggstate)
* entire hash table is reset. The bump allocator is faster for
* allocations and avoids wasting space on the chunk header or
* power-of-two allocations.
- *
- * Like CreateWorkExprContext(), use smaller sizings for smaller work_mem,
- * to avoid large jumps in memory usage.
- */
-
- /*
- * Like CreateWorkExprContext(), use smaller sizings for smaller work_mem,
- * to avoid large jumps in memory usage.
*/
- maxBlockSize = pg_prevpower2_size_t(work_mem * (Size) 1024 / 16);
-
- /* But no bigger than ALLOCSET_DEFAULT_MAXSIZE */
- maxBlockSize = Min(maxBlockSize, ALLOCSET_DEFAULT_MAXSIZE);
-
- /* and no smaller than ALLOCSET_DEFAULT_INITSIZE */
- maxBlockSize = Max(maxBlockSize, ALLOCSET_DEFAULT_INITSIZE);
aggstate->hash_tuplescxt = BumpContextCreate(aggstate->ss.ps.state->es_query_cxt,
"HashAgg hashed tuples",
- ALLOCSET_DEFAULT_MINSIZE,
- ALLOCSET_DEFAULT_INITSIZE,
- maxBlockSize);
-
+ ALLOCSET_DEFAULT_SIZES);
+ MemoryContextCreatePool(aggstate->hash_tuplescxt, work_mem * (Size) 1024);
}
/*
diff --git a/src/include/executor/executor.h b/src/include/executor/executor.h
index 33bbdbfeffb..1e72fbef6f9 100644
--- a/src/include/executor/executor.h
+++ b/src/include/executor/executor.h
@@ -653,7 +653,6 @@ extern void end_tup_output(TupOutputState *tstate);
extern EState *CreateExecutorState(void);
extern void FreeExecutorState(EState *estate);
extern ExprContext *CreateExprContext(EState *estate);
-extern ExprContext *CreateWorkExprContext(EState *estate);
extern ExprContext *CreateStandaloneExprContext(void);
extern void FreeExprContext(ExprContext *econtext, bool isCommit);
extern void ReScanExprContext(ExprContext *econtext);
--
2.43.0