From 545719924797a9068b67e9be5be7feac412de878 Mon Sep 17 00:00:00 2001 From: Andres Freund Date: Thu, 26 Feb 2026 15:24:03 -0500 Subject: [PATCH v2 4/5] MemCTX: Add minimal proxy context type that just defers to malloc Co-authored-by: Matthias van de Meent Co-authored-by: Andres Freund --- src/backend/nodes/gen_node_support.pl | 2 +- src/backend/utils/adt/mcxtfuncs.c | 3 + src/backend/utils/mmgr/Makefile | 1 + src/backend/utils/mmgr/README | 4 + src/backend/utils/mmgr/mcxt.c | 14 +- src/backend/utils/mmgr/meson.build | 1 + src/backend/utils/mmgr/proxy.c | 511 ++++++++++++++++++++++++++ src/include/nodes/memnodes.h | 3 +- src/include/utils/memutils.h | 4 + src/include/utils/memutils_internal.h | 18 +- src/tools/pgindent/typedefs.list | 2 + 11 files changed, 559 insertions(+), 4 deletions(-) create mode 100644 src/backend/utils/mmgr/proxy.c diff --git a/src/backend/nodes/gen_node_support.pl b/src/backend/nodes/gen_node_support.pl index 0b766272018..d3bb9877a62 100644 --- a/src/backend/nodes/gen_node_support.pl +++ b/src/backend/nodes/gen_node_support.pl @@ -136,7 +136,7 @@ my @abstract_types = qw(Node); # they otherwise don't participate in node support. my @extra_tags = qw( IntList OidList XidList - AllocSetContext GenerationContext SlabContext BumpContext + AllocSetContext GenerationContext SlabContext BumpContext ProxyContext TIDBitmap WindowObjectData ); diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index 1a4dbbeb8db..85a93de331e 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -160,6 +160,9 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, case T_BumpContext: type = "Bump"; break; + case T_ProxyContext: + type = "Proxy"; + break; default: type = "???"; break; diff --git a/src/backend/utils/mmgr/Makefile b/src/backend/utils/mmgr/Makefile index 01a1fb85270..64d70bfcdf0 100644 --- a/src/backend/utils/mmgr/Makefile +++ b/src/backend/utils/mmgr/Makefile @@ -22,6 +22,7 @@ OBJS = \ mcxt.o \ memdebug.o \ portalmem.o \ + proxy.o \ slab.o include $(top_srcdir)/src/backend/common.mk diff --git a/src/backend/utils/mmgr/README b/src/backend/utils/mmgr/README index 695088bb66d..6105514f586 100644 --- a/src/backend/utils/mmgr/README +++ b/src/backend/utils/mmgr/README @@ -495,6 +495,10 @@ allocator types also exist which are special-purpose: allocations. Blocks are only free'd back to the operating system when the context is reset or deleted. +* proxy.c (ProxyContext) is best suited for those use cases that require + long-lived allocations, with minimal allocation traffic, and generally + very few allocations; such as relcache's 'index info' contexts. + For further details, please read the header comment in the corresponding .c file. diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 594c7a93bba..1cc6e603c9b 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -132,6 +132,19 @@ static const MemoryContextMethods mcxt_methods[] = { [MCTX_BUMP_ID].check = BumpCheck, #endif + /* proxy.c */ + [MCTX_PROXY_ID].alloc = ProxyAlloc, + [MCTX_PROXY_ID].free_p = ProxyFree, + [MCTX_PROXY_ID].realloc = ProxyRealloc, + [MCTX_PROXY_ID].reset = ProxyReset, + [MCTX_PROXY_ID].delete_context = ProxyDelete, + [MCTX_PROXY_ID].get_chunk_context = ProxyGetChunkContext, + [MCTX_PROXY_ID].get_chunk_space = ProxyGetChunkSpace, + [MCTX_PROXY_ID].is_empty = ProxyIsEmpty, + [MCTX_PROXY_ID].stats = ProxyStats, +#ifdef MEMORY_CONTEXT_CHECKING + [MCTX_PROXY_ID].check = ProxyCheck, +#endif /* * Reserved and unused IDs should have dummy entries here. This allows us @@ -141,7 +154,6 @@ static const MemoryContextMethods mcxt_methods[] = { */ BOGUS_MCTX(MCTX_1_RESERVED_GLIBC_ID), BOGUS_MCTX(MCTX_2_RESERVED_GLIBC_ID), - BOGUS_MCTX(MCTX_8_UNUSED_ID), BOGUS_MCTX(MCTX_9_UNUSED_ID), BOGUS_MCTX(MCTX_10_UNUSED_ID), BOGUS_MCTX(MCTX_11_UNUSED_ID), diff --git a/src/backend/utils/mmgr/meson.build b/src/backend/utils/mmgr/meson.build index 61837e9f91b..6eb5e068864 100644 --- a/src/backend/utils/mmgr/meson.build +++ b/src/backend/utils/mmgr/meson.build @@ -10,5 +10,6 @@ backend_sources += files( 'mcxt.c', 'memdebug.c', 'portalmem.c', + 'proxy.c', 'slab.c', ) diff --git a/src/backend/utils/mmgr/proxy.c b/src/backend/utils/mmgr/proxy.c new file mode 100644 index 00000000000..f07b25244b8 --- /dev/null +++ b/src/backend/utils/mmgr/proxy.c @@ -0,0 +1,511 @@ +/*------------------------------------------------------------------------- +* + * proxy.c + * Proxy allocator definitions. + * + * Proxy is a MemoryContext implementation designed for memory usages which + * require their own memory context, but which generally have few allocations + * that generally have a very long lifetime. Compared to ASet, every + * allocation of a Proxy memory context gets an External chunk, i.e. every + * palloc results in a malloc. + * + * Portions Copyright (c) 2024-2026, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/backend/utils/mmgr/proxy.c + * + * NOTE: + * Proxy is best suited to cases where a memory context with long lifetimes + * is required, but is expected to hold only a small number of small + * allocations, with a total memory usage smaller than the minimum aset block + * size. + * + * Allocations are MAXALIGNed. + * + *------------------------------------------------------------------------- + */ +#include "postgres.h" + +#include "lib/ilist.h" +#include "utils/memdebug.h" +#include "utils/memutils.h" +#include "utils/memutils_memorychunk.h" +#include "utils/memutils_internal.h" + +typedef struct ProxyContext +{ + MemoryContextData header; /* Standard memory-context fields */ + + dlist_head allocations; /* list of all allocations of this memctx */ +} ProxyContext; + +typedef struct ProxyBlock +{ + dlist_node node; + size_t sz; + ProxyContext *context; +} ProxyBlock; + +#define PROXY_CONTEXTSIZE (MAXALIGN(sizeof(ProxyContext))) +#define PROXY_BLOCKHDRSZ (MAXALIGN(sizeof(ProxyBlock))) +#define PROXY_CHUNKHDRSZ (sizeof(MemoryChunk)) + +/* + * ProxyIsValid + * True iff ctx is valid allocation ctx. + */ +#define ProxyIsValid(ctx) \ + ((ctx) && IsA(ctx, ProxyContext)) + +#define ExternalChunkGetBlock(chunk) \ + ((ProxyBlock *) (((char *) chunk) - PROXY_BLOCKHDRSZ)) + + +/* + * ProxyContextCreate + * Create a Proxy memory context + */ +MemoryContext +ProxyContextCreate(MemoryContext parent, const char *name) +{ + size_t allocSize = PROXY_CONTEXTSIZE; + ProxyContext *ctx; + + /* + * Allocate the context struct. Unlike other memory contexts, this + * context doesn't have an attached chunk of palloc'able memory. + */ + ctx = (ProxyContext *) malloc(allocSize); + if (ctx == NULL) + { + MemoryContextStats(TopMemoryContext); + ereport(ERROR, + (errcode(ERRCODE_OUT_OF_MEMORY), + errmsg("out of memory"), + errdetail("Failed while creating memory context \"%s\".", + name))); + } + + VALGRIND_CREATE_MEMPOOL(ctx, 0, false); + VALGRIND_MEMPOOL_ALLOC(ctx, ctx, allocSize); + + dlist_init(&ctx->allocations); + + /* Finally, do the type-independent part of context creation */ + MemoryContextCreate((MemoryContext) ctx, T_ProxyContext, MCTX_PROXY_ID, + parent, name); + + ((MemoryContext) ctx)->mem_allocated = allocSize; + + return (MemoryContext) ctx; +} + +/* + * ProxyAlloc + * Alloc memory into the Proxy memory context. + */ +void * +ProxyAlloc(MemoryContext context, size_t size, int flags) +{ + ProxyContext *ctx = (ProxyContext *) context; + size_t blksize; + ProxyBlock *block; + MemoryChunk *chunk; + + Assert(ProxyIsValid(ctx)); + + /* validate 'size' is within the limits for the given 'flags' */ + MemoryContextCheckSize(context, size, flags); + + /* adjust size for sentinel byte */ +#ifdef MEMORY_CONTEXT_CHECKING + blksize = MAXALIGN(size + 1); +#else + blksize = MAXALIGN(size); +#endif + + blksize += PROXY_BLOCKHDRSZ + PROXY_CHUNKHDRSZ; + + block = (ProxyBlock *) malloc(blksize); + if (block == NULL) + return MemoryContextAllocationFailure(context, size, flags); + + VALGRIND_MEMPOOL_ALLOC(ctx, block, PROXY_BLOCKHDRSZ); + + context->mem_allocated += blksize; + + dlist_node_init(&block->node); + + block->context = ctx; + block->sz = blksize; + + dlist_push_tail(&ctx->allocations, &block->node); + + chunk = (MemoryChunk *) (((char *) block) + PROXY_BLOCKHDRSZ); + + MemoryChunkSetHdrMaskExternal(chunk, MCTX_PROXY_ID); + +#ifdef MEMORY_CONTEXT_CHECKING + chunk->requested_size = size; + /* set mark to catch clobber of "unused" space */ + set_sentinel(MemoryChunkGetPointer(chunk), size); +#endif + +#ifdef RANDOMIZE_ALLOCATED_MEMORY + /* fill the allocated space with junk */ + randomize_mem((char *) MemoryChunkGetPointer(chunk), size); +#endif + + VALGRIND_MAKE_MEM_NOACCESS(chunk, PROXY_CHUNKHDRSZ); + + return MemoryChunkGetPointer(chunk); +} + +/* + * ProxyAlloc + * Free memory from the Proxy memory context. + */ +void +ProxyFree(void *pointer) +{ + ProxyContext *ctx; + MemoryChunk *chunk = PointerGetMemoryChunk(pointer); + ProxyBlock *block; + + VALGRIND_MAKE_MEM_DEFINED(chunk, PROXY_CHUNKHDRSZ); + + Assert(MemoryChunkIsExternal(chunk)); + + block = ExternalChunkGetBlock(chunk); + + ctx = block->context; + + Assert(ProxyIsValid(ctx)); + +#ifdef MEMORY_CONTEXT_CHECKING + /* Test for someone scribbling on unused space in chunk */ + if (!sentinel_ok(pointer, chunk->requested_size)) + elog(WARNING, "detected write past chunk end in %s %p", + ctx->header.name, chunk); +#endif + + /* ok, remove block from the list, and free it */ + dlist_delete_from(&ctx->allocations, &block->node); + + ctx->header.mem_allocated -= block->sz; + +#ifdef CLOBBER_FREED_MEMORY + wipe_mem(block, block->sz); +#endif + + VALGRIND_MEMPOOL_FREE(ctx, block); + + free(block); +} + +/* + * ProxyAlloc + * Realloc memory in the Proxy memory context. + */ +void * +ProxyRealloc(void *pointer, size_t size, int flags) +{ + ProxyBlock *block, + *new_block; + ProxyContext *ctx; + MemoryChunk *chunk = PointerGetMemoryChunk(pointer); + size_t blksize; + + VALGRIND_MAKE_MEM_DEFINED(chunk, PROXY_CHUNKHDRSZ); + + Assert(MemoryChunkIsExternal(chunk)); + + block = ExternalChunkGetBlock(chunk); + + ctx = block->context; + + Assert(ProxyIsValid(ctx)); + + /* only check size in paths where the limits could be hit */ + MemoryContextCheckSize((MemoryContext) ctx, size, flags); + +#ifdef MEMORY_CONTEXT_CHECKING + /* Test for someone scribbling on unused space in chunk */ + if (!sentinel_ok(pointer, chunk->requested_size)) + elog(WARNING, "detected write past chunk end in %s %p", + ctx->header.name, chunk); +#endif + + +#ifdef MEMORY_CONTEXT_CHECKING + /* adjust for sentinel byte, and align */ + blksize = MAXALIGN(size + 1); +#else + blksize = MAXALIGN(size); +#endif + blksize += PROXY_BLOCKHDRSZ + PROXY_CHUNKHDRSZ; + + /* + * Temporarily unlink the allocation, because the address of the + * allocation may change. + */ + dlist_delete_from_thoroughly(&ctx->allocations, &block->node); + + new_block = realloc(block, blksize); + if (new_block == NULL) + { + VALGRIND_MAKE_MEM_NOACCESS(chunk, PROXY_CHUNKHDRSZ); + return MemoryContextAllocationFailure((MemoryContext) ctx, size, flags); + } + + /* Move the block-header vchunk */ + VALGRIND_MEMPOOL_CHANGE(ctx, block, new_block, PROXY_BLOCKHDRSZ); + block = new_block; + + dlist_push_tail(&ctx->allocations, &block->node); + + chunk = (MemoryChunk *) (((char *) block) + PROXY_BLOCKHDRSZ); + + /* randomize the newly allocated memory */ +#ifdef RANDOMIZE_ALLOCATED_MEMORY + if (block->sz < blksize) + randomize_mem(((char *) block) + block->sz, blksize - block->sz); +#endif + + /* update chunk's size data */ +#ifdef MEMORY_CONTEXT_CHECKING + chunk->requested_size = size; + set_sentinel(MemoryChunkGetPointer(chunk), size); +#endif + + /* and adjust the size indicator of the block header */ + block->sz = blksize; + + /* Disallow access to the chunk header. */ + VALGRIND_MAKE_MEM_NOACCESS(chunk, PROXY_CHUNKHDRSZ); + + return MemoryChunkGetPointer(chunk); +} + +/* + * ProxyReset + * Reset the Proxy memory context. + */ +void +ProxyReset(MemoryContext context) +{ + ProxyContext *ctx = (ProxyContext *) context; + + Assert(ProxyIsValid(ctx)); + + while (!dlist_is_empty(&ctx->allocations)) + { + ProxyBlock *block = + dlist_container(ProxyBlock, node, + dlist_pop_head_node(&ctx->allocations)); + size_t blksize = block->sz; + +#ifdef CLOBBER_FREED_MEMORY + wipe_mem(block, blksize); +#endif + + ctx->header.mem_allocated -= blksize; + + /* + * We need to free the block header's vchunk explicitly, although + * the user-data vchunks within will go away in the TRIM below. + * Otherwise Valgrind complains about leaked allocations. + */ + VALGRIND_MEMPOOL_FREE(ctx, block); + + free(block); + } + + Assert(ctx->header.mem_allocated == sizeof(ProxyContext)); + Assert(ProxyIsEmpty(context)); + + /* + * Instruct Valgrind to throw away all the vchunks associated with this + * context, except for the one covering the ProxyContext. This gets rid + * of the vchunks for whatever user data is getting discarded by the + * context reset. + */ + VALGRIND_MEMPOOL_TRIM(ctx, ctx, PROXY_CONTEXTSIZE); +} + +/* + * ProxyAlloc + * Delete this Proxy memory context. + */ +void +ProxyDelete(MemoryContext context) +{ + ProxyContext *ctx = (ProxyContext *) context; + + Assert(ProxyIsValid(ctx)); + +#ifdef MEMORY_CONTEXT_CHECKING + ProxyCheck(context); +#endif + + ProxyReset(context); + + VALGRIND_DESTROY_MEMPOOL(context); + + free(ctx); +} + +/* + * ProxyGetChunkContext + * Return the MemoryContext that 'pointer' belongs to. + */ +MemoryContext +ProxyGetChunkContext(void *pointer) +{ + MemoryChunk *chunk = PointerGetMemoryChunk(pointer); + ProxyBlock *block; + ProxyContext *ctx; + + VALGRIND_MAKE_MEM_DEFINED(chunk, PROXY_CHUNKHDRSZ); + + Assert(MemoryChunkIsExternal(chunk)); + block = ExternalChunkGetBlock(chunk); + + VALGRIND_MAKE_MEM_NOACCESS(chunk, PROXY_CHUNKHDRSZ); + + ctx = block->context; + + Assert(ProxyIsValid(ctx)); + + return (MemoryContext) ctx; +} + +/* + * ProxyGetChunkSpace +* Given a palloc'd chunk, determine the total space + * it occupies (including all memory-allocation overhead). + */ +size_t +ProxyGetChunkSpace(void *pointer) +{ + MemoryChunk *chunk = PointerGetMemoryChunk(pointer); + ProxyBlock *block; + + VALGRIND_MAKE_MEM_DEFINED(chunk, PROXY_CHUNKHDRSZ); + + Assert(MemoryChunkIsExternal(chunk)); + block = ExternalChunkGetBlock(chunk); + + VALGRIND_MAKE_MEM_NOACCESS(chunk, PROXY_CHUNKHDRSZ); + + return block->sz; +} + +/* + * ProxyIsEmpty +* Is the ProxyContext empty of any allocated space? + */ +bool +ProxyIsEmpty(MemoryContext context) +{ + ProxyContext *ctx = (ProxyContext *) context; + + Assert(ProxyIsValid(ctx)); + + return dlist_is_empty(&ctx->allocations); +} + +/* + * ProxyStats + * Compute stats about memory consumption of a Proxy context. + * + * printfunc: if not NULL, pass a human-readable stats string to this. + * passthru: pass this pointer through to printfunc. + * totals: if not NULL, add stats about this context into *totals. + * print_to_stderr: print stats to stderr if true, elog otherwise. + */ +void +ProxyStats(MemoryContext context, MemoryStatsPrintFunc printfunc, + void *passthru, MemoryContextCounters *totals, + bool print_to_stderr) +{ + ProxyContext *ctx = (ProxyContext *) context; + dlist_iter iter; + size_t totalspace; + size_t nchunks = 0; + + totalspace = MAXALIGN(sizeof(ProxyContext)); + + dlist_foreach(iter, &ctx->allocations) + { + ProxyBlock *block = dlist_container(ProxyBlock, node, iter.cur); + + nchunks++; + totalspace += block->sz; + } + + + if (printfunc) + { + char stats_string[200]; + + snprintf(stats_string, sizeof(stats_string), + "%zu total in %zu chunks;", + totalspace, nchunks); + printfunc(context, passthru, stats_string, print_to_stderr); + } + + if (totals) + { + totals->nblocks += nchunks; + totals->totalspace += totalspace; + } +} + +#ifdef MEMORY_CONTEXT_CHECKING +/* + * ProxyCheck + * Walk through chunks and check consistency of memory. + * + * NOTE: report errors as WARNING, *not* ERROR or FATAL. Otherwise you'll + * find yourself in an infinite loop when trouble occurs, because this + * routine will be entered again when elog cleanup tries to release memory! + */ +void +ProxyCheck(MemoryContext context) +{ + ProxyContext *ctx = (ProxyContext *) context; + const char *name = context->name; + dlist_iter iter; + size_t total_allocated = sizeof(ProxyContext); + + /* walk all blocks in this context */ + dlist_foreach(iter, &ctx->allocations) + { + ProxyBlock *block = dlist_container(ProxyBlock, node, iter.cur); + MemoryChunk *chunk = (MemoryChunk *) ((char *) block + PROXY_BLOCKHDRSZ); + + total_allocated += block->sz; + + if (block->sz != MAXALIGN(chunk->requested_size + 1) + PROXY_BLOCKHDRSZ + PROXY_CHUNKHDRSZ) + { + elog(WARNING, "problem in Proxy %s: bad single-chunk %p in block %p", + name, chunk, block); + } + + if (!sentinel_ok(chunk, chunk->requested_size + PROXY_CHUNKHDRSZ)) + { + elog(WARNING, "problem in Proxy %s: detected write past chunk end in block %p, chunk %p", + name, block, chunk); + } + } + + Assert(total_allocated == context->mem_allocated); + + if (total_allocated != ctx->header.mem_allocated) + { + elog(WARNING, "problem in Proxy %s: sum of memory %zd does not match header's %zd", + name, total_allocated, ctx->header.mem_allocated); + } +} +#endif diff --git a/src/include/nodes/memnodes.h b/src/include/nodes/memnodes.h index 4b24778fe1e..a9a4e06d86b 100644 --- a/src/include/nodes/memnodes.h +++ b/src/include/nodes/memnodes.h @@ -147,6 +147,7 @@ typedef struct MemoryContextData (IsA((context), AllocSetContext) || \ IsA((context), SlabContext) || \ IsA((context), GenerationContext) || \ - IsA((context), BumpContext))) + IsA((context), BumpContext) || \ + IsA((context), ProxyContext))) #endif /* MEMNODES_H */ diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 38036c7c703..ff531742c8f 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -150,6 +150,10 @@ extern MemoryContext BumpContextCreate(MemoryContext parent, Size initBlockSize, Size maxBlockSize); +/* proxy.c */ +extern MemoryContext ProxyContextCreate(MemoryContext parent, + const char *name); + /* * Recommended default alloc parameters, suitable for "ordinary" contexts * that might hold quite a lot of data. diff --git a/src/include/utils/memutils_internal.h b/src/include/utils/memutils_internal.h index 475e91b336b..aef1396027e 100644 --- a/src/include/utils/memutils_internal.h +++ b/src/include/utils/memutils_internal.h @@ -93,6 +93,22 @@ extern void BumpStats(MemoryContext context, MemoryStatsPrintFunc printfunc, bool print_to_stderr); #ifdef MEMORY_CONTEXT_CHECKING extern void BumpCheck(MemoryContext context); +#endif + + /* These functions implement the MemoryContext API for the Proxy context. */ +extern void *ProxyAlloc(MemoryContext context, size_t size, int flags); +extern void ProxyFree(void *pointer); +extern void *ProxyRealloc(void *pointer, size_t size, int flags); +extern void ProxyReset(MemoryContext context); +extern void ProxyDelete(MemoryContext context); +extern MemoryContext ProxyGetChunkContext(void *pointer); +extern size_t ProxyGetChunkSpace(void *pointer); +extern bool ProxyIsEmpty(MemoryContext context); +extern void ProxyStats(MemoryContext context, MemoryStatsPrintFunc printfunc, + void *passthru, MemoryContextCounters *totals, + bool print_to_stderr); +#ifdef MEMORY_CONTEXT_CHECKING +extern void ProxyCheck(MemoryContext context); #endif /* @@ -128,7 +144,7 @@ typedef enum MemoryContextMethodID MCTX_SLAB_ID, MCTX_ALIGNED_REDIRECT_ID, MCTX_BUMP_ID, - MCTX_8_UNUSED_ID, + MCTX_PROXY_ID, MCTX_9_UNUSED_ID, MCTX_10_UNUSED_ID, MCTX_11_UNUSED_ID, diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 1040a65bc14..127aaad3e03 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -2431,6 +2431,8 @@ ProjectionInfo ProjectionPath PromptInterruptContext ProtocolVersion +ProxyContext +ProxyChunk PrsStorage PruneFreezeParams PruneFreezeResult -- 2.50.1 (Apple Git-155)