public inbox for [email protected]help / color / mirror / Atom feed
[PATCH v8 4/4] Move the double-plus "Size" columns to the right 52+ messages / 9 participants [nested] [flat]
* [PATCH v8 4/4] Move the double-plus "Size" columns to the right @ 2021-12-17 15:35 Justin Pryzby <[email protected]> 0 siblings, 0 replies; 52+ messages in thread From: Justin Pryzby @ 2021-12-17 15:35 UTC (permalink / raw) \dn, \dA, \db, \l, \d and \dP+ It doesn't make much sense that one cannot show a database's default tablespace without also showing its size, and stat()ing every segment of every relation in the DB. --- src/bin/psql/describe.c | 42 ++++++++++++++--------- src/test/regress/expected/psql.out | 54 +++++++++++++++--------------- 2 files changed, 53 insertions(+), 43 deletions(-) diff --git a/src/bin/psql/describe.c b/src/bin/psql/describe.c index 2e47ea448f7..22fc19a47dd 100644 --- a/src/bin/psql/describe.c +++ b/src/bin/psql/describe.c @@ -240,11 +240,6 @@ describeTablespaces(const char *pattern, int verbose) appendPQExpBufferStr(&buf, ",\n "); printACLColumn(&buf, "spcacl"); - if (verbose > 1) - appendPQExpBuffer(&buf, - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", - gettext_noop("Size")); - appendPQExpBuffer(&buf, ",\n spcoptions AS \"%s\"" ",\n pg_catalog.shobj_description(oid, 'pg_tablespace') AS \"%s\"", @@ -252,6 +247,11 @@ describeTablespaces(const char *pattern, int verbose) gettext_noop("Description")); } + if (verbose > 1) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_tablespace_size(oid)) AS \"%s\"", + gettext_noop("Size")); + appendPQExpBufferStr(&buf, "\nFROM pg_catalog.pg_tablespace\n"); @@ -961,17 +961,22 @@ listAllDbs(const char *pattern, int verbose) gettext_noop("Locale Provider")); appendPQExpBufferStr(&buf, " "); printACLColumn(&buf, "d.datacl"); - if (verbose > 1) + + if (verbose > 0) appendPQExpBuffer(&buf, - ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" - " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" - " ELSE 'No Access'\n" - " END as \"%s\"" ",\n t.spcname as \"%s\"" ",\n pg_catalog.shobj_description(d.oid, 'pg_database') as \"%s\"", - gettext_noop("Size"), gettext_noop("Tablespace"), gettext_noop("Description")); + + if (verbose > 1) + appendPQExpBuffer(&buf, + ",\n CASE WHEN pg_catalog.has_database_privilege(d.datname, 'CONNECT')\n" + " THEN pg_catalog.pg_size_pretty(pg_catalog.pg_database_size(d.datname))\n" + " ELSE 'No Access'\n" + " END as \"%s\"", + gettext_noop("Size")); + appendPQExpBufferStr(&buf, "\nFROM pg_catalog.pg_database d\n"); if (verbose > 0) @@ -3936,10 +3941,13 @@ listTables(const char *tabtypes, const char *pattern, int verbose, bool showSyst gettext_noop("Access method")); appendPQExpBuffer(&buf, - ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_table_size(c.oid)) as \"%s\"" ",\n pg_catalog.obj_description(c.oid, 'pg_class') as \"%s\"", - gettext_noop("Size"), gettext_noop("Description")); + + if (verbose > 1) + appendPQExpBuffer(&buf, + ",\n pg_catalog.pg_size_pretty(pg_catalog.pg_table_size(c.oid)) as \"%s\"", + gettext_noop("Size")); } appendPQExpBufferStr(&buf, @@ -4123,6 +4131,11 @@ listPartitionedTables(const char *reltypes, const char *pattern, int verbose) gettext_noop("Table")); if (verbose > 0) + appendPQExpBuffer(&buf, + ",\n pg_catalog.obj_description(c.oid, 'pg_class') as \"%s\"", + gettext_noop("Description")); + + if (verbose > 1) { if (showNested) { @@ -4139,9 +4152,6 @@ listPartitionedTables(const char *reltypes, const char *pattern, int verbose) ",\n s.tps as \"%s\"", gettext_noop("Total size")); - appendPQExpBuffer(&buf, - ",\n pg_catalog.obj_description(c.oid, 'pg_class') as \"%s\"", - gettext_noop("Description")); } appendPQExpBufferStr(&buf, diff --git a/src/test/regress/expected/psql.out b/src/test/regress/expected/psql.out index a7f5700edc1..2daa4685b4b 100644 --- a/src/test/regress/expected/psql.out +++ b/src/test/regress/expected/psql.out @@ -2858,47 +2858,47 @@ Access method: heap -- AM is displayed for tables, indexes and materialized views. \d+ - List of relations - Schema | Name | Type | Owner | Persistence | Access method | Size | Description ------------------+--------------------+-------------------+----------------------+-------------+---------------+---------+------------- - tableam_display | mat_view_heap_psql | materialized view | regress_display_role | permanent | heap_psql | 0 bytes | - tableam_display | tbl_heap | table | regress_display_role | permanent | heap | 0 bytes | - tableam_display | tbl_heap_psql | table | regress_display_role | permanent | heap_psql | 0 bytes | - tableam_display | view_heap_psql | view | regress_display_role | permanent | | 0 bytes | + List of relations + Schema | Name | Type | Owner | Persistence | Access method | Description +-----------------+--------------------+-------------------+----------------------+-------------+---------------+------------- + tableam_display | mat_view_heap_psql | materialized view | regress_display_role | permanent | heap_psql | + tableam_display | tbl_heap | table | regress_display_role | permanent | heap | + tableam_display | tbl_heap_psql | table | regress_display_role | permanent | heap_psql | + tableam_display | view_heap_psql | view | regress_display_role | permanent | | (4 rows) \dt+ - List of relations - Schema | Name | Type | Owner | Persistence | Access method | Size | Description ------------------+---------------+-------+----------------------+-------------+---------------+---------+------------- - tableam_display | tbl_heap | table | regress_display_role | permanent | heap | 0 bytes | - tableam_display | tbl_heap_psql | table | regress_display_role | permanent | heap_psql | 0 bytes | + List of relations + Schema | Name | Type | Owner | Persistence | Access method | Description +-----------------+---------------+-------+----------------------+-------------+---------------+------------- + tableam_display | tbl_heap | table | regress_display_role | permanent | heap | + tableam_display | tbl_heap_psql | table | regress_display_role | permanent | heap_psql | (2 rows) \dm+ - List of relations - Schema | Name | Type | Owner | Persistence | Access method | Size | Description ------------------+--------------------+-------------------+----------------------+-------------+---------------+---------+------------- - tableam_display | mat_view_heap_psql | materialized view | regress_display_role | permanent | heap_psql | 0 bytes | + List of relations + Schema | Name | Type | Owner | Persistence | Access method | Description +-----------------+--------------------+-------------------+----------------------+-------------+---------------+------------- + tableam_display | mat_view_heap_psql | materialized view | regress_display_role | permanent | heap_psql | (1 row) -- But not for views and sequences. \dv+ - List of relations - Schema | Name | Type | Owner | Persistence | Size | Description ------------------+----------------+------+----------------------+-------------+---------+------------- - tableam_display | view_heap_psql | view | regress_display_role | permanent | 0 bytes | + List of relations + Schema | Name | Type | Owner | Persistence | Description +-----------------+----------------+------+----------------------+-------------+------------- + tableam_display | view_heap_psql | view | regress_display_role | permanent | (1 row) \set HIDE_TABLEAM on \d+ - List of relations - Schema | Name | Type | Owner | Persistence | Size | Description ------------------+--------------------+-------------------+----------------------+-------------+---------+------------- - tableam_display | mat_view_heap_psql | materialized view | regress_display_role | permanent | 0 bytes | - tableam_display | tbl_heap | table | regress_display_role | permanent | 0 bytes | - tableam_display | tbl_heap_psql | table | regress_display_role | permanent | 0 bytes | - tableam_display | view_heap_psql | view | regress_display_role | permanent | 0 bytes | + List of relations + Schema | Name | Type | Owner | Persistence | Description +-----------------+--------------------+-------------------+----------------------+-------------+------------- + tableam_display | mat_view_heap_psql | materialized view | regress_display_role | permanent | + tableam_display | tbl_heap | table | regress_display_role | permanent | + tableam_display | tbl_heap_psql | table | regress_display_role | permanent | + tableam_display | view_heap_psql | view | regress_display_role | permanent | (4 rows) RESET ROLE; -- 2.17.1 --cKDw3XFoqocuprIa-- ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-03-13 13:56 Rahila Syed <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Rahila Syed @ 2025-03-13 13:56 UTC (permalink / raw) To: Daniel Gustafsson <[email protected]>; +Cc: Tomas Vondra <[email protected]>; pgsql-hackers Hi, Please find attached updated and rebased patches. It has the following changes 1. To prevent memory leaks, ensure that the latest statistics published by a process are freed before it exits. This can be achieved by calling dsa_free in the before_shmem_exit callback. 2. Add a level column to maintain consistency with the output of pg_backend_memory_contexts. Thank you, Rahila Syed On Tue, Mar 4, 2025 at 12:30 PM Rahila Syed <[email protected]> wrote: > Hi Daniel, > > Thanks for the rebase, a few mostly superficial comments from a first >> read-through. >> > Thank you for your comments. > > >> The documentation refers to attributes in the return row but the format >> of that >> row isn't displayed which makes following along hard. I think we should >> include a table or a programlisting showing the return data before this >> paragraph. >> >> I included the sql function call and its output in programlisting format > after the > function description. > Since the description was part of a table, I added this additional > information at the > end of that table. > > >> +const char * >> +AssignContextType(NodeTag type) >> This function doesn't actually assign anything so the name is a bit >> misleading, >> it would be better with ContextTypeToString or something similar. >> >> Done. > > >> >> + * By default, only superusers or users with PG_READ_ALL_STATS are >> allowed to >> This sentence is really long and should probably be broken up. >> >> Fixed. > >> >> + * The shared memory buffer has a limited size - it the process has too >> many >> s/it/if/ >> >> Fixed. > > >> + * If the publishing backend does not respond before the condition >> variable >> + * times out, which is set to MEMSTATS_WAIT_TIMEOUT, retry for max_tries >> + * number of times, which is defined by user, before giving up and >> + * returning previously published statistics, if any. >> This comment should mention what happens if the process gives up and >> there is >> no previously published stats. >> >> Done. > > >> >> + int i; >> ... >> + for (i = 0; i < memCtxState[procNumber].total_stats; i++) >> This can be rewritten as "for (int i = 0; .." since we allow C99. >> >> Done. > > >> >> + * process running and consuming lots of memory, that it might end on >> its >> + * own first and its memory contexts are not logged is not a problem. >> This comment is copy/pasted from pg_log_backend_memory_contexts and while >> it >> mostly still apply it should at least be rewritten to not refer to >> logging as >> this function doesn't do that. >> >> Fixed. > > >> >> + ereport(WARNING, >> + (errmsg("PID %d is not a PostgreSQL server process", >> No need to add the extra parenthesis around errmsg anymore, so I think >> new code >> should omit those. >> >> Done. > > >> >> + errhint("Use pg_backend_memory_contexts view instead"))); >> Super nitpick, but errhints should be complete sentences ending with a >> period. >> >> Done. > > >> >> + * statitics have previously been published by the backend. In which >> case, >> s/statitics/statistics/ >> >> Fixed. > > >> >> + * statitics have previously been published by the backend. In which >> case, >> + * check if statistics are not older than curr_timestamp, if they are >> wait >> I think the sentence around the time check is needlessly confusing, could >> it be >> rewritten into something like: >> "A valid DSA pointer isn't proof that statistics are available, it >> can be >> valid due to previously published stats. Check if the stats are >> updated by >> comparing the timestamp, if the stats are newer than our previously >> recorded timestamp from before sending the procsignal they must by >> definition be updated." >> >> Replaced accordingly. > > >> >> + /* Assert for dsa_handle to be valid */ >> Was this intended to be turned into an Assert call? Else it seems better >> to remove. >> > > Added an assert and removed the comment. > > >> + if (print_location != PRINT_STATS_NONE) >> This warrants a comment stating why it makes sense. >> > >> + * Do not print the statistics if print_to_stderr is PRINT_STATS_NONE, >> s/print_to_stderr/print_location/. Also, do we really need >> print_to_stderr in >> this function at all? It seems a bit awkward to combine a boolean and a >> paramter for a tri-state value when the parameter holds the tri_state >> already. >> For readability I think just checking print_location will be better since >> the >> value will be clear, where print_to_stderr=false is less clear in a >> tri-state >> scenario. >> >> I removed the boolean print_to_stderr, the checks are now using > the tri-state enum-print_location. > > >> + * its ancestors to a list, inorder to compute a path. >> s/inorder/in order/ >> >> Fixed. > > >> >> + elog(LOG, "hash table corrupted, can't construct path value"); >> + break; >> This will return either a NIL list or a partial path, but >> PublishMemoryContext >> doesn't really take into consideration that it might be so. Is this >> really >> benign to the point that we can blindly go on? Also, elog(LOG..) is >> mostly for >> tracing or debugging as elog() isn't intended for user facing errors. >> >> I agree that this should be addressed. I added a check for path value > before > storing it in shared memory. If the path is NIL, the path pointer in DSA > will point > to InvalidDsaPointer. > When a client encounters an InvalidDsaPointer it will print NULL in the > path column. > Partial path scenario is unlikely IMO, and I am not sure if it warrants > additional > checks. > > >> +static void >> +compute_num_of_contexts(List *contexts, HTAB *context_id_lookup, >> + int *stats_count, bool get_summary) >> This function does a lot than compute the number of contexts so the name >> seems >> a bit misleading. Perhaps a rename to compute_contexts() or something >> similar? >> >> Renamed to compute_contexts_count_and_ids. > > >> >> + memctx_info[curr_id].name = dsa_allocate0(area, >> + strlen(clipped_ident) + 1); >> These calls can use idlen instead of more strlen() calls no? While there >> is no >> performance benefit, it would increase readability IMHO if the code first >> calculates a value, and then use it. >> >> Done. > > >> >> + strncpy(name, >> + clipped_ident, strlen(clipped_ident)); >> Since clipped_ident has been nul terminated earlier there is no need to >> use >> strncpy, we can instead use strlcpy and take the target buffer size into >> consideration rather than the input string length. >> >> Replaced with the strlcpy calls. > > >> >> PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts >> */ >> + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to log the memory contexts >> */ >> This comment should be different from the LOG_MEMORY_xx one. >> >> Fixed. > > +#define MEM_CONTEXT_SHMEM_STATS_SIZE 30 >> +#define MAX_TYPE_STRING_LENGTH 64 >> These are unused, from an earlier version of the patch perhaps? >> >> Removed > > + * Singe DSA area is created and used by all the processes, >> s/Singe/Since/ >> > > Fixed. > > +typedef struct MemoryContextBackendState >> This is only used in mcxtfuncs.c and can be moved there rather than being >> exported in the header. >> > > This is being used in mcxt.c too in the form of the variable memCtxState. > > >> > > +} MemoryContextId; >> This lacks an entry in the typedefs.list file. >> >> Added. > > Please find attached the updated patches with the above fixes. > > Thank you, > Rahila Syed > Attachments: [application/octet-stream] v17-0001-Preparatory-changes-for-reporting-memory-context-sta.patch (4.3K, ../../CAH2L28vULzqoit+YCKR5UhdT+AR+b1Qcs6Hgpz6nQz6NBT2jug@mail.gmail.com/3-v17-0001-Preparatory-changes-for-reporting-memory-context-sta.patch) download | inline diff: From bce37aa953a3aefbe83d98bbb9fa931598bd347d Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Mon, 3 Feb 2025 15:33:19 +0530 Subject: [PATCH 1/2] Preparatory changes for reporting memory context statistics Ensure that MemoryContextStatsInternal can return number of contexts. Also, provide an option in MemoryContextStatsInternal to return without printing stats to either stderr or logs. --- src/backend/utils/mmgr/mcxt.c | 65 +++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 91060de0ab..34cdcdf2fd 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -135,6 +135,17 @@ static const MemoryContextMethods mcxt_methods[] = { }; #undef BOGUS_MCTX +/* + * This is passed to MemoryContextStatsInternal to determine whether + * to print context statistics or not and where to print them logs or + * stderr. + */ +typedef enum PrintDestination +{ + PRINT_STATS_TO_STDERR = 0, + PRINT_STATS_TO_LOGS, + PRINT_STATS_NONE +} PrintDestination; /* * CurrentMemoryContext @@ -162,7 +173,7 @@ static void MemoryContextCallResetCallbacks(MemoryContext context); static void MemoryContextStatsInternal(MemoryContext context, int level, int max_level, int max_children, MemoryContextCounters *totals, - bool print_to_stderr); + PrintDestination print_location, int *num_contexts); static void MemoryContextStatsPrint(MemoryContext context, void *passthru, const char *stats_string, bool print_to_stderr); @@ -831,11 +842,19 @@ MemoryContextStatsDetail(MemoryContext context, bool print_to_stderr) { MemoryContextCounters grand_totals; + int num_contexts; + PrintDestination print_location; memset(&grand_totals, 0, sizeof(grand_totals)); + if (print_to_stderr) + print_location = PRINT_STATS_TO_STDERR; + else + print_location = PRINT_STATS_TO_LOGS; + + /* num_contexts report number of contexts aggregated in the output */ MemoryContextStatsInternal(context, 0, max_level, max_children, - &grand_totals, print_to_stderr); + &grand_totals, print_location, &num_contexts); if (print_to_stderr) fprintf(stderr, @@ -876,18 +895,43 @@ static void MemoryContextStatsInternal(MemoryContext context, int level, int max_level, int max_children, MemoryContextCounters *totals, - bool print_to_stderr) + PrintDestination print_location, int *num_contexts) { MemoryContext child; int ichild; + bool print_to_stderr = true; + check_stack_depth(); Assert(MemoryContextIsValid(context)); - /* Examine the context itself */ - context->methods->stats(context, - MemoryContextStatsPrint, - &level, - totals, print_to_stderr); + if (print_location == PRINT_STATS_TO_STDERR) + print_to_stderr = true; + else if (print_location == PRINT_STATS_TO_LOGS) + print_to_stderr = false; + + if (print_location != PRINT_STATS_NONE) + { + /* Examine the context itself */ + context->methods->stats(context, + MemoryContextStatsPrint, + &level, + totals, print_to_stderr); + } + + /* + * Do not print the statistics if print_to_stderr is PRINT_STATS_NONE, + * only compute totals. + */ + else + { + /* Examine the context itself */ + context->methods->stats(context, + NULL, + NULL, + totals, print_to_stderr); + } + /* Increment the context count */ + *num_contexts = *num_contexts + 1; /* * Examine children. @@ -907,7 +951,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, MemoryContextStatsInternal(child, level + 1, max_level, max_children, totals, - print_to_stderr); + print_location, num_contexts); } } @@ -925,6 +969,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, ichild++; child = MemoryContextTraverseNext(child, context); } + *num_contexts = *num_contexts + ichild; if (print_to_stderr) { @@ -939,7 +984,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, local_totals.freechunks, local_totals.totalspace - local_totals.freespace); } - else + else if (print_location != PRINT_STATS_NONE) ereport(LOG_SERVER_ONLY, (errhidestmt(true), errhidecontext(true), -- 2.34.1 [application/octet-stream] v17-0002-Function-to-report-memory-context-statistics.patch (54.5K, ../../CAH2L28vULzqoit+YCKR5UhdT+AR+b1Qcs6Hgpz6nQz6NBT2jug@mail.gmail.com/4-v17-0002-Function-to-report-memory-context-statistics.patch) download | inline diff: From 3a570e83771aa1ce9399f2717f16c704cb7a00c0 Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Mon, 3 Feb 2025 15:37:17 +0530 Subject: [PATCH 2/2] Function to report memory context statistics This function sends a signal to a backend to publish statistics of all its memory contexts. Signal handler running in the backend process, sets a flag, which causes it to copy its MemoryContextStats to a DSA, during the next call to CHECK_FOR_INTERRUPTS(). If there are more statistics than that fit in 16MB, the remaining statistics are copied as a cumulative total of the remaining contexts. Once its done, it signals the client backend using a condition variable. The client backend wakes up, reads the shared memory and returns these values in the form of set of records, one for each memory context, to the user, followed by a cumulative total of the remaining contexts, if any. If get_summary is true return statistics of all children of TopMemoryContext with aggregated statistics of their children. User can pass num_of_tries which determines the total number of wait cycles in a client backend for latest statistics. Each cycle wait timeout is set to 1 seconds. Post this the client displays previously published statistics or returns without results. Each backend and auxiliary process has its own slot for reporting the stats. There is an array of such memory slots of size MaxBackends+NumofAuxiliary processes in fixed shared memory. Each of these slots point to a smaller dsa allocations within a single DSA, which contains the stats to be shared by the corresponding process. Each slot has its own LW lock and condition variable for synchronization and communication between the publishing process and the client backend. --- doc/src/sgml/func.sgml | 61 ++ src/backend/postmaster/autovacuum.c | 4 + src/backend/postmaster/checkpointer.c | 4 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 4 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 4 + src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 1 + src/backend/utils/adt/mcxtfuncs.c | 436 +++++++++++++- src/backend/utils/init/globals.c | 1 + src/backend/utils/mmgr/mcxt.c | 567 +++++++++++++++++- src/include/catalog/pg_proc.dat | 10 + src/include/miscadmin.h | 1 + src/include/storage/procsignal.h | 1 + src/include/utils/memutils.h | 69 +++ src/test/regress/expected/sysviews.out | 14 + src/test/regress/sql/sysviews.sql | 14 + src/tools/pgindent/typedefs.list | 4 + 21 files changed, 1169 insertions(+), 43 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 51dd8ad657..b5eee4d4a7 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -28553,6 +28553,50 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres} </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_get_process_memory_contexts</primary> + </indexterm> + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type>, <parameter>get_summary</parameter> <type>boolean</type>, <parameter>num_of_tries</parameter> <type>integer</type> ) + <returnvalue>setof record</returnvalue> + </para> + <para> + This function handles requests to display the memory contexts of a + PostgreSQL process with the specified process ID (PID). It takes three + arguments: <parameter>PID</parameter>, <parameter>get_summary</parameter> + and <parameter>num_of_tries</parameter>. The function can send requests + to both backend and auxiliary processes. + + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context statistics + are aggregated and a cumulative total is displayed. The num_agg_contexts + column indicates the number of contexts aggregated in the displayed + statistics. The num_agg_contexts value is typically 1, meaning that each + context's statistics are displayed separately. + + When <parameter>get_summary</parameter> is set to true, statistics + for memory contexts at levels 1 and 2 are displayed, with level 1 + representing the root node (i.e., TopMemoryContext). + Each level 2 context's statistics represent an aggregate of all its + child contexts' statistics, with num_agg_contexts indicating the number + of these aggregated child contexts. + + When <parameter>get_summary</parameter> is set to false, the + num_agg_contexts value is 1, indicating that individual statistics are + being displayed. + + <parameter>num_of_tries</parameter> indicates the number of times + the client will wait for the latest statistics. The wait per try is 1 + second. This parameter can be increased if the user anticipates a delay + in the response from the reporting process. Conversely, if users are + frequently and periodically querying the process for statistics, or if + there are concurrent requests for statistics of the same process, + lowering the parameter might help achieve a faster response. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> @@ -28671,6 +28715,23 @@ LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 because it may generate a large number of log messages. </para> + <para> + <function>pg_get_process_memory_contexts</function> can be used + to request the memory contexts statistics of any postgres process. For example: +<programlisting> +postgres=# SELECT * FROM pg_get_process_memory_contexts( + (SELECT pid FROM pg_stat_activity + WHERE backend_type = 'checkpointer') + , false, 5) LIMIT 1; + name | ident | type | path | total_bytes | total_nblocks | free_bytes | free_chunks | used_bytes | num_ +agg_contexts | stats_timestamp +------------------+-------+----------+------+-------------+---------------+------------+-------------+------------+----- +-------------+---------------------------------- + TopMemoryContext | | AllocSet | {1} | 102664 | 4 | 3008 | 2 | 99656 | + 1 | 2025-03-04 10:01:57.590543+05:30 +</programlisting> + </para> + </sect2> <sect2 id="functions-admin-backup"> diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 800815dfbc..6630fbf05c 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -779,6 +779,10 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index 0e228d143a..dc1b228b71 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -661,6 +661,10 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index 0ae9bf906e..f24f574e74 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -48,6 +48,10 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index dbe4e1d426..9cdba52054 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -865,6 +865,10 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index 27e86cf393..7149a67fcb 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -192,6 +192,10 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index ccba0f84e6..4da70f6e0a 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -877,6 +877,10 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index 174eed7036..5eee04d52a 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -50,6 +50,7 @@ #include "storage/sinvaladt.h" #include "utils/guc.h" #include "utils/injection_point.h" +#include "utils/memutils.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -340,6 +341,8 @@ CreateOrAttachShmemStructs(void) StatsShmemInit(); WaitEventCustomShmemInit(); InjectionPointShmemInit(); + MemCtxShmemInit(); + MemCtxBackendShmemInit(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index 7d20196550..b59034fdc3 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -690,6 +690,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_GET_MEMORY_CONTEXT)) + HandleGetMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 55ab2da299..00bc19c3a7 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3500,6 +3500,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 3c594415bf..fcf68b47b9 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -159,6 +159,7 @@ WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." +MEM_CTX_PUBLISH "Waiting for backend to publish memory information." ABI_compatibility: diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index 396c2f223b..96a7c634e7 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -17,28 +17,26 @@ #include "funcapi.h" #include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "access/twophase.h" +#include "catalog/pg_authid_d.h" +#include "nodes/pg_list.h" #include "storage/proc.h" #include "storage/procarray.h" +#include "utils/acl.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/memutils.h" +#include "utils/wait_event_types.h" /* ---------- * The max bytes for showing identifiers of MemoryContext. * ---------- */ #define MEMORY_CONTEXT_IDENT_DISPLAY_SIZE 1024 - -/* - * MemoryContextId - * Used for storage of transient identifiers for - * pg_get_backend_memory_contexts. - */ -typedef struct MemoryContextId -{ - MemoryContext context; - int context_id; -} MemoryContextId; +struct MemoryContextBackendState *memCtxState = NULL; +struct MemoryContextState *memCtxArea = NULL; /* * int_list_to_array @@ -143,24 +141,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, else nulls[1] = true; - switch (context->type) - { - case T_AllocSetContext: - type = "AllocSet"; - break; - case T_GenerationContext: - type = "Generation"; - break; - case T_SlabContext: - type = "Slab"; - break; - case T_BumpContext: - type = "Bump"; - break; - default: - type = "???"; - break; - } + type = ContextTypeToString(context->type); values[2] = CStringGetTextDatum(type); values[3] = Int32GetDatum(list_length(path)); /* level */ @@ -175,6 +156,32 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, list_free(path); } +const char * +ContextTypeToString(NodeTag type) +{ + const char *context_type; + + switch (type) + { + case T_AllocSetContext: + context_type = "AllocSet"; + break; + case T_GenerationContext: + context_type = "Generation"; + break; + case T_SlabContext: + context_type = "Slab"; + break; + case T_BumpContext: + context_type = "Bump"; + break; + default: + context_type = "???"; + break; + } + return (context_type); +} + /* * pg_get_backend_memory_contexts * SQL SRF showing backend memory context. @@ -281,7 +288,7 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) * to acquire a lock on an arbitrary process to prevent that. But since * this mechanism is usually used to debug a backend or an auxiliary * process running and consuming lots of memory, that it might end on its - * own first and its memory contexts are not logged is not a problem. + * own first and its memory contexts are not reported is not a problem. */ if (proc == NULL) { @@ -290,7 +297,7 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) * if one backend terminated on its own during the run. */ ereport(WARNING, - (errmsg("PID %d is not a PostgreSQL server process", pid))); + errmsg("PID %d is not a PostgreSQL server process", pid)); PG_RETURN_BOOL(false); } @@ -299,9 +306,374 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) { /* Again, just a warning to allow loops */ ereport(WARNING, - (errmsg("could not send signal to process %d: %m", pid))); + errmsg("could not send signal to process %d: %m", pid)); PG_RETURN_BOOL(false); } PG_RETURN_BOOL(true); } + +/* + * pg_get_process_memory_contexts + * Signal a backend or an auxiliary process to send its memory contexts, + * wait for the results and display them. + * + * By default, only superusers or users with PG_READ_ALL_STATS are allowed to + * signal a process to return the memory contexts. This is because allowing + * any users to issue this request at an unbounded rate would cause lots of + * requests to be sent, which can lead to denial of service. Additional roles + * can be permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to copy the memory context details + * to a dynamic shared memory space. + * + * The shared memory buffer has a limited size - if the process has too many + * memory contexts, the memory contexts that do not fit are summarized + * and represented as cumulative total at the end of the buffer. + * + * After sending the signal, wait on a condition variable. The publishing + * backend, after copying the data to shared memory, sends signal on that + * condition variable. There is one condition variable per publishing + * backend. + * Once condition variable is signalled, check if the memory context + * information is available for reading and display. + * + * If the publishing backend does not respond before the condition variable + * times out, which is set to MEMSTATS_WAIT_TIMEOUT, retry for max_tries + * number of times, which is defined by user, before giving up and + * returning previously published statistics, if any. If previous statistics + * do not exist, return NULL. + */ +Datum +pg_get_process_memory_contexts(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + bool get_summary = PG_GETARG_BOOL(1); + PGPROC *proc; + ProcNumber procNumber = INVALID_PROC_NUMBER; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + dsa_area *area; + MemoryContextEntry *memctx_info; + int num_retries = 0; + TimestampTz curr_timestamp; + int max_tries = PG_GETARG_INT32(2); + + /* + * Only superusers or users with pg_read_all_stats privileges can view the + * memory context statistics of another process + */ + if (!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("memory context statistics privilege error")); + + InitMaterializedSRF(fcinfo, 0); + + /* + * See if the process with given pid is a backend or an auxiliary process. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + proc = AuxiliaryPidGetProc(pid); + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; but by the time we reach kill(), a process for which we + * get a valid proc here might have terminated on its own. There's no way + * to acquire a lock on an arbitrary process to prevent that. But since + * this mechanism is usually used to debug a backend or an auxiliary + * process running and consuming lots of memory, that it might end on its + * own first and its memory contexts are not logged is not a problem. + */ + if (proc == NULL) + { + /* + * This is just a warning so a loop-through-resultset will not abort + * if one backend terminated on its own during the run. + */ + ereport(WARNING, + (errmsg("PID %d is not a PostgreSQL server process", + pid))); + PG_RETURN_NULL(); + } + + procNumber = GetNumberFromPGProc(proc); + if (procNumber == MyProcNumber) + { + ereport(WARNING, + errmsg("cannot return statistics for local backend"), + errhint("Use pg_backend_memory_contexts view instead.")); + PG_RETURN_NULL(); + } + + LWLockAcquire(&memCtxState[procNumber].lw_lock, LW_EXCLUSIVE); + memCtxState[procNumber].get_summary = get_summary; + LWLockRelease(&memCtxState[procNumber].lw_lock); + + curr_timestamp = GetCurrentTimestamp(); + + /* + * Send a signal to a postgresql process, informing it we want it to + * produce information about memory contexts. + */ + if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, procNumber) < 0) + { + ereport(WARNING, + errmsg("could not send signal to process %d: %m", pid)); + PG_RETURN_NULL(); + } + + /* + * A valid DSA pointer isn't proof that statistics are available, it can + * be valid due to previously published stats. Check if the stats are + * updated by comparing the timestamp, if the stats are newer than our + * previously recorded timestamp from before sending the procsignal, they + * must by definition be updated. Wait for max_tries * + * MEMSTATS_WAIT_TIMEOUT, following which display old statistics if + * available or return NULL. + */ + while (1) + { + long msecs; + + /* + * We expect to come out of sleep when the requested process has + * finished publishing the statistics, verified using the valid dsa + * pointer. + * + * Make sure that the information belongs to pid we requested + * information for, Otherwise loop back and wait for the server + * process to finish publishing statistics. + */ + LWLockAcquire(&memCtxState[procNumber].lw_lock, LW_EXCLUSIVE); + msecs = + TimestampDifferenceMilliseconds(curr_timestamp, + memCtxState[procNumber].stats_timestamp); + + /* + * Note in procnumber.h file says that a procNumber can be re-used for + * a different backend immediately after a backend exits. In case an + * old process' data was there and not updated by the current process + * in the slot identified by the procNumber, the pid of the requested + * process and the proc_id might not match. + */ + if (memCtxState[procNumber].proc_id == pid) + { + /* + * Break if the latest stats have been read, indicated by + * statistics timestamp being newer than the current request + * timestamp. + */ + if (DsaPointerIsValid(memCtxState[procNumber].memstats_dsa_pointer) + && msecs > 0) + { + LWLockRelease(&memCtxState[procNumber].lw_lock); + break; + } + + } + LWLockRelease(&memCtxState[procNumber].lw_lock); + + /* + * Recheck the state of the backend before sleeping on the condition + * variable + */ + proc = BackendPidGetProc(pid); + +#define MEMSTATS_WAIT_TIMEOUT 1000 + if (proc == NULL) + proc = AuxiliaryPidGetProc(pid); + if (proc == NULL) + { + ereport(WARNING, + errmsg("PID %d is not a PostgreSQL server process", + pid)); + PG_RETURN_NULL(); + } + if (ConditionVariableTimedSleep(&memCtxState[procNumber].memctx_cv, + MEMSTATS_WAIT_TIMEOUT, + WAIT_EVENT_MEM_CTX_PUBLISH)) + { + /* + * Wait for max_tries defined by user, display previously + * published statistics if any, when max_tries are over. + */ + if (num_retries > max_tries) + { + LWLockAcquire(&memCtxState[procNumber].lw_lock, LW_EXCLUSIVE); + /* Displaying previously published statistics */ + if (DsaPointerIsValid(memCtxState[procNumber].memstats_dsa_pointer)) + { + LWLockRelease(&memCtxState[procNumber].lw_lock); + break; + } + else + { + LWLockRelease(&memCtxState[procNumber].lw_lock); + PG_RETURN_NULL(); + } + } + ereport(LOG, + errmsg("Wait for %d process to publish stats timed out, trying again", + pid)); + num_retries = num_retries + 1; + } + + } + /* We should land here only with a valid DSA handle */ + LWLockAcquire(&memCtxArea->lw_lock, LW_EXCLUSIVE); + Assert(memCtxArea->memstats_dsa_handle != DSA_HANDLE_INVALID); + area = dsa_attach(memCtxArea->memstats_dsa_handle); + LWLockRelease(&memCtxArea->lw_lock); + + /* + * Backend has finished publishing the stats, read them + * + * Read statistics of top level 1 and 2 contexts, if get_summary is true. + */ + LWLockAcquire(&memCtxState[procNumber].lw_lock, LW_EXCLUSIVE); + memctx_info = (MemoryContextEntry *) dsa_get_address(area, + memCtxState[procNumber].memstats_dsa_pointer); + +#define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 12 + for (int i = 0; i < memCtxState[procNumber].total_stats; i++) + { + ArrayType *path_array; + int path_length; + Datum values[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + bool nulls[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + char *name; + char *ident; + Datum *path_datum_array; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + if (DsaPointerIsValid(memctx_info[i].name)) + { + name = (char *) dsa_get_address(area, memctx_info[i].name); + values[0] = CStringGetTextDatum(name); + } + else + nulls[0] = true; + if (DsaPointerIsValid(memctx_info[i].ident)) + { + ident = (char *) dsa_get_address(area, memctx_info[i].ident); + values[1] = CStringGetTextDatum(ident); + } + else + nulls[1] = true; + + if (memctx_info[i].type != NULL) + values[2] = CStringGetTextDatum(memctx_info[i].type); + else + nulls[2] = true; + + path_length = memctx_info[i].path_length; + + if (DsaPointerIsValid(memctx_info[i].path)) + { + path_datum_array = (Datum *) dsa_get_address(area, memctx_info[i].path); + path_array = construct_array_builtin(path_datum_array, + path_length, INT4OID); + + values[3] = PointerGetDatum(path_array); + } + else + nulls[3] = true; + values[4] = Int32GetDatum(path_length); /* level */ + values[5] = Int64GetDatum(memctx_info[i].totalspace); + values[6] = Int64GetDatum(memctx_info[i].nblocks); + values[7] = Int64GetDatum(memctx_info[i].freespace); + values[8] = Int64GetDatum(memctx_info[i].freechunks); + values[9] = Int64GetDatum(memctx_info[i].totalspace - + memctx_info[i].freespace); + values[10] = Int32GetDatum(memctx_info[i].num_agg_stats); + values[11] = TimestampTzGetDatum(memCtxState[procNumber].stats_timestamp); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + LWLockRelease(&memCtxState[procNumber].lw_lock); + + ConditionVariableCancelSleep(); + dsa_detach(area); + + PG_RETURN_NULL(); +} + +/* + * Shared memory sizing for reporting memory context information. + */ +static Size +MemCtxShmemSize(void) +{ + Size TotalProcs = + add_size(MaxBackends, add_size(NUM_AUXILIARY_PROCS, max_prepared_xacts)); + + return mul_size(TotalProcs, sizeof(MemoryContextBackendState)); +} + +/* + * Init shared memory for reporting memory context information. + */ +void +MemCtxBackendShmemInit(void) +{ + bool found; + Size TotalProcs = + add_size(MaxBackends, add_size(NUM_AUXILIARY_PROCS, max_prepared_xacts)); + + memCtxState = (MemoryContextBackendState *) ShmemInitStruct("MemoryContextBackendState", + MemCtxShmemSize(), + &found); + if (!IsUnderPostmaster) + { + Assert(!found); + + for (int i = 0; i < TotalProcs; i++) + { + ConditionVariableInit(&memCtxState[i].memctx_cv); + + LWLockInitialize(&memCtxState[i].lw_lock, + LWLockNewTrancheId()); + LWLockRegisterTranche(memCtxState[i].lw_lock.tranche, + "mem_context_backend_stats_reporting"); + + memCtxState[i].memstats_dsa_pointer = InvalidDsaPointer; + } + } + else + { + Assert(found); + } +} + +/* + * Initialize shared memory for displaying memory + * context statistics + */ +void +MemCtxShmemInit(void) +{ + bool found; + + memCtxArea = (MemoryContextState *) ShmemInitStruct("MemoryContextState", sizeof(MemoryContextState), + &found); + if (!IsUnderPostmaster) + { + Assert(!found); + + LWLockInitialize(&memCtxArea->lw_lock, + LWLockNewTrancheId()); + LWLockRegisterTranche(memCtxArea->lw_lock.tranche, + "mem_context_stats_reporting"); + memCtxArea->memstats_dsa_handle = DSA_HANDLE_INVALID; + } + else + { + Assert(found); + } +} diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index b844f9fdae..13938ccb0f 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -38,6 +38,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t PublishMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 34cdcdf2fd..d1ad444b33 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -19,10 +19,18 @@ *------------------------------------------------------------------------- */ +#include <math.h> #include "postgres.h" #include "mb/pg_wchar.h" #include "miscadmin.h" +#include "nodes/pg_list.h" +#include "storage/fd.h" +#include "storage/lwlock.h" +#include "storage/dsm.h" +#include "storage/ipc.h" +#include "utils/dsa.h" +#include "utils/hsearch.h" #include "utils/memdebug.h" #include "utils/memutils.h" #include "utils/memutils_internal.h" @@ -164,6 +172,7 @@ MemoryContext CacheMemoryContext = NULL; MemoryContext MessageContext = NULL; MemoryContext TopTransactionContext = NULL; MemoryContext CurTransactionContext = NULL; +static bool DsaCleanupRegistered = false; /* This is a transient link to the active portal's memory context: */ MemoryContext PortalContext = NULL; @@ -177,6 +186,17 @@ static void MemoryContextStatsInternal(MemoryContext context, int level, static void MemoryContextStatsPrint(MemoryContext context, void *passthru, const char *stats_string, bool print_to_stderr); +static void PublishMemoryContext(MemoryContextEntry *memctx_infos, + int curr_id, MemoryContext context, + List *path, + MemoryContextCounters stat, + int num_contexts, dsa_area *area); +static void compute_contexts_count_and_ids(List *contexts, HTAB *context_id_lookup, + int *stats_count, + bool get_summary); +static List *compute_context_path(MemoryContext c, HTAB *context_id_lookup); +static void dsa_free_previous_stats(dsa_area *area, int total_stats, dsa_pointer prev_dsa_pointer); +static void AtProcExit_memctx_dsa_free(int code, Datum arg); /* * You should not do memory allocations within a critical section, because @@ -889,7 +909,8 @@ MemoryContextStatsDetail(MemoryContext context, * One recursion level for MemoryContextStats * * Print stats for this context if possible, but in any case accumulate counts - * into *totals (if not NULL). + * into *totals (if not NULL). The callers should make sure that print_location + * is set to PRINT_STATS_STDERR or PRINT_STATS_TO_LOGS or PRINT_STATS_NONE. */ static void MemoryContextStatsInternal(MemoryContext context, int level, @@ -899,36 +920,41 @@ MemoryContextStatsInternal(MemoryContext context, int level, { MemoryContext child; int ichild; - bool print_to_stderr = true; check_stack_depth(); Assert(MemoryContextIsValid(context)); if (print_location == PRINT_STATS_TO_STDERR) - print_to_stderr = true; + { + /* Examine the context itself */ + context->methods->stats(context, + MemoryContextStatsPrint, + &level, + totals, true); + } else if (print_location == PRINT_STATS_TO_LOGS) - print_to_stderr = false; - - if (print_location != PRINT_STATS_NONE) { + /* Examine the context itself */ context->methods->stats(context, MemoryContextStatsPrint, &level, - totals, print_to_stderr); + totals, false); } /* * Do not print the statistics if print_to_stderr is PRINT_STATS_NONE, - * only compute totals. + * only compute totals. This is used in reporting of memory context + * statistics via a sql function. Last parameter is not relevant. */ else { + Assert(print_location == PRINT_STATS_NONE); /* Examine the context itself */ context->methods->stats(context, NULL, NULL, - totals, print_to_stderr); + totals, false); } /* Increment the context count */ *num_contexts = *num_contexts + 1; @@ -971,7 +997,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, } *num_contexts = *num_contexts + ichild; - if (print_to_stderr) + if (print_location == PRINT_STATS_TO_STDERR) { for (int i = 0; i <= level; i++) fprintf(stderr, " "); @@ -984,7 +1010,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, local_totals.freechunks, local_totals.totalspace - local_totals.freespace); } - else if (print_location != PRINT_STATS_NONE) + else if (print_location == PRINT_STATS_TO_LOGS) ereport(LOG_SERVER_ONLY, (errhidestmt(true), errhidecontext(true), @@ -1321,6 +1347,21 @@ HandleLogMemoryContextInterrupt(void) /* latch will be set by procsignal_sigusr1_handler */ } +/* + * HandleGetMemoryContextInterrupt + * Handle receipt of an interrupt indicating publishing of memory + * contexts. + * + * All the actual work is deferred to ProcessLogMemoryContextInterrupt() + */ +void +HandleGetMemoryContextInterrupt(void) +{ + InterruptPending = true; + PublishMemoryContextPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + /* * ProcessLogMemoryContextInterrupt * Perform logging of memory contexts of this backend process. @@ -1358,6 +1399,510 @@ ProcessLogMemoryContextInterrupt(void) MemoryContextStatsDetail(TopMemoryContext, 100, 100, false); } +/* + * ProcessGetMemoryContextInterrupt + * Generate information about memory contexts used by the process. + * + * Performs a breadth first search on the memory context tree, thus parents + * statistics are reported before children in the monitoring function output. + * + * Statistics per context for all the processes are shared via the same dynamic + * shared area. The statistics for contexts that exceed the pre-determined size + * limit, are captured as a cumulative total at the end of individual statistics. + * + * If get_summary is true, we traverse the memory context tree recursively in + * depth first search manner to cover all the children of a parent context, to be + * able to display a cumulative total of memory consumption by a parent. + */ +void +ProcessGetMemoryContextInterrupt(void) +{ + List *contexts; + + HASHCTL ctl; + HTAB *context_id_lookup; + int context_id = 0; + MemoryContext stat_cxt; + MemoryContextEntry *meminfo; + bool get_summary = false; + + dsa_area *area = NULL; + int max_stats; + int idx = MyProcNumber; + int stats_count = 0; + MemoryContextCounters stat; + int num_individual_stats = 0; + + PublishMemoryContextPending = false; + + /* + * Make a new context that will contain the hash table, to ease the + * cleanup. + */ + stat_cxt = AllocSetContextCreate(CurrentMemoryContext, + "Memory context statistics", + ALLOCSET_DEFAULT_SIZES); + + /* + * The hash table used for constructing "path" column of the view, similar + * to its local backend counterpart. + */ + ctl.keysize = sizeof(MemoryContext); + ctl.entrysize = sizeof(MemoryContextId); + ctl.hcxt = stat_cxt; + + context_id_lookup = hash_create("pg_get_remote_backend_memory_contexts", + 256, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + /* List of contexts to process in the next round - start at the top. */ + contexts = list_make1(TopMemoryContext); + + /* Compute the number of stats that can fit in the defined limit */ + max_stats = (MAX_NUM_DEFAULT_SEGMENTS * DSA_DEFAULT_INIT_SEGMENT_SIZE) + / (sizeof(MemoryContextEntry) + (MEM_CONTEXT_MAX_LEVEL + * sizeof(Datum)) + (2 * MEMORY_CONTEXT_IDENT_SHMEM_SIZE)); + + LWLockAcquire(&memCtxState[idx].lw_lock, LW_EXCLUSIVE); + get_summary = memCtxState[idx].get_summary; + LWLockRelease(&memCtxState[idx].lw_lock); + + /* + * Traverse the memory context tree to find total number of contexts. If + * summary is requested report the total number of contexts at level 1 and + * 2 from the top. Also, populate the hash table of context ids. + */ + compute_contexts_count_and_ids(contexts, context_id_lookup, &stats_count, + get_summary); + + /* + * Allocate memory in this process's dsa for storing statistics of the the + * memory contexts upto max_stats, for contexts that don't fit within a + * limit, a cumulative total is written as the last record in the DSA + * segment. + */ + stats_count = (stats_count > max_stats) ? max_stats : stats_count; + + LWLockAcquire(&memCtxArea->lw_lock, LW_EXCLUSIVE); + + /* + * Create a DSA and send handle to the the client process after storing + * the context statistics. If number of contexts exceed a predefined + * limit(8MB), a cumulative total is stored for such contexts. + */ + if (memCtxArea->memstats_dsa_handle == DSA_HANDLE_INVALID) + { + MemoryContext oldcontext = CurrentMemoryContext; + dsa_handle handle; + + MemoryContextSwitchTo(TopMemoryContext); + + area = dsa_create(memCtxArea->lw_lock.tranche); + + handle = dsa_get_handle(area); + MemoryContextSwitchTo(oldcontext); + + dsa_pin_mapping(area); + + /* + * Pin the dsa area, this is to make sure the area remains attachable + * even if current backend exits. This is done so that a waiting + * client gets the stats even after a process exits. + */ + dsa_pin(area); + + /* Set the handle in shared memory */ + memCtxArea->memstats_dsa_handle = handle; + } + + /* + * If DSA exists, created by another process publishing statistics, attach + * to it. + */ + else if (area == NULL) + { + MemoryContext oldcontext = CurrentMemoryContext; + + MemoryContextSwitchTo(TopMemoryContext); + area = dsa_attach(memCtxArea->memstats_dsa_handle); + MemoryContextSwitchTo(oldcontext); + dsa_pin_mapping(area); + } + LWLockRelease(&memCtxArea->lw_lock); + + /* + * Register a callback to free the allocated dsa pointers on process exit + * This is done after we have a valid dsa area. + */ + if (!DsaCleanupRegistered) + { + before_shmem_exit(AtProcExit_memctx_dsa_free, PointerGetDatum(area)); + DsaCleanupRegistered = true; + } + + /* + * Hold the process specific lock to protect writes to process specific + * memory. This way two processes publishing statistics do not block each + * other. + */ + LWLockAcquire(&memCtxState[idx].lw_lock, LW_EXCLUSIVE); + memCtxState[idx].proc_id = MyProcPid; + + if (DsaPointerIsValid(memCtxState[idx].memstats_dsa_pointer)) + { + /* + * Free any previous allocations, free the name, ident and path + * pointers before freeing the pointer that contains them. + */ + dsa_free_previous_stats(area, memCtxState[idx].total_stats, + memCtxState[idx].memstats_dsa_pointer); + dsa_free(area, memCtxState[idx].memstats_dsa_pointer); + memCtxState[idx].memstats_dsa_pointer = InvalidDsaPointer; + } + memCtxState[idx].memstats_dsa_pointer = dsa_allocate0(area, + stats_count * sizeof(MemoryContextEntry)); + + meminfo = (MemoryContextEntry *) dsa_get_address(area, + memCtxState[idx].memstats_dsa_pointer); + + if (get_summary) + { + int ctx_id = 0; + List *path = NIL; + + /* Copy TopMemoryContext statistics to DSA */ + memset(&stat, 0, sizeof(stat)); + (*TopMemoryContext->methods->stats) (TopMemoryContext, NULL, NULL, + &stat, true); + path = lcons_int(1, path); + PublishMemoryContext(meminfo, ctx_id, TopMemoryContext, path, stat, + 1, area); + ctx_id = ctx_id + 1; + + /* + * Copy statistics for each of TopMemoryContexts children(XXX. Make it + * capped at 100). This includes statistics of all of their children + * upto level 100. + */ + + for (MemoryContext c = TopMemoryContext->firstchild; c != NULL; + c = c->nextchild) + { + MemoryContextCounters grand_totals; + int num_contexts = 0; + + path = NIL; + memset(&grand_totals, 0, sizeof(grand_totals)); + + MemoryContextStatsInternal(c, 0, 100, 100, &grand_totals, + PRINT_STATS_NONE, &num_contexts); + + path = compute_context_path(c, context_id_lookup); + + PublishMemoryContext(meminfo, ctx_id, c, path, + grand_totals, num_contexts, area); + ctx_id = ctx_id + 1; + } + memCtxState[idx].total_stats = ctx_id; + goto cleanup; + } + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + char *name; + + /* + * Figure out the transient context_id of this context and each of its + * ancestors. + */ + path = compute_context_path(cur, context_id_lookup); + + if (context_id <= (max_stats - 2)) + { + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1, area); + } + else + { + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + meminfo[max_stats - 1].totalspace += stat.totalspace; + meminfo[max_stats - 1].nblocks += stat.nblocks; + meminfo[max_stats - 1].freespace += stat.freespace; + meminfo[max_stats - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit is reached, write aggregate of the remaining + * statistics. + */ + if (context_id == (max_stats - 2) && context_id < (stats_count - 1)) + { + num_individual_stats = context_id + 1; + meminfo[max_stats - 1].name = dsa_allocate0(area, 17); + name = dsa_get_address(area, meminfo[max_stats - 1].name); + strncpy(name, "Remaining Totals", 16); + meminfo[max_stats - 1].ident = InvalidDsaPointer; + meminfo[max_stats - 1].path = InvalidDsaPointer; + meminfo[max_stats - 1].type = NULL; + } + context_id++; + } + /* No aggregated contexts, individual statistics reported */ + if (context_id < (max_stats - 2)) + { + memCtxState[idx].total_stats = context_id; + } + /* Report number of aggregated memory contexts */ + else + { + meminfo[max_stats - 1].num_agg_stats = context_id - + num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for cumulative + * statistics. + */ + memCtxState[idx].total_stats = num_individual_stats + 1; + } +cleanup: + + /* + * Signal all the waiting client backends after setting the exit condition + * flag + */ + memCtxState[idx].stats_timestamp = GetCurrentTimestamp(); + LWLockRelease(&memCtxState[idx].lw_lock); + ConditionVariableBroadcast(&memCtxState[idx].memctx_cv); + /* Delete the hash table memory context */ + MemoryContextDelete(stat_cxt); + + dsa_detach(area); +} + +/* + * Append the transient context_id of this context and each of + * its ancestors to a list, in order to compute a path. + */ +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + + for (MemoryContext cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + { + ereport(LOG, + errmsg("hash table corrupted, can't construct path value")); + break; + } + path = lcons_int(cur_entry->context_id, path); + } + return path; +} + +/* + * Return the number of contexts allocated currently by the backend + * Assign context ids to each of the contexts. + */ +static void +compute_contexts_count_and_ids(List *contexts, HTAB *context_id_lookup, + int *stats_count, bool get_summary) +{ + foreach_ptr(MemoryContextData, cur, contexts) + { + MemoryContextId *entry; + bool found; + + entry = (MemoryContextId *) hash_search(context_id_lookup, &cur, + HASH_ENTER, &found); + Assert(!found); + + /* context id starts with 1 */ + entry->context_id = (++(*stats_count)); + + /* Append the children of the current context to the main list. */ + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + { + if (get_summary) + { + entry = (MemoryContextId *) hash_search(context_id_lookup, &c, + HASH_ENTER, &found); + Assert(!found); + + entry->context_id = (++(*stats_count)); + } + + contexts = lappend(contexts, c); + } + + /* + * In summary only the first two level(from top) contexts are + * displayed + */ + if (get_summary) + break; + } + +} + +/* Copy the memory context statistics of a single context to a dsa memory */ +static void +PublishMemoryContext(MemoryContextEntry *memctx_info, int curr_id, + MemoryContext context, List *path, + MemoryContextCounters stat, int num_contexts, + dsa_area *area) +{ + char clipped_ident[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + char *name; + char *ident; + Datum *path_array; + + if (context->name != NULL) + { + Assert(strlen(context->name) < MEMORY_CONTEXT_IDENT_SHMEM_SIZE); + memctx_info[curr_id].name = dsa_allocate0(area, strlen(context->name) + 1); + name = (char *) dsa_get_address(area, memctx_info[curr_id].name); + strncpy(name, context->name, strlen(context->name)); + } + else + memctx_info[curr_id].name = InvalidDsaPointer; + + /* Trim and copy the identifier if it is not set to NULL */ + if (context->ident != NULL) + { + int idlen = strlen(context->ident); + + /* + * Some identifiers such as SQL query string can be very long, + * truncate oversize identifiers. + */ + if (idlen >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + idlen = pg_mbcliplen(context->ident, idlen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + memcpy(clipped_ident, context->ident, idlen); + clipped_ident[idlen] = '\0'; + + /* + * To be consistent with logging output, we label dynahash contexts + * with just the hash table name as with MemoryContextStatsPrint(). + */ + if (!strncmp(context->name, "dynahash", 8)) + { + dsa_free(area, memctx_info[curr_id].name); + memctx_info[curr_id].name = dsa_allocate0(area, + strlen(clipped_ident) + 1); + name = (char *) dsa_get_address(area, + memctx_info[curr_id].name); + strlcpy(name, + clipped_ident, idlen + 1); + memctx_info[curr_id].ident = InvalidDsaPointer; + } + else + { + + memctx_info[curr_id].ident = dsa_allocate0(area, + strlen(clipped_ident) + 1); + ident = (char *) dsa_get_address(area, + memctx_info[curr_id].ident); + strlcpy(ident, + clipped_ident, idlen + 1); + } + } + else + memctx_info[curr_id].ident = InvalidDsaPointer; + /* Allocate dsa memory for storing path information */ + if (path == NIL) + memctx_info[curr_id].path = InvalidDsaPointer; + else + { + memctx_info[curr_id].path_length = list_length(path); + memctx_info[curr_id].path = dsa_allocate0(area, + memctx_info[curr_id].path_length + * sizeof(Datum)); + path_array = (Datum *) dsa_get_address(area, memctx_info[curr_id].path); + foreach_int(i, path) + path_array[foreach_current_index(i)] = Int32GetDatum(i); + } + memctx_info[curr_id].type = ContextTypeToString(context->type); + memctx_info[curr_id].totalspace = stat.totalspace; + memctx_info[curr_id].nblocks = stat.nblocks; + memctx_info[curr_id].freespace = stat.freespace; + memctx_info[curr_id].freechunks = stat.freechunks; + memctx_info[curr_id].num_agg_stats = num_contexts; +} + +static void +dsa_free_previous_stats(dsa_area *area, int total_stats, + dsa_pointer prev_dsa_pointer) +{ + MemoryContextEntry *meminfo; + + meminfo = (MemoryContextEntry *) dsa_get_address(area, prev_dsa_pointer); + for (int i = 0; i < total_stats; i++) + { + if (DsaPointerIsValid(meminfo[i].name)) + dsa_free(area, meminfo[i].name); + + if (DsaPointerIsValid(meminfo[i].ident)) + dsa_free(area, meminfo[i].ident); + + if (DsaPointerIsValid(meminfo[i].path)) + dsa_free(area, meminfo[i].path); + } +} + +/* + * Free the memory context statistics stored by this process + * in dsa area. + */ +static void +AtProcExit_memctx_dsa_free(int code, Datum arg) +{ + int idx = MyProcNumber; + dsm_segment *dsm_seg = NULL; + dsa_area *area; + + if (memCtxArea->memstats_dsa_handle != DSA_HANDLE_INVALID) + dsm_seg = dsm_find_mapping(memCtxArea->memstats_dsa_handle); + else + return; + + if (dsm_seg == NULL) + area = dsa_attach(memCtxArea->memstats_dsa_handle); + else + area = (dsa_area *) DatumGetPointer(arg); + + LWLockAcquire(&memCtxState[idx].lw_lock, LW_EXCLUSIVE); + + if (DsaPointerIsValid(memCtxState[idx].memstats_dsa_pointer)) + { + /* + * Free the memory context statistics, free the name, ident and path + * pointers before freeing the pointer that contains these pointers + * and integer statistics. + */ + dsa_free_previous_stats(area, memCtxState[idx].total_stats, + memCtxState[idx].memstats_dsa_pointer); + dsa_free(area, memCtxState[idx].memstats_dsa_pointer); + memCtxState[idx].memstats_dsa_pointer = InvalidDsaPointer; + } + LWLockRelease(&memCtxState[idx].lw_lock); +} + void * palloc(Size size) { diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 42e427f8fe..a999206b07 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8506,6 +8506,16 @@ prorettype => 'bool', proargtypes => 'int4', prosrc => 'pg_log_backend_memory_contexts' }, +# publishing memory contexts of the specified postgres process +{ oid => '2173', descr => 'publish memory contexts of the specified backend', + proname => 'pg_get_process_memory_contexts', provolatile => 'v', + prorows => '100', proretset => 't', proparallel => 'r', + prorettype => 'record', proargtypes => 'int4 bool int4', + proallargtypes => '{int4,bool,int4,text,text,text,_int4,int4,int8,int8,int8,int8,int8,int4,timestamptz}', + proargmodes => '{i,i,i,o,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{oid, summary, num_of_tries, name, ident, type, path, level, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes, num_agg_contexts, stats_timestamp}', + prosrc => 'pg_get_process_memory_contexts' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index 6f16794eb6..63f74e3ce1 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -96,6 +96,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t PublishMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index 022fd8ed93..5d4b2fbfc9 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -35,6 +35,7 @@ typedef enum PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to send the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ /* Recovery conflict reasons */ diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 8abc26abce..a8d1956a82 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -18,6 +18,9 @@ #define MEMUTILS_H #include "nodes/memnodes.h" +#include "storage/condition_variable.h" +#include "storage/lmgr.h" +#include "utils/dsa.h" /* @@ -48,7 +51,10 @@ #define AllocHugeSizeIsValid(size) ((Size) (size) <= MaxAllocHugeSize) +#define MEMORY_CONTEXT_IDENT_SHMEM_SIZE 128 +#define MEM_CONTEXT_MAX_LEVEL 64 +#define MAX_NUM_DEFAULT_SEGMENTS 8 /* * Standard top-level memory contexts. * @@ -319,4 +325,67 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) return true; } +/* Dynamic shared memory state for statistics per context */ +typedef struct MemoryContextEntry +{ + dsa_pointer name; + dsa_pointer ident; + dsa_pointer path; + const char *type; + int path_length; + int64 totalspace; + int64 nblocks; + int64 freespace; + int64 freechunks; + int num_agg_stats; +} MemoryContextEntry; + +/* + * Static shared memory state representing the DSA area + * created for memory context statistics reporting. + * Single DSA area is created and used by all the processes, + * each having its specific dsa allocations for sharing memory + * statistics, tracked by per backend static shared memory state. + */ +typedef struct MemoryContextState +{ + dsa_handle memstats_dsa_handle; + LWLock lw_lock; +} MemoryContextState; + +/* + * Per backend static shared memory state for memory + * context statistics reporting. + */ +typedef struct MemoryContextBackendState +{ + ConditionVariable memctx_cv; + LWLock lw_lock; + int proc_id; + int total_stats; + bool get_summary; + dsa_pointer memstats_dsa_pointer; + TimestampTz stats_timestamp; +} MemoryContextBackendState; + + +/* + * MemoryContextId + * Used for storage of transient identifiers for + * pg_get_backend_memory_contexts. + */ +typedef struct MemoryContextId +{ + MemoryContext context; + int context_id; +} MemoryContextId; + +extern PGDLLIMPORT MemoryContextBackendState *memCtxState; +extern PGDLLIMPORT MemoryContextState *memCtxArea; +extern void ProcessGetMemoryContextInterrupt(void); +extern const char *ContextTypeToString(NodeTag type); +extern void HandleGetMemoryContextInterrupt(void); +extern void MemCtxShmemInit(void); +extern void MemCtxBackendShmemInit(void); + #endif /* MEMUTILS_H */ diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 83228cfca2..dca20ae1a2 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -232,3 +232,17 @@ select * from pg_timezone_abbrevs where abbrev = 'LMT'; LMT | @ 7 hours 52 mins 58 secs ago | f (1 row) +DO $$ +DECLARE + launcher_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='autovacuum launcher' + INTO launcher_pid; + + select type, name, ident + from pg_get_process_memory_contexts(launcher_pid, false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; +NOTICE: (AllocSet,TopMemoryContext,) diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 66179f026b..4767351d4e 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -101,3 +101,17 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs; -- One specific case we can check without much fear of breakage -- is the historical local-mean-time value used for America/Los_Angeles. select * from pg_timezone_abbrevs where abbrev = 'LMT'; + +DO $$ +DECLARE + launcher_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='autovacuum launcher' + INTO launcher_pid; + + select type, name, ident + from pg_get_process_memory_contexts(launcher_pid, false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 93339ef3c5..11e6dad561 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1643,12 +1643,16 @@ MemoizeState MemoizeTuple MemoryChunk MemoryContext +MemoryContextBackendState MemoryContextCallback MemoryContextCallbackFunction MemoryContextCounters MemoryContextData +MemoryContextEntry +MemoryContextId MemoryContextMethodID MemoryContextMethods +MemoryContextState MemoryStatsPrintFunc MergeAction MergeActionState -- 2.34.1 ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-03-15 08:40 Alexander Korotkov <[email protected]> parent: Rahila Syed <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Alexander Korotkov @ 2025-03-15 08:40 UTC (permalink / raw) To: Rahila Syed <[email protected]>; +Cc: Daniel Gustafsson <[email protected]>; Tomas Vondra <[email protected]>; pgsql-hackers Hi, Rahila! On Thu, Mar 13, 2025 at 3:57 PM Rahila Syed <[email protected]> wrote: > > Please find attached updated and rebased patches. It has the following changes > > 1. To prevent memory leaks, ensure that the latest statistics published by a process > are freed before it exits. This can be achieved by calling dsa_free in the > before_shmem_exit callback. > 2. Add a level column to maintain consistency with the output of > pg_backend_memory_contexts. Thank you for your work on this subject. v17-0001-Preparatory-changes-for-reporting-memory-context-sta.patch It looks like we're increasing *num_contexts twice per child memory context. First, it gets increased with a recursive MemoryContextStatsInternal() call, then by adding an ichild. I might be wrong, but I think these calculations at least deserve more comments. v17-0002-Function-to-report-memory-context-statistics.patch + if (procNumber == MyProcNumber) + { + ereport(WARNING, + errmsg("cannot return statistics for local backend"), + errhint("Use pg_backend_memory_contexts view instead.")); + PG_RETURN_NULL(); + } Is it worth it to keep this restriction? Can we fetch info about local memory context for the sake of generality? I know there have been discussions in the thread before, but the mechanism of publishing memory context stats via DSA looks quite complicated. Also, the user probably intends to inspect memory contexts when there is not a huge amount of free memory. So, failure is probable on DSA allocation. Could we do simpler? For instance, allocate some amount of static shared memory and use it as a message queue between processes. As a heavy load is not supposed to be here, I think one queue would be enough. ------ Regards, Alexander Korotkov Supabase ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-03-17 07:52 Rahila Syed <[email protected]> parent: Alexander Korotkov <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Rahila Syed @ 2025-03-17 07:52 UTC (permalink / raw) To: Alexander Korotkov <[email protected]>; +Cc: Daniel Gustafsson <[email protected]>; Tomas Vondra <[email protected]>; pgsql-hackers Hi Alexander, Thank you for the review. > It looks like we're increasing *num_contexts twice per child memory > context. First, it gets increased with a recursive > MemoryContextStatsInternal() call, then by adding an ichild. I might > be wrong, but I think these calculations at least deserve more > comments. > I believe that's not the case. The recursive calls only work for children encountered up to max_level and less than max_children per context. The rest of the children are handled using MemoryContextTraverseNext, without recursive calls. Thus, num_contexts is incremented for those children separately from the recursive call counter. I will add more comments around this. > v17-0002-Function-to-report-memory-context-statistics.patch > > + if (procNumber == MyProcNumber) > + { > + ereport(WARNING, > + errmsg("cannot return statistics for local backend"), > + errhint("Use pg_backend_memory_contexts view instead.")); > + PG_RETURN_NULL(); > + } > > Is it worth it to keep this restriction? Can we fetch info about > local memory context for the sake of generality? > > I think that could be done, but using pg_backend_memory_context would be more efficient in this case. > I know there have been discussions in the thread before, but the > mechanism of publishing memory context stats via DSA looks quite > complicated. Also, the user probably intends to inspect memory > contexts when there is not a huge amount of free memory. So, failure > is probable on DSA allocation. Could we do simpler? For instance, > allocate some amount of static shared memory and use it as a message > queue between processes. As a heavy load is not supposed to be here, > I think one queue would be enough. > > There could be other uses for such a function, such as a monitoring dashboard that periodically queries all running backends for memory statistics. If we use a single queue shared between all the backends, they will need to wait for the queue to become available before sharing their statistics, leading to processing delays at the publishing backend. Even with separate queues for each backend or without expecting concurrent use, publishing statistics could be delayed if a message queue is full. This is because a backend needs to wait for a client process to consume existing messages or statistics before publishing more. If a client process exits without consuming messages, the publishing backend will experience timeouts when trying to publish stats. This will impact backend performance as statistics are published during CHECK_FOR_INTERRUPTS. In the current implementation, the backend publishes all the statistics in one go without waiting for clients to read any statistics. In addition, allocating complete message queues in static shared memory can be expensive, especially since these static structures need to be created even if memory context statistics are never queried. On the contrary, a dsa is created for the feature whenever statistics are first queried. We are not preallocating shared memory for this feature, except for small structures to store the dsa_handle and dsa_pointers for each backend. Thank you, Rahila Syed ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-03-17 08:36 Ashutosh Bapat <[email protected]> parent: Rahila Syed <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Ashutosh Bapat @ 2025-03-17 08:36 UTC (permalink / raw) To: Rahila Syed <[email protected]>; +Cc: Alexander Korotkov <[email protected]>; Daniel Gustafsson <[email protected]>; Tomas Vondra <[email protected]>; pgsql-hackers On Mon, Mar 17, 2025 at 1:23 PM Rahila Syed <[email protected]> wrote: > >> >> v17-0002-Function-to-report-memory-context-statistics.patch >> >> + if (procNumber == MyProcNumber) >> + { >> + ereport(WARNING, >> + errmsg("cannot return statistics for local backend"), >> + errhint("Use pg_backend_memory_contexts view instead.")); >> + PG_RETURN_NULL(); >> + } >> >> Is it worth it to keep this restriction? Can we fetch info about >> local memory context for the sake of generality? >> > > I think that could be done, but using pg_backend_memory_context would > be more efficient in this case. > I have raised a similar concern before. Having two separate functions one for local backend and other for remote is going to be confusing. We should have one function doing both and renamed appropriately. -- Best Wishes, Ashutosh Bapat ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-03-20 07:39 Rahila Syed <[email protected]> parent: Ashutosh Bapat <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Rahila Syed @ 2025-03-20 07:39 UTC (permalink / raw) To: Ashutosh Bapat <[email protected]>; Daniel Gustafsson <[email protected]>; +Cc: Alexander Korotkov <[email protected]>; Tomas Vondra <[email protected]>; pgsql-hackers Hi, >> > >> + if (procNumber == MyProcNumber) > >> + { > >> + ereport(WARNING, > >> + errmsg("cannot return statistics for local backend"), > >> + errhint("Use pg_backend_memory_contexts view > instead.")); > >> + PG_RETURN_NULL(); > >> + } > >> > >> Is it worth it to keep this restriction? Can we fetch info about > >> local memory context for the sake of generality? > >> > > > > I think that could be done, but using pg_backend_memory_context would > > be more efficient in this case. > > > > I have raised a similar concern before. Having two separate functions > one for local backend and other for remote is going to be confusing. > We should have one function doing both and renamed appropriately. > > This is a separate concern from what has been raised by Alexander. He has suggested removing the restriction and fetching local backend statistics also with the proposed function. I've removed this restriction in the latest version of the patch. Now, the proposed function can be used to fetch local backend statistics too. Regarding your suggestion on merging these functions, although they both report memory context statistics, they differ in how they fetch these statistics—locally versus from dynamic shared memory. Additionally, the function signatures are different: the proposed function takes three arguments (pid, get_summary, and num_tries), whereas pg_get_backend_memory_contexts does not take any arguments. Therefore, I believe these functions can remain separate as long as we document their usages correctly. Please find attached rebased and updated patches. I have added some more comments and fixed an issue caused due to registering before_shmem_exit callback from interrupt processing routine. To address this issue, I am registering this callback in the InitProcess() function. This happened because interrupt processing could be triggered from a PG_ENSURE_ERROR_CLEANUP block. This block operates under the assumption that the before_shmem_exit callback registered at the beginning of the block, will be the last one in the registered callback list at the end of the block, which would not be the case if I register before_shmem_exit callback in the interrupt handling routine. Thank you, Rahila Syed Attachments: [application/octet-stream] v18-0002-Function-to-report-memory-context-statistics.patch (54.3K, ../../CAH2L28sMyRh_ZomRxkx_RdaQoLcyGAwKCr1TSmrVudbbR_Q1eQ@mail.gmail.com/3-v18-0002-Function-to-report-memory-context-statistics.patch) download | inline diff: From edcddb2f2c5fa9fb9f10f71c90283cc4c19fadbf Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Mon, 3 Feb 2025 15:37:17 +0530 Subject: [PATCH 2/2] Function to report memory context statistics This function sends a signal to a backend to publish statistics of all its memory contexts. Signal handler running in the backend process, sets a flag, which causes it to copy its MemoryContextStats to a DSA, during the next call to CHECK_FOR_INTERRUPTS(). If there are more statistics than that fit in 16MB, the remaining statistics are copied as a cumulative total of the remaining contexts. Once its done, it signals the client backend using a condition variable. The client backend wakes up, reads the shared memory and returns these values in the form of set of records, one for each memory context, to the user, followed by a cumulative total of the remaining contexts, if any. If get_summary is true return statistics of all children of TopMemoryContext with aggregated statistics of their children. User can pass num_of_tries which determines the total number of wait cycles in a client backend for latest statistics. Each cycle wait timeout is set to 1 seconds. Post this the client displays previously published statistics or returns without results. Each backend and auxiliary process has its own slot for reporting the stats. There is an array of such memory slots of size MaxBackends+NumofAuxiliary processes in fixed shared memory. Each of these slots point to a smaller dsa allocations within a single DSA, which contains the stats to be shared by the corresponding process. Each slot has its own LW lock and condition variable for synchronization and communication between the publishing process and the client backend. --- doc/src/sgml/func.sgml | 61 ++ src/backend/postmaster/autovacuum.c | 4 + src/backend/postmaster/checkpointer.c | 4 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 4 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 4 + src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/storage/lmgr/proc.c | 15 + src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 1 + src/backend/utils/adt/mcxtfuncs.c | 415 ++++++++++++- src/backend/utils/init/globals.c | 1 + src/backend/utils/mmgr/mcxt.c | 568 +++++++++++++++++- src/include/catalog/pg_proc.dat | 10 + src/include/miscadmin.h | 1 + src/include/storage/procsignal.h | 1 + src/include/utils/memutils.h | 70 +++ src/test/regress/expected/sysviews.out | 14 + src/test/regress/sql/sysviews.sql | 14 + src/tools/pgindent/typedefs.list | 4 + 22 files changed, 1167 insertions(+), 41 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 2ab5661602..5cb850de2e 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -28570,6 +28570,50 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres} </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_get_process_memory_contexts</primary> + </indexterm> + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type>, <parameter>get_summary</parameter> <type>boolean</type>, <parameter>num_of_tries</parameter> <type>integer</type> ) + <returnvalue>setof record</returnvalue> + </para> + <para> + This function handles requests to display the memory contexts of a + PostgreSQL process with the specified process ID (PID). It takes three + arguments: <parameter>PID</parameter>, <parameter>get_summary</parameter> + and <parameter>num_of_tries</parameter>. The function can send requests + to both backend and auxiliary processes. + + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context statistics + are aggregated and a cumulative total is displayed. The num_agg_contexts + column indicates the number of contexts aggregated in the displayed + statistics. The num_agg_contexts value is typically 1, meaning that each + context's statistics are displayed separately. + + When <parameter>get_summary</parameter> is set to true, statistics + for memory contexts at levels 1 and 2 are displayed, with level 1 + representing the root node (i.e., TopMemoryContext). + Each level 2 context's statistics represent an aggregate of all its + child contexts' statistics, with num_agg_contexts indicating the number + of these aggregated child contexts. + + When <parameter>get_summary</parameter> is set to false, the + num_agg_contexts value is 1, indicating that individual statistics are + being displayed. + + <parameter>num_of_tries</parameter> indicates the number of times + the client will wait for the latest statistics. The wait per try is 1 + second. This parameter can be increased if the user anticipates a delay + in the response from the reporting process. Conversely, if users are + frequently and periodically querying the process for statistics, or if + there are concurrent requests for statistics of the same process, + lowering the parameter might help achieve a faster response. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> @@ -28688,6 +28732,23 @@ LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 because it may generate a large number of log messages. </para> + <para> + <function>pg_get_process_memory_contexts</function> can be used + to request the memory contexts statistics of any postgres process. For example: +<programlisting> +postgres=# SELECT * FROM pg_get_process_memory_contexts( + (SELECT pid FROM pg_stat_activity + WHERE backend_type = 'checkpointer') + , false, 5) LIMIT 1; + name | ident | type | path | total_bytes | total_nblocks | free_bytes | free_chunks | used_bytes | num_ +agg_contexts | stats_timestamp +------------------+-------+----------+------+-------------+---------------+------------+-------------+------------+----- +-------------+---------------------------------- + TopMemoryContext | | AllocSet | {1} | 102664 | 4 | 3008 | 2 | 99656 | + 1 | 2025-03-04 10:01:57.590543+05:30 +</programlisting> + </para> + </sect2> <sect2 id="functions-admin-backup"> diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 2513a8ef8a..16756152b7 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -781,6 +781,10 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index fda91ffd1c..d3cb3f1891 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -663,6 +663,10 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index 0ae9bf906e..f24f574e74 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -48,6 +48,10 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index 7e622ae4bd..cb7408acf4 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -867,6 +867,10 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index 27e86cf393..7149a67fcb 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -192,6 +192,10 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index 0fec4f1f87..c7a76711cc 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -879,6 +879,10 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index 2fa045e6b0..362a6dc952 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -51,6 +51,7 @@ #include "storage/sinvaladt.h" #include "utils/guc.h" #include "utils/injection_point.h" +#include "utils/memutils.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -343,6 +344,8 @@ CreateOrAttachShmemStructs(void) WaitEventCustomShmemInit(); InjectionPointShmemInit(); AioShmemInit(); + MemCtxShmemInit(); + MemCtxBackendShmemInit(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index 7d20196550..b59034fdc3 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -690,6 +690,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_GET_MEMORY_CONTEXT)) + HandleGetMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index e4ca861a8e..c50dcbc491 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -50,6 +50,7 @@ #include "storage/procsignal.h" #include "storage/spin.h" #include "storage/standby.h" +#include "utils/memutils.h" #include "utils/timeout.h" #include "utils/timestamp.h" @@ -497,6 +498,13 @@ InitProcess(void) */ PGSemaphoreReset(MyProc->sem); + /* + * The before shmem exit callback frees the dsa memory occupied by the + * latest memory context statistics that could be published by this + * backend if requested. + */ + before_shmem_exit(AtProcExit_memstats_dsa_free, 0); + /* * Arrange to clean up at backend exit. */ @@ -671,6 +679,13 @@ InitAuxiliaryProcess(void) */ PGSemaphoreReset(MyProc->sem); + /* + * The before shmem exit callback frees the dsa memory occupied by the + * latest memory context statistics that could be published by this + * process if requested. + */ + before_shmem_exit(AtProcExit_memstats_dsa_free, 0); + /* * Arrange to clean up at process exit. */ diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 0554a4ae3c..b9ff50a929 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3507,6 +3507,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 9fa12a555e..e014e895bf 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -160,6 +160,7 @@ WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." +MEM_CTX_PUBLISH "Waiting for backend to publish memory information." ABI_compatibility: diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index 396c2f223b..462c4e48cf 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -17,28 +17,26 @@ #include "funcapi.h" #include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "access/twophase.h" +#include "catalog/pg_authid_d.h" +#include "nodes/pg_list.h" #include "storage/proc.h" #include "storage/procarray.h" +#include "utils/acl.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/memutils.h" +#include "utils/wait_event_types.h" /* ---------- * The max bytes for showing identifiers of MemoryContext. * ---------- */ #define MEMORY_CONTEXT_IDENT_DISPLAY_SIZE 1024 - -/* - * MemoryContextId - * Used for storage of transient identifiers for - * pg_get_backend_memory_contexts. - */ -typedef struct MemoryContextId -{ - MemoryContext context; - int context_id; -} MemoryContextId; +struct MemoryContextBackendState *memCtxState = NULL; +struct MemoryContextState *memCtxArea = NULL; /* * int_list_to_array @@ -143,24 +141,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, else nulls[1] = true; - switch (context->type) - { - case T_AllocSetContext: - type = "AllocSet"; - break; - case T_GenerationContext: - type = "Generation"; - break; - case T_SlabContext: - type = "Slab"; - break; - case T_BumpContext: - type = "Bump"; - break; - default: - type = "???"; - break; - } + type = ContextTypeToString(context->type); values[2] = CStringGetTextDatum(type); values[3] = Int32GetDatum(list_length(path)); /* level */ @@ -175,6 +156,32 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, list_free(path); } +const char * +ContextTypeToString(NodeTag type) +{ + const char *context_type; + + switch (type) + { + case T_AllocSetContext: + context_type = "AllocSet"; + break; + case T_GenerationContext: + context_type = "Generation"; + break; + case T_SlabContext: + context_type = "Slab"; + break; + case T_BumpContext: + context_type = "Bump"; + break; + default: + context_type = "???"; + break; + } + return (context_type); +} + /* * pg_get_backend_memory_contexts * SQL SRF showing backend memory context. @@ -305,3 +312,353 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } + +/* + * pg_get_process_memory_contexts + * Signal a backend or an auxiliary process to send its memory contexts, + * wait for the results and display them. + * + * By default, only superusers or users with PG_READ_ALL_STATS are allowed to + * signal a process to return the memory contexts. This is because allowing + * any users to issue this request at an unbounded rate would cause lots of + * requests to be sent, which can lead to denial of service. Additional roles + * can be permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to copy the memory context details + * to a dynamic shared memory space. + * + * We have defined a limit on dsa memory that could be allocated per process - + * if the process has more memory contexts than that can fit in the allocated + * size, the excess contexts are summarized and represented as cumulative total + * at the end of the buffer. + * + * After sending the signal, wait on a condition variable. The publishing + * backend, after copying the data to shared memory, sends signal on that + * condition variable. There is one condition variable per publishing + * backend. + * Once condition variable is signalled, check if the latest memory context + * information is available and display. + * + * If the publishing backend does not respond before the condition variable + * times out, which is set to MEMSTATS_WAIT_TIMEOUT, retry for max_tries + * number of times, which is defined by user, before giving up and + * returning previously published statistics, if any. If previous statistics + * do not exist, return NULL. + */ +Datum +pg_get_process_memory_contexts(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + bool get_summary = PG_GETARG_BOOL(1); + PGPROC *proc; + ProcNumber procNumber = INVALID_PROC_NUMBER; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + dsa_area *area; + MemoryContextEntry *memctx_info; + int num_retries = 0; + TimestampTz curr_timestamp; + int max_tries = PG_GETARG_INT32(2); + + /* + * Only superusers or users with pg_read_all_stats privileges can view the + * memory context statistics of another process + */ + if (!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("memory context statistics privilege error")); + + InitMaterializedSRF(fcinfo, 0); + + /* + * See if the process with given pid is a backend or an auxiliary process. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + proc = AuxiliaryPidGetProc(pid); + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; but by the time we reach kill(), a process for which we + * get a valid proc here might have terminated on its own. There's no way + * to acquire a lock on an arbitrary process to prevent that. But since + * this mechanism is usually used to debug a backend or an auxiliary + * process running and consuming lots of memory, that it might end on its + * own first and its memory contexts are not logged is not a problem. + */ + if (proc == NULL) + { + /* + * This is just a warning so a loop-through-resultset will not abort + * if one backend terminated on its own during the run. + */ + ereport(WARNING, + (errmsg("PID %d is not a PostgreSQL server process", + pid))); + PG_RETURN_NULL(); + } + + procNumber = GetNumberFromPGProc(proc); + + LWLockAcquire(&memCtxState[procNumber].lw_lock, LW_EXCLUSIVE); + memCtxState[procNumber].get_summary = get_summary; + LWLockRelease(&memCtxState[procNumber].lw_lock); + + curr_timestamp = GetCurrentTimestamp(); + + /* + * Send a signal to a postgresql process, informing it we want it to + * produce information about memory contexts. + */ + if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, procNumber) < 0) + { + ereport(WARNING, + errmsg("could not send signal to process %d: %m", pid)); + PG_RETURN_NULL(); + } + + /* + * A valid DSA pointer isn't proof that statistics are available, it can + * be valid due to previously published stats. Check if the stats are + * updated by comparing the timestamp, if the stats are newer than our + * previously recorded timestamp from before sending the procsignal, they + * must by definition be updated. Wait for max_tries * + * MEMSTATS_WAIT_TIMEOUT, following which display old statistics if + * available or return NULL. + */ + while (1) + { + long msecs; + + /* + * We expect to come out of sleep when the requested process has + * finished publishing the statistics, verified using the valid dsa + * pointer. + * + * Make sure that the information belongs to pid we requested + * information for, Otherwise loop back and wait for the server + * process to finish publishing statistics. + */ + LWLockAcquire(&memCtxState[procNumber].lw_lock, LW_EXCLUSIVE); + msecs = + TimestampDifferenceMilliseconds(curr_timestamp, + memCtxState[procNumber].stats_timestamp); + + /* + * Note in procnumber.h file says that a procNumber can be re-used for + * a different backend immediately after a backend exits. In case an + * old process' data was there and not updated by the current process + * in the slot identified by the procNumber, the pid of the requested + * process and the proc_id might not match. + */ + if (memCtxState[procNumber].proc_id == pid) + { + /* + * Break if the latest stats have been read, indicated by + * statistics timestamp being newer than the current request + * timestamp. + */ + if (DsaPointerIsValid(memCtxState[procNumber].memstats_dsa_pointer) + && msecs > 0) + break; + + } + LWLockRelease(&memCtxState[procNumber].lw_lock); + + /* + * Recheck the state of the backend before sleeping on the condition + * variable + */ + proc = BackendPidGetProc(pid); + +#define MEMSTATS_WAIT_TIMEOUT 1000 + if (proc == NULL) + proc = AuxiliaryPidGetProc(pid); + if (proc == NULL) + { + ereport(WARNING, + errmsg("PID %d is not a PostgreSQL server process", + pid)); + PG_RETURN_NULL(); + } + if (ConditionVariableTimedSleep(&memCtxState[procNumber].memctx_cv, + MEMSTATS_WAIT_TIMEOUT, + WAIT_EVENT_MEM_CTX_PUBLISH)) + { + /* + * Wait for max_tries defined by user, display previously + * published statistics if any, when max_tries are over. + */ + if (num_retries > max_tries) + { + LWLockAcquire(&memCtxState[procNumber].lw_lock, LW_EXCLUSIVE); + /* Displaying previously published statistics */ + if (DsaPointerIsValid(memCtxState[procNumber].memstats_dsa_pointer)) + break; + else + { + LWLockRelease(&memCtxState[procNumber].lw_lock); + PG_RETURN_NULL(); + } + } + ereport(LOG, + errmsg("Wait for %d process to publish stats timed out, trying again", + pid)); + num_retries = num_retries + 1; + } + + } + /* We should land here only with a valid DSA handle */ + Assert(memCtxArea->memstats_dsa_handle != DSA_HANDLE_INVALID); + area = dsa_attach(memCtxArea->memstats_dsa_handle); + + /* + * Backend has finished publishing the stats, read them + * + * Read statistics of top level 1 and 2 contexts, if get_summary is true. + */ + memctx_info = (MemoryContextEntry *) dsa_get_address(area, + memCtxState[procNumber].memstats_dsa_pointer); + +#define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 12 + for (int i = 0; i < memCtxState[procNumber].total_stats; i++) + { + ArrayType *path_array; + int path_length; + Datum values[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + bool nulls[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + char *name; + char *ident; + Datum *path_datum_array; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + if (DsaPointerIsValid(memctx_info[i].name)) + { + name = (char *) dsa_get_address(area, memctx_info[i].name); + values[0] = CStringGetTextDatum(name); + } + else + nulls[0] = true; + if (DsaPointerIsValid(memctx_info[i].ident)) + { + ident = (char *) dsa_get_address(area, memctx_info[i].ident); + values[1] = CStringGetTextDatum(ident); + } + else + nulls[1] = true; + + if (memctx_info[i].type != NULL) + values[2] = CStringGetTextDatum(memctx_info[i].type); + else + nulls[2] = true; + + path_length = memctx_info[i].path_length; + + if (DsaPointerIsValid(memctx_info[i].path)) + { + path_datum_array = (Datum *) dsa_get_address(area, memctx_info[i].path); + path_array = construct_array_builtin(path_datum_array, + path_length, INT4OID); + + values[3] = PointerGetDatum(path_array); + } + else + nulls[3] = true; + values[4] = Int32GetDatum(path_length); /* level */ + values[5] = Int64GetDatum(memctx_info[i].totalspace); + values[6] = Int64GetDatum(memctx_info[i].nblocks); + values[7] = Int64GetDatum(memctx_info[i].freespace); + values[8] = Int64GetDatum(memctx_info[i].freechunks); + values[9] = Int64GetDatum(memctx_info[i].totalspace - + memctx_info[i].freespace); + values[10] = Int32GetDatum(memctx_info[i].num_agg_stats); + values[11] = TimestampTzGetDatum(memCtxState[procNumber].stats_timestamp); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + LWLockRelease(&memCtxState[procNumber].lw_lock); + + ConditionVariableCancelSleep(); + dsa_detach(area); + + PG_RETURN_NULL(); +} + +/* + * Shared memory sizing for reporting memory context information. + */ +static Size +MemCtxShmemSize(void) +{ + Size TotalProcs = + add_size(MaxBackends, add_size(NUM_AUXILIARY_PROCS, max_prepared_xacts)); + + return mul_size(TotalProcs, sizeof(MemoryContextBackendState)); +} + +/* + * Init shared memory for reporting memory context information. + */ +void +MemCtxBackendShmemInit(void) +{ + bool found; + Size TotalProcs = + add_size(MaxBackends, add_size(NUM_AUXILIARY_PROCS, max_prepared_xacts)); + + memCtxState = (MemoryContextBackendState *) ShmemInitStruct("MemoryContextBackendState", + MemCtxShmemSize(), + &found); + if (!IsUnderPostmaster) + { + Assert(!found); + + for (int i = 0; i < TotalProcs; i++) + { + ConditionVariableInit(&memCtxState[i].memctx_cv); + + LWLockInitialize(&memCtxState[i].lw_lock, + LWLockNewTrancheId()); + LWLockRegisterTranche(memCtxState[i].lw_lock.tranche, + "mem_context_backend_stats_reporting"); + + memCtxState[i].memstats_dsa_pointer = InvalidDsaPointer; + } + } + else + { + Assert(found); + } +} + +/* + * Initialize shared memory for displaying memory + * context statistics + */ +void +MemCtxShmemInit(void) +{ + bool found; + + memCtxArea = (MemoryContextState *) ShmemInitStruct("MemoryContextState", sizeof(MemoryContextState), + &found); + if (!IsUnderPostmaster) + { + Assert(!found); + + LWLockInitialize(&memCtxArea->lw_lock, + LWLockNewTrancheId()); + LWLockRegisterTranche(memCtxArea->lw_lock.tranche, + "mem_context_stats_reporting"); + memCtxArea->memstats_dsa_handle = DSA_HANDLE_INVALID; + } + else + { + Assert(found); + } +} diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index b844f9fdae..13938ccb0f 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -38,6 +38,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t PublishMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 34cdcdf2fd..214330aa7a 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -19,10 +19,18 @@ *------------------------------------------------------------------------- */ +#include <math.h> #include "postgres.h" #include "mb/pg_wchar.h" #include "miscadmin.h" +#include "nodes/pg_list.h" +#include "storage/fd.h" +#include "storage/lwlock.h" +#include "storage/dsm.h" +#include "storage/ipc.h" +#include "utils/dsa.h" +#include "utils/hsearch.h" #include "utils/memdebug.h" #include "utils/memutils.h" #include "utils/memutils_internal.h" @@ -177,6 +185,16 @@ static void MemoryContextStatsInternal(MemoryContext context, int level, static void MemoryContextStatsPrint(MemoryContext context, void *passthru, const char *stats_string, bool print_to_stderr); +static void PublishMemoryContext(MemoryContextEntry *memctx_infos, + int curr_id, MemoryContext context, + List *path, + MemoryContextCounters stat, + int num_contexts, dsa_area *area); +static void compute_contexts_count_and_ids(List *contexts, HTAB *context_id_lookup, + int *stats_count, + bool get_summary); +static List *compute_context_path(MemoryContext c, HTAB *context_id_lookup); +static void dsa_free_previous_stats(dsa_area *area, int total_stats, dsa_pointer prev_dsa_pointer); /* * You should not do memory allocations within a critical section, because @@ -889,7 +907,8 @@ MemoryContextStatsDetail(MemoryContext context, * One recursion level for MemoryContextStats * * Print stats for this context if possible, but in any case accumulate counts - * into *totals (if not NULL). + * into *totals (if not NULL). The callers should make sure that print_location + * is set to PRINT_STATS_STDERR or PRINT_STATS_TO_LOGS or PRINT_STATS_NONE. */ static void MemoryContextStatsInternal(MemoryContext context, int level, @@ -899,38 +918,43 @@ MemoryContextStatsInternal(MemoryContext context, int level, { MemoryContext child; int ichild; - bool print_to_stderr = true; check_stack_depth(); Assert(MemoryContextIsValid(context)); if (print_location == PRINT_STATS_TO_STDERR) - print_to_stderr = true; + { + /* Examine the context itself */ + context->methods->stats(context, + MemoryContextStatsPrint, + &level, + totals, true); + } else if (print_location == PRINT_STATS_TO_LOGS) - print_to_stderr = false; - - if (print_location != PRINT_STATS_NONE) { + /* Examine the context itself */ context->methods->stats(context, MemoryContextStatsPrint, &level, - totals, print_to_stderr); + totals, false); } /* * Do not print the statistics if print_to_stderr is PRINT_STATS_NONE, - * only compute totals. + * only compute totals. This is used in reporting of memory context + * statistics via a sql function. Last parameter is not relevant. */ else { + Assert(print_location == PRINT_STATS_NONE); /* Examine the context itself */ context->methods->stats(context, NULL, NULL, - totals, print_to_stderr); + totals, false); } - /* Increment the context count */ + /* Increment the context count for each of the recursive call */ *num_contexts = *num_contexts + 1; /* @@ -969,9 +993,14 @@ MemoryContextStatsInternal(MemoryContext context, int level, ichild++; child = MemoryContextTraverseNext(child, context); } + + /* + * Add the count of children contexts which are traversed in the + * non-recursive manner. + */ *num_contexts = *num_contexts + ichild; - if (print_to_stderr) + if (print_location == PRINT_STATS_TO_STDERR) { for (int i = 0; i <= level; i++) fprintf(stderr, " "); @@ -984,7 +1013,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, local_totals.freechunks, local_totals.totalspace - local_totals.freespace); } - else if (print_location != PRINT_STATS_NONE) + else if (print_location == PRINT_STATS_TO_LOGS) ereport(LOG_SERVER_ONLY, (errhidestmt(true), errhidecontext(true), @@ -1321,6 +1350,21 @@ HandleLogMemoryContextInterrupt(void) /* latch will be set by procsignal_sigusr1_handler */ } +/* + * HandleGetMemoryContextInterrupt + * Handle receipt of an interrupt indicating publishing of memory + * contexts. + * + * All the actual work is deferred to ProcessLogMemoryContextInterrupt() + */ +void +HandleGetMemoryContextInterrupt(void) +{ + InterruptPending = true; + PublishMemoryContextPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + /* * ProcessLogMemoryContextInterrupt * Perform logging of memory contexts of this backend process. @@ -1358,6 +1402,506 @@ ProcessLogMemoryContextInterrupt(void) MemoryContextStatsDetail(TopMemoryContext, 100, 100, false); } +/* + * ProcessGetMemoryContextInterrupt + * Generate information about memory contexts used by the process. + * + * Performs a breadth first search on the memory context tree, thus parents + * statistics are reported before children in the monitoring function output. + * + * Statistics for all the processes are shared via the same dynamic shared area. + * + * We calculate maximum number of context's statistics that can be displayed + * using a pre-determined limit for memory available per process for this utility + * and the estimate of the size of statistics for each context. + * The remaining context statistics if any are captured as a cumulative total + * at the end of individual context's statistics. + * + * If get_summary is true, we display the level 1 and level 2 contexts. For that + * we traverse the memory context tree recursively in depth first search manner + * to cover all the children of a parent context, to be able to display a + * cumulative total of memory consumption by a parent at level 2. + */ +void +ProcessGetMemoryContextInterrupt(void) +{ + List *contexts; + + HASHCTL ctl; + HTAB *context_id_lookup; + int context_id = 0; + MemoryContext stat_cxt; + MemoryContextEntry *meminfo; + bool get_summary = false; + + dsa_area *area = NULL; + int max_stats; + int idx = MyProcNumber; + int stats_count = 0; + MemoryContextCounters stat; + int num_individual_stats = 0; + + PublishMemoryContextPending = false; + + /* + * Make a new context that will contain the hash table, to ease the + * cleanup. + */ + stat_cxt = AllocSetContextCreate(CurrentMemoryContext, + "Memory context statistics", + ALLOCSET_DEFAULT_SIZES); + + /* + * The hash table is used for constructing "path" column of the view, + * similar to its local backend counterpart. + */ + ctl.keysize = sizeof(MemoryContext); + ctl.entrysize = sizeof(MemoryContextId); + ctl.hcxt = stat_cxt; + + context_id_lookup = hash_create("pg_get_remote_backend_memory_contexts", + 256, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + /* List of contexts to process in the next round - start at the top. */ + contexts = list_make1(TopMemoryContext); + + /* Compute the number of stats that can fit in the defined limit */ + max_stats = (MAX_SEGMENTS_PER_BACKEND * DSA_DEFAULT_INIT_SEGMENT_SIZE) + / (sizeof(MemoryContextEntry) + (MEM_CONTEXT_MAX_LEVEL + * sizeof(Datum)) + (2 * MEMORY_CONTEXT_IDENT_SHMEM_SIZE)); + LWLockAcquire(&memCtxState[idx].lw_lock, LW_EXCLUSIVE); + get_summary = memCtxState[idx].get_summary; + LWLockRelease(&memCtxState[idx].lw_lock); + + /* + * Traverse the memory context tree to find total number of contexts. If + * summary is requested report the total number of contexts at level 1 and + * 2 from the top. Also, populate the hash table of context ids. + */ + compute_contexts_count_and_ids(contexts, context_id_lookup, &stats_count, + get_summary); + + /* + * Allocate memory in this process's dsa for storing statistics of the the + * memory contexts upto max_stats, for contexts that don't fit within a + * limit, a cumulative total is written as the last record in the DSA + * segment. + */ + stats_count = (stats_count > max_stats) ? max_stats : stats_count; + + LWLockAcquire(&memCtxArea->lw_lock, LW_EXCLUSIVE); + + /* + * Create a DSA and send handle to the the client process after storing + * the context statistics. If number of contexts exceed a predefined + * limit(8MB), a cumulative total is stored for such contexts. + */ + if (memCtxArea->memstats_dsa_handle == DSA_HANDLE_INVALID) + { + MemoryContext oldcontext = CurrentMemoryContext; + dsa_handle handle; + + MemoryContextSwitchTo(TopMemoryContext); + + area = dsa_create(memCtxArea->lw_lock.tranche); + + handle = dsa_get_handle(area); + MemoryContextSwitchTo(oldcontext); + + dsa_pin_mapping(area); + + /* + * Pin the dsa area, this is to make sure the area remains attachable + * even if current backend exits. This is done so that a waiting + * client gets the stats even after a process exits. + */ + dsa_pin(area); + + /* Set the handle in shared memory */ + memCtxArea->memstats_dsa_handle = handle; + } + + /* + * If DSA exists, created by another process publishing statistics, or by + * the previous execution of this function by this process, attach to it. + */ + else if (area == NULL) + { + MemoryContext oldcontext = CurrentMemoryContext; + + MemoryContextSwitchTo(TopMemoryContext); + area = dsa_attach(memCtxArea->memstats_dsa_handle); + MemoryContextSwitchTo(oldcontext); + dsa_pin_mapping(area); + } + LWLockRelease(&memCtxArea->lw_lock); + + /* + * Hold the process lock to protect writes to process specific memory. Two + * processes publishing statistics do not block each other. + */ + LWLockAcquire(&memCtxState[idx].lw_lock, LW_EXCLUSIVE); + memCtxState[idx].proc_id = MyProcPid; + + if (DsaPointerIsValid(memCtxState[idx].memstats_dsa_pointer)) + { + /* + * Free any previous allocations, free the name, ident and path + * pointers before freeing the pointer that contains them. + */ + dsa_free_previous_stats(area, memCtxState[idx].total_stats, + memCtxState[idx].memstats_dsa_pointer); + dsa_free(area, memCtxState[idx].memstats_dsa_pointer); + memCtxState[idx].memstats_dsa_pointer = InvalidDsaPointer; + } + memCtxState[idx].memstats_dsa_pointer = dsa_allocate0(area, + stats_count * sizeof(MemoryContextEntry)); + + meminfo = (MemoryContextEntry *) dsa_get_address(area, + memCtxState[idx].memstats_dsa_pointer); + + if (get_summary) + { + int ctx_id = 0; + List *path = NIL; + + /* Copy TopMemoryContext statistics to DSA */ + memset(&stat, 0, sizeof(stat)); + (*TopMemoryContext->methods->stats) (TopMemoryContext, NULL, NULL, + &stat, true); + path = lcons_int(1, path); + PublishMemoryContext(meminfo, ctx_id, TopMemoryContext, path, stat, + 1, area); + ctx_id = ctx_id + 1; + + /* + * Copy statistics for each of TopMemoryContexts children. This + * includes statistics of all of their children upto level 100. + */ + for (MemoryContext c = TopMemoryContext->firstchild; c != NULL; + c = c->nextchild) + { + MemoryContextCounters grand_totals; + int num_contexts = 0; + + path = NIL; + memset(&grand_totals, 0, sizeof(grand_totals)); + + MemoryContextStatsInternal(c, 0, 100, 100, &grand_totals, + PRINT_STATS_NONE, &num_contexts); + + path = compute_context_path(c, context_id_lookup); + + PublishMemoryContext(meminfo, ctx_id, c, path, + grand_totals, num_contexts, area); + ctx_id = ctx_id + 1; + } + memCtxState[idx].total_stats = ctx_id; + goto cleanup; + } + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + char *name; + + /* + * Figure out the transient context_id of this context and each of its + * ancestors, to compute a path for this context. + */ + path = compute_context_path(cur, context_id_lookup); + + if (context_id <= (max_stats - 2)) + { + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1, area); + } + else + { + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + meminfo[max_stats - 1].totalspace += stat.totalspace; + meminfo[max_stats - 1].nblocks += stat.nblocks; + meminfo[max_stats - 1].freespace += stat.freespace; + meminfo[max_stats - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit per process is reached, write aggregate of the + * remaining statistics. + */ + if (context_id == (max_stats - 2) && context_id < (stats_count - 1)) + { + num_individual_stats = context_id + 1; + meminfo[max_stats - 1].name = dsa_allocate0(area, 17); + name = dsa_get_address(area, meminfo[max_stats - 1].name); + strncpy(name, "Remaining Totals", 16); + meminfo[max_stats - 1].ident = InvalidDsaPointer; + meminfo[max_stats - 1].path = InvalidDsaPointer; + meminfo[max_stats - 1].type = NULL; + } + context_id++; + } + /* No aggregated contexts, individual statistics reported */ + if (context_id < (max_stats - 2)) + { + memCtxState[idx].total_stats = context_id; + } + /* Report number of aggregated memory contexts */ + else + { + meminfo[max_stats - 1].num_agg_stats = context_id - + num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for cumulative + * statistics. + */ + memCtxState[idx].total_stats = num_individual_stats + 1; + } +cleanup: + + /* + * Signal all the waiting client backends after copying all the statistics + */ + memCtxState[idx].stats_timestamp = GetCurrentTimestamp(); + LWLockRelease(&memCtxState[idx].lw_lock); + ConditionVariableBroadcast(&memCtxState[idx].memctx_cv); + /* Delete the hash table memory context */ + MemoryContextDelete(stat_cxt); + + dsa_detach(area); +} + +/* + * Append the transient context_id of this context and each of + * its ancestors to a list, in order to compute a path. + */ +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + + for (MemoryContext cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + { + ereport(LOG, + errmsg("hash table corrupted, can't construct path value")); + break; + } + path = lcons_int(cur_entry->context_id, path); + } + return path; +} + +/* + * Return the number of contexts allocated currently by the backend + * Assign context ids to each of the contexts. + */ +static void +compute_contexts_count_and_ids(List *contexts, HTAB *context_id_lookup, + int *stats_count, bool get_summary) +{ + foreach_ptr(MemoryContextData, cur, contexts) + { + MemoryContextId *entry; + bool found; + + entry = (MemoryContextId *) hash_search(context_id_lookup, &cur, + HASH_ENTER, &found); + Assert(!found); + + /* context id starts with 1 */ + entry->context_id = (++(*stats_count)); + + /* Append the children of the current context to the main list. */ + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + { + if (get_summary) + { + entry = (MemoryContextId *) hash_search(context_id_lookup, &c, + HASH_ENTER, &found); + Assert(!found); + + entry->context_id = (++(*stats_count)); + } + + contexts = lappend(contexts, c); + } + + /* + * In summary only the first two level(from top) contexts are + * displayed + */ + if (get_summary) + break; + } + +} + +/* Copy the memory context statistics of a single context to a dsa memory */ +static void +PublishMemoryContext(MemoryContextEntry *memctx_info, int curr_id, + MemoryContext context, List *path, + MemoryContextCounters stat, int num_contexts, + dsa_area *area) +{ + char clipped_ident[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + char *name; + char *ident; + Datum *path_array; + + if (context->name != NULL) + { + Assert(strlen(context->name) < MEMORY_CONTEXT_IDENT_SHMEM_SIZE); + memctx_info[curr_id].name = dsa_allocate0(area, strlen(context->name) + 1); + name = (char *) dsa_get_address(area, memctx_info[curr_id].name); + strncpy(name, context->name, strlen(context->name)); + } + else + memctx_info[curr_id].name = InvalidDsaPointer; + + /* Trim and copy the identifier if it is not set to NULL */ + if (context->ident != NULL) + { + int idlen = strlen(context->ident); + + /* + * Some identifiers such as SQL query string can be very long, + * truncate oversize identifiers. + */ + if (idlen >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + idlen = pg_mbcliplen(context->ident, idlen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + memcpy(clipped_ident, context->ident, idlen); + clipped_ident[idlen] = '\0'; + + /* + * To be consistent with logging output, we label dynahash contexts + * with just the hash table name as with MemoryContextStatsPrint(). + */ + if (!strncmp(context->name, "dynahash", 8)) + { + dsa_free(area, memctx_info[curr_id].name); + memctx_info[curr_id].name = dsa_allocate0(area, + strlen(clipped_ident) + 1); + name = (char *) dsa_get_address(area, + memctx_info[curr_id].name); + strlcpy(name, + clipped_ident, idlen + 1); + memctx_info[curr_id].ident = InvalidDsaPointer; + } + else + { + + memctx_info[curr_id].ident = dsa_allocate0(area, + strlen(clipped_ident) + 1); + ident = (char *) dsa_get_address(area, + memctx_info[curr_id].ident); + strlcpy(ident, + clipped_ident, idlen + 1); + } + } + else + memctx_info[curr_id].ident = InvalidDsaPointer; + /* Allocate dsa memory for storing path information */ + if (path == NIL) + memctx_info[curr_id].path = InvalidDsaPointer; + else + { + memctx_info[curr_id].path_length = list_length(path); + memctx_info[curr_id].path = dsa_allocate0(area, + memctx_info[curr_id].path_length + * sizeof(Datum)); + path_array = (Datum *) dsa_get_address(area, memctx_info[curr_id].path); + foreach_int(i, path) + path_array[foreach_current_index(i)] = Int32GetDatum(i); + } + memctx_info[curr_id].type = ContextTypeToString(context->type); + memctx_info[curr_id].totalspace = stat.totalspace; + memctx_info[curr_id].nblocks = stat.nblocks; + memctx_info[curr_id].freespace = stat.freespace; + memctx_info[curr_id].freechunks = stat.freechunks; + memctx_info[curr_id].num_agg_stats = num_contexts; +} + +static void +dsa_free_previous_stats(dsa_area *area, int total_stats, + dsa_pointer prev_dsa_pointer) +{ + MemoryContextEntry *meminfo; + + meminfo = (MemoryContextEntry *) dsa_get_address(area, prev_dsa_pointer); + for (int i = 0; i < total_stats; i++) + { + if (DsaPointerIsValid(meminfo[i].name)) + dsa_free(area, meminfo[i].name); + + if (DsaPointerIsValid(meminfo[i].ident)) + dsa_free(area, meminfo[i].ident); + + if (DsaPointerIsValid(meminfo[i].path)) + dsa_free(area, meminfo[i].path); + } +} + +/* + * Free the memory context statistics stored by this process + * in dsa area. + */ +void +AtProcExit_memstats_dsa_free(int code, Datum arg) +{ + int idx = MyProcNumber; + dsm_segment *dsm_seg = NULL; + dsa_area *area = NULL; + + if (memCtxArea->memstats_dsa_handle != DSA_HANDLE_INVALID) + dsm_seg = dsm_find_mapping(memCtxArea->memstats_dsa_handle); + else + return; + + LWLockAcquire(&memCtxState[idx].lw_lock, LW_EXCLUSIVE); + + if (!DsaPointerIsValid(memCtxState[idx].memstats_dsa_pointer)) + { + LWLockRelease(&memCtxState[idx].lw_lock); + return; + } + + /* If the dsm mapping could not be found, attach to the area */ + if (dsm_seg == NULL) + area = dsa_attach(memCtxArea->memstats_dsa_handle); + else + return; + + /* + * Free the memory context statistics, free the name, ident and path + * pointers before freeing the pointer that contains these pointers and + * integer statistics. + */ + dsa_free_previous_stats(area, memCtxState[idx].total_stats, + memCtxState[idx].memstats_dsa_pointer); + dsa_free(area, memCtxState[idx].memstats_dsa_pointer); + memCtxState[idx].memstats_dsa_pointer = InvalidDsaPointer; + + dsa_detach(area); + LWLockRelease(&memCtxState[idx].lw_lock); +} + void * palloc(Size size) { diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 890822eaf7..5dd9d920e8 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8509,6 +8509,16 @@ prorettype => 'bool', proargtypes => 'int4', prosrc => 'pg_log_backend_memory_contexts' }, +# publishing memory contexts of the specified postgres process +{ oid => '2173', descr => 'publish memory contexts of the specified backend', + proname => 'pg_get_process_memory_contexts', provolatile => 'v', + prorows => '100', proretset => 't', proparallel => 'r', + prorettype => 'record', proargtypes => 'int4 bool int4', + proallargtypes => '{int4,bool,int4,text,text,text,_int4,int4,int8,int8,int8,int8,int8,int4,timestamptz}', + proargmodes => '{i,i,i,o,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{oid, summary, num_of_tries, name, ident, type, path, level, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes, num_agg_contexts, stats_timestamp}', + prosrc => 'pg_get_process_memory_contexts' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index 603d042435..d3c44df6e1 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -96,6 +96,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t PublishMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index 022fd8ed93..5d4b2fbfc9 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -35,6 +35,7 @@ typedef enum PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to send the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ /* Recovery conflict reasons */ diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 8abc26abce..10fab7e580 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -18,6 +18,9 @@ #define MEMUTILS_H #include "nodes/memnodes.h" +#include "storage/condition_variable.h" +#include "storage/lmgr.h" +#include "utils/dsa.h" /* @@ -48,7 +51,10 @@ #define AllocHugeSizeIsValid(size) ((Size) (size) <= MaxAllocHugeSize) +#define MEMORY_CONTEXT_IDENT_SHMEM_SIZE 128 +#define MEM_CONTEXT_MAX_LEVEL 64 +#define MAX_SEGMENTS_PER_BACKEND 8 /* * Standard top-level memory contexts. * @@ -319,4 +325,68 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) return true; } +/* Dynamic shared memory state for statistics per context */ +typedef struct MemoryContextEntry +{ + dsa_pointer name; + dsa_pointer ident; + dsa_pointer path; + const char *type; + int path_length; + int64 totalspace; + int64 nblocks; + int64 freespace; + int64 freechunks; + int num_agg_stats; +} MemoryContextEntry; + +/* + * Static shared memory state representing the DSA area + * created for memory context statistics reporting. + * Single DSA area is created and used by all the processes, + * each having its specific dsa allocations for sharing memory + * statistics, tracked by per backend static shared memory state. + */ +typedef struct MemoryContextState +{ + dsa_handle memstats_dsa_handle; + LWLock lw_lock; +} MemoryContextState; + +/* + * Per backend static shared memory state for memory + * context statistics reporting. + */ +typedef struct MemoryContextBackendState +{ + ConditionVariable memctx_cv; + LWLock lw_lock; + int proc_id; + int total_stats; + bool get_summary; + dsa_pointer memstats_dsa_pointer; + TimestampTz stats_timestamp; +} MemoryContextBackendState; + + +/* + * MemoryContextId + * Used for storage of transient identifiers for + * pg_get_backend_memory_contexts. + */ +typedef struct MemoryContextId +{ + MemoryContext context; + int context_id; +} MemoryContextId; + +extern PGDLLIMPORT MemoryContextBackendState *memCtxState; +extern PGDLLIMPORT MemoryContextState *memCtxArea; +extern void ProcessGetMemoryContextInterrupt(void); +extern const char *ContextTypeToString(NodeTag type); +extern void HandleGetMemoryContextInterrupt(void); +extern void MemCtxShmemInit(void); +extern void MemCtxBackendShmemInit(void); +extern void AtProcExit_memstats_dsa_free(int code, Datum arg); + #endif /* MEMUTILS_H */ diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 83228cfca2..dca20ae1a2 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -232,3 +232,17 @@ select * from pg_timezone_abbrevs where abbrev = 'LMT'; LMT | @ 7 hours 52 mins 58 secs ago | f (1 row) +DO $$ +DECLARE + launcher_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='autovacuum launcher' + INTO launcher_pid; + + select type, name, ident + from pg_get_process_memory_contexts(launcher_pid, false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; +NOTICE: (AllocSet,TopMemoryContext,) diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 66179f026b..4767351d4e 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -101,3 +101,17 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs; -- One specific case we can check without much fear of breakage -- is the historical local-mean-time value used for America/Los_Angeles. select * from pg_timezone_abbrevs where abbrev = 'LMT'; + +DO $$ +DECLARE + launcher_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='autovacuum launcher' + INTO launcher_pid; + + select type, name, ident + from pg_get_process_memory_contexts(launcher_pid, false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index bfa276d2d3..78f5f1ec09 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1648,12 +1648,16 @@ MemoizeState MemoizeTuple MemoryChunk MemoryContext +MemoryContextBackendState MemoryContextCallback MemoryContextCallbackFunction MemoryContextCounters MemoryContextData +MemoryContextEntry +MemoryContextId MemoryContextMethodID MemoryContextMethods +MemoryContextState MemoryStatsPrintFunc MergeAction MergeActionState -- 2.34.1 [application/octet-stream] v18-0001-Preparatory-changes-for-reporting-memory-context-sta.patch (4.3K, ../../CAH2L28sMyRh_ZomRxkx_RdaQoLcyGAwKCr1TSmrVudbbR_Q1eQ@mail.gmail.com/4-v18-0001-Preparatory-changes-for-reporting-memory-context-sta.patch) download | inline diff: From db2c1f522305b345038a8bcb9f73a9a49b044af9 Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Mon, 3 Feb 2025 15:33:19 +0530 Subject: [PATCH 1/2] Preparatory changes for reporting memory context statistics Ensure that MemoryContextStatsInternal can return number of contexts. Also, provide an option in MemoryContextStatsInternal to return without printing stats to either stderr or logs. --- src/backend/utils/mmgr/mcxt.c | 65 +++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 91060de0ab..34cdcdf2fd 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -135,6 +135,17 @@ static const MemoryContextMethods mcxt_methods[] = { }; #undef BOGUS_MCTX +/* + * This is passed to MemoryContextStatsInternal to determine whether + * to print context statistics or not and where to print them logs or + * stderr. + */ +typedef enum PrintDestination +{ + PRINT_STATS_TO_STDERR = 0, + PRINT_STATS_TO_LOGS, + PRINT_STATS_NONE +} PrintDestination; /* * CurrentMemoryContext @@ -162,7 +173,7 @@ static void MemoryContextCallResetCallbacks(MemoryContext context); static void MemoryContextStatsInternal(MemoryContext context, int level, int max_level, int max_children, MemoryContextCounters *totals, - bool print_to_stderr); + PrintDestination print_location, int *num_contexts); static void MemoryContextStatsPrint(MemoryContext context, void *passthru, const char *stats_string, bool print_to_stderr); @@ -831,11 +842,19 @@ MemoryContextStatsDetail(MemoryContext context, bool print_to_stderr) { MemoryContextCounters grand_totals; + int num_contexts; + PrintDestination print_location; memset(&grand_totals, 0, sizeof(grand_totals)); + if (print_to_stderr) + print_location = PRINT_STATS_TO_STDERR; + else + print_location = PRINT_STATS_TO_LOGS; + + /* num_contexts report number of contexts aggregated in the output */ MemoryContextStatsInternal(context, 0, max_level, max_children, - &grand_totals, print_to_stderr); + &grand_totals, print_location, &num_contexts); if (print_to_stderr) fprintf(stderr, @@ -876,18 +895,43 @@ static void MemoryContextStatsInternal(MemoryContext context, int level, int max_level, int max_children, MemoryContextCounters *totals, - bool print_to_stderr) + PrintDestination print_location, int *num_contexts) { MemoryContext child; int ichild; + bool print_to_stderr = true; + check_stack_depth(); Assert(MemoryContextIsValid(context)); - /* Examine the context itself */ - context->methods->stats(context, - MemoryContextStatsPrint, - &level, - totals, print_to_stderr); + if (print_location == PRINT_STATS_TO_STDERR) + print_to_stderr = true; + else if (print_location == PRINT_STATS_TO_LOGS) + print_to_stderr = false; + + if (print_location != PRINT_STATS_NONE) + { + /* Examine the context itself */ + context->methods->stats(context, + MemoryContextStatsPrint, + &level, + totals, print_to_stderr); + } + + /* + * Do not print the statistics if print_to_stderr is PRINT_STATS_NONE, + * only compute totals. + */ + else + { + /* Examine the context itself */ + context->methods->stats(context, + NULL, + NULL, + totals, print_to_stderr); + } + /* Increment the context count */ + *num_contexts = *num_contexts + 1; /* * Examine children. @@ -907,7 +951,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, MemoryContextStatsInternal(child, level + 1, max_level, max_children, totals, - print_to_stderr); + print_location, num_contexts); } } @@ -925,6 +969,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, ichild++; child = MemoryContextTraverseNext(child, context); } + *num_contexts = *num_contexts + ichild; if (print_to_stderr) { @@ -939,7 +984,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, local_totals.freechunks, local_totals.totalspace - local_totals.freespace); } - else + else if (print_location != PRINT_STATS_NONE) ereport(LOG_SERVER_ONLY, (errhidestmt(true), errhidecontext(true), -- 2.34.1 ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-03-25 14:14 Daniel Gustafsson <[email protected]> parent: Rahila Syed <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Daniel Gustafsson @ 2025-03-25 14:14 UTC (permalink / raw) To: Rahila Syed <[email protected]>; +Cc: Ashutosh Bapat <[email protected]>; Alexander Korotkov <[email protected]>; Tomas Vondra <[email protected]>; pgsql-hackers > On 20 Mar 2025, at 08:39, Rahila Syed <[email protected]> wrote: Thanks for the new version, I believe this will be a welcome tool in the debugging toolbox. I took a cleanup pass over the docs with among others the below changes: * You had broken the text in paragraphs, but without <para/> tags they are rendered as a single blob of text so added that. * Removed the "(PID)" explanation as process id is used elsewhere on the same page already without explanation. * Added <productname/> markup on PostgreSQL * Added <literal/> markup on paramater values * Switched the example query output to use \x * Added a note on when pg_backend_memory_contexts is a better choice The paragraphs need some re-indenting to avoid too long lines, but I opted out of doing so here to make reviewing the changes easier. A few comments on the code (all comments are performed in 0003 attached here which also has smaller cleanups wrt indentation, code style etc): +#include <math.h> I don't think we need this, maybe it was from an earlier version of the patch? +MEM_CTX_PUBLISH "Waiting for backend to publish memory information." I wonder if this should really be "process" and not backend? + default: + context_type = "???"; + break; In ContextTypeToString() I'm having second thoughts about this, there shouldn't be any legitimate use-case of passing a nodetag this function which would fail MemoryContextIsValid(). I wonder if we aren't helping callers more by erroring out rather than silently returning an unknown? I haven't changed this but maybe we should to set the API right from the start? + * if the process has more memory contexts than that can fit in the allocated s/than that can/than what can/? + errmsg("memory context statistics privilege error")); Similar checks elsewhere in the tree mostly use "permission denied to .." so I think we should adopt that here as well. + LWLockAcquire(&memCtxState[procNumber].lw_lock, LW_EXCLUSIVE); + msecs = + TimestampDifferenceMilliseconds(curr_timestamp, + memCtxState[procNumber].stats_timestamp); Since we only want to consider the stats if they are from the current process, we can delay checking the time difference until after we've checked the pid and thus reduce the amount of time we hold the lock in the error case. + /* + * Recheck the state of the backend before sleeping on the condition + * variable + */ + proc = BackendPidGetProc(pid); Here we are really rechecking that the process is still alive, but I wonder if we should take the opportunity to ensure that the type is what we expect it to be? If the pid has moved from being a backend to an aux proc or vice versa we really don't want to go on. + ereport(WARNING, + errmsg("PID %d is not a PostgreSQL server process", + pid)); I wonder if we should differentiate between the warnings? When we hit this in the loop the errmsg is describing a slightly different case. I did leave it for now, but it's food for thought if we should perhaps reword this one. + ereport(LOG, + errmsg("Wait for %d process to publish stats timed out, trying again", + pid)); This should probably by DEBUG1, in a congested cluster it might cause a fair bit of logging which isn't really helping the user. Also, nitpick, errmsg starts with a lowercase letter. +static Size +MemCtxShmemSize(void) We don't really need this function anymore and keeping it separate we risk it going out of sync with the matching calcuation in MemCtxBackendShmemInit, so I think we should condense into one. else { + Assert(print_location == PRINT_STATS_NONE); Rather than an if-then-else and an assert we can use a switch statement without a default, this way we'll automatically get a warning if a value is missed. + ereport(LOG, + errmsg("hash table corrupted, can't construct path value")); I know you switched from elog(LOG.. to ereport(LOG.. but I still think a LOG entry stating corruption isn't helpful, it's not actionable for the user. Given that it's a case that shouldn't happen I wonder if we should downgrade it to an Assert(false) and potentially a DEBUG1? -- Daniel Gustafsson Attachments: [application/octet-stream] v19-0001-Preparatory-changes-for-reporting-memory-context.patch (4.3K, ../../[email protected]/2-v19-0001-Preparatory-changes-for-reporting-memory-context.patch) download | inline diff: From ba6368a9db0e1f420897cca302ed2a80c94f2ca1 Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Mon, 3 Feb 2025 15:33:19 +0530 Subject: [PATCH v19 1/3] Preparatory changes for reporting memory context statistics Ensure that MemoryContextStatsInternal can return number of contexts. Also, provide an option in MemoryContextStatsInternal to return without printing stats to either stderr or logs. --- src/backend/utils/mmgr/mcxt.c | 65 +++++++++++++++++++++++++++++------ 1 file changed, 55 insertions(+), 10 deletions(-) diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 91060de0ab7..34cdcdf2fd3 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -135,6 +135,17 @@ static const MemoryContextMethods mcxt_methods[] = { }; #undef BOGUS_MCTX +/* + * This is passed to MemoryContextStatsInternal to determine whether + * to print context statistics or not and where to print them logs or + * stderr. + */ +typedef enum PrintDestination +{ + PRINT_STATS_TO_STDERR = 0, + PRINT_STATS_TO_LOGS, + PRINT_STATS_NONE +} PrintDestination; /* * CurrentMemoryContext @@ -162,7 +173,7 @@ static void MemoryContextCallResetCallbacks(MemoryContext context); static void MemoryContextStatsInternal(MemoryContext context, int level, int max_level, int max_children, MemoryContextCounters *totals, - bool print_to_stderr); + PrintDestination print_location, int *num_contexts); static void MemoryContextStatsPrint(MemoryContext context, void *passthru, const char *stats_string, bool print_to_stderr); @@ -831,11 +842,19 @@ MemoryContextStatsDetail(MemoryContext context, bool print_to_stderr) { MemoryContextCounters grand_totals; + int num_contexts; + PrintDestination print_location; memset(&grand_totals, 0, sizeof(grand_totals)); + if (print_to_stderr) + print_location = PRINT_STATS_TO_STDERR; + else + print_location = PRINT_STATS_TO_LOGS; + + /* num_contexts report number of contexts aggregated in the output */ MemoryContextStatsInternal(context, 0, max_level, max_children, - &grand_totals, print_to_stderr); + &grand_totals, print_location, &num_contexts); if (print_to_stderr) fprintf(stderr, @@ -876,18 +895,43 @@ static void MemoryContextStatsInternal(MemoryContext context, int level, int max_level, int max_children, MemoryContextCounters *totals, - bool print_to_stderr) + PrintDestination print_location, int *num_contexts) { MemoryContext child; int ichild; + bool print_to_stderr = true; + check_stack_depth(); Assert(MemoryContextIsValid(context)); - /* Examine the context itself */ - context->methods->stats(context, - MemoryContextStatsPrint, - &level, - totals, print_to_stderr); + if (print_location == PRINT_STATS_TO_STDERR) + print_to_stderr = true; + else if (print_location == PRINT_STATS_TO_LOGS) + print_to_stderr = false; + + if (print_location != PRINT_STATS_NONE) + { + /* Examine the context itself */ + context->methods->stats(context, + MemoryContextStatsPrint, + &level, + totals, print_to_stderr); + } + + /* + * Do not print the statistics if print_to_stderr is PRINT_STATS_NONE, + * only compute totals. + */ + else + { + /* Examine the context itself */ + context->methods->stats(context, + NULL, + NULL, + totals, print_to_stderr); + } + /* Increment the context count */ + *num_contexts = *num_contexts + 1; /* * Examine children. @@ -907,7 +951,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, MemoryContextStatsInternal(child, level + 1, max_level, max_children, totals, - print_to_stderr); + print_location, num_contexts); } } @@ -925,6 +969,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, ichild++; child = MemoryContextTraverseNext(child, context); } + *num_contexts = *num_contexts + ichild; if (print_to_stderr) { @@ -939,7 +984,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, local_totals.freechunks, local_totals.totalspace - local_totals.freespace); } - else + else if (print_location != PRINT_STATS_NONE) ereport(LOG_SERVER_ONLY, (errhidestmt(true), errhidecontext(true), -- 2.39.3 (Apple Git-146) [application/octet-stream] v19-0002-Function-to-report-memory-context-statistics.patch (54.3K, ../../[email protected]/3-v19-0002-Function-to-report-memory-context-statistics.patch) download | inline diff: From 991c7404073c4dfa4e2dd55e6f169ec2c9868ee4 Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Mon, 3 Feb 2025 15:37:17 +0530 Subject: [PATCH v19 2/3] Function to report memory context statistics This function sends a signal to a backend to publish statistics of all its memory contexts. Signal handler running in the backend process, sets a flag, which causes it to copy its MemoryContextStats to a DSA, during the next call to CHECK_FOR_INTERRUPTS(). If there are more statistics than that fit in 16MB, the remaining statistics are copied as a cumulative total of the remaining contexts. Once its done, it signals the client backend using a condition variable. The client backend wakes up, reads the shared memory and returns these values in the form of set of records, one for each memory context, to the user, followed by a cumulative total of the remaining contexts, if any. If get_summary is true return statistics of all children of TopMemoryContext with aggregated statistics of their children. User can pass num_of_tries which determines the total number of wait cycles in a client backend for latest statistics. Each cycle wait timeout is set to 1 seconds. Post this the client displays previously published statistics or returns without results. Each backend and auxiliary process has its own slot for reporting the stats. There is an array of such memory slots of size MaxBackends+NumofAuxiliary processes in fixed shared memory. Each of these slots point to a smaller dsa allocations within a single DSA, which contains the stats to be shared by the corresponding process. Each slot has its own LW lock and condition variable for synchronization and communication between the publishing process and the client backend. --- doc/src/sgml/func.sgml | 61 ++ src/backend/postmaster/autovacuum.c | 4 + src/backend/postmaster/checkpointer.c | 4 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 4 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 4 + src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/storage/lmgr/proc.c | 15 + src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 1 + src/backend/utils/adt/mcxtfuncs.c | 415 ++++++++++++- src/backend/utils/init/globals.c | 1 + src/backend/utils/mmgr/mcxt.c | 568 +++++++++++++++++- src/include/catalog/pg_proc.dat | 10 + src/include/miscadmin.h | 1 + src/include/storage/procsignal.h | 1 + src/include/utils/memutils.h | 70 +++ src/test/regress/expected/sysviews.out | 14 + src/test/regress/sql/sysviews.sql | 14 + src/tools/pgindent/typedefs.list | 4 + 22 files changed, 1167 insertions(+), 41 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 6fa1d6586b8..1ab2ce12662 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -28570,6 +28570,50 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres} </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_get_process_memory_contexts</primary> + </indexterm> + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type>, <parameter>get_summary</parameter> <type>boolean</type>, <parameter>num_of_tries</parameter> <type>integer</type> ) + <returnvalue>setof record</returnvalue> + </para> + <para> + This function handles requests to display the memory contexts of a + PostgreSQL process with the specified process ID (PID). It takes three + arguments: <parameter>PID</parameter>, <parameter>get_summary</parameter> + and <parameter>num_of_tries</parameter>. The function can send requests + to both backend and auxiliary processes. + + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context statistics + are aggregated and a cumulative total is displayed. The num_agg_contexts + column indicates the number of contexts aggregated in the displayed + statistics. The num_agg_contexts value is typically 1, meaning that each + context's statistics are displayed separately. + + When <parameter>get_summary</parameter> is set to true, statistics + for memory contexts at levels 1 and 2 are displayed, with level 1 + representing the root node (i.e., TopMemoryContext). + Each level 2 context's statistics represent an aggregate of all its + child contexts' statistics, with num_agg_contexts indicating the number + of these aggregated child contexts. + + When <parameter>get_summary</parameter> is set to false, the + num_agg_contexts value is 1, indicating that individual statistics are + being displayed. + + <parameter>num_of_tries</parameter> indicates the number of times + the client will wait for the latest statistics. The wait per try is 1 + second. This parameter can be increased if the user anticipates a delay + in the response from the reporting process. Conversely, if users are + frequently and periodically querying the process for statistics, or if + there are concurrent requests for statistics of the same process, + lowering the parameter might help achieve a faster response. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> @@ -28688,6 +28732,23 @@ LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 because it may generate a large number of log messages. </para> + <para> + <function>pg_get_process_memory_contexts</function> can be used + to request the memory contexts statistics of any postgres process. For example: +<programlisting> +postgres=# SELECT * FROM pg_get_process_memory_contexts( + (SELECT pid FROM pg_stat_activity + WHERE backend_type = 'checkpointer') + , false, 5) LIMIT 1; + name | ident | type | path | total_bytes | total_nblocks | free_bytes | free_chunks | used_bytes | num_ +agg_contexts | stats_timestamp +------------------+-------+----------+------+-------------+---------------+------------+-------------+------------+----- +-------------+---------------------------------- + TopMemoryContext | | AllocSet | {1} | 102664 | 4 | 3008 | 2 | 99656 | + 1 | 2025-03-04 10:01:57.590543+05:30 +</programlisting> + </para> + </sect2> <sect2 id="functions-admin-backup"> diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 2513a8ef8a6..16756152b71 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -781,6 +781,10 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index fda91ffd1ce..d3cb3f1891c 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -663,6 +663,10 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index 0ae9bf906ec..f24f574e748 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -48,6 +48,10 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index 7e622ae4bd2..cb7408acf4c 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -867,6 +867,10 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index 27e86cf393f..7149a67fcbc 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -192,6 +192,10 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index 0fec4f1f871..c7a76711cc5 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -879,6 +879,10 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index 2fa045e6b0f..362a6dc9528 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -51,6 +51,7 @@ #include "storage/sinvaladt.h" #include "utils/guc.h" #include "utils/injection_point.h" +#include "utils/memutils.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -343,6 +344,8 @@ CreateOrAttachShmemStructs(void) WaitEventCustomShmemInit(); InjectionPointShmemInit(); AioShmemInit(); + MemCtxShmemInit(); + MemCtxBackendShmemInit(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index 7d201965503..b59034fdc38 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -690,6 +690,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_GET_MEMORY_CONTEXT)) + HandleGetMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index e4ca861a8e6..c50dcbc4914 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -50,6 +50,7 @@ #include "storage/procsignal.h" #include "storage/spin.h" #include "storage/standby.h" +#include "utils/memutils.h" #include "utils/timeout.h" #include "utils/timestamp.h" @@ -497,6 +498,13 @@ InitProcess(void) */ PGSemaphoreReset(MyProc->sem); + /* + * The before shmem exit callback frees the dsa memory occupied by the + * latest memory context statistics that could be published by this + * backend if requested. + */ + before_shmem_exit(AtProcExit_memstats_dsa_free, 0); + /* * Arrange to clean up at backend exit. */ @@ -671,6 +679,13 @@ InitAuxiliaryProcess(void) */ PGSemaphoreReset(MyProc->sem); + /* + * The before shmem exit callback frees the dsa memory occupied by the + * latest memory context statistics that could be published by this + * process if requested. + */ + before_shmem_exit(AtProcExit_memstats_dsa_free, 0); + /* * Arrange to clean up at process exit. */ diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 4d2edb10658..08d17a19316 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3531,6 +3531,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 9fa12a555e8..e014e895bfb 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -160,6 +160,7 @@ WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." +MEM_CTX_PUBLISH "Waiting for backend to publish memory information." ABI_compatibility: diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index 396c2f223b4..462c4e48cf0 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -17,28 +17,26 @@ #include "funcapi.h" #include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "access/twophase.h" +#include "catalog/pg_authid_d.h" +#include "nodes/pg_list.h" #include "storage/proc.h" #include "storage/procarray.h" +#include "utils/acl.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/memutils.h" +#include "utils/wait_event_types.h" /* ---------- * The max bytes for showing identifiers of MemoryContext. * ---------- */ #define MEMORY_CONTEXT_IDENT_DISPLAY_SIZE 1024 - -/* - * MemoryContextId - * Used for storage of transient identifiers for - * pg_get_backend_memory_contexts. - */ -typedef struct MemoryContextId -{ - MemoryContext context; - int context_id; -} MemoryContextId; +struct MemoryContextBackendState *memCtxState = NULL; +struct MemoryContextState *memCtxArea = NULL; /* * int_list_to_array @@ -143,24 +141,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, else nulls[1] = true; - switch (context->type) - { - case T_AllocSetContext: - type = "AllocSet"; - break; - case T_GenerationContext: - type = "Generation"; - break; - case T_SlabContext: - type = "Slab"; - break; - case T_BumpContext: - type = "Bump"; - break; - default: - type = "???"; - break; - } + type = ContextTypeToString(context->type); values[2] = CStringGetTextDatum(type); values[3] = Int32GetDatum(list_length(path)); /* level */ @@ -175,6 +156,32 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, list_free(path); } +const char * +ContextTypeToString(NodeTag type) +{ + const char *context_type; + + switch (type) + { + case T_AllocSetContext: + context_type = "AllocSet"; + break; + case T_GenerationContext: + context_type = "Generation"; + break; + case T_SlabContext: + context_type = "Slab"; + break; + case T_BumpContext: + context_type = "Bump"; + break; + default: + context_type = "???"; + break; + } + return (context_type); +} + /* * pg_get_backend_memory_contexts * SQL SRF showing backend memory context. @@ -305,3 +312,353 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } + +/* + * pg_get_process_memory_contexts + * Signal a backend or an auxiliary process to send its memory contexts, + * wait for the results and display them. + * + * By default, only superusers or users with PG_READ_ALL_STATS are allowed to + * signal a process to return the memory contexts. This is because allowing + * any users to issue this request at an unbounded rate would cause lots of + * requests to be sent, which can lead to denial of service. Additional roles + * can be permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to copy the memory context details + * to a dynamic shared memory space. + * + * We have defined a limit on dsa memory that could be allocated per process - + * if the process has more memory contexts than that can fit in the allocated + * size, the excess contexts are summarized and represented as cumulative total + * at the end of the buffer. + * + * After sending the signal, wait on a condition variable. The publishing + * backend, after copying the data to shared memory, sends signal on that + * condition variable. There is one condition variable per publishing + * backend. + * Once condition variable is signalled, check if the latest memory context + * information is available and display. + * + * If the publishing backend does not respond before the condition variable + * times out, which is set to MEMSTATS_WAIT_TIMEOUT, retry for max_tries + * number of times, which is defined by user, before giving up and + * returning previously published statistics, if any. If previous statistics + * do not exist, return NULL. + */ +Datum +pg_get_process_memory_contexts(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + bool get_summary = PG_GETARG_BOOL(1); + PGPROC *proc; + ProcNumber procNumber = INVALID_PROC_NUMBER; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + dsa_area *area; + MemoryContextEntry *memctx_info; + int num_retries = 0; + TimestampTz curr_timestamp; + int max_tries = PG_GETARG_INT32(2); + + /* + * Only superusers or users with pg_read_all_stats privileges can view the + * memory context statistics of another process + */ + if (!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("memory context statistics privilege error")); + + InitMaterializedSRF(fcinfo, 0); + + /* + * See if the process with given pid is a backend or an auxiliary process. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + proc = AuxiliaryPidGetProc(pid); + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; but by the time we reach kill(), a process for which we + * get a valid proc here might have terminated on its own. There's no way + * to acquire a lock on an arbitrary process to prevent that. But since + * this mechanism is usually used to debug a backend or an auxiliary + * process running and consuming lots of memory, that it might end on its + * own first and its memory contexts are not logged is not a problem. + */ + if (proc == NULL) + { + /* + * This is just a warning so a loop-through-resultset will not abort + * if one backend terminated on its own during the run. + */ + ereport(WARNING, + (errmsg("PID %d is not a PostgreSQL server process", + pid))); + PG_RETURN_NULL(); + } + + procNumber = GetNumberFromPGProc(proc); + + LWLockAcquire(&memCtxState[procNumber].lw_lock, LW_EXCLUSIVE); + memCtxState[procNumber].get_summary = get_summary; + LWLockRelease(&memCtxState[procNumber].lw_lock); + + curr_timestamp = GetCurrentTimestamp(); + + /* + * Send a signal to a postgresql process, informing it we want it to + * produce information about memory contexts. + */ + if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, procNumber) < 0) + { + ereport(WARNING, + errmsg("could not send signal to process %d: %m", pid)); + PG_RETURN_NULL(); + } + + /* + * A valid DSA pointer isn't proof that statistics are available, it can + * be valid due to previously published stats. Check if the stats are + * updated by comparing the timestamp, if the stats are newer than our + * previously recorded timestamp from before sending the procsignal, they + * must by definition be updated. Wait for max_tries * + * MEMSTATS_WAIT_TIMEOUT, following which display old statistics if + * available or return NULL. + */ + while (1) + { + long msecs; + + /* + * We expect to come out of sleep when the requested process has + * finished publishing the statistics, verified using the valid dsa + * pointer. + * + * Make sure that the information belongs to pid we requested + * information for, Otherwise loop back and wait for the server + * process to finish publishing statistics. + */ + LWLockAcquire(&memCtxState[procNumber].lw_lock, LW_EXCLUSIVE); + msecs = + TimestampDifferenceMilliseconds(curr_timestamp, + memCtxState[procNumber].stats_timestamp); + + /* + * Note in procnumber.h file says that a procNumber can be re-used for + * a different backend immediately after a backend exits. In case an + * old process' data was there and not updated by the current process + * in the slot identified by the procNumber, the pid of the requested + * process and the proc_id might not match. + */ + if (memCtxState[procNumber].proc_id == pid) + { + /* + * Break if the latest stats have been read, indicated by + * statistics timestamp being newer than the current request + * timestamp. + */ + if (DsaPointerIsValid(memCtxState[procNumber].memstats_dsa_pointer) + && msecs > 0) + break; + + } + LWLockRelease(&memCtxState[procNumber].lw_lock); + + /* + * Recheck the state of the backend before sleeping on the condition + * variable + */ + proc = BackendPidGetProc(pid); + +#define MEMSTATS_WAIT_TIMEOUT 1000 + if (proc == NULL) + proc = AuxiliaryPidGetProc(pid); + if (proc == NULL) + { + ereport(WARNING, + errmsg("PID %d is not a PostgreSQL server process", + pid)); + PG_RETURN_NULL(); + } + if (ConditionVariableTimedSleep(&memCtxState[procNumber].memctx_cv, + MEMSTATS_WAIT_TIMEOUT, + WAIT_EVENT_MEM_CTX_PUBLISH)) + { + /* + * Wait for max_tries defined by user, display previously + * published statistics if any, when max_tries are over. + */ + if (num_retries > max_tries) + { + LWLockAcquire(&memCtxState[procNumber].lw_lock, LW_EXCLUSIVE); + /* Displaying previously published statistics */ + if (DsaPointerIsValid(memCtxState[procNumber].memstats_dsa_pointer)) + break; + else + { + LWLockRelease(&memCtxState[procNumber].lw_lock); + PG_RETURN_NULL(); + } + } + ereport(LOG, + errmsg("Wait for %d process to publish stats timed out, trying again", + pid)); + num_retries = num_retries + 1; + } + + } + /* We should land here only with a valid DSA handle */ + Assert(memCtxArea->memstats_dsa_handle != DSA_HANDLE_INVALID); + area = dsa_attach(memCtxArea->memstats_dsa_handle); + + /* + * Backend has finished publishing the stats, read them + * + * Read statistics of top level 1 and 2 contexts, if get_summary is true. + */ + memctx_info = (MemoryContextEntry *) dsa_get_address(area, + memCtxState[procNumber].memstats_dsa_pointer); + +#define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 12 + for (int i = 0; i < memCtxState[procNumber].total_stats; i++) + { + ArrayType *path_array; + int path_length; + Datum values[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + bool nulls[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + char *name; + char *ident; + Datum *path_datum_array; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + if (DsaPointerIsValid(memctx_info[i].name)) + { + name = (char *) dsa_get_address(area, memctx_info[i].name); + values[0] = CStringGetTextDatum(name); + } + else + nulls[0] = true; + if (DsaPointerIsValid(memctx_info[i].ident)) + { + ident = (char *) dsa_get_address(area, memctx_info[i].ident); + values[1] = CStringGetTextDatum(ident); + } + else + nulls[1] = true; + + if (memctx_info[i].type != NULL) + values[2] = CStringGetTextDatum(memctx_info[i].type); + else + nulls[2] = true; + + path_length = memctx_info[i].path_length; + + if (DsaPointerIsValid(memctx_info[i].path)) + { + path_datum_array = (Datum *) dsa_get_address(area, memctx_info[i].path); + path_array = construct_array_builtin(path_datum_array, + path_length, INT4OID); + + values[3] = PointerGetDatum(path_array); + } + else + nulls[3] = true; + values[4] = Int32GetDatum(path_length); /* level */ + values[5] = Int64GetDatum(memctx_info[i].totalspace); + values[6] = Int64GetDatum(memctx_info[i].nblocks); + values[7] = Int64GetDatum(memctx_info[i].freespace); + values[8] = Int64GetDatum(memctx_info[i].freechunks); + values[9] = Int64GetDatum(memctx_info[i].totalspace - + memctx_info[i].freespace); + values[10] = Int32GetDatum(memctx_info[i].num_agg_stats); + values[11] = TimestampTzGetDatum(memCtxState[procNumber].stats_timestamp); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + LWLockRelease(&memCtxState[procNumber].lw_lock); + + ConditionVariableCancelSleep(); + dsa_detach(area); + + PG_RETURN_NULL(); +} + +/* + * Shared memory sizing for reporting memory context information. + */ +static Size +MemCtxShmemSize(void) +{ + Size TotalProcs = + add_size(MaxBackends, add_size(NUM_AUXILIARY_PROCS, max_prepared_xacts)); + + return mul_size(TotalProcs, sizeof(MemoryContextBackendState)); +} + +/* + * Init shared memory for reporting memory context information. + */ +void +MemCtxBackendShmemInit(void) +{ + bool found; + Size TotalProcs = + add_size(MaxBackends, add_size(NUM_AUXILIARY_PROCS, max_prepared_xacts)); + + memCtxState = (MemoryContextBackendState *) ShmemInitStruct("MemoryContextBackendState", + MemCtxShmemSize(), + &found); + if (!IsUnderPostmaster) + { + Assert(!found); + + for (int i = 0; i < TotalProcs; i++) + { + ConditionVariableInit(&memCtxState[i].memctx_cv); + + LWLockInitialize(&memCtxState[i].lw_lock, + LWLockNewTrancheId()); + LWLockRegisterTranche(memCtxState[i].lw_lock.tranche, + "mem_context_backend_stats_reporting"); + + memCtxState[i].memstats_dsa_pointer = InvalidDsaPointer; + } + } + else + { + Assert(found); + } +} + +/* + * Initialize shared memory for displaying memory + * context statistics + */ +void +MemCtxShmemInit(void) +{ + bool found; + + memCtxArea = (MemoryContextState *) ShmemInitStruct("MemoryContextState", sizeof(MemoryContextState), + &found); + if (!IsUnderPostmaster) + { + Assert(!found); + + LWLockInitialize(&memCtxArea->lw_lock, + LWLockNewTrancheId()); + LWLockRegisterTranche(memCtxArea->lw_lock.tranche, + "mem_context_stats_reporting"); + memCtxArea->memstats_dsa_handle = DSA_HANDLE_INVALID; + } + else + { + Assert(found); + } +} diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index b844f9fdaef..13938ccb0f5 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -38,6 +38,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t PublishMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 34cdcdf2fd3..214330aa7a5 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -19,10 +19,18 @@ *------------------------------------------------------------------------- */ +#include <math.h> #include "postgres.h" #include "mb/pg_wchar.h" #include "miscadmin.h" +#include "nodes/pg_list.h" +#include "storage/fd.h" +#include "storage/lwlock.h" +#include "storage/dsm.h" +#include "storage/ipc.h" +#include "utils/dsa.h" +#include "utils/hsearch.h" #include "utils/memdebug.h" #include "utils/memutils.h" #include "utils/memutils_internal.h" @@ -177,6 +185,16 @@ static void MemoryContextStatsInternal(MemoryContext context, int level, static void MemoryContextStatsPrint(MemoryContext context, void *passthru, const char *stats_string, bool print_to_stderr); +static void PublishMemoryContext(MemoryContextEntry *memctx_infos, + int curr_id, MemoryContext context, + List *path, + MemoryContextCounters stat, + int num_contexts, dsa_area *area); +static void compute_contexts_count_and_ids(List *contexts, HTAB *context_id_lookup, + int *stats_count, + bool get_summary); +static List *compute_context_path(MemoryContext c, HTAB *context_id_lookup); +static void dsa_free_previous_stats(dsa_area *area, int total_stats, dsa_pointer prev_dsa_pointer); /* * You should not do memory allocations within a critical section, because @@ -889,7 +907,8 @@ MemoryContextStatsDetail(MemoryContext context, * One recursion level for MemoryContextStats * * Print stats for this context if possible, but in any case accumulate counts - * into *totals (if not NULL). + * into *totals (if not NULL). The callers should make sure that print_location + * is set to PRINT_STATS_STDERR or PRINT_STATS_TO_LOGS or PRINT_STATS_NONE. */ static void MemoryContextStatsInternal(MemoryContext context, int level, @@ -899,38 +918,43 @@ MemoryContextStatsInternal(MemoryContext context, int level, { MemoryContext child; int ichild; - bool print_to_stderr = true; check_stack_depth(); Assert(MemoryContextIsValid(context)); if (print_location == PRINT_STATS_TO_STDERR) - print_to_stderr = true; + { + /* Examine the context itself */ + context->methods->stats(context, + MemoryContextStatsPrint, + &level, + totals, true); + } else if (print_location == PRINT_STATS_TO_LOGS) - print_to_stderr = false; - - if (print_location != PRINT_STATS_NONE) { + /* Examine the context itself */ context->methods->stats(context, MemoryContextStatsPrint, &level, - totals, print_to_stderr); + totals, false); } /* * Do not print the statistics if print_to_stderr is PRINT_STATS_NONE, - * only compute totals. + * only compute totals. This is used in reporting of memory context + * statistics via a sql function. Last parameter is not relevant. */ else { + Assert(print_location == PRINT_STATS_NONE); /* Examine the context itself */ context->methods->stats(context, NULL, NULL, - totals, print_to_stderr); + totals, false); } - /* Increment the context count */ + /* Increment the context count for each of the recursive call */ *num_contexts = *num_contexts + 1; /* @@ -969,9 +993,14 @@ MemoryContextStatsInternal(MemoryContext context, int level, ichild++; child = MemoryContextTraverseNext(child, context); } + + /* + * Add the count of children contexts which are traversed in the + * non-recursive manner. + */ *num_contexts = *num_contexts + ichild; - if (print_to_stderr) + if (print_location == PRINT_STATS_TO_STDERR) { for (int i = 0; i <= level; i++) fprintf(stderr, " "); @@ -984,7 +1013,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, local_totals.freechunks, local_totals.totalspace - local_totals.freespace); } - else if (print_location != PRINT_STATS_NONE) + else if (print_location == PRINT_STATS_TO_LOGS) ereport(LOG_SERVER_ONLY, (errhidestmt(true), errhidecontext(true), @@ -1321,6 +1350,21 @@ HandleLogMemoryContextInterrupt(void) /* latch will be set by procsignal_sigusr1_handler */ } +/* + * HandleGetMemoryContextInterrupt + * Handle receipt of an interrupt indicating publishing of memory + * contexts. + * + * All the actual work is deferred to ProcessLogMemoryContextInterrupt() + */ +void +HandleGetMemoryContextInterrupt(void) +{ + InterruptPending = true; + PublishMemoryContextPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + /* * ProcessLogMemoryContextInterrupt * Perform logging of memory contexts of this backend process. @@ -1358,6 +1402,506 @@ ProcessLogMemoryContextInterrupt(void) MemoryContextStatsDetail(TopMemoryContext, 100, 100, false); } +/* + * ProcessGetMemoryContextInterrupt + * Generate information about memory contexts used by the process. + * + * Performs a breadth first search on the memory context tree, thus parents + * statistics are reported before children in the monitoring function output. + * + * Statistics for all the processes are shared via the same dynamic shared area. + * + * We calculate maximum number of context's statistics that can be displayed + * using a pre-determined limit for memory available per process for this utility + * and the estimate of the size of statistics for each context. + * The remaining context statistics if any are captured as a cumulative total + * at the end of individual context's statistics. + * + * If get_summary is true, we display the level 1 and level 2 contexts. For that + * we traverse the memory context tree recursively in depth first search manner + * to cover all the children of a parent context, to be able to display a + * cumulative total of memory consumption by a parent at level 2. + */ +void +ProcessGetMemoryContextInterrupt(void) +{ + List *contexts; + + HASHCTL ctl; + HTAB *context_id_lookup; + int context_id = 0; + MemoryContext stat_cxt; + MemoryContextEntry *meminfo; + bool get_summary = false; + + dsa_area *area = NULL; + int max_stats; + int idx = MyProcNumber; + int stats_count = 0; + MemoryContextCounters stat; + int num_individual_stats = 0; + + PublishMemoryContextPending = false; + + /* + * Make a new context that will contain the hash table, to ease the + * cleanup. + */ + stat_cxt = AllocSetContextCreate(CurrentMemoryContext, + "Memory context statistics", + ALLOCSET_DEFAULT_SIZES); + + /* + * The hash table is used for constructing "path" column of the view, + * similar to its local backend counterpart. + */ + ctl.keysize = sizeof(MemoryContext); + ctl.entrysize = sizeof(MemoryContextId); + ctl.hcxt = stat_cxt; + + context_id_lookup = hash_create("pg_get_remote_backend_memory_contexts", + 256, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + /* List of contexts to process in the next round - start at the top. */ + contexts = list_make1(TopMemoryContext); + + /* Compute the number of stats that can fit in the defined limit */ + max_stats = (MAX_SEGMENTS_PER_BACKEND * DSA_DEFAULT_INIT_SEGMENT_SIZE) + / (sizeof(MemoryContextEntry) + (MEM_CONTEXT_MAX_LEVEL + * sizeof(Datum)) + (2 * MEMORY_CONTEXT_IDENT_SHMEM_SIZE)); + LWLockAcquire(&memCtxState[idx].lw_lock, LW_EXCLUSIVE); + get_summary = memCtxState[idx].get_summary; + LWLockRelease(&memCtxState[idx].lw_lock); + + /* + * Traverse the memory context tree to find total number of contexts. If + * summary is requested report the total number of contexts at level 1 and + * 2 from the top. Also, populate the hash table of context ids. + */ + compute_contexts_count_and_ids(contexts, context_id_lookup, &stats_count, + get_summary); + + /* + * Allocate memory in this process's dsa for storing statistics of the the + * memory contexts upto max_stats, for contexts that don't fit within a + * limit, a cumulative total is written as the last record in the DSA + * segment. + */ + stats_count = (stats_count > max_stats) ? max_stats : stats_count; + + LWLockAcquire(&memCtxArea->lw_lock, LW_EXCLUSIVE); + + /* + * Create a DSA and send handle to the the client process after storing + * the context statistics. If number of contexts exceed a predefined + * limit(8MB), a cumulative total is stored for such contexts. + */ + if (memCtxArea->memstats_dsa_handle == DSA_HANDLE_INVALID) + { + MemoryContext oldcontext = CurrentMemoryContext; + dsa_handle handle; + + MemoryContextSwitchTo(TopMemoryContext); + + area = dsa_create(memCtxArea->lw_lock.tranche); + + handle = dsa_get_handle(area); + MemoryContextSwitchTo(oldcontext); + + dsa_pin_mapping(area); + + /* + * Pin the dsa area, this is to make sure the area remains attachable + * even if current backend exits. This is done so that a waiting + * client gets the stats even after a process exits. + */ + dsa_pin(area); + + /* Set the handle in shared memory */ + memCtxArea->memstats_dsa_handle = handle; + } + + /* + * If DSA exists, created by another process publishing statistics, or by + * the previous execution of this function by this process, attach to it. + */ + else if (area == NULL) + { + MemoryContext oldcontext = CurrentMemoryContext; + + MemoryContextSwitchTo(TopMemoryContext); + area = dsa_attach(memCtxArea->memstats_dsa_handle); + MemoryContextSwitchTo(oldcontext); + dsa_pin_mapping(area); + } + LWLockRelease(&memCtxArea->lw_lock); + + /* + * Hold the process lock to protect writes to process specific memory. Two + * processes publishing statistics do not block each other. + */ + LWLockAcquire(&memCtxState[idx].lw_lock, LW_EXCLUSIVE); + memCtxState[idx].proc_id = MyProcPid; + + if (DsaPointerIsValid(memCtxState[idx].memstats_dsa_pointer)) + { + /* + * Free any previous allocations, free the name, ident and path + * pointers before freeing the pointer that contains them. + */ + dsa_free_previous_stats(area, memCtxState[idx].total_stats, + memCtxState[idx].memstats_dsa_pointer); + dsa_free(area, memCtxState[idx].memstats_dsa_pointer); + memCtxState[idx].memstats_dsa_pointer = InvalidDsaPointer; + } + memCtxState[idx].memstats_dsa_pointer = dsa_allocate0(area, + stats_count * sizeof(MemoryContextEntry)); + + meminfo = (MemoryContextEntry *) dsa_get_address(area, + memCtxState[idx].memstats_dsa_pointer); + + if (get_summary) + { + int ctx_id = 0; + List *path = NIL; + + /* Copy TopMemoryContext statistics to DSA */ + memset(&stat, 0, sizeof(stat)); + (*TopMemoryContext->methods->stats) (TopMemoryContext, NULL, NULL, + &stat, true); + path = lcons_int(1, path); + PublishMemoryContext(meminfo, ctx_id, TopMemoryContext, path, stat, + 1, area); + ctx_id = ctx_id + 1; + + /* + * Copy statistics for each of TopMemoryContexts children. This + * includes statistics of all of their children upto level 100. + */ + for (MemoryContext c = TopMemoryContext->firstchild; c != NULL; + c = c->nextchild) + { + MemoryContextCounters grand_totals; + int num_contexts = 0; + + path = NIL; + memset(&grand_totals, 0, sizeof(grand_totals)); + + MemoryContextStatsInternal(c, 0, 100, 100, &grand_totals, + PRINT_STATS_NONE, &num_contexts); + + path = compute_context_path(c, context_id_lookup); + + PublishMemoryContext(meminfo, ctx_id, c, path, + grand_totals, num_contexts, area); + ctx_id = ctx_id + 1; + } + memCtxState[idx].total_stats = ctx_id; + goto cleanup; + } + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + char *name; + + /* + * Figure out the transient context_id of this context and each of its + * ancestors, to compute a path for this context. + */ + path = compute_context_path(cur, context_id_lookup); + + if (context_id <= (max_stats - 2)) + { + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1, area); + } + else + { + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + meminfo[max_stats - 1].totalspace += stat.totalspace; + meminfo[max_stats - 1].nblocks += stat.nblocks; + meminfo[max_stats - 1].freespace += stat.freespace; + meminfo[max_stats - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit per process is reached, write aggregate of the + * remaining statistics. + */ + if (context_id == (max_stats - 2) && context_id < (stats_count - 1)) + { + num_individual_stats = context_id + 1; + meminfo[max_stats - 1].name = dsa_allocate0(area, 17); + name = dsa_get_address(area, meminfo[max_stats - 1].name); + strncpy(name, "Remaining Totals", 16); + meminfo[max_stats - 1].ident = InvalidDsaPointer; + meminfo[max_stats - 1].path = InvalidDsaPointer; + meminfo[max_stats - 1].type = NULL; + } + context_id++; + } + /* No aggregated contexts, individual statistics reported */ + if (context_id < (max_stats - 2)) + { + memCtxState[idx].total_stats = context_id; + } + /* Report number of aggregated memory contexts */ + else + { + meminfo[max_stats - 1].num_agg_stats = context_id - + num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for cumulative + * statistics. + */ + memCtxState[idx].total_stats = num_individual_stats + 1; + } +cleanup: + + /* + * Signal all the waiting client backends after copying all the statistics + */ + memCtxState[idx].stats_timestamp = GetCurrentTimestamp(); + LWLockRelease(&memCtxState[idx].lw_lock); + ConditionVariableBroadcast(&memCtxState[idx].memctx_cv); + /* Delete the hash table memory context */ + MemoryContextDelete(stat_cxt); + + dsa_detach(area); +} + +/* + * Append the transient context_id of this context and each of + * its ancestors to a list, in order to compute a path. + */ +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + + for (MemoryContext cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + { + ereport(LOG, + errmsg("hash table corrupted, can't construct path value")); + break; + } + path = lcons_int(cur_entry->context_id, path); + } + return path; +} + +/* + * Return the number of contexts allocated currently by the backend + * Assign context ids to each of the contexts. + */ +static void +compute_contexts_count_and_ids(List *contexts, HTAB *context_id_lookup, + int *stats_count, bool get_summary) +{ + foreach_ptr(MemoryContextData, cur, contexts) + { + MemoryContextId *entry; + bool found; + + entry = (MemoryContextId *) hash_search(context_id_lookup, &cur, + HASH_ENTER, &found); + Assert(!found); + + /* context id starts with 1 */ + entry->context_id = (++(*stats_count)); + + /* Append the children of the current context to the main list. */ + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + { + if (get_summary) + { + entry = (MemoryContextId *) hash_search(context_id_lookup, &c, + HASH_ENTER, &found); + Assert(!found); + + entry->context_id = (++(*stats_count)); + } + + contexts = lappend(contexts, c); + } + + /* + * In summary only the first two level(from top) contexts are + * displayed + */ + if (get_summary) + break; + } + +} + +/* Copy the memory context statistics of a single context to a dsa memory */ +static void +PublishMemoryContext(MemoryContextEntry *memctx_info, int curr_id, + MemoryContext context, List *path, + MemoryContextCounters stat, int num_contexts, + dsa_area *area) +{ + char clipped_ident[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + char *name; + char *ident; + Datum *path_array; + + if (context->name != NULL) + { + Assert(strlen(context->name) < MEMORY_CONTEXT_IDENT_SHMEM_SIZE); + memctx_info[curr_id].name = dsa_allocate0(area, strlen(context->name) + 1); + name = (char *) dsa_get_address(area, memctx_info[curr_id].name); + strncpy(name, context->name, strlen(context->name)); + } + else + memctx_info[curr_id].name = InvalidDsaPointer; + + /* Trim and copy the identifier if it is not set to NULL */ + if (context->ident != NULL) + { + int idlen = strlen(context->ident); + + /* + * Some identifiers such as SQL query string can be very long, + * truncate oversize identifiers. + */ + if (idlen >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + idlen = pg_mbcliplen(context->ident, idlen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + memcpy(clipped_ident, context->ident, idlen); + clipped_ident[idlen] = '\0'; + + /* + * To be consistent with logging output, we label dynahash contexts + * with just the hash table name as with MemoryContextStatsPrint(). + */ + if (!strncmp(context->name, "dynahash", 8)) + { + dsa_free(area, memctx_info[curr_id].name); + memctx_info[curr_id].name = dsa_allocate0(area, + strlen(clipped_ident) + 1); + name = (char *) dsa_get_address(area, + memctx_info[curr_id].name); + strlcpy(name, + clipped_ident, idlen + 1); + memctx_info[curr_id].ident = InvalidDsaPointer; + } + else + { + + memctx_info[curr_id].ident = dsa_allocate0(area, + strlen(clipped_ident) + 1); + ident = (char *) dsa_get_address(area, + memctx_info[curr_id].ident); + strlcpy(ident, + clipped_ident, idlen + 1); + } + } + else + memctx_info[curr_id].ident = InvalidDsaPointer; + /* Allocate dsa memory for storing path information */ + if (path == NIL) + memctx_info[curr_id].path = InvalidDsaPointer; + else + { + memctx_info[curr_id].path_length = list_length(path); + memctx_info[curr_id].path = dsa_allocate0(area, + memctx_info[curr_id].path_length + * sizeof(Datum)); + path_array = (Datum *) dsa_get_address(area, memctx_info[curr_id].path); + foreach_int(i, path) + path_array[foreach_current_index(i)] = Int32GetDatum(i); + } + memctx_info[curr_id].type = ContextTypeToString(context->type); + memctx_info[curr_id].totalspace = stat.totalspace; + memctx_info[curr_id].nblocks = stat.nblocks; + memctx_info[curr_id].freespace = stat.freespace; + memctx_info[curr_id].freechunks = stat.freechunks; + memctx_info[curr_id].num_agg_stats = num_contexts; +} + +static void +dsa_free_previous_stats(dsa_area *area, int total_stats, + dsa_pointer prev_dsa_pointer) +{ + MemoryContextEntry *meminfo; + + meminfo = (MemoryContextEntry *) dsa_get_address(area, prev_dsa_pointer); + for (int i = 0; i < total_stats; i++) + { + if (DsaPointerIsValid(meminfo[i].name)) + dsa_free(area, meminfo[i].name); + + if (DsaPointerIsValid(meminfo[i].ident)) + dsa_free(area, meminfo[i].ident); + + if (DsaPointerIsValid(meminfo[i].path)) + dsa_free(area, meminfo[i].path); + } +} + +/* + * Free the memory context statistics stored by this process + * in dsa area. + */ +void +AtProcExit_memstats_dsa_free(int code, Datum arg) +{ + int idx = MyProcNumber; + dsm_segment *dsm_seg = NULL; + dsa_area *area = NULL; + + if (memCtxArea->memstats_dsa_handle != DSA_HANDLE_INVALID) + dsm_seg = dsm_find_mapping(memCtxArea->memstats_dsa_handle); + else + return; + + LWLockAcquire(&memCtxState[idx].lw_lock, LW_EXCLUSIVE); + + if (!DsaPointerIsValid(memCtxState[idx].memstats_dsa_pointer)) + { + LWLockRelease(&memCtxState[idx].lw_lock); + return; + } + + /* If the dsm mapping could not be found, attach to the area */ + if (dsm_seg == NULL) + area = dsa_attach(memCtxArea->memstats_dsa_handle); + else + return; + + /* + * Free the memory context statistics, free the name, ident and path + * pointers before freeing the pointer that contains these pointers and + * integer statistics. + */ + dsa_free_previous_stats(area, memCtxState[idx].total_stats, + memCtxState[idx].memstats_dsa_pointer); + dsa_free(area, memCtxState[idx].memstats_dsa_pointer); + memCtxState[idx].memstats_dsa_pointer = InvalidDsaPointer; + + dsa_detach(area); + LWLockRelease(&memCtxState[idx].lw_lock); +} + void * palloc(Size size) { diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 0d29ef50ff2..e2957c3906d 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8509,6 +8509,16 @@ prorettype => 'bool', proargtypes => 'int4', prosrc => 'pg_log_backend_memory_contexts' }, +# publishing memory contexts of the specified postgres process +{ oid => '2173', descr => 'publish memory contexts of the specified backend', + proname => 'pg_get_process_memory_contexts', provolatile => 'v', + prorows => '100', proretset => 't', proparallel => 'r', + prorettype => 'record', proargtypes => 'int4 bool int4', + proallargtypes => '{int4,bool,int4,text,text,text,_int4,int4,int8,int8,int8,int8,int8,int4,timestamptz}', + proargmodes => '{i,i,i,o,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{oid, summary, num_of_tries, name, ident, type, path, level, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes, num_agg_contexts, stats_timestamp}', + prosrc => 'pg_get_process_memory_contexts' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index 603d0424354..d3c44df6e13 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -96,6 +96,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t PublishMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index 022fd8ed933..5d4b2fbfc9c 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -35,6 +35,7 @@ typedef enum PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to send the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ /* Recovery conflict reasons */ diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 8abc26abce2..10fab7e5804 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -18,6 +18,9 @@ #define MEMUTILS_H #include "nodes/memnodes.h" +#include "storage/condition_variable.h" +#include "storage/lmgr.h" +#include "utils/dsa.h" /* @@ -48,7 +51,10 @@ #define AllocHugeSizeIsValid(size) ((Size) (size) <= MaxAllocHugeSize) +#define MEMORY_CONTEXT_IDENT_SHMEM_SIZE 128 +#define MEM_CONTEXT_MAX_LEVEL 64 +#define MAX_SEGMENTS_PER_BACKEND 8 /* * Standard top-level memory contexts. * @@ -319,4 +325,68 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) return true; } +/* Dynamic shared memory state for statistics per context */ +typedef struct MemoryContextEntry +{ + dsa_pointer name; + dsa_pointer ident; + dsa_pointer path; + const char *type; + int path_length; + int64 totalspace; + int64 nblocks; + int64 freespace; + int64 freechunks; + int num_agg_stats; +} MemoryContextEntry; + +/* + * Static shared memory state representing the DSA area + * created for memory context statistics reporting. + * Single DSA area is created and used by all the processes, + * each having its specific dsa allocations for sharing memory + * statistics, tracked by per backend static shared memory state. + */ +typedef struct MemoryContextState +{ + dsa_handle memstats_dsa_handle; + LWLock lw_lock; +} MemoryContextState; + +/* + * Per backend static shared memory state for memory + * context statistics reporting. + */ +typedef struct MemoryContextBackendState +{ + ConditionVariable memctx_cv; + LWLock lw_lock; + int proc_id; + int total_stats; + bool get_summary; + dsa_pointer memstats_dsa_pointer; + TimestampTz stats_timestamp; +} MemoryContextBackendState; + + +/* + * MemoryContextId + * Used for storage of transient identifiers for + * pg_get_backend_memory_contexts. + */ +typedef struct MemoryContextId +{ + MemoryContext context; + int context_id; +} MemoryContextId; + +extern PGDLLIMPORT MemoryContextBackendState *memCtxState; +extern PGDLLIMPORT MemoryContextState *memCtxArea; +extern void ProcessGetMemoryContextInterrupt(void); +extern const char *ContextTypeToString(NodeTag type); +extern void HandleGetMemoryContextInterrupt(void); +extern void MemCtxShmemInit(void); +extern void MemCtxBackendShmemInit(void); +extern void AtProcExit_memstats_dsa_free(int code, Datum arg); + #endif /* MEMUTILS_H */ diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 83228cfca29..dca20ae1a26 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -232,3 +232,17 @@ select * from pg_timezone_abbrevs where abbrev = 'LMT'; LMT | @ 7 hours 52 mins 58 secs ago | f (1 row) +DO $$ +DECLARE + launcher_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='autovacuum launcher' + INTO launcher_pid; + + select type, name, ident + from pg_get_process_memory_contexts(launcher_pid, false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; +NOTICE: (AllocSet,TopMemoryContext,) diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 66179f026b3..4767351d4e2 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -101,3 +101,17 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs; -- One specific case we can check without much fear of breakage -- is the historical local-mean-time value used for America/Los_Angeles. select * from pg_timezone_abbrevs where abbrev = 'LMT'; + +DO $$ +DECLARE + launcher_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='autovacuum launcher' + INTO launcher_pid; + + select type, name, ident + from pg_get_process_memory_contexts(launcher_pid, false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 3fbf5a4c212..6cd7a30e0be 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1649,12 +1649,16 @@ MemoizeState MemoizeTuple MemoryChunk MemoryContext +MemoryContextBackendState MemoryContextCallback MemoryContextCallbackFunction MemoryContextCounters MemoryContextData +MemoryContextEntry +MemoryContextId MemoryContextMethodID MemoryContextMethods +MemoryContextState MemoryStatsPrintFunc MergeAction MergeActionState -- 2.39.3 (Apple Git-146) [application/octet-stream] v19-0003-Review-comments-and-fixups.patch (18.2K, ../../[email protected]/4-v19-0003-Review-comments-and-fixups.patch) download | inline diff: From d9e8a1488472db17bcb3b275488232e5db94d018 Mon Sep 17 00:00:00 2001 From: Daniel Gustafsson <[email protected]> Date: Tue, 25 Mar 2025 13:47:52 +0100 Subject: [PATCH v19 3/3] Review comments and fixups --- doc/src/sgml/func.sgml | 62 +++++++++++------- src/backend/utils/adt/mcxtfuncs.c | 103 +++++++++++++++--------------- src/backend/utils/mmgr/mcxt.c | 78 +++++++++++----------- 3 files changed, 131 insertions(+), 112 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 1ab2ce12662..f8a3b9d0cf6 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -28580,34 +28580,36 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres} </para> <para> This function handles requests to display the memory contexts of a - PostgreSQL process with the specified process ID (PID). It takes three - arguments: <parameter>PID</parameter>, <parameter>get_summary</parameter> - and <parameter>num_of_tries</parameter>. The function can send requests + <productname>PostgreSQL</productname> process with the specified process ID. It takes three + arguments: <parameter>pid</parameter>, <parameter>get_summary</parameter> + and <parameter>num_of_tries</parameter>. The function can send requests to both backend and auxiliary processes. - + </para> + <para> After receiving memory context statistics from the target process, it - returns the results as one row per context. If all the contexts don't + returns the results as one row per context. If all the contexts don't fit within the pre-determined size limit, the remaining context statistics - are aggregated and a cumulative total is displayed. The num_agg_contexts + are aggregated and a cumulative total is displayed. The <literal>num_agg_contexts</literal> column indicates the number of contexts aggregated in the displayed - statistics. The num_agg_contexts value is typically 1, meaning that each + statistics. The <literal>num_agg_contexts</literal> value is typically 1, meaning that each context's statistics are displayed separately. - - When <parameter>get_summary</parameter> is set to true, statistics + </para> + <para> + When <parameter>get_summary</parameter> is set to <literal>true</literal>, statistics for memory contexts at levels 1 and 2 are displayed, with level 1 representing the root node (i.e., TopMemoryContext). Each level 2 context's statistics represent an aggregate of all its - child contexts' statistics, with num_agg_contexts indicating the number + child contexts' statistics, with <literal>num_agg_contexts</literal> indicating the number of these aggregated child contexts. - - When <parameter>get_summary</parameter> is set to false, the + When <parameter>get_summary</parameter> is set to <literal>false</literal>, the num_agg_contexts value is 1, indicating that individual statistics are being displayed. - + </para> + <para> <parameter>num_of_tries</parameter> indicates the number of times - the client will wait for the latest statistics. The wait per try is 1 - second. This parameter can be increased if the user anticipates a delay - in the response from the reporting process. Conversely, if users are + the client will wait for the latest statistics. The wait per try is 1 + second. This parameter can be increased if the user anticipates a delay + in the response from the reporting process. Conversely, if users are frequently and periodically querying the process for statistics, or if there are concurrent requests for statistics of the same process, lowering the parameter might help achieve a faster response. @@ -28740,13 +28742,29 @@ postgres=# SELECT * FROM pg_get_process_memory_contexts( (SELECT pid FROM pg_stat_activity WHERE backend_type = 'checkpointer') , false, 5) LIMIT 1; - name | ident | type | path | total_bytes | total_nblocks | free_bytes | free_chunks | used_bytes | num_ -agg_contexts | stats_timestamp -------------------+-------+----------+------+-------------+---------------+------------+-------------+------------+----- --------------+---------------------------------- - TopMemoryContext | | AllocSet | {1} | 102664 | 4 | 3008 | 2 | 99656 | - 1 | 2025-03-04 10:01:57.590543+05:30 +-[ RECORD 1 ]----+------------------------------ +name | TopMemoryContext +ident | +type | AllocSet +path | {1} +level | 1 +total_bytes | 90304 +total_nblocks | 3 +free_bytes | 2880 +free_chunks | 1 +used_bytes | 87424 +num_agg_contexts | 1 +stats_timestamp | 2025-03-24 13:55:47.796698+01 </programlisting> + <note> + <para> + While <function>pg_get_process_memory_contexts</function> can be used to + query memory contexts of the local backend, + <structname>pg_backend_memory_contexts</structname> + (see <xref linkend="view-pg-backend-memory-contexts"/> for more details) + will be less resource intensive when only the local backend is of interest. + </para> + </note> </para> </sect2> diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index 462c4e48cf0..f9145b9faaa 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -330,7 +330,7 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) * to a dynamic shared memory space. * * We have defined a limit on dsa memory that could be allocated per process - - * if the process has more memory contexts than that can fit in the allocated + * if the process has more memory contexts than what can fit in the allocated * size, the excess contexts are summarized and represented as cumulative total * at the end of the buffer. * @@ -354,6 +354,7 @@ pg_get_process_memory_contexts(PG_FUNCTION_ARGS) bool get_summary = PG_GETARG_BOOL(1); PGPROC *proc; ProcNumber procNumber = INVALID_PROC_NUMBER; + bool proc_is_aux = false; ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; dsa_area *area; MemoryContextEntry *memctx_info; @@ -368,25 +369,25 @@ pg_get_process_memory_contexts(PG_FUNCTION_ARGS) if (!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) ereport(ERROR, errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), - errmsg("memory context statistics privilege error")); + errmsg("permission denied to extract memory context statistics")); InitMaterializedSRF(fcinfo, 0); /* - * See if the process with given pid is a backend or an auxiliary process. + * See if the process with given pid is a backend or an auxiliary process + * and remember the type for when we requery the process later. */ proc = BackendPidGetProc(pid); if (proc == NULL) + { proc = AuxiliaryPidGetProc(pid); + proc_is_aux = true; + } /* * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid - * isn't valid; but by the time we reach kill(), a process for which we - * get a valid proc here might have terminated on its own. There's no way - * to acquire a lock on an arbitrary process to prevent that. But since - * this mechanism is usually used to debug a backend or an auxiliary - * process running and consuming lots of memory, that it might end on its - * own first and its memory contexts are not logged is not a problem. + * isn't valid; this is however not a problem and leave with a WARNING. + * See comment in pg_log_backend_memory_contexts for a discussion on this. */ if (proc == NULL) { @@ -395,8 +396,7 @@ pg_get_process_memory_contexts(PG_FUNCTION_ARGS) * if one backend terminated on its own during the run. */ ereport(WARNING, - (errmsg("PID %d is not a PostgreSQL server process", - pid))); + (errmsg("PID %d is not a PostgreSQL server process", pid))); PG_RETURN_NULL(); } @@ -409,8 +409,8 @@ pg_get_process_memory_contexts(PG_FUNCTION_ARGS) curr_timestamp = GetCurrentTimestamp(); /* - * Send a signal to a postgresql process, informing it we want it to - * produce information about memory contexts. + * Send a signal to a PostgreSQL process, informing it we want it to + * produce information about its memory contexts. */ if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, procNumber) < 0) { @@ -442,9 +442,6 @@ pg_get_process_memory_contexts(PG_FUNCTION_ARGS) * process to finish publishing statistics. */ LWLockAcquire(&memCtxState[procNumber].lw_lock, LW_EXCLUSIVE); - msecs = - TimestampDifferenceMilliseconds(curr_timestamp, - memCtxState[procNumber].stats_timestamp); /* * Note in procnumber.h file says that a procNumber can be re-used for @@ -460,29 +457,38 @@ pg_get_process_memory_contexts(PG_FUNCTION_ARGS) * statistics timestamp being newer than the current request * timestamp. */ + msecs = TimestampDifferenceMilliseconds(curr_timestamp, + memCtxState[procNumber].stats_timestamp); + if (DsaPointerIsValid(memCtxState[procNumber].memstats_dsa_pointer) && msecs > 0) break; - } LWLockRelease(&memCtxState[procNumber].lw_lock); /* * Recheck the state of the backend before sleeping on the condition - * variable + * variable to ensure the process is still alive. Only check the + * relevant process type based on the earlier PID check. */ - proc = BackendPidGetProc(pid); - -#define MEMSTATS_WAIT_TIMEOUT 1000 - if (proc == NULL) + if (proc_is_aux) proc = AuxiliaryPidGetProc(pid); + else + proc = BackendPidGetProc(pid); + + /* + * The process ending during memory context processing is not an + * error. + */ if (proc == NULL) { ereport(WARNING, - errmsg("PID %d is not a PostgreSQL server process", - pid)); + errmsg("PID %d is not a PostgreSQL server process", pid)); PG_RETURN_NULL(); } + +#define MEMSTATS_WAIT_TIMEOUT 1000 + if (ConditionVariableTimedSleep(&memCtxState[procNumber].memctx_cv, MEMSTATS_WAIT_TIMEOUT, WAIT_EVENT_MEM_CTX_PUBLISH)) @@ -503,12 +509,11 @@ pg_get_process_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_NULL(); } } - ereport(LOG, - errmsg("Wait for %d process to publish stats timed out, trying again", + ereport(DEBUG1, + errmsg("timed out waiting for process with PID %d to publish stats, retrying", pid)); num_retries = num_retries + 1; } - } /* We should land here only with a valid DSA handle */ Assert(memCtxArea->memstats_dsa_handle != DSA_HANDLE_INVALID); @@ -519,8 +524,8 @@ pg_get_process_memory_contexts(PG_FUNCTION_ARGS) * * Read statistics of top level 1 and 2 contexts, if get_summary is true. */ - memctx_info = (MemoryContextEntry *) dsa_get_address(area, - memCtxState[procNumber].memstats_dsa_pointer); + memctx_info = (MemoryContextEntry *) + dsa_get_address(area, memCtxState[procNumber].memstats_dsa_pointer); #define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 12 for (int i = 0; i < memCtxState[procNumber].total_stats; i++) @@ -543,6 +548,7 @@ pg_get_process_memory_contexts(PG_FUNCTION_ARGS) } else nulls[0] = true; + if (DsaPointerIsValid(memctx_info[i].ident)) { ident = (char *) dsa_get_address(area, memctx_info[i].ident); @@ -563,11 +569,11 @@ pg_get_process_memory_contexts(PG_FUNCTION_ARGS) path_datum_array = (Datum *) dsa_get_address(area, memctx_info[i].path); path_array = construct_array_builtin(path_datum_array, path_length, INT4OID); - values[3] = PointerGetDatum(path_array); } else nulls[3] = true; + values[4] = Int32GetDatum(path_length); /* level */ values[5] = Int64GetDatum(memctx_info[i].totalspace); values[6] = Int64GetDatum(memctx_info[i].nblocks); @@ -589,18 +595,6 @@ pg_get_process_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_NULL(); } -/* - * Shared memory sizing for reporting memory context information. - */ -static Size -MemCtxShmemSize(void) -{ - Size TotalProcs = - add_size(MaxBackends, add_size(NUM_AUXILIARY_PROCS, max_prepared_xacts)); - - return mul_size(TotalProcs, sizeof(MemoryContextBackendState)); -} - /* * Init shared memory for reporting memory context information. */ @@ -608,12 +602,16 @@ void MemCtxBackendShmemInit(void) { bool found; - Size TotalProcs = - add_size(MaxBackends, add_size(NUM_AUXILIARY_PROCS, max_prepared_xacts)); + Size TotalProcs; + + TotalProcs = add_size(MaxBackends, NUM_AUXILIARY_PROCS); + TotalProcs = add_size(TotalProcs, max_prepared_xacts); + + memCtxState = (MemoryContextBackendState *) + ShmemInitStruct("MemoryContextBackendState", + mul_size(TotalProcs, sizeof(MemoryContextBackendState)), + &found); - memCtxState = (MemoryContextBackendState *) ShmemInitStruct("MemoryContextBackendState", - MemCtxShmemSize(), - &found); if (!IsUnderPostmaster) { Assert(!found); @@ -622,8 +620,7 @@ MemCtxBackendShmemInit(void) { ConditionVariableInit(&memCtxState[i].memctx_cv); - LWLockInitialize(&memCtxState[i].lw_lock, - LWLockNewTrancheId()); + LWLockInitialize(&memCtxState[i].lw_lock, LWLockNewTrancheId()); LWLockRegisterTranche(memCtxState[i].lw_lock.tranche, "mem_context_backend_stats_reporting"); @@ -637,16 +634,16 @@ MemCtxBackendShmemInit(void) } /* - * Initialize shared memory for displaying memory - * context statistics + * Initialize shared memory for displaying memory context statistics */ void MemCtxShmemInit(void) { bool found; - memCtxArea = (MemoryContextState *) ShmemInitStruct("MemoryContextState", sizeof(MemoryContextState), - &found); + memCtxArea = (MemoryContextState *) + ShmemInitStruct("MemoryContextState", sizeof(MemoryContextState), &found); + if (!IsUnderPostmaster) { Assert(!found); diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 214330aa7a5..4db48230241 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -19,7 +19,6 @@ *------------------------------------------------------------------------- */ -#include <math.h> #include "postgres.h" #include "mb/pg_wchar.h" @@ -922,38 +921,39 @@ MemoryContextStatsInternal(MemoryContext context, int level, check_stack_depth(); Assert(MemoryContextIsValid(context)); - if (print_location == PRINT_STATS_TO_STDERR) - { - /* Examine the context itself */ - context->methods->stats(context, - MemoryContextStatsPrint, - &level, - totals, true); - } - else if (print_location == PRINT_STATS_TO_LOGS) + switch (print_location) { + case PRINT_STATS_TO_STDERR: + /* Examine the context itself */ + context->methods->stats(context, + MemoryContextStatsPrint, + &level, + totals, true); + break; - /* Examine the context itself */ - context->methods->stats(context, - MemoryContextStatsPrint, - &level, - totals, false); - } + case PRINT_STATS_TO_LOGS: + /* Examine the context itself */ + context->methods->stats(context, + MemoryContextStatsPrint, + &level, + totals, false); + break; - /* - * Do not print the statistics if print_to_stderr is PRINT_STATS_NONE, - * only compute totals. This is used in reporting of memory context - * statistics via a sql function. Last parameter is not relevant. - */ - else - { - Assert(print_location == PRINT_STATS_NONE); - /* Examine the context itself */ - context->methods->stats(context, - NULL, - NULL, - totals, false); + case PRINT_STATS_NONE: + + /* + * Do not print the statistics if print_location is + * PRINT_STATS_NONE, only compute totals. This is used in + * reporting of memory context statistics via a sql function. Last + * parameter is not relevant. + */ + context->methods->stats(context, + NULL, + NULL, + totals, false); + break; } + /* Increment the context count for each of the recursive call */ *num_contexts = *num_contexts + 1; @@ -1601,6 +1601,7 @@ ProcessGetMemoryContextInterrupt(void) memCtxState[idx].total_stats = ctx_id; goto cleanup; } + foreach_ptr(MemoryContextData, cur, contexts) { List *path = NIL; @@ -1665,6 +1666,7 @@ ProcessGetMemoryContextInterrupt(void) */ memCtxState[idx].total_stats = num_individual_stats + 1; } + cleanup: /* @@ -1680,8 +1682,8 @@ cleanup: } /* - * Append the transient context_id of this context and each of - * its ancestors to a list, in order to compute a path. + * Append the transient context_id of this context and each of its ancestors + * to a list, in order to compute a path. */ static List * compute_context_path(MemoryContext c, HTAB *context_id_lookup) @@ -1763,6 +1765,8 @@ PublishMemoryContext(MemoryContextEntry *memctx_info, int curr_id, char *ident; Datum *path_array; + Assert(MemoryContextIsValid(context)); + if (context->name != NULL) { Assert(strlen(context->name) < MEMORY_CONTEXT_IDENT_SHMEM_SIZE); @@ -1817,6 +1821,7 @@ PublishMemoryContext(MemoryContextEntry *memctx_info, int curr_id, } else memctx_info[curr_id].ident = InvalidDsaPointer; + /* Allocate dsa memory for storing path information */ if (path == NIL) memctx_info[curr_id].path = InvalidDsaPointer; @@ -1869,11 +1874,11 @@ AtProcExit_memstats_dsa_free(int code, Datum arg) dsm_segment *dsm_seg = NULL; dsa_area *area = NULL; - if (memCtxArea->memstats_dsa_handle != DSA_HANDLE_INVALID) - dsm_seg = dsm_find_mapping(memCtxArea->memstats_dsa_handle); - else + if (memCtxArea->memstats_dsa_handle == DSA_HANDLE_INVALID) return; + dsm_seg = dsm_find_mapping(memCtxArea->memstats_dsa_handle); + LWLockAcquire(&memCtxState[idx].lw_lock, LW_EXCLUSIVE); if (!DsaPointerIsValid(memCtxState[idx].memstats_dsa_pointer)) @@ -1883,10 +1888,9 @@ AtProcExit_memstats_dsa_free(int code, Datum arg) } /* If the dsm mapping could not be found, attach to the area */ - if (dsm_seg == NULL) - area = dsa_attach(memCtxArea->memstats_dsa_handle); - else + if (dsm_seg != NULL) return; + area = dsa_attach(memCtxArea->memstats_dsa_handle); /* * Free the memory context statistics, free the name, ident and path -- 2.39.3 (Apple Git-146) ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-03-26 10:34 Rahila Syed <[email protected]> parent: Daniel Gustafsson <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Rahila Syed @ 2025-03-26 10:34 UTC (permalink / raw) To: Daniel Gustafsson <[email protected]>; +Cc: pgsql-hackers Hi Daniel, Thank you for your review. I have incorporated all your changes in v20 patches and ensured that the review comments corresponding to 0001 patch are included in that patch and not in 0002. > > +MEM_CTX_PUBLISH "Waiting for backend to publish memory information." > I wonder if this should really be "process" and not backend? > > Fixed. > > + default: > + context_type = "???"; > + break; > In ContextTypeToString() I'm having second thoughts about this, there > shouldn't > be any legitimate use-case of passing a nodetag this function which would > fail > MemoryContextIsValid(). I wonder if we aren't helping callers more by > erroring > out rather than silently returning an unknown? I haven't changed this but > maybe we should to set the API right from the start? > I cannot think of any legitimate scenario where the context type would be unknown. However, if we were to throw an error, it would prevent us from reporting any memory usage information when the context type is unidentified. Perhaps, it would be more informative and less restrictive to label it as "Unrecognized" or "Unknown." I wonder if this was the reasoning behind doing it when it was added with the pg_backend_memory_contexts() function. > > + /* > + * Recheck the state of the backend before sleeping on the > condition > + * variable > + */ > + proc = BackendPidGetProc(pid); > Here we are really rechecking that the process is still alive, but I > wonder if > we should take the opportunity to ensure that the type is what we expect > it to > be? If the pid has moved from being a backend to an aux proc or vice > versa we > really don't want to go on. > > The reasoning makes sense to me. For periodic monitoring of all processes, any PID that reincarnates into a different type could be queried in subsequent function calls. Regarding targeted monitoring of a specific process, such a reincarnated process would exhibit a completely different memory consumption, likely not aligning with the user's original intent behind requesting the statistics. > > + ereport(WARNING, > + errmsg("PID %d is not a PostgreSQL server process", > + pid)); > I wonder if we should differentiate between the warnings? When we hit > this in > the loop the errmsg is describing a slightly different case. I did leave > it > for now, but it's food for thought if we should perhaps reword this one. > > Changed it to "PID %d is no longer the same PostgreSQL server process". > > + ereport(LOG, > + errmsg("hash table corrupted, can't construct path > value")); > I know you switched from elog(LOG.. to ereport(LOG.. but I still think a > LOG > entry stating corruption isn't helpful, it's not actionable for the user. > Given that it's a case that shouldn't happen I wonder if we should > downgrade it > to an Assert(false) and potentially a DEBUG1? > > How about changing it to ERROR, in accordance with current occurrences of the same message? I did it in the attached version, however I am open to changing it to an Assert(false) and DEBUG1. Apart from the above, I made the following improvements. 1. Eliminated the unnecessary creation of an extra memory context before calling hash_create. The hash_create function already generates a memory context containing the hash table, enabling easy memory deallocation by simply deleting the context via hash_destroy. Therefore, the patch relies on hash_destroy for memory management instead of manual freeing. 2. Optimized memory usage by storing the path as an array of integers rather than as an array of Datums. This approach conserves DSA memory allocated for storing this information. 3. Miscellaneous comment cleanups and introduced macros to simplify calculations. Thank you, Rahila Syed Attachments: [application/octet-stream] v20-0002-Function-to-report-memory-context-statistics.patch (52.0K, ../../CAH2L28v9EU4dxKUpvMt_CFAzG72CYusPWxADsgT=cwFJP-fP0A@mail.gmail.com/3-v20-0002-Function-to-report-memory-context-statistics.patch) download | inline diff: From 3416b7238a22a1cb70036184f3e589280d92695e Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Wed, 26 Mar 2025 15:08:16 +0530 Subject: [PATCH 2/2] Function to report memory context statistics This function sends a signal to a backend to publish statistics of all its memory contexts. Signal handler running in the backend process, sets a flag, which causes it to copy its MemoryContextStats to a DSA, during the next call to CHECK_FOR_INTERRUPTS(). If there are more statistics than that fit in 16MB, the remaining statistics are copied as a cumulative total of the remaining contexts. Once its done, it signals the client backend using a condition variable. The client backend wakes up, reads the shared memory and returns these values in the form of set of records, one for each memory context, to the user, followed by a cumulative total of the remaining contexts, if any. If get_summary is true return statistics of all children of TopMemoryContext with aggregated statistics of their children. User can pass num_of_tries which determines the total number of wait cycles in a client backend for latest statistics. Each cycle wait timeout is set to 1 seconds. Post this the client displays previously published statistics or returns without results. Each backend and auxiliary process has its own slot for reporting the stats. There is an array of such memory slots of size MaxBackends+NumofAuxiliary processes in fixed shared memory. Each of these slots point to a smaller dsa allocations within a single DSA, which contains the stats to be shared by the corresponding process. Each slot has its own LW lock and condition variable for synchronization and communication between the publishing process and the client backend. --- doc/src/sgml/func.sgml | 79 +++ src/backend/postmaster/autovacuum.c | 4 + src/backend/postmaster/checkpointer.c | 4 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 4 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 4 + src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/storage/lmgr/proc.c | 15 + src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 1 + src/backend/utils/adt/mcxtfuncs.c | 415 +++++++++++++- src/backend/utils/init/globals.c | 1 + src/backend/utils/mmgr/mcxt.c | 529 ++++++++++++++++++ src/include/catalog/pg_proc.dat | 10 + src/include/miscadmin.h | 1 + src/include/storage/procsignal.h | 1 + src/include/utils/memutils.h | 72 +++ src/test/regress/expected/sysviews.out | 14 + src/test/regress/sql/sysviews.sql | 14 + src/tools/pgindent/typedefs.list | 4 + 22 files changed, 1160 insertions(+), 29 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index f8c1deb04e..cfe4badb83 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -28570,6 +28570,52 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres} </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_get_process_memory_contexts</primary> + </indexterm> + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type>, <parameter>get_summary</parameter> <type>boolean</type>, <parameter>num_of_tries</parameter> <type>integer</type> ) + <returnvalue>setof record</returnvalue> + </para> + <para> + This function handles requests to display the memory contexts of a + <productname>PostgreSQL</productname> process with the specified process ID. It takes three + arguments: <parameter>pid</parameter>, <parameter>get_summary</parameter> + and <parameter>num_of_tries</parameter>. The function can send requests + to both backend and auxiliary processes. + </para> + <para> + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context statistics + are aggregated and a cumulative total is displayed. The <literal>num_agg_contexts</literal> + column indicates the number of contexts aggregated in the displayed + statistics. The <literal>num_agg_contexts</literal> value is typically 1, meaning that each + context's statistics are displayed separately. + </para> + <para> + When <parameter>get_summary</parameter> is set to <literal>true</literal>, statistics + for memory contexts at levels 1 and 2 are displayed, with level 1 + representing the root node (i.e., TopMemoryContext). + Each level 2 context's statistics represent an aggregate of all its + child contexts' statistics, with <literal>num_agg_contexts</literal> indicating the number + of these aggregated child contexts. + When <parameter>get_summary</parameter> is set to <literal>false</literal>, the + num_agg_contexts value is 1, indicating that individual statistics are + being displayed. + </para> + <para> + <parameter>num_of_tries</parameter> indicates the number of times + the client will wait for the latest statistics. The wait per try is 1 + second. This parameter can be increased if the user anticipates a delay + in the response from the reporting process. Conversely, if users are + frequently and periodically querying the process for statistics, or if + there are concurrent requests for statistics of the same process, + lowering the parameter might help achieve a faster response. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> @@ -28688,6 +28734,39 @@ LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 because it may generate a large number of log messages. </para> + <para> + <function>pg_get_process_memory_contexts</function> can be used + to request the memory contexts statistics of any postgres process. For example: +<programlisting> +postgres=# SELECT * FROM pg_get_process_memory_contexts( + (SELECT pid FROM pg_stat_activity + WHERE backend_type = 'checkpointer') + , false, 5) LIMIT 1; +-[ RECORD 1 ]----+------------------------------ +name | TopMemoryContext +ident | +type | AllocSet +path | {1} +level | 1 +total_bytes | 90304 +total_nblocks | 3 +free_bytes | 2880 +free_chunks | 1 +used_bytes | 87424 +num_agg_contexts | 1 +stats_timestamp | 2025-03-24 13:55:47.796698+01 +</programlisting> + <note> + <para> + While <function>pg_get_process_memory_contexts</function> can be used to + query memory contexts of the local backend, + <structname>pg_backend_memory_contexts</structname> + (see <xref linkend="view-pg-backend-memory-contexts"/> for more details) + will be less resource intensive when only the local backend is of interest. + </para> + </note> + </para> + </sect2> <sect2 id="functions-admin-backup"> diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 2513a8ef8a..16756152b7 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -781,6 +781,10 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index fda91ffd1c..d3cb3f1891 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -663,6 +663,10 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index 0ae9bf906e..f24f574e74 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -48,6 +48,10 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index 7e622ae4bd..cb7408acf4 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -867,6 +867,10 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index 27e86cf393..7149a67fcb 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -192,6 +192,10 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index 0fec4f1f87..c7a76711cc 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -879,6 +879,10 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index 2fa045e6b0..362a6dc952 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -51,6 +51,7 @@ #include "storage/sinvaladt.h" #include "utils/guc.h" #include "utils/injection_point.h" +#include "utils/memutils.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -343,6 +344,8 @@ CreateOrAttachShmemStructs(void) WaitEventCustomShmemInit(); InjectionPointShmemInit(); AioShmemInit(); + MemCtxShmemInit(); + MemCtxBackendShmemInit(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index 7d20196550..b59034fdc3 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -690,6 +690,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_GET_MEMORY_CONTEXT)) + HandleGetMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index e4ca861a8e..c50dcbc491 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -50,6 +50,7 @@ #include "storage/procsignal.h" #include "storage/spin.h" #include "storage/standby.h" +#include "utils/memutils.h" #include "utils/timeout.h" #include "utils/timestamp.h" @@ -497,6 +498,13 @@ InitProcess(void) */ PGSemaphoreReset(MyProc->sem); + /* + * The before shmem exit callback frees the dsa memory occupied by the + * latest memory context statistics that could be published by this + * backend if requested. + */ + before_shmem_exit(AtProcExit_memstats_dsa_free, 0); + /* * Arrange to clean up at backend exit. */ @@ -671,6 +679,13 @@ InitAuxiliaryProcess(void) */ PGSemaphoreReset(MyProc->sem); + /* + * The before shmem exit callback frees the dsa memory occupied by the + * latest memory context statistics that could be published by this + * process if requested. + */ + before_shmem_exit(AtProcExit_memstats_dsa_free, 0); + /* * Arrange to clean up at process exit. */ diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 4d2edb1065..08d17a1931 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3531,6 +3531,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 9fa12a555e..746496d122 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -160,6 +160,7 @@ WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." +MEM_CTX_PUBLISH "Waiting for a process to publish memory information." ABI_compatibility: diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index 396c2f223b..83f513ad61 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -17,28 +17,26 @@ #include "funcapi.h" #include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "access/twophase.h" +#include "catalog/pg_authid_d.h" +#include "nodes/pg_list.h" #include "storage/proc.h" #include "storage/procarray.h" +#include "utils/acl.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/memutils.h" +#include "utils/wait_event_types.h" /* ---------- * The max bytes for showing identifiers of MemoryContext. * ---------- */ #define MEMORY_CONTEXT_IDENT_DISPLAY_SIZE 1024 - -/* - * MemoryContextId - * Used for storage of transient identifiers for - * pg_get_backend_memory_contexts. - */ -typedef struct MemoryContextId -{ - MemoryContext context; - int context_id; -} MemoryContextId; +struct MemoryContextBackendState *memCtxState = NULL; +struct MemoryContextState *memCtxArea = NULL; /* * int_list_to_array @@ -143,24 +141,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, else nulls[1] = true; - switch (context->type) - { - case T_AllocSetContext: - type = "AllocSet"; - break; - case T_GenerationContext: - type = "Generation"; - break; - case T_SlabContext: - type = "Slab"; - break; - case T_BumpContext: - type = "Bump"; - break; - default: - type = "???"; - break; - } + type = ContextTypeToString(context->type); values[2] = CStringGetTextDatum(type); values[3] = Int32GetDatum(list_length(path)); /* level */ @@ -175,6 +156,32 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, list_free(path); } +const char * +ContextTypeToString(NodeTag type) +{ + const char *context_type; + + switch (type) + { + case T_AllocSetContext: + context_type = "AllocSet"; + break; + case T_GenerationContext: + context_type = "Generation"; + break; + case T_SlabContext: + context_type = "Slab"; + break; + case T_BumpContext: + context_type = "Bump"; + break; + default: + context_type = "???"; + break; + } + return (context_type); +} + /* * pg_get_backend_memory_contexts * SQL SRF showing backend memory context. @@ -305,3 +312,353 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } + +/* + * pg_get_process_memory_contexts + * Signal a backend or an auxiliary process to send its memory contexts, + * wait for the results and display them. + * + * By default, only superusers or users with PG_READ_ALL_STATS are allowed to + * signal a process to return the memory contexts. This is because allowing + * any users to issue this request at an unbounded rate would cause lots of + * requests to be sent, which can lead to denial of service. Additional roles + * can be permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to copy the memory context details + * to a dynamic shared memory space. + * + * We have defined a limit on dsa memory that could be allocated per process - + * if the process has more memory contexts than what can fit in the allocated + * size, the excess contexts are summarized and represented as cumulative total + * at the end of the buffer. + * + * After sending the signal, wait on a condition variable. The publishing + * backend, after copying the data to shared memory, sends signal on that + * condition variable. There is one condition variable per publishing + * backend. + * Once condition variable is signalled, check if the latest memory context + * information is available and display. + * + * If the publishing backend does not respond before the condition variable + * times out, which is set to MEMSTATS_WAIT_TIMEOUT, retry for max_tries + * number of times, which is defined by user, before giving up and + * returning previously published statistics, if any. If previous statistics + * do not exist, return NULL. + */ +Datum +pg_get_process_memory_contexts(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + bool get_summary = PG_GETARG_BOOL(1); + PGPROC *proc; + ProcNumber procNumber = INVALID_PROC_NUMBER; + bool proc_is_aux = false; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + dsa_area *area; + MemoryContextEntry *memctx_info; + int num_retries = 0; + TimestampTz curr_timestamp; + int max_tries = PG_GETARG_INT32(2); + + /* + * Only superusers or users with pg_read_all_stats privileges can view the + * memory context statistics of another process + */ + if (!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("permission denied to extract memory context statistics")); + + InitMaterializedSRF(fcinfo, 0); + + /* + * See if the process with given pid is a backend or an auxiliary process + * and remember the type for when we requery the process later. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + { + proc = AuxiliaryPidGetProc(pid); + proc_is_aux = true; + } + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; this is however not a problem and leave with a WARNING. + * See comment in pg_log_backend_memory_contexts for a discussion on this. + */ + if (proc == NULL) + { + /* + * This is just a warning so a loop-through-resultset will not abort + * if one backend terminated on its own during the run. + */ + ereport(WARNING, + (errmsg("PID %d is not a PostgreSQL server process", pid))); + PG_RETURN_NULL(); + } + + procNumber = GetNumberFromPGProc(proc); + + LWLockAcquire(&memCtxState[procNumber].lw_lock, LW_EXCLUSIVE); + memCtxState[procNumber].get_summary = get_summary; + LWLockRelease(&memCtxState[procNumber].lw_lock); + + curr_timestamp = GetCurrentTimestamp(); + + /* + * Send a signal to a PostgreSQL process, informing it we want it to + * produce information about its memory contexts. + */ + if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, procNumber) < 0) + { + ereport(WARNING, + errmsg("could not send signal to process %d: %m", pid)); + PG_RETURN_NULL(); + } + + /* + * A valid DSA pointer isn't proof that statistics are available, it can + * be valid due to previously published stats. Check if the stats are + * updated by comparing the timestamp, if the stats are newer than our + * previously recorded timestamp from before sending the procsignal, they + * must by definition be updated. Wait for max_tries * + * MEMSTATS_WAIT_TIMEOUT, following which display old statistics if + * available or return NULL. + */ + while (1) + { + long msecs; + + /* + * We expect to come out of sleep when the requested process has + * finished publishing the statistics, verified using the valid dsa + * pointer. + * + * Make sure that the information belongs to pid we requested + * information for, Otherwise loop back and wait for the server + * process to finish publishing statistics. + */ + LWLockAcquire(&memCtxState[procNumber].lw_lock, LW_EXCLUSIVE); + + /* + * Note in procnumber.h file says that a procNumber can be re-used for + * a different backend immediately after a backend exits. In case an + * old process' data was there and not updated by the current process + * in the slot identified by the procNumber, the pid of the requested + * process and the proc_id might not match. + */ + if (memCtxState[procNumber].proc_id == pid) + { + /* + * Break if the latest stats have been read, indicated by + * statistics timestamp being newer than the current request + * timestamp. + */ + msecs = TimestampDifferenceMilliseconds(curr_timestamp, + memCtxState[procNumber].stats_timestamp); + + if (DsaPointerIsValid(memCtxState[procNumber].memstats_dsa_pointer) + && msecs > 0) + break; + } + LWLockRelease(&memCtxState[procNumber].lw_lock); + + /* + * Recheck the state of the backend before sleeping on the condition + * variable to ensure the process is still alive. Only check the + * relevant process type based on the earlier PID check. + */ + if (proc_is_aux) + proc = AuxiliaryPidGetProc(pid); + else + proc = BackendPidGetProc(pid); + + /* + * The process ending during memory context processing is not an + * error. + */ + if (proc == NULL) + { + ereport(WARNING, + errmsg("PID %d is no longer the same PostgreSQL server process", pid)); + PG_RETURN_NULL(); + } + +#define MEMSTATS_WAIT_TIMEOUT 1000 + + if (ConditionVariableTimedSleep(&memCtxState[procNumber].memctx_cv, + MEMSTATS_WAIT_TIMEOUT, + WAIT_EVENT_MEM_CTX_PUBLISH)) + { + /* + * Wait for max_tries defined by user, display previously + * published statistics if any, when max_tries are over. + */ + if (num_retries > max_tries) + { + LWLockAcquire(&memCtxState[procNumber].lw_lock, LW_EXCLUSIVE); + /* Displaying previously published statistics */ + if (DsaPointerIsValid(memCtxState[procNumber].memstats_dsa_pointer)) + break; + else + { + LWLockRelease(&memCtxState[procNumber].lw_lock); + PG_RETURN_NULL(); + } + } + ereport(DEBUG1, + errmsg("timed out waiting for process with PID %d to publish stats, retrying", + pid)); + num_retries = num_retries + 1; + } + } + /* We should land here only with a valid DSA handle */ + Assert(memCtxArea->memstats_dsa_handle != DSA_HANDLE_INVALID); + area = dsa_attach(memCtxArea->memstats_dsa_handle); + + /* + * Backend has finished publishing the stats, read them + * + * Read statistics of top level 1 and 2 contexts, if get_summary is true. + */ + memctx_info = (MemoryContextEntry *) + dsa_get_address(area, memCtxState[procNumber].memstats_dsa_pointer); + +#define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 12 + for (int i = 0; i < memCtxState[procNumber].total_stats; i++) + { + ArrayType *path_array; + int path_length; + Datum values[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + bool nulls[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + char *name; + char *ident; + Datum *path_datum = NULL; + int *path_int = NULL; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + if (DsaPointerIsValid(memctx_info[i].name)) + { + name = (char *) dsa_get_address(area, memctx_info[i].name); + values[0] = CStringGetTextDatum(name); + } + else + nulls[0] = true; + + if (DsaPointerIsValid(memctx_info[i].ident)) + { + ident = (char *) dsa_get_address(area, memctx_info[i].ident); + values[1] = CStringGetTextDatum(ident); + } + else + nulls[1] = true; + + if (memctx_info[i].type != NULL) + values[2] = CStringGetTextDatum(memctx_info[i].type); + else + nulls[2] = true; + + path_length = memctx_info[i].path_length; + path_datum = (Datum *) palloc(path_length * sizeof(Datum)); + if (DsaPointerIsValid(memctx_info[i].path)) + { + path_int = (int *) dsa_get_address(area, memctx_info[i].path); + for (int j = 0; j < path_length; j++) + path_datum[j] = Int32GetDatum(path_int[j]); + path_array = construct_array_builtin(path_datum, + path_length, INT4OID); + values[3] = PointerGetDatum(path_array); + } + else + nulls[3] = true; + + values[4] = Int32GetDatum(path_length); /* level */ + values[5] = Int64GetDatum(memctx_info[i].totalspace); + values[6] = Int64GetDatum(memctx_info[i].nblocks); + values[7] = Int64GetDatum(memctx_info[i].freespace); + values[8] = Int64GetDatum(memctx_info[i].freechunks); + values[9] = Int64GetDatum(memctx_info[i].totalspace - + memctx_info[i].freespace); + values[10] = Int32GetDatum(memctx_info[i].num_agg_stats); + values[11] = TimestampTzGetDatum(memCtxState[procNumber].stats_timestamp); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + LWLockRelease(&memCtxState[procNumber].lw_lock); + + ConditionVariableCancelSleep(); + dsa_detach(area); + + PG_RETURN_NULL(); +} + +/* + * Init shared memory for reporting memory context information. + */ +void +MemCtxBackendShmemInit(void) +{ + bool found; + Size TotalProcs; + + TotalProcs = add_size(MaxBackends, NUM_AUXILIARY_PROCS); + TotalProcs = add_size(TotalProcs, max_prepared_xacts); + + memCtxState = (MemoryContextBackendState *) + ShmemInitStruct("MemoryContextBackendState", + mul_size(TotalProcs, sizeof(MemoryContextBackendState)), + &found); + + if (!IsUnderPostmaster) + { + Assert(!found); + + for (int i = 0; i < TotalProcs; i++) + { + ConditionVariableInit(&memCtxState[i].memctx_cv); + + LWLockInitialize(&memCtxState[i].lw_lock, LWLockNewTrancheId()); + LWLockRegisterTranche(memCtxState[i].lw_lock.tranche, + "mem_context_backend_stats_reporting"); + + memCtxState[i].memstats_dsa_pointer = InvalidDsaPointer; + } + } + else + { + Assert(found); + } +} + +/* + * Initialize shared memory for displaying memory context statistics + */ +void +MemCtxShmemInit(void) +{ + bool found; + + memCtxArea = (MemoryContextState *) + ShmemInitStruct("MemoryContextState", sizeof(MemoryContextState), &found); + + if (!IsUnderPostmaster) + { + Assert(!found); + + LWLockInitialize(&memCtxArea->lw_lock, + LWLockNewTrancheId()); + LWLockRegisterTranche(memCtxArea->lw_lock.tranche, + "mem_context_stats_reporting"); + memCtxArea->memstats_dsa_handle = DSA_HANDLE_INVALID; + } + else + { + Assert(found); + } +} diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index b844f9fdae..13938ccb0f 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -38,6 +38,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t PublishMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 05af956930..3979e121ea 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -23,6 +23,13 @@ #include "mb/pg_wchar.h" #include "miscadmin.h" +#include "nodes/pg_list.h" +#include "storage/fd.h" +#include "storage/lwlock.h" +#include "storage/dsm.h" +#include "storage/ipc.h" +#include "utils/dsa.h" +#include "utils/hsearch.h" #include "utils/memdebug.h" #include "utils/memutils.h" #include "utils/memutils_internal.h" @@ -177,6 +184,16 @@ static void MemoryContextStatsInternal(MemoryContext context, int level, static void MemoryContextStatsPrint(MemoryContext context, void *passthru, const char *stats_string, bool print_to_stderr); +static void PublishMemoryContext(MemoryContextEntry *memctx_infos, + int curr_id, MemoryContext context, + List *path, + MemoryContextCounters stat, + int num_contexts, dsa_area *area); +static void compute_contexts_count_and_ids(List *contexts, HTAB *context_id_lookup, + int *stats_count, + bool get_summary); +static List *compute_context_path(MemoryContext c, HTAB *context_id_lookup); +static void dsa_free_previous_stats(dsa_area *area, int total_stats, dsa_pointer prev_dsa_pointer); /* * You should not do memory allocations within a critical section, because @@ -1331,6 +1348,21 @@ HandleLogMemoryContextInterrupt(void) /* latch will be set by procsignal_sigusr1_handler */ } +/* + * HandleGetMemoryContextInterrupt + * Handle receipt of an interrupt indicating publishing of memory + * contexts. + * + * All the actual work is deferred to ProcessLogMemoryContextInterrupt() + */ +void +HandleGetMemoryContextInterrupt(void) +{ + InterruptPending = true; + PublishMemoryContextPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + /* * ProcessLogMemoryContextInterrupt * Perform logging of memory contexts of this backend process. @@ -1368,6 +1400,503 @@ ProcessLogMemoryContextInterrupt(void) MemoryContextStatsDetail(TopMemoryContext, 100, 100, false); } +/* + * ProcessGetMemoryContextInterrupt + * Generate information about memory contexts used by the process. + * + * Performs a breadth first search on the memory context tree, thus parents + * statistics are reported before their children in the monitoring function + * output. + * + * Statistics for all the processes are shared via the same dynamic shared area. + * Statistics written by each process are tracked independently in per-process + * dsa pointers. These pointers are stored in static shared memory. + * + * We calculate maximum number of context's statistics that can be displayed + * using a pre-determined limit for memory available per process for this utility + * maximum size of statistics for each context. + * The remaining context statistics if any are captured as a cumulative total + * at the end of individual context's statistics. + * + * If get_summary is true, we capture the level 1 and level 2 contexts statistics. + * For that we traverse the memory context tree recursively in depth first search + * manner to cover all the children of a parent context, to be able to display a + * cumulative total of memory consumption by a parent at level 2 and all its + * children. + */ +void +ProcessGetMemoryContextInterrupt(void) +{ + List *contexts; + + HASHCTL ctl; + HTAB *context_id_lookup; + int context_id = 0; + MemoryContextEntry *meminfo; + bool get_summary = false; + + dsa_area *area = NULL; + int max_stats; + int idx = MyProcNumber; + int stats_count = 0; + MemoryContextCounters stat; + int num_individual_stats = 0; + + PublishMemoryContextPending = false; + + /* + * The hash table is used for constructing "path" column of the view, + * similar to its local backend counterpart. + */ + ctl.keysize = sizeof(MemoryContext); + ctl.entrysize = sizeof(MemoryContextId); + ctl.hcxt = CurrentMemoryContext; + + context_id_lookup = hash_create("pg_get_remote_backend_memory_contexts", + 256, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + /* List of contexts to process in the next round - start at the top. */ + contexts = list_make1(TopMemoryContext); + + /* Compute the number of stats that can fit in the defined limit */ + max_stats = (MAX_SEGMENTS_PER_BACKEND * DSA_DEFAULT_INIT_SEGMENT_SIZE) + / (MAX_MEMORY_CONTEXT_STATS_SIZE); + LWLockAcquire(&memCtxState[idx].lw_lock, LW_EXCLUSIVE); + get_summary = memCtxState[idx].get_summary; + LWLockRelease(&memCtxState[idx].lw_lock); + + /* + * Traverse the memory context tree to find total number of contexts. If + * summary is requested report the total number of contexts at level 1 and + * 2 from the top. Also, populate the hash table of context ids. + */ + compute_contexts_count_and_ids(contexts, context_id_lookup, &stats_count, + get_summary); + + /* + * Allocate memory in this process's dsa for storing statistics of the the + * memory contexts upto max_stats, for contexts that don't fit within a + * limit, a cumulative total is written as the last record in the DSA + * segment. + */ + stats_count = (stats_count > max_stats) ? max_stats : stats_count; + + LWLockAcquire(&memCtxArea->lw_lock, LW_EXCLUSIVE); + + /* + * Create a DSA and send handle to the the client process after storing + * the context statistics. If number of contexts exceed a predefined + * limit(8MB), a cumulative total is stored for such contexts. + */ + if (memCtxArea->memstats_dsa_handle == DSA_HANDLE_INVALID) + { + MemoryContext oldcontext = CurrentMemoryContext; + dsa_handle handle; + + MemoryContextSwitchTo(TopMemoryContext); + + area = dsa_create(memCtxArea->lw_lock.tranche); + + handle = dsa_get_handle(area); + MemoryContextSwitchTo(oldcontext); + + dsa_pin_mapping(area); + + /* + * Pin the dsa area, this is to make sure the area remains attachable + * even if current backend exits. This is done so that a waiting + * client gets the stats even after a process exits. + */ + dsa_pin(area); + + /* Set the handle in shared memory */ + memCtxArea->memstats_dsa_handle = handle; + } + + /* + * If DSA exists, created by another process publishing statistics, or by + * the previous execution of this function by this process, attach to it. + */ + else if (area == NULL) + { + MemoryContext oldcontext = CurrentMemoryContext; + + MemoryContextSwitchTo(TopMemoryContext); + area = dsa_attach(memCtxArea->memstats_dsa_handle); + MemoryContextSwitchTo(oldcontext); + dsa_pin_mapping(area); + } + LWLockRelease(&memCtxArea->lw_lock); + + /* + * Hold the process lock to protect writes to process specific memory. Two + * processes publishing statistics do not block each other. + */ + LWLockAcquire(&memCtxState[idx].lw_lock, LW_EXCLUSIVE); + memCtxState[idx].proc_id = MyProcPid; + + if (DsaPointerIsValid(memCtxState[idx].memstats_dsa_pointer)) + { + /* + * Free any previous allocations, free the name, ident and path + * pointers before freeing the pointer that contains them. + */ + dsa_free_previous_stats(area, memCtxState[idx].total_stats, + memCtxState[idx].memstats_dsa_pointer); + + dsa_free(area, memCtxState[idx].memstats_dsa_pointer); + memCtxState[idx].memstats_dsa_pointer = InvalidDsaPointer; + } + memCtxState[idx].memstats_dsa_pointer = dsa_allocate0(area, + stats_count * sizeof(MemoryContextEntry)); + + meminfo = (MemoryContextEntry *) dsa_get_address(area, + memCtxState[idx].memstats_dsa_pointer); + + if (get_summary) + { + int ctx_id = 0; + List *path = NIL; + + /* Copy TopMemoryContext statistics to DSA */ + memset(&stat, 0, sizeof(stat)); + (*TopMemoryContext->methods->stats) (TopMemoryContext, NULL, NULL, + &stat, true); + path = lcons_int(1, path); + PublishMemoryContext(meminfo, ctx_id, TopMemoryContext, path, stat, + 1, area); + ctx_id = ctx_id + 1; + + /* + * Copy statistics for each of TopMemoryContexts children. This + * includes statistics of all of their children upto level 100. + */ + for (MemoryContext c = TopMemoryContext->firstchild; c != NULL; + c = c->nextchild) + { + MemoryContextCounters grand_totals; + int num_contexts = 0; + + path = NIL; + memset(&grand_totals, 0, sizeof(grand_totals)); + + MemoryContextStatsInternal(c, 0, 100, 100, &grand_totals, + PRINT_STATS_NONE, &num_contexts); + + path = compute_context_path(c, context_id_lookup); + + PublishMemoryContext(meminfo, ctx_id, c, path, + grand_totals, num_contexts, area); + ctx_id = ctx_id + 1; + } + memCtxState[idx].total_stats = ctx_id; + goto cleanup; + } + + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + char *name; + + /* + * Figure out the transient context_id of this context and each of its + * ancestors, to compute a path for this context. + */ + path = compute_context_path(cur, context_id_lookup); + + if (context_id <= (max_stats - 2)) + { + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1, area); + } + else + { + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + meminfo[max_stats - 1].totalspace += stat.totalspace; + meminfo[max_stats - 1].nblocks += stat.nblocks; + meminfo[max_stats - 1].freespace += stat.freespace; + meminfo[max_stats - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit per process is reached, write aggregate of the + * remaining statistics. + */ + if (context_id == (max_stats - 2) && context_id < (stats_count - 1)) + { + num_individual_stats = context_id + 1; + meminfo[max_stats - 1].name = dsa_allocate0(area, 17); + name = dsa_get_address(area, meminfo[max_stats - 1].name); + strncpy(name, "Remaining Totals", 16); + meminfo[max_stats - 1].ident = InvalidDsaPointer; + meminfo[max_stats - 1].path = InvalidDsaPointer; + meminfo[max_stats - 1].type = NULL; + } + context_id++; + } + /* Statistics are not aggregated, i.e individual statistics reported */ + if (context_id < (max_stats - 2)) + { + memCtxState[idx].total_stats = context_id; + } + /* Report number of aggregated memory contexts */ + else + { + meminfo[max_stats - 1].num_agg_stats = context_id - + num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for cumulative + * statistics. + */ + memCtxState[idx].total_stats = num_individual_stats + 1; + } + +cleanup: + + /* + * Signal all the waiting client backends after copying all the statistics + */ + memCtxState[idx].stats_timestamp = GetCurrentTimestamp(); + LWLockRelease(&memCtxState[idx].lw_lock); + ConditionVariableBroadcast(&memCtxState[idx].memctx_cv); + hash_destroy(context_id_lookup); + dsa_detach(area); +} + +/* + * Append the transient context_id of this context and each of its ancestors + * to a list, in order to compute a path. + */ +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + + for (MemoryContext cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + { + ereport(ERROR, + errmsg("hash table corrupted, can't construct path value")); + break; + } + path = lcons_int(cur_entry->context_id, path); + } + return path; +} + +/* + * Return the number of contexts allocated currently by the backend + * Assign context ids to each of the contexts. + */ +static void +compute_contexts_count_and_ids(List *contexts, HTAB *context_id_lookup, + int *stats_count, bool get_summary) +{ + foreach_ptr(MemoryContextData, cur, contexts) + { + MemoryContextId *entry; + bool found; + + entry = (MemoryContextId *) hash_search(context_id_lookup, &cur, + HASH_ENTER, &found); + Assert(!found); + + /* context id starts with 1 */ + entry->context_id = (++(*stats_count)); + + /* Append the children of the current context to the main list. */ + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + { + if (get_summary) + { + entry = (MemoryContextId *) hash_search(context_id_lookup, &c, + HASH_ENTER, &found); + Assert(!found); + + entry->context_id = (++(*stats_count)); + } + + contexts = lappend(contexts, c); + } + + /* + * In summary only the first two level(from top) contexts are + * displayed + */ + if (get_summary) + break; + } + +} + +/* Copy the memory context statistics of a single context to a dsa memory */ +static void +PublishMemoryContext(MemoryContextEntry *memctx_info, int curr_id, + MemoryContext context, List *path, + MemoryContextCounters stat, int num_contexts, + dsa_area *area) +{ + char clipped_ident[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + char *name; + char *ident; + int *path_list; + + Assert(MemoryContextIsValid(context)); + + if (context->name != NULL) + { + Assert(strlen(context->name) < MEMORY_CONTEXT_IDENT_SHMEM_SIZE); + memctx_info[curr_id].name = dsa_allocate0(area, strlen(context->name) + 1); + name = (char *) dsa_get_address(area, memctx_info[curr_id].name); + strncpy(name, context->name, strlen(context->name)); + } + else + memctx_info[curr_id].name = InvalidDsaPointer; + + /* Trim and copy the identifier if it is not set to NULL */ + if (context->ident != NULL) + { + int idlen = strlen(context->ident); + + /* + * Some identifiers such as SQL query string can be very long, + * truncate oversize identifiers. + */ + if (idlen >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + idlen = pg_mbcliplen(context->ident, idlen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + memcpy(clipped_ident, context->ident, idlen); + clipped_ident[idlen] = '\0'; + + /* + * To be consistent with logging output, we label dynahash contexts + * with just the hash table name as with MemoryContextStatsPrint(). + */ + if (!strncmp(context->name, "dynahash", 8)) + { + dsa_free(area, memctx_info[curr_id].name); + memctx_info[curr_id].name = dsa_allocate0(area, + strlen(clipped_ident) + 1); + name = (char *) dsa_get_address(area, + memctx_info[curr_id].name); + strlcpy(name, + clipped_ident, idlen + 1); + memctx_info[curr_id].ident = InvalidDsaPointer; + } + else + { + + memctx_info[curr_id].ident = dsa_allocate0(area, + strlen(clipped_ident) + 1); + ident = (char *) dsa_get_address(area, + memctx_info[curr_id].ident); + strlcpy(ident, + clipped_ident, idlen + 1); + } + } + else + memctx_info[curr_id].ident = InvalidDsaPointer; + + /* Allocate dsa memory for storing path information */ + if (path == NIL) + memctx_info[curr_id].path = InvalidDsaPointer; + else + { + memctx_info[curr_id].path_length = list_length(path); + memctx_info[curr_id].path = dsa_allocate0(area, + memctx_info[curr_id].path_length + * sizeof(int)); + path_list = (int *) dsa_get_address(area, memctx_info[curr_id].path); + foreach_int(i, path) + path_list[foreach_current_index(i)] = i; + } + memctx_info[curr_id].type = ContextTypeToString(context->type); + memctx_info[curr_id].totalspace = stat.totalspace; + memctx_info[curr_id].nblocks = stat.nblocks; + memctx_info[curr_id].freespace = stat.freespace; + memctx_info[curr_id].freechunks = stat.freechunks; + memctx_info[curr_id].num_agg_stats = num_contexts; +} + +static void +dsa_free_previous_stats(dsa_area *area, int total_stats, + dsa_pointer prev_dsa_pointer) +{ + MemoryContextEntry *meminfo; + + meminfo = (MemoryContextEntry *) dsa_get_address(area, prev_dsa_pointer); + for (int i = 0; i < total_stats; i++) + { + if (DsaPointerIsValid(meminfo[i].name)) + dsa_free(area, meminfo[i].name); + + if (DsaPointerIsValid(meminfo[i].ident)) + dsa_free(area, meminfo[i].ident); + + if (DsaPointerIsValid(meminfo[i].path)) + dsa_free(area, meminfo[i].path); + } +} + +/* + * Free the memory context statistics stored by this process + * in dsa area. + */ +void +AtProcExit_memstats_dsa_free(int code, Datum arg) +{ + int idx = MyProcNumber; + dsm_segment *dsm_seg = NULL; + dsa_area *area = NULL; + + if (memCtxArea->memstats_dsa_handle == DSA_HANDLE_INVALID) + return; + + dsm_seg = dsm_find_mapping(memCtxArea->memstats_dsa_handle); + + LWLockAcquire(&memCtxState[idx].lw_lock, LW_EXCLUSIVE); + + if (!DsaPointerIsValid(memCtxState[idx].memstats_dsa_pointer)) + { + LWLockRelease(&memCtxState[idx].lw_lock); + return; + } + + /* If the dsm mapping could not be found, attach to the area */ + if (dsm_seg != NULL) + return; + area = dsa_attach(memCtxArea->memstats_dsa_handle); + + /* + * Free the memory context statistics, free the name, ident and path + * pointers before freeing the pointer that contains these pointers and + * integer statistics. + */ + dsa_free_previous_stats(area, memCtxState[idx].total_stats, + memCtxState[idx].memstats_dsa_pointer); + dsa_free(area, memCtxState[idx].memstats_dsa_pointer); + memCtxState[idx].memstats_dsa_pointer = InvalidDsaPointer; + + dsa_detach(area); + LWLockRelease(&memCtxState[idx].lw_lock); +} + void * palloc(Size size) { diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 3f7b82e02b..760d345820 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8509,6 +8509,16 @@ prorettype => 'bool', proargtypes => 'int4', prosrc => 'pg_log_backend_memory_contexts' }, +# publishing memory contexts of the specified postgres process +{ oid => '2173', descr => 'publish memory contexts of the specified backend', + proname => 'pg_get_process_memory_contexts', provolatile => 'v', + prorows => '100', proretset => 't', proparallel => 'r', + prorettype => 'record', proargtypes => 'int4 bool int4', + proallargtypes => '{int4,bool,int4,text,text,text,_int4,int4,int8,int8,int8,int8,int8,int4,timestamptz}', + proargmodes => '{i,i,i,o,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{oid, summary, num_of_tries, name, ident, type, path, level, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes, num_agg_contexts, stats_timestamp}', + prosrc => 'pg_get_process_memory_contexts' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index 603d042435..d3c44df6e1 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -96,6 +96,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t PublishMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index 022fd8ed93..5d4b2fbfc9 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -35,6 +35,7 @@ typedef enum PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to send the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ /* Recovery conflict reasons */ diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 8abc26abce..ecbdd69597 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -18,6 +18,9 @@ #define MEMUTILS_H #include "nodes/memnodes.h" +#include "storage/condition_variable.h" +#include "storage/lmgr.h" +#include "utils/dsa.h" /* @@ -48,7 +51,12 @@ #define AllocHugeSizeIsValid(size) ((Size) (size) <= MaxAllocHugeSize) +#define MEMORY_CONTEXT_IDENT_SHMEM_SIZE 128 +#define MEM_CONTEXT_MAX_LEVEL 64 +#define MAX_SEGMENTS_PER_BACKEND 8 +#define MEM_CONTEXT_PATH_SIZE MEM_CONTEXT_MAX_LEVEL * sizeof(int) +#define MAX_MEMORY_CONTEXT_STATS_SIZE sizeof(MemoryContextEntry) + MEM_CONTEXT_PATH_SIZE + 2 * MEMORY_CONTEXT_IDENT_SHMEM_SIZE /* * Standard top-level memory contexts. * @@ -319,4 +327,68 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) return true; } +/* Dynamic shared memory state for statistics per context */ +typedef struct MemoryContextEntry +{ + dsa_pointer name; + dsa_pointer ident; + dsa_pointer path; + const char *type; + int path_length; + int64 totalspace; + int64 nblocks; + int64 freespace; + int64 freechunks; + int num_agg_stats; +} MemoryContextEntry; + +/* + * Static shared memory state representing the DSA area + * created for memory context statistics reporting. + * Single DSA area is created and used by all the processes, + * each having its specific dsa allocations for sharing memory + * statistics, tracked by per backend static shared memory state. + */ +typedef struct MemoryContextState +{ + dsa_handle memstats_dsa_handle; + LWLock lw_lock; +} MemoryContextState; + +/* + * Per backend static shared memory state for memory + * context statistics reporting. + */ +typedef struct MemoryContextBackendState +{ + ConditionVariable memctx_cv; + LWLock lw_lock; + int proc_id; + int total_stats; + bool get_summary; + dsa_pointer memstats_dsa_pointer; + TimestampTz stats_timestamp; +} MemoryContextBackendState; + + +/* + * MemoryContextId + * Used for storage of transient identifiers for + * pg_get_backend_memory_contexts. + */ +typedef struct MemoryContextId +{ + MemoryContext context; + int context_id; +} MemoryContextId; + +extern PGDLLIMPORT MemoryContextBackendState *memCtxState; +extern PGDLLIMPORT MemoryContextState *memCtxArea; +extern void ProcessGetMemoryContextInterrupt(void); +extern const char *ContextTypeToString(NodeTag type); +extern void HandleGetMemoryContextInterrupt(void); +extern void MemCtxShmemInit(void); +extern void MemCtxBackendShmemInit(void); +extern void AtProcExit_memstats_dsa_free(int code, Datum arg); + #endif /* MEMUTILS_H */ diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 83228cfca2..dca20ae1a2 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -232,3 +232,17 @@ select * from pg_timezone_abbrevs where abbrev = 'LMT'; LMT | @ 7 hours 52 mins 58 secs ago | f (1 row) +DO $$ +DECLARE + launcher_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='autovacuum launcher' + INTO launcher_pid; + + select type, name, ident + from pg_get_process_memory_contexts(launcher_pid, false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; +NOTICE: (AllocSet,TopMemoryContext,) diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 66179f026b..4767351d4e 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -101,3 +101,17 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs; -- One specific case we can check without much fear of breakage -- is the historical local-mean-time value used for America/Los_Angeles. select * from pg_timezone_abbrevs where abbrev = 'LMT'; + +DO $$ +DECLARE + launcher_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='autovacuum launcher' + INTO launcher_pid; + + select type, name, ident + from pg_get_process_memory_contexts(launcher_pid, false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 3fbf5a4c21..6cd7a30e0b 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1649,12 +1649,16 @@ MemoizeState MemoizeTuple MemoryChunk MemoryContext +MemoryContextBackendState MemoryContextCallback MemoryContextCallbackFunction MemoryContextCounters MemoryContextData +MemoryContextEntry +MemoryContextId MemoryContextMethodID MemoryContextMethods +MemoryContextState MemoryStatsPrintFunc MergeAction MergeActionState -- 2.34.1 [application/octet-stream] v20-0001-Preparatory-changes-for-reporting-memory-context-sta.patch (5.1K, ../../CAH2L28v9EU4dxKUpvMt_CFAzG72CYusPWxADsgT=cwFJP-fP0A@mail.gmail.com/4-v20-0001-Preparatory-changes-for-reporting-memory-context-sta.patch) download | inline diff: From 4d534f76678f0eb0fce00d68c520773d18d3295f Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Wed, 26 Mar 2025 14:38:39 +0530 Subject: [PATCH 1/2] Preparatory changes for reporting memory context statistics Ensure that MemoryContextStatsInternal can return number of contexts. Also, provide an option in MemoryContextStatsInternal to return without printing stats to either stderr or logs. --- src/backend/utils/mmgr/mcxt.c | 77 ++++++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 11 deletions(-) diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 91060de0ab..05af956930 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -135,6 +135,17 @@ static const MemoryContextMethods mcxt_methods[] = { }; #undef BOGUS_MCTX +/* + * This is passed to MemoryContextStatsInternal to determine whether + * to print context statistics or not and where to print them logs or + * stderr. + */ +typedef enum PrintDestination +{ + PRINT_STATS_TO_STDERR = 0, + PRINT_STATS_TO_LOGS, + PRINT_STATS_NONE +} PrintDestination; /* * CurrentMemoryContext @@ -162,7 +173,7 @@ static void MemoryContextCallResetCallbacks(MemoryContext context); static void MemoryContextStatsInternal(MemoryContext context, int level, int max_level, int max_children, MemoryContextCounters *totals, - bool print_to_stderr); + PrintDestination print_location, int *num_contexts); static void MemoryContextStatsPrint(MemoryContext context, void *passthru, const char *stats_string, bool print_to_stderr); @@ -831,11 +842,19 @@ MemoryContextStatsDetail(MemoryContext context, bool print_to_stderr) { MemoryContextCounters grand_totals; + int num_contexts; + PrintDestination print_location; memset(&grand_totals, 0, sizeof(grand_totals)); + if (print_to_stderr) + print_location = PRINT_STATS_TO_STDERR; + else + print_location = PRINT_STATS_TO_LOGS; + + /* num_contexts report number of contexts aggregated in the output */ MemoryContextStatsInternal(context, 0, max_level, max_children, - &grand_totals, print_to_stderr); + &grand_totals, print_location, &num_contexts); if (print_to_stderr) fprintf(stderr, @@ -870,13 +889,14 @@ MemoryContextStatsDetail(MemoryContext context, * One recursion level for MemoryContextStats * * Print stats for this context if possible, but in any case accumulate counts - * into *totals (if not NULL). + * into *totals (if not NULL). The callers should make sure that print_location + * is set to PRINT_STATS_STDERR or PRINT_STATS_TO_LOGS or PRINT_STATS_NONE. */ static void MemoryContextStatsInternal(MemoryContext context, int level, int max_level, int max_children, MemoryContextCounters *totals, - bool print_to_stderr) + PrintDestination print_location, int *num_contexts) { MemoryContext child; int ichild; @@ -884,10 +904,39 @@ MemoryContextStatsInternal(MemoryContext context, int level, Assert(MemoryContextIsValid(context)); /* Examine the context itself */ - context->methods->stats(context, - MemoryContextStatsPrint, - &level, - totals, print_to_stderr); + switch (print_location) + { + case PRINT_STATS_TO_STDERR: + context->methods->stats(context, + MemoryContextStatsPrint, + &level, + totals, true); + break; + + case PRINT_STATS_TO_LOGS: + context->methods->stats(context, + MemoryContextStatsPrint, + &level, + totals, false); + break; + + case PRINT_STATS_NONE: + + /* + * Do not print the statistics if print_location is + * PRINT_STATS_NONE, only compute totals. This is used in + * reporting of memory context statistics via a sql function. Last + * parameter is not relevant. ++ */ + context->methods->stats(context, + NULL, + NULL, + totals, false); + break; + } + + /* Increment the context count for each of the recursive call */ + *num_contexts = *num_contexts + 1; /* * Examine children. @@ -907,7 +956,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, MemoryContextStatsInternal(child, level + 1, max_level, max_children, totals, - print_to_stderr); + print_location, num_contexts); } } @@ -926,7 +975,13 @@ MemoryContextStatsInternal(MemoryContext context, int level, child = MemoryContextTraverseNext(child, context); } - if (print_to_stderr) + /* + * Add the count of children contexts which are traversed in the + * non-recursive manner. + */ + *num_contexts = *num_contexts + ichild; + + if (print_location == PRINT_STATS_TO_STDERR) { for (int i = 0; i <= level; i++) fprintf(stderr, " "); @@ -939,7 +994,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, local_totals.freechunks, local_totals.totalspace - local_totals.freespace); } - else + else if (print_location == PRINT_STATS_TO_LOGS) ereport(LOG_SERVER_ONLY, (errhidestmt(true), errhidecontext(true), -- 2.34.1 ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-04-02 21:44 Daniel Gustafsson <[email protected]> parent: Rahila Syed <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Daniel Gustafsson @ 2025-04-02 21:44 UTC (permalink / raw) To: Rahila Syed <[email protected]>; +Cc: pgsql-hackers > On 26 Mar 2025, at 11:34, Rahila Syed <[email protected]> wrote: > + ereport(LOG, > + errmsg("hash table corrupted, can't construct path value")); > I know you switched from elog(LOG.. to ereport(LOG.. but I still think a LOG > entry stating corruption isn't helpful, it's not actionable for the user. > Given that it's a case that shouldn't happen I wonder if we should downgrade it > to an Assert(false) and potentially a DEBUG1? > > How about changing it to ERROR, in accordance with current occurrences of the > same message? I did it in the attached version, however I am open to changing > it to an Assert(false) and DEBUG1. In the attached I moved it to an elog() as it's an internal error, and spending translation effort on it seems fruitless. > 1. Eliminated the unnecessary creation of an extra memory context before calling hash_create. > The hash_create function already generates a memory context containing the hash table, > enabling easy memory deallocation by simply deleting the context via hash_destroy. > Therefore, the patch relies on hash_destroy for memory management instead of manual freeing. Nice > 2. Optimized memory usage by storing the path as an array of integers rather than as an array of > Datums. > This approach conserves DSA memory allocated for storing this information. Ah yes, much better. The attached v21 has a few improvements: * The function documentation didn't specify the return type, only the fact that it's setof record. I've added all output columns. * Some general cleaups of the docs with better markup, improved xref linking and various rewording. * Comment cleanups and language alignment * Added a missing_ok parameter to ContextTypeToString(). While all callers are fine with unknown context types, if we introduce an API for this it seems prudent to not place that burden on callers but to take it on in the function. * Renamed get_summary to just summary, and num_of_tries to retries which feels more in line with the naming convention in other functions * Deferred calling InitMaterializedSRF() until after the PID has been checked for validity. * Pulled back the timeout to 500msec from 1 second. In running congested pgbench simulations I saw better performance and improved results in getting stats. * Replaced strncpy with strlcpy and consistently used idlen to keep all length calculations equal. * Fixed misspelled param name in pg_proc.dat * Pulled back maximum memory usage from 8Mb to 1Mb. 8Mb for the duration of a process (once allocated) is a lot for a niche feature and I while I'm still not sure 1Mb is the right value I think from experimentation that it's closer. I think this version is close to a committable state, will spend a little more time testing, polishing and rewriting the commit message. I will also play around with placement within the memory context code files to keep it from making backpatch issues. -- Daniel Gustafsson Attachments: [application/octet-stream] v21-0002-Function-to-report-memory-context-statistics.patch (54.1K, ../../[email protected]/2-v21-0002-Function-to-report-memory-context-statistics.patch) download | inline diff: From ddaf0ff1fdc7f0b68743bb8676d04ca7c2c63432 Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Wed, 26 Mar 2025 15:08:16 +0530 Subject: [PATCH v21 2/2] Function to report memory context statistics This function sends a signal to a backend to publish statistics of all its memory contexts. Signal handler running in the backend process, sets a flag, which causes it to copy its MemoryContextStats to a DSA, during the next call to CHECK_FOR_INTERRUPTS(). If there are more statistics than that fit in 16MB, the remaining statistics are copied as a cumulative total of the remaining contexts. Once its done, it signals the client backend using a condition variable. The client backend wakes up, reads the shared memory and returns these values in the form of set of records, one for each memory context, to the user, followed by a cumulative total of the remaining contexts, if any. If get_summary is true return statistics of all children of TopMemoryContext with aggregated statistics of their children. User can pass num_of_tries which determines the total number of wait cycles in a client backend for latest statistics. Each cycle wait timeout is set to 1 seconds. Post this the client displays previously published statistics or returns without results. Each backend and auxiliary process has its own slot for reporting the stats. There is an array of such memory slots of size MaxBackends+NumofAuxiliary processes in fixed shared memory. Each of these slots point to a smaller dsa allocations within a single DSA, which contains the stats to be shared by the corresponding process. Each slot has its own LW lock and condition variable for synchronization and communication between the publishing process and the client backend. --- doc/src/sgml/func.sgml | 94 ++++ src/backend/postmaster/autovacuum.c | 4 + src/backend/postmaster/checkpointer.c | 4 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 4 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 4 + src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/storage/lmgr/proc.c | 15 + src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 1 + src/backend/utils/adt/mcxtfuncs.c | 432 +++++++++++++- src/backend/utils/init/globals.c | 1 + src/backend/utils/mmgr/mcxt.c | 531 +++++++++++++++++- src/include/catalog/pg_proc.dat | 10 + src/include/miscadmin.h | 1 + src/include/storage/procsignal.h | 1 + src/include/utils/memutils.h | 69 +++ src/test/regress/expected/sysviews.out | 14 + src/test/regress/sql/sysviews.sql | 14 + src/tools/pgindent/typedefs.list | 4 + 22 files changed, 1190 insertions(+), 30 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 2488e9ba998..b163da128c9 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -28645,6 +28645,67 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres} </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_get_process_memory_contexts</primary> + </indexterm> + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type>, <parameter>summary</parameter> <type>boolean</type>, <parameter>retries</parameter> <type>integer</type> ) + <returnvalue>setof record</returnvalue> + ( <parameter>name</parameter> <type>text</type>, + <parameter>ident</parameter> <type>text</type>, + <parameter>type</parameter> <type>text</type>, + <parameter>path</parameter> <type>integer[]</type>, + <parameter>level</parameter> <type>integer</type>, + <parameter>total_bytes</parameter> <type>bigint</type>, + <parameter>total_nblocks</parameter> <type>bigint</type>, + <parameter>free_bytes</parameter> <type>bigint</type>, + <parameter>free_chunks</parameter> <type>bigint</type>, + <parameter>used_bytes</parameter> <type>bigint</type>, + <parameter>num_agg_contexts</parameter> <type>integer</type>, + <parameter>stats_timestamp</parameter> <type>timestamptz</type> ) + </para> + <para> + This function handles requests to display the memory contexts of a + <productname>PostgreSQL</productname> process with the specified + process ID. The function can be used to send requests to backends as + well as <glossterm linkend="glossary-auxiliary-proc">auxiliary processes</glossterm>. + </para> + <para> + Memory contexts are arranged in a tree-like hierarchy. When + <parameter>summary</parameter> is <literal>true</literal>, statistics + for memory contexts at levels 1 and 2 are displayed, with level 1 + representing the root node (i.e., <literal>TopMemoryContext</literal>). + Statistics for contexts on level 2 and below are aggregates of all + child contexts' statistics, where <literal>num_agg_contexts</literal> + indicate the number aggregated child contexts. When + <parameter>summary</parameter> is <literal>false</literal>, + <literal>the num_agg_contexts</literal> value is <literal>1</literal>, + indicating that individual statistics are being displayed. + </para> + <para> + Busy processes can delay reporting memory context statistics, + <parameter>retries</parameter> sets the number of times the client will + retry to get updated statistics. The sleep per try is + <literal>0.5</literal> seconds. This parameter can be increased if the + user anticipates a delay in the response from the reporting process. + Conversely, if users are frequently and periodically querying the + process for statistics, or if there are concurrent requests for + statistics of the same process, lowering the parameter might help + achieve a faster response. + </para> + <para> + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context + statistics are aggregated and a cumulative total is displayed. The + <literal>num_agg_contexts</literal> column indicates the number of + contexts aggregated in the displayed statistics. When + <literal>num_agg_contexts</literal> is <literal>1</literal> is means + that the context statistics are displayed separately. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> @@ -28784,6 +28845,39 @@ LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 because it may generate a large number of log messages. </para> + <para> + <function>pg_get_process_memory_contexts</function> can be used + to request the memory contexts statistics of any postgres process. For example: +<programlisting> +postgres=# SELECT * FROM pg_get_process_memory_contexts( + (SELECT pid FROM pg_stat_activity + WHERE backend_type = 'checkpointer') + , false, 5) LIMIT 1; +-[ RECORD 1 ]----+------------------------------ +name | TopMemoryContext +ident | +type | AllocSet +path | {1} +level | 1 +total_bytes | 90304 +total_nblocks | 3 +free_bytes | 2880 +free_chunks | 1 +used_bytes | 87424 +num_agg_contexts | 1 +stats_timestamp | 2025-03-24 13:55:47.796698+01 +</programlisting> + <note> + <para> + While <function>pg_get_process_memory_contexts</function> can be used to + query memory contexts of the local backend, + <structname>pg_backend_memory_contexts</structname> + (see <xref linkend="view-pg-backend-memory-contexts"/> for more details) + will be less resource intensive when only the local backend is of interest. + </para> + </note> + </para> + </sect2> <sect2 id="functions-admin-backup"> diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 2513a8ef8a6..16756152b71 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -781,6 +781,10 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index fda91ffd1ce..d3cb3f1891c 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -663,6 +663,10 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index 0ae9bf906ec..f24f574e748 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -48,6 +48,10 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index 7e622ae4bd2..cb7408acf4c 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -867,6 +867,10 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index 27e86cf393f..7149a67fcbc 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -192,6 +192,10 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index 0fec4f1f871..c7a76711cc5 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -879,6 +879,10 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index 2fa045e6b0f..362a6dc9528 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -51,6 +51,7 @@ #include "storage/sinvaladt.h" #include "utils/guc.h" #include "utils/injection_point.h" +#include "utils/memutils.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -343,6 +344,8 @@ CreateOrAttachShmemStructs(void) WaitEventCustomShmemInit(); InjectionPointShmemInit(); AioShmemInit(); + MemCtxShmemInit(); + MemCtxBackendShmemInit(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index 7d201965503..b59034fdc38 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -690,6 +690,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_GET_MEMORY_CONTEXT)) + HandleGetMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 066319afe2b..026c4bc992f 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -50,6 +50,7 @@ #include "storage/procsignal.h" #include "storage/spin.h" #include "storage/standby.h" +#include "utils/memutils.h" #include "utils/timeout.h" #include "utils/timestamp.h" @@ -497,6 +498,13 @@ InitProcess(void) */ PGSemaphoreReset(MyProc->sem); + /* + * The before shmem exit callback frees the DSA memory occupied by the + * latest memory context statistics that could be published by this + * backend if requested. + */ + before_shmem_exit(AtProcExit_memstats_dsa_free, 0); + /* * Arrange to clean up at backend exit. */ @@ -671,6 +679,13 @@ InitAuxiliaryProcess(void) */ PGSemaphoreReset(MyProc->sem); + /* + * The before shmem exit callback frees the DSA memory occupied by the + * latest memory context statistics that could be published by this + * process if requested. + */ + before_shmem_exit(AtProcExit_memstats_dsa_free, 0); + /* * Arrange to clean up at process exit. */ diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index aec65007bb6..ad020adee93 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3531,6 +3531,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 8bce14c38fd..ee0e42535b6 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -161,6 +161,7 @@ WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." +MEM_CTX_PUBLISH "Waiting for a process to publish memory information." ABI_compatibility: diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index 396c2f223b4..375509f2551 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -17,28 +17,26 @@ #include "funcapi.h" #include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "access/twophase.h" +#include "catalog/pg_authid_d.h" +#include "nodes/pg_list.h" #include "storage/proc.h" #include "storage/procarray.h" +#include "utils/acl.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/memutils.h" +#include "utils/wait_event_types.h" /* ---------- * The max bytes for showing identifiers of MemoryContext. * ---------- */ #define MEMORY_CONTEXT_IDENT_DISPLAY_SIZE 1024 - -/* - * MemoryContextId - * Used for storage of transient identifiers for - * pg_get_backend_memory_contexts. - */ -typedef struct MemoryContextId -{ - MemoryContext context; - int context_id; -} MemoryContextId; +struct MemoryContextBackendState *memCtxState = NULL; +struct MemoryContextState *memCtxArea = NULL; /* * int_list_to_array @@ -143,24 +141,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, else nulls[1] = true; - switch (context->type) - { - case T_AllocSetContext: - type = "AllocSet"; - break; - case T_GenerationContext: - type = "Generation"; - break; - case T_SlabContext: - type = "Slab"; - break; - case T_BumpContext: - type = "Bump"; - break; - default: - type = "???"; - break; - } + type = ContextTypeToString(context->type, true); values[2] = CStringGetTextDatum(type); values[3] = Int32GetDatum(list_length(path)); /* level */ @@ -175,6 +156,44 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, list_free(path); } +/* + * ContextTypeToString + * Returns a textual representation of a context type + * + * This should cover the same types as MemoryContextIsValid. If missing_ok is + * false then execution will error out on invalid context types. + */ +const char * +ContextTypeToString(NodeTag type, bool missing_ok) +{ + const char *context_type; + + switch (type) + { + case T_AllocSetContext: + context_type = "AllocSet"; + break; + case T_GenerationContext: + context_type = "Generation"; + break; + case T_SlabContext: + context_type = "Slab"; + break; + case T_BumpContext: + context_type = "Bump"; + break; + default: + if (missing_ok) + context_type = "???"; + else + ereport(ERROR, + errcode(ERRCODE_UNDEFINED_OBJECT), + errmsg("invalid memory context type specified")); + break; + } + return (context_type); +} + /* * pg_get_backend_memory_contexts * SQL SRF showing backend memory context. @@ -305,3 +324,358 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } + +/* + * pg_get_process_memory_contexts + * Signal a backend or an auxiliary process to send its memory contexts, + * wait for the results and display them. + * + * By default, only superusers or users with PG_READ_ALL_STATS are allowed to + * signal a process to return the memory contexts. This is because allowing + * any users to issue this request at an unbounded rate would cause lots of + * requests to be sent, which can lead to denial of service. Additional roles + * can be permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to copy the memory context details + * to a dynamic shared memory space. + * + * We have defined a limit on DSA memory that could be allocated per process - + * if the process has more memory contexts than what can fit in the allocated + * size, the excess contexts are summarized and represented as cumulative total + * at the end of the buffer. + * + * After sending the signal, wait on a condition variable. The publishing + * backend, after copying the data to shared memory, sends signal on that + * condition variable. There is one condition variable per publishing backend. + * Once the condition variable is signalled, check if the latest memory context + * information is available and display. + * + * If the publishing backend does not respond before the condition variable + * times out, which is set to MEMSTATS_WAIT_TIMEOUT, retry for max_retries + * number of times, which is defined by user, before giving up and + * returning previously published statistics, if any. If no previous statistics + * exist, return NULL. + */ +Datum +pg_get_process_memory_contexts(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + bool get_summary = PG_GETARG_BOOL(1); + PGPROC *proc; + ProcNumber procNumber = INVALID_PROC_NUMBER; + bool proc_is_aux = false; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + dsa_area *area; + MemoryContextEntry *memctx_info; + int num_retries = 0; + TimestampTz curr_timestamp; + int max_retries = PG_GETARG_INT32(2); + + /* + * Only superusers or users with pg_read_all_stats privileges can view the + * memory context statistics of another process. + */ + if (!has_privs_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS)) + ereport(ERROR, + errcode(ERRCODE_OBJECT_NOT_IN_PREREQUISITE_STATE), + errmsg("permission denied to extract memory context statistics")); + + /* + * See if the process with given pid is a backend or an auxiliary process + * and remember the type for when we requery the process later. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + { + proc = AuxiliaryPidGetProc(pid); + proc_is_aux = true; + } + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; this is however not a problem and leave with a WARNING. + * See comment in pg_log_backend_memory_contexts for a discussion on this. + */ + if (proc == NULL) + { + /* + * This is just a warning so a loop-through-resultset will not abort + * if one backend terminated on its own during the run. + */ + ereport(WARNING, + (errmsg("PID %d is not a PostgreSQL server process", pid))); + PG_RETURN_NULL(); + } + + InitMaterializedSRF(fcinfo, 0); + + procNumber = GetNumberFromPGProc(proc); + + LWLockAcquire(&memCtxState[procNumber].lw_lock, LW_EXCLUSIVE); + memCtxState[procNumber].get_summary = get_summary; + LWLockRelease(&memCtxState[procNumber].lw_lock); + + curr_timestamp = GetCurrentTimestamp(); + + /* + * Send a signal to a PostgreSQL process, informing it we want it to + * produce information about its memory contexts. + */ + if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, procNumber) < 0) + { + ereport(WARNING, + errmsg("could not send signal to process %d: %m", pid)); + PG_RETURN_NULL(); + } + + /* + * A valid DSA pointer isn't proof that statistics are available, it can + * be valid due to previously published stats. Check if the stats are + * updated by comparing the timestamp, if the stats are newer than our + * previously recorded timestamp from before sending the procsignal, they + * must by definition be updated. Wait for max_retries * + * MEMSTATS_WAIT_TIMEOUT, following which display old statistics if + * available or return NULL. + */ + while (1) + { + long msecs; + + /* + * We expect to come out of sleep when the requested process has + * finished publishing the statistics, verified using the valid DSA + * pointer. + * + * Make sure that the information belongs to pid we requested + * information for, Otherwise loop back and wait for the server + * process to finish publishing statistics. + */ + LWLockAcquire(&memCtxState[procNumber].lw_lock, LW_EXCLUSIVE); + + /* + * Note in procnumber.h file says that a procNumber can be re-used for + * a different backend immediately after a backend exits. In case an + * old process' data was there and not updated by the current process + * in the slot identified by the procNumber, the pid of the requested + * process and the proc_id might not match. + */ + if (memCtxState[procNumber].proc_id == pid) + { + /* + * Break if the latest stats have been read, indicated by + * statistics timestamp being newer than the current request + * timestamp. + */ + msecs = TimestampDifferenceMilliseconds(curr_timestamp, + memCtxState[procNumber].stats_timestamp); + + if (DsaPointerIsValid(memCtxState[procNumber].memstats_dsa_pointer) + && msecs > 0) + break; + } + LWLockRelease(&memCtxState[procNumber].lw_lock); + + /* + * Recheck the state of the backend before sleeping on the condition + * variable to ensure the process is still alive. Only check the + * relevant process type based on the earlier PID check. + */ + if (proc_is_aux) + proc = AuxiliaryPidGetProc(pid); + else + proc = BackendPidGetProc(pid); + + /* + * The process ending during memory context processing is not an + * error. + */ + if (proc == NULL) + { + ereport(WARNING, + errmsg("PID %d is no longer a PostgreSQL server process", + pid)); + PG_RETURN_NULL(); + } + +#define MEMSTATS_WAIT_TIMEOUT 500 + + if (ConditionVariableTimedSleep(&memCtxState[procNumber].memctx_cv, + MEMSTATS_WAIT_TIMEOUT, + WAIT_EVENT_MEM_CTX_PUBLISH)) + { + /* + * Wait for max_retries, as defined by the user. If no updated + * statistics are available within the wait time defined by + * max_retries then display previously published statistics if + * there are any. If no previous statistics are available then + * return NULL. + */ + if (num_retries > max_retries) + { + LWLockAcquire(&memCtxState[procNumber].lw_lock, LW_EXCLUSIVE); + /* Displaying previously published statistics if available */ + if (DsaPointerIsValid(memCtxState[procNumber].memstats_dsa_pointer)) + break; + else + { + LWLockRelease(&memCtxState[procNumber].lw_lock); + PG_RETURN_NULL(); + } + } + ereport(DEBUG1, + errmsg("timed out waiting for process with PID %d to publish stats, retrying", + pid)); + num_retries = num_retries + 1; + } + } + + /* + * We should only reach here with a valid DSA handle, either containing + * updated statistics or previously published statistics (identified by + * the timestamp. + */ + Assert(memCtxArea->memstats_dsa_handle != DSA_HANDLE_INVALID); + area = dsa_attach(memCtxArea->memstats_dsa_handle); + + /* + * Backend has finished publishing the stats, project them. + */ + memctx_info = (MemoryContextEntry *) + dsa_get_address(area, memCtxState[procNumber].memstats_dsa_pointer); + +#define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 12 + for (int i = 0; i < memCtxState[procNumber].total_stats; i++) + { + ArrayType *path_array; + int path_length; + Datum values[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + bool nulls[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + char *name; + char *ident; + Datum *path_datum = NULL; + int *path_int = NULL; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + if (DsaPointerIsValid(memctx_info[i].name)) + { + name = (char *) dsa_get_address(area, memctx_info[i].name); + values[0] = CStringGetTextDatum(name); + } + else + nulls[0] = true; + + if (DsaPointerIsValid(memctx_info[i].ident)) + { + ident = (char *) dsa_get_address(area, memctx_info[i].ident); + values[1] = CStringGetTextDatum(ident); + } + else + nulls[1] = true; + + if (memctx_info[i].type != NULL) + values[2] = CStringGetTextDatum(memctx_info[i].type); + else + nulls[2] = true; + + path_length = memctx_info[i].path_length; + path_datum = (Datum *) palloc(path_length * sizeof(Datum)); + if (DsaPointerIsValid(memctx_info[i].path)) + { + path_int = (int *) dsa_get_address(area, memctx_info[i].path); + for (int j = 0; j < path_length; j++) + path_datum[j] = Int32GetDatum(path_int[j]); + path_array = construct_array_builtin(path_datum, path_length, INT4OID); + values[3] = PointerGetDatum(path_array); + } + else + nulls[3] = true; + + values[4] = Int32GetDatum(path_length); /* level */ + values[5] = Int64GetDatum(memctx_info[i].totalspace); + values[6] = Int64GetDatum(memctx_info[i].nblocks); + values[7] = Int64GetDatum(memctx_info[i].freespace); + values[8] = Int64GetDatum(memctx_info[i].freechunks); + values[9] = Int64GetDatum(memctx_info[i].totalspace - + memctx_info[i].freespace); + values[10] = Int32GetDatum(memctx_info[i].num_agg_stats); + values[11] = TimestampTzGetDatum(memCtxState[procNumber].stats_timestamp); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + LWLockRelease(&memCtxState[procNumber].lw_lock); + + ConditionVariableCancelSleep(); + dsa_detach(area); + + PG_RETURN_NULL(); +} + +/* + * Init shared memory for reporting memory context information. + */ +void +MemCtxBackendShmemInit(void) +{ + bool found; + Size TotalProcs; + + TotalProcs = add_size(MaxBackends, NUM_AUXILIARY_PROCS); + TotalProcs = add_size(TotalProcs, max_prepared_xacts); + + memCtxState = (MemoryContextBackendState *) + ShmemInitStruct("MemoryContextBackendState", + mul_size(TotalProcs, sizeof(MemoryContextBackendState)), + &found); + + if (!IsUnderPostmaster) + { + Assert(!found); + + for (int i = 0; i < TotalProcs; i++) + { + ConditionVariableInit(&memCtxState[i].memctx_cv); + + LWLockInitialize(&memCtxState[i].lw_lock, LWLockNewTrancheId()); + LWLockRegisterTranche(memCtxState[i].lw_lock.tranche, + "mem_context_backend_stats_reporting"); + + memCtxState[i].memstats_dsa_pointer = InvalidDsaPointer; + } + } + else + { + Assert(found); + } +} + +/* + * Initialize shared memory for displaying memory context statistics + */ +void +MemCtxShmemInit(void) +{ + bool found; + + memCtxArea = (MemoryContextState *) + ShmemInitStruct("MemoryContextState", sizeof(MemoryContextState), &found); + + if (!IsUnderPostmaster) + { + Assert(!found); + + LWLockInitialize(&memCtxArea->lw_lock, + LWLockNewTrancheId()); + LWLockRegisterTranche(memCtxArea->lw_lock.tranche, + "mem_context_stats_reporting"); + memCtxArea->memstats_dsa_handle = DSA_HANDLE_INVALID; + } + else + { + Assert(found); + } +} diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index b844f9fdaef..13938ccb0f5 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -38,6 +38,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t PublishMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 2cbde8f39c3..bfcd65a69a5 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -23,6 +23,13 @@ #include "mb/pg_wchar.h" #include "miscadmin.h" +#include "nodes/pg_list.h" +#include "storage/fd.h" +#include "storage/lwlock.h" +#include "storage/dsm.h" +#include "storage/ipc.h" +#include "utils/dsa.h" +#include "utils/hsearch.h" #include "utils/memdebug.h" #include "utils/memutils.h" #include "utils/memutils_internal.h" @@ -177,6 +184,16 @@ static void MemoryContextStatsInternal(MemoryContext context, int level, static void MemoryContextStatsPrint(MemoryContext context, void *passthru, const char *stats_string, bool print_to_stderr); +static void PublishMemoryContext(MemoryContextEntry *memctx_infos, + int curr_id, MemoryContext context, + List *path, + MemoryContextCounters stat, + int num_contexts, dsa_area *area); +static void compute_contexts_count_and_ids(List *contexts, HTAB *context_id_lookup, + int *stats_count, + bool get_summary); +static List *compute_context_path(MemoryContext c, HTAB *context_id_lookup); +static void dsa_free_previous_stats(dsa_area *area, int total_stats, dsa_pointer prev_dsa_pointer); /* * You should not do memory allocations within a critical section, because @@ -927,7 +944,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, * PRINT_STATS_NONE, only compute totals. This is used in * reporting of memory context statistics via a sql function. Last * parameter is not relevant. -+ */ + */ context->methods->stats(context, NULL, NULL, @@ -1331,6 +1348,22 @@ HandleLogMemoryContextInterrupt(void) /* latch will be set by procsignal_sigusr1_handler */ } +/* + * HandleGetMemoryContextInterrupt + * Handle receipt of an interrupt indicating a request to publish memory + * contexts statistics. + * + * All the actual work is deferred to ProcessGetMemoryContextInterrupt() as + * this cannot be performed in a signal handler. + */ +void +HandleGetMemoryContextInterrupt(void) +{ + InterruptPending = true; + PublishMemoryContextPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + /* * ProcessLogMemoryContextInterrupt * Perform logging of memory contexts of this backend process. @@ -1368,6 +1401,502 @@ ProcessLogMemoryContextInterrupt(void) MemoryContextStatsDetail(TopMemoryContext, 100, 100, false); } +/* + * ProcessGetMemoryContextInterrupt + * Generate information about memory contexts used by the process. + * + * Performs a breadth first search on the memory context tree, thus parents + * statistics are reported before their children in the monitoring function + * output. + * + * Statistics for all the processes are shared via the same dynamic shared + * area. Statistics written by each process are tracked independently in + * per-process DSA pointers. These pointers are stored in static shared memory. + * + * We calculate maximum number of context's statistics that can be displayed + * using a pre-determined limit for memory available per process for this + * utility maximum size of statistics for each context. The remaining context + * statistics if any are captured as a cumulative total at the end of + * individual context's statistics. + * + * If get_summary is true, we capture the level 1 and level 2 contexts + * statistics. For that we traverse the memory context tree recursively in + * depth first search manner to cover all the children of a parent context, to + * be able to display a cumulative total of memory consumption by a parent at + * level 2 and all its children. + */ +void +ProcessGetMemoryContextInterrupt(void) +{ + List *contexts; + HASHCTL ctl; + HTAB *context_id_lookup; + int context_id = 0; + MemoryContextEntry *meminfo; + bool get_summary = false; + dsa_area *area = NULL; + int max_stats; + int idx = MyProcNumber; + int stats_count = 0; + MemoryContextCounters stat; + int num_individual_stats = 0; + + PublishMemoryContextPending = false; + + /* + * The hash table is used for constructing "path" column of the view, + * similar to its local backend counterpart. + */ + ctl.keysize = sizeof(MemoryContext); + ctl.entrysize = sizeof(MemoryContextId); + ctl.hcxt = CurrentMemoryContext; + + context_id_lookup = hash_create("pg_get_remote_backend_memory_contexts", + 256, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + /* List of contexts to process in the next round - start at the top. */ + contexts = list_make1(TopMemoryContext); + + /* Compute the number of stats that can fit in the defined limit */ + max_stats = (MAX_SEGMENTS_PER_BACKEND * DSA_DEFAULT_INIT_SEGMENT_SIZE) + / (MAX_MEMORY_CONTEXT_STATS_SIZE); + LWLockAcquire(&memCtxState[idx].lw_lock, LW_EXCLUSIVE); + get_summary = memCtxState[idx].get_summary; + LWLockRelease(&memCtxState[idx].lw_lock); + + /* + * Traverse the memory context tree to find total number of contexts. If + * summary is requested report the total number of contexts at level 1 and + * 2 from the top. Also, populate the hash table of context ids. + */ + compute_contexts_count_and_ids(contexts, context_id_lookup, &stats_count, + get_summary); + + /* + * Allocate memory in this process's DSA for storing statistics of the the + * memory contexts upto max_stats, for contexts that don't fit within a + * limit, a cumulative total is written as the last record in the DSA + * segment. + */ + stats_count = (stats_count > max_stats) ? max_stats : stats_count; + + LWLockAcquire(&memCtxArea->lw_lock, LW_EXCLUSIVE); + + /* + * Create a DSA and send handle to the the client process after storing + * the context statistics. If number of contexts exceed a predefined + * limit(8MB), a cumulative total is stored for such contexts. + */ + if (memCtxArea->memstats_dsa_handle == DSA_HANDLE_INVALID) + { + MemoryContext oldcontext = CurrentMemoryContext; + dsa_handle handle; + + MemoryContextSwitchTo(TopMemoryContext); + + area = dsa_create(memCtxArea->lw_lock.tranche); + + handle = dsa_get_handle(area); + MemoryContextSwitchTo(oldcontext); + + dsa_pin_mapping(area); + + /* + * Pin the DSA area, this is to make sure the area remains attachable + * even if current backend exits. This is done so that the statistics + * are published even if the process exits while a client is waiting. + */ + dsa_pin(area); + + /* Set the handle in shared memory */ + memCtxArea->memstats_dsa_handle = handle; + } + + /* + * If DSA exists, created by another process publishing statistics, or by + * the previous execution of this function by this process, attach to it. + */ + else if (area == NULL) + { + MemoryContext oldcontext = CurrentMemoryContext; + + MemoryContextSwitchTo(TopMemoryContext); + area = dsa_attach(memCtxArea->memstats_dsa_handle); + MemoryContextSwitchTo(oldcontext); + dsa_pin_mapping(area); + } + LWLockRelease(&memCtxArea->lw_lock); + + /* + * Hold the process lock to protect writes to process specific memory. Two + * processes publishing statistics do not block each other. + */ + LWLockAcquire(&memCtxState[idx].lw_lock, LW_EXCLUSIVE); + memCtxState[idx].proc_id = MyProcPid; + + if (DsaPointerIsValid(memCtxState[idx].memstats_dsa_pointer)) + { + /* + * Free any previous allocations, free the name, ident and path + * pointers before freeing the pointer that contains them. + */ + dsa_free_previous_stats(area, memCtxState[idx].total_stats, + memCtxState[idx].memstats_dsa_pointer); + + dsa_free(area, memCtxState[idx].memstats_dsa_pointer); + memCtxState[idx].memstats_dsa_pointer = InvalidDsaPointer; + } + memCtxState[idx].memstats_dsa_pointer = + dsa_allocate0(area, stats_count * sizeof(MemoryContextEntry)); + + meminfo = (MemoryContextEntry *) + dsa_get_address(area, memCtxState[idx].memstats_dsa_pointer); + + if (get_summary) + { + int ctx_id = 0; + List *path = NIL; + + /* Copy TopMemoryContext statistics to DSA */ + memset(&stat, 0, sizeof(stat)); + (*TopMemoryContext->methods->stats) (TopMemoryContext, NULL, NULL, + &stat, true); + path = lcons_int(1, path); + PublishMemoryContext(meminfo, ctx_id, TopMemoryContext, path, stat, + 1, area); + ctx_id = ctx_id + 1; + + /* + * Copy statistics for each of TopMemoryContexts children. This + * includes statistics of all of their children upto level 100. + */ + for (MemoryContext c = TopMemoryContext->firstchild; c != NULL; + c = c->nextchild) + { + MemoryContextCounters grand_totals; + int num_contexts = 0; + + path = NIL; + memset(&grand_totals, 0, sizeof(grand_totals)); + + MemoryContextStatsInternal(c, 0, 100, 100, &grand_totals, + PRINT_STATS_NONE, &num_contexts); + + path = compute_context_path(c, context_id_lookup); + + PublishMemoryContext(meminfo, ctx_id, c, path, + grand_totals, num_contexts, area); + ctx_id = ctx_id + 1; + } + memCtxState[idx].total_stats = ctx_id; + goto cleanup; + } + + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + char *name; + + /* + * Figure out the transient context_id of this context and each of its + * ancestors, to compute a path for this context. + */ + path = compute_context_path(cur, context_id_lookup); + + if (context_id <= (max_stats - 2)) + { + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1, area); + } + else + { + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + meminfo[max_stats - 1].totalspace += stat.totalspace; + meminfo[max_stats - 1].nblocks += stat.nblocks; + meminfo[max_stats - 1].freespace += stat.freespace; + meminfo[max_stats - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit per process is reached, write aggregate of the + * remaining statistics. + */ + if (context_id == (max_stats - 2) && context_id < (stats_count - 1)) + { + num_individual_stats = context_id + 1; + meminfo[max_stats - 1].name = dsa_allocate0(area, 17); + name = dsa_get_address(area, meminfo[max_stats - 1].name); + strncpy(name, "Remaining Totals", 16); + meminfo[max_stats - 1].ident = InvalidDsaPointer; + meminfo[max_stats - 1].path = InvalidDsaPointer; + meminfo[max_stats - 1].type = NULL; + } + context_id++; + } + /* Statistics are not aggregated, i.e individual statistics reported */ + if (context_id < (max_stats - 2)) + { + memCtxState[idx].total_stats = context_id; + } + /* Report number of aggregated memory contexts */ + else + { + meminfo[max_stats - 1].num_agg_stats = context_id - + num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for cumulative + * statistics. + */ + memCtxState[idx].total_stats = num_individual_stats + 1; + } + +cleanup: + + /* + * Signal all the waiting client backends after copying all the statistics + */ + memCtxState[idx].stats_timestamp = GetCurrentTimestamp(); + LWLockRelease(&memCtxState[idx].lw_lock); + ConditionVariableBroadcast(&memCtxState[idx].memctx_cv); + hash_destroy(context_id_lookup); + dsa_detach(area); +} + +/* + * compute_context_path + * + * Append the transient context_id of this context and each of its ancestors + * to a list, in order to compute a path. + */ +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + + for (MemoryContext cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + elog(ERROR, "hash table corrupted, can't construct path value"); + + path = lcons_int(cur_entry->context_id, path); + } + return path; +} + +/* + * Return the number of contexts allocated currently by the backend + * Assign context ids to each of the contexts. + */ +static void +compute_contexts_count_and_ids(List *contexts, HTAB *context_id_lookup, + int *stats_count, bool get_summary) +{ + foreach_ptr(MemoryContextData, cur, contexts) + { + MemoryContextId *entry; + bool found; + + entry = (MemoryContextId *) hash_search(context_id_lookup, &cur, + HASH_ENTER, &found); + Assert(!found); + + /* context id starts with 1 */ + entry->context_id = (++(*stats_count)); + + /* Append the children of the current context to the main list. */ + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + { + if (get_summary) + { + entry = (MemoryContextId *) hash_search(context_id_lookup, &c, + HASH_ENTER, &found); + Assert(!found); + + entry->context_id = (++(*stats_count)); + } + + contexts = lappend(contexts, c); + } + + /* + * In summary only the first two level (from top) contexts are + * displayed + */ + if (get_summary) + break; + } +} + +/* + * PublishMemoryContext + * + * Copy the memory context statistics of a single context to a DSA memory + */ +static void +PublishMemoryContext(MemoryContextEntry *memctx_info, int curr_id, + MemoryContext context, List *path, + MemoryContextCounters stat, int num_contexts, + dsa_area *area) +{ + char clipped_ident[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + char *name; + char *ident; + int *path_list; + + Assert(MemoryContextIsValid(context)); + + if (context->name != NULL) + { + Assert(strlen(context->name) < MEMORY_CONTEXT_IDENT_SHMEM_SIZE); + memctx_info[curr_id].name = dsa_allocate0(area, strlen(context->name) + 1); + name = (char *) dsa_get_address(area, memctx_info[curr_id].name); + strlcpy(name, context->name, strlen(context->name) + 1); + } + else + memctx_info[curr_id].name = InvalidDsaPointer; + + /* Trim and copy the identifier if it is not set to NULL */ + if (context->ident != NULL) + { + int idlen = strlen(context->ident); + + /* + * Some identifiers such as SQL query string can be very long, + * truncate oversize identifiers. + */ + if (idlen >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + idlen = pg_mbcliplen(context->ident, idlen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + memcpy(clipped_ident, context->ident, idlen); + clipped_ident[idlen] = '\0'; + + /* + * To be consistent with logging output, we label dynahash contexts + * with just the hash table name as with MemoryContextStatsPrint(). + */ + if (strncmp(context->name, "dynahash", 8) == 0) + { + dsa_free(area, memctx_info[curr_id].name); + memctx_info[curr_id].name = dsa_allocate0(area, idlen + 1); + name = (char *) dsa_get_address(area, memctx_info[curr_id].name); + strlcpy(name, clipped_ident, idlen + 1); + memctx_info[curr_id].ident = InvalidDsaPointer; + } + else + { + memctx_info[curr_id].ident = dsa_allocate0(area, idlen + 1); + ident = (char *) dsa_get_address(area, memctx_info[curr_id].ident); + strlcpy(ident, clipped_ident, idlen + 1); + } + } + else + memctx_info[curr_id].ident = InvalidDsaPointer; + + /* Allocate DSA memory for storing path information */ + if (path == NIL) + memctx_info[curr_id].path = InvalidDsaPointer; + else + { + memctx_info[curr_id].path_length = list_length(path); + memctx_info[curr_id].path = dsa_allocate0(area, + memctx_info[curr_id].path_length * sizeof(int)); + path_list = (int *) dsa_get_address(area, memctx_info[curr_id].path); + foreach_int(i, path) + path_list[foreach_current_index(i)] = i; + } + memctx_info[curr_id].type = ContextTypeToString(context->type, true); + memctx_info[curr_id].totalspace = stat.totalspace; + memctx_info[curr_id].nblocks = stat.nblocks; + memctx_info[curr_id].freespace = stat.freespace; + memctx_info[curr_id].freechunks = stat.freechunks; + memctx_info[curr_id].num_agg_stats = num_contexts; +} + +/* + * dsa_free_previous_stats + * + * Worker for freeing resources from a MemoryContextEntry. Callers are + * responsible for ensuring that the DSA pointer is valid. + */ +static void +dsa_free_previous_stats(dsa_area *area, int total_stats, + dsa_pointer prev_dsa_pointer) +{ + MemoryContextEntry *meminfo; + + meminfo = (MemoryContextEntry *) dsa_get_address(area, prev_dsa_pointer); + Assert(meminfo != NULL); + for (int i = 0; i < total_stats; i++) + { + if (DsaPointerIsValid(meminfo[i].name)) + dsa_free(area, meminfo[i].name); + + if (DsaPointerIsValid(meminfo[i].ident)) + dsa_free(area, meminfo[i].ident); + + if (DsaPointerIsValid(meminfo[i].path)) + dsa_free(area, meminfo[i].path); + } +} + +/* + * Free the memory context statistics stored by this process + * in DSA area. + */ +void +AtProcExit_memstats_dsa_free(int code, Datum arg) +{ + int idx = MyProcNumber; + dsm_segment *dsm_seg = NULL; + dsa_area *area = NULL; + + if (memCtxArea->memstats_dsa_handle == DSA_HANDLE_INVALID) + return; + + dsm_seg = dsm_find_mapping(memCtxArea->memstats_dsa_handle); + + LWLockAcquire(&memCtxState[idx].lw_lock, LW_EXCLUSIVE); + + if (!DsaPointerIsValid(memCtxState[idx].memstats_dsa_pointer)) + { + LWLockRelease(&memCtxState[idx].lw_lock); + return; + } + + /* If the dsm mapping could not be found, attach to the area */ + if (dsm_seg != NULL) + return; + area = dsa_attach(memCtxArea->memstats_dsa_handle); + + /* + * Free the memory context statistics, free the name, ident and path + * pointers before freeing the pointer that contains these pointers and + * integer statistics. + */ + dsa_free_previous_stats(area, memCtxState[idx].total_stats, + memCtxState[idx].memstats_dsa_pointer); + dsa_free(area, memCtxState[idx].memstats_dsa_pointer); + memCtxState[idx].memstats_dsa_pointer = InvalidDsaPointer; + + dsa_detach(area); + LWLockRelease(&memCtxState[idx].lw_lock); +} + void * palloc(Size size) { diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 6b57b7e18d9..ff25f729d94 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8535,6 +8535,16 @@ prorettype => 'bool', proargtypes => 'int4', prosrc => 'pg_log_backend_memory_contexts' }, +# publishing memory contexts of the specified postgres process +{ oid => '2173', descr => 'publish memory contexts of the specified backend', + proname => 'pg_get_process_memory_contexts', provolatile => 'v', + prorows => '100', proretset => 't', proparallel => 'r', + prorettype => 'record', proargtypes => 'int4 bool int4', + proallargtypes => '{int4,bool,int4,text,text,text,_int4,int4,int8,int8,int8,int8,int8,int4,timestamptz}', + proargmodes => '{i,i,i,o,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{pid, summary, retries, name, ident, type, path, level, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes, num_agg_contexts, stats_timestamp}', + prosrc => 'pg_get_process_memory_contexts' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index 603d0424354..d3c44df6e13 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -96,6 +96,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t PublishMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index 022fd8ed933..5d4b2fbfc9c 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -35,6 +35,7 @@ typedef enum PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to send the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ /* Recovery conflict reasons */ diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 8abc26abce2..da6b633cbda 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -18,6 +18,9 @@ #define MEMUTILS_H #include "nodes/memnodes.h" +#include "storage/condition_variable.h" +#include "storage/lmgr.h" +#include "utils/dsa.h" /* @@ -48,7 +51,12 @@ #define AllocHugeSizeIsValid(size) ((Size) (size) <= MaxAllocHugeSize) +#define MEMORY_CONTEXT_IDENT_SHMEM_SIZE 128 +#define MEM_CONTEXT_MAX_LEVEL 64 +#define MAX_SEGMENTS_PER_BACKEND 1 +#define MEM_CONTEXT_PATH_SIZE MEM_CONTEXT_MAX_LEVEL * sizeof(int) +#define MAX_MEMORY_CONTEXT_STATS_SIZE sizeof(MemoryContextEntry) + MEM_CONTEXT_PATH_SIZE + 2 * MEMORY_CONTEXT_IDENT_SHMEM_SIZE /* * Standard top-level memory contexts. * @@ -319,4 +327,65 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) return true; } +/* Dynamic shared memory state for statistics per context */ +typedef struct MemoryContextEntry +{ + dsa_pointer name; + dsa_pointer ident; + dsa_pointer path; + const char *type; + int path_length; + int64 totalspace; + int64 nblocks; + int64 freespace; + int64 freechunks; + int num_agg_stats; +} MemoryContextEntry; + +/* + * Static shared memory state representing the DSA area created for memory + * context statistics reporting. A single DSA area is created and used by all + * the processes, each having its specific DSA allocations for sharing memory + * statistics, tracked by per backend static shared memory state. + */ +typedef struct MemoryContextState +{ + dsa_handle memstats_dsa_handle; + LWLock lw_lock; +} MemoryContextState; + +/* + * Per backend static shared memory state for memory context statistics + * reporting. + */ +typedef struct MemoryContextBackendState +{ + ConditionVariable memctx_cv; + LWLock lw_lock; + int proc_id; + int total_stats; + bool get_summary; + dsa_pointer memstats_dsa_pointer; + TimestampTz stats_timestamp; +} MemoryContextBackendState; + + +/* + * Used for storage of transient identifiers for pg_get_backend_memory_contexts + */ +typedef struct MemoryContextId +{ + MemoryContext context; + int context_id; +} MemoryContextId; + +extern PGDLLIMPORT MemoryContextBackendState *memCtxState; +extern PGDLLIMPORT MemoryContextState *memCtxArea; +extern void ProcessGetMemoryContextInterrupt(void); +extern const char *ContextTypeToString(NodeTag type, bool missing_ok); +extern void HandleGetMemoryContextInterrupt(void); +extern void MemCtxShmemInit(void); +extern void MemCtxBackendShmemInit(void); +extern void AtProcExit_memstats_dsa_free(int code, Datum arg); + #endif /* MEMUTILS_H */ diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 83228cfca29..dca20ae1a26 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -232,3 +232,17 @@ select * from pg_timezone_abbrevs where abbrev = 'LMT'; LMT | @ 7 hours 52 mins 58 secs ago | f (1 row) +DO $$ +DECLARE + launcher_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='autovacuum launcher' + INTO launcher_pid; + + select type, name, ident + from pg_get_process_memory_contexts(launcher_pid, false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; +NOTICE: (AllocSet,TopMemoryContext,) diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 66179f026b3..4767351d4e2 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -101,3 +101,17 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs; -- One specific case we can check without much fear of breakage -- is the historical local-mean-time value used for America/Los_Angeles. select * from pg_timezone_abbrevs where abbrev = 'LMT'; + +DO $$ +DECLARE + launcher_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='autovacuum launcher' + INTO launcher_pid; + + select type, name, ident + from pg_get_process_memory_contexts(launcher_pid, false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 449bafc123c..f083690c5ce 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1654,12 +1654,16 @@ MemoizeState MemoizeTuple MemoryChunk MemoryContext +MemoryContextBackendState MemoryContextCallback MemoryContextCallbackFunction MemoryContextCounters MemoryContextData +MemoryContextEntry +MemoryContextId MemoryContextMethodID MemoryContextMethods +MemoryContextState MemoryStatsPrintFunc MergeAction MergeActionState -- 2.39.3 (Apple Git-146) [application/octet-stream] v21-0001-Preparatory-changes-for-reporting-memory-context.patch (5.1K, ../../[email protected]/3-v21-0001-Preparatory-changes-for-reporting-memory-context.patch) download | inline diff: From 3f3597952fac250eeb55aa1b09a6409c4417beab Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Wed, 26 Mar 2025 14:38:39 +0530 Subject: [PATCH v21 1/2] Preparatory changes for reporting memory context statistics Ensure that MemoryContextStatsInternal can return number of contexts. Also, provide an option in MemoryContextStatsInternal to return without printing stats to either stderr or logs. --- src/backend/utils/mmgr/mcxt.c | 77 ++++++++++++++++++++++++++++++----- 1 file changed, 66 insertions(+), 11 deletions(-) diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index d98ae9db6be..2cbde8f39c3 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -135,6 +135,17 @@ static const MemoryContextMethods mcxt_methods[] = { }; #undef BOGUS_MCTX +/* + * This is passed to MemoryContextStatsInternal to determine whether + * to print context statistics or not and where to print them logs or + * stderr. + */ +typedef enum PrintDestination +{ + PRINT_STATS_TO_STDERR = 0, + PRINT_STATS_TO_LOGS, + PRINT_STATS_NONE +} PrintDestination; /* * CurrentMemoryContext @@ -162,7 +173,7 @@ static void MemoryContextCallResetCallbacks(MemoryContext context); static void MemoryContextStatsInternal(MemoryContext context, int level, int max_level, int max_children, MemoryContextCounters *totals, - bool print_to_stderr); + PrintDestination print_location, int *num_contexts); static void MemoryContextStatsPrint(MemoryContext context, void *passthru, const char *stats_string, bool print_to_stderr); @@ -831,11 +842,19 @@ MemoryContextStatsDetail(MemoryContext context, bool print_to_stderr) { MemoryContextCounters grand_totals; + int num_contexts; + PrintDestination print_location; memset(&grand_totals, 0, sizeof(grand_totals)); + if (print_to_stderr) + print_location = PRINT_STATS_TO_STDERR; + else + print_location = PRINT_STATS_TO_LOGS; + + /* num_contexts report number of contexts aggregated in the output */ MemoryContextStatsInternal(context, 0, max_level, max_children, - &grand_totals, print_to_stderr); + &grand_totals, print_location, &num_contexts); if (print_to_stderr) fprintf(stderr, @@ -870,13 +889,14 @@ MemoryContextStatsDetail(MemoryContext context, * One recursion level for MemoryContextStats * * Print stats for this context if possible, but in any case accumulate counts - * into *totals (if not NULL). + * into *totals (if not NULL). The callers should make sure that print_location + * is set to PRINT_STATS_STDERR or PRINT_STATS_TO_LOGS or PRINT_STATS_NONE. */ static void MemoryContextStatsInternal(MemoryContext context, int level, int max_level, int max_children, MemoryContextCounters *totals, - bool print_to_stderr) + PrintDestination print_location, int *num_contexts) { MemoryContext child; int ichild; @@ -884,10 +904,39 @@ MemoryContextStatsInternal(MemoryContext context, int level, Assert(MemoryContextIsValid(context)); /* Examine the context itself */ - context->methods->stats(context, - MemoryContextStatsPrint, - &level, - totals, print_to_stderr); + switch (print_location) + { + case PRINT_STATS_TO_STDERR: + context->methods->stats(context, + MemoryContextStatsPrint, + &level, + totals, true); + break; + + case PRINT_STATS_TO_LOGS: + context->methods->stats(context, + MemoryContextStatsPrint, + &level, + totals, false); + break; + + case PRINT_STATS_NONE: + + /* + * Do not print the statistics if print_location is + * PRINT_STATS_NONE, only compute totals. This is used in + * reporting of memory context statistics via a sql function. Last + * parameter is not relevant. ++ */ + context->methods->stats(context, + NULL, + NULL, + totals, false); + break; + } + + /* Increment the context count for each of the recursive call */ + *num_contexts = *num_contexts + 1; /* * Examine children. @@ -907,7 +956,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, MemoryContextStatsInternal(child, level + 1, max_level, max_children, totals, - print_to_stderr); + print_location, num_contexts); } } @@ -926,7 +975,13 @@ MemoryContextStatsInternal(MemoryContext context, int level, child = MemoryContextTraverseNext(child, context); } - if (print_to_stderr) + /* + * Add the count of children contexts which are traversed in the + * non-recursive manner. + */ + *num_contexts = *num_contexts + ichild; + + if (print_location == PRINT_STATS_TO_STDERR) { for (int i = 0; i <= level; i++) fprintf(stderr, " "); @@ -939,7 +994,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, local_totals.freechunks, local_totals.totalspace - local_totals.freespace); } - else + else if (print_location == PRINT_STATS_TO_LOGS) ereport(LOG_SERVER_ONLY, (errhidestmt(true), errhidecontext(true), -- 2.39.3 (Apple Git-146) ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-04-07 13:41 Daniel Gustafsson <[email protected]> parent: Daniel Gustafsson <[email protected]> 0 siblings, 2 replies; 52+ messages in thread From: Daniel Gustafsson @ 2025-04-07 13:41 UTC (permalink / raw) To: Rahila Syed <[email protected]>; +Cc: pgsql-hackers Following up on some off-list comments, attached is a v26 with a few small last changes: * Improved documentation (docs and comments) * Fixed up Shmem sizing and init * Delayed registering to the shmem cleanup to get it earlier in cleanup * Renamed a few datastructures to improve readability * Various bits of polish I think this function can be a valuable debugging aid going forward. -- Daniel Gustafsson Attachments: [application/octet-stream] v26-0001-Add-function-to-get-memory-context-stats-for-pro.patch (63.0K, ../../[email protected]/2-v26-0001-Add-function-to-get-memory-context-stats-for-pro.patch) download | inline diff: From 3fb017a35bf94cda6a95c6a6dfd074f289a2714f Mon Sep 17 00:00:00 2001 From: Daniel Gustafsson <[email protected]> Date: Mon, 7 Apr 2025 03:06:47 +0200 Subject: [PATCH v26] Add function to get memory context stats for processes This adds a function for retrieving memory context statistics and information from backends as well as auxiliary processes. When calling the function it sends a signal to the specified process to submit statistics regarding its memory contexts into dynamic shared memory. Each memory context is returned in detail, followed by a cumulative total in case the number of contexts exceed the max allocated amount of shared memory. Each process is limited to use at most 1Mb memory for this. A summary can also be explicitly requested by the user, this will return the TopMemoryContext and a cumulative total of all lower contexts. In order to not block on busy processes the caller specifies the number of seconds during which to retry before timing out. In the case where no statistics are published within the set timeout, the last known statistics are returned, or NULL if no previously published statistics exist. This allows dash- board type usages to continually publish data even if the target process is temporarily congested. Context records contain a timestamp to indicate when they were submitted. Author: Rahila Syed <[email protected]> Reviewed-by: Daniel Gustafsson <[email protected]> Reviewed-by: Tomas Vondra <[email protected]> Reviewed-by: Atsushi Torikoshi <[email protected]> Reviewed-by: Fujii Masao <[email protected]> Reviewed-by: Alexander Korotkov <[email protected]> Discussion: https://postgr.es/m/CAH2L28v8mc9HDt8QoSJ8TRmKau_8FM_HKS41NeO9-6ZAkuZKXw@mail.gmail.com --- doc/src/sgml/func.sgml | 171 +++++ src/backend/catalog/system_views.sql | 5 + src/backend/postmaster/autovacuum.c | 4 + src/backend/postmaster/auxprocess.c | 7 + src/backend/postmaster/checkpointer.c | 4 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 4 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 4 + src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/storage/lmgr/proc.c | 1 + src/backend/tcop/backend_startup.c | 7 + src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 1 + src/backend/utils/adt/mcxtfuncs.c | 406 ++++++++++- src/backend/utils/init/globals.c | 1 + src/backend/utils/mmgr/mcxt.c | 636 +++++++++++++++++- src/include/catalog/pg_proc.dat | 10 + src/include/miscadmin.h | 1 + src/include/storage/procsignal.h | 1 + src/include/utils/memutils.h | 81 +++ src/test/regress/expected/sysviews.out | 19 + src/test/regress/sql/sysviews.sql | 18 + src/tools/pgindent/typedefs.list | 4 + 25 files changed, 1362 insertions(+), 40 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 0224f93733d..347f45a417d 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -28650,6 +28650,144 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres} </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_get_process_memory_contexts</primary> + </indexterm> + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type>, <parameter>summary</parameter> <type>boolean</type>, <parameter>timeout</parameter> <type>float</type> ) + <returnvalue>setof record</returnvalue> + ( <parameter>name</parameter> <type>text</type>, + <parameter>ident</parameter> <type>text</type>, + <parameter>type</parameter> <type>text</type>, + <parameter>path</parameter> <type>integer[]</type>, + <parameter>level</parameter> <type>integer</type>, + <parameter>total_bytes</parameter> <type>bigint</type>, + <parameter>total_nblocks</parameter> <type>bigint</type>, + <parameter>free_bytes</parameter> <type>bigint</type>, + <parameter>free_chunks</parameter> <type>bigint</type>, + <parameter>used_bytes</parameter> <type>bigint</type>, + <parameter>num_agg_contexts</parameter> <type>integer</type>, + <parameter>stats_timestamp</parameter> <type>timestamptz</type> ) + </para> + <para> + This function handles requests to display the memory contexts of a + <productname>PostgreSQL</productname> process with the specified + process ID. The function can be used to send requests to backends as + well as <glossterm linkend="glossary-auxiliary-proc">auxiliary processes</glossterm>. + </para> + <para> + The returned record contains extended statistics per each memory + context: + <itemizedlist spacing="compact"> + <listitem> + <para> + <parameter>name</parameter> - The name of the memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>ident</parameter> - Memory context ID (if any). + </para> + </listitem> + <listitem> + <para> + <parameter>type</parameter> - The type of memory context, possible + values are: AllocSet, Generation, Slab and Bump. + </para> + </listitem> + <listitem> + <para> + <parameter>path</parameter> - Memory contexts are organized in a + tree model with TopMemoryContext as the root, and all other memory + contexts as nodes in the tree. The <parameter>path</parameter> + displays the path from the root to the current memory context. The + path is limited to 100 children per node, which each node limited + to a max depth of 100, to preserve memory during reporting. The + printed path will also be limited to 100 nodes counting from the + TopMemoryContext. + </para> + </listitem> + <listitem> + <para> + <parameter>level</parameter> - The level in the tree of the current + memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_bytes</parameter> - The total number of bytes + allocated to this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_nblocks</parameter> - The total number of blocks + used for the allocated memory. + </para> + </listitem> + <listitem> + <para> + <parameter>free_bytes</parameter> - The amount of free memory in + this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>free_chunks</parameter> - The number of chunks that + <parameter>free_bytes</parameter> corresponds to. + </para> + </listitem> + <listitem> + <para> + <parameter>used_bytes</parameter> - The total number of bytes + currently occupied. + </para> + </listitem> + <listitem> + <para> + <parameter>num_agg_contexts</parameter> - The number of memory + contexts aggregated in the displayed statistics. + </para> + </listitem> + <listitem> + <para> + <parameter>stats_timestamp</parameter> - When the statistics were + extracted from the process. + </para> + </listitem> + </itemizedlist> + </para> + <para> + When <parameter>summary</parameter> is <literal>true</literal>, statistics + for memory contexts at levels 1 and 2 are displayed, with level 1 + representing the root node (i.e., <literal>TopMemoryContext</literal>). + Statistics for contexts on level 2 and below are aggregates of all + child contexts' statistics, where <literal>num_agg_contexts</literal> + indicate the number aggregated child contexts. When + <parameter>summary</parameter> is <literal>false</literal>, + <literal>the num_agg_contexts</literal> value is <literal>1</literal>, + indicating that individual statistics are being displayed. The levels + are limited to the first 100 contexts. + </para> + <para> + Busy processes can delay reporting memory context statistics, + <parameter>timeout</parameter> specifies the number of seconds + to wait for updated statistics. <parameter>timeout</parameter> can be + specified in fractions of a second. + </para> + <para> + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context + statistics are aggregated and a cumulative total is displayed. The + <literal>num_agg_contexts</literal> column indicates the number of + contexts aggregated in the displayed statistics. When + <literal>num_agg_contexts</literal> is <literal>1</literal> is means + that the context statistics are displayed separately. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> @@ -28789,6 +28927,39 @@ LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 because it may generate a large number of log messages. </para> + <para> + <function>pg_get_process_memory_contexts</function> can be used + to request the memory contexts statistics of any postgres process. For example: +<programlisting> +postgres=# SELECT * FROM pg_get_process_memory_contexts( + (SELECT pid FROM pg_stat_activity + WHERE backend_type = 'checkpointer'), + false, 0.5) LIMIT 1; +-[ RECORD 1 ]----+------------------------------ +name | TopMemoryContext +ident | +type | AllocSet +path | {1} +level | 1 +total_bytes | 90304 +total_nblocks | 3 +free_bytes | 2880 +free_chunks | 1 +used_bytes | 87424 +num_agg_contexts | 1 +stats_timestamp | 2025-03-24 13:55:47.796698+01 +</programlisting> + <note> + <para> + While <function>pg_get_process_memory_contexts</function> can be used to + query memory contexts of the local backend, + <structname>pg_backend_memory_contexts</structname> + (see <xref linkend="view-pg-backend-memory-contexts"/> for more details) + will be less resource intensive when only the local backend is of interest. + </para> + </note> + </para> + </sect2> <sect2 id="functions-admin-backup"> diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index 273008db37f..1166e99a000 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -666,6 +666,11 @@ GRANT SELECT ON pg_backend_memory_contexts TO pg_read_all_stats; REVOKE EXECUTE ON FUNCTION pg_get_backend_memory_contexts() FROM PUBLIC; GRANT EXECUTE ON FUNCTION pg_get_backend_memory_contexts() TO pg_read_all_stats; +REVOKE EXECUTE ON FUNCTION + pg_get_process_memory_contexts(integer, boolean, float) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION + pg_get_process_memory_contexts(integer, boolean, float) TO pg_read_all_stats; + -- Statistics views CREATE VIEW pg_stat_all_tables AS diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 2513a8ef8a6..16756152b71 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -781,6 +781,10 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/auxprocess.c b/src/backend/postmaster/auxprocess.c index 4f6795f7265..d3b4df27935 100644 --- a/src/backend/postmaster/auxprocess.c +++ b/src/backend/postmaster/auxprocess.c @@ -84,6 +84,13 @@ AuxiliaryProcessMainCommon(void) /* register a before-shutdown callback for LWLock cleanup */ before_shmem_exit(ShutdownAuxiliaryProcess, 0); + /* + * The before shmem exit callback frees the DSA memory occupied by the + * latest memory context statistics that could be published by this aux + * proc if requested. + */ + before_shmem_exit(AtProcExit_memstats_dsa_free, 0); + SetProcessingMode(NormalProcessing); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index fda91ffd1ce..d3cb3f1891c 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -663,6 +663,10 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index 0ae9bf906ec..f24f574e748 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -48,6 +48,10 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index 7e622ae4bd2..cb7408acf4c 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -867,6 +867,10 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index 27e86cf393f..7149a67fcbc 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -192,6 +192,10 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index 0fec4f1f871..c7a76711cc5 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -879,6 +879,10 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index 2fa045e6b0f..00c76d05356 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -51,6 +51,7 @@ #include "storage/sinvaladt.h" #include "utils/guc.h" #include "utils/injection_point.h" +#include "utils/memutils.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -150,6 +151,7 @@ CalculateShmemSize(int *num_semaphores) size = add_size(size, InjectionPointShmemSize()); size = add_size(size, SlotSyncShmemSize()); size = add_size(size, AioShmemSize()); + size = add_size(size, MemoryContextReportingShmemSize()); /* include additional requested shmem from preload libraries */ size = add_size(size, total_addin_request); @@ -343,6 +345,7 @@ CreateOrAttachShmemStructs(void) WaitEventCustomShmemInit(); InjectionPointShmemInit(); AioShmemInit(); + MemoryContextReportingShmemInit(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index b7c39a4c5f0..a3c2cd12277 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -690,6 +690,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_GET_MEMORY_CONTEXT)) + HandleGetMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index e9ef0fbfe32..f194e6b3dcc 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -50,6 +50,7 @@ #include "storage/procsignal.h" #include "storage/spin.h" #include "storage/standby.h" +#include "utils/memutils.h" #include "utils/timeout.h" #include "utils/timestamp.h" diff --git a/src/backend/tcop/backend_startup.c b/src/backend/tcop/backend_startup.c index dde8d5b3517..5701e52f3a5 100644 --- a/src/backend/tcop/backend_startup.c +++ b/src/backend/tcop/backend_startup.c @@ -115,6 +115,13 @@ BackendMain(const void *startup_data, size_t startup_data_len) */ InitProcess(); + /* + * The before shmem exit callback frees the DSA memory occupied by the + * latest memory context statistics that could be published by this + * backend if requested. + */ + before_shmem_exit(AtProcExit_memstats_dsa_free, 0); + /* * Make sure we aren't in PostmasterContext anymore. (We can't delete it * just yet, though, because InitPostgres will need the HBA data.) diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 6ae9f38f0c8..dc4c600922d 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3535,6 +3535,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 8bce14c38fd..ee0e42535b6 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -161,6 +161,7 @@ WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." +MEM_CTX_PUBLISH "Waiting for a process to publish memory information." ABI_compatibility: diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index 396c2f223b4..7d3d4b3b1c6 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -17,28 +17,24 @@ #include "funcapi.h" #include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "access/twophase.h" +#include "catalog/pg_authid_d.h" #include "storage/proc.h" #include "storage/procarray.h" +#include "utils/acl.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/wait_event_types.h" /* ---------- * The max bytes for showing identifiers of MemoryContext. * ---------- */ #define MEMORY_CONTEXT_IDENT_DISPLAY_SIZE 1024 - -/* - * MemoryContextId - * Used for storage of transient identifiers for - * pg_get_backend_memory_contexts. - */ -typedef struct MemoryContextId -{ - MemoryContext context; - int context_id; -} MemoryContextId; +struct MemoryContextBackendState *memCtxState = NULL; +struct MemoryContextState *memCtxArea = NULL; /* * int_list_to_array @@ -143,24 +139,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, else nulls[1] = true; - switch (context->type) - { - case T_AllocSetContext: - type = "AllocSet"; - break; - case T_GenerationContext: - type = "Generation"; - break; - case T_SlabContext: - type = "Slab"; - break; - case T_BumpContext: - type = "Bump"; - break; - default: - type = "???"; - break; - } + type = ContextTypeToString(context->type); values[2] = CStringGetTextDatum(type); values[3] = Int32GetDatum(list_length(path)); /* level */ @@ -175,6 +154,38 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, list_free(path); } +/* + * ContextTypeToString + * Returns a textual representation of a context type + * + * This should cover the same types as MemoryContextIsValid. + */ +const char * +ContextTypeToString(NodeTag type) +{ + const char *context_type; + + switch (type) + { + case T_AllocSetContext: + context_type = "AllocSet"; + break; + case T_GenerationContext: + context_type = "Generation"; + break; + case T_SlabContext: + context_type = "Slab"; + break; + case T_BumpContext: + context_type = "Bump"; + break; + default: + context_type = "???"; + break; + } + return (context_type); +} + /* * pg_get_backend_memory_contexts * SQL SRF showing backend memory context. @@ -305,3 +316,340 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } + +/* + * pg_get_process_memory_contexts + * Signal a backend or an auxiliary process to send its memory contexts, + * wait for the results and display them. + * + * By default, only superusers or users with PG_READ_ALL_STATS are allowed to + * signal a process to return the memory contexts. This is because allowing + * any users to issue this request at an unbounded rate would cause lots of + * requests to be sent, which can lead to denial of service. Additional roles + * can be permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to copy the memory context details + * to a dynamic shared memory space. + * + * We have defined a limit on DSA memory that could be allocated per process - + * if the process has more memory contexts than what can fit in the allocated + * size, the excess contexts are summarized and represented as cumulative total + * at the end of the buffer. + * + * After sending the signal, wait on a condition variable. The publishing + * backend, after copying the data to shared memory, sends signal on that + * condition variable. There is one condition variable per publishing backend. + * Once the condition variable is signalled, check if the latest memory context + * information is available and display. + * + * If the publishing backend does not respond before the condition variable + * times out, which is set to MEMSTATS_WAIT_TIMEOUT, retry given that there is + * time left within the timeout specified by the user, before giving up and + * returning previously published statistics, if any. If no previous statistics + * exist, return NULL. + */ +#define MEMSTATS_WAIT_TIMEOUT 100 +Datum +pg_get_process_memory_contexts(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + bool summary = PG_GETARG_BOOL(1); + double timeout = PG_GETARG_FLOAT8(2); + double timer = 0; + PGPROC *proc; + ProcNumber procNumber = INVALID_PROC_NUMBER; + bool proc_is_aux = false; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + dsa_area *area; + MemoryContextStatsEntry *memctx_info; + TimestampTz curr_timestamp; + + /* + * See if the process with given pid is a backend or an auxiliary process + * and remember the type for when we requery the process later. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + { + proc = AuxiliaryPidGetProc(pid); + proc_is_aux = true; + } + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; this is however not a problem and leave with a WARNING. + * See comment in pg_log_backend_memory_contexts for a discussion on this. + */ + if (proc == NULL) + { + /* + * This is just a warning so a loop-through-resultset will not abort + * if one backend terminated on its own during the run. + */ + ereport(WARNING, + (errmsg("PID %d is not a PostgreSQL server process", pid))); + PG_RETURN_NULL(); + } + + InitMaterializedSRF(fcinfo, 0); + + procNumber = GetNumberFromPGProc(proc); + + LWLockAcquire(&memCtxState[procNumber].lw_lock, LW_EXCLUSIVE); + memCtxState[procNumber].summary = summary; + LWLockRelease(&memCtxState[procNumber].lw_lock); + + curr_timestamp = GetCurrentTimestamp(); + + /* + * Send a signal to a PostgreSQL process, informing it we want it to + * produce information about its memory contexts. + */ + if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, procNumber) < 0) + { + ereport(WARNING, + errmsg("could not send signal to process %d: %m", pid)); + PG_RETURN_NULL(); + } + + /* + * A valid DSA pointer isn't proof that statistics are available, it can + * be valid due to previously published stats. Check if the stats are + * updated by comparing the timestamp, if the stats are newer than our + * previously recorded timestamp from before sending the procsignal, they + * must by definition be updated. Wait for the timeout specified by the + * user, following which display old statistics if available or return + * NULL. + */ + while (1) + { + long msecs; + + /* + * We expect to come out of sleep when the requested process has + * finished publishing the statistics, verified using the valid DSA + * pointer. + * + * Make sure that the information belongs to pid we requested + * information for, Otherwise loop back and wait for the server + * process to finish publishing statistics. + */ + LWLockAcquire(&memCtxState[procNumber].lw_lock, LW_EXCLUSIVE); + + /* + * Note in procnumber.h file says that a procNumber can be re-used for + * a different backend immediately after a backend exits. In case an + * old process' data was there and not updated by the current process + * in the slot identified by the procNumber, the pid of the requested + * process and the proc_id might not match. + */ + if (memCtxState[procNumber].proc_id == pid) + { + /* + * Break if the latest stats have been read, indicated by + * statistics timestamp being newer than the current request + * timestamp. + */ + msecs = TimestampDifferenceMilliseconds(curr_timestamp, + memCtxState[procNumber].stats_timestamp); + + if (DsaPointerIsValid(memCtxState[procNumber].memstats_dsa_pointer) + && msecs >= 0) + break; + } + LWLockRelease(&memCtxState[procNumber].lw_lock); + + /* + * Recheck the state of the backend before sleeping on the condition + * variable to ensure the process is still alive. Only check the + * relevant process type based on the earlier PID check. + */ + if (proc_is_aux) + proc = AuxiliaryPidGetProc(pid); + else + proc = BackendPidGetProc(pid); + + /* + * The process ending during memory context processing is not an + * error. + */ + if (proc == NULL) + { + ereport(WARNING, + errmsg("PID %d is no longer a PostgreSQL server process", + pid)); + PG_RETURN_NULL(); + } + + if (ConditionVariableTimedSleep(&memCtxState[procNumber].memctx_cv, + MEMSTATS_WAIT_TIMEOUT, + WAIT_EVENT_MEM_CTX_PUBLISH)) + { + timer += MEMSTATS_WAIT_TIMEOUT; + + /* + * Wait for the timeout as defined by the user. If no updated + * statistics are available within the allowed time then display + * previously published statistics if there are any. If no + * previous statistics are available then return NULL. The timer + * is defined in milliseconds since thats what the condition + * variable sleep uses. + */ + if ((timer * 1000) >= timeout) + { + LWLockAcquire(&memCtxState[procNumber].lw_lock, LW_EXCLUSIVE); + /* Displaying previously published statistics if available */ + if (DsaPointerIsValid(memCtxState[procNumber].memstats_dsa_pointer)) + break; + else + { + LWLockRelease(&memCtxState[procNumber].lw_lock); + PG_RETURN_NULL(); + } + } + } + } + + /* + * We should only reach here with a valid DSA handle, either containing + * updated statistics or previously published statistics (identified by + * the timestamp. + */ + Assert(memCtxArea->memstats_dsa_handle != DSA_HANDLE_INVALID); + area = dsa_attach(memCtxArea->memstats_dsa_handle); + + /* + * Backend has finished publishing the stats, project them. + */ + memctx_info = (MemoryContextStatsEntry *) + dsa_get_address(area, memCtxState[procNumber].memstats_dsa_pointer); + +#define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 12 + for (int i = 0; i < memCtxState[procNumber].total_stats; i++) + { + ArrayType *path_array; + int path_length; + Datum values[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + bool nulls[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + char *name; + char *ident; + Datum *path_datum = NULL; + int *path_int = NULL; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + if (DsaPointerIsValid(memctx_info[i].name)) + { + name = (char *) dsa_get_address(area, memctx_info[i].name); + values[0] = CStringGetTextDatum(name); + } + else + nulls[0] = true; + + if (DsaPointerIsValid(memctx_info[i].ident)) + { + ident = (char *) dsa_get_address(area, memctx_info[i].ident); + values[1] = CStringGetTextDatum(ident); + } + else + nulls[1] = true; + + if (memctx_info[i].type != NULL) + values[2] = CStringGetTextDatum(memctx_info[i].type); + else + nulls[2] = true; + + path_length = memctx_info[i].path_length; + path_datum = (Datum *) palloc(path_length * sizeof(Datum)); + if (DsaPointerIsValid(memctx_info[i].path)) + { + path_int = (int *) dsa_get_address(area, memctx_info[i].path); + for (int j = 0; j < path_length; j++) + path_datum[j] = Int32GetDatum(path_int[j]); + path_array = construct_array_builtin(path_datum, path_length, INT4OID); + values[3] = PointerGetDatum(path_array); + } + else + nulls[3] = true; + + values[4] = Int32GetDatum(memctx_info[i].levels); + values[5] = Int64GetDatum(memctx_info[i].totalspace); + values[6] = Int64GetDatum(memctx_info[i].nblocks); + values[7] = Int64GetDatum(memctx_info[i].freespace); + values[8] = Int64GetDatum(memctx_info[i].freechunks); + values[9] = Int64GetDatum(memctx_info[i].totalspace - + memctx_info[i].freespace); + values[10] = Int32GetDatum(memctx_info[i].num_agg_stats); + values[11] = TimestampTzGetDatum(memCtxState[procNumber].stats_timestamp); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + LWLockRelease(&memCtxState[procNumber].lw_lock); + + ConditionVariableCancelSleep(); + dsa_detach(area); + + PG_RETURN_NULL(); +} + +Size +MemoryContextReportingShmemSize(void) +{ + Size sz = 0; + Size TotalProcs = 0; + + TotalProcs = add_size(TotalProcs, NUM_AUXILIARY_PROCS); + TotalProcs = add_size(TotalProcs, MaxBackends); + + sz = add_size(sz, sizeof(MemoryContextState)); + sz = mul_size(sz, sizeof(MemoryContextBackendState)); + + return sz; +} + +/* + * Initialize shared memory for displaying memory context statistics + */ +void +MemoryContextReportingShmemInit(void) +{ + bool found; + + memCtxArea = (MemoryContextState *) + ShmemInitStruct("MemoryContextState", sizeof(MemoryContextState), &found); + + if (!IsUnderPostmaster) + { + Assert(!found); + LWLockInitialize(&memCtxArea->lw_lock, LWLockNewTrancheId()); + LWLockRegisterTranche(memCtxArea->lw_lock.tranche, + "mem_context_stats_reporting"); + memCtxArea->memstats_dsa_handle = DSA_HANDLE_INVALID; + } + else + Assert(found); + + memCtxState = (MemoryContextBackendState *) + ShmemInitStruct("MemoryContextBackendState", + ((MaxBackends + NUM_AUXILIARY_PROCS) * sizeof(MemoryContextBackendState)), + &found); + + if (!IsUnderPostmaster) + { + Assert(!found); + for (int i = 0; i < (MaxBackends + NUM_AUXILIARY_PROCS); i++) + { + ConditionVariableInit(&memCtxState[i].memctx_cv); + LWLockInitialize(&memCtxState[i].lw_lock, LWLockNewTrancheId()); + LWLockRegisterTranche(memCtxState[i].lw_lock.tranche, + "mem_context_backend_stats_reporting"); + memCtxState[i].memstats_dsa_pointer = InvalidDsaPointer; + } + } + else + Assert(found); +} diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index 2152aad97d9..92304a1f124 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -39,6 +39,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t PublishMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index d98ae9db6be..b3ed304d161 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -23,6 +23,11 @@ #include "mb/pg_wchar.h" #include "miscadmin.h" +#include "nodes/pg_list.h" +#include "storage/lwlock.h" +#include "storage/ipc.h" +#include "utils/dsa.h" +#include "utils/hsearch.h" #include "utils/memdebug.h" #include "utils/memutils.h" #include "utils/memutils_internal.h" @@ -135,6 +140,17 @@ static const MemoryContextMethods mcxt_methods[] = { }; #undef BOGUS_MCTX +/* + * This is passed to MemoryContextStatsInternal to determine whether + * to print context statistics or not and where to print them logs or + * stderr. + */ +typedef enum PrintDestination +{ + PRINT_STATS_TO_STDERR = 0, + PRINT_STATS_TO_LOGS, + PRINT_STATS_NONE +} PrintDestination; /* * CurrentMemoryContext @@ -162,10 +178,24 @@ static void MemoryContextCallResetCallbacks(MemoryContext context); static void MemoryContextStatsInternal(MemoryContext context, int level, int max_level, int max_children, MemoryContextCounters *totals, - bool print_to_stderr); + PrintDestination print_location, + int *num_contexts); static void MemoryContextStatsPrint(MemoryContext context, void *passthru, const char *stats_string, bool print_to_stderr); +static void PublishMemoryContext(MemoryContextStatsEntry *memctx_infos, + int curr_id, MemoryContext context, + List *path, + MemoryContextCounters stat, + int num_contexts, dsa_area *area, + int max_levels); +static void compute_contexts_count_and_ids(List *contexts, HTAB *context_id_lookup, + int *stats_count, + bool summary); +static List *compute_context_path(MemoryContext c, HTAB *context_id_lookup); +static void free_memorycontextstate_dsa(dsa_area *area, int total_stats, + dsa_pointer prev_dsa_pointer); +static void signal_memorycontext_reporting(void); /* * You should not do memory allocations within a critical section, because @@ -831,11 +861,19 @@ MemoryContextStatsDetail(MemoryContext context, bool print_to_stderr) { MemoryContextCounters grand_totals; + int num_contexts; + PrintDestination print_location; memset(&grand_totals, 0, sizeof(grand_totals)); + if (print_to_stderr) + print_location = PRINT_STATS_TO_STDERR; + else + print_location = PRINT_STATS_TO_LOGS; + + /* num_contexts report number of contexts aggregated in the output */ MemoryContextStatsInternal(context, 0, max_level, max_children, - &grand_totals, print_to_stderr); + &grand_totals, print_location, &num_contexts); if (print_to_stderr) fprintf(stderr, @@ -870,13 +908,14 @@ MemoryContextStatsDetail(MemoryContext context, * One recursion level for MemoryContextStats * * Print stats for this context if possible, but in any case accumulate counts - * into *totals (if not NULL). + * into *totals (if not NULL). The callers should make sure that print_location + * is set to PRINT_STATS_STDERR or PRINT_STATS_TO_LOGS or PRINT_STATS_NONE. */ static void MemoryContextStatsInternal(MemoryContext context, int level, int max_level, int max_children, MemoryContextCounters *totals, - bool print_to_stderr) + PrintDestination print_location, int *num_contexts) { MemoryContext child; int ichild; @@ -884,10 +923,39 @@ MemoryContextStatsInternal(MemoryContext context, int level, Assert(MemoryContextIsValid(context)); /* Examine the context itself */ - context->methods->stats(context, - MemoryContextStatsPrint, - &level, - totals, print_to_stderr); + switch (print_location) + { + case PRINT_STATS_TO_STDERR: + context->methods->stats(context, + MemoryContextStatsPrint, + &level, + totals, true); + break; + + case PRINT_STATS_TO_LOGS: + context->methods->stats(context, + MemoryContextStatsPrint, + &level, + totals, false); + break; + + case PRINT_STATS_NONE: + + /* + * Do not print the statistics if print_location is + * PRINT_STATS_NONE, only compute totals. This is used in + * reporting of memory context statistics via a sql function. Last + * parameter is not relevant. + */ + context->methods->stats(context, + NULL, + NULL, + totals, false); + break; + } + + /* Increment the context count for each of the recursive call */ + *num_contexts = *num_contexts + 1; /* * Examine children. @@ -907,7 +975,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, MemoryContextStatsInternal(child, level + 1, max_level, max_children, totals, - print_to_stderr); + print_location, num_contexts); } } @@ -926,7 +994,13 @@ MemoryContextStatsInternal(MemoryContext context, int level, child = MemoryContextTraverseNext(child, context); } - if (print_to_stderr) + /* + * Add the count of children contexts which are traversed in the + * non-recursive manner. + */ + *num_contexts = *num_contexts + ichild; + + if (print_location == PRINT_STATS_TO_STDERR) { for (int i = 0; i <= level; i++) fprintf(stderr, " "); @@ -939,7 +1013,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, local_totals.freechunks, local_totals.totalspace - local_totals.freespace); } - else + else if (print_location == PRINT_STATS_TO_LOGS) ereport(LOG_SERVER_ONLY, (errhidestmt(true), errhidecontext(true), @@ -1276,6 +1350,22 @@ HandleLogMemoryContextInterrupt(void) /* latch will be set by procsignal_sigusr1_handler */ } +/* + * HandleGetMemoryContextInterrupt + * Handle receipt of an interrupt indicating a request to publish memory + * contexts statistics. + * + * All the actual work is deferred to ProcessGetMemoryContextInterrupt() as + * this cannot be performed in a signal handler. + */ +void +HandleGetMemoryContextInterrupt(void) +{ + InterruptPending = true; + PublishMemoryContextPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + /* * ProcessLogMemoryContextInterrupt * Perform logging of memory contexts of this backend process. @@ -1313,6 +1403,530 @@ ProcessLogMemoryContextInterrupt(void) MemoryContextStatsDetail(TopMemoryContext, 100, 100, false); } +/* + * ProcessGetMemoryContextInterrupt + * Generate information about memory contexts used by the process. + * + * Performs a breadth first search on the memory context tree, thus parents + * statistics are reported before their children in the monitoring function + * output. + * + * Statistics for all the processes are shared via the same dynamic shared + * area. Statistics written by each process are tracked independently in + * per-process DSA pointers. These pointers are stored in static shared memory. + * + * We calculate maximum number of context's statistics that can be displayed + * using a pre-determined limit for memory available per process for this + * utility maximum size of statistics for each context. The remaining context + * statistics if any are captured as a cumulative total at the end of + * individual context's statistics. + * + * If summary is true, we capture the level 1 and level 2 contexts + * statistics. For that we traverse the memory context tree recursively in + * depth first search manner to cover all the children of a parent context, to + * be able to display a cumulative total of memory consumption by a parent at + * level 2 and all its children. + */ +void +ProcessGetMemoryContextInterrupt(void) +{ + List *contexts; + HASHCTL ctl; + HTAB *context_id_lookup; + int context_id = 0; + MemoryContextStatsEntry *meminfo; + bool summary = false; + dsa_area *area = NULL; + int max_stats; + int idx = MyProcNumber; + int stats_count = 0; + int stats_num = 0; + MemoryContextCounters stat; + int num_individual_stats = 0; + + PublishMemoryContextPending = false; + + /* + * The hash table is used for constructing "path" column of the view, + * similar to its local backend counterpart. + */ + ctl.keysize = sizeof(MemoryContext); + ctl.entrysize = sizeof(MemoryContextId); + ctl.hcxt = CurrentMemoryContext; + + context_id_lookup = hash_create("pg_get_remote_backend_memory_contexts", + 256, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + /* List of contexts to process in the next round - start at the top. */ + contexts = list_make1(TopMemoryContext); + + /* Compute the number of stats that can fit in the defined limit */ + max_stats = (MAX_SEGMENTS_PER_BACKEND * DSA_DEFAULT_INIT_SEGMENT_SIZE) + / (MAX_MEMORY_CONTEXT_STATS_SIZE); + LWLockAcquire(&memCtxState[idx].lw_lock, LW_EXCLUSIVE); + summary = memCtxState[idx].summary; + LWLockRelease(&memCtxState[idx].lw_lock); + + /* + * Traverse the memory context tree to find total number of contexts. If + * summary is requested report the total number of contexts at level 1 and + * 2 from the top. Also, populate the hash table of context ids. + */ + compute_contexts_count_and_ids(contexts, context_id_lookup, &stats_count, + summary); + + /* + * Allocate memory in this process's DSA for storing statistics of the the + * memory contexts upto max_stats, for contexts that don't fit within a + * limit, a cumulative total is written as the last record in the DSA + * segment. + */ + stats_num = Min(stats_count, max_stats); + + LWLockAcquire(&memCtxArea->lw_lock, LW_EXCLUSIVE); + + /* + * Create a DSA and send handle to the the client process after storing + * the context statistics. If number of contexts exceed a predefined + * limit(8MB), a cumulative total is stored for such contexts. + */ + if (memCtxArea->memstats_dsa_handle == DSA_HANDLE_INVALID) + { + MemoryContext oldcontext = CurrentMemoryContext; + dsa_handle handle; + + MemoryContextSwitchTo(TopMemoryContext); + + area = dsa_create(memCtxArea->lw_lock.tranche); + + handle = dsa_get_handle(area); + MemoryContextSwitchTo(oldcontext); + + dsa_pin_mapping(area); + + /* + * Pin the DSA area, this is to make sure the area remains attachable + * even if current backend exits. This is done so that the statistics + * are published even if the process exits while a client is waiting. + */ + dsa_pin(area); + + /* Set the handle in shared memory */ + memCtxArea->memstats_dsa_handle = handle; + } + + /* + * If DSA exists, created by another process publishing statistics, or by + * the previous execution of this function by this process, attach to it. + */ + else if (area == NULL) + { + MemoryContext oldcontext = CurrentMemoryContext; + + MemoryContextSwitchTo(TopMemoryContext); + area = dsa_attach(memCtxArea->memstats_dsa_handle); + MemoryContextSwitchTo(oldcontext); + dsa_pin_mapping(area); + } + LWLockRelease(&memCtxArea->lw_lock); + + /* + * Hold the process lock to protect writes to process specific memory. Two + * processes publishing statistics do not block each other. + */ + LWLockAcquire(&memCtxState[idx].lw_lock, LW_EXCLUSIVE); + memCtxState[idx].proc_id = MyProcPid; + + if (DsaPointerIsValid(memCtxState[idx].memstats_dsa_pointer)) + { + /* + * Free any previous allocations, free the name, ident and path + * pointers before freeing the pointer that contains them. + */ + free_memorycontextstate_dsa(area, memCtxState[idx].total_stats, + memCtxState[idx].memstats_dsa_pointer); + + dsa_free(area, memCtxState[idx].memstats_dsa_pointer); + memCtxState[idx].memstats_dsa_pointer = InvalidDsaPointer; + } + memCtxState[idx].memstats_dsa_pointer = + dsa_allocate0(area, stats_num * sizeof(MemoryContextStatsEntry)); + + meminfo = (MemoryContextStatsEntry *) + dsa_get_address(area, memCtxState[idx].memstats_dsa_pointer); + + if (summary) + { + int ctx_id = 0; + List *path = NIL; + + /* Copy TopMemoryContext statistics to DSA */ + memset(&stat, 0, sizeof(stat)); + (*TopMemoryContext->methods->stats) (TopMemoryContext, NULL, NULL, + &stat, true); + path = lcons_int(1, path); + PublishMemoryContext(meminfo, ctx_id, TopMemoryContext, path, stat, + 1, area, 100); + ctx_id = ctx_id + 1; + + /* + * Copy statistics for each of TopMemoryContexts children. This + * includes statistics of at most 100 children per node, with each + * child node limited to a depth of 100 in its subtree. + */ + for (MemoryContext c = TopMemoryContext->firstchild; c != NULL; + c = c->nextchild) + { + MemoryContextCounters grand_totals; + int num_contexts = 0; + int level = 0; + + path = NIL; + memset(&grand_totals, 0, sizeof(grand_totals)); + + MemoryContextStatsInternal(c, level, 100, 100, &grand_totals, + PRINT_STATS_NONE, &num_contexts); + + path = compute_context_path(c, context_id_lookup); + + PublishMemoryContext(meminfo, ctx_id, c, path, + grand_totals, num_contexts, area, 100); + ctx_id = ctx_id + 1; + } + memCtxState[idx].total_stats = ctx_id; + /* Notify waiting backends and return */ + hash_destroy(context_id_lookup); + dsa_detach(area); + signal_memorycontext_reporting(); + } + + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + + /* + * Figure out the transient context_id of this context and each of its + * ancestors, to compute a path for this context. + */ + path = compute_context_path(cur, context_id_lookup); + + /* Account for saving one statistics slot for cumulative reporting */ + if (context_id < (max_stats - 1) || stats_count <= max_stats) + { + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1, area, 100); + } + else + { + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + meminfo[max_stats - 1].totalspace += stat.totalspace; + meminfo[max_stats - 1].nblocks += stat.nblocks; + meminfo[max_stats - 1].freespace += stat.freespace; + meminfo[max_stats - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit per process is reached, write aggregate of the + * remaining statistics. + * + * We can store contexts from 0 to max_stats - 1. When stats_count is + * greater than max_stats, we stop reporting individual statistics + * when context_id equals max_stats - 2. As we use max_stats - 1 array + * slot for reporting cumulative statistics or "Remaining Totals". + */ + if (stats_count > max_stats && context_id == (max_stats - 2)) + { + char *nameptr; + int namelen = strlen("Remaining Totals"); + + num_individual_stats = context_id + 1; + meminfo[max_stats - 1].name = dsa_allocate0(area, namelen + 1); + nameptr = dsa_get_address(area, meminfo[max_stats - 1].name); + strncpy(nameptr, "Remaining Totals", namelen); + meminfo[max_stats - 1].ident = InvalidDsaPointer; + meminfo[max_stats - 1].path = InvalidDsaPointer; + meminfo[max_stats - 1].type = NULL; + } + context_id++; + } + + /* + * Statistics are not aggregated, i.e individual statistics reported when + * stats_count <= max_stats. + */ + if (stats_count <= max_stats) + { + memCtxState[idx].total_stats = context_id; + } + /* Report number of aggregated memory contexts */ + else + { + meminfo[max_stats - 1].num_agg_stats = context_id - + num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for cumulative + * statistics. + */ + memCtxState[idx].total_stats = num_individual_stats + 1; + } + + /* Notify waiting backends and return */ + hash_destroy(context_id_lookup); + dsa_detach(area); + signal_memorycontext_reporting(); +} + +/* + * Signal all the waiting client backends after copying all the statistics. + */ +static void +signal_memorycontext_reporting(void) +{ + memCtxState[MyProcNumber].stats_timestamp = GetCurrentTimestamp(); + LWLockRelease(&memCtxState[MyProcNumber].lw_lock); + ConditionVariableBroadcast(&memCtxState[MyProcNumber].memctx_cv); +} + +/* + * compute_context_path + * + * Append the transient context_id of this context and each of its ancestors + * to a list, in order to compute a path. + */ +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + MemoryContext cur_context; + + for (cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + elog(ERROR, "hash table corrupted, can't construct path value"); + + path = lcons_int(cur_entry->context_id, path); + } + + return path; +} + +/* + * Return the number of contexts allocated currently by the backend + * Assign context ids to each of the contexts. + */ +static void +compute_contexts_count_and_ids(List *contexts, HTAB *context_id_lookup, + int *stats_count, bool summary) +{ + foreach_ptr(MemoryContextData, cur, contexts) + { + MemoryContextId *entry; + bool found; + + entry = (MemoryContextId *) hash_search(context_id_lookup, &cur, + HASH_ENTER, &found); + Assert(!found); + + /* context id starts with 1 */ + entry->context_id = ++(*stats_count); + + /* Append the children of the current context to the main list. */ + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + { + if (summary) + { + entry = (MemoryContextId *) hash_search(context_id_lookup, &c, + HASH_ENTER, &found); + Assert(!found); + + entry->context_id = ++(*stats_count); + } + + contexts = lappend(contexts, c); + } + + /* + * In summary mode only the first two level (from top) contexts are + * displayed. + */ + if (summary) + break; + } +} + +/* + * PublishMemoryContext + * + * Copy the memory context statistics of a single context to a DSA memory + */ +static void +PublishMemoryContext(MemoryContextStatsEntry *memctx_info, int curr_id, + MemoryContext context, List *path, + MemoryContextCounters stat, int num_contexts, + dsa_area *area, int max_levels) +{ + const char *ident = context->ident; + const char *name = context->name; + int *path_list; + + /* + * To be consistent with logging output, we label dynahash contexts with + * just the hash table name as with MemoryContextStatsPrint(). + */ + if (context->ident && strncmp(context->name, "dynahash", 8) == 0) + { + name = context->ident; + ident = NULL; + } + + if (name != NULL) + { + int namelen = strlen(name); + char *nameptr; + + if (strlen(name) >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + namelen = pg_mbcliplen(name, namelen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + memctx_info[curr_id].name = dsa_allocate0(area, namelen + 1); + nameptr = (char *) dsa_get_address(area, memctx_info[curr_id].name); + strlcpy(nameptr, name, namelen + 1); + } + else + memctx_info[curr_id].name = InvalidDsaPointer; + + /* Trim and copy the identifier if it is not set to NULL */ + if (ident != NULL) + { + int idlen = strlen(context->ident); + char *identptr; + + /* + * Some identifiers such as SQL query string can be very long, + * truncate oversize identifiers. + */ + if (idlen >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + idlen = pg_mbcliplen(ident, idlen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + memctx_info[curr_id].ident = dsa_allocate0(area, idlen + 1); + identptr = (char *) dsa_get_address(area, memctx_info[curr_id].ident); + strlcpy(identptr, ident, idlen + 1); + } + else + memctx_info[curr_id].ident = InvalidDsaPointer; + + /* Allocate DSA memory for storing path information */ + if (path == NIL) + memctx_info[curr_id].path = InvalidDsaPointer; + else + { + int levels = Min(list_length(path), max_levels); + + memctx_info[curr_id].path_length = levels; + memctx_info[curr_id].path = dsa_allocate0(area, levels * sizeof(int)); + memctx_info[curr_id].levels = list_length(path); + path_list = (int *) dsa_get_address(area, memctx_info[curr_id].path); + + foreach_int(i, path) + { + path_list[foreach_current_index(i)] = i; + if (--levels == 0) + break; + } + } + memctx_info[curr_id].type = ContextTypeToString(context->type); + memctx_info[curr_id].totalspace = stat.totalspace; + memctx_info[curr_id].nblocks = stat.nblocks; + memctx_info[curr_id].freespace = stat.freespace; + memctx_info[curr_id].freechunks = stat.freechunks; + memctx_info[curr_id].num_agg_stats = num_contexts; +} + +/* + * free_memorycontextstate_dsa + * + * Worker for freeing resources from a MemoryContextStatsEntry. Callers are + * responsible for ensuring that the DSA pointer is valid. + */ +static void +free_memorycontextstate_dsa(dsa_area *area, int total_stats, + dsa_pointer prev_dsa_pointer) +{ + MemoryContextStatsEntry *meminfo; + + meminfo = (MemoryContextStatsEntry *) dsa_get_address(area, prev_dsa_pointer); + Assert(meminfo != NULL); + for (int i = 0; i < total_stats; i++) + { + if (DsaPointerIsValid(meminfo[i].name)) + dsa_free(area, meminfo[i].name); + + if (DsaPointerIsValid(meminfo[i].ident)) + dsa_free(area, meminfo[i].ident); + + if (DsaPointerIsValid(meminfo[i].path)) + dsa_free(area, meminfo[i].path); + } +} + +/* + * Free the memory context statistics stored by this process + * in DSA area. + */ +void +AtProcExit_memstats_dsa_free(int code, Datum arg) +{ + int idx = MyProcNumber; + dsm_segment *dsm_seg = NULL; + dsa_area *area = NULL; + + if (memCtxArea->memstats_dsa_handle == DSA_HANDLE_INVALID) + return; + + dsm_seg = dsm_find_mapping(memCtxArea->memstats_dsa_handle); + + LWLockAcquire(&memCtxState[idx].lw_lock, LW_EXCLUSIVE); + + if (!DsaPointerIsValid(memCtxState[idx].memstats_dsa_pointer)) + { + LWLockRelease(&memCtxState[idx].lw_lock); + return; + } + + /* If the dsm mapping could not be found, attach to the area */ + if (dsm_seg != NULL) + return; + area = dsa_attach(memCtxArea->memstats_dsa_handle); + + /* + * Free the memory context statistics, free the name, ident and path + * pointers before freeing the pointer that contains these pointers and + * integer statistics. + */ + free_memorycontextstate_dsa(area, memCtxState[idx].total_stats, + memCtxState[idx].memstats_dsa_pointer); + dsa_free(area, memCtxState[idx].memstats_dsa_pointer); + memCtxState[idx].memstats_dsa_pointer = InvalidDsaPointer; + + dsa_detach(area); + LWLockRelease(&memCtxState[idx].lw_lock); +} + void * palloc(Size size) { diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 5d5be8ba4e1..90675be66f6 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8559,6 +8559,16 @@ prorettype => 'bool', proargtypes => 'int4', prosrc => 'pg_log_backend_memory_contexts' }, +# publishing memory contexts of the specified postgres process +{ oid => '2173', descr => 'publish memory contexts of the specified backend', + proname => 'pg_get_process_memory_contexts', provolatile => 'v', + prorows => '100', proretset => 't', proparallel => 'r', + prorettype => 'record', proargtypes => 'int4 bool float8', + proallargtypes => '{int4,bool,float8,text,text,text,_int4,int4,int8,int8,int8,int8,int8,int4,timestamptz}', + proargmodes => '{i,i,i,o,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{pid, summary, retries, name, ident, type, path, level, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes, num_agg_contexts, stats_timestamp}', + prosrc => 'pg_get_process_memory_contexts' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index 0d8528b2875..58b2496a9cb 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -96,6 +96,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t PublishMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index 016dfd9b3f6..cfe14631445 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -35,6 +35,7 @@ typedef enum PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to send the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ /* Recovery conflict reasons */ diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 8abc26abce2..bfeb1575276 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -18,6 +18,9 @@ #define MEMUTILS_H #include "nodes/memnodes.h" +#include "storage/condition_variable.h" +#include "storage/lmgr.h" +#include "utils/dsa.h" /* @@ -48,6 +51,22 @@ #define AllocHugeSizeIsValid(size) ((Size) (size) <= MaxAllocHugeSize) +/* + * Memory Context reporting size limits. + */ + +/* Max length of context name and ident */ +#define MEMORY_CONTEXT_IDENT_SHMEM_SIZE 64 +/* Maximum size (in Mb) of DSA area per process */ +#define MAX_SEGMENTS_PER_BACKEND 1 +/* + * Maximum size per context. Actual size may be lower as this assumes the worst + * case of deepest path and longest identifiers (name and ident, thus the + * multiplication by 2). The path depth is limited to 100 like for memory + * context logging. + */ +#define MAX_MEMORY_CONTEXT_STATS_SIZE sizeof(MemoryContextStatsEntry) + \ + (100 * sizeof(int)) + (2 * MEMORY_CONTEXT_IDENT_SHMEM_SIZE) /* * Standard top-level memory contexts. @@ -319,4 +338,66 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) return true; } +/* Dynamic shared memory state for statistics per context */ +typedef struct MemoryContextStatsEntry +{ + dsa_pointer name; + dsa_pointer ident; + dsa_pointer path; + const char *type; + int path_length; + int levels; + int64 totalspace; + int64 nblocks; + int64 freespace; + int64 freechunks; + int num_agg_stats; +} MemoryContextStatsEntry; + +/* + * Static shared memory state representing the DSA area created for memory + * context statistics reporting. A single DSA area is created and used by all + * the processes, each having its specific DSA allocations for sharing memory + * statistics, tracked by per backend static shared memory state. + */ +typedef struct MemoryContextState +{ + dsa_handle memstats_dsa_handle; + LWLock lw_lock; +} MemoryContextState; + +/* + * Per backend static shared memory state for memory context statistics + * reporting. + */ +typedef struct MemoryContextBackendState +{ + ConditionVariable memctx_cv; + LWLock lw_lock; + int proc_id; + int total_stats; + bool summary; + dsa_pointer memstats_dsa_pointer; + TimestampTz stats_timestamp; +} MemoryContextBackendState; + + +/* + * Used for storage of transient identifiers for pg_get_backend_memory_contexts + */ +typedef struct MemoryContextId +{ + MemoryContext context; + int context_id; +} MemoryContextId; + +extern PGDLLIMPORT MemoryContextBackendState *memCtxState; +extern PGDLLIMPORT MemoryContextState *memCtxArea; +extern void ProcessGetMemoryContextInterrupt(void); +extern const char *ContextTypeToString(NodeTag type); +extern void HandleGetMemoryContextInterrupt(void); +extern Size MemoryContextReportingShmemSize(void); +extern void MemoryContextReportingShmemInit(void); +extern void AtProcExit_memstats_dsa_free(int code, Datum arg); + #endif /* MEMUTILS_H */ diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 83228cfca29..343fc8ca2a1 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -232,3 +232,22 @@ select * from pg_timezone_abbrevs where abbrev = 'LMT'; LMT | @ 7 hours 52 mins 58 secs ago | f (1 row) +DO $$ +DECLARE + launcher_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='autovacuum launcher' + INTO launcher_pid; + + select type, name, ident + from pg_get_process_memory_contexts(launcher_pid, false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; +NOTICE: (AllocSet,TopMemoryContext,) +NOTICE: (AllocSet,TopMemoryContext,) diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 66179f026b3..dd92b520070 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -101,3 +101,21 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs; -- One specific case we can check without much fear of breakage -- is the historical local-mean-time value used for America/Los_Angeles. select * from pg_timezone_abbrevs where abbrev = 'LMT'; + +DO $$ +DECLARE + launcher_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='autovacuum launcher' + INTO launcher_pid; + + select type, name, ident + from pg_get_process_memory_contexts(launcher_pid, false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index d42b943ef94..c2fcc47a803 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1662,12 +1662,16 @@ MemoizeState MemoizeTuple MemoryChunk MemoryContext +MemoryContextBackendState MemoryContextCallback MemoryContextCallbackFunction MemoryContextCounters MemoryContextData +MemoryContextId MemoryContextMethodID MemoryContextMethods +MemoryContextState +MemoryContextStatsEntry MemoryStatsPrintFunc MergeAction MergeActionState -- 2.39.3 (Apple Git-146) ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-04-07 16:38 Andres Freund <[email protected]> parent: Daniel Gustafsson <[email protected]> 1 sibling, 0 replies; 52+ messages in thread From: Andres Freund @ 2025-04-07 16:38 UTC (permalink / raw) To: Rahila Syed <[email protected]>; +Cc: Daniel Gustafsson <[email protected]>; pgsql-hackers Hi, On 2025-04-07 21:57:57 +0530, Rahila Syed wrote: > > > diff --git a/src/backend/postmaster/auxprocess.c > > b/src/backend/postmaster/auxprocess.c > > > index 4f6795f7265..d3b4df27935 100644 > > > --- a/src/backend/postmaster/auxprocess.c > > > +++ b/src/backend/postmaster/auxprocess.c > > > @@ -84,6 +84,13 @@ AuxiliaryProcessMainCommon(void) > > > /* register a before-shutdown callback for LWLock cleanup */ > > > before_shmem_exit(ShutdownAuxiliaryProcess, 0); > > > > > > + /* > > > + * The before shmem exit callback frees the DSA memory occupied by > > the > > > + * latest memory context statistics that could be published by > > this aux > > > + * proc if requested. > > > + */ > > > + before_shmem_exit(AtProcExit_memstats_dsa_free, 0); > > > + > > > SetProcessingMode(NormalProcessing); > > > } > > > > How about putting it into BaseInit()? Or maybe just register it when its > > first used? > > > > > Problem with registering it when dsa is first used is that dsa is used in an > interrupt handler. The handler could be called from the > PG_ENSURE_ERROR_CLEANUP block. This block operates under the assumption that > the before_shmem_exit callback registered at the beginning, will be the last > one in the registered callback list at the end of the block. However, this > won't be the case if a callback is registered from an interrupt handler > called in the PG_ENSURE_ERROR_CLEANUP block. Ugh, I really dislike PG_ENSURE_ERROR_CLEANUP(). That's not an argument against moving it to BaseInit() though, as that's called before procsignal is even initialized and before signals are unmasked. > I don't really understand why DSA_DEFAULT_INIT_SEGMENT_SIZE is > > something that makes sense to use here? > > > > > To determine the memory limit per backend in multiples of > DSA_DEFAULT_INIT_SEGMENT_SIZE. > Currently it is set to 1 * DSA_DEFAULT_INIT_SEGMENT_SIZE. > Since a call to dsa_create would create a DSA segment of this size, I > thought it makes sense > to define a limit related to the segment size. I strongly disagree. The limit should be in an understandable unit, not on another subystems's defaults that might change at some point. > > + /* If the dsm mapping could not be found, attach to the area */ > > > + if (dsm_seg != NULL) > > > + return; > > > > I don't understand what we do here with the dsm? Why do we not need > > cleanup > > if we are already attached to the dsm segment? > > > > I am not expecting to hit this case, since we are always detaching from the > dsa. Pretty sure it's reachable, consider a failure of dsa_allocate(). That'll throw an error, while attached to the segment. > This could be an assert but since it is a cleanup code, I thought returning > would be a harmless step. The problem is that the code seems wrong - if we are already attached we'll leak the memory! As I also mentioned, I don't understand why we're constantly attaching/detaching from the dsa/dsm either. It just seems to make things more complicated an dmore expensive. Greetings, Andres Freund ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-04-08 00:03 Andres Freund <[email protected]> parent: Daniel Gustafsson <[email protected]> 1 sibling, 1 reply; 52+ messages in thread From: Andres Freund @ 2025-04-08 00:03 UTC (permalink / raw) To: Daniel Gustafsson <[email protected]>; +Cc: Rahila Syed <[email protected]>; pgsql-hackers Hi, On 2025-04-08 01:17:17 +0200, Daniel Gustafsson wrote: > > On 7 Apr 2025, at 17:43, Andres Freund <[email protected]> wrote: > > >> + /* > >> + * Hold the process lock to protect writes to process specific memory. Two > >> + * processes publishing statistics do not block each other. > >> + */ > > > > s/specific/process specific/ > > That's what it says though.. isn't it? I might be missing something obvious. Understandable confusion, not sure what my brain was doing anymore either... > >> +} MemoryContextState; > > > > IMO that's too generic a name for something in a header. > > > >> +} MemoryContextId; > > > > This too. Particularly because MemoryContextData->ident exist but is > > something different. > > Renamed both to use MemoryContextReporting* namespace, which leaves > MemoryContextReportingBackendState at an unwieldly long name. I'm running out > of ideas on how to improve and it does make purpose quite explicit at least. How about MemoryContextReportingBackendState -> MemoryStatsBackendState MemoryContextReportingId -> MemoryStatsContextId MemoryContextReportingSharedState -> MemoryStatsCtl MemoryContextReportingStatsEntry -> MemoryStatsEntry > >> + /* context id starts with 1 */ > >> + entry->context_id = ++(*stats_count); > > > > Given that we don't actually do anything here relating to starting with 1, I > > find that comment confusing. > > Reworded, not sure if it's much better tbh. I'd probably just remove the comment. > > Hm. First I thought we'd leak memory if this second (and subsequent) > > dsa_allocate failed. Then I thought we'd be ok, because the memory would be > > memory because it'd be reachable from memCtxState[idx].memstats_dsa_pointer. > > > > But I think it wouldn't *quite* work, because memCtxState[idx].total_stats is > > only set *after* we would have failed. > > Keeping a running total in .total_stats should make the leak window smaller. Why not just initialize .total_stats *before* calling any fallible code? Afaict it's zero-allocated, so the free function should have no problem dealing with the entries that haven't yet been populated/ Greetings, Andres Freund ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-04-08 08:03 Daniel Gustafsson <[email protected]> parent: Andres Freund <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Daniel Gustafsson @ 2025-04-08 08:03 UTC (permalink / raw) To: Rahila Syed <[email protected]>; +Cc: Andres Freund <[email protected]>; pgsql-hackers > On 8 Apr 2025, at 07:40, Rahila Syed <[email protected]> wrote: > >> > Renamed both to use MemoryContextReporting* namespace, which leaves >> > MemoryContextReportingBackendState at an unwieldly long name. I'm running out >> > of ideas on how to improve and it does make purpose quite explicit at least. >> >> How about >> >> MemoryContextReportingBackendState -> MemoryStatsBackendState >> MemoryContextReportingId -> MemoryStatsContextId >> MemoryContextReportingSharedState -> MemoryStatsCtl >> MemoryContextReportingStatsEntry -> MemoryStatsEntry > > Fixed accordingly. That's much better, thanks. There was a bug in the shmem init function which caused it to fail on Windows, the attached fixes that. -- Daniel Gustafsson  Attachments: [application/octet-stream] v29-0001-Add-function-to-get-memory-context-stats-for-pro.patch (65.2K, ../../[email protected]/3-v29-0001-Add-function-to-get-memory-context-stats-for-pro.patch) download | inline diff: From ec215974fff965040e3531ac57c3fc3023be07ff Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Tue, 8 Apr 2025 09:38:12 +0530 Subject: [PATCH v29] Add function to get memory context stats for processes This adds a function for retrieving memory context statistics and information from backends as well as auxiliary processes. When calling the function it sends a signal to the specified process to submit statistics regarding its memory contexts into dynamic shared memory. Each memory context is returned in detail, followed by a cumulative total in case the number of contexts exceed the max allocated amount of shared memory. Each process is limited to use at most 1Mb memory for this. A summary can also be explicitly requested by the user, this will return the TopMemoryContext and a cumulative total of all lower contexts. In order to not block on busy processes the caller specifies the number of seconds during which to retry before timing out. In the case where no statistics are published within the set timeout, the last known statistics are returned, or NULL if no previously published statistics exist. This allows dash- board type usages to continually publish data even if the target process is temporarily congested. Context records contain a timestamp to indicate when they were submitted. Author: Rahila Syed <[email protected]> Reviewed-by: Daniel Gustafsson <[email protected]> Reviewed-by: Andres Freund <[email protected]> Reviewed-by: Tomas Vondra <[email protected]> Reviewed-by: Atsushi Torikoshi <[email protected]> Reviewed-by: Fujii Masao <[email protected]> Reviewed-by: Alexander Korotkov <[email protected]> Discussion: https://postgr.es/m/CAH2L28v8mc9HDt8QoSJ8TRmKau_8FM_HKS41NeO9-6ZAkuZKXw@mail.gmail.com --- doc/src/sgml/func.sgml | 171 +++++ src/backend/catalog/system_views.sql | 5 + src/backend/postmaster/autovacuum.c | 4 + src/backend/postmaster/checkpointer.c | 4 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 4 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 4 + src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/storage/lmgr/lwlock.c | 2 + src/backend/storage/lmgr/proc.c | 1 + src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 1 + src/backend/utils/adt/mcxtfuncs.c | 426 +++++++++++- src/backend/utils/init/globals.c | 1 + src/backend/utils/init/postinit.c | 7 + src/backend/utils/mmgr/mcxt.c | 644 +++++++++++++++++- src/include/catalog/pg_proc.dat | 10 + src/include/miscadmin.h | 1 + src/include/storage/lwlock.h | 2 + src/include/storage/procsignal.h | 1 + src/include/utils/memutils.h | 82 +++ src/test/regress/expected/sysviews.out | 19 + src/test/regress/sql/sysviews.sql | 18 + src/tools/pgindent/typedefs.list | 4 + 26 files changed, 1383 insertions(+), 45 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 9ab070adffb..42ec4340da1 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -28663,6 +28663,144 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres} </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_get_process_memory_contexts</primary> + </indexterm> + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type>, <parameter>summary</parameter> <type>boolean</type>, <parameter>timeout</parameter> <type>float</type> ) + <returnvalue>setof record</returnvalue> + ( <parameter>name</parameter> <type>text</type>, + <parameter>ident</parameter> <type>text</type>, + <parameter>type</parameter> <type>text</type>, + <parameter>path</parameter> <type>integer[]</type>, + <parameter>level</parameter> <type>integer</type>, + <parameter>total_bytes</parameter> <type>bigint</type>, + <parameter>total_nblocks</parameter> <type>bigint</type>, + <parameter>free_bytes</parameter> <type>bigint</type>, + <parameter>free_chunks</parameter> <type>bigint</type>, + <parameter>used_bytes</parameter> <type>bigint</type>, + <parameter>num_agg_contexts</parameter> <type>integer</type>, + <parameter>stats_timestamp</parameter> <type>timestamptz</type> ) + </para> + <para> + This function handles requests to display the memory contexts of a + <productname>PostgreSQL</productname> process with the specified + process ID. The function can be used to send requests to backends as + well as <glossterm linkend="glossary-auxiliary-proc">auxiliary processes</glossterm>. + </para> + <para> + The returned record contains extended statistics per each memory + context: + <itemizedlist spacing="compact"> + <listitem> + <para> + <parameter>name</parameter> - The name of the memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>ident</parameter> - Memory context ID (if any). + </para> + </listitem> + <listitem> + <para> + <parameter>type</parameter> - The type of memory context, possible + values are: AllocSet, Generation, Slab and Bump. + </para> + </listitem> + <listitem> + <para> + <parameter>path</parameter> - Memory contexts are organized in a + tree model with TopMemoryContext as the root, and all other memory + contexts as nodes in the tree. The <parameter>path</parameter> + displays the path from the root to the current memory context. The + path is limited to 100 children per node, which each node limited + to a max depth of 100, to preserve memory during reporting. The + printed path will also be limited to 100 nodes counting from the + TopMemoryContext. + </para> + </listitem> + <listitem> + <para> + <parameter>level</parameter> - The level in the tree of the current + memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_bytes</parameter> - The total number of bytes + allocated to this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_nblocks</parameter> - The total number of blocks + used for the allocated memory. + </para> + </listitem> + <listitem> + <para> + <parameter>free_bytes</parameter> - The amount of free memory in + this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>free_chunks</parameter> - The number of chunks that + <parameter>free_bytes</parameter> corresponds to. + </para> + </listitem> + <listitem> + <para> + <parameter>used_bytes</parameter> - The total number of bytes + currently occupied. + </para> + </listitem> + <listitem> + <para> + <parameter>num_agg_contexts</parameter> - The number of memory + contexts aggregated in the displayed statistics. + </para> + </listitem> + <listitem> + <para> + <parameter>stats_timestamp</parameter> - When the statistics were + extracted from the process. + </para> + </listitem> + </itemizedlist> + </para> + <para> + When <parameter>summary</parameter> is <literal>true</literal>, statistics + for memory contexts at levels 1 and 2 are displayed, with level 1 + representing the root node (i.e., <literal>TopMemoryContext</literal>). + Statistics for contexts on level 2 and below are aggregates of all + child contexts' statistics, where <literal>num_agg_contexts</literal> + indicate the number aggregated child contexts. When + <parameter>summary</parameter> is <literal>false</literal>, + <literal>the num_agg_contexts</literal> value is <literal>1</literal>, + indicating that individual statistics are being displayed. The levels + are limited to the first 100 contexts. + </para> + <para> + Busy processes can delay reporting memory context statistics, + <parameter>timeout</parameter> specifies the number of seconds + to wait for updated statistics. <parameter>timeout</parameter> can be + specified in fractions of a second. + </para> + <para> + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context + statistics are aggregated and a cumulative total is displayed. The + <literal>num_agg_contexts</literal> column indicates the number of + contexts aggregated in the displayed statistics. When + <literal>num_agg_contexts</literal> is <literal>1</literal> is means + that the context statistics are displayed separately. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> @@ -28802,6 +28940,39 @@ LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 because it may generate a large number of log messages. </para> + <para> + <function>pg_get_process_memory_contexts</function> can be used + to request the memory contexts statistics of any postgres process. For example: +<programlisting> +postgres=# SELECT * FROM pg_get_process_memory_contexts( + (SELECT pid FROM pg_stat_activity + WHERE backend_type = 'checkpointer'), + false, 0.5) LIMIT 1; +-[ RECORD 1 ]----+------------------------------ +name | TopMemoryContext +ident | +type | AllocSet +path | {1} +level | 1 +total_bytes | 90304 +total_nblocks | 3 +free_bytes | 2880 +free_chunks | 1 +used_bytes | 87424 +num_agg_contexts | 1 +stats_timestamp | 2025-03-24 13:55:47.796698+01 +</programlisting> + <note> + <para> + While <function>pg_get_process_memory_contexts</function> can be used to + query memory contexts of the local backend, + <structname>pg_backend_memory_contexts</structname> + (see <xref linkend="view-pg-backend-memory-contexts"/> for more details) + will be less resource intensive when only the local backend is of interest. + </para> + </note> + </para> + </sect2> <sect2 id="functions-admin-backup"> diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index 08f780a2e63..15efb02badb 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -674,6 +674,11 @@ GRANT SELECT ON pg_backend_memory_contexts TO pg_read_all_stats; REVOKE EXECUTE ON FUNCTION pg_get_backend_memory_contexts() FROM PUBLIC; GRANT EXECUTE ON FUNCTION pg_get_backend_memory_contexts() TO pg_read_all_stats; +REVOKE EXECUTE ON FUNCTION + pg_get_process_memory_contexts(integer, boolean, float) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION + pg_get_process_memory_contexts(integer, boolean, float) TO pg_read_all_stats; + -- Statistics views CREATE VIEW pg_stat_all_tables AS diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 2513a8ef8a6..16756152b71 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -781,6 +781,10 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index fda91ffd1ce..d3cb3f1891c 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -663,6 +663,10 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index 0ae9bf906ec..f24f574e748 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -48,6 +48,10 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index 7e622ae4bd2..cb7408acf4c 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -867,6 +867,10 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index 27e86cf393f..7149a67fcbc 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -192,6 +192,10 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index 0fec4f1f871..c7a76711cc5 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -879,6 +879,10 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index 2fa045e6b0f..00c76d05356 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -51,6 +51,7 @@ #include "storage/sinvaladt.h" #include "utils/guc.h" #include "utils/injection_point.h" +#include "utils/memutils.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -150,6 +151,7 @@ CalculateShmemSize(int *num_semaphores) size = add_size(size, InjectionPointShmemSize()); size = add_size(size, SlotSyncShmemSize()); size = add_size(size, AioShmemSize()); + size = add_size(size, MemoryContextReportingShmemSize()); /* include additional requested shmem from preload libraries */ size = add_size(size, total_addin_request); @@ -343,6 +345,7 @@ CreateOrAttachShmemStructs(void) WaitEventCustomShmemInit(); InjectionPointShmemInit(); AioShmemInit(); + MemoryContextReportingShmemInit(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index b7c39a4c5f0..a3c2cd12277 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -690,6 +690,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_GET_MEMORY_CONTEXT)) + HandleGetMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c index 3df29658f18..dc4d96c16af 100644 --- a/src/backend/storage/lmgr/lwlock.c +++ b/src/backend/storage/lmgr/lwlock.c @@ -178,6 +178,8 @@ static const char *const BuiltinTrancheNames[] = { [LWTRANCHE_XACT_SLRU] = "XactSLRU", [LWTRANCHE_PARALLEL_VACUUM_DSA] = "ParallelVacuumDSA", [LWTRANCHE_AIO_URING_COMPLETION] = "AioUringCompletion", + [LWTRANCHE_MEMORY_CONTEXT_REPORTING_STATE] = "MemoryContextReportingState", + [LWTRANCHE_MEMORY_CONTEXT_REPORTING_PROC] = "MemoryContextReportingPerProcess", }; StaticAssertDecl(lengthof(BuiltinTrancheNames) == diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index e9ef0fbfe32..f194e6b3dcc 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -50,6 +50,7 @@ #include "storage/procsignal.h" #include "storage/spin.h" #include "storage/standby.h" +#include "utils/memutils.h" #include "utils/timeout.h" #include "utils/timestamp.h" diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 6ae9f38f0c8..dc4c600922d 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3535,6 +3535,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 8bce14c38fd..23eaf559c8d 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -161,6 +161,7 @@ WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." +MEM_CXT_PUBLISH "Waiting for a process to publish memory information." ABI_compatibility: diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index 396c2f223b4..d459c89cfde 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -17,28 +17,25 @@ #include "funcapi.h" #include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "access/twophase.h" +#include "catalog/pg_authid_d.h" #include "storage/proc.h" #include "storage/procarray.h" +#include "utils/acl.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/memutils.h" +#include "utils/wait_event_types.h" /* ---------- * The max bytes for showing identifiers of MemoryContext. * ---------- */ #define MEMORY_CONTEXT_IDENT_DISPLAY_SIZE 1024 - -/* - * MemoryContextId - * Used for storage of transient identifiers for - * pg_get_backend_memory_contexts. - */ -typedef struct MemoryContextId -{ - MemoryContext context; - int context_id; -} MemoryContextId; +struct MemoryStatsBackendState *memCxtState = NULL; +struct MemoryStatsCtl *memCxtArea = NULL; /* * int_list_to_array @@ -89,7 +86,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, */ for (MemoryContext cur = context; cur != NULL; cur = cur->parent) { - MemoryContextId *entry; + MemoryStatsContextId *entry; bool found; entry = hash_search(context_id_lookup, &cur, HASH_FIND, &found); @@ -143,24 +140,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, else nulls[1] = true; - switch (context->type) - { - case T_AllocSetContext: - type = "AllocSet"; - break; - case T_GenerationContext: - type = "Generation"; - break; - case T_SlabContext: - type = "Slab"; - break; - case T_BumpContext: - type = "Bump"; - break; - default: - type = "???"; - break; - } + type = ContextTypeToString(context->type); values[2] = CStringGetTextDatum(type); values[3] = Int32GetDatum(list_length(path)); /* level */ @@ -175,6 +155,38 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, list_free(path); } +/* + * ContextTypeToString + * Returns a textual representation of a context type + * + * This should cover the same types as MemoryContextIsValid. + */ +const char * +ContextTypeToString(NodeTag type) +{ + const char *context_type; + + switch (type) + { + case T_AllocSetContext: + context_type = "AllocSet"; + break; + case T_GenerationContext: + context_type = "Generation"; + break; + case T_SlabContext: + context_type = "Slab"; + break; + case T_BumpContext: + context_type = "Bump"; + break; + default: + context_type = "???"; + break; + } + return context_type; +} + /* * pg_get_backend_memory_contexts * SQL SRF showing backend memory context. @@ -189,7 +201,7 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) HTAB *context_id_lookup; ctl.keysize = sizeof(MemoryContext); - ctl.entrysize = sizeof(MemoryContextId); + ctl.entrysize = sizeof(MemoryStatsContextId); ctl.hcxt = CurrentMemoryContext; context_id_lookup = hash_create("pg_get_backend_memory_contexts", @@ -216,7 +228,7 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) foreach_ptr(MemoryContextData, cur, contexts) { - MemoryContextId *entry; + MemoryStatsContextId *entry; bool found; /* @@ -224,8 +236,8 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) * PutMemoryContextsStatsTupleStore needs this to populate the "path" * column with the parent context_ids. */ - entry = (MemoryContextId *) hash_search(context_id_lookup, &cur, - HASH_ENTER, &found); + entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &cur, + HASH_ENTER, &found); entry->context_id = context_id++; Assert(!found); @@ -305,3 +317,349 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } + +/* + * pg_get_process_memory_contexts + * Signal a backend or an auxiliary process to send its memory contexts, + * wait for the results and display them. + * + * By default, only superusers or users with PG_READ_ALL_STATS are allowed to + * signal a process to return the memory contexts. This is because allowing + * any users to issue this request at an unbounded rate would cause lots of + * requests to be sent, which can lead to denial of service. Additional roles + * can be permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to copy the memory context details + * to a dynamic shared memory space. + * + * We have defined a limit on DSA memory that could be allocated per process - + * if the process has more memory contexts than what can fit in the allocated + * size, the excess contexts are summarized and represented as cumulative total + * at the end of the buffer. + * + * After sending the signal, wait on a condition variable. The publishing + * backend, after copying the data to shared memory, sends signal on that + * condition variable. There is one condition variable per publishing backend. + * Once the condition variable is signalled, check if the latest memory context + * information is available and display. + * + * If the publishing backend does not respond before the condition variable + * times out, which is set to MEMSTATS_WAIT_TIMEOUT, retry given that there is + * time left within the timeout specified by the user, before giving up and + * returning previously published statistics, if any. If no previous statistics + * exist, return NULL. + */ +#define MEMSTATS_WAIT_TIMEOUT 100 +Datum +pg_get_process_memory_contexts(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + bool summary = PG_GETARG_BOOL(1); + double timeout = PG_GETARG_FLOAT8(2); + PGPROC *proc; + ProcNumber procNumber = INVALID_PROC_NUMBER; + bool proc_is_aux = false; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + MemoryStatsEntry *memcxt_info; + TimestampTz start_timestamp; + + /* + * See if the process with given pid is a backend or an auxiliary process + * and remember the type for when we requery the process later. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + { + proc = AuxiliaryPidGetProc(pid); + proc_is_aux = true; + } + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; this is however not a problem and leave with a WARNING. + * See comment in pg_log_backend_memory_contexts for a discussion on this. + */ + if (proc == NULL) + { + /* + * This is just a warning so a loop-through-resultset will not abort + * if one backend terminated on its own during the run. + */ + ereport(WARNING, + (errmsg("PID %d is not a PostgreSQL server process", pid))); + PG_RETURN_NULL(); + } + + InitMaterializedSRF(fcinfo, 0); + + procNumber = GetNumberFromPGProc(proc); + + LWLockAcquire(&memCxtState[procNumber].lw_lock, LW_EXCLUSIVE); + memCxtState[procNumber].summary = summary; + LWLockRelease(&memCxtState[procNumber].lw_lock); + + start_timestamp = GetCurrentTimestamp(); + + /* + * Send a signal to a PostgreSQL process, informing it we want it to + * produce information about its memory contexts. + */ + if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, procNumber) < 0) + { + ereport(WARNING, + errmsg("could not send signal to process %d: %m", pid)); + PG_RETURN_NULL(); + } + + /* + * Even if the proc has published statistics, the may not be due to the + * current request, but previously published stats. Check if the stats + * are updated by comparing the timestamp, if the stats are newer than our + * previously recorded timestamp from before sending the procsignal, they + * must by definition be updated. Wait for the timeout specified by the + * user, following which display old statistics if available or return + * NULL. + */ + while (1) + { + long msecs; + + /* + * We expect to come out of sleep when the requested process has + * finished publishing the statistics, verified using the valid DSA + * pointer. + * + * Make sure that the information belongs to pid we requested + * information for, Otherwise loop back and wait for the server + * process to finish publishing statistics. + */ + LWLockAcquire(&memCxtState[procNumber].lw_lock, LW_EXCLUSIVE); + + /* + * Note in procnumber.h file says that a procNumber can be re-used for + * a different backend immediately after a backend exits. In case an + * old process' data was there and not updated by the current process + * in the slot identified by the procNumber, the pid of the requested + * process and the proc_id might not match. + */ + if (memCxtState[procNumber].proc_id == pid) + { + /* + * Break if the latest stats have been read, indicated by + * statistics timestamp being newer than the current request + * timestamp. + */ + msecs = TimestampDifferenceMilliseconds(start_timestamp, + memCxtState[procNumber].stats_timestamp); + + if (DsaPointerIsValid(memCxtState[procNumber].memstats_dsa_pointer) + && msecs > 0) + break; + } + LWLockRelease(&memCxtState[procNumber].lw_lock); + + /* + * Recheck the state of the backend before sleeping on the condition + * variable to ensure the process is still alive. Only check the + * relevant process type based on the earlier PID check. + */ + if (proc_is_aux) + proc = AuxiliaryPidGetProc(pid); + else + proc = BackendPidGetProc(pid); + + /* + * The process ending during memory context processing is not an + * error. + */ + if (proc == NULL) + { + ereport(WARNING, + errmsg("PID %d is no longer a PostgreSQL server process", + pid)); + PG_RETURN_NULL(); + } + + msecs = TimestampDifferenceMilliseconds(start_timestamp, GetCurrentTimestamp()); + + /* + * If we haven't already exceeded the timeout value, sleep for the + * remainder of the timeout on the condition variable. + */ + if (msecs > 0 && msecs < (timeout * 1000)) + { + /* + * Wait for the timeout as defined by the user. If no updated + * statistics are available within the allowed time then display + * previously published statistics if there are any. If no + * previous statistics are available then return NULL. The timer + * is defined in milliseconds since thats what the condition + * variable sleep uses. + */ + if (ConditionVariableTimedSleep(&memCxtState[procNumber].memcxt_cv, + ((timeout * 1000) - msecs), WAIT_EVENT_MEM_CXT_PUBLISH)) + { + LWLockAcquire(&memCxtState[procNumber].lw_lock, LW_EXCLUSIVE); + /* Displaying previously published statistics if available */ + if (DsaPointerIsValid(memCxtState[procNumber].memstats_dsa_pointer)) + break; + else + { + LWLockRelease(&memCxtState[procNumber].lw_lock); + PG_RETURN_NULL(); + } + } + } + else + { + LWLockAcquire(&memCxtState[procNumber].lw_lock, LW_EXCLUSIVE); + /* Displaying previously published statistics if available */ + if (DsaPointerIsValid(memCxtState[procNumber].memstats_dsa_pointer)) + break; + else + { + LWLockRelease(&memCxtState[procNumber].lw_lock); + PG_RETURN_NULL(); + } + } + } + + /* + * We should only reach here with a valid DSA handle, either containing + * updated statistics or previously published statistics (identified by + * the timestamp. + */ + Assert(memCxtArea->memstats_dsa_handle != DSA_HANDLE_INVALID); + /* Attach to the dsa area if we have not already done so */ + if (area == NULL) + { + MemoryContext oldcontext = CurrentMemoryContext; + + MemoryContextSwitchTo(TopMemoryContext); + area = dsa_attach(memCxtArea->memstats_dsa_handle); + MemoryContextSwitchTo(oldcontext); + dsa_pin_mapping(area); + } + + /* + * Backend has finished publishing the stats, project them. + */ + memcxt_info = (MemoryStatsEntry *) + dsa_get_address(area, memCxtState[procNumber].memstats_dsa_pointer); + +#define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 12 + for (int i = 0; i < memCxtState[procNumber].total_stats; i++) + { + ArrayType *path_array; + int path_length; + Datum values[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + bool nulls[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + char *name; + char *ident; + Datum *path_datum = NULL; + int *path_int = NULL; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + if (DsaPointerIsValid(memcxt_info[i].name)) + { + name = (char *) dsa_get_address(area, memcxt_info[i].name); + values[0] = CStringGetTextDatum(name); + } + else + nulls[0] = true; + + if (DsaPointerIsValid(memcxt_info[i].ident)) + { + ident = (char *) dsa_get_address(area, memcxt_info[i].ident); + values[1] = CStringGetTextDatum(ident); + } + else + nulls[1] = true; + + values[2] = CStringGetTextDatum(ContextTypeToString(memcxt_info[i].type)); + + path_length = memcxt_info[i].path_length; + path_datum = (Datum *) palloc(path_length * sizeof(Datum)); + if (DsaPointerIsValid(memcxt_info[i].path)) + { + path_int = (int *) dsa_get_address(area, memcxt_info[i].path); + for (int j = 0; j < path_length; j++) + path_datum[j] = Int32GetDatum(path_int[j]); + path_array = construct_array_builtin(path_datum, path_length, INT4OID); + values[3] = PointerGetDatum(path_array); + } + else + nulls[3] = true; + + values[4] = Int32GetDatum(memcxt_info[i].levels); + values[5] = Int64GetDatum(memcxt_info[i].totalspace); + values[6] = Int64GetDatum(memcxt_info[i].nblocks); + values[7] = Int64GetDatum(memcxt_info[i].freespace); + values[8] = Int64GetDatum(memcxt_info[i].freechunks); + values[9] = Int64GetDatum(memcxt_info[i].totalspace - + memcxt_info[i].freespace); + values[10] = Int32GetDatum(memcxt_info[i].num_agg_stats); + values[11] = TimestampTzGetDatum(memCxtState[procNumber].stats_timestamp); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + LWLockRelease(&memCxtState[procNumber].lw_lock); + + ConditionVariableCancelSleep(); + + PG_RETURN_NULL(); +} + +Size +MemoryContextReportingShmemSize(void) +{ + Size sz = 0; + Size TotalProcs = 0; + + TotalProcs = add_size(TotalProcs, NUM_AUXILIARY_PROCS); + TotalProcs = add_size(TotalProcs, MaxBackends); + sz = add_size(sz, mul_size(TotalProcs, sizeof(MemoryStatsBackendState))); + + sz = add_size(sz, sizeof(MemoryStatsCtl)); + + return sz; +} + +/* + * Initialize shared memory for displaying memory context statistics + */ +void +MemoryContextReportingShmemInit(void) +{ + bool found; + + memCxtArea = (MemoryStatsCtl *) + ShmemInitStruct("MemoryStatsCtl", + sizeof(MemoryStatsCtl), &found); + + if (!found) + { + LWLockInitialize(&memCxtArea->lw_lock, LWTRANCHE_MEMORY_CONTEXT_REPORTING_STATE); + memCxtArea->memstats_dsa_handle = DSA_HANDLE_INVALID; + } + + memCxtState = (MemoryStatsBackendState *) + ShmemInitStruct("MemoryStatsBackendState", + ((MaxBackends + NUM_AUXILIARY_PROCS) * sizeof(MemoryStatsBackendState)), + &found); + + if (found) + return; + + for (int i = 0; i < (MaxBackends + NUM_AUXILIARY_PROCS); i++) + { + ConditionVariableInit(&memCxtState[i].memcxt_cv); + LWLockInitialize(&memCxtState[i].lw_lock, LWTRANCHE_MEMORY_CONTEXT_REPORTING_PROC); + memCxtState[i].memstats_dsa_pointer = InvalidDsaPointer; + } +} diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index 2152aad97d9..92304a1f124 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -39,6 +39,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t PublishMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index c09c4d404ba..01309ef3f86 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -667,6 +667,13 @@ BaseInit(void) * drop ephemeral slots, which in turn triggers stats reporting. */ ReplicationSlotInitialize(); + + /* + * The before shmem exit callback frees the DSA memory occupied by the + * latest memory context statistics that could be published by this proc + * if requested. + */ + before_shmem_exit(AtProcExit_memstats_cleanup, 0); } diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index d98ae9db6be..f3a588e1375 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -23,6 +23,11 @@ #include "mb/pg_wchar.h" #include "miscadmin.h" +#include "nodes/pg_list.h" +#include "storage/lwlock.h" +#include "storage/ipc.h" +#include "utils/dsa.h" +#include "utils/hsearch.h" #include "utils/memdebug.h" #include "utils/memutils.h" #include "utils/memutils_internal.h" @@ -135,6 +140,17 @@ static const MemoryContextMethods mcxt_methods[] = { }; #undef BOGUS_MCTX +/* + * This is passed to MemoryContextStatsInternal to determine whether + * to print context statistics or not and where to print them logs or + * stderr. + */ +typedef enum PrintDestination +{ + PRINT_STATS_TO_STDERR = 0, + PRINT_STATS_TO_LOGS, + PRINT_STATS_NONE +} PrintDestination; /* * CurrentMemoryContext @@ -156,16 +172,31 @@ MemoryContext CurTransactionContext = NULL; /* This is a transient link to the active portal's memory context: */ MemoryContext PortalContext = NULL; +dsa_area *area = NULL; static void MemoryContextDeleteOnly(MemoryContext context); static void MemoryContextCallResetCallbacks(MemoryContext context); static void MemoryContextStatsInternal(MemoryContext context, int level, int max_level, int max_children, MemoryContextCounters *totals, - bool print_to_stderr); + PrintDestination print_location, + int *num_contexts); static void MemoryContextStatsPrint(MemoryContext context, void *passthru, const char *stats_string, bool print_to_stderr); +static void PublishMemoryContext(MemoryStatsEntry *memcxt_infos, + int curr_id, MemoryContext context, + List *path, + MemoryContextCounters stat, + int num_contexts, dsa_area *area, + int max_levels); +static void compute_contexts_count_and_ids(List *contexts, HTAB *context_id_lookup, + int *stats_count, + bool summary); +static List *compute_context_path(MemoryContext c, HTAB *context_id_lookup); +static void free_memorycontextstate_dsa(dsa_area *area, int total_stats, + dsa_pointer prev_dsa_pointer); +static void end_memorycontext_reporting(void); /* * You should not do memory allocations within a critical section, because @@ -831,11 +862,19 @@ MemoryContextStatsDetail(MemoryContext context, bool print_to_stderr) { MemoryContextCounters grand_totals; + int num_contexts; + PrintDestination print_location; memset(&grand_totals, 0, sizeof(grand_totals)); + if (print_to_stderr) + print_location = PRINT_STATS_TO_STDERR; + else + print_location = PRINT_STATS_TO_LOGS; + + /* num_contexts report number of contexts aggregated in the output */ MemoryContextStatsInternal(context, 0, max_level, max_children, - &grand_totals, print_to_stderr); + &grand_totals, print_location, &num_contexts); if (print_to_stderr) fprintf(stderr, @@ -870,13 +909,14 @@ MemoryContextStatsDetail(MemoryContext context, * One recursion level for MemoryContextStats * * Print stats for this context if possible, but in any case accumulate counts - * into *totals (if not NULL). + * into *totals (if not NULL). The callers should make sure that print_location + * is set to PRINT_STATS_STDERR or PRINT_STATS_TO_LOGS or PRINT_STATS_NONE. */ static void MemoryContextStatsInternal(MemoryContext context, int level, int max_level, int max_children, MemoryContextCounters *totals, - bool print_to_stderr) + PrintDestination print_location, int *num_contexts) { MemoryContext child; int ichild; @@ -884,10 +924,39 @@ MemoryContextStatsInternal(MemoryContext context, int level, Assert(MemoryContextIsValid(context)); /* Examine the context itself */ - context->methods->stats(context, - MemoryContextStatsPrint, - &level, - totals, print_to_stderr); + switch (print_location) + { + case PRINT_STATS_TO_STDERR: + context->methods->stats(context, + MemoryContextStatsPrint, + &level, + totals, true); + break; + + case PRINT_STATS_TO_LOGS: + context->methods->stats(context, + MemoryContextStatsPrint, + &level, + totals, false); + break; + + case PRINT_STATS_NONE: + + /* + * Do not print the statistics if print_location is + * PRINT_STATS_NONE, only compute totals. This is used in + * reporting of memory context statistics via a sql function. Last + * parameter is not relevant. + */ + context->methods->stats(context, + NULL, + NULL, + totals, false); + break; + } + + /* Increment the context count for each of the recursive call */ + *num_contexts = *num_contexts + 1; /* * Examine children. @@ -907,7 +976,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, MemoryContextStatsInternal(child, level + 1, max_level, max_children, totals, - print_to_stderr); + print_location, num_contexts); } } @@ -926,7 +995,13 @@ MemoryContextStatsInternal(MemoryContext context, int level, child = MemoryContextTraverseNext(child, context); } - if (print_to_stderr) + /* + * Add the count of children contexts which are traversed in the + * non-recursive manner. + */ + *num_contexts = *num_contexts + ichild; + + if (print_location == PRINT_STATS_TO_STDERR) { for (int i = 0; i <= level; i++) fprintf(stderr, " "); @@ -939,7 +1014,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, local_totals.freechunks, local_totals.totalspace - local_totals.freespace); } - else + else if (print_location == PRINT_STATS_TO_LOGS) ereport(LOG_SERVER_ONLY, (errhidestmt(true), errhidecontext(true), @@ -1276,6 +1351,22 @@ HandleLogMemoryContextInterrupt(void) /* latch will be set by procsignal_sigusr1_handler */ } +/* + * HandleGetMemoryContextInterrupt + * Handle receipt of an interrupt indicating a request to publish memory + * contexts statistics. + * + * All the actual work is deferred to ProcessGetMemoryContextInterrupt() as + * this cannot be performed in a signal handler. + */ +void +HandleGetMemoryContextInterrupt(void) +{ + InterruptPending = true; + PublishMemoryContextPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + /* * ProcessLogMemoryContextInterrupt * Perform logging of memory contexts of this backend process. @@ -1313,6 +1404,537 @@ ProcessLogMemoryContextInterrupt(void) MemoryContextStatsDetail(TopMemoryContext, 100, 100, false); } +/* + * ProcessGetMemoryContextInterrupt + * Generate information about memory contexts used by the process. + * + * Performs a breadth first search on the memory context tree, thus parents + * statistics are reported before their children in the monitoring function + * output. + * + * Statistics for all the processes are shared via the same dynamic shared + * area. Statistics written by each process are tracked independently in + * per-process DSA pointers. These pointers are stored in static shared memory. + * + * We calculate maximum number of context's statistics that can be displayed + * using a pre-determined limit for memory available per process for this + * utility maximum size of statistics for each context. The remaining context + * statistics if any are captured as a cumulative total at the end of + * individual context's statistics. + * + * If summary is true, we capture the level 1 and level 2 contexts + * statistics. For that we traverse the memory context tree recursively in + * depth first search manner to cover all the children of a parent context, to + * be able to display a cumulative total of memory consumption by a parent at + * level 2 and all its children. + */ +void +ProcessGetMemoryContextInterrupt(void) +{ + List *contexts; + HASHCTL ctl; + HTAB *context_id_lookup; + int context_id = 0; + MemoryStatsEntry *meminfo; + bool summary = false; + int max_stats; + int idx = MyProcNumber; + int stats_count = 0; + int stats_num = 0; + MemoryContextCounters stat; + int num_individual_stats = 0; + + PublishMemoryContextPending = false; + + /* + * The hash table is used for constructing "path" column of the view, + * similar to its local backend counterpart. + */ + ctl.keysize = sizeof(MemoryContext); + ctl.entrysize = sizeof(MemoryStatsContextId); + ctl.hcxt = CurrentMemoryContext; + + context_id_lookup = hash_create("pg_get_remote_backend_memory_contexts", + 256, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + /* List of contexts to process in the next round - start at the top. */ + contexts = list_make1(TopMemoryContext); + + /* Compute the number of stats that can fit in the defined limit */ + max_stats = + MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND / MAX_MEMORY_CONTEXT_STATS_SIZE; + LWLockAcquire(&memCxtState[idx].lw_lock, LW_EXCLUSIVE); + summary = memCxtState[idx].summary; + LWLockRelease(&memCxtState[idx].lw_lock); + + /* + * Traverse the memory context tree to find total number of contexts. If + * summary is requested report the total number of contexts at level 1 and + * 2 from the top. Also, populate the hash table of context ids. + */ + compute_contexts_count_and_ids(contexts, context_id_lookup, &stats_count, + summary); + + /* + * Allocate memory in this process's DSA for storing statistics of the the + * memory contexts upto max_stats, for contexts that don't fit within a + * limit, a cumulative total is written as the last record in the DSA + * segment. + */ + stats_num = Min(stats_count, max_stats); + + LWLockAcquire(&memCxtArea->lw_lock, LW_EXCLUSIVE); + + /* + * Create a DSA and send handle to the the client process after storing + * the context statistics. If number of contexts exceed a predefined + * limit(8MB), a cumulative total is stored for such contexts. + */ + if (memCxtArea->memstats_dsa_handle == DSA_HANDLE_INVALID) + { + MemoryContext oldcontext = CurrentMemoryContext; + dsa_handle handle; + + MemoryContextSwitchTo(TopMemoryContext); + + area = dsa_create(memCxtArea->lw_lock.tranche); + + handle = dsa_get_handle(area); + MemoryContextSwitchTo(oldcontext); + + dsa_pin_mapping(area); + + /* + * Pin the DSA area, this is to make sure the area remains attachable + * even if current backend exits. This is done so that the statistics + * are published even if the process exits while a client is waiting. + */ + dsa_pin(area); + + /* Set the handle in shared memory */ + memCxtArea->memstats_dsa_handle = handle; + } + + /* + * If DSA exists, created by another process publishing statistics, attach + * to it. + */ + else if (area == NULL) + { + MemoryContext oldcontext = CurrentMemoryContext; + + MemoryContextSwitchTo(TopMemoryContext); + area = dsa_attach(memCxtArea->memstats_dsa_handle); + MemoryContextSwitchTo(oldcontext); + dsa_pin_mapping(area); + } + LWLockRelease(&memCxtArea->lw_lock); + + /* + * Hold the process lock to protect writes to process specific memory. Two + * processes publishing statistics do not block each other. + */ + LWLockAcquire(&memCxtState[idx].lw_lock, LW_EXCLUSIVE); + memCxtState[idx].proc_id = MyProcPid; + + if (DsaPointerIsValid(memCxtState[idx].memstats_dsa_pointer)) + { + /* + * Free any previous allocations, free the name, ident and path + * pointers before freeing the pointer that contains them. + */ + free_memorycontextstate_dsa(area, memCxtState[idx].total_stats, + memCxtState[idx].memstats_dsa_pointer); + } + /* + * Assigning total stats before allocating memory so that memory cleanup + * can run if any subsequent dsa_allocate call to allocate name/ident/path + * fails. + */ + memCxtState[idx].total_stats = stats_num; + memCxtState[idx].memstats_dsa_pointer = + dsa_allocate0(area, stats_num * sizeof(MemoryStatsEntry)); + + meminfo = (MemoryStatsEntry *) + dsa_get_address(area, memCxtState[idx].memstats_dsa_pointer); + + if (summary) + { + int cxt_id = 0; + List *path = NIL; + + /* Copy TopMemoryContext statistics to DSA */ + memset(&stat, 0, sizeof(stat)); + (*TopMemoryContext->methods->stats) (TopMemoryContext, NULL, NULL, + &stat, true); + path = lcons_int(1, path); + PublishMemoryContext(meminfo, cxt_id, TopMemoryContext, path, stat, + 1, area, 100); + cxt_id = cxt_id + 1; + + /* + * Copy statistics for each of TopMemoryContexts children. This + * includes statistics of at most 100 children per node, with each + * child node limited to a depth of 100 in its subtree. + */ + for (MemoryContext c = TopMemoryContext->firstchild; c != NULL; + c = c->nextchild) + { + MemoryContextCounters grand_totals; + int num_contexts = 0; + int level = 0; + + path = NIL; + memset(&grand_totals, 0, sizeof(grand_totals)); + + MemoryContextStatsInternal(c, level, 100, 100, &grand_totals, + PRINT_STATS_NONE, &num_contexts); + + path = compute_context_path(c, context_id_lookup); + + /* + * Register the stats entry first, that way the cleanup handler + * can reach it in case of allocation failures of one or more + * members. + */ + memCxtState[idx].total_stats = cxt_id++; + PublishMemoryContext(meminfo, cxt_id, c, path, + grand_totals, num_contexts, area, 100); + } + memCxtState[idx].total_stats = cxt_id; + + end_memorycontext_reporting(); + + /* Notify waiting backends and return */ + hash_destroy(context_id_lookup); + + return; + } + + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + + /* + * Figure out the transient context_id of this context and each of its + * ancestors, to compute a path for this context. + */ + path = compute_context_path(cur, context_id_lookup); + + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + /* Account for saving one statistics slot for cumulative reporting */ + if (context_id < (max_stats - 1) || stats_count <= max_stats) + { + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1, area, 100); + } + else + { + meminfo[max_stats - 1].totalspace += stat.totalspace; + meminfo[max_stats - 1].nblocks += stat.nblocks; + meminfo[max_stats - 1].freespace += stat.freespace; + meminfo[max_stats - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit per process is reached, write aggregate of the + * remaining statistics. + * + * We can store contexts from 0 to max_stats - 1. When stats_count is + * greater than max_stats, we stop reporting individual statistics + * when context_id equals max_stats - 2. As we use max_stats - 1 array + * slot for reporting cumulative statistics or "Remaining Totals". + */ + if (stats_count > max_stats && context_id == (max_stats - 2)) + { + char *nameptr; + int namelen = strlen("Remaining Totals"); + + num_individual_stats = context_id + 1; + meminfo[max_stats - 1].name = dsa_allocate(area, namelen + 1); + nameptr = dsa_get_address(area, meminfo[max_stats - 1].name); + strncpy(nameptr, "Remaining Totals", namelen); + meminfo[max_stats - 1].ident = InvalidDsaPointer; + meminfo[max_stats - 1].path = InvalidDsaPointer; + meminfo[max_stats - 1].type = 0; + } + context_id++; + } + + /* + * Statistics are not aggregated, i.e individual statistics reported when + * stats_count <= max_stats. + */ + if (stats_count <= max_stats) + { + memCxtState[idx].total_stats = context_id; + } + /* Report number of aggregated memory contexts */ + else + { + meminfo[max_stats - 1].num_agg_stats = context_id - + num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for cumulative + * statistics. + */ + memCxtState[idx].total_stats = num_individual_stats + 1; + } + + /* Notify waiting backends and return */ + end_memorycontext_reporting(); + + hash_destroy(context_id_lookup); +} + +/* + * Update timestamp and signal all the waiting client backends after copying + * all the statistics. + */ +static void +end_memorycontext_reporting(void) +{ + memCxtState[MyProcNumber].stats_timestamp = GetCurrentTimestamp(); + LWLockRelease(&memCxtState[MyProcNumber].lw_lock); + ConditionVariableBroadcast(&memCxtState[MyProcNumber].memcxt_cv); +} + +/* + * compute_context_path + * + * Append the transient context_id of this context and each of its ancestors + * to a list, in order to compute a path. + */ +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + MemoryContext cur_context; + + for (cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryStatsContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + elog(ERROR, "hash table corrupted, can't construct path value"); + + path = lcons_int(cur_entry->context_id, path); + } + + return path; +} + +/* + * Return the number of contexts allocated currently by the backend + * Assign context ids to each of the contexts. + */ +static void +compute_contexts_count_and_ids(List *contexts, HTAB *context_id_lookup, + int *stats_count, bool summary) +{ + foreach_ptr(MemoryContextData, cur, contexts) + { + MemoryStatsContextId *entry; + bool found; + + entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &cur, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 so increment the stats_count + * before assigning + */ + entry->context_id = ++(*stats_count); + + /* Append the children of the current context to the main list. */ + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + { + if (summary) + { + entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &c, + HASH_ENTER, &found); + Assert(!found); + + entry->context_id = ++(*stats_count); + } + + contexts = lappend(contexts, c); + } + + /* + * In summary mode only the first two level (from top) contexts are + * displayed. + */ + if (summary) + break; + } +} + +/* + * PublishMemoryContext + * + * Copy the memory context statistics of a single context to a DSA memory + */ +static void +PublishMemoryContext(MemoryStatsEntry *memcxt_info, int curr_id, + MemoryContext context, List *path, + MemoryContextCounters stat, int num_contexts, + dsa_area *area, int max_levels) +{ + const char *ident = context->ident; + const char *name = context->name; + int *path_list; + + /* + * To be consistent with logging output, we label dynahash contexts with + * just the hash table name as with MemoryContextStatsPrint(). + */ + if (context->ident && strncmp(context->name, "dynahash", 8) == 0) + { + name = context->ident; + ident = NULL; + } + + if (name != NULL) + { + int namelen = strlen(name); + char *nameptr; + + if (strlen(name) >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + namelen = pg_mbcliplen(name, namelen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + memcxt_info[curr_id].name = dsa_allocate(area, namelen + 1); + nameptr = (char *) dsa_get_address(area, memcxt_info[curr_id].name); + strlcpy(nameptr, name, namelen + 1); + } + else + memcxt_info[curr_id].name = InvalidDsaPointer; + + /* Trim and copy the identifier if it is not set to NULL */ + if (ident != NULL) + { + int idlen = strlen(context->ident); + char *identptr; + + /* + * Some identifiers such as SQL query string can be very long, + * truncate oversize identifiers. + */ + if (idlen >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + idlen = pg_mbcliplen(ident, idlen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + memcxt_info[curr_id].ident = dsa_allocate(area, idlen + 1); + identptr = (char *) dsa_get_address(area, memcxt_info[curr_id].ident); + strlcpy(identptr, ident, idlen + 1); + } + else + memcxt_info[curr_id].ident = InvalidDsaPointer; + + /* Allocate DSA memory for storing path information */ + if (path == NIL) + memcxt_info[curr_id].path = InvalidDsaPointer; + else + { + int levels = Min(list_length(path), max_levels); + + memcxt_info[curr_id].path_length = levels; + memcxt_info[curr_id].path = dsa_allocate0(area, levels * sizeof(int)); + memcxt_info[curr_id].levels = list_length(path); + path_list = (int *) dsa_get_address(area, memcxt_info[curr_id].path); + + foreach_int(i, path) + { + path_list[foreach_current_index(i)] = i; + if (--levels == 0) + break; + } + } + memcxt_info[curr_id].type = context->type; + memcxt_info[curr_id].totalspace = stat.totalspace; + memcxt_info[curr_id].nblocks = stat.nblocks; + memcxt_info[curr_id].freespace = stat.freespace; + memcxt_info[curr_id].freechunks = stat.freechunks; + memcxt_info[curr_id].num_agg_stats = num_contexts; +} + +/* + * free_memorycontextstate_dsa + * + * Worker for freeing resources from a MemoryStatsEntry. Callers are + * responsible for ensuring that the DSA pointer is valid. + */ +static void +free_memorycontextstate_dsa(dsa_area *area, int total_stats, + dsa_pointer prev_dsa_pointer) +{ + MemoryStatsEntry *meminfo; + + meminfo = (MemoryStatsEntry *) dsa_get_address(area, prev_dsa_pointer); + Assert(meminfo != NULL); + for (int i = 0; i < total_stats; i++) + { + if (DsaPointerIsValid(meminfo[i].name)) + dsa_free(area, meminfo[i].name); + + if (DsaPointerIsValid(meminfo[i].ident)) + dsa_free(area, meminfo[i].ident); + + if (DsaPointerIsValid(meminfo[i].path)) + dsa_free(area, meminfo[i].path); + } + + dsa_free(area, memCxtState[MyProcNumber].memstats_dsa_pointer); + memCxtState[MyProcNumber].memstats_dsa_pointer = InvalidDsaPointer; +} + +/* + * Free the memory context statistics stored by this process + * in DSA area. + */ +void +AtProcExit_memstats_cleanup(int code, Datum arg) +{ + int idx = MyProcNumber; + + if (memCxtArea->memstats_dsa_handle == DSA_HANDLE_INVALID) + return; + + LWLockAcquire(&memCxtState[idx].lw_lock, LW_EXCLUSIVE); + + if (!DsaPointerIsValid(memCxtState[idx].memstats_dsa_pointer)) + { + LWLockRelease(&memCxtState[idx].lw_lock); + return; + } + + /* If the dsa mapping could not be found, attach to the area */ + if (area == NULL) + area = dsa_attach(memCxtArea->memstats_dsa_handle); + + /* + * Free the memory context statistics, free the name, ident and path + * pointers before freeing the pointer that contains these pointers and + * integer statistics. + */ + free_memorycontextstate_dsa(area, memCxtState[idx].total_stats, + memCxtState[idx].memstats_dsa_pointer); + + dsa_detach(area); + LWLockRelease(&memCxtState[idx].lw_lock); +} + void * palloc(Size size) { diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 37a484147a8..4708f55be18 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8571,6 +8571,16 @@ prorettype => 'bool', proargtypes => 'int4', prosrc => 'pg_log_backend_memory_contexts' }, +# publishing memory contexts of the specified postgres process +{ oid => '2173', descr => 'publish memory contexts of the specified backend', + proname => 'pg_get_process_memory_contexts', provolatile => 'v', + prorows => '100', proretset => 't', proparallel => 'r', + prorettype => 'record', proargtypes => 'int4 bool float8', + proallargtypes => '{int4,bool,float8,text,text,text,_int4,int4,int8,int8,int8,int8,int8,int4,timestamptz}', + proargmodes => '{i,i,i,o,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{pid, summary, retries, name, ident, type, path, level, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes, num_agg_contexts, stats_timestamp}', + prosrc => 'pg_get_process_memory_contexts' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index 0d8528b2875..58b2496a9cb 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -96,6 +96,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t PublishMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h index 4df1d25c045..d333f338ebb 100644 --- a/src/include/storage/lwlock.h +++ b/src/include/storage/lwlock.h @@ -219,6 +219,8 @@ typedef enum BuiltinTrancheIds LWTRANCHE_XACT_SLRU, LWTRANCHE_PARALLEL_VACUUM_DSA, LWTRANCHE_AIO_URING_COMPLETION, + LWTRANCHE_MEMORY_CONTEXT_REPORTING_STATE, + LWTRANCHE_MEMORY_CONTEXT_REPORTING_PROC, LWTRANCHE_FIRST_USER_DEFINED, } BuiltinTrancheIds; diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index 016dfd9b3f6..cfe14631445 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -35,6 +35,7 @@ typedef enum PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to send the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ /* Recovery conflict reasons */ diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 8abc26abce2..d328270fafc 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -18,6 +18,9 @@ #define MEMUTILS_H #include "nodes/memnodes.h" +#include "storage/condition_variable.h" +#include "storage/lmgr.h" +#include "utils/dsa.h" /* @@ -48,6 +51,23 @@ #define AllocHugeSizeIsValid(size) ((Size) (size) <= MaxAllocHugeSize) +/* + * Memory Context reporting size limits. + */ + +/* Max length of context name and ident */ +#define MEMORY_CONTEXT_IDENT_SHMEM_SIZE 64 +/* Maximum size (in bytes) of DSA area per process */ +#define MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND ((size_t) (1 * 1024 * 1024)) + +/* + * Maximum size per context. Actual size may be lower as this assumes the worst + * case of deepest path and longest identifiers (name and ident, thus the + * multiplication by 2). The path depth is limited to 100 like for memory + * context logging. + */ +#define MAX_MEMORY_CONTEXT_STATS_SIZE (sizeof(MemoryStatsEntry) + \ + (100 * sizeof(int)) + (2 * MEMORY_CONTEXT_IDENT_SHMEM_SIZE)) /* * Standard top-level memory contexts. @@ -319,4 +339,66 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) return true; } +/* Dynamic shared memory state for statistics per context */ +typedef struct MemoryStatsEntry +{ + dsa_pointer name; + dsa_pointer ident; + dsa_pointer path; + NodeTag type; + int path_length; + int levels; + int64 totalspace; + int64 nblocks; + int64 freespace; + int64 freechunks; + int num_agg_stats; +} MemoryStatsEntry; + +/* + * Static shared memory state representing the DSA area created for memory + * context statistics reporting. A single DSA area is created and used by all + * the processes, each having its specific DSA allocations for sharing memory + * statistics, tracked by per backend static shared memory state. + */ +typedef struct MemoryStatsCtl +{ + dsa_handle memstats_dsa_handle; + LWLock lw_lock; +} MemoryStatsCtl; + +/* + * Per backend static shared memory state for memory context statistics + * reporting. + */ +typedef struct MemoryStatsBackendState +{ + ConditionVariable memcxt_cv; + LWLock lw_lock; + int proc_id; + int total_stats; + bool summary; + dsa_pointer memstats_dsa_pointer; + TimestampTz stats_timestamp; +} MemoryStatsBackendState; + + +/* + * Used for storage of transient identifiers for pg_get_backend_memory_contexts + */ +typedef struct MemoryStatsContextId +{ + MemoryContext context; + int context_id; +} MemoryStatsContextId; + +extern PGDLLIMPORT MemoryStatsBackendState *memCxtState; +extern PGDLLIMPORT MemoryStatsCtl *memCxtArea; +extern void ProcessGetMemoryContextInterrupt(void); +extern const char *ContextTypeToString(NodeTag type); +extern void HandleGetMemoryContextInterrupt(void); +extern Size MemoryContextReportingShmemSize(void); +extern void MemoryContextReportingShmemInit(void); +extern void AtProcExit_memstats_cleanup(int code, Datum arg); +extern dsa_area *area; #endif /* MEMUTILS_H */ diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 83228cfca29..ae17d028ed3 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -232,3 +232,22 @@ select * from pg_timezone_abbrevs where abbrev = 'LMT'; LMT | @ 7 hours 52 mins 58 secs ago | f (1 row) +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; +NOTICE: (AllocSet,TopMemoryContext,) +NOTICE: (AllocSet,TopMemoryContext,) diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 66179f026b3..d0917b6868e 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -101,3 +101,21 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs; -- One specific case we can check without much fear of breakage -- is the historical local-mean-time value used for America/Los_Angeles. select * from pg_timezone_abbrevs where abbrev = 'LMT'; + +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 87e6da8d25e..780e4c4fc07 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1671,6 +1671,10 @@ MemoryContextCounters MemoryContextData MemoryContextMethodID MemoryContextMethods +MemoryStatsBackendState +MemoryStatsContextId +MemoryStatsCtl +MemoryStatsEntry MemoryStatsPrintFunc MergeAction MergeActionState -- 2.39.3 (Apple Git-146) ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-04-30 10:43 Daniel Gustafsson <[email protected]> parent: Daniel Gustafsson <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Daniel Gustafsson @ 2025-04-30 10:43 UTC (permalink / raw) To: Peter Eisentraut <[email protected]>; +Cc: Rahila Syed <[email protected]>; pgsql-hackers > On 30 Apr 2025, at 12:14, Peter Eisentraut <[email protected]> wrote: > > On 29.04.25 15:13, Rahila Syed wrote: >> Please find attached a patch with some comments and documentation changes. >> Additionaly, added a missing '\0' termination to "Remaining Totals" string. >> I think this became necessary after we replaced dsa_allocate0() >> with dsa_allocate() is the latest version. > > > strncpy(nameptr, "Remaining Totals", namelen); > > + nameptr[namelen] = '\0'; > > Looks like a case for strlcpy()? True. I did go ahead with the strncpy and nul terminator assignment, mostly out of muscle memory, but I agree that this would be a good place for a strlcpy() instead. -- Daniel Gustafsson ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-07-11 15:31 Rahila Syed <[email protected]> parent: Daniel Gustafsson <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Rahila Syed @ 2025-07-11 15:31 UTC (permalink / raw) To: pgsql-hackers Hi, Please find attached the latest memory context statistics monitoring patch. It has been redesigned to address several issues highlighted in the thread [1] and [2]. Here are some key highlights of the new design: - All DSA processing has been moved out of the CFI handler function. Now, all the dynamic shared memory needed to store the statistics is created and deleted in the client function. This change addresses concerns that DSA APIs are too high level to be safely called from interrupt handlers. There was also a concern that DSA API calls might not provide re-entrancy, which could cause issues if CFI is invoked from a DSA function in the future. - The static shared memory array has been replaced with a DSHASH table which now holds metadata such as pointers to actual statistics for each process. - dsm_registry.c APIs are used for creating and attaching to DSA and DSHASH table, which helps prevent code duplication. -To address the memory leak concern, we create an exclusive memory context under the NULL context, which does not fall under the TopMemoryContext tree, to handle all the memory allocations in ProcessGetMemoryContextInterrupt. This ensures the memory context created by the function does not affect its outcome. The memory context is reset at the end of the function, which helps prevent any memory leaks. - Changes made to the mcxt.c file have been relocated to mcxtfuncs.c, which now contains all the existing memory statistics-related functions along with the code for the proposed function. The overall flow of a request is as follows: 1. A client backend running the pg_get_process_memory_contexts function creates a DSA and allocates memory to store statistics, tracked by DSA pointer. This pointer is stored in a DSHASH entry for each client querying the statistics of any process. The client shares its DSHASH table key with the server process using a static shared array of keys indexed by the server's procNumber. It notifies the server process to publish statistics by using SendProcSignal. 2. When a PostgreSQL server process handles the request for memory statistics, the CFI function accesses the client hash key stored in its procNumber slot of the shared keys array. The server process then retrieves the DSHASH entry to obtain the DSA pointer allocated by the client, for storing the statistics. After storing the statistics, it notifies the client through its condition variable. 3. Although the DSA is created just once, the memory inside the DSA is allocated and released by the client process as soon as it finishes reading the statistics. If it fails to do so, it is deleted by the before_shmem_exit callback when the client exits. The client's entry in DSHASH table is also deleted when the client exits. 4. The DSA and DSHASH table are not created until pg_get_process_memory_context function is called. Once created, any client backend querying statistics and any PostgreSQL process publishing statistics will attach to the same area and table. Please let me know your thoughts. Thank you, Rahila Syed [1]. PostgreSQL: Re: pgsql: Add function to get memory context stats for processes <https://www.postgresql.org/message-id/CA%2BTgmoaey-kOP1k5FaUnQFd1fR0majVebWcL8ogfLbG_nt-Ytg%40mail.g...; [2]. PostgreSQL: Re: Prevent an error on attaching/creating a DSM/DSA from an interrupt handler. <https://www.postgresql.org/message-id/flat/8B873D49-E0E5-4F9F-B8D6-CA4836B825CD%40yesql.se#7026d2fe4...; On Wed, Apr 30, 2025 at 4:13 PM Daniel Gustafsson <[email protected]> wrote: > > On 30 Apr 2025, at 12:14, Peter Eisentraut <[email protected]> wrote: > > > > On 29.04.25 15:13, Rahila Syed wrote: > >> Please find attached a patch with some comments and documentation > changes. > >> Additionaly, added a missing '\0' termination to "Remaining Totals" > string. > >> I think this became necessary after we replaced dsa_allocate0() > >> with dsa_allocate() is the latest version. > > > > > strncpy(nameptr, "Remaining Totals", namelen); > > > + nameptr[namelen] = '\0'; > > > > Looks like a case for strlcpy()? > > True. I did go ahead with the strncpy and nul terminator assignment, > mostly > out of muscle memory, but I agree that this would be a good place for a > strlcpy() instead. > > -- > Daniel Gustafsson > > Attachments: [application/octet-stream] v30-0001-Add-pg_get_process_memory_context-function.patch (60.0K, ../../CAH2L28sc-rEhyntPLoaC2XUa0ZjS5ka6KzEbuSVxQBBnUYu1KQ@mail.gmail.com/3-v30-0001-Add-pg_get_process_memory_context-function.patch) download | inline diff: From c4ea611583dd36c1a3facf7d3185c1e9e93b17b2 Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Mon, 30 Jun 2025 12:11:00 +0530 Subject: [PATCH] Add pg_get_process_memory_context function This adds a function for retrieving memory context statistics and information from backends as well as auxiliary processes. The intended usecase is cluster debugging when under memory pressure or unanticipated memory usage characteristics. When calling the function it sends a signal to the specified process to submit statistics regarding its memory contexts into dynamic shared memory. Each memory context is returned in detail, followed by a cumulative total in case the number of contexts exceed the max allocated amount of shared memory. Each process is limited to use at most 1Mb memory for this. A summary can also be explicitly requested by the user, this will return the TopMemoryContext and a cumulative total of all lower contexts. In order to not block on busy processes the caller specifies the number of seconds during which to retry before timing out. In the case where no statistics are published within the set timeout, NULL is returned. --- doc/src/sgml/func.sgml | 164 ++++ src/backend/catalog/system_views.sql | 5 + src/backend/postmaster/autovacuum.c | 4 + src/backend/postmaster/checkpointer.c | 4 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 4 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 4 + src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/storage/lmgr/lwlock.c | 1 + src/backend/storage/lmgr/proc.c | 1 + src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 1 + src/backend/utils/adt/mcxtfuncs.c | 837 +++++++++++++++++- src/backend/utils/adt/pg_locale.c | 1 - src/backend/utils/init/globals.c | 1 + src/backend/utils/init/postinit.c | 7 + src/backend/utils/mb/mbutils.c | 1 - src/backend/utils/mmgr/mcxt.c | 71 +- src/include/catalog/pg_proc.dat | 10 + src/include/miscadmin.h | 1 + src/include/storage/lwlock.h | 1 + src/include/storage/procsignal.h | 1 + src/include/utils/memutils.h | 92 +- src/test/regress/expected/sysviews.out | 19 + src/test/regress/sql/sysviews.sql | 18 + src/tools/pgindent/typedefs.list | 3 + 28 files changed, 1227 insertions(+), 41 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index c28aa71f570..ba082030ea2 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -28716,6 +28716,137 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres} </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_get_process_memory_contexts</primary> + </indexterm> + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type>, <parameter>summary</parameter> <type>boolean</type>, <parameter>timeout</parameter> <type>float</type> ) + <returnvalue>setof record</returnvalue> + ( <parameter>name</parameter> <type>text</type>, + <parameter>ident</parameter> <type>text</type>, + <parameter>type</parameter> <type>text</type>, + <parameter>path</parameter> <type>integer[]</type>, + <parameter>level</parameter> <type>integer</type>, + <parameter>total_bytes</parameter> <type>bigint</type>, + <parameter>total_nblocks</parameter> <type>bigint</type>, + <parameter>free_bytes</parameter> <type>bigint</type>, + <parameter>free_chunks</parameter> <type>bigint</type>, + <parameter>used_bytes</parameter> <type>bigint</type>, + <parameter>num_agg_contexts</parameter> <type>integer</type>, + <parameter>stats_timestamp</parameter> <type>timestamptz</type> ) + </para> + <para> + This function handles requests to display the memory contexts of a + <productname>PostgreSQL</productname> process with the specified + process ID. The function can be used to send requests to backends as + well as <glossterm linkend="glossary-auxiliary-proc">auxiliary processes</glossterm>. + </para> + <para> + The returned record contains extended statistics per each memory + context: + <itemizedlist spacing="compact"> + <listitem> + <para> + <parameter>name</parameter> - The name of the memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>ident</parameter> - Memory context ID (if any). + </para> + </listitem> + <listitem> + <para> + <parameter>type</parameter> - The type of memory context, possible + values are: AllocSet, Generation, Slab and Bump. + </para> + </listitem> + <listitem> + <para> + <parameter>path</parameter> - Memory contexts are organized in a + tree model with TopMemoryContext as the root, and all other memory + contexts as nodes in the tree. The <parameter>path</parameter> + displays the path from the root to the current memory context. The + path is limited to 100 children per node, which each node limited + to a max depth of 100, to preserve memory during reporting. The + printed path will also be limited to 100 nodes counting from the + TopMemoryContext. + </para> + </listitem> + <listitem> + <para> + <parameter>level</parameter> - The level in the tree of the current + memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_bytes</parameter> - The total number of bytes + allocated to this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_nblocks</parameter> - The total number of blocks + used for the allocated memory. + </para> + </listitem> + <listitem> + <para> + <parameter>free_bytes</parameter> - The amount of free memory in + this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>free_chunks</parameter> - The number of chunks that + <parameter>free_bytes</parameter> corresponds to. + </para> + </listitem> + <listitem> + <para> + <parameter>used_bytes</parameter> - The total number of bytes + currently occupied. + </para> + </listitem> + <listitem> + <para> + <parameter>num_agg_contexts</parameter> - The number of memory + contexts aggregated in the displayed statistics. + </para> + </listitem> + </itemizedlist> + </para> + <para> + When <parameter>summary</parameter> is <literal>true</literal>, statistics + for memory contexts at levels 1 and 2 are displayed, with level 1 + representing the root node (i.e., <literal>TopMemoryContext</literal>). + Statistics for contexts on level 2 and below are aggregates of all + child contexts' statistics, where <literal>num_agg_contexts</literal> + indicate the number aggregated child contexts. When + <parameter>summary</parameter> is <literal>false</literal>, + <literal>the num_agg_contexts</literal> value is <literal>1</literal>, + indicating that individual statistics are being displayed. + </para> + <para> + Busy processes can delay reporting memory context statistics, + <parameter>timeout</parameter> specifies the number of seconds + to wait for updated statistics. <parameter>timeout</parameter> can be + specified in fractions of a second. + </para> + <para> + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context + statistics are aggregated and a cumulative total is displayed. The + <literal>num_agg_contexts</literal> column indicates the number of + contexts aggregated in the displayed statistics. When + <literal>num_agg_contexts</literal> is <literal>1</literal> it means + that the context statistics are displayed separately. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> @@ -28855,6 +28986,39 @@ LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 because it may generate a large number of log messages. </para> + <para> + <function>pg_get_process_memory_contexts</function> can be used to request + memory contexts statistics of any <productname>PostgreSQL</productname> + process. For example: +<programlisting> +postgres=# SELECT * FROM pg_get_process_memory_contexts( + (SELECT pid FROM pg_stat_activity + WHERE backend_type = 'checkpointer'), + false, 0.5) LIMIT 1; +-[ RECORD 1 ]----+------------------------------ +name | TopMemoryContext +ident | +type | AllocSet +path | {1} +level | 1 +total_bytes | 90304 +total_nblocks | 3 +free_bytes | 2880 +free_chunks | 1 +used_bytes | 87424 +num_agg_contexts | 1 +</programlisting> + <note> + <para> + While <function>pg_get_process_memory_contexts</function> can be used to + query memory contexts of the local backend, + <structname>pg_backend_memory_contexts</structname> + (see <xref linkend="view-pg-backend-memory-contexts"/> for more details) + will be less resource intensive when only the local backend is of interest. + </para> + </note> + </para> + </sect2> <sect2 id="functions-admin-backup"> diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index b2d5332effc..33b5fcb9119 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -682,6 +682,11 @@ GRANT SELECT ON pg_backend_memory_contexts TO pg_read_all_stats; REVOKE EXECUTE ON FUNCTION pg_get_backend_memory_contexts() FROM PUBLIC; GRANT EXECUTE ON FUNCTION pg_get_backend_memory_contexts() TO pg_read_all_stats; +REVOKE EXECUTE ON FUNCTION + pg_get_process_memory_contexts(integer, boolean, float) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION + pg_get_process_memory_contexts(integer, boolean, float) TO pg_read_all_stats; + -- Statistics views CREATE VIEW pg_stat_all_tables AS diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 9474095f271..5e7e8081c05 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -781,6 +781,10 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index fda91ffd1ce..d3cb3f1891c 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -663,6 +663,10 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index 0ae9bf906ec..f24f574e748 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -48,6 +48,10 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index 78e39e5f866..ac97a39447c 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -867,6 +867,10 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index 27e86cf393f..7149a67fcbc 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -192,6 +192,10 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index 777c9a8d555..5d14684f6b2 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -879,6 +879,10 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index 2fa045e6b0f..fe3d32e40b0 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -51,6 +51,7 @@ #include "storage/sinvaladt.h" #include "utils/guc.h" #include "utils/injection_point.h" +#include "utils/memutils.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -150,6 +151,7 @@ CalculateShmemSize(int *num_semaphores) size = add_size(size, InjectionPointShmemSize()); size = add_size(size, SlotSyncShmemSize()); size = add_size(size, AioShmemSize()); + size = add_size(size, MemoryContextKeysShmemSize() + sizeof(LWLockPadded)); /* include additional requested shmem from preload libraries */ size = add_size(size, total_addin_request); @@ -343,6 +345,7 @@ CreateOrAttachShmemStructs(void) WaitEventCustomShmemInit(); InjectionPointShmemInit(); AioShmemInit(); + MemoryContextKeysShmemInit(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index a9bb540b55a..ce69e26d720 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -691,6 +691,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_GET_MEMORY_CONTEXT)) + HandleGetMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); diff --git a/src/backend/storage/lmgr/lwlock.c b/src/backend/storage/lmgr/lwlock.c index 46f44bc4511..a7b5ede2b12 100644 --- a/src/backend/storage/lmgr/lwlock.c +++ b/src/backend/storage/lmgr/lwlock.c @@ -178,6 +178,7 @@ static const char *const BuiltinTrancheNames[] = { [LWTRANCHE_XACT_SLRU] = "XactSLRU", [LWTRANCHE_PARALLEL_VACUUM_DSA] = "ParallelVacuumDSA", [LWTRANCHE_AIO_URING_COMPLETION] = "AioUringCompletion", + [LWTRANCHE_MEMORY_CONTEXT_KEYS] = "MemoryContextReportingKeys", }; StaticAssertDecl(lengthof(BuiltinTrancheNames) == diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index e9ef0fbfe32..f194e6b3dcc 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -50,6 +50,7 @@ #include "storage/procsignal.h" #include "storage/spin.h" #include "storage/standby.h" +#include "utils/memutils.h" #include "utils/timeout.h" #include "utils/timestamp.h" diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 2f8c3d5f918..83db8a20efb 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3533,6 +3533,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 4da68312b5f..78b1fa5ca43 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -161,6 +161,7 @@ WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." +MEM_CXT_PUBLISH "Waiting for a process to publish memory information." ABI_compatibility: diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index fe6dce9cba3..b44bcb18118 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -15,13 +15,38 @@ #include "postgres.h" +#include "access/twophase.h" +#include "catalog/pg_authid_d.h" #include "funcapi.h" #include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "storage/dsm_registry.h" #include "storage/proc.h" #include "storage/procarray.h" +#include "utils/acl.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/memutils.h" +#include "utils/wait_event_types.h" + +#define CLIENT_KEY_SIZE 64 + +static LWLock *client_keys_lock = NULL; +static int *client_keys = NULL; +static dshash_table *MemoryStatsDsHash = NULL; +static dsa_area *MemoryStatsDsaArea = NULL; + +static void dsa_cleanup(MemoryStatsDSHashEntry *entry); +static const char *ContextTypeToString(NodeTag type); +static void PublishMemoryContext(MemoryStatsEntry *memcxt_info, + int curr_id, MemoryContext context, + List *path, + MemoryContextCounters stat, + int num_contexts, int max_levels); +static List *compute_context_path(MemoryContext c, HTAB *context_id_lookup); +static void end_memorycontext_reporting(MemoryStatsDSHashEntry *entry, MemoryContext oldcontext, + HTAB *context_id_lookup); /* ---------- * The max bytes for showing identifiers of MemoryContext. @@ -89,7 +114,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, */ for (MemoryContext cur = context; cur != NULL; cur = cur->parent) { - MemoryContextId *entry; + MemoryStatsContextId *entry; bool found; entry = hash_search(context_id_lookup, &cur, HASH_FIND, &found); @@ -143,24 +168,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, else nulls[1] = true; - switch (context->type) - { - case T_AllocSetContext: - type = "AllocSet"; - break; - case T_GenerationContext: - type = "Generation"; - break; - case T_SlabContext: - type = "Slab"; - break; - case T_BumpContext: - type = "Bump"; - break; - default: - type = "???"; - break; - } + type = ContextTypeToString(context->type); values[2] = CStringGetTextDatum(type); values[3] = Int32GetDatum(list_length(path)); /* level */ @@ -175,6 +183,38 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, list_free(path); } +/* + * ContextTypeToString + * Returns a textual representation of a context type + * + * This should cover the same types as MemoryContextIsValid. + */ +const char * +ContextTypeToString(NodeTag type) +{ + const char *context_type; + + switch (type) + { + case T_AllocSetContext: + context_type = "AllocSet"; + break; + case T_GenerationContext: + context_type = "Generation"; + break; + case T_SlabContext: + context_type = "Slab"; + break; + case T_BumpContext: + context_type = "Bump"; + break; + default: + context_type = "???"; + break; + } + return context_type; +} + /* * pg_get_backend_memory_contexts * SQL SRF showing backend memory context. @@ -189,7 +229,7 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) HTAB *context_id_lookup; ctl.keysize = sizeof(MemoryContext); - ctl.entrysize = sizeof(MemoryContextId); + ctl.entrysize = sizeof(MemoryStatsContextId); ctl.hcxt = CurrentMemoryContext; context_id_lookup = hash_create("pg_get_backend_memory_contexts", @@ -216,7 +256,7 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) foreach_ptr(MemoryContextData, cur, contexts) { - MemoryContextId *entry; + MemoryStatsContextId *entry; bool found; /* @@ -224,8 +264,8 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) * PutMemoryContextsStatsTupleStore needs this to populate the "path" * column with the parent context_ids. */ - entry = (MemoryContextId *) hash_search(context_id_lookup, &cur, - HASH_ENTER, &found); + entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &cur, + HASH_ENTER, &found); entry->context_id = context_id++; Assert(!found); @@ -305,3 +345,754 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } + +/* + * pg_get_process_memory_contexts + * Signal a backend or an auxiliary process to send its memory contexts, + * wait for the results and display them. + * + * By default, only superusers or users with ROLE_PG_READ_ALL_STATS are allowed + * to signal a process to return the memory contexts. This is because allowing + * any users to issue this request at an unbounded rate would cause lots of + * requests to be sent, which can lead to denial of service. Additional roles + * can be permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to copy the memory context details + * to a dynamic shared memory space. + * + * We have defined a limit on DSA memory that could be allocated per process - + * if the process has more memory contexts than what can fit in the allocated + * size, the excess contexts are summarized and represented as cumulative total + * at the end of the buffer. + * + * After sending the signal, wait on a condition variable. The publishing + * backend, after copying the data to shared memory, sends signal on that + * condition variable. There is one condition variable per client process. + * Once the condition variable is signalled, check if the latest memory context + * information is available and display. + * + * If the publishing backend does not respond before the condition variable + * times out, which is set to MEMSTATS_WAIT_TIMEOUT, retry given that there is + * time left within the timeout specified by the user, before giving up and + * returning previously published statistics, if any. If no previous statistics + * exist, return NULL. + */ +#define MEMSTATS_WAIT_TIMEOUT 100 +Datum +pg_get_process_memory_contexts(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + bool summary = PG_GETARG_BOOL(1); + double timeout = PG_GETARG_FLOAT8(2); + PGPROC *proc; + ProcNumber procNumber = INVALID_PROC_NUMBER; + bool proc_is_aux = false; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + MemoryStatsEntry *memcxt_info; + MemoryStatsDSHashEntry *entry; + bool found; + char key[CLIENT_KEY_SIZE]; + + /* + * See if the process with given pid is a backend or an auxiliary process + * and remember the type for when we requery the process later. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + { + proc = AuxiliaryPidGetProc(pid); + proc_is_aux = true; + } + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; this is however not a problem and leave with a WARNING. + * See comment in pg_log_backend_memory_contexts for a discussion on this. + */ + if (proc == NULL) + { + /* + * This is just a warning so a loop-through-resultset will not abort + * if one backend terminated on its own during the run. + */ + ereport(WARNING, + errmsg("PID %d is not a PostgreSQL server process", pid)); + PG_RETURN_NULL(); + } + + InitMaterializedSRF(fcinfo, 0); + + procNumber = GetNumberFromPGProc(proc); + + /* + * Create a DSA to allocate memory for copying memory contexts statistics. + * Allocate the memory in the DSA and send dsa pointer to the server + * process for storing the context statistics. If number of contexts + * exceed a predefined limit(1MB), a cumulative total is stored for such + * contexts. + * + * The DSA is created once for the lifetime of the server, and only + * attached in subsequent calls. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", &found); + + /* + * The dsa pointers containing statistics for each client are stored in a + * dshash table. In addition to dsa pointer, each entry in this table also + * contains information about the statistics, condition variable for + * signalling between client and the server and miscellaneous data + * specific to a request. There is one entry per client request in the + * hash table. + */ + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", &memctx_dsh_params, &found); + + snprintf(key, sizeof(key), "%d", MyProcNumber); + + /* + * Check if the publishing process slot is empty and store this clients + * key i.e its procNumber. This informs the publishing process that it is + * supposed to write statistics in the hash entry corresponding to this + * client. + */ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + if (client_keys[procNumber] == -1) + client_keys[procNumber] = MyProcNumber; + else + { + ereport(WARNING, + errmsg("server process is processing previous request %d: %m", pid)); + LWLockRelease(client_keys_lock); + PG_RETURN_NULL(); + } + LWLockRelease(client_keys_lock); + + /* + * Insert an entry for this client in DSHASH table the first time this + * function is called. This entry is deleted when the process exits in + * before_shmem_exit call. + * + * dshash_find_or_insert locks the entry to prevent the publisher from + * reading before client has updated the entry. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + if (!found) + ConditionVariableInit(&entry->memcxt_cv); + + /* + * Allocate 1MB of memory for the backend to publish its statistics on + * every call to this function. The meomry is freed at the end of the + * function. + */ + Assert(!DsaPointerIsValid(entry->memstats_dsa_pointer)); + entry->memstats_dsa_pointer = + dsa_allocate0(MemoryStatsDsaArea, MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND); + entry->summary = summary; + dshash_release_lock(MemoryStatsDsHash, entry); + + /* + * Send a signal to a PostgreSQL process, informing it we want it to + * produce information about its memory contexts. + */ + if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, procNumber) < 0) + { + ereport(WARNING, + errmsg("could not send signal to process %d: %m", pid)); + PG_RETURN_NULL(); + } + + while (1) + { + + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + Assert(found); + + /* + * We expect to come out of sleep when the requested process has + * finished publishing the statistics, verified using the correct + * entry in the proc_id field. + * + * Make sure that the information belongs to pid we requested + * information for, Otherwise loop back and wait for the server + * process to finish publishing statistics. + * + * Note in procnumber.h file says that a procNumber can be re-used for + * a different backend immediately after a backend exits. In case an + * old process' data was there and not updated by the current process + * in the slot identified by the procNumber, the pid of the requested + * process and the proc_id might not match. + * + */ + if (entry->proc_id == pid) + break; + + dshash_release_lock(MemoryStatsDsHash, entry); + + /* + * Recheck the state of the backend before sleeping on the condition + * variable to ensure the process is still alive. Only check the + * relevant process type based on the earlier PID check. + */ + if (proc_is_aux) + proc = AuxiliaryPidGetProc(pid); + else + proc = BackendPidGetProc(pid); + + /* + * The process ending during memory context processing is not an + * error. + */ + if (proc == NULL) + { + ereport(WARNING, + errmsg("PID %d is no longer a PostgreSQL server process", + pid)); + dsa_cleanup(entry); + PG_RETURN_NULL(); + } + + + /* + * Wait for the timeout as defined by the user. If no statistics are + * available within the allowed time then return NULL. The timer is + * defined in milliseconds since that's what the condition variable + * sleep uses. + */ + if (ConditionVariableTimedSleep(&entry->memcxt_cv, + (timeout * 1000), WAIT_EVENT_MEM_CXT_PUBLISH)) + { + /* Timeout has expired, return NULL */ + dsa_cleanup(entry); + PG_RETURN_NULL(); + } + } + + /* + * Backend has finished publishing the stats, project them. + */ + memcxt_info = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + +#define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 11 + for (int i = 0; i < entry->total_stats; i++) + { + ArrayType *path_array; + int path_length; + Datum values[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + bool nulls[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + Datum *path_datum = NULL; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + if (memcxt_info[i].name[0] != '\0') + { + values[0] = CStringGetTextDatum(memcxt_info[i].name); + } + else + nulls[0] = true; + + if (memcxt_info[i].ident[0] != '\0') + { + values[1] = CStringGetTextDatum(memcxt_info[i].ident); + } + else + nulls[1] = true; + + values[2] = CStringGetTextDatum(ContextTypeToString(memcxt_info[i].type)); + + path_length = memcxt_info[i].path_length; + path_datum = (Datum *) palloc(path_length * sizeof(Datum)); + if (memcxt_info[i].path[0] != 0) + { + for (int j = 0; j < path_length; j++) + path_datum[j] = Int32GetDatum(memcxt_info[i].path[j]); + path_array = construct_array_builtin(path_datum, path_length, INT4OID); + values[3] = PointerGetDatum(path_array); + } + else + nulls[3] = true; + + values[4] = Int32GetDatum(memcxt_info[i].levels); + values[5] = Int64GetDatum(memcxt_info[i].totalspace); + values[6] = Int64GetDatum(memcxt_info[i].nblocks); + values[7] = Int64GetDatum(memcxt_info[i].freespace); + values[8] = Int64GetDatum(memcxt_info[i].freechunks); + values[9] = Int64GetDatum(memcxt_info[i].totalspace - + memcxt_info[i].freespace); + values[10] = Int32GetDatum(memcxt_info[i].num_agg_stats); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + dsa_cleanup(entry); + dshash_release_lock(MemoryStatsDsHash, entry); + + ConditionVariableCancelSleep(); + + PG_RETURN_NULL(); +} + +static void +dsa_cleanup(MemoryStatsDSHashEntry *entry) +{ + Assert(MemoryStatsDsaArea != NULL); + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + entry->memstats_dsa_pointer = InvalidDsaPointer; + entry->proc_id = 0; +} +void +MemoryContextKeysShmemInit(void) +{ + bool found; + + client_keys = (int *) + ShmemInitStruct("MemoryContextKeys", + MemoryContextKeysShmemSize() + sizeof(LWLockPadded), &found); + client_keys_lock = (LWLock *) ((char *) client_keys + MemoryContextKeysShmemSize()); + + if (!found) + { + MemSet(client_keys, -1, MemoryContextKeysShmemSize()); + LWLockInitialize(client_keys_lock, LWTRANCHE_MEMORY_CONTEXT_KEYS); + } +} + +Size +MemoryContextKeysShmemSize(void) +{ + Size sz = 0; + Size TotalProcs = 0; + + TotalProcs = add_size(TotalProcs, NUM_AUXILIARY_PROCS); + TotalProcs = add_size(TotalProcs, MaxBackends); + sz = add_size(sz, mul_size(TotalProcs, sizeof(int))); + + return sz; +} + +/* + * HandleGetMemoryContextInterrupt + * Handle receipt of an interrupt indicating a request to publish memory + * contexts statistics. + * + * All the actual work is deferred to ProcessGetMemoryContextInterrupt() as + * this cannot be performed in a signal handler. + */ +void +HandleGetMemoryContextInterrupt(void) +{ + InterruptPending = true; + PublishMemoryContextPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + +/* + * ProcessGetMemoryContextInterrupt + * Generate information about memory contexts used by the process. + * + * Performs a breadth first search on the memory context tree, thus parents + * statistics are reported before their children in the monitoring function + * output. + * + * Statistics for all the processes are shared via the same dynamic shared + * area. Statistics written by each process are tracked independently in + * per-process DSA pointers. These pointers are stored in static shared memory. + * + * We calculate maximum number of context's statistics that can be displayed + * using a pre-determined limit for memory available per process for this + * utility maximum size of statistics for each context. The remaining context + * statistics if any are captured as a cumulative total at the end of + * individual context's statistics. + * + * If summary is true, we capture the level 1 and level 2 contexts + * statistics. For that we traverse the memory context tree recursively in + * depth first search manner to cover all the children of a parent context, to + * be able to display a cumulative total of memory consumption by a parent at + * level 2 and all its children. + */ +void +ProcessGetMemoryContextInterrupt(void) +{ + List *contexts; + HASHCTL ctl; + HTAB *context_id_lookup; + int context_id = 0; + MemoryStatsEntry *meminfo; + bool summary = false; + MemoryContextCounters stat; + int num_individual_stats = 0; + bool found; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + int clientProcNumber; + MemoryContext memstats_ctx = NULL; + MemoryContext oldcontext = NULL; + + PublishMemoryContextPending = false; + + /* + * Create a new memory context which is not a part of TopMemoryContext + * tree. This context is used to allocate all memory in this function. + * This helps in keeping the memory allocation in this function to report + * memory consumption statistics separate. So that it does not affect the + * output of this function. + */ + memstats_ctx = AllocSetContextCreate((MemoryContext) NULL, "publish_memory_context_statistics", + ALLOCSET_SMALL_SIZES); + oldcontext = MemoryContextSwitchTo(memstats_ctx); + + /* + * The hash table is used for constructing "path" column of the view, + * similar to its local backend counterpart. + */ + ctl.keysize = sizeof(MemoryContext); + ctl.entrysize = sizeof(MemoryStatsContextId); + ctl.hcxt = CurrentMemoryContext; + + context_id_lookup = hash_create("pg_get_remote_backend_memory_contexts", + 256, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + /* List of contexts to process in the next round - start at the top. */ + contexts = list_make1(TopMemoryContext); + + /* + * If DSA exists, created by another process requesting statistics, attach + * to it. We expect the client process to create required DSA and Dshash + * table. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", &found); + + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", &memctx_dsh_params, &found); + + /* Retreive the client key fo publishing statistics */ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + Assert(client_keys[MyProcNumber] != -1); + clientProcNumber = client_keys[MyProcNumber]; + LWLockRelease(client_keys_lock); + + snprintf(key, CLIENT_KEY_SIZE, "%d", clientProcNumber); + + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + summary = entry->summary; + + /* + * The entry lock is held by dshash_find_or_insert to protect writes to + * process specific memory. Two different processes publishing statistics + * do not block each other. + */ + entry->proc_id = MyProcPid; + + /* Should be allocated by a client backend that is requesting statistics */ + Assert(entry->memstats_dsa_pointer != InvalidDsaPointer); + meminfo = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + if (summary) + { + int cxt_id = 0; + List *path = NIL; + MemoryStatsContextId *contextid_entry; + + /* Copy TopMemoryContext statistics to DSA */ + memset(&stat, 0, sizeof(stat)); + (*TopMemoryContext->methods->stats) (TopMemoryContext, NULL, NULL, + &stat, true); + path = lcons_int(1, path); + PublishMemoryContext(meminfo, cxt_id, TopMemoryContext, path, stat, + 1, 100); + + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &TopMemoryContext, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = cxt_id + 1; + + /* + * Copy statistics for each of TopMemoryContexts children. This + * includes statistics of at most 100 children per node, with each + * child node limited to a depth of 100 in its subtree. + */ + for (MemoryContext c = TopMemoryContext->firstchild; c != NULL; + c = c->nextchild) + { + MemoryContextCounters grand_totals; + int num_contexts = 0; + + path = NIL; + memset(&grand_totals, 0, sizeof(grand_totals)); + + cxt_id++; + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &c, + HASH_ENTER, &found); + Assert(!found); + contextid_entry->context_id = cxt_id + 1; + + MemoryContextStatsInternal(c, 1, 100, 100, &grand_totals, + PRINT_STATS_NONE, &num_contexts); + + path = compute_context_path(c, context_id_lookup); + + PublishMemoryContext(meminfo, cxt_id, c, path, + grand_totals, num_contexts, 100); + } + entry->total_stats = cxt_id + 1; + + /* Notify waiting backends and return */ + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + return; + } + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + MemoryStatsContextId *contextid_entry; + + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &cur, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = context_id + 1; + + /* + * Figure out the transient context_id of this context and each of its + * ancestors, to compute a path for this context. + */ + path = compute_context_path(cur, context_id_lookup); + + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + /* Account for saving one statistics slot for cumulative reporting */ + if (context_id < (MAX_MEMORY_CONTEXT_STATS_NUM - 1)) + { + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1, 100); + } + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].totalspace += stat.totalspace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].nblocks += stat.nblocks; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freespace += stat.freespace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit per process is reached, write aggregate of the + * remaining statistics. + * + * We can store contexts from 0 to max_stats - 1. When context_id is + * greater than max_stats, we stop reporting individual statistics + * when context_id equals max_stats - 2. As we use max_stats - 1 array + * slot for reporting cumulative statistics or "Remaining Totals". + */ + if (context_id == (MAX_MEMORY_CONTEXT_STATS_NUM - 2)) + { + int namelen = strlen("Remaining Totals"); + + num_individual_stats = context_id + 1; + strlcpy(meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].name, "Remaining Totals", namelen + 1); + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].ident[0] = '\0'; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].path[0] = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].type = 0; + } + context_id++; + + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + contexts = lappend(contexts, c); + } + + /* + * Statistics are not aggregated, i.e individual statistics reported when + * context_id <= max_stats. + */ + if (context_id <= MAX_MEMORY_CONTEXT_STATS_NUM) + { + entry->total_stats = context_id; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = 1; + } + /* Report number of aggregated memory contexts */ + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = context_id - + num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for cumulative + * statistics. + */ + entry->total_stats = num_individual_stats + 1; + } + + /* Notify waiting backends and return */ + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); +} + +/* + * Update timestamp and signal all the waiting client backends after copying + * all the statistics. + */ +static void +end_memorycontext_reporting(MemoryStatsDSHashEntry *entry, MemoryContext oldcontext, HTAB *context_id_lookup) +{ + MemoryContext curr_ctx = CurrentMemoryContext; + + dshash_release_lock(MemoryStatsDsHash, entry); + ConditionVariableBroadcast(&entry->memcxt_cv); + + /* + * Empty this processes slot, so other clients can request memory + * statistics + */ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + client_keys[MyProcNumber] = -1; + LWLockRelease(client_keys_lock); + + hash_destroy(context_id_lookup); + MemoryContextSwitchTo(oldcontext); + MemoryContextReset(curr_ctx); +} + +/* + * compute_context_path + * + * Append the transient context_id of this context and each of its ancestors + * to a list, in order to compute a path. + */ +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + MemoryContext cur_context; + + for (cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryStatsContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + elog(ERROR, "hash table corrupted, can't construct path value"); + + path = lcons_int(cur_entry->context_id, path); + } + + return path; +} + +/* + * PublishMemoryContext + * + * Copy the memory context statistics of a single context to a DSA memory + */ +static void +PublishMemoryContext(MemoryStatsEntry *memcxt_info, int curr_id, + MemoryContext context, List *path, + MemoryContextCounters stat, int num_contexts, int max_levels) +{ + const char *ident = context->ident; + const char *name = context->name; + + /* + * To be consistent with logging output, we label dynahash contexts with + * just the hash table name as with MemoryContextStatsPrint(). + */ + if (context->ident && strncmp(context->name, "dynahash", 8) == 0) + { + name = context->ident; + ident = NULL; + } + + if (name != NULL) + { + int namelen = strlen(name); + + if (strlen(name) >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + namelen = pg_mbcliplen(name, namelen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].name, name, namelen + 1); + } + else + /* Clearing the array */ + memcxt_info[curr_id].name[0] = '\0'; + + /* Trim and copy the identifier if it is not set to NULL */ + if (ident != NULL) + { + int idlen = strlen(context->ident); + + /* + * Some identifiers such as SQL query string can be very long, + * truncate oversize identifiers. + */ + if (idlen >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + idlen = pg_mbcliplen(ident, idlen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].ident, ident, idlen + 1); + } + else + memcxt_info[curr_id].ident[0] = '\0'; + + /* Allocate DSA memory for storing path information */ + if (path == NIL) + memcxt_info[curr_id].path[0] = 0; + else + { + int levels = Min(list_length(path), max_levels); + + memcxt_info[curr_id].path_length = levels; + memcxt_info[curr_id].levels = list_length(path); + + foreach_int(i, path) + { + memcxt_info[curr_id].path[foreach_current_index(i)] = i; + if (--levels == 0) + break; + } + } + memcxt_info[curr_id].type = context->type; + memcxt_info[curr_id].totalspace = stat.totalspace; + memcxt_info[curr_id].nblocks = stat.nblocks; + memcxt_info[curr_id].freespace = stat.freespace; + memcxt_info[curr_id].freechunks = stat.freechunks; + memcxt_info[curr_id].num_agg_stats = num_contexts; +} + +void +AtProcExit_memstats_cleanup(int code, Datum arg) +{ + int idx = MyProcNumber; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + bool found; + + if (MemoryStatsDsHash != NULL) + { + snprintf(key, CLIENT_KEY_SIZE, "%d", idx); + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + if (found) + { + if (MemoryStatsDsaArea != NULL && + DsaPointerIsValid(entry->memstats_dsa_pointer)) + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + } + dshash_delete_entry(MemoryStatsDsHash, entry); + } +} diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c index 97c2ac1faf9..ab768a7a91f 100644 --- a/src/backend/utils/adt/pg_locale.c +++ b/src/backend/utils/adt/pg_locale.c @@ -45,7 +45,6 @@ #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/pg_locale.h" -#include "utils/relcache.h" #include "utils/syscache.h" #ifdef WIN32 diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index d31cb45a058..92b0446b80c 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -39,6 +39,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t PublishMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index c86ceefda94..89d72cdd5ff 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -663,6 +663,13 @@ BaseInit(void) * drop ephemeral slots, which in turn triggers stats reporting. */ ReplicationSlotInitialize(); + + /* + * The before shmem exit callback frees the DSA memory occupied by the + * latest memory context statistics that could be published by this proc + * if requested. + */ + before_shmem_exit(AtProcExit_memstats_cleanup, 0); } diff --git a/src/backend/utils/mb/mbutils.c b/src/backend/utils/mb/mbutils.c index 886ecbad871..308016d7763 100644 --- a/src/backend/utils/mb/mbutils.c +++ b/src/backend/utils/mb/mbutils.c @@ -39,7 +39,6 @@ #include "mb/pg_wchar.h" #include "utils/fmgrprotos.h" #include "utils/memutils.h" -#include "utils/relcache.h" #include "varatt.h" /* diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 15fa4d0a55e..d6b081d1bbf 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -23,6 +23,7 @@ #include "mb/pg_wchar.h" #include "miscadmin.h" +#include "utils/hsearch.h" #include "utils/memdebug.h" #include "utils/memutils.h" #include "utils/memutils_internal.h" @@ -159,10 +160,6 @@ MemoryContext PortalContext = NULL; static void MemoryContextDeleteOnly(MemoryContext context); static void MemoryContextCallResetCallbacks(MemoryContext context); -static void MemoryContextStatsInternal(MemoryContext context, int level, - int max_level, int max_children, - MemoryContextCounters *totals, - bool print_to_stderr); static void MemoryContextStatsPrint(MemoryContext context, void *passthru, const char *stats_string, bool print_to_stderr); @@ -831,11 +828,19 @@ MemoryContextStatsDetail(MemoryContext context, bool print_to_stderr) { MemoryContextCounters grand_totals; + int num_contexts; + PrintDestination print_location; memset(&grand_totals, 0, sizeof(grand_totals)); + if (print_to_stderr) + print_location = PRINT_STATS_TO_STDERR; + else + print_location = PRINT_STATS_TO_LOGS; + + /* num_contexts report number of contexts aggregated in the output */ MemoryContextStatsInternal(context, 1, max_level, max_children, - &grand_totals, print_to_stderr); + &grand_totals, print_location, &num_contexts); if (print_to_stderr) fprintf(stderr, @@ -870,13 +875,14 @@ MemoryContextStatsDetail(MemoryContext context, * One recursion level for MemoryContextStats * * Print stats for this context if possible, but in any case accumulate counts - * into *totals (if not NULL). + * into *totals (if not NULL). The callers should make sure that print_location + * is set to PRINT_STATS_TO_STDERR or PRINT_STATS_TO_LOGS or PRINT_STATS_NONE. */ -static void +void MemoryContextStatsInternal(MemoryContext context, int level, int max_level, int max_children, MemoryContextCounters *totals, - bool print_to_stderr) + PrintDestination print_location, int *num_contexts) { MemoryContext child; int ichild; @@ -884,10 +890,39 @@ MemoryContextStatsInternal(MemoryContext context, int level, Assert(MemoryContextIsValid(context)); /* Examine the context itself */ - context->methods->stats(context, - MemoryContextStatsPrint, - &level, - totals, print_to_stderr); + switch (print_location) + { + case PRINT_STATS_TO_STDERR: + context->methods->stats(context, + MemoryContextStatsPrint, + &level, + totals, true); + break; + + case PRINT_STATS_TO_LOGS: + context->methods->stats(context, + MemoryContextStatsPrint, + &level, + totals, false); + break; + + case PRINT_STATS_NONE: + + /* + * Do not print the statistics if print_location is + * PRINT_STATS_NONE, only compute totals. This is used in + * reporting of memory context statistics via a sql function. Last + * parameter is not relevant. + */ + context->methods->stats(context, + NULL, + NULL, + totals, false); + break; + } + + /* Increment the context count for each of the recursive call */ + *num_contexts = *num_contexts + 1; /* * Examine children. @@ -907,7 +942,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, MemoryContextStatsInternal(child, level + 1, max_level, max_children, totals, - print_to_stderr); + print_location, num_contexts); } } @@ -926,7 +961,13 @@ MemoryContextStatsInternal(MemoryContext context, int level, child = MemoryContextTraverseNext(child, context); } - if (print_to_stderr) + /* + * Add the count of children contexts which are traversed in the + * non-recursive manner. + */ + *num_contexts = *num_contexts + ichild; + + if (print_location == PRINT_STATS_TO_STDERR) { for (int i = 0; i < level; i++) fprintf(stderr, " "); @@ -939,7 +980,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, local_totals.freechunks, local_totals.totalspace - local_totals.freespace); } - else + else if (print_location == PRINT_STATS_TO_LOGS) ereport(LOG_SERVER_ONLY, (errhidestmt(true), errhidecontext(true), diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 1fc19146f46..4bc4474b580 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8597,6 +8597,16 @@ prorettype => 'bool', proargtypes => 'int4', prosrc => 'pg_log_backend_memory_contexts' }, +# publishing memory contexts of the specified postgres process +{ oid => '2173', descr => 'publish memory contexts of the specified backend', + proname => 'pg_get_process_memory_contexts', provolatile => 'v', + prorows => '100', proretset => 't', proparallel => 'r', + prorettype => 'record', proargtypes => 'int4 bool float8', + proallargtypes => '{int4,bool,float8,text,text,text,_int4,int4,int8,int8,int8,int8,int8,int4}', + proargmodes => '{i,i,i,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{pid, summary, timeout, name, ident, type, path, level, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes, num_agg_contexts}', + prosrc => 'pg_get_process_memory_contexts' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index 1bef98471c3..1e59a7f910f 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -96,6 +96,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t PublishMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/storage/lwlock.h b/src/include/storage/lwlock.h index 08a72569ae5..638407adf39 100644 --- a/src/include/storage/lwlock.h +++ b/src/include/storage/lwlock.h @@ -221,6 +221,7 @@ typedef enum BuiltinTrancheIds LWTRANCHE_XACT_SLRU, LWTRANCHE_PARALLEL_VACUUM_DSA, LWTRANCHE_AIO_URING_COMPLETION, + LWTRANCHE_MEMORY_CONTEXT_KEYS, LWTRANCHE_FIRST_USER_DEFINED, } BuiltinTrancheIds; diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index afeeb1ca019..345d5a0ecb1 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -35,6 +35,7 @@ typedef enum PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to send the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ /* Recovery conflict reasons */ diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 8abc26abce2..01835d56021 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -18,7 +18,10 @@ #define MEMUTILS_H #include "nodes/memnodes.h" - +#include "storage/condition_variable.h" +#include "storage/lmgr.h" +#include "utils/dsa.h" +#include "lib/dshash.h" /* * MaxAllocSize, MaxAllocHugeSize @@ -48,6 +51,23 @@ #define AllocHugeSizeIsValid(size) ((Size) (size) <= MaxAllocHugeSize) +/* + * Memory Context reporting size limits. + */ + +/* Max length of context name and ident */ +#define MEMORY_CONTEXT_IDENT_SHMEM_SIZE 64 +/* Maximum size (in bytes) of DSA area per process */ +#define MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND ((size_t) (1 * 1024 * 1024)) + +/* + * Maximum size per context. Actual size may be lower as this assumes the worst + * case of deepest path and longest identifiers (name and ident, thus the + * multiplication by 2). The path depth is limited to 100 like for memory + * context logging. + */ +#define MAX_MEMORY_CONTEXT_STATS_SIZE (sizeof(MemoryStatsEntry)) +#define MAX_MEMORY_CONTEXT_STATS_NUM MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND / MAX_MEMORY_CONTEXT_STATS_SIZE /* * Standard top-level memory contexts. @@ -319,4 +339,74 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) return true; } +/* Dynamic shared memory state for statistics per context */ +typedef struct MemoryStatsEntry +{ + char name[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + char ident[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + int path[100]; + NodeTag type; + int path_length; + int levels; + int64 totalspace; + int64 nblocks; + int64 freespace; + int64 freechunks; + int num_agg_stats; +} MemoryStatsEntry; + +/* + * Per backend dynamic shared hash entry for memory context statistics + * reporting. + */ +typedef struct MemoryStatsDSHashEntry +{ + char key[64]; + ConditionVariable memcxt_cv; + int proc_id; + int total_stats; + bool summary; + dsa_pointer memstats_dsa_pointer; + TimestampTz stats_timestamp; +} MemoryStatsDSHashEntry; + +static const dshash_parameters memctx_dsh_params = { + offsetof(MemoryStatsDSHashEntry, memcxt_cv), + sizeof(MemoryStatsDSHashEntry), + dshash_strcmp, + dshash_strhash, + dshash_strcpy +}; + +/* + * Used for storage of transient identifiers for pg_get_backend_memory_contexts + */ +typedef struct MemoryStatsContextId +{ + MemoryContext context; + int context_id; +} MemoryStatsContextId; + +/* + * This is passed to MemoryContextStatsInternal to determine whether + * to print context statistics or not and where to print them logs or + * stderr. + */ +typedef enum PrintDestination +{ + PRINT_STATS_TO_STDERR = 0, + PRINT_STATS_TO_LOGS, + PRINT_STATS_NONE +} PrintDestination; + +extern void ProcessGetMemoryContextInterrupt(void); +extern void HandleGetMemoryContextInterrupt(void); +extern void MemoryContextKeysShmemInit(void); +extern Size MemoryContextKeysShmemSize(void); +extern void MemoryContextStatsInternal(MemoryContext context, int level, + int max_level, int max_children, + MemoryContextCounters *totals, + PrintDestination print_location, + int *num_contexts); +extern void AtProcExit_memstats_cleanup(int code, Datum arg); #endif /* MEMUTILS_H */ diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 83228cfca29..ae17d028ed3 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -232,3 +232,22 @@ select * from pg_timezone_abbrevs where abbrev = 'LMT'; LMT | @ 7 hours 52 mins 58 secs ago | f (1 row) +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; +NOTICE: (AllocSet,TopMemoryContext,) +NOTICE: (AllocSet,TopMemoryContext,) diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 66179f026b3..d0917b6868e 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -101,3 +101,21 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs; -- One specific case we can check without much fear of breakage -- is the historical local-mean-time value used for America/Los_Angeles. select * from pg_timezone_abbrevs where abbrev = 'LMT'; + +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 83192038571..fedae342032 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1683,6 +1683,9 @@ MemoryContextData MemoryContextId MemoryContextMethodID MemoryContextMethods +MemoryStatsContextId +MemoryStatsEntry +MemoryStatsDSHashEntry MemoryStatsPrintFunc MergeAction MergeActionState -- 2.34.1 ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-07-29 13:40 Rahila Syed <[email protected]> parent: Rahila Syed <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Rahila Syed @ 2025-07-29 13:40 UTC (permalink / raw) To: pgsql-hackers; +Cc: Daniel Gustafsson <[email protected]> Hi, Please find attached an updated patch. It contains the following changes. 1. It needed a rebase as highlighted by cfbot <https://cfbot.cputube.org/patch_5938.log;. The method for adding an LWLock was updated in commit-2047ad068139f0b8c6da73d0b845ca9ba30fb33d, so the patch has been adjusted to reflect this change. 2. Updated some comments to align with the latest patch design. 3. Eliminated an unnecessary assertion Thank you, Rahila Syed On Fri, Jul 11, 2025 at 9:01 PM Rahila Syed <[email protected]> wrote: > Hi, > > Please find attached the latest memory context statistics monitoring > patch. > It has been redesigned to address several issues highlighted in the thread > [1] > and [2]. > > Here are some key highlights of the new design: > > - All DSA processing has been moved out of the CFI handler function. Now, > all the dynamic shared memory > needed to store the statistics is created and deleted in the client > function. This change addresses concerns > that DSA APIs are too high level to be safely called from interrupt > handlers. There was also a concern that > DSA API calls might not provide re-entrancy, which could cause issues if > CFI is invoked from a DSA function > in the future. > > - The static shared memory array has been replaced with a DSHASH table > which now holds metadata such as > pointers to actual statistics for each process. > > - dsm_registry.c APIs are used for creating and attaching to DSA and > DSHASH table, which helps prevent code > duplication. > > -To address the memory leak concern, we create an exclusive memory context > under the NULL context, which > does not fall under the TopMemoryContext tree, to handle all the memory > allocations in ProcessGetMemoryContextInterrupt. > This ensures the memory context created by the function does not affect > its outcome. > The memory context is reset at the end of the function, which helps > prevent any memory leaks. > > - Changes made to the mcxt.c file have been relocated to mcxtfuncs.c, > which now contains all the existing > memory statistics-related functions along with the code for the proposed > function. > > The overall flow of a request is as follows: > > 1. A client backend running the pg_get_process_memory_contexts function > creates a DSA and allocates memory > to store statistics, tracked by DSA pointer. This pointer is stored in a > DSHASH entry for each client querying the > statistics of any process. > The client shares its DSHASH table key with the server process using a > static shared array of keys indexed > by the server's procNumber. It notifies the server process to publish > statistics by using SendProcSignal. > > 2. When a PostgreSQL server process handles the request for memory > statistics, the CFI function accesses the > client hash key stored in its procNumber slot of the shared keys array. > The server process then retrieves the > DSHASH entry to obtain the DSA pointer allocated by the client, for > storing the statistics. > After storing the statistics, it notifies the client through its > condition variable. > > 3. Although the DSA is created just once, the memory inside the DSA is > allocated and released by the client > process as soon as it finishes reading the statistics. > If it fails to do so, it is deleted by the before_shmem_exit callback when > the client exits. The client's entry in DSHASH > table is also deleted when the client exits. > > 4. The DSA and DSHASH table are not created > until pg_get_process_memory_context function is called. > Once created, any client backend querying statistics and any PostgreSQL > process publishing statistics will > attach to the same area and table. > > Please let me know your thoughts. > > Thank you, > Rahila Syed > > [1]. PostgreSQL: Re: pgsql: Add function to get memory context stats for > processes > <https://www.postgresql.org/message-id/CA%2BTgmoaey-kOP1k5FaUnQFd1fR0majVebWcL8ogfLbG_nt-Ytg%40mail.g...; > [2]. PostgreSQL: Re: Prevent an error on attaching/creating a DSM/DSA > from an interrupt handler. > <https://www.postgresql.org/message-id/flat/8B873D49-E0E5-4F9F-B8D6-CA4836B825CD%40yesql.se#7026d2fe4...; > > On Wed, Apr 30, 2025 at 4:13 PM Daniel Gustafsson <[email protected]> wrote: > >> > On 30 Apr 2025, at 12:14, Peter Eisentraut <[email protected]> >> wrote: >> > >> > On 29.04.25 15:13, Rahila Syed wrote: >> >> Please find attached a patch with some comments and documentation >> changes. >> >> Additionaly, added a missing '\0' termination to "Remaining Totals" >> string. >> >> I think this became necessary after we replaced dsa_allocate0() >> >> with dsa_allocate() is the latest version. >> > >> > > strncpy(nameptr, "Remaining Totals", namelen); >> > > + nameptr[namelen] = '\0'; >> > >> > Looks like a case for strlcpy()? >> >> True. I did go ahead with the strncpy and nul terminator assignment, >> mostly >> out of muscle memory, but I agree that this would be a good place for a >> strlcpy() instead. >> >> -- >> Daniel Gustafsson >> >> Attachments: [application/octet-stream] v31-0001-Add-pg_get_process_memory_context-function.patch (59.6K, ../../CAH2L28vCCgye_+kJt22RAFzZfYbO7ytSrp-hR6-SenBcm_cN+w@mail.gmail.com/3-v31-0001-Add-pg_get_process_memory_context-function.patch) download | inline diff: From 9487a3d5d673bb0ec5521ad8bdb1e6cb603a51a6 Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Mon, 30 Jun 2025 12:11:00 +0530 Subject: [PATCH] Add pg_get_process_memory_context function This adds a function for retrieving memory context statistics and information from backends as well as auxiliary processes. The intended usecase is cluster debugging when under memory pressure or unanticipated memory usage characteristics. When calling the function it sends a signal to the specified process to submit statistics regarding its memory contexts into dynamic shared memory. Each memory context is returned in detail, followed by a cumulative total in case the number of contexts exceed the max allocated amount of shared memory. Each process is limited to use at most 1Mb memory for this. A summary can also be explicitly requested by the user, this will return the TopMemoryContext and a cumulative total of all lower contexts. In order to not block on busy processes the caller specifies the number of seconds during which to retry before timing out. In the case where no statistics are published within the set timeout, NULL is returned. --- doc/src/sgml/func.sgml | 164 ++++ src/backend/catalog/system_views.sql | 5 + src/backend/postmaster/autovacuum.c | 4 + src/backend/postmaster/checkpointer.c | 4 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 4 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 4 + src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/storage/lmgr/proc.c | 1 + src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 2 + src/backend/utils/adt/mcxtfuncs.c | 831 +++++++++++++++++- src/backend/utils/adt/pg_locale.c | 1 - src/backend/utils/init/globals.c | 1 + src/backend/utils/init/postinit.c | 7 + src/backend/utils/mb/mbutils.c | 1 - src/backend/utils/mmgr/mcxt.c | 71 +- src/include/catalog/pg_proc.dat | 10 + src/include/miscadmin.h | 1 + src/include/storage/lwlocklist.h | 1 + src/include/storage/procsignal.h | 1 + src/include/utils/memutils.h | 92 +- src/test/regress/expected/sysviews.out | 19 + src/test/regress/sql/sysviews.sql | 18 + src/tools/pgindent/typedefs.list | 3 + 27 files changed, 1221 insertions(+), 41 deletions(-) diff --git a/doc/src/sgml/func.sgml b/doc/src/sgml/func.sgml index 74a16af04ad..2cde766072d 100644 --- a/doc/src/sgml/func.sgml +++ b/doc/src/sgml/func.sgml @@ -28719,6 +28719,137 @@ acl | {postgres=arwdDxtm/postgres,foo=r/postgres} </para></entry> </row> + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_get_process_memory_contexts</primary> + </indexterm> + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type>, <parameter>summary</parameter> <type>boolean</type>, <parameter>timeout</parameter> <type>float</type> ) + <returnvalue>setof record</returnvalue> + ( <parameter>name</parameter> <type>text</type>, + <parameter>ident</parameter> <type>text</type>, + <parameter>type</parameter> <type>text</type>, + <parameter>path</parameter> <type>integer[]</type>, + <parameter>level</parameter> <type>integer</type>, + <parameter>total_bytes</parameter> <type>bigint</type>, + <parameter>total_nblocks</parameter> <type>bigint</type>, + <parameter>free_bytes</parameter> <type>bigint</type>, + <parameter>free_chunks</parameter> <type>bigint</type>, + <parameter>used_bytes</parameter> <type>bigint</type>, + <parameter>num_agg_contexts</parameter> <type>integer</type>, + <parameter>stats_timestamp</parameter> <type>timestamptz</type> ) + </para> + <para> + This function handles requests to display the memory contexts of a + <productname>PostgreSQL</productname> process with the specified + process ID. The function can be used to send requests to backends as + well as <glossterm linkend="glossary-auxiliary-proc">auxiliary processes</glossterm>. + </para> + <para> + The returned record contains extended statistics per each memory + context: + <itemizedlist spacing="compact"> + <listitem> + <para> + <parameter>name</parameter> - The name of the memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>ident</parameter> - Memory context ID (if any). + </para> + </listitem> + <listitem> + <para> + <parameter>type</parameter> - The type of memory context, possible + values are: AllocSet, Generation, Slab and Bump. + </para> + </listitem> + <listitem> + <para> + <parameter>path</parameter> - Memory contexts are organized in a + tree model with TopMemoryContext as the root, and all other memory + contexts as nodes in the tree. The <parameter>path</parameter> + displays the path from the root to the current memory context. The + path is limited to 100 children per node, which each node limited + to a max depth of 100, to preserve memory during reporting. The + printed path will also be limited to 100 nodes counting from the + TopMemoryContext. + </para> + </listitem> + <listitem> + <para> + <parameter>level</parameter> - The level in the tree of the current + memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_bytes</parameter> - The total number of bytes + allocated to this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_nblocks</parameter> - The total number of blocks + used for the allocated memory. + </para> + </listitem> + <listitem> + <para> + <parameter>free_bytes</parameter> - The amount of free memory in + this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>free_chunks</parameter> - The number of chunks that + <parameter>free_bytes</parameter> corresponds to. + </para> + </listitem> + <listitem> + <para> + <parameter>used_bytes</parameter> - The total number of bytes + currently occupied. + </para> + </listitem> + <listitem> + <para> + <parameter>num_agg_contexts</parameter> - The number of memory + contexts aggregated in the displayed statistics. + </para> + </listitem> + </itemizedlist> + </para> + <para> + When <parameter>summary</parameter> is <literal>true</literal>, statistics + for memory contexts at levels 1 and 2 are displayed, with level 1 + representing the root node (i.e., <literal>TopMemoryContext</literal>). + Statistics for contexts on level 2 and below are aggregates of all + child contexts' statistics, where <literal>num_agg_contexts</literal> + indicate the number aggregated child contexts. When + <parameter>summary</parameter> is <literal>false</literal>, + <literal>the num_agg_contexts</literal> value is <literal>1</literal>, + indicating that individual statistics are being displayed. + </para> + <para> + Busy processes can delay reporting memory context statistics, + <parameter>timeout</parameter> specifies the number of seconds + to wait for updated statistics. <parameter>timeout</parameter> can be + specified in fractions of a second. + </para> + <para> + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context + statistics are aggregated and a cumulative total is displayed. The + <literal>num_agg_contexts</literal> column indicates the number of + contexts aggregated in the displayed statistics. When + <literal>num_agg_contexts</literal> is <literal>1</literal> it means + that the context statistics are displayed separately. + </para></entry> + </row> + <row> <entry role="func_table_entry"><para role="func_signature"> <indexterm> @@ -28858,6 +28989,39 @@ LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 because it may generate a large number of log messages. </para> + <para> + <function>pg_get_process_memory_contexts</function> can be used to request + memory contexts statistics of any <productname>PostgreSQL</productname> + process. For example: +<programlisting> +postgres=# SELECT * FROM pg_get_process_memory_contexts( + (SELECT pid FROM pg_stat_activity + WHERE backend_type = 'checkpointer'), + false, 0.5) LIMIT 1; +-[ RECORD 1 ]----+------------------------------ +name | TopMemoryContext +ident | +type | AllocSet +path | {1} +level | 1 +total_bytes | 90304 +total_nblocks | 3 +free_bytes | 2880 +free_chunks | 1 +used_bytes | 87424 +num_agg_contexts | 1 +</programlisting> + <note> + <para> + While <function>pg_get_process_memory_contexts</function> can be used to + query memory contexts of the local backend, + <structname>pg_backend_memory_contexts</structname> + (see <xref linkend="view-pg-backend-memory-contexts"/> for more details) + will be less resource intensive when only the local backend is of interest. + </para> + </note> + </para> + </sect2> <sect2 id="functions-admin-backup"> diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index f6eca09ee15..49740fadb6d 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -682,6 +682,11 @@ GRANT SELECT ON pg_backend_memory_contexts TO pg_read_all_stats; REVOKE EXECUTE ON FUNCTION pg_get_backend_memory_contexts() FROM PUBLIC; GRANT EXECUTE ON FUNCTION pg_get_backend_memory_contexts() TO pg_read_all_stats; +REVOKE EXECUTE ON FUNCTION + pg_get_process_memory_contexts(integer, boolean, float) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION + pg_get_process_memory_contexts(integer, boolean, float) TO pg_read_all_stats; + -- Statistics views CREATE VIEW pg_stat_all_tables AS diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 9474095f271..5e7e8081c05 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -781,6 +781,10 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index 8490148a47d..767fc6a0014 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -679,6 +679,10 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index 0ae9bf906ec..f24f574e748 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -48,6 +48,10 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index 78e39e5f866..ac97a39447c 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -867,6 +867,10 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index 27e86cf393f..7149a67fcbc 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -192,6 +192,10 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index 777c9a8d555..5d14684f6b2 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -879,6 +879,10 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index 2fa045e6b0f..fe3d32e40b0 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -51,6 +51,7 @@ #include "storage/sinvaladt.h" #include "utils/guc.h" #include "utils/injection_point.h" +#include "utils/memutils.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -150,6 +151,7 @@ CalculateShmemSize(int *num_semaphores) size = add_size(size, InjectionPointShmemSize()); size = add_size(size, SlotSyncShmemSize()); size = add_size(size, AioShmemSize()); + size = add_size(size, MemoryContextKeysShmemSize() + sizeof(LWLockPadded)); /* include additional requested shmem from preload libraries */ size = add_size(size, total_addin_request); @@ -343,6 +345,7 @@ CreateOrAttachShmemStructs(void) WaitEventCustomShmemInit(); InjectionPointShmemInit(); AioShmemInit(); + MemoryContextKeysShmemInit(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index a9bb540b55a..ce69e26d720 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -691,6 +691,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_GET_MEMORY_CONTEXT)) + HandleGetMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index e9ef0fbfe32..f194e6b3dcc 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -50,6 +50,7 @@ #include "storage/procsignal.h" #include "storage/spin.h" #include "storage/standby.h" +#include "utils/memutils.h" #include "utils/timeout.h" #include "utils/timestamp.h" diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index a297606cdd7..c8cac8811ee 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3534,6 +3534,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 0be307d2ca0..54f91a76a1b 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -161,6 +161,7 @@ WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." +MEM_CXT_PUBLISH "Waiting for a process to publish memory information." ABI_compatibility: @@ -406,6 +407,7 @@ SubtransSLRU "Waiting to access the sub-transaction SLRU cache." XactSLRU "Waiting to access the transaction status SLRU cache." ParallelVacuumDSA "Waiting for parallel vacuum dynamic shared memory allocation." AioUringCompletion "Waiting for another process to complete IO via io_uring." +MemoryContextReportingKeys "Waiting for another process to complete reading or writing the memory reporting keys." # No "ABI_compatibility" region here as WaitEventLWLock has its own C code. diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index fe6dce9cba3..32a03f12b1a 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -15,13 +15,38 @@ #include "postgres.h" +#include "access/twophase.h" +#include "catalog/pg_authid_d.h" #include "funcapi.h" #include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "storage/dsm_registry.h" #include "storage/proc.h" #include "storage/procarray.h" +#include "utils/acl.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/memutils.h" +#include "utils/wait_event_types.h" + +#define CLIENT_KEY_SIZE 64 + +static LWLock *client_keys_lock = NULL; +static int *client_keys = NULL; +static dshash_table *MemoryStatsDsHash = NULL; +static dsa_area *MemoryStatsDsaArea = NULL; + +static void memstats_dsa_cleanup(MemoryStatsDSHashEntry *entry); +static const char *ContextTypeToString(NodeTag type); +static void PublishMemoryContext(MemoryStatsEntry *memcxt_info, + int curr_id, MemoryContext context, + List *path, + MemoryContextCounters stat, + int num_contexts, int max_levels); +static List *compute_context_path(MemoryContext c, HTAB *context_id_lookup); +static void end_memorycontext_reporting(MemoryStatsDSHashEntry *entry, MemoryContext oldcontext, + HTAB *context_id_lookup); /* ---------- * The max bytes for showing identifiers of MemoryContext. @@ -89,7 +114,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, */ for (MemoryContext cur = context; cur != NULL; cur = cur->parent) { - MemoryContextId *entry; + MemoryStatsContextId *entry; bool found; entry = hash_search(context_id_lookup, &cur, HASH_FIND, &found); @@ -143,24 +168,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, else nulls[1] = true; - switch (context->type) - { - case T_AllocSetContext: - type = "AllocSet"; - break; - case T_GenerationContext: - type = "Generation"; - break; - case T_SlabContext: - type = "Slab"; - break; - case T_BumpContext: - type = "Bump"; - break; - default: - type = "???"; - break; - } + type = ContextTypeToString(context->type); values[2] = CStringGetTextDatum(type); values[3] = Int32GetDatum(list_length(path)); /* level */ @@ -175,6 +183,38 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, list_free(path); } +/* + * ContextTypeToString + * Returns a textual representation of a context type + * + * This should cover the same types as MemoryContextIsValid. + */ +const char * +ContextTypeToString(NodeTag type) +{ + const char *context_type; + + switch (type) + { + case T_AllocSetContext: + context_type = "AllocSet"; + break; + case T_GenerationContext: + context_type = "Generation"; + break; + case T_SlabContext: + context_type = "Slab"; + break; + case T_BumpContext: + context_type = "Bump"; + break; + default: + context_type = "???"; + break; + } + return context_type; +} + /* * pg_get_backend_memory_contexts * SQL SRF showing backend memory context. @@ -189,7 +229,7 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) HTAB *context_id_lookup; ctl.keysize = sizeof(MemoryContext); - ctl.entrysize = sizeof(MemoryContextId); + ctl.entrysize = sizeof(MemoryStatsContextId); ctl.hcxt = CurrentMemoryContext; context_id_lookup = hash_create("pg_get_backend_memory_contexts", @@ -216,7 +256,7 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) foreach_ptr(MemoryContextData, cur, contexts) { - MemoryContextId *entry; + MemoryStatsContextId *entry; bool found; /* @@ -224,8 +264,8 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) * PutMemoryContextsStatsTupleStore needs this to populate the "path" * column with the parent context_ids. */ - entry = (MemoryContextId *) hash_search(context_id_lookup, &cur, - HASH_ENTER, &found); + entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &cur, + HASH_ENTER, &found); entry->context_id = context_id++; Assert(!found); @@ -305,3 +345,748 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } + +/* + * pg_get_process_memory_contexts + * Signal a backend or an auxiliary process to send its memory contexts, + * wait for the results and display them. + * + * By default, only superusers or users with ROLE_PG_READ_ALL_STATS are allowed + * to signal a process to return the memory contexts. This is because allowing + * any users to issue this request at an unbounded rate would cause lots of + * requests to be sent, which can lead to denial of service. Additional roles + * can be permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to copy the memory context details + * to a dynamic shared memory space. + * + * We have defined a limit on DSA memory that could be allocated per process - + * if the process has more memory contexts than what can fit in the allocated + * size, the excess contexts are summarized and represented as cumulative total + * at the end of the buffer. + * + * After sending the signal, wait on a condition variable. The publishing + * backend, after copying the data to shared memory, sends signal on that + * condition variable. There is one condition variable per client process. + * Once the condition variable is signalled, check if the latest memory context + * information is available and display. + * + * If the publishing backend does not respond before the condition variable + * times out, which is set to MEMSTATS_WAIT_TIMEOUT, retry given that there is + * time left within the timeout specified by the user, before giving up and + * returning previously published statistics, if any. If no previous statistics + * exist, return NULL. + */ +#define MEMSTATS_WAIT_TIMEOUT 100 +Datum +pg_get_process_memory_contexts(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + bool summary = PG_GETARG_BOOL(1); + double timeout = PG_GETARG_FLOAT8(2); + PGPROC *proc; + ProcNumber procNumber = INVALID_PROC_NUMBER; + bool proc_is_aux = false; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + MemoryStatsEntry *memcxt_info; + MemoryStatsDSHashEntry *entry; + bool found; + char key[CLIENT_KEY_SIZE]; + + /* + * See if the process with given pid is a backend or an auxiliary process + * and remember the type for when we requery the process later. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + { + proc = AuxiliaryPidGetProc(pid); + proc_is_aux = true; + } + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; this is however not a problem and leave with a WARNING. + * See comment in pg_log_backend_memory_contexts for a discussion on this. + */ + if (proc == NULL) + { + /* + * This is just a warning so a loop-through-resultset will not abort + * if one backend terminated on its own during the run. + */ + ereport(WARNING, + errmsg("PID %d is not a PostgreSQL server process", pid)); + PG_RETURN_NULL(); + } + + InitMaterializedSRF(fcinfo, 0); + + procNumber = GetNumberFromPGProc(proc); + + /* + * Create a DSA to allocate memory for copying memory contexts statistics. + * Allocate the memory in the DSA and send dsa pointer to the server + * process for storing the context statistics. If number of contexts + * exceed a predefined limit(1MB), a cumulative total is stored for such + * contexts. + * + * The DSA is created once for the lifetime of the server, and only + * attached in subsequent calls. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", &found); + + /* + * The dsa pointers containing statistics for each client are stored in a + * dshash table. In addition to dsa pointer, each entry in this table also + * contains information about the statistics, condition variable for + * signalling between client and the server and miscellaneous data + * specific to a request. There is one entry per client request in the + * hash table. + */ + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", &memctx_dsh_params, &found); + + snprintf(key, sizeof(key), "%d", MyProcNumber); + + /* + * Check if the publishing process slot is empty and store this clients + * key i.e its procNumber. This informs the publishing process that it is + * supposed to write statistics in the hash entry corresponding to this + * client. + */ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + if (client_keys[procNumber] == -1) + client_keys[procNumber] = MyProcNumber; + else + { + ereport(WARNING, + errmsg("server process is processing previous request %d: %m", pid)); + LWLockRelease(client_keys_lock); + PG_RETURN_NULL(); + } + LWLockRelease(client_keys_lock); + + /* + * Insert an entry for this client in DSHASH table the first time this + * function is called. This entry is deleted when the process exits in + * before_shmem_exit call. + * + * dshash_find_or_insert locks the entry to prevent the publisher from + * reading before client has updated the entry. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + if (!found) + ConditionVariableInit(&entry->memcxt_cv); + + /* + * Allocate 1MB of memory for the backend to publish its statistics on + * every call to this function. The memory is freed at the end of the + * function. + */ + entry->memstats_dsa_pointer = + dsa_allocate0(MemoryStatsDsaArea, MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND); + entry->summary = summary; + dshash_release_lock(MemoryStatsDsHash, entry); + + /* + * Send a signal to a PostgreSQL process, informing it we want it to + * produce information about its memory contexts. + */ + if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, procNumber) < 0) + { + ereport(WARNING, + errmsg("could not send signal to process %d: %m", pid)); + PG_RETURN_NULL(); + } + + while (1) + { + + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + Assert(found); + + /* + * We expect to come out of sleep when the requested process has + * finished publishing the statistics, verified using the correct + * entry in the proc_id field. + * + * Make sure that the information belongs to pid we requested + * information for, Otherwise loop back and wait for the server + * process to finish publishing statistics. + * + */ + if (entry->proc_id == pid) + break; + + dshash_release_lock(MemoryStatsDsHash, entry); + + /* + * Recheck the state of the backend before sleeping on the condition + * variable to ensure the process is still alive. Only check the + * relevant process type based on the earlier PID check. + */ + if (proc_is_aux) + proc = AuxiliaryPidGetProc(pid); + else + proc = BackendPidGetProc(pid); + + /* + * The process ending during memory context processing is not an + * error. + */ + if (proc == NULL) + { + ereport(WARNING, + errmsg("PID %d is no longer a PostgreSQL server process", + pid)); + memstats_dsa_cleanup(entry); + PG_RETURN_NULL(); + } + + + /* + * Wait for the timeout as defined by the user. If no statistics are + * available within the allowed time then return NULL. The timer is + * defined in milliseconds since that's what the condition variable + * sleep uses. + */ + if (ConditionVariableTimedSleep(&entry->memcxt_cv, + (timeout * 1000), WAIT_EVENT_MEM_CXT_PUBLISH)) + { + /* Timeout has expired, return NULL */ + memstats_dsa_cleanup(entry); + PG_RETURN_NULL(); + } + } + + /* + * Backend has finished publishing the stats, project them. + */ + memcxt_info = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + +#define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 11 + for (int i = 0; i < entry->total_stats; i++) + { + ArrayType *path_array; + int path_length; + Datum values[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + bool nulls[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + Datum *path_datum = NULL; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + if (memcxt_info[i].name[0] != '\0') + { + values[0] = CStringGetTextDatum(memcxt_info[i].name); + } + else + nulls[0] = true; + + if (memcxt_info[i].ident[0] != '\0') + { + values[1] = CStringGetTextDatum(memcxt_info[i].ident); + } + else + nulls[1] = true; + + values[2] = CStringGetTextDatum(ContextTypeToString(memcxt_info[i].type)); + + path_length = memcxt_info[i].path_length; + path_datum = (Datum *) palloc(path_length * sizeof(Datum)); + if (memcxt_info[i].path[0] != 0) + { + for (int j = 0; j < path_length; j++) + path_datum[j] = Int32GetDatum(memcxt_info[i].path[j]); + path_array = construct_array_builtin(path_datum, path_length, INT4OID); + values[3] = PointerGetDatum(path_array); + } + else + nulls[3] = true; + + values[4] = Int32GetDatum(memcxt_info[i].levels); + values[5] = Int64GetDatum(memcxt_info[i].totalspace); + values[6] = Int64GetDatum(memcxt_info[i].nblocks); + values[7] = Int64GetDatum(memcxt_info[i].freespace); + values[8] = Int64GetDatum(memcxt_info[i].freechunks); + values[9] = Int64GetDatum(memcxt_info[i].totalspace - + memcxt_info[i].freespace); + values[10] = Int32GetDatum(memcxt_info[i].num_agg_stats); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + memstats_dsa_cleanup(entry); + dshash_release_lock(MemoryStatsDsHash, entry); + + ConditionVariableCancelSleep(); + + PG_RETURN_NULL(); +} + +static void +memstats_dsa_cleanup(MemoryStatsDSHashEntry *entry) +{ + Assert(MemoryStatsDsaArea != NULL); + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + entry->memstats_dsa_pointer = InvalidDsaPointer; + entry->proc_id = 0; +} +void +MemoryContextKeysShmemInit(void) +{ + bool found; + + client_keys = (int *) + ShmemInitStruct("MemoryContextKeys", + MemoryContextKeysShmemSize() + sizeof(LWLockPadded), &found); + client_keys_lock = (LWLock *) ((char *) client_keys + MemoryContextKeysShmemSize()); + + if (!found) + { + MemSet(client_keys, -1, MemoryContextKeysShmemSize()); + LWLockInitialize(client_keys_lock, LWTRANCHE_MEMORY_CONTEXT_KEYS); + } +} + +Size +MemoryContextKeysShmemSize(void) +{ + Size sz = 0; + Size TotalProcs = 0; + + TotalProcs = add_size(TotalProcs, NUM_AUXILIARY_PROCS); + TotalProcs = add_size(TotalProcs, MaxBackends); + sz = add_size(sz, mul_size(TotalProcs, sizeof(int))); + + return sz; +} + +/* + * HandleGetMemoryContextInterrupt + * Handle receipt of an interrupt indicating a request to publish memory + * contexts statistics. + * + * All the actual work is deferred to ProcessGetMemoryContextInterrupt() as + * this cannot be performed in a signal handler. + */ +void +HandleGetMemoryContextInterrupt(void) +{ + InterruptPending = true; + PublishMemoryContextPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + +/* + * ProcessGetMemoryContextInterrupt + * Generate information about memory contexts used by the process. + * + * Performs a breadth first search on the memory context tree, thus parents + * statistics are reported before their children in the monitoring function + * output. + * + * Statistics for all the processes are shared via the same dynamic shared + * area. Individual statistics are tracked independently in + * per-process DSA pointers. These pointers are stored in a dshash table with + * key as requesting clients ProcNumber. + * + * We calculate maximum number of context's statistics that can be displayed + * using a pre-determined limit for memory available per process for this + * utility and maximum size of statistics for each context. The remaining + * context statistics if any are captured as a cumulative total at the end of + * individual context's statistics. + * + * If summary is true, we capture the level 1 and level 2 contexts + * statistics. For that we traverse the memory context tree recursively in + * depth first search manner to cover all the children of a parent context, to + * be able to display a cumulative total of memory consumption by a parent at + * level 2 and all its children. + */ +void +ProcessGetMemoryContextInterrupt(void) +{ + List *contexts; + HASHCTL ctl; + HTAB *context_id_lookup; + int context_id = 0; + MemoryStatsEntry *meminfo; + bool summary = false; + MemoryContextCounters stat; + int num_individual_stats = 0; + bool found; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + int clientProcNumber; + MemoryContext memstats_ctx = NULL; + MemoryContext oldcontext = NULL; + + PublishMemoryContextPending = false; + + /* + * Create a new memory context which is not a part of TopMemoryContext + * tree. This context is used to allocate all memory in this function. + * This helps in keeping the memory allocation in this function to report + * memory consumption statistics separate. So that it does not affect the + * output of this function. + */ + memstats_ctx = AllocSetContextCreate((MemoryContext) NULL, "publish_memory_context_statistics", + ALLOCSET_SMALL_SIZES); + oldcontext = MemoryContextSwitchTo(memstats_ctx); + + /* + * The hash table is used for constructing "path" column of the view, + * similar to its local backend counterpart. + */ + ctl.keysize = sizeof(MemoryContext); + ctl.entrysize = sizeof(MemoryStatsContextId); + ctl.hcxt = CurrentMemoryContext; + + context_id_lookup = hash_create("pg_get_remote_backend_memory_contexts", + 256, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + /* List of contexts to process in the next round - start at the top. */ + contexts = list_make1(TopMemoryContext); + + /* + * If DSA exists, created by another process requesting statistics, attach + * to it. We expect the client process to create required DSA and Dshash + * table. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", &found); + + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", &memctx_dsh_params, &found); + + /* Retreive the client key fo publishing statistics */ + LWLockAcquire(client_keys_lock, LW_SHARED); + Assert(client_keys[MyProcNumber] != -1); + clientProcNumber = client_keys[MyProcNumber]; + LWLockRelease(client_keys_lock); + + snprintf(key, CLIENT_KEY_SIZE, "%d", clientProcNumber); + + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + summary = entry->summary; + + /* + * The entry lock is held by dshash_find_or_insert to protect writes to + * process specific memory. Two different processes publishing statistics + * do not block each other. + */ + entry->proc_id = MyProcPid; + + /* Should be allocated by a client backend that is requesting statistics */ + Assert(entry->memstats_dsa_pointer != InvalidDsaPointer); + meminfo = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + if (summary) + { + int cxt_id = 0; + List *path = NIL; + MemoryStatsContextId *contextid_entry; + + /* Copy TopMemoryContext statistics to DSA */ + memset(&stat, 0, sizeof(stat)); + (*TopMemoryContext->methods->stats) (TopMemoryContext, NULL, NULL, + &stat, true); + path = lcons_int(1, path); + PublishMemoryContext(meminfo, cxt_id, TopMemoryContext, path, stat, + 1, 100); + + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &TopMemoryContext, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = cxt_id + 1; + + /* + * Copy statistics for each of TopMemoryContexts children. This + * includes statistics of at most 100 children per node, with each + * child node limited to a depth of 100 in its subtree. + */ + for (MemoryContext c = TopMemoryContext->firstchild; c != NULL; + c = c->nextchild) + { + MemoryContextCounters grand_totals; + int num_contexts = 0; + + path = NIL; + memset(&grand_totals, 0, sizeof(grand_totals)); + + cxt_id++; + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &c, + HASH_ENTER, &found); + Assert(!found); + contextid_entry->context_id = cxt_id + 1; + + MemoryContextStatsInternal(c, 1, 100, 100, &grand_totals, + PRINT_STATS_NONE, &num_contexts); + + path = compute_context_path(c, context_id_lookup); + + PublishMemoryContext(meminfo, cxt_id, c, path, + grand_totals, num_contexts, 100); + } + entry->total_stats = cxt_id + 1; + + /* Notify waiting backends and return */ + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + return; + } + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + MemoryStatsContextId *contextid_entry; + + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &cur, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = context_id + 1; + + /* + * Figure out the transient context_id of this context and each of its + * ancestors, to compute a path for this context. + */ + path = compute_context_path(cur, context_id_lookup); + + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + /* Account for saving one statistics slot for cumulative reporting */ + if (context_id < (MAX_MEMORY_CONTEXT_STATS_NUM - 1)) + { + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1, 100); + } + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].totalspace += stat.totalspace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].nblocks += stat.nblocks; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freespace += stat.freespace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit per process is reached, write aggregate of the + * remaining statistics. + * + * We can store contexts from 0 to max_stats - 1. When context_id is + * greater than max_stats, we stop reporting individual statistics + * when context_id equals max_stats - 2. As we use max_stats - 1 array + * slot for reporting cumulative statistics or "Remaining Totals". + */ + if (context_id == (MAX_MEMORY_CONTEXT_STATS_NUM - 2)) + { + int namelen = strlen("Remaining Totals"); + + num_individual_stats = context_id + 1; + strlcpy(meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].name, "Remaining Totals", namelen + 1); + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].ident[0] = '\0'; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].path[0] = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].type = 0; + } + context_id++; + + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + contexts = lappend(contexts, c); + } + + /* + * Statistics are not aggregated, i.e individual statistics reported when + * context_id <= max_stats. + */ + if (context_id <= MAX_MEMORY_CONTEXT_STATS_NUM) + { + entry->total_stats = context_id; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = 1; + } + /* Report number of aggregated memory contexts */ + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = context_id - + num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for cumulative + * statistics. + */ + entry->total_stats = num_individual_stats + 1; + } + + /* Notify waiting backends and return */ + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); +} + +/* + * Update timestamp and signal all the waiting client backends after copying + * all the statistics. + */ +static void +end_memorycontext_reporting(MemoryStatsDSHashEntry *entry, MemoryContext oldcontext, HTAB *context_id_lookup) +{ + MemoryContext curr_ctx = CurrentMemoryContext; + + dshash_release_lock(MemoryStatsDsHash, entry); + ConditionVariableBroadcast(&entry->memcxt_cv); + + /* + * Empty this processes slot, so other clients can request memory + * statistics + */ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + client_keys[MyProcNumber] = -1; + LWLockRelease(client_keys_lock); + + hash_destroy(context_id_lookup); + MemoryContextSwitchTo(oldcontext); + MemoryContextReset(curr_ctx); +} + +/* + * compute_context_path + * + * Append the transient context_id of this context and each of its ancestors + * to a list, in order to compute a path. + */ +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + MemoryContext cur_context; + + for (cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryStatsContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + elog(ERROR, "hash table corrupted, can't construct path value"); + + path = lcons_int(cur_entry->context_id, path); + } + + return path; +} + +/* + * PublishMemoryContext + * + * Copy the memory context statistics of a single context to a DSA memory + */ +static void +PublishMemoryContext(MemoryStatsEntry *memcxt_info, int curr_id, + MemoryContext context, List *path, + MemoryContextCounters stat, int num_contexts, int max_levels) +{ + const char *ident = context->ident; + const char *name = context->name; + + /* + * To be consistent with logging output, we label dynahash contexts with + * just the hash table name as with MemoryContextStatsPrint(). + */ + if (context->ident && strncmp(context->name, "dynahash", 8) == 0) + { + name = context->ident; + ident = NULL; + } + + if (name != NULL) + { + int namelen = strlen(name); + + if (strlen(name) >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + namelen = pg_mbcliplen(name, namelen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].name, name, namelen + 1); + } + else + /* Clearing the array */ + memcxt_info[curr_id].name[0] = '\0'; + + /* Trim and copy the identifier if it is not set to NULL */ + if (ident != NULL) + { + int idlen = strlen(context->ident); + + /* + * Some identifiers such as SQL query string can be very long, + * truncate oversize identifiers. + */ + if (idlen >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + idlen = pg_mbcliplen(ident, idlen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].ident, ident, idlen + 1); + } + else + memcxt_info[curr_id].ident[0] = '\0'; + + /* Allocate DSA memory for storing path information */ + if (path == NIL) + memcxt_info[curr_id].path[0] = 0; + else + { + int levels = Min(list_length(path), max_levels); + + memcxt_info[curr_id].path_length = levels; + memcxt_info[curr_id].levels = list_length(path); + + foreach_int(i, path) + { + memcxt_info[curr_id].path[foreach_current_index(i)] = i; + if (--levels == 0) + break; + } + } + memcxt_info[curr_id].type = context->type; + memcxt_info[curr_id].totalspace = stat.totalspace; + memcxt_info[curr_id].nblocks = stat.nblocks; + memcxt_info[curr_id].freespace = stat.freespace; + memcxt_info[curr_id].freechunks = stat.freechunks; + memcxt_info[curr_id].num_agg_stats = num_contexts; +} + +void +AtProcExit_memstats_cleanup(int code, Datum arg) +{ + int idx = MyProcNumber; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + bool found; + + if (MemoryStatsDsHash != NULL) + { + snprintf(key, CLIENT_KEY_SIZE, "%d", idx); + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + if (found) + { + if (MemoryStatsDsaArea != NULL && + DsaPointerIsValid(entry->memstats_dsa_pointer)) + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + } + dshash_delete_entry(MemoryStatsDsHash, entry); + } +} diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c index 97c2ac1faf9..ab768a7a91f 100644 --- a/src/backend/utils/adt/pg_locale.c +++ b/src/backend/utils/adt/pg_locale.c @@ -45,7 +45,6 @@ #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/pg_locale.h" -#include "utils/relcache.h" #include "utils/syscache.h" #ifdef WIN32 diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index d31cb45a058..92b0446b80c 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -39,6 +39,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t PublishMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index 641e535a73c..fb3f2d21fa0 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -662,6 +662,13 @@ BaseInit(void) * drop ephemeral slots, which in turn triggers stats reporting. */ ReplicationSlotInitialize(); + + /* + * The before shmem exit callback frees the DSA memory occupied by the + * latest memory context statistics that could be published by this proc + * if requested. + */ + before_shmem_exit(AtProcExit_memstats_cleanup, 0); } diff --git a/src/backend/utils/mb/mbutils.c b/src/backend/utils/mb/mbutils.c index 886ecbad871..308016d7763 100644 --- a/src/backend/utils/mb/mbutils.c +++ b/src/backend/utils/mb/mbutils.c @@ -39,7 +39,6 @@ #include "mb/pg_wchar.h" #include "utils/fmgrprotos.h" #include "utils/memutils.h" -#include "utils/relcache.h" #include "varatt.h" /* diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index ce01dce9861..a6eda19edce 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -23,6 +23,7 @@ #include "mb/pg_wchar.h" #include "miscadmin.h" +#include "utils/hsearch.h" #include "utils/memdebug.h" #include "utils/memutils.h" #include "utils/memutils_internal.h" @@ -159,10 +160,6 @@ MemoryContext PortalContext = NULL; static void MemoryContextDeleteOnly(MemoryContext context); static void MemoryContextCallResetCallbacks(MemoryContext context); -static void MemoryContextStatsInternal(MemoryContext context, int level, - int max_level, int max_children, - MemoryContextCounters *totals, - bool print_to_stderr); static void MemoryContextStatsPrint(MemoryContext context, void *passthru, const char *stats_string, bool print_to_stderr); @@ -864,11 +861,19 @@ MemoryContextStatsDetail(MemoryContext context, bool print_to_stderr) { MemoryContextCounters grand_totals; + int num_contexts; + PrintDestination print_location; memset(&grand_totals, 0, sizeof(grand_totals)); + if (print_to_stderr) + print_location = PRINT_STATS_TO_STDERR; + else + print_location = PRINT_STATS_TO_LOGS; + + /* num_contexts report number of contexts aggregated in the output */ MemoryContextStatsInternal(context, 1, max_level, max_children, - &grand_totals, print_to_stderr); + &grand_totals, print_location, &num_contexts); if (print_to_stderr) fprintf(stderr, @@ -903,13 +908,14 @@ MemoryContextStatsDetail(MemoryContext context, * One recursion level for MemoryContextStats * * Print stats for this context if possible, but in any case accumulate counts - * into *totals (if not NULL). + * into *totals (if not NULL). The callers should make sure that print_location + * is set to PRINT_STATS_TO_STDERR or PRINT_STATS_TO_LOGS or PRINT_STATS_NONE. */ -static void +void MemoryContextStatsInternal(MemoryContext context, int level, int max_level, int max_children, MemoryContextCounters *totals, - bool print_to_stderr) + PrintDestination print_location, int *num_contexts) { MemoryContext child; int ichild; @@ -917,10 +923,39 @@ MemoryContextStatsInternal(MemoryContext context, int level, Assert(MemoryContextIsValid(context)); /* Examine the context itself */ - context->methods->stats(context, - MemoryContextStatsPrint, - &level, - totals, print_to_stderr); + switch (print_location) + { + case PRINT_STATS_TO_STDERR: + context->methods->stats(context, + MemoryContextStatsPrint, + &level, + totals, true); + break; + + case PRINT_STATS_TO_LOGS: + context->methods->stats(context, + MemoryContextStatsPrint, + &level, + totals, false); + break; + + case PRINT_STATS_NONE: + + /* + * Do not print the statistics if print_location is + * PRINT_STATS_NONE, only compute totals. This is used in + * reporting of memory context statistics via a sql function. Last + * parameter is not relevant. + */ + context->methods->stats(context, + NULL, + NULL, + totals, false); + break; + } + + /* Increment the context count for each of the recursive call */ + *num_contexts = *num_contexts + 1; /* * Examine children. @@ -940,7 +975,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, MemoryContextStatsInternal(child, level + 1, max_level, max_children, totals, - print_to_stderr); + print_location, num_contexts); } } @@ -959,7 +994,13 @@ MemoryContextStatsInternal(MemoryContext context, int level, child = MemoryContextTraverseNext(child, context); } - if (print_to_stderr) + /* + * Add the count of children contexts which are traversed in the + * non-recursive manner. + */ + *num_contexts = *num_contexts + ichild; + + if (print_location == PRINT_STATS_TO_STDERR) { for (int i = 0; i < level; i++) fprintf(stderr, " "); @@ -972,7 +1013,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, local_totals.freechunks, local_totals.totalspace - local_totals.freespace); } - else + else if (print_location == PRINT_STATS_TO_LOGS) ereport(LOG_SERVER_ONLY, (errhidestmt(true), errhidecontext(true), diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 3ee8fed7e53..707c965ecf5 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8597,6 +8597,16 @@ prorettype => 'bool', proargtypes => 'int4', prosrc => 'pg_log_backend_memory_contexts' }, +# publishing memory contexts of the specified postgres process +{ oid => '2173', descr => 'publish memory contexts of the specified backend', + proname => 'pg_get_process_memory_contexts', provolatile => 'v', + prorows => '100', proretset => 't', proparallel => 'r', + prorettype => 'record', proargtypes => 'int4 bool float8', + proallargtypes => '{int4,bool,float8,text,text,text,_int4,int4,int8,int8,int8,int8,int8,int4}', + proargmodes => '{i,i,i,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{pid, summary, timeout, name, ident, type, path, level, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes, num_agg_contexts}', + prosrc => 'pg_get_process_memory_contexts' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index 1bef98471c3..1e59a7f910f 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -96,6 +96,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t PublishMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h index 208d2e3a8ed..72ace053e9d 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -135,3 +135,4 @@ PG_LWLOCKTRANCHE(SUBTRANS_SLRU, SubtransSLRU) PG_LWLOCKTRANCHE(XACT_SLRU, XactSLRU) PG_LWLOCKTRANCHE(PARALLEL_VACUUM_DSA, ParallelVacuumDSA) PG_LWLOCKTRANCHE(AIO_URING_COMPLETION, AioUringCompletion) +PG_LWLOCKTRANCHE(MEMORY_CONTEXT_KEYS, MemoryContextReportingKeys) diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index afeeb1ca019..345d5a0ecb1 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -35,6 +35,7 @@ typedef enum PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to send the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ /* Recovery conflict reasons */ diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 8abc26abce2..01835d56021 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -18,7 +18,10 @@ #define MEMUTILS_H #include "nodes/memnodes.h" - +#include "storage/condition_variable.h" +#include "storage/lmgr.h" +#include "utils/dsa.h" +#include "lib/dshash.h" /* * MaxAllocSize, MaxAllocHugeSize @@ -48,6 +51,23 @@ #define AllocHugeSizeIsValid(size) ((Size) (size) <= MaxAllocHugeSize) +/* + * Memory Context reporting size limits. + */ + +/* Max length of context name and ident */ +#define MEMORY_CONTEXT_IDENT_SHMEM_SIZE 64 +/* Maximum size (in bytes) of DSA area per process */ +#define MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND ((size_t) (1 * 1024 * 1024)) + +/* + * Maximum size per context. Actual size may be lower as this assumes the worst + * case of deepest path and longest identifiers (name and ident, thus the + * multiplication by 2). The path depth is limited to 100 like for memory + * context logging. + */ +#define MAX_MEMORY_CONTEXT_STATS_SIZE (sizeof(MemoryStatsEntry)) +#define MAX_MEMORY_CONTEXT_STATS_NUM MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND / MAX_MEMORY_CONTEXT_STATS_SIZE /* * Standard top-level memory contexts. @@ -319,4 +339,74 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) return true; } +/* Dynamic shared memory state for statistics per context */ +typedef struct MemoryStatsEntry +{ + char name[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + char ident[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + int path[100]; + NodeTag type; + int path_length; + int levels; + int64 totalspace; + int64 nblocks; + int64 freespace; + int64 freechunks; + int num_agg_stats; +} MemoryStatsEntry; + +/* + * Per backend dynamic shared hash entry for memory context statistics + * reporting. + */ +typedef struct MemoryStatsDSHashEntry +{ + char key[64]; + ConditionVariable memcxt_cv; + int proc_id; + int total_stats; + bool summary; + dsa_pointer memstats_dsa_pointer; + TimestampTz stats_timestamp; +} MemoryStatsDSHashEntry; + +static const dshash_parameters memctx_dsh_params = { + offsetof(MemoryStatsDSHashEntry, memcxt_cv), + sizeof(MemoryStatsDSHashEntry), + dshash_strcmp, + dshash_strhash, + dshash_strcpy +}; + +/* + * Used for storage of transient identifiers for pg_get_backend_memory_contexts + */ +typedef struct MemoryStatsContextId +{ + MemoryContext context; + int context_id; +} MemoryStatsContextId; + +/* + * This is passed to MemoryContextStatsInternal to determine whether + * to print context statistics or not and where to print them logs or + * stderr. + */ +typedef enum PrintDestination +{ + PRINT_STATS_TO_STDERR = 0, + PRINT_STATS_TO_LOGS, + PRINT_STATS_NONE +} PrintDestination; + +extern void ProcessGetMemoryContextInterrupt(void); +extern void HandleGetMemoryContextInterrupt(void); +extern void MemoryContextKeysShmemInit(void); +extern Size MemoryContextKeysShmemSize(void); +extern void MemoryContextStatsInternal(MemoryContext context, int level, + int max_level, int max_children, + MemoryContextCounters *totals, + PrintDestination print_location, + int *num_contexts); +extern void AtProcExit_memstats_cleanup(int code, Datum arg); #endif /* MEMUTILS_H */ diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 83228cfca29..ae17d028ed3 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -232,3 +232,22 @@ select * from pg_timezone_abbrevs where abbrev = 'LMT'; LMT | @ 7 hours 52 mins 58 secs ago | f (1 row) +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; +NOTICE: (AllocSet,TopMemoryContext,) +NOTICE: (AllocSet,TopMemoryContext,) diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 66179f026b3..d0917b6868e 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -101,3 +101,21 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs; -- One specific case we can check without much fear of breakage -- is the historical local-mean-time value used for America/Los_Angeles. select * from pg_timezone_abbrevs where abbrev = 'LMT'; + +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 3daba26b237..e614ae25a8c 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1681,6 +1681,9 @@ MemoryContextData MemoryContextId MemoryContextMethodID MemoryContextMethods +MemoryStatsContextId +MemoryStatsEntry +MemoryStatsDSHashEntry MemoryStatsPrintFunc MergeAction MergeActionState -- 2.34.1 ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-08-08 09:26 Rahila Syed <[email protected]> parent: Rahila Syed <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Rahila Syed @ 2025-08-08 09:26 UTC (permalink / raw) To: pgsql-hackers; +Cc: Daniel Gustafsson <[email protected]> Hi, CFbot indicated that the patch requires a rebase, so I've attached an updated version. The documentation for this feature is now included in the new func-admin.sgml file, due to recent changes in the documentation of sql functions. The following are results from a performance test: pgbench is initialized as follows : pgbench -i -s 100 postgres Test1 - pgbench -c 16 -j 16 postgres -T 100 TPS: 745.02 (average of 3 runs) Test2- pgbench -c 16 -j 16 postgres -T 100 while memory usage of any postgres process is monitored concurrently every 0.1 seconds, using the following method: SELECT * FROM pg_get_process_memory_contexts( (SELECT pid FROM pg_stat_activity ORDER BY random() LIMIT 1) , false, 5); TPS: 750.66 (average of 3 runs) I have not observed any performance decline resulting from the concurrent execution of the memory monitoring function. Thank you, Rahila Syed On Tue, Jul 29, 2025 at 7:10 PM Rahila Syed <[email protected]> wrote: > Hi, > > Please find attached an updated patch. It contains the following changes. > > 1. It needed a rebase as highlighted by cfbot > <https://cfbot.cputube.org/patch_5938.log;. The method for adding an > LWLock was updated in commit-2047ad068139f0b8c6da73d0b845ca9ba30fb33d, so > the patch has been adjusted to reflect this change. > 2. Updated some comments to align with the latest patch design. > 3. Eliminated an unnecessary assertion > > Thank you, > Rahila Syed > > On Fri, Jul 11, 2025 at 9:01 PM Rahila Syed <[email protected]> > wrote: > >> Hi, >> >> Please find attached the latest memory context statistics monitoring >> patch. >> It has been redesigned to address several issues highlighted in the >> thread [1] >> and [2]. >> >> Here are some key highlights of the new design: >> >> - All DSA processing has been moved out of the CFI handler function. Now, >> all the dynamic shared memory >> needed to store the statistics is created and deleted in the client >> function. This change addresses concerns >> that DSA APIs are too high level to be safely called from interrupt >> handlers. There was also a concern that >> DSA API calls might not provide re-entrancy, which could cause issues if >> CFI is invoked from a DSA function >> in the future. >> >> - The static shared memory array has been replaced with a DSHASH table >> which now holds metadata such as >> pointers to actual statistics for each process. >> >> - dsm_registry.c APIs are used for creating and attaching to DSA and >> DSHASH table, which helps prevent code >> duplication. >> >> -To address the memory leak concern, we create an exclusive memory >> context under the NULL context, which >> does not fall under the TopMemoryContext tree, to handle all the memory >> allocations in ProcessGetMemoryContextInterrupt. >> This ensures the memory context created by the function does not affect >> its outcome. >> The memory context is reset at the end of the function, which helps >> prevent any memory leaks. >> >> - Changes made to the mcxt.c file have been relocated to mcxtfuncs.c, >> which now contains all the existing >> memory statistics-related functions along with the code for the proposed >> function. >> >> The overall flow of a request is as follows: >> >> 1. A client backend running the pg_get_process_memory_contexts function >> creates a DSA and allocates memory >> to store statistics, tracked by DSA pointer. This pointer is stored in a >> DSHASH entry for each client querying the >> statistics of any process. >> The client shares its DSHASH table key with the server process using a >> static shared array of keys indexed >> by the server's procNumber. It notifies the server process to publish >> statistics by using SendProcSignal. >> >> 2. When a PostgreSQL server process handles the request for memory >> statistics, the CFI function accesses the >> client hash key stored in its procNumber slot of the shared keys array. >> The server process then retrieves the >> DSHASH entry to obtain the DSA pointer allocated by the client, for >> storing the statistics. >> After storing the statistics, it notifies the client through its >> condition variable. >> >> 3. Although the DSA is created just once, the memory inside the DSA is >> allocated and released by the client >> process as soon as it finishes reading the statistics. >> If it fails to do so, it is deleted by the before_shmem_exit callback >> when the client exits. The client's entry in DSHASH >> table is also deleted when the client exits. >> >> 4. The DSA and DSHASH table are not created >> until pg_get_process_memory_context function is called. >> Once created, any client backend querying statistics and any PostgreSQL >> process publishing statistics will >> attach to the same area and table. >> >> Please let me know your thoughts. >> >> Thank you, >> Rahila Syed >> >> [1]. PostgreSQL: Re: pgsql: Add function to get memory context stats for >> processes >> <https://www.postgresql.org/message-id/CA%2BTgmoaey-kOP1k5FaUnQFd1fR0majVebWcL8ogfLbG_nt-Ytg%40mail.g...; >> [2]. PostgreSQL: Re: Prevent an error on attaching/creating a DSM/DSA >> from an interrupt handler. >> <https://www.postgresql.org/message-id/flat/8B873D49-E0E5-4F9F-B8D6-CA4836B825CD%40yesql.se#7026d2fe4...; >> >> On Wed, Apr 30, 2025 at 4:13 PM Daniel Gustafsson <[email protected]> >> wrote: >> >>> > On 30 Apr 2025, at 12:14, Peter Eisentraut <[email protected]> >>> wrote: >>> > >>> > On 29.04.25 15:13, Rahila Syed wrote: >>> >> Please find attached a patch with some comments and documentation >>> changes. >>> >> Additionaly, added a missing '\0' termination to "Remaining Totals" >>> string. >>> >> I think this became necessary after we replaced dsa_allocate0() >>> >> with dsa_allocate() is the latest version. >>> > >>> > > strncpy(nameptr, "Remaining Totals", namelen); >>> > > + nameptr[namelen] = '\0'; >>> > >>> > Looks like a case for strlcpy()? >>> >>> True. I did go ahead with the strncpy and nul terminator assignment, >>> mostly >>> out of muscle memory, but I agree that this would be a good place for a >>> strlcpy() instead. >>> >>> -- >>> Daniel Gustafsson >>> >>> Attachments: [application/octet-stream] v32-0001-Add-pg_get_process_memory_context-function.patch (59.6K, ../../CAH2L28t=O1k+5wdcP88rgty3OLZisTU72WGH8Dp2MxJjwn7=fw@mail.gmail.com/3-v32-0001-Add-pg_get_process_memory_context-function.patch) download | inline diff: From 17873d9d45f4207a1c227b49801befbcdb49b93d Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Mon, 30 Jun 2025 12:11:00 +0530 Subject: [PATCH] Add pg_get_process_memory_context function This adds a function for retrieving memory context statistics and information from backends as well as auxiliary processes. The intended usecase is cluster debugging when under memory pressure or unanticipated memory usage characteristics. When calling the function it sends a signal to the specified process to submit statistics regarding its memory contexts into dynamic shared memory. Each memory context is returned in detail, followed by a cumulative total in case the number of contexts exceed the max allocated amount of shared memory. Each process is limited to use at most 1Mb memory for this. A summary can also be explicitly requested by the user, this will return the TopMemoryContext and a cumulative total of all lower contexts. In order to not block on busy processes the caller specifies the number of seconds during which to retry before timing out. In the case where no statistics are published within the set timeout, NULL is returned. --- doc/src/sgml/func/func-admin.sgml | 164 ++++ src/backend/catalog/system_views.sql | 5 + src/backend/postmaster/autovacuum.c | 4 + src/backend/postmaster/checkpointer.c | 4 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 4 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 4 + src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/storage/lmgr/proc.c | 1 + src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 2 + src/backend/utils/adt/mcxtfuncs.c | 831 +++++++++++++++++- src/backend/utils/adt/pg_locale.c | 1 - src/backend/utils/init/globals.c | 1 + src/backend/utils/init/postinit.c | 7 + src/backend/utils/mb/mbutils.c | 1 - src/backend/utils/mmgr/mcxt.c | 71 +- src/include/catalog/pg_proc.dat | 10 + src/include/miscadmin.h | 1 + src/include/storage/lwlocklist.h | 1 + src/include/storage/procsignal.h | 1 + src/include/utils/memutils.h | 92 +- src/test/regress/expected/sysviews.out | 19 + src/test/regress/sql/sysviews.sql | 18 + src/tools/pgindent/typedefs.list | 3 + 27 files changed, 1221 insertions(+), 41 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 446fdfe56f4..689e9231a7e 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -251,6 +251,137 @@ <literal>false</literal> is returned. </para></entry> </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_get_process_memory_contexts</primary> + </indexterm> + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type>, <parameter>summary</parameter> <type>boolean</type>, <parameter>timeout</parameter> <type>float</type> ) + <returnvalue>setof record</returnvalue> + ( <parameter>name</parameter> <type>text</type>, + <parameter>ident</parameter> <type>text</type>, + <parameter>type</parameter> <type>text</type>, + <parameter>path</parameter> <type>integer[]</type>, + <parameter>level</parameter> <type>integer</type>, + <parameter>total_bytes</parameter> <type>bigint</type>, + <parameter>total_nblocks</parameter> <type>bigint</type>, + <parameter>free_bytes</parameter> <type>bigint</type>, + <parameter>free_chunks</parameter> <type>bigint</type>, + <parameter>used_bytes</parameter> <type>bigint</type>, + <parameter>num_agg_contexts</parameter> <type>integer</type>, + <parameter>stats_timestamp</parameter> <type>timestamptz</type> ) + </para> + <para> + This function handles requests to display the memory contexts of a + <productname>PostgreSQL</productname> process with the specified + process ID. The function can be used to send requests to backends as + well as <glossterm linkend="glossary-auxiliary-proc">auxiliary processes</glossterm>. + </para> + <para> + The returned record contains extended statistics per each memory + context: + <itemizedlist spacing="compact"> + <listitem> + <para> + <parameter>name</parameter> - The name of the memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>ident</parameter> - Memory context ID (if any). + </para> + </listitem> + <listitem> + <para> + <parameter>type</parameter> - The type of memory context, possible + values are: AllocSet, Generation, Slab and Bump. + </para> + </listitem> + <listitem> + <para> + <parameter>path</parameter> - Memory contexts are organized in a + tree model with TopMemoryContext as the root, and all other memory + contexts as nodes in the tree. The <parameter>path</parameter> + displays the path from the root to the current memory context. The + path is limited to 100 children per node, which each node limited + to a max depth of 100, to preserve memory during reporting. The + printed path will also be limited to 100 nodes counting from the + TopMemoryContext. + </para> + </listitem> + <listitem> + <para> + <parameter>level</parameter> - The level in the tree of the current + memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_bytes</parameter> - The total number of bytes + allocated to this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_nblocks</parameter> - The total number of blocks + used for the allocated memory. + </para> + </listitem> + <listitem> + <para> + <parameter>free_bytes</parameter> - The amount of free memory in + this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>free_chunks</parameter> - The number of chunks that + <parameter>free_bytes</parameter> corresponds to. + </para> + </listitem> + <listitem> + <para> + <parameter>used_bytes</parameter> - The total number of bytes + currently occupied. + </para> + </listitem> + <listitem> + <para> + <parameter>num_agg_contexts</parameter> - The number of memory + contexts aggregated in the displayed statistics. + </para> + </listitem> + </itemizedlist> + </para> + <para> + When <parameter>summary</parameter> is <literal>true</literal>, statistics + for memory contexts at levels 1 and 2 are displayed, with level 1 + representing the root node (i.e., <literal>TopMemoryContext</literal>). + Statistics for contexts on level 2 and below are aggregates of all + child contexts' statistics, where <literal>num_agg_contexts</literal> + indicate the number aggregated child contexts. When + <parameter>summary</parameter> is <literal>false</literal>, + <literal>the num_agg_contexts</literal> value is <literal>1</literal>, + indicating that individual statistics are being displayed. + </para> + <para> + Busy processes can delay reporting memory context statistics, + <parameter>timeout</parameter> specifies the number of seconds + to wait for updated statistics. <parameter>timeout</parameter> can be + specified in fractions of a second. + </para> + <para> + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context + statistics are aggregated and a cumulative total is displayed. The + <literal>num_agg_contexts</literal> column indicates the number of + contexts aggregated in the displayed statistics. When + <literal>num_agg_contexts</literal> is <literal>1</literal> it means + that the context statistics are displayed separately. + </para></entry> + </row> </tbody> </tgroup> </table> @@ -302,6 +433,39 @@ LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 because it may generate a large number of log messages. </para> + <para> + <function>pg_get_process_memory_contexts</function> can be used to request + memory contexts statistics of any <productname>PostgreSQL</productname> + process. For example: +<programlisting> +postgres=# SELECT * FROM pg_get_process_memory_contexts( + (SELECT pid FROM pg_stat_activity + WHERE backend_type = 'checkpointer'), + false, 0.5) LIMIT 1; +-[ RECORD 1 ]----+------------------------------ +name | TopMemoryContext +ident | +type | AllocSet +path | {1} +level | 1 +total_bytes | 90304 +total_nblocks | 3 +free_bytes | 2880 +free_chunks | 1 +used_bytes | 87424 +num_agg_contexts | 1 +</programlisting> + <note> + <para> + While <function>pg_get_process_memory_contexts</function> can be used to + query memory contexts of the local backend, + <structname>pg_backend_memory_contexts</structname> + (see <xref linkend="view-pg-backend-memory-contexts"/> for more details) + will be less resource intensive when only the local backend is of interest. + </para> + </note> + </para> + </sect2> <sect2 id="functions-admin-backup"> diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index 1b3c5a55882..022586d6a55 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -682,6 +682,11 @@ GRANT SELECT ON pg_backend_memory_contexts TO pg_read_all_stats; REVOKE EXECUTE ON FUNCTION pg_get_backend_memory_contexts() FROM PUBLIC; GRANT EXECUTE ON FUNCTION pg_get_backend_memory_contexts() TO pg_read_all_stats; +REVOKE EXECUTE ON FUNCTION + pg_get_process_memory_contexts(integer, boolean, float) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION + pg_get_process_memory_contexts(integer, boolean, float) TO pg_read_all_stats; + -- Statistics views CREATE VIEW pg_stat_all_tables AS diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index ff96b36d710..3875f76564d 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -791,6 +791,10 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index e84e8663e96..5b3e08805bf 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -679,6 +679,10 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index 0ae9bf906ec..f24f574e748 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -48,6 +48,10 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index 78e39e5f866..ac97a39447c 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -867,6 +867,10 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index 27e86cf393f..7149a67fcbc 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -192,6 +192,10 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index 777c9a8d555..5d14684f6b2 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -879,6 +879,10 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index 2fa045e6b0f..fe3d32e40b0 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -51,6 +51,7 @@ #include "storage/sinvaladt.h" #include "utils/guc.h" #include "utils/injection_point.h" +#include "utils/memutils.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -150,6 +151,7 @@ CalculateShmemSize(int *num_semaphores) size = add_size(size, InjectionPointShmemSize()); size = add_size(size, SlotSyncShmemSize()); size = add_size(size, AioShmemSize()); + size = add_size(size, MemoryContextKeysShmemSize() + sizeof(LWLockPadded)); /* include additional requested shmem from preload libraries */ size = add_size(size, total_addin_request); @@ -343,6 +345,7 @@ CreateOrAttachShmemStructs(void) WaitEventCustomShmemInit(); InjectionPointShmemInit(); AioShmemInit(); + MemoryContextKeysShmemInit(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index 087821311cc..8963285cc12 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -691,6 +691,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_GET_MEMORY_CONTEXT)) + HandleGetMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index e9ef0fbfe32..f194e6b3dcc 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -50,6 +50,7 @@ #include "storage/procsignal.h" #include "storage/spin.h" #include "storage/standby.h" +#include "utils/memutils.h" #include "utils/timeout.h" #include "utils/timestamp.h" diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 0cecd464902..3933b6db607 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3534,6 +3534,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 0be307d2ca0..54f91a76a1b 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -161,6 +161,7 @@ WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." +MEM_CXT_PUBLISH "Waiting for a process to publish memory information." ABI_compatibility: @@ -406,6 +407,7 @@ SubtransSLRU "Waiting to access the sub-transaction SLRU cache." XactSLRU "Waiting to access the transaction status SLRU cache." ParallelVacuumDSA "Waiting for parallel vacuum dynamic shared memory allocation." AioUringCompletion "Waiting for another process to complete IO via io_uring." +MemoryContextReportingKeys "Waiting for another process to complete reading or writing the memory reporting keys." # No "ABI_compatibility" region here as WaitEventLWLock has its own C code. diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index fe6dce9cba3..32a03f12b1a 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -15,13 +15,38 @@ #include "postgres.h" +#include "access/twophase.h" +#include "catalog/pg_authid_d.h" #include "funcapi.h" #include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "storage/dsm_registry.h" #include "storage/proc.h" #include "storage/procarray.h" +#include "utils/acl.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/memutils.h" +#include "utils/wait_event_types.h" + +#define CLIENT_KEY_SIZE 64 + +static LWLock *client_keys_lock = NULL; +static int *client_keys = NULL; +static dshash_table *MemoryStatsDsHash = NULL; +static dsa_area *MemoryStatsDsaArea = NULL; + +static void memstats_dsa_cleanup(MemoryStatsDSHashEntry *entry); +static const char *ContextTypeToString(NodeTag type); +static void PublishMemoryContext(MemoryStatsEntry *memcxt_info, + int curr_id, MemoryContext context, + List *path, + MemoryContextCounters stat, + int num_contexts, int max_levels); +static List *compute_context_path(MemoryContext c, HTAB *context_id_lookup); +static void end_memorycontext_reporting(MemoryStatsDSHashEntry *entry, MemoryContext oldcontext, + HTAB *context_id_lookup); /* ---------- * The max bytes for showing identifiers of MemoryContext. @@ -89,7 +114,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, */ for (MemoryContext cur = context; cur != NULL; cur = cur->parent) { - MemoryContextId *entry; + MemoryStatsContextId *entry; bool found; entry = hash_search(context_id_lookup, &cur, HASH_FIND, &found); @@ -143,24 +168,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, else nulls[1] = true; - switch (context->type) - { - case T_AllocSetContext: - type = "AllocSet"; - break; - case T_GenerationContext: - type = "Generation"; - break; - case T_SlabContext: - type = "Slab"; - break; - case T_BumpContext: - type = "Bump"; - break; - default: - type = "???"; - break; - } + type = ContextTypeToString(context->type); values[2] = CStringGetTextDatum(type); values[3] = Int32GetDatum(list_length(path)); /* level */ @@ -175,6 +183,38 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, list_free(path); } +/* + * ContextTypeToString + * Returns a textual representation of a context type + * + * This should cover the same types as MemoryContextIsValid. + */ +const char * +ContextTypeToString(NodeTag type) +{ + const char *context_type; + + switch (type) + { + case T_AllocSetContext: + context_type = "AllocSet"; + break; + case T_GenerationContext: + context_type = "Generation"; + break; + case T_SlabContext: + context_type = "Slab"; + break; + case T_BumpContext: + context_type = "Bump"; + break; + default: + context_type = "???"; + break; + } + return context_type; +} + /* * pg_get_backend_memory_contexts * SQL SRF showing backend memory context. @@ -189,7 +229,7 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) HTAB *context_id_lookup; ctl.keysize = sizeof(MemoryContext); - ctl.entrysize = sizeof(MemoryContextId); + ctl.entrysize = sizeof(MemoryStatsContextId); ctl.hcxt = CurrentMemoryContext; context_id_lookup = hash_create("pg_get_backend_memory_contexts", @@ -216,7 +256,7 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) foreach_ptr(MemoryContextData, cur, contexts) { - MemoryContextId *entry; + MemoryStatsContextId *entry; bool found; /* @@ -224,8 +264,8 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) * PutMemoryContextsStatsTupleStore needs this to populate the "path" * column with the parent context_ids. */ - entry = (MemoryContextId *) hash_search(context_id_lookup, &cur, - HASH_ENTER, &found); + entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &cur, + HASH_ENTER, &found); entry->context_id = context_id++; Assert(!found); @@ -305,3 +345,748 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } + +/* + * pg_get_process_memory_contexts + * Signal a backend or an auxiliary process to send its memory contexts, + * wait for the results and display them. + * + * By default, only superusers or users with ROLE_PG_READ_ALL_STATS are allowed + * to signal a process to return the memory contexts. This is because allowing + * any users to issue this request at an unbounded rate would cause lots of + * requests to be sent, which can lead to denial of service. Additional roles + * can be permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to copy the memory context details + * to a dynamic shared memory space. + * + * We have defined a limit on DSA memory that could be allocated per process - + * if the process has more memory contexts than what can fit in the allocated + * size, the excess contexts are summarized and represented as cumulative total + * at the end of the buffer. + * + * After sending the signal, wait on a condition variable. The publishing + * backend, after copying the data to shared memory, sends signal on that + * condition variable. There is one condition variable per client process. + * Once the condition variable is signalled, check if the latest memory context + * information is available and display. + * + * If the publishing backend does not respond before the condition variable + * times out, which is set to MEMSTATS_WAIT_TIMEOUT, retry given that there is + * time left within the timeout specified by the user, before giving up and + * returning previously published statistics, if any. If no previous statistics + * exist, return NULL. + */ +#define MEMSTATS_WAIT_TIMEOUT 100 +Datum +pg_get_process_memory_contexts(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + bool summary = PG_GETARG_BOOL(1); + double timeout = PG_GETARG_FLOAT8(2); + PGPROC *proc; + ProcNumber procNumber = INVALID_PROC_NUMBER; + bool proc_is_aux = false; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + MemoryStatsEntry *memcxt_info; + MemoryStatsDSHashEntry *entry; + bool found; + char key[CLIENT_KEY_SIZE]; + + /* + * See if the process with given pid is a backend or an auxiliary process + * and remember the type for when we requery the process later. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + { + proc = AuxiliaryPidGetProc(pid); + proc_is_aux = true; + } + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; this is however not a problem and leave with a WARNING. + * See comment in pg_log_backend_memory_contexts for a discussion on this. + */ + if (proc == NULL) + { + /* + * This is just a warning so a loop-through-resultset will not abort + * if one backend terminated on its own during the run. + */ + ereport(WARNING, + errmsg("PID %d is not a PostgreSQL server process", pid)); + PG_RETURN_NULL(); + } + + InitMaterializedSRF(fcinfo, 0); + + procNumber = GetNumberFromPGProc(proc); + + /* + * Create a DSA to allocate memory for copying memory contexts statistics. + * Allocate the memory in the DSA and send dsa pointer to the server + * process for storing the context statistics. If number of contexts + * exceed a predefined limit(1MB), a cumulative total is stored for such + * contexts. + * + * The DSA is created once for the lifetime of the server, and only + * attached in subsequent calls. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", &found); + + /* + * The dsa pointers containing statistics for each client are stored in a + * dshash table. In addition to dsa pointer, each entry in this table also + * contains information about the statistics, condition variable for + * signalling between client and the server and miscellaneous data + * specific to a request. There is one entry per client request in the + * hash table. + */ + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", &memctx_dsh_params, &found); + + snprintf(key, sizeof(key), "%d", MyProcNumber); + + /* + * Check if the publishing process slot is empty and store this clients + * key i.e its procNumber. This informs the publishing process that it is + * supposed to write statistics in the hash entry corresponding to this + * client. + */ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + if (client_keys[procNumber] == -1) + client_keys[procNumber] = MyProcNumber; + else + { + ereport(WARNING, + errmsg("server process is processing previous request %d: %m", pid)); + LWLockRelease(client_keys_lock); + PG_RETURN_NULL(); + } + LWLockRelease(client_keys_lock); + + /* + * Insert an entry for this client in DSHASH table the first time this + * function is called. This entry is deleted when the process exits in + * before_shmem_exit call. + * + * dshash_find_or_insert locks the entry to prevent the publisher from + * reading before client has updated the entry. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + if (!found) + ConditionVariableInit(&entry->memcxt_cv); + + /* + * Allocate 1MB of memory for the backend to publish its statistics on + * every call to this function. The memory is freed at the end of the + * function. + */ + entry->memstats_dsa_pointer = + dsa_allocate0(MemoryStatsDsaArea, MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND); + entry->summary = summary; + dshash_release_lock(MemoryStatsDsHash, entry); + + /* + * Send a signal to a PostgreSQL process, informing it we want it to + * produce information about its memory contexts. + */ + if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, procNumber) < 0) + { + ereport(WARNING, + errmsg("could not send signal to process %d: %m", pid)); + PG_RETURN_NULL(); + } + + while (1) + { + + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + Assert(found); + + /* + * We expect to come out of sleep when the requested process has + * finished publishing the statistics, verified using the correct + * entry in the proc_id field. + * + * Make sure that the information belongs to pid we requested + * information for, Otherwise loop back and wait for the server + * process to finish publishing statistics. + * + */ + if (entry->proc_id == pid) + break; + + dshash_release_lock(MemoryStatsDsHash, entry); + + /* + * Recheck the state of the backend before sleeping on the condition + * variable to ensure the process is still alive. Only check the + * relevant process type based on the earlier PID check. + */ + if (proc_is_aux) + proc = AuxiliaryPidGetProc(pid); + else + proc = BackendPidGetProc(pid); + + /* + * The process ending during memory context processing is not an + * error. + */ + if (proc == NULL) + { + ereport(WARNING, + errmsg("PID %d is no longer a PostgreSQL server process", + pid)); + memstats_dsa_cleanup(entry); + PG_RETURN_NULL(); + } + + + /* + * Wait for the timeout as defined by the user. If no statistics are + * available within the allowed time then return NULL. The timer is + * defined in milliseconds since that's what the condition variable + * sleep uses. + */ + if (ConditionVariableTimedSleep(&entry->memcxt_cv, + (timeout * 1000), WAIT_EVENT_MEM_CXT_PUBLISH)) + { + /* Timeout has expired, return NULL */ + memstats_dsa_cleanup(entry); + PG_RETURN_NULL(); + } + } + + /* + * Backend has finished publishing the stats, project them. + */ + memcxt_info = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + +#define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 11 + for (int i = 0; i < entry->total_stats; i++) + { + ArrayType *path_array; + int path_length; + Datum values[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + bool nulls[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + Datum *path_datum = NULL; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + if (memcxt_info[i].name[0] != '\0') + { + values[0] = CStringGetTextDatum(memcxt_info[i].name); + } + else + nulls[0] = true; + + if (memcxt_info[i].ident[0] != '\0') + { + values[1] = CStringGetTextDatum(memcxt_info[i].ident); + } + else + nulls[1] = true; + + values[2] = CStringGetTextDatum(ContextTypeToString(memcxt_info[i].type)); + + path_length = memcxt_info[i].path_length; + path_datum = (Datum *) palloc(path_length * sizeof(Datum)); + if (memcxt_info[i].path[0] != 0) + { + for (int j = 0; j < path_length; j++) + path_datum[j] = Int32GetDatum(memcxt_info[i].path[j]); + path_array = construct_array_builtin(path_datum, path_length, INT4OID); + values[3] = PointerGetDatum(path_array); + } + else + nulls[3] = true; + + values[4] = Int32GetDatum(memcxt_info[i].levels); + values[5] = Int64GetDatum(memcxt_info[i].totalspace); + values[6] = Int64GetDatum(memcxt_info[i].nblocks); + values[7] = Int64GetDatum(memcxt_info[i].freespace); + values[8] = Int64GetDatum(memcxt_info[i].freechunks); + values[9] = Int64GetDatum(memcxt_info[i].totalspace - + memcxt_info[i].freespace); + values[10] = Int32GetDatum(memcxt_info[i].num_agg_stats); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + memstats_dsa_cleanup(entry); + dshash_release_lock(MemoryStatsDsHash, entry); + + ConditionVariableCancelSleep(); + + PG_RETURN_NULL(); +} + +static void +memstats_dsa_cleanup(MemoryStatsDSHashEntry *entry) +{ + Assert(MemoryStatsDsaArea != NULL); + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + entry->memstats_dsa_pointer = InvalidDsaPointer; + entry->proc_id = 0; +} +void +MemoryContextKeysShmemInit(void) +{ + bool found; + + client_keys = (int *) + ShmemInitStruct("MemoryContextKeys", + MemoryContextKeysShmemSize() + sizeof(LWLockPadded), &found); + client_keys_lock = (LWLock *) ((char *) client_keys + MemoryContextKeysShmemSize()); + + if (!found) + { + MemSet(client_keys, -1, MemoryContextKeysShmemSize()); + LWLockInitialize(client_keys_lock, LWTRANCHE_MEMORY_CONTEXT_KEYS); + } +} + +Size +MemoryContextKeysShmemSize(void) +{ + Size sz = 0; + Size TotalProcs = 0; + + TotalProcs = add_size(TotalProcs, NUM_AUXILIARY_PROCS); + TotalProcs = add_size(TotalProcs, MaxBackends); + sz = add_size(sz, mul_size(TotalProcs, sizeof(int))); + + return sz; +} + +/* + * HandleGetMemoryContextInterrupt + * Handle receipt of an interrupt indicating a request to publish memory + * contexts statistics. + * + * All the actual work is deferred to ProcessGetMemoryContextInterrupt() as + * this cannot be performed in a signal handler. + */ +void +HandleGetMemoryContextInterrupt(void) +{ + InterruptPending = true; + PublishMemoryContextPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + +/* + * ProcessGetMemoryContextInterrupt + * Generate information about memory contexts used by the process. + * + * Performs a breadth first search on the memory context tree, thus parents + * statistics are reported before their children in the monitoring function + * output. + * + * Statistics for all the processes are shared via the same dynamic shared + * area. Individual statistics are tracked independently in + * per-process DSA pointers. These pointers are stored in a dshash table with + * key as requesting clients ProcNumber. + * + * We calculate maximum number of context's statistics that can be displayed + * using a pre-determined limit for memory available per process for this + * utility and maximum size of statistics for each context. The remaining + * context statistics if any are captured as a cumulative total at the end of + * individual context's statistics. + * + * If summary is true, we capture the level 1 and level 2 contexts + * statistics. For that we traverse the memory context tree recursively in + * depth first search manner to cover all the children of a parent context, to + * be able to display a cumulative total of memory consumption by a parent at + * level 2 and all its children. + */ +void +ProcessGetMemoryContextInterrupt(void) +{ + List *contexts; + HASHCTL ctl; + HTAB *context_id_lookup; + int context_id = 0; + MemoryStatsEntry *meminfo; + bool summary = false; + MemoryContextCounters stat; + int num_individual_stats = 0; + bool found; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + int clientProcNumber; + MemoryContext memstats_ctx = NULL; + MemoryContext oldcontext = NULL; + + PublishMemoryContextPending = false; + + /* + * Create a new memory context which is not a part of TopMemoryContext + * tree. This context is used to allocate all memory in this function. + * This helps in keeping the memory allocation in this function to report + * memory consumption statistics separate. So that it does not affect the + * output of this function. + */ + memstats_ctx = AllocSetContextCreate((MemoryContext) NULL, "publish_memory_context_statistics", + ALLOCSET_SMALL_SIZES); + oldcontext = MemoryContextSwitchTo(memstats_ctx); + + /* + * The hash table is used for constructing "path" column of the view, + * similar to its local backend counterpart. + */ + ctl.keysize = sizeof(MemoryContext); + ctl.entrysize = sizeof(MemoryStatsContextId); + ctl.hcxt = CurrentMemoryContext; + + context_id_lookup = hash_create("pg_get_remote_backend_memory_contexts", + 256, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + /* List of contexts to process in the next round - start at the top. */ + contexts = list_make1(TopMemoryContext); + + /* + * If DSA exists, created by another process requesting statistics, attach + * to it. We expect the client process to create required DSA and Dshash + * table. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", &found); + + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", &memctx_dsh_params, &found); + + /* Retreive the client key fo publishing statistics */ + LWLockAcquire(client_keys_lock, LW_SHARED); + Assert(client_keys[MyProcNumber] != -1); + clientProcNumber = client_keys[MyProcNumber]; + LWLockRelease(client_keys_lock); + + snprintf(key, CLIENT_KEY_SIZE, "%d", clientProcNumber); + + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + summary = entry->summary; + + /* + * The entry lock is held by dshash_find_or_insert to protect writes to + * process specific memory. Two different processes publishing statistics + * do not block each other. + */ + entry->proc_id = MyProcPid; + + /* Should be allocated by a client backend that is requesting statistics */ + Assert(entry->memstats_dsa_pointer != InvalidDsaPointer); + meminfo = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + if (summary) + { + int cxt_id = 0; + List *path = NIL; + MemoryStatsContextId *contextid_entry; + + /* Copy TopMemoryContext statistics to DSA */ + memset(&stat, 0, sizeof(stat)); + (*TopMemoryContext->methods->stats) (TopMemoryContext, NULL, NULL, + &stat, true); + path = lcons_int(1, path); + PublishMemoryContext(meminfo, cxt_id, TopMemoryContext, path, stat, + 1, 100); + + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &TopMemoryContext, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = cxt_id + 1; + + /* + * Copy statistics for each of TopMemoryContexts children. This + * includes statistics of at most 100 children per node, with each + * child node limited to a depth of 100 in its subtree. + */ + for (MemoryContext c = TopMemoryContext->firstchild; c != NULL; + c = c->nextchild) + { + MemoryContextCounters grand_totals; + int num_contexts = 0; + + path = NIL; + memset(&grand_totals, 0, sizeof(grand_totals)); + + cxt_id++; + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &c, + HASH_ENTER, &found); + Assert(!found); + contextid_entry->context_id = cxt_id + 1; + + MemoryContextStatsInternal(c, 1, 100, 100, &grand_totals, + PRINT_STATS_NONE, &num_contexts); + + path = compute_context_path(c, context_id_lookup); + + PublishMemoryContext(meminfo, cxt_id, c, path, + grand_totals, num_contexts, 100); + } + entry->total_stats = cxt_id + 1; + + /* Notify waiting backends and return */ + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + return; + } + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + MemoryStatsContextId *contextid_entry; + + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &cur, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = context_id + 1; + + /* + * Figure out the transient context_id of this context and each of its + * ancestors, to compute a path for this context. + */ + path = compute_context_path(cur, context_id_lookup); + + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + /* Account for saving one statistics slot for cumulative reporting */ + if (context_id < (MAX_MEMORY_CONTEXT_STATS_NUM - 1)) + { + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1, 100); + } + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].totalspace += stat.totalspace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].nblocks += stat.nblocks; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freespace += stat.freespace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit per process is reached, write aggregate of the + * remaining statistics. + * + * We can store contexts from 0 to max_stats - 1. When context_id is + * greater than max_stats, we stop reporting individual statistics + * when context_id equals max_stats - 2. As we use max_stats - 1 array + * slot for reporting cumulative statistics or "Remaining Totals". + */ + if (context_id == (MAX_MEMORY_CONTEXT_STATS_NUM - 2)) + { + int namelen = strlen("Remaining Totals"); + + num_individual_stats = context_id + 1; + strlcpy(meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].name, "Remaining Totals", namelen + 1); + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].ident[0] = '\0'; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].path[0] = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].type = 0; + } + context_id++; + + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + contexts = lappend(contexts, c); + } + + /* + * Statistics are not aggregated, i.e individual statistics reported when + * context_id <= max_stats. + */ + if (context_id <= MAX_MEMORY_CONTEXT_STATS_NUM) + { + entry->total_stats = context_id; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = 1; + } + /* Report number of aggregated memory contexts */ + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = context_id - + num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for cumulative + * statistics. + */ + entry->total_stats = num_individual_stats + 1; + } + + /* Notify waiting backends and return */ + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); +} + +/* + * Update timestamp and signal all the waiting client backends after copying + * all the statistics. + */ +static void +end_memorycontext_reporting(MemoryStatsDSHashEntry *entry, MemoryContext oldcontext, HTAB *context_id_lookup) +{ + MemoryContext curr_ctx = CurrentMemoryContext; + + dshash_release_lock(MemoryStatsDsHash, entry); + ConditionVariableBroadcast(&entry->memcxt_cv); + + /* + * Empty this processes slot, so other clients can request memory + * statistics + */ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + client_keys[MyProcNumber] = -1; + LWLockRelease(client_keys_lock); + + hash_destroy(context_id_lookup); + MemoryContextSwitchTo(oldcontext); + MemoryContextReset(curr_ctx); +} + +/* + * compute_context_path + * + * Append the transient context_id of this context and each of its ancestors + * to a list, in order to compute a path. + */ +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + MemoryContext cur_context; + + for (cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryStatsContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + elog(ERROR, "hash table corrupted, can't construct path value"); + + path = lcons_int(cur_entry->context_id, path); + } + + return path; +} + +/* + * PublishMemoryContext + * + * Copy the memory context statistics of a single context to a DSA memory + */ +static void +PublishMemoryContext(MemoryStatsEntry *memcxt_info, int curr_id, + MemoryContext context, List *path, + MemoryContextCounters stat, int num_contexts, int max_levels) +{ + const char *ident = context->ident; + const char *name = context->name; + + /* + * To be consistent with logging output, we label dynahash contexts with + * just the hash table name as with MemoryContextStatsPrint(). + */ + if (context->ident && strncmp(context->name, "dynahash", 8) == 0) + { + name = context->ident; + ident = NULL; + } + + if (name != NULL) + { + int namelen = strlen(name); + + if (strlen(name) >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + namelen = pg_mbcliplen(name, namelen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].name, name, namelen + 1); + } + else + /* Clearing the array */ + memcxt_info[curr_id].name[0] = '\0'; + + /* Trim and copy the identifier if it is not set to NULL */ + if (ident != NULL) + { + int idlen = strlen(context->ident); + + /* + * Some identifiers such as SQL query string can be very long, + * truncate oversize identifiers. + */ + if (idlen >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + idlen = pg_mbcliplen(ident, idlen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].ident, ident, idlen + 1); + } + else + memcxt_info[curr_id].ident[0] = '\0'; + + /* Allocate DSA memory for storing path information */ + if (path == NIL) + memcxt_info[curr_id].path[0] = 0; + else + { + int levels = Min(list_length(path), max_levels); + + memcxt_info[curr_id].path_length = levels; + memcxt_info[curr_id].levels = list_length(path); + + foreach_int(i, path) + { + memcxt_info[curr_id].path[foreach_current_index(i)] = i; + if (--levels == 0) + break; + } + } + memcxt_info[curr_id].type = context->type; + memcxt_info[curr_id].totalspace = stat.totalspace; + memcxt_info[curr_id].nblocks = stat.nblocks; + memcxt_info[curr_id].freespace = stat.freespace; + memcxt_info[curr_id].freechunks = stat.freechunks; + memcxt_info[curr_id].num_agg_stats = num_contexts; +} + +void +AtProcExit_memstats_cleanup(int code, Datum arg) +{ + int idx = MyProcNumber; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + bool found; + + if (MemoryStatsDsHash != NULL) + { + snprintf(key, CLIENT_KEY_SIZE, "%d", idx); + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + if (found) + { + if (MemoryStatsDsaArea != NULL && + DsaPointerIsValid(entry->memstats_dsa_pointer)) + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + } + dshash_delete_entry(MemoryStatsDsHash, entry); + } +} diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c index 97c2ac1faf9..ab768a7a91f 100644 --- a/src/backend/utils/adt/pg_locale.c +++ b/src/backend/utils/adt/pg_locale.c @@ -45,7 +45,6 @@ #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/pg_locale.h" -#include "utils/relcache.h" #include "utils/syscache.h" #ifdef WIN32 diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index d31cb45a058..92b0446b80c 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -39,6 +39,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t PublishMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index 641e535a73c..fb3f2d21fa0 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -662,6 +662,13 @@ BaseInit(void) * drop ephemeral slots, which in turn triggers stats reporting. */ ReplicationSlotInitialize(); + + /* + * The before shmem exit callback frees the DSA memory occupied by the + * latest memory context statistics that could be published by this proc + * if requested. + */ + before_shmem_exit(AtProcExit_memstats_cleanup, 0); } diff --git a/src/backend/utils/mb/mbutils.c b/src/backend/utils/mb/mbutils.c index 886ecbad871..308016d7763 100644 --- a/src/backend/utils/mb/mbutils.c +++ b/src/backend/utils/mb/mbutils.c @@ -39,7 +39,6 @@ #include "mb/pg_wchar.h" #include "utils/fmgrprotos.h" #include "utils/memutils.h" -#include "utils/relcache.h" #include "varatt.h" /* diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 47fd774c7d2..3a5422c7273 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -40,6 +40,7 @@ #include "mb/pg_wchar.h" #include "miscadmin.h" +#include "utils/hsearch.h" #include "utils/memdebug.h" #include "utils/memutils.h" #include "utils/memutils_internal.h" @@ -176,10 +177,6 @@ MemoryContext PortalContext = NULL; static void MemoryContextDeleteOnly(MemoryContext context); static void MemoryContextCallResetCallbacks(MemoryContext context); -static void MemoryContextStatsInternal(MemoryContext context, int level, - int max_level, int max_children, - MemoryContextCounters *totals, - bool print_to_stderr); static void MemoryContextStatsPrint(MemoryContext context, void *passthru, const char *stats_string, bool print_to_stderr); @@ -877,11 +874,19 @@ MemoryContextStatsDetail(MemoryContext context, bool print_to_stderr) { MemoryContextCounters grand_totals; + int num_contexts; + PrintDestination print_location; memset(&grand_totals, 0, sizeof(grand_totals)); + if (print_to_stderr) + print_location = PRINT_STATS_TO_STDERR; + else + print_location = PRINT_STATS_TO_LOGS; + + /* num_contexts report number of contexts aggregated in the output */ MemoryContextStatsInternal(context, 1, max_level, max_children, - &grand_totals, print_to_stderr); + &grand_totals, print_location, &num_contexts); if (print_to_stderr) fprintf(stderr, @@ -916,13 +921,14 @@ MemoryContextStatsDetail(MemoryContext context, * One recursion level for MemoryContextStats * * Print stats for this context if possible, but in any case accumulate counts - * into *totals (if not NULL). + * into *totals (if not NULL). The callers should make sure that print_location + * is set to PRINT_STATS_TO_STDERR or PRINT_STATS_TO_LOGS or PRINT_STATS_NONE. */ -static void +void MemoryContextStatsInternal(MemoryContext context, int level, int max_level, int max_children, MemoryContextCounters *totals, - bool print_to_stderr) + PrintDestination print_location, int *num_contexts) { MemoryContext child; int ichild; @@ -930,10 +936,39 @@ MemoryContextStatsInternal(MemoryContext context, int level, Assert(MemoryContextIsValid(context)); /* Examine the context itself */ - context->methods->stats(context, - MemoryContextStatsPrint, - &level, - totals, print_to_stderr); + switch (print_location) + { + case PRINT_STATS_TO_STDERR: + context->methods->stats(context, + MemoryContextStatsPrint, + &level, + totals, true); + break; + + case PRINT_STATS_TO_LOGS: + context->methods->stats(context, + MemoryContextStatsPrint, + &level, + totals, false); + break; + + case PRINT_STATS_NONE: + + /* + * Do not print the statistics if print_location is + * PRINT_STATS_NONE, only compute totals. This is used in + * reporting of memory context statistics via a sql function. Last + * parameter is not relevant. + */ + context->methods->stats(context, + NULL, + NULL, + totals, false); + break; + } + + /* Increment the context count for each of the recursive call */ + *num_contexts = *num_contexts + 1; /* * Examine children. @@ -953,7 +988,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, MemoryContextStatsInternal(child, level + 1, max_level, max_children, totals, - print_to_stderr); + print_location, num_contexts); } } @@ -972,7 +1007,13 @@ MemoryContextStatsInternal(MemoryContext context, int level, child = MemoryContextTraverseNext(child, context); } - if (print_to_stderr) + /* + * Add the count of children contexts which are traversed in the + * non-recursive manner. + */ + *num_contexts = *num_contexts + ichild; + + if (print_location == PRINT_STATS_TO_STDERR) { for (int i = 0; i < level; i++) fprintf(stderr, " "); @@ -985,7 +1026,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, local_totals.freechunks, local_totals.totalspace - local_totals.freespace); } - else + else if (print_location == PRINT_STATS_TO_LOGS) ereport(LOG_SERVER_ONLY, (errhidestmt(true), errhidecontext(true), diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 118d6da1ace..3d6f42606fd 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8597,6 +8597,16 @@ prorettype => 'bool', proargtypes => 'int4', prosrc => 'pg_log_backend_memory_contexts' }, +# publishing memory contexts of the specified postgres process +{ oid => '2173', descr => 'publish memory contexts of the specified backend', + proname => 'pg_get_process_memory_contexts', provolatile => 'v', + prorows => '100', proretset => 't', proparallel => 'r', + prorettype => 'record', proargtypes => 'int4 bool float8', + proallargtypes => '{int4,bool,float8,text,text,text,_int4,int4,int8,int8,int8,int8,int8,int4}', + proargmodes => '{i,i,i,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{pid, summary, timeout, name, ident, type, path, level, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes, num_agg_contexts}', + prosrc => 'pg_get_process_memory_contexts' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index 1bef98471c3..1e59a7f910f 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -96,6 +96,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t PublishMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h index 208d2e3a8ed..72ace053e9d 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -135,3 +135,4 @@ PG_LWLOCKTRANCHE(SUBTRANS_SLRU, SubtransSLRU) PG_LWLOCKTRANCHE(XACT_SLRU, XactSLRU) PG_LWLOCKTRANCHE(PARALLEL_VACUUM_DSA, ParallelVacuumDSA) PG_LWLOCKTRANCHE(AIO_URING_COMPLETION, AioUringCompletion) +PG_LWLOCKTRANCHE(MEMORY_CONTEXT_KEYS, MemoryContextReportingKeys) diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index afeeb1ca019..345d5a0ecb1 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -35,6 +35,7 @@ typedef enum PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to send the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ /* Recovery conflict reasons */ diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 8abc26abce2..01835d56021 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -18,7 +18,10 @@ #define MEMUTILS_H #include "nodes/memnodes.h" - +#include "storage/condition_variable.h" +#include "storage/lmgr.h" +#include "utils/dsa.h" +#include "lib/dshash.h" /* * MaxAllocSize, MaxAllocHugeSize @@ -48,6 +51,23 @@ #define AllocHugeSizeIsValid(size) ((Size) (size) <= MaxAllocHugeSize) +/* + * Memory Context reporting size limits. + */ + +/* Max length of context name and ident */ +#define MEMORY_CONTEXT_IDENT_SHMEM_SIZE 64 +/* Maximum size (in bytes) of DSA area per process */ +#define MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND ((size_t) (1 * 1024 * 1024)) + +/* + * Maximum size per context. Actual size may be lower as this assumes the worst + * case of deepest path and longest identifiers (name and ident, thus the + * multiplication by 2). The path depth is limited to 100 like for memory + * context logging. + */ +#define MAX_MEMORY_CONTEXT_STATS_SIZE (sizeof(MemoryStatsEntry)) +#define MAX_MEMORY_CONTEXT_STATS_NUM MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND / MAX_MEMORY_CONTEXT_STATS_SIZE /* * Standard top-level memory contexts. @@ -319,4 +339,74 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) return true; } +/* Dynamic shared memory state for statistics per context */ +typedef struct MemoryStatsEntry +{ + char name[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + char ident[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + int path[100]; + NodeTag type; + int path_length; + int levels; + int64 totalspace; + int64 nblocks; + int64 freespace; + int64 freechunks; + int num_agg_stats; +} MemoryStatsEntry; + +/* + * Per backend dynamic shared hash entry for memory context statistics + * reporting. + */ +typedef struct MemoryStatsDSHashEntry +{ + char key[64]; + ConditionVariable memcxt_cv; + int proc_id; + int total_stats; + bool summary; + dsa_pointer memstats_dsa_pointer; + TimestampTz stats_timestamp; +} MemoryStatsDSHashEntry; + +static const dshash_parameters memctx_dsh_params = { + offsetof(MemoryStatsDSHashEntry, memcxt_cv), + sizeof(MemoryStatsDSHashEntry), + dshash_strcmp, + dshash_strhash, + dshash_strcpy +}; + +/* + * Used for storage of transient identifiers for pg_get_backend_memory_contexts + */ +typedef struct MemoryStatsContextId +{ + MemoryContext context; + int context_id; +} MemoryStatsContextId; + +/* + * This is passed to MemoryContextStatsInternal to determine whether + * to print context statistics or not and where to print them logs or + * stderr. + */ +typedef enum PrintDestination +{ + PRINT_STATS_TO_STDERR = 0, + PRINT_STATS_TO_LOGS, + PRINT_STATS_NONE +} PrintDestination; + +extern void ProcessGetMemoryContextInterrupt(void); +extern void HandleGetMemoryContextInterrupt(void); +extern void MemoryContextKeysShmemInit(void); +extern Size MemoryContextKeysShmemSize(void); +extern void MemoryContextStatsInternal(MemoryContext context, int level, + int max_level, int max_children, + MemoryContextCounters *totals, + PrintDestination print_location, + int *num_contexts); +extern void AtProcExit_memstats_cleanup(int code, Datum arg); #endif /* MEMUTILS_H */ diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 83228cfca29..ae17d028ed3 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -232,3 +232,22 @@ select * from pg_timezone_abbrevs where abbrev = 'LMT'; LMT | @ 7 hours 52 mins 58 secs ago | f (1 row) +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; +NOTICE: (AllocSet,TopMemoryContext,) +NOTICE: (AllocSet,TopMemoryContext,) diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 66179f026b3..d0917b6868e 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -101,3 +101,21 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs; -- One specific case we can check without much fear of breakage -- is the historical local-mean-time value used for America/Los_Angeles. select * from pg_timezone_abbrevs where abbrev = 'LMT'; + +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index e6f2e93b2d6..d2399465df6 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1680,6 +1680,9 @@ MemoryContextData MemoryContextId MemoryContextMethodID MemoryContextMethods +MemoryStatsContextId +MemoryStatsEntry +MemoryStatsDSHashEntry MemoryStatsPrintFunc MergeAction MergeActionState -- 2.34.1 ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-08-12 00:34 torikoshia <[email protected]> parent: Rahila Syed <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: torikoshia @ 2025-08-12 00:34 UTC (permalink / raw) To: Rahila Syed <[email protected]>; +Cc: pgsql-hackers; Daniel Gustafsson <[email protected]> On 2025-08-08 18:26, Rahila Syed wrote: Hi, thanks for working on this again. > Hi, > > CFbot indicated that the patch requires a rebase, so I've attached an > updated version. Here are some comments and questions for v32 patch: > --- a/doc/src/sgml/func/func-admin.sgml > +++ b/doc/src/sgml/func/func-admin.sgml > @@ -251,6 +251,137 @@ > <literal>false</literal> is returned. > </para></entry> > </row> > + > + <row> > + <entry role="func_table_entry"><para role="func_signature"> > + <indexterm> > + <primary>pg_get_process_memory_contexts</primary> This function is added at the end of Table "9.96. Server Signaling Functions", but since pg_get_process_memory_contexts outputs essentially the same information as pg_log_backend_memory_contexts, it might be better to place them next to each other in the table. > + <parameter>stats_timestamp</parameter> <type>timestamptz</type> ) > +typedef struct MemoryStatsDSHashEntry > +{ > + char key[64]; > + ConditionVariable memcxt_cv; > + int proc_id; > + int total_stats; > + bool summary; > + dsa_pointer memstats_dsa_pointer; > + TimestampTz stats_timestamp; > +} MemoryStatsDSHashEntry; stats_timestamp appears only in the two places below in the patch, but it does not seem to be actually output. Is this column unnecessary? =# select * from pg_get_process_memory_contexts(pg_backend_pid(), true, 10); -[ RECORD 1 ]----+----------------------------- name | TopMemoryContext ident | [NULL] type | AllocSet path | {1} level | 1 total_bytes | 222400 total_nblocks | 8 free_bytes | 4776 free_chunks | 8 used_bytes | 217624 num_agg_contexts | 1 Specifying 0 for timeout causes a crash: =# select * from pg_get_process_memory_contexts(74526, true, 0); (0 rows) =# select 1; WARNING: terminating connection because of crash of another server process DETAIL: The postmaster has commanded this server process to roll back the current transaction and exit, because another server process exited abnormally and possibly corrupted shared memory. HINT: In a moment you should be able to reconnect to the database and repeat your command. server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. Should 0 be handled safely and treated as “no timeout”, or rejected as an error? Similarly, specifying a negative value for timeout still works: =# select * from pg_get_process_memory_contexts(30590, true, -10); It might be better to reject negative values similar to pg_terminate_backend(). > context_id_lookup = > hash_create("pg_get_remote_backend_memory_contexts", > + /* Retreive the client key fo publishing statistics */ fo -> for? > + * If the publishing backend does not respond before the condition > variable > + * times out, which is set to MEMSTATS_WAIT_TIMEOUT, retry given that > there is > + * time left within the timeout specified by the user, before giving > up and > + * returning previously published statistics, if any. If no previous > statistics > + * exist, return NULL. > + */ > +#define MEMSTATS_WAIT_TIMEOUT 100 MEMSTATS_WAIT_TIMEOUT is defined, but it doesn’t seem to be used. -- Regards, -- Atsushi Torikoshi Seconded from NTT DATA Japan Corporation to SRA OSS K.K. ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-08-13 22:35 Rahila Syed <[email protected]> parent: torikoshia <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Rahila Syed @ 2025-08-13 22:35 UTC (permalink / raw) To: torikoshia <[email protected]>; +Cc: pgsql-hackers; Daniel Gustafsson <[email protected]> Hi Torikoshia, Thank you for reviewing the patch. > > This function is added at the end of Table "9.96. Server Signaling > Functions", but since pg_get_process_memory_contexts outputs essentially > the same information as pg_log_backend_memory_contexts, it might be > better to place them next to each other in the table. > > The idea was to place the new addition at the end of the table instead of in the middle. I’m fine with putting them together, though. I’ll do that in the next version unless there’s a reason not to. > > > + <parameter>stats_timestamp</parameter> <type>timestamptz</type> ) > > > +typedef struct MemoryStatsDSHashEntry > > +{ > > + char key[64]; > > + ConditionVariable memcxt_cv; > > + int proc_id; > > + int total_stats; > > + bool summary; > > + dsa_pointer memstats_dsa_pointer; > > + TimestampTz stats_timestamp; > > +} MemoryStatsDSHashEntry; > > stats_timestamp appears only in the two places below in the patch, but > it does not seem to be actually output. > Is this column unnecessary? > > Thank you for pointing this out. This is removed in the attached patch, as it was a remnant from the previous design. As old statistics are discarded in the new design, a timestamp field is not needed anymore. > > Specifying 0 for timeout causes a crash: > Should 0 be handled safely and treated as “no timeout”, or rejected as > an error? > Good catch. The crash has been resolved in the attached patch. It was caused by a missing ConditionVariableCancelSleep() call when exiting without statistics due to a timeout value of 0. A 0 timeout means that statistics should only be retrieved if they are immediately available, without waiting. We could exit with a warning/error saying "too low timeout", but I think it's worthwhile to try fetching the statistics if possible. Similarly, specifying a negative value for timeout still works: > > =# select * from pg_get_process_memory_contexts(30590, true, -10); > > It might be better to reject negative values similar to > pg_terminate_backend(). > > > Fixed as suggested by you in the attached patch. Currently, negative values are interpreted as an indefinite wait for statistics. This could cause the client to hang if the server process exits without providing statistics. To avoid this, it would be better to exit after displaying a warning when the user specifies negative timeouts. > > > + /* Retreive the client key fo publishing statistics */ > > fo -> for? > Fixed. > + */ > > +#define MEMSTATS_WAIT_TIMEOUT 100 > > MEMSTATS_WAIT_TIMEOUT is defined, but it doesn’t seem to be used. > > This is removed now as it was a leftover from the previous design. The attached patch also fixes an assertion failure I observed when a client times out before the last requested process can publish its statistics. A client frees the memory reserved for storing the statistics when it exits the function after timeout. Since a server process was notified, it might attempt to read the same client entry and access the dsa memory reserved for statistics resulting in the assertion failure. I resolved this by including a check for this scenario and then exiting the handler function accordingly. Thank you, Rahila Syed Attachments: [application/octet-stream] v33-0001-Add-pg_get_process_memory_context-function.patch (61.1K, ../../CAH2L28sF02nZj5vEL49EsTx+TfYpB3dFgShgn0=W1V-uFZjejw@mail.gmail.com/3-v33-0001-Add-pg_get_process_memory_context-function.patch) download | inline diff: From f37e942fa7732795dc37ef5ef3759622f1b71eef Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Mon, 30 Jun 2025 12:11:00 +0530 Subject: [PATCH] Add pg_get_process_memory_context function This adds a function for retrieving memory context statistics and information from backends as well as auxiliary processes. The intended usecase is cluster debugging when under memory pressure or unanticipated memory usage characteristics. When calling the function it sends a signal to the specified process to submit statistics regarding its memory contexts into dynamic shared memory. Each memory context is returned in detail, followed by a cumulative total in case the number of contexts exceed the max allocated amount of shared memory. Each process is limited to use at most 1Mb memory for this. A summary can also be explicitly requested by the user, this will return the TopMemoryContext and a cumulative total of all lower contexts. In order to not block on busy processes the caller specifies the number of seconds during which to retry before timing out. In the case where no statistics are published within the set timeout, NULL is returned. --- doc/src/sgml/func/func-admin.sgml | 164 ++++ src/backend/catalog/system_views.sql | 5 + src/backend/postmaster/autovacuum.c | 4 + src/backend/postmaster/checkpointer.c | 4 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 4 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 4 + src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/storage/lmgr/proc.c | 1 + src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 2 + src/backend/utils/adt/mcxtfuncs.c | 887 +++++++++++++++++- src/backend/utils/adt/pg_locale.c | 1 - src/backend/utils/init/globals.c | 1 + src/backend/utils/init/postinit.c | 7 + src/backend/utils/mb/mbutils.c | 1 - src/backend/utils/mmgr/mcxt.c | 71 +- src/include/catalog/pg_proc.dat | 10 + src/include/miscadmin.h | 1 + src/include/storage/lwlocklist.h | 1 + src/include/storage/procsignal.h | 1 + src/include/utils/memutils.h | 92 +- src/test/regress/expected/sysviews.out | 19 + src/test/regress/sql/sysviews.sql | 18 + src/tools/pgindent/typedefs.list | 3 + 27 files changed, 1277 insertions(+), 41 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 446fdfe56f4..689e9231a7e 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -251,6 +251,137 @@ <literal>false</literal> is returned. </para></entry> </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_get_process_memory_contexts</primary> + </indexterm> + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type>, <parameter>summary</parameter> <type>boolean</type>, <parameter>timeout</parameter> <type>float</type> ) + <returnvalue>setof record</returnvalue> + ( <parameter>name</parameter> <type>text</type>, + <parameter>ident</parameter> <type>text</type>, + <parameter>type</parameter> <type>text</type>, + <parameter>path</parameter> <type>integer[]</type>, + <parameter>level</parameter> <type>integer</type>, + <parameter>total_bytes</parameter> <type>bigint</type>, + <parameter>total_nblocks</parameter> <type>bigint</type>, + <parameter>free_bytes</parameter> <type>bigint</type>, + <parameter>free_chunks</parameter> <type>bigint</type>, + <parameter>used_bytes</parameter> <type>bigint</type>, + <parameter>num_agg_contexts</parameter> <type>integer</type>, + <parameter>stats_timestamp</parameter> <type>timestamptz</type> ) + </para> + <para> + This function handles requests to display the memory contexts of a + <productname>PostgreSQL</productname> process with the specified + process ID. The function can be used to send requests to backends as + well as <glossterm linkend="glossary-auxiliary-proc">auxiliary processes</glossterm>. + </para> + <para> + The returned record contains extended statistics per each memory + context: + <itemizedlist spacing="compact"> + <listitem> + <para> + <parameter>name</parameter> - The name of the memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>ident</parameter> - Memory context ID (if any). + </para> + </listitem> + <listitem> + <para> + <parameter>type</parameter> - The type of memory context, possible + values are: AllocSet, Generation, Slab and Bump. + </para> + </listitem> + <listitem> + <para> + <parameter>path</parameter> - Memory contexts are organized in a + tree model with TopMemoryContext as the root, and all other memory + contexts as nodes in the tree. The <parameter>path</parameter> + displays the path from the root to the current memory context. The + path is limited to 100 children per node, which each node limited + to a max depth of 100, to preserve memory during reporting. The + printed path will also be limited to 100 nodes counting from the + TopMemoryContext. + </para> + </listitem> + <listitem> + <para> + <parameter>level</parameter> - The level in the tree of the current + memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_bytes</parameter> - The total number of bytes + allocated to this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_nblocks</parameter> - The total number of blocks + used for the allocated memory. + </para> + </listitem> + <listitem> + <para> + <parameter>free_bytes</parameter> - The amount of free memory in + this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>free_chunks</parameter> - The number of chunks that + <parameter>free_bytes</parameter> corresponds to. + </para> + </listitem> + <listitem> + <para> + <parameter>used_bytes</parameter> - The total number of bytes + currently occupied. + </para> + </listitem> + <listitem> + <para> + <parameter>num_agg_contexts</parameter> - The number of memory + contexts aggregated in the displayed statistics. + </para> + </listitem> + </itemizedlist> + </para> + <para> + When <parameter>summary</parameter> is <literal>true</literal>, statistics + for memory contexts at levels 1 and 2 are displayed, with level 1 + representing the root node (i.e., <literal>TopMemoryContext</literal>). + Statistics for contexts on level 2 and below are aggregates of all + child contexts' statistics, where <literal>num_agg_contexts</literal> + indicate the number aggregated child contexts. When + <parameter>summary</parameter> is <literal>false</literal>, + <literal>the num_agg_contexts</literal> value is <literal>1</literal>, + indicating that individual statistics are being displayed. + </para> + <para> + Busy processes can delay reporting memory context statistics, + <parameter>timeout</parameter> specifies the number of seconds + to wait for updated statistics. <parameter>timeout</parameter> can be + specified in fractions of a second. + </para> + <para> + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context + statistics are aggregated and a cumulative total is displayed. The + <literal>num_agg_contexts</literal> column indicates the number of + contexts aggregated in the displayed statistics. When + <literal>num_agg_contexts</literal> is <literal>1</literal> it means + that the context statistics are displayed separately. + </para></entry> + </row> </tbody> </tgroup> </table> @@ -302,6 +433,39 @@ LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 because it may generate a large number of log messages. </para> + <para> + <function>pg_get_process_memory_contexts</function> can be used to request + memory contexts statistics of any <productname>PostgreSQL</productname> + process. For example: +<programlisting> +postgres=# SELECT * FROM pg_get_process_memory_contexts( + (SELECT pid FROM pg_stat_activity + WHERE backend_type = 'checkpointer'), + false, 0.5) LIMIT 1; +-[ RECORD 1 ]----+------------------------------ +name | TopMemoryContext +ident | +type | AllocSet +path | {1} +level | 1 +total_bytes | 90304 +total_nblocks | 3 +free_bytes | 2880 +free_chunks | 1 +used_bytes | 87424 +num_agg_contexts | 1 +</programlisting> + <note> + <para> + While <function>pg_get_process_memory_contexts</function> can be used to + query memory contexts of the local backend, + <structname>pg_backend_memory_contexts</structname> + (see <xref linkend="view-pg-backend-memory-contexts"/> for more details) + will be less resource intensive when only the local backend is of interest. + </para> + </note> + </para> + </sect2> <sect2 id="functions-admin-backup"> diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index 1b3c5a55882..022586d6a55 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -682,6 +682,11 @@ GRANT SELECT ON pg_backend_memory_contexts TO pg_read_all_stats; REVOKE EXECUTE ON FUNCTION pg_get_backend_memory_contexts() FROM PUBLIC; GRANT EXECUTE ON FUNCTION pg_get_backend_memory_contexts() TO pg_read_all_stats; +REVOKE EXECUTE ON FUNCTION + pg_get_process_memory_contexts(integer, boolean, float) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION + pg_get_process_memory_contexts(integer, boolean, float) TO pg_read_all_stats; + -- Statistics views CREATE VIEW pg_stat_all_tables AS diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index ff96b36d710..3875f76564d 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -791,6 +791,10 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index e84e8663e96..5b3e08805bf 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -679,6 +679,10 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index 0ae9bf906ec..f24f574e748 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -48,6 +48,10 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index 78e39e5f866..ac97a39447c 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -867,6 +867,10 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index 27e86cf393f..7149a67fcbc 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -192,6 +192,10 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index 777c9a8d555..5d14684f6b2 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -879,6 +879,10 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index 2fa045e6b0f..fe3d32e40b0 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -51,6 +51,7 @@ #include "storage/sinvaladt.h" #include "utils/guc.h" #include "utils/injection_point.h" +#include "utils/memutils.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -150,6 +151,7 @@ CalculateShmemSize(int *num_semaphores) size = add_size(size, InjectionPointShmemSize()); size = add_size(size, SlotSyncShmemSize()); size = add_size(size, AioShmemSize()); + size = add_size(size, MemoryContextKeysShmemSize() + sizeof(LWLockPadded)); /* include additional requested shmem from preload libraries */ size = add_size(size, total_addin_request); @@ -343,6 +345,7 @@ CreateOrAttachShmemStructs(void) WaitEventCustomShmemInit(); InjectionPointShmemInit(); AioShmemInit(); + MemoryContextKeysShmemInit(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index 087821311cc..8963285cc12 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -691,6 +691,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_GET_MEMORY_CONTEXT)) + HandleGetMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index e9ef0fbfe32..f194e6b3dcc 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -50,6 +50,7 @@ #include "storage/procsignal.h" #include "storage/spin.h" #include "storage/standby.h" +#include "utils/memutils.h" #include "utils/timeout.h" #include "utils/timestamp.h" diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 0cecd464902..3933b6db607 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3534,6 +3534,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 0be307d2ca0..54f91a76a1b 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -161,6 +161,7 @@ WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." +MEM_CXT_PUBLISH "Waiting for a process to publish memory information." ABI_compatibility: @@ -406,6 +407,7 @@ SubtransSLRU "Waiting to access the sub-transaction SLRU cache." XactSLRU "Waiting to access the transaction status SLRU cache." ParallelVacuumDSA "Waiting for parallel vacuum dynamic shared memory allocation." AioUringCompletion "Waiting for another process to complete IO via io_uring." +MemoryContextReportingKeys "Waiting for another process to complete reading or writing the memory reporting keys." # No "ABI_compatibility" region here as WaitEventLWLock has its own C code. diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index fe6dce9cba3..ce507be3e85 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -15,13 +15,38 @@ #include "postgres.h" +#include "access/twophase.h" +#include "catalog/pg_authid_d.h" #include "funcapi.h" #include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "storage/dsm_registry.h" #include "storage/proc.h" #include "storage/procarray.h" +#include "utils/acl.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/memutils.h" +#include "utils/wait_event_types.h" + +#define CLIENT_KEY_SIZE 64 + +static LWLock *client_keys_lock = NULL; +static int *client_keys = NULL; +static dshash_table *MemoryStatsDsHash = NULL; +static dsa_area *MemoryStatsDsaArea = NULL; + +static void memstats_dsa_cleanup(MemoryStatsDSHashEntry *entry); +static const char *ContextTypeToString(NodeTag type); +static void PublishMemoryContext(MemoryStatsEntry *memcxt_info, + int curr_id, MemoryContext context, + List *path, + MemoryContextCounters stat, + int num_contexts, int max_levels); +static List *compute_context_path(MemoryContext c, HTAB *context_id_lookup); +static void end_memorycontext_reporting(MemoryStatsDSHashEntry *entry, MemoryContext oldcontext, + HTAB *context_id_lookup); /* ---------- * The max bytes for showing identifiers of MemoryContext. @@ -89,7 +114,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, */ for (MemoryContext cur = context; cur != NULL; cur = cur->parent) { - MemoryContextId *entry; + MemoryStatsContextId *entry; bool found; entry = hash_search(context_id_lookup, &cur, HASH_FIND, &found); @@ -143,24 +168,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, else nulls[1] = true; - switch (context->type) - { - case T_AllocSetContext: - type = "AllocSet"; - break; - case T_GenerationContext: - type = "Generation"; - break; - case T_SlabContext: - type = "Slab"; - break; - case T_BumpContext: - type = "Bump"; - break; - default: - type = "???"; - break; - } + type = ContextTypeToString(context->type); values[2] = CStringGetTextDatum(type); values[3] = Int32GetDatum(list_length(path)); /* level */ @@ -175,6 +183,38 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, list_free(path); } +/* + * ContextTypeToString + * Returns a textual representation of a context type + * + * This should cover the same types as MemoryContextIsValid. + */ +const char * +ContextTypeToString(NodeTag type) +{ + const char *context_type; + + switch (type) + { + case T_AllocSetContext: + context_type = "AllocSet"; + break; + case T_GenerationContext: + context_type = "Generation"; + break; + case T_SlabContext: + context_type = "Slab"; + break; + case T_BumpContext: + context_type = "Bump"; + break; + default: + context_type = "???"; + break; + } + return context_type; +} + /* * pg_get_backend_memory_contexts * SQL SRF showing backend memory context. @@ -189,7 +229,7 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) HTAB *context_id_lookup; ctl.keysize = sizeof(MemoryContext); - ctl.entrysize = sizeof(MemoryContextId); + ctl.entrysize = sizeof(MemoryStatsContextId); ctl.hcxt = CurrentMemoryContext; context_id_lookup = hash_create("pg_get_backend_memory_contexts", @@ -216,7 +256,7 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) foreach_ptr(MemoryContextData, cur, contexts) { - MemoryContextId *entry; + MemoryStatsContextId *entry; bool found; /* @@ -224,8 +264,8 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) * PutMemoryContextsStatsTupleStore needs this to populate the "path" * column with the parent context_ids. */ - entry = (MemoryContextId *) hash_search(context_id_lookup, &cur, - HASH_ENTER, &found); + entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &cur, + HASH_ENTER, &found); entry->context_id = context_id++; Assert(!found); @@ -305,3 +345,804 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } + +/* + * pg_get_process_memory_contexts + * Signal a backend or an auxiliary process to send its memory contexts, + * wait for the results and display them. + * + * By default, only superusers or users with ROLE_PG_READ_ALL_STATS are allowed + * to signal a process to return the memory contexts. This is because allowing + * any users to issue this request at an unbounded rate would cause lots of + * requests to be sent, which can lead to denial of service. Additional roles + * can be permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to copy the memory context details + * to a dynamic shared memory space. + * + * We have defined a limit on DSA memory that could be allocated per process - + * if the process has more memory contexts than what can fit in the allocated + * size, the excess contexts are summarized and represented as cumulative total + * at the end of the buffer. + * + * After sending the signal, wait on a condition variable. The publishing + * backend, after copying the data to shared memory, sends signal on that + * condition variable. There is one condition variable per client process. + * Once the condition variable is signalled, check if the latest memory context + * information is available and display. + * + * If the publishing backend does not respond before the condition variable + * times out, which is set to timeout value specified by the user, give up and + * return NULL. + */ +Datum +pg_get_process_memory_contexts(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + bool summary = PG_GETARG_BOOL(1); + double timeout = PG_GETARG_FLOAT8(2); + PGPROC *proc; + ProcNumber procNumber = INVALID_PROC_NUMBER; + bool proc_is_aux = false; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + MemoryStatsEntry *memcxt_info; + MemoryStatsDSHashEntry *entry; + bool found; + char key[CLIENT_KEY_SIZE]; + + if (timeout < 0) + { + /* + * This is just a warning so a loop-through-resultset will not abort + * if one backend terminated on its own during the run. + */ + ereport(WARNING, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("\"timeout\" must not be negative"))); + PG_RETURN_NULL(); + } + + /* + * See if the process with given pid is a backend or an auxiliary process + * and remember the type for when we requery the process later. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + { + proc = AuxiliaryPidGetProc(pid); + proc_is_aux = true; + } + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; this is however not a problem and leave with a WARNING. + * See comment in pg_log_backend_memory_contexts for a discussion on this. + */ + if (proc == NULL) + { + /* + * This is just a warning so a loop-through-resultset will not abort + * if one backend terminated on its own during the run. + */ + ereport(WARNING, + errmsg("PID %d is not a PostgreSQL server process", pid)); + PG_RETURN_NULL(); + } + + InitMaterializedSRF(fcinfo, 0); + + procNumber = GetNumberFromPGProc(proc); + + /* + * Create a DSA to allocate memory for copying memory contexts statistics. + * Allocate the memory in the DSA and send dsa pointer to the server + * process for storing the context statistics. If number of contexts + * exceed a predefined limit(1MB), a cumulative total is stored for such + * contexts. + * + * The DSA is created once for the lifetime of the server, and only + * attached in subsequent calls. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", &found); + + /* + * The dsa pointers containing statistics for each client are stored in a + * dshash table. In addition to dsa pointer, each entry in this table also + * contains information about the statistics, condition variable for + * signalling between client and the server and miscellaneous data + * specific to a request. There is one entry per client request in the + * hash table. + */ + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", &memctx_dsh_params, &found); + + snprintf(key, sizeof(key), "%d", MyProcNumber); + + /* + * Check if the publishing process slot is empty and store this clients + * key i.e its procNumber. This informs the publishing process that it is + * supposed to write statistics in the hash entry corresponding to this + * client. + */ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + if (client_keys[procNumber] == -1) + client_keys[procNumber] = MyProcNumber; + else + { + ereport(WARNING, + errmsg("server process %d is processing previous request", pid)); + LWLockRelease(client_keys_lock); + PG_RETURN_NULL(); + } + LWLockRelease(client_keys_lock); + + /* + * Insert an entry for this client in DSHASH table the first time this + * function is called. This entry is deleted when the process exits in + * before_shmem_exit call. + * + * dshash_find_or_insert locks the entry to prevent the publisher from + * reading before client has updated the entry. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + if (!found) + { + entry->stats_written = false; + ConditionVariableInit(&entry->memcxt_cv); + } + + /* + * Allocate 1MB of memory for the backend to publish its statistics on + * every call to this function. The memory is freed at the end of the + * function. + */ + entry->memstats_dsa_pointer = + dsa_allocate0(MemoryStatsDsaArea, MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND); + + /* + * Specify whether a summary of statistics is requested, before signalling + * the server. + */ + entry->summary = summary; + + /* + * Indicate which server process statistics are being requested from. + * If this client times out before the last requested process can publish its + * statistics, it may send a new request to another server process. Since the + * previous server was notified, it might attempt to read the same client entry + * and respond incorrectly with its statistics. By storing the server ID in the + * client entry, we prevent any previously signalled server process from writing + * its statistics in the space meant for the newly requested process. + */ + entry->server_id = pid; + dshash_release_lock(MemoryStatsDsHash, entry); + + PG_TRY(); + { + /* + * Send a signal to a PostgreSQL process, informing it we want it to + * produce information about its memory contexts. + */ + if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, procNumber) < 0) + { + ereport(WARNING, + errmsg("could not send signal to process %d: %m", pid)); + memstats_dsa_cleanup(entry); + PG_RETURN_NULL(); + } + + while (1) + { + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + Assert(found); + + /* + * We expect to come out of sleep when the requested process has + * finished publishing the statistics, verified using a boolean + * stats_written. + */ + if (entry->stats_written) + break; + + dshash_release_lock(MemoryStatsDsHash, entry); + + /* + * Recheck the state of the backend before sleeping on the + * condition variable to ensure the process is still alive. Only + * check the relevant process type based on the earlier PID check. + */ + if (proc_is_aux) + proc = AuxiliaryPidGetProc(pid); + else + proc = BackendPidGetProc(pid); + + /* + * The process ending during memory context processing is not an + * error. + */ + if (proc == NULL) + { + ereport(WARNING, + errmsg("PID %d is no longer a PostgreSQL server process", + pid)); + memstats_dsa_cleanup(entry); + ConditionVariableCancelSleep(); + PG_RETURN_NULL(); + } + + + /* + * Wait for the timeout as defined by the user. If no statistics + * are available within the allowed time then return NULL. The + * timer is defined in milliseconds since that's what the + * condition variable sleep uses. + */ + if (ConditionVariableTimedSleep(&entry->memcxt_cv, + (timeout * 1000), WAIT_EVENT_MEM_CXT_PUBLISH)) + { + /* Timeout has expired, return NULL */ + memstats_dsa_cleanup(entry); + ConditionVariableCancelSleep(); + PG_RETURN_NULL(); + } + } + + /* + * Backend has finished publishing the stats, project them. + */ + memcxt_info = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + +#define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 11 + for (int i = 0; i < entry->total_stats; i++) + { + ArrayType *path_array; + int path_length; + Datum values[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + bool nulls[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + Datum *path_datum = NULL; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + if (memcxt_info[i].name[0] != '\0') + { + values[0] = CStringGetTextDatum(memcxt_info[i].name); + } + else + nulls[0] = true; + + if (memcxt_info[i].ident[0] != '\0') + { + values[1] = CStringGetTextDatum(memcxt_info[i].ident); + } + else + nulls[1] = true; + + values[2] = CStringGetTextDatum(ContextTypeToString(memcxt_info[i].type)); + + path_length = memcxt_info[i].path_length; + path_datum = (Datum *) palloc(path_length * sizeof(Datum)); + if (memcxt_info[i].path[0] != 0) + { + for (int j = 0; j < path_length; j++) + path_datum[j] = Int32GetDatum(memcxt_info[i].path[j]); + path_array = construct_array_builtin(path_datum, path_length, INT4OID); + values[3] = PointerGetDatum(path_array); + } + else + nulls[3] = true; + + values[4] = Int32GetDatum(memcxt_info[i].levels); + values[5] = Int64GetDatum(memcxt_info[i].totalspace); + values[6] = Int64GetDatum(memcxt_info[i].nblocks); + values[7] = Int64GetDatum(memcxt_info[i].freespace); + values[8] = Int64GetDatum(memcxt_info[i].freechunks); + values[9] = Int64GetDatum(memcxt_info[i].totalspace - + memcxt_info[i].freespace); + values[10] = Int32GetDatum(memcxt_info[i].num_agg_stats); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + memstats_dsa_cleanup(entry); + dshash_release_lock(MemoryStatsDsHash, entry); + + ConditionVariableCancelSleep(); + + } + PG_CATCH(); + { + memstats_dsa_cleanup(entry); + dshash_release_lock(MemoryStatsDsHash, entry); + + ConditionVariableCancelSleep(); + } + PG_END_TRY(); + + PG_RETURN_NULL(); +} + +static void +memstats_dsa_cleanup(MemoryStatsDSHashEntry *entry) +{ + Assert(MemoryStatsDsaArea != NULL); + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + entry->memstats_dsa_pointer = InvalidDsaPointer; + entry->stats_written = false; + entry->server_id = 0; +} +void +MemoryContextKeysShmemInit(void) +{ + bool found; + + client_keys = (int *) + ShmemInitStruct("MemoryContextKeys", + MemoryContextKeysShmemSize() + sizeof(LWLockPadded), &found); + client_keys_lock = (LWLock *) ((char *) client_keys + MemoryContextKeysShmemSize()); + + if (!found) + { + MemSet(client_keys, -1, MemoryContextKeysShmemSize()); + LWLockInitialize(client_keys_lock, LWTRANCHE_MEMORY_CONTEXT_KEYS); + } +} + +Size +MemoryContextKeysShmemSize(void) +{ + Size sz = 0; + Size TotalProcs = 0; + + TotalProcs = add_size(TotalProcs, NUM_AUXILIARY_PROCS); + TotalProcs = add_size(TotalProcs, MaxBackends); + sz = add_size(sz, mul_size(TotalProcs, sizeof(int))); + + return sz; +} + +/* + * HandleGetMemoryContextInterrupt + * Handle receipt of an interrupt indicating a request to publish memory + * contexts statistics. + * + * All the actual work is deferred to ProcessGetMemoryContextInterrupt() as + * this cannot be performed in a signal handler. + */ +void +HandleGetMemoryContextInterrupt(void) +{ + InterruptPending = true; + PublishMemoryContextPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + +/* + * ProcessGetMemoryContextInterrupt + * Generate information about memory contexts used by the process. + * + * Performs a breadth first search on the memory context tree, thus parents + * statistics are reported before their children in the monitoring function + * output. + * + * Statistics for all the processes are shared via the same dynamic shared + * area. Individual statistics are tracked independently in + * per-process DSA pointers. These pointers are stored in a dshash table with + * key as requesting clients ProcNumber. + * + * We calculate maximum number of context's statistics that can be displayed + * using a pre-determined limit for memory available per process for this + * utility and maximum size of statistics for each context. The remaining + * context statistics if any are captured as a cumulative total at the end of + * individual context's statistics. + * + * If summary is true, we capture the level 1 and level 2 contexts + * statistics. For that we traverse the memory context tree recursively in + * depth first search manner to cover all the children of a parent context, to + * be able to display a cumulative total of memory consumption by a parent at + * level 2 and all its children. + */ +void +ProcessGetMemoryContextInterrupt(void) +{ + List *contexts; + HASHCTL ctl; + HTAB *context_id_lookup; + int context_id = 0; + MemoryStatsEntry *meminfo; + bool summary = false; + MemoryContextCounters stat; + int num_individual_stats = 0; + bool found; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + int clientProcNumber; + MemoryContext memstats_ctx = NULL; + MemoryContext oldcontext = NULL; + + PublishMemoryContextPending = false; + + /* + * Create a new memory context which is not a part of TopMemoryContext + * tree. This context is used to allocate all memory in this function. + * This helps in keeping the memory allocation in this function to report + * memory consumption statistics separate. So that it does not affect the + * output of this function. + */ + memstats_ctx = AllocSetContextCreate((MemoryContext) NULL, "publish_memory_context_statistics", + ALLOCSET_SMALL_SIZES); + oldcontext = MemoryContextSwitchTo(memstats_ctx); + + /* + * The hash table is used for constructing "path" column of the view, + * similar to its local backend counterpart. + */ + ctl.keysize = sizeof(MemoryContext); + ctl.entrysize = sizeof(MemoryStatsContextId); + ctl.hcxt = CurrentMemoryContext; + + context_id_lookup = hash_create("pg_get_remote_backend_memory_contexts", + 256, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + /* List of contexts to process in the next round - start at the top. */ + contexts = list_make1(TopMemoryContext); + + /* + * If DSA exists, created by another process requesting statistics, attach + * to it. We expect the client process to create required DSA and Dshash + * table. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", &found); + + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", &memctx_dsh_params, &found); + + /* Retreive the client key for publishing statistics */ + LWLockAcquire(client_keys_lock, LW_SHARED); + Assert(client_keys[MyProcNumber] != -1); + clientProcNumber = client_keys[MyProcNumber]; + LWLockRelease(client_keys_lock); + + snprintf(key, CLIENT_KEY_SIZE, "%d", clientProcNumber); + + /* + * The entry lock is held by dshash_find_or_insert to protect writes to + * process specific memory. Two different processes publishing statistics + * do not block each other. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + /* Entry has been deleted due to client process exit */ + if (!found) + { + entry->stats_written = false; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + return; + } + + /* The client has timed out waiting for us to write statistics */ + if (entry->server_id != MyProcPid) + { + entry->stats_written = false; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + return; + } + + summary = entry->summary; + + /* Should be allocated by a client backend that is requesting statistics */ + Assert(entry->memstats_dsa_pointer != InvalidDsaPointer); + meminfo = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + if (summary) + { + int cxt_id = 0; + List *path = NIL; + MemoryStatsContextId *contextid_entry; + + /* Copy TopMemoryContext statistics to DSA */ + memset(&stat, 0, sizeof(stat)); + (*TopMemoryContext->methods->stats) (TopMemoryContext, NULL, NULL, + &stat, true); + path = lcons_int(1, path); + PublishMemoryContext(meminfo, cxt_id, TopMemoryContext, path, stat, + 1, 100); + + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &TopMemoryContext, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = cxt_id + 1; + + /* + * Copy statistics for each of TopMemoryContexts children. This + * includes statistics of at most 100 children per node, with each + * child node limited to a depth of 100 in its subtree. + */ + for (MemoryContext c = TopMemoryContext->firstchild; c != NULL; + c = c->nextchild) + { + MemoryContextCounters grand_totals; + int num_contexts = 0; + + path = NIL; + memset(&grand_totals, 0, sizeof(grand_totals)); + + cxt_id++; + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &c, + HASH_ENTER, &found); + Assert(!found); + contextid_entry->context_id = cxt_id + 1; + + MemoryContextStatsInternal(c, 1, 100, 100, &grand_totals, + PRINT_STATS_NONE, &num_contexts); + + path = compute_context_path(c, context_id_lookup); + + PublishMemoryContext(meminfo, cxt_id, c, path, + grand_totals, num_contexts, 100); + } + entry->total_stats = cxt_id + 1; + + /* Notify waiting backends and return */ + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + return; + } + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + MemoryStatsContextId *contextid_entry; + + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &cur, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = context_id + 1; + + /* + * Figure out the transient context_id of this context and each of its + * ancestors, to compute a path for this context. + */ + path = compute_context_path(cur, context_id_lookup); + + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + /* Account for saving one statistics slot for cumulative reporting */ + if (context_id < (MAX_MEMORY_CONTEXT_STATS_NUM - 1)) + { + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1, 100); + } + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].totalspace += stat.totalspace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].nblocks += stat.nblocks; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freespace += stat.freespace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit per process is reached, write aggregate of the + * remaining statistics. + * + * We can store contexts from 0 to max_stats - 1. When context_id is + * greater than max_stats, we stop reporting individual statistics + * when context_id equals max_stats - 2. As we use max_stats - 1 array + * slot for reporting cumulative statistics or "Remaining Totals". + */ + if (context_id == (MAX_MEMORY_CONTEXT_STATS_NUM - 2)) + { + int namelen = strlen("Remaining Totals"); + + num_individual_stats = context_id + 1; + strlcpy(meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].name, "Remaining Totals", namelen + 1); + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].ident[0] = '\0'; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].path[0] = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].type = 0; + } + context_id++; + + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + contexts = lappend(contexts, c); + } + + /* + * Statistics are not aggregated, i.e individual statistics reported when + * context_id <= max_stats. + */ + if (context_id <= MAX_MEMORY_CONTEXT_STATS_NUM) + { + entry->total_stats = context_id; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = 1; + } + /* Report number of aggregated memory contexts */ + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = context_id - + num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for cumulative + * statistics. + */ + entry->total_stats = num_individual_stats + 1; + } + entry->stats_written = true; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + /* Notify waiting backends and return */ + ConditionVariableBroadcast(&entry->memcxt_cv); +} + +/* + * Update timestamp and signal all the waiting client backends after copying + * all the statistics. + */ +static void +end_memorycontext_reporting(MemoryStatsDSHashEntry *entry, MemoryContext oldcontext, HTAB *context_id_lookup) +{ + MemoryContext curr_ctx = CurrentMemoryContext; + + dshash_release_lock(MemoryStatsDsHash, entry); + + /* + * Empty this processes slot, so other clients can request memory + * statistics + */ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + client_keys[MyProcNumber] = -1; + LWLockRelease(client_keys_lock); + + hash_destroy(context_id_lookup); + MemoryContextSwitchTo(oldcontext); + MemoryContextReset(curr_ctx); +} + +/* + * compute_context_path + * + * Append the transient context_id of this context and each of its ancestors + * to a list, in order to compute a path. + */ +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + MemoryContext cur_context; + + for (cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryStatsContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + elog(ERROR, "hash table corrupted, can't construct path value"); + + path = lcons_int(cur_entry->context_id, path); + } + + return path; +} + +/* + * PublishMemoryContext + * + * Copy the memory context statistics of a single context to a DSA memory + */ +static void +PublishMemoryContext(MemoryStatsEntry *memcxt_info, int curr_id, + MemoryContext context, List *path, + MemoryContextCounters stat, int num_contexts, int max_levels) +{ + const char *ident = context->ident; + const char *name = context->name; + + /* + * To be consistent with logging output, we label dynahash contexts with + * just the hash table name as with MemoryContextStatsPrint(). + */ + if (context->ident && strncmp(context->name, "dynahash", 8) == 0) + { + name = context->ident; + ident = NULL; + } + + if (name != NULL) + { + int namelen = strlen(name); + + if (strlen(name) >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + namelen = pg_mbcliplen(name, namelen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].name, name, namelen + 1); + } + else + /* Clearing the array */ + memcxt_info[curr_id].name[0] = '\0'; + + /* Trim and copy the identifier if it is not set to NULL */ + if (ident != NULL) + { + int idlen = strlen(context->ident); + + /* + * Some identifiers such as SQL query string can be very long, + * truncate oversize identifiers. + */ + if (idlen >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + idlen = pg_mbcliplen(ident, idlen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].ident, ident, idlen + 1); + } + else + memcxt_info[curr_id].ident[0] = '\0'; + + /* Allocate DSA memory for storing path information */ + if (path == NIL) + memcxt_info[curr_id].path[0] = 0; + else + { + int levels = Min(list_length(path), max_levels); + + memcxt_info[curr_id].path_length = levels; + memcxt_info[curr_id].levels = list_length(path); + + foreach_int(i, path) + { + memcxt_info[curr_id].path[foreach_current_index(i)] = i; + if (--levels == 0) + break; + } + } + memcxt_info[curr_id].type = context->type; + memcxt_info[curr_id].totalspace = stat.totalspace; + memcxt_info[curr_id].nblocks = stat.nblocks; + memcxt_info[curr_id].freespace = stat.freespace; + memcxt_info[curr_id].freechunks = stat.freechunks; + memcxt_info[curr_id].num_agg_stats = num_contexts; +} + +void +AtProcExit_memstats_cleanup(int code, Datum arg) +{ + int idx = MyProcNumber; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + bool found; + + if (MemoryStatsDsHash != NULL) + { + snprintf(key, CLIENT_KEY_SIZE, "%d", idx); + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + if (found) + { + if (MemoryStatsDsaArea != NULL && + DsaPointerIsValid(entry->memstats_dsa_pointer)) + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + } + dshash_delete_entry(MemoryStatsDsHash, entry); + } + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + client_keys[idx] = -1; + LWLockRelease(client_keys_lock); +} diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c index 97c2ac1faf9..ab768a7a91f 100644 --- a/src/backend/utils/adt/pg_locale.c +++ b/src/backend/utils/adt/pg_locale.c @@ -45,7 +45,6 @@ #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/pg_locale.h" -#include "utils/relcache.h" #include "utils/syscache.h" #ifdef WIN32 diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index d31cb45a058..92b0446b80c 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -39,6 +39,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t PublishMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index 641e535a73c..fb3f2d21fa0 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -662,6 +662,13 @@ BaseInit(void) * drop ephemeral slots, which in turn triggers stats reporting. */ ReplicationSlotInitialize(); + + /* + * The before shmem exit callback frees the DSA memory occupied by the + * latest memory context statistics that could be published by this proc + * if requested. + */ + before_shmem_exit(AtProcExit_memstats_cleanup, 0); } diff --git a/src/backend/utils/mb/mbutils.c b/src/backend/utils/mb/mbutils.c index 886ecbad871..308016d7763 100644 --- a/src/backend/utils/mb/mbutils.c +++ b/src/backend/utils/mb/mbutils.c @@ -39,7 +39,6 @@ #include "mb/pg_wchar.h" #include "utils/fmgrprotos.h" #include "utils/memutils.h" -#include "utils/relcache.h" #include "varatt.h" /* diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 47fd774c7d2..3a5422c7273 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -40,6 +40,7 @@ #include "mb/pg_wchar.h" #include "miscadmin.h" +#include "utils/hsearch.h" #include "utils/memdebug.h" #include "utils/memutils.h" #include "utils/memutils_internal.h" @@ -176,10 +177,6 @@ MemoryContext PortalContext = NULL; static void MemoryContextDeleteOnly(MemoryContext context); static void MemoryContextCallResetCallbacks(MemoryContext context); -static void MemoryContextStatsInternal(MemoryContext context, int level, - int max_level, int max_children, - MemoryContextCounters *totals, - bool print_to_stderr); static void MemoryContextStatsPrint(MemoryContext context, void *passthru, const char *stats_string, bool print_to_stderr); @@ -877,11 +874,19 @@ MemoryContextStatsDetail(MemoryContext context, bool print_to_stderr) { MemoryContextCounters grand_totals; + int num_contexts; + PrintDestination print_location; memset(&grand_totals, 0, sizeof(grand_totals)); + if (print_to_stderr) + print_location = PRINT_STATS_TO_STDERR; + else + print_location = PRINT_STATS_TO_LOGS; + + /* num_contexts report number of contexts aggregated in the output */ MemoryContextStatsInternal(context, 1, max_level, max_children, - &grand_totals, print_to_stderr); + &grand_totals, print_location, &num_contexts); if (print_to_stderr) fprintf(stderr, @@ -916,13 +921,14 @@ MemoryContextStatsDetail(MemoryContext context, * One recursion level for MemoryContextStats * * Print stats for this context if possible, but in any case accumulate counts - * into *totals (if not NULL). + * into *totals (if not NULL). The callers should make sure that print_location + * is set to PRINT_STATS_TO_STDERR or PRINT_STATS_TO_LOGS or PRINT_STATS_NONE. */ -static void +void MemoryContextStatsInternal(MemoryContext context, int level, int max_level, int max_children, MemoryContextCounters *totals, - bool print_to_stderr) + PrintDestination print_location, int *num_contexts) { MemoryContext child; int ichild; @@ -930,10 +936,39 @@ MemoryContextStatsInternal(MemoryContext context, int level, Assert(MemoryContextIsValid(context)); /* Examine the context itself */ - context->methods->stats(context, - MemoryContextStatsPrint, - &level, - totals, print_to_stderr); + switch (print_location) + { + case PRINT_STATS_TO_STDERR: + context->methods->stats(context, + MemoryContextStatsPrint, + &level, + totals, true); + break; + + case PRINT_STATS_TO_LOGS: + context->methods->stats(context, + MemoryContextStatsPrint, + &level, + totals, false); + break; + + case PRINT_STATS_NONE: + + /* + * Do not print the statistics if print_location is + * PRINT_STATS_NONE, only compute totals. This is used in + * reporting of memory context statistics via a sql function. Last + * parameter is not relevant. + */ + context->methods->stats(context, + NULL, + NULL, + totals, false); + break; + } + + /* Increment the context count for each of the recursive call */ + *num_contexts = *num_contexts + 1; /* * Examine children. @@ -953,7 +988,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, MemoryContextStatsInternal(child, level + 1, max_level, max_children, totals, - print_to_stderr); + print_location, num_contexts); } } @@ -972,7 +1007,13 @@ MemoryContextStatsInternal(MemoryContext context, int level, child = MemoryContextTraverseNext(child, context); } - if (print_to_stderr) + /* + * Add the count of children contexts which are traversed in the + * non-recursive manner. + */ + *num_contexts = *num_contexts + ichild; + + if (print_location == PRINT_STATS_TO_STDERR) { for (int i = 0; i < level; i++) fprintf(stderr, " "); @@ -985,7 +1026,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, local_totals.freechunks, local_totals.totalspace - local_totals.freespace); } - else + else if (print_location == PRINT_STATS_TO_LOGS) ereport(LOG_SERVER_ONLY, (errhidestmt(true), errhidecontext(true), diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 118d6da1ace..3d6f42606fd 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8597,6 +8597,16 @@ prorettype => 'bool', proargtypes => 'int4', prosrc => 'pg_log_backend_memory_contexts' }, +# publishing memory contexts of the specified postgres process +{ oid => '2173', descr => 'publish memory contexts of the specified backend', + proname => 'pg_get_process_memory_contexts', provolatile => 'v', + prorows => '100', proretset => 't', proparallel => 'r', + prorettype => 'record', proargtypes => 'int4 bool float8', + proallargtypes => '{int4,bool,float8,text,text,text,_int4,int4,int8,int8,int8,int8,int8,int4}', + proargmodes => '{i,i,i,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{pid, summary, timeout, name, ident, type, path, level, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes, num_agg_contexts}', + prosrc => 'pg_get_process_memory_contexts' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index 1bef98471c3..1e59a7f910f 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -96,6 +96,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t PublishMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h index 208d2e3a8ed..72ace053e9d 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -135,3 +135,4 @@ PG_LWLOCKTRANCHE(SUBTRANS_SLRU, SubtransSLRU) PG_LWLOCKTRANCHE(XACT_SLRU, XactSLRU) PG_LWLOCKTRANCHE(PARALLEL_VACUUM_DSA, ParallelVacuumDSA) PG_LWLOCKTRANCHE(AIO_URING_COMPLETION, AioUringCompletion) +PG_LWLOCKTRANCHE(MEMORY_CONTEXT_KEYS, MemoryContextReportingKeys) diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index afeeb1ca019..345d5a0ecb1 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -35,6 +35,7 @@ typedef enum PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to send the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ /* Recovery conflict reasons */ diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 8abc26abce2..799cfe38cab 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -18,7 +18,10 @@ #define MEMUTILS_H #include "nodes/memnodes.h" - +#include "storage/condition_variable.h" +#include "storage/lmgr.h" +#include "utils/dsa.h" +#include "lib/dshash.h" /* * MaxAllocSize, MaxAllocHugeSize @@ -48,6 +51,23 @@ #define AllocHugeSizeIsValid(size) ((Size) (size) <= MaxAllocHugeSize) +/* + * Memory Context reporting size limits. + */ + +/* Max length of context name and ident */ +#define MEMORY_CONTEXT_IDENT_SHMEM_SIZE 64 +/* Maximum size (in bytes) of DSA area per process */ +#define MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND ((size_t) (1 * 1024 * 1024)) + +/* + * Maximum size per context. Actual size may be lower as this assumes the worst + * case of deepest path and longest identifiers (name and ident, thus the + * multiplication by 2). The path depth is limited to 100 like for memory + * context logging. + */ +#define MAX_MEMORY_CONTEXT_STATS_SIZE (sizeof(MemoryStatsEntry)) +#define MAX_MEMORY_CONTEXT_STATS_NUM MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND / MAX_MEMORY_CONTEXT_STATS_SIZE /* * Standard top-level memory contexts. @@ -319,4 +339,74 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) return true; } +/* Dynamic shared memory state for statistics per context */ +typedef struct MemoryStatsEntry +{ + char name[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + char ident[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + int path[100]; + NodeTag type; + int path_length; + int levels; + int64 totalspace; + int64 nblocks; + int64 freespace; + int64 freechunks; + int num_agg_stats; +} MemoryStatsEntry; + +/* + * Per backend dynamic shared hash entry for memory context statistics + * reporting. + */ +typedef struct MemoryStatsDSHashEntry +{ + char key[64]; + ConditionVariable memcxt_cv; + bool stats_written; + int server_id; + int total_stats; + bool summary; + dsa_pointer memstats_dsa_pointer; +} MemoryStatsDSHashEntry; + +static const dshash_parameters memctx_dsh_params = { + offsetof(MemoryStatsDSHashEntry, memcxt_cv), + sizeof(MemoryStatsDSHashEntry), + dshash_strcmp, + dshash_strhash, + dshash_strcpy +}; + +/* + * Used for storage of transient identifiers for pg_get_backend_memory_contexts + */ +typedef struct MemoryStatsContextId +{ + MemoryContext context; + int context_id; +} MemoryStatsContextId; + +/* + * This is passed to MemoryContextStatsInternal to determine whether + * to print context statistics or not and where to print them logs or + * stderr. + */ +typedef enum PrintDestination +{ + PRINT_STATS_TO_STDERR = 0, + PRINT_STATS_TO_LOGS, + PRINT_STATS_NONE +} PrintDestination; + +extern void ProcessGetMemoryContextInterrupt(void); +extern void HandleGetMemoryContextInterrupt(void); +extern void MemoryContextKeysShmemInit(void); +extern Size MemoryContextKeysShmemSize(void); +extern void MemoryContextStatsInternal(MemoryContext context, int level, + int max_level, int max_children, + MemoryContextCounters *totals, + PrintDestination print_location, + int *num_contexts); +extern void AtProcExit_memstats_cleanup(int code, Datum arg); #endif /* MEMUTILS_H */ diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 83228cfca29..ae17d028ed3 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -232,3 +232,22 @@ select * from pg_timezone_abbrevs where abbrev = 'LMT'; LMT | @ 7 hours 52 mins 58 secs ago | f (1 row) +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; +NOTICE: (AllocSet,TopMemoryContext,) +NOTICE: (AllocSet,TopMemoryContext,) diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 66179f026b3..d0917b6868e 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -101,3 +101,21 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs; -- One specific case we can check without much fear of breakage -- is the historical local-mean-time value used for America/Los_Angeles. select * from pg_timezone_abbrevs where abbrev = 'LMT'; + +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index e6f2e93b2d6..d2399465df6 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1680,6 +1680,9 @@ MemoryContextData MemoryContextId MemoryContextMethodID MemoryContextMethods +MemoryStatsContextId +MemoryStatsEntry +MemoryStatsDSHashEntry MemoryStatsPrintFunc MergeAction MergeActionState -- 2.34.1 ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-08-18 13:32 torikoshia <[email protected]> parent: Rahila Syed <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: torikoshia @ 2025-08-18 13:32 UTC (permalink / raw) To: Rahila Syed <[email protected]>; +Cc: pgsql-hackers; Daniel Gustafsson <[email protected]> On 2025-08-14 07:35, Rahila Syed wrote: > Hi Torikoshia, > > Thank you for reviewing the patch. > >> This function is added at the end of Table "9.96. Server Signaling >> Functions", but since pg_get_process_memory_contexts outputs >> essentially >> the same information as pg_log_backend_memory_contexts, it might be >> better to place them next to each other in the table. > > The idea was to place the new addition at the end of the table instead > of in the middle. > I’m fine with putting them together, though. I’ll do that in the > next version unless there’s a > reason not to. > >>> + <parameter>stats_timestamp</parameter> >> <type>timestamptz</type> ) >> >>> +typedef struct MemoryStatsDSHashEntry >>> +{ >>> + char key[64]; >>> + ConditionVariable memcxt_cv; >>> + int proc_id; >>> + int total_stats; >>> + bool summary; >>> + dsa_pointer memstats_dsa_pointer; >>> + TimestampTz stats_timestamp; >>> +} MemoryStatsDSHashEntry; >> >> stats_timestamp appears only in the two places below in the patch, >> but >> it does not seem to be actually output. >> Is this column unnecessary? > > Thank you for pointing this out. This is removed in the attached > patch, as it was a > remnant from the previous design. As old statistics are discarded in > the new design, > a timestamp field is not needed anymore. > >> Specifying 0 for timeout causes a crash: >> Should 0 be handled safely and treated as “no timeout”, or >> rejected as >> an error? > > Good catch. > The crash has been resolved in the attached patch. It was caused by a > missing > ConditionVariableCancelSleep() call when exiting without statistics > due to a timeout value of 0. > A 0 timeout means that statistics should only be retrieved if they are > immediately available, > without waiting. We could exit with a warning/error saying "too low > timeout", but I think it's worthwhile > to try fetching the statistics if possible. > >> Similarly, specifying a negative value for timeout still works: >> >> =# select * from pg_get_process_memory_contexts(30590, true, >> -10); >> >> It might be better to reject negative values similar to >> pg_terminate_backend(). > > Fixed as suggested by you in the attached patch. > Currently, negative values are interpreted as an indefinite wait for > statistics. > This could cause the client to hang if the server process exits > without providing statistics. > To avoid this, it would be better to exit after displaying a warning > when the user specifies > negative timeouts. > >>> + /* Retreive the client key fo publishing statistics */ >> >> fo -> for? > > Fixed. > >>> + */ >>> +#define MEMSTATS_WAIT_TIMEOUT 100 >> >> MEMSTATS_WAIT_TIMEOUT is defined, but it doesn’t seem to be used. > > This is removed now as it was a leftover from the previous design. > > The attached patch also fixes an assertion failure I observed when a > client times out > before the last requested process can publish its statistics. A client > frees the memory > reserved for storing the statistics when it exits the function after > timeout. Since a > server process was notified, it might attempt to read the same client > entry and access the dsa > memory reserved for statistics resulting in the assertion > failure. I resolved this by including a check for this scenario and > then exiting the handler > function accordingly. Thanks for updating the patch! However, when I ran pg_get_process_memory_contexts() with summary = true, it took a while and returned nothing: =# select pg_get_process_memory_contexts(pg_backend_pid(), true, 1) from pg_stat_activity ; pg_get_process_memory_contexts -------------------------------- (0 rows) Time: 6026.291 ms (00:06.026) Since v32 patch quickly returned the memory contexts as expected with the same parameter specified, there seems to be some degradation. Could you check it? -- Regards, -- Atsushi Torikoshi Seconded from NTT DATA Japan Corporation to SRA OSS K.K. ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-08-19 21:42 Rahila Syed <[email protected]> parent: torikoshia <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Rahila Syed @ 2025-08-19 21:42 UTC (permalink / raw) To: torikoshia <[email protected]>; +Cc: pgsql-hackers; Daniel Gustafsson <[email protected]> Hi, =# select pg_get_process_memory_contexts(pg_backend_pid(), true, 1) > from pg_stat_activity ; > > pg_get_process_memory_contexts > -------------------------------- > (0 rows) > > Time: 6026.291 ms (00:06.026) > > Since v32 patch quickly returned the memory contexts as expected with > the same parameter specified, there seems to be some degradation. Could > you check it? > Thank you for reporting this failure. This issue was a regression caused by the absence of a ConditionVariableSignal() call in the summary = true code path, which happened due to recent code refactoring. PFA the fix. Thank you, Rahila Syed Attachments: [application/octet-stream] v34-0001-Add-pg_get_process_memory_context-function.patch (61.2K, ../../CAH2L28uL=Vm9E4Uf=tkg2Ki7_uDuAXORHVyCA7K_L=PAQ2p4sA@mail.gmail.com/3-v34-0001-Add-pg_get_process_memory_context-function.patch) download | inline diff: From ae939692b3bb507484136a3f32c239b6976ce0c2 Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Mon, 30 Jun 2025 12:11:00 +0530 Subject: [PATCH] Add pg_get_process_memory_context function This adds a function for retrieving memory context statistics and information from backends as well as auxiliary processes. The intended usecase is cluster debugging when under memory pressure or unanticipated memory usage characteristics. When calling the function it sends a signal to the specified process to submit statistics regarding its memory contexts into dynamic shared memory. Each memory context is returned in detail, followed by a cumulative total in case the number of contexts exceed the max allocated amount of shared memory. Each process is limited to use at most 1Mb memory for this. A summary can also be explicitly requested by the user, this will return the TopMemoryContext and a cumulative total of all lower contexts. In order to not block on busy processes the caller specifies the number of seconds during which to retry before timing out. In the case where no statistics are published within the set timeout, NULL is returned. --- doc/src/sgml/func/func-admin.sgml | 164 ++++ src/backend/catalog/system_views.sql | 5 + src/backend/postmaster/autovacuum.c | 4 + src/backend/postmaster/checkpointer.c | 4 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 4 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 4 + src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/storage/lmgr/proc.c | 1 + src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 2 + src/backend/utils/adt/mcxtfuncs.c | 889 +++++++++++++++++- src/backend/utils/adt/pg_locale.c | 1 - src/backend/utils/init/globals.c | 1 + src/backend/utils/init/postinit.c | 7 + src/backend/utils/mb/mbutils.c | 1 - src/backend/utils/mmgr/mcxt.c | 71 +- src/include/catalog/pg_proc.dat | 10 + src/include/miscadmin.h | 1 + src/include/storage/lwlocklist.h | 1 + src/include/storage/procsignal.h | 1 + src/include/utils/memutils.h | 92 +- src/test/regress/expected/sysviews.out | 19 + src/test/regress/sql/sysviews.sql | 18 + src/tools/pgindent/typedefs.list | 3 + 27 files changed, 1279 insertions(+), 41 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 6347fe60b0c..43131c60882 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -251,6 +251,137 @@ <literal>false</literal> is returned. </para></entry> </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_get_process_memory_contexts</primary> + </indexterm> + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type>, <parameter>summary</parameter> <type>boolean</type>, <parameter>timeout</parameter> <type>float</type> ) + <returnvalue>setof record</returnvalue> + ( <parameter>name</parameter> <type>text</type>, + <parameter>ident</parameter> <type>text</type>, + <parameter>type</parameter> <type>text</type>, + <parameter>path</parameter> <type>integer[]</type>, + <parameter>level</parameter> <type>integer</type>, + <parameter>total_bytes</parameter> <type>bigint</type>, + <parameter>total_nblocks</parameter> <type>bigint</type>, + <parameter>free_bytes</parameter> <type>bigint</type>, + <parameter>free_chunks</parameter> <type>bigint</type>, + <parameter>used_bytes</parameter> <type>bigint</type>, + <parameter>num_agg_contexts</parameter> <type>integer</type>, + <parameter>stats_timestamp</parameter> <type>timestamptz</type> ) + </para> + <para> + This function handles requests to display the memory contexts of a + <productname>PostgreSQL</productname> process with the specified + process ID. The function can be used to send requests to backends as + well as <glossterm linkend="glossary-auxiliary-proc">auxiliary processes</glossterm>. + </para> + <para> + The returned record contains extended statistics per each memory + context: + <itemizedlist spacing="compact"> + <listitem> + <para> + <parameter>name</parameter> - The name of the memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>ident</parameter> - Memory context ID (if any). + </para> + </listitem> + <listitem> + <para> + <parameter>type</parameter> - The type of memory context, possible + values are: AllocSet, Generation, Slab and Bump. + </para> + </listitem> + <listitem> + <para> + <parameter>path</parameter> - Memory contexts are organized in a + tree model with TopMemoryContext as the root, and all other memory + contexts as nodes in the tree. The <parameter>path</parameter> + displays the path from the root to the current memory context. The + path is limited to 100 children per node, which each node limited + to a max depth of 100, to preserve memory during reporting. The + printed path will also be limited to 100 nodes counting from the + TopMemoryContext. + </para> + </listitem> + <listitem> + <para> + <parameter>level</parameter> - The level in the tree of the current + memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_bytes</parameter> - The total number of bytes + allocated to this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_nblocks</parameter> - The total number of blocks + used for the allocated memory. + </para> + </listitem> + <listitem> + <para> + <parameter>free_bytes</parameter> - The amount of free memory in + this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>free_chunks</parameter> - The number of chunks that + <parameter>free_bytes</parameter> corresponds to. + </para> + </listitem> + <listitem> + <para> + <parameter>used_bytes</parameter> - The total number of bytes + currently occupied. + </para> + </listitem> + <listitem> + <para> + <parameter>num_agg_contexts</parameter> - The number of memory + contexts aggregated in the displayed statistics. + </para> + </listitem> + </itemizedlist> + </para> + <para> + When <parameter>summary</parameter> is <literal>true</literal>, statistics + for memory contexts at levels 1 and 2 are displayed, with level 1 + representing the root node (i.e., <literal>TopMemoryContext</literal>). + Statistics for contexts on level 2 and below are aggregates of all + child contexts' statistics, where <literal>num_agg_contexts</literal> + indicate the number aggregated child contexts. When + <parameter>summary</parameter> is <literal>false</literal>, + <literal>the num_agg_contexts</literal> value is <literal>1</literal>, + indicating that individual statistics are being displayed. + </para> + <para> + Busy processes can delay reporting memory context statistics, + <parameter>timeout</parameter> specifies the number of seconds + to wait for updated statistics. <parameter>timeout</parameter> can be + specified in fractions of a second. + </para> + <para> + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context + statistics are aggregated and a cumulative total is displayed. The + <literal>num_agg_contexts</literal> column indicates the number of + contexts aggregated in the displayed statistics. When + <literal>num_agg_contexts</literal> is <literal>1</literal> it means + that the context statistics are displayed separately. + </para></entry> + </row> </tbody> </tgroup> </table> @@ -302,6 +433,39 @@ LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 because it may generate a large number of log messages. </para> + <para> + <function>pg_get_process_memory_contexts</function> can be used to request + memory contexts statistics of any <productname>PostgreSQL</productname> + process. For example: +<programlisting> +postgres=# SELECT * FROM pg_get_process_memory_contexts( + (SELECT pid FROM pg_stat_activity + WHERE backend_type = 'checkpointer'), + false, 0.5) LIMIT 1; +-[ RECORD 1 ]----+------------------------------ +name | TopMemoryContext +ident | +type | AllocSet +path | {1} +level | 1 +total_bytes | 90304 +total_nblocks | 3 +free_bytes | 2880 +free_chunks | 1 +used_bytes | 87424 +num_agg_contexts | 1 +</programlisting> + <note> + <para> + While <function>pg_get_process_memory_contexts</function> can be used to + query memory contexts of the local backend, + <structname>pg_backend_memory_contexts</structname> + (see <xref linkend="view-pg-backend-memory-contexts"/> for more details) + will be less resource intensive when only the local backend is of interest. + </para> + </note> + </para> + </sect2> <sect2 id="functions-admin-backup"> diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index 1b3c5a55882..022586d6a55 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -682,6 +682,11 @@ GRANT SELECT ON pg_backend_memory_contexts TO pg_read_all_stats; REVOKE EXECUTE ON FUNCTION pg_get_backend_memory_contexts() FROM PUBLIC; GRANT EXECUTE ON FUNCTION pg_get_backend_memory_contexts() TO pg_read_all_stats; +REVOKE EXECUTE ON FUNCTION + pg_get_process_memory_contexts(integer, boolean, float) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION + pg_get_process_memory_contexts(integer, boolean, float) TO pg_read_all_stats; + -- Statistics views CREATE VIEW pg_stat_all_tables AS diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index ff96b36d710..3875f76564d 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -791,6 +791,10 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index e84e8663e96..5b3e08805bf 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -679,6 +679,10 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index 0ae9bf906ec..f24f574e748 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -48,6 +48,10 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index 78e39e5f866..ac97a39447c 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -867,6 +867,10 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index 27e86cf393f..7149a67fcbc 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -192,6 +192,10 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index 777c9a8d555..5d14684f6b2 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -879,6 +879,10 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index 2fa045e6b0f..fe3d32e40b0 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -51,6 +51,7 @@ #include "storage/sinvaladt.h" #include "utils/guc.h" #include "utils/injection_point.h" +#include "utils/memutils.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -150,6 +151,7 @@ CalculateShmemSize(int *num_semaphores) size = add_size(size, InjectionPointShmemSize()); size = add_size(size, SlotSyncShmemSize()); size = add_size(size, AioShmemSize()); + size = add_size(size, MemoryContextKeysShmemSize() + sizeof(LWLockPadded)); /* include additional requested shmem from preload libraries */ size = add_size(size, total_addin_request); @@ -343,6 +345,7 @@ CreateOrAttachShmemStructs(void) WaitEventCustomShmemInit(); InjectionPointShmemInit(); AioShmemInit(); + MemoryContextKeysShmemInit(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index 087821311cc..8963285cc12 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -691,6 +691,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_GET_MEMORY_CONTEXT)) + HandleGetMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index e9ef0fbfe32..f194e6b3dcc 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -50,6 +50,7 @@ #include "storage/procsignal.h" #include "storage/spin.h" #include "storage/standby.h" +#include "utils/memutils.h" #include "utils/timeout.h" #include "utils/timestamp.h" diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 0cecd464902..3933b6db607 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3534,6 +3534,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 0be307d2ca0..54f91a76a1b 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -161,6 +161,7 @@ WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." +MEM_CXT_PUBLISH "Waiting for a process to publish memory information." ABI_compatibility: @@ -406,6 +407,7 @@ SubtransSLRU "Waiting to access the sub-transaction SLRU cache." XactSLRU "Waiting to access the transaction status SLRU cache." ParallelVacuumDSA "Waiting for parallel vacuum dynamic shared memory allocation." AioUringCompletion "Waiting for another process to complete IO via io_uring." +MemoryContextReportingKeys "Waiting for another process to complete reading or writing the memory reporting keys." # No "ABI_compatibility" region here as WaitEventLWLock has its own C code. diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index fe6dce9cba3..e465e156f77 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -15,13 +15,38 @@ #include "postgres.h" +#include "access/twophase.h" +#include "catalog/pg_authid_d.h" #include "funcapi.h" #include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "storage/dsm_registry.h" #include "storage/proc.h" #include "storage/procarray.h" +#include "utils/acl.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/memutils.h" +#include "utils/wait_event_types.h" + +#define CLIENT_KEY_SIZE 64 + +static LWLock *client_keys_lock = NULL; +static int *client_keys = NULL; +static dshash_table *MemoryStatsDsHash = NULL; +static dsa_area *MemoryStatsDsaArea = NULL; + +static void memstats_dsa_cleanup(MemoryStatsDSHashEntry *entry); +static const char *ContextTypeToString(NodeTag type); +static void PublishMemoryContext(MemoryStatsEntry *memcxt_info, + int curr_id, MemoryContext context, + List *path, + MemoryContextCounters stat, + int num_contexts, int max_levels); +static List *compute_context_path(MemoryContext c, HTAB *context_id_lookup); +static void end_memorycontext_reporting(MemoryStatsDSHashEntry *entry, MemoryContext oldcontext, + HTAB *context_id_lookup); /* ---------- * The max bytes for showing identifiers of MemoryContext. @@ -89,7 +114,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, */ for (MemoryContext cur = context; cur != NULL; cur = cur->parent) { - MemoryContextId *entry; + MemoryStatsContextId *entry; bool found; entry = hash_search(context_id_lookup, &cur, HASH_FIND, &found); @@ -143,24 +168,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, else nulls[1] = true; - switch (context->type) - { - case T_AllocSetContext: - type = "AllocSet"; - break; - case T_GenerationContext: - type = "Generation"; - break; - case T_SlabContext: - type = "Slab"; - break; - case T_BumpContext: - type = "Bump"; - break; - default: - type = "???"; - break; - } + type = ContextTypeToString(context->type); values[2] = CStringGetTextDatum(type); values[3] = Int32GetDatum(list_length(path)); /* level */ @@ -175,6 +183,38 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, list_free(path); } +/* + * ContextTypeToString + * Returns a textual representation of a context type + * + * This should cover the same types as MemoryContextIsValid. + */ +const char * +ContextTypeToString(NodeTag type) +{ + const char *context_type; + + switch (type) + { + case T_AllocSetContext: + context_type = "AllocSet"; + break; + case T_GenerationContext: + context_type = "Generation"; + break; + case T_SlabContext: + context_type = "Slab"; + break; + case T_BumpContext: + context_type = "Bump"; + break; + default: + context_type = "???"; + break; + } + return context_type; +} + /* * pg_get_backend_memory_contexts * SQL SRF showing backend memory context. @@ -189,7 +229,7 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) HTAB *context_id_lookup; ctl.keysize = sizeof(MemoryContext); - ctl.entrysize = sizeof(MemoryContextId); + ctl.entrysize = sizeof(MemoryStatsContextId); ctl.hcxt = CurrentMemoryContext; context_id_lookup = hash_create("pg_get_backend_memory_contexts", @@ -216,7 +256,7 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) foreach_ptr(MemoryContextData, cur, contexts) { - MemoryContextId *entry; + MemoryStatsContextId *entry; bool found; /* @@ -224,8 +264,8 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) * PutMemoryContextsStatsTupleStore needs this to populate the "path" * column with the parent context_ids. */ - entry = (MemoryContextId *) hash_search(context_id_lookup, &cur, - HASH_ENTER, &found); + entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &cur, + HASH_ENTER, &found); entry->context_id = context_id++; Assert(!found); @@ -305,3 +345,806 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } + +/* + * pg_get_process_memory_contexts + * Signal a backend or an auxiliary process to send its memory contexts, + * wait for the results and display them. + * + * By default, only superusers or users with ROLE_PG_READ_ALL_STATS are allowed + * to signal a process to return the memory contexts. This is because allowing + * any users to issue this request at an unbounded rate would cause lots of + * requests to be sent, which can lead to denial of service. Additional roles + * can be permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to copy the memory context details + * to a dynamic shared memory space. + * + * We have defined a limit on DSA memory that could be allocated per process - + * if the process has more memory contexts than what can fit in the allocated + * size, the excess contexts are summarized and represented as cumulative total + * at the end of the buffer. + * + * After sending the signal, wait on a condition variable. The publishing + * backend, after copying the data to shared memory, sends signal on that + * condition variable. There is one condition variable per client process. + * Once the condition variable is signalled, check if the latest memory context + * information is available and display. + * + * If the publishing backend does not respond before the condition variable + * times out, which is set to timeout value specified by the user, give up and + * return NULL. + */ +Datum +pg_get_process_memory_contexts(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + bool summary = PG_GETARG_BOOL(1); + double timeout = PG_GETARG_FLOAT8(2); + PGPROC *proc; + ProcNumber procNumber = INVALID_PROC_NUMBER; + bool proc_is_aux = false; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + MemoryStatsEntry *memcxt_info; + MemoryStatsDSHashEntry *entry; + bool found; + char key[CLIENT_KEY_SIZE]; + + if (timeout < 0) + { + /* + * This is just a warning so a loop-through-resultset will not abort + * if one backend terminated on its own during the run. + */ + ereport(WARNING, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("\"timeout\" must not be negative"))); + PG_RETURN_NULL(); + } + + /* + * See if the process with given pid is a backend or an auxiliary process + * and remember the type for when we requery the process later. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + { + proc = AuxiliaryPidGetProc(pid); + proc_is_aux = true; + } + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; this is however not a problem and leave with a WARNING. + * See comment in pg_log_backend_memory_contexts for a discussion on this. + */ + if (proc == NULL) + { + /* + * This is just a warning so a loop-through-resultset will not abort + * if one backend terminated on its own during the run. + */ + ereport(WARNING, + errmsg("PID %d is not a PostgreSQL server process", pid)); + PG_RETURN_NULL(); + } + + InitMaterializedSRF(fcinfo, 0); + + procNumber = GetNumberFromPGProc(proc); + + /* + * Create a DSA to allocate memory for copying memory contexts statistics. + * Allocate the memory in the DSA and send dsa pointer to the server + * process for storing the context statistics. If number of contexts + * exceed a predefined limit(1MB), a cumulative total is stored for such + * contexts. + * + * The DSA is created once for the lifetime of the server, and only + * attached in subsequent calls. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", &found); + + /* + * The dsa pointers containing statistics for each client are stored in a + * dshash table. In addition to dsa pointer, each entry in this table also + * contains information about the statistics, condition variable for + * signalling between client and the server and miscellaneous data + * specific to a request. There is one entry per client request in the + * hash table. + */ + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", &memctx_dsh_params, &found); + + snprintf(key, sizeof(key), "%d", MyProcNumber); + + /* + * Check if the publishing process slot is empty and store this clients + * key i.e its procNumber. This informs the publishing process that it is + * supposed to write statistics in the hash entry corresponding to this + * client. + */ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + if (client_keys[procNumber] == -1) + client_keys[procNumber] = MyProcNumber; + else + { + ereport(WARNING, + errmsg("server process %d is processing previous request", pid)); + LWLockRelease(client_keys_lock); + PG_RETURN_NULL(); + } + LWLockRelease(client_keys_lock); + + /* + * Insert an entry for this client in DSHASH table the first time this + * function is called. This entry is deleted when the process exits in + * before_shmem_exit call. + * + * dshash_find_or_insert locks the entry to prevent the publisher from + * reading before client has updated the entry. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + if (!found) + { + entry->stats_written = false; + ConditionVariableInit(&entry->memcxt_cv); + } + + /* + * Allocate 1MB of memory for the backend to publish its statistics on + * every call to this function. The memory is freed at the end of the + * function. + */ + entry->memstats_dsa_pointer = + dsa_allocate0(MemoryStatsDsaArea, MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND); + + /* + * Specify whether a summary of statistics is requested, before signalling + * the server. + */ + entry->summary = summary; + + /* + * Indicate which server process statistics are being requested from. + * If this client times out before the last requested process can publish its + * statistics, it may send a new request to another server process. Since the + * previous server was notified, it might attempt to read the same client entry + * and respond incorrectly with its statistics. By storing the server ID in the + * client entry, we prevent any previously signalled server process from writing + * its statistics in the space meant for the newly requested process. + */ + entry->server_id = pid; + dshash_release_lock(MemoryStatsDsHash, entry); + + PG_TRY(); + { + /* + * Send a signal to a PostgreSQL process, informing it we want it to + * produce information about its memory contexts. + */ + if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, procNumber) < 0) + { + ereport(WARNING, + errmsg("could not send signal to process %d: %m", pid)); + memstats_dsa_cleanup(entry); + PG_RETURN_NULL(); + } + + while (1) + { + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + Assert(found); + + /* + * We expect to come out of sleep when the requested process has + * finished publishing the statistics, verified using a boolean + * stats_written. + */ + if (entry->stats_written) + break; + + dshash_release_lock(MemoryStatsDsHash, entry); + + /* + * Recheck the state of the backend before sleeping on the + * condition variable to ensure the process is still alive. Only + * check the relevant process type based on the earlier PID check. + */ + if (proc_is_aux) + proc = AuxiliaryPidGetProc(pid); + else + proc = BackendPidGetProc(pid); + + /* + * The process ending during memory context processing is not an + * error. + */ + if (proc == NULL) + { + ereport(WARNING, + errmsg("PID %d is no longer a PostgreSQL server process", + pid)); + memstats_dsa_cleanup(entry); + ConditionVariableCancelSleep(); + PG_RETURN_NULL(); + } + + + /* + * Wait for the timeout as defined by the user. If no statistics + * are available within the allowed time then return NULL. The + * timer is defined in milliseconds since that's what the + * condition variable sleep uses. + */ + if (ConditionVariableTimedSleep(&entry->memcxt_cv, + (timeout * 1000), WAIT_EVENT_MEM_CXT_PUBLISH)) + { + /* Timeout has expired, return NULL */ + memstats_dsa_cleanup(entry); + ConditionVariableCancelSleep(); + PG_RETURN_NULL(); + } + } + + /* + * Backend has finished publishing the stats, project them. + */ + memcxt_info = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + +#define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 11 + for (int i = 0; i < entry->total_stats; i++) + { + ArrayType *path_array; + int path_length; + Datum values[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + bool nulls[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + Datum *path_datum = NULL; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + if (memcxt_info[i].name[0] != '\0') + { + values[0] = CStringGetTextDatum(memcxt_info[i].name); + } + else + nulls[0] = true; + + if (memcxt_info[i].ident[0] != '\0') + { + values[1] = CStringGetTextDatum(memcxt_info[i].ident); + } + else + nulls[1] = true; + + values[2] = CStringGetTextDatum(ContextTypeToString(memcxt_info[i].type)); + + path_length = memcxt_info[i].path_length; + path_datum = (Datum *) palloc(path_length * sizeof(Datum)); + if (memcxt_info[i].path[0] != 0) + { + for (int j = 0; j < path_length; j++) + path_datum[j] = Int32GetDatum(memcxt_info[i].path[j]); + path_array = construct_array_builtin(path_datum, path_length, INT4OID); + values[3] = PointerGetDatum(path_array); + } + else + nulls[3] = true; + + values[4] = Int32GetDatum(memcxt_info[i].levels); + values[5] = Int64GetDatum(memcxt_info[i].totalspace); + values[6] = Int64GetDatum(memcxt_info[i].nblocks); + values[7] = Int64GetDatum(memcxt_info[i].freespace); + values[8] = Int64GetDatum(memcxt_info[i].freechunks); + values[9] = Int64GetDatum(memcxt_info[i].totalspace - + memcxt_info[i].freespace); + values[10] = Int32GetDatum(memcxt_info[i].num_agg_stats); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + memstats_dsa_cleanup(entry); + dshash_release_lock(MemoryStatsDsHash, entry); + + ConditionVariableCancelSleep(); + + } + PG_CATCH(); + { + memstats_dsa_cleanup(entry); + dshash_release_lock(MemoryStatsDsHash, entry); + + ConditionVariableCancelSleep(); + } + PG_END_TRY(); + + PG_RETURN_NULL(); +} + +static void +memstats_dsa_cleanup(MemoryStatsDSHashEntry *entry) +{ + Assert(MemoryStatsDsaArea != NULL); + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + entry->memstats_dsa_pointer = InvalidDsaPointer; + entry->stats_written = false; + entry->server_id = 0; +} +void +MemoryContextKeysShmemInit(void) +{ + bool found; + + client_keys = (int *) + ShmemInitStruct("MemoryContextKeys", + MemoryContextKeysShmemSize() + sizeof(LWLockPadded), &found); + client_keys_lock = (LWLock *) ((char *) client_keys + MemoryContextKeysShmemSize()); + + if (!found) + { + MemSet(client_keys, -1, MemoryContextKeysShmemSize()); + LWLockInitialize(client_keys_lock, LWTRANCHE_MEMORY_CONTEXT_KEYS); + } +} + +Size +MemoryContextKeysShmemSize(void) +{ + Size sz = 0; + Size TotalProcs = 0; + + TotalProcs = add_size(TotalProcs, NUM_AUXILIARY_PROCS); + TotalProcs = add_size(TotalProcs, MaxBackends); + sz = add_size(sz, mul_size(TotalProcs, sizeof(int))); + + return sz; +} + +/* + * HandleGetMemoryContextInterrupt + * Handle receipt of an interrupt indicating a request to publish memory + * contexts statistics. + * + * All the actual work is deferred to ProcessGetMemoryContextInterrupt() as + * this cannot be performed in a signal handler. + */ +void +HandleGetMemoryContextInterrupt(void) +{ + InterruptPending = true; + PublishMemoryContextPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + +/* + * ProcessGetMemoryContextInterrupt + * Generate information about memory contexts used by the process. + * + * Performs a breadth first search on the memory context tree, thus parents + * statistics are reported before their children in the monitoring function + * output. + * + * Statistics for all the processes are shared via the same dynamic shared + * area. Individual statistics are tracked independently in + * per-process DSA pointers. These pointers are stored in a dshash table with + * key as requesting clients ProcNumber. + * + * We calculate maximum number of context's statistics that can be displayed + * using a pre-determined limit for memory available per process for this + * utility and maximum size of statistics for each context. The remaining + * context statistics if any are captured as a cumulative total at the end of + * individual context's statistics. + * + * If summary is true, we capture the level 1 and level 2 contexts + * statistics. For that we traverse the memory context tree recursively in + * depth first search manner to cover all the children of a parent context, to + * be able to display a cumulative total of memory consumption by a parent at + * level 2 and all its children. + */ +void +ProcessGetMemoryContextInterrupt(void) +{ + List *contexts; + HASHCTL ctl; + HTAB *context_id_lookup; + int context_id = 0; + MemoryStatsEntry *meminfo; + bool summary = false; + MemoryContextCounters stat; + int num_individual_stats = 0; + bool found; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + int clientProcNumber; + MemoryContext memstats_ctx = NULL; + MemoryContext oldcontext = NULL; + + PublishMemoryContextPending = false; + + /* + * Create a new memory context which is not a part of TopMemoryContext + * tree. This context is used to allocate all memory in this function. + * This helps in keeping the memory allocation in this function to report + * memory consumption statistics separate. So that it does not affect the + * output of this function. + */ + memstats_ctx = AllocSetContextCreate((MemoryContext) NULL, "publish_memory_context_statistics", + ALLOCSET_SMALL_SIZES); + oldcontext = MemoryContextSwitchTo(memstats_ctx); + + /* + * The hash table is used for constructing "path" column of the view, + * similar to its local backend counterpart. + */ + ctl.keysize = sizeof(MemoryContext); + ctl.entrysize = sizeof(MemoryStatsContextId); + ctl.hcxt = CurrentMemoryContext; + + context_id_lookup = hash_create("pg_get_remote_backend_memory_contexts", + 256, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + /* List of contexts to process in the next round - start at the top. */ + contexts = list_make1(TopMemoryContext); + + /* + * If DSA exists, created by another process requesting statistics, attach + * to it. We expect the client process to create required DSA and Dshash + * table. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", &found); + + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", &memctx_dsh_params, &found); + + /* Retreive the client key for publishing statistics */ + LWLockAcquire(client_keys_lock, LW_SHARED); + Assert(client_keys[MyProcNumber] != -1); + clientProcNumber = client_keys[MyProcNumber]; + LWLockRelease(client_keys_lock); + + snprintf(key, CLIENT_KEY_SIZE, "%d", clientProcNumber); + + /* + * The entry lock is held by dshash_find_or_insert to protect writes to + * process specific memory. Two different processes publishing statistics + * do not block each other. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + /* Entry has been deleted due to client process exit */ + if (!found) + { + entry->stats_written = false; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + return; + } + + /* The client has timed out waiting for us to write statistics */ + if (entry->server_id != MyProcPid) + { + entry->stats_written = false; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + return; + } + + summary = entry->summary; + + /* Should be allocated by a client backend that is requesting statistics */ + Assert(entry->memstats_dsa_pointer != InvalidDsaPointer); + meminfo = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + if (summary) + { + int cxt_id = 0; + List *path = NIL; + MemoryStatsContextId *contextid_entry; + + /* Copy TopMemoryContext statistics to DSA */ + memset(&stat, 0, sizeof(stat)); + (*TopMemoryContext->methods->stats) (TopMemoryContext, NULL, NULL, + &stat, true); + path = lcons_int(1, path); + PublishMemoryContext(meminfo, cxt_id, TopMemoryContext, path, stat, + 1, 100); + + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &TopMemoryContext, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = cxt_id + 1; + + /* + * Copy statistics for each of TopMemoryContexts children. This + * includes statistics of at most 100 children per node, with each + * child node limited to a depth of 100 in its subtree. + */ + for (MemoryContext c = TopMemoryContext->firstchild; c != NULL; + c = c->nextchild) + { + MemoryContextCounters grand_totals; + int num_contexts = 0; + + path = NIL; + memset(&grand_totals, 0, sizeof(grand_totals)); + + cxt_id++; + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &c, + HASH_ENTER, &found); + Assert(!found); + contextid_entry->context_id = cxt_id + 1; + + MemoryContextStatsInternal(c, 1, 100, 100, &grand_totals, + PRINT_STATS_NONE, &num_contexts); + + path = compute_context_path(c, context_id_lookup); + + PublishMemoryContext(meminfo, cxt_id, c, path, + grand_totals, num_contexts, 100); + } + entry->total_stats = cxt_id + 1; + + entry->stats_written = true; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + /* Notify waiting backends and return */ + ConditionVariableBroadcast(&entry->memcxt_cv); + return; + } + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + MemoryStatsContextId *contextid_entry; + + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &cur, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = context_id + 1; + + /* + * Figure out the transient context_id of this context and each of its + * ancestors, to compute a path for this context. + */ + path = compute_context_path(cur, context_id_lookup); + + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + /* Account for saving one statistics slot for cumulative reporting */ + if (context_id < (MAX_MEMORY_CONTEXT_STATS_NUM - 1)) + { + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1, 100); + } + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].totalspace += stat.totalspace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].nblocks += stat.nblocks; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freespace += stat.freespace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit per process is reached, write aggregate of the + * remaining statistics. + * + * We can store contexts from 0 to max_stats - 1. When context_id is + * greater than max_stats, we stop reporting individual statistics + * when context_id equals max_stats - 2. As we use max_stats - 1 array + * slot for reporting cumulative statistics or "Remaining Totals". + */ + if (context_id == (MAX_MEMORY_CONTEXT_STATS_NUM - 2)) + { + int namelen = strlen("Remaining Totals"); + + num_individual_stats = context_id + 1; + strlcpy(meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].name, "Remaining Totals", namelen + 1); + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].ident[0] = '\0'; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].path[0] = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].type = 0; + } + context_id++; + + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + contexts = lappend(contexts, c); + } + + /* + * Statistics are not aggregated, i.e individual statistics reported when + * context_id <= max_stats. + */ + if (context_id <= MAX_MEMORY_CONTEXT_STATS_NUM) + { + entry->total_stats = context_id; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = 1; + } + /* Report number of aggregated memory contexts */ + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = context_id - + num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for cumulative + * statistics. + */ + entry->total_stats = num_individual_stats + 1; + } + entry->stats_written = true; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + /* Notify waiting backends and return */ + ConditionVariableBroadcast(&entry->memcxt_cv); +} + +/* + * Update timestamp and signal all the waiting client backends after copying + * all the statistics. + */ +static void +end_memorycontext_reporting(MemoryStatsDSHashEntry *entry, MemoryContext oldcontext, HTAB *context_id_lookup) +{ + MemoryContext curr_ctx = CurrentMemoryContext; + + dshash_release_lock(MemoryStatsDsHash, entry); + + /* + * Empty this processes slot, so other clients can request memory + * statistics + */ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + client_keys[MyProcNumber] = -1; + LWLockRelease(client_keys_lock); + + hash_destroy(context_id_lookup); + MemoryContextSwitchTo(oldcontext); + MemoryContextReset(curr_ctx); +} + +/* + * compute_context_path + * + * Append the transient context_id of this context and each of its ancestors + * to a list, in order to compute a path. + */ +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + MemoryContext cur_context; + + for (cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryStatsContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + elog(ERROR, "hash table corrupted, can't construct path value"); + + path = lcons_int(cur_entry->context_id, path); + } + + return path; +} + +/* + * PublishMemoryContext + * + * Copy the memory context statistics of a single context to a DSA memory + */ +static void +PublishMemoryContext(MemoryStatsEntry *memcxt_info, int curr_id, + MemoryContext context, List *path, + MemoryContextCounters stat, int num_contexts, int max_levels) +{ + const char *ident = context->ident; + const char *name = context->name; + + /* + * To be consistent with logging output, we label dynahash contexts with + * just the hash table name as with MemoryContextStatsPrint(). + */ + if (context->ident && strncmp(context->name, "dynahash", 8) == 0) + { + name = context->ident; + ident = NULL; + } + + if (name != NULL) + { + int namelen = strlen(name); + + if (strlen(name) >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + namelen = pg_mbcliplen(name, namelen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].name, name, namelen + 1); + } + else + /* Clearing the array */ + memcxt_info[curr_id].name[0] = '\0'; + + /* Trim and copy the identifier if it is not set to NULL */ + if (ident != NULL) + { + int idlen = strlen(context->ident); + + /* + * Some identifiers such as SQL query string can be very long, + * truncate oversize identifiers. + */ + if (idlen >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + idlen = pg_mbcliplen(ident, idlen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].ident, ident, idlen + 1); + } + else + memcxt_info[curr_id].ident[0] = '\0'; + + /* Allocate DSA memory for storing path information */ + if (path == NIL) + memcxt_info[curr_id].path[0] = 0; + else + { + int levels = Min(list_length(path), max_levels); + + memcxt_info[curr_id].path_length = levels; + memcxt_info[curr_id].levels = list_length(path); + + foreach_int(i, path) + { + memcxt_info[curr_id].path[foreach_current_index(i)] = i; + if (--levels == 0) + break; + } + } + memcxt_info[curr_id].type = context->type; + memcxt_info[curr_id].totalspace = stat.totalspace; + memcxt_info[curr_id].nblocks = stat.nblocks; + memcxt_info[curr_id].freespace = stat.freespace; + memcxt_info[curr_id].freechunks = stat.freechunks; + memcxt_info[curr_id].num_agg_stats = num_contexts; +} + +void +AtProcExit_memstats_cleanup(int code, Datum arg) +{ + int idx = MyProcNumber; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + bool found; + + if (MemoryStatsDsHash != NULL) + { + snprintf(key, CLIENT_KEY_SIZE, "%d", idx); + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + if (found) + { + if (MemoryStatsDsaArea != NULL && + DsaPointerIsValid(entry->memstats_dsa_pointer)) + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + } + dshash_delete_entry(MemoryStatsDsHash, entry); + } + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + client_keys[idx] = -1; + LWLockRelease(client_keys_lock); +} diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c index 97c2ac1faf9..ab768a7a91f 100644 --- a/src/backend/utils/adt/pg_locale.c +++ b/src/backend/utils/adt/pg_locale.c @@ -45,7 +45,6 @@ #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/pg_locale.h" -#include "utils/relcache.h" #include "utils/syscache.h" #ifdef WIN32 diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index d31cb45a058..92b0446b80c 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -39,6 +39,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t PublishMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index 641e535a73c..fb3f2d21fa0 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -662,6 +662,13 @@ BaseInit(void) * drop ephemeral slots, which in turn triggers stats reporting. */ ReplicationSlotInitialize(); + + /* + * The before shmem exit callback frees the DSA memory occupied by the + * latest memory context statistics that could be published by this proc + * if requested. + */ + before_shmem_exit(AtProcExit_memstats_cleanup, 0); } diff --git a/src/backend/utils/mb/mbutils.c b/src/backend/utils/mb/mbutils.c index 886ecbad871..308016d7763 100644 --- a/src/backend/utils/mb/mbutils.c +++ b/src/backend/utils/mb/mbutils.c @@ -39,7 +39,6 @@ #include "mb/pg_wchar.h" #include "utils/fmgrprotos.h" #include "utils/memutils.h" -#include "utils/relcache.h" #include "varatt.h" /* diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 47fd774c7d2..3a5422c7273 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -40,6 +40,7 @@ #include "mb/pg_wchar.h" #include "miscadmin.h" +#include "utils/hsearch.h" #include "utils/memdebug.h" #include "utils/memutils.h" #include "utils/memutils_internal.h" @@ -176,10 +177,6 @@ MemoryContext PortalContext = NULL; static void MemoryContextDeleteOnly(MemoryContext context); static void MemoryContextCallResetCallbacks(MemoryContext context); -static void MemoryContextStatsInternal(MemoryContext context, int level, - int max_level, int max_children, - MemoryContextCounters *totals, - bool print_to_stderr); static void MemoryContextStatsPrint(MemoryContext context, void *passthru, const char *stats_string, bool print_to_stderr); @@ -877,11 +874,19 @@ MemoryContextStatsDetail(MemoryContext context, bool print_to_stderr) { MemoryContextCounters grand_totals; + int num_contexts; + PrintDestination print_location; memset(&grand_totals, 0, sizeof(grand_totals)); + if (print_to_stderr) + print_location = PRINT_STATS_TO_STDERR; + else + print_location = PRINT_STATS_TO_LOGS; + + /* num_contexts report number of contexts aggregated in the output */ MemoryContextStatsInternal(context, 1, max_level, max_children, - &grand_totals, print_to_stderr); + &grand_totals, print_location, &num_contexts); if (print_to_stderr) fprintf(stderr, @@ -916,13 +921,14 @@ MemoryContextStatsDetail(MemoryContext context, * One recursion level for MemoryContextStats * * Print stats for this context if possible, but in any case accumulate counts - * into *totals (if not NULL). + * into *totals (if not NULL). The callers should make sure that print_location + * is set to PRINT_STATS_TO_STDERR or PRINT_STATS_TO_LOGS or PRINT_STATS_NONE. */ -static void +void MemoryContextStatsInternal(MemoryContext context, int level, int max_level, int max_children, MemoryContextCounters *totals, - bool print_to_stderr) + PrintDestination print_location, int *num_contexts) { MemoryContext child; int ichild; @@ -930,10 +936,39 @@ MemoryContextStatsInternal(MemoryContext context, int level, Assert(MemoryContextIsValid(context)); /* Examine the context itself */ - context->methods->stats(context, - MemoryContextStatsPrint, - &level, - totals, print_to_stderr); + switch (print_location) + { + case PRINT_STATS_TO_STDERR: + context->methods->stats(context, + MemoryContextStatsPrint, + &level, + totals, true); + break; + + case PRINT_STATS_TO_LOGS: + context->methods->stats(context, + MemoryContextStatsPrint, + &level, + totals, false); + break; + + case PRINT_STATS_NONE: + + /* + * Do not print the statistics if print_location is + * PRINT_STATS_NONE, only compute totals. This is used in + * reporting of memory context statistics via a sql function. Last + * parameter is not relevant. + */ + context->methods->stats(context, + NULL, + NULL, + totals, false); + break; + } + + /* Increment the context count for each of the recursive call */ + *num_contexts = *num_contexts + 1; /* * Examine children. @@ -953,7 +988,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, MemoryContextStatsInternal(child, level + 1, max_level, max_children, totals, - print_to_stderr); + print_location, num_contexts); } } @@ -972,7 +1007,13 @@ MemoryContextStatsInternal(MemoryContext context, int level, child = MemoryContextTraverseNext(child, context); } - if (print_to_stderr) + /* + * Add the count of children contexts which are traversed in the + * non-recursive manner. + */ + *num_contexts = *num_contexts + ichild; + + if (print_location == PRINT_STATS_TO_STDERR) { for (int i = 0; i < level; i++) fprintf(stderr, " "); @@ -985,7 +1026,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, local_totals.freechunks, local_totals.totalspace - local_totals.freespace); } - else + else if (print_location == PRINT_STATS_TO_LOGS) ereport(LOG_SERVER_ONLY, (errhidestmt(true), errhidecontext(true), diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 118d6da1ace..3d6f42606fd 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8597,6 +8597,16 @@ prorettype => 'bool', proargtypes => 'int4', prosrc => 'pg_log_backend_memory_contexts' }, +# publishing memory contexts of the specified postgres process +{ oid => '2173', descr => 'publish memory contexts of the specified backend', + proname => 'pg_get_process_memory_contexts', provolatile => 'v', + prorows => '100', proretset => 't', proparallel => 'r', + prorettype => 'record', proargtypes => 'int4 bool float8', + proallargtypes => '{int4,bool,float8,text,text,text,_int4,int4,int8,int8,int8,int8,int8,int4}', + proargmodes => '{i,i,i,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{pid, summary, timeout, name, ident, type, path, level, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes, num_agg_contexts}', + prosrc => 'pg_get_process_memory_contexts' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index 1bef98471c3..1e59a7f910f 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -96,6 +96,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t PublishMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h index 208d2e3a8ed..72ace053e9d 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -135,3 +135,4 @@ PG_LWLOCKTRANCHE(SUBTRANS_SLRU, SubtransSLRU) PG_LWLOCKTRANCHE(XACT_SLRU, XactSLRU) PG_LWLOCKTRANCHE(PARALLEL_VACUUM_DSA, ParallelVacuumDSA) PG_LWLOCKTRANCHE(AIO_URING_COMPLETION, AioUringCompletion) +PG_LWLOCKTRANCHE(MEMORY_CONTEXT_KEYS, MemoryContextReportingKeys) diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index afeeb1ca019..345d5a0ecb1 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -35,6 +35,7 @@ typedef enum PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to send the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ /* Recovery conflict reasons */ diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 8abc26abce2..799cfe38cab 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -18,7 +18,10 @@ #define MEMUTILS_H #include "nodes/memnodes.h" - +#include "storage/condition_variable.h" +#include "storage/lmgr.h" +#include "utils/dsa.h" +#include "lib/dshash.h" /* * MaxAllocSize, MaxAllocHugeSize @@ -48,6 +51,23 @@ #define AllocHugeSizeIsValid(size) ((Size) (size) <= MaxAllocHugeSize) +/* + * Memory Context reporting size limits. + */ + +/* Max length of context name and ident */ +#define MEMORY_CONTEXT_IDENT_SHMEM_SIZE 64 +/* Maximum size (in bytes) of DSA area per process */ +#define MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND ((size_t) (1 * 1024 * 1024)) + +/* + * Maximum size per context. Actual size may be lower as this assumes the worst + * case of deepest path and longest identifiers (name and ident, thus the + * multiplication by 2). The path depth is limited to 100 like for memory + * context logging. + */ +#define MAX_MEMORY_CONTEXT_STATS_SIZE (sizeof(MemoryStatsEntry)) +#define MAX_MEMORY_CONTEXT_STATS_NUM MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND / MAX_MEMORY_CONTEXT_STATS_SIZE /* * Standard top-level memory contexts. @@ -319,4 +339,74 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) return true; } +/* Dynamic shared memory state for statistics per context */ +typedef struct MemoryStatsEntry +{ + char name[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + char ident[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + int path[100]; + NodeTag type; + int path_length; + int levels; + int64 totalspace; + int64 nblocks; + int64 freespace; + int64 freechunks; + int num_agg_stats; +} MemoryStatsEntry; + +/* + * Per backend dynamic shared hash entry for memory context statistics + * reporting. + */ +typedef struct MemoryStatsDSHashEntry +{ + char key[64]; + ConditionVariable memcxt_cv; + bool stats_written; + int server_id; + int total_stats; + bool summary; + dsa_pointer memstats_dsa_pointer; +} MemoryStatsDSHashEntry; + +static const dshash_parameters memctx_dsh_params = { + offsetof(MemoryStatsDSHashEntry, memcxt_cv), + sizeof(MemoryStatsDSHashEntry), + dshash_strcmp, + dshash_strhash, + dshash_strcpy +}; + +/* + * Used for storage of transient identifiers for pg_get_backend_memory_contexts + */ +typedef struct MemoryStatsContextId +{ + MemoryContext context; + int context_id; +} MemoryStatsContextId; + +/* + * This is passed to MemoryContextStatsInternal to determine whether + * to print context statistics or not and where to print them logs or + * stderr. + */ +typedef enum PrintDestination +{ + PRINT_STATS_TO_STDERR = 0, + PRINT_STATS_TO_LOGS, + PRINT_STATS_NONE +} PrintDestination; + +extern void ProcessGetMemoryContextInterrupt(void); +extern void HandleGetMemoryContextInterrupt(void); +extern void MemoryContextKeysShmemInit(void); +extern Size MemoryContextKeysShmemSize(void); +extern void MemoryContextStatsInternal(MemoryContext context, int level, + int max_level, int max_children, + MemoryContextCounters *totals, + PrintDestination print_location, + int *num_contexts); +extern void AtProcExit_memstats_cleanup(int code, Datum arg); #endif /* MEMUTILS_H */ diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 83228cfca29..ae17d028ed3 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -232,3 +232,22 @@ select * from pg_timezone_abbrevs where abbrev = 'LMT'; LMT | @ 7 hours 52 mins 58 secs ago | f (1 row) +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; +NOTICE: (AllocSet,TopMemoryContext,) +NOTICE: (AllocSet,TopMemoryContext,) diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 66179f026b3..d0917b6868e 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -101,3 +101,21 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs; -- One specific case we can check without much fear of breakage -- is the historical local-mean-time value used for America/Los_Angeles. select * from pg_timezone_abbrevs where abbrev = 'LMT'; + +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index e4a9ec65ab4..e456af77206 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1680,6 +1680,9 @@ MemoryContextData MemoryContextId MemoryContextMethodID MemoryContextMethods +MemoryStatsContextId +MemoryStatsEntry +MemoryStatsDSHashEntry MemoryStatsPrintFunc MergeAction MergeActionState -- 2.34.1 ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-10-07 00:51 torikoshia <[email protected]> parent: Rahila Syed <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: torikoshia @ 2025-10-07 00:51 UTC (permalink / raw) To: Rahila Syed <[email protected]>; +Cc: pgsql-hackers; Daniel Gustafsson <[email protected]> On 2025-08-20 06:42, Rahila Syed wrote: > PFA the fix. Thanks for updating the patch! Specifying a very small timeout value (such as 0 or 0.0001) and repeatedly executing the function seems to cause unexpected behavior. In some cases, it even leads to a crash. For example: (session1)=# select pg_backend_pid(); pg_backend_pid ---------------- 50917 (session2)=# select pg_get_process_memory_contexts(50917, true, 0.0001); pg_get_process_memory_contexts -------------------------------- (0 rows) (session2)=# \watch 0.01 pg_get_process_memory_contexts -------------------------------- (,,???,,0,0,0,0,0,0,0) ... (21 rows) (session2)=# \watch 0.01 pg_get_process_memory_contexts -------------------------------- (0 rows) ... server closed the connection unexpectedly This probably means the server terminated abnormally before or while processing the request. The connection to the server was lost. Attempting reset: Failed. This issue occurs on my M1 Mac, but I couldn’t reproduce it on Ubuntu, so it might be environment-dependent. Looking at the logs, Assert() is failing: 2025-10-07 08:48:26.766 JST [local] psql [23626] WARNING: 01000: server process 23646 is processing previous request 2025-10-07 08:48:26.766 JST [local] psql [23626] LOCATION: pg_get_process_memory_contexts, mcxtfuncs.c:476 TRAP: failed Assert("victim->magic == FREE_PAGE_SPAN_LEADER_MAGIC"), File: "freepage.c", Line: 1379, PID: 23626 0 postgres 0x000000010357fdf4 ExceptionalCondition + 216 1 postgres 0x00000001035cbe18 FreePageManagerGetInternal + 684 2 postgres 0x00000001035cbb18 FreePageManagerGet + 40 3 postgres 0x00000001035c84cc dsa_allocate_extended + 788 4 postgres 0x0000000103453af0 pg_get_process_memory_contexts + 992 5 postgres 0x0000000103007e94 ExecMakeFunctionResultSet + 616 6 postgres 0x00000001030506b8 ExecProjectSRF + 304 7 postgres 0x0000000103050434 ExecProjectSet + 268 8 postgres 0x0000000103003270 ExecProcNodeFirst + 92 9 postgres 0x0000000102ffa398 ExecProcNode + 60 10 postgres 0x0000000102ff5050 ExecutePlan + 244 11 postgres 0x0000000102ff4ee0 standard_ExecutorRun + 456 12 postgres 0x0000000102ff4d08 ExecutorRun + 84 13 postgres 0x0000000103341c84 PortalRunSelect + 296 14 postgres 0x0000000103341694 PortalRun + 656 15 postgres 0x000000010333c4bc exec_simple_query + 1388 16 postgres 0x000000010333b5d0 PostgresMain + 3252 17 postgres 0x0000000103332750 BackendInitialize + 0 18 postgres 0x0000000103209e48 postmaster_child_launch + 456 19 postgres 0x00000001032118c8 BackendStartup + 304 20 postgres 0x000000010320f72c ServerLoop + 372 21 postgres 0x000000010320e1e4 PostmasterMain + 6448 22 postgres 0x0000000103094b0c main + 924 23 dyld 0x0000000199dc2b98 start + 6076 Could you please check if you can reproduce this crash on your environment? And a few minor comments on the patch itself: > + <parameter>stats_timestamp</parameter> > <type>timestamptz</type> ) As discussed earlier, I believe we decided to remove stats_timestamp, but it seems it’s still mentioned here. > + * Update timestamp and signal all the waiting client backends after > copying > + * all the statistics. > + */ > +static void > +end_memorycontext_reporting(MemoryStatsDSHashEntry *entry, > MemoryContext oldcontext, HTAB *context_id_lookup) Should “Update timestamp” in this comment also be removed for consistency? The column order differs slightly from pg_backend_memory_contexts. If there’s no strong reason for the difference, perhaps aligning the order might improve consistency: =# select * from pg_get_process_memory_contexts(pg_backend_pid(), true, 1) ; name | TopMemoryContext ident | [NULL] type | AllocSet path | {1} level | 1 total_bytes | 222400 =# select * from pg_backend_memory_contexts; name | TopMemoryContext ident | [NULL] type | AllocSet level | 1 path | {1} total_bytes | 99232 ... Regards, -- Atsushi Torikoshi Seconded from NTT DATA Japan Corporation to SRA OSS K.K. ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-10-09 08:43 Rahila Syed <[email protected]> parent: torikoshia <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Rahila Syed @ 2025-10-09 08:43 UTC (permalink / raw) To: torikoshia <[email protected]>; +Cc: pgsql-hackers; Daniel Gustafsson <[email protected]> Hi Torikoshia, Thank you for testing and reviewing the patch. > > This issue occurs on my M1 Mac, but I couldn’t reproduce it on Ubuntu, > so it might be environment-dependent. > Looking at the logs, Assert() is failing: > > 2025-10-07 08:48:26.766 JST [local] psql [23626] WARNING: 01000: > server process 23646 is processing previous request > 2025-10-07 08:48:26.766 JST [local] psql [23626] LOCATION: > pg_get_process_memory_contexts, mcxtfuncs.c:476 > TRAP: failed Assert("victim->magic == FREE_PAGE_SPAN_LEADER_MAGIC"), > File: "freepage.c", Line: 1379, PID: 23626 > 0 postgres 0x000000010357fdf4 > ExceptionalCondition + 216 > 1 postgres 0x00000001035cbe18 > FreePageManagerGetInternal + 684 > 2 postgres 0x00000001035cbb18 > FreePageManagerGet + 40 > 3 postgres 0x00000001035c84cc > dsa_allocate_extended + 788 > 4 postgres 0x0000000103453af0 > pg_get_process_memory_contexts + 992 > 5 postgres 0x0000000103007e94 > ExecMakeFunctionResultSet + 616 > 6 postgres 0x00000001030506b8 > ExecProjectSRF + 304 > 7 postgres 0x0000000103050434 > ExecProjectSet + 268 > 8 postgres 0x0000000103003270 > ExecProcNodeFirst + 92 > 9 postgres 0x0000000102ffa398 > ExecProcNode + 60 > 10 postgres 0x0000000102ff5050 ExecutePlan > + 244 > 11 postgres 0x0000000102ff4ee0 > standard_ExecutorRun + 456 > 12 postgres 0x0000000102ff4d08 ExecutorRun > + 84 > 13 postgres 0x0000000103341c84 > PortalRunSelect + 296 > 14 postgres 0x0000000103341694 PortalRun + > 656 > 15 postgres 0x000000010333c4bc > exec_simple_query + 1388 > 16 postgres 0x000000010333b5d0 > PostgresMain + 3252 > 17 postgres 0x0000000103332750 > BackendInitialize + 0 > 18 postgres 0x0000000103209e48 > postmaster_child_launch + 456 > 19 postgres 0x00000001032118c8 > BackendStartup + 304 > 20 postgres 0x000000010320f72c ServerLoop > + 372 > 21 postgres 0x000000010320e1e4 > PostmasterMain + 6448 > 22 postgres 0x0000000103094b0c main + 924 > 23 dyld 0x0000000199dc2b98 start + > 6076 > > > Could you please check if you can reproduce this crash on your > environment? > > I haven't been able to reproduce this issue on Ubuntu. A colleague also tested it on their Mac and didn't encounter the problem. I do have a fix in this area that I believe should address an edge case where data might be written to freed DSA memory. Kindly test using the v35 patch and let me know if you still see the issue. > > As discussed earlier, I believe we decided to remove stats_timestamp, > but it seems it’s still mentioned here. > > > Fixed. > > + * Update timestamp and signal all the waiting client backends after > > copying > > + * all the statistics. > > + */ > > +static void > > +end_memorycontext_reporting(MemoryStatsDSHashEntry *entry, > > MemoryContext oldcontext, HTAB *context_id_lookup) > > Should “Update timestamp” in this comment also be removed for > consistency? > > Fixed. > > The column order differs slightly from pg_backend_memory_contexts. > If there’s no strong reason for the difference, perhaps aligning the > order might improve consistency: > > Makes sense. I will fix this in the next iteration of the patch. I am also attaching a test which implements crash testing using injection points. I plan to improve the tests further to increase the test coverage of the feature. Thank you, Rahila Syed Attachments: [application/octet-stream] 0001-v35-0001-Add-pg_get_process_memory_context-function.patch (61.7K, ../../CAH2L28tACFygy9oXbTA8pPQ6KE8uDiO9CgAhku7ERNpdj1YRTw@mail.gmail.com/3-0001-v35-0001-Add-pg_get_process_memory_context-function.patch) download | inline diff: From 57a3627b90b4e05d5d15ddef1edad280296120ab Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Tue, 7 Oct 2025 19:38:53 +0530 Subject: [PATCH 1/2] Add pg_get_process_memory_context function This adds a function for retrieving memory context statistics and information from backends as well as auxiliary processes. The intended usecase is cluster debugging when under memory pressure or unanticipated memory usage characteristics. When calling the function it sends a signal to the specified process to submit statistics regarding its memory contexts into dynamic shared memory. Each memory context is returned in detail, followed by a cumulative total in case the number of contexts exceed the max allocated amount of shared memory. Each process is limited to use at most 1Mb memory for this. A summary can also be explicitly requested by the user, this will return the TopMemoryContext and a cumulative total of all lower contexts. In order to not block on busy processes the caller specifies the number of seconds during which to retry before timing out. In the case where no statistics are published within the set timeout, NULL is returned. --- doc/src/sgml/func/func-admin.sgml | 163 ++++ src/backend/catalog/system_views.sql | 5 + src/backend/postmaster/autovacuum.c | 4 + src/backend/postmaster/checkpointer.c | 4 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 4 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 4 + src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/storage/lmgr/proc.c | 1 + src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 2 + src/backend/utils/adt/mcxtfuncs.c | 907 +++++++++++++++++- src/backend/utils/adt/pg_locale.c | 1 - src/backend/utils/init/globals.c | 1 + src/backend/utils/init/postinit.c | 7 + src/backend/utils/mb/mbutils.c | 1 - src/backend/utils/mmgr/mcxt.c | 71 +- src/include/catalog/pg_proc.dat | 10 + src/include/miscadmin.h | 1 + src/include/storage/lwlocklist.h | 1 + src/include/storage/procsignal.h | 1 + src/include/utils/memutils.h | 92 +- src/test/regress/expected/sysviews.out | 19 + src/test/regress/sql/sysviews.sql | 18 + src/tools/pgindent/typedefs.list | 3 + 27 files changed, 1296 insertions(+), 41 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 1b465bc8ba7..a9d60202a57 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -251,6 +251,136 @@ <literal>false</literal> is returned. </para></entry> </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_get_process_memory_contexts</primary> + </indexterm> + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type>, <parameter>summary</parameter> <type>boolean</type>, <parameter>timeout</parameter> <type>float</type> ) + <returnvalue>setof record</returnvalue> + ( <parameter>name</parameter> <type>text</type>, + <parameter>ident</parameter> <type>text</type>, + <parameter>type</parameter> <type>text</type>, + <parameter>path</parameter> <type>integer[]</type>, + <parameter>level</parameter> <type>integer</type>, + <parameter>total_bytes</parameter> <type>bigint</type>, + <parameter>total_nblocks</parameter> <type>bigint</type>, + <parameter>free_bytes</parameter> <type>bigint</type>, + <parameter>free_chunks</parameter> <type>bigint</type>, + <parameter>used_bytes</parameter> <type>bigint</type>, + <parameter>num_agg_contexts</parameter> <type>integer</type> ) + </para> + <para> + This function handles requests to display the memory contexts of a + <productname>PostgreSQL</productname> process with the specified + process ID. The function can be used to send requests to backends as + well as <glossterm linkend="glossary-auxiliary-proc">auxiliary processes</glossterm>. + </para> + <para> + The returned record contains extended statistics per each memory + context: + <itemizedlist spacing="compact"> + <listitem> + <para> + <parameter>name</parameter> - The name of the memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>ident</parameter> - Memory context ID (if any). + </para> + </listitem> + <listitem> + <para> + <parameter>type</parameter> - The type of memory context, possible + values are: AllocSet, Generation, Slab and Bump. + </para> + </listitem> + <listitem> + <para> + <parameter>path</parameter> - Memory contexts are organized in a + tree model with TopMemoryContext as the root, and all other memory + contexts as nodes in the tree. The <parameter>path</parameter> + displays the path from the root to the current memory context. The + path is limited to 100 children per node, which each node limited + to a max depth of 100, to preserve memory during reporting. The + printed path will also be limited to 100 nodes counting from the + TopMemoryContext. + </para> + </listitem> + <listitem> + <para> + <parameter>level</parameter> - The level in the tree of the current + memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_bytes</parameter> - The total number of bytes + allocated to this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_nblocks</parameter> - The total number of blocks + used for the allocated memory. + </para> + </listitem> + <listitem> + <para> + <parameter>free_bytes</parameter> - The amount of free memory in + this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>free_chunks</parameter> - The number of chunks that + <parameter>free_bytes</parameter> corresponds to. + </para> + </listitem> + <listitem> + <para> + <parameter>used_bytes</parameter> - The total number of bytes + currently occupied. + </para> + </listitem> + <listitem> + <para> + <parameter>num_agg_contexts</parameter> - The number of memory + contexts aggregated in the displayed statistics. + </para> + </listitem> + </itemizedlist> + </para> + <para> + When <parameter>summary</parameter> is <literal>true</literal>, statistics + for memory contexts at levels 1 and 2 are displayed, with level 1 + representing the root node (i.e., <literal>TopMemoryContext</literal>). + Statistics for contexts on level 2 and below are aggregates of all + child contexts' statistics, where <literal>num_agg_contexts</literal> + indicate the number aggregated child contexts. When + <parameter>summary</parameter> is <literal>false</literal>, + <literal>the num_agg_contexts</literal> value is <literal>1</literal>, + indicating that individual statistics are being displayed. + </para> + <para> + Busy processes can delay reporting memory context statistics, + <parameter>timeout</parameter> specifies the number of seconds + to wait for updated statistics. <parameter>timeout</parameter> can be + specified in fractions of a second. + </para> + <para> + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context + statistics are aggregated and a cumulative total is displayed. The + <literal>num_agg_contexts</literal> column indicates the number of + contexts aggregated in the displayed statistics. When + <literal>num_agg_contexts</literal> is <literal>1</literal> it means + that the context statistics are displayed separately. + </para></entry> + </row> </tbody> </tgroup> </table> @@ -302,6 +432,39 @@ LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 because it may generate a large number of log messages. </para> + <para> + <function>pg_get_process_memory_contexts</function> can be used to request + memory contexts statistics of any <productname>PostgreSQL</productname> + process. For example: +<programlisting> +postgres=# SELECT * FROM pg_get_process_memory_contexts( + (SELECT pid FROM pg_stat_activity + WHERE backend_type = 'checkpointer'), + false, 0.5) LIMIT 1; +-[ RECORD 1 ]----+------------------------------ +name | TopMemoryContext +ident | +type | AllocSet +path | {1} +level | 1 +total_bytes | 90304 +total_nblocks | 3 +free_bytes | 2880 +free_chunks | 1 +used_bytes | 87424 +num_agg_contexts | 1 +</programlisting> + <note> + <para> + While <function>pg_get_process_memory_contexts</function> can be used to + query memory contexts of the local backend, + <structname>pg_backend_memory_contexts</structname> + (see <xref linkend="view-pg-backend-memory-contexts"/> for more details) + will be less resource intensive when only the local backend is of interest. + </para> + </note> + </para> + </sect2> <sect2 id="functions-admin-backup"> diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index 884b6a23817..41d80661320 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -682,6 +682,11 @@ GRANT SELECT ON pg_backend_memory_contexts TO pg_read_all_stats; REVOKE EXECUTE ON FUNCTION pg_get_backend_memory_contexts() FROM PUBLIC; GRANT EXECUTE ON FUNCTION pg_get_backend_memory_contexts() TO pg_read_all_stats; +REVOKE EXECUTE ON FUNCTION + pg_get_process_memory_contexts(integer, boolean, float) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION + pg_get_process_memory_contexts(integer, boolean, float) TO pg_read_all_stats; + -- Statistics views CREATE VIEW pg_stat_all_tables AS diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index fb5d3b27224..16092f619b2 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -790,6 +790,10 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index e84e8663e96..5b3e08805bf 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -679,6 +679,10 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index ba63b84dfc5..29454b8bf8a 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -48,6 +48,10 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index 78e39e5f866..ac97a39447c 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -867,6 +867,10 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index 27e86cf393f..7149a67fcbc 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -192,6 +192,10 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index e1f142f20c7..c711c887ef6 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -879,6 +879,10 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index 2fa045e6b0f..fe3d32e40b0 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -51,6 +51,7 @@ #include "storage/sinvaladt.h" #include "utils/guc.h" #include "utils/injection_point.h" +#include "utils/memutils.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -150,6 +151,7 @@ CalculateShmemSize(int *num_semaphores) size = add_size(size, InjectionPointShmemSize()); size = add_size(size, SlotSyncShmemSize()); size = add_size(size, AioShmemSize()); + size = add_size(size, MemoryContextKeysShmemSize() + sizeof(LWLockPadded)); /* include additional requested shmem from preload libraries */ size = add_size(size, total_addin_request); @@ -343,6 +345,7 @@ CreateOrAttachShmemStructs(void) WaitEventCustomShmemInit(); InjectionPointShmemInit(); AioShmemInit(); + MemoryContextKeysShmemInit(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index 087821311cc..8963285cc12 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -691,6 +691,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_GET_MEMORY_CONTEXT)) + HandleGetMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 96f29aafc39..550a3a77bb8 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -50,6 +50,7 @@ #include "storage/procsignal.h" #include "storage/spin.h" #include "storage/standby.h" +#include "utils/memutils.h" #include "utils/timeout.h" #include "utils/timestamp.h" diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index d356830f756..0fdd2202ddd 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3538,6 +3538,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 7553f6eacef..cdbc7309206 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -160,6 +160,7 @@ WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." +MEM_CXT_PUBLISH "Waiting for a process to publish memory information." ABI_compatibility: @@ -401,6 +402,7 @@ SubtransSLRU "Waiting to access the sub-transaction SLRU cache." XactSLRU "Waiting to access the transaction status SLRU cache." ParallelVacuumDSA "Waiting for parallel vacuum dynamic shared memory allocation." AioUringCompletion "Waiting for another process to complete IO via io_uring." +MemoryContextReportingKeys "Waiting for another process to complete reading or writing the memory reporting keys." # No "ABI_compatibility" region here as WaitEventLWLock has its own C code. diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index fe6dce9cba3..17b50e853d5 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -15,13 +15,38 @@ #include "postgres.h" +#include "access/twophase.h" +#include "catalog/pg_authid_d.h" #include "funcapi.h" #include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "storage/dsm_registry.h" #include "storage/proc.h" #include "storage/procarray.h" +#include "utils/acl.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/memutils.h" +#include "utils/wait_event_types.h" + +#define CLIENT_KEY_SIZE 64 + +static LWLock *client_keys_lock = NULL; +static int *client_keys = NULL; +static dshash_table *MemoryStatsDsHash = NULL; +static dsa_area *MemoryStatsDsaArea = NULL; + +static void memstats_dsa_cleanup(char *key); +static const char *ContextTypeToString(NodeTag type); +static void PublishMemoryContext(MemoryStatsEntry *memcxt_info, + int curr_id, MemoryContext context, + List *path, + MemoryContextCounters stat, + int num_contexts, int max_levels); +static List *compute_context_path(MemoryContext c, HTAB *context_id_lookup); +static void end_memorycontext_reporting(MemoryStatsDSHashEntry *entry, MemoryContext oldcontext, + HTAB *context_id_lookup); /* ---------- * The max bytes for showing identifiers of MemoryContext. @@ -89,7 +114,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, */ for (MemoryContext cur = context; cur != NULL; cur = cur->parent) { - MemoryContextId *entry; + MemoryStatsContextId *entry; bool found; entry = hash_search(context_id_lookup, &cur, HASH_FIND, &found); @@ -143,24 +168,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, else nulls[1] = true; - switch (context->type) - { - case T_AllocSetContext: - type = "AllocSet"; - break; - case T_GenerationContext: - type = "Generation"; - break; - case T_SlabContext: - type = "Slab"; - break; - case T_BumpContext: - type = "Bump"; - break; - default: - type = "???"; - break; - } + type = ContextTypeToString(context->type); values[2] = CStringGetTextDatum(type); values[3] = Int32GetDatum(list_length(path)); /* level */ @@ -175,6 +183,38 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, list_free(path); } +/* + * ContextTypeToString + * Returns a textual representation of a context type + * + * This should cover the same types as MemoryContextIsValid. + */ +const char * +ContextTypeToString(NodeTag type) +{ + const char *context_type; + + switch (type) + { + case T_AllocSetContext: + context_type = "AllocSet"; + break; + case T_GenerationContext: + context_type = "Generation"; + break; + case T_SlabContext: + context_type = "Slab"; + break; + case T_BumpContext: + context_type = "Bump"; + break; + default: + context_type = "???"; + break; + } + return context_type; +} + /* * pg_get_backend_memory_contexts * SQL SRF showing backend memory context. @@ -189,7 +229,7 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) HTAB *context_id_lookup; ctl.keysize = sizeof(MemoryContext); - ctl.entrysize = sizeof(MemoryContextId); + ctl.entrysize = sizeof(MemoryStatsContextId); ctl.hcxt = CurrentMemoryContext; context_id_lookup = hash_create("pg_get_backend_memory_contexts", @@ -216,7 +256,7 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) foreach_ptr(MemoryContextData, cur, contexts) { - MemoryContextId *entry; + MemoryStatsContextId *entry; bool found; /* @@ -224,8 +264,8 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) * PutMemoryContextsStatsTupleStore needs this to populate the "path" * column with the parent context_ids. */ - entry = (MemoryContextId *) hash_search(context_id_lookup, &cur, - HASH_ENTER, &found); + entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &cur, + HASH_ENTER, &found); entry->context_id = context_id++; Assert(!found); @@ -305,3 +345,824 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } + +/* + * pg_get_process_memory_contexts + * Signal a backend or an auxiliary process to send its memory contexts, + * wait for the results and display them. + * + * By default, only superusers or users with ROLE_PG_READ_ALL_STATS are allowed + * to signal a process to return the memory contexts. This is because allowing + * any users to issue this request at an unbounded rate would cause lots of + * requests to be sent, which can lead to denial of service. Additional roles + * can be permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to copy the memory context details + * to a dynamic shared memory space. + * + * We have defined a limit on DSA memory that could be allocated per process - + * if the process has more memory contexts than what can fit in the allocated + * size, the excess contexts are summarized and represented as cumulative total + * at the end of the buffer. + * + * After sending the signal, wait on a condition variable. The publishing + * backend, after copying the data to shared memory, sends signal on that + * condition variable. There is one condition variable per client process. + * Once the condition variable is signalled, check if the latest memory context + * information is available and display. + * + * If the publishing backend does not respond before the condition variable + * times out, which is set to timeout value specified by the user, give up and + * return NULL. + */ +Datum +pg_get_process_memory_contexts(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + bool summary = PG_GETARG_BOOL(1); + double timeout = PG_GETARG_FLOAT8(2); + PGPROC *proc; + ProcNumber procNumber = INVALID_PROC_NUMBER; + bool proc_is_aux = false; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + MemoryStatsEntry *memcxt_info; + MemoryStatsDSHashEntry *entry; + bool found; + char key[CLIENT_KEY_SIZE]; + + if (timeout < 0) + { + /* + * This is just a warning so a loop-through-resultset will not abort + * if one backend terminated on its own during the run. + */ + ereport(WARNING, + (errcode(ERRCODE_NUMERIC_VALUE_OUT_OF_RANGE), + errmsg("\"timeout\" must not be negative"))); + PG_RETURN_NULL(); + } + + /* + * See if the process with given pid is a backend or an auxiliary process + * and remember the type for when we requery the process later. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + { + proc = AuxiliaryPidGetProc(pid); + proc_is_aux = true; + } + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; this is however not a problem and leave with a WARNING. + * See comment in pg_log_backend_memory_contexts for a discussion on this. + */ + if (proc == NULL) + { + /* + * This is just a warning so a loop-through-resultset will not abort + * if one backend terminated on its own during the run. + */ + ereport(WARNING, + errmsg("PID %d is not a PostgreSQL server process", pid)); + PG_RETURN_NULL(); + } + + InitMaterializedSRF(fcinfo, 0); + + procNumber = GetNumberFromPGProc(proc); + + /* + * Create a DSA to allocate memory for copying memory contexts statistics. + * Allocate the memory in the DSA and send dsa pointer to the server + * process for storing the context statistics. If number of contexts + * exceed a predefined limit(1MB), a cumulative total is stored for such + * contexts. + * + * The DSA is created once for the lifetime of the server, and only + * attached in subsequent calls. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", &found); + + /* + * The dsa pointers containing statistics for each client are stored in a + * dshash table. In addition to dsa pointer, each entry in this table also + * contains information about the statistics, condition variable for + * signalling between client and the server and miscellaneous data + * specific to a request. There is one entry per client request in the + * hash table. + */ + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", &memctx_dsh_params, &found); + + snprintf(key, sizeof(key), "%d", MyProcNumber); + + /* + * Check if the publishing process slot is empty and store this clients + * key i.e its procNumber. This informs the publishing process that it is + * supposed to write statistics in the hash entry corresponding to this + * client. + */ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + + /* + * XXX. If the process exits without cleaning up its slot, i.e in case of an + * abrupt crash the client_keys slot won't be reset thus resulting in false + * negative and WARNING would be thrown in case another process with same + * slot index is queried for statistics. + */ + if (client_keys[procNumber] == -1) + client_keys[procNumber] = MyProcNumber; + else + { + ereport(WARNING, + errmsg("server process %d is processing previous request", pid)); + LWLockRelease(client_keys_lock); + PG_RETURN_NULL(); + } + LWLockRelease(client_keys_lock); + + /* + * Insert an entry for this client in DSHASH table the first time this + * function is called. This entry is deleted when the process exits in + * before_shmem_exit call. + * + * dshash_find_or_insert locks the entry to prevent the publisher from + * reading before client has updated the entry. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + if (!found) + { + entry->stats_written = false; + ConditionVariableInit(&entry->memcxt_cv); + } + + /* + * Allocate 1MB of memory for the backend to publish its statistics on + * every call to this function. The memory is freed at the end of the + * function. + */ + entry->memstats_dsa_pointer = + dsa_allocate0(MemoryStatsDsaArea, MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND); + + /* + * Specify whether a summary of statistics is requested, before signalling + * the server. + */ + entry->summary = summary; + + /* + * Indicate which server process statistics are being requested from. + * If this client times out before the last requested process can publish its + * statistics, it may send a new request to another server process. Since the + * previous server was notified, it might attempt to read the same client entry + * and respond incorrectly with its statistics. By storing the server ID in the + * client entry, we prevent any previously signalled server process from writing + * its statistics in the space meant for the newly requested process. + */ + entry->server_id = pid; + dshash_release_lock(MemoryStatsDsHash, entry); + + PG_TRY(); + { + /* + * Send a signal to a PostgreSQL process, informing it we want it to + * produce information about its memory contexts. + */ + if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, procNumber) < 0) + { + ereport(WARNING, + errmsg("could not send signal to process %d: %m", pid)); + memstats_dsa_cleanup(key); + PG_RETURN_NULL(); + } + + while (1) + { + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + Assert(found); + + /* + * We expect to come out of sleep when the requested process has + * finished publishing the statistics, verified using a boolean + * stats_written. + */ + if (entry->stats_written) + break; + + dshash_release_lock(MemoryStatsDsHash, entry); + + /* + * Recheck the state of the backend before sleeping on the + * condition variable to ensure the process is still alive. Only + * check the relevant process type based on the earlier PID check. + */ + if (proc_is_aux) + proc = AuxiliaryPidGetProc(pid); + else + proc = BackendPidGetProc(pid); + + /* + * The process ending during memory context processing is not an + * error. + */ + if (proc == NULL) + { + ereport(WARNING, + errmsg("PID %d is no longer a PostgreSQL server process", + pid)); + memstats_dsa_cleanup(key); + ConditionVariableCancelSleep(); + PG_RETURN_NULL(); + } + + + /* + * Wait for the timeout as defined by the user. If no statistics + * are available within the allowed time then return NULL. The + * timer is defined in milliseconds since that's what the + * condition variable sleep uses. + */ + if (ConditionVariableTimedSleep(&entry->memcxt_cv, + (timeout * 1000), WAIT_EVENT_MEM_CXT_PUBLISH)) + { + /* Timeout has expired, return NULL */ + memstats_dsa_cleanup(key); + ConditionVariableCancelSleep(); + PG_RETURN_NULL(); + } + } + + /* + * Backend has finished publishing the stats, project them. + */ + memcxt_info = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + +#define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 11 + for (int i = 0; i < entry->total_stats; i++) + { + ArrayType *path_array; + int path_length; + Datum values[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + bool nulls[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + Datum *path_datum = NULL; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + if (memcxt_info[i].name[0] != '\0') + { + values[0] = CStringGetTextDatum(memcxt_info[i].name); + } + else + nulls[0] = true; + + if (memcxt_info[i].ident[0] != '\0') + { + values[1] = CStringGetTextDatum(memcxt_info[i].ident); + } + else + nulls[1] = true; + + values[2] = CStringGetTextDatum(ContextTypeToString(memcxt_info[i].type)); + + path_length = memcxt_info[i].path_length; + path_datum = (Datum *) palloc(path_length * sizeof(Datum)); + if (memcxt_info[i].path[0] != 0) + { + for (int j = 0; j < path_length; j++) + path_datum[j] = Int32GetDatum(memcxt_info[i].path[j]); + path_array = construct_array_builtin(path_datum, path_length, INT4OID); + values[3] = PointerGetDatum(path_array); + } + else + nulls[3] = true; + + values[4] = Int32GetDatum(memcxt_info[i].levels); + values[5] = Int64GetDatum(memcxt_info[i].totalspace); + values[6] = Int64GetDatum(memcxt_info[i].nblocks); + values[7] = Int64GetDatum(memcxt_info[i].freespace); + values[8] = Int64GetDatum(memcxt_info[i].freechunks); + values[9] = Int64GetDatum(memcxt_info[i].totalspace - + memcxt_info[i].freespace); + values[10] = Int32GetDatum(memcxt_info[i].num_agg_stats); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + dshash_release_lock(MemoryStatsDsHash, entry); + memstats_dsa_cleanup(key); + + ConditionVariableCancelSleep(); + + } + PG_CATCH(); + { + memstats_dsa_cleanup(key); + ConditionVariableCancelSleep(); + } + PG_END_TRY(); + + PG_RETURN_NULL(); +} + +static void +memstats_dsa_cleanup(char *key) +{ + MemoryStatsDSHashEntry *entry; + + entry = dshash_find(MemoryStatsDsHash, key, true); + + Assert(MemoryStatsDsaArea != NULL); + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + entry->memstats_dsa_pointer = InvalidDsaPointer; + entry->stats_written = false; + entry->server_id = 0; + + dshash_release_lock(MemoryStatsDsHash, entry); +} +void +MemoryContextKeysShmemInit(void) +{ + bool found; + + client_keys = (int *) + ShmemInitStruct("MemoryContextKeys", + MemoryContextKeysShmemSize() + sizeof(LWLockPadded), &found); + client_keys_lock = (LWLock *) ((char *) client_keys + MemoryContextKeysShmemSize()); + + if (!found) + { + MemSet(client_keys, -1, MemoryContextKeysShmemSize()); + LWLockInitialize(client_keys_lock, LWTRANCHE_MEMORY_CONTEXT_KEYS); + } +} + +Size +MemoryContextKeysShmemSize(void) +{ + Size sz = 0; + Size TotalProcs = 0; + + TotalProcs = add_size(TotalProcs, NUM_AUXILIARY_PROCS); + TotalProcs = add_size(TotalProcs, MaxBackends); + sz = add_size(sz, mul_size(TotalProcs, sizeof(int))); + + return sz; +} + +/* + * HandleGetMemoryContextInterrupt + * Handle receipt of an interrupt indicating a request to publish memory + * contexts statistics. + * + * All the actual work is deferred to ProcessGetMemoryContextInterrupt() as + * this cannot be performed in a signal handler. + */ +void +HandleGetMemoryContextInterrupt(void) +{ + InterruptPending = true; + PublishMemoryContextPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + +/* + * ProcessGetMemoryContextInterrupt + * Generate information about memory contexts used by the process. + * + * Performs a breadth first search on the memory context tree, thus parents + * statistics are reported before their children in the monitoring function + * output. + * + * Statistics for all the processes are shared via the same dynamic shared + * area. Individual statistics are tracked independently in + * per-process DSA pointers. These pointers are stored in a dshash table with + * key as requesting clients ProcNumber. + * + * We calculate maximum number of context's statistics that can be displayed + * using a pre-determined limit for memory available per process for this + * utility and maximum size of statistics for each context. The remaining + * context statistics if any are captured as a cumulative total at the end of + * individual context's statistics. + * + * If summary is true, we capture the level 1 and level 2 contexts + * statistics. For that we traverse the memory context tree recursively in + * depth first search manner to cover all the children of a parent context, to + * be able to display a cumulative total of memory consumption by a parent at + * level 2 and all its children. + */ +void +ProcessGetMemoryContextInterrupt(void) +{ + List *contexts; + HASHCTL ctl; + HTAB *context_id_lookup; + int context_id = 0; + MemoryStatsEntry *meminfo; + bool summary = false; + MemoryContextCounters stat; + int num_individual_stats = 0; + bool found; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + int clientProcNumber; + MemoryContext memstats_ctx = NULL; + MemoryContext oldcontext = NULL; + + PublishMemoryContextPending = false; + + /* + * Create a new memory context which is not a part of TopMemoryContext + * tree. This context is used to allocate all memory in this function. + * This helps in keeping the memory allocation in this function to report + * memory consumption statistics separate. So that it does not affect the + * output of this function. + */ + memstats_ctx = AllocSetContextCreate((MemoryContext) NULL, "publish_memory_context_statistics", + ALLOCSET_SMALL_SIZES); + oldcontext = MemoryContextSwitchTo(memstats_ctx); + + /* + * The hash table is used for constructing "path" column of the view, + * similar to its local backend counterpart. + */ + ctl.keysize = sizeof(MemoryContext); + ctl.entrysize = sizeof(MemoryStatsContextId); + ctl.hcxt = CurrentMemoryContext; + + context_id_lookup = hash_create("pg_get_remote_backend_memory_contexts", + 256, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + /* List of contexts to process in the next round - start at the top. */ + contexts = list_make1(TopMemoryContext); + + /* + * If DSA exists, created by another process requesting statistics, attach + * to it. We expect the client process to create required DSA and Dshash + * table. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", &found); + + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", &memctx_dsh_params, &found); + + /* Retreive the client key for publishing statistics */ + LWLockAcquire(client_keys_lock, LW_SHARED); + Assert(client_keys[MyProcNumber] != -1); + clientProcNumber = client_keys[MyProcNumber]; + LWLockRelease(client_keys_lock); + + snprintf(key, CLIENT_KEY_SIZE, "%d", clientProcNumber); + + /* + * The entry lock is held by dshash_find_or_insert to protect writes to + * process specific memory. Two different processes publishing statistics + * do not block each other. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + /* + * Entry has been deleted due to client process exit + * Make sure that the client always deletes the entry + * after taking required lock or this function may end up writing + * to unallocated memory. + */ + if (!found) + { + entry->stats_written = false; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + return; + } + + /* + * The client has timed out waiting for us to write statistics and is + * requesting statistics from some other process + */ + if (entry->server_id != MyProcPid) + { + entry->stats_written = false; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + return; + } + + summary = entry->summary; + + /* Should be allocated by a client backend that is requesting statistics */ + Assert(entry->memstats_dsa_pointer != InvalidDsaPointer); + meminfo = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + if (summary) + { + int cxt_id = 0; + List *path = NIL; + MemoryStatsContextId *contextid_entry; + + /* Copy TopMemoryContext statistics to DSA */ + memset(&stat, 0, sizeof(stat)); + (*TopMemoryContext->methods->stats) (TopMemoryContext, NULL, NULL, + &stat, true); + path = lcons_int(1, path); + PublishMemoryContext(meminfo, cxt_id, TopMemoryContext, path, stat, + 1, 100); + + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &TopMemoryContext, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = cxt_id + 1; + + /* + * Copy statistics for each of TopMemoryContexts children. This + * includes statistics of at most 100 children per node, with each + * child node limited to a depth of 100 in its subtree. + */ + for (MemoryContext c = TopMemoryContext->firstchild; c != NULL; + c = c->nextchild) + { + MemoryContextCounters grand_totals; + int num_contexts = 0; + + path = NIL; + memset(&grand_totals, 0, sizeof(grand_totals)); + + cxt_id++; + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &c, + HASH_ENTER, &found); + Assert(!found); + contextid_entry->context_id = cxt_id + 1; + + MemoryContextStatsInternal(c, 1, 100, 100, &grand_totals, + PRINT_STATS_NONE, &num_contexts); + + path = compute_context_path(c, context_id_lookup); + + PublishMemoryContext(meminfo, cxt_id, c, path, + grand_totals, num_contexts, 100); + } + entry->total_stats = cxt_id + 1; + + entry->stats_written = true; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + /* Notify waiting backends and return */ + ConditionVariableBroadcast(&entry->memcxt_cv); + return; + } + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + MemoryStatsContextId *contextid_entry; + + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &cur, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = context_id + 1; + + /* + * Figure out the transient context_id of this context and each of its + * ancestors, to compute a path for this context. + */ + path = compute_context_path(cur, context_id_lookup); + + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + /* Account for saving one statistics slot for cumulative reporting */ + if (context_id < (MAX_MEMORY_CONTEXT_STATS_NUM - 1)) + { + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1, 100); + } + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].totalspace += stat.totalspace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].nblocks += stat.nblocks; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freespace += stat.freespace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit per process is reached, write aggregate of the + * remaining statistics. + * + * We can store contexts from 0 to max_stats - 1. When context_id is + * greater than max_stats, we stop reporting individual statistics + * when context_id equals max_stats - 2. As we use max_stats - 1 array + * slot for reporting cumulative statistics or "Remaining Totals". + */ + if (context_id == (MAX_MEMORY_CONTEXT_STATS_NUM - 2)) + { + int namelen = strlen("Remaining Totals"); + + num_individual_stats = context_id + 1; + strlcpy(meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].name, "Remaining Totals", namelen + 1); + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].ident[0] = '\0'; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].path[0] = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].type = 0; + } + context_id++; + + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + contexts = lappend(contexts, c); + } + + /* + * Statistics are not aggregated, i.e individual statistics reported when + * context_id <= max_stats. + */ + if (context_id <= MAX_MEMORY_CONTEXT_STATS_NUM) + { + entry->total_stats = context_id; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = 1; + } + /* Report number of aggregated memory contexts */ + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = context_id - + num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for cumulative + * statistics. + */ + entry->total_stats = num_individual_stats + 1; + } + entry->stats_written = true; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + /* Notify waiting backends and return */ + ConditionVariableBroadcast(&entry->memcxt_cv); +} + +/* + * Clean up before exit from ProcessGetMemoryContextInterrupt + */ +static void +end_memorycontext_reporting(MemoryStatsDSHashEntry *entry, MemoryContext oldcontext, HTAB *context_id_lookup) +{ + MemoryContext curr_ctx = CurrentMemoryContext; + + dshash_release_lock(MemoryStatsDsHash, entry); + + /* + * Empty this processes slot, so other clients can request memory + * statistics + */ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + client_keys[MyProcNumber] = -1; + LWLockRelease(client_keys_lock); + + hash_destroy(context_id_lookup); + MemoryContextSwitchTo(oldcontext); + MemoryContextReset(curr_ctx); +} + +/* + * compute_context_path + * + * Append the transient context_id of this context and each of its ancestors + * to a list, in order to compute a path. + */ +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + MemoryContext cur_context; + + for (cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryStatsContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + elog(ERROR, "hash table corrupted, can't construct path value"); + + path = lcons_int(cur_entry->context_id, path); + } + + return path; +} + +/* + * PublishMemoryContext + * + * Copy the memory context statistics of a single context to a DSA memory + */ +static void +PublishMemoryContext(MemoryStatsEntry *memcxt_info, int curr_id, + MemoryContext context, List *path, + MemoryContextCounters stat, int num_contexts, int max_levels) +{ + const char *ident = context->ident; + const char *name = context->name; + + /* + * To be consistent with logging output, we label dynahash contexts with + * just the hash table name as with MemoryContextStatsPrint(). + */ + if (context->ident && strncmp(context->name, "dynahash", 8) == 0) + { + name = context->ident; + ident = NULL; + } + + if (name != NULL) + { + int namelen = strlen(name); + + if (strlen(name) >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + namelen = pg_mbcliplen(name, namelen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].name, name, namelen + 1); + } + else + /* Clearing the array */ + memcxt_info[curr_id].name[0] = '\0'; + + /* Trim and copy the identifier if it is not set to NULL */ + if (ident != NULL) + { + int idlen = strlen(context->ident); + + /* + * Some identifiers such as SQL query string can be very long, + * truncate oversize identifiers. + */ + if (idlen >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + idlen = pg_mbcliplen(ident, idlen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].ident, ident, idlen + 1); + } + else + memcxt_info[curr_id].ident[0] = '\0'; + + /* Allocate DSA memory for storing path information */ + if (path == NIL) + memcxt_info[curr_id].path[0] = 0; + else + { + int levels = Min(list_length(path), max_levels); + + memcxt_info[curr_id].path_length = levels; + memcxt_info[curr_id].levels = list_length(path); + + foreach_int(i, path) + { + memcxt_info[curr_id].path[foreach_current_index(i)] = i; + if (--levels == 0) + break; + } + } + memcxt_info[curr_id].type = context->type; + memcxt_info[curr_id].totalspace = stat.totalspace; + memcxt_info[curr_id].nblocks = stat.nblocks; + memcxt_info[curr_id].freespace = stat.freespace; + memcxt_info[curr_id].freechunks = stat.freechunks; + memcxt_info[curr_id].num_agg_stats = num_contexts; +} + +void +AtProcExit_memstats_cleanup(int code, Datum arg) +{ + int idx = MyProcNumber; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + bool found; + + if (MemoryStatsDsHash != NULL) + { + snprintf(key, CLIENT_KEY_SIZE, "%d", idx); + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + if (found) + { + if (MemoryStatsDsaArea != NULL && + DsaPointerIsValid(entry->memstats_dsa_pointer)) + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + } + dshash_delete_entry(MemoryStatsDsHash, entry); + } + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + client_keys[idx] = -1; + LWLockRelease(client_keys_lock); +} diff --git a/src/backend/utils/adt/pg_locale.c b/src/backend/utils/adt/pg_locale.c index 97c2ac1faf9..ab768a7a91f 100644 --- a/src/backend/utils/adt/pg_locale.c +++ b/src/backend/utils/adt/pg_locale.c @@ -45,7 +45,6 @@ #include "utils/lsyscache.h" #include "utils/memutils.h" #include "utils/pg_locale.h" -#include "utils/relcache.h" #include "utils/syscache.h" #ifdef WIN32 diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index d31cb45a058..92b0446b80c 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -39,6 +39,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t PublishMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index 641e535a73c..fb3f2d21fa0 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -662,6 +662,13 @@ BaseInit(void) * drop ephemeral slots, which in turn triggers stats reporting. */ ReplicationSlotInitialize(); + + /* + * The before shmem exit callback frees the DSA memory occupied by the + * latest memory context statistics that could be published by this proc + * if requested. + */ + before_shmem_exit(AtProcExit_memstats_cleanup, 0); } diff --git a/src/backend/utils/mb/mbutils.c b/src/backend/utils/mb/mbutils.c index 886ecbad871..308016d7763 100644 --- a/src/backend/utils/mb/mbutils.c +++ b/src/backend/utils/mb/mbutils.c @@ -39,7 +39,6 @@ #include "mb/pg_wchar.h" #include "utils/fmgrprotos.h" #include "utils/memutils.h" -#include "utils/relcache.h" #include "varatt.h" /* diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 47fd774c7d2..3a5422c7273 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -40,6 +40,7 @@ #include "mb/pg_wchar.h" #include "miscadmin.h" +#include "utils/hsearch.h" #include "utils/memdebug.h" #include "utils/memutils.h" #include "utils/memutils_internal.h" @@ -176,10 +177,6 @@ MemoryContext PortalContext = NULL; static void MemoryContextDeleteOnly(MemoryContext context); static void MemoryContextCallResetCallbacks(MemoryContext context); -static void MemoryContextStatsInternal(MemoryContext context, int level, - int max_level, int max_children, - MemoryContextCounters *totals, - bool print_to_stderr); static void MemoryContextStatsPrint(MemoryContext context, void *passthru, const char *stats_string, bool print_to_stderr); @@ -877,11 +874,19 @@ MemoryContextStatsDetail(MemoryContext context, bool print_to_stderr) { MemoryContextCounters grand_totals; + int num_contexts; + PrintDestination print_location; memset(&grand_totals, 0, sizeof(grand_totals)); + if (print_to_stderr) + print_location = PRINT_STATS_TO_STDERR; + else + print_location = PRINT_STATS_TO_LOGS; + + /* num_contexts report number of contexts aggregated in the output */ MemoryContextStatsInternal(context, 1, max_level, max_children, - &grand_totals, print_to_stderr); + &grand_totals, print_location, &num_contexts); if (print_to_stderr) fprintf(stderr, @@ -916,13 +921,14 @@ MemoryContextStatsDetail(MemoryContext context, * One recursion level for MemoryContextStats * * Print stats for this context if possible, but in any case accumulate counts - * into *totals (if not NULL). + * into *totals (if not NULL). The callers should make sure that print_location + * is set to PRINT_STATS_TO_STDERR or PRINT_STATS_TO_LOGS or PRINT_STATS_NONE. */ -static void +void MemoryContextStatsInternal(MemoryContext context, int level, int max_level, int max_children, MemoryContextCounters *totals, - bool print_to_stderr) + PrintDestination print_location, int *num_contexts) { MemoryContext child; int ichild; @@ -930,10 +936,39 @@ MemoryContextStatsInternal(MemoryContext context, int level, Assert(MemoryContextIsValid(context)); /* Examine the context itself */ - context->methods->stats(context, - MemoryContextStatsPrint, - &level, - totals, print_to_stderr); + switch (print_location) + { + case PRINT_STATS_TO_STDERR: + context->methods->stats(context, + MemoryContextStatsPrint, + &level, + totals, true); + break; + + case PRINT_STATS_TO_LOGS: + context->methods->stats(context, + MemoryContextStatsPrint, + &level, + totals, false); + break; + + case PRINT_STATS_NONE: + + /* + * Do not print the statistics if print_location is + * PRINT_STATS_NONE, only compute totals. This is used in + * reporting of memory context statistics via a sql function. Last + * parameter is not relevant. + */ + context->methods->stats(context, + NULL, + NULL, + totals, false); + break; + } + + /* Increment the context count for each of the recursive call */ + *num_contexts = *num_contexts + 1; /* * Examine children. @@ -953,7 +988,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, MemoryContextStatsInternal(child, level + 1, max_level, max_children, totals, - print_to_stderr); + print_location, num_contexts); } } @@ -972,7 +1007,13 @@ MemoryContextStatsInternal(MemoryContext context, int level, child = MemoryContextTraverseNext(child, context); } - if (print_to_stderr) + /* + * Add the count of children contexts which are traversed in the + * non-recursive manner. + */ + *num_contexts = *num_contexts + ichild; + + if (print_location == PRINT_STATS_TO_STDERR) { for (int i = 0; i < level; i++) fprintf(stderr, " "); @@ -985,7 +1026,7 @@ MemoryContextStatsInternal(MemoryContext context, int level, local_totals.freechunks, local_totals.totalspace - local_totals.freespace); } - else + else if (print_location == PRINT_STATS_TO_LOGS) ereport(LOG_SERVER_ONLY, (errhidestmt(true), errhidecontext(true), diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 7c20180637f..8859ed449d0 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8613,6 +8613,16 @@ prorettype => 'bool', proargtypes => 'int4', prosrc => 'pg_log_backend_memory_contexts' }, +# publishing memory contexts of the specified postgres process +{ oid => '2173', descr => 'publish memory contexts of the specified backend', + proname => 'pg_get_process_memory_contexts', provolatile => 'v', + prorows => '100', proretset => 't', proparallel => 'r', + prorettype => 'record', proargtypes => 'int4 bool float8', + proallargtypes => '{int4,bool,float8,text,text,text,_int4,int4,int8,int8,int8,int8,int8,int4}', + proargmodes => '{i,i,i,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{pid, summary, timeout, name, ident, type, path, level, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes, num_agg_contexts}', + prosrc => 'pg_get_process_memory_contexts' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index 1bef98471c3..1e59a7f910f 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -96,6 +96,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t PublishMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h index 06a1ffd4b08..4c71d756a2d 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -135,3 +135,4 @@ PG_LWLOCKTRANCHE(SUBTRANS_SLRU, SubtransSLRU) PG_LWLOCKTRANCHE(XACT_SLRU, XactSLRU) PG_LWLOCKTRANCHE(PARALLEL_VACUUM_DSA, ParallelVacuumDSA) PG_LWLOCKTRANCHE(AIO_URING_COMPLETION, AioUringCompletion) +PG_LWLOCKTRANCHE(MEMORY_CONTEXT_KEYS, MemoryContextReportingKeys) diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index afeeb1ca019..345d5a0ecb1 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -35,6 +35,7 @@ typedef enum PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to send the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ /* Recovery conflict reasons */ diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 7bbe5a36959..38451035fd4 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -18,7 +18,10 @@ #define MEMUTILS_H #include "nodes/memnodes.h" - +#include "storage/condition_variable.h" +#include "storage/lmgr.h" +#include "utils/dsa.h" +#include "lib/dshash.h" /* * MaxAllocSize, MaxAllocHugeSize @@ -48,6 +51,23 @@ #define AllocHugeSizeIsValid(size) ((Size) (size) <= MaxAllocHugeSize) +/* + * Memory Context reporting size limits. + */ + +/* Max length of context name and ident */ +#define MEMORY_CONTEXT_IDENT_SHMEM_SIZE 64 +/* Maximum size (in bytes) of DSA area per process */ +#define MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND ((size_t) (1 * 1024 * 1024)) + +/* + * Maximum size per context. Actual size may be lower as this assumes the worst + * case of deepest path and longest identifiers (name and ident, thus the + * multiplication by 2). The path depth is limited to 100 like for memory + * context logging. + */ +#define MAX_MEMORY_CONTEXT_STATS_SIZE (sizeof(MemoryStatsEntry)) +#define MAX_MEMORY_CONTEXT_STATS_NUM MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND / MAX_MEMORY_CONTEXT_STATS_SIZE /* * Standard top-level memory contexts. @@ -319,4 +339,74 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) return true; } +/* Dynamic shared memory state for statistics per context */ +typedef struct MemoryStatsEntry +{ + char name[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + char ident[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + int path[100]; + NodeTag type; + int path_length; + int levels; + int64 totalspace; + int64 nblocks; + int64 freespace; + int64 freechunks; + int num_agg_stats; +} MemoryStatsEntry; + +/* + * Per backend dynamic shared hash entry for memory context statistics + * reporting. + */ +typedef struct MemoryStatsDSHashEntry +{ + char key[64]; + ConditionVariable memcxt_cv; + bool stats_written; + int server_id; + int total_stats; + bool summary; + dsa_pointer memstats_dsa_pointer; +} MemoryStatsDSHashEntry; + +static const dshash_parameters memctx_dsh_params = { + offsetof(MemoryStatsDSHashEntry, memcxt_cv), + sizeof(MemoryStatsDSHashEntry), + dshash_strcmp, + dshash_strhash, + dshash_strcpy +}; + +/* + * Used for storage of transient identifiers for pg_get_backend_memory_contexts + */ +typedef struct MemoryStatsContextId +{ + MemoryContext context; + int context_id; +} MemoryStatsContextId; + +/* + * This is passed to MemoryContextStatsInternal to determine whether + * to print context statistics or not and where to print them logs or + * stderr. + */ +typedef enum PrintDestination +{ + PRINT_STATS_TO_STDERR = 0, + PRINT_STATS_TO_LOGS, + PRINT_STATS_NONE +} PrintDestination; + +extern void ProcessGetMemoryContextInterrupt(void); +extern void HandleGetMemoryContextInterrupt(void); +extern void MemoryContextKeysShmemInit(void); +extern Size MemoryContextKeysShmemSize(void); +extern void MemoryContextStatsInternal(MemoryContext context, int level, + int max_level, int max_children, + MemoryContextCounters *totals, + PrintDestination print_location, + int *num_contexts); +extern void AtProcExit_memstats_cleanup(int code, Datum arg); #endif /* MEMUTILS_H */ diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 83228cfca29..ae17d028ed3 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -232,3 +232,22 @@ select * from pg_timezone_abbrevs where abbrev = 'LMT'; LMT | @ 7 hours 52 mins 58 secs ago | f (1 row) +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; +NOTICE: (AllocSet,TopMemoryContext,) +NOTICE: (AllocSet,TopMemoryContext,) diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 66179f026b3..d0917b6868e 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -101,3 +101,21 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs; -- One specific case we can check without much fear of breakage -- is the historical local-mean-time value used for America/Los_Angeles. select * from pg_timezone_abbrevs where abbrev = 'LMT'; + +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false, 20) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 37f26f6c6b7..63fd382d4a0 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1680,6 +1680,9 @@ MemoryContextData MemoryContextId MemoryContextMethodID MemoryContextMethods +MemoryStatsContextId +MemoryStatsEntry +MemoryStatsDSHashEntry MemoryStatsPrintFunc MergeAction MergeActionState -- 2.34.1 [application/octet-stream] 0002-Test-fixes.patch (8.0K, ../../CAH2L28tACFygy9oXbTA8pPQ6KE8uDiO9CgAhku7ERNpdj1YRTw@mail.gmail.com/4-0002-Test-fixes.patch) download | inline diff: From b537ccf2b5f56eedcb22f066e00a6d61aececa9e Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Thu, 9 Oct 2025 12:07:20 +0530 Subject: [PATCH] Test fixes --- src/backend/utils/adt/mcxtfuncs.c | 2 + src/test/modules/Makefile | 1 + .../test_memcontext_reporting/Makefile | 32 +++++++++ .../t/001_memcontext_inj.pl | 45 ++++++++++++ .../test_memcontext_reporting--1.0.sql | 7 ++ .../test_memcontext_reporting.c | 68 +++++++++++++++++++ .../test_memcontext_reporting.control | 4 ++ 7 files changed, 159 insertions(+) create mode 100644 src/test/modules/test_memcontext_reporting/Makefile create mode 100644 src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting--1.0.sql create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index aa4e0a2e670..8c3d3f321aa 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -27,6 +27,7 @@ #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/injection_point.h" #include "utils/memutils.h" #include "utils/wait_event_types.h" @@ -541,6 +542,7 @@ pg_get_process_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_NULL(); } + INJECTION_POINT("memcontext-client-crash", NULL); while (1) { entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index 902a7954101..a31a2578c18 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -31,6 +31,7 @@ SUBDIRS = \ test_json_parser \ test_lfind \ test_lwlock_tranches \ + test_memcontext_reporting \ test_misc \ test_oat_hooks \ test_parser \ diff --git a/src/test/modules/test_memcontext_reporting/Makefile b/src/test/modules/test_memcontext_reporting/Makefile new file mode 100644 index 00000000000..01a7baa0263 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/Makefile @@ -0,0 +1,32 @@ +# src/test/modules/test_memcontext_reporting/Makefile + +EXTRA_INSTALL = src/test/modules/injection_points + +export enable_injection_points +MODULE_big = test_memcontext_reporting +OBJS = \ + $(WIN32RES) \ + test_memcontext_reporting.o +PGFILEDESC = "test_memcontext_reporting - test code for memory context reporting" + +EXTENSION = test_memcontext_reporting +DATA = test_memcontext_reporting--1.0.sql + +REGRESS = test_memcontext_reporting + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_memcontext_reporting +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif + +check: + $(prove_check) + +installcheck: + $(prove_installcheck) diff --git a/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl new file mode 100644 index 00000000000..842f32376fd --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl @@ -0,0 +1,45 @@ +# Copyright (c) 2025, PostgreSQL Global Development Group + +# Test suite for testing enabling data checksums in an online cluster, +# comprising of a primary and a replicated standby, with concurrent activity +# via pgbench runs + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} + +# Create and start a cluster with one node +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init(allows_streaming => 1, no_data_checksums => 1); +# max_connections need to be bumped in order to accommodate for pgbench clients +# and log_statement is dialled down since it otherwise will generate enormous +# amounts of logging. Page verification failures are still logged. +$node->append_conf( + 'postgresql.conf', + qq[ +max_connections = 100 +log_statement = none +]); +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION test_memcontext_reporting;'); +# Attaching to an injection point that crashes memory context client +$node->safe_psql('postgres', 'SELECT memcontext_crash_client();'); + +my $pid = $node->safe_psql('postgres', "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); + +#Client should have crashed +$node->safe_psql('postgres', "select pg_get_process_memory_contexts($pid, true, 5);"); +print "PID"; +print $pid; +#Query the same process for memory context using some other client and it should succeed. +my $topcontext_name = $node->safe_psql('postgres', "select name from pg_get_process_memory_contexts($pid, true, 5) where path = '{1}';"); +ok($topcontext_name = 'TopMemoryContext'); +done_testing(); diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting--1.0.sql b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting--1.0.sql new file mode 100644 index 00000000000..7f628bf24e2 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting--1.0.sql @@ -0,0 +1,7 @@ +CREATE FUNCTION memcontext_crash_server() +RETURNS pg_catalog.void +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION memcontext_crash_client() +RETURNS pg_catalog.void +AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c new file mode 100644 index 00000000000..f77875b437f --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c @@ -0,0 +1,68 @@ +/* + * ------------------------------------------------------------------------- + * + * Copyright (c) 2025, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c + * + * ------------------------------------------------------------------------- + */ + +#include "postgres.h" +#include "utils/injection_point.h" +#include "funcapi.h" +#include "utils/injection_point.h" + +PG_MODULE_MAGIC; + +void crash(const char *name, const void *private_data, void *arg); + +void +crash(const char *name, const void *private_data, void *arg) +{ + abort(); +} + +/* + * memcontext_crash_client + * + * Ensure that the client process aborts in between memory context + * reporting. + */ +PG_FUNCTION_INFO_V1(memcontext_crash_client); +Datum +memcontext_crash_client(PG_FUNCTION_ARGS) +{ +#ifdef USE_INJECTION_POINTS + InjectionPointAttach("memcontext-client-crash", + "test_memcontext_reporting", "crash", NULL, 0); + +#else + elog(ERROR, + "test is not working as intended when injection points are disabled"); +#endif + PG_RETURN_VOID(); +} + +/* + * memcontext_crash_server + * + * Ensure that the server process crashes in between memory context + * reporting. + */ +PG_FUNCTION_INFO_V1(memcontext_crash_server); +Datum +memcontext_crash_server(PG_FUNCTION_ARGS) +{ +#ifdef USE_INJECTION_POINTS + InjectionPointAttach("memcontext-server-crash", + "test_memcontext_reporting", "crash", NULL, 0); + +#else + elog(ERROR, + "test is not working as intended when injection points are disabled"); +#endif + PG_RETURN_VOID(); +} + diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control new file mode 100644 index 00000000000..48b501682d5 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control @@ -0,0 +1,4 @@ +comment = 'Test code for memcontext reporting' +default_version = '1.0' +module_pathname = '$libdir/test_memcontext_reporting' +relocatable = true -- 2.34.1 ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-10-14 09:00 torikoshia <[email protected]> parent: Rahila Syed <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: torikoshia @ 2025-10-14 09:00 UTC (permalink / raw) To: Rahila Syed <[email protected]>; +Cc: pgsql-hackers; Daniel Gustafsson <[email protected]> On 2025-10-09 17:43, Rahila Syed wrote: > Hi Torikoshia, > > Thank you for testing and reviewing the patch. > >> This issue occurs on my M1 Mac, but I couldn’t reproduce it on >> Ubuntu, >> so it might be environment-dependent. > >> Looking at the logs, Assert() is failing: >> >> 2025-10-07 08:48:26.766 JST [local] psql [23626] WARNING: 01000: >> >> server process 23646 is processing previous request >> 2025-10-07 08:48:26.766 JST [local] psql [23626] LOCATION: >> pg_get_process_memory_contexts, mcxtfuncs.c:476 >> TRAP: failed Assert("victim->magic == >> FREE_PAGE_SPAN_LEADER_MAGIC"), >> File: "freepage.c", Line: 1379, PID: 23626 >> 0 postgres 0x000000010357fdf4 >> ExceptionalCondition + 216 >> 1 postgres 0x00000001035cbe18 >> FreePageManagerGetInternal + 684 >> 2 postgres 0x00000001035cbb18 >> FreePageManagerGet + 40 >> 3 postgres 0x00000001035c84cc >> dsa_allocate_extended + 788 >> 4 postgres 0x0000000103453af0 >> pg_get_process_memory_contexts + 992 >> 5 postgres 0x0000000103007e94 >> ExecMakeFunctionResultSet + 616 >> 6 postgres 0x00000001030506b8 >> ExecProjectSRF + 304 >> 7 postgres 0x0000000103050434 >> ExecProjectSet + 268 >> 8 postgres 0x0000000103003270 >> ExecProcNodeFirst + 92 >> 9 postgres 0x0000000102ffa398 >> ExecProcNode + 60 >> 10 postgres 0x0000000102ff5050 >> ExecutePlan >> + 244 >> 11 postgres 0x0000000102ff4ee0 >> standard_ExecutorRun + 456 >> 12 postgres 0x0000000102ff4d08 >> ExecutorRun >> + 84 >> 13 postgres 0x0000000103341c84 >> PortalRunSelect + 296 >> 14 postgres 0x0000000103341694 >> PortalRun + >> 656 >> 15 postgres 0x000000010333c4bc >> exec_simple_query + 1388 >> 16 postgres 0x000000010333b5d0 >> PostgresMain + 3252 >> 17 postgres 0x0000000103332750 >> BackendInitialize + 0 >> 18 postgres 0x0000000103209e48 >> postmaster_child_launch + 456 >> 19 postgres 0x00000001032118c8 >> BackendStartup + 304 >> 20 postgres 0x000000010320f72c >> ServerLoop >> + 372 >> 21 postgres 0x000000010320e1e4 >> PostmasterMain + 6448 >> 22 postgres 0x0000000103094b0c main + >> 924 >> 23 dyld 0x0000000199dc2b98 start >> + >> 6076 >> >> Could you please check if you can reproduce this crash on your >> environment? > > I haven't been able to reproduce this issue on Ubuntu. A colleague > also tested it on their Mac > and didn't encounter the problem. I do have a fix in this area that I > believe should address an edge > case where data might be written to freed DSA memory. > > Kindly test using the v35 patch and let me know if you still see the > issue. Thanks for the update. v35 works fine on my environment. I ran the same test and haven’t encountered the crash anymore. The addition of the following code appears to have resolved the issue: +memstats_dsa_cleanup(char *key) +{ + MemoryStatsDSHashEntry *entry; + + entry = dshash_find(MemoryStatsDsHash, key, true); Since you seem to make a next version patch, I understand v35 is an interim patch, so this isn’t a major concern, but I encountered trailing whitespace warnings when applying the patches: $ git apply 0001-v35-0001-Add-pg_get_process_memory_context-function.patch 0001-v35-0001-Add-pg_get_process_memory_context-function.patch:705: trailing whitespace. 0001-v35-0001-Add-pg_get_process_memory_context-function.patch:1066: trailing whitespace. -- Regards, -- Atsushi Torikoshi Seconded from NTT DATA Japan Corporation to SRA OSS K.K. ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-10-28 05:36 Rahila Syed <[email protected]> parent: torikoshia <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Rahila Syed @ 2025-10-28 05:36 UTC (permalink / raw) To: torikoshia <[email protected]>; +Cc: pgsql-hackers; Daniel Gustafsson <[email protected]> Hi, PFA an updated v39 patch which is ready for review in the upcoming commitfest. > v35 works fine on my environment. > I ran the same test and haven’t encountered the crash anymore. > Thank you for testing and confirming the fix. > The addition of the following code appears to have resolved the issue: > > +memstats_dsa_cleanup(char *key) > +{ > + MemoryStatsDSHashEntry *entry; > + > + entry = dshash_find(MemoryStatsDsHash, key, true); > Yes, without this code, the dsa memory was being freed in the timeout path without acquiring a lock. Since you seem to make a next version patch, I understand v35 is an > interim patch, > so this isn’t a major concern, but I encountered trailing whitespace > warnings when applying the patches. > $ git apply > 0001-v35-0001-Add-pg_get_process_memory_context-function.patch > 0001-v35-0001-Add-pg_get_process_memory_context-function.patch:705: > trailing whitespace. > 0001-v35-0001-Add-pg_get_process_memory_context-function.patch:1066: > trailing whitespace. > > Thanks, should be fixed now. The updated patch contains the following changes. These changes are addressing some review comments discussed off list and a couple of bugs found while doing injection points tests. 1. All the changes made to MemoryContextStatsInternal and MemoryContextStatsDetail are removed. Instead of modifying these functions, I have written a separate function MemoryContextStatsCounter that takes care of counting statistics. This approach ensures that the existing functions remain unchanged. 2. Changes to ensure that the wait loop does not exceed the prescribed wait time. Additional exit condition has been added to the infinite loop that waits for request completion. This allows the pg_get_memoy_context_statistics function to return if the elapsed time goes beyond a set limit i.e the following timeout. 3. The user facing timeout is removed as that would complicate the user interface. CFIs are called frequently and the requests are likely to be addressed promptly. A predefined macro MEMORY_CONTEXT_STATS_TIMEOUT 5 (secs) is used for timeout instead. This would also remove the possibility of a user setting very low timeouts, which could cause requests to be incomplete and result in NULL outputs. 4. Miscellaneous cleanups to improve comments and remove left over comments from older versions. Also, removed an unnecessary argument from the PublishMemoryContext function. 5. Addressed Torikoshias suggestion to change the order of columns to match pg_backend_memory_contexts. 6. Attached is a test module that tests error handling by introducing errors using injection points. I have resolved a few bugs, so the memory monitoring function now runs correctly after the previous request ended with an error. Thank you, Rahila Syed Attachments: [application/octet-stream] v39-0001-Add-function-to-report-memory-context-statistics.patch (57.7K, ../../CAH2L28t=c_iBj0+LoKX0TSTrNJB2nTgH6MPD7QgRb8kSBZ7HVg@mail.gmail.com/3-v39-0001-Add-function-to-report-memory-context-statistics.patch) download | inline diff: From 2bd097622985e2c800ee0ca00c9bb08887ea66f7 Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Thu, 23 Oct 2025 17:31:52 +0530 Subject: [PATCH 1/2] Add function to report memory context statistics This adds a function for retrieving memory context statistics and information from backends as well as auxiliary processes. The intended usecase is cluster debugging when under memory pressure or unanticipated memory usage characteristics. When calling the function it sends a signal to the specified process to submit statistics regarding its memory contexts into dynamic shared memory. Each memory context is returned in detail, followed by a cumulative total in case the number of contexts exceed the max allocated amount of shared memory. Each process is limited to use at most 1Mb memory for this. A summary can also be explicitly requested by the user, this will return the TopMemoryContext and a cumulative total of all lower contexts. In order to not block on busy processes, we have hardcoded the number of seconds during which to retry before timing out. In the case where no statistics are published within the set timeout, NULL is returned --- doc/src/sgml/func/func-admin.sgml | 157 +++ src/backend/catalog/system_views.sql | 5 + src/backend/postmaster/autovacuum.c | 4 + src/backend/postmaster/checkpointer.c | 4 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 4 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 4 + src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/storage/lmgr/proc.c | 1 + src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 2 + src/backend/utils/adt/mcxtfuncs.c | 910 +++++++++++++++++- src/backend/utils/init/globals.c | 1 + src/backend/utils/init/postinit.c | 7 + src/backend/utils/mmgr/mcxt.c | 29 + src/include/catalog/pg_proc.dat | 10 + src/include/miscadmin.h | 1 + src/include/storage/lwlocklist.h | 1 + src/include/storage/procsignal.h | 1 + src/include/utils/memutils.h | 81 +- src/test/regress/expected/sysviews.out | 19 + src/test/regress/sql/sysviews.sql | 18 + src/tools/pgindent/typedefs.list | 3 + 25 files changed, 1255 insertions(+), 24 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 1b465bc8ba7..a5c66837241 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -251,6 +251,130 @@ <literal>false</literal> is returned. </para></entry> </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_get_process_memory_contexts</primary> + </indexterm> + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type>, <parameter>summary</parameter> <type>boolean</type> ) + <returnvalue>setof record</returnvalue> + ( <parameter>name</parameter> <type>text</type>, + <parameter>ident</parameter> <type>text</type>, + <parameter>type</parameter> <type>text</type>, + <parameter>level</parameter> <type>integer</type>, + <parameter>path</parameter> <type>integer[]</type>, + <parameter>total_bytes</parameter> <type>bigint</type>, + <parameter>total_nblocks</parameter> <type>bigint</type>, + <parameter>free_bytes</parameter> <type>bigint</type>, + <parameter>free_chunks</parameter> <type>bigint</type>, + <parameter>used_bytes</parameter> <type>bigint</type>, + <parameter>num_agg_contexts</parameter> <type>integer</type> ) + </para> + <para> + This function handles requests to display the memory contexts of a + <productname>PostgreSQL</productname> process with the specified + process ID. The function can be used to send requests to backends as + well as <glossterm linkend="glossary-auxiliary-proc">auxiliary processes</glossterm>. + </para> + <para> + The returned record contains extended statistics per each memory + context: + <itemizedlist spacing="compact"> + <listitem> + <para> + <parameter>name</parameter> - The name of the memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>ident</parameter> - Memory context ID (if any). + </para> + </listitem> + <listitem> + <para> + <parameter>type</parameter> - The type of memory context, possible + values are: AllocSet, Generation, Slab and Bump. + </para> + </listitem> + <listitem> + <para> + <parameter>level</parameter> - The level in the tree of the current + memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>path</parameter> - Memory contexts are organized in a + tree model with TopMemoryContext as the root, and all other memory + contexts as nodes in the tree. The <parameter>path</parameter> + displays the path from the root to the current memory context. The + path is limited to 100 children per node, which each node limited + to a max depth of 100, to preserve memory during reporting. The + printed path will also be limited to 100 nodes counting from the + TopMemoryContext. + </para> + </listitem> + <listitem> + <para> + <parameter>total_bytes</parameter> - The total number of bytes + allocated to this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_nblocks</parameter> - The total number of blocks + used for the allocated memory. + </para> + </listitem> + <listitem> + <para> + <parameter>free_bytes</parameter> - The amount of free memory in + this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>free_chunks</parameter> - The number of chunks that + <parameter>free_bytes</parameter> corresponds to. + </para> + </listitem> + <listitem> + <para> + <parameter>used_bytes</parameter> - The total number of bytes + currently occupied. + </para> + </listitem> + <listitem> + <para> + <parameter>num_agg_contexts</parameter> - The number of memory + contexts aggregated in the displayed statistics. + </para> + </listitem> + </itemizedlist> + </para> + <para> + When <parameter>summary</parameter> is <literal>true</literal>, statistics + for memory contexts at levels 1 and 2 are displayed, with level 1 + representing the root node (i.e., <literal>TopMemoryContext</literal>). + Statistics for contexts on level 2 and below are aggregates of all + child contexts' statistics, where <literal>num_agg_contexts</literal> + indicate the number aggregated child contexts. When + <parameter>summary</parameter> is <literal>false</literal>, + <literal>the num_agg_contexts</literal> value is <literal>1</literal>, + indicating that individual statistics are being displayed. + </para> + <para> + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context + statistics are aggregated and a cumulative total is displayed. The + <literal>num_agg_contexts</literal> column indicates the number of + contexts aggregated in the displayed statistics. When + <literal>num_agg_contexts</literal> is <literal>1</literal> it means + that the context statistics are displayed separately. + </para></entry> + </row> </tbody> </tgroup> </table> @@ -302,6 +426,39 @@ LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 because it may generate a large number of log messages. </para> + <para> + <function>pg_get_process_memory_contexts</function> can be used to request + memory contexts statistics of any <productname>PostgreSQL</productname> + process. For example: +<programlisting> +postgres=# SELECT * FROM pg_get_process_memory_contexts( + (SELECT pid FROM pg_stat_activity + WHERE backend_type = 'checkpointer'), + false) LIMIT 1; +-[ RECORD 1 ]----+------------------------------ +name | TopMemoryContext +ident | +type | AllocSet +level | 1 +path | {1} +total_bytes | 90304 +total_nblocks | 3 +free_bytes | 2880 +free_chunks | 1 +used_bytes | 87424 +num_agg_contexts | 1 +</programlisting> + <note> + <para> + While <function>pg_get_process_memory_contexts</function> can be used to + query memory contexts of the local backend, + <structname>pg_backend_memory_contexts</structname> + (see <xref linkend="view-pg-backend-memory-contexts"/> for more details) + will be less resource intensive when only the local backend is of interest. + </para> + </note> + </para> + </sect2> <sect2 id="functions-admin-backup"> diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index 823776c1498..3bb2a9d3c9b 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -692,6 +692,11 @@ GRANT SELECT ON pg_backend_memory_contexts TO pg_read_all_stats; REVOKE EXECUTE ON FUNCTION pg_get_backend_memory_contexts() FROM PUBLIC; GRANT EXECUTE ON FUNCTION pg_get_backend_memory_contexts() TO pg_read_all_stats; +REVOKE EXECUTE ON FUNCTION + pg_get_process_memory_contexts(integer, boolean) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION + pg_get_process_memory_contexts(integer, boolean) TO pg_read_all_stats; + -- Statistics views CREATE VIEW pg_stat_all_tables AS diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 5084af7dfb6..8bf6f6eb743 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -791,6 +791,10 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index e84e8663e96..5b3e08805bf 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -679,6 +679,10 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index ba63b84dfc5..29454b8bf8a 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -48,6 +48,10 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index 78e39e5f866..ac97a39447c 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -867,6 +867,10 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index 27e86cf393f..7149a67fcbc 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -192,6 +192,10 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index e1f142f20c7..c711c887ef6 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -879,6 +879,10 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index 2fa045e6b0f..fe3d32e40b0 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -51,6 +51,7 @@ #include "storage/sinvaladt.h" #include "utils/guc.h" #include "utils/injection_point.h" +#include "utils/memutils.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -150,6 +151,7 @@ CalculateShmemSize(int *num_semaphores) size = add_size(size, InjectionPointShmemSize()); size = add_size(size, SlotSyncShmemSize()); size = add_size(size, AioShmemSize()); + size = add_size(size, MemoryContextKeysShmemSize() + sizeof(LWLockPadded)); /* include additional requested shmem from preload libraries */ size = add_size(size, total_addin_request); @@ -343,6 +345,7 @@ CreateOrAttachShmemStructs(void) WaitEventCustomShmemInit(); InjectionPointShmemInit(); AioShmemInit(); + MemoryContextKeysShmemInit(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index 087821311cc..8963285cc12 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -691,6 +691,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_GET_MEMORY_CONTEXT)) + HandleGetMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 96f29aafc39..550a3a77bb8 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -50,6 +50,7 @@ #include "storage/procsignal.h" #include "storage/spin.h" #include "storage/standby.h" +#include "utils/memutils.h" #include "utils/timeout.h" #include "utils/timestamp.h" diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 7dd75a490aa..e726f40dfbb 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3539,6 +3539,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 7553f6eacef..cdbc7309206 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -160,6 +160,7 @@ WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." +MEM_CXT_PUBLISH "Waiting for a process to publish memory information." ABI_compatibility: @@ -401,6 +402,7 @@ SubtransSLRU "Waiting to access the sub-transaction SLRU cache." XactSLRU "Waiting to access the transaction status SLRU cache." ParallelVacuumDSA "Waiting for parallel vacuum dynamic shared memory allocation." AioUringCompletion "Waiting for another process to complete IO via io_uring." +MemoryContextReportingKeys "Waiting for another process to complete reading or writing the memory reporting keys." # No "ABI_compatibility" region here as WaitEventLWLock has its own C code. diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index fe6dce9cba3..a62f3d6dc93 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -15,20 +15,51 @@ #include "postgres.h" +#include "access/twophase.h" +#include "catalog/pg_authid_d.h" #include "funcapi.h" #include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "storage/dsm_registry.h" #include "storage/proc.h" #include "storage/procarray.h" +#include "utils/acl.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/injection_point.h" +#include "utils/memutils.h" +#include "utils/wait_event_types.h" + +#define CLIENT_KEY_SIZE 64 + +static LWLock *client_keys_lock = NULL; +static int *client_keys = NULL; +static dshash_table *MemoryStatsDsHash = NULL; +static dsa_area *MemoryStatsDsaArea = NULL; + +static void memstats_dsa_cleanup(char *key); +static const char *ContextTypeToString(NodeTag type); +static void PublishMemoryContext(MemoryStatsEntry *memcxt_info, + int curr_id, MemoryContext context, + List *path, + MemoryContextCounters stat, + int num_contexts); +static List *compute_context_path(MemoryContext c, HTAB *context_id_lookup); +static void end_memorycontext_reporting(MemoryStatsDSHashEntry *entry, MemoryContext oldcontext, + HTAB *context_id_lookup); /* ---------- * The max bytes for showing identifiers of MemoryContext. + * This is used by pg_get_backend_memory_context - view used for local backend. * ---------- */ #define MEMORY_CONTEXT_IDENT_DISPLAY_SIZE 1024 +#define MAX_PATH_DISPLAY_LENGTH 100 +/* Timeout in seconds */ +#define MEMORY_STATS_MAX_TIMEOUT 5 + /* * MemoryContextId * Used for storage of transient identifiers for @@ -89,7 +120,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, */ for (MemoryContext cur = context; cur != NULL; cur = cur->parent) { - MemoryContextId *entry; + MemoryStatsContextId *entry; bool found; entry = hash_search(context_id_lookup, &cur, HASH_FIND, &found); @@ -143,24 +174,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, else nulls[1] = true; - switch (context->type) - { - case T_AllocSetContext: - type = "AllocSet"; - break; - case T_GenerationContext: - type = "Generation"; - break; - case T_SlabContext: - type = "Slab"; - break; - case T_BumpContext: - type = "Bump"; - break; - default: - type = "???"; - break; - } + type = ContextTypeToString(context->type); values[2] = CStringGetTextDatum(type); values[3] = Int32GetDatum(list_length(path)); /* level */ @@ -175,6 +189,38 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, list_free(path); } +/* + * ContextTypeToString + * Returns a textual representation of a context type + * + * This should cover the same types as MemoryContextIsValid. + */ +const char * +ContextTypeToString(NodeTag type) +{ + const char *context_type; + + switch (type) + { + case T_AllocSetContext: + context_type = "AllocSet"; + break; + case T_GenerationContext: + context_type = "Generation"; + break; + case T_SlabContext: + context_type = "Slab"; + break; + case T_BumpContext: + context_type = "Bump"; + break; + default: + context_type = "???"; + break; + } + return context_type; +} + /* * pg_get_backend_memory_contexts * SQL SRF showing backend memory context. @@ -189,7 +235,7 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) HTAB *context_id_lookup; ctl.keysize = sizeof(MemoryContext); - ctl.entrysize = sizeof(MemoryContextId); + ctl.entrysize = sizeof(MemoryStatsContextId); ctl.hcxt = CurrentMemoryContext; context_id_lookup = hash_create("pg_get_backend_memory_contexts", @@ -216,7 +262,7 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) foreach_ptr(MemoryContextData, cur, contexts) { - MemoryContextId *entry; + MemoryStatsContextId *entry; bool found; /* @@ -224,8 +270,8 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) * PutMemoryContextsStatsTupleStore needs this to populate the "path" * column with the parent context_ids. */ - entry = (MemoryContextId *) hash_search(context_id_lookup, &cur, - HASH_ENTER, &found); + entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &cur, + HASH_ENTER, &found); entry->context_id = context_id++; Assert(!found); @@ -305,3 +351,821 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } + +/* + * pg_get_process_memory_contexts + * Signal a backend or an auxiliary process to send its memory contexts, + * wait for the results and display them. + * + * By default, only superusers or users with ROLE_PG_READ_ALL_STATS are allowed + * to signal a process to return the memory contexts. This is because allowing + * any users to issue this request at an unbounded rate would cause lots of + * requests to be sent, which can lead to denial of service. Additional roles + * can be permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to copy the memory context details + * to a dynamic shared memory space. + * + * We have defined a limit on DSA memory that could be allocated per process - + * if the process has more memory contexts than what can fit in the allocated + * size, the excess contexts are summarized and represented as cumulative total + * at the end of the buffer. + * + * After sending the signal, wait on a condition variable. The publishing + * backend, after copying the data to shared memory, sends signal on that + * condition variable. There is one condition variable per client process. + * Once the condition variable is signalled, check if the latest memory context + * information is available and display. + * + * If the publishing backend does not respond before the condition variable + * times out, which is set to a predefined value MEMORY_STATS_MAX_TIMEOUT, give up + * and return NULL. + */ +Datum +pg_get_process_memory_contexts(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + bool summary = PG_GETARG_BOOL(1); + PGPROC *proc; + ProcNumber procNumber = INVALID_PROC_NUMBER; + bool proc_is_aux = false; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + MemoryStatsEntry *memcxt_info; + MemoryStatsDSHashEntry *entry; + bool found; + char key[CLIENT_KEY_SIZE]; + TimestampTz start_timestamp; + + /* + * See if the process with given pid is a backend or an auxiliary process + * and remember the type for when we requery the process later. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + { + proc = AuxiliaryPidGetProc(pid); + proc_is_aux = true; + } + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; this is however not a problem and leave with a WARNING. + * See comment in pg_log_backend_memory_contexts for a discussion on this. + */ + if (proc == NULL) + { + /* + * This is a warning because we don't want to break loops. + */ + ereport(WARNING, + errmsg("PID %d is not a PostgreSQL server process", pid)); + PG_RETURN_NULL(); + } + + InitMaterializedSRF(fcinfo, 0); + + procNumber = GetNumberFromPGProc(proc); + + /* + * Create a DSA to allocate memory for copying memory contexts statistics. + * Allocate the memory in the DSA and send dsa pointer to the server + * process for storing the context statistics. If number of contexts + * exceed a predefined limit (1MB), a cumulative total is stored for such + * contexts. + * + * The DSA is created once for the lifetime of the server, and only + * attached in subsequent calls. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", &found); + + /* + * The dsa pointers containing statistics for each client are stored in a + * dshash table. In addition to dsa pointer, each entry in this table also + * contains information about the statistics, condition variable for + * signalling between client and the server and miscellaneous data + * specific to a request. There is one entry per client request in the + * hash table. + */ + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", &memctx_dsh_params, &found); + + snprintf(key, sizeof(key), "%d", MyProcNumber); + + /* + * Check if the publishing process slot is empty and store this clients + * key i.e its procNumber. This informs the publishing process that it is + * supposed to write statistics in the hash entry corresponding to this + * client. + */ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + + /* + * XXX. If the process exits without cleaning up its slot, i.e in case of + * an abrupt crash the client_keys slot won't be reset thus resulting in + * false negative and WARNING would be thrown in case another process with + * same slot index is queried for statistics. + */ + if (client_keys[procNumber] == -1) + client_keys[procNumber] = MyProcNumber; + else + { + LWLockRelease(client_keys_lock); + ereport(WARNING, + errmsg("server process %d is processing previous request", pid)); + PG_RETURN_NULL(); + } + LWLockRelease(client_keys_lock); + + /* + * Insert an entry for this client in DSHASH table the first time this + * function is called. This entry is deleted when the process exits in + * before_shmem_exit call. + * + * dshash_find_or_insert locks the entry to prevent the publisher from + * reading before client has updated the entry. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + if (!found) + { + entry->stats_written = false; + ConditionVariableInit(&entry->memcxt_cv); + } + + /* + * Allocate 1MB of memory for the backend to publish its statistics on + * every call to this function. The memory is freed at the end of the + * function. + */ + entry->memstats_dsa_pointer = + dsa_allocate0(MemoryStatsDsaArea, MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND); + + /* + * Specify whether a summary of statistics is requested, before signalling + * the server. + */ + entry->summary = summary; + + /* + * Indicate which server process statistics are being requested from. If + * this client times out before the last requested process can publish its + * statistics, it may send a new request to another server process. Since + * the previous server was notified, it might attempt to read the same + * client entry and respond incorrectly with its statistics. By storing + * the server ID in the client entry, we prevent any previously signalled + * server process from writing its statistics in the space meant for the + * newly requested process. + */ + entry->target_server_id = pid; + dshash_release_lock(MemoryStatsDsHash, entry); + + /* + * Send a signal to a PostgreSQL process, informing it we want it to + * produce information about its memory contexts. + */ + if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, procNumber) < 0) + { + memstats_dsa_cleanup(key); + ereport(WARNING, + errmsg("could not send signal to process %d: %m", pid)); + PG_RETURN_NULL(); + } + start_timestamp = GetCurrentTimestamp(); + + while (1) + { + long elapsed_time; + + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + Assert(found); + + INJECTION_POINT("memcontext-client-crash", NULL); + + memcxt_info = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + /* + * We expect to come out of sleep when the requested process has + * finished publishing the statistics, verified using a boolean + * stats_written. + * + * Make sure that the statistics are actually written by checking that + * the name of the context is not NULL. This is done to ensure that + * the subsequent waits for statistics do not return spuriously if the + * previous call to the function ended in error and thus could not + * clear the stats_written flag. + */ + if (entry->stats_written && memcxt_info[0].name[0] != '\0') + break; + + dshash_release_lock(MemoryStatsDsHash, entry); + + elapsed_time = TimestampDifferenceMilliseconds(start_timestamp, + GetCurrentTimestamp()); + /* Return if we have already exceeded the timeout */ + if (elapsed_time >= MEMORY_STATS_MAX_TIMEOUT * 1000) + { + memstats_dsa_cleanup(key); + PG_RETURN_NULL(); + } + + /* + * Recheck the state of the backend before sleeping on the condition + * variable to ensure the process is still alive. Only check the + * relevant process type based on the earlier PID check. + */ + if (proc_is_aux) + proc = AuxiliaryPidGetProc(pid); + else + proc = BackendPidGetProc(pid); + + /* + * The target server process ending during memory context processing + * is not an error. + */ + if (proc == NULL) + { + memstats_dsa_cleanup(key); + ConditionVariableCancelSleep(); + ereport(WARNING, + errmsg("PID %d is no longer a PostgreSQL server process", + pid)); + PG_RETURN_NULL(); + } + + INJECTION_POINT("memcontext-client-crash", NULL); + + /* + * Wait for MEMORY_STATS_MAX_TIMEOUT. If no statistics are available + * within the allowed time then return NULL. The timer is defined in + * milliseconds since that's what the condition variable sleep uses. + */ + if (ConditionVariableTimedSleep(&entry->memcxt_cv, + (MEMORY_STATS_MAX_TIMEOUT * 1000), WAIT_EVENT_MEM_CXT_PUBLISH)) + { + /* Timeout has expired, return NULL */ + memstats_dsa_cleanup(key); + ConditionVariableCancelSleep(); + PG_RETURN_NULL(); + } + } + + /* + * Backend has finished publishing the stats, project them. + */ +#define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 11 + for (int i = 0; i < entry->total_stats; i++) + { + ArrayType *path_array; + int path_length; + Datum values[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + bool nulls[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + Datum *path_datum = NULL; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + Assert(memcxt_info[i].name[0] != '\0'); + values[0] = CStringGetTextDatum(memcxt_info[i].name); + + if (memcxt_info[i].ident[0] != '\0') + values[1] = CStringGetTextDatum(memcxt_info[i].ident); + else + nulls[1] = true; + + values[2] = CStringGetTextDatum(ContextTypeToString(memcxt_info[i].type)); + values[3] = Int32GetDatum(memcxt_info[i].levels); + + path_length = memcxt_info[i].path_length; + path_datum = (Datum *) palloc(path_length * sizeof(Datum)); + if (memcxt_info[i].path[0] != 0) + { + for (int j = 0; j < path_length; j++) + path_datum[j] = Int32GetDatum(memcxt_info[i].path[j]); + path_array = construct_array_builtin(path_datum, path_length, INT4OID); + values[4] = PointerGetDatum(path_array); + } + else + nulls[4] = true; + + values[5] = Int64GetDatum(memcxt_info[i].totalspace); + values[6] = Int64GetDatum(memcxt_info[i].nblocks); + values[7] = Int64GetDatum(memcxt_info[i].freespace); + values[8] = Int64GetDatum(memcxt_info[i].freechunks); + values[9] = Int64GetDatum(memcxt_info[i].totalspace - + memcxt_info[i].freespace); + values[10] = Int32GetDatum(memcxt_info[i].num_agg_stats); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + dshash_release_lock(MemoryStatsDsHash, entry); + memstats_dsa_cleanup(key); + + ConditionVariableCancelSleep(); + + PG_RETURN_NULL(); +} + +static void +memstats_dsa_cleanup(char *key) +{ + MemoryStatsDSHashEntry *entry; + + entry = dshash_find(MemoryStatsDsHash, key, true); + + Assert(MemoryStatsDsaArea != NULL); + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + entry->memstats_dsa_pointer = InvalidDsaPointer; + entry->stats_written = false; + entry->target_server_id = 0; + + dshash_release_lock(MemoryStatsDsHash, entry); +} +void +MemoryContextKeysShmemInit(void) +{ + bool found; + + client_keys = (int *) + ShmemInitStruct("MemoryContextKeys", + MemoryContextKeysShmemSize() + sizeof(LWLockPadded), &found); + client_keys_lock = (LWLock *) ((char *) client_keys + MemoryContextKeysShmemSize()); + + if (!found) + { + MemSet(client_keys, -1, MemoryContextKeysShmemSize()); + LWLockInitialize(client_keys_lock, LWTRANCHE_MEMORY_CONTEXT_KEYS); + } +} + +Size +MemoryContextKeysShmemSize(void) +{ + Size sz = 0; + Size TotalProcs = 0; + + TotalProcs = add_size(TotalProcs, NUM_AUXILIARY_PROCS); + TotalProcs = add_size(TotalProcs, MaxBackends); + sz = add_size(sz, mul_size(TotalProcs, sizeof(int))); + + return sz; +} + +/* + * HandleGetMemoryContextInterrupt + * Handle receipt of an interrupt indicating a request to publish memory + * contexts statistics. + * + * All the actual work is deferred to ProcessGetMemoryContextInterrupt() as + * this cannot be performed in a signal handler. + */ +void +HandleGetMemoryContextInterrupt(void) +{ + InterruptPending = true; + PublishMemoryContextPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + +/* + * ProcessGetMemoryContextInterrupt + * Generate information about memory contexts used by the process. + * + * Performs a breadth first search on the memory context tree, thus parents + * statistics are reported before their children in the monitoring function + * output. + * + * Statistics for all the processes are shared via the same dynamic shared + * area. Individual statistics are tracked independently in + * per-process DSA pointers. These pointers are stored in a dshash table with + * key as requesting clients ProcNumber. + * + * We calculate maximum number of context's statistics that can be displayed + * using a pre-determined limit for memory available per process for this + * utility and maximum size of statistics for each context. The remaining + * context statistics if any are captured as a cumulative total at the end of + * individual context's statistics. + * + * If summary is true, we capture the level 1 and level 2 contexts + * statistics. For that we traverse the memory context tree recursively in + * depth first search manner to cover all the children of a parent context, to + * be able to display a cumulative total of memory consumption by a parent at + * level 2 and all its children. + */ +void +ProcessGetMemoryContextInterrupt(void) +{ + List *contexts; + HASHCTL ctl; + HTAB *context_id_lookup; + int context_id = 0; + MemoryStatsEntry *meminfo; + bool summary = false; + MemoryContextCounters stat; + int num_individual_stats = 0; + bool found; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + int clientProcNumber; + MemoryContext memstats_ctx = NULL; + MemoryContext oldcontext = NULL; + + PublishMemoryContextPending = false; + + /* + * Retreive the client key for publishing statistics and reset it to -1, + * so other clients can request memory statistics from this process + */ + LWLockAcquire(client_keys_lock, LW_SHARED); + Assert(client_keys[MyProcNumber] != -1); + clientProcNumber = client_keys[MyProcNumber]; + client_keys[MyProcNumber] = -1; + LWLockRelease(client_keys_lock); + + /* + * Create a new memory context which is not a part of TopMemoryContext + * tree. This context is used to allocate all memory in this function. + * This helps in keeping the memory allocation in this function to report + * memory consumption statistics separate. So that it does not affect the + * output of this function. + */ + memstats_ctx = AllocSetContextCreate((MemoryContext) NULL, "publish_memory_context_statistics", + ALLOCSET_SMALL_SIZES); + oldcontext = MemoryContextSwitchTo(memstats_ctx); + + /* + * The hash table is used for constructing "path" column of the view, + * similar to its local backend counterpart. + */ + ctl.keysize = sizeof(MemoryContext); + ctl.entrysize = sizeof(MemoryStatsContextId); + ctl.hcxt = CurrentMemoryContext; + + context_id_lookup = hash_create("pg_get_remote_backend_memory_contexts", + 256, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + /* List of contexts to process in the next round - start at the top. */ + contexts = list_make1(TopMemoryContext); + + /* + * If DSA exists, created by another process requesting statistics, attach + * to it. We expect the client process to create required DSA and Dshash + * table. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", &found); + + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", &memctx_dsh_params, &found); + + + snprintf(key, CLIENT_KEY_SIZE, "%d", clientProcNumber); + + /* + * The entry lock is held by dshash_find_or_insert to protect writes to + * process specific memory. Two different processes publishing statistics + * do not block each other. + */ + INJECTION_POINT("memcontext-server-crash", NULL); + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + /* + * Entry has been deleted due to client process exit. Make sure that the + * client always deletes the entry after taking required lock or this + * function may end up writing to unallocated memory. + */ + if (!found) + { + entry->stats_written = false; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + return; + } + + /* + * The client has timed out waiting for us to write statistics and is + * requesting statistics from some other process + */ + if (entry->target_server_id != MyProcPid) + { + entry->stats_written = false; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + return; + } + summary = entry->summary; + + /* Should be allocated by a client backend that is requesting statistics */ + Assert(entry->memstats_dsa_pointer != InvalidDsaPointer); + meminfo = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + if (summary) + { + int cxt_id = 0; + List *path = NIL; + MemoryStatsContextId *contextid_entry; + + /* Copy TopMemoryContext statistics to DSA */ + memset(&stat, 0, sizeof(stat)); + (*TopMemoryContext->methods->stats) (TopMemoryContext, NULL, NULL, + &stat, true); + path = lcons_int(1, path); + PublishMemoryContext(meminfo, cxt_id, TopMemoryContext, path, stat, + 1); + + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &TopMemoryContext, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = cxt_id + 1; + + /* + * Copy statistics for each of TopMemoryContexts children. This + * includes statistics of at most 100 children per node, with each + * child node limited to a depth of 100 in its subtree. + */ + for (MemoryContext c = TopMemoryContext->firstchild; c != NULL; + c = c->nextchild) + { + MemoryContextCounters grand_totals; + int num_contexts = 0; + + path = NIL; + memset(&grand_totals, 0, sizeof(grand_totals)); + + cxt_id++; + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &c, + HASH_ENTER, &found); + Assert(!found); + contextid_entry->context_id = cxt_id + 1; + + MemoryContextStatsCounter(c, &grand_totals, &num_contexts); + + path = compute_context_path(c, context_id_lookup); + + PublishMemoryContext(meminfo, cxt_id, c, path, + grand_totals, num_contexts); + } + entry->total_stats = cxt_id + 1; + + entry->stats_written = true; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + /* Notify waiting backends and return */ + ConditionVariableBroadcast(&entry->memcxt_cv); + return; + } + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + MemoryStatsContextId *contextid_entry; + + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &cur, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = context_id + 1; + + /* + * Figure out the transient context_id of this context and each of its + * ancestors, to compute a path for this context. + */ + path = compute_context_path(cur, context_id_lookup); + + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + /* Account for saving one statistics slot for cumulative reporting */ + if (context_id < (MAX_MEMORY_CONTEXT_STATS_NUM - 1)) + { + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1); + } + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].totalspace += stat.totalspace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].nblocks += stat.nblocks; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freespace += stat.freespace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit per process is reached, write aggregate of the + * remaining statistics. + * + * We can store contexts from 0 to max_stats - 1. When context_id is + * greater than max_stats, we stop reporting individual statistics + * when context_id equals max_stats - 2. As we use max_stats - 1 array + * slot for reporting cumulative statistics or "Remaining Totals". + */ + if (context_id == (MAX_MEMORY_CONTEXT_STATS_NUM - 2)) + { + int namelen = strlen("Remaining Totals"); + + num_individual_stats = context_id + 1; + strlcpy(meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].name, "Remaining Totals", namelen + 1); + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].ident[0] = '\0'; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].path[0] = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].type = 0; + } + context_id++; + + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + contexts = lappend(contexts, c); + } + + /* + * Statistics are not aggregated, i.e individual statistics reported when + * context_id <= max_stats. + */ + if (context_id <= MAX_MEMORY_CONTEXT_STATS_NUM) + { + entry->total_stats = context_id; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = 1; + } + /* Report number of aggregated memory contexts */ + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = context_id - + num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for cumulative + * statistics. + */ + entry->total_stats = num_individual_stats + 1; + } + entry->stats_written = true; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + /* Notify waiting backends and return */ + ConditionVariableBroadcast(&entry->memcxt_cv); +} + +/* + * Clean up before exit from ProcessGetMemoryContextInterrupt + */ +static void +end_memorycontext_reporting(MemoryStatsDSHashEntry *entry, MemoryContext oldcontext, HTAB *context_id_lookup) +{ + MemoryContext curr_ctx = CurrentMemoryContext; + + dshash_release_lock(MemoryStatsDsHash, entry); + + hash_destroy(context_id_lookup); + MemoryContextSwitchTo(oldcontext); + MemoryContextReset(curr_ctx); +} + +/* + * compute_context_path + * + * Append the transient context_id of this context and each of its ancestors + * to a list, in order to compute a path. + */ +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + MemoryContext cur_context; + + for (cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryStatsContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + elog(ERROR, "hash table corrupted, can't construct path value"); + + path = lcons_int(cur_entry->context_id, path); + } + + return path; +} + +/* + * PublishMemoryContext + * + * Copy the memory context statistics of a single context to a DSA memory + */ +static void +PublishMemoryContext(MemoryStatsEntry *memcxt_info, int curr_id, + MemoryContext context, List *path, + MemoryContextCounters stat, int num_contexts) +{ + const char *ident = context->ident; + const char *name = context->name; + + /* + * To be consistent with logging output, we label dynahash contexts with + * just the hash table name as with MemoryContextStatsPrint(). + */ + if (context->ident && strncmp(context->name, "dynahash", 8) == 0) + { + name = context->ident; + ident = NULL; + } + + if (name != NULL) + { + int namelen = strlen(name); + + if (strlen(name) >= MEMORY_CONTEXT_NAME_SHMEM_SIZE) + namelen = pg_mbcliplen(name, namelen, + MEMORY_CONTEXT_NAME_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].name, name, namelen + 1); + } + else + /* Clearing the array */ + memcxt_info[curr_id].name[0] = '\0'; + + /* Trim and copy the identifier if it is not set to NULL */ + if (ident != NULL) + { + int idlen = strlen(context->ident); + + /* + * Some identifiers such as SQL query string can be very long, + * truncate oversize identifiers. + */ + if (idlen >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + idlen = pg_mbcliplen(ident, idlen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].ident, ident, idlen + 1); + } + else + memcxt_info[curr_id].ident[0] = '\0'; + + /* Allocate DSA memory for storing path information */ + if (path == NIL) + memcxt_info[curr_id].path[0] = 0; + else + { + int levels = Min(list_length(path), MAX_PATH_DISPLAY_LENGTH); + + memcxt_info[curr_id].path_length = levels; + memcxt_info[curr_id].levels = list_length(path); + + foreach_int(i, path) + { + memcxt_info[curr_id].path[foreach_current_index(i)] = i; + if (--levels == 0) + break; + } + } + memcxt_info[curr_id].type = context->type; + memcxt_info[curr_id].totalspace = stat.totalspace; + memcxt_info[curr_id].nblocks = stat.nblocks; + memcxt_info[curr_id].freespace = stat.freespace; + memcxt_info[curr_id].freechunks = stat.freechunks; + memcxt_info[curr_id].num_agg_stats = num_contexts; +} + +void +AtProcExit_memstats_cleanup(int code, Datum arg) +{ + int idx = MyProcNumber; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + bool found; + + if (MemoryStatsDsHash != NULL) + { + snprintf(key, CLIENT_KEY_SIZE, "%d", idx); + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + if (found) + { + if (MemoryStatsDsaArea != NULL && + DsaPointerIsValid(entry->memstats_dsa_pointer)) + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + } + dshash_delete_entry(MemoryStatsDsHash, entry); + } + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + client_keys[idx] = -1; + LWLockRelease(client_keys_lock); +} + +/* Used for testing purposes */ +dsa_area * +pg_get_memstats_dsa_area(void) +{ + if (MemoryStatsDsaArea != NULL) + return MemoryStatsDsaArea; + else + return NULL; +} diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index d31cb45a058..92b0446b80c 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -39,6 +39,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t PublishMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index 641e535a73c..fb3f2d21fa0 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -662,6 +662,13 @@ BaseInit(void) * drop ephemeral slots, which in turn triggers stats reporting. */ ReplicationSlotInitialize(); + + /* + * The before shmem exit callback frees the DSA memory occupied by the + * latest memory context statistics that could be published by this proc + * if requested. + */ + before_shmem_exit(AtProcExit_memstats_cleanup, 0); } diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 47fd774c7d2..56c2048c67a 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -1008,6 +1008,35 @@ MemoryContextStatsInternal(MemoryContext context, int level, } } + +/* + * MemoryContextStatsCounter + * + * Accumulate statistics counts into *totals. totals should not be NULL. + * This involves a non-recursive tree traversal. + */ +void +MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts) +{ + int ichild = 1; + + context->methods->stats(context, NULL, NULL, totals, false); + + for (MemoryContext curr = context->firstchild; + curr != NULL; + curr = MemoryContextTraverseNext(curr, context)) + { + curr->methods->stats(curr, NULL, NULL, totals, false); + ichild++; + } + + /* + * Add the count of children contexts which are traversed + */ + *num_contexts = *num_contexts + ichild; +} + /* * MemoryContextStatsPrint * Print callback used by MemoryContextStatsInternal diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index b51d2b17379..52e8e525d86 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8617,6 +8617,16 @@ prorettype => 'bool', proargtypes => 'int4', prosrc => 'pg_log_backend_memory_contexts' }, +# publishing memory contexts of the specified postgres process +{ oid => '2173', descr => 'publish memory contexts of the specified backend', + proname => 'pg_get_process_memory_contexts', provolatile => 'v', + prorows => '100', proretset => 't', proparallel => 'r', + prorettype => 'record', proargtypes => 'int4 bool', + proallargtypes => '{int4,bool,text,text,text,int4,_int4,int8,int8,int8,int8,int8,int4}', + proargmodes => '{i,i,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{pid, summary, name, ident, type, level, path, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes, num_agg_contexts}', + prosrc => 'pg_get_process_memory_contexts' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index 1bef98471c3..1e59a7f910f 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -96,6 +96,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t PublishMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h index 06a1ffd4b08..4c71d756a2d 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -135,3 +135,4 @@ PG_LWLOCKTRANCHE(SUBTRANS_SLRU, SubtransSLRU) PG_LWLOCKTRANCHE(XACT_SLRU, XactSLRU) PG_LWLOCKTRANCHE(PARALLEL_VACUUM_DSA, ParallelVacuumDSA) PG_LWLOCKTRANCHE(AIO_URING_COMPLETION, AioUringCompletion) +PG_LWLOCKTRANCHE(MEMORY_CONTEXT_KEYS, MemoryContextReportingKeys) diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index afeeb1ca019..345d5a0ecb1 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -35,6 +35,7 @@ typedef enum PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to send the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ /* Recovery conflict reasons */ diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 7bbe5a36959..2d7220cde45 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -18,7 +18,10 @@ #define MEMUTILS_H #include "nodes/memnodes.h" - +#include "storage/condition_variable.h" +#include "storage/lmgr.h" +#include "utils/dsa.h" +#include "lib/dshash.h" /* * MaxAllocSize, MaxAllocHugeSize @@ -48,6 +51,26 @@ #define AllocHugeSizeIsValid(size) ((Size) (size) <= MaxAllocHugeSize) +/* + * Memory Context reporting size limits. + */ + +/* Max length of context name and ident, to keep it consistent + * with ProcessLogMemoryContext() + */ +#define MEMORY_CONTEXT_IDENT_SHMEM_SIZE 100 +#define MEMORY_CONTEXT_NAME_SHMEM_SIZE 100 + +/* Maximum size (in bytes) of DSA area per process */ +#define MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND ((size_t) (1 * 1024 * 1024)) + +/* + * Maximum size per context statistics. The identifier and name are statically + * allocated arrays of size 100 bytes. + * The path depth is limited to 100 like for memory context logging. + */ +#define MAX_MEMORY_CONTEXT_STATS_SIZE (sizeof(MemoryStatsEntry)) +#define MAX_MEMORY_CONTEXT_STATS_NUM MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND / MAX_MEMORY_CONTEXT_STATS_SIZE /* * Standard top-level memory contexts. @@ -149,6 +172,7 @@ extern MemoryContext BumpContextCreate(MemoryContext parent, Size minContextSize, Size initBlockSize, Size maxBlockSize); +extern dsa_area *pg_get_memstats_dsa_area(void); /* * Recommended default alloc parameters, suitable for "ordinary" contexts @@ -319,4 +343,59 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) return true; } +/* Dynamic shared memory state for statistics per context */ +typedef struct MemoryStatsEntry +{ + char name[MEMORY_CONTEXT_NAME_SHMEM_SIZE]; + char ident[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + int path[100]; + NodeTag type; + int path_length; + int levels; + int64 totalspace; + int64 nblocks; + int64 freespace; + int64 freechunks; + int num_agg_stats; +} MemoryStatsEntry; + +/* + * Per backend dynamic shared hash entry for memory context statistics + * reporting. + */ +typedef struct MemoryStatsDSHashEntry +{ + char key[64]; + ConditionVariable memcxt_cv; + bool stats_written; + int target_server_id; + int total_stats; + bool summary; + dsa_pointer memstats_dsa_pointer; +} MemoryStatsDSHashEntry; + +static const dshash_parameters memctx_dsh_params = { + offsetof(MemoryStatsDSHashEntry, memcxt_cv), + sizeof(MemoryStatsDSHashEntry), + dshash_strcmp, + dshash_strhash, + dshash_strcpy +}; + +/* + * Used for storage of transient identifiers for pg_get_backend_memory_contexts + */ +typedef struct MemoryStatsContextId +{ + MemoryContext context; + int context_id; +} MemoryStatsContextId; + +extern void ProcessGetMemoryContextInterrupt(void); +extern void HandleGetMemoryContextInterrupt(void); +extern void MemoryContextKeysShmemInit(void); +extern Size MemoryContextKeysShmemSize(void); +extern void MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts); +extern void AtProcExit_memstats_cleanup(int code, Datum arg); #endif /* MEMUTILS_H */ diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 3b37fafa65b..21c65ad2d10 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -233,3 +233,22 @@ select * from pg_timezone_abbrevs where abbrev = 'LMT'; LMT | @ 7 hours 52 mins 58 secs ago | f (1 row) +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; +NOTICE: (AllocSet,TopMemoryContext,) +NOTICE: (AllocSet,TopMemoryContext,) diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 66179f026b3..c9da4fc8c90 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -101,3 +101,21 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs; -- One specific case we can check without much fear of breakage -- is the historical local-mean-time value used for America/Los_Angeles. select * from pg_timezone_abbrevs where abbrev = 'LMT'; + +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 377a7946585..e9a7adfd380 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1682,6 +1682,9 @@ MemoryContextData MemoryContextId MemoryContextMethodID MemoryContextMethods +MemoryStatsContextId +MemoryStatsEntry +MemoryStatsDSHashEntry MemoryStatsPrintFunc MergeAction MergeActionState -- 2.34.1 [application/octet-stream] v2-0002-Test-module-to-test-memory-context-reporting-with-in.patch (9.5K, ../../CAH2L28t=c_iBj0+LoKX0TSTrNJB2nTgH6MPD7QgRb8kSBZ7HVg@mail.gmail.com/4-v2-0002-Test-module-to-test-memory-context-reporting-with-in.patch) download | inline diff: From e21832c610574f48496c961a7318070b720568de Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Thu, 23 Oct 2025 18:01:36 +0530 Subject: [PATCH 2/2] Test module to test memory context reporting with injection points --- src/test/modules/Makefile | 1 + .../test_memcontext_reporting/Makefile | 32 +++++ .../t/001_memcontext_inj.pl | 58 +++++++++ .../test_memcontext_reporting--1.0.sql | 11 ++ .../test_memcontext_reporting.c | 123 ++++++++++++++++++ .../test_memcontext_reporting.control | 4 + 6 files changed, 229 insertions(+) create mode 100644 src/test/modules/test_memcontext_reporting/Makefile create mode 100644 src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting--1.0.sql create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index 902a7954101..a31a2578c18 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -31,6 +31,7 @@ SUBDIRS = \ test_json_parser \ test_lfind \ test_lwlock_tranches \ + test_memcontext_reporting \ test_misc \ test_oat_hooks \ test_parser \ diff --git a/src/test/modules/test_memcontext_reporting/Makefile b/src/test/modules/test_memcontext_reporting/Makefile new file mode 100644 index 00000000000..01a7baa0263 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/Makefile @@ -0,0 +1,32 @@ +# src/test/modules/test_memcontext_reporting/Makefile + +EXTRA_INSTALL = src/test/modules/injection_points + +export enable_injection_points +MODULE_big = test_memcontext_reporting +OBJS = \ + $(WIN32RES) \ + test_memcontext_reporting.o +PGFILEDESC = "test_memcontext_reporting - test code for memory context reporting" + +EXTENSION = test_memcontext_reporting +DATA = test_memcontext_reporting--1.0.sql + +REGRESS = test_memcontext_reporting + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_memcontext_reporting +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif + +check: + $(prove_check) + +installcheck: + $(prove_installcheck) diff --git a/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl new file mode 100644 index 00000000000..69d8489eb37 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl @@ -0,0 +1,58 @@ +# Copyright (c) 2025, PostgreSQL Global Development Group + +# Test suite for testing memory context statistics reporting + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} +my $psql_err; +# Create and start a cluster with one node +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init(allows_streaming => 1); +# max_connections need to be bumped in order to accommodate for pgbench clients +# and log_statement is dialled down since it otherwise will generate enormous +# amounts of logging. Page verification failures are still logged. +$node->append_conf( + 'postgresql.conf', + qq[ +max_connections = 100 +log_statement = none +]); +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION test_memcontext_reporting;'); +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); +# Attaching to a client process injection point that throws an error +$node->safe_psql('postgres', "select injection_points_attach('memcontext-client-crash', 'error');"); + +my $pid = $node->safe_psql('postgres', "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); +print "PID"; +print $pid; + +#Client should have thrown error +$node->psql('postgres', qq(select pg_get_process_memory_contexts($pid, true);), stderr => \$psql_err); +like ( $psql_err, qr/error triggered for injection point memcontext-client-crash/); + +#Query the same process after detaching the injection point, using some other client and it should succeed. +$node->safe_psql('postgres', "select injection_points_detach('memcontext-client-crash');"); +my $topcontext_name = $node->safe_psql('postgres', "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';"); +ok($topcontext_name = 'TopMemoryContext'); + +# Attaching to a target process injection point that throws an error +$node->safe_psql('postgres', "select injection_points_attach('memcontext-server-crash', 'error');"); + +#Server should have thrown error +$node->psql('postgres', qq(select pg_get_process_memory_contexts($pid, true);), stderr => \$psql_err); + +#Query the same process after detaching the injection point, using some other client and it should succeed. +$node->safe_psql('postgres', "select injection_points_detach('memcontext-server-crash');"); +$topcontext_name = $node->safe_psql('postgres', "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';"); +ok($topcontext_name = 'TopMemoryContext'); +done_testing(); diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting--1.0.sql b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting--1.0.sql new file mode 100644 index 00000000000..181daf429d0 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting--1.0.sql @@ -0,0 +1,11 @@ +CREATE FUNCTION memcontext_crash_server() +RETURNS pg_catalog.void +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION memcontext_crash_client() +RETURNS pg_catalog.void +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION dsa_dump_sql() +RETURNS bigint +AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c new file mode 100644 index 00000000000..955155524c2 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c @@ -0,0 +1,123 @@ +/* + * ------------------------------------------------------------------------- + * + * Copyright (c) 2025, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c + * + * ------------------------------------------------------------------------- + */ + +#include "postgres.h" +#include "utils/injection_point.h" +#include "funcapi.h" +#include "utils/injection_point.h" +#include "storage/dsm_registry.h" + +PG_MODULE_MAGIC; + +extern PGDLLEXPORT void crash(const char *name, const void *private_data, void *arg); + +void +crash(const char *name, const void *private_data, void *arg) +{ + abort(); +} + +/* + * memcontext_crash_client + * + * Ensure that the client process aborts in between memory context + * reporting. + */ +PG_FUNCTION_INFO_V1(memcontext_crash_client); +Datum +memcontext_crash_client(PG_FUNCTION_ARGS) +{ +#ifdef USE_INJECTION_POINTS + InjectionPointAttach("memcontext-client-crash", + "test_memcontext_reporting", "crash", NULL, 0); + +#else + elog(ERROR, + "test is not working as intended when injection points are disabled"); +#endif + PG_RETURN_VOID(); +} + +PG_FUNCTION_INFO_V1(memcontext_detach_client); +Datum +memcontext_detach_client(PG_FUNCTION_ARGS) +{ +#ifdef USE_INJECTION_POINTS + InjectionPointDetach("memcontext-client-crash"); + +#else + elog(ERROR, + "test is not working as intended when injection points are disabled"); +#endif + PG_RETURN_VOID(); +} + +/* + * memcontext_crash_server + * + * Ensure that the server process crashes in between memory context + * reporting. + */ +PG_FUNCTION_INFO_V1(memcontext_crash_server); +Datum +memcontext_crash_server(PG_FUNCTION_ARGS) +{ +#ifdef USE_INJECTION_POINTS + InjectionPointAttach("memcontext-server-crash", + "test_memcontext_reporting", "crash", NULL, 0); + +#else + elog(ERROR, + "test is not working as intended when injection points are disabled"); +#endif + PG_RETURN_VOID(); +} + +/* + * memcontext_detach_server + * + * Detach the injection point which crashes the server + * reporting. + */ +PG_FUNCTION_INFO_V1(memcontext_detach_server); +Datum +memcontext_detach_server(PG_FUNCTION_ARGS) +{ +#ifdef USE_INJECTION_POINTS + InjectionPointDetach("memcontext-server-crash"); + +#else + elog(ERROR, + "test is not working as intended when injection points are disabled"); +#endif + PG_RETURN_VOID(); +} + +/* + * dsa_dump_sql + */ +PG_FUNCTION_INFO_V1(dsa_dump_sql); +Datum +dsa_dump_sql(PG_FUNCTION_ARGS) +{ + bool found; + size_t tot_size; + dsa_area *memstats_dsa_area; + + memstats_dsa_area = pg_get_memstats_dsa_area(); + + if (memstats_dsa_area == NULL) + memstats_dsa_area = GetNamedDSA("memory_context_statistics_dsa", &found); + + tot_size = dsa_get_total_size(memstats_dsa_area); + dsa_detach(memstats_dsa_area); + PG_RETURN_INT64(tot_size); +} diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control new file mode 100644 index 00000000000..48b501682d5 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control @@ -0,0 +1,4 @@ +comment = 'Test code for memcontext reporting' +default_version = '1.0' +module_pathname = '$libdir/test_memcontext_reporting' +relocatable = true -- 2.34.1 ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-11-07 22:55 Rahila Syed <[email protected]> parent: Rahila Syed <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Rahila Syed @ 2025-11-07 22:55 UTC (permalink / raw) To: pgsql-hackers; +Cc: Daniel Gustafsson <[email protected]>; torikoshia <[email protected]> Hi, I have attached a version 40 patch that has been rebased onto the latest master branch, as CFbot indicated a rebase was needed. The test module patch is unchanged. Thank you, Rahila Syed On Tue, Oct 28, 2025 at 11:06 AM Rahila Syed <[email protected]> wrote: > Hi, > > PFA an updated v39 patch which is ready for review in the upcoming > commitfest. > > >> v35 works fine on my environment. >> I ran the same test and haven’t encountered the crash anymore. >> > > Thank you for testing and confirming the fix. > > >> The addition of the following code appears to have resolved the issue: >> >> +memstats_dsa_cleanup(char *key) >> +{ >> + MemoryStatsDSHashEntry *entry; >> + >> + entry = dshash_find(MemoryStatsDsHash, key, true); >> > > Yes, without this code, the dsa memory was being freed in the timeout path > without acquiring a lock. > > Since you seem to make a next version patch, I understand v35 is an >> interim patch, >> so this isn’t a major concern, but I encountered trailing whitespace >> warnings when applying the patches. >> > $ git apply >> 0001-v35-0001-Add-pg_get_process_memory_context-function.patch >> 0001-v35-0001-Add-pg_get_process_memory_context-function.patch:705: >> trailing whitespace. >> 0001-v35-0001-Add-pg_get_process_memory_context-function.patch:1066: >> trailing whitespace. >> >> > Thanks, should be fixed now. > > The updated patch contains the following changes. These changes are > addressing some review comments > discussed off list and a couple of bugs found while doing injection points > tests. > > 1. > All the changes made to MemoryContextStatsInternal and > MemoryContextStatsDetail are removed. > Instead of modifying these functions, I have written a separate function > MemoryContextStatsCounter > that takes care of counting statistics. This approach ensures that the > existing functions remain unchanged. > > 2. Changes to ensure that the wait loop does not exceed the prescribed > wait time. > Additional exit condition has been added to the infinite loop that waits > for request completion. > This allows the pg_get_memoy_context_statistics function to return if the > elapsed time goes beyond > a set limit i.e the following timeout. > > 3. The user facing timeout is removed as that would complicate the user > interface. CFIs > are called frequently and the requests are likely to be addressed promptly. > A predefined macro MEMORY_CONTEXT_STATS_TIMEOUT 5 (secs) is used for > timeout > instead. This would also remove the possibility of a user setting very > low timeouts, which > could cause requests to be incomplete and result in NULL outputs. > > 4. Miscellaneous cleanups to improve comments and remove left over > comments from older > versions. Also, removed an unnecessary argument from the > PublishMemoryContext function. > > 5. Addressed Torikoshias suggestion to change the order of columns to match > pg_backend_memory_contexts. > > 6. Attached is a test module that tests error handling by introducing > errors using > injection points. I have resolved a few bugs, so the memory monitoring > function > now runs correctly after the previous request ended with an error. > > Thank you, > Rahila Syed > Attachments: [application/octet-stream] v40-0001-Add-function-to-report-memory-context-statistics.patch (57.7K, ../../CAH2L28v5qXPpfmVLFDcMFL4MMqsKP92FXhW2eosHA5LbatKnrw@mail.gmail.com/3-v40-0001-Add-function-to-report-memory-context-statistics.patch) download | inline diff: From c8cddf59c3d9b1f6967acd86ec79945bac06c2f0 Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Sat, 8 Nov 2025 04:06:21 +0530 Subject: [PATCH] Add function to report memory context statistics This adds a function for retrieving memory context statistics and information from backends as well as auxiliary processes. The intended usecase is cluster debugging when under memory pressure or unanticipated memory usage characteristics. When calling the function it sends a signal to the specified process to submit statistics regarding its memory contexts into dynamic shared memory. Each memory context is returned in detail, followed by a cumulative total in case the number of contexts exceed the max allocated amount of shared memory. Each process is limited to use at most 1Mb memory for this. A summary can also be explicitly requested by the user, this will return the TopMemoryContext and a cumulative total of all lower contexts. In order to not block on busy processes, we have hardcoded the number of seconds during which to retry before timing out. In the case where no statistics are published within the set timeout, NULL is returned --- doc/src/sgml/func/func-admin.sgml | 157 +++ src/backend/catalog/system_views.sql | 5 + src/backend/postmaster/autovacuum.c | 4 + src/backend/postmaster/checkpointer.c | 4 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 4 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 4 + src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/storage/lmgr/proc.c | 1 + src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 2 + src/backend/utils/adt/mcxtfuncs.c | 910 +++++++++++++++++- src/backend/utils/init/globals.c | 1 + src/backend/utils/init/postinit.c | 7 + src/backend/utils/mmgr/mcxt.c | 29 + src/include/catalog/pg_proc.dat | 10 + src/include/miscadmin.h | 1 + src/include/storage/lwlocklist.h | 1 + src/include/storage/procsignal.h | 1 + src/include/utils/memutils.h | 81 +- src/test/regress/expected/sysviews.out | 19 + src/test/regress/sql/sysviews.sql | 18 + src/tools/pgindent/typedefs.list | 3 + 25 files changed, 1255 insertions(+), 24 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 1b465bc8ba7..a5c66837241 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -251,6 +251,130 @@ <literal>false</literal> is returned. </para></entry> </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_get_process_memory_contexts</primary> + </indexterm> + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type>, <parameter>summary</parameter> <type>boolean</type> ) + <returnvalue>setof record</returnvalue> + ( <parameter>name</parameter> <type>text</type>, + <parameter>ident</parameter> <type>text</type>, + <parameter>type</parameter> <type>text</type>, + <parameter>level</parameter> <type>integer</type>, + <parameter>path</parameter> <type>integer[]</type>, + <parameter>total_bytes</parameter> <type>bigint</type>, + <parameter>total_nblocks</parameter> <type>bigint</type>, + <parameter>free_bytes</parameter> <type>bigint</type>, + <parameter>free_chunks</parameter> <type>bigint</type>, + <parameter>used_bytes</parameter> <type>bigint</type>, + <parameter>num_agg_contexts</parameter> <type>integer</type> ) + </para> + <para> + This function handles requests to display the memory contexts of a + <productname>PostgreSQL</productname> process with the specified + process ID. The function can be used to send requests to backends as + well as <glossterm linkend="glossary-auxiliary-proc">auxiliary processes</glossterm>. + </para> + <para> + The returned record contains extended statistics per each memory + context: + <itemizedlist spacing="compact"> + <listitem> + <para> + <parameter>name</parameter> - The name of the memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>ident</parameter> - Memory context ID (if any). + </para> + </listitem> + <listitem> + <para> + <parameter>type</parameter> - The type of memory context, possible + values are: AllocSet, Generation, Slab and Bump. + </para> + </listitem> + <listitem> + <para> + <parameter>level</parameter> - The level in the tree of the current + memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>path</parameter> - Memory contexts are organized in a + tree model with TopMemoryContext as the root, and all other memory + contexts as nodes in the tree. The <parameter>path</parameter> + displays the path from the root to the current memory context. The + path is limited to 100 children per node, which each node limited + to a max depth of 100, to preserve memory during reporting. The + printed path will also be limited to 100 nodes counting from the + TopMemoryContext. + </para> + </listitem> + <listitem> + <para> + <parameter>total_bytes</parameter> - The total number of bytes + allocated to this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_nblocks</parameter> - The total number of blocks + used for the allocated memory. + </para> + </listitem> + <listitem> + <para> + <parameter>free_bytes</parameter> - The amount of free memory in + this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>free_chunks</parameter> - The number of chunks that + <parameter>free_bytes</parameter> corresponds to. + </para> + </listitem> + <listitem> + <para> + <parameter>used_bytes</parameter> - The total number of bytes + currently occupied. + </para> + </listitem> + <listitem> + <para> + <parameter>num_agg_contexts</parameter> - The number of memory + contexts aggregated in the displayed statistics. + </para> + </listitem> + </itemizedlist> + </para> + <para> + When <parameter>summary</parameter> is <literal>true</literal>, statistics + for memory contexts at levels 1 and 2 are displayed, with level 1 + representing the root node (i.e., <literal>TopMemoryContext</literal>). + Statistics for contexts on level 2 and below are aggregates of all + child contexts' statistics, where <literal>num_agg_contexts</literal> + indicate the number aggregated child contexts. When + <parameter>summary</parameter> is <literal>false</literal>, + <literal>the num_agg_contexts</literal> value is <literal>1</literal>, + indicating that individual statistics are being displayed. + </para> + <para> + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context + statistics are aggregated and a cumulative total is displayed. The + <literal>num_agg_contexts</literal> column indicates the number of + contexts aggregated in the displayed statistics. When + <literal>num_agg_contexts</literal> is <literal>1</literal> it means + that the context statistics are displayed separately. + </para></entry> + </row> </tbody> </tgroup> </table> @@ -302,6 +426,39 @@ LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 because it may generate a large number of log messages. </para> + <para> + <function>pg_get_process_memory_contexts</function> can be used to request + memory contexts statistics of any <productname>PostgreSQL</productname> + process. For example: +<programlisting> +postgres=# SELECT * FROM pg_get_process_memory_contexts( + (SELECT pid FROM pg_stat_activity + WHERE backend_type = 'checkpointer'), + false) LIMIT 1; +-[ RECORD 1 ]----+------------------------------ +name | TopMemoryContext +ident | +type | AllocSet +level | 1 +path | {1} +total_bytes | 90304 +total_nblocks | 3 +free_bytes | 2880 +free_chunks | 1 +used_bytes | 87424 +num_agg_contexts | 1 +</programlisting> + <note> + <para> + While <function>pg_get_process_memory_contexts</function> can be used to + query memory contexts of the local backend, + <structname>pg_backend_memory_contexts</structname> + (see <xref linkend="view-pg-backend-memory-contexts"/> for more details) + will be less resource intensive when only the local backend is of interest. + </para> + </note> + </para> + </sect2> <sect2 id="functions-admin-backup"> diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index 059e8778ca7..c63fd6783bd 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -692,6 +692,11 @@ GRANT SELECT ON pg_backend_memory_contexts TO pg_read_all_stats; REVOKE EXECUTE ON FUNCTION pg_get_backend_memory_contexts() FROM PUBLIC; GRANT EXECUTE ON FUNCTION pg_get_backend_memory_contexts() TO pg_read_all_stats; +REVOKE EXECUTE ON FUNCTION + pg_get_process_memory_contexts(integer, boolean) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION + pg_get_process_memory_contexts(integer, boolean) TO pg_read_all_stats; + -- Statistics views CREATE VIEW pg_stat_all_tables AS diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index ed19c74bb19..34bdb88fa7f 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -791,6 +791,10 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index e84e8663e96..5b3e08805bf 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -679,6 +679,10 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index ba63b84dfc5..29454b8bf8a 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -48,6 +48,10 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index ce6b5299324..fdd385e492d 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -871,6 +871,10 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index 27e86cf393f..7149a67fcbc 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -192,6 +192,10 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index c4a888a081c..00f03b36ed8 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -879,6 +879,10 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index b23d0c19360..a5ed58a18c5 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -52,6 +52,7 @@ #include "storage/sinvaladt.h" #include "utils/guc.h" #include "utils/injection_point.h" +#include "utils/memutils.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -140,6 +141,7 @@ CalculateShmemSize(void) size = add_size(size, SlotSyncShmemSize()); size = add_size(size, AioShmemSize()); size = add_size(size, WaitLSNShmemSize()); + size = add_size(size, MemoryContextKeysShmemSize() + sizeof(LWLockPadded)); /* include additional requested shmem from preload libraries */ size = add_size(size, total_addin_request); @@ -328,6 +330,7 @@ CreateOrAttachShmemStructs(void) InjectionPointShmemInit(); AioShmemInit(); WaitLSNShmemInit(); + MemoryContextKeysShmemInit(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index 087821311cc..8963285cc12 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -691,6 +691,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_GET_MEMORY_CONTEXT)) + HandleGetMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1504fafe6d8..c5e69151756 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -51,6 +51,7 @@ #include "storage/procsignal.h" #include "storage/spin.h" #include "storage/standby.h" +#include "utils/memutils.h" #include "utils/timeout.h" #include "utils/timestamp.h" diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 2bd89102686..da8f2b97986 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3549,6 +3549,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index c1ac71ff7f2..644d8d988e1 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -162,6 +162,7 @@ WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." +MEM_CXT_PUBLISH "Waiting for a process to publish memory information." ABI_compatibility: @@ -404,6 +405,7 @@ SubtransSLRU "Waiting to access the sub-transaction SLRU cache." XactSLRU "Waiting to access the transaction status SLRU cache." ParallelVacuumDSA "Waiting for parallel vacuum dynamic shared memory allocation." AioUringCompletion "Waiting for another process to complete IO via io_uring." +MemoryContextReportingKeys "Waiting for another process to complete reading or writing the memory reporting keys." # No "ABI_compatibility" region here as WaitEventLWLock has its own C code. diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index fe6dce9cba3..a62f3d6dc93 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -15,20 +15,51 @@ #include "postgres.h" +#include "access/twophase.h" +#include "catalog/pg_authid_d.h" #include "funcapi.h" #include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "storage/dsm_registry.h" #include "storage/proc.h" #include "storage/procarray.h" +#include "utils/acl.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/injection_point.h" +#include "utils/memutils.h" +#include "utils/wait_event_types.h" + +#define CLIENT_KEY_SIZE 64 + +static LWLock *client_keys_lock = NULL; +static int *client_keys = NULL; +static dshash_table *MemoryStatsDsHash = NULL; +static dsa_area *MemoryStatsDsaArea = NULL; + +static void memstats_dsa_cleanup(char *key); +static const char *ContextTypeToString(NodeTag type); +static void PublishMemoryContext(MemoryStatsEntry *memcxt_info, + int curr_id, MemoryContext context, + List *path, + MemoryContextCounters stat, + int num_contexts); +static List *compute_context_path(MemoryContext c, HTAB *context_id_lookup); +static void end_memorycontext_reporting(MemoryStatsDSHashEntry *entry, MemoryContext oldcontext, + HTAB *context_id_lookup); /* ---------- * The max bytes for showing identifiers of MemoryContext. + * This is used by pg_get_backend_memory_context - view used for local backend. * ---------- */ #define MEMORY_CONTEXT_IDENT_DISPLAY_SIZE 1024 +#define MAX_PATH_DISPLAY_LENGTH 100 +/* Timeout in seconds */ +#define MEMORY_STATS_MAX_TIMEOUT 5 + /* * MemoryContextId * Used for storage of transient identifiers for @@ -89,7 +120,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, */ for (MemoryContext cur = context; cur != NULL; cur = cur->parent) { - MemoryContextId *entry; + MemoryStatsContextId *entry; bool found; entry = hash_search(context_id_lookup, &cur, HASH_FIND, &found); @@ -143,24 +174,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, else nulls[1] = true; - switch (context->type) - { - case T_AllocSetContext: - type = "AllocSet"; - break; - case T_GenerationContext: - type = "Generation"; - break; - case T_SlabContext: - type = "Slab"; - break; - case T_BumpContext: - type = "Bump"; - break; - default: - type = "???"; - break; - } + type = ContextTypeToString(context->type); values[2] = CStringGetTextDatum(type); values[3] = Int32GetDatum(list_length(path)); /* level */ @@ -175,6 +189,38 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, list_free(path); } +/* + * ContextTypeToString + * Returns a textual representation of a context type + * + * This should cover the same types as MemoryContextIsValid. + */ +const char * +ContextTypeToString(NodeTag type) +{ + const char *context_type; + + switch (type) + { + case T_AllocSetContext: + context_type = "AllocSet"; + break; + case T_GenerationContext: + context_type = "Generation"; + break; + case T_SlabContext: + context_type = "Slab"; + break; + case T_BumpContext: + context_type = "Bump"; + break; + default: + context_type = "???"; + break; + } + return context_type; +} + /* * pg_get_backend_memory_contexts * SQL SRF showing backend memory context. @@ -189,7 +235,7 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) HTAB *context_id_lookup; ctl.keysize = sizeof(MemoryContext); - ctl.entrysize = sizeof(MemoryContextId); + ctl.entrysize = sizeof(MemoryStatsContextId); ctl.hcxt = CurrentMemoryContext; context_id_lookup = hash_create("pg_get_backend_memory_contexts", @@ -216,7 +262,7 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) foreach_ptr(MemoryContextData, cur, contexts) { - MemoryContextId *entry; + MemoryStatsContextId *entry; bool found; /* @@ -224,8 +270,8 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) * PutMemoryContextsStatsTupleStore needs this to populate the "path" * column with the parent context_ids. */ - entry = (MemoryContextId *) hash_search(context_id_lookup, &cur, - HASH_ENTER, &found); + entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &cur, + HASH_ENTER, &found); entry->context_id = context_id++; Assert(!found); @@ -305,3 +351,821 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } + +/* + * pg_get_process_memory_contexts + * Signal a backend or an auxiliary process to send its memory contexts, + * wait for the results and display them. + * + * By default, only superusers or users with ROLE_PG_READ_ALL_STATS are allowed + * to signal a process to return the memory contexts. This is because allowing + * any users to issue this request at an unbounded rate would cause lots of + * requests to be sent, which can lead to denial of service. Additional roles + * can be permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to copy the memory context details + * to a dynamic shared memory space. + * + * We have defined a limit on DSA memory that could be allocated per process - + * if the process has more memory contexts than what can fit in the allocated + * size, the excess contexts are summarized and represented as cumulative total + * at the end of the buffer. + * + * After sending the signal, wait on a condition variable. The publishing + * backend, after copying the data to shared memory, sends signal on that + * condition variable. There is one condition variable per client process. + * Once the condition variable is signalled, check if the latest memory context + * information is available and display. + * + * If the publishing backend does not respond before the condition variable + * times out, which is set to a predefined value MEMORY_STATS_MAX_TIMEOUT, give up + * and return NULL. + */ +Datum +pg_get_process_memory_contexts(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + bool summary = PG_GETARG_BOOL(1); + PGPROC *proc; + ProcNumber procNumber = INVALID_PROC_NUMBER; + bool proc_is_aux = false; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + MemoryStatsEntry *memcxt_info; + MemoryStatsDSHashEntry *entry; + bool found; + char key[CLIENT_KEY_SIZE]; + TimestampTz start_timestamp; + + /* + * See if the process with given pid is a backend or an auxiliary process + * and remember the type for when we requery the process later. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + { + proc = AuxiliaryPidGetProc(pid); + proc_is_aux = true; + } + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; this is however not a problem and leave with a WARNING. + * See comment in pg_log_backend_memory_contexts for a discussion on this. + */ + if (proc == NULL) + { + /* + * This is a warning because we don't want to break loops. + */ + ereport(WARNING, + errmsg("PID %d is not a PostgreSQL server process", pid)); + PG_RETURN_NULL(); + } + + InitMaterializedSRF(fcinfo, 0); + + procNumber = GetNumberFromPGProc(proc); + + /* + * Create a DSA to allocate memory for copying memory contexts statistics. + * Allocate the memory in the DSA and send dsa pointer to the server + * process for storing the context statistics. If number of contexts + * exceed a predefined limit (1MB), a cumulative total is stored for such + * contexts. + * + * The DSA is created once for the lifetime of the server, and only + * attached in subsequent calls. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", &found); + + /* + * The dsa pointers containing statistics for each client are stored in a + * dshash table. In addition to dsa pointer, each entry in this table also + * contains information about the statistics, condition variable for + * signalling between client and the server and miscellaneous data + * specific to a request. There is one entry per client request in the + * hash table. + */ + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", &memctx_dsh_params, &found); + + snprintf(key, sizeof(key), "%d", MyProcNumber); + + /* + * Check if the publishing process slot is empty and store this clients + * key i.e its procNumber. This informs the publishing process that it is + * supposed to write statistics in the hash entry corresponding to this + * client. + */ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + + /* + * XXX. If the process exits without cleaning up its slot, i.e in case of + * an abrupt crash the client_keys slot won't be reset thus resulting in + * false negative and WARNING would be thrown in case another process with + * same slot index is queried for statistics. + */ + if (client_keys[procNumber] == -1) + client_keys[procNumber] = MyProcNumber; + else + { + LWLockRelease(client_keys_lock); + ereport(WARNING, + errmsg("server process %d is processing previous request", pid)); + PG_RETURN_NULL(); + } + LWLockRelease(client_keys_lock); + + /* + * Insert an entry for this client in DSHASH table the first time this + * function is called. This entry is deleted when the process exits in + * before_shmem_exit call. + * + * dshash_find_or_insert locks the entry to prevent the publisher from + * reading before client has updated the entry. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + if (!found) + { + entry->stats_written = false; + ConditionVariableInit(&entry->memcxt_cv); + } + + /* + * Allocate 1MB of memory for the backend to publish its statistics on + * every call to this function. The memory is freed at the end of the + * function. + */ + entry->memstats_dsa_pointer = + dsa_allocate0(MemoryStatsDsaArea, MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND); + + /* + * Specify whether a summary of statistics is requested, before signalling + * the server. + */ + entry->summary = summary; + + /* + * Indicate which server process statistics are being requested from. If + * this client times out before the last requested process can publish its + * statistics, it may send a new request to another server process. Since + * the previous server was notified, it might attempt to read the same + * client entry and respond incorrectly with its statistics. By storing + * the server ID in the client entry, we prevent any previously signalled + * server process from writing its statistics in the space meant for the + * newly requested process. + */ + entry->target_server_id = pid; + dshash_release_lock(MemoryStatsDsHash, entry); + + /* + * Send a signal to a PostgreSQL process, informing it we want it to + * produce information about its memory contexts. + */ + if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, procNumber) < 0) + { + memstats_dsa_cleanup(key); + ereport(WARNING, + errmsg("could not send signal to process %d: %m", pid)); + PG_RETURN_NULL(); + } + start_timestamp = GetCurrentTimestamp(); + + while (1) + { + long elapsed_time; + + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + Assert(found); + + INJECTION_POINT("memcontext-client-crash", NULL); + + memcxt_info = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + /* + * We expect to come out of sleep when the requested process has + * finished publishing the statistics, verified using a boolean + * stats_written. + * + * Make sure that the statistics are actually written by checking that + * the name of the context is not NULL. This is done to ensure that + * the subsequent waits for statistics do not return spuriously if the + * previous call to the function ended in error and thus could not + * clear the stats_written flag. + */ + if (entry->stats_written && memcxt_info[0].name[0] != '\0') + break; + + dshash_release_lock(MemoryStatsDsHash, entry); + + elapsed_time = TimestampDifferenceMilliseconds(start_timestamp, + GetCurrentTimestamp()); + /* Return if we have already exceeded the timeout */ + if (elapsed_time >= MEMORY_STATS_MAX_TIMEOUT * 1000) + { + memstats_dsa_cleanup(key); + PG_RETURN_NULL(); + } + + /* + * Recheck the state of the backend before sleeping on the condition + * variable to ensure the process is still alive. Only check the + * relevant process type based on the earlier PID check. + */ + if (proc_is_aux) + proc = AuxiliaryPidGetProc(pid); + else + proc = BackendPidGetProc(pid); + + /* + * The target server process ending during memory context processing + * is not an error. + */ + if (proc == NULL) + { + memstats_dsa_cleanup(key); + ConditionVariableCancelSleep(); + ereport(WARNING, + errmsg("PID %d is no longer a PostgreSQL server process", + pid)); + PG_RETURN_NULL(); + } + + INJECTION_POINT("memcontext-client-crash", NULL); + + /* + * Wait for MEMORY_STATS_MAX_TIMEOUT. If no statistics are available + * within the allowed time then return NULL. The timer is defined in + * milliseconds since that's what the condition variable sleep uses. + */ + if (ConditionVariableTimedSleep(&entry->memcxt_cv, + (MEMORY_STATS_MAX_TIMEOUT * 1000), WAIT_EVENT_MEM_CXT_PUBLISH)) + { + /* Timeout has expired, return NULL */ + memstats_dsa_cleanup(key); + ConditionVariableCancelSleep(); + PG_RETURN_NULL(); + } + } + + /* + * Backend has finished publishing the stats, project them. + */ +#define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 11 + for (int i = 0; i < entry->total_stats; i++) + { + ArrayType *path_array; + int path_length; + Datum values[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + bool nulls[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + Datum *path_datum = NULL; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + Assert(memcxt_info[i].name[0] != '\0'); + values[0] = CStringGetTextDatum(memcxt_info[i].name); + + if (memcxt_info[i].ident[0] != '\0') + values[1] = CStringGetTextDatum(memcxt_info[i].ident); + else + nulls[1] = true; + + values[2] = CStringGetTextDatum(ContextTypeToString(memcxt_info[i].type)); + values[3] = Int32GetDatum(memcxt_info[i].levels); + + path_length = memcxt_info[i].path_length; + path_datum = (Datum *) palloc(path_length * sizeof(Datum)); + if (memcxt_info[i].path[0] != 0) + { + for (int j = 0; j < path_length; j++) + path_datum[j] = Int32GetDatum(memcxt_info[i].path[j]); + path_array = construct_array_builtin(path_datum, path_length, INT4OID); + values[4] = PointerGetDatum(path_array); + } + else + nulls[4] = true; + + values[5] = Int64GetDatum(memcxt_info[i].totalspace); + values[6] = Int64GetDatum(memcxt_info[i].nblocks); + values[7] = Int64GetDatum(memcxt_info[i].freespace); + values[8] = Int64GetDatum(memcxt_info[i].freechunks); + values[9] = Int64GetDatum(memcxt_info[i].totalspace - + memcxt_info[i].freespace); + values[10] = Int32GetDatum(memcxt_info[i].num_agg_stats); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + dshash_release_lock(MemoryStatsDsHash, entry); + memstats_dsa_cleanup(key); + + ConditionVariableCancelSleep(); + + PG_RETURN_NULL(); +} + +static void +memstats_dsa_cleanup(char *key) +{ + MemoryStatsDSHashEntry *entry; + + entry = dshash_find(MemoryStatsDsHash, key, true); + + Assert(MemoryStatsDsaArea != NULL); + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + entry->memstats_dsa_pointer = InvalidDsaPointer; + entry->stats_written = false; + entry->target_server_id = 0; + + dshash_release_lock(MemoryStatsDsHash, entry); +} +void +MemoryContextKeysShmemInit(void) +{ + bool found; + + client_keys = (int *) + ShmemInitStruct("MemoryContextKeys", + MemoryContextKeysShmemSize() + sizeof(LWLockPadded), &found); + client_keys_lock = (LWLock *) ((char *) client_keys + MemoryContextKeysShmemSize()); + + if (!found) + { + MemSet(client_keys, -1, MemoryContextKeysShmemSize()); + LWLockInitialize(client_keys_lock, LWTRANCHE_MEMORY_CONTEXT_KEYS); + } +} + +Size +MemoryContextKeysShmemSize(void) +{ + Size sz = 0; + Size TotalProcs = 0; + + TotalProcs = add_size(TotalProcs, NUM_AUXILIARY_PROCS); + TotalProcs = add_size(TotalProcs, MaxBackends); + sz = add_size(sz, mul_size(TotalProcs, sizeof(int))); + + return sz; +} + +/* + * HandleGetMemoryContextInterrupt + * Handle receipt of an interrupt indicating a request to publish memory + * contexts statistics. + * + * All the actual work is deferred to ProcessGetMemoryContextInterrupt() as + * this cannot be performed in a signal handler. + */ +void +HandleGetMemoryContextInterrupt(void) +{ + InterruptPending = true; + PublishMemoryContextPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + +/* + * ProcessGetMemoryContextInterrupt + * Generate information about memory contexts used by the process. + * + * Performs a breadth first search on the memory context tree, thus parents + * statistics are reported before their children in the monitoring function + * output. + * + * Statistics for all the processes are shared via the same dynamic shared + * area. Individual statistics are tracked independently in + * per-process DSA pointers. These pointers are stored in a dshash table with + * key as requesting clients ProcNumber. + * + * We calculate maximum number of context's statistics that can be displayed + * using a pre-determined limit for memory available per process for this + * utility and maximum size of statistics for each context. The remaining + * context statistics if any are captured as a cumulative total at the end of + * individual context's statistics. + * + * If summary is true, we capture the level 1 and level 2 contexts + * statistics. For that we traverse the memory context tree recursively in + * depth first search manner to cover all the children of a parent context, to + * be able to display a cumulative total of memory consumption by a parent at + * level 2 and all its children. + */ +void +ProcessGetMemoryContextInterrupt(void) +{ + List *contexts; + HASHCTL ctl; + HTAB *context_id_lookup; + int context_id = 0; + MemoryStatsEntry *meminfo; + bool summary = false; + MemoryContextCounters stat; + int num_individual_stats = 0; + bool found; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + int clientProcNumber; + MemoryContext memstats_ctx = NULL; + MemoryContext oldcontext = NULL; + + PublishMemoryContextPending = false; + + /* + * Retreive the client key for publishing statistics and reset it to -1, + * so other clients can request memory statistics from this process + */ + LWLockAcquire(client_keys_lock, LW_SHARED); + Assert(client_keys[MyProcNumber] != -1); + clientProcNumber = client_keys[MyProcNumber]; + client_keys[MyProcNumber] = -1; + LWLockRelease(client_keys_lock); + + /* + * Create a new memory context which is not a part of TopMemoryContext + * tree. This context is used to allocate all memory in this function. + * This helps in keeping the memory allocation in this function to report + * memory consumption statistics separate. So that it does not affect the + * output of this function. + */ + memstats_ctx = AllocSetContextCreate((MemoryContext) NULL, "publish_memory_context_statistics", + ALLOCSET_SMALL_SIZES); + oldcontext = MemoryContextSwitchTo(memstats_ctx); + + /* + * The hash table is used for constructing "path" column of the view, + * similar to its local backend counterpart. + */ + ctl.keysize = sizeof(MemoryContext); + ctl.entrysize = sizeof(MemoryStatsContextId); + ctl.hcxt = CurrentMemoryContext; + + context_id_lookup = hash_create("pg_get_remote_backend_memory_contexts", + 256, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + /* List of contexts to process in the next round - start at the top. */ + contexts = list_make1(TopMemoryContext); + + /* + * If DSA exists, created by another process requesting statistics, attach + * to it. We expect the client process to create required DSA and Dshash + * table. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", &found); + + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", &memctx_dsh_params, &found); + + + snprintf(key, CLIENT_KEY_SIZE, "%d", clientProcNumber); + + /* + * The entry lock is held by dshash_find_or_insert to protect writes to + * process specific memory. Two different processes publishing statistics + * do not block each other. + */ + INJECTION_POINT("memcontext-server-crash", NULL); + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + /* + * Entry has been deleted due to client process exit. Make sure that the + * client always deletes the entry after taking required lock or this + * function may end up writing to unallocated memory. + */ + if (!found) + { + entry->stats_written = false; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + return; + } + + /* + * The client has timed out waiting for us to write statistics and is + * requesting statistics from some other process + */ + if (entry->target_server_id != MyProcPid) + { + entry->stats_written = false; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + return; + } + summary = entry->summary; + + /* Should be allocated by a client backend that is requesting statistics */ + Assert(entry->memstats_dsa_pointer != InvalidDsaPointer); + meminfo = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + if (summary) + { + int cxt_id = 0; + List *path = NIL; + MemoryStatsContextId *contextid_entry; + + /* Copy TopMemoryContext statistics to DSA */ + memset(&stat, 0, sizeof(stat)); + (*TopMemoryContext->methods->stats) (TopMemoryContext, NULL, NULL, + &stat, true); + path = lcons_int(1, path); + PublishMemoryContext(meminfo, cxt_id, TopMemoryContext, path, stat, + 1); + + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &TopMemoryContext, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = cxt_id + 1; + + /* + * Copy statistics for each of TopMemoryContexts children. This + * includes statistics of at most 100 children per node, with each + * child node limited to a depth of 100 in its subtree. + */ + for (MemoryContext c = TopMemoryContext->firstchild; c != NULL; + c = c->nextchild) + { + MemoryContextCounters grand_totals; + int num_contexts = 0; + + path = NIL; + memset(&grand_totals, 0, sizeof(grand_totals)); + + cxt_id++; + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &c, + HASH_ENTER, &found); + Assert(!found); + contextid_entry->context_id = cxt_id + 1; + + MemoryContextStatsCounter(c, &grand_totals, &num_contexts); + + path = compute_context_path(c, context_id_lookup); + + PublishMemoryContext(meminfo, cxt_id, c, path, + grand_totals, num_contexts); + } + entry->total_stats = cxt_id + 1; + + entry->stats_written = true; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + /* Notify waiting backends and return */ + ConditionVariableBroadcast(&entry->memcxt_cv); + return; + } + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + MemoryStatsContextId *contextid_entry; + + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &cur, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = context_id + 1; + + /* + * Figure out the transient context_id of this context and each of its + * ancestors, to compute a path for this context. + */ + path = compute_context_path(cur, context_id_lookup); + + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + /* Account for saving one statistics slot for cumulative reporting */ + if (context_id < (MAX_MEMORY_CONTEXT_STATS_NUM - 1)) + { + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1); + } + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].totalspace += stat.totalspace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].nblocks += stat.nblocks; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freespace += stat.freespace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit per process is reached, write aggregate of the + * remaining statistics. + * + * We can store contexts from 0 to max_stats - 1. When context_id is + * greater than max_stats, we stop reporting individual statistics + * when context_id equals max_stats - 2. As we use max_stats - 1 array + * slot for reporting cumulative statistics or "Remaining Totals". + */ + if (context_id == (MAX_MEMORY_CONTEXT_STATS_NUM - 2)) + { + int namelen = strlen("Remaining Totals"); + + num_individual_stats = context_id + 1; + strlcpy(meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].name, "Remaining Totals", namelen + 1); + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].ident[0] = '\0'; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].path[0] = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].type = 0; + } + context_id++; + + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + contexts = lappend(contexts, c); + } + + /* + * Statistics are not aggregated, i.e individual statistics reported when + * context_id <= max_stats. + */ + if (context_id <= MAX_MEMORY_CONTEXT_STATS_NUM) + { + entry->total_stats = context_id; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = 1; + } + /* Report number of aggregated memory contexts */ + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = context_id - + num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for cumulative + * statistics. + */ + entry->total_stats = num_individual_stats + 1; + } + entry->stats_written = true; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + /* Notify waiting backends and return */ + ConditionVariableBroadcast(&entry->memcxt_cv); +} + +/* + * Clean up before exit from ProcessGetMemoryContextInterrupt + */ +static void +end_memorycontext_reporting(MemoryStatsDSHashEntry *entry, MemoryContext oldcontext, HTAB *context_id_lookup) +{ + MemoryContext curr_ctx = CurrentMemoryContext; + + dshash_release_lock(MemoryStatsDsHash, entry); + + hash_destroy(context_id_lookup); + MemoryContextSwitchTo(oldcontext); + MemoryContextReset(curr_ctx); +} + +/* + * compute_context_path + * + * Append the transient context_id of this context and each of its ancestors + * to a list, in order to compute a path. + */ +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + MemoryContext cur_context; + + for (cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryStatsContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + elog(ERROR, "hash table corrupted, can't construct path value"); + + path = lcons_int(cur_entry->context_id, path); + } + + return path; +} + +/* + * PublishMemoryContext + * + * Copy the memory context statistics of a single context to a DSA memory + */ +static void +PublishMemoryContext(MemoryStatsEntry *memcxt_info, int curr_id, + MemoryContext context, List *path, + MemoryContextCounters stat, int num_contexts) +{ + const char *ident = context->ident; + const char *name = context->name; + + /* + * To be consistent with logging output, we label dynahash contexts with + * just the hash table name as with MemoryContextStatsPrint(). + */ + if (context->ident && strncmp(context->name, "dynahash", 8) == 0) + { + name = context->ident; + ident = NULL; + } + + if (name != NULL) + { + int namelen = strlen(name); + + if (strlen(name) >= MEMORY_CONTEXT_NAME_SHMEM_SIZE) + namelen = pg_mbcliplen(name, namelen, + MEMORY_CONTEXT_NAME_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].name, name, namelen + 1); + } + else + /* Clearing the array */ + memcxt_info[curr_id].name[0] = '\0'; + + /* Trim and copy the identifier if it is not set to NULL */ + if (ident != NULL) + { + int idlen = strlen(context->ident); + + /* + * Some identifiers such as SQL query string can be very long, + * truncate oversize identifiers. + */ + if (idlen >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + idlen = pg_mbcliplen(ident, idlen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].ident, ident, idlen + 1); + } + else + memcxt_info[curr_id].ident[0] = '\0'; + + /* Allocate DSA memory for storing path information */ + if (path == NIL) + memcxt_info[curr_id].path[0] = 0; + else + { + int levels = Min(list_length(path), MAX_PATH_DISPLAY_LENGTH); + + memcxt_info[curr_id].path_length = levels; + memcxt_info[curr_id].levels = list_length(path); + + foreach_int(i, path) + { + memcxt_info[curr_id].path[foreach_current_index(i)] = i; + if (--levels == 0) + break; + } + } + memcxt_info[curr_id].type = context->type; + memcxt_info[curr_id].totalspace = stat.totalspace; + memcxt_info[curr_id].nblocks = stat.nblocks; + memcxt_info[curr_id].freespace = stat.freespace; + memcxt_info[curr_id].freechunks = stat.freechunks; + memcxt_info[curr_id].num_agg_stats = num_contexts; +} + +void +AtProcExit_memstats_cleanup(int code, Datum arg) +{ + int idx = MyProcNumber; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + bool found; + + if (MemoryStatsDsHash != NULL) + { + snprintf(key, CLIENT_KEY_SIZE, "%d", idx); + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + if (found) + { + if (MemoryStatsDsaArea != NULL && + DsaPointerIsValid(entry->memstats_dsa_pointer)) + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + } + dshash_delete_entry(MemoryStatsDsHash, entry); + } + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + client_keys[idx] = -1; + LWLockRelease(client_keys_lock); +} + +/* Used for testing purposes */ +dsa_area * +pg_get_memstats_dsa_area(void) +{ + if (MemoryStatsDsaArea != NULL) + return MemoryStatsDsaArea; + else + return NULL; +} diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index d31cb45a058..92b0446b80c 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -39,6 +39,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t PublishMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index 98f9598cd78..202403ebc63 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -658,6 +658,13 @@ BaseInit(void) * drop ephemeral slots, which in turn triggers stats reporting. */ ReplicationSlotInitialize(); + + /* + * The before shmem exit callback frees the DSA memory occupied by the + * latest memory context statistics that could be published by this proc + * if requested. + */ + before_shmem_exit(AtProcExit_memstats_cleanup, 0); } diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 47fd774c7d2..56c2048c67a 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -1008,6 +1008,35 @@ MemoryContextStatsInternal(MemoryContext context, int level, } } + +/* + * MemoryContextStatsCounter + * + * Accumulate statistics counts into *totals. totals should not be NULL. + * This involves a non-recursive tree traversal. + */ +void +MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts) +{ + int ichild = 1; + + context->methods->stats(context, NULL, NULL, totals, false); + + for (MemoryContext curr = context->firstchild; + curr != NULL; + curr = MemoryContextTraverseNext(curr, context)) + { + curr->methods->stats(curr, NULL, NULL, totals, false); + ichild++; + } + + /* + * Add the count of children contexts which are traversed + */ + *num_contexts = *num_contexts + ichild; +} + /* * MemoryContextStatsPrint * Print callback used by MemoryContextStatsInternal diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 5cf9e12fcb9..bb72b85457d 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8617,6 +8617,16 @@ prorettype => 'bool', proargtypes => 'int4', prosrc => 'pg_log_backend_memory_contexts' }, +# publishing memory contexts of the specified postgres process +{ oid => '2173', descr => 'publish memory contexts of the specified backend', + proname => 'pg_get_process_memory_contexts', provolatile => 'v', + prorows => '100', proretset => 't', proparallel => 'r', + prorettype => 'record', proargtypes => 'int4 bool', + proallargtypes => '{int4,bool,text,text,text,int4,_int4,int8,int8,int8,int8,int8,int4}', + proargmodes => '{i,i,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{pid, summary, name, ident, type, level, path, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes, num_agg_contexts}', + prosrc => 'pg_get_process_memory_contexts' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index 9a7d733ddef..b76f24baed6 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -96,6 +96,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t PublishMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h index 5b0ce383408..613e769c84e 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -136,3 +136,4 @@ PG_LWLOCKTRANCHE(SUBTRANS_SLRU, SubtransSLRU) PG_LWLOCKTRANCHE(XACT_SLRU, XactSLRU) PG_LWLOCKTRANCHE(PARALLEL_VACUUM_DSA, ParallelVacuumDSA) PG_LWLOCKTRANCHE(AIO_URING_COMPLETION, AioUringCompletion) +PG_LWLOCKTRANCHE(MEMORY_CONTEXT_KEYS, MemoryContextReportingKeys) diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index afeeb1ca019..345d5a0ecb1 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -35,6 +35,7 @@ typedef enum PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to send the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ /* Recovery conflict reasons */ diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 7bbe5a36959..2d7220cde45 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -18,7 +18,10 @@ #define MEMUTILS_H #include "nodes/memnodes.h" - +#include "storage/condition_variable.h" +#include "storage/lmgr.h" +#include "utils/dsa.h" +#include "lib/dshash.h" /* * MaxAllocSize, MaxAllocHugeSize @@ -48,6 +51,26 @@ #define AllocHugeSizeIsValid(size) ((Size) (size) <= MaxAllocHugeSize) +/* + * Memory Context reporting size limits. + */ + +/* Max length of context name and ident, to keep it consistent + * with ProcessLogMemoryContext() + */ +#define MEMORY_CONTEXT_IDENT_SHMEM_SIZE 100 +#define MEMORY_CONTEXT_NAME_SHMEM_SIZE 100 + +/* Maximum size (in bytes) of DSA area per process */ +#define MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND ((size_t) (1 * 1024 * 1024)) + +/* + * Maximum size per context statistics. The identifier and name are statically + * allocated arrays of size 100 bytes. + * The path depth is limited to 100 like for memory context logging. + */ +#define MAX_MEMORY_CONTEXT_STATS_SIZE (sizeof(MemoryStatsEntry)) +#define MAX_MEMORY_CONTEXT_STATS_NUM MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND / MAX_MEMORY_CONTEXT_STATS_SIZE /* * Standard top-level memory contexts. @@ -149,6 +172,7 @@ extern MemoryContext BumpContextCreate(MemoryContext parent, Size minContextSize, Size initBlockSize, Size maxBlockSize); +extern dsa_area *pg_get_memstats_dsa_area(void); /* * Recommended default alloc parameters, suitable for "ordinary" contexts @@ -319,4 +343,59 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) return true; } +/* Dynamic shared memory state for statistics per context */ +typedef struct MemoryStatsEntry +{ + char name[MEMORY_CONTEXT_NAME_SHMEM_SIZE]; + char ident[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + int path[100]; + NodeTag type; + int path_length; + int levels; + int64 totalspace; + int64 nblocks; + int64 freespace; + int64 freechunks; + int num_agg_stats; +} MemoryStatsEntry; + +/* + * Per backend dynamic shared hash entry for memory context statistics + * reporting. + */ +typedef struct MemoryStatsDSHashEntry +{ + char key[64]; + ConditionVariable memcxt_cv; + bool stats_written; + int target_server_id; + int total_stats; + bool summary; + dsa_pointer memstats_dsa_pointer; +} MemoryStatsDSHashEntry; + +static const dshash_parameters memctx_dsh_params = { + offsetof(MemoryStatsDSHashEntry, memcxt_cv), + sizeof(MemoryStatsDSHashEntry), + dshash_strcmp, + dshash_strhash, + dshash_strcpy +}; + +/* + * Used for storage of transient identifiers for pg_get_backend_memory_contexts + */ +typedef struct MemoryStatsContextId +{ + MemoryContext context; + int context_id; +} MemoryStatsContextId; + +extern void ProcessGetMemoryContextInterrupt(void); +extern void HandleGetMemoryContextInterrupt(void); +extern void MemoryContextKeysShmemInit(void); +extern Size MemoryContextKeysShmemSize(void); +extern void MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts); +extern void AtProcExit_memstats_cleanup(int code, Datum arg); #endif /* MEMUTILS_H */ diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 3b37fafa65b..21c65ad2d10 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -233,3 +233,22 @@ select * from pg_timezone_abbrevs where abbrev = 'LMT'; LMT | @ 7 hours 52 mins 58 secs ago | f (1 row) +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; +NOTICE: (AllocSet,TopMemoryContext,) +NOTICE: (AllocSet,TopMemoryContext,) diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 66179f026b3..c9da4fc8c90 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -101,3 +101,21 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs; -- One specific case we can check without much fear of breakage -- is the historical local-mean-time value used for America/Los_Angeles. select * from pg_timezone_abbrevs where abbrev = 'LMT'; + +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 432509277c9..2990c807f45 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1684,6 +1684,9 @@ MemoryContextData MemoryContextId MemoryContextMethodID MemoryContextMethods +MemoryStatsContextId +MemoryStatsEntry +MemoryStatsDSHashEntry MemoryStatsPrintFunc MergeAction MergeActionState -- 2.34.1 [application/octet-stream] v2-0002-Test-module-to-test-memory-context-reporting-with-in.patch (9.5K, ../../CAH2L28v5qXPpfmVLFDcMFL4MMqsKP92FXhW2eosHA5LbatKnrw@mail.gmail.com/4-v2-0002-Test-module-to-test-memory-context-reporting-with-in.patch) download | inline diff: From e21832c610574f48496c961a7318070b720568de Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Thu, 23 Oct 2025 18:01:36 +0530 Subject: [PATCH 2/2] Test module to test memory context reporting with injection points --- src/test/modules/Makefile | 1 + .../test_memcontext_reporting/Makefile | 32 +++++ .../t/001_memcontext_inj.pl | 58 +++++++++ .../test_memcontext_reporting--1.0.sql | 11 ++ .../test_memcontext_reporting.c | 123 ++++++++++++++++++ .../test_memcontext_reporting.control | 4 + 6 files changed, 229 insertions(+) create mode 100644 src/test/modules/test_memcontext_reporting/Makefile create mode 100644 src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting--1.0.sql create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index 902a7954101..a31a2578c18 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -31,6 +31,7 @@ SUBDIRS = \ test_json_parser \ test_lfind \ test_lwlock_tranches \ + test_memcontext_reporting \ test_misc \ test_oat_hooks \ test_parser \ diff --git a/src/test/modules/test_memcontext_reporting/Makefile b/src/test/modules/test_memcontext_reporting/Makefile new file mode 100644 index 00000000000..01a7baa0263 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/Makefile @@ -0,0 +1,32 @@ +# src/test/modules/test_memcontext_reporting/Makefile + +EXTRA_INSTALL = src/test/modules/injection_points + +export enable_injection_points +MODULE_big = test_memcontext_reporting +OBJS = \ + $(WIN32RES) \ + test_memcontext_reporting.o +PGFILEDESC = "test_memcontext_reporting - test code for memory context reporting" + +EXTENSION = test_memcontext_reporting +DATA = test_memcontext_reporting--1.0.sql + +REGRESS = test_memcontext_reporting + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_memcontext_reporting +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif + +check: + $(prove_check) + +installcheck: + $(prove_installcheck) diff --git a/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl new file mode 100644 index 00000000000..69d8489eb37 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl @@ -0,0 +1,58 @@ +# Copyright (c) 2025, PostgreSQL Global Development Group + +# Test suite for testing memory context statistics reporting + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} +my $psql_err; +# Create and start a cluster with one node +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init(allows_streaming => 1); +# max_connections need to be bumped in order to accommodate for pgbench clients +# and log_statement is dialled down since it otherwise will generate enormous +# amounts of logging. Page verification failures are still logged. +$node->append_conf( + 'postgresql.conf', + qq[ +max_connections = 100 +log_statement = none +]); +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION test_memcontext_reporting;'); +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); +# Attaching to a client process injection point that throws an error +$node->safe_psql('postgres', "select injection_points_attach('memcontext-client-crash', 'error');"); + +my $pid = $node->safe_psql('postgres', "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); +print "PID"; +print $pid; + +#Client should have thrown error +$node->psql('postgres', qq(select pg_get_process_memory_contexts($pid, true);), stderr => \$psql_err); +like ( $psql_err, qr/error triggered for injection point memcontext-client-crash/); + +#Query the same process after detaching the injection point, using some other client and it should succeed. +$node->safe_psql('postgres', "select injection_points_detach('memcontext-client-crash');"); +my $topcontext_name = $node->safe_psql('postgres', "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';"); +ok($topcontext_name = 'TopMemoryContext'); + +# Attaching to a target process injection point that throws an error +$node->safe_psql('postgres', "select injection_points_attach('memcontext-server-crash', 'error');"); + +#Server should have thrown error +$node->psql('postgres', qq(select pg_get_process_memory_contexts($pid, true);), stderr => \$psql_err); + +#Query the same process after detaching the injection point, using some other client and it should succeed. +$node->safe_psql('postgres', "select injection_points_detach('memcontext-server-crash');"); +$topcontext_name = $node->safe_psql('postgres', "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';"); +ok($topcontext_name = 'TopMemoryContext'); +done_testing(); diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting--1.0.sql b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting--1.0.sql new file mode 100644 index 00000000000..181daf429d0 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting--1.0.sql @@ -0,0 +1,11 @@ +CREATE FUNCTION memcontext_crash_server() +RETURNS pg_catalog.void +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION memcontext_crash_client() +RETURNS pg_catalog.void +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION dsa_dump_sql() +RETURNS bigint +AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c new file mode 100644 index 00000000000..955155524c2 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c @@ -0,0 +1,123 @@ +/* + * ------------------------------------------------------------------------- + * + * Copyright (c) 2025, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c + * + * ------------------------------------------------------------------------- + */ + +#include "postgres.h" +#include "utils/injection_point.h" +#include "funcapi.h" +#include "utils/injection_point.h" +#include "storage/dsm_registry.h" + +PG_MODULE_MAGIC; + +extern PGDLLEXPORT void crash(const char *name, const void *private_data, void *arg); + +void +crash(const char *name, const void *private_data, void *arg) +{ + abort(); +} + +/* + * memcontext_crash_client + * + * Ensure that the client process aborts in between memory context + * reporting. + */ +PG_FUNCTION_INFO_V1(memcontext_crash_client); +Datum +memcontext_crash_client(PG_FUNCTION_ARGS) +{ +#ifdef USE_INJECTION_POINTS + InjectionPointAttach("memcontext-client-crash", + "test_memcontext_reporting", "crash", NULL, 0); + +#else + elog(ERROR, + "test is not working as intended when injection points are disabled"); +#endif + PG_RETURN_VOID(); +} + +PG_FUNCTION_INFO_V1(memcontext_detach_client); +Datum +memcontext_detach_client(PG_FUNCTION_ARGS) +{ +#ifdef USE_INJECTION_POINTS + InjectionPointDetach("memcontext-client-crash"); + +#else + elog(ERROR, + "test is not working as intended when injection points are disabled"); +#endif + PG_RETURN_VOID(); +} + +/* + * memcontext_crash_server + * + * Ensure that the server process crashes in between memory context + * reporting. + */ +PG_FUNCTION_INFO_V1(memcontext_crash_server); +Datum +memcontext_crash_server(PG_FUNCTION_ARGS) +{ +#ifdef USE_INJECTION_POINTS + InjectionPointAttach("memcontext-server-crash", + "test_memcontext_reporting", "crash", NULL, 0); + +#else + elog(ERROR, + "test is not working as intended when injection points are disabled"); +#endif + PG_RETURN_VOID(); +} + +/* + * memcontext_detach_server + * + * Detach the injection point which crashes the server + * reporting. + */ +PG_FUNCTION_INFO_V1(memcontext_detach_server); +Datum +memcontext_detach_server(PG_FUNCTION_ARGS) +{ +#ifdef USE_INJECTION_POINTS + InjectionPointDetach("memcontext-server-crash"); + +#else + elog(ERROR, + "test is not working as intended when injection points are disabled"); +#endif + PG_RETURN_VOID(); +} + +/* + * dsa_dump_sql + */ +PG_FUNCTION_INFO_V1(dsa_dump_sql); +Datum +dsa_dump_sql(PG_FUNCTION_ARGS) +{ + bool found; + size_t tot_size; + dsa_area *memstats_dsa_area; + + memstats_dsa_area = pg_get_memstats_dsa_area(); + + if (memstats_dsa_area == NULL) + memstats_dsa_area = GetNamedDSA("memory_context_statistics_dsa", &found); + + tot_size = dsa_get_total_size(memstats_dsa_area); + dsa_detach(memstats_dsa_area); + PG_RETURN_INT64(tot_size); +} diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control new file mode 100644 index 00000000000..48b501682d5 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control @@ -0,0 +1,4 @@ +comment = 'Test code for memcontext reporting' +default_version = '1.0' +module_pathname = '$libdir/test_memcontext_reporting' +relocatable = true -- 2.34.1 ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-11-20 09:56 Daniel Gustafsson <[email protected]> parent: Rahila Syed <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Daniel Gustafsson @ 2025-11-20 09:56 UTC (permalink / raw) To: Rahila Syed <[email protected]>; +Cc: pgsql-hackers; torikoshia <[email protected]> > On 7 Nov 2025, at 23:55, Rahila Syed <[email protected]> wrote: > I have attached a version 40 patch that has been rebased onto the > latest master branch, as CFbot indicated a rebase was needed. Thanks for the rebase, below are a few mostly superficial comments on the patch: +#include "access/twophase.h" +#include "catalog/pg_authid_d.h" ... +#include "utils/acl.h" Are these actually required to be included? - MemoryContextId *entry; + MemoryStatsContextId *entry; Why is this needed? MemoryStatsContextId is identical to MemoryContextId and is too only used in mcxtfuncs.c so there is no need to expose it in memutils.h. Can't you just use MemoryContextId everywhere or am I missing something? +#define CLIENT_KEY_SIZE 64 + +static LWLock *client_keys_lock = NULL; +static int *client_keys = NULL; +static dshash_table *MemoryStatsDsHash = NULL; +static dsa_area *MemoryStatsDsaArea = NULL; These new additions have in some cases too generic names (client_keys etc) and they all lack comments explaining why they're needed. Maybe a leading block comment explaining they are used for process memory context reporting, and then inline comments on each with their use? +#define CLIENT_KEY_SIZE 64 ... + char key[CLIENT_KEY_SIZE]; ... + snprintf(key, sizeof(key), "%d", MyProcNumber); Given that MyProcNumber is an index into the proc array, it seems excessive to use 64 bytes to store it, can't we get away with a small stack allocation? + * Retreive the client key for publishing statistics and reset it to -1, s/Retreive/Retrieve/ + ProcNumber procNumber = INVALID_PROC_NUMBER; This variable is never accessed before getting re-assigned, so this assignment in the variable definition can be removed per project style. + InitMaterializedSRF(fcinfo, 0); Can this initialization be postponed till when we know the ResultSetInfo is needed? While a micro optimization, it seems we can avoid that overhead in case the query errors out? + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", &memctx_dsh_params, &found); Nitpick, but there are a few oversize lines, like this one, which need to be wrapped to match project style. + /* + * XXX. If the process exits without cleaning up its slot, i.e in case of + * an abrupt crash the client_keys slot won't be reset thus resulting in + * false negative and WARNING would be thrown in case another process with + * same slot index is queried for statistics. + */ + if (client_keys[procNumber] == -1) + client_keys[procNumber] = MyProcNumber; + else + { + LWLockRelease(client_keys_lock); + ereport(WARNING, + errmsg("server process %d is processing previous request", pid)); + PG_RETURN_NULL(); + } AFAICT this mean that a failure to clean up (through a crash for example) can block a future backend from reporting which isn't entirely ideal. Is there anything we can do to mitigate this? + bool summary = false; In ProcessGetMemoryContextInterrupt(), can't we just read entry->summary rather than define a local variable and assign it? We already read lots of other fields from entry directly so it seems more readable to be consistent. + /* + * Add the count of children contexts which are traversed + */ + *num_contexts = *num_contexts + ichild; Isn't this really the number of children + the parent context? ichild starts at one to (AIUI) include the parent context. Also, MemoryContextStatsCounter should also make sure to set num_contexts to zero before adding to it. +#define MAX_MEMORY_CONTEXT_STATS_SIZE (sizeof(MemoryStatsEntry)) +#define MAX_MEMORY_CONTEXT_STATS_NUM MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND / MAX_MEMORY_CONTEXT_STATS_SIZE I don't think MAX_MEMORY_CONTEXT_STATS_SIZE adds any value as it's only used once, on the line directly after its definition. We can just use the expansion of ((sizeof(MemoryStatsEntry)) when defining MAX_MEMORY_CONTEXT_STATS_NUM. > The test module patch is unchanged. Please include all commits in the series even if they aren't updated since the CFBot cannot pick them up otherwise. -- Daniel Gustafsson ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-11-25 07:20 Rahila Syed <[email protected]> parent: Daniel Gustafsson <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Rahila Syed @ 2025-11-25 07:20 UTC (permalink / raw) To: Daniel Gustafsson <[email protected]>; +Cc: pgsql-hackers; torikoshia <[email protected]> Hi Daniel, Thank you for your comments. Please find attached v41 with all the comments addressed. > > +#include "access/twophase.h" > +#include "catalog/pg_authid_d.h" > ... > +#include "utils/acl.h" > Are these actually required to be included? > > Removed these. > - MemoryContextId *entry; > + MemoryStatsContextId *entry; > Why is this needed? MemoryStatsContextId is identical to MemoryContextId > and > is too only used in mcxtfuncs.c so there is no need to expose it in > memutils.h. > Can't you just use MemoryContextId everywhere or am I missing something? > > MemoryContextId has been renamed to MemoryStatsContextId for better code readability. I removed the leftover MemoryContextId definition. Also, I moved it out of memutils.h. Did the same with some other structures and definitions which were only used in mcxtfuncs.c > +#define CLIENT_KEY_SIZE 64 > + > +static LWLock *client_keys_lock = NULL; > +static int *client_keys = NULL; > +static dshash_table *MemoryStatsDsHash = NULL; > +static dsa_area *MemoryStatsDsaArea = NULL; > These new additions have in some cases too generic names (client_keys etc) > and > they all lack comments explaining why they're needed. Maybe a leading > block > comment explaining they are used for process memory context reporting, and > then > inline comments on each with their use? > > Added comments. > +#define CLIENT_KEY_SIZE 64 > ... > + char key[CLIENT_KEY_SIZE]; > ... > + snprintf(key, sizeof(key), "%d", MyProcNumber); > Given that MyProcNumber is an index into the proc array, it seems > excessive to > use 64 bytes to store it, can't we get away with a small stack allocation? > I agree. Defined it as 32 bytes as MyProcNumber is of size uint32. Kindly let me know if you think it can be reduced further. + * Retreive the client key for publishing statistics and reset it to -1, > s/Retreive/Retrieve/ > Fixed. > + ProcNumber procNumber = INVALID_PROC_NUMBER; > This variable is never accessed before getting re-assigned, so this > assignment > in the variable definition can be removed per project style. > > > Fixed too. > + InitMaterializedSRF(fcinfo, 0); > Can this initialization be postponed till when we know the ResultSetInfo is > needed? While a micro optimization, it seems we can avoid that overhead in > case the query errors out? > > Good point. Added this just before the result set is getting populated. > + if (MemoryStatsDsHash == NULL) > + MemoryStatsDsHash = > GetNamedDSHash("memory_context_statistics_dshash", &memctx_dsh_params, > &found); > Nitpick, but there are a few oversize lines, like this one, which need to > be > wrapped to match project style. > > I have edited this accordingly. > + /* > + * XXX. If the process exits without cleaning up its slot, i.e in case > of > + * an abrupt crash the client_keys slot won't be reset thus resulting > in > + * false negative and WARNING would be thrown in case another process > with > + * same slot index is queried for statistics. > + */ > + if (client_keys[procNumber] == -1) > + client_keys[procNumber] = MyProcNumber; > + else > + { > + LWLockRelease(client_keys_lock); > + ereport(WARNING, > + errmsg("server process %d is processing previous request", > pid)); > + PG_RETURN_NULL(); > + } > AFAICT this mean that a failure to clean up (through a crash for example) > can > block a future backend from reporting which isn't entirely ideal. Is there > anything we can do to mitigate this? > > Yes, we can reset it when the client times out, as long as we verify that the value corresponds to our ProcNumber and not another client's request. Fixed accordingly. > > + bool summary = false; > In ProcessGetMemoryContextInterrupt(), can't we just read entry->summary > rather > than define a local variable and assign it? We already read lots of other > fields from entry directly so it seems more readable to be consistent. > > Fixed. > > + /* > + * Add the count of children contexts which are traversed > + */ > + *num_contexts = *num_contexts + ichild; > Isn't this really the number of children + the parent context? ichild > starts > at one to (AIUI) include the parent context. Also, > MemoryContextStatsCounter > should also make sure to set num_contexts to zero before adding to it. > > Yes. Adjusted the comment to match this and set num_contexts to zero. > > +#define MAX_MEMORY_CONTEXT_STATS_SIZE (sizeof(MemoryStatsEntry)) > +#define MAX_MEMORY_CONTEXT_STATS_NUM > MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND / MAX_MEMORY_CONTEXT_STATS_SIZE > I don't think MAX_MEMORY_CONTEXT_STATS_SIZE adds any value as it's only > used > once, on the line directly after its definition. We can just use the > expansion > of ((sizeof(MemoryStatsEntry)) when defining MAX_MEMORY_CONTEXT_STATS_NUM. > > Fixed. I've attached the test patch as is, I will clean it up and do further improvements to it. Thank you, Rahila Syed Attachments: [application/octet-stream] v41-0001-Add-function-to-report-memory-context-statistics.patch (58.6K, ../../CAH2L28ucG7zLjLRU4m_hCaGfo_UwUyw89OZR9Q8t8YMriyijgw@mail.gmail.com/3-v41-0001-Add-function-to-report-memory-context-statistics.patch) download | inline diff: From f37252238a63e4023a06beac1b6e3dd0887fffcf Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Mon, 24 Nov 2025 17:05:47 +0530 Subject: [PATCH] Add function to report memory context statistics This adds a function for retrieving memory context statistics and information from backends as well as auxiliary processes. The intended usecase is cluster debugging when under memory pressure or unanticipated memory usage characteristics. When calling the function it sends a signal to the specified process to submit statistics regarding its memory contexts into dynamic shared memory. Each memory context is returned in detail, followed by a cumulative total in case the number of contexts exceed the max allocated amount of shared memory. Each process is limited to use at most 1Mb memory for this. A summary can also be explicitly requested by the user, this will return the TopMemoryContext and a cumulative total of all lower contexts. In order to not block on busy processes, we have hardcoded the number of seconds during which to retry before timing out. In the case where no statistics are published within the set timeout, NULL is returned --- doc/src/sgml/func/func-admin.sgml | 157 +++ src/backend/catalog/system_views.sql | 5 + src/backend/postmaster/autovacuum.c | 4 + src/backend/postmaster/checkpointer.c | 4 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 4 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 4 + src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/storage/lmgr/proc.c | 1 + src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 2 + src/backend/utils/adt/mcxtfuncs.c | 1013 ++++++++++++++++- src/backend/utils/init/globals.c | 1 + src/backend/utils/init/postinit.c | 7 + src/backend/utils/mmgr/mcxt.c | 31 + src/include/catalog/pg_proc.dat | 10 + src/include/miscadmin.h | 1 + src/include/storage/lwlocklist.h | 1 + src/include/storage/procsignal.h | 1 + src/include/utils/memutils.h | 11 +- src/test/regress/expected/sysviews.out | 19 + src/test/regress/sql/sysviews.sql | 18 + src/tools/pgindent/typedefs.list | 4 +- 25 files changed, 1285 insertions(+), 30 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 1b465bc8ba7..a5c66837241 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -251,6 +251,130 @@ <literal>false</literal> is returned. </para></entry> </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_get_process_memory_contexts</primary> + </indexterm> + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type>, <parameter>summary</parameter> <type>boolean</type> ) + <returnvalue>setof record</returnvalue> + ( <parameter>name</parameter> <type>text</type>, + <parameter>ident</parameter> <type>text</type>, + <parameter>type</parameter> <type>text</type>, + <parameter>level</parameter> <type>integer</type>, + <parameter>path</parameter> <type>integer[]</type>, + <parameter>total_bytes</parameter> <type>bigint</type>, + <parameter>total_nblocks</parameter> <type>bigint</type>, + <parameter>free_bytes</parameter> <type>bigint</type>, + <parameter>free_chunks</parameter> <type>bigint</type>, + <parameter>used_bytes</parameter> <type>bigint</type>, + <parameter>num_agg_contexts</parameter> <type>integer</type> ) + </para> + <para> + This function handles requests to display the memory contexts of a + <productname>PostgreSQL</productname> process with the specified + process ID. The function can be used to send requests to backends as + well as <glossterm linkend="glossary-auxiliary-proc">auxiliary processes</glossterm>. + </para> + <para> + The returned record contains extended statistics per each memory + context: + <itemizedlist spacing="compact"> + <listitem> + <para> + <parameter>name</parameter> - The name of the memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>ident</parameter> - Memory context ID (if any). + </para> + </listitem> + <listitem> + <para> + <parameter>type</parameter> - The type of memory context, possible + values are: AllocSet, Generation, Slab and Bump. + </para> + </listitem> + <listitem> + <para> + <parameter>level</parameter> - The level in the tree of the current + memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>path</parameter> - Memory contexts are organized in a + tree model with TopMemoryContext as the root, and all other memory + contexts as nodes in the tree. The <parameter>path</parameter> + displays the path from the root to the current memory context. The + path is limited to 100 children per node, which each node limited + to a max depth of 100, to preserve memory during reporting. The + printed path will also be limited to 100 nodes counting from the + TopMemoryContext. + </para> + </listitem> + <listitem> + <para> + <parameter>total_bytes</parameter> - The total number of bytes + allocated to this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_nblocks</parameter> - The total number of blocks + used for the allocated memory. + </para> + </listitem> + <listitem> + <para> + <parameter>free_bytes</parameter> - The amount of free memory in + this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>free_chunks</parameter> - The number of chunks that + <parameter>free_bytes</parameter> corresponds to. + </para> + </listitem> + <listitem> + <para> + <parameter>used_bytes</parameter> - The total number of bytes + currently occupied. + </para> + </listitem> + <listitem> + <para> + <parameter>num_agg_contexts</parameter> - The number of memory + contexts aggregated in the displayed statistics. + </para> + </listitem> + </itemizedlist> + </para> + <para> + When <parameter>summary</parameter> is <literal>true</literal>, statistics + for memory contexts at levels 1 and 2 are displayed, with level 1 + representing the root node (i.e., <literal>TopMemoryContext</literal>). + Statistics for contexts on level 2 and below are aggregates of all + child contexts' statistics, where <literal>num_agg_contexts</literal> + indicate the number aggregated child contexts. When + <parameter>summary</parameter> is <literal>false</literal>, + <literal>the num_agg_contexts</literal> value is <literal>1</literal>, + indicating that individual statistics are being displayed. + </para> + <para> + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context + statistics are aggregated and a cumulative total is displayed. The + <literal>num_agg_contexts</literal> column indicates the number of + contexts aggregated in the displayed statistics. When + <literal>num_agg_contexts</literal> is <literal>1</literal> it means + that the context statistics are displayed separately. + </para></entry> + </row> </tbody> </tgroup> </table> @@ -302,6 +426,39 @@ LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 because it may generate a large number of log messages. </para> + <para> + <function>pg_get_process_memory_contexts</function> can be used to request + memory contexts statistics of any <productname>PostgreSQL</productname> + process. For example: +<programlisting> +postgres=# SELECT * FROM pg_get_process_memory_contexts( + (SELECT pid FROM pg_stat_activity + WHERE backend_type = 'checkpointer'), + false) LIMIT 1; +-[ RECORD 1 ]----+------------------------------ +name | TopMemoryContext +ident | +type | AllocSet +level | 1 +path | {1} +total_bytes | 90304 +total_nblocks | 3 +free_bytes | 2880 +free_chunks | 1 +used_bytes | 87424 +num_agg_contexts | 1 +</programlisting> + <note> + <para> + While <function>pg_get_process_memory_contexts</function> can be used to + query memory contexts of the local backend, + <structname>pg_backend_memory_contexts</structname> + (see <xref linkend="view-pg-backend-memory-contexts"/> for more details) + will be less resource intensive when only the local backend is of interest. + </para> + </note> + </para> + </sect2> <sect2 id="functions-admin-backup"> diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index 95ad29a64b9..6fe67e950bf 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -692,6 +692,11 @@ GRANT SELECT ON pg_backend_memory_contexts TO pg_read_all_stats; REVOKE EXECUTE ON FUNCTION pg_get_backend_memory_contexts() FROM PUBLIC; GRANT EXECUTE ON FUNCTION pg_get_backend_memory_contexts() TO pg_read_all_stats; +REVOKE EXECUTE ON FUNCTION + pg_get_process_memory_contexts(integer, boolean) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION + pg_get_process_memory_contexts(integer, boolean) TO pg_read_all_stats; + -- Statistics views CREATE VIEW pg_stat_all_tables AS diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 1c38488f2cb..561d88ebb4d 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -791,6 +791,10 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index e84e8663e96..5b3e08805bf 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -679,6 +679,10 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index ba63b84dfc5..29454b8bf8a 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -48,6 +48,10 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index ce6b5299324..fdd385e492d 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -871,6 +871,10 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index 27e86cf393f..7149a67fcbc 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -192,6 +192,10 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index c4a888a081c..00f03b36ed8 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -879,6 +879,10 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index b23d0c19360..a5ed58a18c5 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -52,6 +52,7 @@ #include "storage/sinvaladt.h" #include "utils/guc.h" #include "utils/injection_point.h" +#include "utils/memutils.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -140,6 +141,7 @@ CalculateShmemSize(void) size = add_size(size, SlotSyncShmemSize()); size = add_size(size, AioShmemSize()); size = add_size(size, WaitLSNShmemSize()); + size = add_size(size, MemoryContextKeysShmemSize() + sizeof(LWLockPadded)); /* include additional requested shmem from preload libraries */ size = add_size(size, total_addin_request); @@ -328,6 +330,7 @@ CreateOrAttachShmemStructs(void) InjectionPointShmemInit(); AioShmemInit(); WaitLSNShmemInit(); + MemoryContextKeysShmemInit(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index 087821311cc..8963285cc12 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -691,6 +691,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_GET_MEMORY_CONTEXT)) + HandleGetMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1504fafe6d8..c5e69151756 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -51,6 +51,7 @@ #include "storage/procsignal.h" #include "storage/spin.h" #include "storage/standby.h" +#include "utils/memutils.h" #include "utils/timeout.h" #include "utils/timestamp.h" diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 7dd75a490aa..e726f40dfbb 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3539,6 +3539,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index c1ac71ff7f2..644d8d988e1 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -162,6 +162,7 @@ WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." +MEM_CXT_PUBLISH "Waiting for a process to publish memory information." ABI_compatibility: @@ -404,6 +405,7 @@ SubtransSLRU "Waiting to access the sub-transaction SLRU cache." XactSLRU "Waiting to access the transaction status SLRU cache." ParallelVacuumDSA "Waiting for parallel vacuum dynamic shared memory allocation." AioUringCompletion "Waiting for another process to complete IO via io_uring." +MemoryContextReportingKeys "Waiting for another process to complete reading or writing the memory reporting keys." # No "ABI_compatibility" region here as WaitEventLWLock has its own C code. diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index fe6dce9cba3..8e0e3116f1f 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -17,28 +17,138 @@ #include "funcapi.h" #include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "storage/dsm_registry.h" #include "storage/proc.h" #include "storage/procarray.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/injection_point.h" +#include "utils/memutils.h" +#include "utils/wait_event_types.h" + +/* + * Memory Context reporting size limits. + */ + +/* Max length of context name and ident, to keep it consistent + * with ProcessLogMemoryContext() + */ +#define MEMORY_CONTEXT_IDENT_SHMEM_SIZE 100 +#define MEMORY_CONTEXT_NAME_SHMEM_SIZE 100 + +/* Maximum size (in bytes) of DSA area per process */ +#define MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND ((size_t) (1 * 1024 * 1024)) + +/* + * Maximum number of memory context statistics is calculated by dividing + * max memory allocated per backend with maximum size per context statistics. + * The identifier and name are statically allocated arrays of size 100 bytes. + * The path depth is limited to 100 like for memory context logging. + */ +#define MAX_MEMORY_CONTEXT_STATS_NUM MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND / (sizeof(MemoryStatsEntry)) + +/* Size of dshash key */ +#define CLIENT_KEY_SIZE 32 + +/* Dynamic shared memory state for reporting statistics per context */ +typedef struct MemoryStatsEntry +{ + char name[MEMORY_CONTEXT_NAME_SHMEM_SIZE]; + char ident[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + int path[100]; + NodeTag type; + int path_length; + int levels; + int64 totalspace; + int64 nblocks; + int64 freespace; + int64 freechunks; + int num_agg_stats; +} MemoryStatsEntry; + +/* + * Per backend dynamic shared hash entry for memory context statistics + * reporting. + */ +typedef struct MemoryStatsDSHashEntry +{ + char key[64]; + ConditionVariable memcxt_cv; + bool stats_written; + int target_server_id; + int total_stats; + bool summary; + dsa_pointer memstats_dsa_pointer; +} MemoryStatsDSHashEntry; + +static const dshash_parameters memctx_dsh_params = { + offsetof(MemoryStatsDSHashEntry, memcxt_cv), + sizeof(MemoryStatsDSHashEntry), + dshash_strcmp, + dshash_strhash, + dshash_strcpy +}; + +/* + * These are used for reporting memory context + * statistics of a process. + */ + +/* Lock to control access to client_keys array */ +static LWLock *client_keys_lock = NULL; + +/* Array to store the keys of MemoryStatsDsHash */ +static int *client_keys = NULL; + +/* + * Table to store pointers to dsa memory containing + * memory statistics and other meta data. There is one + * entry per client backend request, keyed by ProcNumber of + * the client obtained from client_keys array above. + */ +static dshash_table *MemoryStatsDsHash = NULL; + +/* + * Dsa area which stores the actual memory context + * statistics. + */ +static dsa_area *MemoryStatsDsaArea = NULL; + +static void memstats_dsa_cleanup(char *key); +static void memstats_client_key_reset(int ProcNumber); +static const char *ContextTypeToString(NodeTag type); +static void PublishMemoryContext(MemoryStatsEntry *memcxt_info, + int curr_id, MemoryContext context, + List *path, + MemoryContextCounters stat, + int num_contexts); +static List *compute_context_path(MemoryContext c, HTAB *context_id_lookup); +static void end_memorycontext_reporting(MemoryStatsDSHashEntry *entry, MemoryContext oldcontext, + HTAB *context_id_lookup); /* ---------- * The max bytes for showing identifiers of MemoryContext. + * This is used by pg_get_backend_memory_context - view used for local backend. * ---------- */ #define MEMORY_CONTEXT_IDENT_DISPLAY_SIZE 1024 +#define MAX_PATH_DISPLAY_LENGTH 100 +/* Timeout in seconds */ +#define MEMORY_STATS_MAX_TIMEOUT 5 + /* - * MemoryContextId + * MemoryStatsContextId * Used for storage of transient identifiers for - * pg_get_backend_memory_contexts. + * pg_get_backend_memory_contexts and the likes. */ -typedef struct MemoryContextId +typedef struct MemoryStatsContextId { MemoryContext context; int context_id; -} MemoryContextId; +} MemoryStatsContextId; /* * int_list_to_array @@ -89,7 +199,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, */ for (MemoryContext cur = context; cur != NULL; cur = cur->parent) { - MemoryContextId *entry; + MemoryStatsContextId *entry; bool found; entry = hash_search(context_id_lookup, &cur, HASH_FIND, &found); @@ -143,24 +253,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, else nulls[1] = true; - switch (context->type) - { - case T_AllocSetContext: - type = "AllocSet"; - break; - case T_GenerationContext: - type = "Generation"; - break; - case T_SlabContext: - type = "Slab"; - break; - case T_BumpContext: - type = "Bump"; - break; - default: - type = "???"; - break; - } + type = ContextTypeToString(context->type); values[2] = CStringGetTextDatum(type); values[3] = Int32GetDatum(list_length(path)); /* level */ @@ -175,6 +268,38 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, list_free(path); } +/* + * ContextTypeToString + * Returns a textual representation of a context type + * + * This should cover the same types as MemoryContextIsValid. + */ +const char * +ContextTypeToString(NodeTag type) +{ + const char *context_type; + + switch (type) + { + case T_AllocSetContext: + context_type = "AllocSet"; + break; + case T_GenerationContext: + context_type = "Generation"; + break; + case T_SlabContext: + context_type = "Slab"; + break; + case T_BumpContext: + context_type = "Bump"; + break; + default: + context_type = "???"; + break; + } + return context_type; +} + /* * pg_get_backend_memory_contexts * SQL SRF showing backend memory context. @@ -189,7 +314,7 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) HTAB *context_id_lookup; ctl.keysize = sizeof(MemoryContext); - ctl.entrysize = sizeof(MemoryContextId); + ctl.entrysize = sizeof(MemoryStatsContextId); ctl.hcxt = CurrentMemoryContext; context_id_lookup = hash_create("pg_get_backend_memory_contexts", @@ -216,7 +341,7 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) foreach_ptr(MemoryContextData, cur, contexts) { - MemoryContextId *entry; + MemoryStatsContextId *entry; bool found; /* @@ -224,8 +349,8 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) * PutMemoryContextsStatsTupleStore needs this to populate the "path" * column with the parent context_ids. */ - entry = (MemoryContextId *) hash_search(context_id_lookup, &cur, - HASH_ENTER, &found); + entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &cur, + HASH_ENTER, &found); entry->context_id = context_id++; Assert(!found); @@ -305,3 +430,837 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } + +/* + * pg_get_process_memory_contexts + * Signal a backend or an auxiliary process to send its memory contexts, + * wait for the results and display them. + * + * By default, only superusers or users with ROLE_PG_READ_ALL_STATS are allowed + * to signal a process to return the memory contexts. This is because allowing + * any users to issue this request at an unbounded rate would cause lots of + * requests to be sent, which can lead to denial of service. Additional roles + * can be permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to copy the memory context details + * to a dynamic shared memory space. + * + * We have defined a limit on DSA memory that could be allocated per process - + * if the process has more memory contexts than what can fit in the allocated + * size, the excess contexts are summarized and represented as cumulative total + * at the end of the buffer. + * + * After sending the signal, wait on a condition variable. The publishing + * backend, after copying the data to shared memory, sends signal on that + * condition variable. There is one condition variable per client process. + * Once the condition variable is signalled, check if the latest memory context + * information is available and display. + * + * If the publishing backend does not respond before the condition variable + * times out, which is set to a predefined value MEMORY_STATS_MAX_TIMEOUT, give up + * and return NULL. + */ +Datum +pg_get_process_memory_contexts(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + bool summary = PG_GETARG_BOOL(1); + PGPROC *proc; + ProcNumber procNumber; + bool proc_is_aux = false; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + MemoryStatsEntry *memcxt_info; + MemoryStatsDSHashEntry *entry; + bool found; + char key[CLIENT_KEY_SIZE]; + TimestampTz start_timestamp; + + /* + * See if the process with given pid is a backend or an auxiliary process + * and remember the type for when we requery the process later. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + { + proc = AuxiliaryPidGetProc(pid); + proc_is_aux = true; + } + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; this is however not a problem and leave with a WARNING. + * See comment in pg_log_backend_memory_contexts for a discussion on this. + */ + if (proc == NULL) + { + /* + * This is a warning because we don't want to break loops. + */ + ereport(WARNING, + errmsg("PID %d is not a PostgreSQL server process", pid)); + PG_RETURN_NULL(); + } + + procNumber = GetNumberFromPGProc(proc); + + /* + * Create a DSA to allocate memory for copying memory contexts statistics. + * Allocate the memory in the DSA and send dsa pointer to the server + * process for storing the context statistics. If number of contexts + * exceed a predefined limit (1MB), a cumulative total is stored for such + * contexts. + * + * The DSA is created once for the lifetime of the server, and only + * attached in subsequent calls. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", &found); + + /* + * The dsa pointers containing statistics for each client are stored in a + * dshash table. In addition to dsa pointer, each entry in this table also + * contains information about the statistics, condition variable for + * signalling between client and the server and miscellaneous data + * specific to a request. There is one entry per client request in the + * hash table. + */ + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); + + snprintf(key, sizeof(key), "%d", MyProcNumber); + + /* + * Check if the publishing process slot is empty and store this clients + * key i.e its procNumber. This informs the publishing process that it is + * supposed to write statistics in the hash entry corresponding to this + * client. + */ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + + if (client_keys[procNumber] == -1) + client_keys[procNumber] = MyProcNumber; + else + { + LWLockRelease(client_keys_lock); + ereport(WARNING, + errmsg("server process %d is processing previous request", pid)); + PG_RETURN_NULL(); + } + LWLockRelease(client_keys_lock); + + /* + * Insert an entry for this client in DSHASH table the first time this + * function is called. This entry is deleted when the process exits in + * before_shmem_exit call. + * + * dshash_find_or_insert locks the entry to prevent the publisher from + * reading before client has updated the entry. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + if (!found) + { + entry->stats_written = false; + ConditionVariableInit(&entry->memcxt_cv); + } + + /* + * Allocate 1MB of memory for the backend to publish its statistics on + * every call to this function. The memory is freed at the end of the + * function. + */ + entry->memstats_dsa_pointer = + dsa_allocate0(MemoryStatsDsaArea, MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND); + + /* + * Specify whether a summary of statistics is requested, before signalling + * the server. + */ + entry->summary = summary; + + /* + * Indicate which server process statistics are being requested from. If + * this client times out before the last requested process can publish its + * statistics, it may send a new request to another server process. Since + * the previous server was notified, it might attempt to read the same + * client entry and respond incorrectly with its statistics. By storing + * the server ID in the client entry, we prevent any previously signalled + * server process from writing its statistics in the space meant for the + * newly requested process. + */ + entry->target_server_id = pid; + dshash_release_lock(MemoryStatsDsHash, entry); + + /* + * Send a signal to a PostgreSQL process, informing it we want it to + * produce information about its memory contexts. + */ + if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, procNumber) < 0) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ereport(WARNING, + errmsg("could not send signal to process %d: %m", pid)); + PG_RETURN_NULL(); + } + start_timestamp = GetCurrentTimestamp(); + + while (1) + { + long elapsed_time; + + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + Assert(found); + + INJECTION_POINT("memcontext-client-crash", NULL); + + memcxt_info = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + /* + * We expect to come out of sleep when the requested process has + * finished publishing the statistics, verified using a boolean + * stats_written. + * + * Make sure that the statistics are actually written by checking that + * the name of the context is not NULL. This is done to ensure that + * the subsequent waits for statistics do not return spuriously if the + * previous call to the function ended in error and thus could not + * clear the stats_written flag. + */ + if (entry->stats_written && memcxt_info[0].name[0] != '\0') + break; + + dshash_release_lock(MemoryStatsDsHash, entry); + + elapsed_time = TimestampDifferenceMilliseconds(start_timestamp, + GetCurrentTimestamp()); + /* Return if we have already exceeded the timeout */ + if (elapsed_time >= MEMORY_STATS_MAX_TIMEOUT * 1000) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + PG_RETURN_NULL(); + } + + /* + * Recheck the state of the backend before sleeping on the condition + * variable to ensure the process is still alive. Only check the + * relevant process type based on the earlier PID check. + */ + if (proc_is_aux) + proc = AuxiliaryPidGetProc(pid); + else + proc = BackendPidGetProc(pid); + + /* + * The target server process ending during memory context processing + * is not an error. + */ + if (proc == NULL) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + ereport(WARNING, + errmsg("PID %d is no longer a PostgreSQL server process", + pid)); + PG_RETURN_NULL(); + } + + INJECTION_POINT("memcontext-client-crash", NULL); + + /* + * Wait for MEMORY_STATS_MAX_TIMEOUT. If no statistics are available + * within the allowed time then return NULL. The timer is defined in + * milliseconds since that's what the condition variable sleep uses. + */ + if (ConditionVariableTimedSleep(&entry->memcxt_cv, + (MEMORY_STATS_MAX_TIMEOUT * 1000), WAIT_EVENT_MEM_CXT_PUBLISH)) + { + /* Timeout has expired, return NULL */ + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + PG_RETURN_NULL(); + } + } + + InitMaterializedSRF(fcinfo, 0); + + /* + * Backend has finished publishing the stats, project them. + */ +#define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 11 + for (int i = 0; i < entry->total_stats; i++) + { + ArrayType *path_array; + int path_length; + Datum values[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + bool nulls[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + Datum *path_datum = NULL; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + Assert(memcxt_info[i].name[0] != '\0'); + values[0] = CStringGetTextDatum(memcxt_info[i].name); + + if (memcxt_info[i].ident[0] != '\0') + values[1] = CStringGetTextDatum(memcxt_info[i].ident); + else + nulls[1] = true; + + values[2] = CStringGetTextDatum(ContextTypeToString(memcxt_info[i].type)); + values[3] = Int32GetDatum(memcxt_info[i].levels); + + path_length = memcxt_info[i].path_length; + path_datum = (Datum *) palloc(path_length * sizeof(Datum)); + if (memcxt_info[i].path[0] != 0) + { + for (int j = 0; j < path_length; j++) + path_datum[j] = Int32GetDatum(memcxt_info[i].path[j]); + path_array = construct_array_builtin(path_datum, path_length, INT4OID); + values[4] = PointerGetDatum(path_array); + } + else + nulls[4] = true; + + values[5] = Int64GetDatum(memcxt_info[i].totalspace); + values[6] = Int64GetDatum(memcxt_info[i].nblocks); + values[7] = Int64GetDatum(memcxt_info[i].freespace); + values[8] = Int64GetDatum(memcxt_info[i].freechunks); + values[9] = Int64GetDatum(memcxt_info[i].totalspace - + memcxt_info[i].freespace); + values[10] = Int32GetDatum(memcxt_info[i].num_agg_stats); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + dshash_release_lock(MemoryStatsDsHash, entry); + memstats_dsa_cleanup(key); + + ConditionVariableCancelSleep(); + + PG_RETURN_NULL(); +} + +static void +memstats_dsa_cleanup(char *key) +{ + MemoryStatsDSHashEntry *entry; + + entry = dshash_find(MemoryStatsDsHash, key, true); + + Assert(MemoryStatsDsaArea != NULL); + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + entry->memstats_dsa_pointer = InvalidDsaPointer; + entry->stats_written = false; + entry->target_server_id = 0; + + dshash_release_lock(MemoryStatsDsHash, entry); +} + +/* + * Remove this process from the publishing process' + * client key slot, if the stats publishing process has failed to do so. + */ +static void +memstats_client_key_reset(int procNumber) +{ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + + if (client_keys[procNumber] == MyProcNumber) + client_keys[procNumber] = -1; + LWLockRelease(client_keys_lock); +} + +void +MemoryContextKeysShmemInit(void) +{ + bool found; + + client_keys = (int *) + ShmemInitStruct("MemoryContextKeys", + MemoryContextKeysShmemSize() + sizeof(LWLockPadded), &found); + client_keys_lock = (LWLock *) ((char *) client_keys + MemoryContextKeysShmemSize()); + + if (!found) + { + MemSet(client_keys, -1, MemoryContextKeysShmemSize()); + LWLockInitialize(client_keys_lock, LWTRANCHE_MEMORY_CONTEXT_KEYS); + } +} + +Size +MemoryContextKeysShmemSize(void) +{ + Size sz = 0; + Size TotalProcs = 0; + + TotalProcs = add_size(TotalProcs, NUM_AUXILIARY_PROCS); + TotalProcs = add_size(TotalProcs, MaxBackends); + sz = add_size(sz, mul_size(TotalProcs, sizeof(int))); + + return sz; +} + +/* + * HandleGetMemoryContextInterrupt + * Handle receipt of an interrupt indicating a request to publish memory + * contexts statistics. + * + * All the actual work is deferred to ProcessGetMemoryContextInterrupt() as + * this cannot be performed in a signal handler. + */ +void +HandleGetMemoryContextInterrupt(void) +{ + InterruptPending = true; + PublishMemoryContextPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + +/* + * ProcessGetMemoryContextInterrupt + * Generate information about memory contexts used by the process. + * + * Performs a breadth first search on the memory context tree, thus parents + * statistics are reported before their children in the monitoring function + * output. + * + * Statistics for all the processes are shared via the same dynamic shared + * area. Individual statistics are tracked independently in + * per-process DSA pointers. These pointers are stored in a dshash table with + * key as requesting clients ProcNumber. + * + * We calculate maximum number of context's statistics that can be displayed + * using a pre-determined limit for memory available per process for this + * utility and maximum size of statistics for each context. The remaining + * context statistics if any are captured as a cumulative total at the end of + * individual context's statistics. + * + * If summary is true, we capture the level 1 and level 2 contexts + * statistics. For that we traverse the memory context tree recursively in + * depth first search manner to cover all the children of a parent context, to + * be able to display a cumulative total of memory consumption by a parent at + * level 2 and all its children. + */ +void +ProcessGetMemoryContextInterrupt(void) +{ + List *contexts; + HASHCTL ctl; + HTAB *context_id_lookup; + int context_id = 0; + MemoryStatsEntry *meminfo; + MemoryContextCounters stat; + int num_individual_stats = 0; + bool found; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + int clientProcNumber; + MemoryContext memstats_ctx = NULL; + MemoryContext oldcontext = NULL; + + PublishMemoryContextPending = false; + + /* + * Retrieve the client key for publishing statistics and reset it to -1, + * so other clients can request memory statistics from this process + */ + LWLockAcquire(client_keys_lock, LW_SHARED); + Assert(client_keys[MyProcNumber] != -1); + clientProcNumber = client_keys[MyProcNumber]; + client_keys[MyProcNumber] = -1; + LWLockRelease(client_keys_lock); + + /* + * Create a new memory context which is not a part of TopMemoryContext + * tree. This context is used to allocate all memory in this function. + * This helps in keeping the memory allocation in this function to report + * memory consumption statistics separate. So that it does not affect the + * output of this function. + */ + memstats_ctx = AllocSetContextCreate((MemoryContext) NULL, "publish_memory_context_statistics", + ALLOCSET_SMALL_SIZES); + oldcontext = MemoryContextSwitchTo(memstats_ctx); + + /* + * The hash table is used for constructing "path" column of the view, + * similar to its local backend counterpart. + */ + ctl.keysize = sizeof(MemoryContext); + ctl.entrysize = sizeof(MemoryStatsContextId); + ctl.hcxt = CurrentMemoryContext; + + context_id_lookup = hash_create("pg_get_remote_backend_memory_contexts", + 256, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + /* List of contexts to process in the next round - start at the top. */ + contexts = list_make1(TopMemoryContext); + + /* + * If DSA exists, created by another process requesting statistics, attach + * to it. We expect the client process to create required DSA and Dshash + * table. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); + + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); + + + snprintf(key, CLIENT_KEY_SIZE, "%d", clientProcNumber); + + /* + * The entry lock is held by dshash_find_or_insert to protect writes to + * process specific memory. Two different processes publishing statistics + * do not block each other. + */ + INJECTION_POINT("memcontext-server-crash", NULL); + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + /* + * Entry has been deleted due to client process exit. Make sure that the + * client always deletes the entry after taking required lock or this + * function may end up writing to unallocated memory. + */ + if (!found) + { + entry->stats_written = false; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + return; + } + + /* + * The client has timed out waiting for us to write statistics and is + * requesting statistics from some other process + */ + if (entry->target_server_id != MyProcPid) + { + entry->stats_written = false; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + return; + } + + /* Should be allocated by a client backend that is requesting statistics */ + Assert(entry->memstats_dsa_pointer != InvalidDsaPointer); + meminfo = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + if (entry->summary) + { + int cxt_id = 0; + List *path = NIL; + MemoryStatsContextId *contextid_entry; + + /* Copy TopMemoryContext statistics to DSA */ + memset(&stat, 0, sizeof(stat)); + (*TopMemoryContext->methods->stats) (TopMemoryContext, NULL, NULL, + &stat, true); + path = lcons_int(1, path); + PublishMemoryContext(meminfo, cxt_id, TopMemoryContext, path, stat, + 1); + + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &TopMemoryContext, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = cxt_id + 1; + + /* + * Copy statistics for each of TopMemoryContexts children. This + * includes statistics of at most 100 children per node, with each + * child node limited to a depth of 100 in its subtree. + */ + for (MemoryContext c = TopMemoryContext->firstchild; c != NULL; + c = c->nextchild) + { + MemoryContextCounters grand_totals; + int num_contexts = 0; + + path = NIL; + memset(&grand_totals, 0, sizeof(grand_totals)); + + cxt_id++; + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &c, + HASH_ENTER, &found); + Assert(!found); + contextid_entry->context_id = cxt_id + 1; + + MemoryContextStatsCounter(c, &grand_totals, &num_contexts); + + path = compute_context_path(c, context_id_lookup); + + PublishMemoryContext(meminfo, cxt_id, c, path, + grand_totals, num_contexts); + } + entry->total_stats = cxt_id + 1; + + entry->stats_written = true; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + /* Notify waiting backends and return */ + ConditionVariableBroadcast(&entry->memcxt_cv); + return; + } + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + MemoryStatsContextId *contextid_entry; + + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &cur, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = context_id + 1; + + /* + * Figure out the transient context_id of this context and each of its + * ancestors, to compute a path for this context. + */ + path = compute_context_path(cur, context_id_lookup); + + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + /* Account for saving one statistics slot for cumulative reporting */ + if (context_id < (MAX_MEMORY_CONTEXT_STATS_NUM - 1)) + { + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1); + } + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].totalspace += stat.totalspace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].nblocks += stat.nblocks; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freespace += stat.freespace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit per process is reached, write aggregate of the + * remaining statistics. + * + * We can store contexts from 0 to max_stats - 1. When context_id is + * greater than max_stats, we stop reporting individual statistics + * when context_id equals max_stats - 2. As we use max_stats - 1 array + * slot for reporting cumulative statistics or "Remaining Totals". + */ + if (context_id == (MAX_MEMORY_CONTEXT_STATS_NUM - 2)) + { + int namelen = strlen("Remaining Totals"); + + num_individual_stats = context_id + 1; + strlcpy(meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].name, + "Remaining Totals", namelen + 1); + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].ident[0] = '\0'; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].path[0] = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].type = 0; + } + context_id++; + + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + contexts = lappend(contexts, c); + } + + /* + * Statistics are not aggregated, i.e individual statistics reported when + * context_id <= max_stats. + */ + if (context_id <= MAX_MEMORY_CONTEXT_STATS_NUM) + { + entry->total_stats = context_id; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = 1; + } + /* Report number of aggregated memory contexts */ + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = context_id - + num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for cumulative + * statistics. + */ + entry->total_stats = num_individual_stats + 1; + } + entry->stats_written = true; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + /* Notify waiting backends and return */ + ConditionVariableBroadcast(&entry->memcxt_cv); +} + +/* + * Clean up before exit from ProcessGetMemoryContextInterrupt + */ +static void +end_memorycontext_reporting(MemoryStatsDSHashEntry *entry, + MemoryContext oldcontext, HTAB *context_id_lookup) +{ + MemoryContext curr_ctx = CurrentMemoryContext; + + dshash_release_lock(MemoryStatsDsHash, entry); + + hash_destroy(context_id_lookup); + MemoryContextSwitchTo(oldcontext); + MemoryContextReset(curr_ctx); +} + +/* + * compute_context_path + * + * Append the transient context_id of this context and each of its ancestors + * to a list, in order to compute a path. + */ +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + MemoryContext cur_context; + + for (cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryStatsContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + elog(ERROR, "hash table corrupted, can't construct path value"); + + path = lcons_int(cur_entry->context_id, path); + } + + return path; +} + +/* + * PublishMemoryContext + * + * Copy the memory context statistics of a single context to a DSA memory + */ +static void +PublishMemoryContext(MemoryStatsEntry *memcxt_info, int curr_id, + MemoryContext context, List *path, + MemoryContextCounters stat, int num_contexts) +{ + const char *ident = context->ident; + const char *name = context->name; + + /* + * To be consistent with logging output, we label dynahash contexts with + * just the hash table name as with MemoryContextStatsPrint(). + */ + if (context->ident && strncmp(context->name, "dynahash", 8) == 0) + { + name = context->ident; + ident = NULL; + } + + if (name != NULL) + { + int namelen = strlen(name); + + if (strlen(name) >= MEMORY_CONTEXT_NAME_SHMEM_SIZE) + namelen = pg_mbcliplen(name, namelen, + MEMORY_CONTEXT_NAME_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].name, name, namelen + 1); + } + else + /* Clearing the array */ + memcxt_info[curr_id].name[0] = '\0'; + + /* Trim and copy the identifier if it is not set to NULL */ + if (ident != NULL) + { + int idlen = strlen(context->ident); + + /* + * Some identifiers such as SQL query string can be very long, + * truncate oversize identifiers. + */ + if (idlen >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + idlen = pg_mbcliplen(ident, idlen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].ident, ident, idlen + 1); + } + else + memcxt_info[curr_id].ident[0] = '\0'; + + /* Allocate DSA memory for storing path information */ + if (path == NIL) + memcxt_info[curr_id].path[0] = 0; + else + { + int levels = Min(list_length(path), MAX_PATH_DISPLAY_LENGTH); + + memcxt_info[curr_id].path_length = levels; + memcxt_info[curr_id].levels = list_length(path); + + foreach_int(i, path) + { + memcxt_info[curr_id].path[foreach_current_index(i)] = i; + if (--levels == 0) + break; + } + } + memcxt_info[curr_id].type = context->type; + memcxt_info[curr_id].totalspace = stat.totalspace; + memcxt_info[curr_id].nblocks = stat.nblocks; + memcxt_info[curr_id].freespace = stat.freespace; + memcxt_info[curr_id].freechunks = stat.freechunks; + memcxt_info[curr_id].num_agg_stats = num_contexts; +} + +void +AtProcExit_memstats_cleanup(int code, Datum arg) +{ + int idx = MyProcNumber; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + bool found; + + if (MemoryStatsDsHash != NULL) + { + snprintf(key, CLIENT_KEY_SIZE, "%d", idx); + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + if (found) + { + if (MemoryStatsDsaArea != NULL && + DsaPointerIsValid(entry->memstats_dsa_pointer)) + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + } + dshash_delete_entry(MemoryStatsDsHash, entry); + } + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + client_keys[idx] = -1; + LWLockRelease(client_keys_lock); +} + +/* Used for testing purposes */ +dsa_area * +pg_get_memstats_dsa_area(void) +{ + if (MemoryStatsDsaArea != NULL) + return MemoryStatsDsaArea; + else + return NULL; +} diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index d31cb45a058..92b0446b80c 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -39,6 +39,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t PublishMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index 98f9598cd78..202403ebc63 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -658,6 +658,13 @@ BaseInit(void) * drop ephemeral slots, which in turn triggers stats reporting. */ ReplicationSlotInitialize(); + + /* + * The before shmem exit callback frees the DSA memory occupied by the + * latest memory context statistics that could be published by this proc + * if requested. + */ + before_shmem_exit(AtProcExit_memstats_cleanup, 0); } diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 47fd774c7d2..31c4de9f0b4 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -1008,6 +1008,37 @@ MemoryContextStatsInternal(MemoryContext context, int level, } } + +/* + * MemoryContextStatsCounter + * + * Accumulate statistics counts into *totals. totals should not be NULL. + * This involves a non-recursive tree traversal. + */ +void +MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts) +{ + int ichild = 1; + + *num_contexts = 0; + context->methods->stats(context, NULL, NULL, totals, false); + + for (MemoryContext curr = context->firstchild; + curr != NULL; + curr = MemoryContextTraverseNext(curr, context)) + { + curr->methods->stats(curr, NULL, NULL, totals, false); + ichild++; + } + + /* + * Add the count of all the children contexts which are traversed + * including the parent. + */ + *num_contexts = *num_contexts + ichild; +} + /* * MemoryContextStatsPrint * Print callback used by MemoryContextStatsInternal diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 1edb18958f7..5e532b6df21 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8617,6 +8617,16 @@ prorettype => 'bool', proargtypes => 'int4', prosrc => 'pg_log_backend_memory_contexts' }, +# publishing memory contexts of the specified postgres process +{ oid => '2173', descr => 'publish memory contexts of the specified backend', + proname => 'pg_get_process_memory_contexts', provolatile => 'v', + prorows => '100', proretset => 't', proparallel => 'r', + prorettype => 'record', proargtypes => 'int4 bool', + proallargtypes => '{int4,bool,text,text,text,int4,_int4,int8,int8,int8,int8,int8,int4}', + proargmodes => '{i,i,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{pid, summary, name, ident, type, level, path, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes, num_agg_contexts}', + prosrc => 'pg_get_process_memory_contexts' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index 9a7d733ddef..b76f24baed6 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -96,6 +96,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t PublishMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h index 5b0ce383408..613e769c84e 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -136,3 +136,4 @@ PG_LWLOCKTRANCHE(SUBTRANS_SLRU, SubtransSLRU) PG_LWLOCKTRANCHE(XACT_SLRU, XactSLRU) PG_LWLOCKTRANCHE(PARALLEL_VACUUM_DSA, ParallelVacuumDSA) PG_LWLOCKTRANCHE(AIO_URING_COMPLETION, AioUringCompletion) +PG_LWLOCKTRANCHE(MEMORY_CONTEXT_KEYS, MemoryContextReportingKeys) diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index afeeb1ca019..345d5a0ecb1 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -35,6 +35,7 @@ typedef enum PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to send the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ /* Recovery conflict reasons */ diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 7bbe5a36959..70df562c6dd 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -18,7 +18,7 @@ #define MEMUTILS_H #include "nodes/memnodes.h" - +#include "utils/dsa.h" /* * MaxAllocSize, MaxAllocHugeSize @@ -48,7 +48,6 @@ #define AllocHugeSizeIsValid(size) ((Size) (size) <= MaxAllocHugeSize) - /* * Standard top-level memory contexts. * @@ -149,6 +148,7 @@ extern MemoryContext BumpContextCreate(MemoryContext parent, Size minContextSize, Size initBlockSize, Size maxBlockSize); +extern dsa_area *pg_get_memstats_dsa_area(void); /* * Recommended default alloc parameters, suitable for "ordinary" contexts @@ -319,4 +319,11 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) return true; } +extern void ProcessGetMemoryContextInterrupt(void); +extern void HandleGetMemoryContextInterrupt(void); +extern void MemoryContextKeysShmemInit(void); +extern Size MemoryContextKeysShmemSize(void); +extern void MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts); +extern void AtProcExit_memstats_cleanup(int code, Datum arg); #endif /* MEMUTILS_H */ diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 3b37fafa65b..21c65ad2d10 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -233,3 +233,22 @@ select * from pg_timezone_abbrevs where abbrev = 'LMT'; LMT | @ 7 hours 52 mins 58 secs ago | f (1 row) +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; +NOTICE: (AllocSet,TopMemoryContext,) +NOTICE: (AllocSet,TopMemoryContext,) diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 66179f026b3..c9da4fc8c90 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -101,3 +101,21 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs; -- One specific case we can check without much fear of breakage -- is the historical local-mean-time value used for America/Los_Angeles. select * from pg_timezone_abbrevs where abbrev = 'LMT'; + +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 57a8f0366a5..9bd241af59d 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1684,9 +1684,11 @@ MemoryContextCallback MemoryContextCallbackFunction MemoryContextCounters MemoryContextData -MemoryContextId MemoryContextMethodID MemoryContextMethods +MemoryStatsContextId +MemoryStatsEntry +MemoryStatsDSHashEntry MemoryStatsPrintFunc MergeAction MergeActionState -- 2.34.1 [application/octet-stream] v2-0002-Test-module-to-test-memory-context-reporting-with-in (1).patch (9.5K, ../../CAH2L28ucG7zLjLRU4m_hCaGfo_UwUyw89OZR9Q8t8YMriyijgw@mail.gmail.com/4-v2-0002-Test-module-to-test-memory-context-reporting-with-in%20%281%29.patch) download | inline diff: From e21832c610574f48496c961a7318070b720568de Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Thu, 23 Oct 2025 18:01:36 +0530 Subject: [PATCH 2/2] Test module to test memory context reporting with injection points --- src/test/modules/Makefile | 1 + .../test_memcontext_reporting/Makefile | 32 +++++ .../t/001_memcontext_inj.pl | 58 +++++++++ .../test_memcontext_reporting--1.0.sql | 11 ++ .../test_memcontext_reporting.c | 123 ++++++++++++++++++ .../test_memcontext_reporting.control | 4 + 6 files changed, 229 insertions(+) create mode 100644 src/test/modules/test_memcontext_reporting/Makefile create mode 100644 src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting--1.0.sql create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index 902a7954101..a31a2578c18 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -31,6 +31,7 @@ SUBDIRS = \ test_json_parser \ test_lfind \ test_lwlock_tranches \ + test_memcontext_reporting \ test_misc \ test_oat_hooks \ test_parser \ diff --git a/src/test/modules/test_memcontext_reporting/Makefile b/src/test/modules/test_memcontext_reporting/Makefile new file mode 100644 index 00000000000..01a7baa0263 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/Makefile @@ -0,0 +1,32 @@ +# src/test/modules/test_memcontext_reporting/Makefile + +EXTRA_INSTALL = src/test/modules/injection_points + +export enable_injection_points +MODULE_big = test_memcontext_reporting +OBJS = \ + $(WIN32RES) \ + test_memcontext_reporting.o +PGFILEDESC = "test_memcontext_reporting - test code for memory context reporting" + +EXTENSION = test_memcontext_reporting +DATA = test_memcontext_reporting--1.0.sql + +REGRESS = test_memcontext_reporting + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_memcontext_reporting +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif + +check: + $(prove_check) + +installcheck: + $(prove_installcheck) diff --git a/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl new file mode 100644 index 00000000000..69d8489eb37 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl @@ -0,0 +1,58 @@ +# Copyright (c) 2025, PostgreSQL Global Development Group + +# Test suite for testing memory context statistics reporting + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} +my $psql_err; +# Create and start a cluster with one node +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init(allows_streaming => 1); +# max_connections need to be bumped in order to accommodate for pgbench clients +# and log_statement is dialled down since it otherwise will generate enormous +# amounts of logging. Page verification failures are still logged. +$node->append_conf( + 'postgresql.conf', + qq[ +max_connections = 100 +log_statement = none +]); +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION test_memcontext_reporting;'); +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); +# Attaching to a client process injection point that throws an error +$node->safe_psql('postgres', "select injection_points_attach('memcontext-client-crash', 'error');"); + +my $pid = $node->safe_psql('postgres', "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); +print "PID"; +print $pid; + +#Client should have thrown error +$node->psql('postgres', qq(select pg_get_process_memory_contexts($pid, true);), stderr => \$psql_err); +like ( $psql_err, qr/error triggered for injection point memcontext-client-crash/); + +#Query the same process after detaching the injection point, using some other client and it should succeed. +$node->safe_psql('postgres', "select injection_points_detach('memcontext-client-crash');"); +my $topcontext_name = $node->safe_psql('postgres', "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';"); +ok($topcontext_name = 'TopMemoryContext'); + +# Attaching to a target process injection point that throws an error +$node->safe_psql('postgres', "select injection_points_attach('memcontext-server-crash', 'error');"); + +#Server should have thrown error +$node->psql('postgres', qq(select pg_get_process_memory_contexts($pid, true);), stderr => \$psql_err); + +#Query the same process after detaching the injection point, using some other client and it should succeed. +$node->safe_psql('postgres', "select injection_points_detach('memcontext-server-crash');"); +$topcontext_name = $node->safe_psql('postgres', "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';"); +ok($topcontext_name = 'TopMemoryContext'); +done_testing(); diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting--1.0.sql b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting--1.0.sql new file mode 100644 index 00000000000..181daf429d0 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting--1.0.sql @@ -0,0 +1,11 @@ +CREATE FUNCTION memcontext_crash_server() +RETURNS pg_catalog.void +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION memcontext_crash_client() +RETURNS pg_catalog.void +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION dsa_dump_sql() +RETURNS bigint +AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c new file mode 100644 index 00000000000..955155524c2 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c @@ -0,0 +1,123 @@ +/* + * ------------------------------------------------------------------------- + * + * Copyright (c) 2025, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c + * + * ------------------------------------------------------------------------- + */ + +#include "postgres.h" +#include "utils/injection_point.h" +#include "funcapi.h" +#include "utils/injection_point.h" +#include "storage/dsm_registry.h" + +PG_MODULE_MAGIC; + +extern PGDLLEXPORT void crash(const char *name, const void *private_data, void *arg); + +void +crash(const char *name, const void *private_data, void *arg) +{ + abort(); +} + +/* + * memcontext_crash_client + * + * Ensure that the client process aborts in between memory context + * reporting. + */ +PG_FUNCTION_INFO_V1(memcontext_crash_client); +Datum +memcontext_crash_client(PG_FUNCTION_ARGS) +{ +#ifdef USE_INJECTION_POINTS + InjectionPointAttach("memcontext-client-crash", + "test_memcontext_reporting", "crash", NULL, 0); + +#else + elog(ERROR, + "test is not working as intended when injection points are disabled"); +#endif + PG_RETURN_VOID(); +} + +PG_FUNCTION_INFO_V1(memcontext_detach_client); +Datum +memcontext_detach_client(PG_FUNCTION_ARGS) +{ +#ifdef USE_INJECTION_POINTS + InjectionPointDetach("memcontext-client-crash"); + +#else + elog(ERROR, + "test is not working as intended when injection points are disabled"); +#endif + PG_RETURN_VOID(); +} + +/* + * memcontext_crash_server + * + * Ensure that the server process crashes in between memory context + * reporting. + */ +PG_FUNCTION_INFO_V1(memcontext_crash_server); +Datum +memcontext_crash_server(PG_FUNCTION_ARGS) +{ +#ifdef USE_INJECTION_POINTS + InjectionPointAttach("memcontext-server-crash", + "test_memcontext_reporting", "crash", NULL, 0); + +#else + elog(ERROR, + "test is not working as intended when injection points are disabled"); +#endif + PG_RETURN_VOID(); +} + +/* + * memcontext_detach_server + * + * Detach the injection point which crashes the server + * reporting. + */ +PG_FUNCTION_INFO_V1(memcontext_detach_server); +Datum +memcontext_detach_server(PG_FUNCTION_ARGS) +{ +#ifdef USE_INJECTION_POINTS + InjectionPointDetach("memcontext-server-crash"); + +#else + elog(ERROR, + "test is not working as intended when injection points are disabled"); +#endif + PG_RETURN_VOID(); +} + +/* + * dsa_dump_sql + */ +PG_FUNCTION_INFO_V1(dsa_dump_sql); +Datum +dsa_dump_sql(PG_FUNCTION_ARGS) +{ + bool found; + size_t tot_size; + dsa_area *memstats_dsa_area; + + memstats_dsa_area = pg_get_memstats_dsa_area(); + + if (memstats_dsa_area == NULL) + memstats_dsa_area = GetNamedDSA("memory_context_statistics_dsa", &found); + + tot_size = dsa_get_total_size(memstats_dsa_area); + dsa_detach(memstats_dsa_area); + PG_RETURN_INT64(tot_size); +} diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control new file mode 100644 index 00000000000..48b501682d5 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control @@ -0,0 +1,4 @@ +comment = 'Test code for memcontext reporting' +default_version = '1.0' +module_pathname = '$libdir/test_memcontext_reporting' +relocatable = true -- 2.34.1 ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-11-28 09:22 Rahila Syed <[email protected]> parent: Rahila Syed <[email protected]> 0 siblings, 2 replies; 52+ messages in thread From: Rahila Syed @ 2025-11-28 09:22 UTC (permalink / raw) To: Daniel Gustafsson <[email protected]>; +Cc: pgsql-hackers; torikoshia <[email protected]> Hi, I'm attaching the updated patches, which primarily include cleanup and have been rebased following the CFbot report. Thank you, Rahila Syed On Tue, Nov 25, 2025 at 12:50 PM Rahila Syed <[email protected]> wrote: > Hi Daniel, > > Thank you for your comments. Please find attached v41 with all the > comments addressed. > > >> >> +#include "access/twophase.h" >> +#include "catalog/pg_authid_d.h" >> ... >> +#include "utils/acl.h" >> Are these actually required to be included? >> >> > Removed these. > > >> - MemoryContextId *entry; >> + MemoryStatsContextId *entry; >> Why is this needed? MemoryStatsContextId is identical to MemoryContextId >> and >> is too only used in mcxtfuncs.c so there is no need to expose it in >> memutils.h. >> Can't you just use MemoryContextId everywhere or am I missing something? >> >> > MemoryContextId has been renamed to MemoryStatsContextId for better > code readability. I removed the leftover MemoryContextId definition. > Also, I moved it out of memutils.h. Did the same with some other structures > and definitions which were only used in mcxtfuncs.c > > >> +#define CLIENT_KEY_SIZE 64 >> + >> +static LWLock *client_keys_lock = NULL; >> +static int *client_keys = NULL; >> +static dshash_table *MemoryStatsDsHash = NULL; >> +static dsa_area *MemoryStatsDsaArea = NULL; >> These new additions have in some cases too generic names (client_keys >> etc) and >> they all lack comments explaining why they're needed. Maybe a leading >> block >> comment explaining they are used for process memory context reporting, >> and then >> inline comments on each with their use? >> >> > Added comments. > > >> +#define CLIENT_KEY_SIZE 64 >> ... >> + char key[CLIENT_KEY_SIZE]; >> ... >> + snprintf(key, sizeof(key), "%d", MyProcNumber); >> Given that MyProcNumber is an index into the proc array, it seems >> excessive to >> use 64 bytes to store it, can't we get away with a small stack allocation? >> > > I agree. Defined it as 32 bytes as MyProcNumber is of size uint32. Kindly > let me know if you think it can be reduced further. > > > + * Retreive the client key for publishing statistics and reset it to >> -1, >> s/Retreive/Retrieve/ >> > > Fixed. > > >> + ProcNumber procNumber = INVALID_PROC_NUMBER; >> This variable is never accessed before getting re-assigned, so this >> assignment >> in the variable definition can be removed per project style. >> >> >> > Fixed too. > > >> + InitMaterializedSRF(fcinfo, 0); >> Can this initialization be postponed till when we know the ResultSetInfo >> is >> needed? While a micro optimization, it seems we can avoid that overhead >> in >> case the query errors out? >> >> > Good point. Added this just before the result set is getting populated. > > >> + if (MemoryStatsDsHash == NULL) >> + MemoryStatsDsHash = >> GetNamedDSHash("memory_context_statistics_dshash", &memctx_dsh_params, >> &found); >> Nitpick, but there are a few oversize lines, like this one, which need to >> be >> wrapped to match project style. >> >> > I have edited this accordingly. > > >> + /* >> + * XXX. If the process exits without cleaning up its slot, i.e in >> case of >> + * an abrupt crash the client_keys slot won't be reset thus resulting >> in >> + * false negative and WARNING would be thrown in case another process >> with >> + * same slot index is queried for statistics. >> + */ >> + if (client_keys[procNumber] == -1) >> + client_keys[procNumber] = MyProcNumber; >> + else >> + { >> + LWLockRelease(client_keys_lock); >> + ereport(WARNING, >> + errmsg("server process %d is processing previous >> request", pid)); >> + PG_RETURN_NULL(); >> + } >> AFAICT this mean that a failure to clean up (through a crash for example) >> can >> block a future backend from reporting which isn't entirely ideal. Is >> there >> anything we can do to mitigate this? >> >> > Yes, we can reset it when the client times out, as long as we verify that > the value corresponds > to our ProcNumber and not another client's request. Fixed accordingly. > > >> >> + bool summary = false; >> In ProcessGetMemoryContextInterrupt(), can't we just read entry->summary >> rather >> than define a local variable and assign it? We already read lots of other >> fields from entry directly so it seems more readable to be consistent. >> >> > Fixed. > > >> >> + /* >> + * Add the count of children contexts which are traversed >> + */ >> + *num_contexts = *num_contexts + ichild; >> Isn't this really the number of children + the parent context? ichild >> starts >> at one to (AIUI) include the parent context. Also, >> MemoryContextStatsCounter >> should also make sure to set num_contexts to zero before adding to it. >> >> > Yes. Adjusted the comment to match this and set num_contexts to zero. > > >> >> +#define MAX_MEMORY_CONTEXT_STATS_SIZE (sizeof(MemoryStatsEntry)) >> +#define MAX_MEMORY_CONTEXT_STATS_NUM >> MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND / MAX_MEMORY_CONTEXT_STATS_SIZE >> I don't think MAX_MEMORY_CONTEXT_STATS_SIZE adds any value as it's only >> used >> once, on the line directly after its definition. We can just use the >> expansion >> of ((sizeof(MemoryStatsEntry)) when defining MAX_MEMORY_CONTEXT_STATS_NUM. >> >> > Fixed. > > I've attached the test patch as is, I will clean it up and do further > improvements to it. > > Thank you, > Rahila Syed > Attachments: [application/octet-stream] v42-0002-Test-module-to-test-memory-context-reporting-with-in.patch (9.0K, ../../CAH2L28sMK4qyo9xH9Jy7UTj1seZDDi4+ATjSSKTcrfcvk_V=ig@mail.gmail.com/3-v42-0002-Test-module-to-test-memory-context-reporting-with-in.patch) download | inline diff: From d11c41bf3055a3a8a4c109e49a87a49ffaeaaa88 Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Fri, 28 Nov 2025 14:46:38 +0530 Subject: [PATCH 2/2] Test module to test memory context reporting with injection points --- src/test/modules/Makefile | 1 + .../test_memcontext_reporting/Makefile | 32 ++++++ .../t/001_memcontext_inj.pl | 58 ++++++++++ .../test_memcontext_reporting--1.0.sql | 7 ++ .../test_memcontext_reporting.c | 102 ++++++++++++++++++ .../test_memcontext_reporting.control | 4 + 6 files changed, 204 insertions(+) create mode 100644 src/test/modules/test_memcontext_reporting/Makefile create mode 100644 src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting--1.0.sql create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index d079b91b1a2..1ed0cdc66b3 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -32,6 +32,7 @@ SUBDIRS = \ test_json_parser \ test_lfind \ test_lwlock_tranches \ + test_memcontext_reporting \ test_misc \ test_oat_hooks \ test_parser \ diff --git a/src/test/modules/test_memcontext_reporting/Makefile b/src/test/modules/test_memcontext_reporting/Makefile new file mode 100644 index 00000000000..01a7baa0263 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/Makefile @@ -0,0 +1,32 @@ +# src/test/modules/test_memcontext_reporting/Makefile + +EXTRA_INSTALL = src/test/modules/injection_points + +export enable_injection_points +MODULE_big = test_memcontext_reporting +OBJS = \ + $(WIN32RES) \ + test_memcontext_reporting.o +PGFILEDESC = "test_memcontext_reporting - test code for memory context reporting" + +EXTENSION = test_memcontext_reporting +DATA = test_memcontext_reporting--1.0.sql + +REGRESS = test_memcontext_reporting + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_memcontext_reporting +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif + +check: + $(prove_check) + +installcheck: + $(prove_installcheck) diff --git a/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl new file mode 100644 index 00000000000..69d8489eb37 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl @@ -0,0 +1,58 @@ +# Copyright (c) 2025, PostgreSQL Global Development Group + +# Test suite for testing memory context statistics reporting + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} +my $psql_err; +# Create and start a cluster with one node +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init(allows_streaming => 1); +# max_connections need to be bumped in order to accommodate for pgbench clients +# and log_statement is dialled down since it otherwise will generate enormous +# amounts of logging. Page verification failures are still logged. +$node->append_conf( + 'postgresql.conf', + qq[ +max_connections = 100 +log_statement = none +]); +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION test_memcontext_reporting;'); +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); +# Attaching to a client process injection point that throws an error +$node->safe_psql('postgres', "select injection_points_attach('memcontext-client-crash', 'error');"); + +my $pid = $node->safe_psql('postgres', "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); +print "PID"; +print $pid; + +#Client should have thrown error +$node->psql('postgres', qq(select pg_get_process_memory_contexts($pid, true);), stderr => \$psql_err); +like ( $psql_err, qr/error triggered for injection point memcontext-client-crash/); + +#Query the same process after detaching the injection point, using some other client and it should succeed. +$node->safe_psql('postgres', "select injection_points_detach('memcontext-client-crash');"); +my $topcontext_name = $node->safe_psql('postgres', "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';"); +ok($topcontext_name = 'TopMemoryContext'); + +# Attaching to a target process injection point that throws an error +$node->safe_psql('postgres', "select injection_points_attach('memcontext-server-crash', 'error');"); + +#Server should have thrown error +$node->psql('postgres', qq(select pg_get_process_memory_contexts($pid, true);), stderr => \$psql_err); + +#Query the same process after detaching the injection point, using some other client and it should succeed. +$node->safe_psql('postgres', "select injection_points_detach('memcontext-server-crash');"); +$topcontext_name = $node->safe_psql('postgres', "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';"); +ok($topcontext_name = 'TopMemoryContext'); +done_testing(); diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting--1.0.sql b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting--1.0.sql new file mode 100644 index 00000000000..4f787cf789e --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting--1.0.sql @@ -0,0 +1,7 @@ +CREATE FUNCTION memcontext_crash_server() +RETURNS pg_catalog.void +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION memcontext_crash_client() +RETURNS pg_catalog.void +AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c new file mode 100644 index 00000000000..774ae7df49d --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c @@ -0,0 +1,102 @@ +/* + * ------------------------------------------------------------------------- + * + * Copyright (c) 2025, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c + * + * ------------------------------------------------------------------------- + */ + +#include "postgres.h" +#include "utils/injection_point.h" +#include "funcapi.h" +#include "utils/injection_point.h" +#include "storage/dsm_registry.h" + +PG_MODULE_MAGIC; + +extern PGDLLEXPORT void crash(const char *name, const void *private_data, void *arg); + +void +crash(const char *name, const void *private_data, void *arg) +{ + abort(); +} + +/* + * memcontext_crash_client + * + * Ensure that the client process aborts in between memory context + * reporting. + */ +PG_FUNCTION_INFO_V1(memcontext_crash_client); +Datum +memcontext_crash_client(PG_FUNCTION_ARGS) +{ +#ifdef USE_INJECTION_POINTS + InjectionPointAttach("memcontext-client-crash", + "test_memcontext_reporting", "crash", NULL, 0); + +#else + elog(ERROR, + "test is not working as intended when injection points are disabled"); +#endif + PG_RETURN_VOID(); +} + +PG_FUNCTION_INFO_V1(memcontext_detach_client); +Datum +memcontext_detach_client(PG_FUNCTION_ARGS) +{ +#ifdef USE_INJECTION_POINTS + InjectionPointDetach("memcontext-client-crash"); + +#else + elog(ERROR, + "test is not working as intended when injection points are disabled"); +#endif + PG_RETURN_VOID(); +} + +/* + * memcontext_crash_server + * + * Ensure that the server process crashes in between memory context + * reporting. + */ +PG_FUNCTION_INFO_V1(memcontext_crash_server); +Datum +memcontext_crash_server(PG_FUNCTION_ARGS) +{ +#ifdef USE_INJECTION_POINTS + InjectionPointAttach("memcontext-server-crash", + "test_memcontext_reporting", "crash", NULL, 0); + +#else + elog(ERROR, + "test is not working as intended when injection points are disabled"); +#endif + PG_RETURN_VOID(); +} + +/* + * memcontext_detach_server + * + * Detach the injection point which crashes the server + * reporting. + */ +PG_FUNCTION_INFO_V1(memcontext_detach_server); +Datum +memcontext_detach_server(PG_FUNCTION_ARGS) +{ +#ifdef USE_INJECTION_POINTS + InjectionPointDetach("memcontext-server-crash"); + +#else + elog(ERROR, + "test is not working as intended when injection points are disabled"); +#endif + PG_RETURN_VOID(); +} diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control new file mode 100644 index 00000000000..48b501682d5 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control @@ -0,0 +1,4 @@ +comment = 'Test code for memcontext reporting' +default_version = '1.0' +module_pathname = '$libdir/test_memcontext_reporting' +relocatable = true -- 2.34.1 [application/octet-stream] v42-0001-Add-function-to-report-memory-context-statistics.patch (58.2K, ../../CAH2L28sMK4qyo9xH9Jy7UTj1seZDDi4+ATjSSKTcrfcvk_V=ig@mail.gmail.com/4-v42-0001-Add-function-to-report-memory-context-statistics.patch) download | inline diff: From a929a519d994a4ced1fd69ce64dd83d5bfc8ff19 Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Thu, 27 Nov 2025 14:39:43 +0530 Subject: [PATCH] Add function to report memory context statistics This adds a function for retrieving memory context statistics and information from backends as well as auxiliary processes. The intended usecase is cluster debugging when under memory pressure or unanticipated memory usage characteristics. When calling the function it sends a signal to the specified process to submit statistics regarding its memory contexts into dynamic shared memory. Each memory context is returned in detail, followed by a cumulative total in case the number of contexts exceed the max allocated amount of shared memory. Each process is limited to use at most 1Mb memory for this. A summary can also be explicitly requested by the user, this will return the TopMemoryContext and a cumulative total of all lower contexts. In order to not block on busy processes, we have hardcoded the number of seconds during which to retry before timing out. In the case where no statistics are published within the set timeout, NULL is returned --- doc/src/sgml/func/func-admin.sgml | 157 +++ src/backend/catalog/system_views.sql | 5 + src/backend/postmaster/autovacuum.c | 4 + src/backend/postmaster/checkpointer.c | 4 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 4 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 4 + src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/storage/lmgr/proc.c | 1 + src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 2 + src/backend/utils/adt/mcxtfuncs.c | 1011 ++++++++++++++++- src/backend/utils/init/globals.c | 1 + src/backend/utils/init/postinit.c | 7 + src/backend/utils/mmgr/mcxt.c | 31 + src/include/catalog/pg_proc.dat | 10 + src/include/miscadmin.h | 1 + src/include/storage/lwlocklist.h | 1 + src/include/storage/procsignal.h | 1 + src/include/utils/memutils.h | 10 +- src/test/regress/expected/sysviews.out | 19 + src/test/regress/sql/sysviews.sql | 18 + src/tools/pgindent/typedefs.list | 4 +- 25 files changed, 1282 insertions(+), 30 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 1b465bc8ba7..a5c66837241 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -251,6 +251,130 @@ <literal>false</literal> is returned. </para></entry> </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_get_process_memory_contexts</primary> + </indexterm> + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type>, <parameter>summary</parameter> <type>boolean</type> ) + <returnvalue>setof record</returnvalue> + ( <parameter>name</parameter> <type>text</type>, + <parameter>ident</parameter> <type>text</type>, + <parameter>type</parameter> <type>text</type>, + <parameter>level</parameter> <type>integer</type>, + <parameter>path</parameter> <type>integer[]</type>, + <parameter>total_bytes</parameter> <type>bigint</type>, + <parameter>total_nblocks</parameter> <type>bigint</type>, + <parameter>free_bytes</parameter> <type>bigint</type>, + <parameter>free_chunks</parameter> <type>bigint</type>, + <parameter>used_bytes</parameter> <type>bigint</type>, + <parameter>num_agg_contexts</parameter> <type>integer</type> ) + </para> + <para> + This function handles requests to display the memory contexts of a + <productname>PostgreSQL</productname> process with the specified + process ID. The function can be used to send requests to backends as + well as <glossterm linkend="glossary-auxiliary-proc">auxiliary processes</glossterm>. + </para> + <para> + The returned record contains extended statistics per each memory + context: + <itemizedlist spacing="compact"> + <listitem> + <para> + <parameter>name</parameter> - The name of the memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>ident</parameter> - Memory context ID (if any). + </para> + </listitem> + <listitem> + <para> + <parameter>type</parameter> - The type of memory context, possible + values are: AllocSet, Generation, Slab and Bump. + </para> + </listitem> + <listitem> + <para> + <parameter>level</parameter> - The level in the tree of the current + memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>path</parameter> - Memory contexts are organized in a + tree model with TopMemoryContext as the root, and all other memory + contexts as nodes in the tree. The <parameter>path</parameter> + displays the path from the root to the current memory context. The + path is limited to 100 children per node, which each node limited + to a max depth of 100, to preserve memory during reporting. The + printed path will also be limited to 100 nodes counting from the + TopMemoryContext. + </para> + </listitem> + <listitem> + <para> + <parameter>total_bytes</parameter> - The total number of bytes + allocated to this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_nblocks</parameter> - The total number of blocks + used for the allocated memory. + </para> + </listitem> + <listitem> + <para> + <parameter>free_bytes</parameter> - The amount of free memory in + this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>free_chunks</parameter> - The number of chunks that + <parameter>free_bytes</parameter> corresponds to. + </para> + </listitem> + <listitem> + <para> + <parameter>used_bytes</parameter> - The total number of bytes + currently occupied. + </para> + </listitem> + <listitem> + <para> + <parameter>num_agg_contexts</parameter> - The number of memory + contexts aggregated in the displayed statistics. + </para> + </listitem> + </itemizedlist> + </para> + <para> + When <parameter>summary</parameter> is <literal>true</literal>, statistics + for memory contexts at levels 1 and 2 are displayed, with level 1 + representing the root node (i.e., <literal>TopMemoryContext</literal>). + Statistics for contexts on level 2 and below are aggregates of all + child contexts' statistics, where <literal>num_agg_contexts</literal> + indicate the number aggregated child contexts. When + <parameter>summary</parameter> is <literal>false</literal>, + <literal>the num_agg_contexts</literal> value is <literal>1</literal>, + indicating that individual statistics are being displayed. + </para> + <para> + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context + statistics are aggregated and a cumulative total is displayed. The + <literal>num_agg_contexts</literal> column indicates the number of + contexts aggregated in the displayed statistics. When + <literal>num_agg_contexts</literal> is <literal>1</literal> it means + that the context statistics are displayed separately. + </para></entry> + </row> </tbody> </tgroup> </table> @@ -302,6 +426,39 @@ LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 because it may generate a large number of log messages. </para> + <para> + <function>pg_get_process_memory_contexts</function> can be used to request + memory contexts statistics of any <productname>PostgreSQL</productname> + process. For example: +<programlisting> +postgres=# SELECT * FROM pg_get_process_memory_contexts( + (SELECT pid FROM pg_stat_activity + WHERE backend_type = 'checkpointer'), + false) LIMIT 1; +-[ RECORD 1 ]----+------------------------------ +name | TopMemoryContext +ident | +type | AllocSet +level | 1 +path | {1} +total_bytes | 90304 +total_nblocks | 3 +free_bytes | 2880 +free_chunks | 1 +used_bytes | 87424 +num_agg_contexts | 1 +</programlisting> + <note> + <para> + While <function>pg_get_process_memory_contexts</function> can be used to + query memory contexts of the local backend, + <structname>pg_backend_memory_contexts</structname> + (see <xref linkend="view-pg-backend-memory-contexts"/> for more details) + will be less resource intensive when only the local backend is of interest. + </para> + </note> + </para> + </sect2> <sect2 id="functions-admin-backup"> diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index 6fffdb9398e..47c5422f4ad 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -692,6 +692,11 @@ GRANT SELECT ON pg_backend_memory_contexts TO pg_read_all_stats; REVOKE EXECUTE ON FUNCTION pg_get_backend_memory_contexts() FROM PUBLIC; GRANT EXECUTE ON FUNCTION pg_get_backend_memory_contexts() TO pg_read_all_stats; +REVOKE EXECUTE ON FUNCTION + pg_get_process_memory_contexts(integer, boolean) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION + pg_get_process_memory_contexts(integer, boolean) TO pg_read_all_stats; + -- Statistics views CREATE VIEW pg_stat_all_tables AS diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 1c38488f2cb..561d88ebb4d 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -791,6 +791,10 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index e84e8663e96..5b3e08805bf 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -679,6 +679,10 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index ba63b84dfc5..29454b8bf8a 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -48,6 +48,10 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index ce6b5299324..fdd385e492d 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -871,6 +871,10 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index 27e86cf393f..7149a67fcbc 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -192,6 +192,10 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index c4a888a081c..00f03b36ed8 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -879,6 +879,10 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index b23d0c19360..a5ed58a18c5 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -52,6 +52,7 @@ #include "storage/sinvaladt.h" #include "utils/guc.h" #include "utils/injection_point.h" +#include "utils/memutils.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -140,6 +141,7 @@ CalculateShmemSize(void) size = add_size(size, SlotSyncShmemSize()); size = add_size(size, AioShmemSize()); size = add_size(size, WaitLSNShmemSize()); + size = add_size(size, MemoryContextKeysShmemSize() + sizeof(LWLockPadded)); /* include additional requested shmem from preload libraries */ size = add_size(size, total_addin_request); @@ -328,6 +330,7 @@ CreateOrAttachShmemStructs(void) InjectionPointShmemInit(); AioShmemInit(); WaitLSNShmemInit(); + MemoryContextKeysShmemInit(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index 087821311cc..8963285cc12 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -691,6 +691,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_GET_MEMORY_CONTEXT)) + HandleGetMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 1504fafe6d8..c5e69151756 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -51,6 +51,7 @@ #include "storage/procsignal.h" #include "storage/spin.h" #include "storage/standby.h" +#include "utils/memutils.h" #include "utils/timeout.h" #include "utils/timestamp.h" diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 7dd75a490aa..e726f40dfbb 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3539,6 +3539,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index c1ac71ff7f2..644d8d988e1 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -162,6 +162,7 @@ WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." +MEM_CXT_PUBLISH "Waiting for a process to publish memory information." ABI_compatibility: @@ -404,6 +405,7 @@ SubtransSLRU "Waiting to access the sub-transaction SLRU cache." XactSLRU "Waiting to access the transaction status SLRU cache." ParallelVacuumDSA "Waiting for parallel vacuum dynamic shared memory allocation." AioUringCompletion "Waiting for another process to complete IO via io_uring." +MemoryContextReportingKeys "Waiting for another process to complete reading or writing the memory reporting keys." # No "ABI_compatibility" region here as WaitEventLWLock has its own C code. diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index fe6dce9cba3..c661eef7ae9 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -17,28 +17,138 @@ #include "funcapi.h" #include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "storage/dsm_registry.h" #include "storage/proc.h" #include "storage/procarray.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/injection_point.h" +#include "utils/memutils.h" +#include "utils/wait_event_types.h" + +/* + * Memory Context reporting size limits. + */ + +/* Max length of context name and ident, to keep it consistent + * with ProcessLogMemoryContext() + */ +#define MEMORY_CONTEXT_IDENT_SHMEM_SIZE 100 +#define MEMORY_CONTEXT_NAME_SHMEM_SIZE 100 + +/* Maximum size (in bytes) of DSA area per process */ +#define MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND ((size_t) (1 * 1024 * 1024)) + +/* + * Maximum number of memory context statistics is calculated by dividing + * max memory allocated per backend with maximum size per context statistics. + * The identifier and name are statically allocated arrays of size 100 bytes. + * The path depth is limited to 100 like for memory context logging. + */ +#define MAX_MEMORY_CONTEXT_STATS_NUM MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND / (sizeof(MemoryStatsEntry)) + +/* Size of dshash key */ +#define CLIENT_KEY_SIZE 32 + +/* Dynamic shared memory state for reporting statistics per context */ +typedef struct MemoryStatsEntry +{ + char name[MEMORY_CONTEXT_NAME_SHMEM_SIZE]; + char ident[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + int path[100]; + NodeTag type; + int path_length; + int levels; + int64 totalspace; + int64 nblocks; + int64 freespace; + int64 freechunks; + int num_agg_stats; +} MemoryStatsEntry; + +/* + * Per backend dynamic shared hash entry for memory context statistics + * reporting. + */ +typedef struct MemoryStatsDSHashEntry +{ + char key[64]; + ConditionVariable memcxt_cv; + bool stats_written; + int target_server_id; + int total_stats; + bool summary; + dsa_pointer memstats_dsa_pointer; +} MemoryStatsDSHashEntry; + +static const dshash_parameters memctx_dsh_params = { + offsetof(MemoryStatsDSHashEntry, memcxt_cv), + sizeof(MemoryStatsDSHashEntry), + dshash_strcmp, + dshash_strhash, + dshash_strcpy +}; + +/* + * These are used for reporting memory context + * statistics of a process. + */ + +/* Lock to control access to client_keys array */ +static LWLock *client_keys_lock = NULL; + +/* Array to store the keys of MemoryStatsDsHash */ +static int *client_keys = NULL; + +/* + * Table to store pointers to dsa memory containing + * memory statistics and other meta data. There is one + * entry per client backend request, keyed by ProcNumber of + * the client obtained from client_keys array above. + */ +static dshash_table *MemoryStatsDsHash = NULL; + +/* + * Dsa area which stores the actual memory context + * statistics. + */ +static dsa_area *MemoryStatsDsaArea = NULL; + +static void memstats_dsa_cleanup(char *key); +static void memstats_client_key_reset(int ProcNumber); +static const char *ContextTypeToString(NodeTag type); +static void PublishMemoryContext(MemoryStatsEntry *memcxt_info, + int curr_id, MemoryContext context, + List *path, + MemoryContextCounters stat, + int num_contexts); +static List *compute_context_path(MemoryContext c, HTAB *context_id_lookup); +static void end_memorycontext_reporting(MemoryStatsDSHashEntry *entry, MemoryContext oldcontext, + HTAB *context_id_lookup); /* ---------- * The max bytes for showing identifiers of MemoryContext. + * This is used by pg_get_backend_memory_context - view used for local backend. * ---------- */ #define MEMORY_CONTEXT_IDENT_DISPLAY_SIZE 1024 +#define MAX_PATH_DISPLAY_LENGTH 100 +/* Timeout in seconds */ +#define MEMORY_STATS_MAX_TIMEOUT 5 + /* - * MemoryContextId + * MemoryStatsContextId * Used for storage of transient identifiers for - * pg_get_backend_memory_contexts. + * pg_get_backend_memory_contexts and the likes. */ -typedef struct MemoryContextId +typedef struct MemoryStatsContextId { MemoryContext context; int context_id; -} MemoryContextId; +} MemoryStatsContextId; /* * int_list_to_array @@ -89,7 +199,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, */ for (MemoryContext cur = context; cur != NULL; cur = cur->parent) { - MemoryContextId *entry; + MemoryStatsContextId *entry; bool found; entry = hash_search(context_id_lookup, &cur, HASH_FIND, &found); @@ -143,24 +253,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, else nulls[1] = true; - switch (context->type) - { - case T_AllocSetContext: - type = "AllocSet"; - break; - case T_GenerationContext: - type = "Generation"; - break; - case T_SlabContext: - type = "Slab"; - break; - case T_BumpContext: - type = "Bump"; - break; - default: - type = "???"; - break; - } + type = ContextTypeToString(context->type); values[2] = CStringGetTextDatum(type); values[3] = Int32GetDatum(list_length(path)); /* level */ @@ -175,6 +268,38 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, list_free(path); } +/* + * ContextTypeToString + * Returns a textual representation of a context type + * + * This should cover the same types as MemoryContextIsValid. + */ +const char * +ContextTypeToString(NodeTag type) +{ + const char *context_type; + + switch (type) + { + case T_AllocSetContext: + context_type = "AllocSet"; + break; + case T_GenerationContext: + context_type = "Generation"; + break; + case T_SlabContext: + context_type = "Slab"; + break; + case T_BumpContext: + context_type = "Bump"; + break; + default: + context_type = "???"; + break; + } + return context_type; +} + /* * pg_get_backend_memory_contexts * SQL SRF showing backend memory context. @@ -189,7 +314,7 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) HTAB *context_id_lookup; ctl.keysize = sizeof(MemoryContext); - ctl.entrysize = sizeof(MemoryContextId); + ctl.entrysize = sizeof(MemoryStatsContextId); ctl.hcxt = CurrentMemoryContext; context_id_lookup = hash_create("pg_get_backend_memory_contexts", @@ -216,7 +341,7 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) foreach_ptr(MemoryContextData, cur, contexts) { - MemoryContextId *entry; + MemoryStatsContextId *entry; bool found; /* @@ -224,8 +349,8 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) * PutMemoryContextsStatsTupleStore needs this to populate the "path" * column with the parent context_ids. */ - entry = (MemoryContextId *) hash_search(context_id_lookup, &cur, - HASH_ENTER, &found); + entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &cur, + HASH_ENTER, &found); entry->context_id = context_id++; Assert(!found); @@ -305,3 +430,835 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } + +/* + * pg_get_process_memory_contexts + * Signal a backend or an auxiliary process to send its memory contexts, + * wait for the results and display them. + * + * By default, only superusers or users with ROLE_PG_READ_ALL_STATS are allowed + * to signal a process to return the memory contexts. This is because allowing + * any users to issue this request at an unbounded rate would cause lots of + * requests to be sent, which can lead to denial of service. Additional roles + * can be permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to copy the memory context details + * to a dynamic shared memory space. + * + * We have defined a limit on DSA memory that could be allocated per process - + * if the process has more memory contexts than what can fit in the allocated + * size, the excess contexts are summarized and represented as cumulative total + * at the end of the buffer. + * + * After sending the signal, wait on a condition variable. The publishing + * backend, after copying the data to shared memory, sends signal on that + * condition variable. There is one condition variable per client process. + * Once the condition variable is signalled, check if the latest memory context + * information is available and display. + * + * If the publishing backend does not respond before the condition variable + * times out, which is set to a predefined value MEMORY_STATS_MAX_TIMEOUT, give up + * and return NULL. + */ +Datum +pg_get_process_memory_contexts(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + bool summary = PG_GETARG_BOOL(1); + PGPROC *proc; + ProcNumber procNumber; + bool proc_is_aux = false; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + MemoryStatsEntry *memcxt_info; + MemoryStatsDSHashEntry *entry; + bool found; + char key[CLIENT_KEY_SIZE]; + TimestampTz start_timestamp; + + /* + * See if the process with given pid is a backend or an auxiliary process + * and remember the type for when we requery the process later. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + { + proc = AuxiliaryPidGetProc(pid); + proc_is_aux = true; + } + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; this is however not a problem and leave with a WARNING. + * See comment in pg_log_backend_memory_contexts for a discussion on this. + */ + if (proc == NULL) + { + /* + * This is a warning because we don't want to break loops. + */ + ereport(WARNING, + errmsg("PID %d is not a PostgreSQL server process", + pid)); + PG_RETURN_NULL(); + } + + procNumber = GetNumberFromPGProc(proc); + + /* + * Create a DSA to allocate memory for copying memory contexts statistics. + * Allocate the memory in the DSA and send dsa pointer to the server + * process for storing the context statistics. If number of contexts + * exceed a predefined limit (1MB), a cumulative total is stored for such + * contexts. + * + * The DSA is created once for the lifetime of the server, and only + * attached in subsequent calls. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); + + /* + * The dsa pointers containing statistics for each client are stored in a + * dshash table. In addition to dsa pointer, each entry in this table also + * contains information about the statistics, condition variable for + * signalling between client and the server and miscellaneous data + * specific to a request. There is one entry per client request in the + * hash table. + */ + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); + + snprintf(key, sizeof(key), "%d", MyProcNumber); + + /* + * Check if the publishing process slot is empty and store this clients + * key i.e its procNumber. This informs the publishing process that it is + * supposed to write statistics in the hash entry corresponding to this + * client. + */ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + + if (client_keys[procNumber] == -1) + client_keys[procNumber] = MyProcNumber; + else + { + LWLockRelease(client_keys_lock); + ereport(WARNING, + errmsg("server process %d is processing previous request", + pid)); + PG_RETURN_NULL(); + } + LWLockRelease(client_keys_lock); + + /* + * Insert an entry for this client in DSHASH table the first time this + * function is called. This entry is deleted when the process exits in + * before_shmem_exit call. + * + * dshash_find_or_insert locks the entry to prevent the publisher from + * reading before client has updated the entry. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + if (!found) + { + entry->stats_written = false; + ConditionVariableInit(&entry->memcxt_cv); + } + + /* + * Allocate 1MB of memory for the backend to publish its statistics on + * every call to this function. The memory is freed at the end of the + * function. + */ + entry->memstats_dsa_pointer = + dsa_allocate0(MemoryStatsDsaArea, MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND); + + /* + * Specify whether a summary of statistics is requested, before signalling + * the server. + */ + entry->summary = summary; + + /* + * Indicate which server process statistics are being requested from. If + * this client times out before the last requested process can publish its + * statistics, it may send a new request to another server process. Since + * the previous server was notified, it might attempt to read the same + * client entry and respond incorrectly with its statistics. By storing + * the server ID in the client entry, we prevent any previously signalled + * server process from writing its statistics in the space meant for the + * newly requested process. + */ + entry->target_server_id = pid; + dshash_release_lock(MemoryStatsDsHash, entry); + + /* + * Send a signal to a PostgreSQL process, informing it we want it to + * produce information about its memory contexts. + */ + if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, procNumber) < 0) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ereport(WARNING, + errmsg("could not send signal to process %d: %m", + pid)); + PG_RETURN_NULL(); + } + start_timestamp = GetCurrentTimestamp(); + + while (1) + { + long elapsed_time; + + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + Assert(found); + + INJECTION_POINT("memcontext-client-crash", NULL); + + memcxt_info = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + /* + * We expect to come out of sleep when the requested process has + * finished publishing the statistics, verified using a boolean + * stats_written. + * + * Make sure that the statistics are actually written by checking that + * the name of the context is not NULL. This is done to ensure that + * the subsequent waits for statistics do not return spuriously if the + * previous call to the function ended in error and thus could not + * clear the stats_written flag. + */ + if (entry->stats_written && memcxt_info[0].name[0] != '\0') + break; + + dshash_release_lock(MemoryStatsDsHash, entry); + + elapsed_time = TimestampDifferenceMilliseconds(start_timestamp, + GetCurrentTimestamp()); + /* Return if we have already exceeded the timeout */ + if (elapsed_time >= MEMORY_STATS_MAX_TIMEOUT * 1000) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + PG_RETURN_NULL(); + } + + /* + * Recheck the state of the backend before sleeping on the condition + * variable to ensure the process is still alive. Only check the + * relevant process type based on the earlier PID check. + */ + if (proc_is_aux) + proc = AuxiliaryPidGetProc(pid); + else + proc = BackendPidGetProc(pid); + + /* + * The target server process ending during memory context processing + * is not an error. + */ + if (proc == NULL) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + ereport(WARNING, + errmsg("PID %d is no longer a PostgreSQL server process", + pid)); + PG_RETURN_NULL(); + } + + /* + * Wait for MEMORY_STATS_MAX_TIMEOUT. If no statistics are available + * within the allowed time then return NULL. The timer is defined in + * milliseconds since that's what the condition variable sleep uses. + */ + if (ConditionVariableTimedSleep(&entry->memcxt_cv, + (MEMORY_STATS_MAX_TIMEOUT * 1000), + WAIT_EVENT_MEM_CXT_PUBLISH)) + { + /* Timeout has expired, return NULL */ + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + PG_RETURN_NULL(); + } + } + + InitMaterializedSRF(fcinfo, 0); + + /* + * Backend has finished publishing the stats, project them. + */ +#define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 11 + for (int i = 0; i < entry->total_stats; i++) + { + ArrayType *path_array; + int path_length; + Datum values[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + bool nulls[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + Datum *path_datum = NULL; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + Assert(memcxt_info[i].name[0] != '\0'); + values[0] = CStringGetTextDatum(memcxt_info[i].name); + + if (memcxt_info[i].ident[0] != '\0') + values[1] = CStringGetTextDatum(memcxt_info[i].ident); + else + nulls[1] = true; + + values[2] = CStringGetTextDatum(ContextTypeToString(memcxt_info[i].type)); + values[3] = Int32GetDatum(memcxt_info[i].levels); + + path_length = memcxt_info[i].path_length; + path_datum = (Datum *) palloc(path_length * sizeof(Datum)); + if (memcxt_info[i].path[0] != 0) + { + for (int j = 0; j < path_length; j++) + path_datum[j] = Int32GetDatum(memcxt_info[i].path[j]); + path_array = construct_array_builtin(path_datum, + path_length, + INT4OID); + values[4] = PointerGetDatum(path_array); + } + else + nulls[4] = true; + + values[5] = Int64GetDatum(memcxt_info[i].totalspace); + values[6] = Int64GetDatum(memcxt_info[i].nblocks); + values[7] = Int64GetDatum(memcxt_info[i].freespace); + values[8] = Int64GetDatum(memcxt_info[i].freechunks); + values[9] = Int64GetDatum(memcxt_info[i].totalspace - + memcxt_info[i].freespace); + values[10] = Int32GetDatum(memcxt_info[i].num_agg_stats); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + dshash_release_lock(MemoryStatsDsHash, entry); + memstats_dsa_cleanup(key); + + ConditionVariableCancelSleep(); + + PG_RETURN_NULL(); +} + +static void +memstats_dsa_cleanup(char *key) +{ + MemoryStatsDSHashEntry *entry; + + entry = dshash_find(MemoryStatsDsHash, key, true); + + Assert(MemoryStatsDsaArea != NULL); + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + entry->memstats_dsa_pointer = InvalidDsaPointer; + entry->stats_written = false; + entry->target_server_id = 0; + + dshash_release_lock(MemoryStatsDsHash, entry); +} + +/* + * Remove this process from the publishing process' + * client key slot, if the stats publishing process has failed to do so. + */ +static void +memstats_client_key_reset(int procNumber) +{ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + + if (client_keys[procNumber] == MyProcNumber) + client_keys[procNumber] = -1; + LWLockRelease(client_keys_lock); +} + +void +MemoryContextKeysShmemInit(void) +{ + bool found; + + client_keys = (int *) + ShmemInitStruct("MemoryContextKeys", + MemoryContextKeysShmemSize() + sizeof(LWLockPadded), &found); + client_keys_lock = (LWLock *) ((char *) client_keys + MemoryContextKeysShmemSize()); + + if (!found) + { + MemSet(client_keys, -1, MemoryContextKeysShmemSize()); + LWLockInitialize(client_keys_lock, LWTRANCHE_MEMORY_CONTEXT_KEYS); + } +} + +Size +MemoryContextKeysShmemSize(void) +{ + Size sz = 0; + Size TotalProcs = 0; + + TotalProcs = add_size(TotalProcs, NUM_AUXILIARY_PROCS); + TotalProcs = add_size(TotalProcs, MaxBackends); + sz = add_size(sz, mul_size(TotalProcs, sizeof(int))); + + return sz; +} + +/* + * HandleGetMemoryContextInterrupt + * Handle receipt of an interrupt indicating a request to publish memory + * contexts statistics. + * + * All the actual work is deferred to ProcessGetMemoryContextInterrupt() as + * this cannot be performed in a signal handler. + */ +void +HandleGetMemoryContextInterrupt(void) +{ + InterruptPending = true; + PublishMemoryContextPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + +/* + * ProcessGetMemoryContextInterrupt + * Generate information about memory contexts used by the process. + * + * Performs a breadth first search on the memory context tree, thus parents + * statistics are reported before their children in the monitoring function + * output. + * + * Statistics for all the processes are shared via the same dynamic shared + * area. Individual statistics are tracked independently in + * per-process DSA pointers. These pointers are stored in a dshash table with + * key as requesting clients ProcNumber. + * + * We calculate maximum number of context's statistics that can be displayed + * using a pre-determined limit for memory available per process for this + * utility and maximum size of statistics for each context. The remaining + * context statistics if any are captured as a cumulative total at the end of + * individual context's statistics. + * + * If summary is true, we capture the level 1 and level 2 contexts + * statistics. For that we traverse the memory context tree recursively in + * depth first search manner to cover all the children of a parent context, to + * be able to display a cumulative total of memory consumption by a parent at + * level 2 and all its children. + */ +void +ProcessGetMemoryContextInterrupt(void) +{ + List *contexts; + HASHCTL ctl; + HTAB *context_id_lookup; + int context_id = 0; + MemoryStatsEntry *meminfo; + MemoryContextCounters stat; + int num_individual_stats = 0; + bool found; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + int clientProcNumber; + MemoryContext memstats_ctx = NULL; + MemoryContext oldcontext = NULL; + + PublishMemoryContextPending = false; + + /* + * Retrieve the client key for publishing statistics and reset it to -1, + * so other clients can request memory statistics from this process + */ + LWLockAcquire(client_keys_lock, LW_SHARED); + Assert(client_keys[MyProcNumber] != -1); + clientProcNumber = client_keys[MyProcNumber]; + client_keys[MyProcNumber] = -1; + LWLockRelease(client_keys_lock); + + /* + * Create a new memory context which is not a part of TopMemoryContext + * tree. This context is used to allocate all memory in this function. + * This helps in keeping the memory allocation in this function to report + * memory consumption statistics separate. So that it does not affect the + * output of this function. + */ + memstats_ctx = AllocSetContextCreate((MemoryContext) NULL, "publish_memory_context_statistics", + ALLOCSET_SMALL_SIZES); + oldcontext = MemoryContextSwitchTo(memstats_ctx); + + /* + * The hash table is used for constructing "path" column of the view, + * similar to its local backend counterpart. + */ + ctl.keysize = sizeof(MemoryContext); + ctl.entrysize = sizeof(MemoryStatsContextId); + ctl.hcxt = CurrentMemoryContext; + + context_id_lookup = hash_create("pg_get_remote_backend_memory_contexts", + 256, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + /* List of contexts to process in the next round - start at the top. */ + contexts = list_make1(TopMemoryContext); + + /* + * If DSA exists, created by another process requesting statistics, attach + * to it. We expect the client process to create required DSA and Dshash + * table. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); + + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); + + + snprintf(key, CLIENT_KEY_SIZE, "%d", clientProcNumber); + + /* + * The entry lock is held by dshash_find_or_insert to protect writes to + * process specific memory. Two different processes publishing statistics + * do not block each other. + */ + INJECTION_POINT("memcontext-server-crash", NULL); + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + /* + * Entry has been deleted due to client process exit. Make sure that the + * client always deletes the entry after taking required lock or this + * function may end up writing to unallocated memory. + */ + if (!found) + { + entry->stats_written = false; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + return; + } + + /* + * The client has timed out waiting for us to write statistics and is + * requesting statistics from some other process + */ + if (entry->target_server_id != MyProcPid) + { + entry->stats_written = false; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + return; + } + + /* Should be allocated by a client backend that is requesting statistics */ + Assert(entry->memstats_dsa_pointer != InvalidDsaPointer); + meminfo = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + if (entry->summary) + { + int cxt_id = 0; + List *path = NIL; + MemoryStatsContextId *contextid_entry; + + /* Copy TopMemoryContext statistics to DSA */ + memset(&stat, 0, sizeof(stat)); + (*TopMemoryContext->methods->stats) (TopMemoryContext, NULL, NULL, + &stat, true); + path = lcons_int(1, path); + PublishMemoryContext(meminfo, cxt_id, TopMemoryContext, path, stat, + 1); + + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, + &TopMemoryContext, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = cxt_id + 1; + + /* + * Copy statistics for each of TopMemoryContexts children. This + * includes statistics of at most 100 children per node, with each + * child node limited to a depth of 100 in its subtree. + */ + for (MemoryContext c = TopMemoryContext->firstchild; c != NULL; + c = c->nextchild) + { + MemoryContextCounters grand_totals; + int num_contexts = 0; + + path = NIL; + memset(&grand_totals, 0, sizeof(grand_totals)); + + cxt_id++; + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, + &c, HASH_ENTER, &found); + Assert(!found); + contextid_entry->context_id = cxt_id + 1; + + MemoryContextStatsCounter(c, &grand_totals, &num_contexts); + + path = compute_context_path(c, context_id_lookup); + + PublishMemoryContext(meminfo, cxt_id, c, path, + grand_totals, num_contexts); + } + entry->total_stats = cxt_id + 1; + + entry->stats_written = true; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + /* Notify waiting client backend and return */ + ConditionVariableSignal(&entry->memcxt_cv); + return; + } + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + MemoryStatsContextId *contextid_entry; + + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, + &cur, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = context_id + 1; + + /* + * Figure out the transient context_id of this context and each of its + * ancestors, to compute a path for this context. + */ + path = compute_context_path(cur, context_id_lookup); + + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + /* Account for saving one statistics slot for cumulative reporting */ + if (context_id < (MAX_MEMORY_CONTEXT_STATS_NUM - 1)) + { + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1); + } + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].totalspace += stat.totalspace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].nblocks += stat.nblocks; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freespace += stat.freespace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit per process is reached, write aggregate of the + * remaining statistics. + * + * We can store contexts from 0 to max_stats - 1. When context_id is + * greater than max_stats, we stop reporting individual statistics + * when context_id equals max_stats - 2. As we use max_stats - 1 array + * slot for reporting cumulative statistics or "Remaining Totals". + */ + if (context_id == (MAX_MEMORY_CONTEXT_STATS_NUM - 2)) + { + int namelen = strlen("Remaining Totals"); + + num_individual_stats = context_id + 1; + strlcpy(meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].name, + "Remaining Totals", namelen + 1); + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].ident[0] = '\0'; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].path[0] = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].type = 0; + } + context_id++; + + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + contexts = lappend(contexts, c); + } + + /* + * Statistics are not aggregated, i.e individual statistics reported when + * context_id <= max_stats. + */ + if (context_id <= MAX_MEMORY_CONTEXT_STATS_NUM) + { + entry->total_stats = context_id; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = 1; + } + /* Report number of aggregated memory contexts */ + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = context_id + - num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for cumulative + * statistics. + */ + entry->total_stats = num_individual_stats + 1; + } + entry->stats_written = true; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + /* Notify waiting client backend and return */ + ConditionVariableSignal(&entry->memcxt_cv); +} + +/* + * Clean up before exit from ProcessGetMemoryContextInterrupt + */ +static void +end_memorycontext_reporting(MemoryStatsDSHashEntry *entry, + MemoryContext oldcontext, HTAB *context_id_lookup) +{ + MemoryContext curr_ctx = CurrentMemoryContext; + + dshash_release_lock(MemoryStatsDsHash, entry); + + hash_destroy(context_id_lookup); + MemoryContextSwitchTo(oldcontext); + MemoryContextReset(curr_ctx); +} + +/* + * compute_context_path + * + * Append the transient context_id of this context and each of its ancestors + * to a list, in order to compute a path. + */ +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + MemoryContext cur_context; + + for (cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryStatsContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + elog(ERROR, "hash table corrupted, can't construct path value"); + + path = lcons_int(cur_entry->context_id, path); + } + + return path; +} + +/* + * PublishMemoryContext + * + * Copy the memory context statistics of a single context to a DSA memory + */ +static void +PublishMemoryContext(MemoryStatsEntry *memcxt_info, int curr_id, + MemoryContext context, List *path, + MemoryContextCounters stat, int num_contexts) +{ + const char *ident = context->ident; + const char *name = context->name; + + /* + * To be consistent with logging output, we label dynahash contexts with + * just the hash table name as with MemoryContextStatsPrint(). + */ + if (context->ident && strncmp(context->name, "dynahash", 8) == 0) + { + name = context->ident; + ident = NULL; + } + + if (name != NULL) + { + int namelen = strlen(name); + + if (strlen(name) >= MEMORY_CONTEXT_NAME_SHMEM_SIZE) + namelen = pg_mbcliplen(name, namelen, + MEMORY_CONTEXT_NAME_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].name, name, namelen + 1); + } + else + /* Clearing the array */ + memcxt_info[curr_id].name[0] = '\0'; + + /* Trim and copy the identifier if it is not set to NULL */ + if (ident != NULL) + { + int idlen = strlen(context->ident); + + /* + * Some identifiers such as SQL query string can be very long, + * truncate oversize identifiers. + */ + if (idlen >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + idlen = pg_mbcliplen(ident, idlen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].ident, ident, idlen + 1); + } + else + memcxt_info[curr_id].ident[0] = '\0'; + + /* Allocate DSA memory for storing path information */ + if (path == NIL) + memcxt_info[curr_id].path[0] = 0; + else + { + int levels = Min(list_length(path), MAX_PATH_DISPLAY_LENGTH); + + memcxt_info[curr_id].path_length = levels; + memcxt_info[curr_id].levels = list_length(path); + + foreach_int(i, path) + { + memcxt_info[curr_id].path[foreach_current_index(i)] = i; + if (--levels == 0) + break; + } + } + memcxt_info[curr_id].type = context->type; + memcxt_info[curr_id].totalspace = stat.totalspace; + memcxt_info[curr_id].nblocks = stat.nblocks; + memcxt_info[curr_id].freespace = stat.freespace; + memcxt_info[curr_id].freechunks = stat.freechunks; + memcxt_info[curr_id].num_agg_stats = num_contexts; +} + +void +AtProcExit_memstats_cleanup(int code, Datum arg) +{ + int idx = MyProcNumber; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + bool found; + + if (MemoryStatsDsHash != NULL) + { + snprintf(key, CLIENT_KEY_SIZE, "%d", idx); + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + if (found) + { + if (MemoryStatsDsaArea != NULL && + DsaPointerIsValid(entry->memstats_dsa_pointer)) + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + } + dshash_delete_entry(MemoryStatsDsHash, entry); + } + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + client_keys[idx] = -1; + LWLockRelease(client_keys_lock); +} diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index d31cb45a058..92b0446b80c 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -39,6 +39,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t PublishMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index 98f9598cd78..202403ebc63 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -658,6 +658,13 @@ BaseInit(void) * drop ephemeral slots, which in turn triggers stats reporting. */ ReplicationSlotInitialize(); + + /* + * The before shmem exit callback frees the DSA memory occupied by the + * latest memory context statistics that could be published by this proc + * if requested. + */ + before_shmem_exit(AtProcExit_memstats_cleanup, 0); } diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 47fd774c7d2..31c4de9f0b4 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -1008,6 +1008,37 @@ MemoryContextStatsInternal(MemoryContext context, int level, } } + +/* + * MemoryContextStatsCounter + * + * Accumulate statistics counts into *totals. totals should not be NULL. + * This involves a non-recursive tree traversal. + */ +void +MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts) +{ + int ichild = 1; + + *num_contexts = 0; + context->methods->stats(context, NULL, NULL, totals, false); + + for (MemoryContext curr = context->firstchild; + curr != NULL; + curr = MemoryContextTraverseNext(curr, context)) + { + curr->methods->stats(curr, NULL, NULL, totals, false); + ichild++; + } + + /* + * Add the count of all the children contexts which are traversed + * including the parent. + */ + *num_contexts = *num_contexts + ichild; +} + /* * MemoryContextStatsPrint * Print callback used by MemoryContextStatsInternal diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 66431940700..8af5c016365 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8617,6 +8617,16 @@ prorettype => 'bool', proargtypes => 'int4', prosrc => 'pg_log_backend_memory_contexts' }, +# publishing memory contexts of the specified postgres process +{ oid => '2173', descr => 'publish memory contexts of the specified backend', + proname => 'pg_get_process_memory_contexts', provolatile => 'v', + prorows => '100', proretset => 't', proparallel => 'r', + prorettype => 'record', proargtypes => 'int4 bool', + proallargtypes => '{int4,bool,text,text,text,int4,_int4,int8,int8,int8,int8,int8,int4}', + proargmodes => '{i,i,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{pid, summary, name, ident, type, level, path, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes, num_agg_contexts}', + prosrc => 'pg_get_process_memory_contexts' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index 9a7d733ddef..b76f24baed6 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -96,6 +96,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t PublishMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h index 5b0ce383408..613e769c84e 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -136,3 +136,4 @@ PG_LWLOCKTRANCHE(SUBTRANS_SLRU, SubtransSLRU) PG_LWLOCKTRANCHE(XACT_SLRU, XactSLRU) PG_LWLOCKTRANCHE(PARALLEL_VACUUM_DSA, ParallelVacuumDSA) PG_LWLOCKTRANCHE(AIO_URING_COMPLETION, AioUringCompletion) +PG_LWLOCKTRANCHE(MEMORY_CONTEXT_KEYS, MemoryContextReportingKeys) diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index afeeb1ca019..345d5a0ecb1 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -35,6 +35,7 @@ typedef enum PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to send the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ /* Recovery conflict reasons */ diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 7bbe5a36959..4296667cbf0 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -18,7 +18,7 @@ #define MEMUTILS_H #include "nodes/memnodes.h" - +#include "utils/dsa.h" /* * MaxAllocSize, MaxAllocHugeSize @@ -48,7 +48,6 @@ #define AllocHugeSizeIsValid(size) ((Size) (size) <= MaxAllocHugeSize) - /* * Standard top-level memory contexts. * @@ -319,4 +318,11 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) return true; } +extern void ProcessGetMemoryContextInterrupt(void); +extern void HandleGetMemoryContextInterrupt(void); +extern void MemoryContextKeysShmemInit(void); +extern Size MemoryContextKeysShmemSize(void); +extern void MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts); +extern void AtProcExit_memstats_cleanup(int code, Datum arg); #endif /* MEMUTILS_H */ diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 3b37fafa65b..21c65ad2d10 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -233,3 +233,22 @@ select * from pg_timezone_abbrevs where abbrev = 'LMT'; LMT | @ 7 hours 52 mins 58 secs ago | f (1 row) +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; +NOTICE: (AllocSet,TopMemoryContext,) +NOTICE: (AllocSet,TopMemoryContext,) diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 66179f026b3..c9da4fc8c90 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -101,3 +101,21 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs; -- One specific case we can check without much fear of breakage -- is the historical local-mean-time value used for America/Los_Angeles. select * from pg_timezone_abbrevs where abbrev = 'LMT'; + +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index e3c3523b5b2..d9b45bdd721 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1686,9 +1686,11 @@ MemoryContextCallback MemoryContextCallbackFunction MemoryContextCounters MemoryContextData -MemoryContextId MemoryContextMethodID MemoryContextMethods +MemoryStatsContextId +MemoryStatsEntry +MemoryStatsDSHashEntry MemoryStatsPrintFunc MergeAction MergeActionState -- 2.34.1 ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-12-08 01:26 torikoshia <[email protected]> parent: Rahila Syed <[email protected]> 1 sibling, 1 reply; 52+ messages in thread From: torikoshia @ 2025-12-08 01:26 UTC (permalink / raw) To: Rahila Syed <[email protected]>; +Cc: Daniel Gustafsson <[email protected]>; pgsql-hackers On 2025-11-28 18:22, Rahila Syed wrote: Hi, > I'm attaching the updated patches, which primarily include cleanup and > have been rebased > following the CFbot report. Thanks for updating the patch! I observed an assertion failure when forcing a timeout as follows: ``` $ psql (pid:38587)=# $ kill -s SIGSTOP 38587 $ psql (pid:38618) =# select * from pg_get_process_memory_contexts(38587, false); name | ident | type | level | path | total_bytes | total_nblocks | free_bytes | free_chunks | used_bytes | num_agg_contexts --------+--------+--------+--------+--------+-------------+---------------+------------+-------------+------------+------------------ [NULL] | [NULL] | [NULL] | [NULL] | [NULL] | [NULL] | [NULL] | [NULL] | [NULL] | [NULL] | [NULL] (1 row) Time: 5013.515 ms (00:05.014) $ kill -s SIGCONT 38587 $ tail postgresql.log TRAP: failed Assert("client_keys[MyProcNumber] != -1"), File: "mcxtfuncs.c", Line: 881, PID: 38587 0 postgres 0x0000000104943400 ExceptionalCondition + 216 1 postgres 0x000000010480f738 ProcessGetMemoryContextInterrupt + 140 2 postgres 0x00000001046f2710 ProcessInterrupts + 3008 3 postgres 0x00000001046f1a78 ProcessClientReadInterrupt + 80 4 postgres 0x0000000104433994 secure_read + 404 5 postgres 0x00000001044411dc pq_recvbuf + 260 6 postgres 0x0000000104441088 pq_getbyte + 96 7 postgres 0x00000001046fa0fc SocketBackend + 44 8 postgres 0x00000001046f6d3c ReadCommand + 44 9 postgres 0x00000001046f6284 PostgresMain + 2900 10 postgres 0x00000001046ed558 BackendInitialize + 0 11 postgres 0x00000001045c0a48 postmaster_child_launch + 456 12 postgres 0x00000001045c8520 BackendStartup + 304 13 postgres 0x00000001045c636c ServerLoop + 372 14 postgres 0x00000001045c4e24 PostmasterMain + 6448 15 postgres 0x0000000104445b4c main + 924 16 dyld 0x0000000188662b98 start + 6076 2025-12-08 07:35:32.608 JST [38540] LOG: 00000: client backend (PID 38587) was terminated by signal 6: Abort trap: 6 2025-12-08 07:35:32.608 JST [38540] LOCATION: LogChildExit, postmaster.c:2872 ``` Below are comments regarding the v42-0001 patch: > In order to not block on busy processes, we have hardcoded > the number of seconds during which to retry before timing out. > In the case where no statistics are published within the set > timeout, NULL is returned. It might be good to also document in func-admin.sgml that the function times out after 5 seconds when the target backend does not respond, and that in such a case NULLs are returned. + * If DSA exists, created by another process requesting statistics, attach + * to it. We expect the client process to create required DSA and Dshash + * table. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); + + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); From the comment, it sounded to me as if the client executing pg_get_process_memory_contexts() might not create the DSA in some cases. Is it correct to assume that such a situation can happen? In [1], as a response to concerns about using DSA inside a CFI handler, you wrote that “all the dynamic shared memory needed to store the statistics is created and deleted in the client function”. So I understood that it would never create the DSA inside the CFI handler. If that understanding is correct, perhaps the comment should be reworded to make that clear. + context_id_lookup = hash_create("pg_get_remote_backend_memory_contexts", This appears to use the old function name. Should this be updated to "pg_get_process_memory_contexts" instead? [1] https://www.postgresql.org/message-id/CAH2L28sc-rEhyntPLoaC2XUa0ZjS5ka6KzEbuSVxQBBnUYu1KQ%40mail.gma... -- Regards, -- Atsushi Torikoshi Seconded from NTT DATA Japan Corporation to SRA OSS K.K. ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-12-08 09:43 Daniel Gustafsson <[email protected]> parent: Rahila Syed <[email protected]> 1 sibling, 1 reply; 52+ messages in thread From: Daniel Gustafsson @ 2025-12-08 09:43 UTC (permalink / raw) To: Rahila Syed <[email protected]>; +Cc: pgsql-hackers; torikoshia <[email protected]> > On 28 Nov 2025, at 10:22, Rahila Syed <[email protected]> wrote: > > Hi, > > I'm attaching the updated patches, which primarily include cleanup and have been rebased > following the CFbot report. Thanks for the patch, below are a few comments and suggestions. As I was reviewing I tweaked the below and have attached the comments as changes in 0003. == in func-admin.sgml + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type>, <parameter>summary</parameter> <type>boolean</type> ) We recently simplified the UI by removing the timeout, and the more I think about it the more I am convinced that there is more simplification to be had. The most likely usage pattern, IMO, will be to get all the contexts and not the summary, so we can make the summary parameter DEFAULT to false. This allows most uses to just pass the pid, without complicating the code at all. == in mcxtfuncs.c +/* Size of dshash key */ +#define CLIENT_KEY_SIZE 32 That's still pretty generous isn't it? We are printing a uint32 into it so the highest number it can reach is ~4 billion (which while the upper limit, is quite theoretic in this case). 10 + 1 bytes should suffice to store right? - * MemoryContextId + * MemoryStatsContextId Sorry, but I still don't agree with this rename and I think we should skip it, if only to avoid changes to existing parts of the code. + * Entry has been deleted due to client process exit. Make sure that the + * client always deletes the entry after taking required lock or this + * function may end up writing to unallocated memory. Can you explain this a bit further, I'm not sure I get it. The code goes on to release a lock immediately and then destroys the hash. Who is responsible for destroying the entry? == In system-views.sql +REVOKE EXECUTE ON FUNCTION + pg_get_process_memory_contexts(integer, boolean) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION + pg_get_process_memory_contexts(integer, boolean) TO pg_read_all_stats; This is not a view, and the functions aren't used to drive a view, so these should not be defined here. The above mentioned change to add DEFAULT handling to the summary parameter fixes this in the attached. == In ProcessGetMemoryContextInterrupt() I'm not a fan of having to exit's from the function doing duplicative cleanups, in the attached I've wrapped them in a conditional to just have one exit path. What do you think about that? == In PublishMemoryContext() + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); Looking at this more I don't really like that resetting the memory context is done via a separate function, when that function must be called from the exact right place to ensure CurrentMemoryContext is what it thinks it is. It's all a bit too magic. Since this is only called in 3 places I would prefer to inline the code in PublishMemoryContext(). == In PublishMemoryContext() + const char *ident = context->ident; + const char *name = context->name; ident and name are defined as const, but they are later assigned to after the initial assignment. I think we need to unconstify these. == In PublishMemoryContext() + if (strlen(name) >= MEMORY_CONTEXT_NAME_SHMEM_SIZE) We already have namelen which is set to exactly strlen(name), so let's reuse that for readability. == In PublishMemoryContext() + /* Allocate DSA memory for storing path information */ This comment is no longer accurate is it? The DSA has already been allocated at this point. == In memutils.h +#include "utils/dsa.h" This is not needed. I also did some smaller comment rewording and reflowing, some smaller cleanups and a fresh pgindent/pgperltidy run. The attached 0003 contains the above. -- Daniel Gustafsson Attachments: [application/octet-stream] v43-0001-Add-function-to-report-memory-context-statistics.patch (58.2K, ../../[email protected]/2-v43-0001-Add-function-to-report-memory-context-statistics.patch) download | inline diff: From 8f50ab607db07dc125fda2831e1d234c2a630ccb Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Thu, 27 Nov 2025 14:39:43 +0530 Subject: [PATCH v43 1/3] Add function to report memory context statistics This adds a function for retrieving memory context statistics and information from backends as well as auxiliary processes. The intended usecase is cluster debugging when under memory pressure or unanticipated memory usage characteristics. When calling the function it sends a signal to the specified process to submit statistics regarding its memory contexts into dynamic shared memory. Each memory context is returned in detail, followed by a cumulative total in case the number of contexts exceed the max allocated amount of shared memory. Each process is limited to use at most 1Mb memory for this. A summary can also be explicitly requested by the user, this will return the TopMemoryContext and a cumulative total of all lower contexts. In order to not block on busy processes, we have hardcoded the number of seconds during which to retry before timing out. In the case where no statistics are published within the set timeout, NULL is returned --- doc/src/sgml/func/func-admin.sgml | 157 +++ src/backend/catalog/system_views.sql | 5 + src/backend/postmaster/autovacuum.c | 4 + src/backend/postmaster/checkpointer.c | 4 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 4 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 4 + src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/storage/lmgr/proc.c | 1 + src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 2 + src/backend/utils/adt/mcxtfuncs.c | 1011 ++++++++++++++++- src/backend/utils/init/globals.c | 1 + src/backend/utils/init/postinit.c | 7 + src/backend/utils/mmgr/mcxt.c | 31 + src/include/catalog/pg_proc.dat | 10 + src/include/miscadmin.h | 1 + src/include/storage/lwlocklist.h | 1 + src/include/storage/procsignal.h | 1 + src/include/utils/memutils.h | 10 +- src/test/regress/expected/sysviews.out | 19 + src/test/regress/sql/sysviews.sql | 18 + src/tools/pgindent/typedefs.list | 4 +- 25 files changed, 1282 insertions(+), 30 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 1b465bc8ba7..a5c66837241 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -251,6 +251,130 @@ <literal>false</literal> is returned. </para></entry> </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_get_process_memory_contexts</primary> + </indexterm> + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type>, <parameter>summary</parameter> <type>boolean</type> ) + <returnvalue>setof record</returnvalue> + ( <parameter>name</parameter> <type>text</type>, + <parameter>ident</parameter> <type>text</type>, + <parameter>type</parameter> <type>text</type>, + <parameter>level</parameter> <type>integer</type>, + <parameter>path</parameter> <type>integer[]</type>, + <parameter>total_bytes</parameter> <type>bigint</type>, + <parameter>total_nblocks</parameter> <type>bigint</type>, + <parameter>free_bytes</parameter> <type>bigint</type>, + <parameter>free_chunks</parameter> <type>bigint</type>, + <parameter>used_bytes</parameter> <type>bigint</type>, + <parameter>num_agg_contexts</parameter> <type>integer</type> ) + </para> + <para> + This function handles requests to display the memory contexts of a + <productname>PostgreSQL</productname> process with the specified + process ID. The function can be used to send requests to backends as + well as <glossterm linkend="glossary-auxiliary-proc">auxiliary processes</glossterm>. + </para> + <para> + The returned record contains extended statistics per each memory + context: + <itemizedlist spacing="compact"> + <listitem> + <para> + <parameter>name</parameter> - The name of the memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>ident</parameter> - Memory context ID (if any). + </para> + </listitem> + <listitem> + <para> + <parameter>type</parameter> - The type of memory context, possible + values are: AllocSet, Generation, Slab and Bump. + </para> + </listitem> + <listitem> + <para> + <parameter>level</parameter> - The level in the tree of the current + memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>path</parameter> - Memory contexts are organized in a + tree model with TopMemoryContext as the root, and all other memory + contexts as nodes in the tree. The <parameter>path</parameter> + displays the path from the root to the current memory context. The + path is limited to 100 children per node, which each node limited + to a max depth of 100, to preserve memory during reporting. The + printed path will also be limited to 100 nodes counting from the + TopMemoryContext. + </para> + </listitem> + <listitem> + <para> + <parameter>total_bytes</parameter> - The total number of bytes + allocated to this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_nblocks</parameter> - The total number of blocks + used for the allocated memory. + </para> + </listitem> + <listitem> + <para> + <parameter>free_bytes</parameter> - The amount of free memory in + this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>free_chunks</parameter> - The number of chunks that + <parameter>free_bytes</parameter> corresponds to. + </para> + </listitem> + <listitem> + <para> + <parameter>used_bytes</parameter> - The total number of bytes + currently occupied. + </para> + </listitem> + <listitem> + <para> + <parameter>num_agg_contexts</parameter> - The number of memory + contexts aggregated in the displayed statistics. + </para> + </listitem> + </itemizedlist> + </para> + <para> + When <parameter>summary</parameter> is <literal>true</literal>, statistics + for memory contexts at levels 1 and 2 are displayed, with level 1 + representing the root node (i.e., <literal>TopMemoryContext</literal>). + Statistics for contexts on level 2 and below are aggregates of all + child contexts' statistics, where <literal>num_agg_contexts</literal> + indicate the number aggregated child contexts. When + <parameter>summary</parameter> is <literal>false</literal>, + <literal>the num_agg_contexts</literal> value is <literal>1</literal>, + indicating that individual statistics are being displayed. + </para> + <para> + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context + statistics are aggregated and a cumulative total is displayed. The + <literal>num_agg_contexts</literal> column indicates the number of + contexts aggregated in the displayed statistics. When + <literal>num_agg_contexts</literal> is <literal>1</literal> it means + that the context statistics are displayed separately. + </para></entry> + </row> </tbody> </tgroup> </table> @@ -302,6 +426,39 @@ LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 because it may generate a large number of log messages. </para> + <para> + <function>pg_get_process_memory_contexts</function> can be used to request + memory contexts statistics of any <productname>PostgreSQL</productname> + process. For example: +<programlisting> +postgres=# SELECT * FROM pg_get_process_memory_contexts( + (SELECT pid FROM pg_stat_activity + WHERE backend_type = 'checkpointer'), + false) LIMIT 1; +-[ RECORD 1 ]----+------------------------------ +name | TopMemoryContext +ident | +type | AllocSet +level | 1 +path | {1} +total_bytes | 90304 +total_nblocks | 3 +free_bytes | 2880 +free_chunks | 1 +used_bytes | 87424 +num_agg_contexts | 1 +</programlisting> + <note> + <para> + While <function>pg_get_process_memory_contexts</function> can be used to + query memory contexts of the local backend, + <structname>pg_backend_memory_contexts</structname> + (see <xref linkend="view-pg-backend-memory-contexts"/> for more details) + will be less resource intensive when only the local backend is of interest. + </para> + </note> + </para> + </sect2> <sect2 id="functions-admin-backup"> diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index 086c4c8fb6f..17ec512622b 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -692,6 +692,11 @@ GRANT SELECT ON pg_backend_memory_contexts TO pg_read_all_stats; REVOKE EXECUTE ON FUNCTION pg_get_backend_memory_contexts() FROM PUBLIC; GRANT EXECUTE ON FUNCTION pg_get_backend_memory_contexts() TO pg_read_all_stats; +REVOKE EXECUTE ON FUNCTION + pg_get_process_memory_contexts(integer, boolean) FROM PUBLIC; +GRANT EXECUTE ON FUNCTION + pg_get_process_memory_contexts(integer, boolean) TO pg_read_all_stats; + -- Statistics views CREATE VIEW pg_stat_all_tables AS diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 1c38488f2cb..561d88ebb4d 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -791,6 +791,10 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index e84e8663e96..5b3e08805bf 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -679,6 +679,10 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index ba63b84dfc5..29454b8bf8a 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -48,6 +48,10 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index ce6b5299324..fdd385e492d 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -871,6 +871,10 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index 27e86cf393f..7149a67fcbc 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -192,6 +192,10 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index c4a888a081c..00f03b36ed8 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -879,6 +879,10 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index b23d0c19360..a5ed58a18c5 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -52,6 +52,7 @@ #include "storage/sinvaladt.h" #include "utils/guc.h" #include "utils/injection_point.h" +#include "utils/memutils.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -140,6 +141,7 @@ CalculateShmemSize(void) size = add_size(size, SlotSyncShmemSize()); size = add_size(size, AioShmemSize()); size = add_size(size, WaitLSNShmemSize()); + size = add_size(size, MemoryContextKeysShmemSize() + sizeof(LWLockPadded)); /* include additional requested shmem from preload libraries */ size = add_size(size, total_addin_request); @@ -328,6 +330,7 @@ CreateOrAttachShmemStructs(void) InjectionPointShmemInit(); AioShmemInit(); WaitLSNShmemInit(); + MemoryContextKeysShmemInit(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index 087821311cc..8963285cc12 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -691,6 +691,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_GET_MEMORY_CONTEXT)) + HandleGetMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index ebc3f4ca457..27b3b51cf2d 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -51,6 +51,7 @@ #include "storage/procsignal.h" #include "storage/spin.h" #include "storage/standby.h" +#include "utils/memutils.h" #include "utils/timeout.h" #include "utils/timestamp.h" diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 7dd75a490aa..e726f40dfbb 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3539,6 +3539,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index c1ac71ff7f2..644d8d988e1 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -162,6 +162,7 @@ WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." +MEM_CXT_PUBLISH "Waiting for a process to publish memory information." ABI_compatibility: @@ -404,6 +405,7 @@ SubtransSLRU "Waiting to access the sub-transaction SLRU cache." XactSLRU "Waiting to access the transaction status SLRU cache." ParallelVacuumDSA "Waiting for parallel vacuum dynamic shared memory allocation." AioUringCompletion "Waiting for another process to complete IO via io_uring." +MemoryContextReportingKeys "Waiting for another process to complete reading or writing the memory reporting keys." # No "ABI_compatibility" region here as WaitEventLWLock has its own C code. diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index fe6dce9cba3..c661eef7ae9 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -17,28 +17,138 @@ #include "funcapi.h" #include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "storage/dsm_registry.h" #include "storage/proc.h" #include "storage/procarray.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/injection_point.h" +#include "utils/memutils.h" +#include "utils/wait_event_types.h" + +/* + * Memory Context reporting size limits. + */ + +/* Max length of context name and ident, to keep it consistent + * with ProcessLogMemoryContext() + */ +#define MEMORY_CONTEXT_IDENT_SHMEM_SIZE 100 +#define MEMORY_CONTEXT_NAME_SHMEM_SIZE 100 + +/* Maximum size (in bytes) of DSA area per process */ +#define MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND ((size_t) (1 * 1024 * 1024)) + +/* + * Maximum number of memory context statistics is calculated by dividing + * max memory allocated per backend with maximum size per context statistics. + * The identifier and name are statically allocated arrays of size 100 bytes. + * The path depth is limited to 100 like for memory context logging. + */ +#define MAX_MEMORY_CONTEXT_STATS_NUM MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND / (sizeof(MemoryStatsEntry)) + +/* Size of dshash key */ +#define CLIENT_KEY_SIZE 32 + +/* Dynamic shared memory state for reporting statistics per context */ +typedef struct MemoryStatsEntry +{ + char name[MEMORY_CONTEXT_NAME_SHMEM_SIZE]; + char ident[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + int path[100]; + NodeTag type; + int path_length; + int levels; + int64 totalspace; + int64 nblocks; + int64 freespace; + int64 freechunks; + int num_agg_stats; +} MemoryStatsEntry; + +/* + * Per backend dynamic shared hash entry for memory context statistics + * reporting. + */ +typedef struct MemoryStatsDSHashEntry +{ + char key[64]; + ConditionVariable memcxt_cv; + bool stats_written; + int target_server_id; + int total_stats; + bool summary; + dsa_pointer memstats_dsa_pointer; +} MemoryStatsDSHashEntry; + +static const dshash_parameters memctx_dsh_params = { + offsetof(MemoryStatsDSHashEntry, memcxt_cv), + sizeof(MemoryStatsDSHashEntry), + dshash_strcmp, + dshash_strhash, + dshash_strcpy +}; + +/* + * These are used for reporting memory context + * statistics of a process. + */ + +/* Lock to control access to client_keys array */ +static LWLock *client_keys_lock = NULL; + +/* Array to store the keys of MemoryStatsDsHash */ +static int *client_keys = NULL; + +/* + * Table to store pointers to dsa memory containing + * memory statistics and other meta data. There is one + * entry per client backend request, keyed by ProcNumber of + * the client obtained from client_keys array above. + */ +static dshash_table *MemoryStatsDsHash = NULL; + +/* + * Dsa area which stores the actual memory context + * statistics. + */ +static dsa_area *MemoryStatsDsaArea = NULL; + +static void memstats_dsa_cleanup(char *key); +static void memstats_client_key_reset(int ProcNumber); +static const char *ContextTypeToString(NodeTag type); +static void PublishMemoryContext(MemoryStatsEntry *memcxt_info, + int curr_id, MemoryContext context, + List *path, + MemoryContextCounters stat, + int num_contexts); +static List *compute_context_path(MemoryContext c, HTAB *context_id_lookup); +static void end_memorycontext_reporting(MemoryStatsDSHashEntry *entry, MemoryContext oldcontext, + HTAB *context_id_lookup); /* ---------- * The max bytes for showing identifiers of MemoryContext. + * This is used by pg_get_backend_memory_context - view used for local backend. * ---------- */ #define MEMORY_CONTEXT_IDENT_DISPLAY_SIZE 1024 +#define MAX_PATH_DISPLAY_LENGTH 100 +/* Timeout in seconds */ +#define MEMORY_STATS_MAX_TIMEOUT 5 + /* - * MemoryContextId + * MemoryStatsContextId * Used for storage of transient identifiers for - * pg_get_backend_memory_contexts. + * pg_get_backend_memory_contexts and the likes. */ -typedef struct MemoryContextId +typedef struct MemoryStatsContextId { MemoryContext context; int context_id; -} MemoryContextId; +} MemoryStatsContextId; /* * int_list_to_array @@ -89,7 +199,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, */ for (MemoryContext cur = context; cur != NULL; cur = cur->parent) { - MemoryContextId *entry; + MemoryStatsContextId *entry; bool found; entry = hash_search(context_id_lookup, &cur, HASH_FIND, &found); @@ -143,24 +253,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, else nulls[1] = true; - switch (context->type) - { - case T_AllocSetContext: - type = "AllocSet"; - break; - case T_GenerationContext: - type = "Generation"; - break; - case T_SlabContext: - type = "Slab"; - break; - case T_BumpContext: - type = "Bump"; - break; - default: - type = "???"; - break; - } + type = ContextTypeToString(context->type); values[2] = CStringGetTextDatum(type); values[3] = Int32GetDatum(list_length(path)); /* level */ @@ -175,6 +268,38 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, list_free(path); } +/* + * ContextTypeToString + * Returns a textual representation of a context type + * + * This should cover the same types as MemoryContextIsValid. + */ +const char * +ContextTypeToString(NodeTag type) +{ + const char *context_type; + + switch (type) + { + case T_AllocSetContext: + context_type = "AllocSet"; + break; + case T_GenerationContext: + context_type = "Generation"; + break; + case T_SlabContext: + context_type = "Slab"; + break; + case T_BumpContext: + context_type = "Bump"; + break; + default: + context_type = "???"; + break; + } + return context_type; +} + /* * pg_get_backend_memory_contexts * SQL SRF showing backend memory context. @@ -189,7 +314,7 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) HTAB *context_id_lookup; ctl.keysize = sizeof(MemoryContext); - ctl.entrysize = sizeof(MemoryContextId); + ctl.entrysize = sizeof(MemoryStatsContextId); ctl.hcxt = CurrentMemoryContext; context_id_lookup = hash_create("pg_get_backend_memory_contexts", @@ -216,7 +341,7 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) foreach_ptr(MemoryContextData, cur, contexts) { - MemoryContextId *entry; + MemoryStatsContextId *entry; bool found; /* @@ -224,8 +349,8 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) * PutMemoryContextsStatsTupleStore needs this to populate the "path" * column with the parent context_ids. */ - entry = (MemoryContextId *) hash_search(context_id_lookup, &cur, - HASH_ENTER, &found); + entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &cur, + HASH_ENTER, &found); entry->context_id = context_id++; Assert(!found); @@ -305,3 +430,835 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } + +/* + * pg_get_process_memory_contexts + * Signal a backend or an auxiliary process to send its memory contexts, + * wait for the results and display them. + * + * By default, only superusers or users with ROLE_PG_READ_ALL_STATS are allowed + * to signal a process to return the memory contexts. This is because allowing + * any users to issue this request at an unbounded rate would cause lots of + * requests to be sent, which can lead to denial of service. Additional roles + * can be permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to copy the memory context details + * to a dynamic shared memory space. + * + * We have defined a limit on DSA memory that could be allocated per process - + * if the process has more memory contexts than what can fit in the allocated + * size, the excess contexts are summarized and represented as cumulative total + * at the end of the buffer. + * + * After sending the signal, wait on a condition variable. The publishing + * backend, after copying the data to shared memory, sends signal on that + * condition variable. There is one condition variable per client process. + * Once the condition variable is signalled, check if the latest memory context + * information is available and display. + * + * If the publishing backend does not respond before the condition variable + * times out, which is set to a predefined value MEMORY_STATS_MAX_TIMEOUT, give up + * and return NULL. + */ +Datum +pg_get_process_memory_contexts(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + bool summary = PG_GETARG_BOOL(1); + PGPROC *proc; + ProcNumber procNumber; + bool proc_is_aux = false; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + MemoryStatsEntry *memcxt_info; + MemoryStatsDSHashEntry *entry; + bool found; + char key[CLIENT_KEY_SIZE]; + TimestampTz start_timestamp; + + /* + * See if the process with given pid is a backend or an auxiliary process + * and remember the type for when we requery the process later. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + { + proc = AuxiliaryPidGetProc(pid); + proc_is_aux = true; + } + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; this is however not a problem and leave with a WARNING. + * See comment in pg_log_backend_memory_contexts for a discussion on this. + */ + if (proc == NULL) + { + /* + * This is a warning because we don't want to break loops. + */ + ereport(WARNING, + errmsg("PID %d is not a PostgreSQL server process", + pid)); + PG_RETURN_NULL(); + } + + procNumber = GetNumberFromPGProc(proc); + + /* + * Create a DSA to allocate memory for copying memory contexts statistics. + * Allocate the memory in the DSA and send dsa pointer to the server + * process for storing the context statistics. If number of contexts + * exceed a predefined limit (1MB), a cumulative total is stored for such + * contexts. + * + * The DSA is created once for the lifetime of the server, and only + * attached in subsequent calls. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); + + /* + * The dsa pointers containing statistics for each client are stored in a + * dshash table. In addition to dsa pointer, each entry in this table also + * contains information about the statistics, condition variable for + * signalling between client and the server and miscellaneous data + * specific to a request. There is one entry per client request in the + * hash table. + */ + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); + + snprintf(key, sizeof(key), "%d", MyProcNumber); + + /* + * Check if the publishing process slot is empty and store this clients + * key i.e its procNumber. This informs the publishing process that it is + * supposed to write statistics in the hash entry corresponding to this + * client. + */ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + + if (client_keys[procNumber] == -1) + client_keys[procNumber] = MyProcNumber; + else + { + LWLockRelease(client_keys_lock); + ereport(WARNING, + errmsg("server process %d is processing previous request", + pid)); + PG_RETURN_NULL(); + } + LWLockRelease(client_keys_lock); + + /* + * Insert an entry for this client in DSHASH table the first time this + * function is called. This entry is deleted when the process exits in + * before_shmem_exit call. + * + * dshash_find_or_insert locks the entry to prevent the publisher from + * reading before client has updated the entry. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + if (!found) + { + entry->stats_written = false; + ConditionVariableInit(&entry->memcxt_cv); + } + + /* + * Allocate 1MB of memory for the backend to publish its statistics on + * every call to this function. The memory is freed at the end of the + * function. + */ + entry->memstats_dsa_pointer = + dsa_allocate0(MemoryStatsDsaArea, MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND); + + /* + * Specify whether a summary of statistics is requested, before signalling + * the server. + */ + entry->summary = summary; + + /* + * Indicate which server process statistics are being requested from. If + * this client times out before the last requested process can publish its + * statistics, it may send a new request to another server process. Since + * the previous server was notified, it might attempt to read the same + * client entry and respond incorrectly with its statistics. By storing + * the server ID in the client entry, we prevent any previously signalled + * server process from writing its statistics in the space meant for the + * newly requested process. + */ + entry->target_server_id = pid; + dshash_release_lock(MemoryStatsDsHash, entry); + + /* + * Send a signal to a PostgreSQL process, informing it we want it to + * produce information about its memory contexts. + */ + if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, procNumber) < 0) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ereport(WARNING, + errmsg("could not send signal to process %d: %m", + pid)); + PG_RETURN_NULL(); + } + start_timestamp = GetCurrentTimestamp(); + + while (1) + { + long elapsed_time; + + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + Assert(found); + + INJECTION_POINT("memcontext-client-crash", NULL); + + memcxt_info = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + /* + * We expect to come out of sleep when the requested process has + * finished publishing the statistics, verified using a boolean + * stats_written. + * + * Make sure that the statistics are actually written by checking that + * the name of the context is not NULL. This is done to ensure that + * the subsequent waits for statistics do not return spuriously if the + * previous call to the function ended in error and thus could not + * clear the stats_written flag. + */ + if (entry->stats_written && memcxt_info[0].name[0] != '\0') + break; + + dshash_release_lock(MemoryStatsDsHash, entry); + + elapsed_time = TimestampDifferenceMilliseconds(start_timestamp, + GetCurrentTimestamp()); + /* Return if we have already exceeded the timeout */ + if (elapsed_time >= MEMORY_STATS_MAX_TIMEOUT * 1000) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + PG_RETURN_NULL(); + } + + /* + * Recheck the state of the backend before sleeping on the condition + * variable to ensure the process is still alive. Only check the + * relevant process type based on the earlier PID check. + */ + if (proc_is_aux) + proc = AuxiliaryPidGetProc(pid); + else + proc = BackendPidGetProc(pid); + + /* + * The target server process ending during memory context processing + * is not an error. + */ + if (proc == NULL) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + ereport(WARNING, + errmsg("PID %d is no longer a PostgreSQL server process", + pid)); + PG_RETURN_NULL(); + } + + /* + * Wait for MEMORY_STATS_MAX_TIMEOUT. If no statistics are available + * within the allowed time then return NULL. The timer is defined in + * milliseconds since that's what the condition variable sleep uses. + */ + if (ConditionVariableTimedSleep(&entry->memcxt_cv, + (MEMORY_STATS_MAX_TIMEOUT * 1000), + WAIT_EVENT_MEM_CXT_PUBLISH)) + { + /* Timeout has expired, return NULL */ + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + PG_RETURN_NULL(); + } + } + + InitMaterializedSRF(fcinfo, 0); + + /* + * Backend has finished publishing the stats, project them. + */ +#define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 11 + for (int i = 0; i < entry->total_stats; i++) + { + ArrayType *path_array; + int path_length; + Datum values[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + bool nulls[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + Datum *path_datum = NULL; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + Assert(memcxt_info[i].name[0] != '\0'); + values[0] = CStringGetTextDatum(memcxt_info[i].name); + + if (memcxt_info[i].ident[0] != '\0') + values[1] = CStringGetTextDatum(memcxt_info[i].ident); + else + nulls[1] = true; + + values[2] = CStringGetTextDatum(ContextTypeToString(memcxt_info[i].type)); + values[3] = Int32GetDatum(memcxt_info[i].levels); + + path_length = memcxt_info[i].path_length; + path_datum = (Datum *) palloc(path_length * sizeof(Datum)); + if (memcxt_info[i].path[0] != 0) + { + for (int j = 0; j < path_length; j++) + path_datum[j] = Int32GetDatum(memcxt_info[i].path[j]); + path_array = construct_array_builtin(path_datum, + path_length, + INT4OID); + values[4] = PointerGetDatum(path_array); + } + else + nulls[4] = true; + + values[5] = Int64GetDatum(memcxt_info[i].totalspace); + values[6] = Int64GetDatum(memcxt_info[i].nblocks); + values[7] = Int64GetDatum(memcxt_info[i].freespace); + values[8] = Int64GetDatum(memcxt_info[i].freechunks); + values[9] = Int64GetDatum(memcxt_info[i].totalspace - + memcxt_info[i].freespace); + values[10] = Int32GetDatum(memcxt_info[i].num_agg_stats); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + dshash_release_lock(MemoryStatsDsHash, entry); + memstats_dsa_cleanup(key); + + ConditionVariableCancelSleep(); + + PG_RETURN_NULL(); +} + +static void +memstats_dsa_cleanup(char *key) +{ + MemoryStatsDSHashEntry *entry; + + entry = dshash_find(MemoryStatsDsHash, key, true); + + Assert(MemoryStatsDsaArea != NULL); + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + entry->memstats_dsa_pointer = InvalidDsaPointer; + entry->stats_written = false; + entry->target_server_id = 0; + + dshash_release_lock(MemoryStatsDsHash, entry); +} + +/* + * Remove this process from the publishing process' + * client key slot, if the stats publishing process has failed to do so. + */ +static void +memstats_client_key_reset(int procNumber) +{ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + + if (client_keys[procNumber] == MyProcNumber) + client_keys[procNumber] = -1; + LWLockRelease(client_keys_lock); +} + +void +MemoryContextKeysShmemInit(void) +{ + bool found; + + client_keys = (int *) + ShmemInitStruct("MemoryContextKeys", + MemoryContextKeysShmemSize() + sizeof(LWLockPadded), &found); + client_keys_lock = (LWLock *) ((char *) client_keys + MemoryContextKeysShmemSize()); + + if (!found) + { + MemSet(client_keys, -1, MemoryContextKeysShmemSize()); + LWLockInitialize(client_keys_lock, LWTRANCHE_MEMORY_CONTEXT_KEYS); + } +} + +Size +MemoryContextKeysShmemSize(void) +{ + Size sz = 0; + Size TotalProcs = 0; + + TotalProcs = add_size(TotalProcs, NUM_AUXILIARY_PROCS); + TotalProcs = add_size(TotalProcs, MaxBackends); + sz = add_size(sz, mul_size(TotalProcs, sizeof(int))); + + return sz; +} + +/* + * HandleGetMemoryContextInterrupt + * Handle receipt of an interrupt indicating a request to publish memory + * contexts statistics. + * + * All the actual work is deferred to ProcessGetMemoryContextInterrupt() as + * this cannot be performed in a signal handler. + */ +void +HandleGetMemoryContextInterrupt(void) +{ + InterruptPending = true; + PublishMemoryContextPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + +/* + * ProcessGetMemoryContextInterrupt + * Generate information about memory contexts used by the process. + * + * Performs a breadth first search on the memory context tree, thus parents + * statistics are reported before their children in the monitoring function + * output. + * + * Statistics for all the processes are shared via the same dynamic shared + * area. Individual statistics are tracked independently in + * per-process DSA pointers. These pointers are stored in a dshash table with + * key as requesting clients ProcNumber. + * + * We calculate maximum number of context's statistics that can be displayed + * using a pre-determined limit for memory available per process for this + * utility and maximum size of statistics for each context. The remaining + * context statistics if any are captured as a cumulative total at the end of + * individual context's statistics. + * + * If summary is true, we capture the level 1 and level 2 contexts + * statistics. For that we traverse the memory context tree recursively in + * depth first search manner to cover all the children of a parent context, to + * be able to display a cumulative total of memory consumption by a parent at + * level 2 and all its children. + */ +void +ProcessGetMemoryContextInterrupt(void) +{ + List *contexts; + HASHCTL ctl; + HTAB *context_id_lookup; + int context_id = 0; + MemoryStatsEntry *meminfo; + MemoryContextCounters stat; + int num_individual_stats = 0; + bool found; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + int clientProcNumber; + MemoryContext memstats_ctx = NULL; + MemoryContext oldcontext = NULL; + + PublishMemoryContextPending = false; + + /* + * Retrieve the client key for publishing statistics and reset it to -1, + * so other clients can request memory statistics from this process + */ + LWLockAcquire(client_keys_lock, LW_SHARED); + Assert(client_keys[MyProcNumber] != -1); + clientProcNumber = client_keys[MyProcNumber]; + client_keys[MyProcNumber] = -1; + LWLockRelease(client_keys_lock); + + /* + * Create a new memory context which is not a part of TopMemoryContext + * tree. This context is used to allocate all memory in this function. + * This helps in keeping the memory allocation in this function to report + * memory consumption statistics separate. So that it does not affect the + * output of this function. + */ + memstats_ctx = AllocSetContextCreate((MemoryContext) NULL, "publish_memory_context_statistics", + ALLOCSET_SMALL_SIZES); + oldcontext = MemoryContextSwitchTo(memstats_ctx); + + /* + * The hash table is used for constructing "path" column of the view, + * similar to its local backend counterpart. + */ + ctl.keysize = sizeof(MemoryContext); + ctl.entrysize = sizeof(MemoryStatsContextId); + ctl.hcxt = CurrentMemoryContext; + + context_id_lookup = hash_create("pg_get_remote_backend_memory_contexts", + 256, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + /* List of contexts to process in the next round - start at the top. */ + contexts = list_make1(TopMemoryContext); + + /* + * If DSA exists, created by another process requesting statistics, attach + * to it. We expect the client process to create required DSA and Dshash + * table. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); + + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); + + + snprintf(key, CLIENT_KEY_SIZE, "%d", clientProcNumber); + + /* + * The entry lock is held by dshash_find_or_insert to protect writes to + * process specific memory. Two different processes publishing statistics + * do not block each other. + */ + INJECTION_POINT("memcontext-server-crash", NULL); + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + /* + * Entry has been deleted due to client process exit. Make sure that the + * client always deletes the entry after taking required lock or this + * function may end up writing to unallocated memory. + */ + if (!found) + { + entry->stats_written = false; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + return; + } + + /* + * The client has timed out waiting for us to write statistics and is + * requesting statistics from some other process + */ + if (entry->target_server_id != MyProcPid) + { + entry->stats_written = false; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + return; + } + + /* Should be allocated by a client backend that is requesting statistics */ + Assert(entry->memstats_dsa_pointer != InvalidDsaPointer); + meminfo = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + if (entry->summary) + { + int cxt_id = 0; + List *path = NIL; + MemoryStatsContextId *contextid_entry; + + /* Copy TopMemoryContext statistics to DSA */ + memset(&stat, 0, sizeof(stat)); + (*TopMemoryContext->methods->stats) (TopMemoryContext, NULL, NULL, + &stat, true); + path = lcons_int(1, path); + PublishMemoryContext(meminfo, cxt_id, TopMemoryContext, path, stat, + 1); + + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, + &TopMemoryContext, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = cxt_id + 1; + + /* + * Copy statistics for each of TopMemoryContexts children. This + * includes statistics of at most 100 children per node, with each + * child node limited to a depth of 100 in its subtree. + */ + for (MemoryContext c = TopMemoryContext->firstchild; c != NULL; + c = c->nextchild) + { + MemoryContextCounters grand_totals; + int num_contexts = 0; + + path = NIL; + memset(&grand_totals, 0, sizeof(grand_totals)); + + cxt_id++; + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, + &c, HASH_ENTER, &found); + Assert(!found); + contextid_entry->context_id = cxt_id + 1; + + MemoryContextStatsCounter(c, &grand_totals, &num_contexts); + + path = compute_context_path(c, context_id_lookup); + + PublishMemoryContext(meminfo, cxt_id, c, path, + grand_totals, num_contexts); + } + entry->total_stats = cxt_id + 1; + + entry->stats_written = true; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + /* Notify waiting client backend and return */ + ConditionVariableSignal(&entry->memcxt_cv); + return; + } + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + MemoryStatsContextId *contextid_entry; + + contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, + &cur, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = context_id + 1; + + /* + * Figure out the transient context_id of this context and each of its + * ancestors, to compute a path for this context. + */ + path = compute_context_path(cur, context_id_lookup); + + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + /* Account for saving one statistics slot for cumulative reporting */ + if (context_id < (MAX_MEMORY_CONTEXT_STATS_NUM - 1)) + { + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1); + } + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].totalspace += stat.totalspace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].nblocks += stat.nblocks; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freespace += stat.freespace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit per process is reached, write aggregate of the + * remaining statistics. + * + * We can store contexts from 0 to max_stats - 1. When context_id is + * greater than max_stats, we stop reporting individual statistics + * when context_id equals max_stats - 2. As we use max_stats - 1 array + * slot for reporting cumulative statistics or "Remaining Totals". + */ + if (context_id == (MAX_MEMORY_CONTEXT_STATS_NUM - 2)) + { + int namelen = strlen("Remaining Totals"); + + num_individual_stats = context_id + 1; + strlcpy(meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].name, + "Remaining Totals", namelen + 1); + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].ident[0] = '\0'; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].path[0] = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].type = 0; + } + context_id++; + + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + contexts = lappend(contexts, c); + } + + /* + * Statistics are not aggregated, i.e individual statistics reported when + * context_id <= max_stats. + */ + if (context_id <= MAX_MEMORY_CONTEXT_STATS_NUM) + { + entry->total_stats = context_id; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = 1; + } + /* Report number of aggregated memory contexts */ + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = context_id + - num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for cumulative + * statistics. + */ + entry->total_stats = num_individual_stats + 1; + } + entry->stats_written = true; + end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + /* Notify waiting client backend and return */ + ConditionVariableSignal(&entry->memcxt_cv); +} + +/* + * Clean up before exit from ProcessGetMemoryContextInterrupt + */ +static void +end_memorycontext_reporting(MemoryStatsDSHashEntry *entry, + MemoryContext oldcontext, HTAB *context_id_lookup) +{ + MemoryContext curr_ctx = CurrentMemoryContext; + + dshash_release_lock(MemoryStatsDsHash, entry); + + hash_destroy(context_id_lookup); + MemoryContextSwitchTo(oldcontext); + MemoryContextReset(curr_ctx); +} + +/* + * compute_context_path + * + * Append the transient context_id of this context and each of its ancestors + * to a list, in order to compute a path. + */ +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + MemoryContext cur_context; + + for (cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryStatsContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + elog(ERROR, "hash table corrupted, can't construct path value"); + + path = lcons_int(cur_entry->context_id, path); + } + + return path; +} + +/* + * PublishMemoryContext + * + * Copy the memory context statistics of a single context to a DSA memory + */ +static void +PublishMemoryContext(MemoryStatsEntry *memcxt_info, int curr_id, + MemoryContext context, List *path, + MemoryContextCounters stat, int num_contexts) +{ + const char *ident = context->ident; + const char *name = context->name; + + /* + * To be consistent with logging output, we label dynahash contexts with + * just the hash table name as with MemoryContextStatsPrint(). + */ + if (context->ident && strncmp(context->name, "dynahash", 8) == 0) + { + name = context->ident; + ident = NULL; + } + + if (name != NULL) + { + int namelen = strlen(name); + + if (strlen(name) >= MEMORY_CONTEXT_NAME_SHMEM_SIZE) + namelen = pg_mbcliplen(name, namelen, + MEMORY_CONTEXT_NAME_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].name, name, namelen + 1); + } + else + /* Clearing the array */ + memcxt_info[curr_id].name[0] = '\0'; + + /* Trim and copy the identifier if it is not set to NULL */ + if (ident != NULL) + { + int idlen = strlen(context->ident); + + /* + * Some identifiers such as SQL query string can be very long, + * truncate oversize identifiers. + */ + if (idlen >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + idlen = pg_mbcliplen(ident, idlen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].ident, ident, idlen + 1); + } + else + memcxt_info[curr_id].ident[0] = '\0'; + + /* Allocate DSA memory for storing path information */ + if (path == NIL) + memcxt_info[curr_id].path[0] = 0; + else + { + int levels = Min(list_length(path), MAX_PATH_DISPLAY_LENGTH); + + memcxt_info[curr_id].path_length = levels; + memcxt_info[curr_id].levels = list_length(path); + + foreach_int(i, path) + { + memcxt_info[curr_id].path[foreach_current_index(i)] = i; + if (--levels == 0) + break; + } + } + memcxt_info[curr_id].type = context->type; + memcxt_info[curr_id].totalspace = stat.totalspace; + memcxt_info[curr_id].nblocks = stat.nblocks; + memcxt_info[curr_id].freespace = stat.freespace; + memcxt_info[curr_id].freechunks = stat.freechunks; + memcxt_info[curr_id].num_agg_stats = num_contexts; +} + +void +AtProcExit_memstats_cleanup(int code, Datum arg) +{ + int idx = MyProcNumber; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + bool found; + + if (MemoryStatsDsHash != NULL) + { + snprintf(key, CLIENT_KEY_SIZE, "%d", idx); + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + if (found) + { + if (MemoryStatsDsaArea != NULL && + DsaPointerIsValid(entry->memstats_dsa_pointer)) + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + } + dshash_delete_entry(MemoryStatsDsHash, entry); + } + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + client_keys[idx] = -1; + LWLockRelease(client_keys_lock); +} diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index d31cb45a058..92b0446b80c 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -39,6 +39,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t PublishMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index 98f9598cd78..202403ebc63 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -658,6 +658,13 @@ BaseInit(void) * drop ephemeral slots, which in turn triggers stats reporting. */ ReplicationSlotInitialize(); + + /* + * The before shmem exit callback frees the DSA memory occupied by the + * latest memory context statistics that could be published by this proc + * if requested. + */ + before_shmem_exit(AtProcExit_memstats_cleanup, 0); } diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 47fd774c7d2..31c4de9f0b4 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -1008,6 +1008,37 @@ MemoryContextStatsInternal(MemoryContext context, int level, } } + +/* + * MemoryContextStatsCounter + * + * Accumulate statistics counts into *totals. totals should not be NULL. + * This involves a non-recursive tree traversal. + */ +void +MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts) +{ + int ichild = 1; + + *num_contexts = 0; + context->methods->stats(context, NULL, NULL, totals, false); + + for (MemoryContext curr = context->firstchild; + curr != NULL; + curr = MemoryContextTraverseNext(curr, context)) + { + curr->methods->stats(curr, NULL, NULL, totals, false); + ichild++; + } + + /* + * Add the count of all the children contexts which are traversed + * including the parent. + */ + *num_contexts = *num_contexts + ichild; +} + /* * MemoryContextStatsPrint * Print callback used by MemoryContextStatsInternal diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 66af2d96d67..5713bbb1550 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8617,6 +8617,16 @@ prorettype => 'bool', proargtypes => 'int4', prosrc => 'pg_log_backend_memory_contexts' }, +# publishing memory contexts of the specified postgres process +{ oid => '2173', descr => 'publish memory contexts of the specified backend', + proname => 'pg_get_process_memory_contexts', provolatile => 'v', + prorows => '100', proretset => 't', proparallel => 'r', + prorettype => 'record', proargtypes => 'int4 bool', + proallargtypes => '{int4,bool,text,text,text,int4,_int4,int8,int8,int8,int8,int8,int4}', + proargmodes => '{i,i,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{pid, summary, name, ident, type, level, path, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes, num_agg_contexts}', + prosrc => 'pg_get_process_memory_contexts' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index 9a7d733ddef..b76f24baed6 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -96,6 +96,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t PublishMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h index 5b0ce383408..613e769c84e 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -136,3 +136,4 @@ PG_LWLOCKTRANCHE(SUBTRANS_SLRU, SubtransSLRU) PG_LWLOCKTRANCHE(XACT_SLRU, XactSLRU) PG_LWLOCKTRANCHE(PARALLEL_VACUUM_DSA, ParallelVacuumDSA) PG_LWLOCKTRANCHE(AIO_URING_COMPLETION, AioUringCompletion) +PG_LWLOCKTRANCHE(MEMORY_CONTEXT_KEYS, MemoryContextReportingKeys) diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index afeeb1ca019..345d5a0ecb1 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -35,6 +35,7 @@ typedef enum PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to send the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ /* Recovery conflict reasons */ diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 7bbe5a36959..4296667cbf0 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -18,7 +18,7 @@ #define MEMUTILS_H #include "nodes/memnodes.h" - +#include "utils/dsa.h" /* * MaxAllocSize, MaxAllocHugeSize @@ -48,7 +48,6 @@ #define AllocHugeSizeIsValid(size) ((Size) (size) <= MaxAllocHugeSize) - /* * Standard top-level memory contexts. * @@ -319,4 +318,11 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) return true; } +extern void ProcessGetMemoryContextInterrupt(void); +extern void HandleGetMemoryContextInterrupt(void); +extern void MemoryContextKeysShmemInit(void); +extern Size MemoryContextKeysShmemSize(void); +extern void MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts); +extern void AtProcExit_memstats_cleanup(int code, Datum arg); #endif /* MEMUTILS_H */ diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 3b37fafa65b..21c65ad2d10 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -233,3 +233,22 @@ select * from pg_timezone_abbrevs where abbrev = 'LMT'; LMT | @ 7 hours 52 mins 58 secs ago | f (1 row) +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; +NOTICE: (AllocSet,TopMemoryContext,) +NOTICE: (AllocSet,TopMemoryContext,) diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 66179f026b3..c9da4fc8c90 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -101,3 +101,21 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs; -- One specific case we can check without much fear of breakage -- is the historical local-mean-time value used for America/Los_Angeles. select * from pg_timezone_abbrevs where abbrev = 'LMT'; + +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index cf3f6a7dafd..aa898f025c0 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1686,9 +1686,11 @@ MemoryContextCallback MemoryContextCallbackFunction MemoryContextCounters MemoryContextData -MemoryContextId MemoryContextMethodID MemoryContextMethods +MemoryStatsContextId +MemoryStatsEntry +MemoryStatsDSHashEntry MemoryStatsPrintFunc MergeAction MergeActionState -- 2.39.3 (Apple Git-146) [application/octet-stream] v43-0002-Test-module-to-test-memory-context-reporting-wit.patch (9.0K, ../../[email protected]/3-v43-0002-Test-module-to-test-memory-context-reporting-wit.patch) download | inline diff: From 00c38517db21893543f0af5b0c2f4eccc0a23658 Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Fri, 28 Nov 2025 14:46:38 +0530 Subject: [PATCH v43 2/3] Test module to test memory context reporting with injection points --- src/test/modules/Makefile | 1 + .../test_memcontext_reporting/Makefile | 32 ++++++ .../t/001_memcontext_inj.pl | 58 ++++++++++ .../test_memcontext_reporting--1.0.sql | 7 ++ .../test_memcontext_reporting.c | 102 ++++++++++++++++++ .../test_memcontext_reporting.control | 4 + 6 files changed, 204 insertions(+) create mode 100644 src/test/modules/test_memcontext_reporting/Makefile create mode 100644 src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting--1.0.sql create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index d079b91b1a2..1ed0cdc66b3 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -32,6 +32,7 @@ SUBDIRS = \ test_json_parser \ test_lfind \ test_lwlock_tranches \ + test_memcontext_reporting \ test_misc \ test_oat_hooks \ test_parser \ diff --git a/src/test/modules/test_memcontext_reporting/Makefile b/src/test/modules/test_memcontext_reporting/Makefile new file mode 100644 index 00000000000..01a7baa0263 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/Makefile @@ -0,0 +1,32 @@ +# src/test/modules/test_memcontext_reporting/Makefile + +EXTRA_INSTALL = src/test/modules/injection_points + +export enable_injection_points +MODULE_big = test_memcontext_reporting +OBJS = \ + $(WIN32RES) \ + test_memcontext_reporting.o +PGFILEDESC = "test_memcontext_reporting - test code for memory context reporting" + +EXTENSION = test_memcontext_reporting +DATA = test_memcontext_reporting--1.0.sql + +REGRESS = test_memcontext_reporting + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_memcontext_reporting +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif + +check: + $(prove_check) + +installcheck: + $(prove_installcheck) diff --git a/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl new file mode 100644 index 00000000000..69d8489eb37 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl @@ -0,0 +1,58 @@ +# Copyright (c) 2025, PostgreSQL Global Development Group + +# Test suite for testing memory context statistics reporting + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} +my $psql_err; +# Create and start a cluster with one node +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init(allows_streaming => 1); +# max_connections need to be bumped in order to accommodate for pgbench clients +# and log_statement is dialled down since it otherwise will generate enormous +# amounts of logging. Page verification failures are still logged. +$node->append_conf( + 'postgresql.conf', + qq[ +max_connections = 100 +log_statement = none +]); +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION test_memcontext_reporting;'); +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); +# Attaching to a client process injection point that throws an error +$node->safe_psql('postgres', "select injection_points_attach('memcontext-client-crash', 'error');"); + +my $pid = $node->safe_psql('postgres', "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); +print "PID"; +print $pid; + +#Client should have thrown error +$node->psql('postgres', qq(select pg_get_process_memory_contexts($pid, true);), stderr => \$psql_err); +like ( $psql_err, qr/error triggered for injection point memcontext-client-crash/); + +#Query the same process after detaching the injection point, using some other client and it should succeed. +$node->safe_psql('postgres', "select injection_points_detach('memcontext-client-crash');"); +my $topcontext_name = $node->safe_psql('postgres', "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';"); +ok($topcontext_name = 'TopMemoryContext'); + +# Attaching to a target process injection point that throws an error +$node->safe_psql('postgres', "select injection_points_attach('memcontext-server-crash', 'error');"); + +#Server should have thrown error +$node->psql('postgres', qq(select pg_get_process_memory_contexts($pid, true);), stderr => \$psql_err); + +#Query the same process after detaching the injection point, using some other client and it should succeed. +$node->safe_psql('postgres', "select injection_points_detach('memcontext-server-crash');"); +$topcontext_name = $node->safe_psql('postgres', "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';"); +ok($topcontext_name = 'TopMemoryContext'); +done_testing(); diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting--1.0.sql b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting--1.0.sql new file mode 100644 index 00000000000..4f787cf789e --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting--1.0.sql @@ -0,0 +1,7 @@ +CREATE FUNCTION memcontext_crash_server() +RETURNS pg_catalog.void +AS 'MODULE_PATHNAME' LANGUAGE C; + +CREATE FUNCTION memcontext_crash_client() +RETURNS pg_catalog.void +AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c new file mode 100644 index 00000000000..774ae7df49d --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c @@ -0,0 +1,102 @@ +/* + * ------------------------------------------------------------------------- + * + * Copyright (c) 2025, PostgreSQL Global Development Group + * + * IDENTIFICATION + * src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c + * + * ------------------------------------------------------------------------- + */ + +#include "postgres.h" +#include "utils/injection_point.h" +#include "funcapi.h" +#include "utils/injection_point.h" +#include "storage/dsm_registry.h" + +PG_MODULE_MAGIC; + +extern PGDLLEXPORT void crash(const char *name, const void *private_data, void *arg); + +void +crash(const char *name, const void *private_data, void *arg) +{ + abort(); +} + +/* + * memcontext_crash_client + * + * Ensure that the client process aborts in between memory context + * reporting. + */ +PG_FUNCTION_INFO_V1(memcontext_crash_client); +Datum +memcontext_crash_client(PG_FUNCTION_ARGS) +{ +#ifdef USE_INJECTION_POINTS + InjectionPointAttach("memcontext-client-crash", + "test_memcontext_reporting", "crash", NULL, 0); + +#else + elog(ERROR, + "test is not working as intended when injection points are disabled"); +#endif + PG_RETURN_VOID(); +} + +PG_FUNCTION_INFO_V1(memcontext_detach_client); +Datum +memcontext_detach_client(PG_FUNCTION_ARGS) +{ +#ifdef USE_INJECTION_POINTS + InjectionPointDetach("memcontext-client-crash"); + +#else + elog(ERROR, + "test is not working as intended when injection points are disabled"); +#endif + PG_RETURN_VOID(); +} + +/* + * memcontext_crash_server + * + * Ensure that the server process crashes in between memory context + * reporting. + */ +PG_FUNCTION_INFO_V1(memcontext_crash_server); +Datum +memcontext_crash_server(PG_FUNCTION_ARGS) +{ +#ifdef USE_INJECTION_POINTS + InjectionPointAttach("memcontext-server-crash", + "test_memcontext_reporting", "crash", NULL, 0); + +#else + elog(ERROR, + "test is not working as intended when injection points are disabled"); +#endif + PG_RETURN_VOID(); +} + +/* + * memcontext_detach_server + * + * Detach the injection point which crashes the server + * reporting. + */ +PG_FUNCTION_INFO_V1(memcontext_detach_server); +Datum +memcontext_detach_server(PG_FUNCTION_ARGS) +{ +#ifdef USE_INJECTION_POINTS + InjectionPointDetach("memcontext-server-crash"); + +#else + elog(ERROR, + "test is not working as intended when injection points are disabled"); +#endif + PG_RETURN_VOID(); +} diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control new file mode 100644 index 00000000000..48b501682d5 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control @@ -0,0 +1,4 @@ +comment = 'Test code for memcontext reporting' +default_version = '1.0' +module_pathname = '$libdir/test_memcontext_reporting' +relocatable = true -- 2.39.3 (Apple Git-146) [application/octet-stream] v43-0003-Review-comments.patch (29.1K, ../../[email protected]/4-v43-0003-Review-comments.patch) download | inline diff: From 257b7d87aaa55c415e205afad9ba6ae90e8d6195 Mon Sep 17 00:00:00 2001 From: Daniel Gustafsson <[email protected]> Date: Mon, 8 Dec 2025 10:38:43 +0100 Subject: [PATCH v43 3/3] Review comments --- doc/src/sgml/func/func-admin.sgml | 4 +- src/backend/catalog/system_functions.sql | 14 + src/backend/catalog/system_views.sql | 5 - src/backend/utils/adt/mcxtfuncs.c | 314 +++++++++--------- src/include/utils/memutils.h | 2 +- .../t/001_memcontext_inj.pl | 42 ++- src/tools/pgindent/typedefs.list | 2 +- 7 files changed, 195 insertions(+), 188 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index a5c66837241..3e71ced60a2 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -257,7 +257,7 @@ <indexterm> <primary>pg_get_process_memory_contexts</primary> </indexterm> - <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type>, <parameter>summary</parameter> <type>boolean</type> ) + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type> <optional>,<parameter>summary</parameter> <type>boolean</type> <literal>DEFAULT</literal> <literal>false</literal></optional> ) <returnvalue>setof record</returnvalue> ( <parameter>name</parameter> <type>text</type>, <parameter>ident</parameter> <type>text</type>, @@ -360,7 +360,7 @@ Statistics for contexts on level 2 and below are aggregates of all child contexts' statistics, where <literal>num_agg_contexts</literal> indicate the number aggregated child contexts. When - <parameter>summary</parameter> is <literal>false</literal>, + <parameter>summary</parameter> is <literal>false</literal> (the default), <literal>the num_agg_contexts</literal> value is <literal>1</literal>, indicating that individual statistics are being displayed. </para> diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql index 2d946d6d9e9..7b40bac5f57 100644 --- a/src/backend/catalog/system_functions.sql +++ b/src/backend/catalog/system_functions.sql @@ -657,6 +657,17 @@ LANGUAGE INTERNAL STRICT VOLATILE PARALLEL UNSAFE AS 'pg_replication_origin_session_setup'; +CREATE OR REPLACE FUNCTION + pg_get_process_memory_contexts(IN pid integer, IN summary boolean DEFAULT false, + OUT name text, OUT ident text, OUT type text, OUT level integer, + OUT path integer[], OUT total_bytes bigint, OUT total_nblocks bigint, + OUT free_bytes bigint, OUT free_chunks bigint, OUT used_bytes bigint, + OUT num_agg_contexts integer) +RETURNS SETOF RECORD +LANGUAGE INTERNAL +STRICT VOLATILE PARALLEL UNSAFE +AS 'pg_get_process_memory_contexts'; + -- -- The default permissions for functions mean that anyone can execute them. -- A number of functions shouldn't be executable by just anyone, but rather @@ -782,6 +793,7 @@ REVOKE EXECUTE ON FUNCTION pg_ls_logicalmapdir() FROM PUBLIC; REVOKE EXECUTE ON FUNCTION pg_ls_replslotdir(text) FROM PUBLIC; +REVOKE EXECUTE ON FUNCTION pg_get_process_memory_contexts(integer, boolean) FROM PUBLIC; -- -- We also set up some things as accessible to standard roles. -- @@ -808,6 +820,8 @@ GRANT EXECUTE ON FUNCTION pg_current_logfile() TO pg_monitor; GRANT EXECUTE ON FUNCTION pg_current_logfile(text) TO pg_monitor; +GRANT EXECUTE ON FUNCTION pg_get_process_memory_contexts(integer, boolean) TO pg_read_all_stats; + GRANT pg_read_all_settings TO pg_monitor; GRANT pg_read_all_stats TO pg_monitor; diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql index 17ec512622b..086c4c8fb6f 100644 --- a/src/backend/catalog/system_views.sql +++ b/src/backend/catalog/system_views.sql @@ -692,11 +692,6 @@ GRANT SELECT ON pg_backend_memory_contexts TO pg_read_all_stats; REVOKE EXECUTE ON FUNCTION pg_get_backend_memory_contexts() FROM PUBLIC; GRANT EXECUTE ON FUNCTION pg_get_backend_memory_contexts() TO pg_read_all_stats; -REVOKE EXECUTE ON FUNCTION - pg_get_process_memory_contexts(integer, boolean) FROM PUBLIC; -GRANT EXECUTE ON FUNCTION - pg_get_process_memory_contexts(integer, boolean) TO pg_read_all_stats; - -- Statistics views CREATE VIEW pg_stat_all_tables AS diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index c661eef7ae9..f1b5c3a0887 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -49,8 +49,11 @@ */ #define MAX_MEMORY_CONTEXT_STATS_NUM MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND / (sizeof(MemoryStatsEntry)) -/* Size of dshash key */ -#define CLIENT_KEY_SIZE 32 +/* + * Size of dshash key. The key is a uint32 rendered as a string, 10 chars + * plus space for a NULL terminator can hold all the values. + */ +#define CLIENT_KEY_SIZE (10 + 1) /* Dynamic shared memory state for reporting statistics per context */ typedef struct MemoryStatsEntry @@ -92,8 +95,7 @@ static const dshash_parameters memctx_dsh_params = { }; /* - * These are used for reporting memory context - * statistics of a process. + * These are used for reporting memory context statistics of a process. */ /* Lock to control access to client_keys array */ @@ -103,10 +105,9 @@ static LWLock *client_keys_lock = NULL; static int *client_keys = NULL; /* - * Table to store pointers to dsa memory containing - * memory statistics and other meta data. There is one - * entry per client backend request, keyed by ProcNumber of - * the client obtained from client_keys array above. + * Table to store pointers to DSA memory containing memory statistics and other + * metadata. There is one entry per client backend request, keyed by ProcNumber + * of the client obtained from client_keys array above. */ static dshash_table *MemoryStatsDsHash = NULL; @@ -125,8 +126,6 @@ static void PublishMemoryContext(MemoryStatsEntry *memcxt_info, MemoryContextCounters stat, int num_contexts); static List *compute_context_path(MemoryContext c, HTAB *context_id_lookup); -static void end_memorycontext_reporting(MemoryStatsDSHashEntry *entry, MemoryContext oldcontext, - HTAB *context_id_lookup); /* ---------- * The max bytes for showing identifiers of MemoryContext. @@ -140,15 +139,14 @@ static void end_memorycontext_reporting(MemoryStatsDSHashEntry *entry, MemoryCon #define MEMORY_STATS_MAX_TIMEOUT 5 /* - * MemoryStatsContextId - * Used for storage of transient identifiers for - * pg_get_backend_memory_contexts and the likes. + * MemoryContextId + * Used for storage of transient identifiers for memory context reporting */ -typedef struct MemoryStatsContextId +typedef struct MemoryContextId { MemoryContext context; int context_id; -} MemoryStatsContextId; +} MemoryContextId; /* * int_list_to_array @@ -199,7 +197,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, */ for (MemoryContext cur = context; cur != NULL; cur = cur->parent) { - MemoryStatsContextId *entry; + MemoryContextId *entry; bool found; entry = hash_search(context_id_lookup, &cur, HASH_FIND, &found); @@ -314,7 +312,7 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) HTAB *context_id_lookup; ctl.keysize = sizeof(MemoryContext); - ctl.entrysize = sizeof(MemoryStatsContextId); + ctl.entrysize = sizeof(MemoryContextId); ctl.hcxt = CurrentMemoryContext; context_id_lookup = hash_create("pg_get_backend_memory_contexts", @@ -341,7 +339,7 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) foreach_ptr(MemoryContextData, cur, contexts) { - MemoryStatsContextId *entry; + MemoryContextId *entry; bool found; /* @@ -349,8 +347,8 @@ pg_get_backend_memory_contexts(PG_FUNCTION_ARGS) * PutMemoryContextsStatsTupleStore needs this to populate the "path" * column with the parent context_ids. */ - entry = (MemoryStatsContextId *) hash_search(context_id_lookup, &cur, - HASH_ENTER, &found); + entry = (MemoryContextId *) hash_search(context_id_lookup, &cur, + HASH_ENTER, &found); entry->context_id = context_id++; Assert(!found); @@ -437,10 +435,8 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) * wait for the results and display them. * * By default, only superusers or users with ROLE_PG_READ_ALL_STATS are allowed - * to signal a process to return the memory contexts. This is because allowing - * any users to issue this request at an unbounded rate would cause lots of - * requests to be sent, which can lead to denial of service. Additional roles - * can be permitted with GRANT. + * to signal a process to return the memory contexts. Additional roles can be + * permitted with GRANT. * * On receipt of this signal, a backend or an auxiliary process sets the flag * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() @@ -453,14 +449,14 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) * at the end of the buffer. * * After sending the signal, wait on a condition variable. The publishing - * backend, after copying the data to shared memory, sends signal on that + * backend, after copying the data to shared memory, sends a signal on that * condition variable. There is one condition variable per client process. * Once the condition variable is signalled, check if the latest memory context * information is available and display. * * If the publishing backend does not respond before the condition variable - * times out, which is set to a predefined value MEMORY_STATS_MAX_TIMEOUT, give up - * and return NULL. + * times out, which is set to a predefined value MEMORY_STATS_MAX_TIMEOUT, give + * up and return NULL. */ Datum pg_get_process_memory_contexts(PG_FUNCTION_ARGS) @@ -495,12 +491,8 @@ pg_get_process_memory_contexts(PG_FUNCTION_ARGS) */ if (proc == NULL) { - /* - * This is a warning because we don't want to break loops. - */ ereport(WARNING, - errmsg("PID %d is not a PostgreSQL server process", - pid)); + errmsg("PID %d is not a PostgreSQL server process", pid)); PG_RETURN_NULL(); } @@ -508,7 +500,7 @@ pg_get_process_memory_contexts(PG_FUNCTION_ARGS) /* * Create a DSA to allocate memory for copying memory contexts statistics. - * Allocate the memory in the DSA and send dsa pointer to the server + * Allocate the memory in the DSA and send DSA pointer to the server * process for storing the context statistics. If number of contexts * exceed a predefined limit (1MB), a cumulative total is stored for such * contexts. @@ -521,8 +513,8 @@ pg_get_process_memory_contexts(PG_FUNCTION_ARGS) &found); /* - * The dsa pointers containing statistics for each client are stored in a - * dshash table. In addition to dsa pointer, each entry in this table also + * The DSA pointers containing statistics for each client are stored in a + * dshash table. In addition to DSA pointer, each entry in this table also * contains information about the statistics, condition variable for * signalling between client and the server and miscellaneous data * specific to a request. There is one entry per client request in the @@ -838,9 +830,9 @@ HandleGetMemoryContextInterrupt(void) * output. * * Statistics for all the processes are shared via the same dynamic shared - * area. Individual statistics are tracked independently in - * per-process DSA pointers. These pointers are stored in a dshash table with - * key as requesting clients ProcNumber. + * area. Individual statistics are tracked independently in per-process DSA + * pointers. These pointers are stored in a dshash table with key as requesting + * clients ProcNumber. * * We calculate maximum number of context's statistics that can be displayed * using a pre-determined limit for memory available per process for this @@ -848,11 +840,11 @@ HandleGetMemoryContextInterrupt(void) * context statistics if any are captured as a cumulative total at the end of * individual context's statistics. * - * If summary is true, we capture the level 1 and level 2 contexts - * statistics. For that we traverse the memory context tree recursively in - * depth first search manner to cover all the children of a parent context, to - * be able to display a cumulative total of memory consumption by a parent at - * level 2 and all its children. + * If summary is true, we capture the level 1 and level 2 contexts statistics. + * For that we traverse the memory context tree recursively in depth first + * search manner to cover all the children of a parent context, to be able to + * display a cumulative total of memory consumption by a parent at level 2 and + * all its children. */ void ProcessGetMemoryContextInterrupt(void) @@ -899,7 +891,7 @@ ProcessGetMemoryContextInterrupt(void) * similar to its local backend counterpart. */ ctl.keysize = sizeof(MemoryContext); - ctl.entrysize = sizeof(MemoryStatsContextId); + ctl.entrysize = sizeof(MemoryContextId); ctl.hcxt = CurrentMemoryContext; context_id_lookup = hash_create("pg_get_remote_backend_memory_contexts", @@ -912,7 +904,7 @@ ProcessGetMemoryContextInterrupt(void) /* * If DSA exists, created by another process requesting statistics, attach - * to it. We expect the client process to create required DSA and Dshash + * to it. We expect the client process to create required DSA and DSHash * table. */ if (MemoryStatsDsaArea == NULL) @@ -923,7 +915,6 @@ ProcessGetMemoryContextInterrupt(void) MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", &memctx_dsh_params, &found); - snprintf(key, CLIENT_KEY_SIZE, "%d", clientProcNumber); /* @@ -935,25 +926,24 @@ ProcessGetMemoryContextInterrupt(void) entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); /* - * Entry has been deleted due to client process exit. Make sure that the - * client always deletes the entry after taking required lock or this - * function may end up writing to unallocated memory. + * Check if the entry has been deleted due to calling process exiting, or + * if the caller has timed out waiting for us and have issued a request to + * another backend. + * + * XXX ?: Make sure that the client always deletes the entry after taking + * required lock or this function may end up writing to unallocated + * memory. */ - if (!found) + if (!found || entry->target_server_id != MyProcPid) { entry->stats_written = false; - end_memorycontext_reporting(entry, oldcontext, context_id_lookup); - return; - } - /* - * The client has timed out waiting for us to write statistics and is - * requesting statistics from some other process - */ - if (entry->target_server_id != MyProcPid) - { - entry->stats_written = false; - end_memorycontext_reporting(entry, oldcontext, context_id_lookup); + dshash_release_lock(MemoryStatsDsHash, entry); + + hash_destroy(context_id_lookup); + MemoryContextSwitchTo(oldcontext); + MemoryContextReset(memstats_ctx); + return; } @@ -966,7 +956,7 @@ ProcessGetMemoryContextInterrupt(void) { int cxt_id = 0; List *path = NIL; - MemoryStatsContextId *contextid_entry; + MemoryContextId *contextid_entry; /* Copy TopMemoryContext statistics to DSA */ memset(&stat, 0, sizeof(stat)); @@ -976,9 +966,9 @@ ProcessGetMemoryContextInterrupt(void) PublishMemoryContext(meminfo, cxt_id, TopMemoryContext, path, stat, 1); - contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, - &TopMemoryContext, - HASH_ENTER, &found); + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &TopMemoryContext, + HASH_ENTER, &found); Assert(!found); /* @@ -1001,8 +991,8 @@ ProcessGetMemoryContextInterrupt(void) memset(&grand_totals, 0, sizeof(grand_totals)); cxt_id++; - contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, - &c, HASH_ENTER, &found); + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &c, HASH_ENTER, &found); Assert(!found); contextid_entry->context_id = cxt_id + 1; @@ -1014,119 +1004,111 @@ ProcessGetMemoryContextInterrupt(void) grand_totals, num_contexts); } entry->total_stats = cxt_id + 1; - - entry->stats_written = true; - end_memorycontext_reporting(entry, oldcontext, context_id_lookup); - /* Notify waiting client backend and return */ - ConditionVariableSignal(&entry->memcxt_cv); - return; } - foreach_ptr(MemoryContextData, cur, contexts) + else { - List *path = NIL; - MemoryStatsContextId *contextid_entry; + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + MemoryContextId *contextid_entry; - contextid_entry = (MemoryStatsContextId *) hash_search(context_id_lookup, - &cur, - HASH_ENTER, &found); - Assert(!found); + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &cur, + HASH_ENTER, &found); + Assert(!found); - /* - * context id starts with 1 - */ - contextid_entry->context_id = context_id + 1; + /* + * context id starts with 1 + */ + contextid_entry->context_id = context_id + 1; + + /* + * Figure out the transient context_id of this context and each of + * its ancestors, to compute a path for this context. + */ + path = compute_context_path(cur, context_id_lookup); + + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + /* Account for saving one statistics slot for cumulative reporting */ + if (context_id < (MAX_MEMORY_CONTEXT_STATS_NUM - 1)) + { + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1); + } + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].totalspace += stat.totalspace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].nblocks += stat.nblocks; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freespace += stat.freespace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit per process is reached, write aggregate of the + * remaining statistics. + * + * We can store contexts from 0 to max_stats - 1. When context_id + * is greater than max_stats, we stop reporting individual + * statistics when context_id equals max_stats - 2. As we use + * max_stats - 1 array slot for reporting cumulative statistics or + * "Remaining Totals". + */ + if (context_id == (MAX_MEMORY_CONTEXT_STATS_NUM - 2)) + { + int namelen = strlen("Remaining Totals"); + + num_individual_stats = context_id + 1; + strlcpy(meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].name, + "Remaining Totals", namelen + 1); + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].ident[0] = '\0'; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].path[0] = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].type = 0; + } + context_id++; + + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + contexts = lappend(contexts, c); + } /* - * Figure out the transient context_id of this context and each of its - * ancestors, to compute a path for this context. + * Check if there are aggregated statistics or not in the result set. + * Statistics are individually reported when context_id <= max_stats, + * only if context_id > max_stats will there be aggregates. */ - path = compute_context_path(cur, context_id_lookup); - - /* Examine the context stats */ - memset(&stat, 0, sizeof(stat)); - (*cur->methods->stats) (cur, NULL, NULL, &stat, true); - - /* Account for saving one statistics slot for cumulative reporting */ - if (context_id < (MAX_MEMORY_CONTEXT_STATS_NUM - 1)) + if (context_id <= MAX_MEMORY_CONTEXT_STATS_NUM) { - /* Copy statistics to DSA memory */ - PublishMemoryContext(meminfo, context_id, cur, path, stat, 1); - } - else - { - meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].totalspace += stat.totalspace; - meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].nblocks += stat.nblocks; - meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freespace += stat.freespace; - meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freechunks += stat.freechunks; + entry->total_stats = context_id; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = 1; } /* - * DSA max limit per process is reached, write aggregate of the - * remaining statistics. - * - * We can store contexts from 0 to max_stats - 1. When context_id is - * greater than max_stats, we stop reporting individual statistics - * when context_id equals max_stats - 2. As we use max_stats - 1 array - * slot for reporting cumulative statistics or "Remaining Totals". + * The number of contexts exceeded the space available, so report the + * number of aggregated memory contexts */ - if (context_id == (MAX_MEMORY_CONTEXT_STATS_NUM - 2)) + else { - int namelen = strlen("Remaining Totals"); - - num_individual_stats = context_id + 1; - strlcpy(meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].name, - "Remaining Totals", namelen + 1); - meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].ident[0] = '\0'; - meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].path[0] = 0; - meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].type = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = + context_id - num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for + * cumulative statistics. + */ + entry->total_stats = num_individual_stats + 1; } - context_id++; - - for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) - contexts = lappend(contexts, c); } - /* - * Statistics are not aggregated, i.e individual statistics reported when - * context_id <= max_stats. - */ - if (context_id <= MAX_MEMORY_CONTEXT_STATS_NUM) - { - entry->total_stats = context_id; - meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = 1; - } - /* Report number of aggregated memory contexts */ - else - { - meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = context_id - - num_individual_stats; - - /* - * Total stats equals num_individual_stats + 1 record for cumulative - * statistics. - */ - entry->total_stats = num_individual_stats + 1; - } entry->stats_written = true; - end_memorycontext_reporting(entry, oldcontext, context_id_lookup); - /* Notify waiting client backend and return */ - ConditionVariableSignal(&entry->memcxt_cv); -} - -/* - * Clean up before exit from ProcessGetMemoryContextInterrupt - */ -static void -end_memorycontext_reporting(MemoryStatsDSHashEntry *entry, - MemoryContext oldcontext, HTAB *context_id_lookup) -{ - MemoryContext curr_ctx = CurrentMemoryContext; - dshash_release_lock(MemoryStatsDsHash, entry); - hash_destroy(context_id_lookup); + MemoryContextSwitchTo(oldcontext); - MemoryContextReset(curr_ctx); + MemoryContextReset(memstats_ctx); + /* Notify waiting client backend and return */ + ConditionVariableSignal(&entry->memcxt_cv); } /* @@ -1144,7 +1126,7 @@ compute_context_path(MemoryContext c, HTAB *context_id_lookup) for (cur_context = c; cur_context != NULL; cur_context = cur_context->parent) { - MemoryStatsContextId *cur_entry; + MemoryContextId *cur_entry; cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); @@ -1167,8 +1149,8 @@ PublishMemoryContext(MemoryStatsEntry *memcxt_info, int curr_id, MemoryContext context, List *path, MemoryContextCounters stat, int num_contexts) { - const char *ident = context->ident; - const char *name = context->name; + char *ident = unconstify(char *, context->ident); + char *name = unconstify(char *, context->name); /* * To be consistent with logging output, we label dynahash contexts with @@ -1176,7 +1158,7 @@ PublishMemoryContext(MemoryStatsEntry *memcxt_info, int curr_id, */ if (context->ident && strncmp(context->name, "dynahash", 8) == 0) { - name = context->ident; + name = unconstify(char *, context->ident); ident = NULL; } @@ -1184,7 +1166,7 @@ PublishMemoryContext(MemoryStatsEntry *memcxt_info, int curr_id, { int namelen = strlen(name); - if (strlen(name) >= MEMORY_CONTEXT_NAME_SHMEM_SIZE) + if (namelen >= MEMORY_CONTEXT_NAME_SHMEM_SIZE) namelen = pg_mbcliplen(name, namelen, MEMORY_CONTEXT_NAME_SHMEM_SIZE - 1); @@ -1212,7 +1194,7 @@ PublishMemoryContext(MemoryStatsEntry *memcxt_info, int curr_id, else memcxt_info[curr_id].ident[0] = '\0'; - /* Allocate DSA memory for storing path information */ + /* Store the path */ if (path == NIL) memcxt_info[curr_id].path[0] = 0; else diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 4296667cbf0..617de0ebf91 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -18,7 +18,6 @@ #define MEMUTILS_H #include "nodes/memnodes.h" -#include "utils/dsa.h" /* * MaxAllocSize, MaxAllocHugeSize @@ -48,6 +47,7 @@ #define AllocHugeSizeIsValid(size) ((Size) (size) <= MaxAllocHugeSize) + /* * Standard top-level memory contexts. * diff --git a/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl index 69d8489eb37..8fa12d1f693 100644 --- a/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl +++ b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl @@ -11,7 +11,7 @@ use Test::More; if ($ENV{enable_injection_points} ne 'yes') { - plan skip_all => 'Injection points not supported by this build'; + plan skip_all => 'Injection points not supported by this build'; } my $psql_err; # Create and start a cluster with one node @@ -21,8 +21,8 @@ $node->init(allows_streaming => 1); # and log_statement is dialled down since it otherwise will generate enormous # amounts of logging. Page verification failures are still logged. $node->append_conf( - 'postgresql.conf', - qq[ + 'postgresql.conf', + qq[ max_connections = 100 log_statement = none ]); @@ -30,29 +30,45 @@ $node->start; $node->safe_psql('postgres', 'CREATE EXTENSION test_memcontext_reporting;'); $node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); # Attaching to a client process injection point that throws an error -$node->safe_psql('postgres', "select injection_points_attach('memcontext-client-crash', 'error');"); +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-client-crash', 'error');"); -my $pid = $node->safe_psql('postgres', "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); +my $pid = $node->safe_psql('postgres', + "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); print "PID"; print $pid; #Client should have thrown error -$node->psql('postgres', qq(select pg_get_process_memory_contexts($pid, true);), stderr => \$psql_err); -like ( $psql_err, qr/error triggered for injection point memcontext-client-crash/); +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); +like($psql_err, + qr/error triggered for injection point memcontext-client-crash/); #Query the same process after detaching the injection point, using some other client and it should succeed. -$node->safe_psql('postgres', "select injection_points_detach('memcontext-client-crash');"); -my $topcontext_name = $node->safe_psql('postgres', "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';"); +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-client-crash');"); +my $topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); ok($topcontext_name = 'TopMemoryContext'); # Attaching to a target process injection point that throws an error -$node->safe_psql('postgres', "select injection_points_attach('memcontext-server-crash', 'error');"); +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-server-crash', 'error');"); #Server should have thrown error -$node->psql('postgres', qq(select pg_get_process_memory_contexts($pid, true);), stderr => \$psql_err); +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); #Query the same process after detaching the injection point, using some other client and it should succeed. -$node->safe_psql('postgres', "select injection_points_detach('memcontext-server-crash');"); -$topcontext_name = $node->safe_psql('postgres', "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';"); +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-crash');"); +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); ok($topcontext_name = 'TopMemoryContext'); done_testing(); diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index aa898f025c0..5e3122a468b 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1686,9 +1686,9 @@ MemoryContextCallback MemoryContextCallbackFunction MemoryContextCounters MemoryContextData +MemoryContextId MemoryContextMethodID MemoryContextMethods -MemoryStatsContextId MemoryStatsEntry MemoryStatsDSHashEntry MemoryStatsPrintFunc -- 2.39.3 (Apple Git-146) ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-12-12 09:46 Rahila Syed <[email protected]> parent: torikoshia <[email protected]> 0 siblings, 0 replies; 52+ messages in thread From: Rahila Syed @ 2025-12-12 09:46 UTC (permalink / raw) To: torikoshia <[email protected]>; +Cc: Daniel Gustafsson <[email protected]>; pgsql-hackers Hi, Sorry for the late response. Thank you for your reviewing and testing the patch. On Mon, Dec 8, 2025 at 6:56 AM torikoshia <[email protected]> wrote: > On 2025-11-28 18:22, Rahila Syed wrote: > > Hi, > > > I'm attaching the updated patches, which primarily include cleanup and > > have been rebased > > following the CFbot report. > > Thanks for updating the patch! > > I observed an assertion failure when forcing a timeout as follows: > > Good catch. This assertion is no longer valid because of recent updates that reset the client_keys slot for a request when the client exits with a timeout. To address this, I’ve replaced the assertion with a check for -1 and now return from the function in that case. It might be good to also document in func-admin.sgml that the function > times out after 5 seconds when the target backend does not respond, and > that in such a case NULLs are returned. > > Added this. From the comment, it sounded to me as if the client executing > pg_get_process_memory_contexts() might not create the DSA in some cases. > Is it correct to assume that such a situation can happen? > In [1], as a response to concerns about using DSA inside a CFI handler, > you wrote that “all the dynamic shared memory needed to store the > statistics is created and deleted in the client function”. > So I understood that it would never create the DSA inside the CFI > handler. > If that understanding is correct, perhaps the comment should be reworded > to make that clear. > > Yes, your understanding is correct. I reworded the comment accordingly. > + context_id_lookup = > hash_create("pg_get_remote_backend_memory_contexts", > > This appears to use the old function name. Should this be updated to > "pg_get_process_memory_contexts" instead? > > Modified this. I will post the updated patch in response to Daniel's message that follows your email. Thank you, Rahila Syed ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-12-12 11:04 Rahila Syed <[email protected]> parent: Daniel Gustafsson <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Rahila Syed @ 2025-12-12 11:04 UTC (permalink / raw) To: Daniel Gustafsson <[email protected]>; +Cc: pgsql-hackers; torikoshia <[email protected]> Hi Daniel, > > Thanks for the patch, below are a few comments and suggestions. As I was > reviewing I tweaked the below and have attached the comments as changes in > 0003. > Thank you for the improvements. All your changes look good to me. I have incorporated those in the v44 patch. > + * Entry has been deleted due to client process exit. Make sure that > the > + * client always deletes the entry after taking required lock or this > + * function may end up writing to unallocated memory. > Can you explain this a bit further, I'm not sure I get it. The code goes > on to > release a lock immediately and then destroys the hash. Who is responsible > for > destroying the entry? > This just points to the general requirements of taking a lock before writing to a shared variable. This serves as a warning to other processes not to delete the entry without taking a lock, since we are about to write to the entry. > == In ProcessGetMemoryContextInterrupt() > I'm not a fan of having to exit's from the function doing duplicative > cleanups, > in the attached I've wrapped them in a conditional to just have one exit > path. > What do you think about that? > I agree with your approach. It certainly makes the code more concise and easier to read. > == In PublishMemoryContext() > + /* Allocate DSA memory for storing path information */ > This comment is no longer accurate is it? The DSA has already been > allocated > at this point. > > Yes, it is not valid anymore. Fixed accordingly. Apart from this, I cleaned up the test module by removing unnecessary sql functions, added some more injection points based tests and a few minor tweaks. Please find attached updated and rebased patches. Thank you, Rahila Syed Attachments: [application/octet-stream] v44-0001-Add-function-to-report-memory-context-statistics.patch (57.7K, ../../CAH2L28vK5T_+u4hOvxm+z_9povNPQWMg=ZxtdD00hngY2QibWA@mail.gmail.com/3-v44-0001-Add-function-to-report-memory-context-statistics.patch) download | inline diff: From 4b20c9f6da3c9d92d3af85ddbb761843eb3d04b1 Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Thu, 27 Nov 2025 14:39:43 +0530 Subject: [PATCH 1/2] Add function to report memory context statistics This adds a function for retrieving memory context statistics and information from backends as well as auxiliary processes. The intended usecase is cluster debugging when under memory pressure or unanticipated memory usage characteristics. When calling the function it sends a signal to the specified process to submit statistics regarding its memory contexts into dynamic shared memory. Each memory context is returned in detail, followed by a cumulative total in case the number of contexts exceed the max allocated amount of shared memory. Each process is limited to use at most 1Mb memory for this. A summary can also be explicitly requested by the user, this will return the TopMemoryContext and a cumulative total of all lower contexts. In order to not block on busy processes, we have hardcoded the number of seconds during which to retry before timing out. In the case where no statistics are published within the set timeout, NULL is returned --- doc/src/sgml/func/func-admin.sgml | 159 +++ src/backend/catalog/system_functions.sql | 14 + src/backend/postmaster/autovacuum.c | 4 + src/backend/postmaster/checkpointer.c | 4 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 4 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 4 + src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/storage/lmgr/proc.c | 1 + src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 2 + src/backend/utils/adt/mcxtfuncs.c | 1005 ++++++++++++++++- src/backend/utils/init/globals.c | 1 + src/backend/utils/init/postinit.c | 7 + src/backend/utils/mmgr/mcxt.c | 31 + src/include/catalog/pg_proc.dat | 10 + src/include/miscadmin.h | 1 + src/include/storage/lwlocklist.h | 1 + src/include/storage/procsignal.h | 1 + src/include/utils/memutils.h | 8 +- src/test/regress/expected/sysviews.out | 19 + src/test/regress/sql/sysviews.sql | 18 + src/tools/pgindent/typedefs.list | 2 + 25 files changed, 1292 insertions(+), 21 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 1b465bc8ba7..3e849a132e9 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -251,6 +251,132 @@ <literal>false</literal> is returned. </para></entry> </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_get_process_memory_contexts</primary> + </indexterm> + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type> <optional>,<parameter>summary</parameter> <type>boolean</type> <literal>DEFAULT</literal> <literal>false</literal></optional> ) + <returnvalue>setof record</returnvalue> + ( <parameter>name</parameter> <type>text</type>, + <parameter>ident</parameter> <type>text</type>, + <parameter>type</parameter> <type>text</type>, + <parameter>level</parameter> <type>integer</type>, + <parameter>path</parameter> <type>integer[]</type>, + <parameter>total_bytes</parameter> <type>bigint</type>, + <parameter>total_nblocks</parameter> <type>bigint</type>, + <parameter>free_bytes</parameter> <type>bigint</type>, + <parameter>free_chunks</parameter> <type>bigint</type>, + <parameter>used_bytes</parameter> <type>bigint</type>, + <parameter>num_agg_contexts</parameter> <type>integer</type> ) + </para> + <para> + This function handles requests to display the memory contexts of a + <productname>PostgreSQL</productname> process with the specified + process ID. The function can be used to send requests to backends as + well as <glossterm linkend="glossary-auxiliary-proc">auxiliary processes</glossterm>. + If the process does not respond with memory contexts statistics in 5 seconds, + function returns NULL. + </para> + <para> + The returned record contains extended statistics per each memory + context: + <itemizedlist spacing="compact"> + <listitem> + <para> + <parameter>name</parameter> - The name of the memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>ident</parameter> - Memory context ID (if any). + </para> + </listitem> + <listitem> + <para> + <parameter>type</parameter> - The type of memory context, possible + values are: AllocSet, Generation, Slab and Bump. + </para> + </listitem> + <listitem> + <para> + <parameter>level</parameter> - The level in the tree of the current + memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>path</parameter> - Memory contexts are organized in a + tree model with TopMemoryContext as the root, and all other memory + contexts as nodes in the tree. The <parameter>path</parameter> + displays the path from the root to the current memory context. The + path is limited to 100 children per node, which each node limited + to a max depth of 100, to preserve memory during reporting. The + printed path will also be limited to 100 nodes counting from the + TopMemoryContext. + </para> + </listitem> + <listitem> + <para> + <parameter>total_bytes</parameter> - The total number of bytes + allocated to this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_nblocks</parameter> - The total number of blocks + used for the allocated memory. + </para> + </listitem> + <listitem> + <para> + <parameter>free_bytes</parameter> - The amount of free memory in + this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>free_chunks</parameter> - The number of chunks that + <parameter>free_bytes</parameter> corresponds to. + </para> + </listitem> + <listitem> + <para> + <parameter>used_bytes</parameter> - The total number of bytes + currently occupied. + </para> + </listitem> + <listitem> + <para> + <parameter>num_agg_contexts</parameter> - The number of memory + contexts aggregated in the displayed statistics. + </para> + </listitem> + </itemizedlist> + </para> + <para> + When <parameter>summary</parameter> is <literal>true</literal>, statistics + for memory contexts at levels 1 and 2 are displayed, with level 1 + representing the root node (i.e., <literal>TopMemoryContext</literal>). + Statistics for contexts on level 2 and below are aggregates of all + child contexts' statistics, where <literal>num_agg_contexts</literal> + indicate the number aggregated child contexts. When + <parameter>summary</parameter> is <literal>false</literal> (the default), + <literal>the num_agg_contexts</literal> value is <literal>1</literal>, + indicating that individual statistics are being displayed. + </para> + <para> + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context + statistics are aggregated and a cumulative total is displayed. The + <literal>num_agg_contexts</literal> column indicates the number of + contexts aggregated in the displayed statistics. When + <literal>num_agg_contexts</literal> is <literal>1</literal> it means + that the context statistics are displayed separately. + </para></entry> + </row> </tbody> </tgroup> </table> @@ -302,6 +428,39 @@ LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 because it may generate a large number of log messages. </para> + <para> + <function>pg_get_process_memory_contexts</function> can be used to request + memory contexts statistics of any <productname>PostgreSQL</productname> + process. For example: +<programlisting> +postgres=# SELECT * FROM pg_get_process_memory_contexts( + (SELECT pid FROM pg_stat_activity + WHERE backend_type = 'checkpointer'), + false) LIMIT 1; +-[ RECORD 1 ]----+------------------------------ +name | TopMemoryContext +ident | +type | AllocSet +level | 1 +path | {1} +total_bytes | 90304 +total_nblocks | 3 +free_bytes | 2880 +free_chunks | 1 +used_bytes | 87424 +num_agg_contexts | 1 +</programlisting> + <note> + <para> + While <function>pg_get_process_memory_contexts</function> can be used to + query memory contexts of the local backend, + <structname>pg_backend_memory_contexts</structname> + (see <xref linkend="view-pg-backend-memory-contexts"/> for more details) + will be less resource intensive when only the local backend is of interest. + </para> + </note> + </para> + </sect2> <sect2 id="functions-admin-backup"> diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql index 2d946d6d9e9..7b40bac5f57 100644 --- a/src/backend/catalog/system_functions.sql +++ b/src/backend/catalog/system_functions.sql @@ -657,6 +657,17 @@ LANGUAGE INTERNAL STRICT VOLATILE PARALLEL UNSAFE AS 'pg_replication_origin_session_setup'; +CREATE OR REPLACE FUNCTION + pg_get_process_memory_contexts(IN pid integer, IN summary boolean DEFAULT false, + OUT name text, OUT ident text, OUT type text, OUT level integer, + OUT path integer[], OUT total_bytes bigint, OUT total_nblocks bigint, + OUT free_bytes bigint, OUT free_chunks bigint, OUT used_bytes bigint, + OUT num_agg_contexts integer) +RETURNS SETOF RECORD +LANGUAGE INTERNAL +STRICT VOLATILE PARALLEL UNSAFE +AS 'pg_get_process_memory_contexts'; + -- -- The default permissions for functions mean that anyone can execute them. -- A number of functions shouldn't be executable by just anyone, but rather @@ -782,6 +793,7 @@ REVOKE EXECUTE ON FUNCTION pg_ls_logicalmapdir() FROM PUBLIC; REVOKE EXECUTE ON FUNCTION pg_ls_replslotdir(text) FROM PUBLIC; +REVOKE EXECUTE ON FUNCTION pg_get_process_memory_contexts(integer, boolean) FROM PUBLIC; -- -- We also set up some things as accessible to standard roles. -- @@ -808,6 +820,8 @@ GRANT EXECUTE ON FUNCTION pg_current_logfile() TO pg_monitor; GRANT EXECUTE ON FUNCTION pg_current_logfile(text) TO pg_monitor; +GRANT EXECUTE ON FUNCTION pg_get_process_memory_contexts(integer, boolean) TO pg_read_all_stats; + GRANT pg_read_all_settings TO pg_monitor; GRANT pg_read_all_stats TO pg_monitor; diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 1bd3924e35e..baba657904b 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -791,6 +791,10 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index 7f8cf1fa2ec..749e68553b9 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -679,6 +679,10 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index ba63b84dfc5..29454b8bf8a 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -48,6 +48,10 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index 3a65d841725..b89617d78db 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -871,6 +871,10 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index 27e86cf393f..7149a67fcbc 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -192,6 +192,10 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index e7e4d652f97..eb86648f7b7 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -879,6 +879,10 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index b23d0c19360..a5ed58a18c5 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -52,6 +52,7 @@ #include "storage/sinvaladt.h" #include "utils/guc.h" #include "utils/injection_point.h" +#include "utils/memutils.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -140,6 +141,7 @@ CalculateShmemSize(void) size = add_size(size, SlotSyncShmemSize()); size = add_size(size, AioShmemSize()); size = add_size(size, WaitLSNShmemSize()); + size = add_size(size, MemoryContextKeysShmemSize() + sizeof(LWLockPadded)); /* include additional requested shmem from preload libraries */ size = add_size(size, total_addin_request); @@ -328,6 +330,7 @@ CreateOrAttachShmemStructs(void) InjectionPointShmemInit(); AioShmemInit(); WaitLSNShmemInit(); + MemoryContextKeysShmemInit(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index 087821311cc..8963285cc12 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -691,6 +691,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_GET_MEMORY_CONTEXT)) + HandleGetMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index ebc3f4ca457..27b3b51cf2d 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -51,6 +51,7 @@ #include "storage/procsignal.h" #include "storage/spin.h" #include "storage/standby.h" +#include "utils/memutils.h" #include "utils/timeout.h" #include "utils/timestamp.h" diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 7dd75a490aa..e726f40dfbb 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3539,6 +3539,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index f39830dbb34..3889228b1ed 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -162,6 +162,7 @@ WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." +MEM_CXT_PUBLISH "Waiting for a process to publish memory information." ABI_compatibility: @@ -405,6 +406,7 @@ SubtransSLRU "Waiting to access the sub-transaction SLRU cache." XactSLRU "Waiting to access the transaction status SLRU cache." ParallelVacuumDSA "Waiting for parallel vacuum dynamic shared memory allocation." AioUringCompletion "Waiting for another process to complete IO via io_uring." +MemoryContextReportingKeys "Waiting for another process to complete reading or writing the memory reporting keys." # No "ABI_compatibility" region here as WaitEventLWLock has its own C code. diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index 46dfb3dd133..c3ec83bd1ed 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -17,22 +17,130 @@ #include "funcapi.h" #include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "storage/dsm_registry.h" #include "storage/proc.h" #include "storage/procarray.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/injection_point.h" +#include "utils/memutils.h" +#include "utils/wait_event_types.h" + +/* + * Memory Context reporting size limits. + */ + +/* Max length of context name and ident, to keep it consistent + * with ProcessLogMemoryContext() + */ +#define MEMORY_CONTEXT_IDENT_SHMEM_SIZE 100 +#define MEMORY_CONTEXT_NAME_SHMEM_SIZE 100 + +/* Maximum size (in bytes) of DSA area per process */ +#define MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND ((size_t) (1 * 1024 * 1024)) + +/* + * Maximum number of memory context statistics is calculated by dividing + * max memory allocated per backend with maximum size per context statistics. + * The identifier and name are statically allocated arrays of size 100 bytes. + * The path depth is limited to 100 like for memory context logging. + */ +#define MAX_MEMORY_CONTEXT_STATS_NUM MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND / (sizeof(MemoryStatsEntry)) + +/* + * Size of dshash key. The key is a uint32 rendered as a string, 10 chars + * plus space for a NULL terminator can hold all the values. + */ +#define CLIENT_KEY_SIZE (10 + 1) + +/* Dynamic shared memory state for reporting statistics per context */ +typedef struct MemoryStatsEntry +{ + char name[MEMORY_CONTEXT_NAME_SHMEM_SIZE]; + char ident[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + int path[100]; + NodeTag type; + int path_length; + int levels; + int64 totalspace; + int64 nblocks; + int64 freespace; + int64 freechunks; + int num_agg_stats; +} MemoryStatsEntry; + +/* + * Per backend dynamic shared hash entry for memory context statistics + * reporting. + */ +typedef struct MemoryStatsDSHashEntry +{ + char key[64]; + ConditionVariable memcxt_cv; + bool stats_written; + int target_server_id; + int total_stats; + bool summary; + dsa_pointer memstats_dsa_pointer; +} MemoryStatsDSHashEntry; + +static const dshash_parameters memctx_dsh_params = { + offsetof(MemoryStatsDSHashEntry, memcxt_cv), + sizeof(MemoryStatsDSHashEntry), + dshash_strcmp, + dshash_strhash, + dshash_strcpy +}; + +/* + * These are used for reporting memory context statistics of a process. + */ + +/* Lock to control access to client_keys array */ +static LWLock *client_keys_lock = NULL; + +/* Array to store the keys of MemoryStatsDsHash */ +static int *client_keys = NULL; + +/* + * Table to store pointers to DSA memory containing memory statistics and other + * metadata. There is one entry per client backend request, keyed by ProcNumber + * of the client obtained from client_keys array above. + */ +static dshash_table *MemoryStatsDsHash = NULL; + +/* + * Dsa area which stores the actual memory context + * statistics. + */ +static dsa_area *MemoryStatsDsaArea = NULL; + +static void memstats_dsa_cleanup(char *key); +static void memstats_client_key_reset(int ProcNumber); +static const char *ContextTypeToString(NodeTag type); +static void PublishMemoryContext(MemoryStatsEntry *memcxt_info, + int curr_id, MemoryContext context, + List *path, + MemoryContextCounters stat, + int num_contexts); +static List *compute_context_path(MemoryContext c, HTAB *context_id_lookup); /* ---------- * The max bytes for showing identifiers of MemoryContext. + * This is used by pg_get_backend_memory_context - view used for local backend. * ---------- */ #define MEMORY_CONTEXT_IDENT_DISPLAY_SIZE 1024 +#define MAX_PATH_DISPLAY_LENGTH 100 +/* Timeout in seconds */ +#define MEMORY_STATS_MAX_TIMEOUT 5 + /* * MemoryContextId - * Used for storage of transient identifiers for - * pg_get_backend_memory_contexts. + * Used for storage of transient identifiers for memory context reporting */ typedef struct MemoryContextId { @@ -143,24 +251,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, else nulls[1] = true; - switch (context->type) - { - case T_AllocSetContext: - type = "AllocSet"; - break; - case T_GenerationContext: - type = "Generation"; - break; - case T_SlabContext: - type = "Slab"; - break; - case T_BumpContext: - type = "Bump"; - break; - default: - type = "???"; - break; - } + type = ContextTypeToString(context->type); values[2] = CStringGetTextDatum(type); values[3] = Int32GetDatum(list_length(path)); /* level */ @@ -175,6 +266,38 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, list_free(path); } +/* + * ContextTypeToString + * Returns a textual representation of a context type + * + * This should cover the same types as MemoryContextIsValid. + */ +const char * +ContextTypeToString(NodeTag type) +{ + const char *context_type; + + switch (type) + { + case T_AllocSetContext: + context_type = "AllocSet"; + break; + case T_GenerationContext: + context_type = "Generation"; + break; + case T_SlabContext: + context_type = "Slab"; + break; + case T_BumpContext: + context_type = "Bump"; + break; + default: + context_type = "???"; + break; + } + return context_type; +} + /* * pg_get_backend_memory_contexts * SQL SRF showing backend memory context. @@ -305,3 +428,845 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } + +/* + * pg_get_process_memory_contexts + * Signal a backend or an auxiliary process to send its memory contexts, + * wait for the results and display them. + * + * By default, only superusers or users with ROLE_PG_READ_ALL_STATS are allowed + * to signal a process to return the memory contexts. Additional roles can be + * permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to copy the memory context details + * to a dynamic shared memory space. + * + * We have defined a limit on DSA memory that could be allocated per process - + * if the process has more memory contexts than what can fit in the allocated + * size, the excess contexts are summarized and represented as cumulative total + * at the end of the buffer. + * + * After sending the signal, wait on a condition variable. The publishing + * backend, after copying the data to shared memory, sends a signal on that + * condition variable. There is one condition variable per client process. + * Once the condition variable is signalled, check if the latest memory context + * information is available and display. + * + * If the publishing backend does not respond before the condition variable + * times out, which is set to a predefined value MEMORY_STATS_MAX_TIMEOUT, give + * up and return NULL. + */ +Datum +pg_get_process_memory_contexts(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + bool summary = PG_GETARG_BOOL(1); + PGPROC *proc; + ProcNumber procNumber; + bool proc_is_aux = false; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + MemoryStatsEntry *memcxt_info; + MemoryStatsDSHashEntry *entry; + bool found; + char key[CLIENT_KEY_SIZE]; + TimestampTz start_timestamp; + + /* + * See if the process with given pid is a backend or an auxiliary process + * and remember the type for when we requery the process later. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + { + proc = AuxiliaryPidGetProc(pid); + proc_is_aux = true; + } + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; this is however not a problem and leave with a WARNING. + * See comment in pg_log_backend_memory_contexts for a discussion on this. + */ + if (proc == NULL) + { + ereport(WARNING, + errmsg("PID %d is not a PostgreSQL server process", pid)); + PG_RETURN_NULL(); + } + + procNumber = GetNumberFromPGProc(proc); + + /* + * Check if the server process slot is not empty and exit early Non-empty + * slot means some other client backend is requesting the statistics from + * the same server process. + */ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + if (client_keys[procNumber] != -1) + { + LWLockRelease(client_keys_lock); + ereport(WARNING, + errmsg("server process %d is processing previous request", + pid)); + PG_RETURN_NULL(); + } + LWLockRelease(client_keys_lock); + + /* + * Create a DSA to allocate memory for copying memory contexts statistics. + * Allocate the memory in the DSA and send DSA pointer to the server + * process for storing the context statistics. If number of contexts + * exceed a predefined limit (1MB), a cumulative total is stored for such + * contexts. + * + * The DSA is created once for the lifetime of the server, and only + * attached in subsequent calls. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); + + /* + * The DSA pointers containing statistics for each client are stored in a + * dshash table. In addition to DSA pointer, each entry in this table also + * contains information about the statistics, condition variable for + * signalling between client and the server and miscellaneous data + * specific to a request. There is one entry per client request in the + * hash table. + */ + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); + + snprintf(key, sizeof(key), "%d", MyProcNumber); + + /* + * Insert an entry for this client in DSHASH table the first time this + * function is called. This entry is deleted when the process exits in + * before_shmem_exit call. + * + * dshash_find_or_insert locks the entry to prevent the publisher from + * reading before client has updated the entry. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + if (!found) + { + entry->stats_written = false; + ConditionVariableInit(&entry->memcxt_cv); + } + + /* + * Allocate 1MB of memory for the backend to publish its statistics on + * every call to this function. The memory is freed at the end of the + * function. + */ + entry->memstats_dsa_pointer = + dsa_allocate0(MemoryStatsDsaArea, MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND); + + /* + * Specify whether a summary of statistics is requested, before signalling + * the server. + */ + entry->summary = summary; + + /* + * Indicate which server process statistics are being requested from. If + * this client times out before the last requested process can publish its + * statistics, it may send a new request to another server process. Since + * the previous server was notified, it might attempt to read the same + * client entry and respond incorrectly with its statistics. By storing + * the server ID in the client entry, we prevent any previously signalled + * server process from writing its statistics in the space meant for the + * newly requested process. + */ + entry->target_server_id = pid; + dshash_release_lock(MemoryStatsDsHash, entry); + + /* + * Check if the publishing process slot is empty and store this clients + * key i.e its procNumber. This informs the publishing process that it is + * supposed to write statistics in the hash entry corresponding to this + * client. + */ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + if (client_keys[procNumber] == -1) + client_keys[procNumber] = MyProcNumber; + else + { + LWLockRelease(client_keys_lock); + ereport(WARNING, + errmsg("server process %d is processing previous request", + pid)); + PG_RETURN_NULL(); + } + LWLockRelease(client_keys_lock); + + /* + * Send a signal to a PostgreSQL process, informing it we want it to + * produce information about its memory contexts. + */ + if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, procNumber) < 0) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ereport(WARNING, + errmsg("could not send signal to process %d: %m", + pid)); + PG_RETURN_NULL(); + } + start_timestamp = GetCurrentTimestamp(); + + while (1) + { + long elapsed_time; + + INJECTION_POINT("memcontext-client-injection", NULL); + + elapsed_time = TimestampDifferenceMilliseconds(start_timestamp, + GetCurrentTimestamp()); + /* Return if we have already exceeded the timeout */ + if (elapsed_time >= MEMORY_STATS_MAX_TIMEOUT * 1000) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + PG_RETURN_NULL(); + } + + /* + * Recheck the state of the backend before sleeping on the condition + * variable to ensure the process is still alive. Only check the + * relevant process type based on the earlier PID check. + */ + if (proc_is_aux) + proc = AuxiliaryPidGetProc(pid); + else + proc = BackendPidGetProc(pid); + + /* + * The target server process ending during memory context processing + * is not an error. + */ + if (proc == NULL) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + ereport(WARNING, + errmsg("PID %d is no longer a PostgreSQL server process", + pid)); + PG_RETURN_NULL(); + } + + /* + * Wait for MEMORY_STATS_MAX_TIMEOUT. If no statistics are available + * within the allowed time then return NULL. The timer is defined in + * milliseconds since that's what the condition variable sleep uses. + */ + if (ConditionVariableTimedSleep(&entry->memcxt_cv, + (MEMORY_STATS_MAX_TIMEOUT * 1000), + WAIT_EVENT_MEM_CXT_PUBLISH)) + { + /* Timeout has expired, return NULL */ + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + PG_RETURN_NULL(); + } + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + Assert(found); + + memcxt_info = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + /* + * We expect to come out of sleep when the requested process has + * finished publishing the statistics, verified using a boolean + * stats_written. + * + * Make sure that the statistics are actually written by checking that + * the name of the context is not NULL. This is done to ensure that + * the subsequent waits for statistics do not return spuriously if the + * previous call to the function ended in error and thus could not + * clear the stats_written flag. + */ + if (entry->stats_written && memcxt_info[0].name[0] != '\0') + break; + + dshash_release_lock(MemoryStatsDsHash, entry); + + } + + InitMaterializedSRF(fcinfo, 0); + + /* + * Backend has finished publishing the stats, project them. + */ +#define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 11 + for (int i = 0; i < entry->total_stats; i++) + { + ArrayType *path_array; + int path_length; + Datum values[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + bool nulls[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + Datum *path_datum = NULL; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + Assert(memcxt_info[i].name[0] != '\0'); + values[0] = CStringGetTextDatum(memcxt_info[i].name); + + if (memcxt_info[i].ident[0] != '\0') + values[1] = CStringGetTextDatum(memcxt_info[i].ident); + else + nulls[1] = true; + + values[2] = CStringGetTextDatum(ContextTypeToString(memcxt_info[i].type)); + values[3] = Int32GetDatum(memcxt_info[i].levels); + + path_length = memcxt_info[i].path_length; + path_datum = (Datum *) palloc(path_length * sizeof(Datum)); + if (memcxt_info[i].path[0] != 0) + { + for (int j = 0; j < path_length; j++) + path_datum[j] = Int32GetDatum(memcxt_info[i].path[j]); + path_array = construct_array_builtin(path_datum, + path_length, + INT4OID); + values[4] = PointerGetDatum(path_array); + } + else + nulls[4] = true; + + values[5] = Int64GetDatum(memcxt_info[i].totalspace); + values[6] = Int64GetDatum(memcxt_info[i].nblocks); + values[7] = Int64GetDatum(memcxt_info[i].freespace); + values[8] = Int64GetDatum(memcxt_info[i].freechunks); + values[9] = Int64GetDatum(memcxt_info[i].totalspace - + memcxt_info[i].freespace); + values[10] = Int32GetDatum(memcxt_info[i].num_agg_stats); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + dshash_release_lock(MemoryStatsDsHash, entry); + memstats_dsa_cleanup(key); + + ConditionVariableCancelSleep(); + + PG_RETURN_NULL(); +} + +static void +memstats_dsa_cleanup(char *key) +{ + MemoryStatsDSHashEntry *entry; + + entry = dshash_find(MemoryStatsDsHash, key, true); + + Assert(MemoryStatsDsaArea != NULL); + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + entry->memstats_dsa_pointer = InvalidDsaPointer; + entry->stats_written = false; + entry->target_server_id = 0; + + dshash_release_lock(MemoryStatsDsHash, entry); +} + +/* + * Remove this process from the publishing process' + * client key slot, if the stats publishing process has failed to do so. + */ +static void +memstats_client_key_reset(int procNumber) +{ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + + if (client_keys[procNumber] == MyProcNumber) + client_keys[procNumber] = -1; + LWLockRelease(client_keys_lock); +} + +void +MemoryContextKeysShmemInit(void) +{ + bool found; + + client_keys = (int *) + ShmemInitStruct("MemoryContextKeys", + MemoryContextKeysShmemSize() + sizeof(LWLockPadded), &found); + client_keys_lock = (LWLock *) ((char *) client_keys + MemoryContextKeysShmemSize()); + + if (!found) + { + MemSet(client_keys, -1, MemoryContextKeysShmemSize()); + LWLockInitialize(client_keys_lock, LWTRANCHE_MEMORY_CONTEXT_KEYS); + } +} + +Size +MemoryContextKeysShmemSize(void) +{ + Size sz = 0; + Size TotalProcs = 0; + + TotalProcs = add_size(TotalProcs, NUM_AUXILIARY_PROCS); + TotalProcs = add_size(TotalProcs, MaxBackends); + sz = add_size(sz, mul_size(TotalProcs, sizeof(int))); + + return sz; +} + +/* + * HandleGetMemoryContextInterrupt + * Handle receipt of an interrupt indicating a request to publish memory + * contexts statistics. + * + * All the actual work is deferred to ProcessGetMemoryContextInterrupt() as + * this cannot be performed in a signal handler. + */ +void +HandleGetMemoryContextInterrupt(void) +{ + InterruptPending = true; + PublishMemoryContextPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + +/* + * ProcessGetMemoryContextInterrupt + * Generate information about memory contexts used by the process. + * + * Performs a breadth first search on the memory context tree, thus parents + * statistics are reported before their children in the monitoring function + * output. + * + * Statistics for all the processes are shared via the same dynamic shared + * area. Individual statistics are tracked independently in per-process DSA + * pointers. These pointers are stored in a dshash table with key as requesting + * clients ProcNumber. + * + * We calculate maximum number of context's statistics that can be displayed + * using a pre-determined limit for memory available per process for this + * utility and maximum size of statistics for each context. The remaining + * context statistics if any are captured as a cumulative total at the end of + * individual context's statistics. + * + * If summary is true, we capture the level 1 and level 2 contexts statistics. + * For that we traverse the memory context tree recursively in depth first + * search manner to cover all the children of a parent context, to be able to + * display a cumulative total of memory consumption by a parent at level 2 and + * all its children. + */ +void +ProcessGetMemoryContextInterrupt(void) +{ + List *contexts; + HASHCTL ctl; + HTAB *context_id_lookup; + int context_id = 0; + MemoryStatsEntry *meminfo; + MemoryContextCounters stat; + int num_individual_stats = 0; + bool found; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + int clientProcNumber; + MemoryContext memstats_ctx = NULL; + MemoryContext oldcontext = NULL; + + PublishMemoryContextPending = false; + + INJECTION_POINT("memcontext-server-wait", NULL); + + /* + * Retrieve the client key for publishing statistics and reset it to -1, + * so other clients can request memory statistics from this process. + * Return if the client_key is -1, which means the requesting client has + * timed out. + */ + LWLockAcquire(client_keys_lock, LW_SHARED); + if (client_keys[MyProcNumber] == -1) + { + LWLockRelease(client_keys_lock); + return; + } + else + { + clientProcNumber = client_keys[MyProcNumber]; + client_keys[MyProcNumber] = -1; + LWLockRelease(client_keys_lock); + } + + /* + * Create a new memory context which is not a part of TopMemoryContext + * tree. This context is used to allocate all memory in this function. + * This helps in keeping the memory allocation in this function to report + * memory consumption statistics separate. So that it does not affect the + * output of this function. + */ + memstats_ctx = AllocSetContextCreate((MemoryContext) NULL, + "publish_memory_context_statistics", + ALLOCSET_SMALL_SIZES); + oldcontext = MemoryContextSwitchTo(memstats_ctx); + + /* + * The hash table is used for constructing "path" column of the view, + * similar to its local backend counterpart. + */ + ctl.keysize = sizeof(MemoryContext); + ctl.entrysize = sizeof(MemoryContextId); + ctl.hcxt = CurrentMemoryContext; + + context_id_lookup = hash_create("pg_get_process_memory_contexts", + 256, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + /* List of contexts to process in the next round - start at the top. */ + contexts = list_make1(TopMemoryContext); + + /* + * The client process should have created the required DSA and DSHash + * table. Here we just attach to those. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); + + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); + + snprintf(key, CLIENT_KEY_SIZE, "%d", clientProcNumber); + + /* + * The entry lock is held by dshash_find_or_insert to protect writes to + * process specific memory. Two different processes publishing statistics + * do not block each other. + */ + INJECTION_POINT("memcontext-server-injection", NULL); + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + /* + * Check if the entry has been deleted due to calling process exiting, or + * if the caller has timed out waiting for us and have issued a request to + * another backend. + * + * Make sure that the client always deletes the entry after taking + * required lock or this function may end up writing to unallocated + * memory. + */ + if (!found || entry->target_server_id != MyProcPid) + { + entry->stats_written = false; + + dshash_release_lock(MemoryStatsDsHash, entry); + + hash_destroy(context_id_lookup); + MemoryContextSwitchTo(oldcontext); + MemoryContextReset(memstats_ctx); + + return; + } + + /* Should be allocated by a client backend that is requesting statistics */ + Assert(entry->memstats_dsa_pointer != InvalidDsaPointer); + meminfo = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + if (entry->summary) + { + int cxt_id = 0; + List *path = NIL; + MemoryContextId *contextid_entry; + + /* Copy TopMemoryContext statistics to DSA */ + memset(&stat, 0, sizeof(stat)); + (*TopMemoryContext->methods->stats) (TopMemoryContext, NULL, NULL, + &stat, true); + path = lcons_int(1, path); + PublishMemoryContext(meminfo, cxt_id, TopMemoryContext, path, stat, + 1); + + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &TopMemoryContext, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = cxt_id + 1; + + /* + * Copy statistics for each of TopMemoryContexts children. This + * includes statistics of at most 100 children per node, with each + * child node limited to a depth of 100 in its subtree. + */ + for (MemoryContext c = TopMemoryContext->firstchild; c != NULL; + c = c->nextchild) + { + MemoryContextCounters grand_totals; + int num_contexts = 0; + + path = NIL; + memset(&grand_totals, 0, sizeof(grand_totals)); + + cxt_id++; + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &c, HASH_ENTER, &found); + Assert(!found); + contextid_entry->context_id = cxt_id + 1; + + MemoryContextStatsCounter(c, &grand_totals, &num_contexts); + + path = compute_context_path(c, context_id_lookup); + + PublishMemoryContext(meminfo, cxt_id, c, path, + grand_totals, num_contexts); + } + entry->total_stats = cxt_id + 1; + } + else + { + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + MemoryContextId *contextid_entry; + + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &cur, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = context_id + 1; + + /* + * Figure out the transient context_id of this context and each of + * its ancestors, to compute a path for this context. + */ + path = compute_context_path(cur, context_id_lookup); + + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + /* Account for saving one statistics slot for cumulative reporting */ + if (context_id < (MAX_MEMORY_CONTEXT_STATS_NUM - 1)) + { + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1); + } + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].totalspace += stat.totalspace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].nblocks += stat.nblocks; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freespace += stat.freespace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit per process is reached, write aggregate of the + * remaining statistics. + * + * We can store contexts from 0 to max_stats - 1. When context_id + * is greater than max_stats, we stop reporting individual + * statistics when context_id equals max_stats - 2. As we use + * max_stats - 1 array slot for reporting cumulative statistics or + * "Remaining Totals". + */ + if (context_id == (MAX_MEMORY_CONTEXT_STATS_NUM - 2)) + { + int namelen = strlen("Remaining Totals"); + + num_individual_stats = context_id + 1; + strlcpy(meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].name, + "Remaining Totals", namelen + 1); + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].ident[0] = '\0'; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].path[0] = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].type = 0; + } + context_id++; + + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + contexts = lappend(contexts, c); + } + + /* + * Check if there are aggregated statistics or not in the result set. + * Statistics are individually reported when context_id <= max_stats, + * only if context_id > max_stats will there be aggregates. + */ + if (context_id <= MAX_MEMORY_CONTEXT_STATS_NUM) + { + entry->total_stats = context_id; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = 1; + } + + /* + * The number of contexts exceeded the space available, so report the + * number of aggregated memory contexts + */ + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = + context_id - num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for + * cumulative statistics. + */ + entry->total_stats = num_individual_stats + 1; + } + } + + entry->stats_written = true; + dshash_release_lock(MemoryStatsDsHash, entry); + hash_destroy(context_id_lookup); + + MemoryContextSwitchTo(oldcontext); + MemoryContextReset(memstats_ctx); + /* Notify waiting client backend and return */ + ConditionVariableSignal(&entry->memcxt_cv); +} + +/* + * compute_context_path + * + * Append the transient context_id of this context and each of its ancestors + * to a list, in order to compute a path. + */ +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + MemoryContext cur_context; + + for (cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + elog(ERROR, "hash table corrupted, can't construct path value"); + + path = lcons_int(cur_entry->context_id, path); + } + + return path; +} + +/* + * PublishMemoryContext + * + * Copy the memory context statistics of a single context to a DSA memory + */ +static void +PublishMemoryContext(MemoryStatsEntry *memcxt_info, int curr_id, + MemoryContext context, List *path, + MemoryContextCounters stat, int num_contexts) +{ + char *ident = unconstify(char *, context->ident); + char *name = unconstify(char *, context->name); + + /* + * To be consistent with logging output, we label dynahash contexts with + * just the hash table name as with MemoryContextStatsPrint(). + */ + if (context->ident && strncmp(context->name, "dynahash", 8) == 0) + { + name = unconstify(char *, context->ident); + ident = NULL; + } + + if (name != NULL) + { + int namelen = strlen(name); + + if (namelen >= MEMORY_CONTEXT_NAME_SHMEM_SIZE) + namelen = pg_mbcliplen(name, namelen, + MEMORY_CONTEXT_NAME_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].name, name, namelen + 1); + } + else + /* Clearing the array */ + memcxt_info[curr_id].name[0] = '\0'; + + /* Trim and copy the identifier if it is not set to NULL */ + if (ident != NULL) + { + int idlen = strlen(context->ident); + + /* + * Some identifiers such as SQL query string can be very long, + * truncate oversize identifiers. + */ + if (idlen >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + idlen = pg_mbcliplen(ident, idlen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].ident, ident, idlen + 1); + } + else + memcxt_info[curr_id].ident[0] = '\0'; + + /* Store the path */ + if (path == NIL) + memcxt_info[curr_id].path[0] = 0; + else + { + int levels = Min(list_length(path), MAX_PATH_DISPLAY_LENGTH); + + memcxt_info[curr_id].path_length = levels; + memcxt_info[curr_id].levels = list_length(path); + + foreach_int(i, path) + { + memcxt_info[curr_id].path[foreach_current_index(i)] = i; + if (--levels == 0) + break; + } + } + memcxt_info[curr_id].type = context->type; + memcxt_info[curr_id].totalspace = stat.totalspace; + memcxt_info[curr_id].nblocks = stat.nblocks; + memcxt_info[curr_id].freespace = stat.freespace; + memcxt_info[curr_id].freechunks = stat.freechunks; + memcxt_info[curr_id].num_agg_stats = num_contexts; +} + +void +AtProcExit_memstats_cleanup(int code, Datum arg) +{ + int idx = MyProcNumber; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + bool found; + + if (MemoryStatsDsHash != NULL) + { + snprintf(key, CLIENT_KEY_SIZE, "%d", idx); + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + if (found) + { + if (MemoryStatsDsaArea != NULL && + DsaPointerIsValid(entry->memstats_dsa_pointer)) + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + } + dshash_delete_entry(MemoryStatsDsHash, entry); + } + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + client_keys[idx] = -1; + LWLockRelease(client_keys_lock); +} diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index d31cb45a058..92b0446b80c 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -39,6 +39,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t PublishMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index 4ed69ac7ba2..c5a36dcbc95 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -658,6 +658,13 @@ BaseInit(void) * drop ephemeral slots, which in turn triggers stats reporting. */ ReplicationSlotInitialize(); + + /* + * The before shmem exit callback frees the DSA memory occupied by the + * latest memory context statistics that could be published by this proc + * if requested. + */ + before_shmem_exit(AtProcExit_memstats_cleanup, 0); } diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 47fd774c7d2..31c4de9f0b4 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -1008,6 +1008,37 @@ MemoryContextStatsInternal(MemoryContext context, int level, } } + +/* + * MemoryContextStatsCounter + * + * Accumulate statistics counts into *totals. totals should not be NULL. + * This involves a non-recursive tree traversal. + */ +void +MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts) +{ + int ichild = 1; + + *num_contexts = 0; + context->methods->stats(context, NULL, NULL, totals, false); + + for (MemoryContext curr = context->firstchild; + curr != NULL; + curr = MemoryContextTraverseNext(curr, context)) + { + curr->methods->stats(curr, NULL, NULL, totals, false); + ichild++; + } + + /* + * Add the count of all the children contexts which are traversed + * including the parent. + */ + *num_contexts = *num_contexts + ichild; +} + /* * MemoryContextStatsPrint * Print callback used by MemoryContextStatsInternal diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fd9448ec7b9..bef24d625d9 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8617,6 +8617,16 @@ prorettype => 'bool', proargtypes => 'int4', prosrc => 'pg_log_backend_memory_contexts' }, +# publishing memory contexts of the specified postgres process +{ oid => '2173', descr => 'publish memory contexts of the specified backend', + proname => 'pg_get_process_memory_contexts', provolatile => 'v', + prorows => '100', proretset => 't', proparallel => 'r', + prorettype => 'record', proargtypes => 'int4 bool', + proallargtypes => '{int4,bool,text,text,text,int4,_int4,int8,int8,int8,int8,int8,int4}', + proargmodes => '{i,i,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{pid, summary, name, ident, type, level, path, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes, num_agg_contexts}', + prosrc => 'pg_get_process_memory_contexts' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index 9a7d733ddef..b76f24baed6 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -96,6 +96,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t PublishMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h index 5b0ce383408..613e769c84e 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -136,3 +136,4 @@ PG_LWLOCKTRANCHE(SUBTRANS_SLRU, SubtransSLRU) PG_LWLOCKTRANCHE(XACT_SLRU, XactSLRU) PG_LWLOCKTRANCHE(PARALLEL_VACUUM_DSA, ParallelVacuumDSA) PG_LWLOCKTRANCHE(AIO_URING_COMPLETION, AioUringCompletion) +PG_LWLOCKTRANCHE(MEMORY_CONTEXT_KEYS, MemoryContextReportingKeys) diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index afeeb1ca019..345d5a0ecb1 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -35,6 +35,7 @@ typedef enum PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to send the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ /* Recovery conflict reasons */ diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 7bbe5a36959..617de0ebf91 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -19,7 +19,6 @@ #include "nodes/memnodes.h" - /* * MaxAllocSize, MaxAllocHugeSize * Quasi-arbitrary limits on size of allocations. @@ -319,4 +318,11 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) return true; } +extern void ProcessGetMemoryContextInterrupt(void); +extern void HandleGetMemoryContextInterrupt(void); +extern void MemoryContextKeysShmemInit(void); +extern Size MemoryContextKeysShmemSize(void); +extern void MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts); +extern void AtProcExit_memstats_cleanup(int code, Datum arg); #endif /* MEMUTILS_H */ diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 0411db832f1..3799ef7c862 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -233,3 +233,22 @@ select * from pg_timezone_abbrevs where abbrev = 'LMT'; LMT | @ 7 hours 52 mins 58 secs ago | f (1 row) +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; +NOTICE: (AllocSet,TopMemoryContext,) +NOTICE: (AllocSet,TopMemoryContext,) diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 66179f026b3..c9da4fc8c90 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -101,3 +101,21 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs; -- One specific case we can check without much fear of breakage -- is the historical local-mean-time value used for America/Los_Angeles. select * from pg_timezone_abbrevs where abbrev = 'LMT'; + +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9dd65b10254..eb25f426cf2 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1690,6 +1690,8 @@ MemoryContextData MemoryContextId MemoryContextMethodID MemoryContextMethods +MemoryStatsEntry +MemoryStatsDSHashEntry MemoryStatsPrintFunc MergeAction MergeActionState -- 2.34.1 [application/octet-stream] v44-0002-Test-module-to-test-memory-context-reporting-wit.patch (8.8K, ../../CAH2L28vK5T_+u4hOvxm+z_9povNPQWMg=ZxtdD00hngY2QibWA@mail.gmail.com/4-v44-0002-Test-module-to-test-memory-context-reporting-wit.patch) download | inline diff: From b2315034628f0ae6572f9a4887a54f14974105c2 Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Fri, 28 Nov 2025 14:46:38 +0530 Subject: [PATCH 2/2] Test module to test memory context reporting with injection points --- src/test/modules/Makefile | 1 + .../test_memcontext_reporting/Makefile | 29 ++++ .../t/001_memcontext_inj.pl | 150 ++++++++++++++++++ .../test_memcontext_reporting.c | 12 ++ .../test_memcontext_reporting.control | 4 + 5 files changed, 196 insertions(+) create mode 100644 src/test/modules/test_memcontext_reporting/Makefile create mode 100644 src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index 4c6d56d97d8..1156d731014 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -34,6 +34,7 @@ SUBDIRS = \ test_json_parser \ test_lfind \ test_lwlock_tranches \ + test_memcontext_reporting \ test_misc \ test_oat_hooks \ test_parser \ diff --git a/src/test/modules/test_memcontext_reporting/Makefile b/src/test/modules/test_memcontext_reporting/Makefile new file mode 100644 index 00000000000..0a2dfc44f1c --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/Makefile @@ -0,0 +1,29 @@ +# src/test/modules/test_memcontext_reporting/Makefile + +EXTRA_INSTALL = src/test/modules/injection_points + +export enable_injection_points +MODULE_big = test_memcontext_reporting +OBJS = \ + $(WIN32RES) \ + test_memcontext_reporting.o +PGFILEDESC = "test_memcontext_reporting - test code for memory context reporting" + +REGRESS = test_memcontext_reporting + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_memcontext_reporting +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif + +check: + $(prove_check) + +installcheck: + $(prove_installcheck) diff --git a/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl new file mode 100644 index 00000000000..b491d6ebc0a --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl @@ -0,0 +1,150 @@ +# Copyright (c) 2025, PostgreSQL Global Development Group + +# Test suite for testing memory context statistics reporting + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} +my $psql_err; +my $psql_out; +# Create and start a cluster with one node +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->append_conf( + 'postgresql.conf', + qq[ +max_connections = 100 +log_statement = none +restart_after_crash = false +]); +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); + +# Attaching to a client process's injection point that throws an error +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-client-injection', 'error');" +); + +my $pid = $node->safe_psql('postgres', + "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); + +#Client should have thrown error +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); +like($psql_err, + qr/error triggered for injection point memcontext-client-injection/); + +#Query the same process after detaching the injection point, using some other client and it should succeed. +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-client-injection');"); +my $topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name = 'TopMemoryContext'); + +# Attaching to a target process injection point that throws an error +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-server-injection', 'error');" +); + +#Server should have thrown error +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); + +#Query the same process after detaching the injection point, using some other client and it should succeed. +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-injection');"); +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name = 'TopMemoryContext'); + +# Test that two concurrent requests to the same process results in a warning for +# one of those + +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-client-injection', 'wait');"); +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-server-wait', 'wait');"); +my $psql_session1 = $node->background_psql('postgres'); +$psql_session1->query_until( + qr//, + qq( + SELECT pg_get_process_memory_contexts($pid, true); +)); +$node->wait_for_event('client backend', 'memcontext-client-injection'); +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); +ok($psql_err =~ + /WARNING: server process $pid is processing previous request/); +#Wake the client up. +$node->safe_psql('postgres', + "SELECT injection_points_wakeup('memcontext-client-injection');"); + +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-client-injection');"); +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-wait');"); + +# Test the client process exiting with timeout does not break the server process + +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-server-wait', 'wait');"); +# Following client query times out, returning NULL as output +$node->psql( + 'postgres', + qq(select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';), + stdout => \$psql_out); +ok($psql_out = 'NULL'); +#Wakeup the server process up and detach the injection point. +$node->safe_psql('postgres', + "SELECT injection_points_wakeup('memcontext-server-wait');"); +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-wait');"); +#Query the same server process again and it should succeed. +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name = 'TopMemoryContext'); + +# Test if the monitoring works fine, when the client backend crashes. +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-client-injection', 'test_memcontext_reporting', 'crash', NULL);" +); + +#Client will crash +$node->psql( + 'postgres', + qq(select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';), + stderr => \$psql_err); +like($psql_err, + qr/WARNING: terminating connection because of crash of another server process|server closed the connection unexpectedly|connection to server was lost|could not send data to server/ +); + +# Wait till server restarts +$node->restart; +$node->poll_query_until('postgres', "SELECT 1;", '1'); + +#Querying memory stats should succeed after server start +$pid = $node->safe_psql('postgres', + "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name = 'TopMemoryContext'); + +done_testing(); diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c new file mode 100644 index 00000000000..d641f3616dc --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c @@ -0,0 +1,12 @@ +#include "postgres.h" +#include "funcapi.h" + +PG_MODULE_MAGIC; + +extern PGDLLEXPORT void crash(const char *name, const void *private_data, void *arg); + +void +crash(const char *name, const void *private_data, void *arg) +{ + abort(); +} diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control new file mode 100644 index 00000000000..48b501682d5 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control @@ -0,0 +1,4 @@ +comment = 'Test code for memcontext reporting' +default_version = '1.0' +module_pathname = '$libdir/test_memcontext_reporting' +relocatable = true -- 2.34.1 ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-12-18 19:54 Rahila Syed <[email protected]> parent: Rahila Syed <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Rahila Syed @ 2025-12-18 19:54 UTC (permalink / raw) To: Daniel Gustafsson <[email protected]>; +Cc: pgsql-hackers; torikoshia <[email protected]> Hi, PFA the updated and rebased patches. To summarize the work done in this thread, here are some key concerns discussed in threads [1] and [2] and steps taken to address those: *DSA APIs and CFI Handler Safety*: DSA APIs, being high-level, are unsafe to call from the CFI handler, which can be invoked from low-level code. This concern was particularly raised for APIs like `dsa_allocate()` and `dsa_create()`. To resolve this, these APIs have been moved out of the CFI handler function. Now, the dynamic shared memory needed to store the statistics is allocated and deleted in the client function. The only operation performed in the CFI handler is `dsm_attach()`, which attaches to DSA for copying statistics. Since dsm_attach() only maps the existing DSM into the current process address space and does not create a new DSM, I don't see any specific reason why it would be unsafe to call it from the CFI handler. *Memory Leak in TopMemoryContext*: A memory leak was reported in the TopMemoryContext and there were concerns that memory allocated while executing the memory statistics reporting function could impact its output. To address this, we create an exclusive memory context under the NULL context to handle all memory allocations in `ProcessGetMemoryContextInterrupt`. This context does not fall under the TopMemoryContext tree, ensuring that allocations do not affect the function's outcome. The memory context is reset at the end of the function, preventing leaks. *Error Reported in Thread [2]*: This issue has been fixed by switching to a NULL resource owner before attaching to DSM in the CFI handler. *Other Improvements*: 1. Simplified the user interface by removing the `timeout` argument and using a constant value instead. 2. Provided a default for the `get_summary` argument so users do not need to pass a value if they choose not to. 3. The dsm_registry APIs are used to create and attach to DSA and DSHASH tables, which helps avoid code duplication. 4. Replaced the static shared memory array with a DSHASH table, which holds metadata such as pointers to memory containing statistics for each process. 5. The updates previously made to mcxt.c have been moved to mcxtfuncs.c, which now includes all the existing memory statistics functions as well as the code for the new proposed function 6. One function that relies on an unexported API is added in mcxt.c Thank you, Rahila Syed [1]. PostgreSQL: Re: pgsql: Add function to get memory context stats for processes <https://www.postgresql.org/message-id/CA%2BTgmoaey-kOP1k5FaUnQFd1fR0majVebWcL8ogfLbG_nt-Ytg%40mail.g...; [2]. PostgreSQL: Re: Prevent an error on attaching/creating a DSM/DSA from an interrupt handler. <https://www.postgresql.org/message-id/flat/8B873D49-E0E5-4F9F-B8D6-CA4836B825CD%40yesql.se#7026d2fe4...; Attachments: [application/octet-stream] v45-0001-Add-function-to-report-memory-context-statistics.patch (57.9K, ../../CAH2L28tUUf8o0Ec-5d8XPe=hno3A9Ob4nQ7TgrM1Qt_vJ0Oo2Q@mail.gmail.com/3-v45-0001-Add-function-to-report-memory-context-statistics.patch) download | inline diff: From 024a1ce3cf4a75d025ecf6f906beac3b3d634bb2 Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Thu, 27 Nov 2025 14:39:43 +0530 Subject: [PATCH 1/2] Add function to report memory context statistics This adds a function for retrieving memory context statistics and information from backends as well as auxiliary processes. The intended usecase is cluster debugging when under memory pressure or unanticipated memory usage characteristics. When calling the function it sends a signal to the specified process to submit statistics regarding its memory contexts into dynamic shared memory. Each memory context is returned in detail, followed by a cumulative total in case the number of contexts exceed the max allocated amount of shared memory. Each process is limited to use at most 1Mb memory for this. A summary can also be explicitly requested by the user, this will return the TopMemoryContext and a cumulative total of all lower contexts. In order to not block on busy processes, we have hardcoded the number of seconds during which to retry before timing out. In the case where no statistics are published within the set timeout, NULL is returned --- doc/src/sgml/func/func-admin.sgml | 159 +++ src/backend/catalog/system_functions.sql | 14 + src/backend/postmaster/autovacuum.c | 4 + src/backend/postmaster/checkpointer.c | 4 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 4 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 4 + src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/storage/lmgr/proc.c | 1 + src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 2 + src/backend/utils/adt/mcxtfuncs.c | 1009 ++++++++++++++++- src/backend/utils/init/globals.c | 1 + src/backend/utils/init/postinit.c | 7 + src/backend/utils/mmgr/mcxt.c | 31 + src/include/catalog/pg_proc.dat | 10 + src/include/miscadmin.h | 1 + src/include/storage/lwlocklist.h | 1 + src/include/storage/procsignal.h | 1 + src/include/utils/memutils.h | 8 +- src/test/regress/expected/sysviews.out | 19 + src/test/regress/sql/sysviews.sql | 18 + src/tools/pgindent/typedefs.list | 2 + 25 files changed, 1296 insertions(+), 21 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 2896cd9e429..5eac0e5f73c 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -251,6 +251,132 @@ <literal>false</literal> is returned. </para></entry> </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_get_process_memory_contexts</primary> + </indexterm> + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type> <optional>,<parameter>summary</parameter> <type>boolean</type> <literal>DEFAULT</literal> <literal>false</literal></optional> ) + <returnvalue>setof record</returnvalue> + ( <parameter>name</parameter> <type>text</type>, + <parameter>ident</parameter> <type>text</type>, + <parameter>type</parameter> <type>text</type>, + <parameter>level</parameter> <type>integer</type>, + <parameter>path</parameter> <type>integer[]</type>, + <parameter>total_bytes</parameter> <type>bigint</type>, + <parameter>total_nblocks</parameter> <type>bigint</type>, + <parameter>free_bytes</parameter> <type>bigint</type>, + <parameter>free_chunks</parameter> <type>bigint</type>, + <parameter>used_bytes</parameter> <type>bigint</type>, + <parameter>num_agg_contexts</parameter> <type>integer</type> ) + </para> + <para> + This function handles requests to display the memory contexts of a + <productname>PostgreSQL</productname> process with the specified + process ID. The function can be used to send requests to backends as + well as <glossterm linkend="glossary-auxiliary-proc">auxiliary processes</glossterm>. + If the process does not respond with memory contexts statistics in 5 seconds, + function returns NULL. + </para> + <para> + The returned record contains extended statistics per each memory + context: + <itemizedlist spacing="compact"> + <listitem> + <para> + <parameter>name</parameter> - The name of the memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>ident</parameter> - Memory context ID (if any). + </para> + </listitem> + <listitem> + <para> + <parameter>type</parameter> - The type of memory context, possible + values are: AllocSet, Generation, Slab and Bump. + </para> + </listitem> + <listitem> + <para> + <parameter>level</parameter> - The level in the tree of the current + memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>path</parameter> - Memory contexts are organized in a + tree model with TopMemoryContext as the root, and all other memory + contexts as nodes in the tree. The <parameter>path</parameter> + displays the path from the root to the current memory context. The + path is limited to 100 children per node, which each node limited + to a max depth of 100, to preserve memory during reporting. The + printed path will also be limited to 100 nodes counting from the + TopMemoryContext. + </para> + </listitem> + <listitem> + <para> + <parameter>total_bytes</parameter> - The total number of bytes + allocated to this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_nblocks</parameter> - The total number of blocks + used for the allocated memory. + </para> + </listitem> + <listitem> + <para> + <parameter>free_bytes</parameter> - The amount of free memory in + this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>free_chunks</parameter> - The number of chunks that + <parameter>free_bytes</parameter> corresponds to. + </para> + </listitem> + <listitem> + <para> + <parameter>used_bytes</parameter> - The total number of bytes + currently occupied. + </para> + </listitem> + <listitem> + <para> + <parameter>num_agg_contexts</parameter> - The number of memory + contexts aggregated in the displayed statistics. + </para> + </listitem> + </itemizedlist> + </para> + <para> + When <parameter>summary</parameter> is <literal>true</literal>, statistics + for memory contexts at levels 1 and 2 are displayed, with level 1 + representing the root node (i.e., <literal>TopMemoryContext</literal>). + Statistics for contexts on level 2 and below are aggregates of all + child contexts' statistics, where <literal>num_agg_contexts</literal> + indicate the number aggregated child contexts. When + <parameter>summary</parameter> is <literal>false</literal> (the default), + <literal>the num_agg_contexts</literal> value is <literal>1</literal>, + indicating that individual statistics are being displayed. + </para> + <para> + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context + statistics are aggregated and a cumulative total is displayed. The + <literal>num_agg_contexts</literal> column indicates the number of + contexts aggregated in the displayed statistics. When + <literal>num_agg_contexts</literal> is <literal>1</literal> it means + that the context statistics are displayed separately. + </para></entry> + </row> </tbody> </tgroup> </table> @@ -302,6 +428,39 @@ LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 because it may generate a large number of log messages. </para> + <para> + <function>pg_get_process_memory_contexts</function> can be used to request + memory contexts statistics of any <productname>PostgreSQL</productname> + process. For example: +<programlisting> +postgres=# SELECT * FROM pg_get_process_memory_contexts( + (SELECT pid FROM pg_stat_activity + WHERE backend_type = 'checkpointer'), + false) LIMIT 1; +-[ RECORD 1 ]----+------------------------------ +name | TopMemoryContext +ident | +type | AllocSet +level | 1 +path | {1} +total_bytes | 90304 +total_nblocks | 3 +free_bytes | 2880 +free_chunks | 1 +used_bytes | 87424 +num_agg_contexts | 1 +</programlisting> + <note> + <para> + While <function>pg_get_process_memory_contexts</function> can be used to + query memory contexts of the local backend, + <structname>pg_backend_memory_contexts</structname> + (see <xref linkend="view-pg-backend-memory-contexts"/> for more details) + will be less resource intensive when only the local backend is of interest. + </para> + </note> + </para> + </sect2> <sect2 id="functions-admin-backup"> diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql index 2d946d6d9e9..7b40bac5f57 100644 --- a/src/backend/catalog/system_functions.sql +++ b/src/backend/catalog/system_functions.sql @@ -657,6 +657,17 @@ LANGUAGE INTERNAL STRICT VOLATILE PARALLEL UNSAFE AS 'pg_replication_origin_session_setup'; +CREATE OR REPLACE FUNCTION + pg_get_process_memory_contexts(IN pid integer, IN summary boolean DEFAULT false, + OUT name text, OUT ident text, OUT type text, OUT level integer, + OUT path integer[], OUT total_bytes bigint, OUT total_nblocks bigint, + OUT free_bytes bigint, OUT free_chunks bigint, OUT used_bytes bigint, + OUT num_agg_contexts integer) +RETURNS SETOF RECORD +LANGUAGE INTERNAL +STRICT VOLATILE PARALLEL UNSAFE +AS 'pg_get_process_memory_contexts'; + -- -- The default permissions for functions mean that anyone can execute them. -- A number of functions shouldn't be executable by just anyone, but rather @@ -782,6 +793,7 @@ REVOKE EXECUTE ON FUNCTION pg_ls_logicalmapdir() FROM PUBLIC; REVOKE EXECUTE ON FUNCTION pg_ls_replslotdir(text) FROM PUBLIC; +REVOKE EXECUTE ON FUNCTION pg_get_process_memory_contexts(integer, boolean) FROM PUBLIC; -- -- We also set up some things as accessible to standard roles. -- @@ -808,6 +820,8 @@ GRANT EXECUTE ON FUNCTION pg_current_logfile() TO pg_monitor; GRANT EXECUTE ON FUNCTION pg_current_logfile(text) TO pg_monitor; +GRANT EXECUTE ON FUNCTION pg_get_process_memory_contexts(integer, boolean) TO pg_read_all_stats; + GRANT pg_read_all_settings TO pg_monitor; GRANT pg_read_all_stats TO pg_monitor; diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 1bd3924e35e..baba657904b 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -791,6 +791,10 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index 7f8cf1fa2ec..749e68553b9 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -679,6 +679,10 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index ba63b84dfc5..29454b8bf8a 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -48,6 +48,10 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index 3a65d841725..b89617d78db 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -871,6 +871,10 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index 27e86cf393f..7149a67fcbc 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -192,6 +192,10 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index e7e4d652f97..eb86648f7b7 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -879,6 +879,10 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index b23d0c19360..a5ed58a18c5 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -52,6 +52,7 @@ #include "storage/sinvaladt.h" #include "utils/guc.h" #include "utils/injection_point.h" +#include "utils/memutils.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -140,6 +141,7 @@ CalculateShmemSize(void) size = add_size(size, SlotSyncShmemSize()); size = add_size(size, AioShmemSize()); size = add_size(size, WaitLSNShmemSize()); + size = add_size(size, MemoryContextKeysShmemSize() + sizeof(LWLockPadded)); /* include additional requested shmem from preload libraries */ size = add_size(size, total_addin_request); @@ -328,6 +330,7 @@ CreateOrAttachShmemStructs(void) InjectionPointShmemInit(); AioShmemInit(); WaitLSNShmemInit(); + MemoryContextKeysShmemInit(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index 087821311cc..8963285cc12 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -691,6 +691,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_GET_MEMORY_CONTEXT)) + HandleGetMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index ebc3f4ca457..27b3b51cf2d 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -51,6 +51,7 @@ #include "storage/procsignal.h" #include "storage/spin.h" #include "storage/standby.h" +#include "utils/memutils.h" #include "utils/timeout.h" #include "utils/timestamp.h" diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 7dd75a490aa..e726f40dfbb 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3539,6 +3539,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index c0632bf901a..bf75b891495 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -162,6 +162,7 @@ WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." +MEM_CXT_PUBLISH "Waiting for a process to publish memory information." ABI_compatibility: @@ -405,6 +406,7 @@ SubtransSLRU "Waiting to access the sub-transaction SLRU cache." XactSLRU "Waiting to access the transaction status SLRU cache." ParallelVacuumDSA "Waiting for parallel vacuum dynamic shared memory allocation." AioUringCompletion "Waiting for another process to complete IO via io_uring." +MemoryContextReportingKeys "Waiting for another process to complete reading or writing the memory reporting keys." # No "ABI_compatibility" region here as WaitEventLWLock has its own C code. diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index 46dfb3dd133..babf8706513 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -17,22 +17,130 @@ #include "funcapi.h" #include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "storage/dsm_registry.h" #include "storage/proc.h" #include "storage/procarray.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/injection_point.h" +#include "utils/memutils.h" +#include "utils/wait_event_types.h" + +/* + * Memory Context reporting size limits. + */ + +/* Max length of context name and ident, to keep it consistent + * with ProcessLogMemoryContext() + */ +#define MEMORY_CONTEXT_IDENT_SHMEM_SIZE 100 +#define MEMORY_CONTEXT_NAME_SHMEM_SIZE 100 + +/* Maximum size (in bytes) of DSA area per process */ +#define MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND ((size_t) (1 * 1024 * 1024)) + +/* + * Maximum number of memory context statistics is calculated by dividing + * max memory allocated per backend with maximum size per context statistics. + * The identifier and name are statically allocated arrays of size 100 bytes. + * The path depth is limited to 100 like for memory context logging. + */ +#define MAX_MEMORY_CONTEXT_STATS_NUM MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND / (sizeof(MemoryStatsEntry)) + +/* + * Size of dshash key. The key is a uint32 rendered as a string, 10 chars + * plus space for a NULL terminator can hold all the values. + */ +#define CLIENT_KEY_SIZE (10 + 1) + +/* Dynamic shared memory state for reporting statistics per context */ +typedef struct MemoryStatsEntry +{ + char name[MEMORY_CONTEXT_NAME_SHMEM_SIZE]; + char ident[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + int path[100]; + NodeTag type; + int path_length; + int levels; + int64 totalspace; + int64 nblocks; + int64 freespace; + int64 freechunks; + int num_agg_stats; +} MemoryStatsEntry; + +/* + * Per backend dynamic shared hash entry for memory context statistics + * reporting. + */ +typedef struct MemoryStatsDSHashEntry +{ + char key[64]; + ConditionVariable memcxt_cv; + bool stats_written; + int target_server_id; + int total_stats; + bool summary; + dsa_pointer memstats_dsa_pointer; +} MemoryStatsDSHashEntry; + +static const dshash_parameters memctx_dsh_params = { + offsetof(MemoryStatsDSHashEntry, memcxt_cv), + sizeof(MemoryStatsDSHashEntry), + dshash_strcmp, + dshash_strhash, + dshash_strcpy +}; + +/* + * These are used for reporting memory context statistics of a process. + */ + +/* Lock to control access to client_keys array */ +static LWLock *client_keys_lock = NULL; + +/* Array to store the keys of MemoryStatsDsHash */ +static int *client_keys = NULL; + +/* + * Table to store pointers to DSA memory containing memory statistics and other + * metadata. There is one entry per client backend request, keyed by ProcNumber + * of the client obtained from client_keys array above. + */ +static dshash_table *MemoryStatsDsHash = NULL; + +/* + * Dsa area which stores the actual memory context + * statistics. + */ +static dsa_area *MemoryStatsDsaArea = NULL; + +static void memstats_dsa_cleanup(char *key); +static void memstats_client_key_reset(int ProcNumber); +static const char *ContextTypeToString(NodeTag type); +static void PublishMemoryContext(MemoryStatsEntry *memcxt_info, + int curr_id, MemoryContext context, + List *path, + MemoryContextCounters stat, + int num_contexts); +static List *compute_context_path(MemoryContext c, HTAB *context_id_lookup); /* ---------- * The max bytes for showing identifiers of MemoryContext. + * This is used by pg_get_backend_memory_context - view used for local backend. * ---------- */ #define MEMORY_CONTEXT_IDENT_DISPLAY_SIZE 1024 +#define MAX_PATH_DISPLAY_LENGTH 100 +/* Timeout in seconds */ +#define MEMORY_STATS_MAX_TIMEOUT 5 + /* * MemoryContextId - * Used for storage of transient identifiers for - * pg_get_backend_memory_contexts. + * Used for storage of transient identifiers for memory context reporting */ typedef struct MemoryContextId { @@ -143,24 +251,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, else nulls[1] = true; - switch (context->type) - { - case T_AllocSetContext: - type = "AllocSet"; - break; - case T_GenerationContext: - type = "Generation"; - break; - case T_SlabContext: - type = "Slab"; - break; - case T_BumpContext: - type = "Bump"; - break; - default: - type = "???"; - break; - } + type = ContextTypeToString(context->type); values[2] = CStringGetTextDatum(type); values[3] = Int32GetDatum(list_length(path)); /* level */ @@ -175,6 +266,38 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, list_free(path); } +/* + * ContextTypeToString + * Returns a textual representation of a context type + * + * This should cover the same types as MemoryContextIsValid. + */ +const char * +ContextTypeToString(NodeTag type) +{ + const char *context_type; + + switch (type) + { + case T_AllocSetContext: + context_type = "AllocSet"; + break; + case T_GenerationContext: + context_type = "Generation"; + break; + case T_SlabContext: + context_type = "Slab"; + break; + case T_BumpContext: + context_type = "Bump"; + break; + default: + context_type = "???"; + break; + } + return context_type; +} + /* * pg_get_backend_memory_contexts * SQL SRF showing backend memory context. @@ -305,3 +428,849 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } + +/* + * pg_get_process_memory_contexts + * Signal a backend or an auxiliary process to send its memory contexts, + * wait for the results and display them. + * + * By default, only superusers or users with ROLE_PG_READ_ALL_STATS are allowed + * to signal a process to return the memory contexts. Additional roles can be + * permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to copy the memory context details + * to a dynamic shared memory space. + * + * We have defined a limit on DSA memory that could be allocated per process - + * if the process has more memory contexts than what can fit in the allocated + * size, the excess contexts are summarized and represented as cumulative total + * at the end of the buffer. + * + * After sending the signal, wait on a condition variable. The publishing + * backend, after copying the data to shared memory, sends a signal on that + * condition variable. There is one condition variable per client process. + * Once the condition variable is signalled, check if the latest memory context + * information is available and display. + * + * If the publishing backend does not respond before the condition variable + * times out, which is set to a predefined value MEMORY_STATS_MAX_TIMEOUT, give + * up and return NULL. + */ +Datum +pg_get_process_memory_contexts(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + bool summary = PG_GETARG_BOOL(1); + PGPROC *proc; + ProcNumber procNumber; + bool proc_is_aux = false; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + MemoryStatsEntry *memcxt_info; + MemoryStatsDSHashEntry *entry; + bool found; + char key[CLIENT_KEY_SIZE]; + TimestampTz start_timestamp; + + /* + * See if the process with given pid is a backend or an auxiliary process + * and remember the type for when we requery the process later. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + { + proc = AuxiliaryPidGetProc(pid); + proc_is_aux = true; + } + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; this is however not a problem and leave with a WARNING. + * See comment in pg_log_backend_memory_contexts for a discussion on this. + */ + if (proc == NULL) + { + ereport(WARNING, + errmsg("PID %d is not a PostgreSQL server process", pid)); + PG_RETURN_NULL(); + } + + procNumber = GetNumberFromPGProc(proc); + + /* + * Check if the server process slot is not empty and exit early Non-empty + * slot means some other client backend is requesting the statistics from + * the same server process. + */ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + if (client_keys[procNumber] != -1) + { + LWLockRelease(client_keys_lock); + ereport(WARNING, + errmsg("server process %d is processing previous request", + pid)); + PG_RETURN_NULL(); + } + LWLockRelease(client_keys_lock); + + /* + * Create a DSA to allocate memory for copying memory contexts statistics. + * Allocate the memory in the DSA and send DSA pointer to the server + * process for storing the context statistics. If number of contexts + * exceed a predefined limit (1MB), a cumulative total is stored for such + * contexts. + * + * The DSA is created once for the lifetime of the server, and only + * attached in subsequent calls. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); + + /* + * The DSA pointers containing statistics for each client are stored in a + * dshash table. In addition to DSA pointer, each entry in this table also + * contains information about the statistics, condition variable for + * signalling between client and the server and miscellaneous data + * specific to a request. There is one entry per client request in the + * hash table. + */ + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); + + snprintf(key, sizeof(key), "%d", MyProcNumber); + + /* + * Insert an entry for this client in DSHASH table the first time this + * function is called. This entry is deleted when the process exits in + * before_shmem_exit call. + * + * dshash_find_or_insert locks the entry to prevent the publisher from + * reading before client has updated the entry. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + if (!found) + { + entry->stats_written = false; + ConditionVariableInit(&entry->memcxt_cv); + } + + /* + * Allocate 1MB of memory for the backend to publish its statistics on + * every call to this function. The memory is freed at the end of the + * function. + */ + entry->memstats_dsa_pointer = + dsa_allocate0(MemoryStatsDsaArea, MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND); + + /* + * Specify whether a summary of statistics is requested, before signalling + * the server. + */ + entry->summary = summary; + + /* + * Indicate which server process statistics are being requested from. If + * this client times out before the last requested process can publish its + * statistics, it may send a new request to another server process. Since + * the previous server was notified, it might attempt to read the same + * client entry and respond incorrectly with its statistics. By storing + * the server ID in the client entry, we prevent any previously signalled + * server process from writing its statistics in the space meant for the + * newly requested process. + */ + entry->target_server_id = pid; + dshash_release_lock(MemoryStatsDsHash, entry); + + /* + * Check if the publishing process slot is empty and store this clients + * key i.e its procNumber. This informs the publishing process that it is + * supposed to write statistics in the hash entry corresponding to this + * client. + */ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + if (client_keys[procNumber] == -1) + client_keys[procNumber] = MyProcNumber; + else + { + LWLockRelease(client_keys_lock); + ereport(WARNING, + errmsg("server process %d is processing previous request", + pid)); + PG_RETURN_NULL(); + } + LWLockRelease(client_keys_lock); + + /* + * Send a signal to a PostgreSQL process, informing it we want it to + * produce information about its memory contexts. + */ + if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, procNumber) < 0) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ereport(WARNING, + errmsg("could not send signal to process %d: %m", + pid)); + PG_RETURN_NULL(); + } + start_timestamp = GetCurrentTimestamp(); + + while (1) + { + long elapsed_time; + + INJECTION_POINT("memcontext-client-injection", NULL); + + elapsed_time = TimestampDifferenceMilliseconds(start_timestamp, + GetCurrentTimestamp()); + /* Return if we have already exceeded the timeout */ + if (elapsed_time >= MEMORY_STATS_MAX_TIMEOUT * 1000) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + PG_RETURN_NULL(); + } + + /* + * Recheck the state of the backend before sleeping on the condition + * variable to ensure the process is still alive. Only check the + * relevant process type based on the earlier PID check. + */ + if (proc_is_aux) + proc = AuxiliaryPidGetProc(pid); + else + proc = BackendPidGetProc(pid); + + /* + * The target server process ending during memory context processing + * is not an error. + */ + if (proc == NULL) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + ereport(WARNING, + errmsg("PID %d is no longer a PostgreSQL server process", + pid)); + PG_RETURN_NULL(); + } + + /* + * Wait for MEMORY_STATS_MAX_TIMEOUT. If no statistics are available + * within the allowed time then return NULL. The timer is defined in + * milliseconds since that's what the condition variable sleep uses. + */ + if (ConditionVariableTimedSleep(&entry->memcxt_cv, + (MEMORY_STATS_MAX_TIMEOUT * 1000), + WAIT_EVENT_MEM_CXT_PUBLISH)) + { + /* Timeout has expired, return NULL */ + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + PG_RETURN_NULL(); + } + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + Assert(found); + + memcxt_info = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + /* + * We expect to come out of sleep when the requested process has + * finished publishing the statistics, verified using a boolean + * stats_written. + * + * Make sure that the statistics are actually written by checking that + * the name of the context is not NULL. This is done to ensure that + * the subsequent waits for statistics do not return spuriously if the + * previous call to the function ended in error and thus could not + * clear the stats_written flag. + */ + if (entry->stats_written && memcxt_info[0].name[0] != '\0') + break; + + dshash_release_lock(MemoryStatsDsHash, entry); + + } + + InitMaterializedSRF(fcinfo, 0); + + /* + * Backend has finished publishing the stats, project them. + */ +#define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 11 + for (int i = 0; i < entry->total_stats; i++) + { + ArrayType *path_array; + int path_length; + Datum values[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + bool nulls[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + Datum *path_datum = NULL; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + Assert(memcxt_info[i].name[0] != '\0'); + values[0] = CStringGetTextDatum(memcxt_info[i].name); + + if (memcxt_info[i].ident[0] != '\0') + values[1] = CStringGetTextDatum(memcxt_info[i].ident); + else + nulls[1] = true; + + values[2] = CStringGetTextDatum(ContextTypeToString(memcxt_info[i].type)); + values[3] = Int32GetDatum(memcxt_info[i].levels); + + path_length = memcxt_info[i].path_length; + path_datum = (Datum *) palloc(path_length * sizeof(Datum)); + if (memcxt_info[i].path[0] != 0) + { + for (int j = 0; j < path_length; j++) + path_datum[j] = Int32GetDatum(memcxt_info[i].path[j]); + path_array = construct_array_builtin(path_datum, + path_length, + INT4OID); + values[4] = PointerGetDatum(path_array); + } + else + nulls[4] = true; + + values[5] = Int64GetDatum(memcxt_info[i].totalspace); + values[6] = Int64GetDatum(memcxt_info[i].nblocks); + values[7] = Int64GetDatum(memcxt_info[i].freespace); + values[8] = Int64GetDatum(memcxt_info[i].freechunks); + values[9] = Int64GetDatum(memcxt_info[i].totalspace - + memcxt_info[i].freespace); + values[10] = Int32GetDatum(memcxt_info[i].num_agg_stats); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + dshash_release_lock(MemoryStatsDsHash, entry); + memstats_dsa_cleanup(key); + + ConditionVariableCancelSleep(); + + PG_RETURN_NULL(); +} + +static void +memstats_dsa_cleanup(char *key) +{ + MemoryStatsDSHashEntry *entry; + + entry = dshash_find(MemoryStatsDsHash, key, true); + + Assert(MemoryStatsDsaArea != NULL); + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + entry->memstats_dsa_pointer = InvalidDsaPointer; + entry->stats_written = false; + entry->target_server_id = 0; + + dshash_release_lock(MemoryStatsDsHash, entry); +} + +/* + * Remove this process from the publishing process' + * client key slot, if the stats publishing process has failed to do so. + */ +static void +memstats_client_key_reset(int procNumber) +{ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + + if (client_keys[procNumber] == MyProcNumber) + client_keys[procNumber] = -1; + LWLockRelease(client_keys_lock); +} + +void +MemoryContextKeysShmemInit(void) +{ + bool found; + + client_keys = (int *) + ShmemInitStruct("MemoryContextKeys", + MemoryContextKeysShmemSize() + sizeof(LWLockPadded), &found); + client_keys_lock = (LWLock *) ((char *) client_keys + MemoryContextKeysShmemSize()); + + if (!found) + { + MemSet(client_keys, -1, MemoryContextKeysShmemSize()); + LWLockInitialize(client_keys_lock, LWTRANCHE_MEMORY_CONTEXT_KEYS); + } +} + +Size +MemoryContextKeysShmemSize(void) +{ + Size sz = 0; + Size TotalProcs = 0; + + TotalProcs = add_size(TotalProcs, NUM_AUXILIARY_PROCS); + TotalProcs = add_size(TotalProcs, MaxBackends); + sz = add_size(sz, mul_size(TotalProcs, sizeof(int))); + + return sz; +} + +/* + * HandleGetMemoryContextInterrupt + * Handle receipt of an interrupt indicating a request to publish memory + * contexts statistics. + * + * All the actual work is deferred to ProcessGetMemoryContextInterrupt() as + * this cannot be performed in a signal handler. + */ +void +HandleGetMemoryContextInterrupt(void) +{ + InterruptPending = true; + PublishMemoryContextPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + +/* + * ProcessGetMemoryContextInterrupt + * Generate information about memory contexts used by the process. + * + * Performs a breadth first search on the memory context tree, thus parents + * statistics are reported before their children in the monitoring function + * output. + * + * Statistics for all the processes are shared via the same dynamic shared + * area. Individual statistics are tracked independently in per-process DSA + * pointers. These pointers are stored in a dshash table with key as requesting + * clients ProcNumber. + * + * We calculate maximum number of context's statistics that can be displayed + * using a pre-determined limit for memory available per process for this + * utility and maximum size of statistics for each context. The remaining + * context statistics if any are captured as a cumulative total at the end of + * individual context's statistics. + * + * If summary is true, we capture the level 1 and level 2 contexts statistics. + * For that we traverse the memory context tree recursively in depth first + * search manner to cover all the children of a parent context, to be able to + * display a cumulative total of memory consumption by a parent at level 2 and + * all its children. + */ +void +ProcessGetMemoryContextInterrupt(void) +{ + List *contexts; + HASHCTL ctl; + HTAB *context_id_lookup; + int context_id = 0; + MemoryStatsEntry *meminfo; + MemoryContextCounters stat; + int num_individual_stats = 0; + bool found; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + int clientProcNumber; + MemoryContext memstats_ctx = NULL; + MemoryContext oldcontext = NULL; + ResourceOwner currentOwner; + + PublishMemoryContextPending = false; + + INJECTION_POINT("memcontext-server-wait", NULL); + + /* + * Retrieve the client key for publishing statistics and reset it to -1, + * so other clients can request memory statistics from this process. + * Return if the client_key is -1, which means the requesting client has + * timed out. + */ + LWLockAcquire(client_keys_lock, LW_SHARED); + if (client_keys[MyProcNumber] == -1) + { + LWLockRelease(client_keys_lock); + return; + } + else + { + clientProcNumber = client_keys[MyProcNumber]; + client_keys[MyProcNumber] = -1; + LWLockRelease(client_keys_lock); + } + + /* + * Create a new memory context which is not a part of TopMemoryContext + * tree. This context is used to allocate all memory in this function. + * This helps in keeping the memory allocation in this function to report + * memory consumption statistics separate. So that it does not affect the + * output of this function. + */ + memstats_ctx = AllocSetContextCreate((MemoryContext) NULL, + "publish_memory_context_statistics", + ALLOCSET_SMALL_SIZES); + oldcontext = MemoryContextSwitchTo(memstats_ctx); + + /* + * The hash table is used for constructing "path" column of the view, + * similar to its local backend counterpart. + */ + ctl.keysize = sizeof(MemoryContext); + ctl.entrysize = sizeof(MemoryContextId); + ctl.hcxt = CurrentMemoryContext; + + context_id_lookup = hash_create("pg_get_process_memory_contexts", + 256, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + /* List of contexts to process in the next round - start at the top. */ + contexts = list_make1(TopMemoryContext); + + currentOwner = CurrentResourceOwner; + CurrentResourceOwner = NULL; + /* + * The client process should have created the required DSA and DSHash + * table. Here we just attach to those. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); + + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); + CurrentResourceOwner = currentOwner; + + snprintf(key, CLIENT_KEY_SIZE, "%d", clientProcNumber); + + /* + * The entry lock is held by dshash_find_or_insert to protect writes to + * process specific memory. Two different processes publishing statistics + * do not block each other. + */ + INJECTION_POINT("memcontext-server-injection", NULL); + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + /* + * Check if the entry has been deleted due to calling process exiting, or + * if the caller has timed out waiting for us and have issued a request to + * another backend. + * + * Make sure that the client always deletes the entry after taking + * required lock or this function may end up writing to unallocated + * memory. + */ + if (!found || entry->target_server_id != MyProcPid) + { + entry->stats_written = false; + + dshash_release_lock(MemoryStatsDsHash, entry); + + hash_destroy(context_id_lookup); + MemoryContextSwitchTo(oldcontext); + MemoryContextReset(memstats_ctx); + + return; + } + + /* Should be allocated by a client backend that is requesting statistics */ + Assert(entry->memstats_dsa_pointer != InvalidDsaPointer); + meminfo = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + if (entry->summary) + { + int cxt_id = 0; + List *path = NIL; + MemoryContextId *contextid_entry; + + /* Copy TopMemoryContext statistics to DSA */ + memset(&stat, 0, sizeof(stat)); + (*TopMemoryContext->methods->stats) (TopMemoryContext, NULL, NULL, + &stat, true); + path = lcons_int(1, path); + PublishMemoryContext(meminfo, cxt_id, TopMemoryContext, path, stat, + 1); + + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &TopMemoryContext, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = cxt_id + 1; + + /* + * Copy statistics for each of TopMemoryContexts children. This + * includes statistics of at most 100 children per node, with each + * child node limited to a depth of 100 in its subtree. + */ + for (MemoryContext c = TopMemoryContext->firstchild; c != NULL; + c = c->nextchild) + { + MemoryContextCounters grand_totals; + int num_contexts = 0; + + path = NIL; + memset(&grand_totals, 0, sizeof(grand_totals)); + + cxt_id++; + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &c, HASH_ENTER, &found); + Assert(!found); + contextid_entry->context_id = cxt_id + 1; + + MemoryContextStatsCounter(c, &grand_totals, &num_contexts); + + path = compute_context_path(c, context_id_lookup); + + PublishMemoryContext(meminfo, cxt_id, c, path, + grand_totals, num_contexts); + } + entry->total_stats = cxt_id + 1; + } + else + { + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + MemoryContextId *contextid_entry; + + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &cur, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = context_id + 1; + + /* + * Figure out the transient context_id of this context and each of + * its ancestors, to compute a path for this context. + */ + path = compute_context_path(cur, context_id_lookup); + + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + /* Account for saving one statistics slot for cumulative reporting */ + if (context_id < (MAX_MEMORY_CONTEXT_STATS_NUM - 1)) + { + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1); + } + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].totalspace += stat.totalspace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].nblocks += stat.nblocks; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freespace += stat.freespace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit per process is reached, write aggregate of the + * remaining statistics. + * + * We can store contexts from 0 to max_stats - 1. When context_id + * is greater than max_stats, we stop reporting individual + * statistics when context_id equals max_stats - 2. As we use + * max_stats - 1 array slot for reporting cumulative statistics or + * "Remaining Totals". + */ + if (context_id == (MAX_MEMORY_CONTEXT_STATS_NUM - 2)) + { + int namelen = strlen("Remaining Totals"); + + num_individual_stats = context_id + 1; + strlcpy(meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].name, + "Remaining Totals", namelen + 1); + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].ident[0] = '\0'; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].path[0] = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].type = 0; + } + context_id++; + + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + contexts = lappend(contexts, c); + } + + /* + * Check if there are aggregated statistics or not in the result set. + * Statistics are individually reported when context_id <= max_stats, + * only if context_id > max_stats will there be aggregates. + */ + if (context_id <= MAX_MEMORY_CONTEXT_STATS_NUM) + { + entry->total_stats = context_id; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = 1; + } + + /* + * The number of contexts exceeded the space available, so report the + * number of aggregated memory contexts + */ + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = + context_id - num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for + * cumulative statistics. + */ + entry->total_stats = num_individual_stats + 1; + } + } + + entry->stats_written = true; + dshash_release_lock(MemoryStatsDsHash, entry); + hash_destroy(context_id_lookup); + + MemoryContextSwitchTo(oldcontext); + MemoryContextReset(memstats_ctx); + /* Notify waiting client backend and return */ + ConditionVariableSignal(&entry->memcxt_cv); +} + +/* + * compute_context_path + * + * Append the transient context_id of this context and each of its ancestors + * to a list, in order to compute a path. + */ +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + MemoryContext cur_context; + + for (cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + elog(ERROR, "hash table corrupted, can't construct path value"); + + path = lcons_int(cur_entry->context_id, path); + } + + return path; +} + +/* + * PublishMemoryContext + * + * Copy the memory context statistics of a single context to a DSA memory + */ +static void +PublishMemoryContext(MemoryStatsEntry *memcxt_info, int curr_id, + MemoryContext context, List *path, + MemoryContextCounters stat, int num_contexts) +{ + char *ident = unconstify(char *, context->ident); + char *name = unconstify(char *, context->name); + + /* + * To be consistent with logging output, we label dynahash contexts with + * just the hash table name as with MemoryContextStatsPrint(). + */ + if (context->ident && strncmp(context->name, "dynahash", 8) == 0) + { + name = unconstify(char *, context->ident); + ident = NULL; + } + + if (name != NULL) + { + int namelen = strlen(name); + + if (namelen >= MEMORY_CONTEXT_NAME_SHMEM_SIZE) + namelen = pg_mbcliplen(name, namelen, + MEMORY_CONTEXT_NAME_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].name, name, namelen + 1); + } + else + /* Clearing the array */ + memcxt_info[curr_id].name[0] = '\0'; + + /* Trim and copy the identifier if it is not set to NULL */ + if (ident != NULL) + { + int idlen = strlen(context->ident); + + /* + * Some identifiers such as SQL query string can be very long, + * truncate oversize identifiers. + */ + if (idlen >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + idlen = pg_mbcliplen(ident, idlen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].ident, ident, idlen + 1); + } + else + memcxt_info[curr_id].ident[0] = '\0'; + + /* Store the path */ + if (path == NIL) + memcxt_info[curr_id].path[0] = 0; + else + { + int levels = Min(list_length(path), MAX_PATH_DISPLAY_LENGTH); + + memcxt_info[curr_id].path_length = levels; + memcxt_info[curr_id].levels = list_length(path); + + foreach_int(i, path) + { + memcxt_info[curr_id].path[foreach_current_index(i)] = i; + if (--levels == 0) + break; + } + } + memcxt_info[curr_id].type = context->type; + memcxt_info[curr_id].totalspace = stat.totalspace; + memcxt_info[curr_id].nblocks = stat.nblocks; + memcxt_info[curr_id].freespace = stat.freespace; + memcxt_info[curr_id].freechunks = stat.freechunks; + memcxt_info[curr_id].num_agg_stats = num_contexts; +} + +void +AtProcExit_memstats_cleanup(int code, Datum arg) +{ + int idx = MyProcNumber; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + bool found; + + if (MemoryStatsDsHash != NULL) + { + snprintf(key, CLIENT_KEY_SIZE, "%d", idx); + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + if (found) + { + if (MemoryStatsDsaArea != NULL && + DsaPointerIsValid(entry->memstats_dsa_pointer)) + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + } + dshash_delete_entry(MemoryStatsDsHash, entry); + } + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + client_keys[idx] = -1; + LWLockRelease(client_keys_lock); +} diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index d31cb45a058..92b0446b80c 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -39,6 +39,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t PublishMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index 4ed69ac7ba2..c5a36dcbc95 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -658,6 +658,13 @@ BaseInit(void) * drop ephemeral slots, which in turn triggers stats reporting. */ ReplicationSlotInitialize(); + + /* + * The before shmem exit callback frees the DSA memory occupied by the + * latest memory context statistics that could be published by this proc + * if requested. + */ + before_shmem_exit(AtProcExit_memstats_cleanup, 0); } diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 47fd774c7d2..31c4de9f0b4 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -1008,6 +1008,37 @@ MemoryContextStatsInternal(MemoryContext context, int level, } } + +/* + * MemoryContextStatsCounter + * + * Accumulate statistics counts into *totals. totals should not be NULL. + * This involves a non-recursive tree traversal. + */ +void +MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts) +{ + int ichild = 1; + + *num_contexts = 0; + context->methods->stats(context, NULL, NULL, totals, false); + + for (MemoryContext curr = context->firstchild; + curr != NULL; + curr = MemoryContextTraverseNext(curr, context)) + { + curr->methods->stats(curr, NULL, NULL, totals, false); + ichild++; + } + + /* + * Add the count of all the children contexts which are traversed + * including the parent. + */ + *num_contexts = *num_contexts + ichild; +} + /* * MemoryContextStatsPrint * Print callback used by MemoryContextStatsInternal diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fd9448ec7b9..bef24d625d9 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8617,6 +8617,16 @@ prorettype => 'bool', proargtypes => 'int4', prosrc => 'pg_log_backend_memory_contexts' }, +# publishing memory contexts of the specified postgres process +{ oid => '2173', descr => 'publish memory contexts of the specified backend', + proname => 'pg_get_process_memory_contexts', provolatile => 'v', + prorows => '100', proretset => 't', proparallel => 'r', + prorettype => 'record', proargtypes => 'int4 bool', + proallargtypes => '{int4,bool,text,text,text,int4,_int4,int8,int8,int8,int8,int8,int4}', + proargmodes => '{i,i,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{pid, summary, name, ident, type, level, path, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes, num_agg_contexts}', + prosrc => 'pg_get_process_memory_contexts' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index 9a7d733ddef..b76f24baed6 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -96,6 +96,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t PublishMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h index 5b0ce383408..613e769c84e 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -136,3 +136,4 @@ PG_LWLOCKTRANCHE(SUBTRANS_SLRU, SubtransSLRU) PG_LWLOCKTRANCHE(XACT_SLRU, XactSLRU) PG_LWLOCKTRANCHE(PARALLEL_VACUUM_DSA, ParallelVacuumDSA) PG_LWLOCKTRANCHE(AIO_URING_COMPLETION, AioUringCompletion) +PG_LWLOCKTRANCHE(MEMORY_CONTEXT_KEYS, MemoryContextReportingKeys) diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index afeeb1ca019..345d5a0ecb1 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -35,6 +35,7 @@ typedef enum PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to send the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ /* Recovery conflict reasons */ diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 7bbe5a36959..617de0ebf91 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -19,7 +19,6 @@ #include "nodes/memnodes.h" - /* * MaxAllocSize, MaxAllocHugeSize * Quasi-arbitrary limits on size of allocations. @@ -319,4 +318,11 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) return true; } +extern void ProcessGetMemoryContextInterrupt(void); +extern void HandleGetMemoryContextInterrupt(void); +extern void MemoryContextKeysShmemInit(void); +extern Size MemoryContextKeysShmemSize(void); +extern void MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts); +extern void AtProcExit_memstats_cleanup(int code, Datum arg); #endif /* MEMUTILS_H */ diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 0411db832f1..3799ef7c862 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -233,3 +233,22 @@ select * from pg_timezone_abbrevs where abbrev = 'LMT'; LMT | @ 7 hours 52 mins 58 secs ago | f (1 row) +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; +NOTICE: (AllocSet,TopMemoryContext,) +NOTICE: (AllocSet,TopMemoryContext,) diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 66179f026b3..c9da4fc8c90 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -101,3 +101,21 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs; -- One specific case we can check without much fear of breakage -- is the historical local-mean-time value used for America/Los_Angeles. select * from pg_timezone_abbrevs where abbrev = 'LMT'; + +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 04845d5e680..6b964360718 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1700,6 +1700,8 @@ MemoryContextData MemoryContextId MemoryContextMethodID MemoryContextMethods +MemoryStatsEntry +MemoryStatsDSHashEntry MemoryStatsPrintFunc MergeAction MergeActionState -- 2.34.1 [application/octet-stream] v45-0002-Test-module-to-test-memory-context-reporting-wit.patch (8.8K, ../../CAH2L28tUUf8o0Ec-5d8XPe=hno3A9Ob4nQ7TgrM1Qt_vJ0Oo2Q@mail.gmail.com/4-v45-0002-Test-module-to-test-memory-context-reporting-wit.patch) download | inline diff: From e9c37a3f67f00821b998aec8443797c710703474 Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Fri, 28 Nov 2025 14:46:38 +0530 Subject: [PATCH 2/2] Test module to test memory context reporting with injection points --- src/test/modules/Makefile | 1 + .../test_memcontext_reporting/Makefile | 29 ++++ .../t/001_memcontext_inj.pl | 150 ++++++++++++++++++ .../test_memcontext_reporting.c | 12 ++ .../test_memcontext_reporting.control | 4 + 5 files changed, 196 insertions(+) create mode 100644 src/test/modules/test_memcontext_reporting/Makefile create mode 100644 src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index 4c6d56d97d8..1156d731014 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -34,6 +34,7 @@ SUBDIRS = \ test_json_parser \ test_lfind \ test_lwlock_tranches \ + test_memcontext_reporting \ test_misc \ test_oat_hooks \ test_parser \ diff --git a/src/test/modules/test_memcontext_reporting/Makefile b/src/test/modules/test_memcontext_reporting/Makefile new file mode 100644 index 00000000000..0a2dfc44f1c --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/Makefile @@ -0,0 +1,29 @@ +# src/test/modules/test_memcontext_reporting/Makefile + +EXTRA_INSTALL = src/test/modules/injection_points + +export enable_injection_points +MODULE_big = test_memcontext_reporting +OBJS = \ + $(WIN32RES) \ + test_memcontext_reporting.o +PGFILEDESC = "test_memcontext_reporting - test code for memory context reporting" + +REGRESS = test_memcontext_reporting + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_memcontext_reporting +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif + +check: + $(prove_check) + +installcheck: + $(prove_installcheck) diff --git a/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl new file mode 100644 index 00000000000..b491d6ebc0a --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl @@ -0,0 +1,150 @@ +# Copyright (c) 2025, PostgreSQL Global Development Group + +# Test suite for testing memory context statistics reporting + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} +my $psql_err; +my $psql_out; +# Create and start a cluster with one node +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->append_conf( + 'postgresql.conf', + qq[ +max_connections = 100 +log_statement = none +restart_after_crash = false +]); +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); + +# Attaching to a client process's injection point that throws an error +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-client-injection', 'error');" +); + +my $pid = $node->safe_psql('postgres', + "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); + +#Client should have thrown error +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); +like($psql_err, + qr/error triggered for injection point memcontext-client-injection/); + +#Query the same process after detaching the injection point, using some other client and it should succeed. +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-client-injection');"); +my $topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name = 'TopMemoryContext'); + +# Attaching to a target process injection point that throws an error +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-server-injection', 'error');" +); + +#Server should have thrown error +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); + +#Query the same process after detaching the injection point, using some other client and it should succeed. +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-injection');"); +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name = 'TopMemoryContext'); + +# Test that two concurrent requests to the same process results in a warning for +# one of those + +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-client-injection', 'wait');"); +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-server-wait', 'wait');"); +my $psql_session1 = $node->background_psql('postgres'); +$psql_session1->query_until( + qr//, + qq( + SELECT pg_get_process_memory_contexts($pid, true); +)); +$node->wait_for_event('client backend', 'memcontext-client-injection'); +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); +ok($psql_err =~ + /WARNING: server process $pid is processing previous request/); +#Wake the client up. +$node->safe_psql('postgres', + "SELECT injection_points_wakeup('memcontext-client-injection');"); + +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-client-injection');"); +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-wait');"); + +# Test the client process exiting with timeout does not break the server process + +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-server-wait', 'wait');"); +# Following client query times out, returning NULL as output +$node->psql( + 'postgres', + qq(select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';), + stdout => \$psql_out); +ok($psql_out = 'NULL'); +#Wakeup the server process up and detach the injection point. +$node->safe_psql('postgres', + "SELECT injection_points_wakeup('memcontext-server-wait');"); +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-wait');"); +#Query the same server process again and it should succeed. +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name = 'TopMemoryContext'); + +# Test if the monitoring works fine, when the client backend crashes. +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-client-injection', 'test_memcontext_reporting', 'crash', NULL);" +); + +#Client will crash +$node->psql( + 'postgres', + qq(select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';), + stderr => \$psql_err); +like($psql_err, + qr/WARNING: terminating connection because of crash of another server process|server closed the connection unexpectedly|connection to server was lost|could not send data to server/ +); + +# Wait till server restarts +$node->restart; +$node->poll_query_until('postgres', "SELECT 1;", '1'); + +#Querying memory stats should succeed after server start +$pid = $node->safe_psql('postgres', + "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name = 'TopMemoryContext'); + +done_testing(); diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c new file mode 100644 index 00000000000..d641f3616dc --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c @@ -0,0 +1,12 @@ +#include "postgres.h" +#include "funcapi.h" + +PG_MODULE_MAGIC; + +extern PGDLLEXPORT void crash(const char *name, const void *private_data, void *arg); + +void +crash(const char *name, const void *private_data, void *arg) +{ + abort(); +} diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control new file mode 100644 index 00000000000..48b501682d5 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control @@ -0,0 +1,4 @@ +comment = 'Test code for memcontext reporting' +default_version = '1.0' +module_pathname = '$libdir/test_memcontext_reporting' +relocatable = true -- 2.34.1 ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2025-12-29 08:56 Rahila Syed <[email protected]> parent: Rahila Syed <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Rahila Syed @ 2025-12-29 08:56 UTC (permalink / raw) To: Daniel Gustafsson <[email protected]>; torikoshia <[email protected]>; pgsql-hackers Hi, I've included some additional description inline and attached rebased patches after CFbot reported a conflict. > *DSA APIs and CFI Handler Safety*: DSA APIs, being high-level, are unsafe > to call from the CFI handler, > which can be invoked from low-level code. This concern was particularly > raised for APIs like `dsa_allocate()` > and `dsa_create()`. > To resolve this, these APIs have been moved out of the CFI handler > function. Now, the dynamic shared memory > needed to store the statistics is allocated and deleted in the client > function. The only operation performed in the CFI > handler is `dsm_attach()`, which attaches to DSA for copying statistics. > Since dsm_attach() only maps the existing > DSM into the current process address space and does not create a new DSM, > I don't see any specific reason why > it would be unsafe to call it from the CFI handler. > > Following are the details about the use of DSM in the patch: - DSA Creation: A Dynamic Shared Area (DSA) is used to store memory context statistics. - Client Process: When fetching memory context statistics, the client allocates a 1 MB chunk in the DSA, reads the statistics from the memory chunk, copies it into a tuple store, and then frees the chunk. - Storage: Pointers to these chunks are stored in a DSHASH table indexed by the client’s proc number. Each entry in the DSHASH table also stores additional metadata related to the client’s request. - Attachment: Backends only attach to the DSM segments for the DSA and DSHASH table when necessary i.e when a process queries memory context statistics or is queried by another backend. Once attached, they remain so until the session ends, at which point they remove their DSHASH entry if any and detach from DSA and DSHASH segments. - Lifecycle: The DSA and DSHASH structures are created upon the first SQL function invocation and destroyed on server restart. > *Error Reported in Thread [2]*: This issue has been fixed by switching to > a NULL resource owner before attaching > to DSM in the CFI handler. > > This error mentioned in thread [2] is triggered during CFI() call from secure_read() when a backend is waiting for commands and it has an open transaction which is going to abort Below are some details about this fix. It is safe to temporarily set the resource owner to NULL before attaching to the DSA and DSHASH, since these segments are intended to be attached for the full session and are detached only when the session ends. We also restore the original resource owner immediately after the attach completes. Other possible fixes include: 1.Adjusting resource‑owner behavior Either allow resource‑owner enlargement during release, or delay marking it as releasing until the abort actually begins. 2. Updating DSM registry APIs (e.g., GetNamedDSA) Detect when the current resource owner is in a releasing state and temporarily set CurrentResourceOwner to NULL before calling dsa_attach. 3. Handling it in the DSA layer This was discussed in thread [2], but concerns were raised that DSA should not compensate for incorrect caller state; the caller must ensure the resource owner is valid. Kindly let me know your views. Thank you, Rahila Syed [2]. PostgreSQL: Re: Prevent an error on attaching/creating a DSM/DSA from an interrupt handler <https://www.postgresql.org/message-id/flat/8B873D49-E0E5-4F9F-B8D6-CA4836B825CD%40yesql.se#7026d2fe4...; Attachments: [application/octet-stream] v46-0002-Test-module-to-test-memory-context-reporting-wit.patch (8.8K, ../../CAH2L28vF7R08NUo6Sme8LydRaFcKhS6fu0psfxUQoxsVVqJMwA@mail.gmail.com/3-v46-0002-Test-module-to-test-memory-context-reporting-wit.patch) download | inline diff: From 6e1e38079e24d1d2fb96f013ee1ca04424564a18 Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Fri, 28 Nov 2025 14:46:38 +0530 Subject: [PATCH 2/2] Test module to test memory context reporting with injection points --- src/test/modules/Makefile | 1 + .../test_memcontext_reporting/Makefile | 29 ++++ .../t/001_memcontext_inj.pl | 150 ++++++++++++++++++ .../test_memcontext_reporting.c | 12 ++ .../test_memcontext_reporting.control | 4 + 5 files changed, 196 insertions(+) create mode 100644 src/test/modules/test_memcontext_reporting/Makefile create mode 100644 src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index 4c6d56d97d8..1156d731014 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -34,6 +34,7 @@ SUBDIRS = \ test_json_parser \ test_lfind \ test_lwlock_tranches \ + test_memcontext_reporting \ test_misc \ test_oat_hooks \ test_parser \ diff --git a/src/test/modules/test_memcontext_reporting/Makefile b/src/test/modules/test_memcontext_reporting/Makefile new file mode 100644 index 00000000000..0a2dfc44f1c --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/Makefile @@ -0,0 +1,29 @@ +# src/test/modules/test_memcontext_reporting/Makefile + +EXTRA_INSTALL = src/test/modules/injection_points + +export enable_injection_points +MODULE_big = test_memcontext_reporting +OBJS = \ + $(WIN32RES) \ + test_memcontext_reporting.o +PGFILEDESC = "test_memcontext_reporting - test code for memory context reporting" + +REGRESS = test_memcontext_reporting + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_memcontext_reporting +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif + +check: + $(prove_check) + +installcheck: + $(prove_installcheck) diff --git a/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl new file mode 100644 index 00000000000..b491d6ebc0a --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl @@ -0,0 +1,150 @@ +# Copyright (c) 2025, PostgreSQL Global Development Group + +# Test suite for testing memory context statistics reporting + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} +my $psql_err; +my $psql_out; +# Create and start a cluster with one node +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->append_conf( + 'postgresql.conf', + qq[ +max_connections = 100 +log_statement = none +restart_after_crash = false +]); +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); + +# Attaching to a client process's injection point that throws an error +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-client-injection', 'error');" +); + +my $pid = $node->safe_psql('postgres', + "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); + +#Client should have thrown error +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); +like($psql_err, + qr/error triggered for injection point memcontext-client-injection/); + +#Query the same process after detaching the injection point, using some other client and it should succeed. +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-client-injection');"); +my $topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name = 'TopMemoryContext'); + +# Attaching to a target process injection point that throws an error +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-server-injection', 'error');" +); + +#Server should have thrown error +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); + +#Query the same process after detaching the injection point, using some other client and it should succeed. +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-injection');"); +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name = 'TopMemoryContext'); + +# Test that two concurrent requests to the same process results in a warning for +# one of those + +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-client-injection', 'wait');"); +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-server-wait', 'wait');"); +my $psql_session1 = $node->background_psql('postgres'); +$psql_session1->query_until( + qr//, + qq( + SELECT pg_get_process_memory_contexts($pid, true); +)); +$node->wait_for_event('client backend', 'memcontext-client-injection'); +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); +ok($psql_err =~ + /WARNING: server process $pid is processing previous request/); +#Wake the client up. +$node->safe_psql('postgres', + "SELECT injection_points_wakeup('memcontext-client-injection');"); + +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-client-injection');"); +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-wait');"); + +# Test the client process exiting with timeout does not break the server process + +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-server-wait', 'wait');"); +# Following client query times out, returning NULL as output +$node->psql( + 'postgres', + qq(select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';), + stdout => \$psql_out); +ok($psql_out = 'NULL'); +#Wakeup the server process up and detach the injection point. +$node->safe_psql('postgres', + "SELECT injection_points_wakeup('memcontext-server-wait');"); +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-wait');"); +#Query the same server process again and it should succeed. +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name = 'TopMemoryContext'); + +# Test if the monitoring works fine, when the client backend crashes. +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-client-injection', 'test_memcontext_reporting', 'crash', NULL);" +); + +#Client will crash +$node->psql( + 'postgres', + qq(select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';), + stderr => \$psql_err); +like($psql_err, + qr/WARNING: terminating connection because of crash of another server process|server closed the connection unexpectedly|connection to server was lost|could not send data to server/ +); + +# Wait till server restarts +$node->restart; +$node->poll_query_until('postgres', "SELECT 1;", '1'); + +#Querying memory stats should succeed after server start +$pid = $node->safe_psql('postgres', + "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name = 'TopMemoryContext'); + +done_testing(); diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c new file mode 100644 index 00000000000..d641f3616dc --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c @@ -0,0 +1,12 @@ +#include "postgres.h" +#include "funcapi.h" + +PG_MODULE_MAGIC; + +extern PGDLLEXPORT void crash(const char *name, const void *private_data, void *arg); + +void +crash(const char *name, const void *private_data, void *arg) +{ + abort(); +} diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control new file mode 100644 index 00000000000..48b501682d5 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control @@ -0,0 +1,4 @@ +comment = 'Test code for memcontext reporting' +default_version = '1.0' +module_pathname = '$libdir/test_memcontext_reporting' +relocatable = true -- 2.34.1 [application/octet-stream] v46-0001-Add-function-to-report-memory-context-statistics.patch (57.9K, ../../CAH2L28vF7R08NUo6Sme8LydRaFcKhS6fu0psfxUQoxsVVqJMwA@mail.gmail.com/4-v46-0001-Add-function-to-report-memory-context-statistics.patch) download | inline diff: From 6b97338f59f073cadbff4d086f2a062b770f9280 Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Thu, 27 Nov 2025 14:39:43 +0530 Subject: [PATCH 1/2] Add function to report memory context statistics This adds a function for retrieving memory context statistics and information from backends as well as auxiliary processes. The intended usecase is cluster debugging when under memory pressure or unanticipated memory usage characteristics. When calling the function it sends a signal to the specified process to submit statistics regarding its memory contexts into dynamic shared memory. Each memory context is returned in detail, followed by a cumulative total in case the number of contexts exceed the max allocated amount of shared memory. Each process is limited to use at most 1Mb memory for this. A summary can also be explicitly requested by the user, this will return the TopMemoryContext and a cumulative total of all lower contexts. In order to not block on busy processes, we have hardcoded the number of seconds during which to retry before timing out. In the case where no statistics are published within the set timeout, NULL is returned --- doc/src/sgml/func/func-admin.sgml | 159 +++ src/backend/catalog/system_functions.sql | 14 + src/backend/postmaster/autovacuum.c | 4 + src/backend/postmaster/checkpointer.c | 4 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 4 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 4 + src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/storage/lmgr/proc.c | 1 + src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 2 + src/backend/utils/adt/mcxtfuncs.c | 1009 ++++++++++++++++- src/backend/utils/init/globals.c | 1 + src/backend/utils/init/postinit.c | 7 + src/backend/utils/mmgr/mcxt.c | 31 + src/include/catalog/pg_proc.dat | 10 + src/include/miscadmin.h | 1 + src/include/storage/lwlocklist.h | 1 + src/include/storage/procsignal.h | 1 + src/include/utils/memutils.h | 8 +- src/test/regress/expected/sysviews.out | 19 + src/test/regress/sql/sysviews.sql | 18 + src/tools/pgindent/typedefs.list | 2 + 25 files changed, 1296 insertions(+), 21 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 2896cd9e429..5eac0e5f73c 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -251,6 +251,132 @@ <literal>false</literal> is returned. </para></entry> </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_get_process_memory_contexts</primary> + </indexterm> + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type> <optional>,<parameter>summary</parameter> <type>boolean</type> <literal>DEFAULT</literal> <literal>false</literal></optional> ) + <returnvalue>setof record</returnvalue> + ( <parameter>name</parameter> <type>text</type>, + <parameter>ident</parameter> <type>text</type>, + <parameter>type</parameter> <type>text</type>, + <parameter>level</parameter> <type>integer</type>, + <parameter>path</parameter> <type>integer[]</type>, + <parameter>total_bytes</parameter> <type>bigint</type>, + <parameter>total_nblocks</parameter> <type>bigint</type>, + <parameter>free_bytes</parameter> <type>bigint</type>, + <parameter>free_chunks</parameter> <type>bigint</type>, + <parameter>used_bytes</parameter> <type>bigint</type>, + <parameter>num_agg_contexts</parameter> <type>integer</type> ) + </para> + <para> + This function handles requests to display the memory contexts of a + <productname>PostgreSQL</productname> process with the specified + process ID. The function can be used to send requests to backends as + well as <glossterm linkend="glossary-auxiliary-proc">auxiliary processes</glossterm>. + If the process does not respond with memory contexts statistics in 5 seconds, + function returns NULL. + </para> + <para> + The returned record contains extended statistics per each memory + context: + <itemizedlist spacing="compact"> + <listitem> + <para> + <parameter>name</parameter> - The name of the memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>ident</parameter> - Memory context ID (if any). + </para> + </listitem> + <listitem> + <para> + <parameter>type</parameter> - The type of memory context, possible + values are: AllocSet, Generation, Slab and Bump. + </para> + </listitem> + <listitem> + <para> + <parameter>level</parameter> - The level in the tree of the current + memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>path</parameter> - Memory contexts are organized in a + tree model with TopMemoryContext as the root, and all other memory + contexts as nodes in the tree. The <parameter>path</parameter> + displays the path from the root to the current memory context. The + path is limited to 100 children per node, which each node limited + to a max depth of 100, to preserve memory during reporting. The + printed path will also be limited to 100 nodes counting from the + TopMemoryContext. + </para> + </listitem> + <listitem> + <para> + <parameter>total_bytes</parameter> - The total number of bytes + allocated to this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_nblocks</parameter> - The total number of blocks + used for the allocated memory. + </para> + </listitem> + <listitem> + <para> + <parameter>free_bytes</parameter> - The amount of free memory in + this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>free_chunks</parameter> - The number of chunks that + <parameter>free_bytes</parameter> corresponds to. + </para> + </listitem> + <listitem> + <para> + <parameter>used_bytes</parameter> - The total number of bytes + currently occupied. + </para> + </listitem> + <listitem> + <para> + <parameter>num_agg_contexts</parameter> - The number of memory + contexts aggregated in the displayed statistics. + </para> + </listitem> + </itemizedlist> + </para> + <para> + When <parameter>summary</parameter> is <literal>true</literal>, statistics + for memory contexts at levels 1 and 2 are displayed, with level 1 + representing the root node (i.e., <literal>TopMemoryContext</literal>). + Statistics for contexts on level 2 and below are aggregates of all + child contexts' statistics, where <literal>num_agg_contexts</literal> + indicate the number aggregated child contexts. When + <parameter>summary</parameter> is <literal>false</literal> (the default), + <literal>the num_agg_contexts</literal> value is <literal>1</literal>, + indicating that individual statistics are being displayed. + </para> + <para> + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context + statistics are aggregated and a cumulative total is displayed. The + <literal>num_agg_contexts</literal> column indicates the number of + contexts aggregated in the displayed statistics. When + <literal>num_agg_contexts</literal> is <literal>1</literal> it means + that the context statistics are displayed separately. + </para></entry> + </row> </tbody> </tgroup> </table> @@ -302,6 +428,39 @@ LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 because it may generate a large number of log messages. </para> + <para> + <function>pg_get_process_memory_contexts</function> can be used to request + memory contexts statistics of any <productname>PostgreSQL</productname> + process. For example: +<programlisting> +postgres=# SELECT * FROM pg_get_process_memory_contexts( + (SELECT pid FROM pg_stat_activity + WHERE backend_type = 'checkpointer'), + false) LIMIT 1; +-[ RECORD 1 ]----+------------------------------ +name | TopMemoryContext +ident | +type | AllocSet +level | 1 +path | {1} +total_bytes | 90304 +total_nblocks | 3 +free_bytes | 2880 +free_chunks | 1 +used_bytes | 87424 +num_agg_contexts | 1 +</programlisting> + <note> + <para> + While <function>pg_get_process_memory_contexts</function> can be used to + query memory contexts of the local backend, + <structname>pg_backend_memory_contexts</structname> + (see <xref linkend="view-pg-backend-memory-contexts"/> for more details) + will be less resource intensive when only the local backend is of interest. + </para> + </note> + </para> + </sect2> <sect2 id="functions-admin-backup"> diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql index 2d946d6d9e9..7b40bac5f57 100644 --- a/src/backend/catalog/system_functions.sql +++ b/src/backend/catalog/system_functions.sql @@ -657,6 +657,17 @@ LANGUAGE INTERNAL STRICT VOLATILE PARALLEL UNSAFE AS 'pg_replication_origin_session_setup'; +CREATE OR REPLACE FUNCTION + pg_get_process_memory_contexts(IN pid integer, IN summary boolean DEFAULT false, + OUT name text, OUT ident text, OUT type text, OUT level integer, + OUT path integer[], OUT total_bytes bigint, OUT total_nblocks bigint, + OUT free_bytes bigint, OUT free_chunks bigint, OUT used_bytes bigint, + OUT num_agg_contexts integer) +RETURNS SETOF RECORD +LANGUAGE INTERNAL +STRICT VOLATILE PARALLEL UNSAFE +AS 'pg_get_process_memory_contexts'; + -- -- The default permissions for functions mean that anyone can execute them. -- A number of functions shouldn't be executable by just anyone, but rather @@ -782,6 +793,7 @@ REVOKE EXECUTE ON FUNCTION pg_ls_logicalmapdir() FROM PUBLIC; REVOKE EXECUTE ON FUNCTION pg_ls_replslotdir(text) FROM PUBLIC; +REVOKE EXECUTE ON FUNCTION pg_get_process_memory_contexts(integer, boolean) FROM PUBLIC; -- -- We also set up some things as accessible to standard roles. -- @@ -808,6 +820,8 @@ GRANT EXECUTE ON FUNCTION pg_current_logfile() TO pg_monitor; GRANT EXECUTE ON FUNCTION pg_current_logfile(text) TO pg_monitor; +GRANT EXECUTE ON FUNCTION pg_get_process_memory_contexts(integer, boolean) TO pg_read_all_stats; + GRANT pg_read_all_settings TO pg_monitor; GRANT pg_read_all_stats TO pg_monitor; diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 1bd3924e35e..baba657904b 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -791,6 +791,10 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index 2eac8ac30d3..a22f77b3673 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -685,6 +685,10 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index ba63b84dfc5..29454b8bf8a 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -48,6 +48,10 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index 3a65d841725..b89617d78db 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -871,6 +871,10 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index 27e86cf393f..7149a67fcbc 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -192,6 +192,10 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index e7e4d652f97..eb86648f7b7 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -879,6 +879,10 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index adebba625e6..a68721b6f4d 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -52,6 +52,7 @@ #include "storage/sinvaladt.h" #include "utils/guc.h" #include "utils/injection_point.h" +#include "utils/memutils.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -141,6 +142,7 @@ CalculateShmemSize(void) size = add_size(size, AioShmemSize()); size = add_size(size, WaitLSNShmemSize()); size = add_size(size, LogicalDecodingCtlShmemSize()); + size = add_size(size, MemoryContextKeysShmemSize() + sizeof(LWLockPadded)); /* include additional requested shmem from preload libraries */ size = add_size(size, total_addin_request); @@ -330,6 +332,7 @@ CreateOrAttachShmemStructs(void) AioShmemInit(); WaitLSNShmemInit(); LogicalDecodingCtlShmemInit(); + MemoryContextKeysShmemInit(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index b0b93d96091..ac4d08ce422 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -694,6 +694,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_GET_MEMORY_CONTEXT)) + HandleGetMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index ebc3f4ca457..27b3b51cf2d 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -51,6 +51,7 @@ #include "storage/procsignal.h" #include "storage/spin.h" #include "storage/standby.h" +#include "utils/memutils.h" #include "utils/timeout.h" #include "utils/timestamp.h" diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 7dd75a490aa..e726f40dfbb 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3539,6 +3539,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index dcfadbd5aae..4df4f8ae121 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -162,6 +162,7 @@ WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." +MEM_CXT_PUBLISH "Waiting for a process to publish memory information." ABI_compatibility: @@ -406,6 +407,7 @@ SubtransSLRU "Waiting to access the sub-transaction SLRU cache." XactSLRU "Waiting to access the transaction status SLRU cache." ParallelVacuumDSA "Waiting for parallel vacuum dynamic shared memory allocation." AioUringCompletion "Waiting for another process to complete IO via io_uring." +MemoryContextReportingKeys "Waiting for another process to complete reading or writing the memory reporting keys." # No "ABI_compatibility" region here as WaitEventLWLock has its own C code. diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index 46dfb3dd133..babf8706513 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -17,22 +17,130 @@ #include "funcapi.h" #include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "storage/dsm_registry.h" #include "storage/proc.h" #include "storage/procarray.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/injection_point.h" +#include "utils/memutils.h" +#include "utils/wait_event_types.h" + +/* + * Memory Context reporting size limits. + */ + +/* Max length of context name and ident, to keep it consistent + * with ProcessLogMemoryContext() + */ +#define MEMORY_CONTEXT_IDENT_SHMEM_SIZE 100 +#define MEMORY_CONTEXT_NAME_SHMEM_SIZE 100 + +/* Maximum size (in bytes) of DSA area per process */ +#define MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND ((size_t) (1 * 1024 * 1024)) + +/* + * Maximum number of memory context statistics is calculated by dividing + * max memory allocated per backend with maximum size per context statistics. + * The identifier and name are statically allocated arrays of size 100 bytes. + * The path depth is limited to 100 like for memory context logging. + */ +#define MAX_MEMORY_CONTEXT_STATS_NUM MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND / (sizeof(MemoryStatsEntry)) + +/* + * Size of dshash key. The key is a uint32 rendered as a string, 10 chars + * plus space for a NULL terminator can hold all the values. + */ +#define CLIENT_KEY_SIZE (10 + 1) + +/* Dynamic shared memory state for reporting statistics per context */ +typedef struct MemoryStatsEntry +{ + char name[MEMORY_CONTEXT_NAME_SHMEM_SIZE]; + char ident[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + int path[100]; + NodeTag type; + int path_length; + int levels; + int64 totalspace; + int64 nblocks; + int64 freespace; + int64 freechunks; + int num_agg_stats; +} MemoryStatsEntry; + +/* + * Per backend dynamic shared hash entry for memory context statistics + * reporting. + */ +typedef struct MemoryStatsDSHashEntry +{ + char key[64]; + ConditionVariable memcxt_cv; + bool stats_written; + int target_server_id; + int total_stats; + bool summary; + dsa_pointer memstats_dsa_pointer; +} MemoryStatsDSHashEntry; + +static const dshash_parameters memctx_dsh_params = { + offsetof(MemoryStatsDSHashEntry, memcxt_cv), + sizeof(MemoryStatsDSHashEntry), + dshash_strcmp, + dshash_strhash, + dshash_strcpy +}; + +/* + * These are used for reporting memory context statistics of a process. + */ + +/* Lock to control access to client_keys array */ +static LWLock *client_keys_lock = NULL; + +/* Array to store the keys of MemoryStatsDsHash */ +static int *client_keys = NULL; + +/* + * Table to store pointers to DSA memory containing memory statistics and other + * metadata. There is one entry per client backend request, keyed by ProcNumber + * of the client obtained from client_keys array above. + */ +static dshash_table *MemoryStatsDsHash = NULL; + +/* + * Dsa area which stores the actual memory context + * statistics. + */ +static dsa_area *MemoryStatsDsaArea = NULL; + +static void memstats_dsa_cleanup(char *key); +static void memstats_client_key_reset(int ProcNumber); +static const char *ContextTypeToString(NodeTag type); +static void PublishMemoryContext(MemoryStatsEntry *memcxt_info, + int curr_id, MemoryContext context, + List *path, + MemoryContextCounters stat, + int num_contexts); +static List *compute_context_path(MemoryContext c, HTAB *context_id_lookup); /* ---------- * The max bytes for showing identifiers of MemoryContext. + * This is used by pg_get_backend_memory_context - view used for local backend. * ---------- */ #define MEMORY_CONTEXT_IDENT_DISPLAY_SIZE 1024 +#define MAX_PATH_DISPLAY_LENGTH 100 +/* Timeout in seconds */ +#define MEMORY_STATS_MAX_TIMEOUT 5 + /* * MemoryContextId - * Used for storage of transient identifiers for - * pg_get_backend_memory_contexts. + * Used for storage of transient identifiers for memory context reporting */ typedef struct MemoryContextId { @@ -143,24 +251,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, else nulls[1] = true; - switch (context->type) - { - case T_AllocSetContext: - type = "AllocSet"; - break; - case T_GenerationContext: - type = "Generation"; - break; - case T_SlabContext: - type = "Slab"; - break; - case T_BumpContext: - type = "Bump"; - break; - default: - type = "???"; - break; - } + type = ContextTypeToString(context->type); values[2] = CStringGetTextDatum(type); values[3] = Int32GetDatum(list_length(path)); /* level */ @@ -175,6 +266,38 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, list_free(path); } +/* + * ContextTypeToString + * Returns a textual representation of a context type + * + * This should cover the same types as MemoryContextIsValid. + */ +const char * +ContextTypeToString(NodeTag type) +{ + const char *context_type; + + switch (type) + { + case T_AllocSetContext: + context_type = "AllocSet"; + break; + case T_GenerationContext: + context_type = "Generation"; + break; + case T_SlabContext: + context_type = "Slab"; + break; + case T_BumpContext: + context_type = "Bump"; + break; + default: + context_type = "???"; + break; + } + return context_type; +} + /* * pg_get_backend_memory_contexts * SQL SRF showing backend memory context. @@ -305,3 +428,849 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } + +/* + * pg_get_process_memory_contexts + * Signal a backend or an auxiliary process to send its memory contexts, + * wait for the results and display them. + * + * By default, only superusers or users with ROLE_PG_READ_ALL_STATS are allowed + * to signal a process to return the memory contexts. Additional roles can be + * permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to copy the memory context details + * to a dynamic shared memory space. + * + * We have defined a limit on DSA memory that could be allocated per process - + * if the process has more memory contexts than what can fit in the allocated + * size, the excess contexts are summarized and represented as cumulative total + * at the end of the buffer. + * + * After sending the signal, wait on a condition variable. The publishing + * backend, after copying the data to shared memory, sends a signal on that + * condition variable. There is one condition variable per client process. + * Once the condition variable is signalled, check if the latest memory context + * information is available and display. + * + * If the publishing backend does not respond before the condition variable + * times out, which is set to a predefined value MEMORY_STATS_MAX_TIMEOUT, give + * up and return NULL. + */ +Datum +pg_get_process_memory_contexts(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + bool summary = PG_GETARG_BOOL(1); + PGPROC *proc; + ProcNumber procNumber; + bool proc_is_aux = false; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + MemoryStatsEntry *memcxt_info; + MemoryStatsDSHashEntry *entry; + bool found; + char key[CLIENT_KEY_SIZE]; + TimestampTz start_timestamp; + + /* + * See if the process with given pid is a backend or an auxiliary process + * and remember the type for when we requery the process later. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + { + proc = AuxiliaryPidGetProc(pid); + proc_is_aux = true; + } + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; this is however not a problem and leave with a WARNING. + * See comment in pg_log_backend_memory_contexts for a discussion on this. + */ + if (proc == NULL) + { + ereport(WARNING, + errmsg("PID %d is not a PostgreSQL server process", pid)); + PG_RETURN_NULL(); + } + + procNumber = GetNumberFromPGProc(proc); + + /* + * Check if the server process slot is not empty and exit early Non-empty + * slot means some other client backend is requesting the statistics from + * the same server process. + */ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + if (client_keys[procNumber] != -1) + { + LWLockRelease(client_keys_lock); + ereport(WARNING, + errmsg("server process %d is processing previous request", + pid)); + PG_RETURN_NULL(); + } + LWLockRelease(client_keys_lock); + + /* + * Create a DSA to allocate memory for copying memory contexts statistics. + * Allocate the memory in the DSA and send DSA pointer to the server + * process for storing the context statistics. If number of contexts + * exceed a predefined limit (1MB), a cumulative total is stored for such + * contexts. + * + * The DSA is created once for the lifetime of the server, and only + * attached in subsequent calls. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); + + /* + * The DSA pointers containing statistics for each client are stored in a + * dshash table. In addition to DSA pointer, each entry in this table also + * contains information about the statistics, condition variable for + * signalling between client and the server and miscellaneous data + * specific to a request. There is one entry per client request in the + * hash table. + */ + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); + + snprintf(key, sizeof(key), "%d", MyProcNumber); + + /* + * Insert an entry for this client in DSHASH table the first time this + * function is called. This entry is deleted when the process exits in + * before_shmem_exit call. + * + * dshash_find_or_insert locks the entry to prevent the publisher from + * reading before client has updated the entry. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + if (!found) + { + entry->stats_written = false; + ConditionVariableInit(&entry->memcxt_cv); + } + + /* + * Allocate 1MB of memory for the backend to publish its statistics on + * every call to this function. The memory is freed at the end of the + * function. + */ + entry->memstats_dsa_pointer = + dsa_allocate0(MemoryStatsDsaArea, MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND); + + /* + * Specify whether a summary of statistics is requested, before signalling + * the server. + */ + entry->summary = summary; + + /* + * Indicate which server process statistics are being requested from. If + * this client times out before the last requested process can publish its + * statistics, it may send a new request to another server process. Since + * the previous server was notified, it might attempt to read the same + * client entry and respond incorrectly with its statistics. By storing + * the server ID in the client entry, we prevent any previously signalled + * server process from writing its statistics in the space meant for the + * newly requested process. + */ + entry->target_server_id = pid; + dshash_release_lock(MemoryStatsDsHash, entry); + + /* + * Check if the publishing process slot is empty and store this clients + * key i.e its procNumber. This informs the publishing process that it is + * supposed to write statistics in the hash entry corresponding to this + * client. + */ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + if (client_keys[procNumber] == -1) + client_keys[procNumber] = MyProcNumber; + else + { + LWLockRelease(client_keys_lock); + ereport(WARNING, + errmsg("server process %d is processing previous request", + pid)); + PG_RETURN_NULL(); + } + LWLockRelease(client_keys_lock); + + /* + * Send a signal to a PostgreSQL process, informing it we want it to + * produce information about its memory contexts. + */ + if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, procNumber) < 0) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ereport(WARNING, + errmsg("could not send signal to process %d: %m", + pid)); + PG_RETURN_NULL(); + } + start_timestamp = GetCurrentTimestamp(); + + while (1) + { + long elapsed_time; + + INJECTION_POINT("memcontext-client-injection", NULL); + + elapsed_time = TimestampDifferenceMilliseconds(start_timestamp, + GetCurrentTimestamp()); + /* Return if we have already exceeded the timeout */ + if (elapsed_time >= MEMORY_STATS_MAX_TIMEOUT * 1000) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + PG_RETURN_NULL(); + } + + /* + * Recheck the state of the backend before sleeping on the condition + * variable to ensure the process is still alive. Only check the + * relevant process type based on the earlier PID check. + */ + if (proc_is_aux) + proc = AuxiliaryPidGetProc(pid); + else + proc = BackendPidGetProc(pid); + + /* + * The target server process ending during memory context processing + * is not an error. + */ + if (proc == NULL) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + ereport(WARNING, + errmsg("PID %d is no longer a PostgreSQL server process", + pid)); + PG_RETURN_NULL(); + } + + /* + * Wait for MEMORY_STATS_MAX_TIMEOUT. If no statistics are available + * within the allowed time then return NULL. The timer is defined in + * milliseconds since that's what the condition variable sleep uses. + */ + if (ConditionVariableTimedSleep(&entry->memcxt_cv, + (MEMORY_STATS_MAX_TIMEOUT * 1000), + WAIT_EVENT_MEM_CXT_PUBLISH)) + { + /* Timeout has expired, return NULL */ + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + PG_RETURN_NULL(); + } + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + Assert(found); + + memcxt_info = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + /* + * We expect to come out of sleep when the requested process has + * finished publishing the statistics, verified using a boolean + * stats_written. + * + * Make sure that the statistics are actually written by checking that + * the name of the context is not NULL. This is done to ensure that + * the subsequent waits for statistics do not return spuriously if the + * previous call to the function ended in error and thus could not + * clear the stats_written flag. + */ + if (entry->stats_written && memcxt_info[0].name[0] != '\0') + break; + + dshash_release_lock(MemoryStatsDsHash, entry); + + } + + InitMaterializedSRF(fcinfo, 0); + + /* + * Backend has finished publishing the stats, project them. + */ +#define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 11 + for (int i = 0; i < entry->total_stats; i++) + { + ArrayType *path_array; + int path_length; + Datum values[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + bool nulls[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + Datum *path_datum = NULL; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + Assert(memcxt_info[i].name[0] != '\0'); + values[0] = CStringGetTextDatum(memcxt_info[i].name); + + if (memcxt_info[i].ident[0] != '\0') + values[1] = CStringGetTextDatum(memcxt_info[i].ident); + else + nulls[1] = true; + + values[2] = CStringGetTextDatum(ContextTypeToString(memcxt_info[i].type)); + values[3] = Int32GetDatum(memcxt_info[i].levels); + + path_length = memcxt_info[i].path_length; + path_datum = (Datum *) palloc(path_length * sizeof(Datum)); + if (memcxt_info[i].path[0] != 0) + { + for (int j = 0; j < path_length; j++) + path_datum[j] = Int32GetDatum(memcxt_info[i].path[j]); + path_array = construct_array_builtin(path_datum, + path_length, + INT4OID); + values[4] = PointerGetDatum(path_array); + } + else + nulls[4] = true; + + values[5] = Int64GetDatum(memcxt_info[i].totalspace); + values[6] = Int64GetDatum(memcxt_info[i].nblocks); + values[7] = Int64GetDatum(memcxt_info[i].freespace); + values[8] = Int64GetDatum(memcxt_info[i].freechunks); + values[9] = Int64GetDatum(memcxt_info[i].totalspace - + memcxt_info[i].freespace); + values[10] = Int32GetDatum(memcxt_info[i].num_agg_stats); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + dshash_release_lock(MemoryStatsDsHash, entry); + memstats_dsa_cleanup(key); + + ConditionVariableCancelSleep(); + + PG_RETURN_NULL(); +} + +static void +memstats_dsa_cleanup(char *key) +{ + MemoryStatsDSHashEntry *entry; + + entry = dshash_find(MemoryStatsDsHash, key, true); + + Assert(MemoryStatsDsaArea != NULL); + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + entry->memstats_dsa_pointer = InvalidDsaPointer; + entry->stats_written = false; + entry->target_server_id = 0; + + dshash_release_lock(MemoryStatsDsHash, entry); +} + +/* + * Remove this process from the publishing process' + * client key slot, if the stats publishing process has failed to do so. + */ +static void +memstats_client_key_reset(int procNumber) +{ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + + if (client_keys[procNumber] == MyProcNumber) + client_keys[procNumber] = -1; + LWLockRelease(client_keys_lock); +} + +void +MemoryContextKeysShmemInit(void) +{ + bool found; + + client_keys = (int *) + ShmemInitStruct("MemoryContextKeys", + MemoryContextKeysShmemSize() + sizeof(LWLockPadded), &found); + client_keys_lock = (LWLock *) ((char *) client_keys + MemoryContextKeysShmemSize()); + + if (!found) + { + MemSet(client_keys, -1, MemoryContextKeysShmemSize()); + LWLockInitialize(client_keys_lock, LWTRANCHE_MEMORY_CONTEXT_KEYS); + } +} + +Size +MemoryContextKeysShmemSize(void) +{ + Size sz = 0; + Size TotalProcs = 0; + + TotalProcs = add_size(TotalProcs, NUM_AUXILIARY_PROCS); + TotalProcs = add_size(TotalProcs, MaxBackends); + sz = add_size(sz, mul_size(TotalProcs, sizeof(int))); + + return sz; +} + +/* + * HandleGetMemoryContextInterrupt + * Handle receipt of an interrupt indicating a request to publish memory + * contexts statistics. + * + * All the actual work is deferred to ProcessGetMemoryContextInterrupt() as + * this cannot be performed in a signal handler. + */ +void +HandleGetMemoryContextInterrupt(void) +{ + InterruptPending = true; + PublishMemoryContextPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + +/* + * ProcessGetMemoryContextInterrupt + * Generate information about memory contexts used by the process. + * + * Performs a breadth first search on the memory context tree, thus parents + * statistics are reported before their children in the monitoring function + * output. + * + * Statistics for all the processes are shared via the same dynamic shared + * area. Individual statistics are tracked independently in per-process DSA + * pointers. These pointers are stored in a dshash table with key as requesting + * clients ProcNumber. + * + * We calculate maximum number of context's statistics that can be displayed + * using a pre-determined limit for memory available per process for this + * utility and maximum size of statistics for each context. The remaining + * context statistics if any are captured as a cumulative total at the end of + * individual context's statistics. + * + * If summary is true, we capture the level 1 and level 2 contexts statistics. + * For that we traverse the memory context tree recursively in depth first + * search manner to cover all the children of a parent context, to be able to + * display a cumulative total of memory consumption by a parent at level 2 and + * all its children. + */ +void +ProcessGetMemoryContextInterrupt(void) +{ + List *contexts; + HASHCTL ctl; + HTAB *context_id_lookup; + int context_id = 0; + MemoryStatsEntry *meminfo; + MemoryContextCounters stat; + int num_individual_stats = 0; + bool found; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + int clientProcNumber; + MemoryContext memstats_ctx = NULL; + MemoryContext oldcontext = NULL; + ResourceOwner currentOwner; + + PublishMemoryContextPending = false; + + INJECTION_POINT("memcontext-server-wait", NULL); + + /* + * Retrieve the client key for publishing statistics and reset it to -1, + * so other clients can request memory statistics from this process. + * Return if the client_key is -1, which means the requesting client has + * timed out. + */ + LWLockAcquire(client_keys_lock, LW_SHARED); + if (client_keys[MyProcNumber] == -1) + { + LWLockRelease(client_keys_lock); + return; + } + else + { + clientProcNumber = client_keys[MyProcNumber]; + client_keys[MyProcNumber] = -1; + LWLockRelease(client_keys_lock); + } + + /* + * Create a new memory context which is not a part of TopMemoryContext + * tree. This context is used to allocate all memory in this function. + * This helps in keeping the memory allocation in this function to report + * memory consumption statistics separate. So that it does not affect the + * output of this function. + */ + memstats_ctx = AllocSetContextCreate((MemoryContext) NULL, + "publish_memory_context_statistics", + ALLOCSET_SMALL_SIZES); + oldcontext = MemoryContextSwitchTo(memstats_ctx); + + /* + * The hash table is used for constructing "path" column of the view, + * similar to its local backend counterpart. + */ + ctl.keysize = sizeof(MemoryContext); + ctl.entrysize = sizeof(MemoryContextId); + ctl.hcxt = CurrentMemoryContext; + + context_id_lookup = hash_create("pg_get_process_memory_contexts", + 256, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + /* List of contexts to process in the next round - start at the top. */ + contexts = list_make1(TopMemoryContext); + + currentOwner = CurrentResourceOwner; + CurrentResourceOwner = NULL; + /* + * The client process should have created the required DSA and DSHash + * table. Here we just attach to those. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); + + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); + CurrentResourceOwner = currentOwner; + + snprintf(key, CLIENT_KEY_SIZE, "%d", clientProcNumber); + + /* + * The entry lock is held by dshash_find_or_insert to protect writes to + * process specific memory. Two different processes publishing statistics + * do not block each other. + */ + INJECTION_POINT("memcontext-server-injection", NULL); + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + /* + * Check if the entry has been deleted due to calling process exiting, or + * if the caller has timed out waiting for us and have issued a request to + * another backend. + * + * Make sure that the client always deletes the entry after taking + * required lock or this function may end up writing to unallocated + * memory. + */ + if (!found || entry->target_server_id != MyProcPid) + { + entry->stats_written = false; + + dshash_release_lock(MemoryStatsDsHash, entry); + + hash_destroy(context_id_lookup); + MemoryContextSwitchTo(oldcontext); + MemoryContextReset(memstats_ctx); + + return; + } + + /* Should be allocated by a client backend that is requesting statistics */ + Assert(entry->memstats_dsa_pointer != InvalidDsaPointer); + meminfo = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + if (entry->summary) + { + int cxt_id = 0; + List *path = NIL; + MemoryContextId *contextid_entry; + + /* Copy TopMemoryContext statistics to DSA */ + memset(&stat, 0, sizeof(stat)); + (*TopMemoryContext->methods->stats) (TopMemoryContext, NULL, NULL, + &stat, true); + path = lcons_int(1, path); + PublishMemoryContext(meminfo, cxt_id, TopMemoryContext, path, stat, + 1); + + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &TopMemoryContext, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = cxt_id + 1; + + /* + * Copy statistics for each of TopMemoryContexts children. This + * includes statistics of at most 100 children per node, with each + * child node limited to a depth of 100 in its subtree. + */ + for (MemoryContext c = TopMemoryContext->firstchild; c != NULL; + c = c->nextchild) + { + MemoryContextCounters grand_totals; + int num_contexts = 0; + + path = NIL; + memset(&grand_totals, 0, sizeof(grand_totals)); + + cxt_id++; + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &c, HASH_ENTER, &found); + Assert(!found); + contextid_entry->context_id = cxt_id + 1; + + MemoryContextStatsCounter(c, &grand_totals, &num_contexts); + + path = compute_context_path(c, context_id_lookup); + + PublishMemoryContext(meminfo, cxt_id, c, path, + grand_totals, num_contexts); + } + entry->total_stats = cxt_id + 1; + } + else + { + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + MemoryContextId *contextid_entry; + + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &cur, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = context_id + 1; + + /* + * Figure out the transient context_id of this context and each of + * its ancestors, to compute a path for this context. + */ + path = compute_context_path(cur, context_id_lookup); + + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + /* Account for saving one statistics slot for cumulative reporting */ + if (context_id < (MAX_MEMORY_CONTEXT_STATS_NUM - 1)) + { + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1); + } + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].totalspace += stat.totalspace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].nblocks += stat.nblocks; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freespace += stat.freespace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit per process is reached, write aggregate of the + * remaining statistics. + * + * We can store contexts from 0 to max_stats - 1. When context_id + * is greater than max_stats, we stop reporting individual + * statistics when context_id equals max_stats - 2. As we use + * max_stats - 1 array slot for reporting cumulative statistics or + * "Remaining Totals". + */ + if (context_id == (MAX_MEMORY_CONTEXT_STATS_NUM - 2)) + { + int namelen = strlen("Remaining Totals"); + + num_individual_stats = context_id + 1; + strlcpy(meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].name, + "Remaining Totals", namelen + 1); + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].ident[0] = '\0'; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].path[0] = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].type = 0; + } + context_id++; + + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + contexts = lappend(contexts, c); + } + + /* + * Check if there are aggregated statistics or not in the result set. + * Statistics are individually reported when context_id <= max_stats, + * only if context_id > max_stats will there be aggregates. + */ + if (context_id <= MAX_MEMORY_CONTEXT_STATS_NUM) + { + entry->total_stats = context_id; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = 1; + } + + /* + * The number of contexts exceeded the space available, so report the + * number of aggregated memory contexts + */ + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = + context_id - num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for + * cumulative statistics. + */ + entry->total_stats = num_individual_stats + 1; + } + } + + entry->stats_written = true; + dshash_release_lock(MemoryStatsDsHash, entry); + hash_destroy(context_id_lookup); + + MemoryContextSwitchTo(oldcontext); + MemoryContextReset(memstats_ctx); + /* Notify waiting client backend and return */ + ConditionVariableSignal(&entry->memcxt_cv); +} + +/* + * compute_context_path + * + * Append the transient context_id of this context and each of its ancestors + * to a list, in order to compute a path. + */ +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + MemoryContext cur_context; + + for (cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + elog(ERROR, "hash table corrupted, can't construct path value"); + + path = lcons_int(cur_entry->context_id, path); + } + + return path; +} + +/* + * PublishMemoryContext + * + * Copy the memory context statistics of a single context to a DSA memory + */ +static void +PublishMemoryContext(MemoryStatsEntry *memcxt_info, int curr_id, + MemoryContext context, List *path, + MemoryContextCounters stat, int num_contexts) +{ + char *ident = unconstify(char *, context->ident); + char *name = unconstify(char *, context->name); + + /* + * To be consistent with logging output, we label dynahash contexts with + * just the hash table name as with MemoryContextStatsPrint(). + */ + if (context->ident && strncmp(context->name, "dynahash", 8) == 0) + { + name = unconstify(char *, context->ident); + ident = NULL; + } + + if (name != NULL) + { + int namelen = strlen(name); + + if (namelen >= MEMORY_CONTEXT_NAME_SHMEM_SIZE) + namelen = pg_mbcliplen(name, namelen, + MEMORY_CONTEXT_NAME_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].name, name, namelen + 1); + } + else + /* Clearing the array */ + memcxt_info[curr_id].name[0] = '\0'; + + /* Trim and copy the identifier if it is not set to NULL */ + if (ident != NULL) + { + int idlen = strlen(context->ident); + + /* + * Some identifiers such as SQL query string can be very long, + * truncate oversize identifiers. + */ + if (idlen >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + idlen = pg_mbcliplen(ident, idlen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].ident, ident, idlen + 1); + } + else + memcxt_info[curr_id].ident[0] = '\0'; + + /* Store the path */ + if (path == NIL) + memcxt_info[curr_id].path[0] = 0; + else + { + int levels = Min(list_length(path), MAX_PATH_DISPLAY_LENGTH); + + memcxt_info[curr_id].path_length = levels; + memcxt_info[curr_id].levels = list_length(path); + + foreach_int(i, path) + { + memcxt_info[curr_id].path[foreach_current_index(i)] = i; + if (--levels == 0) + break; + } + } + memcxt_info[curr_id].type = context->type; + memcxt_info[curr_id].totalspace = stat.totalspace; + memcxt_info[curr_id].nblocks = stat.nblocks; + memcxt_info[curr_id].freespace = stat.freespace; + memcxt_info[curr_id].freechunks = stat.freechunks; + memcxt_info[curr_id].num_agg_stats = num_contexts; +} + +void +AtProcExit_memstats_cleanup(int code, Datum arg) +{ + int idx = MyProcNumber; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + bool found; + + if (MemoryStatsDsHash != NULL) + { + snprintf(key, CLIENT_KEY_SIZE, "%d", idx); + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + if (found) + { + if (MemoryStatsDsaArea != NULL && + DsaPointerIsValid(entry->memstats_dsa_pointer)) + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + } + dshash_delete_entry(MemoryStatsDsHash, entry); + } + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + client_keys[idx] = -1; + LWLockRelease(client_keys_lock); +} diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index d31cb45a058..92b0446b80c 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -39,6 +39,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t PublishMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index b7e94ca45bd..942ee5c34f4 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -661,6 +661,13 @@ BaseInit(void) * drop ephemeral slots, which in turn triggers stats reporting. */ ReplicationSlotInitialize(); + + /* + * The before shmem exit callback frees the DSA memory occupied by the + * latest memory context statistics that could be published by this proc + * if requested. + */ + before_shmem_exit(AtProcExit_memstats_cleanup, 0); } diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 5c1a06d86fd..d601069b99f 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -1011,6 +1011,37 @@ MemoryContextStatsInternal(MemoryContext context, int level, } } + +/* + * MemoryContextStatsCounter + * + * Accumulate statistics counts into *totals. totals should not be NULL. + * This involves a non-recursive tree traversal. + */ +void +MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts) +{ + int ichild = 1; + + *num_contexts = 0; + context->methods->stats(context, NULL, NULL, totals, false); + + for (MemoryContext curr = context->firstchild; + curr != NULL; + curr = MemoryContextTraverseNext(curr, context)) + { + curr->methods->stats(curr, NULL, NULL, totals, false); + ichild++; + } + + /* + * Add the count of all the children contexts which are traversed + * including the parent. + */ + *num_contexts = *num_contexts + ichild; +} + /* * MemoryContextStatsPrint * Print callback used by MemoryContextStatsInternal diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index fd9448ec7b9..bef24d625d9 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8617,6 +8617,16 @@ prorettype => 'bool', proargtypes => 'int4', prosrc => 'pg_log_backend_memory_contexts' }, +# publishing memory contexts of the specified postgres process +{ oid => '2173', descr => 'publish memory contexts of the specified backend', + proname => 'pg_get_process_memory_contexts', provolatile => 'v', + prorows => '100', proretset => 't', proparallel => 'r', + prorettype => 'record', proargtypes => 'int4 bool', + proallargtypes => '{int4,bool,text,text,text,int4,_int4,int8,int8,int8,int8,int8,int4}', + proargmodes => '{i,i,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{pid, summary, name, ident, type, level, path, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes, num_agg_contexts}', + prosrc => 'pg_get_process_memory_contexts' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index 9a7d733ddef..b76f24baed6 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -96,6 +96,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t PublishMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h index 533344509e9..8f83c8801a7 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -137,3 +137,4 @@ PG_LWLOCKTRANCHE(SUBTRANS_SLRU, SubtransSLRU) PG_LWLOCKTRANCHE(XACT_SLRU, XactSLRU) PG_LWLOCKTRANCHE(PARALLEL_VACUUM_DSA, ParallelVacuumDSA) PG_LWLOCKTRANCHE(AIO_URING_COMPLETION, AioUringCompletion) +PG_LWLOCKTRANCHE(MEMORY_CONTEXT_KEYS, MemoryContextReportingKeys) diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index 8e428f298c6..1e5f5b1f957 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -35,6 +35,7 @@ typedef enum PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to send the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ /* Recovery conflict reasons */ diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 7bbe5a36959..617de0ebf91 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -19,7 +19,6 @@ #include "nodes/memnodes.h" - /* * MaxAllocSize, MaxAllocHugeSize * Quasi-arbitrary limits on size of allocations. @@ -319,4 +318,11 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) return true; } +extern void ProcessGetMemoryContextInterrupt(void); +extern void HandleGetMemoryContextInterrupt(void); +extern void MemoryContextKeysShmemInit(void); +extern Size MemoryContextKeysShmemSize(void); +extern void MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts); +extern void AtProcExit_memstats_cleanup(int code, Datum arg); #endif /* MEMUTILS_H */ diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 0411db832f1..3799ef7c862 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -233,3 +233,22 @@ select * from pg_timezone_abbrevs where abbrev = 'LMT'; LMT | @ 7 hours 52 mins 58 secs ago | f (1 row) +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; +NOTICE: (AllocSet,TopMemoryContext,) +NOTICE: (AllocSet,TopMemoryContext,) diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 66179f026b3..c9da4fc8c90 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -101,3 +101,21 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs; -- One specific case we can check without much fear of breakage -- is the historical local-mean-time value used for America/Los_Angeles. select * from pg_timezone_abbrevs where abbrev = 'LMT'; + +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index ceb3fc5d980..36d76262030 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1701,6 +1701,8 @@ MemoryContextData MemoryContextId MemoryContextMethodID MemoryContextMethods +MemoryStatsEntry +MemoryStatsDSHashEntry MemoryStatsPrintFunc MergeAction MergeActionState -- 2.34.1 ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2026-01-09 11:22 Rahila Syed <[email protected]> parent: Rahila Syed <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Rahila Syed @ 2026-01-09 11:22 UTC (permalink / raw) To: Daniel Gustafsson <[email protected]>; torikoshia <[email protected]>; pgsql-hackers Hi, >> *Error Reported in Thread [2]*: This issue has been fixed by switching >> to a NULL resource owner before attaching >> to DSM in the CFI handler. >> >> > This error mentioned in thread [2] is triggered during CFI() call from > secure_read() when a > backend is waiting for commands and it has an open transaction which is > going to abort > > Below are some details about this fix. > > It is safe to temporarily set the resource owner to NULL before attaching > to the DSA > and DSHASH, since these segments are intended to be attached for the full > session > and are detached only when the session ends. > We also restore the original resource owner immediately after the attach > completes. > > After further discussion and reviewing Robert's email[1] on this topic, a safer solution is to avoid running ProcessGetMemoryContextInterrupt during an aborted transaction. This should help prevent additional errors when the transaction is already in error handling state. Also, reporting memory context statistics from an aborting transaction won't be very useful as some of that memory usage won't be valid after abort completes. Attached is the updated patch that addresses this. > Other possible fixes include: > 1.Adjusting resource‑owner behavior > Either allow resource‑owner enlargement during release, or delay marking > it as releasing until > the abort actually begins. > Sorry, this point is invalid as resource-owner is already being marked as releasing from AbortTransaction. Thank you, Rahila Syed [1]. https://www.postgresql.org/message-id/CA%2BTgmoapJ6erjT21uPO12wTtoOmj6w-dp6T3qySN%2BNSc1cdEKw%40mail... > Attachments: [application/octet-stream] v47-0002-Test-module-to-test-memory-context-reporting-wit.patch (8.8K, ../../CAH2L28vqKDTogugPnr54hY8k6enP02U_qMBUtnmo7CbA6OfhHQ@mail.gmail.com/3-v47-0002-Test-module-to-test-memory-context-reporting-wit.patch) download | inline diff: From 83cf295b1c90fb43c7939c98f66e9aa4159d923f Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Fri, 28 Nov 2025 14:46:38 +0530 Subject: [PATCH 2/2] Test module to test memory context reporting with injection points --- src/test/modules/Makefile | 1 + .../test_memcontext_reporting/Makefile | 29 ++++ .../t/001_memcontext_inj.pl | 150 ++++++++++++++++++ .../test_memcontext_reporting.c | 12 ++ .../test_memcontext_reporting.control | 4 + 5 files changed, 196 insertions(+) create mode 100644 src/test/modules/test_memcontext_reporting/Makefile create mode 100644 src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index 4c6d56d97d8..1156d731014 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -34,6 +34,7 @@ SUBDIRS = \ test_json_parser \ test_lfind \ test_lwlock_tranches \ + test_memcontext_reporting \ test_misc \ test_oat_hooks \ test_parser \ diff --git a/src/test/modules/test_memcontext_reporting/Makefile b/src/test/modules/test_memcontext_reporting/Makefile new file mode 100644 index 00000000000..0a2dfc44f1c --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/Makefile @@ -0,0 +1,29 @@ +# src/test/modules/test_memcontext_reporting/Makefile + +EXTRA_INSTALL = src/test/modules/injection_points + +export enable_injection_points +MODULE_big = test_memcontext_reporting +OBJS = \ + $(WIN32RES) \ + test_memcontext_reporting.o +PGFILEDESC = "test_memcontext_reporting - test code for memory context reporting" + +REGRESS = test_memcontext_reporting + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_memcontext_reporting +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif + +check: + $(prove_check) + +installcheck: + $(prove_installcheck) diff --git a/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl new file mode 100644 index 00000000000..b491d6ebc0a --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl @@ -0,0 +1,150 @@ +# Copyright (c) 2025, PostgreSQL Global Development Group + +# Test suite for testing memory context statistics reporting + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} +my $psql_err; +my $psql_out; +# Create and start a cluster with one node +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->append_conf( + 'postgresql.conf', + qq[ +max_connections = 100 +log_statement = none +restart_after_crash = false +]); +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); + +# Attaching to a client process's injection point that throws an error +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-client-injection', 'error');" +); + +my $pid = $node->safe_psql('postgres', + "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); + +#Client should have thrown error +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); +like($psql_err, + qr/error triggered for injection point memcontext-client-injection/); + +#Query the same process after detaching the injection point, using some other client and it should succeed. +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-client-injection');"); +my $topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name = 'TopMemoryContext'); + +# Attaching to a target process injection point that throws an error +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-server-injection', 'error');" +); + +#Server should have thrown error +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); + +#Query the same process after detaching the injection point, using some other client and it should succeed. +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-injection');"); +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name = 'TopMemoryContext'); + +# Test that two concurrent requests to the same process results in a warning for +# one of those + +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-client-injection', 'wait');"); +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-server-wait', 'wait');"); +my $psql_session1 = $node->background_psql('postgres'); +$psql_session1->query_until( + qr//, + qq( + SELECT pg_get_process_memory_contexts($pid, true); +)); +$node->wait_for_event('client backend', 'memcontext-client-injection'); +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); +ok($psql_err =~ + /WARNING: server process $pid is processing previous request/); +#Wake the client up. +$node->safe_psql('postgres', + "SELECT injection_points_wakeup('memcontext-client-injection');"); + +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-client-injection');"); +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-wait');"); + +# Test the client process exiting with timeout does not break the server process + +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-server-wait', 'wait');"); +# Following client query times out, returning NULL as output +$node->psql( + 'postgres', + qq(select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';), + stdout => \$psql_out); +ok($psql_out = 'NULL'); +#Wakeup the server process up and detach the injection point. +$node->safe_psql('postgres', + "SELECT injection_points_wakeup('memcontext-server-wait');"); +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-wait');"); +#Query the same server process again and it should succeed. +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name = 'TopMemoryContext'); + +# Test if the monitoring works fine, when the client backend crashes. +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-client-injection', 'test_memcontext_reporting', 'crash', NULL);" +); + +#Client will crash +$node->psql( + 'postgres', + qq(select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';), + stderr => \$psql_err); +like($psql_err, + qr/WARNING: terminating connection because of crash of another server process|server closed the connection unexpectedly|connection to server was lost|could not send data to server/ +); + +# Wait till server restarts +$node->restart; +$node->poll_query_until('postgres', "SELECT 1;", '1'); + +#Querying memory stats should succeed after server start +$pid = $node->safe_psql('postgres', + "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name = 'TopMemoryContext'); + +done_testing(); diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c new file mode 100644 index 00000000000..d641f3616dc --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c @@ -0,0 +1,12 @@ +#include "postgres.h" +#include "funcapi.h" + +PG_MODULE_MAGIC; + +extern PGDLLEXPORT void crash(const char *name, const void *private_data, void *arg); + +void +crash(const char *name, const void *private_data, void *arg) +{ + abort(); +} diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control new file mode 100644 index 00000000000..48b501682d5 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control @@ -0,0 +1,4 @@ +comment = 'Test code for memcontext reporting' +default_version = '1.0' +module_pathname = '$libdir/test_memcontext_reporting' +relocatable = true -- 2.34.1 [application/octet-stream] v47-0001-Add-function-to-report-memory-context-statistics.patch (57.9K, ../../CAH2L28vqKDTogugPnr54hY8k6enP02U_qMBUtnmo7CbA6OfhHQ@mail.gmail.com/4-v47-0001-Add-function-to-report-memory-context-statistics.patch) download | inline diff: From 781a7649bf8fb5e89fb900664f9a8d05844eb8a9 Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Thu, 27 Nov 2025 14:39:43 +0530 Subject: [PATCH 1/2] Add function to report memory context statistics This adds a function for retrieving memory context statistics and information from backends as well as auxiliary processes. The intended usecase is cluster debugging when under memory pressure or unanticipated memory usage characteristics. When calling the function it sends a signal to the specified process to submit statistics regarding its memory contexts into dynamic shared memory. Each memory context is returned in detail, followed by a cumulative total in case the number of contexts exceed the max allocated amount of shared memory. Each process is limited to use at most 1Mb memory for this. A summary can also be explicitly requested by the user, this will return the TopMemoryContext and a cumulative total of all lower contexts. In order to not block on busy processes, we have hardcoded the number of seconds during which to retry before timing out. In the case where no statistics are published within the set timeout, NULL is returned --- doc/src/sgml/func/func-admin.sgml | 159 +++ src/backend/catalog/system_functions.sql | 14 + src/backend/postmaster/autovacuum.c | 4 + src/backend/postmaster/checkpointer.c | 4 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 4 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 4 + src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/storage/lmgr/proc.c | 1 + src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 2 + src/backend/utils/adt/mcxtfuncs.c | 1012 ++++++++++++++++- src/backend/utils/init/globals.c | 1 + src/backend/utils/init/postinit.c | 7 + src/backend/utils/mmgr/mcxt.c | 31 + src/include/catalog/pg_proc.dat | 10 + src/include/miscadmin.h | 1 + src/include/storage/lwlocklist.h | 1 + src/include/storage/procsignal.h | 1 + src/include/utils/memutils.h | 8 +- src/test/regress/expected/sysviews.out | 19 + src/test/regress/sql/sysviews.sql | 18 + src/tools/pgindent/typedefs.list | 2 + 25 files changed, 1299 insertions(+), 21 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 2896cd9e429..5eac0e5f73c 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -251,6 +251,132 @@ <literal>false</literal> is returned. </para></entry> </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_get_process_memory_contexts</primary> + </indexterm> + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type> <optional>,<parameter>summary</parameter> <type>boolean</type> <literal>DEFAULT</literal> <literal>false</literal></optional> ) + <returnvalue>setof record</returnvalue> + ( <parameter>name</parameter> <type>text</type>, + <parameter>ident</parameter> <type>text</type>, + <parameter>type</parameter> <type>text</type>, + <parameter>level</parameter> <type>integer</type>, + <parameter>path</parameter> <type>integer[]</type>, + <parameter>total_bytes</parameter> <type>bigint</type>, + <parameter>total_nblocks</parameter> <type>bigint</type>, + <parameter>free_bytes</parameter> <type>bigint</type>, + <parameter>free_chunks</parameter> <type>bigint</type>, + <parameter>used_bytes</parameter> <type>bigint</type>, + <parameter>num_agg_contexts</parameter> <type>integer</type> ) + </para> + <para> + This function handles requests to display the memory contexts of a + <productname>PostgreSQL</productname> process with the specified + process ID. The function can be used to send requests to backends as + well as <glossterm linkend="glossary-auxiliary-proc">auxiliary processes</glossterm>. + If the process does not respond with memory contexts statistics in 5 seconds, + function returns NULL. + </para> + <para> + The returned record contains extended statistics per each memory + context: + <itemizedlist spacing="compact"> + <listitem> + <para> + <parameter>name</parameter> - The name of the memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>ident</parameter> - Memory context ID (if any). + </para> + </listitem> + <listitem> + <para> + <parameter>type</parameter> - The type of memory context, possible + values are: AllocSet, Generation, Slab and Bump. + </para> + </listitem> + <listitem> + <para> + <parameter>level</parameter> - The level in the tree of the current + memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>path</parameter> - Memory contexts are organized in a + tree model with TopMemoryContext as the root, and all other memory + contexts as nodes in the tree. The <parameter>path</parameter> + displays the path from the root to the current memory context. The + path is limited to 100 children per node, which each node limited + to a max depth of 100, to preserve memory during reporting. The + printed path will also be limited to 100 nodes counting from the + TopMemoryContext. + </para> + </listitem> + <listitem> + <para> + <parameter>total_bytes</parameter> - The total number of bytes + allocated to this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_nblocks</parameter> - The total number of blocks + used for the allocated memory. + </para> + </listitem> + <listitem> + <para> + <parameter>free_bytes</parameter> - The amount of free memory in + this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>free_chunks</parameter> - The number of chunks that + <parameter>free_bytes</parameter> corresponds to. + </para> + </listitem> + <listitem> + <para> + <parameter>used_bytes</parameter> - The total number of bytes + currently occupied. + </para> + </listitem> + <listitem> + <para> + <parameter>num_agg_contexts</parameter> - The number of memory + contexts aggregated in the displayed statistics. + </para> + </listitem> + </itemizedlist> + </para> + <para> + When <parameter>summary</parameter> is <literal>true</literal>, statistics + for memory contexts at levels 1 and 2 are displayed, with level 1 + representing the root node (i.e., <literal>TopMemoryContext</literal>). + Statistics for contexts on level 2 and below are aggregates of all + child contexts' statistics, where <literal>num_agg_contexts</literal> + indicate the number aggregated child contexts. When + <parameter>summary</parameter> is <literal>false</literal> (the default), + <literal>the num_agg_contexts</literal> value is <literal>1</literal>, + indicating that individual statistics are being displayed. + </para> + <para> + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context + statistics are aggregated and a cumulative total is displayed. The + <literal>num_agg_contexts</literal> column indicates the number of + contexts aggregated in the displayed statistics. When + <literal>num_agg_contexts</literal> is <literal>1</literal> it means + that the context statistics are displayed separately. + </para></entry> + </row> </tbody> </tgroup> </table> @@ -302,6 +428,39 @@ LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 because it may generate a large number of log messages. </para> + <para> + <function>pg_get_process_memory_contexts</function> can be used to request + memory contexts statistics of any <productname>PostgreSQL</productname> + process. For example: +<programlisting> +postgres=# SELECT * FROM pg_get_process_memory_contexts( + (SELECT pid FROM pg_stat_activity + WHERE backend_type = 'checkpointer'), + false) LIMIT 1; +-[ RECORD 1 ]----+------------------------------ +name | TopMemoryContext +ident | +type | AllocSet +level | 1 +path | {1} +total_bytes | 90304 +total_nblocks | 3 +free_bytes | 2880 +free_chunks | 1 +used_bytes | 87424 +num_agg_contexts | 1 +</programlisting> + <note> + <para> + While <function>pg_get_process_memory_contexts</function> can be used to + query memory contexts of the local backend, + <structname>pg_backend_memory_contexts</structname> + (see <xref linkend="view-pg-backend-memory-contexts"/> for more details) + will be less resource intensive when only the local backend is of interest. + </para> + </note> + </para> + </sect2> <sect2 id="functions-admin-backup"> diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql index eb9e31ae1bf..d1416e5534d 100644 --- a/src/backend/catalog/system_functions.sql +++ b/src/backend/catalog/system_functions.sql @@ -657,6 +657,17 @@ LANGUAGE INTERNAL STRICT VOLATILE PARALLEL UNSAFE AS 'pg_replication_origin_session_setup'; +CREATE OR REPLACE FUNCTION + pg_get_process_memory_contexts(IN pid integer, IN summary boolean DEFAULT false, + OUT name text, OUT ident text, OUT type text, OUT level integer, + OUT path integer[], OUT total_bytes bigint, OUT total_nblocks bigint, + OUT free_bytes bigint, OUT free_chunks bigint, OUT used_bytes bigint, + OUT num_agg_contexts integer) +RETURNS SETOF RECORD +LANGUAGE INTERNAL +STRICT VOLATILE PARALLEL UNSAFE +AS 'pg_get_process_memory_contexts'; + -- -- The default permissions for functions mean that anyone can execute them. -- A number of functions shouldn't be executable by just anyone, but rather @@ -782,6 +793,7 @@ REVOKE EXECUTE ON FUNCTION pg_ls_logicalmapdir() FROM PUBLIC; REVOKE EXECUTE ON FUNCTION pg_ls_replslotdir(text) FROM PUBLIC; +REVOKE EXECUTE ON FUNCTION pg_get_process_memory_contexts(integer, boolean) FROM PUBLIC; -- -- We also set up some things as accessible to standard roles. -- @@ -808,6 +820,8 @@ GRANT EXECUTE ON FUNCTION pg_current_logfile() TO pg_monitor; GRANT EXECUTE ON FUNCTION pg_current_logfile(text) TO pg_monitor; +GRANT EXECUTE ON FUNCTION pg_get_process_memory_contexts(integer, boolean) TO pg_read_all_stats; + GRANT pg_read_all_settings TO pg_monitor; GRANT pg_read_all_stats TO pg_monitor; diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 3e507d23cc9..fbebe506495 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -791,6 +791,10 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index 6482c21b8f9..1b7d5e7ffdc 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -685,6 +685,10 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index a2c0ff012c5..cb18ce80893 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -48,6 +48,10 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index 1a20387c4bd..3ac0fba225a 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -871,6 +871,10 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index a1a4f65f9a9..18a9e7f85e1 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -192,6 +192,10 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index c3d56c866d3..49dc3e88023 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -879,6 +879,10 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index 85c67b2c183..211199e985a 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -52,6 +52,7 @@ #include "storage/sinvaladt.h" #include "utils/guc.h" #include "utils/injection_point.h" +#include "utils/memutils.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -141,6 +142,7 @@ CalculateShmemSize(void) size = add_size(size, AioShmemSize()); size = add_size(size, WaitLSNShmemSize()); size = add_size(size, LogicalDecodingCtlShmemSize()); + size = add_size(size, MemoryContextKeysShmemSize() + sizeof(LWLockPadded)); /* include additional requested shmem from preload libraries */ size = add_size(size, total_addin_request); @@ -330,6 +332,7 @@ CreateOrAttachShmemStructs(void) AioShmemInit(); WaitLSNShmemInit(); LogicalDecodingCtlShmemInit(); + MemoryContextKeysShmemInit(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index 8e56922dcea..601e01d2574 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -694,6 +694,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_GET_MEMORY_CONTEXT)) + HandleGetMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 66274029c74..61957fc9b74 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -51,6 +51,7 @@ #include "storage/procsignal.h" #include "storage/spin.h" #include "storage/standby.h" +#include "utils/memutils.h" #include "utils/timeout.h" #include "utils/timestamp.h" diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index e54bf1e760f..199245e4c1f 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3539,6 +3539,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 3299de23bb3..6f78075bdfc 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -163,6 +163,7 @@ WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." +MEM_CXT_PUBLISH "Waiting for a process to publish memory information." ABI_compatibility: @@ -407,6 +408,7 @@ SubtransSLRU "Waiting to access the sub-transaction SLRU cache." XactSLRU "Waiting to access the transaction status SLRU cache." ParallelVacuumDSA "Waiting for parallel vacuum dynamic shared memory allocation." AioUringCompletion "Waiting for another process to complete IO via io_uring." +MemoryContextReportingKeys "Waiting for another process to complete reading or writing the memory reporting keys." # No "ABI_compatibility" region here as WaitEventLWLock has its own C code. diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index 12b8d4cefaf..335fdcdc329 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -17,22 +17,130 @@ #include "funcapi.h" #include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "storage/dsm_registry.h" #include "storage/proc.h" #include "storage/procarray.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/injection_point.h" +#include "utils/memutils.h" +#include "utils/wait_event_types.h" + +/* + * Memory Context reporting size limits. + */ + +/* Max length of context name and ident, to keep it consistent + * with ProcessLogMemoryContext() + */ +#define MEMORY_CONTEXT_IDENT_SHMEM_SIZE 100 +#define MEMORY_CONTEXT_NAME_SHMEM_SIZE 100 + +/* Maximum size (in bytes) of DSA area per process */ +#define MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND ((size_t) (1 * 1024 * 1024)) + +/* + * Maximum number of memory context statistics is calculated by dividing + * max memory allocated per backend with maximum size per context statistics. + * The identifier and name are statically allocated arrays of size 100 bytes. + * The path depth is limited to 100 like for memory context logging. + */ +#define MAX_MEMORY_CONTEXT_STATS_NUM MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND / (sizeof(MemoryStatsEntry)) + +/* + * Size of dshash key. The key is a uint32 rendered as a string, 10 chars + * plus space for a NULL terminator can hold all the values. + */ +#define CLIENT_KEY_SIZE (10 + 1) + +/* Dynamic shared memory state for reporting statistics per context */ +typedef struct MemoryStatsEntry +{ + char name[MEMORY_CONTEXT_NAME_SHMEM_SIZE]; + char ident[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + int path[100]; + NodeTag type; + int path_length; + int levels; + int64 totalspace; + int64 nblocks; + int64 freespace; + int64 freechunks; + int num_agg_stats; +} MemoryStatsEntry; + +/* + * Per backend dynamic shared hash entry for memory context statistics + * reporting. + */ +typedef struct MemoryStatsDSHashEntry +{ + char key[64]; + ConditionVariable memcxt_cv; + bool stats_written; + int target_server_id; + int total_stats; + bool summary; + dsa_pointer memstats_dsa_pointer; +} MemoryStatsDSHashEntry; + +static const dshash_parameters memctx_dsh_params = { + offsetof(MemoryStatsDSHashEntry, memcxt_cv), + sizeof(MemoryStatsDSHashEntry), + dshash_strcmp, + dshash_strhash, + dshash_strcpy +}; + +/* + * These are used for reporting memory context statistics of a process. + */ + +/* Lock to control access to client_keys array */ +static LWLock *client_keys_lock = NULL; + +/* Array to store the keys of MemoryStatsDsHash */ +static int *client_keys = NULL; + +/* + * Table to store pointers to DSA memory containing memory statistics and other + * metadata. There is one entry per client backend request, keyed by ProcNumber + * of the client obtained from client_keys array above. + */ +static dshash_table *MemoryStatsDsHash = NULL; + +/* + * Dsa area which stores the actual memory context + * statistics. + */ +static dsa_area *MemoryStatsDsaArea = NULL; + +static void memstats_dsa_cleanup(char *key); +static void memstats_client_key_reset(int ProcNumber); +static const char *ContextTypeToString(NodeTag type); +static void PublishMemoryContext(MemoryStatsEntry *memcxt_info, + int curr_id, MemoryContext context, + List *path, + MemoryContextCounters stat, + int num_contexts); +static List *compute_context_path(MemoryContext c, HTAB *context_id_lookup); /* ---------- * The max bytes for showing identifiers of MemoryContext. + * This is used by pg_get_backend_memory_context - view used for local backend. * ---------- */ #define MEMORY_CONTEXT_IDENT_DISPLAY_SIZE 1024 +#define MAX_PATH_DISPLAY_LENGTH 100 +/* Timeout in seconds */ +#define MEMORY_STATS_MAX_TIMEOUT 5 + /* * MemoryContextId - * Used for storage of transient identifiers for - * pg_get_backend_memory_contexts. + * Used for storage of transient identifiers for memory context reporting */ typedef struct MemoryContextId { @@ -143,24 +251,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, else nulls[1] = true; - switch (context->type) - { - case T_AllocSetContext: - type = "AllocSet"; - break; - case T_GenerationContext: - type = "Generation"; - break; - case T_SlabContext: - type = "Slab"; - break; - case T_BumpContext: - type = "Bump"; - break; - default: - type = "???"; - break; - } + type = ContextTypeToString(context->type); values[2] = CStringGetTextDatum(type); values[3] = Int32GetDatum(list_length(path)); /* level */ @@ -175,6 +266,38 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, list_free(path); } +/* + * ContextTypeToString + * Returns a textual representation of a context type + * + * This should cover the same types as MemoryContextIsValid. + */ +const char * +ContextTypeToString(NodeTag type) +{ + const char *context_type; + + switch (type) + { + case T_AllocSetContext: + context_type = "AllocSet"; + break; + case T_GenerationContext: + context_type = "Generation"; + break; + case T_SlabContext: + context_type = "Slab"; + break; + case T_BumpContext: + context_type = "Bump"; + break; + default: + context_type = "???"; + break; + } + return context_type; +} + /* * pg_get_backend_memory_contexts * SQL SRF showing backend memory context. @@ -305,3 +428,852 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } + +/* + * pg_get_process_memory_contexts + * Signal a backend or an auxiliary process to send its memory contexts, + * wait for the results and display them. + * + * By default, only superusers or users with ROLE_PG_READ_ALL_STATS are allowed + * to signal a process to return the memory contexts. Additional roles can be + * permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to copy the memory context details + * to a dynamic shared memory space. + * + * We have defined a limit on DSA memory that could be allocated per process - + * if the process has more memory contexts than what can fit in the allocated + * size, the excess contexts are summarized and represented as cumulative total + * at the end of the buffer. + * + * After sending the signal, wait on a condition variable. The publishing + * backend, after copying the data to shared memory, sends a signal on that + * condition variable. There is one condition variable per client process. + * Once the condition variable is signalled, check if the latest memory context + * information is available and display. + * + * If the publishing backend does not respond before the condition variable + * times out, which is set to a predefined value MEMORY_STATS_MAX_TIMEOUT, give + * up and return NULL. + */ +Datum +pg_get_process_memory_contexts(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + bool summary = PG_GETARG_BOOL(1); + PGPROC *proc; + ProcNumber procNumber; + bool proc_is_aux = false; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + MemoryStatsEntry *memcxt_info; + MemoryStatsDSHashEntry *entry; + bool found; + char key[CLIENT_KEY_SIZE]; + TimestampTz start_timestamp; + + /* + * See if the process with given pid is a backend or an auxiliary process + * and remember the type for when we requery the process later. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + { + proc = AuxiliaryPidGetProc(pid); + proc_is_aux = true; + } + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; this is however not a problem and leave with a WARNING. + * See comment in pg_log_backend_memory_contexts for a discussion on this. + */ + if (proc == NULL) + { + ereport(WARNING, + errmsg("PID %d is not a PostgreSQL server process", pid)); + PG_RETURN_NULL(); + } + + procNumber = GetNumberFromPGProc(proc); + + /* + * Check if the server process slot is not empty and exit early Non-empty + * slot means some other client backend is requesting the statistics from + * the same server process. + */ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + if (client_keys[procNumber] != -1) + { + LWLockRelease(client_keys_lock); + ereport(WARNING, + errmsg("server process %d is processing previous request", + pid)); + PG_RETURN_NULL(); + } + LWLockRelease(client_keys_lock); + + /* + * Create a DSA to allocate memory for copying memory contexts statistics. + * Allocate the memory in the DSA and send DSA pointer to the server + * process for storing the context statistics. If number of contexts + * exceed a predefined limit (1MB), a cumulative total is stored for such + * contexts. + * + * The DSA is created once for the lifetime of the server, and only + * attached in subsequent calls. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); + + /* + * The DSA pointers containing statistics for each client are stored in a + * dshash table. In addition to DSA pointer, each entry in this table also + * contains information about the statistics, condition variable for + * signalling between client and the server and miscellaneous data + * specific to a request. There is one entry per client request in the + * hash table. + */ + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); + + snprintf(key, sizeof(key), "%d", MyProcNumber); + + /* + * Insert an entry for this client in DSHASH table the first time this + * function is called. This entry is deleted when the process exits in + * before_shmem_exit call. + * + * dshash_find_or_insert locks the entry to prevent the publisher from + * reading before client has updated the entry. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + if (!found) + { + entry->stats_written = false; + ConditionVariableInit(&entry->memcxt_cv); + } + + /* + * Allocate 1MB of memory for the backend to publish its statistics on + * every call to this function. The memory is freed at the end of the + * function. + */ + entry->memstats_dsa_pointer = + dsa_allocate0(MemoryStatsDsaArea, MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND); + + /* + * Specify whether a summary of statistics is requested, before signalling + * the server. + */ + entry->summary = summary; + + /* + * Indicate which server process statistics are being requested from. If + * this client times out before the last requested process can publish its + * statistics, it may send a new request to another server process. Since + * the previous server was notified, it might attempt to read the same + * client entry and respond incorrectly with its statistics. By storing + * the server ID in the client entry, we prevent any previously signalled + * server process from writing its statistics in the space meant for the + * newly requested process. + */ + entry->target_server_id = pid; + dshash_release_lock(MemoryStatsDsHash, entry); + + /* + * Check if the publishing process slot is empty and store this clients + * key i.e its procNumber. This informs the publishing process that it is + * supposed to write statistics in the hash entry corresponding to this + * client. + */ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + if (client_keys[procNumber] == -1) + client_keys[procNumber] = MyProcNumber; + else + { + LWLockRelease(client_keys_lock); + ereport(WARNING, + errmsg("server process %d is processing previous request", + pid)); + PG_RETURN_NULL(); + } + LWLockRelease(client_keys_lock); + + /* + * Send a signal to a PostgreSQL process, informing it we want it to + * produce information about its memory contexts. + */ + if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, procNumber) < 0) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ereport(WARNING, + errmsg("could not send signal to process %d: %m", + pid)); + PG_RETURN_NULL(); + } + start_timestamp = GetCurrentTimestamp(); + + while (1) + { + long elapsed_time; + + INJECTION_POINT("memcontext-client-injection", NULL); + + elapsed_time = TimestampDifferenceMilliseconds(start_timestamp, + GetCurrentTimestamp()); + /* Return if we have already exceeded the timeout */ + if (elapsed_time >= MEMORY_STATS_MAX_TIMEOUT * 1000) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + PG_RETURN_NULL(); + } + + /* + * Recheck the state of the backend before sleeping on the condition + * variable to ensure the process is still alive. Only check the + * relevant process type based on the earlier PID check. + */ + if (proc_is_aux) + proc = AuxiliaryPidGetProc(pid); + else + proc = BackendPidGetProc(pid); + + /* + * The target server process ending during memory context processing + * is not an error. + */ + if (proc == NULL) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + ereport(WARNING, + errmsg("PID %d is no longer a PostgreSQL server process", + pid)); + PG_RETURN_NULL(); + } + + /* + * Wait for MEMORY_STATS_MAX_TIMEOUT. If no statistics are available + * within the allowed time then return NULL. The timer is defined in + * milliseconds since that's what the condition variable sleep uses. + */ + if (ConditionVariableTimedSleep(&entry->memcxt_cv, + (MEMORY_STATS_MAX_TIMEOUT * 1000), + WAIT_EVENT_MEM_CXT_PUBLISH)) + { + /* Timeout has expired, return NULL */ + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + PG_RETURN_NULL(); + } + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + Assert(found); + + memcxt_info = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + /* + * We expect to come out of sleep when the requested process has + * finished publishing the statistics, verified using a boolean + * stats_written. + * + * Make sure that the statistics are actually written by checking that + * the name of the context is not NULL. This is done to ensure that + * the subsequent waits for statistics do not return spuriously if the + * previous call to the function ended in error and thus could not + * clear the stats_written flag. + */ + if (entry->stats_written && memcxt_info[0].name[0] != '\0') + break; + + dshash_release_lock(MemoryStatsDsHash, entry); + + } + + InitMaterializedSRF(fcinfo, 0); + + /* + * Backend has finished publishing the stats, project them. + */ +#define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 11 + for (int i = 0; i < entry->total_stats; i++) + { + ArrayType *path_array; + int path_length; + Datum values[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + bool nulls[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + Datum *path_datum = NULL; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + Assert(memcxt_info[i].name[0] != '\0'); + values[0] = CStringGetTextDatum(memcxt_info[i].name); + + if (memcxt_info[i].ident[0] != '\0') + values[1] = CStringGetTextDatum(memcxt_info[i].ident); + else + nulls[1] = true; + + values[2] = CStringGetTextDatum(ContextTypeToString(memcxt_info[i].type)); + values[3] = Int32GetDatum(memcxt_info[i].levels); + + path_length = memcxt_info[i].path_length; + path_datum = (Datum *) palloc(path_length * sizeof(Datum)); + if (memcxt_info[i].path[0] != 0) + { + for (int j = 0; j < path_length; j++) + path_datum[j] = Int32GetDatum(memcxt_info[i].path[j]); + path_array = construct_array_builtin(path_datum, + path_length, + INT4OID); + values[4] = PointerGetDatum(path_array); + } + else + nulls[4] = true; + + values[5] = Int64GetDatum(memcxt_info[i].totalspace); + values[6] = Int64GetDatum(memcxt_info[i].nblocks); + values[7] = Int64GetDatum(memcxt_info[i].freespace); + values[8] = Int64GetDatum(memcxt_info[i].freechunks); + values[9] = Int64GetDatum(memcxt_info[i].totalspace - + memcxt_info[i].freespace); + values[10] = Int32GetDatum(memcxt_info[i].num_agg_stats); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + dshash_release_lock(MemoryStatsDsHash, entry); + memstats_dsa_cleanup(key); + + ConditionVariableCancelSleep(); + + PG_RETURN_NULL(); +} + +static void +memstats_dsa_cleanup(char *key) +{ + MemoryStatsDSHashEntry *entry; + + entry = dshash_find(MemoryStatsDsHash, key, true); + + Assert(MemoryStatsDsaArea != NULL); + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + entry->memstats_dsa_pointer = InvalidDsaPointer; + entry->stats_written = false; + entry->target_server_id = 0; + + dshash_release_lock(MemoryStatsDsHash, entry); +} + +/* + * Remove this process from the publishing process' + * client key slot, if the stats publishing process has failed to do so. + */ +static void +memstats_client_key_reset(int procNumber) +{ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + + if (client_keys[procNumber] == MyProcNumber) + client_keys[procNumber] = -1; + LWLockRelease(client_keys_lock); +} + +void +MemoryContextKeysShmemInit(void) +{ + bool found; + + client_keys = (int *) + ShmemInitStruct("MemoryContextKeys", + MemoryContextKeysShmemSize() + sizeof(LWLockPadded), &found); + client_keys_lock = (LWLock *) ((char *) client_keys + MemoryContextKeysShmemSize()); + + if (!found) + { + MemSet(client_keys, -1, MemoryContextKeysShmemSize()); + LWLockInitialize(client_keys_lock, LWTRANCHE_MEMORY_CONTEXT_KEYS); + } +} + +Size +MemoryContextKeysShmemSize(void) +{ + Size sz = 0; + Size TotalProcs = 0; + + TotalProcs = add_size(TotalProcs, NUM_AUXILIARY_PROCS); + TotalProcs = add_size(TotalProcs, MaxBackends); + sz = add_size(sz, mul_size(TotalProcs, sizeof(int))); + + return sz; +} + +/* + * HandleGetMemoryContextInterrupt + * Handle receipt of an interrupt indicating a request to publish memory + * contexts statistics. + * + * All the actual work is deferred to ProcessGetMemoryContextInterrupt() as + * this cannot be performed in a signal handler. + */ +void +HandleGetMemoryContextInterrupt(void) +{ + InterruptPending = true; + PublishMemoryContextPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + +/* + * ProcessGetMemoryContextInterrupt + * Generate information about memory contexts used by the process. + * + * Performs a breadth first search on the memory context tree, thus parents + * statistics are reported before their children in the monitoring function + * output. + * + * Statistics for all the processes are shared via the same dynamic shared + * area. Individual statistics are tracked independently in per-process DSA + * pointers. These pointers are stored in a dshash table with key as requesting + * clients ProcNumber. + * + * We calculate maximum number of context's statistics that can be displayed + * using a pre-determined limit for memory available per process for this + * utility and maximum size of statistics for each context. The remaining + * context statistics if any are captured as a cumulative total at the end of + * individual context's statistics. + * + * If summary is true, we capture the level 1 and level 2 contexts statistics. + * For that we traverse the memory context tree recursively in depth first + * search manner to cover all the children of a parent context, to be able to + * display a cumulative total of memory consumption by a parent at level 2 and + * all its children. + */ +void +ProcessGetMemoryContextInterrupt(void) +{ + List *contexts; + HASHCTL ctl; + HTAB *context_id_lookup; + int context_id = 0; + MemoryStatsEntry *meminfo; + MemoryContextCounters stat; + int num_individual_stats = 0; + bool found; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + int clientProcNumber; + MemoryContext memstats_ctx = NULL; + MemoryContext oldcontext = NULL; + + PublishMemoryContextPending = false; + + /* + * Avoid performing any shared memory operations in aborted transaction, + * the caller will get the fallback behaviour of the past known stats. + */ + if (IsAbortedTransactionBlockState()) + return; + + INJECTION_POINT("memcontext-server-wait", NULL); + + /* + * Retrieve the client key for publishing statistics and reset it to -1, + * so other clients can request memory statistics from this process. + * Return if the client_key is -1, which means the requesting client has + * timed out. + */ + LWLockAcquire(client_keys_lock, LW_SHARED); + if (client_keys[MyProcNumber] == -1) + { + LWLockRelease(client_keys_lock); + return; + } + else + { + clientProcNumber = client_keys[MyProcNumber]; + client_keys[MyProcNumber] = -1; + LWLockRelease(client_keys_lock); + } + + /* + * Create a new memory context which is not a part of TopMemoryContext + * tree. This context is used to allocate all memory in this function. + * This helps in keeping the memory allocation in this function to report + * memory consumption statistics separate. So that it does not affect the + * output of this function. + */ + memstats_ctx = AllocSetContextCreate((MemoryContext) NULL, + "publish_memory_context_statistics", + ALLOCSET_SMALL_SIZES); + oldcontext = MemoryContextSwitchTo(memstats_ctx); + + /* + * The hash table is used for constructing "path" column of the view, + * similar to its local backend counterpart. + */ + ctl.keysize = sizeof(MemoryContext); + ctl.entrysize = sizeof(MemoryContextId); + ctl.hcxt = CurrentMemoryContext; + + context_id_lookup = hash_create("pg_get_process_memory_contexts", + 256, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + /* List of contexts to process in the next round - start at the top. */ + contexts = list_make1(TopMemoryContext); + + /* + * The client process should have created the required DSA and DSHash + * table. Here we just attach to those. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); + + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); + + snprintf(key, CLIENT_KEY_SIZE, "%d", clientProcNumber); + + /* + * The entry lock is held by dshash_find_or_insert to protect writes to + * process specific memory. Two different processes publishing statistics + * do not block each other. + */ + INJECTION_POINT("memcontext-server-injection", NULL); + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + /* + * Check if the entry has been deleted due to calling process exiting, or + * if the caller has timed out waiting for us and have issued a request to + * another backend. + * + * Make sure that the client always deletes the entry after taking + * required lock or this function may end up writing to unallocated + * memory. + */ + if (!found || entry->target_server_id != MyProcPid) + { + entry->stats_written = false; + + dshash_release_lock(MemoryStatsDsHash, entry); + + hash_destroy(context_id_lookup); + MemoryContextSwitchTo(oldcontext); + MemoryContextReset(memstats_ctx); + + return; + } + + /* Should be allocated by a client backend that is requesting statistics */ + Assert(entry->memstats_dsa_pointer != InvalidDsaPointer); + meminfo = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + if (entry->summary) + { + int cxt_id = 0; + List *path = NIL; + MemoryContextId *contextid_entry; + + /* Copy TopMemoryContext statistics to DSA */ + memset(&stat, 0, sizeof(stat)); + (*TopMemoryContext->methods->stats) (TopMemoryContext, NULL, NULL, + &stat, true); + path = lcons_int(1, path); + PublishMemoryContext(meminfo, cxt_id, TopMemoryContext, path, stat, + 1); + + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &TopMemoryContext, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = cxt_id + 1; + + /* + * Copy statistics for each of TopMemoryContexts children. This + * includes statistics of at most 100 children per node, with each + * child node limited to a depth of 100 in its subtree. + */ + for (MemoryContext c = TopMemoryContext->firstchild; c != NULL; + c = c->nextchild) + { + MemoryContextCounters grand_totals; + int num_contexts = 0; + + path = NIL; + memset(&grand_totals, 0, sizeof(grand_totals)); + + cxt_id++; + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &c, HASH_ENTER, &found); + Assert(!found); + contextid_entry->context_id = cxt_id + 1; + + MemoryContextStatsCounter(c, &grand_totals, &num_contexts); + + path = compute_context_path(c, context_id_lookup); + + PublishMemoryContext(meminfo, cxt_id, c, path, + grand_totals, num_contexts); + } + entry->total_stats = cxt_id + 1; + } + else + { + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + MemoryContextId *contextid_entry; + + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &cur, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = context_id + 1; + + /* + * Figure out the transient context_id of this context and each of + * its ancestors, to compute a path for this context. + */ + path = compute_context_path(cur, context_id_lookup); + + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + /* Account for saving one statistics slot for cumulative reporting */ + if (context_id < (MAX_MEMORY_CONTEXT_STATS_NUM - 1)) + { + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1); + } + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].totalspace += stat.totalspace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].nblocks += stat.nblocks; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freespace += stat.freespace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit per process is reached, write aggregate of the + * remaining statistics. + * + * We can store contexts from 0 to max_stats - 1. When context_id + * is greater than max_stats, we stop reporting individual + * statistics when context_id equals max_stats - 2. As we use + * max_stats - 1 array slot for reporting cumulative statistics or + * "Remaining Totals". + */ + if (context_id == (MAX_MEMORY_CONTEXT_STATS_NUM - 2)) + { + int namelen = strlen("Remaining Totals"); + + num_individual_stats = context_id + 1; + strlcpy(meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].name, + "Remaining Totals", namelen + 1); + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].ident[0] = '\0'; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].path[0] = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].type = 0; + } + context_id++; + + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + contexts = lappend(contexts, c); + } + + /* + * Check if there are aggregated statistics or not in the result set. + * Statistics are individually reported when context_id <= max_stats, + * only if context_id > max_stats will there be aggregates. + */ + if (context_id <= MAX_MEMORY_CONTEXT_STATS_NUM) + { + entry->total_stats = context_id; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = 1; + } + + /* + * The number of contexts exceeded the space available, so report the + * number of aggregated memory contexts + */ + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = + context_id - num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for + * cumulative statistics. + */ + entry->total_stats = num_individual_stats + 1; + } + } + + entry->stats_written = true; + dshash_release_lock(MemoryStatsDsHash, entry); + hash_destroy(context_id_lookup); + + MemoryContextSwitchTo(oldcontext); + MemoryContextReset(memstats_ctx); + /* Notify waiting client backend and return */ + ConditionVariableSignal(&entry->memcxt_cv); +} + +/* + * compute_context_path + * + * Append the transient context_id of this context and each of its ancestors + * to a list, in order to compute a path. + */ +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + MemoryContext cur_context; + + for (cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + elog(ERROR, "hash table corrupted, can't construct path value"); + + path = lcons_int(cur_entry->context_id, path); + } + + return path; +} + +/* + * PublishMemoryContext + * + * Copy the memory context statistics of a single context to a DSA memory + */ +static void +PublishMemoryContext(MemoryStatsEntry *memcxt_info, int curr_id, + MemoryContext context, List *path, + MemoryContextCounters stat, int num_contexts) +{ + char *ident = unconstify(char *, context->ident); + char *name = unconstify(char *, context->name); + + /* + * To be consistent with logging output, we label dynahash contexts with + * just the hash table name as with MemoryContextStatsPrint(). + */ + if (context->ident && strncmp(context->name, "dynahash", 8) == 0) + { + name = unconstify(char *, context->ident); + ident = NULL; + } + + if (name != NULL) + { + int namelen = strlen(name); + + if (namelen >= MEMORY_CONTEXT_NAME_SHMEM_SIZE) + namelen = pg_mbcliplen(name, namelen, + MEMORY_CONTEXT_NAME_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].name, name, namelen + 1); + } + else + /* Clearing the array */ + memcxt_info[curr_id].name[0] = '\0'; + + /* Trim and copy the identifier if it is not set to NULL */ + if (ident != NULL) + { + int idlen = strlen(context->ident); + + /* + * Some identifiers such as SQL query string can be very long, + * truncate oversize identifiers. + */ + if (idlen >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + idlen = pg_mbcliplen(ident, idlen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].ident, ident, idlen + 1); + } + else + memcxt_info[curr_id].ident[0] = '\0'; + + /* Store the path */ + if (path == NIL) + memcxt_info[curr_id].path[0] = 0; + else + { + int levels = Min(list_length(path), MAX_PATH_DISPLAY_LENGTH); + + memcxt_info[curr_id].path_length = levels; + memcxt_info[curr_id].levels = list_length(path); + + foreach_int(i, path) + { + memcxt_info[curr_id].path[foreach_current_index(i)] = i; + if (--levels == 0) + break; + } + } + memcxt_info[curr_id].type = context->type; + memcxt_info[curr_id].totalspace = stat.totalspace; + memcxt_info[curr_id].nblocks = stat.nblocks; + memcxt_info[curr_id].freespace = stat.freespace; + memcxt_info[curr_id].freechunks = stat.freechunks; + memcxt_info[curr_id].num_agg_stats = num_contexts; +} + +void +AtProcExit_memstats_cleanup(int code, Datum arg) +{ + int idx = MyProcNumber; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + bool found; + + if (MemoryStatsDsHash != NULL) + { + snprintf(key, CLIENT_KEY_SIZE, "%d", idx); + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + if (found) + { + if (MemoryStatsDsaArea != NULL && + DsaPointerIsValid(entry->memstats_dsa_pointer)) + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + } + dshash_delete_entry(MemoryStatsDsHash, entry); + } + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + client_keys[idx] = -1; + LWLockRelease(client_keys_lock); +} diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index 36ad708b360..e246fc7ce1f 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -39,6 +39,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t PublishMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index 3f401faf3de..7e6de81e7d6 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -661,6 +661,13 @@ BaseInit(void) * drop ephemeral slots, which in turn triggers stats reporting. */ ReplicationSlotInitialize(); + + /* + * The before shmem exit callback frees the DSA memory occupied by the + * latest memory context statistics that could be published by this proc + * if requested. + */ + before_shmem_exit(AtProcExit_memstats_cleanup, 0); } diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 073bdb35d2a..fd75345ff5f 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -1011,6 +1011,37 @@ MemoryContextStatsInternal(MemoryContext context, int level, } } + +/* + * MemoryContextStatsCounter + * + * Accumulate statistics counts into *totals. totals should not be NULL. + * This involves a non-recursive tree traversal. + */ +void +MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts) +{ + int ichild = 1; + + *num_contexts = 0; + context->methods->stats(context, NULL, NULL, totals, false); + + for (MemoryContext curr = context->firstchild; + curr != NULL; + curr = MemoryContextTraverseNext(curr, context)) + { + curr->methods->stats(curr, NULL, NULL, totals, false); + ichild++; + } + + /* + * Add the count of all the children contexts which are traversed + * including the parent. + */ + *num_contexts = *num_contexts + ichild; +} + /* * MemoryContextStatsPrint * Print callback used by MemoryContextStatsInternal diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 2ac69bf2df5..ef50383ff4f 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8633,6 +8633,16 @@ prorettype => 'bool', proargtypes => 'int4', prosrc => 'pg_log_backend_memory_contexts' }, +# publishing memory contexts of the specified postgres process +{ oid => '2173', descr => 'publish memory contexts of the specified backend', + proname => 'pg_get_process_memory_contexts', provolatile => 'v', + prorows => '100', proretset => 't', proparallel => 'r', + prorettype => 'record', proargtypes => 'int4 bool', + proallargtypes => '{int4,bool,text,text,text,int4,_int4,int8,int8,int8,int8,int8,int4}', + proargmodes => '{i,i,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{pid, summary, name, ident, type, level, path, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes, num_agg_contexts}', + prosrc => 'pg_get_process_memory_contexts' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index db559b39c4d..7e301affccc 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -96,6 +96,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t PublishMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h index 94f818b9f10..ed249e72a26 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -137,3 +137,4 @@ PG_LWLOCKTRANCHE(SUBTRANS_SLRU, SubtransSLRU) PG_LWLOCKTRANCHE(XACT_SLRU, XactSLRU) PG_LWLOCKTRANCHE(PARALLEL_VACUUM_DSA, ParallelVacuumDSA) PG_LWLOCKTRANCHE(AIO_URING_COMPLETION, AioUringCompletion) +PG_LWLOCKTRANCHE(MEMORY_CONTEXT_KEYS, MemoryContextReportingKeys) diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index e52b8eb7697..4f1bcbf709f 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -35,6 +35,7 @@ typedef enum PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to send the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ /* Recovery conflict reasons */ diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 2bc13c3a054..435a2a27c94 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -19,7 +19,6 @@ #include "nodes/memnodes.h" - /* * MaxAllocSize, MaxAllocHugeSize * Quasi-arbitrary limits on size of allocations. @@ -319,4 +318,11 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) return true; } +extern void ProcessGetMemoryContextInterrupt(void); +extern void HandleGetMemoryContextInterrupt(void); +extern void MemoryContextKeysShmemInit(void); +extern Size MemoryContextKeysShmemSize(void); +extern void MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts); +extern void AtProcExit_memstats_cleanup(int code, Datum arg); #endif /* MEMUTILS_H */ diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 0411db832f1..3799ef7c862 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -233,3 +233,22 @@ select * from pg_timezone_abbrevs where abbrev = 'LMT'; LMT | @ 7 hours 52 mins 58 secs ago | f (1 row) +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; +NOTICE: (AllocSet,TopMemoryContext,) +NOTICE: (AllocSet,TopMemoryContext,) diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 66179f026b3..c9da4fc8c90 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -101,3 +101,21 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs; -- One specific case we can check without much fear of breakage -- is the historical local-mean-time value used for America/Los_Angeles. select * from pg_timezone_abbrevs where abbrev = 'LMT'; + +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 09e7f1d420e..c32d56c0c99 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1701,6 +1701,8 @@ MemoryContextData MemoryContextId MemoryContextMethodID MemoryContextMethods +MemoryStatsEntry +MemoryStatsDSHashEntry MemoryStatsPrintFunc MergeAction MergeActionState -- 2.34.1 ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2026-01-13 19:31 Robert Haas <[email protected]> parent: Rahila Syed <[email protected]> 0 siblings, 2 replies; 52+ messages in thread From: Robert Haas @ 2026-01-13 19:31 UTC (permalink / raw) To: Rahila Syed <[email protected]>; +Cc: Daniel Gustafsson <[email protected]>; torikoshia <[email protected]>; pgsql-hackers On Fri, Jan 9, 2026 at 6:22 AM Rahila Syed <[email protected]> wrote: > After further discussion and reviewing Robert's email[1] on this topic, a safer solution > is to avoid running ProcessGetMemoryContextInterrupt during an aborted transaction. > This should help prevent additional errors when the transaction is already in error handling > state. Also, reporting memory context statistics from an aborting transaction won't > be very useful as some of that memory usage won't be valid after abort completes. > Attached is the updated patch that addresses this. I think the question here is whether it's safe to interrupt the server at an arbitrary CFI to run ProcessGetMemoryContextInterrupt. The good news is that we now know that that function won't be executed (or at least not in a substantive way) when we're an aborted transaction. But we have no guarantee that we're in a transaction at all, which I think is concerning, because it seems hard to write code that behaves properly both inside of a transaction and outside of a transaction. Some things I notice reading through: + LWLockAcquire(client_keys_lock, LW_SHARED); LWLocks normally use NamesLikeThis not names_like_this and are generally created by just listing them in lwlocklist.h. Also, it's not really safe to acquire an LWLock if there's no transaction active. If we error afterward, what will release the LWLock? + else + { + clientProcNumber = client_keys[MyProcNumber]; + client_keys[MyProcNumber] = -1; + LWLockRelease(client_keys_lock); + } The "else" is not really necessary here because the "if" portion ends with "return". + memstats_ctx = AllocSetContextCreate((MemoryContext) NULL, + "publish_memory_context_statistics", + ALLOCSET_SMALL_SIZES); The comments do a good job justifying this, but as far as I know it would be the only instance of this pattern in the entire source tree. Are we really sure we want to deviate from the idea of having the memory context tree be a tree? And is it really so bad if the memory used to report memory contexts is included in the output? + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); ... + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); ... + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); Like LWLockAcquire, all of this code supposes that there's a transaction available to manage resource acquisition and release and to clean up after errors. I doubt that any of this is safe without a transaction. I think my view overall here is: bailing out when the current transaction is aborted is a good start in terms of making this safe, but I don't think it's enough. If we get here when the backend is idle, for example, which seems like it would be possible, then we'll have no active transaction, and then I think that a lot of what's being called here is not going to necessarily work as intended. There are other places in the source code that do various push-ups to get around this problem -- e.g. look at RemoveTempRelationsCallback, which knows that it will be called from a transaction but not whether that transaction will be aborted. I don't think that's the approach you want here: restarting an aborted transaction in this context wouldn't be smart. But spinning one up if there isn't one might be reasonable. See ProcessCatchupInterrupt() for an example of existing code that deals with this problem -- again that's probably not quite the same situation, but it gives you an idea what other people have done before. One other point to note here is that if, by some chance, a failure occurs as a result of receiving a memory-context interrupt, it's going to fail the currently-running transaction. Unlike the problem discussed in the previous paragraph, that's not a system-integrity issue: from the point of view of PostgreSQL's transaction system, having process #1 cause process #2's transaction to fail is 100% OK and nothing will break. From the user's point of view, however, this will be very painful: you just wanted to inspect the state of a running transaction, very possibly a long-running one that had done a lot of work, and you accidentally killed it. So I think it's going to be important that such cases are extremely rare in practice. If they happen because of extremely weird scenarios like somebody manually removing a shared memory segment while it's still in use by PostgreSQL, or the system running out of memory at exactly the wrong moment, I think that's probably fine. If they happen because of something like a low-probability race condition, that's going to be unacceptable to users. Nobody is going to be happy about using the feature if it can cause the transactions that they're querying to randomly die, even if the chances are low. I'm not saying the patch actually has this problem, just that it's something to think very carefully about. -- Robert Haas EDB: http://www.enterprisedb.com ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2026-01-13 20:26 Andres Freund <[email protected]> parent: Robert Haas <[email protected]> 1 sibling, 1 reply; 52+ messages in thread From: Andres Freund @ 2026-01-13 20:26 UTC (permalink / raw) To: Robert Haas <[email protected]>; +Cc: Rahila Syed <[email protected]>; Daniel Gustafsson <[email protected]>; torikoshia <[email protected]>; pgsql-hackers Hi, On 2026-01-13 14:31:00 -0500, Robert Haas wrote: > Also, it's not really safe to acquire an LWLock if there's no > transaction active. If we error afterward, what will release the > LWLock? All the error handling paths (hopefully) have an LWLockReleaseAll()... Which is pretty crucial given that we do stuff outside of transactions in other places. That doesn't mean the other concerns about resource management are unfounded, however. Greetings, Andres Freund ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2026-01-13 21:11 Robert Haas <[email protected]> parent: Andres Freund <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Robert Haas @ 2026-01-13 21:11 UTC (permalink / raw) To: Andres Freund <[email protected]>; +Cc: Rahila Syed <[email protected]>; Daniel Gustafsson <[email protected]>; torikoshia <[email protected]>; pgsql-hackers On Tue, Jan 13, 2026 at 3:26 PM Andres Freund <[email protected]> wrote: > All the error handling paths (hopefully) have an LWLockReleaseAll()... Which > is pretty crucial given that we do stuff outside of transactions in other > places. > > That doesn't mean the other concerns about resource management are unfounded, > however. Yeah, I actually wasn't completely sure about that particular comment. I think what will happen if we ERROR outside of a transaction is that it will become FATAL and kill the backend, but I'm not 100% positive about that. -- Robert Haas EDB: http://www.enterprisedb.com ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2026-01-13 21:47 Andres Freund <[email protected]> parent: Robert Haas <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Andres Freund @ 2026-01-13 21:47 UTC (permalink / raw) To: Robert Haas <[email protected]>; +Cc: Rahila Syed <[email protected]>; Daniel Gustafsson <[email protected]>; torikoshia <[email protected]>; pgsql-hackers Hi, On 2026-01-13 16:11:24 -0500, Robert Haas wrote: > On Tue, Jan 13, 2026 at 3:26 PM Andres Freund <[email protected]> wrote: > > All the error handling paths (hopefully) have an LWLockReleaseAll()... Which > > is pretty crucial given that we do stuff outside of transactions in other > > places. > > > > That doesn't mean the other concerns about resource management are unfounded, > > however. > > Yeah, I actually wasn't completely sure about that particular comment. > I think what will happen if we ERROR outside of a transaction is that > it will become FATAL and kill the backend, but I'm not 100% positive > about that. I'm pretty sure that doesn't generally happen. There's promotion to FATAL if the top-level sigsetjmp() hasn't yet run (c.f. the check for PG_exception_stack in errstart()), but once it has been reached, it stays configured. Greetings, Andres Freund ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2026-01-14 13:26 Robert Haas <[email protected]> parent: Andres Freund <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Robert Haas @ 2026-01-14 13:26 UTC (permalink / raw) To: Andres Freund <[email protected]>; +Cc: Rahila Syed <[email protected]>; Daniel Gustafsson <[email protected]>; torikoshia <[email protected]>; pgsql-hackers On Tue, Jan 13, 2026 at 4:47 PM Andres Freund <[email protected]> wrote: > I'm pretty sure that doesn't generally happen. There's promotion to FATAL if > the top-level sigsetjmp() hasn't yet run (c.f. the check for > PG_exception_stack in errstart()), but once it has been reached, it stays > configured. All right, then I guess I don't fully understand how the error-outside-of-a-transaction case is handled. But I still think that code like this needs to run in a transaction to avoid unexpected and undesirable results. Do you see it differently? -- Robert Haas EDB: http://www.enterprisedb.com ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2026-01-14 13:32 Andres Freund <[email protected]> parent: Robert Haas <[email protected]> 0 siblings, 0 replies; 52+ messages in thread From: Andres Freund @ 2026-01-14 13:32 UTC (permalink / raw) To: Robert Haas <[email protected]>; +Cc: Rahila Syed <[email protected]>; Daniel Gustafsson <[email protected]>; torikoshia <[email protected]>; pgsql-hackers Hi, On 2026-01-14 08:26:24 -0500, Robert Haas wrote: > On Tue, Jan 13, 2026 at 4:47 PM Andres Freund <[email protected]> wrote: > > I'm pretty sure that doesn't generally happen. There's promotion to FATAL if > > the top-level sigsetjmp() hasn't yet run (c.f. the check for > > PG_exception_stack in errstart()), but once it has been reached, it stays > > configured. > > All right, then I guess I don't fully understand how the > error-outside-of-a-transaction case is handled. For those we just rely on the error cleanup inside the top-level sigsetmp() blocks... Which of course is a pretty random subset of cleanups that also differs between process types, which is quite ... not great. > But I still think that > code like this needs to run in a transaction to avoid unexpected and > undesirable results. Do you see it differently? I'm waffling on it a bit, tbh. I think doing it for something like backends it could make sense, but e.g. for something like checkpointer, that normally never runs a transaction, it seems like a bad idea. Mainly because processes that don't run transactions won't have an AbortCurrentTransaction() or such in their top-level sigsetjmp() and thus would never abort the transaction that we started, if there were an error while doing the reporting. Greetings, Andres Freund ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2026-01-19 17:31 Rahila Syed <[email protected]> parent: Robert Haas <[email protected]> 1 sibling, 1 reply; 52+ messages in thread From: Rahila Syed @ 2026-01-19 17:31 UTC (permalink / raw) To: Robert Haas <[email protected]>; +Cc: Daniel Gustafsson <[email protected]>; torikoshia <[email protected]>; pgsql-hackers Hi, Thank you for your feedback. Please find a few responses inline. I am working on incorporating all the feedback in the patch and will share it in a follow-up email. > Some things I notice reading through: > > + LWLockAcquire(client_keys_lock, LW_SHARED); > > LWLocks normally use NamesLikeThis not names_like_this and are > generally created by just listing them in lwlocklist.h. > > I will fix it, accordingly. > + else > + { > + clientProcNumber = client_keys[MyProcNumber]; > + client_keys[MyProcNumber] = -1; > + LWLockRelease(client_keys_lock); > + } > > The "else" is not really necessary here because the "if" portion ends > with "return". > Fixed this in the attached patch. > > + memstats_ctx = AllocSetContextCreate((MemoryContext) NULL, > + > "publish_memory_context_statistics", > + > ALLOCSET_SMALL_SIZES); > > The comments do a good job justifying this, but as far as I know it > would be the only instance of this pattern in the entire source tree. > Are we really sure we want to deviate from the idea of having the > memory context tree be a tree? And is it really so bad if the memory > used to report memory contexts is included in the output? > > This was implemented in response to a review suggestion [1]. If required, it can be updated to create one under CurrentMemoryContext. + MemoryStatsDsaArea = > GetNamedDSA("memory_context_statistics_dsa", > + > &found); > ... > + MemoryStatsDsHash = > GetNamedDSHash("memory_context_statistics_dshash", > + > &memctx_dsh_params, &found); > ... > + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); > > Like LWLockAcquire, all of this code supposes that there's a > transaction available to manage resource acquisition and release and > to clean up after errors. I doubt that any of this is safe without a > transaction. > Starting a transaction from CFI works for a client backend but not for auxiliary processes. When I try to execute StartTransaction() from a checkpointer process, the following assertion fails, Assert(MyProc->vxid.procNumber == vxid.procNumber); This is because MyProc->vxid.procNumber is not set for auxiliary processes. Please find attached a rebased patch, that fixes a build error reported on the commit-fest app. [1]. PostgreSQL: Re: Prevent an error on attaching/creating a DSM/DSA from an interrupt handler. <https://www.postgresql.org/message-id/594293.1747708165%40sss.pgh.pa.us; Thank you, Rahila Syed Attachments: [application/octet-stream] v48-0002-Test-module-to-test-memory-context-reporting-wit.patch (8.8K, ../../CAH2L28u=78SynqRkNn_v0Xo=c7eiaQQ2b5QQ-pSOmMH-z52eqQ@mail.gmail.com/3-v48-0002-Test-module-to-test-memory-context-reporting-wit.patch) download | inline diff: From 56ee69ad6c2a9a1e6cd3c22d8c2bbb3d46a39127 Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Fri, 28 Nov 2025 14:46:38 +0530 Subject: [PATCH 2/2] Test module to test memory context reporting with injection points --- src/test/modules/Makefile | 1 + .../test_memcontext_reporting/Makefile | 29 ++++ .../t/001_memcontext_inj.pl | 150 ++++++++++++++++++ .../test_memcontext_reporting.c | 12 ++ .../test_memcontext_reporting.control | 4 + 5 files changed, 196 insertions(+) create mode 100644 src/test/modules/test_memcontext_reporting/Makefile create mode 100644 src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index 4c6d56d97d8..1156d731014 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -34,6 +34,7 @@ SUBDIRS = \ test_json_parser \ test_lfind \ test_lwlock_tranches \ + test_memcontext_reporting \ test_misc \ test_oat_hooks \ test_parser \ diff --git a/src/test/modules/test_memcontext_reporting/Makefile b/src/test/modules/test_memcontext_reporting/Makefile new file mode 100644 index 00000000000..0a2dfc44f1c --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/Makefile @@ -0,0 +1,29 @@ +# src/test/modules/test_memcontext_reporting/Makefile + +EXTRA_INSTALL = src/test/modules/injection_points + +export enable_injection_points +MODULE_big = test_memcontext_reporting +OBJS = \ + $(WIN32RES) \ + test_memcontext_reporting.o +PGFILEDESC = "test_memcontext_reporting - test code for memory context reporting" + +REGRESS = test_memcontext_reporting + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_memcontext_reporting +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif + +check: + $(prove_check) + +installcheck: + $(prove_installcheck) diff --git a/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl new file mode 100644 index 00000000000..b491d6ebc0a --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl @@ -0,0 +1,150 @@ +# Copyright (c) 2025, PostgreSQL Global Development Group + +# Test suite for testing memory context statistics reporting + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} +my $psql_err; +my $psql_out; +# Create and start a cluster with one node +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->append_conf( + 'postgresql.conf', + qq[ +max_connections = 100 +log_statement = none +restart_after_crash = false +]); +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); + +# Attaching to a client process's injection point that throws an error +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-client-injection', 'error');" +); + +my $pid = $node->safe_psql('postgres', + "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); + +#Client should have thrown error +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); +like($psql_err, + qr/error triggered for injection point memcontext-client-injection/); + +#Query the same process after detaching the injection point, using some other client and it should succeed. +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-client-injection');"); +my $topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name = 'TopMemoryContext'); + +# Attaching to a target process injection point that throws an error +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-server-injection', 'error');" +); + +#Server should have thrown error +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); + +#Query the same process after detaching the injection point, using some other client and it should succeed. +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-injection');"); +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name = 'TopMemoryContext'); + +# Test that two concurrent requests to the same process results in a warning for +# one of those + +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-client-injection', 'wait');"); +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-server-wait', 'wait');"); +my $psql_session1 = $node->background_psql('postgres'); +$psql_session1->query_until( + qr//, + qq( + SELECT pg_get_process_memory_contexts($pid, true); +)); +$node->wait_for_event('client backend', 'memcontext-client-injection'); +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); +ok($psql_err =~ + /WARNING: server process $pid is processing previous request/); +#Wake the client up. +$node->safe_psql('postgres', + "SELECT injection_points_wakeup('memcontext-client-injection');"); + +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-client-injection');"); +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-wait');"); + +# Test the client process exiting with timeout does not break the server process + +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-server-wait', 'wait');"); +# Following client query times out, returning NULL as output +$node->psql( + 'postgres', + qq(select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';), + stdout => \$psql_out); +ok($psql_out = 'NULL'); +#Wakeup the server process up and detach the injection point. +$node->safe_psql('postgres', + "SELECT injection_points_wakeup('memcontext-server-wait');"); +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-wait');"); +#Query the same server process again and it should succeed. +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name = 'TopMemoryContext'); + +# Test if the monitoring works fine, when the client backend crashes. +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-client-injection', 'test_memcontext_reporting', 'crash', NULL);" +); + +#Client will crash +$node->psql( + 'postgres', + qq(select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';), + stderr => \$psql_err); +like($psql_err, + qr/WARNING: terminating connection because of crash of another server process|server closed the connection unexpectedly|connection to server was lost|could not send data to server/ +); + +# Wait till server restarts +$node->restart; +$node->poll_query_until('postgres', "SELECT 1;", '1'); + +#Querying memory stats should succeed after server start +$pid = $node->safe_psql('postgres', + "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name = 'TopMemoryContext'); + +done_testing(); diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c new file mode 100644 index 00000000000..d641f3616dc --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c @@ -0,0 +1,12 @@ +#include "postgres.h" +#include "funcapi.h" + +PG_MODULE_MAGIC; + +extern PGDLLEXPORT void crash(const char *name, const void *private_data, void *arg); + +void +crash(const char *name, const void *private_data, void *arg) +{ + abort(); +} diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control new file mode 100644 index 00000000000..48b501682d5 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control @@ -0,0 +1,4 @@ +comment = 'Test code for memcontext reporting' +default_version = '1.0' +module_pathname = '$libdir/test_memcontext_reporting' +relocatable = true -- 2.34.1 [application/octet-stream] v48-0001-Add-function-to-report-memory-context-statistics.patch (58.0K, ../../CAH2L28u=78SynqRkNn_v0Xo=c7eiaQQ2b5QQ-pSOmMH-z52eqQ@mail.gmail.com/4-v48-0001-Add-function-to-report-memory-context-statistics.patch) download | inline diff: From a2743b93abc6c05c84b544155027a878734351c7 Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Thu, 27 Nov 2025 14:39:43 +0530 Subject: [PATCH 1/2] Add function to report memory context statistics This adds a function for retrieving memory context statistics and information from backends as well as auxiliary processes. The intended usecase is cluster debugging when under memory pressure or unanticipated memory usage characteristics. When calling the function it sends a signal to the specified process to submit statistics regarding its memory contexts into dynamic shared memory. Each memory context is returned in detail, followed by a cumulative total in case the number of contexts exceed the max allocated amount of shared memory. Each process is limited to use at most 1Mb memory for this. A summary can also be explicitly requested by the user, this will return the TopMemoryContext and a cumulative total of all lower contexts. In order to not block on busy processes, we have hardcoded the number of seconds during which to retry before timing out. In the case where no statistics are published within the set timeout, NULL is returned --- doc/src/sgml/func/func-admin.sgml | 159 +++ src/backend/catalog/system_functions.sql | 14 + src/backend/postmaster/autovacuum.c | 4 + src/backend/postmaster/checkpointer.c | 4 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 4 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 4 + src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/storage/lmgr/proc.c | 1 + src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 2 + src/backend/utils/adt/mcxtfuncs.c | 1013 ++++++++++++++++- src/backend/utils/init/globals.c | 1 + src/backend/utils/init/postinit.c | 7 + src/backend/utils/mmgr/mcxt.c | 31 + src/include/catalog/pg_proc.dat | 10 + src/include/miscadmin.h | 1 + src/include/storage/lwlocklist.h | 1 + src/include/storage/procsignal.h | 1 + src/include/utils/memutils.h | 8 +- src/test/regress/expected/sysviews.out | 19 + src/test/regress/sql/sysviews.sql | 18 + src/tools/pgindent/typedefs.list | 2 + 25 files changed, 1300 insertions(+), 21 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index e7ea16f73b3..e6b9d004ac4 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -251,6 +251,132 @@ <literal>false</literal> is returned. </para></entry> </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_get_process_memory_contexts</primary> + </indexterm> + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type> <optional>,<parameter>summary</parameter> <type>boolean</type> <literal>DEFAULT</literal> <literal>false</literal></optional> ) + <returnvalue>setof record</returnvalue> + ( <parameter>name</parameter> <type>text</type>, + <parameter>ident</parameter> <type>text</type>, + <parameter>type</parameter> <type>text</type>, + <parameter>level</parameter> <type>integer</type>, + <parameter>path</parameter> <type>integer[]</type>, + <parameter>total_bytes</parameter> <type>bigint</type>, + <parameter>total_nblocks</parameter> <type>bigint</type>, + <parameter>free_bytes</parameter> <type>bigint</type>, + <parameter>free_chunks</parameter> <type>bigint</type>, + <parameter>used_bytes</parameter> <type>bigint</type>, + <parameter>num_agg_contexts</parameter> <type>integer</type> ) + </para> + <para> + This function handles requests to display the memory contexts of a + <productname>PostgreSQL</productname> process with the specified + process ID. The function can be used to send requests to backends as + well as <glossterm linkend="glossary-auxiliary-proc">auxiliary processes</glossterm>. + If the process does not respond with memory contexts statistics in 5 seconds, + function returns NULL. + </para> + <para> + The returned record contains extended statistics per each memory + context: + <itemizedlist spacing="compact"> + <listitem> + <para> + <parameter>name</parameter> - The name of the memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>ident</parameter> - Memory context ID (if any). + </para> + </listitem> + <listitem> + <para> + <parameter>type</parameter> - The type of memory context, possible + values are: AllocSet, Generation, Slab and Bump. + </para> + </listitem> + <listitem> + <para> + <parameter>level</parameter> - The level in the tree of the current + memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>path</parameter> - Memory contexts are organized in a + tree model with TopMemoryContext as the root, and all other memory + contexts as nodes in the tree. The <parameter>path</parameter> + displays the path from the root to the current memory context. The + path is limited to 100 children per node, which each node limited + to a max depth of 100, to preserve memory during reporting. The + printed path will also be limited to 100 nodes counting from the + TopMemoryContext. + </para> + </listitem> + <listitem> + <para> + <parameter>total_bytes</parameter> - The total number of bytes + allocated to this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_nblocks</parameter> - The total number of blocks + used for the allocated memory. + </para> + </listitem> + <listitem> + <para> + <parameter>free_bytes</parameter> - The amount of free memory in + this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>free_chunks</parameter> - The number of chunks that + <parameter>free_bytes</parameter> corresponds to. + </para> + </listitem> + <listitem> + <para> + <parameter>used_bytes</parameter> - The total number of bytes + currently occupied. + </para> + </listitem> + <listitem> + <para> + <parameter>num_agg_contexts</parameter> - The number of memory + contexts aggregated in the displayed statistics. + </para> + </listitem> + </itemizedlist> + </para> + <para> + When <parameter>summary</parameter> is <literal>true</literal>, statistics + for memory contexts at levels 1 and 2 are displayed, with level 1 + representing the root node (i.e., <literal>TopMemoryContext</literal>). + Statistics for contexts on level 2 and below are aggregates of all + child contexts' statistics, where <literal>num_agg_contexts</literal> + indicate the number aggregated child contexts. When + <parameter>summary</parameter> is <literal>false</literal> (the default), + <literal>the num_agg_contexts</literal> value is <literal>1</literal>, + indicating that individual statistics are being displayed. + </para> + <para> + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context + statistics are aggregated and a cumulative total is displayed. The + <literal>num_agg_contexts</literal> column indicates the number of + contexts aggregated in the displayed statistics. When + <literal>num_agg_contexts</literal> is <literal>1</literal> it means + that the context statistics are displayed separately. + </para></entry> + </row> </tbody> </tgroup> </table> @@ -302,6 +428,39 @@ LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 because it may generate a large number of log messages. </para> + <para> + <function>pg_get_process_memory_contexts</function> can be used to request + memory contexts statistics of any <productname>PostgreSQL</productname> + process. For example: +<programlisting> +postgres=# SELECT * FROM pg_get_process_memory_contexts( + (SELECT pid FROM pg_stat_activity + WHERE backend_type = 'checkpointer'), + false) LIMIT 1; +-[ RECORD 1 ]----+------------------------------ +name | TopMemoryContext +ident | +type | AllocSet +level | 1 +path | {1} +total_bytes | 90304 +total_nblocks | 3 +free_bytes | 2880 +free_chunks | 1 +used_bytes | 87424 +num_agg_contexts | 1 +</programlisting> + <note> + <para> + While <function>pg_get_process_memory_contexts</function> can be used to + query memory contexts of the local backend, + <structname>pg_backend_memory_contexts</structname> + (see <xref linkend="view-pg-backend-memory-contexts"/> for more details) + will be less resource intensive when only the local backend is of interest. + </para> + </note> + </para> + </sect2> <sect2 id="functions-admin-backup"> diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql index eb9e31ae1bf..d1416e5534d 100644 --- a/src/backend/catalog/system_functions.sql +++ b/src/backend/catalog/system_functions.sql @@ -657,6 +657,17 @@ LANGUAGE INTERNAL STRICT VOLATILE PARALLEL UNSAFE AS 'pg_replication_origin_session_setup'; +CREATE OR REPLACE FUNCTION + pg_get_process_memory_contexts(IN pid integer, IN summary boolean DEFAULT false, + OUT name text, OUT ident text, OUT type text, OUT level integer, + OUT path integer[], OUT total_bytes bigint, OUT total_nblocks bigint, + OUT free_bytes bigint, OUT free_chunks bigint, OUT used_bytes bigint, + OUT num_agg_contexts integer) +RETURNS SETOF RECORD +LANGUAGE INTERNAL +STRICT VOLATILE PARALLEL UNSAFE +AS 'pg_get_process_memory_contexts'; + -- -- The default permissions for functions mean that anyone can execute them. -- A number of functions shouldn't be executable by just anyone, but rather @@ -782,6 +793,7 @@ REVOKE EXECUTE ON FUNCTION pg_ls_logicalmapdir() FROM PUBLIC; REVOKE EXECUTE ON FUNCTION pg_ls_replslotdir(text) FROM PUBLIC; +REVOKE EXECUTE ON FUNCTION pg_get_process_memory_contexts(integer, boolean) FROM PUBLIC; -- -- We also set up some things as accessible to standard roles. -- @@ -808,6 +820,8 @@ GRANT EXECUTE ON FUNCTION pg_current_logfile() TO pg_monitor; GRANT EXECUTE ON FUNCTION pg_current_logfile(text) TO pg_monitor; +GRANT EXECUTE ON FUNCTION pg_get_process_memory_contexts(integer, boolean) TO pg_read_all_stats; + GRANT pg_read_all_settings TO pg_monitor; GRANT pg_read_all_stats TO pg_monitor; diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 22379de1e31..a00bac51264 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -788,6 +788,10 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index 6482c21b8f9..1b7d5e7ffdc 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -685,6 +685,10 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index a2c0ff012c5..cb18ce80893 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -48,6 +48,10 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index 1a20387c4bd..3ac0fba225a 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -871,6 +871,10 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index a1a4f65f9a9..18a9e7f85e1 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -192,6 +192,10 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index c3d56c866d3..49dc3e88023 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -879,6 +879,10 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index 85c67b2c183..211199e985a 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -52,6 +52,7 @@ #include "storage/sinvaladt.h" #include "utils/guc.h" #include "utils/injection_point.h" +#include "utils/memutils.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -141,6 +142,7 @@ CalculateShmemSize(void) size = add_size(size, AioShmemSize()); size = add_size(size, WaitLSNShmemSize()); size = add_size(size, LogicalDecodingCtlShmemSize()); + size = add_size(size, MemoryContextKeysShmemSize() + sizeof(LWLockPadded)); /* include additional requested shmem from preload libraries */ size = add_size(size, total_addin_request); @@ -330,6 +332,7 @@ CreateOrAttachShmemStructs(void) AioShmemInit(); WaitLSNShmemInit(); LogicalDecodingCtlShmemInit(); + MemoryContextKeysShmemInit(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index 8e56922dcea..601e01d2574 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -694,6 +694,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_GET_MEMORY_CONTEXT)) + HandleGetMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 063826ae576..0fdfa59d0b2 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -51,6 +51,7 @@ #include "storage/procsignal.h" #include "storage/spin.h" #include "storage/standby.h" +#include "utils/memutils.h" #include "utils/timeout.h" #include "utils/timestamp.h" diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index e54bf1e760f..199245e4c1f 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3539,6 +3539,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 5537a2d2530..f6e8feac6c8 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -163,6 +163,7 @@ WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." +MEM_CXT_PUBLISH "Waiting for a process to publish memory information." ABI_compatibility: @@ -410,6 +411,7 @@ SubtransSLRU "Waiting to access the sub-transaction SLRU cache." XactSLRU "Waiting to access the transaction status SLRU cache." ParallelVacuumDSA "Waiting for parallel vacuum dynamic shared memory allocation." AioUringCompletion "Waiting for another process to complete IO via io_uring." +MemoryContextReportingKeys "Waiting for another process to complete reading or writing the memory reporting keys." # No "ABI_compatibility" region here as WaitEventLWLock has its own C code. diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index 12b8d4cefaf..472ee1b96e3 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -15,24 +15,133 @@ #include "postgres.h" +#include "access/xact.h" #include "funcapi.h" #include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "storage/dsm_registry.h" #include "storage/proc.h" #include "storage/procarray.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/injection_point.h" +#include "utils/memutils.h" +#include "utils/wait_event_types.h" + +/* + * Memory Context reporting size limits. + */ + +/* Max length of context name and ident, to keep it consistent + * with ProcessLogMemoryContext() + */ +#define MEMORY_CONTEXT_IDENT_SHMEM_SIZE 100 +#define MEMORY_CONTEXT_NAME_SHMEM_SIZE 100 + +/* Maximum size (in bytes) of DSA area per process */ +#define MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND ((size_t) (1 * 1024 * 1024)) + +/* + * Maximum number of memory context statistics is calculated by dividing + * max memory allocated per backend with maximum size per context statistics. + * The identifier and name are statically allocated arrays of size 100 bytes. + * The path depth is limited to 100 like for memory context logging. + */ +#define MAX_MEMORY_CONTEXT_STATS_NUM MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND / (sizeof(MemoryStatsEntry)) + +/* + * Size of dshash key. The key is a uint32 rendered as a string, 10 chars + * plus space for a NULL terminator can hold all the values. + */ +#define CLIENT_KEY_SIZE (10 + 1) + +/* Dynamic shared memory state for reporting statistics per context */ +typedef struct MemoryStatsEntry +{ + char name[MEMORY_CONTEXT_NAME_SHMEM_SIZE]; + char ident[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + int path[100]; + NodeTag type; + int path_length; + int levels; + int64 totalspace; + int64 nblocks; + int64 freespace; + int64 freechunks; + int num_agg_stats; +} MemoryStatsEntry; + +/* + * Per backend dynamic shared hash entry for memory context statistics + * reporting. + */ +typedef struct MemoryStatsDSHashEntry +{ + char key[64]; + ConditionVariable memcxt_cv; + bool stats_written; + int target_server_id; + int total_stats; + bool summary; + dsa_pointer memstats_dsa_pointer; +} MemoryStatsDSHashEntry; + +static const dshash_parameters memctx_dsh_params = { + offsetof(MemoryStatsDSHashEntry, memcxt_cv), + sizeof(MemoryStatsDSHashEntry), + dshash_strcmp, + dshash_strhash, + dshash_strcpy +}; + +/* + * These are used for reporting memory context statistics of a process. + */ + +/* Lock to control access to client_keys array */ +static LWLock *client_keys_lock = NULL; + +/* Array to store the keys of MemoryStatsDsHash */ +static int *client_keys = NULL; + +/* + * Table to store pointers to DSA memory containing memory statistics and other + * metadata. There is one entry per client backend request, keyed by ProcNumber + * of the client obtained from client_keys array above. + */ +static dshash_table *MemoryStatsDsHash = NULL; + +/* + * Dsa area which stores the actual memory context + * statistics. + */ +static dsa_area *MemoryStatsDsaArea = NULL; + +static void memstats_dsa_cleanup(char *key); +static void memstats_client_key_reset(int ProcNumber); +static const char *ContextTypeToString(NodeTag type); +static void PublishMemoryContext(MemoryStatsEntry *memcxt_info, + int curr_id, MemoryContext context, + List *path, + MemoryContextCounters stat, + int num_contexts); +static List *compute_context_path(MemoryContext c, HTAB *context_id_lookup); /* ---------- * The max bytes for showing identifiers of MemoryContext. + * This is used by pg_get_backend_memory_context - view used for local backend. * ---------- */ #define MEMORY_CONTEXT_IDENT_DISPLAY_SIZE 1024 +#define MAX_PATH_DISPLAY_LENGTH 100 +/* Timeout in seconds */ +#define MEMORY_STATS_MAX_TIMEOUT 5 + /* * MemoryContextId - * Used for storage of transient identifiers for - * pg_get_backend_memory_contexts. + * Used for storage of transient identifiers for memory context reporting */ typedef struct MemoryContextId { @@ -143,24 +252,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, else nulls[1] = true; - switch (context->type) - { - case T_AllocSetContext: - type = "AllocSet"; - break; - case T_GenerationContext: - type = "Generation"; - break; - case T_SlabContext: - type = "Slab"; - break; - case T_BumpContext: - type = "Bump"; - break; - default: - type = "???"; - break; - } + type = ContextTypeToString(context->type); values[2] = CStringGetTextDatum(type); values[3] = Int32GetDatum(list_length(path)); /* level */ @@ -175,6 +267,38 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, list_free(path); } +/* + * ContextTypeToString + * Returns a textual representation of a context type + * + * This should cover the same types as MemoryContextIsValid. + */ +const char * +ContextTypeToString(NodeTag type) +{ + const char *context_type; + + switch (type) + { + case T_AllocSetContext: + context_type = "AllocSet"; + break; + case T_GenerationContext: + context_type = "Generation"; + break; + case T_SlabContext: + context_type = "Slab"; + break; + case T_BumpContext: + context_type = "Bump"; + break; + default: + context_type = "???"; + break; + } + return context_type; +} + /* * pg_get_backend_memory_contexts * SQL SRF showing backend memory context. @@ -305,3 +429,852 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } + +/* + * pg_get_process_memory_contexts + * Signal a backend or an auxiliary process to send its memory contexts, + * wait for the results and display them. + * + * By default, only superusers or users with ROLE_PG_READ_ALL_STATS are allowed + * to signal a process to return the memory contexts. Additional roles can be + * permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to copy the memory context details + * to a dynamic shared memory space. + * + * We have defined a limit on DSA memory that could be allocated per process - + * if the process has more memory contexts than what can fit in the allocated + * size, the excess contexts are summarized and represented as cumulative total + * at the end of the buffer. + * + * After sending the signal, wait on a condition variable. The publishing + * backend, after copying the data to shared memory, sends a signal on that + * condition variable. There is one condition variable per client process. + * Once the condition variable is signalled, check if the latest memory context + * information is available and display. + * + * If the publishing backend does not respond before the condition variable + * times out, which is set to a predefined value MEMORY_STATS_MAX_TIMEOUT, give + * up and return NULL. + */ +Datum +pg_get_process_memory_contexts(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + bool summary = PG_GETARG_BOOL(1); + PGPROC *proc; + ProcNumber procNumber; + bool proc_is_aux = false; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + MemoryStatsEntry *memcxt_info; + MemoryStatsDSHashEntry *entry; + bool found; + char key[CLIENT_KEY_SIZE]; + TimestampTz start_timestamp; + + /* + * See if the process with given pid is a backend or an auxiliary process + * and remember the type for when we requery the process later. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + { + proc = AuxiliaryPidGetProc(pid); + proc_is_aux = true; + } + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; this is however not a problem and leave with a WARNING. + * See comment in pg_log_backend_memory_contexts for a discussion on this. + */ + if (proc == NULL) + { + ereport(WARNING, + errmsg("PID %d is not a PostgreSQL server process", pid)); + PG_RETURN_NULL(); + } + + procNumber = GetNumberFromPGProc(proc); + + /* + * Check if the server process slot is not empty and exit early Non-empty + * slot means some other client backend is requesting the statistics from + * the same server process. + */ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + if (client_keys[procNumber] != -1) + { + LWLockRelease(client_keys_lock); + ereport(WARNING, + errmsg("server process %d is processing previous request", + pid)); + PG_RETURN_NULL(); + } + LWLockRelease(client_keys_lock); + + /* + * Create a DSA to allocate memory for copying memory contexts statistics. + * Allocate the memory in the DSA and send DSA pointer to the server + * process for storing the context statistics. If number of contexts + * exceed a predefined limit (1MB), a cumulative total is stored for such + * contexts. + * + * The DSA is created once for the lifetime of the server, and only + * attached in subsequent calls. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); + + /* + * The DSA pointers containing statistics for each client are stored in a + * dshash table. In addition to DSA pointer, each entry in this table also + * contains information about the statistics, condition variable for + * signalling between client and the server and miscellaneous data + * specific to a request. There is one entry per client request in the + * hash table. + */ + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); + + snprintf(key, sizeof(key), "%d", MyProcNumber); + + /* + * Insert an entry for this client in DSHASH table the first time this + * function is called. This entry is deleted when the process exits in + * before_shmem_exit call. + * + * dshash_find_or_insert locks the entry to prevent the publisher from + * reading before client has updated the entry. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + if (!found) + { + entry->stats_written = false; + ConditionVariableInit(&entry->memcxt_cv); + } + + /* + * Allocate 1MB of memory for the backend to publish its statistics on + * every call to this function. The memory is freed at the end of the + * function. + */ + entry->memstats_dsa_pointer = + dsa_allocate0(MemoryStatsDsaArea, MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND); + + /* + * Specify whether a summary of statistics is requested, before signalling + * the server. + */ + entry->summary = summary; + + /* + * Indicate which server process statistics are being requested from. If + * this client times out before the last requested process can publish its + * statistics, it may send a new request to another server process. Since + * the previous server was notified, it might attempt to read the same + * client entry and respond incorrectly with its statistics. By storing + * the server ID in the client entry, we prevent any previously signalled + * server process from writing its statistics in the space meant for the + * newly requested process. + */ + entry->target_server_id = pid; + dshash_release_lock(MemoryStatsDsHash, entry); + + /* + * Check if the publishing process slot is empty and store this clients + * key i.e its procNumber. This informs the publishing process that it is + * supposed to write statistics in the hash entry corresponding to this + * client. + */ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + if (client_keys[procNumber] == -1) + client_keys[procNumber] = MyProcNumber; + else + { + LWLockRelease(client_keys_lock); + ereport(WARNING, + errmsg("server process %d is processing previous request", + pid)); + PG_RETURN_NULL(); + } + LWLockRelease(client_keys_lock); + + /* + * Send a signal to a PostgreSQL process, informing it we want it to + * produce information about its memory contexts. + */ + if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, procNumber) < 0) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ereport(WARNING, + errmsg("could not send signal to process %d: %m", + pid)); + PG_RETURN_NULL(); + } + start_timestamp = GetCurrentTimestamp(); + + while (1) + { + long elapsed_time; + + INJECTION_POINT("memcontext-client-injection", NULL); + + elapsed_time = TimestampDifferenceMilliseconds(start_timestamp, + GetCurrentTimestamp()); + /* Return if we have already exceeded the timeout */ + if (elapsed_time >= MEMORY_STATS_MAX_TIMEOUT * 1000) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + PG_RETURN_NULL(); + } + + /* + * Recheck the state of the backend before sleeping on the condition + * variable to ensure the process is still alive. Only check the + * relevant process type based on the earlier PID check. + */ + if (proc_is_aux) + proc = AuxiliaryPidGetProc(pid); + else + proc = BackendPidGetProc(pid); + + /* + * The target server process ending during memory context processing + * is not an error. + */ + if (proc == NULL) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + ereport(WARNING, + errmsg("PID %d is no longer a PostgreSQL server process", + pid)); + PG_RETURN_NULL(); + } + + /* + * Wait for MEMORY_STATS_MAX_TIMEOUT. If no statistics are available + * within the allowed time then return NULL. The timer is defined in + * milliseconds since that's what the condition variable sleep uses. + */ + if (ConditionVariableTimedSleep(&entry->memcxt_cv, + (MEMORY_STATS_MAX_TIMEOUT * 1000), + WAIT_EVENT_MEM_CXT_PUBLISH)) + { + /* Timeout has expired, return NULL */ + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + PG_RETURN_NULL(); + } + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + Assert(found); + + memcxt_info = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + /* + * We expect to come out of sleep when the requested process has + * finished publishing the statistics, verified using a boolean + * stats_written. + * + * Make sure that the statistics are actually written by checking that + * the name of the context is not NULL. This is done to ensure that + * the subsequent waits for statistics do not return spuriously if the + * previous call to the function ended in error and thus could not + * clear the stats_written flag. + */ + if (entry->stats_written && memcxt_info[0].name[0] != '\0') + break; + + dshash_release_lock(MemoryStatsDsHash, entry); + + } + + InitMaterializedSRF(fcinfo, 0); + + /* + * Backend has finished publishing the stats, project them. + */ +#define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 11 + for (int i = 0; i < entry->total_stats; i++) + { + ArrayType *path_array; + int path_length; + Datum values[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + bool nulls[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + Datum *path_datum = NULL; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + Assert(memcxt_info[i].name[0] != '\0'); + values[0] = CStringGetTextDatum(memcxt_info[i].name); + + if (memcxt_info[i].ident[0] != '\0') + values[1] = CStringGetTextDatum(memcxt_info[i].ident); + else + nulls[1] = true; + + values[2] = CStringGetTextDatum(ContextTypeToString(memcxt_info[i].type)); + values[3] = Int32GetDatum(memcxt_info[i].levels); + + path_length = memcxt_info[i].path_length; + path_datum = (Datum *) palloc(path_length * sizeof(Datum)); + if (memcxt_info[i].path[0] != 0) + { + for (int j = 0; j < path_length; j++) + path_datum[j] = Int32GetDatum(memcxt_info[i].path[j]); + path_array = construct_array_builtin(path_datum, + path_length, + INT4OID); + values[4] = PointerGetDatum(path_array); + } + else + nulls[4] = true; + + values[5] = Int64GetDatum(memcxt_info[i].totalspace); + values[6] = Int64GetDatum(memcxt_info[i].nblocks); + values[7] = Int64GetDatum(memcxt_info[i].freespace); + values[8] = Int64GetDatum(memcxt_info[i].freechunks); + values[9] = Int64GetDatum(memcxt_info[i].totalspace - + memcxt_info[i].freespace); + values[10] = Int32GetDatum(memcxt_info[i].num_agg_stats); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + dshash_release_lock(MemoryStatsDsHash, entry); + memstats_dsa_cleanup(key); + + ConditionVariableCancelSleep(); + + PG_RETURN_NULL(); +} + +static void +memstats_dsa_cleanup(char *key) +{ + MemoryStatsDSHashEntry *entry; + + entry = dshash_find(MemoryStatsDsHash, key, true); + + Assert(MemoryStatsDsaArea != NULL); + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + entry->memstats_dsa_pointer = InvalidDsaPointer; + entry->stats_written = false; + entry->target_server_id = 0; + + dshash_release_lock(MemoryStatsDsHash, entry); +} + +/* + * Remove this process from the publishing process' + * client key slot, if the stats publishing process has failed to do so. + */ +static void +memstats_client_key_reset(int procNumber) +{ + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + + if (client_keys[procNumber] == MyProcNumber) + client_keys[procNumber] = -1; + LWLockRelease(client_keys_lock); +} + +void +MemoryContextKeysShmemInit(void) +{ + bool found; + + client_keys = (int *) + ShmemInitStruct("MemoryContextKeys", + MemoryContextKeysShmemSize() + sizeof(LWLockPadded), &found); + client_keys_lock = (LWLock *) ((char *) client_keys + MemoryContextKeysShmemSize()); + + if (!found) + { + MemSet(client_keys, -1, MemoryContextKeysShmemSize()); + LWLockInitialize(client_keys_lock, LWTRANCHE_MEMORY_CONTEXT_KEYS); + } +} + +Size +MemoryContextKeysShmemSize(void) +{ + Size sz = 0; + Size TotalProcs = 0; + + TotalProcs = add_size(TotalProcs, NUM_AUXILIARY_PROCS); + TotalProcs = add_size(TotalProcs, MaxBackends); + sz = add_size(sz, mul_size(TotalProcs, sizeof(int))); + + return sz; +} + +/* + * HandleGetMemoryContextInterrupt + * Handle receipt of an interrupt indicating a request to publish memory + * contexts statistics. + * + * All the actual work is deferred to ProcessGetMemoryContextInterrupt() as + * this cannot be performed in a signal handler. + */ +void +HandleGetMemoryContextInterrupt(void) +{ + InterruptPending = true; + PublishMemoryContextPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + +/* + * ProcessGetMemoryContextInterrupt + * Generate information about memory contexts used by the process. + * + * Performs a breadth first search on the memory context tree, thus parents + * statistics are reported before their children in the monitoring function + * output. + * + * Statistics for all the processes are shared via the same dynamic shared + * area. Individual statistics are tracked independently in per-process DSA + * pointers. These pointers are stored in a dshash table with key as requesting + * clients ProcNumber. + * + * We calculate maximum number of context's statistics that can be displayed + * using a pre-determined limit for memory available per process for this + * utility and maximum size of statistics for each context. The remaining + * context statistics if any are captured as a cumulative total at the end of + * individual context's statistics. + * + * If summary is true, we capture the level 1 and level 2 contexts statistics. + * For that we traverse the memory context tree recursively in depth first + * search manner to cover all the children of a parent context, to be able to + * display a cumulative total of memory consumption by a parent at level 2 and + * all its children. + */ +void +ProcessGetMemoryContextInterrupt(void) +{ + List *contexts; + HASHCTL ctl; + HTAB *context_id_lookup; + int context_id = 0; + MemoryStatsEntry *meminfo; + MemoryContextCounters stat; + int num_individual_stats = 0; + bool found; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + int clientProcNumber; + MemoryContext memstats_ctx = NULL; + MemoryContext oldcontext = NULL; + + PublishMemoryContextPending = false; + + /* + * Avoid performing any shared memory operations in aborted transaction, + * the caller will get the fallback behaviour of the past known stats. + */ + if (IsAbortedTransactionBlockState()) + return; + + INJECTION_POINT("memcontext-server-wait", NULL); + + /* + * Retrieve the client key for publishing statistics and reset it to -1, + * so other clients can request memory statistics from this process. + * Return if the client_key is -1, which means the requesting client has + * timed out. + */ + LWLockAcquire(client_keys_lock, LW_SHARED); + if (client_keys[MyProcNumber] == -1) + { + LWLockRelease(client_keys_lock); + return; + } + else + { + clientProcNumber = client_keys[MyProcNumber]; + client_keys[MyProcNumber] = -1; + LWLockRelease(client_keys_lock); + } + + /* + * Create a new memory context which is not a part of TopMemoryContext + * tree. This context is used to allocate all memory in this function. + * This helps in keeping the memory allocation in this function to report + * memory consumption statistics separate. So that it does not affect the + * output of this function. + */ + memstats_ctx = AllocSetContextCreate((MemoryContext) NULL, + "publish_memory_context_statistics", + ALLOCSET_SMALL_SIZES); + oldcontext = MemoryContextSwitchTo(memstats_ctx); + + /* + * The hash table is used for constructing "path" column of the view, + * similar to its local backend counterpart. + */ + ctl.keysize = sizeof(MemoryContext); + ctl.entrysize = sizeof(MemoryContextId); + ctl.hcxt = CurrentMemoryContext; + + context_id_lookup = hash_create("pg_get_process_memory_contexts", + 256, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + /* List of contexts to process in the next round - start at the top. */ + contexts = list_make1(TopMemoryContext); + + /* + * The client process should have created the required DSA and DSHash + * table. Here we just attach to those. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); + + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); + + snprintf(key, CLIENT_KEY_SIZE, "%d", clientProcNumber); + + /* + * The entry lock is held by dshash_find_or_insert to protect writes to + * process specific memory. Two different processes publishing statistics + * do not block each other. + */ + INJECTION_POINT("memcontext-server-injection", NULL); + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + /* + * Check if the entry has been deleted due to calling process exiting, or + * if the caller has timed out waiting for us and have issued a request to + * another backend. + * + * Make sure that the client always deletes the entry after taking + * required lock or this function may end up writing to unallocated + * memory. + */ + if (!found || entry->target_server_id != MyProcPid) + { + entry->stats_written = false; + + dshash_release_lock(MemoryStatsDsHash, entry); + + hash_destroy(context_id_lookup); + MemoryContextSwitchTo(oldcontext); + MemoryContextReset(memstats_ctx); + + return; + } + + /* Should be allocated by a client backend that is requesting statistics */ + Assert(entry->memstats_dsa_pointer != InvalidDsaPointer); + meminfo = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + if (entry->summary) + { + int cxt_id = 0; + List *path = NIL; + MemoryContextId *contextid_entry; + + /* Copy TopMemoryContext statistics to DSA */ + memset(&stat, 0, sizeof(stat)); + (*TopMemoryContext->methods->stats) (TopMemoryContext, NULL, NULL, + &stat, true); + path = lcons_int(1, path); + PublishMemoryContext(meminfo, cxt_id, TopMemoryContext, path, stat, + 1); + + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &TopMemoryContext, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = cxt_id + 1; + + /* + * Copy statistics for each of TopMemoryContexts children. This + * includes statistics of at most 100 children per node, with each + * child node limited to a depth of 100 in its subtree. + */ + for (MemoryContext c = TopMemoryContext->firstchild; c != NULL; + c = c->nextchild) + { + MemoryContextCounters grand_totals; + int num_contexts = 0; + + path = NIL; + memset(&grand_totals, 0, sizeof(grand_totals)); + + cxt_id++; + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &c, HASH_ENTER, &found); + Assert(!found); + contextid_entry->context_id = cxt_id + 1; + + MemoryContextStatsCounter(c, &grand_totals, &num_contexts); + + path = compute_context_path(c, context_id_lookup); + + PublishMemoryContext(meminfo, cxt_id, c, path, + grand_totals, num_contexts); + } + entry->total_stats = cxt_id + 1; + } + else + { + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + MemoryContextId *contextid_entry; + + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &cur, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = context_id + 1; + + /* + * Figure out the transient context_id of this context and each of + * its ancestors, to compute a path for this context. + */ + path = compute_context_path(cur, context_id_lookup); + + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + /* Account for saving one statistics slot for cumulative reporting */ + if (context_id < (MAX_MEMORY_CONTEXT_STATS_NUM - 1)) + { + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1); + } + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].totalspace += stat.totalspace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].nblocks += stat.nblocks; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freespace += stat.freespace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit per process is reached, write aggregate of the + * remaining statistics. + * + * We can store contexts from 0 to max_stats - 1. When context_id + * is greater than max_stats, we stop reporting individual + * statistics when context_id equals max_stats - 2. As we use + * max_stats - 1 array slot for reporting cumulative statistics or + * "Remaining Totals". + */ + if (context_id == (MAX_MEMORY_CONTEXT_STATS_NUM - 2)) + { + int namelen = strlen("Remaining Totals"); + + num_individual_stats = context_id + 1; + strlcpy(meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].name, + "Remaining Totals", namelen + 1); + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].ident[0] = '\0'; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].path[0] = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].type = 0; + } + context_id++; + + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + contexts = lappend(contexts, c); + } + + /* + * Check if there are aggregated statistics or not in the result set. + * Statistics are individually reported when context_id <= max_stats, + * only if context_id > max_stats will there be aggregates. + */ + if (context_id <= MAX_MEMORY_CONTEXT_STATS_NUM) + { + entry->total_stats = context_id; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = 1; + } + + /* + * The number of contexts exceeded the space available, so report the + * number of aggregated memory contexts + */ + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = + context_id - num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for + * cumulative statistics. + */ + entry->total_stats = num_individual_stats + 1; + } + } + + entry->stats_written = true; + dshash_release_lock(MemoryStatsDsHash, entry); + hash_destroy(context_id_lookup); + + MemoryContextSwitchTo(oldcontext); + MemoryContextReset(memstats_ctx); + /* Notify waiting client backend and return */ + ConditionVariableSignal(&entry->memcxt_cv); +} + +/* + * compute_context_path + * + * Append the transient context_id of this context and each of its ancestors + * to a list, in order to compute a path. + */ +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + MemoryContext cur_context; + + for (cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + elog(ERROR, "hash table corrupted, can't construct path value"); + + path = lcons_int(cur_entry->context_id, path); + } + + return path; +} + +/* + * PublishMemoryContext + * + * Copy the memory context statistics of a single context to a DSA memory + */ +static void +PublishMemoryContext(MemoryStatsEntry *memcxt_info, int curr_id, + MemoryContext context, List *path, + MemoryContextCounters stat, int num_contexts) +{ + char *ident = unconstify(char *, context->ident); + char *name = unconstify(char *, context->name); + + /* + * To be consistent with logging output, we label dynahash contexts with + * just the hash table name as with MemoryContextStatsPrint(). + */ + if (context->ident && strncmp(context->name, "dynahash", 8) == 0) + { + name = unconstify(char *, context->ident); + ident = NULL; + } + + if (name != NULL) + { + int namelen = strlen(name); + + if (namelen >= MEMORY_CONTEXT_NAME_SHMEM_SIZE) + namelen = pg_mbcliplen(name, namelen, + MEMORY_CONTEXT_NAME_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].name, name, namelen + 1); + } + else + /* Clearing the array */ + memcxt_info[curr_id].name[0] = '\0'; + + /* Trim and copy the identifier if it is not set to NULL */ + if (ident != NULL) + { + int idlen = strlen(context->ident); + + /* + * Some identifiers such as SQL query string can be very long, + * truncate oversize identifiers. + */ + if (idlen >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + idlen = pg_mbcliplen(ident, idlen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].ident, ident, idlen + 1); + } + else + memcxt_info[curr_id].ident[0] = '\0'; + + /* Store the path */ + if (path == NIL) + memcxt_info[curr_id].path[0] = 0; + else + { + int levels = Min(list_length(path), MAX_PATH_DISPLAY_LENGTH); + + memcxt_info[curr_id].path_length = levels; + memcxt_info[curr_id].levels = list_length(path); + + foreach_int(i, path) + { + memcxt_info[curr_id].path[foreach_current_index(i)] = i; + if (--levels == 0) + break; + } + } + memcxt_info[curr_id].type = context->type; + memcxt_info[curr_id].totalspace = stat.totalspace; + memcxt_info[curr_id].nblocks = stat.nblocks; + memcxt_info[curr_id].freespace = stat.freespace; + memcxt_info[curr_id].freechunks = stat.freechunks; + memcxt_info[curr_id].num_agg_stats = num_contexts; +} + +void +AtProcExit_memstats_cleanup(int code, Datum arg) +{ + int idx = MyProcNumber; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + bool found; + + if (MemoryStatsDsHash != NULL) + { + snprintf(key, CLIENT_KEY_SIZE, "%d", idx); + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + if (found) + { + if (MemoryStatsDsaArea != NULL && + DsaPointerIsValid(entry->memstats_dsa_pointer)) + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + } + dshash_delete_entry(MemoryStatsDsHash, entry); + } + LWLockAcquire(client_keys_lock, LW_EXCLUSIVE); + client_keys[idx] = -1; + LWLockRelease(client_keys_lock); +} diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index 36ad708b360..e246fc7ce1f 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -39,6 +39,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t PublishMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index 3f401faf3de..7e6de81e7d6 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -661,6 +661,13 @@ BaseInit(void) * drop ephemeral slots, which in turn triggers stats reporting. */ ReplicationSlotInitialize(); + + /* + * The before shmem exit callback frees the DSA memory occupied by the + * latest memory context statistics that could be published by this proc + * if requested. + */ + before_shmem_exit(AtProcExit_memstats_cleanup, 0); } diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 073bdb35d2a..fd75345ff5f 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -1011,6 +1011,37 @@ MemoryContextStatsInternal(MemoryContext context, int level, } } + +/* + * MemoryContextStatsCounter + * + * Accumulate statistics counts into *totals. totals should not be NULL. + * This involves a non-recursive tree traversal. + */ +void +MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts) +{ + int ichild = 1; + + *num_contexts = 0; + context->methods->stats(context, NULL, NULL, totals, false); + + for (MemoryContext curr = context->firstchild; + curr != NULL; + curr = MemoryContextTraverseNext(curr, context)) + { + curr->methods->stats(curr, NULL, NULL, totals, false); + ichild++; + } + + /* + * Add the count of all the children contexts which are traversed + * including the parent. + */ + *num_contexts = *num_contexts + ichild; +} + /* * MemoryContextStatsPrint * Print callback used by MemoryContextStatsInternal diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 894b6a1b6d6..c213211a6b5 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8633,6 +8633,16 @@ prorettype => 'bool', proargtypes => 'int4', prosrc => 'pg_log_backend_memory_contexts' }, +# publishing memory contexts of the specified postgres process +{ oid => '2173', descr => 'publish memory contexts of the specified backend', + proname => 'pg_get_process_memory_contexts', provolatile => 'v', + prorows => '100', proretset => 't', proparallel => 'r', + prorettype => 'record', proargtypes => 'int4 bool', + proallargtypes => '{int4,bool,text,text,text,int4,_int4,int8,int8,int8,int8,int8,int4}', + proargmodes => '{i,i,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{pid, summary, name, ident, type, level, path, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes, num_agg_contexts}', + prosrc => 'pg_get_process_memory_contexts' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index db559b39c4d..7e301affccc 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -96,6 +96,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t PublishMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h index e94ebce95b9..74b611b9e08 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -137,3 +137,4 @@ PG_LWLOCKTRANCHE(SUBTRANS_SLRU, SubtransSLRU) PG_LWLOCKTRANCHE(XACT_SLRU, XactSLRU) PG_LWLOCKTRANCHE(PARALLEL_VACUUM_DSA, ParallelVacuumDSA) PG_LWLOCKTRANCHE(AIO_URING_COMPLETION, AioUringCompletion) +PG_LWLOCKTRANCHE(MEMORY_CONTEXT_KEYS, MemoryContextReportingKeys) diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index e52b8eb7697..4f1bcbf709f 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -35,6 +35,7 @@ typedef enum PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to send the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ /* Recovery conflict reasons */ diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 2bc13c3a054..435a2a27c94 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -19,7 +19,6 @@ #include "nodes/memnodes.h" - /* * MaxAllocSize, MaxAllocHugeSize * Quasi-arbitrary limits on size of allocations. @@ -319,4 +318,11 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) return true; } +extern void ProcessGetMemoryContextInterrupt(void); +extern void HandleGetMemoryContextInterrupt(void); +extern void MemoryContextKeysShmemInit(void); +extern Size MemoryContextKeysShmemSize(void); +extern void MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts); +extern void AtProcExit_memstats_cleanup(int code, Datum arg); #endif /* MEMUTILS_H */ diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 3dd63fd88ed..c84e5adce16 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -233,3 +233,22 @@ select * from pg_timezone_abbrevs where abbrev = 'LMT'; LMT | @ 7 hours 52 mins 58 secs ago | f (1 row) +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; +NOTICE: (AllocSet,TopMemoryContext,) +NOTICE: (AllocSet,TopMemoryContext,) diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 004f9a70e00..d34d2b6211c 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -101,3 +101,21 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs; -- One specific case we can check without much fear of breakage -- is the historical local-mean-time value used for America/Los_Angeles. select * from pg_timezone_abbrevs where abbrev = 'LMT'; + +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 3f3a888fd0e..d057727faa0 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1706,6 +1706,8 @@ MemoryContextData MemoryContextId MemoryContextMethodID MemoryContextMethods +MemoryStatsEntry +MemoryStatsDSHashEntry MemoryStatsPrintFunc MergeAction MergeActionState -- 2.34.1 ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2026-01-19 18:02 Robert Haas <[email protected]> parent: Rahila Syed <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Robert Haas @ 2026-01-19 18:02 UTC (permalink / raw) To: Rahila Syed <[email protected]>; +Cc: Daniel Gustafsson <[email protected]>; torikoshia <[email protected]>; pgsql-hackers On Mon, Jan 19, 2026 at 12:31 PM Rahila Syed <[email protected]> wrote: > Starting a transaction from CFI works for a client backend but > not for auxiliary processes. When I try to execute StartTransaction() > from a checkpointer process, the following assertion fails, > Assert(MyProc->vxid.procNumber == vxid.procNumber); > This is because MyProc->vxid.procNumber is not set for auxiliary > processes. Unfortunately, somebody is going to need to think through - and perhaps test - what happens in each individual type of background process -- not just auxiliary processes but also background workers, including but not limited to parallel workers. Some of them have error recovery logic that is similar to transaction cleanup but only covers a subset of things, in which case the question will be whether that logic handles all the kinds of resources that this code might acquire. Some of them may just straight up kill the process if an error occurs, which is fine for this patch as long as it only happens in extreme situations (e.g. OOM). In other words, we don't necessarily need a transaction specifically, but we need whatever form of error recovery is in use in any given process to be appropriate to the code that this patch proposes to execute. For a regular backend, that's a transaction. Also worth noting: I don't think that all backend types actually use CHECK_FOR_INTERRUPTS(). For those that don't, other updates may be needed to make this feature work. -- Robert Haas EDB: http://www.enterprisedb.com ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2026-02-02 12:28 Rahila Syed <[email protected]> parent: Robert Haas <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Rahila Syed @ 2026-02-02 12:28 UTC (permalink / raw) To: Robert Haas <[email protected]>; +Cc: Daniel Gustafsson <[email protected]>; torikoshia <[email protected]>; pgsql-hackers Hi Robert, Daniel Please find attached the updated patches which incorporate your feedback. Following are responses to Robert's comments: Unfortunately, somebody is going to need to think through - and > perhaps test - what happens in each individual type of background > process -- not just auxiliary processes but also background workers, > including but not limited to parallel workers. Some of them have error > recovery logic that is similar to transaction cleanup but only covers > a subset of things, in which case the question will be whether that > logic handles all the kinds of resources that this code might acquire. > Some of them may just straight up kill the process if an error occurs, > which is fine for this patch as long as it only happens in extreme > situations (e.g. OOM). > I tested the resource cleanup in case of error, with respect to each of the processes types i.e Auxiliary process/background workers/client backends and below are my findings. The only resources acquired within the ProcessGetMemoryContextInterrupt function are of the kind dsm_resowner_desc through GetNamedDSA and GetNamedDsHash. However, these functions make the resource owner forget these resources when they call dsm_pin_mapping. This is done to prevent them from getting detached at the time of error handling or transaction abort/commit. Because when a dsm resource is released, the associated dsm segment is detached. Since we want the DSM segments to remain mapped until a process exits, they are not added to resource owners. This is done intentionally to prevent repeatedly attaching or detaching the DSM segments, if the same process is queried for statistics multiple times. As no resources are tracked during the execution ProcessGetMemoryContextInterrupt irrespective of its execution within or outside a transaction, the error handling in both the cases does not process resource cleanup unless the process exits. During proc_exit, the cleanup/detach runs via dsm_backend_shutdown(). > + LWLockAcquire(client_keys_lock, LW_SHARED); > > LWLocks normally use NamesLikeThis not names_like_this and are > generally created by just listing them in lwlocklist.h. > Fixed. Also, it's not really safe to acquire an LWLock if there's no > transaction active. If we error afterward, what will release the > LWLock? I verified that when an error occurs outside of a transaction in various types of processes, LWLockReleaseAll() is called during error handling. 1. For auxiliary processes - called within sigsetjmp() from main functions. 2. For client backend - called during process exit as error is converted to "FATAL: terminating connection because protocol synchronization was lost" 3. For a background worker like parallel worker - called during process exit, as the worker is killed on encountering an error. + else > + { > + clientProcNumber = client_keys[MyProcNumber]; > + client_keys[MyProcNumber] = -1; > + LWLockRelease(client_keys_lock); > + } > > The "else" is not really necessary here because the "if" portion ends > with "return Fixed accordingly. > One other point to note here is that if, by some chance, a failure occurs as a result of receiving a memory-context interrupt, it's going to fail the currently-running transaction In the updated version, we make sure that no errors are being thrown by ProcessMemoryContextInterrupt and other functions added by this patch. I have also added comments above the GetNamedDSA and GetNamedDsHash functions to highlight the importance of minimizing errors in these functions, as they are called from CFI. This way we try to prevent any non-critical errors from being thrown. Following are responses to Daniel's comments: > > Attached is the updated patch that addresses this. > > A few small comments on v47: > > +static const char *ContextTypeToString(NodeTag type); > I think context_type_to_string() would be a better name on this internal > function to model it closer to the existing int_list_to_array(). > Personally I > would also place it before its first use to avoid the prototype, but that's > personal preference. > > Changed accordingly, also moved compute_context_path() so it appears before its use to avoid the prototype. > > +static void > +memstats_dsa_cleanup(char *key) > This function warrants a documentation comment describing when it should be > used safely. > Added a comment. > > > + memstats_dsa_cleanup(key); > + memstats_client_key_reset(procNumber); > + ConditionVariableCancelSleep(); > + PG_RETURN_NULL(); > I think we should notify the user in these two timeout cases, why not > adding an > ereport(NOTICE, "request for memory context statistics timed out")); or > something with a better wording than that. > > Added this. > + Size sz = 0; > + Size TotalProcs = 0; > + > + TotalProcs = add_size(TotalProcs, NUM_AUXILIARY_PROCS); > + TotalProcs = add_size(TotalProcs, MaxBackends); > + sz = add_size(sz, mul_size(TotalProcs, sizeof(int))); > + > + return sz > As we discussed off-list, the call to add_size() call can be omitted as it > won't affect the calculation. > Fixed accordingly. > > > +# Copyright (c) 2025, PostgreSQL Global Development Group > Here, and possibly elsewhere, it should say 2026 instead I think. > > > Fixed. Thank you, Rahila Syed Attachments: [application/octet-stream] v49-0001-Add-function-to-report-memory-context-statistics.patch (59.9K, ../../CAH2L28umvzm9YwQBa_zrJM0ABC57ZAKPBxi8bD+n=1ADJ3SOZw@mail.gmail.com/3-v49-0001-Add-function-to-report-memory-context-statistics.patch) download | inline diff: From edf9562511c84984ac623733f18eda08ce6b0a68 Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Thu, 27 Nov 2025 14:39:43 +0530 Subject: [PATCH 1/2] Add function to report memory context statistics This adds a function for retrieving memory context statistics and information from backends as well as auxiliary processes. The intended usecase is cluster debugging when under memory pressure or unanticipated memory usage characteristics. When calling the function it sends a signal to the specified process to submit statistics regarding its memory contexts into dynamic shared memory. Each memory context is returned in detail, followed by a cumulative total in case the number of contexts exceed the max allocated amount of shared memory. Each process is limited to use at most 1Mb memory for this. A summary can also be explicitly requested by the user, this will return the TopMemoryContext and a cumulative total of all lower contexts. In order to not block on busy processes, we have hardcoded the number of seconds during which to retry before timing out. In the case where no statistics are published within the set timeout, NULL is returned --- doc/src/sgml/func/func-admin.sgml | 159 +++ src/backend/catalog/system_functions.sql | 14 + src/backend/postmaster/autovacuum.c | 4 + src/backend/postmaster/checkpointer.c | 4 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 4 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 4 + src/backend/storage/ipc/dsm_registry.c | 8 +- src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/storage/lmgr/proc.c | 1 + src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 2 + src/backend/utils/adt/mcxtfuncs.c | 1020 ++++++++++++++++- src/backend/utils/init/globals.c | 1 + src/backend/utils/init/postinit.c | 7 + src/backend/utils/mmgr/mcxt.c | 31 + src/include/catalog/pg_proc.dat | 10 + src/include/miscadmin.h | 1 + src/include/storage/lwlocklist.h | 1 + src/include/storage/procsignal.h | 1 + src/include/utils/memutils.h | 8 +- src/test/regress/expected/sysviews.out | 19 + src/test/regress/sql/sysviews.sql | 18 + src/tools/pgindent/typedefs.list | 2 + 26 files changed, 1313 insertions(+), 23 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 3ac81905d1f..af87b5b19ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -251,6 +251,132 @@ <literal>false</literal> is returned. </para></entry> </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_get_process_memory_contexts</primary> + </indexterm> + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type> <optional>,<parameter>summary</parameter> <type>boolean</type> <literal>DEFAULT</literal> <literal>false</literal></optional> ) + <returnvalue>setof record</returnvalue> + ( <parameter>name</parameter> <type>text</type>, + <parameter>ident</parameter> <type>text</type>, + <parameter>type</parameter> <type>text</type>, + <parameter>level</parameter> <type>integer</type>, + <parameter>path</parameter> <type>integer[]</type>, + <parameter>total_bytes</parameter> <type>bigint</type>, + <parameter>total_nblocks</parameter> <type>bigint</type>, + <parameter>free_bytes</parameter> <type>bigint</type>, + <parameter>free_chunks</parameter> <type>bigint</type>, + <parameter>used_bytes</parameter> <type>bigint</type>, + <parameter>num_agg_contexts</parameter> <type>integer</type> ) + </para> + <para> + This function handles requests to display the memory contexts of a + <productname>PostgreSQL</productname> process with the specified + process ID. The function can be used to send requests to backends as + well as <glossterm linkend="glossary-auxiliary-proc">auxiliary processes</glossterm>. + If the process does not respond with memory contexts statistics in 5 seconds, + function returns NULL. + </para> + <para> + The returned record contains extended statistics per each memory + context: + <itemizedlist spacing="compact"> + <listitem> + <para> + <parameter>name</parameter> - The name of the memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>ident</parameter> - Memory context ID (if any). + </para> + </listitem> + <listitem> + <para> + <parameter>type</parameter> - The type of memory context, possible + values are: AllocSet, Generation, Slab and Bump. + </para> + </listitem> + <listitem> + <para> + <parameter>level</parameter> - The level in the tree of the current + memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>path</parameter> - Memory contexts are organized in a + tree model with TopMemoryContext as the root, and all other memory + contexts as nodes in the tree. The <parameter>path</parameter> + displays the path from the root to the current memory context. The + path is limited to 100 children per node, which each node limited + to a max depth of 100, to preserve memory during reporting. The + printed path will also be limited to 100 nodes counting from the + TopMemoryContext. + </para> + </listitem> + <listitem> + <para> + <parameter>total_bytes</parameter> - The total number of bytes + allocated to this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_nblocks</parameter> - The total number of blocks + used for the allocated memory. + </para> + </listitem> + <listitem> + <para> + <parameter>free_bytes</parameter> - The amount of free memory in + this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>free_chunks</parameter> - The number of chunks that + <parameter>free_bytes</parameter> corresponds to. + </para> + </listitem> + <listitem> + <para> + <parameter>used_bytes</parameter> - The total number of bytes + currently occupied. + </para> + </listitem> + <listitem> + <para> + <parameter>num_agg_contexts</parameter> - The number of memory + contexts aggregated in the displayed statistics. + </para> + </listitem> + </itemizedlist> + </para> + <para> + When <parameter>summary</parameter> is <literal>true</literal>, statistics + for memory contexts at levels 1 and 2 are displayed, with level 1 + representing the root node (i.e., <literal>TopMemoryContext</literal>). + Statistics for contexts on level 2 and below are aggregates of all + child contexts' statistics, where <literal>num_agg_contexts</literal> + indicate the number aggregated child contexts. When + <parameter>summary</parameter> is <literal>false</literal> (the default), + <literal>the num_agg_contexts</literal> value is <literal>1</literal>, + indicating that individual statistics are being displayed. + </para> + <para> + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context + statistics are aggregated and a cumulative total is displayed. The + <literal>num_agg_contexts</literal> column indicates the number of + contexts aggregated in the displayed statistics. When + <literal>num_agg_contexts</literal> is <literal>1</literal> it means + that the context statistics are displayed separately. + </para></entry> + </row> </tbody> </tgroup> </table> @@ -302,6 +428,39 @@ LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 because it may generate a large number of log messages. </para> + <para> + <function>pg_get_process_memory_contexts</function> can be used to request + memory contexts statistics of any <productname>PostgreSQL</productname> + process. For example: +<programlisting> +postgres=# SELECT * FROM pg_get_process_memory_contexts( + (SELECT pid FROM pg_stat_activity + WHERE backend_type = 'checkpointer'), + false) LIMIT 1; +-[ RECORD 1 ]----+------------------------------ +name | TopMemoryContext +ident | +type | AllocSet +level | 1 +path | {1} +total_bytes | 90304 +total_nblocks | 3 +free_bytes | 2880 +free_chunks | 1 +used_bytes | 87424 +num_agg_contexts | 1 +</programlisting> + <note> + <para> + While <function>pg_get_process_memory_contexts</function> can be used to + query memory contexts of the local backend, + <structname>pg_backend_memory_contexts</structname> + (see <xref linkend="view-pg-backend-memory-contexts"/> for more details) + will be less resource intensive when only the local backend is of interest. + </para> + </note> + </para> + </sect2> <sect2 id="functions-admin-backup"> diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql index eb9e31ae1bf..d1416e5534d 100644 --- a/src/backend/catalog/system_functions.sql +++ b/src/backend/catalog/system_functions.sql @@ -657,6 +657,17 @@ LANGUAGE INTERNAL STRICT VOLATILE PARALLEL UNSAFE AS 'pg_replication_origin_session_setup'; +CREATE OR REPLACE FUNCTION + pg_get_process_memory_contexts(IN pid integer, IN summary boolean DEFAULT false, + OUT name text, OUT ident text, OUT type text, OUT level integer, + OUT path integer[], OUT total_bytes bigint, OUT total_nblocks bigint, + OUT free_bytes bigint, OUT free_chunks bigint, OUT used_bytes bigint, + OUT num_agg_contexts integer) +RETURNS SETOF RECORD +LANGUAGE INTERNAL +STRICT VOLATILE PARALLEL UNSAFE +AS 'pg_get_process_memory_contexts'; + -- -- The default permissions for functions mean that anyone can execute them. -- A number of functions shouldn't be executable by just anyone, but rather @@ -782,6 +793,7 @@ REVOKE EXECUTE ON FUNCTION pg_ls_logicalmapdir() FROM PUBLIC; REVOKE EXECUTE ON FUNCTION pg_ls_replslotdir(text) FROM PUBLIC; +REVOKE EXECUTE ON FUNCTION pg_get_process_memory_contexts(integer, boolean) FROM PUBLIC; -- -- We also set up some things as accessible to standard roles. -- @@ -808,6 +820,8 @@ GRANT EXECUTE ON FUNCTION pg_current_logfile() TO pg_monitor; GRANT EXECUTE ON FUNCTION pg_current_logfile(text) TO pg_monitor; +GRANT EXECUTE ON FUNCTION pg_get_process_memory_contexts(integer, boolean) TO pg_read_all_stats; + GRANT pg_read_all_settings TO pg_monitor; GRANT pg_read_all_stats TO pg_monitor; diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 22379de1e31..a00bac51264 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -788,6 +788,10 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index 6482c21b8f9..1b7d5e7ffdc 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -685,6 +685,10 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index a2c0ff012c5..cb18ce80893 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -48,6 +48,10 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index 1a20387c4bd..3ac0fba225a 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -871,6 +871,10 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index a1a4f65f9a9..18a9e7f85e1 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -192,6 +192,10 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index c3d56c866d3..49dc3e88023 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -879,6 +879,10 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c index 068c1577b12..4810babac1b 100644 --- a/src/backend/storage/ipc/dsm_registry.c +++ b/src/backend/storage/ipc/dsm_registry.c @@ -270,7 +270,9 @@ GetNamedDSMSegment(const char *name, size_t size, * This routine returns a pointer to the DSA. A new LWLock tranche ID will be * generated if needed. Note that the lock tranche will be registered with the * provided name. Also note that this should be called at most once for a - * given DSA in each backend. + * given DSA in each backend. It's best to minimize the number of errors thrown + * in this function since it's called from a CFI function. An error here could + * lead to error in any transaction that calls the CFI function. */ dsa_area * GetNamedDSA(const char *name, bool *found) @@ -351,7 +353,9 @@ GetNamedDSA(const char *name, bool *found) * params is ignored; a new LWLock tranche ID will be generated if needed. * Note that the lock tranche will be registered with the provided name. Also * note that this should be called at most once for a given table in each - * backend. + * backend. It's best to minimize the number of errors thrown in this function + * since it's called from a CFI function. An error here could lead to error in + * any transaction that calls the CFI function. */ dshash_table * GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found) diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index 1f7e933d500..63159712003 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -52,6 +52,7 @@ #include "storage/sinvaladt.h" #include "utils/guc.h" #include "utils/injection_point.h" +#include "utils/memutils.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -141,6 +142,7 @@ CalculateShmemSize(void) size = add_size(size, AioShmemSize()); size = add_size(size, WaitLSNShmemSize()); size = add_size(size, LogicalDecodingCtlShmemSize()); + size = add_size(size, MemoryContextKeysShmemSize() + sizeof(LWLockPadded)); /* include additional requested shmem from preload libraries */ size = add_size(size, total_addin_request); @@ -328,6 +330,7 @@ CreateOrAttachShmemStructs(void) AioShmemInit(); WaitLSNShmemInit(); LogicalDecodingCtlShmemInit(); + MemoryContextKeysShmemInit(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index 8e56922dcea..601e01d2574 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -694,6 +694,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_GET_MEMORY_CONTEXT)) + HandleGetMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 696bbb7b911..669ea2bfe58 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -51,6 +51,7 @@ #include "storage/procsignal.h" #include "storage/spin.h" #include "storage/standby.h" +#include "utils/memutils.h" #include "utils/timeout.h" #include "utils/timestamp.h" diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index b4a8d2f3a1c..d36bd2bd664 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3539,6 +3539,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index efde48e76b7..f3e541dcbb5 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -163,6 +163,7 @@ WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." +MEM_CXT_PUBLISH "Waiting for a process to publish memory information." ABI_compatibility: @@ -364,6 +365,7 @@ SerialControl "Waiting to read or update shared <filename>pg_serial</filename> s AioWorkerSubmissionQueue "Waiting to access AIO worker submission queue." WaitLSN "Waiting to read or update shared Wait-for-LSN state." LogicalDecodingControl "Waiting to read or update logical decoding status information." +MemContextReportingClientKeys "Waiting for another process to complete reading or writing the memory reporting keys." # # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE) diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index 12b8d4cefaf..832b17fdf57 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -15,24 +15,128 @@ #include "postgres.h" +#include "access/xact.h" #include "funcapi.h" #include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "storage/dsm_registry.h" #include "storage/proc.h" #include "storage/procarray.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/injection_point.h" +#include "utils/memutils.h" +#include "utils/wait_event_types.h" + +/* + * Memory Context reporting size limits. + */ + +/* Max length of context name and ident, to keep it consistent + * with ProcessLogMemoryContext() + */ +#define MEMORY_CONTEXT_IDENT_SHMEM_SIZE 100 +#define MEMORY_CONTEXT_NAME_SHMEM_SIZE 100 + +/* Maximum size (in bytes) of DSA area per process */ +#define MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND ((size_t) (1 * 1024 * 1024)) + +/* + * Maximum number of memory context statistics is calculated by dividing + * max memory allocated per backend with maximum size per context statistics. + * The identifier and name are statically allocated arrays of size 100 bytes. + * The path depth is limited to 100 like for memory context logging. + */ +#define MAX_MEMORY_CONTEXT_STATS_NUM MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND / (sizeof(MemoryStatsEntry)) + +/* + * Size of dshash key. The key is a uint32 rendered as a string, 10 chars + * plus space for a NULL terminator can hold all the values. + */ +#define CLIENT_KEY_SIZE (10 + 1) + +/* Dynamic shared memory state for reporting statistics per context */ +typedef struct MemoryStatsEntry +{ + char name[MEMORY_CONTEXT_NAME_SHMEM_SIZE]; + char ident[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + int path[100]; + NodeTag type; + int path_length; + int levels; + int64 totalspace; + int64 nblocks; + int64 freespace; + int64 freechunks; + int num_agg_stats; +} MemoryStatsEntry; + +/* + * Per backend dynamic shared hash entry for memory context statistics + * reporting. + */ +typedef struct MemoryStatsDSHashEntry +{ + char key[64]; + ConditionVariable memcxt_cv; + bool stats_written; + int target_server_id; + int total_stats; + bool summary; + dsa_pointer memstats_dsa_pointer; +} MemoryStatsDSHashEntry; + +static const dshash_parameters memctx_dsh_params = { + offsetof(MemoryStatsDSHashEntry, memcxt_cv), + sizeof(MemoryStatsDSHashEntry), + dshash_strcmp, + dshash_strhash, + dshash_strcpy +}; + +/* + * These are used for reporting memory context statistics of a process. + */ + +/* Array to store the keys of MemoryStatsDsHash */ +static int *client_keys = NULL; + +/* + * Table to store pointers to DSA memory containing memory statistics and other + * metadata. There is one entry per client backend request, keyed by ProcNumber + * of the client obtained from client_keys array above. + */ +static dshash_table *MemoryStatsDsHash = NULL; + +/* + * Dsa area which stores the actual memory context + * statistics. + */ +static dsa_area *MemoryStatsDsaArea = NULL; + +static void memstats_dsa_cleanup(char *key); +static void memstats_client_key_reset(int ProcNumber); +static void PublishMemoryContext(MemoryStatsEntry *memcxt_info, + int curr_id, MemoryContext context, + List *path, + MemoryContextCounters stat, + int num_contexts); /* ---------- * The max bytes for showing identifiers of MemoryContext. + * This is used by pg_get_backend_memory_context - view used for local backend. * ---------- */ #define MEMORY_CONTEXT_IDENT_DISPLAY_SIZE 1024 +#define MAX_PATH_DISPLAY_LENGTH 100 +/* Timeout in seconds */ +#define MEMORY_STATS_MAX_TIMEOUT 5 + /* * MemoryContextId - * Used for storage of transient identifiers for - * pg_get_backend_memory_contexts. + * Used for storage of transient identifiers for memory context reporting */ typedef struct MemoryContextId { @@ -62,6 +166,67 @@ int_list_to_array(const List *list) return PointerGetDatum(result_array); } +/* + * context_type_to_string + * Returns a textual representation of a context type + * + * This should cover the same types as MemoryContextIsValid. + */ +static const char * +context_type_to_string(NodeTag type) +{ + const char *context_type; + + switch (type) + { + case T_AllocSetContext: + context_type = "AllocSet"; + break; + case T_GenerationContext: + context_type = "Generation"; + break; + case T_SlabContext: + context_type = "Slab"; + break; + case T_BumpContext: + context_type = "Bump"; + break; + default: + context_type = "???"; + break; + } + return context_type; +} + +/* + * compute_context_path + * + * Append the transient context_id of this context and each of its ancestors + * to a list, in order to compute a path. + */ +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + MemoryContext cur_context; + + for (cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + { + elog(NOTICE, "hash table corrupted, can't construct path value"); + return NIL; + } + path = lcons_int(cur_entry->context_id, path); + } + return path; +} + /* * PutMemoryContextsStatsTupleStore * Add details for the given MemoryContext to 'tupstore'. @@ -143,24 +308,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, else nulls[1] = true; - switch (context->type) - { - case T_AllocSetContext: - type = "AllocSet"; - break; - case T_GenerationContext: - type = "Generation"; - break; - case T_SlabContext: - type = "Slab"; - break; - case T_BumpContext: - type = "Bump"; - break; - default: - type = "???"; - break; - } + type = context_type_to_string(context->type); values[2] = CStringGetTextDatum(type); values[3] = Int32GetDatum(list_length(path)); /* level */ @@ -305,3 +453,835 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } + +/* + * pg_get_process_memory_contexts + * Signal a backend or an auxiliary process to send its memory contexts, + * wait for the results and display them. + * + * By default, only superusers or users with ROLE_PG_READ_ALL_STATS are allowed + * to signal a process to return the memory contexts. Additional roles can be + * permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to copy the memory context details + * to a dynamic shared memory space. + * + * We have defined a limit on DSA memory that could be allocated per process - + * if the process has more memory contexts than what can fit in the allocated + * size, the excess contexts are summarized and represented as cumulative total + * at the end of the buffer. + * + * After sending the signal, wait on a condition variable. The publishing + * backend, after copying the data to shared memory, sends a signal on that + * condition variable. There is one condition variable per client process. + * Once the condition variable is signalled, check if the latest memory context + * information is available and display. + * + * If the publishing backend does not respond before the condition variable + * times out, which is set to a predefined value MEMORY_STATS_MAX_TIMEOUT, give + * up and return NULL. + */ +Datum +pg_get_process_memory_contexts(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + bool summary = PG_GETARG_BOOL(1); + PGPROC *proc; + ProcNumber procNumber; + bool proc_is_aux = false; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + MemoryStatsEntry *memcxt_info; + MemoryStatsDSHashEntry *entry; + bool found; + char key[CLIENT_KEY_SIZE]; + TimestampTz start_timestamp; + + /* + * See if the process with given pid is a backend or an auxiliary process + * and remember the type for when we requery the process later. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + { + proc = AuxiliaryPidGetProc(pid); + proc_is_aux = true; + } + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; this is however not a problem and leave with a WARNING. + * See comment in pg_log_backend_memory_contexts for a discussion on this. + */ + if (proc == NULL) + { + ereport(WARNING, + errmsg("PID %d is not a PostgreSQL server process", pid)); + PG_RETURN_NULL(); + } + + procNumber = GetNumberFromPGProc(proc); + + /* + * Check if the server process slot is not empty and exit early Non-empty + * slot means some other client backend is requesting the statistics from + * the same server process. + */ + LWLockAcquire(MemContextReportingClientKeysLock, LW_EXCLUSIVE); + if (client_keys[procNumber] != -1) + { + LWLockRelease(MemContextReportingClientKeysLock); + ereport(WARNING, + errmsg("server process %d is processing previous request", + pid)); + PG_RETURN_NULL(); + } + LWLockRelease(MemContextReportingClientKeysLock); + + /* + * Create a DSA to allocate memory for copying memory contexts statistics. + * Allocate the memory in the DSA and send DSA pointer to the server + * process for storing the context statistics. If number of contexts + * exceed a predefined limit (1MB), a cumulative total is stored for such + * contexts. + * + * The DSA is created once for the lifetime of the server, and only + * attached in subsequent calls. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); + + /* + * The DSA pointers containing statistics for each client are stored in a + * dshash table. In addition to DSA pointer, each entry in this table also + * contains information about the statistics, condition variable for + * signalling between client and the server and miscellaneous data + * specific to a request. There is one entry per client request in the + * hash table. + */ + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); + + snprintf(key, sizeof(key), "%d", MyProcNumber); + + /* + * Insert an entry for this client in DSHASH table the first time this + * function is called. This entry is deleted when the process exits in + * before_shmem_exit call. + * + * dshash_find_or_insert locks the entry to prevent the publisher from + * reading before client has updated the entry. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + if (!found) + { + entry->stats_written = false; + ConditionVariableInit(&entry->memcxt_cv); + } + + /* + * Allocate 1MB of memory for the backend to publish its statistics on + * every call to this function. The memory is freed at the end of the + * function. + */ + entry->memstats_dsa_pointer = + dsa_allocate0(MemoryStatsDsaArea, MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND); + + /* + * Specify whether a summary of statistics is requested, before signalling + * the server. + */ + entry->summary = summary; + + /* + * Indicate which server process statistics are being requested from. If + * this client times out before the last requested process can publish its + * statistics, it may send a new request to another server process. Since + * the previous server was notified, it might attempt to read the same + * client entry and respond incorrectly with its statistics. By storing + * the server ID in the client entry, we prevent any previously signalled + * server process from writing its statistics in the space meant for the + * newly requested process. + */ + entry->target_server_id = pid; + dshash_release_lock(MemoryStatsDsHash, entry); + + /* + * Check if the publishing process slot is empty and store this clients + * key i.e its procNumber. This informs the publishing process that it is + * supposed to write statistics in the hash entry corresponding to this + * client. + */ + LWLockAcquire(MemContextReportingClientKeysLock, LW_EXCLUSIVE); + if (client_keys[procNumber] == -1) + client_keys[procNumber] = MyProcNumber; + else + { + LWLockRelease(MemContextReportingClientKeysLock); + ereport(WARNING, + errmsg("server process %d is processing previous request", + pid)); + PG_RETURN_NULL(); + } + LWLockRelease(MemContextReportingClientKeysLock); + + /* + * Send a signal to a PostgreSQL process, informing it we want it to + * produce information about its memory contexts. + */ + if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, procNumber) < 0) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ereport(WARNING, + errmsg("could not send signal to process %d: %m", + pid)); + PG_RETURN_NULL(); + } + start_timestamp = GetCurrentTimestamp(); + + while (1) + { + long elapsed_time; + + INJECTION_POINT("memcontext-client-injection", NULL); + + elapsed_time = TimestampDifferenceMilliseconds(start_timestamp, + GetCurrentTimestamp()); + /* Return if we have already exceeded the timeout */ + if (elapsed_time >= MEMORY_STATS_MAX_TIMEOUT * 1000) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + ereport(NOTICE, + errmsg("request for memory context statistics for PID %d timed out", + pid)); + PG_RETURN_NULL(); + } + + /* + * Recheck the state of the backend before sleeping on the condition + * variable to ensure the process is still alive. Only check the + * relevant process type based on the earlier PID check. + */ + if (proc_is_aux) + proc = AuxiliaryPidGetProc(pid); + else + proc = BackendPidGetProc(pid); + + /* + * The target server process ending during memory context processing + * is not an error. + */ + if (proc == NULL) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + ereport(WARNING, + errmsg("PID %d is no longer a PostgreSQL server process", + pid)); + PG_RETURN_NULL(); + } + + /* + * Wait for MEMORY_STATS_MAX_TIMEOUT. If no statistics are available + * within the allowed time then return NULL. The timer is defined in + * milliseconds since that's what the condition variable sleep uses. + */ + if (ConditionVariableTimedSleep(&entry->memcxt_cv, + (MEMORY_STATS_MAX_TIMEOUT * 1000), + WAIT_EVENT_MEM_CXT_PUBLISH)) + { + /* Timeout has expired, return NULL */ + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + ereport(NOTICE, + errmsg("request for memory context statistics for PID %d timed out", + pid)); + PG_RETURN_NULL(); + } + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + Assert(found); + + memcxt_info = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + /* + * We expect to come out of sleep when the requested process has + * finished publishing the statistics, verified using a boolean + * stats_written. + * + * Make sure that the statistics are actually written by checking that + * the name of the context is not NULL. This is done to ensure that + * the subsequent waits for statistics do not return spuriously if the + * previous call to the function ended in error and thus could not + * clear the stats_written flag. + */ + if (entry->stats_written && memcxt_info[0].name[0] != '\0') + break; + + dshash_release_lock(MemoryStatsDsHash, entry); + + } + + InitMaterializedSRF(fcinfo, 0); + + /* + * Backend has finished publishing the stats, project them. + */ +#define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 11 + for (int i = 0; i < entry->total_stats; i++) + { + ArrayType *path_array; + Datum values[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + bool nulls[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + Datum *path_datum = NULL; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + Assert(memcxt_info[i].name[0] != '\0'); + values[0] = CStringGetTextDatum(memcxt_info[i].name); + + if (memcxt_info[i].ident[0] != '\0') + values[1] = CStringGetTextDatum(memcxt_info[i].ident); + else + nulls[1] = true; + + values[2] = CStringGetTextDatum(context_type_to_string(memcxt_info[i].type)); + + if (memcxt_info[i].levels != 0) + values[3] = Int32GetDatum(memcxt_info[i].levels); + else + nulls[3] = true; + + if (memcxt_info[i].path[0] != 0) + { + int path_length; + + path_length = memcxt_info[i].path_length; + path_datum = (Datum *) palloc(path_length * sizeof(Datum)); + + for (int j = 0; j < path_length; j++) + path_datum[j] = Int32GetDatum(memcxt_info[i].path[j]); + path_array = construct_array_builtin(path_datum, + path_length, + INT4OID); + values[4] = PointerGetDatum(path_array); + } + else + nulls[4] = true; + + values[5] = Int64GetDatum(memcxt_info[i].totalspace); + values[6] = Int64GetDatum(memcxt_info[i].nblocks); + values[7] = Int64GetDatum(memcxt_info[i].freespace); + values[8] = Int64GetDatum(memcxt_info[i].freechunks); + values[9] = Int64GetDatum(memcxt_info[i].totalspace - + memcxt_info[i].freespace); + values[10] = Int32GetDatum(memcxt_info[i].num_agg_stats); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + dshash_release_lock(MemoryStatsDsHash, entry); + memstats_dsa_cleanup(key); + + ConditionVariableCancelSleep(); + + PG_RETURN_NULL(); +} + +/* + * This function is executed before returning from the + * pg_get_process_memory_contexts() function. + * It releases the DSA memory allocated for storing the memory statistics + * retrieved by that function. The caller must ensure that the LWLock for + * MemoryStatsDsHash is not already held. +*/ +static void +memstats_dsa_cleanup(char *key) +{ + MemoryStatsDSHashEntry *entry; + + entry = dshash_find(MemoryStatsDsHash, key, true); + + Assert(MemoryStatsDsaArea != NULL); + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + entry->memstats_dsa_pointer = InvalidDsaPointer; + entry->stats_written = false; + entry->target_server_id = 0; + + dshash_release_lock(MemoryStatsDsHash, entry); +} + +/* + * Remove this process from the publishing process' + * client key slot, if the stats publishing process has failed to do so. + */ +static void +memstats_client_key_reset(int procNumber) +{ + LWLockAcquire(MemContextReportingClientKeysLock, LW_EXCLUSIVE); + + if (client_keys[procNumber] == MyProcNumber) + client_keys[procNumber] = -1; + LWLockRelease(MemContextReportingClientKeysLock); +} + +void +MemoryContextKeysShmemInit(void) +{ + bool found; + + client_keys = (int *) + ShmemInitStruct("MemoryContextKeys", + MemoryContextKeysShmemSize(), &found); + + if (!found) + MemSet(client_keys, -1, MemoryContextKeysShmemSize()); +} + +Size +MemoryContextKeysShmemSize(void) +{ + Size TotalProcs = 0; + + TotalProcs = add_size(TotalProcs, NUM_AUXILIARY_PROCS); + TotalProcs = add_size(TotalProcs, MaxBackends); + + return mul_size(TotalProcs, sizeof(int)); +} + +/* + * HandleGetMemoryContextInterrupt + * Handle receipt of an interrupt indicating a request to publish memory + * contexts statistics. + * + * All the actual work is deferred to ProcessGetMemoryContextInterrupt() as + * this cannot be performed in a signal handler. + */ +void +HandleGetMemoryContextInterrupt(void) +{ + InterruptPending = true; + PublishMemoryContextPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + +/* + * ProcessGetMemoryContextInterrupt + * Generate information about memory contexts used by the process. + * + * Performs a breadth first search on the memory context tree, thus parents + * statistics are reported before their children in the monitoring function + * output. + * + * Statistics for all the processes are shared via the same dynamic shared + * area. Individual statistics are tracked independently in per-process DSA + * pointers. These pointers are stored in a dshash table with key as requesting + * clients ProcNumber. + * + * We calculate maximum number of context's statistics that can be displayed + * using a pre-determined limit for memory available per process for this + * utility and maximum size of statistics for each context. The remaining + * context statistics if any are captured as a cumulative total at the end of + * individual context's statistics. + * + * If summary is true, we capture the level 1 and level 2 contexts statistics. + * For that we traverse the memory context tree recursively in depth first + * search manner to cover all the children of a parent context, to be able to + * display a cumulative total of memory consumption by a parent at level 2 and + * all its children. + */ +void +ProcessGetMemoryContextInterrupt(void) +{ + List *contexts; + HASHCTL ctl; + HTAB *context_id_lookup; + int context_id = 0; + MemoryStatsEntry *meminfo; + MemoryContextCounters stat; + int num_individual_stats = 0; + bool found; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + int clientProcNumber; + MemoryContext memstats_ctx = NULL; + MemoryContext oldcontext = NULL; + + PublishMemoryContextPending = false; + + /* + * Avoid performing any shared memory operations in aborted transaction, + * the caller will timeout and return NULL. + */ + if (IsAbortedTransactionBlockState()) + return; + + INJECTION_POINT("memcontext-server-wait", NULL); + + /* + * Retrieve the client key for publishing statistics and reset it to -1, + * so other clients can request memory statistics from this process. + * Return if the client_key is -1, which means the requesting client has + * timed out. + */ + LWLockAcquire(MemContextReportingClientKeysLock, LW_SHARED); + if (client_keys[MyProcNumber] == -1) + { + LWLockRelease(MemContextReportingClientKeysLock); + return; + } + clientProcNumber = client_keys[MyProcNumber]; + client_keys[MyProcNumber] = -1; + LWLockRelease(MemContextReportingClientKeysLock); + + /* + * Create a new memory context which is not a part of TopMemoryContext + * tree. This context is used to allocate all memory in this function. + * This helps in keeping the memory allocation in this function to report + * memory consumption statistics separate. So that it does not affect the + * output of this function. + */ + memstats_ctx = AllocSetContextCreate((MemoryContext) NULL, + "publish_memory_context_statistics", + ALLOCSET_SMALL_SIZES); + oldcontext = MemoryContextSwitchTo(memstats_ctx); + + /* + * The hash table is used for constructing "path" column of the view, + * similar to its local backend counterpart. + */ + ctl.keysize = sizeof(MemoryContext); + ctl.entrysize = sizeof(MemoryContextId); + ctl.hcxt = CurrentMemoryContext; + + context_id_lookup = hash_create("pg_get_process_memory_contexts", + 256, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + /* List of contexts to process in the next round - start at the top. */ + contexts = list_make1(TopMemoryContext); + + /* + * The client process should have created the required DSA and DSHash + * table. Here we just attach to those. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); + + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); + + snprintf(key, CLIENT_KEY_SIZE, "%d", clientProcNumber); + + /* + * The entry lock is held by dshash_find_or_insert to protect writes to + * process specific memory. Two different processes publishing statistics + * do not block each other. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + INJECTION_POINT("memcontext-server-injection", NULL); + + /* + * Check if the entry has been deleted due to calling process exiting, or + * if the caller has timed out waiting for us and have issued a request to + * another backend. + * + * Make sure that the client always deletes the entry after taking + * required lock or this function may end up writing to unallocated + * memory. + */ + if (!found || entry->target_server_id != MyProcPid) + { + entry->stats_written = false; + + dshash_release_lock(MemoryStatsDsHash, entry); + + hash_destroy(context_id_lookup); + MemoryContextSwitchTo(oldcontext); + MemoryContextReset(memstats_ctx); + + return; + } + + /* Should be allocated by a client backend that is requesting statistics */ + Assert(entry->memstats_dsa_pointer != InvalidDsaPointer); + meminfo = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + if (entry->summary) + { + int cxt_id = 0; + List *path = NIL; + MemoryContextId *contextid_entry; + + /* Copy TopMemoryContext statistics to DSA */ + memset(&stat, 0, sizeof(stat)); + (*TopMemoryContext->methods->stats) (TopMemoryContext, NULL, NULL, + &stat, true); + path = lcons_int(1, path); + PublishMemoryContext(meminfo, cxt_id, TopMemoryContext, path, stat, + 1); + + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &TopMemoryContext, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = cxt_id + 1; + + /* + * Copy statistics for each of TopMemoryContexts children. This + * includes statistics of at most 100 children per node, with each + * child node limited to a depth of 100 in its subtree. + */ + for (MemoryContext c = TopMemoryContext->firstchild; c != NULL; + c = c->nextchild) + { + MemoryContextCounters grand_totals; + int num_contexts = 0; + + path = NIL; + memset(&grand_totals, 0, sizeof(grand_totals)); + + cxt_id++; + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &c, HASH_ENTER, &found); + Assert(!found); + contextid_entry->context_id = cxt_id + 1; + + MemoryContextStatsCounter(c, &grand_totals, &num_contexts); + + path = compute_context_path(c, context_id_lookup); + + PublishMemoryContext(meminfo, cxt_id, c, path, + grand_totals, num_contexts); + } + entry->total_stats = cxt_id + 1; + } + else + { + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + MemoryContextId *contextid_entry; + + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &cur, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = context_id + 1; + + /* + * Figure out the transient context_id of this context and each of + * its ancestors, to compute a path for this context. + */ + path = compute_context_path(cur, context_id_lookup); + + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + /* Account for saving one statistics slot for cumulative reporting */ + if (context_id < (MAX_MEMORY_CONTEXT_STATS_NUM - 1)) + { + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1); + } + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].totalspace += stat.totalspace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].nblocks += stat.nblocks; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freespace += stat.freespace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit per process is reached, write aggregate of the + * remaining statistics. + * + * We can store contexts from 0 to max_stats - 1. When context_id + * is greater than max_stats, we stop reporting individual + * statistics when context_id equals max_stats - 2. As we use + * max_stats - 1 array slot for reporting cumulative statistics or + * "Remaining Totals". + */ + if (context_id == (MAX_MEMORY_CONTEXT_STATS_NUM - 2)) + { + int namelen = strlen("Remaining Totals"); + + num_individual_stats = context_id + 1; + strlcpy(meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].name, + "Remaining Totals", namelen + 1); + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].ident[0] = '\0'; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].path[0] = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].type = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].levels = 0; + } + context_id++; + + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + contexts = lappend(contexts, c); + } + + /* + * Check if there are aggregated statistics or not in the result set. + * Statistics are individually reported when context_id <= max_stats, + * only if context_id > max_stats will there be aggregates. + */ + if (context_id <= MAX_MEMORY_CONTEXT_STATS_NUM) + { + entry->total_stats = context_id; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = 1; + } + + /* + * The number of contexts exceeded the space available, so report the + * number of aggregated memory contexts + */ + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = + context_id - num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for + * cumulative statistics. + */ + entry->total_stats = num_individual_stats + 1; + } + } + + entry->stats_written = true; + dshash_release_lock(MemoryStatsDsHash, entry); + hash_destroy(context_id_lookup); + + MemoryContextSwitchTo(oldcontext); + MemoryContextReset(memstats_ctx); + /* Notify waiting client backend and return */ + ConditionVariableSignal(&entry->memcxt_cv); +} + +/* + * PublishMemoryContext + * + * Copy the memory context statistics of a single context to a DSA memory + */ +static void +PublishMemoryContext(MemoryStatsEntry *memcxt_info, int curr_id, + MemoryContext context, List *path, + MemoryContextCounters stat, int num_contexts) +{ + char *ident = unconstify(char *, context->ident); + char *name = unconstify(char *, context->name); + + /* + * To be consistent with logging output, we label dynahash contexts with + * just the hash table name as with MemoryContextStatsPrint(). + */ + if (context->ident && strncmp(context->name, "dynahash", 8) == 0) + { + name = unconstify(char *, context->ident); + ident = NULL; + } + + if (name != NULL) + { + int namelen = strlen(name); + + if (namelen >= MEMORY_CONTEXT_NAME_SHMEM_SIZE) + namelen = pg_mbcliplen(name, namelen, + MEMORY_CONTEXT_NAME_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].name, name, namelen + 1); + } + else + /* Clearing the array */ + memcxt_info[curr_id].name[0] = '\0'; + + /* Trim and copy the identifier if it is not set to NULL */ + if (ident != NULL) + { + int idlen = strlen(context->ident); + + /* + * Some identifiers such as SQL query string can be very long, + * truncate oversize identifiers. + */ + if (idlen >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + idlen = pg_mbcliplen(ident, idlen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].ident, ident, idlen + 1); + } + else + memcxt_info[curr_id].ident[0] = '\0'; + + /* Store the path */ + if (path == NIL) + memcxt_info[curr_id].path[0] = 0; + else + { + int levels = Min(list_length(path), MAX_PATH_DISPLAY_LENGTH); + + memcxt_info[curr_id].path_length = levels; + memcxt_info[curr_id].levels = list_length(path); + + foreach_int(i, path) + { + memcxt_info[curr_id].path[foreach_current_index(i)] = i; + if (--levels == 0) + break; + } + } + memcxt_info[curr_id].type = context->type; + memcxt_info[curr_id].totalspace = stat.totalspace; + memcxt_info[curr_id].nblocks = stat.nblocks; + memcxt_info[curr_id].freespace = stat.freespace; + memcxt_info[curr_id].freechunks = stat.freechunks; + memcxt_info[curr_id].num_agg_stats = num_contexts; +} + +void +AtProcExit_memstats_cleanup(int code, Datum arg) +{ + int idx = MyProcNumber; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + bool found; + + if (MemoryStatsDsHash != NULL) + { + snprintf(key, CLIENT_KEY_SIZE, "%d", idx); + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + if (found) + { + if (MemoryStatsDsaArea != NULL && + DsaPointerIsValid(entry->memstats_dsa_pointer)) + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + } + dshash_delete_entry(MemoryStatsDsHash, entry); + } + LWLockAcquire(MemContextReportingClientKeysLock, LW_EXCLUSIVE); + client_keys[idx] = -1; + LWLockRelease(MemContextReportingClientKeysLock); +} diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index 36ad708b360..e246fc7ce1f 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -39,6 +39,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t PublishMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index 3f401faf3de..7e6de81e7d6 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -661,6 +661,13 @@ BaseInit(void) * drop ephemeral slots, which in turn triggers stats reporting. */ ReplicationSlotInitialize(); + + /* + * The before shmem exit callback frees the DSA memory occupied by the + * latest memory context statistics that could be published by this proc + * if requested. + */ + before_shmem_exit(AtProcExit_memstats_cleanup, 0); } diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 073bdb35d2a..fd75345ff5f 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -1011,6 +1011,37 @@ MemoryContextStatsInternal(MemoryContext context, int level, } } + +/* + * MemoryContextStatsCounter + * + * Accumulate statistics counts into *totals. totals should not be NULL. + * This involves a non-recursive tree traversal. + */ +void +MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts) +{ + int ichild = 1; + + *num_contexts = 0; + context->methods->stats(context, NULL, NULL, totals, false); + + for (MemoryContext curr = context->firstchild; + curr != NULL; + curr = MemoryContextTraverseNext(curr, context)) + { + curr->methods->stats(curr, NULL, NULL, totals, false); + ichild++; + } + + /* + * Add the count of all the children contexts which are traversed + * including the parent. + */ + *num_contexts = *num_contexts + ichild; +} + /* * MemoryContextStatsPrint * Print callback used by MemoryContextStatsInternal diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 5e5e33f64fc..b8a70ddb144 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8633,6 +8633,16 @@ prorettype => 'bool', proargtypes => 'int4', prosrc => 'pg_log_backend_memory_contexts' }, +# publishing memory contexts of the specified postgres process +{ oid => '2173', descr => 'publish memory contexts of the specified backend', + proname => 'pg_get_process_memory_contexts', provolatile => 'v', + prorows => '100', proretset => 't', proparallel => 'r', + prorettype => 'record', proargtypes => 'int4 bool', + proallargtypes => '{int4,bool,text,text,text,int4,_int4,int8,int8,int8,int8,int8,int4}', + proargmodes => '{i,i,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{pid, summary, name, ident, type, level, path, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes, num_agg_contexts}', + prosrc => 'pg_get_process_memory_contexts' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index db559b39c4d..7e301affccc 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -96,6 +96,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t PublishMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h index e94ebce95b9..6dd80689bbb 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -87,6 +87,7 @@ PG_LWLOCK(52, SerialControl) PG_LWLOCK(53, AioWorkerSubmissionQueue) PG_LWLOCK(54, WaitLSN) PG_LWLOCK(55, LogicalDecodingControl) +PG_LWLOCK(56, MemContextReportingClientKeys) /* * There also exist several built-in LWLock tranches. As with the predefined diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index e52b8eb7697..4f1bcbf709f 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -35,6 +35,7 @@ typedef enum PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to send the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ /* Recovery conflict reasons */ diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 11ab1717a16..6e8920bf28d 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -19,7 +19,6 @@ #include "nodes/memnodes.h" - /* * MaxAllocSize, MaxAllocHugeSize * Quasi-arbitrary limits on size of allocations. @@ -319,4 +318,11 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) return true; } +extern void ProcessGetMemoryContextInterrupt(void); +extern void HandleGetMemoryContextInterrupt(void); +extern void MemoryContextKeysShmemInit(void); +extern Size MemoryContextKeysShmemSize(void); +extern void MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts); +extern void AtProcExit_memstats_cleanup(int code, Datum arg); #endif /* MEMUTILS_H */ diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 3dd63fd88ed..c84e5adce16 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -233,3 +233,22 @@ select * from pg_timezone_abbrevs where abbrev = 'LMT'; LMT | @ 7 hours 52 mins 58 secs ago | f (1 row) +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; +NOTICE: (AllocSet,TopMemoryContext,) +NOTICE: (AllocSet,TopMemoryContext,) diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 004f9a70e00..d34d2b6211c 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -101,3 +101,21 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs; -- One specific case we can check without much fear of breakage -- is the historical local-mean-time value used for America/Los_Angeles. select * from pg_timezone_abbrevs where abbrev = 'LMT'; + +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 9f5ee8fd482..b83336f94ce 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1706,6 +1706,8 @@ MemoryContextData MemoryContextId MemoryContextMethodID MemoryContextMethods +MemoryStatsEntry +MemoryStatsDSHashEntry MemoryStatsPrintFunc MergeAction MergeActionState -- 2.34.1 [application/octet-stream] v49-0002-Test-module-to-test-memory-context-reporting-wit.patch (8.8K, ../../CAH2L28umvzm9YwQBa_zrJM0ABC57ZAKPBxi8bD+n=1ADJ3SOZw@mail.gmail.com/4-v49-0002-Test-module-to-test-memory-context-reporting-wit.patch) download | inline diff: From 57127653459b7f2759f2ec30f566a1378dcb43ca Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Fri, 28 Nov 2025 14:46:38 +0530 Subject: [PATCH 2/2] Test module to test memory context reporting with injection points --- src/test/modules/Makefile | 1 + .../test_memcontext_reporting/Makefile | 29 ++++ .../t/001_memcontext_inj.pl | 150 ++++++++++++++++++ .../test_memcontext_reporting.c | 12 ++ .../test_memcontext_reporting.control | 4 + 5 files changed, 196 insertions(+) create mode 100644 src/test/modules/test_memcontext_reporting/Makefile create mode 100644 src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index 44c7163c1cd..8ce9598495c 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -34,6 +34,7 @@ SUBDIRS = \ test_json_parser \ test_lfind \ test_lwlock_tranches \ + test_memcontext_reporting \ test_misc \ test_oat_hooks \ test_parser \ diff --git a/src/test/modules/test_memcontext_reporting/Makefile b/src/test/modules/test_memcontext_reporting/Makefile new file mode 100644 index 00000000000..0a2dfc44f1c --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/Makefile @@ -0,0 +1,29 @@ +# src/test/modules/test_memcontext_reporting/Makefile + +EXTRA_INSTALL = src/test/modules/injection_points + +export enable_injection_points +MODULE_big = test_memcontext_reporting +OBJS = \ + $(WIN32RES) \ + test_memcontext_reporting.o +PGFILEDESC = "test_memcontext_reporting - test code for memory context reporting" + +REGRESS = test_memcontext_reporting + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_memcontext_reporting +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif + +check: + $(prove_check) + +installcheck: + $(prove_installcheck) diff --git a/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl new file mode 100644 index 00000000000..e054ac29f05 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl @@ -0,0 +1,150 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test suite for testing memory context statistics reporting + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} +my $psql_err; +my $psql_out; +# Create and start a cluster with one node +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->append_conf( + 'postgresql.conf', + qq[ +max_connections = 100 +log_statement = none +restart_after_crash = false +]); +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); + +# Attaching to a client process's injection point that throws an error +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-client-injection', 'error');" +); + +my $pid = $node->safe_psql('postgres', + "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); + +#Client should have thrown error +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); +like($psql_err, + qr/error triggered for injection point memcontext-client-injection/); + +#Query the same process after detaching the injection point, using some other client and it should succeed. +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-client-injection');"); +my $topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name = 'TopMemoryContext'); + +# Attaching to a target process injection point that throws an error +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-server-injection', 'error');" +); + +#Server should have thrown error +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); + +#Query the same process after detaching the injection point, using some other client and it should succeed. +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-injection');"); +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name = 'TopMemoryContext'); + +# Test that two concurrent requests to the same process results in a warning for +# one of those + +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-client-injection', 'wait');"); +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-server-wait', 'wait');"); +my $psql_session1 = $node->background_psql('postgres'); +$psql_session1->query_until( + qr//, + qq( + SELECT pg_get_process_memory_contexts($pid, true); +)); +$node->wait_for_event('client backend', 'memcontext-client-injection'); +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); +ok($psql_err =~ + /WARNING: server process $pid is processing previous request/); +#Wake the client up. +$node->safe_psql('postgres', + "SELECT injection_points_wakeup('memcontext-client-injection');"); + +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-client-injection');"); +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-wait');"); + +# Test the client process exiting with timeout does not break the server process + +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-server-wait', 'wait');"); +# Following client query times out, returning NULL as output +$node->psql( + 'postgres', + qq(select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';), + stdout => \$psql_out); +ok($psql_out = 'NULL'); +#Wakeup the server process up and detach the injection point. +$node->safe_psql('postgres', + "SELECT injection_points_wakeup('memcontext-server-wait');"); +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-wait');"); +#Query the same server process again and it should succeed. +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name = 'TopMemoryContext'); + +# Test if the monitoring works fine, when the client backend crashes. +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-client-injection', 'test_memcontext_reporting', 'crash', NULL);" +); + +#Client will crash +$node->psql( + 'postgres', + qq(select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';), + stderr => \$psql_err); +like($psql_err, + qr/WARNING: terminating connection because of crash of another server process|server closed the connection unexpectedly|connection to server was lost|could not send data to server/ +); + +# Wait till server restarts +$node->restart; +$node->poll_query_until('postgres', "SELECT 1;", '1'); + +#Querying memory stats should succeed after server start +$pid = $node->safe_psql('postgres', + "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name = 'TopMemoryContext'); + +done_testing(); diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c new file mode 100644 index 00000000000..d641f3616dc --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c @@ -0,0 +1,12 @@ +#include "postgres.h" +#include "funcapi.h" + +PG_MODULE_MAGIC; + +extern PGDLLEXPORT void crash(const char *name, const void *private_data, void *arg); + +void +crash(const char *name, const void *private_data, void *arg) +{ + abort(); +} diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control new file mode 100644 index 00000000000..48b501682d5 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control @@ -0,0 +1,4 @@ +comment = 'Test code for memcontext reporting' +default_version = '1.0' +module_pathname = '$libdir/test_memcontext_reporting' +relocatable = true -- 2.34.1 ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2026-02-10 00:03 Rahila Syed <[email protected]> parent: Rahila Syed <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Rahila Syed @ 2026-02-10 00:03 UTC (permalink / raw) To: Daniel Gustafsson <[email protected]>; +Cc: torikoshia <[email protected]>; pgsql-hackers; Robert Haas <[email protected]> Hi, Please find attached rebased and updated patches that include a few fixes suggested by Daniel. Fixes include 1. Changed LW_SHARED to LW_EXCLUSIVE when resetting client_keys. 2. Replaced calls to MemoryContextReset with MemoryContextDelete since a new memory context is created each time. 3. Removed leftover code from earlier versions in ipci.c 4. Fix the TAP test to compare strings correctly. 5. Added more comments in the TAP test. Thank you, Rahila Syed Attachments: [application/octet-stream] v50-0001-Add-function-to-report-memory-context-statistics.patch (59.9K, ../../CAH2L28vjHHdpoDKfcLT7nt765k_1X=R4qK7etQyf_DDUg-kNoQ@mail.gmail.com/3-v50-0001-Add-function-to-report-memory-context-statistics.patch) download | inline diff: From 64d533ef0719cc24fdedc1bc787ff9f8975c61bc Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Thu, 27 Nov 2025 14:39:43 +0530 Subject: [PATCH 1/2] Add function to report memory context statistics This adds a function for retrieving memory context statistics and information from backends as well as auxiliary processes. The intended usecase is cluster debugging when under memory pressure or unanticipated memory usage characteristics. When calling the function it sends a signal to the specified process to submit statistics regarding its memory contexts into dynamic shared memory. Each memory context is returned in detail, followed by a cumulative total in case the number of contexts exceed the max allocated amount of shared memory. Each process is limited to use at most 1Mb memory for this. A summary can also be explicitly requested by the user, this will return the TopMemoryContext and a cumulative total of all lower contexts. In order to not block on busy processes, we have hardcoded the number of seconds during which to retry before timing out. In the case where no statistics are published within the set timeout, NULL is returned --- doc/src/sgml/func/func-admin.sgml | 159 +++ src/backend/catalog/system_functions.sql | 14 + src/backend/postmaster/autovacuum.c | 4 + src/backend/postmaster/checkpointer.c | 4 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 4 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 4 + src/backend/storage/ipc/dsm_registry.c | 8 +- src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/storage/lmgr/proc.c | 1 + src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 2 + src/backend/utils/adt/mcxtfuncs.c | 1020 ++++++++++++++++- src/backend/utils/init/globals.c | 1 + src/backend/utils/init/postinit.c | 7 + src/backend/utils/mmgr/mcxt.c | 31 + src/include/catalog/pg_proc.dat | 10 + src/include/miscadmin.h | 1 + src/include/storage/lwlocklist.h | 1 + src/include/storage/procsignal.h | 1 + src/include/utils/memutils.h | 8 +- src/test/regress/expected/sysviews.out | 19 + src/test/regress/sql/sysviews.sql | 18 + src/tools/pgindent/typedefs.list | 2 + 26 files changed, 1313 insertions(+), 23 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 3ac81905d1f..af87b5b19ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -251,6 +251,132 @@ <literal>false</literal> is returned. </para></entry> </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_get_process_memory_contexts</primary> + </indexterm> + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type> <optional>,<parameter>summary</parameter> <type>boolean</type> <literal>DEFAULT</literal> <literal>false</literal></optional> ) + <returnvalue>setof record</returnvalue> + ( <parameter>name</parameter> <type>text</type>, + <parameter>ident</parameter> <type>text</type>, + <parameter>type</parameter> <type>text</type>, + <parameter>level</parameter> <type>integer</type>, + <parameter>path</parameter> <type>integer[]</type>, + <parameter>total_bytes</parameter> <type>bigint</type>, + <parameter>total_nblocks</parameter> <type>bigint</type>, + <parameter>free_bytes</parameter> <type>bigint</type>, + <parameter>free_chunks</parameter> <type>bigint</type>, + <parameter>used_bytes</parameter> <type>bigint</type>, + <parameter>num_agg_contexts</parameter> <type>integer</type> ) + </para> + <para> + This function handles requests to display the memory contexts of a + <productname>PostgreSQL</productname> process with the specified + process ID. The function can be used to send requests to backends as + well as <glossterm linkend="glossary-auxiliary-proc">auxiliary processes</glossterm>. + If the process does not respond with memory contexts statistics in 5 seconds, + function returns NULL. + </para> + <para> + The returned record contains extended statistics per each memory + context: + <itemizedlist spacing="compact"> + <listitem> + <para> + <parameter>name</parameter> - The name of the memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>ident</parameter> - Memory context ID (if any). + </para> + </listitem> + <listitem> + <para> + <parameter>type</parameter> - The type of memory context, possible + values are: AllocSet, Generation, Slab and Bump. + </para> + </listitem> + <listitem> + <para> + <parameter>level</parameter> - The level in the tree of the current + memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>path</parameter> - Memory contexts are organized in a + tree model with TopMemoryContext as the root, and all other memory + contexts as nodes in the tree. The <parameter>path</parameter> + displays the path from the root to the current memory context. The + path is limited to 100 children per node, which each node limited + to a max depth of 100, to preserve memory during reporting. The + printed path will also be limited to 100 nodes counting from the + TopMemoryContext. + </para> + </listitem> + <listitem> + <para> + <parameter>total_bytes</parameter> - The total number of bytes + allocated to this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_nblocks</parameter> - The total number of blocks + used for the allocated memory. + </para> + </listitem> + <listitem> + <para> + <parameter>free_bytes</parameter> - The amount of free memory in + this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>free_chunks</parameter> - The number of chunks that + <parameter>free_bytes</parameter> corresponds to. + </para> + </listitem> + <listitem> + <para> + <parameter>used_bytes</parameter> - The total number of bytes + currently occupied. + </para> + </listitem> + <listitem> + <para> + <parameter>num_agg_contexts</parameter> - The number of memory + contexts aggregated in the displayed statistics. + </para> + </listitem> + </itemizedlist> + </para> + <para> + When <parameter>summary</parameter> is <literal>true</literal>, statistics + for memory contexts at levels 1 and 2 are displayed, with level 1 + representing the root node (i.e., <literal>TopMemoryContext</literal>). + Statistics for contexts on level 2 and below are aggregates of all + child contexts' statistics, where <literal>num_agg_contexts</literal> + indicate the number aggregated child contexts. When + <parameter>summary</parameter> is <literal>false</literal> (the default), + <literal>the num_agg_contexts</literal> value is <literal>1</literal>, + indicating that individual statistics are being displayed. + </para> + <para> + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context + statistics are aggregated and a cumulative total is displayed. The + <literal>num_agg_contexts</literal> column indicates the number of + contexts aggregated in the displayed statistics. When + <literal>num_agg_contexts</literal> is <literal>1</literal> it means + that the context statistics are displayed separately. + </para></entry> + </row> </tbody> </tgroup> </table> @@ -302,6 +428,39 @@ LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 because it may generate a large number of log messages. </para> + <para> + <function>pg_get_process_memory_contexts</function> can be used to request + memory contexts statistics of any <productname>PostgreSQL</productname> + process. For example: +<programlisting> +postgres=# SELECT * FROM pg_get_process_memory_contexts( + (SELECT pid FROM pg_stat_activity + WHERE backend_type = 'checkpointer'), + false) LIMIT 1; +-[ RECORD 1 ]----+------------------------------ +name | TopMemoryContext +ident | +type | AllocSet +level | 1 +path | {1} +total_bytes | 90304 +total_nblocks | 3 +free_bytes | 2880 +free_chunks | 1 +used_bytes | 87424 +num_agg_contexts | 1 +</programlisting> + <note> + <para> + While <function>pg_get_process_memory_contexts</function> can be used to + query memory contexts of the local backend, + <structname>pg_backend_memory_contexts</structname> + (see <xref linkend="view-pg-backend-memory-contexts"/> for more details) + will be less resource intensive when only the local backend is of interest. + </para> + </note> + </para> + </sect2> <sect2 id="functions-admin-backup"> diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql index eb9e31ae1bf..d1416e5534d 100644 --- a/src/backend/catalog/system_functions.sql +++ b/src/backend/catalog/system_functions.sql @@ -657,6 +657,17 @@ LANGUAGE INTERNAL STRICT VOLATILE PARALLEL UNSAFE AS 'pg_replication_origin_session_setup'; +CREATE OR REPLACE FUNCTION + pg_get_process_memory_contexts(IN pid integer, IN summary boolean DEFAULT false, + OUT name text, OUT ident text, OUT type text, OUT level integer, + OUT path integer[], OUT total_bytes bigint, OUT total_nblocks bigint, + OUT free_bytes bigint, OUT free_chunks bigint, OUT used_bytes bigint, + OUT num_agg_contexts integer) +RETURNS SETOF RECORD +LANGUAGE INTERNAL +STRICT VOLATILE PARALLEL UNSAFE +AS 'pg_get_process_memory_contexts'; + -- -- The default permissions for functions mean that anyone can execute them. -- A number of functions shouldn't be executable by just anyone, but rather @@ -782,6 +793,7 @@ REVOKE EXECUTE ON FUNCTION pg_ls_logicalmapdir() FROM PUBLIC; REVOKE EXECUTE ON FUNCTION pg_ls_replslotdir(text) FROM PUBLIC; +REVOKE EXECUTE ON FUNCTION pg_get_process_memory_contexts(integer, boolean) FROM PUBLIC; -- -- We also set up some things as accessible to standard roles. -- @@ -808,6 +820,8 @@ GRANT EXECUTE ON FUNCTION pg_current_logfile() TO pg_monitor; GRANT EXECUTE ON FUNCTION pg_current_logfile(text) TO pg_monitor; +GRANT EXECUTE ON FUNCTION pg_get_process_memory_contexts(integer, boolean) TO pg_read_all_stats; + GRANT pg_read_all_settings TO pg_monitor; GRANT pg_read_all_stats TO pg_monitor; diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 6fde740465f..3c24119c43a 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -787,6 +787,10 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index e03c19123bc..d54193f8890 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -684,6 +684,10 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index a2c0ff012c5..cb18ce80893 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -48,6 +48,10 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index 82731e452fc..e13a40f1427 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -870,6 +870,10 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index cdbe53dd262..1d6d8f22d20 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -192,6 +192,10 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index 2d8f57099fd..72aaf8ae5cc 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -878,6 +878,10 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c index 068c1577b12..4810babac1b 100644 --- a/src/backend/storage/ipc/dsm_registry.c +++ b/src/backend/storage/ipc/dsm_registry.c @@ -270,7 +270,9 @@ GetNamedDSMSegment(const char *name, size_t size, * This routine returns a pointer to the DSA. A new LWLock tranche ID will be * generated if needed. Note that the lock tranche will be registered with the * provided name. Also note that this should be called at most once for a - * given DSA in each backend. + * given DSA in each backend. It's best to minimize the number of errors thrown + * in this function since it's called from a CFI function. An error here could + * lead to error in any transaction that calls the CFI function. */ dsa_area * GetNamedDSA(const char *name, bool *found) @@ -351,7 +353,9 @@ GetNamedDSA(const char *name, bool *found) * params is ignored; a new LWLock tranche ID will be generated if needed. * Note that the lock tranche will be registered with the provided name. Also * note that this should be called at most once for a given table in each - * backend. + * backend. It's best to minimize the number of errors thrown in this function + * since it's called from a CFI function. An error here could lead to error in + * any transaction that calls the CFI function. */ dshash_table * GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found) diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index 1f7e933d500..b0176613987 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -52,6 +52,7 @@ #include "storage/sinvaladt.h" #include "utils/guc.h" #include "utils/injection_point.h" +#include "utils/memutils.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -141,6 +142,7 @@ CalculateShmemSize(void) size = add_size(size, AioShmemSize()); size = add_size(size, WaitLSNShmemSize()); size = add_size(size, LogicalDecodingCtlShmemSize()); + size = add_size(size, MemoryContextKeysShmemSize()); /* include additional requested shmem from preload libraries */ size = add_size(size, total_addin_request); @@ -328,6 +330,7 @@ CreateOrAttachShmemStructs(void) AioShmemInit(); WaitLSNShmemInit(); LogicalDecodingCtlShmemInit(); + MemoryContextKeysShmemInit(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index 8e56922dcea..601e01d2574 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -694,6 +694,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_GET_MEMORY_CONTEXT)) + HandleGetMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 8560a903bc8..f68583aa820 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -51,6 +51,7 @@ #include "storage/procsignal.h" #include "storage/spin.h" #include "storage/standby.h" +#include "utils/memutils.h" #include "utils/timeout.h" #include "utils/timestamp.h" diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 02e9aaa6bca..1478bce89a2 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3518,6 +3518,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 4aa864fe3c3..3e1dc4225b2 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -163,6 +163,7 @@ WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." +MEM_CXT_PUBLISH "Waiting for a process to publish memory information." ABI_compatibility: @@ -366,6 +367,7 @@ SerialControl "Waiting to read or update shared <filename>pg_serial</filename> s AioWorkerSubmissionQueue "Waiting to access AIO worker submission queue." WaitLSN "Waiting to read or update shared Wait-for-LSN state." LogicalDecodingControl "Waiting to read or update logical decoding status information." +MemContextReportingClientKeys "Waiting for another process to complete reading or writing the memory reporting keys." # # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE) diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index 12b8d4cefaf..7386e9527ae 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -15,24 +15,128 @@ #include "postgres.h" +#include "access/xact.h" #include "funcapi.h" #include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "storage/dsm_registry.h" #include "storage/proc.h" #include "storage/procarray.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/injection_point.h" +#include "utils/memutils.h" +#include "utils/wait_event_types.h" + +/* + * Memory Context reporting size limits. + */ + +/* Max length of context name and ident, to keep it consistent + * with ProcessLogMemoryContext() + */ +#define MEMORY_CONTEXT_IDENT_SHMEM_SIZE 100 +#define MEMORY_CONTEXT_NAME_SHMEM_SIZE 100 + +/* Maximum size (in bytes) of DSA area per process */ +#define MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND ((size_t) (1 * 1024 * 1024)) + +/* + * Maximum number of memory context statistics is calculated by dividing + * max memory allocated per backend with maximum size per context statistics. + * The identifier and name are statically allocated arrays of size 100 bytes. + * The path depth is limited to 100 like for memory context logging. + */ +#define MAX_MEMORY_CONTEXT_STATS_NUM MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND / (sizeof(MemoryStatsEntry)) + +/* + * Size of dshash key. The key is a uint32 rendered as a string, 10 chars + * plus space for a NULL terminator can hold all the values. + */ +#define CLIENT_KEY_SIZE (10 + 1) + +/* Dynamic shared memory state for reporting statistics per context */ +typedef struct MemoryStatsEntry +{ + char name[MEMORY_CONTEXT_NAME_SHMEM_SIZE]; + char ident[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + int path[100]; + NodeTag type; + int path_length; + int levels; + int64 totalspace; + int64 nblocks; + int64 freespace; + int64 freechunks; + int num_agg_stats; +} MemoryStatsEntry; + +/* + * Per backend dynamic shared hash entry for memory context statistics + * reporting. + */ +typedef struct MemoryStatsDSHashEntry +{ + char key[64]; + ConditionVariable memcxt_cv; + bool stats_written; + int target_server_id; + int total_stats; + bool summary; + dsa_pointer memstats_dsa_pointer; +} MemoryStatsDSHashEntry; + +static const dshash_parameters memctx_dsh_params = { + offsetof(MemoryStatsDSHashEntry, memcxt_cv), + sizeof(MemoryStatsDSHashEntry), + dshash_strcmp, + dshash_strhash, + dshash_strcpy +}; + +/* + * These are used for reporting memory context statistics of a process. + */ + +/* Array to store the keys of MemoryStatsDsHash */ +static int *client_keys = NULL; + +/* + * Table to store pointers to DSA memory containing memory statistics and other + * metadata. There is one entry per client backend request, keyed by ProcNumber + * of the client obtained from client_keys array above. + */ +static dshash_table *MemoryStatsDsHash = NULL; + +/* + * Dsa area which stores the actual memory context + * statistics. + */ +static dsa_area *MemoryStatsDsaArea = NULL; + +static void memstats_dsa_cleanup(char *key); +static void memstats_client_key_reset(int ProcNumber); +static void PublishMemoryContext(MemoryStatsEntry *memcxt_info, + int curr_id, MemoryContext context, + List *path, + MemoryContextCounters stat, + int num_contexts); /* ---------- * The max bytes for showing identifiers of MemoryContext. + * This is used by pg_get_backend_memory_context - view used for local backend. * ---------- */ #define MEMORY_CONTEXT_IDENT_DISPLAY_SIZE 1024 +#define MAX_PATH_DISPLAY_LENGTH 100 +/* Timeout in seconds */ +#define MEMORY_STATS_MAX_TIMEOUT 5 + /* * MemoryContextId - * Used for storage of transient identifiers for - * pg_get_backend_memory_contexts. + * Used for storage of transient identifiers for memory context reporting */ typedef struct MemoryContextId { @@ -62,6 +166,67 @@ int_list_to_array(const List *list) return PointerGetDatum(result_array); } +/* + * context_type_to_string + * Returns a textual representation of a context type + * + * This should cover the same types as MemoryContextIsValid. + */ +static const char * +context_type_to_string(NodeTag type) +{ + const char *context_type; + + switch (type) + { + case T_AllocSetContext: + context_type = "AllocSet"; + break; + case T_GenerationContext: + context_type = "Generation"; + break; + case T_SlabContext: + context_type = "Slab"; + break; + case T_BumpContext: + context_type = "Bump"; + break; + default: + context_type = "???"; + break; + } + return context_type; +} + +/* + * compute_context_path + * + * Append the transient context_id of this context and each of its ancestors + * to a list, in order to compute a path. + */ +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + MemoryContext cur_context; + + for (cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + { + elog(NOTICE, "hash table corrupted, can't construct path value"); + return NIL; + } + path = lcons_int(cur_entry->context_id, path); + } + return path; +} + /* * PutMemoryContextsStatsTupleStore * Add details for the given MemoryContext to 'tupstore'. @@ -143,24 +308,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, else nulls[1] = true; - switch (context->type) - { - case T_AllocSetContext: - type = "AllocSet"; - break; - case T_GenerationContext: - type = "Generation"; - break; - case T_SlabContext: - type = "Slab"; - break; - case T_BumpContext: - type = "Bump"; - break; - default: - type = "???"; - break; - } + type = context_type_to_string(context->type); values[2] = CStringGetTextDatum(type); values[3] = Int32GetDatum(list_length(path)); /* level */ @@ -305,3 +453,835 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } + +/* + * pg_get_process_memory_contexts + * Signal a backend or an auxiliary process to send its memory contexts, + * wait for the results and display them. + * + * By default, only superusers or users with ROLE_PG_READ_ALL_STATS are allowed + * to signal a process to return the memory contexts. Additional roles can be + * permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to copy the memory context details + * to a dynamic shared memory space. + * + * We have defined a limit on DSA memory that could be allocated per process - + * if the process has more memory contexts than what can fit in the allocated + * size, the excess contexts are summarized and represented as cumulative total + * at the end of the buffer. + * + * After sending the signal, wait on a condition variable. The publishing + * backend, after copying the data to shared memory, sends a signal on that + * condition variable. There is one condition variable per client process. + * Once the condition variable is signalled, check if the latest memory context + * information is available and display. + * + * If the publishing backend does not respond before the condition variable + * times out, which is set to a predefined value MEMORY_STATS_MAX_TIMEOUT, give + * up and return NULL. + */ +Datum +pg_get_process_memory_contexts(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + bool summary = PG_GETARG_BOOL(1); + PGPROC *proc; + ProcNumber procNumber; + bool proc_is_aux = false; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + MemoryStatsEntry *memcxt_info; + MemoryStatsDSHashEntry *entry; + bool found; + char key[CLIENT_KEY_SIZE]; + TimestampTz start_timestamp; + + /* + * See if the process with given pid is a backend or an auxiliary process + * and remember the type for when we requery the process later. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + { + proc = AuxiliaryPidGetProc(pid); + proc_is_aux = true; + } + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; this is however not a problem and leave with a WARNING. + * See comment in pg_log_backend_memory_contexts for a discussion on this. + */ + if (proc == NULL) + { + ereport(WARNING, + errmsg("PID %d is not a PostgreSQL server process", pid)); + PG_RETURN_NULL(); + } + + procNumber = GetNumberFromPGProc(proc); + + /* + * Check if the server process slot is not empty and exit early Non-empty + * slot means some other client backend is requesting the statistics from + * the same server process. + */ + LWLockAcquire(MemContextReportingClientKeysLock, LW_EXCLUSIVE); + if (client_keys[procNumber] != -1) + { + LWLockRelease(MemContextReportingClientKeysLock); + ereport(WARNING, + errmsg("server process %d is processing previous request", + pid)); + PG_RETURN_NULL(); + } + LWLockRelease(MemContextReportingClientKeysLock); + + /* + * Create a DSA to allocate memory for copying memory contexts statistics. + * Allocate the memory in the DSA and send DSA pointer to the server + * process for storing the context statistics. If number of contexts + * exceed a predefined limit (1MB), a cumulative total is stored for such + * contexts. + * + * The DSA is created once for the lifetime of the server, and only + * attached in subsequent calls. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); + + /* + * The DSA pointers containing statistics for each client are stored in a + * dshash table. In addition to DSA pointer, each entry in this table also + * contains information about the statistics, condition variable for + * signalling between client and the server and miscellaneous data + * specific to a request. There is one entry per client request in the + * hash table. + */ + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); + + snprintf(key, sizeof(key), "%d", MyProcNumber); + + /* + * Insert an entry for this client in DSHASH table the first time this + * function is called. This entry is deleted when the process exits in + * before_shmem_exit call. + * + * dshash_find_or_insert locks the entry to prevent the publisher from + * reading before client has updated the entry. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + if (!found) + { + entry->stats_written = false; + ConditionVariableInit(&entry->memcxt_cv); + } + + /* + * Allocate 1MB of memory for the backend to publish its statistics on + * every call to this function. The memory is freed at the end of the + * function. + */ + entry->memstats_dsa_pointer = + dsa_allocate0(MemoryStatsDsaArea, MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND); + + /* + * Specify whether a summary of statistics is requested, before signalling + * the server. + */ + entry->summary = summary; + + /* + * Indicate which server process statistics are being requested from. If + * this client times out before the last requested process can publish its + * statistics, it may send a new request to another server process. Since + * the previous server was notified, it might attempt to read the same + * client entry and respond incorrectly with its statistics. By storing + * the server ID in the client entry, we prevent any previously signalled + * server process from writing its statistics in the space meant for the + * newly requested process. + */ + entry->target_server_id = pid; + dshash_release_lock(MemoryStatsDsHash, entry); + + /* + * Check if the publishing process slot is empty and store this clients + * key i.e its procNumber. This informs the publishing process that it is + * supposed to write statistics in the hash entry corresponding to this + * client. + */ + LWLockAcquire(MemContextReportingClientKeysLock, LW_EXCLUSIVE); + if (client_keys[procNumber] == -1) + client_keys[procNumber] = MyProcNumber; + else + { + LWLockRelease(MemContextReportingClientKeysLock); + ereport(WARNING, + errmsg("server process %d is processing previous request", + pid)); + PG_RETURN_NULL(); + } + LWLockRelease(MemContextReportingClientKeysLock); + + /* + * Send a signal to a PostgreSQL process, informing it we want it to + * produce information about its memory contexts. + */ + if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, procNumber) < 0) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ereport(WARNING, + errmsg("could not send signal to process %d: %m", + pid)); + PG_RETURN_NULL(); + } + start_timestamp = GetCurrentTimestamp(); + + while (1) + { + long elapsed_time; + + INJECTION_POINT("memcontext-client-injection", NULL); + + elapsed_time = TimestampDifferenceMilliseconds(start_timestamp, + GetCurrentTimestamp()); + /* Return if we have already exceeded the timeout */ + if (elapsed_time >= MEMORY_STATS_MAX_TIMEOUT * 1000) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + ereport(NOTICE, + errmsg("request for memory context statistics for PID %d timed out", + pid)); + PG_RETURN_NULL(); + } + + /* + * Recheck the state of the backend before sleeping on the condition + * variable to ensure the process is still alive. Only check the + * relevant process type based on the earlier PID check. + */ + if (proc_is_aux) + proc = AuxiliaryPidGetProc(pid); + else + proc = BackendPidGetProc(pid); + + /* + * The target server process ending during memory context processing + * is not an error. + */ + if (proc == NULL) + { + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + ereport(WARNING, + errmsg("PID %d is no longer a PostgreSQL server process", + pid)); + PG_RETURN_NULL(); + } + + /* + * Wait for MEMORY_STATS_MAX_TIMEOUT. If no statistics are available + * within the allowed time then return NULL. The timer is defined in + * milliseconds since that's what the condition variable sleep uses. + */ + if (ConditionVariableTimedSleep(&entry->memcxt_cv, + (MEMORY_STATS_MAX_TIMEOUT * 1000), + WAIT_EVENT_MEM_CXT_PUBLISH)) + { + /* Timeout has expired, return NULL */ + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + ereport(NOTICE, + errmsg("request for memory context statistics for PID %d timed out", + pid)); + PG_RETURN_NULL(); + } + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + Assert(found); + + memcxt_info = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + /* + * We expect to come out of sleep when the requested process has + * finished publishing the statistics, verified using a boolean + * stats_written. + * + * Make sure that the statistics are actually written by checking that + * the name of the context is not NULL. This is done to ensure that + * the subsequent waits for statistics do not return spuriously if the + * previous call to the function ended in error and thus could not + * clear the stats_written flag. + */ + if (entry->stats_written && memcxt_info[0].name[0] != '\0') + break; + + dshash_release_lock(MemoryStatsDsHash, entry); + + } + + InitMaterializedSRF(fcinfo, 0); + + /* + * Backend has finished publishing the stats, project them. + */ +#define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 11 + for (int i = 0; i < entry->total_stats; i++) + { + ArrayType *path_array; + Datum values[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + bool nulls[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + Datum *path_datum = NULL; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + Assert(memcxt_info[i].name[0] != '\0'); + values[0] = CStringGetTextDatum(memcxt_info[i].name); + + if (memcxt_info[i].ident[0] != '\0') + values[1] = CStringGetTextDatum(memcxt_info[i].ident); + else + nulls[1] = true; + + values[2] = CStringGetTextDatum(context_type_to_string(memcxt_info[i].type)); + + if (memcxt_info[i].levels != 0) + values[3] = Int32GetDatum(memcxt_info[i].levels); + else + nulls[3] = true; + + if (memcxt_info[i].path[0] != 0) + { + int path_length; + + path_length = memcxt_info[i].path_length; + path_datum = (Datum *) palloc(path_length * sizeof(Datum)); + + for (int j = 0; j < path_length; j++) + path_datum[j] = Int32GetDatum(memcxt_info[i].path[j]); + path_array = construct_array_builtin(path_datum, + path_length, + INT4OID); + values[4] = PointerGetDatum(path_array); + } + else + nulls[4] = true; + + values[5] = Int64GetDatum(memcxt_info[i].totalspace); + values[6] = Int64GetDatum(memcxt_info[i].nblocks); + values[7] = Int64GetDatum(memcxt_info[i].freespace); + values[8] = Int64GetDatum(memcxt_info[i].freechunks); + values[9] = Int64GetDatum(memcxt_info[i].totalspace - + memcxt_info[i].freespace); + values[10] = Int32GetDatum(memcxt_info[i].num_agg_stats); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + dshash_release_lock(MemoryStatsDsHash, entry); + memstats_dsa_cleanup(key); + + ConditionVariableCancelSleep(); + + PG_RETURN_NULL(); +} + +/* + * This function is executed before returning from the + * pg_get_process_memory_contexts() function. + * It releases the DSA memory allocated for storing the memory statistics + * retrieved by that function. The caller must ensure that the LWLock for + * MemoryStatsDsHash is not already held. +*/ +static void +memstats_dsa_cleanup(char *key) +{ + MemoryStatsDSHashEntry *entry; + + entry = dshash_find(MemoryStatsDsHash, key, true); + + Assert(MemoryStatsDsaArea != NULL); + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + entry->memstats_dsa_pointer = InvalidDsaPointer; + entry->stats_written = false; + entry->target_server_id = 0; + + dshash_release_lock(MemoryStatsDsHash, entry); +} + +/* + * Remove this process from the publishing process' + * client key slot, if the stats publishing process has failed to do so. + */ +static void +memstats_client_key_reset(int procNumber) +{ + LWLockAcquire(MemContextReportingClientKeysLock, LW_EXCLUSIVE); + + if (client_keys[procNumber] == MyProcNumber) + client_keys[procNumber] = -1; + LWLockRelease(MemContextReportingClientKeysLock); +} + +void +MemoryContextKeysShmemInit(void) +{ + bool found; + + client_keys = (int *) + ShmemInitStruct("MemoryContextKeys", + MemoryContextKeysShmemSize(), &found); + + if (!found) + MemSet(client_keys, -1, MemoryContextKeysShmemSize()); +} + +Size +MemoryContextKeysShmemSize(void) +{ + Size TotalProcs = 0; + + TotalProcs = add_size(TotalProcs, NUM_AUXILIARY_PROCS); + TotalProcs = add_size(TotalProcs, MaxBackends); + + return mul_size(TotalProcs, sizeof(int)); +} + +/* + * HandleGetMemoryContextInterrupt + * Handle receipt of an interrupt indicating a request to publish memory + * contexts statistics. + * + * All the actual work is deferred to ProcessGetMemoryContextInterrupt() as + * this cannot be performed in a signal handler. + */ +void +HandleGetMemoryContextInterrupt(void) +{ + InterruptPending = true; + PublishMemoryContextPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + +/* + * ProcessGetMemoryContextInterrupt + * Generate information about memory contexts used by the process. + * + * Performs a breadth first search on the memory context tree, thus parents + * statistics are reported before their children in the monitoring function + * output. + * + * Statistics for all the processes are shared via the same dynamic shared + * area. Individual statistics are tracked independently in per-process DSA + * pointers. These pointers are stored in a dshash table with key as requesting + * clients ProcNumber. + * + * We calculate maximum number of context's statistics that can be displayed + * using a pre-determined limit for memory available per process for this + * utility and maximum size of statistics for each context. The remaining + * context statistics if any are captured as a cumulative total at the end of + * individual context's statistics. + * + * If summary is true, we capture the level 1 and level 2 contexts statistics. + * For that we traverse the memory context tree recursively in depth first + * search manner to cover all the children of a parent context, to be able to + * display a cumulative total of memory consumption by a parent at level 2 and + * all its children. + */ +void +ProcessGetMemoryContextInterrupt(void) +{ + List *contexts; + HASHCTL ctl; + HTAB *context_id_lookup; + int context_id = 0; + MemoryStatsEntry *meminfo; + MemoryContextCounters stat; + int num_individual_stats = 0; + bool found; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + int clientProcNumber; + MemoryContext memstats_ctx = NULL; + MemoryContext oldcontext = NULL; + + PublishMemoryContextPending = false; + + /* + * Avoid performing any shared memory operations in aborted transaction, + * the caller will timeout and return NULL. + */ + if (IsAbortedTransactionBlockState()) + return; + + INJECTION_POINT("memcontext-server-wait", NULL); + + /* + * Retrieve the client key for publishing statistics and reset it to -1, + * so other clients can request memory statistics from this process. + * Return if the client_key is -1, which means the requesting client has + * timed out. + */ + LWLockAcquire(MemContextReportingClientKeysLock, LW_EXCLUSIVE); + if (client_keys[MyProcNumber] == -1) + { + LWLockRelease(MemContextReportingClientKeysLock); + return; + } + clientProcNumber = client_keys[MyProcNumber]; + client_keys[MyProcNumber] = -1; + LWLockRelease(MemContextReportingClientKeysLock); + + /* + * Create a new memory context which is not a part of TopMemoryContext + * tree. This context is used to allocate all memory in this function. + * This helps in keeping the memory allocation in this function to report + * memory consumption statistics separate. So that it does not affect the + * output of this function. + */ + memstats_ctx = AllocSetContextCreate((MemoryContext) NULL, + "publish_memory_context_statistics", + ALLOCSET_SMALL_SIZES); + oldcontext = MemoryContextSwitchTo(memstats_ctx); + + /* + * The hash table is used for constructing "path" column of the view, + * similar to its local backend counterpart. + */ + ctl.keysize = sizeof(MemoryContext); + ctl.entrysize = sizeof(MemoryContextId); + ctl.hcxt = CurrentMemoryContext; + + context_id_lookup = hash_create("pg_get_process_memory_contexts", + 256, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + /* List of contexts to process in the next round - start at the top. */ + contexts = list_make1(TopMemoryContext); + + /* + * The client process should have created the required DSA and DSHash + * table. Here we just attach to those. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); + + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); + + snprintf(key, CLIENT_KEY_SIZE, "%d", clientProcNumber); + + /* + * The entry lock is held by dshash_find_or_insert to protect writes to + * process specific memory. Two different processes publishing statistics + * do not block each other. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + INJECTION_POINT("memcontext-server-injection", NULL); + + /* + * Check if the entry has been deleted due to calling process exiting, or + * if the caller has timed out waiting for us and have issued a request to + * another backend. + * + * Make sure that the client always deletes the entry after taking + * required lock or this function may end up writing to unallocated + * memory. + */ + if (!found || entry->target_server_id != MyProcPid) + { + entry->stats_written = false; + + dshash_release_lock(MemoryStatsDsHash, entry); + + hash_destroy(context_id_lookup); + MemoryContextSwitchTo(oldcontext); + MemoryContextDelete(memstats_ctx); + + return; + } + + /* Should be allocated by a client backend that is requesting statistics */ + Assert(entry->memstats_dsa_pointer != InvalidDsaPointer); + meminfo = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + if (entry->summary) + { + int cxt_id = 0; + List *path = NIL; + MemoryContextId *contextid_entry; + + /* Copy TopMemoryContext statistics to DSA */ + memset(&stat, 0, sizeof(stat)); + (*TopMemoryContext->methods->stats) (TopMemoryContext, NULL, NULL, + &stat, true); + path = lcons_int(1, path); + PublishMemoryContext(meminfo, cxt_id, TopMemoryContext, path, stat, + 1); + + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &TopMemoryContext, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = cxt_id + 1; + + /* + * Copy statistics for each of TopMemoryContexts children. This + * includes statistics of at most 100 children per node, with each + * child node limited to a depth of 100 in its subtree. + */ + for (MemoryContext c = TopMemoryContext->firstchild; c != NULL; + c = c->nextchild) + { + MemoryContextCounters grand_totals; + int num_contexts = 0; + + path = NIL; + memset(&grand_totals, 0, sizeof(grand_totals)); + + cxt_id++; + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &c, HASH_ENTER, &found); + Assert(!found); + contextid_entry->context_id = cxt_id + 1; + + MemoryContextStatsCounter(c, &grand_totals, &num_contexts); + + path = compute_context_path(c, context_id_lookup); + + PublishMemoryContext(meminfo, cxt_id, c, path, + grand_totals, num_contexts); + } + entry->total_stats = cxt_id + 1; + } + else + { + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + MemoryContextId *contextid_entry; + + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &cur, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = context_id + 1; + + /* + * Figure out the transient context_id of this context and each of + * its ancestors, to compute a path for this context. + */ + path = compute_context_path(cur, context_id_lookup); + + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + /* Account for saving one statistics slot for cumulative reporting */ + if (context_id < (MAX_MEMORY_CONTEXT_STATS_NUM - 1)) + { + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1); + } + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].totalspace += stat.totalspace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].nblocks += stat.nblocks; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freespace += stat.freespace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit per process is reached, write aggregate of the + * remaining statistics. + * + * We can store contexts from 0 to max_stats - 1. When context_id + * is greater than max_stats, we stop reporting individual + * statistics when context_id equals max_stats - 2. As we use + * max_stats - 1 array slot for reporting cumulative statistics or + * "Remaining Totals". + */ + if (context_id == (MAX_MEMORY_CONTEXT_STATS_NUM - 2)) + { + int namelen = strlen("Remaining Totals"); + + num_individual_stats = context_id + 1; + strlcpy(meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].name, + "Remaining Totals", namelen + 1); + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].ident[0] = '\0'; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].path[0] = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].type = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].levels = 0; + } + context_id++; + + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + contexts = lappend(contexts, c); + } + + /* + * Check if there are aggregated statistics or not in the result set. + * Statistics are individually reported when context_id <= max_stats, + * only if context_id > max_stats will there be aggregates. + */ + if (context_id <= MAX_MEMORY_CONTEXT_STATS_NUM) + { + entry->total_stats = context_id; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = 1; + } + + /* + * The number of contexts exceeded the space available, so report the + * number of aggregated memory contexts + */ + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = + context_id - num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for + * cumulative statistics. + */ + entry->total_stats = num_individual_stats + 1; + } + } + + entry->stats_written = true; + dshash_release_lock(MemoryStatsDsHash, entry); + hash_destroy(context_id_lookup); + + MemoryContextSwitchTo(oldcontext); + MemoryContextDelete(memstats_ctx); + /* Notify waiting client backend and return */ + ConditionVariableSignal(&entry->memcxt_cv); +} + +/* + * PublishMemoryContext + * + * Copy the memory context statistics of a single context to a DSA memory + */ +static void +PublishMemoryContext(MemoryStatsEntry *memcxt_info, int curr_id, + MemoryContext context, List *path, + MemoryContextCounters stat, int num_contexts) +{ + char *ident = unconstify(char *, context->ident); + char *name = unconstify(char *, context->name); + + /* + * To be consistent with logging output, we label dynahash contexts with + * just the hash table name as with MemoryContextStatsPrint(). + */ + if (context->ident && strncmp(context->name, "dynahash", 8) == 0) + { + name = unconstify(char *, context->ident); + ident = NULL; + } + + if (name != NULL) + { + int namelen = strlen(name); + + if (namelen >= MEMORY_CONTEXT_NAME_SHMEM_SIZE) + namelen = pg_mbcliplen(name, namelen, + MEMORY_CONTEXT_NAME_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].name, name, namelen + 1); + } + else + /* Clearing the array */ + memcxt_info[curr_id].name[0] = '\0'; + + /* Trim and copy the identifier if it is not set to NULL */ + if (ident != NULL) + { + int idlen = strlen(context->ident); + + /* + * Some identifiers such as SQL query string can be very long, + * truncate oversize identifiers. + */ + if (idlen >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + idlen = pg_mbcliplen(ident, idlen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].ident, ident, idlen + 1); + } + else + memcxt_info[curr_id].ident[0] = '\0'; + + /* Store the path */ + if (path == NIL) + memcxt_info[curr_id].path[0] = 0; + else + { + int levels = Min(list_length(path), MAX_PATH_DISPLAY_LENGTH); + + memcxt_info[curr_id].path_length = levels; + memcxt_info[curr_id].levels = list_length(path); + + foreach_int(i, path) + { + memcxt_info[curr_id].path[foreach_current_index(i)] = i; + if (--levels == 0) + break; + } + } + memcxt_info[curr_id].type = context->type; + memcxt_info[curr_id].totalspace = stat.totalspace; + memcxt_info[curr_id].nblocks = stat.nblocks; + memcxt_info[curr_id].freespace = stat.freespace; + memcxt_info[curr_id].freechunks = stat.freechunks; + memcxt_info[curr_id].num_agg_stats = num_contexts; +} + +void +AtProcExit_memstats_cleanup(int code, Datum arg) +{ + int idx = MyProcNumber; + MemoryStatsDSHashEntry *entry; + char key[CLIENT_KEY_SIZE]; + bool found; + + if (MemoryStatsDsHash != NULL) + { + snprintf(key, CLIENT_KEY_SIZE, "%d", idx); + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + + if (found) + { + if (MemoryStatsDsaArea != NULL && + DsaPointerIsValid(entry->memstats_dsa_pointer)) + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + } + dshash_delete_entry(MemoryStatsDsHash, entry); + } + LWLockAcquire(MemContextReportingClientKeysLock, LW_EXCLUSIVE); + client_keys[idx] = -1; + LWLockRelease(MemContextReportingClientKeysLock); +} diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index 36ad708b360..e246fc7ce1f 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -39,6 +39,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t PublishMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index 3f401faf3de..7e6de81e7d6 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -661,6 +661,13 @@ BaseInit(void) * drop ephemeral slots, which in turn triggers stats reporting. */ ReplicationSlotInitialize(); + + /* + * The before shmem exit callback frees the DSA memory occupied by the + * latest memory context statistics that could be published by this proc + * if requested. + */ + before_shmem_exit(AtProcExit_memstats_cleanup, 0); } diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 073bdb35d2a..fd75345ff5f 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -1011,6 +1011,37 @@ MemoryContextStatsInternal(MemoryContext context, int level, } } + +/* + * MemoryContextStatsCounter + * + * Accumulate statistics counts into *totals. totals should not be NULL. + * This involves a non-recursive tree traversal. + */ +void +MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts) +{ + int ichild = 1; + + *num_contexts = 0; + context->methods->stats(context, NULL, NULL, totals, false); + + for (MemoryContext curr = context->firstchild; + curr != NULL; + curr = MemoryContextTraverseNext(curr, context)) + { + curr->methods->stats(curr, NULL, NULL, totals, false); + ichild++; + } + + /* + * Add the count of all the children contexts which are traversed + * including the parent. + */ + *num_contexts = *num_contexts + ichild; +} + /* * MemoryContextStatsPrint * Print callback used by MemoryContextStatsInternal diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 83f6501df38..de266a15570 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8633,6 +8633,16 @@ prorettype => 'bool', proargtypes => 'int4', prosrc => 'pg_log_backend_memory_contexts' }, +# publishing memory contexts of the specified postgres process +{ oid => '2173', descr => 'publish memory contexts of the specified backend', + proname => 'pg_get_process_memory_contexts', provolatile => 'v', + prorows => '100', proretset => 't', proparallel => 'r', + prorettype => 'record', proargtypes => 'int4 bool', + proallargtypes => '{int4,bool,text,text,text,int4,_int4,int8,int8,int8,int8,int8,int4}', + proargmodes => '{i,i,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{pid, summary, name, ident, type, level, path, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes, num_agg_contexts}', + prosrc => 'pg_get_process_memory_contexts' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index db559b39c4d..7e301affccc 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -96,6 +96,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t PublishMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h index e94ebce95b9..6dd80689bbb 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -87,6 +87,7 @@ PG_LWLOCK(52, SerialControl) PG_LWLOCK(53, AioWorkerSubmissionQueue) PG_LWLOCK(54, WaitLSN) PG_LWLOCK(55, LogicalDecodingControl) +PG_LWLOCK(56, MemContextReportingClientKeys) /* * There also exist several built-in LWLock tranches. As with the predefined diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index e52b8eb7697..4f1bcbf709f 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -35,6 +35,7 @@ typedef enum PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to send the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ /* Recovery conflict reasons */ diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 11ab1717a16..6e8920bf28d 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -19,7 +19,6 @@ #include "nodes/memnodes.h" - /* * MaxAllocSize, MaxAllocHugeSize * Quasi-arbitrary limits on size of allocations. @@ -319,4 +318,11 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) return true; } +extern void ProcessGetMemoryContextInterrupt(void); +extern void HandleGetMemoryContextInterrupt(void); +extern void MemoryContextKeysShmemInit(void); +extern Size MemoryContextKeysShmemSize(void); +extern void MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts); +extern void AtProcExit_memstats_cleanup(int code, Datum arg); #endif /* MEMUTILS_H */ diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 3dd63fd88ed..c84e5adce16 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -233,3 +233,22 @@ select * from pg_timezone_abbrevs where abbrev = 'LMT'; LMT | @ 7 hours 52 mins 58 secs ago | f (1 row) +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; +NOTICE: (AllocSet,TopMemoryContext,) +NOTICE: (AllocSet,TopMemoryContext,) diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 004f9a70e00..d34d2b6211c 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -101,3 +101,21 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs; -- One specific case we can check without much fear of breakage -- is the historical local-mean-time value used for America/Los_Angeles. select * from pg_timezone_abbrevs where abbrev = 'LMT'; + +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 7619845fba9..ee54eec00a5 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1707,6 +1707,8 @@ MemoryContextData MemoryContextId MemoryContextMethodID MemoryContextMethods +MemoryStatsEntry +MemoryStatsDSHashEntry MemoryStatsPrintFunc MergeAction MergeActionState -- 2.34.1 [application/octet-stream] v50-0002-Test-module-to-test-memory-context-reporting-wit.patch (9.0K, ../../CAH2L28vjHHdpoDKfcLT7nt765k_1X=R4qK7etQyf_DDUg-kNoQ@mail.gmail.com/4-v50-0002-Test-module-to-test-memory-context-reporting-wit.patch) download | inline diff: From d694e726a06fcd4e934ad1dc3bb289f0d16fd904 Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Fri, 28 Nov 2025 14:46:38 +0530 Subject: [PATCH 2/2] Test module to test memory context reporting with injection points --- src/test/modules/Makefile | 1 + .../test_memcontext_reporting/Makefile | 29 ++++ .../t/001_memcontext_inj.pl | 154 ++++++++++++++++++ .../test_memcontext_reporting.c | 12 ++ .../test_memcontext_reporting.control | 4 + 5 files changed, 200 insertions(+) create mode 100644 src/test/modules/test_memcontext_reporting/Makefile create mode 100644 src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index 44c7163c1cd..8ce9598495c 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -34,6 +34,7 @@ SUBDIRS = \ test_json_parser \ test_lfind \ test_lwlock_tranches \ + test_memcontext_reporting \ test_misc \ test_oat_hooks \ test_parser \ diff --git a/src/test/modules/test_memcontext_reporting/Makefile b/src/test/modules/test_memcontext_reporting/Makefile new file mode 100644 index 00000000000..0a2dfc44f1c --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/Makefile @@ -0,0 +1,29 @@ +# src/test/modules/test_memcontext_reporting/Makefile + +EXTRA_INSTALL = src/test/modules/injection_points + +export enable_injection_points +MODULE_big = test_memcontext_reporting +OBJS = \ + $(WIN32RES) \ + test_memcontext_reporting.o +PGFILEDESC = "test_memcontext_reporting - test code for memory context reporting" + +REGRESS = test_memcontext_reporting + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_memcontext_reporting +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif + +check: + $(prove_check) + +installcheck: + $(prove_installcheck) diff --git a/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl new file mode 100644 index 00000000000..73665abf902 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl @@ -0,0 +1,154 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test suite for testing memory context statistics reporting + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} +my $psql_err; +my $psql_out; +# Create and start a cluster with one node +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->append_conf( + 'postgresql.conf', + qq[ +max_connections = 100 +log_statement = none +restart_after_crash = false +]); +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); + +# Attaching to a client process's injection point that throws an error +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-client-injection', 'error');" +); + +my $pid = $node->safe_psql('postgres', + "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); + +#Client should have thrown error +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); +like($psql_err, + qr/error triggered for injection point memcontext-client-injection/); + +#Query the same process after detaching the injection point, using some other client and it should succeed. +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-client-injection');"); +my $topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name eq 'TopMemoryContext'); + +# Attaching to a target process injection point that throws an error +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-server-injection', 'error');" +); + +#Server should have thrown error +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); + +#Query the same process after detaching the injection point, using some other client and it should succeed. +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-injection');"); +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name eq 'TopMemoryContext'); + +# Test that two concurrent requests to the same process results in a warning for +# one of those + +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-client-injection', 'wait');"); +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-server-wait', 'wait');"); +my $psql_session1 = $node->background_psql('postgres'); + +# We use query_until so that we can execute additional commands without having to +# wait for this command to complete, since it will pause at injection points. + +$psql_session1->query_until( + qr//, + qq( + SELECT pg_get_process_memory_contexts($pid, true); +)); +$node->wait_for_event('client backend', 'memcontext-client-injection'); +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); +ok($psql_err =~ + /WARNING: server process $pid is processing previous request/); +#Wake the client up. +$node->safe_psql('postgres', + "SELECT injection_points_wakeup('memcontext-client-injection');"); + +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-client-injection');"); +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-wait');"); + +# Test the client process exiting with timeout does not break the server process + +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-server-wait', 'wait');"); +# Following client query times out, returning NULL as output +$node->psql( + 'postgres', + qq(select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';), + stdout => \$psql_out); +is($psql_out, '', 'Query returns null'); +#Wakeup the server process up and detach the injection point. +$node->safe_psql('postgres', + "SELECT injection_points_wakeup('memcontext-server-wait');"); +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-wait');"); +#Query the same server process again and it should succeed. +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name eq 'TopMemoryContext'); + +# Test if the monitoring works fine, when the client backend crashes. +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-client-injection', 'test_memcontext_reporting', 'crash', NULL);" +); + +#Client will crash +$node->psql( + 'postgres', + qq(select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';), + stderr => \$psql_err); +like($psql_err, + qr/WARNING: terminating connection because of crash of another server process|server closed the connection unexpectedly|connection to server was lost|could not send data to server/ +); + +# Wait till server restarts +$node->restart; +$node->poll_query_until('postgres', "SELECT 1;", '1'); + +#Querying memory stats should succeed after server start +$pid = $node->safe_psql('postgres', + "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name eq 'TopMemoryContext'); + +done_testing(); diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c new file mode 100644 index 00000000000..d641f3616dc --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c @@ -0,0 +1,12 @@ +#include "postgres.h" +#include "funcapi.h" + +PG_MODULE_MAGIC; + +extern PGDLLEXPORT void crash(const char *name, const void *private_data, void *arg); + +void +crash(const char *name, const void *private_data, void *arg) +{ + abort(); +} diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control new file mode 100644 index 00000000000..48b501682d5 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control @@ -0,0 +1,4 @@ +comment = 'Test code for memcontext reporting' +default_version = '1.0' +module_pathname = '$libdir/test_memcontext_reporting' +relocatable = true -- 2.34.1 ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2026-02-10 04:50 Chao Li <[email protected]> parent: Rahila Syed <[email protected]> 0 siblings, 2 replies; 52+ messages in thread From: Chao Li @ 2026-02-10 04:50 UTC (permalink / raw) To: Rahila Syed <[email protected]>; +Cc: Daniel Gustafsson <[email protected]>; torikoshia <[email protected]>; pgsql-hackers; Robert Haas <[email protected]> > On Feb 10, 2026, at 08:03, Rahila Syed <[email protected]> wrote: > > Hi, > > Please find attached rebased and updated patches that include a few fixes > suggested by Daniel. > > Fixes include > 1. Changed LW_SHARED to LW_EXCLUSIVE when resetting client_keys. > 2. Replaced calls to MemoryContextReset with MemoryContextDelete since a > new memory context is created each time. > 3. Removed leftover code from earlier versions in ipci.c > 4. Fix the TAP test to compare strings correctly. > 5. Added more comments in the TAP test. > > Thank you, > Rahila Syed > <v50-0001-Add-function-to-report-memory-context-statistics.patch><v50-0002-Test-module-to-test-memory-context-reporting-wit.patch> Hi Rahila, Thanks for the patch. I applied v50 locally and played with it a little bit then I reviewed v50. Basically I think it’s convenient to query a server process’ memory usage via a SQL statement. Here comes my review comments. 1 - 0001- mcxt.c ``` +/* + * MemoryContextStatsCounter + * + * Accumulate statistics counts into *totals. totals should not be NULL. + * This involves a non-recursive tree traversal. + */ +void +MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts) +{ + int ichild = 1; + + *num_contexts = 0; + context->methods->stats(context, NULL, NULL, totals, false); ``` As the header comment says that “totals should not be NULL”, maybe add Assert(total!=NULL) to ensure that. 2 - 0001- mcxt.c ``` + *num_contexts = 0; + context->methods->stats(context, NULL, NULL, totals, false); + + for (MemoryContext curr = context->firstchild; + curr != NULL; + curr = MemoryContextTraverseNext(curr, context)) + { + curr->methods->stats(curr, NULL, NULL, totals, false); + ichild++; + } + + /* + * Add the count of all the children contexts which are traversed + * including the parent. + */ + *num_contexts = *num_contexts + ichild; +} ``` *num_contexts is only initialized to 0, then *num_contexts = *num_contexts + ichild. Looks like the the initialization is unnecessary, and the assignment can just be *num_contexts = ichild. 3 - 0001 - mcxtfuncs.c ``` +typedef struct MemoryStatsEntry +{ + char name[MEMORY_CONTEXT_NAME_SHMEM_SIZE]; + char ident[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + int path[100]; ``` You already defined a constant MAX_PATH_DISPLAY_LENGTH below, would it make sense to pull up the macro definition and use it for “path” definition? 4 - 0001 - mctxfuncs.c ``` +/* + * Per backend dynamic shared hash entry for memory context statistics + * reporting. + */ +typedef struct MemoryStatsDSHashEntry +{ + char key[64]; + ConditionVariable memcxt_cv; + bool stats_written; + int target_server_id; + int total_stats; + bool summary; + dsa_pointer memstats_dsa_pointer; +} MemoryStatsDSHashEntry; + +static const dshash_parameters memctx_dsh_params = { + offsetof(MemoryStatsDSHashEntry, memcxt_cv), + sizeof(MemoryStatsDSHashEntry), + dshash_strcmp, + dshash_strhash, + dshash_strcpy +}; ``` I wonder why we cannot just use the integer ProcNumber as the hash key? I think dshash fully supports fixed-size binary keys. We can use dshash_memcmp and dshash_memhash for compare and hash functions. 5 - 0001 - mctxfuncs.c ``` +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + MemoryContext cur_context; + + for (cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + { + elog(NOTICE, "hash table corrupted, can't construct path value"); + return NIL; + } + path = lcons_int(cur_entry->context_id, path); + } + return path; +} ``` As in the hash entry, path is a 100-elements array, would it make sense to also limit the path list to not append more than 100 items in this function? 6 - 0001 - mctxfuncs.c ``` + entry->stats_written = true; + dshash_release_lock(MemoryStatsDsHash, entry); + hash_destroy(context_id_lookup); + + MemoryContextSwitchTo(oldcontext); + MemoryContextDelete(memstats_ctx); + /* Notify waiting client backend and return */ + ConditionVariableSignal(&entry->memcxt_cv); +} ``` This looks like a race condition. ConditionVariableSignal(&entry->memcxt_cv); is called after the lock is released. So, there is a chance that a process has been terminated, and its before_shmem_exit callback acquired the entry lock and deleted the entry from the hash. So, I think we should send the signal before releasing the lock. 7 - 0001 - mctxfuncs.c ``` + /* + * Wait for MEMORY_STATS_MAX_TIMEOUT. If no statistics are available + * within the allowed time then return NULL. The timer is defined in + * milliseconds since that's what the condition variable sleep uses. + */ + if (ConditionVariableTimedSleep(&entry->memcxt_cv, + (MEMORY_STATS_MAX_TIMEOUT * 1000), + WAIT_EVENT_MEM_CXT_PUBLISH)) ``` For the wait loop, if the condition wakes up for some other reason, it will loop back and wait for the other 5 seconds, then total wait period will exceed 5 seconds, maybe 9 seconds. This is not a big deal, but the doc explicitly says “If the process does not respond with memory contexts statistics in 5 seconds”, so that behavior might be inconsistent with the doc. I think we can calculate remaining time and pass remaining milliseconds into ConditionVariableTimedSleep. 8 - 0001 - mctxfuncs.c ``` + /* + * Wait for MEMORY_STATS_MAX_TIMEOUT. If no statistics are available + * within the allowed time then return NULL. The timer is defined in + * milliseconds since that's what the condition variable sleep uses. + */ + if (ConditionVariableTimedSleep(&entry->memcxt_cv, + (MEMORY_STATS_MAX_TIMEOUT * 1000), + WAIT_EVENT_MEM_CXT_PUBLISH)) + { + /* Timeout has expired, return NULL */ + memstats_dsa_cleanup(key); + memstats_client_key_reset(procNumber); + ConditionVariableCancelSleep(); + ereport(NOTICE, + errmsg("request for memory context statistics for PID %d timed out", + pid)); + PG_RETURN_NULL(); + } + entry = dshash_find_or_insert(MemoryStatsDsHash, key, &found); + Assert(found); ``` After ConditionVariableTimedSleep, rather than Assert(found), I think we should check if (!found). Because it waits for 5 seconds without holding the lock, the process may terminate and delete the entry during the period. 9 - 0001 - proc.c ``` diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c index 8560a903bc8..f68583aa820 100644 --- a/src/backend/storage/lmgr/proc.c +++ b/src/backend/storage/lmgr/proc.c @@ -51,6 +51,7 @@ #include "storage/procsignal.h" #include "storage/spin.h" #include "storage/standby.h" +#include "utils/memutils.h" #include "utils/timeout.h" #include "utils/timestamp.h" ``` This file is sololy added an include without any other change, that seems unneeded. I tried to remove this include, and the build still passed. Best regards, -- Chao Li (Evan) HighGo Software Co., Ltd. https://www.highgo.com/ ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2026-02-15 12:40 Rahila Syed <[email protected]> parent: Chao Li <[email protected]> 1 sibling, 0 replies; 52+ messages in thread From: Rahila Syed @ 2026-02-15 12:40 UTC (permalink / raw) To: Chao Li <[email protected]>; +Cc: Daniel Gustafsson <[email protected]>; torikoshia <[email protected]>; pgsql-hackers; Robert Haas <[email protected]> Hi Chao, Thank you for testing and reviewing. Please find attached the updated patches that incorporate your review comments. > > As the header comment says that “totals should not be NULL”, maybe add > Assert(total!=NULL) to ensure that. > > Added this. > + /* > + * Add the count of all the children contexts which are traversed > + * including the parent. > + */ > + *num_contexts = *num_contexts + ichild; > +} > ``` > > *num_contexts is only initialized to 0, then *num_contexts = *num_contexts > + ichild. Looks like the the initialization is unnecessary, and the > assignment can just be *num_contexts = ichild. > Fixed accordingly. > > + int path[100]; > ``` > > You already defined a constant MAX_PATH_DISPLAY_LENGTH below, would it > make sense to pull up the macro definition and use it for “path” definition? > > Makes sense, done in the attached patch. > + > +static const dshash_parameters memctx_dsh_params = { > + offsetof(MemoryStatsDSHashEntry, memcxt_cv), > + sizeof(MemoryStatsDSHashEntry), > + dshash_strcmp, > + dshash_strhash, > + dshash_strcpy > +}; > ``` > > I wonder why we cannot just use the integer ProcNumber as the hash key? I > think dshash fully supports fixed-size binary keys. We can use > dshash_memcmp and dshash_memhash for compare and hash functions. > I have incorporated your suggestion in the updated patch—using integer keys will be more efficient for memory usage and will help avoid conversions between data types. Thank you for the suggestion. +static List * > +compute_context_path(MemoryContext c, HTAB *context_id_lookup) > +{ > As in the hash entry, path is a 100-elements array, would it make sense to > also limit the path list to not append more than 100 items in this function? > We need to calculate the full path list to report the number of levels in the tree. We could choose to return the number of levels separately from the path list and limit the path list to no more than 100 levels, but we would still have to iterate through all the levels. Returning a single variable that captures both levels and path keeps the function signature simple. Kindly let me know your view. > + dshash_release_lock(MemoryStatsDsHash, entry); > + hash_destroy(context_id_lookup); > + > + MemoryContextSwitchTo(oldcontext); > + MemoryContextDelete(memstats_ctx); > + /* Notify waiting client backend and return */ > + ConditionVariableSignal(&entry->memcxt_cv); > +} > ``` > > This looks like a race condition. > ConditionVariableSignal(&entry->memcxt_cv); is called after the lock is > released. So, there is a chance that a process has been terminated, and its > before_shmem_exit callback acquired the entry lock and deleted the entry > from the hash. So, I think we should send the signal before releasing the > lock. > > Good catch. Fixed accordingly. > > + */ > + if (ConditionVariableTimedSleep(&entry->memcxt_cv, > + > (MEMORY_STATS_MAX_TIMEOUT * 1000), > + > WAIT_EVENT_MEM_CXT_PUBLISH)) > ``` > > For the wait loop, if the condition wakes up for some other reason, it > will loop back and wait for the other 5 seconds, then total wait period > will exceed 5 seconds, maybe 9 seconds. This is not a big deal, but the doc > explicitly says “If the process does not respond with memory contexts > statistics in 5 seconds”, so that behavior might be inconsistent with the > doc. > > I think we can calculate remaining time and pass remaining milliseconds > into ConditionVariableTimedSleep. > > The following check in the beginning of the wait loop is intended to avoid this. /* Return if we have already exceeded the timeout */ if (elapsed_time >= MEMORY_STATS_MAX_TIMEOUT * 1000) { memstats_dsa_cleanup(client_procno); memstats_client_key_reset(server_procno); ConditionVariableCancelSleep(); ereport(NOTICE, errmsg("request for memory context statistics for PID %d timed out", pid)); PG_RETURN_NULL(); } It may still exceed by an amount less than 5 seconds, but it will always be less than 2 * MEMORY_STATS_MAX_TIMEOUT, because we track the elapsed time and exit after the condition above is satisfied. Maybe we should edit the docs to reflect this. After ConditionVariableTimedSleep, rather than Assert(found), I think we > should check if (!found). Because it waits for 5 seconds without holding > the lock, the process may terminate and delete the entry during the period. > > The client backend deletes the entry only upon exiting; no other process can remove the entry. Therefore, it is not possible for the client backend to execute this Assert after the entry has been deleted. > > This file is sololy added an include without any other change, that seems > unneeded. I tried to remove this include, and the build still passed. > > Fixed. Thank you for noticing this. It looks like a remnant from earlier versions. Thank you, Rahila Syed Attachments: [application/octet-stream] v51-0001-Add-function-to-report-memory-context-statistics.patch (59.5K, ../../CAH2L28sC0Bo5nKzmzUaB4_bg1gVM=TSvD18MjkPQ2tkbjc0vjg@mail.gmail.com/3-v51-0001-Add-function-to-report-memory-context-statistics.patch) download | inline diff: From ee89c9f1283475aae4b403246aedf7abd182951f Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Thu, 27 Nov 2025 14:39:43 +0530 Subject: [PATCH 1/2] Add function to report memory context statistics This adds a function for retrieving memory context statistics and information from backends as well as auxiliary processes. The intended usecase is cluster debugging when under memory pressure or unanticipated memory usage characteristics. When calling the function it sends a signal to the specified process to submit statistics regarding its memory contexts into dynamic shared memory. Each memory context is returned in detail, followed by a cumulative total in case the number of contexts exceed the max allocated amount of shared memory. Each process is limited to use at most 1Mb memory for this. A summary can also be explicitly requested by the user, this will return the TopMemoryContext and a cumulative total of all lower contexts. In order to not block on busy processes, we have hardcoded the number of seconds during which to retry before timing out. In the case where no statistics are published within the set timeout, NULL is returned --- doc/src/sgml/func/func-admin.sgml | 159 +++ src/backend/catalog/system_functions.sql | 14 + src/backend/postmaster/autovacuum.c | 4 + src/backend/postmaster/checkpointer.c | 4 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 4 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 4 + src/backend/storage/ipc/dsm_registry.c | 8 +- src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 2 + src/backend/utils/adt/mcxtfuncs.c | 1013 ++++++++++++++++- src/backend/utils/init/globals.c | 1 + src/backend/utils/init/postinit.c | 7 + src/backend/utils/mmgr/mcxt.c | 31 + src/include/catalog/pg_proc.dat | 10 + src/include/miscadmin.h | 1 + src/include/storage/lwlocklist.h | 1 + src/include/storage/procsignal.h | 1 + src/include/utils/memutils.h | 8 +- src/test/regress/expected/sysviews.out | 19 + src/test/regress/sql/sysviews.sql | 18 + src/tools/pgindent/typedefs.list | 2 + 25 files changed, 1305 insertions(+), 23 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 3ac81905d1f..af87b5b19ab 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -251,6 +251,132 @@ <literal>false</literal> is returned. </para></entry> </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_get_process_memory_contexts</primary> + </indexterm> + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type> <optional>,<parameter>summary</parameter> <type>boolean</type> <literal>DEFAULT</literal> <literal>false</literal></optional> ) + <returnvalue>setof record</returnvalue> + ( <parameter>name</parameter> <type>text</type>, + <parameter>ident</parameter> <type>text</type>, + <parameter>type</parameter> <type>text</type>, + <parameter>level</parameter> <type>integer</type>, + <parameter>path</parameter> <type>integer[]</type>, + <parameter>total_bytes</parameter> <type>bigint</type>, + <parameter>total_nblocks</parameter> <type>bigint</type>, + <parameter>free_bytes</parameter> <type>bigint</type>, + <parameter>free_chunks</parameter> <type>bigint</type>, + <parameter>used_bytes</parameter> <type>bigint</type>, + <parameter>num_agg_contexts</parameter> <type>integer</type> ) + </para> + <para> + This function handles requests to display the memory contexts of a + <productname>PostgreSQL</productname> process with the specified + process ID. The function can be used to send requests to backends as + well as <glossterm linkend="glossary-auxiliary-proc">auxiliary processes</glossterm>. + If the process does not respond with memory contexts statistics in 5 seconds, + function returns NULL. + </para> + <para> + The returned record contains extended statistics per each memory + context: + <itemizedlist spacing="compact"> + <listitem> + <para> + <parameter>name</parameter> - The name of the memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>ident</parameter> - Memory context ID (if any). + </para> + </listitem> + <listitem> + <para> + <parameter>type</parameter> - The type of memory context, possible + values are: AllocSet, Generation, Slab and Bump. + </para> + </listitem> + <listitem> + <para> + <parameter>level</parameter> - The level in the tree of the current + memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>path</parameter> - Memory contexts are organized in a + tree model with TopMemoryContext as the root, and all other memory + contexts as nodes in the tree. The <parameter>path</parameter> + displays the path from the root to the current memory context. The + path is limited to 100 children per node, which each node limited + to a max depth of 100, to preserve memory during reporting. The + printed path will also be limited to 100 nodes counting from the + TopMemoryContext. + </para> + </listitem> + <listitem> + <para> + <parameter>total_bytes</parameter> - The total number of bytes + allocated to this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_nblocks</parameter> - The total number of blocks + used for the allocated memory. + </para> + </listitem> + <listitem> + <para> + <parameter>free_bytes</parameter> - The amount of free memory in + this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>free_chunks</parameter> - The number of chunks that + <parameter>free_bytes</parameter> corresponds to. + </para> + </listitem> + <listitem> + <para> + <parameter>used_bytes</parameter> - The total number of bytes + currently occupied. + </para> + </listitem> + <listitem> + <para> + <parameter>num_agg_contexts</parameter> - The number of memory + contexts aggregated in the displayed statistics. + </para> + </listitem> + </itemizedlist> + </para> + <para> + When <parameter>summary</parameter> is <literal>true</literal>, statistics + for memory contexts at levels 1 and 2 are displayed, with level 1 + representing the root node (i.e., <literal>TopMemoryContext</literal>). + Statistics for contexts on level 2 and below are aggregates of all + child contexts' statistics, where <literal>num_agg_contexts</literal> + indicate the number aggregated child contexts. When + <parameter>summary</parameter> is <literal>false</literal> (the default), + <literal>the num_agg_contexts</literal> value is <literal>1</literal>, + indicating that individual statistics are being displayed. + </para> + <para> + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context + statistics are aggregated and a cumulative total is displayed. The + <literal>num_agg_contexts</literal> column indicates the number of + contexts aggregated in the displayed statistics. When + <literal>num_agg_contexts</literal> is <literal>1</literal> it means + that the context statistics are displayed separately. + </para></entry> + </row> </tbody> </tgroup> </table> @@ -302,6 +428,39 @@ LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 because it may generate a large number of log messages. </para> + <para> + <function>pg_get_process_memory_contexts</function> can be used to request + memory contexts statistics of any <productname>PostgreSQL</productname> + process. For example: +<programlisting> +postgres=# SELECT * FROM pg_get_process_memory_contexts( + (SELECT pid FROM pg_stat_activity + WHERE backend_type = 'checkpointer'), + false) LIMIT 1; +-[ RECORD 1 ]----+------------------------------ +name | TopMemoryContext +ident | +type | AllocSet +level | 1 +path | {1} +total_bytes | 90304 +total_nblocks | 3 +free_bytes | 2880 +free_chunks | 1 +used_bytes | 87424 +num_agg_contexts | 1 +</programlisting> + <note> + <para> + While <function>pg_get_process_memory_contexts</function> can be used to + query memory contexts of the local backend, + <structname>pg_backend_memory_contexts</structname> + (see <xref linkend="view-pg-backend-memory-contexts"/> for more details) + will be less resource intensive when only the local backend is of interest. + </para> + </note> + </para> + </sect2> <sect2 id="functions-admin-backup"> diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql index eb9e31ae1bf..d1416e5534d 100644 --- a/src/backend/catalog/system_functions.sql +++ b/src/backend/catalog/system_functions.sql @@ -657,6 +657,17 @@ LANGUAGE INTERNAL STRICT VOLATILE PARALLEL UNSAFE AS 'pg_replication_origin_session_setup'; +CREATE OR REPLACE FUNCTION + pg_get_process_memory_contexts(IN pid integer, IN summary boolean DEFAULT false, + OUT name text, OUT ident text, OUT type text, OUT level integer, + OUT path integer[], OUT total_bytes bigint, OUT total_nblocks bigint, + OUT free_bytes bigint, OUT free_chunks bigint, OUT used_bytes bigint, + OUT num_agg_contexts integer) +RETURNS SETOF RECORD +LANGUAGE INTERNAL +STRICT VOLATILE PARALLEL UNSAFE +AS 'pg_get_process_memory_contexts'; + -- -- The default permissions for functions mean that anyone can execute them. -- A number of functions shouldn't be executable by just anyone, but rather @@ -782,6 +793,7 @@ REVOKE EXECUTE ON FUNCTION pg_ls_logicalmapdir() FROM PUBLIC; REVOKE EXECUTE ON FUNCTION pg_ls_replslotdir(text) FROM PUBLIC; +REVOKE EXECUTE ON FUNCTION pg_get_process_memory_contexts(integer, boolean) FROM PUBLIC; -- -- We also set up some things as accessible to standard roles. -- @@ -808,6 +820,8 @@ GRANT EXECUTE ON FUNCTION pg_current_logfile() TO pg_monitor; GRANT EXECUTE ON FUNCTION pg_current_logfile(text) TO pg_monitor; +GRANT EXECUTE ON FUNCTION pg_get_process_memory_contexts(integer, boolean) TO pg_read_all_stats; + GRANT pg_read_all_settings TO pg_monitor; GRANT pg_read_all_stats TO pg_monitor; diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 6fde740465f..3c24119c43a 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -787,6 +787,10 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index e03c19123bc..d54193f8890 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -684,6 +684,10 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index a2c0ff012c5..cb18ce80893 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -48,6 +48,10 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index 82731e452fc..e13a40f1427 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -870,6 +870,10 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index cdbe53dd262..1d6d8f22d20 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -192,6 +192,10 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index 2d8f57099fd..72aaf8ae5cc 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -878,6 +878,10 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c index 068c1577b12..4810babac1b 100644 --- a/src/backend/storage/ipc/dsm_registry.c +++ b/src/backend/storage/ipc/dsm_registry.c @@ -270,7 +270,9 @@ GetNamedDSMSegment(const char *name, size_t size, * This routine returns a pointer to the DSA. A new LWLock tranche ID will be * generated if needed. Note that the lock tranche will be registered with the * provided name. Also note that this should be called at most once for a - * given DSA in each backend. + * given DSA in each backend. It's best to minimize the number of errors thrown + * in this function since it's called from a CFI function. An error here could + * lead to error in any transaction that calls the CFI function. */ dsa_area * GetNamedDSA(const char *name, bool *found) @@ -351,7 +353,9 @@ GetNamedDSA(const char *name, bool *found) * params is ignored; a new LWLock tranche ID will be generated if needed. * Note that the lock tranche will be registered with the provided name. Also * note that this should be called at most once for a given table in each - * backend. + * backend. It's best to minimize the number of errors thrown in this function + * since it's called from a CFI function. An error here could lead to error in + * any transaction that calls the CFI function. */ dshash_table * GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found) diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index 1f7e933d500..b0176613987 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -52,6 +52,7 @@ #include "storage/sinvaladt.h" #include "utils/guc.h" #include "utils/injection_point.h" +#include "utils/memutils.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -141,6 +142,7 @@ CalculateShmemSize(void) size = add_size(size, AioShmemSize()); size = add_size(size, WaitLSNShmemSize()); size = add_size(size, LogicalDecodingCtlShmemSize()); + size = add_size(size, MemoryContextKeysShmemSize()); /* include additional requested shmem from preload libraries */ size = add_size(size, total_addin_request); @@ -328,6 +330,7 @@ CreateOrAttachShmemStructs(void) AioShmemInit(); WaitLSNShmemInit(); LogicalDecodingCtlShmemInit(); + MemoryContextKeysShmemInit(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index 5d33559926a..c688f3d36aa 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -694,6 +694,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_GET_MEMORY_CONTEXT)) + HandleGetMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index 21de158adbb..c4b009fa532 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3573,6 +3573,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 4aa864fe3c3..3e1dc4225b2 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -163,6 +163,7 @@ WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." +MEM_CXT_PUBLISH "Waiting for a process to publish memory information." ABI_compatibility: @@ -366,6 +367,7 @@ SerialControl "Waiting to read or update shared <filename>pg_serial</filename> s AioWorkerSubmissionQueue "Waiting to access AIO worker submission queue." WaitLSN "Waiting to read or update shared Wait-for-LSN state." LogicalDecodingControl "Waiting to read or update logical decoding status information." +MemContextReportingClientKeys "Waiting for another process to complete reading or writing the memory reporting keys." # # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE) diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index c7f7b8bc2dd..72f25dad3f3 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -15,25 +15,129 @@ #include "postgres.h" +#include "access/xact.h" #include "funcapi.h" #include "mb/pg_wchar.h" +#include "miscadmin.h" +#include "storage/dsm_registry.h" #include "storage/proc.h" #include "storage/procarray.h" #include "storage/procsignal.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/injection_point.h" +#include "utils/memutils.h" +#include "utils/wait_event_types.h" + +/* + * Memory Context reporting size limits. + */ + +/* Max length of context name and ident, to keep it consistent + * with ProcessLogMemoryContext() + */ +#define MEMORY_CONTEXT_IDENT_SHMEM_SIZE 100 +#define MEMORY_CONTEXT_NAME_SHMEM_SIZE 100 +#define MAX_PATH_DISPLAY_LENGTH 100 + +/* Maximum size (in bytes) of DSA area per process */ +#define MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND ((size_t) (1 * 1024 * 1024)) + +/* + * Maximum number of memory context statistics is calculated by dividing + * max memory allocated per backend with maximum size per context statistics. + * The identifier and name are statically allocated arrays of size 100 bytes. + * The path depth is limited to 100 like for memory context logging. + */ +#define MAX_MEMORY_CONTEXT_STATS_NUM MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND / (sizeof(MemoryStatsEntry)) + +/* + * Size of dshash key. The key is a uint32 rendered as a string, 10 chars + * plus space for a NULL terminator can hold all the values. + */ +#define CLIENT_KEY_SIZE (10 + 1) + +/* Dynamic shared memory state for reporting statistics per context */ +typedef struct MemoryStatsEntry +{ + char name[MEMORY_CONTEXT_NAME_SHMEM_SIZE]; + char ident[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + int path[MAX_PATH_DISPLAY_LENGTH]; + NodeTag type; + int path_length; + int levels; + int64 totalspace; + int64 nblocks; + int64 freespace; + int64 freechunks; + int num_agg_stats; +} MemoryStatsEntry; + +/* + * Per backend dynamic shared hash entry for memory context statistics + * reporting. + */ +typedef struct MemoryStatsDSHashEntry +{ + ProcNumber client_key; + ConditionVariable memcxt_cv; + bool stats_written; + int target_server_id; + int total_stats; + bool summary; + dsa_pointer memstats_dsa_pointer; +} MemoryStatsDSHashEntry; + +static const dshash_parameters memctx_dsh_params = { + offsetof(MemoryStatsDSHashEntry, memcxt_cv), + sizeof(MemoryStatsDSHashEntry), + dshash_memcmp, + dshash_memhash, + dshash_memcpy +}; + +/* + * These are used for reporting memory context statistics of a process. + */ + +/* Array to store the keys of MemoryStatsDsHash */ +static int *client_keys = NULL; + +/* + * Table to store pointers to DSA memory containing memory statistics and other + * metadata. There is one entry per client backend request, keyed by ProcNumber + * of the client obtained from client_keys array above. + */ +static dshash_table *MemoryStatsDsHash = NULL; + +/* + * Dsa area which stores the actual memory context + * statistics. + */ +static dsa_area *MemoryStatsDsaArea = NULL; + +static void memstats_dsa_cleanup(int ProcNumber); +static void memstats_client_key_reset(int ProcNumber); +static void PublishMemoryContext(MemoryStatsEntry *memcxt_info, + int curr_id, MemoryContext context, + List *path, + MemoryContextCounters stat, + int num_contexts); /* ---------- * The max bytes for showing identifiers of MemoryContext. + * This is used by pg_get_backend_memory_context - view used for local backend. * ---------- */ #define MEMORY_CONTEXT_IDENT_DISPLAY_SIZE 1024 +/* Timeout in seconds */ +#define MEMORY_STATS_MAX_TIMEOUT 5 + /* * MemoryContextId - * Used for storage of transient identifiers for - * pg_get_backend_memory_contexts. + * Used for storage of transient identifiers for memory context reporting */ typedef struct MemoryContextId { @@ -63,6 +167,67 @@ int_list_to_array(const List *list) return PointerGetDatum(result_array); } +/* + * context_type_to_string + * Returns a textual representation of a context type + * + * This should cover the same types as MemoryContextIsValid. + */ +static const char * +context_type_to_string(NodeTag type) +{ + const char *context_type; + + switch (type) + { + case T_AllocSetContext: + context_type = "AllocSet"; + break; + case T_GenerationContext: + context_type = "Generation"; + break; + case T_SlabContext: + context_type = "Slab"; + break; + case T_BumpContext: + context_type = "Bump"; + break; + default: + context_type = "???"; + break; + } + return context_type; +} + +/* + * compute_context_path + * + * Append the transient context_id of this context and each of its ancestors + * to a list, in order to compute a path. + */ +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + MemoryContext cur_context; + + for (cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + { + elog(NOTICE, "hash table corrupted, can't construct path value"); + return NIL; + } + path = lcons_int(cur_entry->context_id, path); + } + return path; +} + /* * PutMemoryContextsStatsTupleStore * Add details for the given MemoryContext to 'tupstore'. @@ -144,24 +309,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, else nulls[1] = true; - switch (context->type) - { - case T_AllocSetContext: - type = "AllocSet"; - break; - case T_GenerationContext: - type = "Generation"; - break; - case T_SlabContext: - type = "Slab"; - break; - case T_BumpContext: - type = "Bump"; - break; - default: - type = "???"; - break; - } + type = context_type_to_string(context->type); values[2] = CStringGetTextDatum(type); values[3] = Int32GetDatum(list_length(path)); /* level */ @@ -306,3 +454,828 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } + +/* + * pg_get_process_memory_contexts + * Signal a backend or an auxiliary process to send its memory contexts, + * wait for the results and display them. + * + * By default, only superusers or users with ROLE_PG_READ_ALL_STATS are allowed + * to signal a process to return the memory contexts. Additional roles can be + * permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to copy the memory context details + * to a dynamic shared memory space. + * + * We have defined a limit on DSA memory that could be allocated per process - + * if the process has more memory contexts than what can fit in the allocated + * size, the excess contexts are summarized and represented as cumulative total + * at the end of the buffer. + * + * After sending the signal, wait on a condition variable. The publishing + * backend, after copying the data to shared memory, sends a signal on that + * condition variable. There is one condition variable per client process. + * Once the condition variable is signalled, check if the latest memory context + * information is available and display. + * + * If the publishing backend does not respond before the condition variable + * times out, which is set to a predefined value MEMORY_STATS_MAX_TIMEOUT, give + * up and return NULL. + */ +Datum +pg_get_process_memory_contexts(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + bool summary = PG_GETARG_BOOL(1); + PGPROC *proc; + ProcNumber client_procno = MyProcNumber; + ProcNumber server_procno; + bool proc_is_aux = false; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + MemoryStatsEntry *memcxt_info; + MemoryStatsDSHashEntry *entry; + bool found; + TimestampTz start_timestamp; + + /* + * See if the process with given pid is a backend or an auxiliary process + * and remember the type for when we requery the process later. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + { + proc = AuxiliaryPidGetProc(pid); + proc_is_aux = true; + } + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; this is however not a problem and leave with a WARNING. + * See comment in pg_log_backend_memory_contexts for a discussion on this. + */ + if (proc == NULL) + { + ereport(WARNING, + errmsg("PID %d is not a PostgreSQL server process", pid)); + PG_RETURN_NULL(); + } + + server_procno = GetNumberFromPGProc(proc); + + /* + * Check if the server process slot is not empty and exit early Non-empty + * slot means some other client backend is requesting the statistics from + * the same server process. + */ + LWLockAcquire(MemContextReportingClientKeysLock, LW_EXCLUSIVE); + if (client_keys[server_procno] != -1) + { + LWLockRelease(MemContextReportingClientKeysLock); + ereport(WARNING, + errmsg("server process %d is processing previous request", + pid)); + PG_RETURN_NULL(); + } + LWLockRelease(MemContextReportingClientKeysLock); + + /* + * Create a DSA to allocate memory for copying memory contexts statistics. + * Allocate the memory in the DSA and send DSA pointer to the server + * process for storing the context statistics. If number of contexts + * exceed a predefined limit (1MB), a cumulative total is stored for such + * contexts. + * + * The DSA is created once for the lifetime of the server, and only + * attached in subsequent calls. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); + + /* + * The DSA pointers containing statistics for each client are stored in a + * dshash table. In addition to DSA pointer, each entry in this table also + * contains information about the statistics, condition variable for + * signalling between client and the server and miscellaneous data + * specific to a request. There is one entry per client request in the + * hash table. + */ + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); + + /* + * Insert an entry for this client in DSHASH table the first time this + * function is called. This entry is deleted when the process exits in + * before_shmem_exit call. + * + * dshash_find_or_insert locks the entry to prevent the publisher from + * reading before client has updated the entry. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, &client_procno, &found); + if (!found) + { + entry->stats_written = false; + ConditionVariableInit(&entry->memcxt_cv); + } + + /* + * Allocate 1MB of memory for the backend to publish its statistics on + * every call to this function. The memory is freed at the end of the + * function. + */ + entry->memstats_dsa_pointer = + dsa_allocate0(MemoryStatsDsaArea, MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND); + + /* + * Specify whether a summary of statistics is requested, before signalling + * the server. + */ + entry->summary = summary; + + /* + * Indicate which server process statistics are being requested from. If + * this client times out before the last requested process can publish its + * statistics, it may send a new request to another server process. Since + * the previous server was notified, it might attempt to read the same + * client entry and respond incorrectly with its statistics. By storing + * the server ID in the client entry, we prevent any previously signalled + * server process from writing its statistics in the space meant for the + * newly requested process. + */ + entry->target_server_id = pid; + dshash_release_lock(MemoryStatsDsHash, entry); + + /* + * Check if the publishing process slot is empty and store this clients + * key i.e its procNumber. This informs the publishing process that it is + * supposed to write statistics in the hash entry corresponding to this + * client. + */ + LWLockAcquire(MemContextReportingClientKeysLock, LW_EXCLUSIVE); + if (client_keys[server_procno] == -1) + client_keys[server_procno] = MyProcNumber; + else + { + LWLockRelease(MemContextReportingClientKeysLock); + ereport(WARNING, + errmsg("server process %d is processing previous request", + pid)); + PG_RETURN_NULL(); + } + LWLockRelease(MemContextReportingClientKeysLock); + + /* + * Send a signal to a PostgreSQL process, informing it we want it to + * produce information about its memory contexts. + */ + if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, server_procno) < 0) + { + memstats_dsa_cleanup(client_procno); + memstats_client_key_reset(server_procno); + ereport(WARNING, + errmsg("could not send signal to process %d: %m", + pid)); + PG_RETURN_NULL(); + } + start_timestamp = GetCurrentTimestamp(); + + while (1) + { + long elapsed_time; + + INJECTION_POINT("memcontext-client-injection", NULL); + + elapsed_time = TimestampDifferenceMilliseconds(start_timestamp, + GetCurrentTimestamp()); + /* Return if we have already exceeded the timeout */ + if (elapsed_time >= MEMORY_STATS_MAX_TIMEOUT * 1000) + { + memstats_dsa_cleanup(client_procno); + memstats_client_key_reset(server_procno); + ConditionVariableCancelSleep(); + ereport(NOTICE, + errmsg("request for memory context statistics for PID %d timed out", + pid)); + PG_RETURN_NULL(); + } + + /* + * Recheck the state of the backend before sleeping on the condition + * variable to ensure the process is still alive. Only check the + * relevant process type based on the earlier PID check. + */ + if (proc_is_aux) + proc = AuxiliaryPidGetProc(pid); + else + proc = BackendPidGetProc(pid); + + /* + * The target server process ending during memory context processing + * is not an error. + */ + if (proc == NULL) + { + memstats_dsa_cleanup(client_procno); + memstats_client_key_reset(server_procno); + ConditionVariableCancelSleep(); + ereport(WARNING, + errmsg("PID %d is no longer a PostgreSQL server process", + pid)); + PG_RETURN_NULL(); + } + + /* + * Wait for MEMORY_STATS_MAX_TIMEOUT. If no statistics are available + * within the allowed time then return NULL. The timer is defined in + * milliseconds since that's what the condition variable sleep uses. + */ + if (ConditionVariableTimedSleep(&entry->memcxt_cv, + (MEMORY_STATS_MAX_TIMEOUT * 1000), + WAIT_EVENT_MEM_CXT_PUBLISH)) + { + /* Timeout has expired, return NULL */ + memstats_dsa_cleanup(client_procno); + memstats_client_key_reset(server_procno); + ConditionVariableCancelSleep(); + ereport(NOTICE, + errmsg("request for memory context statistics for PID %d timed out", + pid)); + PG_RETURN_NULL(); + } + entry = dshash_find_or_insert(MemoryStatsDsHash, &client_procno, &found); + Assert(found); + + memcxt_info = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + /* + * We expect to come out of sleep when the requested process has + * finished publishing the statistics, verified using a boolean + * stats_written. + * + * Make sure that the statistics are actually written by checking that + * the name of the context is not NULL. This is done to ensure that + * the subsequent waits for statistics do not return spuriously if the + * previous call to the function ended in error and thus could not + * clear the stats_written flag. + */ + if (entry->stats_written && memcxt_info[0].name[0] != '\0') + break; + + dshash_release_lock(MemoryStatsDsHash, entry); + + } + + InitMaterializedSRF(fcinfo, 0); + + /* + * Backend has finished publishing the stats, project them. + */ +#define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 11 + for (int i = 0; i < entry->total_stats; i++) + { + ArrayType *path_array; + Datum values[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + bool nulls[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + Datum *path_datum = NULL; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + Assert(memcxt_info[i].name[0] != '\0'); + values[0] = CStringGetTextDatum(memcxt_info[i].name); + + if (memcxt_info[i].ident[0] != '\0') + values[1] = CStringGetTextDatum(memcxt_info[i].ident); + else + nulls[1] = true; + + values[2] = CStringGetTextDatum(context_type_to_string(memcxt_info[i].type)); + + if (memcxt_info[i].levels != 0) + values[3] = Int32GetDatum(memcxt_info[i].levels); + else + nulls[3] = true; + + if (memcxt_info[i].path[0] != 0) + { + int path_length; + + path_length = memcxt_info[i].path_length; + path_datum = (Datum *) palloc(path_length * sizeof(Datum)); + + for (int j = 0; j < path_length; j++) + path_datum[j] = Int32GetDatum(memcxt_info[i].path[j]); + path_array = construct_array_builtin(path_datum, + path_length, + INT4OID); + values[4] = PointerGetDatum(path_array); + } + else + nulls[4] = true; + + values[5] = Int64GetDatum(memcxt_info[i].totalspace); + values[6] = Int64GetDatum(memcxt_info[i].nblocks); + values[7] = Int64GetDatum(memcxt_info[i].freespace); + values[8] = Int64GetDatum(memcxt_info[i].freechunks); + values[9] = Int64GetDatum(memcxt_info[i].totalspace - + memcxt_info[i].freespace); + values[10] = Int32GetDatum(memcxt_info[i].num_agg_stats); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + dshash_release_lock(MemoryStatsDsHash, entry); + memstats_dsa_cleanup(client_procno); + + ConditionVariableCancelSleep(); + + PG_RETURN_NULL(); +} + +/* + * This function is executed before returning from the + * pg_get_process_memory_contexts() function. + * It releases the DSA memory allocated for storing the memory statistics + * retrieved by that function. The caller must ensure that the LWLock for + * MemoryStatsDsHash is not already held. +*/ +static void +memstats_dsa_cleanup(int procNumber) +{ + MemoryStatsDSHashEntry *entry; + + entry = dshash_find(MemoryStatsDsHash, &procNumber, true); + + Assert(MemoryStatsDsaArea != NULL); + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + entry->memstats_dsa_pointer = InvalidDsaPointer; + entry->stats_written = false; + entry->target_server_id = 0; + + dshash_release_lock(MemoryStatsDsHash, entry); +} + +/* + * Remove this process from the publishing process' + * client key slot, if the stats publishing process has failed to do so. + */ +static void +memstats_client_key_reset(int procNumber) +{ + LWLockAcquire(MemContextReportingClientKeysLock, LW_EXCLUSIVE); + + if (client_keys[procNumber] == MyProcNumber) + client_keys[procNumber] = -1; + LWLockRelease(MemContextReportingClientKeysLock); +} + +void +MemoryContextKeysShmemInit(void) +{ + bool found; + + client_keys = (int *) + ShmemInitStruct("MemoryContextKeys", + MemoryContextKeysShmemSize(), &found); + + if (!found) + MemSet(client_keys, -1, MemoryContextKeysShmemSize()); +} + +Size +MemoryContextKeysShmemSize(void) +{ + Size TotalProcs = 0; + + TotalProcs = add_size(TotalProcs, NUM_AUXILIARY_PROCS); + TotalProcs = add_size(TotalProcs, MaxBackends); + + return mul_size(TotalProcs, sizeof(int)); +} + +/* + * HandleGetMemoryContextInterrupt + * Handle receipt of an interrupt indicating a request to publish memory + * contexts statistics. + * + * All the actual work is deferred to ProcessGetMemoryContextInterrupt() as + * this cannot be performed in a signal handler. + */ +void +HandleGetMemoryContextInterrupt(void) +{ + InterruptPending = true; + PublishMemoryContextPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + +/* + * ProcessGetMemoryContextInterrupt + * Generate information about memory contexts used by the process. + * + * Performs a breadth first search on the memory context tree, thus parents + * statistics are reported before their children in the monitoring function + * output. + * + * Statistics for all the processes are shared via the same dynamic shared + * area. Individual statistics are tracked independently in per-process DSA + * pointers. These pointers are stored in a dshash table with key as requesting + * clients ProcNumber. + * + * We calculate maximum number of context's statistics that can be displayed + * using a pre-determined limit for memory available per process for this + * utility and maximum size of statistics for each context. The remaining + * context statistics if any are captured as a cumulative total at the end of + * individual context's statistics. + * + * If summary is true, we capture the level 1 and level 2 contexts statistics. + * For that we traverse the memory context tree recursively in depth first + * search manner to cover all the children of a parent context, to be able to + * display a cumulative total of memory consumption by a parent at level 2 and + * all its children. + */ +void +ProcessGetMemoryContextInterrupt(void) +{ + List *contexts; + HASHCTL ctl; + HTAB *context_id_lookup; + int context_id = 0; + MemoryStatsEntry *meminfo; + MemoryContextCounters stat; + int num_individual_stats = 0; + bool found; + MemoryStatsDSHashEntry *entry; + int clientProcNumber; + MemoryContext memstats_ctx = NULL; + MemoryContext oldcontext = NULL; + + PublishMemoryContextPending = false; + + /* + * Avoid performing any shared memory operations in aborted transaction, + * the caller will timeout and return NULL. + */ + if (IsAbortedTransactionBlockState()) + return; + + INJECTION_POINT("memcontext-server-wait", NULL); + + /* + * Retrieve the client key for publishing statistics and reset it to -1, + * so other clients can request memory statistics from this process. + * Return if the client_key is -1, which means the requesting client has + * timed out. + */ + LWLockAcquire(MemContextReportingClientKeysLock, LW_EXCLUSIVE); + if (client_keys[MyProcNumber] == -1) + { + LWLockRelease(MemContextReportingClientKeysLock); + return; + } + clientProcNumber = client_keys[MyProcNumber]; + client_keys[MyProcNumber] = -1; + LWLockRelease(MemContextReportingClientKeysLock); + + /* + * Create a new memory context which is not a part of TopMemoryContext + * tree. This context is used to allocate all memory in this function. + * This helps in keeping the memory allocation in this function to report + * memory consumption statistics separate. So that it does not affect the + * output of this function. + */ + memstats_ctx = AllocSetContextCreate((MemoryContext) NULL, + "publish_memory_context_statistics", + ALLOCSET_SMALL_SIZES); + oldcontext = MemoryContextSwitchTo(memstats_ctx); + + /* + * The hash table is used for constructing "path" column of the view, + * similar to its local backend counterpart. + */ + ctl.keysize = sizeof(MemoryContext); + ctl.entrysize = sizeof(MemoryContextId); + ctl.hcxt = CurrentMemoryContext; + + context_id_lookup = hash_create("pg_get_process_memory_contexts", + 256, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + /* List of contexts to process in the next round - start at the top. */ + contexts = list_make1(TopMemoryContext); + + /* + * The client process should have created the required DSA and DSHash + * table. Here we just attach to those. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); + + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); + + /* + * The entry lock is held by dshash_find_or_insert to protect writes to + * process specific memory. Two different processes publishing statistics + * do not block each other. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, &clientProcNumber, &found); + INJECTION_POINT("memcontext-server-injection", NULL); + + /* + * Check if the entry has been deleted due to calling process exiting, or + * if the caller has timed out waiting for us and have issued a request to + * another backend. + * + * Make sure that the client always deletes the entry after taking + * required lock or this function may end up writing to unallocated + * memory. + */ + if (!found || entry->target_server_id != MyProcPid) + { + entry->stats_written = false; + + dshash_release_lock(MemoryStatsDsHash, entry); + + hash_destroy(context_id_lookup); + MemoryContextSwitchTo(oldcontext); + MemoryContextDelete(memstats_ctx); + + return; + } + + /* Should be allocated by a client backend that is requesting statistics */ + Assert(entry->memstats_dsa_pointer != InvalidDsaPointer); + meminfo = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + if (entry->summary) + { + int cxt_id = 0; + List *path = NIL; + MemoryContextId *contextid_entry; + + /* Copy TopMemoryContext statistics to DSA */ + memset(&stat, 0, sizeof(stat)); + (*TopMemoryContext->methods->stats) (TopMemoryContext, NULL, NULL, + &stat, true); + path = lcons_int(1, path); + PublishMemoryContext(meminfo, cxt_id, TopMemoryContext, path, stat, + 1); + + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &TopMemoryContext, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = cxt_id + 1; + + /* + * Copy statistics for each of TopMemoryContexts children. This + * includes statistics of at most 100 children per node, with each + * child node limited to a depth of 100 in its subtree. + */ + for (MemoryContext c = TopMemoryContext->firstchild; c != NULL; + c = c->nextchild) + { + MemoryContextCounters grand_totals; + int num_contexts = 0; + + path = NIL; + memset(&grand_totals, 0, sizeof(grand_totals)); + + cxt_id++; + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &c, HASH_ENTER, &found); + Assert(!found); + contextid_entry->context_id = cxt_id + 1; + + MemoryContextStatsCounter(c, &grand_totals, &num_contexts); + + path = compute_context_path(c, context_id_lookup); + + PublishMemoryContext(meminfo, cxt_id, c, path, + grand_totals, num_contexts); + } + entry->total_stats = cxt_id + 1; + } + else + { + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + MemoryContextId *contextid_entry; + + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &cur, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = context_id + 1; + + /* + * Figure out the transient context_id of this context and each of + * its ancestors, to compute a path for this context. + */ + path = compute_context_path(cur, context_id_lookup); + + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + /* Account for saving one statistics slot for cumulative reporting */ + if (context_id < (MAX_MEMORY_CONTEXT_STATS_NUM - 1)) + { + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1); + } + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].totalspace += stat.totalspace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].nblocks += stat.nblocks; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freespace += stat.freespace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit per process is reached, write aggregate of the + * remaining statistics. + * + * We can store contexts from 0 to max_stats - 1. When context_id + * is greater than max_stats, we stop reporting individual + * statistics when context_id equals max_stats - 2. As we use + * max_stats - 1 array slot for reporting cumulative statistics or + * "Remaining Totals". + */ + if (context_id == (MAX_MEMORY_CONTEXT_STATS_NUM - 2)) + { + int namelen = strlen("Remaining Totals"); + + num_individual_stats = context_id + 1; + strlcpy(meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].name, + "Remaining Totals", namelen + 1); + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].ident[0] = '\0'; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].path[0] = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].type = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].levels = 0; + } + context_id++; + + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + contexts = lappend(contexts, c); + } + + /* + * Check if there are aggregated statistics or not in the result set. + * Statistics are individually reported when context_id <= max_stats, + * only if context_id > max_stats will there be aggregates. + */ + if (context_id <= MAX_MEMORY_CONTEXT_STATS_NUM) + { + entry->total_stats = context_id; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = 1; + } + + /* + * The number of contexts exceeded the space available, so report the + * number of aggregated memory contexts + */ + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = + context_id - num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for + * cumulative statistics. + */ + entry->total_stats = num_individual_stats + 1; + } + } + + entry->stats_written = true; + /* Notify waiting client backend and return */ + ConditionVariableSignal(&entry->memcxt_cv); + dshash_release_lock(MemoryStatsDsHash, entry); + hash_destroy(context_id_lookup); + + MemoryContextSwitchTo(oldcontext); + MemoryContextDelete(memstats_ctx); +} + +/* + * PublishMemoryContext + * + * Copy the memory context statistics of a single context to a DSA memory + */ +static void +PublishMemoryContext(MemoryStatsEntry *memcxt_info, int curr_id, + MemoryContext context, List *path, + MemoryContextCounters stat, int num_contexts) +{ + char *ident = unconstify(char *, context->ident); + char *name = unconstify(char *, context->name); + + /* + * To be consistent with logging output, we label dynahash contexts with + * just the hash table name as with MemoryContextStatsPrint(). + */ + if (context->ident && strncmp(context->name, "dynahash", 8) == 0) + { + name = unconstify(char *, context->ident); + ident = NULL; + } + + if (name != NULL) + { + int namelen = strlen(name); + + if (namelen >= MEMORY_CONTEXT_NAME_SHMEM_SIZE) + namelen = pg_mbcliplen(name, namelen, + MEMORY_CONTEXT_NAME_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].name, name, namelen + 1); + } + else + /* Clearing the array */ + memcxt_info[curr_id].name[0] = '\0'; + + /* Trim and copy the identifier if it is not set to NULL */ + if (ident != NULL) + { + int idlen = strlen(context->ident); + + /* + * Some identifiers such as SQL query string can be very long, + * truncate oversize identifiers. + */ + if (idlen >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + idlen = pg_mbcliplen(ident, idlen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].ident, ident, idlen + 1); + } + else + memcxt_info[curr_id].ident[0] = '\0'; + + /* Store the path */ + if (path == NIL) + memcxt_info[curr_id].path[0] = 0; + else + { + int levels = Min(list_length(path), MAX_PATH_DISPLAY_LENGTH); + + memcxt_info[curr_id].path_length = levels; + memcxt_info[curr_id].levels = list_length(path); + + foreach_int(i, path) + { + memcxt_info[curr_id].path[foreach_current_index(i)] = i; + if (--levels == 0) + break; + } + } + memcxt_info[curr_id].type = context->type; + memcxt_info[curr_id].totalspace = stat.totalspace; + memcxt_info[curr_id].nblocks = stat.nblocks; + memcxt_info[curr_id].freespace = stat.freespace; + memcxt_info[curr_id].freechunks = stat.freechunks; + memcxt_info[curr_id].num_agg_stats = num_contexts; +} + +void +AtProcExit_memstats_cleanup(int code, Datum arg) +{ + int idx = MyProcNumber; + MemoryStatsDSHashEntry *entry; + bool found; + + if (MemoryStatsDsHash != NULL) + { + entry = dshash_find_or_insert(MemoryStatsDsHash, &idx, &found); + + if (found) + { + if (MemoryStatsDsaArea != NULL && + DsaPointerIsValid(entry->memstats_dsa_pointer)) + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + } + dshash_delete_entry(MemoryStatsDsHash, entry); + } + LWLockAcquire(MemContextReportingClientKeysLock, LW_EXCLUSIVE); + client_keys[idx] = -1; + LWLockRelease(MemContextReportingClientKeysLock); +} diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index 36ad708b360..e246fc7ce1f 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -39,6 +39,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t PublishMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index b59e08605cc..ba5d3bf0d7a 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -669,6 +669,13 @@ BaseInit(void) * drop ephemeral slots, which in turn triggers stats reporting. */ ReplicationSlotInitialize(); + + /* + * The before shmem exit callback frees the DSA memory occupied by the + * latest memory context statistics that could be published by this proc + * if requested. + */ + before_shmem_exit(AtProcExit_memstats_cleanup, 0); } diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 073bdb35d2a..2a826cbef17 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -1011,6 +1011,37 @@ MemoryContextStatsInternal(MemoryContext context, int level, } } + +/* + * MemoryContextStatsCounter + * + * Accumulate statistics counts into *totals. totals should not be NULL. + * This involves a non-recursive tree traversal. + */ +void +MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts) +{ + int ichild = 1; + + Assert(totals != NULL); + context->methods->stats(context, NULL, NULL, totals, false); + + for (MemoryContext curr = context->firstchild; + curr != NULL; + curr = MemoryContextTraverseNext(curr, context)) + { + curr->methods->stats(curr, NULL, NULL, totals, false); + ichild++; + } + + /* + * Add the count of all the children contexts which are traversed + * including the parent. + */ + *num_contexts = ichild; +} + /* * MemoryContextStatsPrint * Print callback used by MemoryContextStatsInternal diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index 83f6501df38..de266a15570 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8633,6 +8633,16 @@ prorettype => 'bool', proargtypes => 'int4', prosrc => 'pg_log_backend_memory_contexts' }, +# publishing memory contexts of the specified postgres process +{ oid => '2173', descr => 'publish memory contexts of the specified backend', + proname => 'pg_get_process_memory_contexts', provolatile => 'v', + prorows => '100', proretset => 't', proparallel => 'r', + prorettype => 'record', proargtypes => 'int4 bool', + proallargtypes => '{int4,bool,text,text,text,int4,_int4,int8,int8,int8,int8,int8,int4}', + proargmodes => '{i,i,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{pid, summary, name, ident, type, level, path, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes, num_agg_contexts}', + prosrc => 'pg_get_process_memory_contexts' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index f16f35659b9..38e1e55a9a6 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -96,6 +96,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t PublishMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h index e94ebce95b9..6dd80689bbb 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -87,6 +87,7 @@ PG_LWLOCK(52, SerialControl) PG_LWLOCK(53, AioWorkerSubmissionQueue) PG_LWLOCK(54, WaitLSN) PG_LWLOCK(55, LogicalDecodingControl) +PG_LWLOCK(56, MemContextReportingClientKeys) /* * There also exist several built-in LWLock tranches. As with the predefined diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index 348fba53a93..00ccfcb22a4 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -35,6 +35,7 @@ typedef enum PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to send the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ PROCSIG_RECOVERY_CONFLICT, /* backend is blocking recovery, check * PGPROC->pendingRecoveryConflicts for the diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 11ab1717a16..6e8920bf28d 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -19,7 +19,6 @@ #include "nodes/memnodes.h" - /* * MaxAllocSize, MaxAllocHugeSize * Quasi-arbitrary limits on size of allocations. @@ -319,4 +318,11 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) return true; } +extern void ProcessGetMemoryContextInterrupt(void); +extern void HandleGetMemoryContextInterrupt(void); +extern void MemoryContextKeysShmemInit(void); +extern Size MemoryContextKeysShmemSize(void); +extern void MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts); +extern void AtProcExit_memstats_cleanup(int code, Datum arg); #endif /* MEMUTILS_H */ diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 3dd63fd88ed..c84e5adce16 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -233,3 +233,22 @@ select * from pg_timezone_abbrevs where abbrev = 'LMT'; LMT | @ 7 hours 52 mins 58 secs ago | f (1 row) +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; +NOTICE: (AllocSet,TopMemoryContext,) +NOTICE: (AllocSet,TopMemoryContext,) diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 004f9a70e00..d34d2b6211c 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -101,3 +101,21 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs; -- One specific case we can check without much fear of breakage -- is the historical local-mean-time value used for America/Los_Angeles. select * from pg_timezone_abbrevs where abbrev = 'LMT'; + +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 241945734ec..f90ff78a78b 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1709,6 +1709,8 @@ MemoryContextData MemoryContextId MemoryContextMethodID MemoryContextMethods +MemoryStatsEntry +MemoryStatsDSHashEntry MemoryStatsPrintFunc MergeAction MergeActionState -- 2.34.1 [application/octet-stream] v51-0002-Test-module-to-test-memory-context-reporting-wit.patch (9.0K, ../../CAH2L28sC0Bo5nKzmzUaB4_bg1gVM=TSvD18MjkPQ2tkbjc0vjg@mail.gmail.com/4-v51-0002-Test-module-to-test-memory-context-reporting-wit.patch) download | inline diff: From f9467a7a49f884b90e61c339d3904cb0ad1a6d7a Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Fri, 28 Nov 2025 14:46:38 +0530 Subject: [PATCH 2/2] Test module to test memory context reporting with injection points --- src/test/modules/Makefile | 1 + .../test_memcontext_reporting/Makefile | 29 ++++ .../t/001_memcontext_inj.pl | 154 ++++++++++++++++++ .../test_memcontext_reporting.c | 12 ++ .../test_memcontext_reporting.control | 4 + 5 files changed, 200 insertions(+) create mode 100644 src/test/modules/test_memcontext_reporting/Makefile create mode 100644 src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index 44c7163c1cd..8ce9598495c 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -34,6 +34,7 @@ SUBDIRS = \ test_json_parser \ test_lfind \ test_lwlock_tranches \ + test_memcontext_reporting \ test_misc \ test_oat_hooks \ test_parser \ diff --git a/src/test/modules/test_memcontext_reporting/Makefile b/src/test/modules/test_memcontext_reporting/Makefile new file mode 100644 index 00000000000..0a2dfc44f1c --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/Makefile @@ -0,0 +1,29 @@ +# src/test/modules/test_memcontext_reporting/Makefile + +EXTRA_INSTALL = src/test/modules/injection_points + +export enable_injection_points +MODULE_big = test_memcontext_reporting +OBJS = \ + $(WIN32RES) \ + test_memcontext_reporting.o +PGFILEDESC = "test_memcontext_reporting - test code for memory context reporting" + +REGRESS = test_memcontext_reporting + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_memcontext_reporting +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif + +check: + $(prove_check) + +installcheck: + $(prove_installcheck) diff --git a/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl new file mode 100644 index 00000000000..73665abf902 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl @@ -0,0 +1,154 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test suite for testing memory context statistics reporting + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} +my $psql_err; +my $psql_out; +# Create and start a cluster with one node +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->append_conf( + 'postgresql.conf', + qq[ +max_connections = 100 +log_statement = none +restart_after_crash = false +]); +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); + +# Attaching to a client process's injection point that throws an error +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-client-injection', 'error');" +); + +my $pid = $node->safe_psql('postgres', + "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); + +#Client should have thrown error +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); +like($psql_err, + qr/error triggered for injection point memcontext-client-injection/); + +#Query the same process after detaching the injection point, using some other client and it should succeed. +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-client-injection');"); +my $topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name eq 'TopMemoryContext'); + +# Attaching to a target process injection point that throws an error +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-server-injection', 'error');" +); + +#Server should have thrown error +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); + +#Query the same process after detaching the injection point, using some other client and it should succeed. +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-injection');"); +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name eq 'TopMemoryContext'); + +# Test that two concurrent requests to the same process results in a warning for +# one of those + +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-client-injection', 'wait');"); +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-server-wait', 'wait');"); +my $psql_session1 = $node->background_psql('postgres'); + +# We use query_until so that we can execute additional commands without having to +# wait for this command to complete, since it will pause at injection points. + +$psql_session1->query_until( + qr//, + qq( + SELECT pg_get_process_memory_contexts($pid, true); +)); +$node->wait_for_event('client backend', 'memcontext-client-injection'); +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); +ok($psql_err =~ + /WARNING: server process $pid is processing previous request/); +#Wake the client up. +$node->safe_psql('postgres', + "SELECT injection_points_wakeup('memcontext-client-injection');"); + +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-client-injection');"); +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-wait');"); + +# Test the client process exiting with timeout does not break the server process + +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-server-wait', 'wait');"); +# Following client query times out, returning NULL as output +$node->psql( + 'postgres', + qq(select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';), + stdout => \$psql_out); +is($psql_out, '', 'Query returns null'); +#Wakeup the server process up and detach the injection point. +$node->safe_psql('postgres', + "SELECT injection_points_wakeup('memcontext-server-wait');"); +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-wait');"); +#Query the same server process again and it should succeed. +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name eq 'TopMemoryContext'); + +# Test if the monitoring works fine, when the client backend crashes. +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-client-injection', 'test_memcontext_reporting', 'crash', NULL);" +); + +#Client will crash +$node->psql( + 'postgres', + qq(select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';), + stderr => \$psql_err); +like($psql_err, + qr/WARNING: terminating connection because of crash of another server process|server closed the connection unexpectedly|connection to server was lost|could not send data to server/ +); + +# Wait till server restarts +$node->restart; +$node->poll_query_until('postgres', "SELECT 1;", '1'); + +#Querying memory stats should succeed after server start +$pid = $node->safe_psql('postgres', + "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name eq 'TopMemoryContext'); + +done_testing(); diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c new file mode 100644 index 00000000000..d641f3616dc --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c @@ -0,0 +1,12 @@ +#include "postgres.h" +#include "funcapi.h" + +PG_MODULE_MAGIC; + +extern PGDLLEXPORT void crash(const char *name, const void *private_data, void *arg); + +void +crash(const char *name, const void *private_data, void *arg) +{ + abort(); +} diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control new file mode 100644 index 00000000000..48b501682d5 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control @@ -0,0 +1,4 @@ +comment = 'Test code for memcontext reporting' +default_version = '1.0' +module_pathname = '$libdir/test_memcontext_reporting' +relocatable = true -- 2.34.1 ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2026-02-20 23:13 Rahila Syed <[email protected]> parent: Chao Li <[email protected]> 1 sibling, 1 reply; 52+ messages in thread From: Rahila Syed @ 2026-02-20 23:13 UTC (permalink / raw) To: pgsql-hackers; +Cc: Daniel Gustafsson <[email protected]>; torikoshia <[email protected]>; Robert Haas <[email protected]>; Chao Li <[email protected]> Hi, Please find attached updated and rebased patches which incorporate code fixes suggested off-list by Daniel These changes include a fix to free dsa memory in one of the exit paths from the client-side function along with several documentation and comment improvements. > I think we can calculate remaining time and pass remaining milliseconds > into ConditionVariableTimedSleep. > > This comment by Chao Li was also addressed as part of Daniel's review fixes. Thank you, Rahila Syed Attachments: [application/octet-stream] v52-0001-Add-function-to-report-memory-context-statistics.patch (59.8K, ../../CAH2L28sZ5ELWzXgRNJEYpOR8jk6DdWETgJ7L-xYa9SXQ+n5Z7w@mail.gmail.com/3-v52-0001-Add-function-to-report-memory-context-statistics.patch) download | inline diff: From c56ac6ce0d9fcf456434fa2b8e50bd7aa7091d9f Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Thu, 27 Nov 2025 14:39:43 +0530 Subject: [PATCH 1/2] Add function to report memory context statistics This adds a function for retrieving memory context statistics and information from backends as well as auxiliary processes. The intended usecase is cluster debugging when under memory pressure or unanticipated memory usage characteristics. When calling the function it sends a signal to the specified process to submit statistics regarding its memory contexts into dynamic shared memory. Each memory context is returned in detail, followed by a cumulative total in case the number of contexts exceed the max allocated amount of shared memory. Each process is limited to use at most 1Mb memory for this. A summary can also be explicitly requested by the user, this will return the TopMemoryContext and a cumulative total of all lower contexts. In order to not block on busy processes, we have hardcoded the number of seconds during which to retry before timing out. In the case where no statistics are published within the set timeout, NULL is returned --- doc/src/sgml/func/func-admin.sgml | 159 +++ src/backend/catalog/system_functions.sql | 14 + src/backend/postmaster/autovacuum.c | 4 + src/backend/postmaster/checkpointer.c | 4 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 4 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 4 + src/backend/storage/ipc/dsm_registry.c | 8 +- src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 2 + src/backend/utils/adt/mcxtfuncs.c | 1020 ++++++++++++++++- src/backend/utils/init/globals.c | 1 + src/backend/utils/init/postinit.c | 7 + src/backend/utils/mmgr/mcxt.c | 31 + src/include/catalog/pg_proc.dat | 10 + src/include/miscadmin.h | 1 + src/include/storage/lwlocklist.h | 1 + src/include/storage/procsignal.h | 1 + src/include/utils/memutils.h | 7 + src/test/regress/expected/sysviews.out | 19 + src/test/regress/sql/sysviews.sql | 18 + src/tools/pgindent/typedefs.list | 2 + 25 files changed, 1312 insertions(+), 22 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 3ac81905d1f..120f032f249 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -251,6 +251,132 @@ <literal>false</literal> is returned. </para></entry> </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_get_process_memory_contexts</primary> + </indexterm> + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type> <optional>,<parameter>summary</parameter> <type>boolean</type> <literal>DEFAULT</literal> <literal>false</literal></optional> ) + <returnvalue>setof record</returnvalue> + ( <parameter>name</parameter> <type>text</type>, + <parameter>ident</parameter> <type>text</type>, + <parameter>type</parameter> <type>text</type>, + <parameter>level</parameter> <type>integer</type>, + <parameter>path</parameter> <type>integer[]</type>, + <parameter>total_bytes</parameter> <type>bigint</type>, + <parameter>total_nblocks</parameter> <type>bigint</type>, + <parameter>free_bytes</parameter> <type>bigint</type>, + <parameter>free_chunks</parameter> <type>bigint</type>, + <parameter>used_bytes</parameter> <type>bigint</type>, + <parameter>num_agg_contexts</parameter> <type>integer</type> ) + </para> + <para> + This function handles requests to display the memory contexts of a + <productname>PostgreSQL</productname> process with the specified + process ID. The function can be used to send requests to backends as + well as <glossterm linkend="glossary-auxiliary-proc">auxiliary processes</glossterm>. + If the process does not respond with memory contexts statistics in 5 seconds, + the function returns NULL. + </para> + <para> + The returned record contains extended statistics per each memory + context: + <itemizedlist spacing="compact"> + <listitem> + <para> + <parameter>name</parameter> - The name of the memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>ident</parameter> - Memory context ID (if any). + </para> + </listitem> + <listitem> + <para> + <parameter>type</parameter> - The type of memory context, possible + values are: AllocSet, Generation, Slab and Bump. + </para> + </listitem> + <listitem> + <para> + <parameter>level</parameter> - The level in the tree of the current + memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>path</parameter> - Memory contexts are organized in a + tree model with TopMemoryContext as the root, and all other memory + contexts as nodes in the tree. The <parameter>path</parameter> + displays the path from the root to the current memory context. The + path is limited to 100 children per node, with each node limited + to a max depth of 100, to preserve memory during reporting. The + printed path will also be limited to 100 nodes counting from the + TopMemoryContext. + </para> + </listitem> + <listitem> + <para> + <parameter>total_bytes</parameter> - The total number of bytes + allocated to this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_nblocks</parameter> - The total number of blocks + used for the allocated memory. + </para> + </listitem> + <listitem> + <para> + <parameter>free_bytes</parameter> - The amount of free memory in + this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>free_chunks</parameter> - The number of chunks that + <parameter>free_bytes</parameter> corresponds to. + </para> + </listitem> + <listitem> + <para> + <parameter>used_bytes</parameter> - The total number of bytes + currently occupied. + </para> + </listitem> + <listitem> + <para> + <parameter>num_agg_contexts</parameter> - The number of memory + contexts aggregated in the displayed statistics. + </para> + </listitem> + </itemizedlist> + </para> + <para> + When <parameter>summary</parameter> is <literal>true</literal>, statistics + for memory contexts at levels 1 and 2 are displayed, with level 1 + representing the root node (i.e., <literal>TopMemoryContext</literal>). + Statistics for contexts on level 2 and below are aggregates of all + child contexts' statistics, where <literal>num_agg_contexts</literal> + indicate the number of aggregated child contexts. When + <parameter>summary</parameter> is <literal>false</literal> (the default), + <literal>the num_agg_contexts</literal> value is <literal>1</literal>, + indicating that individual statistics are being displayed. + </para> + <para> + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context + statistics are aggregated and a cumulative total is displayed. The + <literal>num_agg_contexts</literal> column indicates the number of + contexts aggregated in the displayed statistics. A + <literal>num_agg_contexts</literal> value of <literal>1</literal> + indicates that context statistics are displayed separately. + </para></entry> + </row> </tbody> </tgroup> </table> @@ -302,6 +428,39 @@ LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 because it may generate a large number of log messages. </para> + <para> + <function>pg_get_process_memory_contexts</function> can be used to request + memory contexts statistics of any <productname>PostgreSQL</productname> + process. For example: +<programlisting> +postgres=# SELECT * FROM pg_get_process_memory_contexts( + (SELECT pid FROM pg_stat_activity + WHERE backend_type = 'checkpointer'), + false) LIMIT 1; +-[ RECORD 1 ]----+------------------------------ +name | TopMemoryContext +ident | +type | AllocSet +level | 1 +path | {1} +total_bytes | 90304 +total_nblocks | 3 +free_bytes | 2880 +free_chunks | 1 +used_bytes | 87424 +num_agg_contexts | 1 +</programlisting> + <note> + <para> + While <function>pg_get_process_memory_contexts</function> can be used to + query memory contexts of the local backend, + <structname>pg_backend_memory_contexts</structname> + (see <xref linkend="view-pg-backend-memory-contexts"/> for more details) + will be less resource intensive when only the local backend is of interest. + </para> + </note> + </para> + </sect2> <sect2 id="functions-admin-backup"> diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql index 69699f8830a..b62944cee87 100644 --- a/src/backend/catalog/system_functions.sql +++ b/src/backend/catalog/system_functions.sql @@ -378,6 +378,17 @@ BEGIN ATOMIC END; +CREATE OR REPLACE FUNCTION + pg_get_process_memory_contexts(IN pid integer, IN summary boolean DEFAULT false, + OUT name text, OUT ident text, OUT type text, OUT level integer, + OUT path integer[], OUT total_bytes bigint, OUT total_nblocks bigint, + OUT free_bytes bigint, OUT free_chunks bigint, OUT used_bytes bigint, + OUT num_agg_contexts integer) +RETURNS SETOF RECORD +LANGUAGE INTERNAL +STRICT VOLATILE PARALLEL UNSAFE +AS 'pg_get_process_memory_contexts'; + -- -- The default permissions for functions mean that anyone can execute them. -- A number of functions shouldn't be executable by just anyone, but rather @@ -503,6 +514,7 @@ REVOKE EXECUTE ON FUNCTION pg_ls_logicalmapdir() FROM PUBLIC; REVOKE EXECUTE ON FUNCTION pg_ls_replslotdir(text) FROM PUBLIC; +REVOKE EXECUTE ON FUNCTION pg_get_process_memory_contexts(integer, boolean) FROM PUBLIC; -- -- We also set up some things as accessible to standard roles. -- @@ -529,6 +541,8 @@ GRANT EXECUTE ON FUNCTION pg_current_logfile() TO pg_monitor; GRANT EXECUTE ON FUNCTION pg_current_logfile(text) TO pg_monitor; +GRANT EXECUTE ON FUNCTION pg_get_process_memory_contexts(integer, boolean) TO pg_read_all_stats; + GRANT pg_read_all_settings TO pg_monitor; GRANT pg_read_all_stats TO pg_monitor; diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 6fde740465f..3c24119c43a 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -787,6 +787,10 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index e03c19123bc..d54193f8890 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -684,6 +684,10 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index a2c0ff012c5..cb18ce80893 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -48,6 +48,10 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index 82731e452fc..e13a40f1427 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -870,6 +870,10 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index cdbe53dd262..1d6d8f22d20 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -192,6 +192,10 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index 742137edad6..d5a3cb13a4c 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -875,6 +875,10 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c index 068c1577b12..4810babac1b 100644 --- a/src/backend/storage/ipc/dsm_registry.c +++ b/src/backend/storage/ipc/dsm_registry.c @@ -270,7 +270,9 @@ GetNamedDSMSegment(const char *name, size_t size, * This routine returns a pointer to the DSA. A new LWLock tranche ID will be * generated if needed. Note that the lock tranche will be registered with the * provided name. Also note that this should be called at most once for a - * given DSA in each backend. + * given DSA in each backend. It's best to minimize the number of errors thrown + * in this function since it's called from a CFI function. An error here could + * lead to error in any transaction that calls the CFI function. */ dsa_area * GetNamedDSA(const char *name, bool *found) @@ -351,7 +353,9 @@ GetNamedDSA(const char *name, bool *found) * params is ignored; a new LWLock tranche ID will be generated if needed. * Note that the lock tranche will be registered with the provided name. Also * note that this should be called at most once for a given table in each - * backend. + * backend. It's best to minimize the number of errors thrown in this function + * since it's called from a CFI function. An error here could lead to error in + * any transaction that calls the CFI function. */ dshash_table * GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found) diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index 1f7e933d500..b0176613987 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -52,6 +52,7 @@ #include "storage/sinvaladt.h" #include "utils/guc.h" #include "utils/injection_point.h" +#include "utils/memutils.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -141,6 +142,7 @@ CalculateShmemSize(void) size = add_size(size, AioShmemSize()); size = add_size(size, WaitLSNShmemSize()); size = add_size(size, LogicalDecodingCtlShmemSize()); + size = add_size(size, MemoryContextKeysShmemSize()); /* include additional requested shmem from preload libraries */ size = add_size(size, total_addin_request); @@ -328,6 +330,7 @@ CreateOrAttachShmemStructs(void) AioShmemInit(); WaitLSNShmemInit(); LogicalDecodingCtlShmemInit(); + MemoryContextKeysShmemInit(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index 7505c9d3a37..839cbdbdc89 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -695,6 +695,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_GET_MEMORY_CONTEXT)) + HandleGetMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index d01a09dd0c4..9e0d289a604 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3573,6 +3573,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 4aa864fe3c3..3e1dc4225b2 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -163,6 +163,7 @@ WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." +MEM_CXT_PUBLISH "Waiting for a process to publish memory information." ABI_compatibility: @@ -366,6 +367,7 @@ SerialControl "Waiting to read or update shared <filename>pg_serial</filename> s AioWorkerSubmissionQueue "Waiting to access AIO worker submission queue." WaitLSN "Waiting to read or update shared Wait-for-LSN state." LogicalDecodingControl "Waiting to read or update logical decoding status information." +MemContextReportingClientKeys "Waiting for another process to complete reading or writing the memory reporting keys." # # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE) diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index c7f7b8bc2dd..f18b6d47c60 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -15,25 +15,128 @@ #include "postgres.h" +#include "access/xact.h" #include "funcapi.h" #include "mb/pg_wchar.h" +#include "storage/dsm_registry.h" #include "storage/proc.h" #include "storage/procarray.h" #include "storage/procsignal.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/injection_point.h" +#include "utils/memutils.h" +#include "utils/wait_event_types.h" + +/* + * Memory Context reporting size limits. + */ + +/* Max length of context name and ident, to keep it consistent + * with ProcessLogMemoryContext() + */ +#define MEMORY_CONTEXT_IDENT_SHMEM_SIZE 100 +#define MEMORY_CONTEXT_NAME_SHMEM_SIZE 100 +#define MAX_PATH_DISPLAY_LENGTH 100 + +/* Maximum size (in bytes) of DSA area per process */ +#define MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND ((size_t) (1 * 1024 * 1024)) + +/* + * Maximum number of memory context statistics is calculated by dividing + * max memory allocated per backend with maximum size per context statistics. + * The identifier and name are statically allocated arrays of size 100 bytes. + * The path depth is limited to 100 like for memory context logging. + */ +#define MAX_MEMORY_CONTEXT_STATS_NUM MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND / (sizeof(MemoryStatsEntry)) + +/* + * Size of dshash key. The key is a uint32 rendered as a string, 10 chars + * plus space for a NULL terminator can hold all the values. + */ +#define CLIENT_KEY_SIZE (10 + 1) + +/* Dynamic shared memory state for reporting statistics per context */ +typedef struct MemoryStatsEntry +{ + char name[MEMORY_CONTEXT_NAME_SHMEM_SIZE]; + char ident[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + int path[MAX_PATH_DISPLAY_LENGTH]; + NodeTag type; + int path_length; + int levels; + int64 totalspace; + int64 nblocks; + int64 freespace; + int64 freechunks; + int num_agg_stats; +} MemoryStatsEntry; + +/* + * Per backend dynamic shared hash entry for memory context statistics + * reporting. + */ +typedef struct MemoryStatsDSHashEntry +{ + ProcNumber client_key; + ConditionVariable memcxt_cv; + bool stats_written; + int target_server_id; + int total_stats; + bool summary; + dsa_pointer memstats_dsa_pointer; +} MemoryStatsDSHashEntry; + +static const dshash_parameters memctx_dsh_params = { + offsetof(MemoryStatsDSHashEntry, memcxt_cv), + sizeof(MemoryStatsDSHashEntry), + dshash_memcmp, + dshash_memhash, + dshash_memcpy +}; + +/* + * These are used for reporting memory context statistics of a process. + */ + +/* Array to store the keys of MemoryStatsDsHash */ +static int *client_keys = NULL; + +/* + * Table to store pointers to DSA memory containing memory statistics and other + * metadata. There is one entry per client backend request, keyed by ProcNumber + * of the client obtained from client_keys array above. + */ +static dshash_table *MemoryStatsDsHash = NULL; + +/* + * Dsa area which stores the actual memory context + * statistics. + */ +static dsa_area *MemoryStatsDsaArea = NULL; + +static void memstats_dsa_cleanup(int ProcNumber); +static void memstats_client_key_reset(int ProcNumber); +static void PublishMemoryContext(MemoryStatsEntry *memcxt_info, + int curr_id, MemoryContext context, + List *path, + MemoryContextCounters stat, + int num_contexts); /* ---------- * The max bytes for showing identifiers of MemoryContext. + * This is used by pg_get_backend_memory_context - view used for local backend. * ---------- */ #define MEMORY_CONTEXT_IDENT_DISPLAY_SIZE 1024 +/* Timeout in seconds */ +#define MEMORY_STATS_MAX_TIMEOUT 5 + /* * MemoryContextId - * Used for storage of transient identifiers for - * pg_get_backend_memory_contexts. + * Used for storage of transient identifiers for memory context reporting */ typedef struct MemoryContextId { @@ -63,6 +166,67 @@ int_list_to_array(const List *list) return PointerGetDatum(result_array); } +/* + * context_type_to_string + * Returns a textual representation of a context type + * + * This should cover the same types as MemoryContextIsValid. + */ +static const char * +context_type_to_string(NodeTag type) +{ + const char *context_type; + + switch (type) + { + case T_AllocSetContext: + context_type = "AllocSet"; + break; + case T_GenerationContext: + context_type = "Generation"; + break; + case T_SlabContext: + context_type = "Slab"; + break; + case T_BumpContext: + context_type = "Bump"; + break; + default: + context_type = "???"; + break; + } + return context_type; +} + +/* + * compute_context_path + * + * Append the transient context_id of this context and each of its ancestors + * to a list, in order to compute a path. + */ +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + MemoryContext cur_context; + + for (cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + { + elog(NOTICE, "hash table corrupted, can't construct path value"); + return NIL; + } + path = lcons_int(cur_entry->context_id, path); + } + return path; +} + /* * PutMemoryContextsStatsTupleStore * Add details for the given MemoryContext to 'tupstore'. @@ -144,24 +308,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, else nulls[1] = true; - switch (context->type) - { - case T_AllocSetContext: - type = "AllocSet"; - break; - case T_GenerationContext: - type = "Generation"; - break; - case T_SlabContext: - type = "Slab"; - break; - case T_BumpContext: - type = "Bump"; - break; - default: - type = "???"; - break; - } + type = context_type_to_string(context->type); values[2] = CStringGetTextDatum(type); values[3] = Int32GetDatum(list_length(path)); /* level */ @@ -306,3 +453,836 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } + +/* + * pg_get_process_memory_contexts + * Signal a backend or an auxiliary process to send its memory contexts, + * wait for the results and display them. + * + * By default, only superusers or users with ROLE_PG_READ_ALL_STATS are allowed + * to signal a process to return the memory contexts. Additional roles can be + * permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to copy the memory context details + * to a dynamic shared memory space. + * + * We have defined a limit on DSA memory that could be allocated per process - + * if the process has more memory contexts than what can fit in the allocated + * size, the excess contexts are summarized and represented as cumulative total + * at the end of the buffer. + * + * After sending the signal, wait on a condition variable. The publishing + * backend, after copying the data to shared memory, sends a signal on that + * condition variable. There is one condition variable per client process. + * Once the condition variable is signalled, check if the latest memory context + * information is available and display. + * + * If the publishing backend does not respond before the condition variable + * times out, which is set to a predefined value MEMORY_STATS_MAX_TIMEOUT, give + * up and return NULL. + */ +Datum +pg_get_process_memory_contexts(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + bool summary = PG_GETARG_BOOL(1); + PGPROC *proc; + ProcNumber client_procno = MyProcNumber; + ProcNumber server_procno; + bool proc_is_aux = false; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + MemoryStatsEntry *memcxt_info; + MemoryStatsDSHashEntry *entry; + bool found; + TimestampTz start_timestamp; + + /* + * See if the process with given pid is a backend or an auxiliary process + * and remember the type for when we requery the process later. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + { + proc = AuxiliaryPidGetProc(pid); + proc_is_aux = true; + } + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; this is however not a problem and leave with a WARNING. + * See comment in pg_log_backend_memory_contexts for a discussion on this. + */ + if (proc == NULL) + { + ereport(WARNING, + errmsg("PID %d is not a PostgreSQL server process", pid)); + PG_RETURN_NULL(); + } + + server_procno = GetNumberFromPGProc(proc); + + /* + * First, check if the server process slot is occupied; if so, exit early. + * An occupied slot indicates another client backend is already requesting + * statistics from this server process. We should not set the slot yet, + * even if it appears empty, until we are prepared to signal the server + * process. This is because if the following DSA APIs throw an error, the + * server slot may not be reset, leading to it being incorrectly marked as + * occupied and blocking further memory context statistics queries to the + * same process. + */ + LWLockAcquire(MemContextReportingClientKeysLock, LW_EXCLUSIVE); + if (client_keys[server_procno] != -1) + { + LWLockRelease(MemContextReportingClientKeysLock); + ereport(WARNING, + errmsg("server process %d is processing previous request", + pid)); + PG_RETURN_NULL(); + } + LWLockRelease(MemContextReportingClientKeysLock); + + /* + * Create a DSA to allocate memory for copying memory contexts statistics. + * Allocate the memory in the DSA and send DSA pointer to the server + * process for storing the context statistics. If number of contexts + * exceed a predefined limit (1MB), a cumulative total is stored for such + * contexts. + * + * The DSA is created once for the lifetime of the server, and only + * attached in subsequent calls. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); + + /* + * The DSA pointers containing statistics for each client are stored in a + * dshash table. In addition to DSA pointer, each entry in this table also + * contains information about the statistics, condition variable for + * signalling between client and the server and miscellaneous data + * specific to a request. There is one entry per client request in the + * hash table. + */ + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); + + /* + * Insert an entry for this client in DSHASH table the first time this + * function is called. This entry is deleted when the process exits in + * before_shmem_exit call. + * + * dshash_find_or_insert locks the entry to prevent the publisher from + * reading before client has updated the entry. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, &client_procno, &found); + if (!found) + { + entry->stats_written = false; + ConditionVariableInit(&entry->memcxt_cv); + } + + /* + * Allocate 1MB of memory for the backend to publish its statistics on + * every call to this function. The memory is freed at the end of the + * function. + */ + entry->memstats_dsa_pointer = + dsa_allocate0(MemoryStatsDsaArea, MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND); + + /* + * Specify whether a summary of statistics is requested, before signalling + * the server. + */ + entry->summary = summary; + + /* + * Indicate which server process statistics are being requested from. If + * this client times out before the last requested process can publish its + * statistics, it may send a new request to another server process. Since + * the previous server was notified, it might attempt to read the same + * client entry and respond incorrectly with its statistics. By storing + * the server ID in the client entry, we prevent any previously signalled + * server process from writing its statistics in the space meant for the + * newly requested process. + */ + entry->target_server_id = pid; + dshash_release_lock(MemoryStatsDsHash, entry); + + /* + * Check if the publishing process slot is empty and store this clients + * key i.e its procNumber. This informs the publishing process that it is + * supposed to write statistics in the hash entry corresponding to this + * client. + */ + LWLockAcquire(MemContextReportingClientKeysLock, LW_EXCLUSIVE); + if (client_keys[server_procno] == -1) + client_keys[server_procno] = MyProcNumber; + else + { + LWLockRelease(MemContextReportingClientKeysLock); + memstats_dsa_cleanup(client_procno); + ereport(WARNING, + errmsg("server process %d is processing previous request", + pid)); + PG_RETURN_NULL(); + } + LWLockRelease(MemContextReportingClientKeysLock); + + /* + * Send a signal to a PostgreSQL process, informing it we want it to + * produce information about its memory contexts. + */ + if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, server_procno) < 0) + { + memstats_dsa_cleanup(client_procno); + memstats_client_key_reset(server_procno); + ereport(WARNING, + errmsg("could not send signal to process %d: %m", + pid)); + PG_RETURN_NULL(); + } + start_timestamp = GetCurrentTimestamp(); + + while (1) + { + long elapsed_time; + + INJECTION_POINT("memcontext-client-injection", NULL); + + elapsed_time = TimestampDifferenceMilliseconds(start_timestamp, + GetCurrentTimestamp()); + /* Return if we have already exceeded the timeout */ + if (elapsed_time >= MEMORY_STATS_MAX_TIMEOUT * 1000) + { + memstats_dsa_cleanup(client_procno); + memstats_client_key_reset(server_procno); + ConditionVariableCancelSleep(); + ereport(NOTICE, + errmsg("request for memory context statistics for PID %d timed out", + pid)); + PG_RETURN_NULL(); + } + + /* + * Recheck the state of the backend before sleeping on the condition + * variable to ensure the process is still alive. Only check the + * relevant process type based on the earlier PID check. + */ + if (proc_is_aux) + proc = AuxiliaryPidGetProc(pid); + else + proc = BackendPidGetProc(pid); + + /* + * The target server process ending during memory context processing + * is not an error. + */ + if (proc == NULL) + { + memstats_dsa_cleanup(client_procno); + memstats_client_key_reset(server_procno); + ConditionVariableCancelSleep(); + ereport(WARNING, + errmsg("PID %d is no longer a PostgreSQL server process", + pid)); + PG_RETURN_NULL(); + } + + /* + * Wait for at most MEMORY_STATS_MAX_TIMEOUT seconds, if we have been + * woken up spuriously we subtract the time already slept. If no + * statistics are available within the allowed time then return NULL. + * The timer is defined in milliseconds since that's what the + * condition variable sleep uses. + */ + if (ConditionVariableTimedSleep(&entry->memcxt_cv, + (MEMORY_STATS_MAX_TIMEOUT * 1000) - elapsed_time, + WAIT_EVENT_MEM_CXT_PUBLISH)) + { + /* Timeout has expired, return NULL */ + memstats_dsa_cleanup(client_procno); + memstats_client_key_reset(server_procno); + ConditionVariableCancelSleep(); + ereport(NOTICE, + errmsg("request for memory context statistics for PID %d timed out", + pid)); + PG_RETURN_NULL(); + } + entry = dshash_find_or_insert(MemoryStatsDsHash, &client_procno, &found); + Assert(found); + + memcxt_info = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + /* + * We expect to come out of sleep when the requested process has + * finished publishing the statistics, verified using a boolean + * stats_written. + * + * Make sure that the statistics are actually written by checking that + * the name of the context is not NULL. This is done to ensure that + * the subsequent waits for statistics do not return spuriously if the + * previous call to the function ended in error and thus could not + * clear the stats_written flag. + */ + if (entry->stats_written && memcxt_info[0].name[0] != '\0') + break; + + dshash_release_lock(MemoryStatsDsHash, entry); + + } + + InitMaterializedSRF(fcinfo, 0); + + /* + * Backend has finished publishing the stats, project them. + */ +#define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 11 + for (int i = 0; i < entry->total_stats; i++) + { + ArrayType *path_array; + Datum values[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + bool nulls[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + Datum *path_datum = NULL; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + Assert(memcxt_info[i].name[0] != '\0'); + values[0] = CStringGetTextDatum(memcxt_info[i].name); + + if (memcxt_info[i].ident[0] != '\0') + values[1] = CStringGetTextDatum(memcxt_info[i].ident); + else + nulls[1] = true; + + values[2] = CStringGetTextDatum(context_type_to_string(memcxt_info[i].type)); + + if (memcxt_info[i].levels != 0) + values[3] = Int32GetDatum(memcxt_info[i].levels); + else + nulls[3] = true; + + if (memcxt_info[i].path[0] != 0) + { + int path_length; + + path_length = memcxt_info[i].path_length; + path_datum = (Datum *) palloc(path_length * sizeof(Datum)); + + for (int j = 0; j < path_length; j++) + path_datum[j] = Int32GetDatum(memcxt_info[i].path[j]); + path_array = construct_array_builtin(path_datum, + path_length, + INT4OID); + values[4] = PointerGetDatum(path_array); + } + else + nulls[4] = true; + + values[5] = Int64GetDatum(memcxt_info[i].totalspace); + values[6] = Int64GetDatum(memcxt_info[i].nblocks); + values[7] = Int64GetDatum(memcxt_info[i].freespace); + values[8] = Int64GetDatum(memcxt_info[i].freechunks); + values[9] = Int64GetDatum(memcxt_info[i].totalspace - + memcxt_info[i].freespace); + values[10] = Int32GetDatum(memcxt_info[i].num_agg_stats); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + dshash_release_lock(MemoryStatsDsHash, entry); + memstats_dsa_cleanup(client_procno); + + ConditionVariableCancelSleep(); + + PG_RETURN_NULL(); +} + +/* + * This function is executed before returning from the + * pg_get_process_memory_contexts() function. + * It releases the DSA memory allocated for storing the memory statistics + * retrieved by that function. The caller must ensure that the LWLock for + * MemoryStatsDsHash is not already held. +*/ +static void +memstats_dsa_cleanup(int procNumber) +{ + MemoryStatsDSHashEntry *entry; + + entry = dshash_find(MemoryStatsDsHash, &procNumber, true); + + Assert(MemoryStatsDsaArea != NULL); + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + entry->memstats_dsa_pointer = InvalidDsaPointer; + entry->stats_written = false; + entry->target_server_id = 0; + + dshash_release_lock(MemoryStatsDsHash, entry); +} + +/* + * Remove this process from the publishing process' + * client key slot, if the stats publishing process has failed to do so. + */ +static void +memstats_client_key_reset(int procNumber) +{ + LWLockAcquire(MemContextReportingClientKeysLock, LW_EXCLUSIVE); + + if (client_keys[procNumber] == MyProcNumber) + client_keys[procNumber] = -1; + LWLockRelease(MemContextReportingClientKeysLock); +} + +void +MemoryContextKeysShmemInit(void) +{ + bool found; + + client_keys = (int *) + ShmemInitStruct("MemoryContextKeys", + MemoryContextKeysShmemSize(), &found); + + if (!found) + MemSet(client_keys, -1, MemoryContextKeysShmemSize()); +} + +Size +MemoryContextKeysShmemSize(void) +{ + Size TotalProcs = 0; + + TotalProcs = add_size(TotalProcs, NUM_AUXILIARY_PROCS); + TotalProcs = add_size(TotalProcs, MaxBackends); + + return mul_size(TotalProcs, sizeof(int)); +} + +/* + * HandleGetMemoryContextInterrupt + * Handle receipt of an interrupt indicating a request to publish memory + * contexts statistics. + * + * All the actual work is deferred to ProcessGetMemoryContextInterrupt() as + * this cannot be performed in a signal handler. + */ +void +HandleGetMemoryContextInterrupt(void) +{ + InterruptPending = true; + PublishMemoryContextPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + +/* + * ProcessGetMemoryContextInterrupt + * Generate information about memory contexts used by the process. + * + * Performs a breadth first search on the memory context tree, thus parents + * statistics are reported before their children in the monitoring function + * output. + * + * Statistics for all the processes are shared via the same dynamic shared + * area. Individual statistics are tracked independently in per-process DSA + * pointers. These pointers are stored in a dshash table with key as requesting + * clients ProcNumber. + * + * We calculate maximum number of context's statistics that can be displayed + * using a pre-determined limit for memory available per process for this + * utility and maximum size of statistics for each context. The remaining + * context statistics if any are captured as a cumulative total at the end of + * individual context's statistics. + * + * If summary is true, we capture the level 1 and level 2 contexts statistics. + * For that we traverse the memory context tree recursively in depth first + * search manner to cover all the children of a parent context, to be able to + * display a cumulative total of memory consumption by a parent at level 2 and + * all its children. + */ +void +ProcessGetMemoryContextInterrupt(void) +{ + List *contexts; + HASHCTL ctl; + HTAB *context_id_lookup; + int context_id = 0; + MemoryStatsEntry *meminfo; + MemoryContextCounters stat; + int num_individual_stats = 0; + bool found; + MemoryStatsDSHashEntry *entry; + int clientProcNumber; + MemoryContext memstats_ctx = NULL; + MemoryContext oldcontext = NULL; + + PublishMemoryContextPending = false; + + /* + * Avoid performing any shared memory operations in aborted transaction, + * the caller will timeout and return NULL. + */ + if (IsAbortedTransactionBlockState()) + return; + + INJECTION_POINT("memcontext-server-wait", NULL); + + /* + * Retrieve the client key for publishing statistics and reset it to -1, + * so other clients can request memory statistics from this process. + * Return if the client_key is -1, which means the requesting client has + * timed out. + */ + LWLockAcquire(MemContextReportingClientKeysLock, LW_EXCLUSIVE); + if (client_keys[MyProcNumber] == -1) + { + LWLockRelease(MemContextReportingClientKeysLock); + return; + } + clientProcNumber = client_keys[MyProcNumber]; + client_keys[MyProcNumber] = -1; + LWLockRelease(MemContextReportingClientKeysLock); + + /* + * Create a new memory context which is not a part of TopMemoryContext + * tree. This context is used to allocate all memory in this function. + * This helps in keeping the memory allocation in this function to report + * memory consumption statistics separate. So that it does not affect the + * output of this function. + */ + memstats_ctx = AllocSetContextCreate((MemoryContext) NULL, + "publish_memory_context_statistics", + ALLOCSET_SMALL_SIZES); + oldcontext = MemoryContextSwitchTo(memstats_ctx); + + /* + * The hash table is used for constructing "path" column of the view, + * similar to its local backend counterpart. + */ + ctl.keysize = sizeof(MemoryContext); + ctl.entrysize = sizeof(MemoryContextId); + ctl.hcxt = CurrentMemoryContext; + + context_id_lookup = hash_create("pg_get_process_memory_contexts", + 256, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + /* List of contexts to process in the next round - start at the top. */ + contexts = list_make1(TopMemoryContext); + + /* + * The client process should have created the required DSA and DSHash + * table. Here we just attach to those. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); + + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); + + /* + * The entry lock is held by dshash_find_or_insert to protect writes to + * process specific memory. Two different processes publishing statistics + * do not block each other. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, &clientProcNumber, &found); + INJECTION_POINT("memcontext-server-injection", NULL); + + /* + * Check if the entry has been deleted due to calling process exiting, or + * if the caller has timed out waiting for us and have issued a request to + * another backend. + * + * Make sure that the client always deletes the entry after taking + * required lock or this function may end up writing to unallocated + * memory. + */ + if (!found || entry->target_server_id != MyProcPid) + { + entry->stats_written = false; + + dshash_release_lock(MemoryStatsDsHash, entry); + + hash_destroy(context_id_lookup); + MemoryContextSwitchTo(oldcontext); + MemoryContextDelete(memstats_ctx); + + return; + } + + /* Should be allocated by a client backend that is requesting statistics */ + Assert(entry->memstats_dsa_pointer != InvalidDsaPointer); + meminfo = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + if (entry->summary) + { + int cxt_id = 0; + List *path = NIL; + MemoryContextId *contextid_entry; + + /* Copy TopMemoryContext statistics to DSA */ + memset(&stat, 0, sizeof(stat)); + (*TopMemoryContext->methods->stats) (TopMemoryContext, NULL, NULL, + &stat, true); + path = lcons_int(1, path); + PublishMemoryContext(meminfo, cxt_id, TopMemoryContext, path, stat, + 1); + + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &TopMemoryContext, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = cxt_id + 1; + + /* + * Copy statistics for each of TopMemoryContexts children. This + * includes statistics of at most 100 children per node, with each + * child node limited to a depth of 100 in its subtree. + */ + for (MemoryContext c = TopMemoryContext->firstchild; c != NULL; + c = c->nextchild) + { + MemoryContextCounters grand_totals; + int num_contexts = 0; + + path = NIL; + memset(&grand_totals, 0, sizeof(grand_totals)); + + cxt_id++; + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &c, HASH_ENTER, &found); + Assert(!found); + contextid_entry->context_id = cxt_id + 1; + + MemoryContextStatsCounter(c, &grand_totals, &num_contexts); + + path = compute_context_path(c, context_id_lookup); + + PublishMemoryContext(meminfo, cxt_id, c, path, + grand_totals, num_contexts); + } + entry->total_stats = cxt_id + 1; + } + else + { + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + MemoryContextId *contextid_entry; + + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &cur, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = context_id + 1; + + /* + * Figure out the transient context_id of this context and each of + * its ancestors, to compute a path for this context. + */ + path = compute_context_path(cur, context_id_lookup); + + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + /* Account for saving one statistics slot for cumulative reporting */ + if (context_id < (MAX_MEMORY_CONTEXT_STATS_NUM - 1)) + { + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1); + } + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].totalspace += stat.totalspace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].nblocks += stat.nblocks; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freespace += stat.freespace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit per process is reached, write aggregate of the + * remaining statistics. + * + * We can store contexts from 0 to max_stats - 1. When context_id + * is greater than max_stats, we stop reporting individual + * statistics when context_id equals max_stats - 2. As we use + * max_stats - 1 array slot for reporting cumulative statistics or + * "Remaining Totals". + */ + if (context_id == (MAX_MEMORY_CONTEXT_STATS_NUM - 2)) + { + int namelen = strlen("Remaining Totals"); + + num_individual_stats = context_id + 1; + strlcpy(meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].name, + "Remaining Totals", namelen + 1); + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].ident[0] = '\0'; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].path[0] = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].type = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].levels = 0; + } + context_id++; + + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + contexts = lappend(contexts, c); + } + + /* + * Check if there are aggregated statistics or not in the result set. + * Statistics are individually reported when context_id <= max_stats, + * only if context_id > max_stats will there be aggregates. + */ + if (context_id <= MAX_MEMORY_CONTEXT_STATS_NUM) + { + entry->total_stats = context_id; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = 1; + } + + /* + * The number of contexts exceeded the space available, so report the + * number of aggregated memory contexts + */ + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = + context_id - num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for + * cumulative statistics. + */ + entry->total_stats = num_individual_stats + 1; + } + } + + entry->stats_written = true; + /* Notify waiting client backend and return */ + ConditionVariableSignal(&entry->memcxt_cv); + dshash_release_lock(MemoryStatsDsHash, entry); + hash_destroy(context_id_lookup); + + MemoryContextSwitchTo(oldcontext); + MemoryContextDelete(memstats_ctx); +} + +/* + * PublishMemoryContext + * + * Copy the memory context statistics of a single context to a DSA memory + */ +static void +PublishMemoryContext(MemoryStatsEntry *memcxt_info, int curr_id, + MemoryContext context, List *path, + MemoryContextCounters stat, int num_contexts) +{ + char *ident = unconstify(char *, context->ident); + char *name = unconstify(char *, context->name); + + /* + * To be consistent with logging output, we label dynahash contexts with + * just the hash table name as with MemoryContextStatsPrint(). + */ + if (context->ident && strncmp(context->name, "dynahash", 8) == 0) + { + name = unconstify(char *, context->ident); + ident = NULL; + } + + if (name != NULL) + { + int namelen = strlen(name); + + if (namelen >= MEMORY_CONTEXT_NAME_SHMEM_SIZE) + namelen = pg_mbcliplen(name, namelen, + MEMORY_CONTEXT_NAME_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].name, name, namelen + 1); + } + else + /* Clearing the array */ + memcxt_info[curr_id].name[0] = '\0'; + + /* Trim and copy the identifier if it is not set to NULL */ + if (ident != NULL) + { + int idlen = strlen(ident); + + /* + * Some identifiers such as SQL query string can be very long, + * truncate oversize identifiers. + */ + if (idlen >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + idlen = pg_mbcliplen(ident, idlen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].ident, ident, idlen + 1); + } + else + memcxt_info[curr_id].ident[0] = '\0'; + + /* Store the path */ + if (path == NIL) + memcxt_info[curr_id].path[0] = 0; + else + { + int levels = Min(list_length(path), MAX_PATH_DISPLAY_LENGTH); + + memcxt_info[curr_id].path_length = levels; + memcxt_info[curr_id].levels = list_length(path); + + foreach_int(i, path) + { + memcxt_info[curr_id].path[foreach_current_index(i)] = i; + if (--levels == 0) + break; + } + } + memcxt_info[curr_id].type = context->type; + memcxt_info[curr_id].totalspace = stat.totalspace; + memcxt_info[curr_id].nblocks = stat.nblocks; + memcxt_info[curr_id].freespace = stat.freespace; + memcxt_info[curr_id].freechunks = stat.freechunks; + memcxt_info[curr_id].num_agg_stats = num_contexts; +} + +void +AtProcExit_memstats_cleanup(int code, Datum arg) +{ + int idx = MyProcNumber; + MemoryStatsDSHashEntry *entry; + bool found; + + if (MemoryStatsDsHash != NULL) + { + entry = dshash_find_or_insert(MemoryStatsDsHash, &idx, &found); + + if (found) + { + if (MemoryStatsDsaArea != NULL && + DsaPointerIsValid(entry->memstats_dsa_pointer)) + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + } + dshash_delete_entry(MemoryStatsDsHash, entry); + } + LWLockAcquire(MemContextReportingClientKeysLock, LW_EXCLUSIVE); + client_keys[idx] = -1; + LWLockRelease(MemContextReportingClientKeysLock); +} diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index 36ad708b360..e246fc7ce1f 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -39,6 +39,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t PublishMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index b59e08605cc..ba5d3bf0d7a 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -669,6 +669,13 @@ BaseInit(void) * drop ephemeral slots, which in turn triggers stats reporting. */ ReplicationSlotInitialize(); + + /* + * The before shmem exit callback frees the DSA memory occupied by the + * latest memory context statistics that could be published by this proc + * if requested. + */ + before_shmem_exit(AtProcExit_memstats_cleanup, 0); } diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 073bdb35d2a..2a826cbef17 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -1011,6 +1011,37 @@ MemoryContextStatsInternal(MemoryContext context, int level, } } + +/* + * MemoryContextStatsCounter + * + * Accumulate statistics counts into *totals. totals should not be NULL. + * This involves a non-recursive tree traversal. + */ +void +MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts) +{ + int ichild = 1; + + Assert(totals != NULL); + context->methods->stats(context, NULL, NULL, totals, false); + + for (MemoryContext curr = context->firstchild; + curr != NULL; + curr = MemoryContextTraverseNext(curr, context)) + { + curr->methods->stats(curr, NULL, NULL, totals, false); + ichild++; + } + + /* + * Add the count of all the children contexts which are traversed + * including the parent. + */ + *num_contexts = ichild; +} + /* * MemoryContextStatsPrint * Print callback used by MemoryContextStatsInternal diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index dac40992cbc..3667bc2bf41 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8641,6 +8641,16 @@ prorettype => 'bool', proargtypes => 'int4', prosrc => 'pg_log_backend_memory_contexts' }, +# publishing memory contexts of the specified postgres process +{ oid => '2173', descr => 'publish memory contexts of the specified process', + proname => 'pg_get_process_memory_contexts', provolatile => 'v', + prorows => '100', proretset => 't', proparallel => 'u', + prorettype => 'record', proargtypes => 'int4 bool', + proallargtypes => '{int4,bool,text,text,text,int4,_int4,int8,int8,int8,int8,int8,int4}', + proargmodes => '{i,i,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{pid, summary, name, ident, type, level, path, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes, num_agg_contexts}', + prosrc => 'pg_get_process_memory_contexts' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index f16f35659b9..38e1e55a9a6 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -96,6 +96,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t PublishMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h index e94ebce95b9..6dd80689bbb 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -87,6 +87,7 @@ PG_LWLOCK(52, SerialControl) PG_LWLOCK(53, AioWorkerSubmissionQueue) PG_LWLOCK(54, WaitLSN) PG_LWLOCK(55, LogicalDecodingControl) +PG_LWLOCK(56, MemContextReportingClientKeys) /* * There also exist several built-in LWLock tranches. As with the predefined diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index 348fba53a93..00ccfcb22a4 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -35,6 +35,7 @@ typedef enum PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to send the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ PROCSIG_RECOVERY_CONFLICT, /* backend is blocking recovery, check * PGPROC->pendingRecoveryConflicts for the diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 11ab1717a16..40099036308 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -319,4 +319,11 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) return true; } +extern void ProcessGetMemoryContextInterrupt(void); +extern void HandleGetMemoryContextInterrupt(void); +extern void MemoryContextKeysShmemInit(void); +extern Size MemoryContextKeysShmemSize(void); +extern void MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts); +extern void AtProcExit_memstats_cleanup(int code, Datum arg); #endif /* MEMUTILS_H */ diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 3dd63fd88ed..c84e5adce16 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -233,3 +233,22 @@ select * from pg_timezone_abbrevs where abbrev = 'LMT'; LMT | @ 7 hours 52 mins 58 secs ago | f (1 row) +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; +NOTICE: (AllocSet,TopMemoryContext,) +NOTICE: (AllocSet,TopMemoryContext,) diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 004f9a70e00..d34d2b6211c 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -101,3 +101,21 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs; -- One specific case we can check without much fear of breakage -- is the historical local-mean-time value used for America/Los_Angeles. select * from pg_timezone_abbrevs where abbrev = 'LMT'; + +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 241945734ec..f90ff78a78b 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1709,6 +1709,8 @@ MemoryContextData MemoryContextId MemoryContextMethodID MemoryContextMethods +MemoryStatsEntry +MemoryStatsDSHashEntry MemoryStatsPrintFunc MergeAction MergeActionState -- 2.34.1 [application/octet-stream] v52-0002-Test-module-to-test-memory-context-reporting-wit.patch (8.9K, ../../CAH2L28sZ5ELWzXgRNJEYpOR8jk6DdWETgJ7L-xYa9SXQ+n5Z7w@mail.gmail.com/4-v52-0002-Test-module-to-test-memory-context-reporting-wit.patch) download | inline diff: From 705b52ec7d0c66352e0c31397ff5b31f1e018f0d Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Fri, 28 Nov 2025 14:46:38 +0530 Subject: [PATCH 2/2] Test module to test memory context reporting with injection points --- src/test/modules/Makefile | 1 + .../test_memcontext_reporting/Makefile | 25 +++ .../t/001_memcontext_inj.pl | 154 ++++++++++++++++++ .../test_memcontext_reporting.c | 12 ++ .../test_memcontext_reporting.control | 4 + 5 files changed, 196 insertions(+) create mode 100644 src/test/modules/test_memcontext_reporting/Makefile create mode 100644 src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index 44c7163c1cd..8ce9598495c 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -34,6 +34,7 @@ SUBDIRS = \ test_json_parser \ test_lfind \ test_lwlock_tranches \ + test_memcontext_reporting \ test_misc \ test_oat_hooks \ test_parser \ diff --git a/src/test/modules/test_memcontext_reporting/Makefile b/src/test/modules/test_memcontext_reporting/Makefile new file mode 100644 index 00000000000..47bce45bc1d --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/Makefile @@ -0,0 +1,25 @@ +# src/test/modules/test_memcontext_reporting/Makefile + +TAP_TESTS = 1 + +EXTRA_INSTALL = src/test/modules/injection_points + +export enable_injection_points +MODULE_big = test_memcontext_reporting +OBJS = \ + $(WIN32RES) \ + test_memcontext_reporting.o +PGFILEDESC = "test_memcontext_reporting - test code for memory context reporting" + +REGRESS = test_memcontext_reporting + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_memcontext_reporting +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl new file mode 100644 index 00000000000..73665abf902 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl @@ -0,0 +1,154 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test suite for testing memory context statistics reporting + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} +my $psql_err; +my $psql_out; +# Create and start a cluster with one node +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->append_conf( + 'postgresql.conf', + qq[ +max_connections = 100 +log_statement = none +restart_after_crash = false +]); +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); + +# Attaching to a client process's injection point that throws an error +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-client-injection', 'error');" +); + +my $pid = $node->safe_psql('postgres', + "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); + +#Client should have thrown error +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); +like($psql_err, + qr/error triggered for injection point memcontext-client-injection/); + +#Query the same process after detaching the injection point, using some other client and it should succeed. +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-client-injection');"); +my $topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name eq 'TopMemoryContext'); + +# Attaching to a target process injection point that throws an error +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-server-injection', 'error');" +); + +#Server should have thrown error +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); + +#Query the same process after detaching the injection point, using some other client and it should succeed. +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-injection');"); +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name eq 'TopMemoryContext'); + +# Test that two concurrent requests to the same process results in a warning for +# one of those + +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-client-injection', 'wait');"); +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-server-wait', 'wait');"); +my $psql_session1 = $node->background_psql('postgres'); + +# We use query_until so that we can execute additional commands without having to +# wait for this command to complete, since it will pause at injection points. + +$psql_session1->query_until( + qr//, + qq( + SELECT pg_get_process_memory_contexts($pid, true); +)); +$node->wait_for_event('client backend', 'memcontext-client-injection'); +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); +ok($psql_err =~ + /WARNING: server process $pid is processing previous request/); +#Wake the client up. +$node->safe_psql('postgres', + "SELECT injection_points_wakeup('memcontext-client-injection');"); + +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-client-injection');"); +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-wait');"); + +# Test the client process exiting with timeout does not break the server process + +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-server-wait', 'wait');"); +# Following client query times out, returning NULL as output +$node->psql( + 'postgres', + qq(select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';), + stdout => \$psql_out); +is($psql_out, '', 'Query returns null'); +#Wakeup the server process up and detach the injection point. +$node->safe_psql('postgres', + "SELECT injection_points_wakeup('memcontext-server-wait');"); +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-wait');"); +#Query the same server process again and it should succeed. +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name eq 'TopMemoryContext'); + +# Test if the monitoring works fine, when the client backend crashes. +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-client-injection', 'test_memcontext_reporting', 'crash', NULL);" +); + +#Client will crash +$node->psql( + 'postgres', + qq(select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';), + stderr => \$psql_err); +like($psql_err, + qr/WARNING: terminating connection because of crash of another server process|server closed the connection unexpectedly|connection to server was lost|could not send data to server/ +); + +# Wait till server restarts +$node->restart; +$node->poll_query_until('postgres', "SELECT 1;", '1'); + +#Querying memory stats should succeed after server start +$pid = $node->safe_psql('postgres', + "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name eq 'TopMemoryContext'); + +done_testing(); diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c new file mode 100644 index 00000000000..d641f3616dc --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c @@ -0,0 +1,12 @@ +#include "postgres.h" +#include "funcapi.h" + +PG_MODULE_MAGIC; + +extern PGDLLEXPORT void crash(const char *name, const void *private_data, void *arg); + +void +crash(const char *name, const void *private_data, void *arg) +{ + abort(); +} diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control new file mode 100644 index 00000000000..48b501682d5 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control @@ -0,0 +1,4 @@ +comment = 'Test code for memcontext reporting' +default_version = '1.0' +module_pathname = '$libdir/test_memcontext_reporting' +relocatable = true -- 2.34.1 ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2026-02-23 09:58 Daniel Gustafsson <[email protected]> parent: Rahila Syed <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Daniel Gustafsson @ 2026-02-23 09:58 UTC (permalink / raw) To: Rahila Syed <[email protected]>; +Cc: pgsql-hackers; torikoshia <[email protected]>; Robert Haas <[email protected]>; Chao Li <[email protected]> > On 21 Feb 2026, at 00:13, Rahila Syed <[email protected]> wrote: > Please find attached updated and rebased patches which incorporate > code fixes suggested off-list by Daniel > > These changes include a fix to free dsa memory in one of the > exit paths from the client-side function along with several documentation > and comment improvements. Thanks for the new version, I think this is looking pretty good now. The attached .diff.txt (renamed to keep the CFBot from grabbing it) has mostly whitespace fixes, but also a few small things which are discussed below: + <para> + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't Re-reading the docs I think the para's are in the wrong order, as the sentence above makes little sense to be after the output has already been discussed. I think this needs to go earlier, see the attached .diff.txt for what I am proposing. + /* + * The client process should have created the required DSA and DSHash + * table. Here we just attach to those. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); + + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); I think this needs an expanded comment discussing why there is no need the check the DSA and DShash after the GetNamed_ functions, since they can be NULL going in to this. The attached .diff.txt has a proposal along with an Assertion to keep future changes of the API from risking subtly breaking this assumption. +REGRESS = test_memcontext_reporting Since the test module doesn't contain any pg_regress style .sql/.out tests so this line in the Makefile cause a test failure when running with Autoconf. +$node->append_conf( + 'postgresql.conf', + qq[ +max_connections = 100 +log_statement = none +restart_after_crash = false +]); Why do we need to set max_connections for this test? +#Server should have thrown error +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); This test doesn't validate that the server actually errored does it? (There is no proposed fix in the attached.) -- Daniel Gustafsson diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 120f032f249..aa83b11e131 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -274,10 +274,19 @@ <para> This function handles requests to display the memory contexts of a <productname>PostgreSQL</productname> process with the specified - process ID. The function can be used to send requests to backends as - well as <glossterm linkend="glossary-auxiliary-proc">auxiliary processes</glossterm>. - If the process does not respond with memory contexts statistics in 5 seconds, - the function returns NULL. + process ID. The function can be used to send requests to + <glossterm linkend="glossary-backend">backends</glossterm> as well as + <glossterm linkend="glossary-auxiliary-proc">auxiliary processes</glossterm>. + If the process does not respond with memory contexts statistics in 5 + seconds, the function returns NULL. + </para> + <para> + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context + statistics are aggregated and a cumulative total is displayed. The + <literal>num_agg_contexts</literal> column indicates the number of + contexts aggregated in the displayed statistics. </para> <para> The returned record contains extended statistics per each memory @@ -350,7 +359,8 @@ <listitem> <para> <parameter>num_agg_contexts</parameter> - The number of memory - contexts aggregated in the displayed statistics. + contexts aggregated in the displayed statistics. <literal>1</literal> + indicates that context statistics are displayed separately. </para> </listitem> </itemizedlist> @@ -365,16 +375,6 @@ <parameter>summary</parameter> is <literal>false</literal> (the default), <literal>the num_agg_contexts</literal> value is <literal>1</literal>, indicating that individual statistics are being displayed. - </para> - <para> - After receiving memory context statistics from the target process, it - returns the results as one row per context. If all the contexts don't - fit within the pre-determined size limit, the remaining context - statistics are aggregated and a cumulative total is displayed. The - <literal>num_agg_contexts</literal> column indicates the number of - contexts aggregated in the displayed statistics. A - <literal>num_agg_contexts</literal> value of <literal>1</literal> - indicates that context statistics are displayed separately. </para></entry> </row> </tbody> diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index f18b6d47c60..fdf0043211b 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -978,7 +978,11 @@ ProcessGetMemoryContextInterrupt(void) /* * The client process should have created the required DSA and DSHash - * table. Here we just attach to those. + * table, so we can connect to these directly. The GetNamedXXX functions + * either return DSA/DSHash or error out (which we want to avoid as that + * would cause the current transaction to fail) so we don't need to check + * for NULL. An assertion is enough to catch in case this API should be + * changed at some point. */ if (MemoryStatsDsaArea == NULL) MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", @@ -988,6 +992,8 @@ ProcessGetMemoryContextInterrupt(void) MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", &memctx_dsh_params, &found); + Assert(MemoryStatsDsaArea != NULL && MemoryStatsDsHash != NULL); + /* * The entry lock is held by dshash_find_or_insert to protect writes to * process specific memory. Two different processes publishing statistics @@ -1001,9 +1007,8 @@ ProcessGetMemoryContextInterrupt(void) * if the caller has timed out waiting for us and have issued a request to * another backend. * - * Make sure that the client always deletes the entry after taking - * required lock or this function may end up writing to unallocated - * memory. + * Make sure that the client always deletes the entry after taking the + * required lock or this function may end up writing to unallocated memory. */ if (!found || entry->target_server_id != MyProcPid) { @@ -1042,13 +1047,11 @@ ProcessGetMemoryContextInterrupt(void) HASH_ENTER, &found); Assert(!found); - /* - * context id starts with 1 - */ + /* context id starts with 1 */ contextid_entry->context_id = cxt_id + 1; /* - * Copy statistics for each of TopMemoryContexts children. This + * Copy statistics for each of TopMemoryContext's children. This * includes statistics of at most 100 children per node, with each * child node limited to a depth of 100 in its subtree. */ diff --git a/src/test/modules/test_memcontext_reporting/Makefile b/src/test/modules/test_memcontext_reporting/Makefile index 47bce45bc1d..2ce169ffa69 100644 --- a/src/test/modules/test_memcontext_reporting/Makefile +++ b/src/test/modules/test_memcontext_reporting/Makefile @@ -11,8 +11,6 @@ OBJS = \ test_memcontext_reporting.o PGFILEDESC = "test_memcontext_reporting - test code for memory context reporting" -REGRESS = test_memcontext_reporting - ifdef USE_PGXS PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl index 73665abf902..152465c1570 100644 --- a/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl +++ b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl @@ -21,7 +21,6 @@ $node->init; $node->append_conf( 'postgresql.conf', qq[ -max_connections = 100 log_statement = none restart_after_crash = false ]); @@ -36,7 +35,7 @@ $node->safe_psql('postgres', my $pid = $node->safe_psql('postgres', "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); -#Client should have thrown error +# Client should have thrown error $node->psql( 'postgres', qq(select pg_get_process_memory_contexts($pid, true);), @@ -44,7 +43,8 @@ $node->psql( like($psql_err, qr/error triggered for injection point memcontext-client-injection/); -#Query the same process after detaching the injection point, using some other client and it should succeed. +# Query the same process after detaching the injection point, using some other +# client and it should succeed. $node->safe_psql('postgres', "select injection_points_detach('memcontext-client-injection');"); my $topcontext_name = $node->safe_psql('postgres', @@ -57,13 +57,14 @@ $node->safe_psql('postgres', "select injection_points_attach('memcontext-server-injection', 'error');" ); -#Server should have thrown error +# Server should have thrown error $node->psql( 'postgres', qq(select pg_get_process_memory_contexts($pid, true);), stderr => \$psql_err); -#Query the same process after detaching the injection point, using some other client and it should succeed. +# Query the same process after detaching the injection point, using some other +# client and it should succeed. $node->safe_psql('postgres', "select injection_points_detach('memcontext-server-injection');"); $topcontext_name = $node->safe_psql('postgres', @@ -71,8 +72,8 @@ $topcontext_name = $node->safe_psql('postgres', ); ok($topcontext_name eq 'TopMemoryContext'); -# Test that two concurrent requests to the same process results in a warning for -# one of those +# Test that two concurrent requests to the same process results in a warning +# for one of those $node->safe_psql('postgres', "SELECT injection_points_attach('memcontext-client-injection', 'wait');"); @@ -80,8 +81,9 @@ $node->safe_psql('postgres', "SELECT injection_points_attach('memcontext-server-wait', 'wait');"); my $psql_session1 = $node->background_psql('postgres'); -# We use query_until so that we can execute additional commands without having to -# wait for this command to complete, since it will pause at injection points. +# We use query_until so that we can execute additional commands without having +# to wait for this command to complete, since it will pause at injection +# points. $psql_session1->query_until( qr//, @@ -95,7 +97,7 @@ $node->psql( stderr => \$psql_err); ok($psql_err =~ /WARNING: server process $pid is processing previous request/); -#Wake the client up. +# Wake the client up. $node->safe_psql('postgres', "SELECT injection_points_wakeup('memcontext-client-injection');"); @@ -104,7 +106,8 @@ $node->safe_psql('postgres', $node->safe_psql('postgres', "select injection_points_detach('memcontext-server-wait');"); -# Test the client process exiting with timeout does not break the server process +# Test the client process exiting with timeout does not break the server +# process $node->safe_psql('postgres', "SELECT injection_points_attach('memcontext-server-wait', 'wait');"); @@ -114,12 +117,12 @@ $node->psql( qq(select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';), stdout => \$psql_out); is($psql_out, '', 'Query returns null'); -#Wakeup the server process up and detach the injection point. +# Wakeup the server process up and detach the injection point. $node->safe_psql('postgres', "SELECT injection_points_wakeup('memcontext-server-wait');"); $node->safe_psql('postgres', "select injection_points_detach('memcontext-server-wait');"); -#Query the same server process again and it should succeed. +# Query the same server process again and it should succeed. $topcontext_name = $node->safe_psql('postgres', "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" ); @@ -130,7 +133,7 @@ $node->safe_psql('postgres', "select injection_points_attach('memcontext-client-injection', 'test_memcontext_reporting', 'crash', NULL);" ); -#Client will crash +# Client will crash $node->psql( 'postgres', qq(select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';), @@ -143,7 +146,7 @@ like($psql_err, $node->restart; $node->poll_query_until('postgres', "SELECT 1;", '1'); -#Querying memory stats should succeed after server start +# Querying memory stats should succeed after server start $pid = $node->safe_psql('postgres', "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); $topcontext_name = $node->safe_psql('postgres', Attachments: [text/plain] v52comments.diff.txt (10.7K, ../../[email protected]/2-v52comments.diff.txt) download | inline diff: diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 120f032f249..aa83b11e131 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -274,10 +274,19 @@ <para> This function handles requests to display the memory contexts of a <productname>PostgreSQL</productname> process with the specified - process ID. The function can be used to send requests to backends as - well as <glossterm linkend="glossary-auxiliary-proc">auxiliary processes</glossterm>. - If the process does not respond with memory contexts statistics in 5 seconds, - the function returns NULL. + process ID. The function can be used to send requests to + <glossterm linkend="glossary-backend">backends</glossterm> as well as + <glossterm linkend="glossary-auxiliary-proc">auxiliary processes</glossterm>. + If the process does not respond with memory contexts statistics in 5 + seconds, the function returns NULL. + </para> + <para> + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context + statistics are aggregated and a cumulative total is displayed. The + <literal>num_agg_contexts</literal> column indicates the number of + contexts aggregated in the displayed statistics. </para> <para> The returned record contains extended statistics per each memory @@ -350,7 +359,8 @@ <listitem> <para> <parameter>num_agg_contexts</parameter> - The number of memory - contexts aggregated in the displayed statistics. + contexts aggregated in the displayed statistics. <literal>1</literal> + indicates that context statistics are displayed separately. </para> </listitem> </itemizedlist> @@ -365,16 +375,6 @@ <parameter>summary</parameter> is <literal>false</literal> (the default), <literal>the num_agg_contexts</literal> value is <literal>1</literal>, indicating that individual statistics are being displayed. - </para> - <para> - After receiving memory context statistics from the target process, it - returns the results as one row per context. If all the contexts don't - fit within the pre-determined size limit, the remaining context - statistics are aggregated and a cumulative total is displayed. The - <literal>num_agg_contexts</literal> column indicates the number of - contexts aggregated in the displayed statistics. A - <literal>num_agg_contexts</literal> value of <literal>1</literal> - indicates that context statistics are displayed separately. </para></entry> </row> </tbody> diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index f18b6d47c60..fdf0043211b 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -978,7 +978,11 @@ ProcessGetMemoryContextInterrupt(void) /* * The client process should have created the required DSA and DSHash - * table. Here we just attach to those. + * table, so we can connect to these directly. The GetNamedXXX functions + * either return DSA/DSHash or error out (which we want to avoid as that + * would cause the current transaction to fail) so we don't need to check + * for NULL. An assertion is enough to catch in case this API should be + * changed at some point. */ if (MemoryStatsDsaArea == NULL) MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", @@ -988,6 +992,8 @@ ProcessGetMemoryContextInterrupt(void) MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", &memctx_dsh_params, &found); + Assert(MemoryStatsDsaArea != NULL && MemoryStatsDsHash != NULL); + /* * The entry lock is held by dshash_find_or_insert to protect writes to * process specific memory. Two different processes publishing statistics @@ -1001,9 +1007,8 @@ ProcessGetMemoryContextInterrupt(void) * if the caller has timed out waiting for us and have issued a request to * another backend. * - * Make sure that the client always deletes the entry after taking - * required lock or this function may end up writing to unallocated - * memory. + * Make sure that the client always deletes the entry after taking the + * required lock or this function may end up writing to unallocated memory. */ if (!found || entry->target_server_id != MyProcPid) { @@ -1042,13 +1047,11 @@ ProcessGetMemoryContextInterrupt(void) HASH_ENTER, &found); Assert(!found); - /* - * context id starts with 1 - */ + /* context id starts with 1 */ contextid_entry->context_id = cxt_id + 1; /* - * Copy statistics for each of TopMemoryContexts children. This + * Copy statistics for each of TopMemoryContext's children. This * includes statistics of at most 100 children per node, with each * child node limited to a depth of 100 in its subtree. */ diff --git a/src/test/modules/test_memcontext_reporting/Makefile b/src/test/modules/test_memcontext_reporting/Makefile index 47bce45bc1d..2ce169ffa69 100644 --- a/src/test/modules/test_memcontext_reporting/Makefile +++ b/src/test/modules/test_memcontext_reporting/Makefile @@ -11,8 +11,6 @@ OBJS = \ test_memcontext_reporting.o PGFILEDESC = "test_memcontext_reporting - test code for memory context reporting" -REGRESS = test_memcontext_reporting - ifdef USE_PGXS PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl index 73665abf902..152465c1570 100644 --- a/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl +++ b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl @@ -21,7 +21,6 @@ $node->init; $node->append_conf( 'postgresql.conf', qq[ -max_connections = 100 log_statement = none restart_after_crash = false ]); @@ -36,7 +35,7 @@ $node->safe_psql('postgres', my $pid = $node->safe_psql('postgres', "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); -#Client should have thrown error +# Client should have thrown error $node->psql( 'postgres', qq(select pg_get_process_memory_contexts($pid, true);), @@ -44,7 +43,8 @@ $node->psql( like($psql_err, qr/error triggered for injection point memcontext-client-injection/); -#Query the same process after detaching the injection point, using some other client and it should succeed. +# Query the same process after detaching the injection point, using some other +# client and it should succeed. $node->safe_psql('postgres', "select injection_points_detach('memcontext-client-injection');"); my $topcontext_name = $node->safe_psql('postgres', @@ -57,13 +57,14 @@ $node->safe_psql('postgres', "select injection_points_attach('memcontext-server-injection', 'error');" ); -#Server should have thrown error +# Server should have thrown error $node->psql( 'postgres', qq(select pg_get_process_memory_contexts($pid, true);), stderr => \$psql_err); -#Query the same process after detaching the injection point, using some other client and it should succeed. +# Query the same process after detaching the injection point, using some other +# client and it should succeed. $node->safe_psql('postgres', "select injection_points_detach('memcontext-server-injection');"); $topcontext_name = $node->safe_psql('postgres', @@ -71,8 +72,8 @@ $topcontext_name = $node->safe_psql('postgres', ); ok($topcontext_name eq 'TopMemoryContext'); -# Test that two concurrent requests to the same process results in a warning for -# one of those +# Test that two concurrent requests to the same process results in a warning +# for one of those $node->safe_psql('postgres', "SELECT injection_points_attach('memcontext-client-injection', 'wait');"); @@ -80,8 +81,9 @@ $node->safe_psql('postgres', "SELECT injection_points_attach('memcontext-server-wait', 'wait');"); my $psql_session1 = $node->background_psql('postgres'); -# We use query_until so that we can execute additional commands without having to -# wait for this command to complete, since it will pause at injection points. +# We use query_until so that we can execute additional commands without having +# to wait for this command to complete, since it will pause at injection +# points. $psql_session1->query_until( qr//, @@ -95,7 +97,7 @@ $node->psql( stderr => \$psql_err); ok($psql_err =~ /WARNING: server process $pid is processing previous request/); -#Wake the client up. +# Wake the client up. $node->safe_psql('postgres', "SELECT injection_points_wakeup('memcontext-client-injection');"); @@ -104,7 +106,8 @@ $node->safe_psql('postgres', $node->safe_psql('postgres', "select injection_points_detach('memcontext-server-wait');"); -# Test the client process exiting with timeout does not break the server process +# Test the client process exiting with timeout does not break the server +# process $node->safe_psql('postgres', "SELECT injection_points_attach('memcontext-server-wait', 'wait');"); @@ -114,12 +117,12 @@ $node->psql( qq(select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';), stdout => \$psql_out); is($psql_out, '', 'Query returns null'); -#Wakeup the server process up and detach the injection point. +# Wakeup the server process up and detach the injection point. $node->safe_psql('postgres', "SELECT injection_points_wakeup('memcontext-server-wait');"); $node->safe_psql('postgres', "select injection_points_detach('memcontext-server-wait');"); -#Query the same server process again and it should succeed. +# Query the same server process again and it should succeed. $topcontext_name = $node->safe_psql('postgres', "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" ); @@ -130,7 +133,7 @@ $node->safe_psql('postgres', "select injection_points_attach('memcontext-client-injection', 'test_memcontext_reporting', 'crash', NULL);" ); -#Client will crash +# Client will crash $node->psql( 'postgres', qq(select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';), @@ -143,7 +146,7 @@ like($psql_err, $node->restart; $node->poll_query_until('postgres', "SELECT 1;", '1'); -#Querying memory stats should succeed after server start +# Querying memory stats should succeed after server start $pid = $node->safe_psql('postgres', "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); $topcontext_name = $node->safe_psql('postgres', ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2026-02-24 11:57 Rahila Syed <[email protected]> parent: Daniel Gustafsson <[email protected]> 0 siblings, 1 reply; 52+ messages in thread From: Rahila Syed @ 2026-02-24 11:57 UTC (permalink / raw) To: Daniel Gustafsson <[email protected]>; +Cc: pgsql-hackers; torikoshia <[email protected]>; Robert Haas <[email protected]>; Chao Li <[email protected]> Hi Daniel, Thank you for the review. All the changes suggested in the v52comments.diff are incorporated in the attached patches. +#Server should have thrown error > +$node->psql( > + 'postgres', > + qq(select pg_get_process_memory_contexts($pid, true);), > + stderr => \$psql_err); > > This test doesn't validate that the server actually errored does it? > (There is > no proposed fix in the attached.) > > This has been fixed by adding a check for the error returned by the above command. While at it, I also added another crash test to the file, This is similar to the existing test for a client backend crash, but in this scenario, it crashes the server process instead. Thank you, Rahila Syed Attachments: [application/octet-stream] v53-0001-Add-function-to-report-memory-context-statistics.patch (60.1K, ../../CAH2L28sH3VoQDHE_kqgrmPMxUq7zdWzUWVB6kg-vu+c0_42MpA@mail.gmail.com/3-v53-0001-Add-function-to-report-memory-context-statistics.patch) download | inline diff: From 5fed1c8eb14eb0bbf9522cecead9dec86fa38a9a Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Thu, 27 Nov 2025 14:39:43 +0530 Subject: [PATCH 1/2] Add function to report memory context statistics This adds a function for retrieving memory context statistics and information from backends as well as auxiliary processes. The intended usecase is cluster debugging when under memory pressure or unanticipated memory usage characteristics. When calling the function it sends a signal to the specified process to submit statistics regarding its memory contexts into dynamic shared memory. Each memory context is returned in detail, followed by a cumulative total in case the number of contexts exceed the max allocated amount of shared memory. Each process is limited to use at most 1Mb memory for this. A summary can also be explicitly requested by the user, this will return the TopMemoryContext and a cumulative total of all lower contexts. In order to not block on busy processes, we have hardcoded the number of seconds during which to retry before timing out. In the case where no statistics are published within the set timeout, NULL is returned --- doc/src/sgml/func/func-admin.sgml | 159 +++ src/backend/catalog/system_functions.sql | 14 + src/backend/postmaster/autovacuum.c | 4 + src/backend/postmaster/checkpointer.c | 4 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 4 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 4 + src/backend/storage/ipc/dsm_registry.c | 8 +- src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 2 + src/backend/utils/adt/mcxtfuncs.c | 1023 ++++++++++++++++- src/backend/utils/init/globals.c | 1 + src/backend/utils/init/postinit.c | 7 + src/backend/utils/mmgr/mcxt.c | 31 + src/include/catalog/pg_proc.dat | 10 + src/include/miscadmin.h | 1 + src/include/storage/lwlocklist.h | 1 + src/include/storage/procsignal.h | 1 + src/include/utils/memutils.h | 7 + src/test/regress/expected/sysviews.out | 19 + src/test/regress/sql/sysviews.sql | 18 + src/tools/pgindent/typedefs.list | 2 + 25 files changed, 1315 insertions(+), 22 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 3ac81905d1f..aa83b11e131 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -251,6 +251,132 @@ <literal>false</literal> is returned. </para></entry> </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_get_process_memory_contexts</primary> + </indexterm> + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type> <optional>,<parameter>summary</parameter> <type>boolean</type> <literal>DEFAULT</literal> <literal>false</literal></optional> ) + <returnvalue>setof record</returnvalue> + ( <parameter>name</parameter> <type>text</type>, + <parameter>ident</parameter> <type>text</type>, + <parameter>type</parameter> <type>text</type>, + <parameter>level</parameter> <type>integer</type>, + <parameter>path</parameter> <type>integer[]</type>, + <parameter>total_bytes</parameter> <type>bigint</type>, + <parameter>total_nblocks</parameter> <type>bigint</type>, + <parameter>free_bytes</parameter> <type>bigint</type>, + <parameter>free_chunks</parameter> <type>bigint</type>, + <parameter>used_bytes</parameter> <type>bigint</type>, + <parameter>num_agg_contexts</parameter> <type>integer</type> ) + </para> + <para> + This function handles requests to display the memory contexts of a + <productname>PostgreSQL</productname> process with the specified + process ID. The function can be used to send requests to + <glossterm linkend="glossary-backend">backends</glossterm> as well as + <glossterm linkend="glossary-auxiliary-proc">auxiliary processes</glossterm>. + If the process does not respond with memory contexts statistics in 5 + seconds, the function returns NULL. + </para> + <para> + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context + statistics are aggregated and a cumulative total is displayed. The + <literal>num_agg_contexts</literal> column indicates the number of + contexts aggregated in the displayed statistics. + </para> + <para> + The returned record contains extended statistics per each memory + context: + <itemizedlist spacing="compact"> + <listitem> + <para> + <parameter>name</parameter> - The name of the memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>ident</parameter> - Memory context ID (if any). + </para> + </listitem> + <listitem> + <para> + <parameter>type</parameter> - The type of memory context, possible + values are: AllocSet, Generation, Slab and Bump. + </para> + </listitem> + <listitem> + <para> + <parameter>level</parameter> - The level in the tree of the current + memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>path</parameter> - Memory contexts are organized in a + tree model with TopMemoryContext as the root, and all other memory + contexts as nodes in the tree. The <parameter>path</parameter> + displays the path from the root to the current memory context. The + path is limited to 100 children per node, with each node limited + to a max depth of 100, to preserve memory during reporting. The + printed path will also be limited to 100 nodes counting from the + TopMemoryContext. + </para> + </listitem> + <listitem> + <para> + <parameter>total_bytes</parameter> - The total number of bytes + allocated to this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_nblocks</parameter> - The total number of blocks + used for the allocated memory. + </para> + </listitem> + <listitem> + <para> + <parameter>free_bytes</parameter> - The amount of free memory in + this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>free_chunks</parameter> - The number of chunks that + <parameter>free_bytes</parameter> corresponds to. + </para> + </listitem> + <listitem> + <para> + <parameter>used_bytes</parameter> - The total number of bytes + currently occupied. + </para> + </listitem> + <listitem> + <para> + <parameter>num_agg_contexts</parameter> - The number of memory + contexts aggregated in the displayed statistics. <literal>1</literal> + indicates that context statistics are displayed separately. + </para> + </listitem> + </itemizedlist> + </para> + <para> + When <parameter>summary</parameter> is <literal>true</literal>, statistics + for memory contexts at levels 1 and 2 are displayed, with level 1 + representing the root node (i.e., <literal>TopMemoryContext</literal>). + Statistics for contexts on level 2 and below are aggregates of all + child contexts' statistics, where <literal>num_agg_contexts</literal> + indicate the number of aggregated child contexts. When + <parameter>summary</parameter> is <literal>false</literal> (the default), + <literal>the num_agg_contexts</literal> value is <literal>1</literal>, + indicating that individual statistics are being displayed. + </para></entry> + </row> </tbody> </tgroup> </table> @@ -302,6 +428,39 @@ LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 because it may generate a large number of log messages. </para> + <para> + <function>pg_get_process_memory_contexts</function> can be used to request + memory contexts statistics of any <productname>PostgreSQL</productname> + process. For example: +<programlisting> +postgres=# SELECT * FROM pg_get_process_memory_contexts( + (SELECT pid FROM pg_stat_activity + WHERE backend_type = 'checkpointer'), + false) LIMIT 1; +-[ RECORD 1 ]----+------------------------------ +name | TopMemoryContext +ident | +type | AllocSet +level | 1 +path | {1} +total_bytes | 90304 +total_nblocks | 3 +free_bytes | 2880 +free_chunks | 1 +used_bytes | 87424 +num_agg_contexts | 1 +</programlisting> + <note> + <para> + While <function>pg_get_process_memory_contexts</function> can be used to + query memory contexts of the local backend, + <structname>pg_backend_memory_contexts</structname> + (see <xref linkend="view-pg-backend-memory-contexts"/> for more details) + will be less resource intensive when only the local backend is of interest. + </para> + </note> + </para> + </sect2> <sect2 id="functions-admin-backup"> diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql index 69699f8830a..b62944cee87 100644 --- a/src/backend/catalog/system_functions.sql +++ b/src/backend/catalog/system_functions.sql @@ -378,6 +378,17 @@ BEGIN ATOMIC END; +CREATE OR REPLACE FUNCTION + pg_get_process_memory_contexts(IN pid integer, IN summary boolean DEFAULT false, + OUT name text, OUT ident text, OUT type text, OUT level integer, + OUT path integer[], OUT total_bytes bigint, OUT total_nblocks bigint, + OUT free_bytes bigint, OUT free_chunks bigint, OUT used_bytes bigint, + OUT num_agg_contexts integer) +RETURNS SETOF RECORD +LANGUAGE INTERNAL +STRICT VOLATILE PARALLEL UNSAFE +AS 'pg_get_process_memory_contexts'; + -- -- The default permissions for functions mean that anyone can execute them. -- A number of functions shouldn't be executable by just anyone, but rather @@ -503,6 +514,7 @@ REVOKE EXECUTE ON FUNCTION pg_ls_logicalmapdir() FROM PUBLIC; REVOKE EXECUTE ON FUNCTION pg_ls_replslotdir(text) FROM PUBLIC; +REVOKE EXECUTE ON FUNCTION pg_get_process_memory_contexts(integer, boolean) FROM PUBLIC; -- -- We also set up some things as accessible to standard roles. -- @@ -529,6 +541,8 @@ GRANT EXECUTE ON FUNCTION pg_current_logfile() TO pg_monitor; GRANT EXECUTE ON FUNCTION pg_current_logfile(text) TO pg_monitor; +GRANT EXECUTE ON FUNCTION pg_get_process_memory_contexts(integer, boolean) TO pg_read_all_stats; + GRANT pg_read_all_settings TO pg_monitor; GRANT pg_read_all_stats TO pg_monitor; diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 6fde740465f..3c24119c43a 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -787,6 +787,10 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index e03c19123bc..d54193f8890 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -684,6 +684,10 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index a2c0ff012c5..cb18ce80893 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -48,6 +48,10 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index 82731e452fc..e13a40f1427 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -870,6 +870,10 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index cdbe53dd262..1d6d8f22d20 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -192,6 +192,10 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index 742137edad6..d5a3cb13a4c 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -875,6 +875,10 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c index 068c1577b12..4810babac1b 100644 --- a/src/backend/storage/ipc/dsm_registry.c +++ b/src/backend/storage/ipc/dsm_registry.c @@ -270,7 +270,9 @@ GetNamedDSMSegment(const char *name, size_t size, * This routine returns a pointer to the DSA. A new LWLock tranche ID will be * generated if needed. Note that the lock tranche will be registered with the * provided name. Also note that this should be called at most once for a - * given DSA in each backend. + * given DSA in each backend. It's best to minimize the number of errors thrown + * in this function since it's called from a CFI function. An error here could + * lead to error in any transaction that calls the CFI function. */ dsa_area * GetNamedDSA(const char *name, bool *found) @@ -351,7 +353,9 @@ GetNamedDSA(const char *name, bool *found) * params is ignored; a new LWLock tranche ID will be generated if needed. * Note that the lock tranche will be registered with the provided name. Also * note that this should be called at most once for a given table in each - * backend. + * backend. It's best to minimize the number of errors thrown in this function + * since it's called from a CFI function. An error here could lead to error in + * any transaction that calls the CFI function. */ dshash_table * GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found) diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index 1f7e933d500..b0176613987 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -52,6 +52,7 @@ #include "storage/sinvaladt.h" #include "utils/guc.h" #include "utils/injection_point.h" +#include "utils/memutils.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -141,6 +142,7 @@ CalculateShmemSize(void) size = add_size(size, AioShmemSize()); size = add_size(size, WaitLSNShmemSize()); size = add_size(size, LogicalDecodingCtlShmemSize()); + size = add_size(size, MemoryContextKeysShmemSize()); /* include additional requested shmem from preload libraries */ size = add_size(size, total_addin_request); @@ -328,6 +330,7 @@ CreateOrAttachShmemStructs(void) AioShmemInit(); WaitLSNShmemInit(); LogicalDecodingCtlShmemInit(); + MemoryContextKeysShmemInit(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index 7505c9d3a37..839cbdbdc89 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -695,6 +695,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_GET_MEMORY_CONTEXT)) + HandleGetMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index d01a09dd0c4..9e0d289a604 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3573,6 +3573,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 4aa864fe3c3..3e1dc4225b2 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -163,6 +163,7 @@ WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." +MEM_CXT_PUBLISH "Waiting for a process to publish memory information." ABI_compatibility: @@ -366,6 +367,7 @@ SerialControl "Waiting to read or update shared <filename>pg_serial</filename> s AioWorkerSubmissionQueue "Waiting to access AIO worker submission queue." WaitLSN "Waiting to read or update shared Wait-for-LSN state." LogicalDecodingControl "Waiting to read or update logical decoding status information." +MemContextReportingClientKeys "Waiting for another process to complete reading or writing the memory reporting keys." # # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE) diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index c7f7b8bc2dd..fdf0043211b 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -15,25 +15,128 @@ #include "postgres.h" +#include "access/xact.h" #include "funcapi.h" #include "mb/pg_wchar.h" +#include "storage/dsm_registry.h" #include "storage/proc.h" #include "storage/procarray.h" #include "storage/procsignal.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/injection_point.h" +#include "utils/memutils.h" +#include "utils/wait_event_types.h" + +/* + * Memory Context reporting size limits. + */ + +/* Max length of context name and ident, to keep it consistent + * with ProcessLogMemoryContext() + */ +#define MEMORY_CONTEXT_IDENT_SHMEM_SIZE 100 +#define MEMORY_CONTEXT_NAME_SHMEM_SIZE 100 +#define MAX_PATH_DISPLAY_LENGTH 100 + +/* Maximum size (in bytes) of DSA area per process */ +#define MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND ((size_t) (1 * 1024 * 1024)) + +/* + * Maximum number of memory context statistics is calculated by dividing + * max memory allocated per backend with maximum size per context statistics. + * The identifier and name are statically allocated arrays of size 100 bytes. + * The path depth is limited to 100 like for memory context logging. + */ +#define MAX_MEMORY_CONTEXT_STATS_NUM MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND / (sizeof(MemoryStatsEntry)) + +/* + * Size of dshash key. The key is a uint32 rendered as a string, 10 chars + * plus space for a NULL terminator can hold all the values. + */ +#define CLIENT_KEY_SIZE (10 + 1) + +/* Dynamic shared memory state for reporting statistics per context */ +typedef struct MemoryStatsEntry +{ + char name[MEMORY_CONTEXT_NAME_SHMEM_SIZE]; + char ident[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + int path[MAX_PATH_DISPLAY_LENGTH]; + NodeTag type; + int path_length; + int levels; + int64 totalspace; + int64 nblocks; + int64 freespace; + int64 freechunks; + int num_agg_stats; +} MemoryStatsEntry; + +/* + * Per backend dynamic shared hash entry for memory context statistics + * reporting. + */ +typedef struct MemoryStatsDSHashEntry +{ + ProcNumber client_key; + ConditionVariable memcxt_cv; + bool stats_written; + int target_server_id; + int total_stats; + bool summary; + dsa_pointer memstats_dsa_pointer; +} MemoryStatsDSHashEntry; + +static const dshash_parameters memctx_dsh_params = { + offsetof(MemoryStatsDSHashEntry, memcxt_cv), + sizeof(MemoryStatsDSHashEntry), + dshash_memcmp, + dshash_memhash, + dshash_memcpy +}; + +/* + * These are used for reporting memory context statistics of a process. + */ + +/* Array to store the keys of MemoryStatsDsHash */ +static int *client_keys = NULL; + +/* + * Table to store pointers to DSA memory containing memory statistics and other + * metadata. There is one entry per client backend request, keyed by ProcNumber + * of the client obtained from client_keys array above. + */ +static dshash_table *MemoryStatsDsHash = NULL; + +/* + * Dsa area which stores the actual memory context + * statistics. + */ +static dsa_area *MemoryStatsDsaArea = NULL; + +static void memstats_dsa_cleanup(int ProcNumber); +static void memstats_client_key_reset(int ProcNumber); +static void PublishMemoryContext(MemoryStatsEntry *memcxt_info, + int curr_id, MemoryContext context, + List *path, + MemoryContextCounters stat, + int num_contexts); /* ---------- * The max bytes for showing identifiers of MemoryContext. + * This is used by pg_get_backend_memory_context - view used for local backend. * ---------- */ #define MEMORY_CONTEXT_IDENT_DISPLAY_SIZE 1024 +/* Timeout in seconds */ +#define MEMORY_STATS_MAX_TIMEOUT 5 + /* * MemoryContextId - * Used for storage of transient identifiers for - * pg_get_backend_memory_contexts. + * Used for storage of transient identifiers for memory context reporting */ typedef struct MemoryContextId { @@ -63,6 +166,67 @@ int_list_to_array(const List *list) return PointerGetDatum(result_array); } +/* + * context_type_to_string + * Returns a textual representation of a context type + * + * This should cover the same types as MemoryContextIsValid. + */ +static const char * +context_type_to_string(NodeTag type) +{ + const char *context_type; + + switch (type) + { + case T_AllocSetContext: + context_type = "AllocSet"; + break; + case T_GenerationContext: + context_type = "Generation"; + break; + case T_SlabContext: + context_type = "Slab"; + break; + case T_BumpContext: + context_type = "Bump"; + break; + default: + context_type = "???"; + break; + } + return context_type; +} + +/* + * compute_context_path + * + * Append the transient context_id of this context and each of its ancestors + * to a list, in order to compute a path. + */ +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + MemoryContext cur_context; + + for (cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + { + elog(NOTICE, "hash table corrupted, can't construct path value"); + return NIL; + } + path = lcons_int(cur_entry->context_id, path); + } + return path; +} + /* * PutMemoryContextsStatsTupleStore * Add details for the given MemoryContext to 'tupstore'. @@ -144,24 +308,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, else nulls[1] = true; - switch (context->type) - { - case T_AllocSetContext: - type = "AllocSet"; - break; - case T_GenerationContext: - type = "Generation"; - break; - case T_SlabContext: - type = "Slab"; - break; - case T_BumpContext: - type = "Bump"; - break; - default: - type = "???"; - break; - } + type = context_type_to_string(context->type); values[2] = CStringGetTextDatum(type); values[3] = Int32GetDatum(list_length(path)); /* level */ @@ -306,3 +453,839 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } + +/* + * pg_get_process_memory_contexts + * Signal a backend or an auxiliary process to send its memory contexts, + * wait for the results and display them. + * + * By default, only superusers or users with ROLE_PG_READ_ALL_STATS are allowed + * to signal a process to return the memory contexts. Additional roles can be + * permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to copy the memory context details + * to a dynamic shared memory space. + * + * We have defined a limit on DSA memory that could be allocated per process - + * if the process has more memory contexts than what can fit in the allocated + * size, the excess contexts are summarized and represented as cumulative total + * at the end of the buffer. + * + * After sending the signal, wait on a condition variable. The publishing + * backend, after copying the data to shared memory, sends a signal on that + * condition variable. There is one condition variable per client process. + * Once the condition variable is signalled, check if the latest memory context + * information is available and display. + * + * If the publishing backend does not respond before the condition variable + * times out, which is set to a predefined value MEMORY_STATS_MAX_TIMEOUT, give + * up and return NULL. + */ +Datum +pg_get_process_memory_contexts(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + bool summary = PG_GETARG_BOOL(1); + PGPROC *proc; + ProcNumber client_procno = MyProcNumber; + ProcNumber server_procno; + bool proc_is_aux = false; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + MemoryStatsEntry *memcxt_info; + MemoryStatsDSHashEntry *entry; + bool found; + TimestampTz start_timestamp; + + /* + * See if the process with given pid is a backend or an auxiliary process + * and remember the type for when we requery the process later. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + { + proc = AuxiliaryPidGetProc(pid); + proc_is_aux = true; + } + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; this is however not a problem and leave with a WARNING. + * See comment in pg_log_backend_memory_contexts for a discussion on this. + */ + if (proc == NULL) + { + ereport(WARNING, + errmsg("PID %d is not a PostgreSQL server process", pid)); + PG_RETURN_NULL(); + } + + server_procno = GetNumberFromPGProc(proc); + + /* + * First, check if the server process slot is occupied; if so, exit early. + * An occupied slot indicates another client backend is already requesting + * statistics from this server process. We should not set the slot yet, + * even if it appears empty, until we are prepared to signal the server + * process. This is because if the following DSA APIs throw an error, the + * server slot may not be reset, leading to it being incorrectly marked as + * occupied and blocking further memory context statistics queries to the + * same process. + */ + LWLockAcquire(MemContextReportingClientKeysLock, LW_EXCLUSIVE); + if (client_keys[server_procno] != -1) + { + LWLockRelease(MemContextReportingClientKeysLock); + ereport(WARNING, + errmsg("server process %d is processing previous request", + pid)); + PG_RETURN_NULL(); + } + LWLockRelease(MemContextReportingClientKeysLock); + + /* + * Create a DSA to allocate memory for copying memory contexts statistics. + * Allocate the memory in the DSA and send DSA pointer to the server + * process for storing the context statistics. If number of contexts + * exceed a predefined limit (1MB), a cumulative total is stored for such + * contexts. + * + * The DSA is created once for the lifetime of the server, and only + * attached in subsequent calls. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); + + /* + * The DSA pointers containing statistics for each client are stored in a + * dshash table. In addition to DSA pointer, each entry in this table also + * contains information about the statistics, condition variable for + * signalling between client and the server and miscellaneous data + * specific to a request. There is one entry per client request in the + * hash table. + */ + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); + + /* + * Insert an entry for this client in DSHASH table the first time this + * function is called. This entry is deleted when the process exits in + * before_shmem_exit call. + * + * dshash_find_or_insert locks the entry to prevent the publisher from + * reading before client has updated the entry. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, &client_procno, &found); + if (!found) + { + entry->stats_written = false; + ConditionVariableInit(&entry->memcxt_cv); + } + + /* + * Allocate 1MB of memory for the backend to publish its statistics on + * every call to this function. The memory is freed at the end of the + * function. + */ + entry->memstats_dsa_pointer = + dsa_allocate0(MemoryStatsDsaArea, MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND); + + /* + * Specify whether a summary of statistics is requested, before signalling + * the server. + */ + entry->summary = summary; + + /* + * Indicate which server process statistics are being requested from. If + * this client times out before the last requested process can publish its + * statistics, it may send a new request to another server process. Since + * the previous server was notified, it might attempt to read the same + * client entry and respond incorrectly with its statistics. By storing + * the server ID in the client entry, we prevent any previously signalled + * server process from writing its statistics in the space meant for the + * newly requested process. + */ + entry->target_server_id = pid; + dshash_release_lock(MemoryStatsDsHash, entry); + + /* + * Check if the publishing process slot is empty and store this clients + * key i.e its procNumber. This informs the publishing process that it is + * supposed to write statistics in the hash entry corresponding to this + * client. + */ + LWLockAcquire(MemContextReportingClientKeysLock, LW_EXCLUSIVE); + if (client_keys[server_procno] == -1) + client_keys[server_procno] = MyProcNumber; + else + { + LWLockRelease(MemContextReportingClientKeysLock); + memstats_dsa_cleanup(client_procno); + ereport(WARNING, + errmsg("server process %d is processing previous request", + pid)); + PG_RETURN_NULL(); + } + LWLockRelease(MemContextReportingClientKeysLock); + + /* + * Send a signal to a PostgreSQL process, informing it we want it to + * produce information about its memory contexts. + */ + if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, server_procno) < 0) + { + memstats_dsa_cleanup(client_procno); + memstats_client_key_reset(server_procno); + ereport(WARNING, + errmsg("could not send signal to process %d: %m", + pid)); + PG_RETURN_NULL(); + } + start_timestamp = GetCurrentTimestamp(); + + while (1) + { + long elapsed_time; + + INJECTION_POINT("memcontext-client-injection", NULL); + + elapsed_time = TimestampDifferenceMilliseconds(start_timestamp, + GetCurrentTimestamp()); + /* Return if we have already exceeded the timeout */ + if (elapsed_time >= MEMORY_STATS_MAX_TIMEOUT * 1000) + { + memstats_dsa_cleanup(client_procno); + memstats_client_key_reset(server_procno); + ConditionVariableCancelSleep(); + ereport(NOTICE, + errmsg("request for memory context statistics for PID %d timed out", + pid)); + PG_RETURN_NULL(); + } + + /* + * Recheck the state of the backend before sleeping on the condition + * variable to ensure the process is still alive. Only check the + * relevant process type based on the earlier PID check. + */ + if (proc_is_aux) + proc = AuxiliaryPidGetProc(pid); + else + proc = BackendPidGetProc(pid); + + /* + * The target server process ending during memory context processing + * is not an error. + */ + if (proc == NULL) + { + memstats_dsa_cleanup(client_procno); + memstats_client_key_reset(server_procno); + ConditionVariableCancelSleep(); + ereport(WARNING, + errmsg("PID %d is no longer a PostgreSQL server process", + pid)); + PG_RETURN_NULL(); + } + + /* + * Wait for at most MEMORY_STATS_MAX_TIMEOUT seconds, if we have been + * woken up spuriously we subtract the time already slept. If no + * statistics are available within the allowed time then return NULL. + * The timer is defined in milliseconds since that's what the + * condition variable sleep uses. + */ + if (ConditionVariableTimedSleep(&entry->memcxt_cv, + (MEMORY_STATS_MAX_TIMEOUT * 1000) - elapsed_time, + WAIT_EVENT_MEM_CXT_PUBLISH)) + { + /* Timeout has expired, return NULL */ + memstats_dsa_cleanup(client_procno); + memstats_client_key_reset(server_procno); + ConditionVariableCancelSleep(); + ereport(NOTICE, + errmsg("request for memory context statistics for PID %d timed out", + pid)); + PG_RETURN_NULL(); + } + entry = dshash_find_or_insert(MemoryStatsDsHash, &client_procno, &found); + Assert(found); + + memcxt_info = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + /* + * We expect to come out of sleep when the requested process has + * finished publishing the statistics, verified using a boolean + * stats_written. + * + * Make sure that the statistics are actually written by checking that + * the name of the context is not NULL. This is done to ensure that + * the subsequent waits for statistics do not return spuriously if the + * previous call to the function ended in error and thus could not + * clear the stats_written flag. + */ + if (entry->stats_written && memcxt_info[0].name[0] != '\0') + break; + + dshash_release_lock(MemoryStatsDsHash, entry); + + } + + InitMaterializedSRF(fcinfo, 0); + + /* + * Backend has finished publishing the stats, project them. + */ +#define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 11 + for (int i = 0; i < entry->total_stats; i++) + { + ArrayType *path_array; + Datum values[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + bool nulls[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + Datum *path_datum = NULL; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + Assert(memcxt_info[i].name[0] != '\0'); + values[0] = CStringGetTextDatum(memcxt_info[i].name); + + if (memcxt_info[i].ident[0] != '\0') + values[1] = CStringGetTextDatum(memcxt_info[i].ident); + else + nulls[1] = true; + + values[2] = CStringGetTextDatum(context_type_to_string(memcxt_info[i].type)); + + if (memcxt_info[i].levels != 0) + values[3] = Int32GetDatum(memcxt_info[i].levels); + else + nulls[3] = true; + + if (memcxt_info[i].path[0] != 0) + { + int path_length; + + path_length = memcxt_info[i].path_length; + path_datum = (Datum *) palloc(path_length * sizeof(Datum)); + + for (int j = 0; j < path_length; j++) + path_datum[j] = Int32GetDatum(memcxt_info[i].path[j]); + path_array = construct_array_builtin(path_datum, + path_length, + INT4OID); + values[4] = PointerGetDatum(path_array); + } + else + nulls[4] = true; + + values[5] = Int64GetDatum(memcxt_info[i].totalspace); + values[6] = Int64GetDatum(memcxt_info[i].nblocks); + values[7] = Int64GetDatum(memcxt_info[i].freespace); + values[8] = Int64GetDatum(memcxt_info[i].freechunks); + values[9] = Int64GetDatum(memcxt_info[i].totalspace - + memcxt_info[i].freespace); + values[10] = Int32GetDatum(memcxt_info[i].num_agg_stats); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + dshash_release_lock(MemoryStatsDsHash, entry); + memstats_dsa_cleanup(client_procno); + + ConditionVariableCancelSleep(); + + PG_RETURN_NULL(); +} + +/* + * This function is executed before returning from the + * pg_get_process_memory_contexts() function. + * It releases the DSA memory allocated for storing the memory statistics + * retrieved by that function. The caller must ensure that the LWLock for + * MemoryStatsDsHash is not already held. +*/ +static void +memstats_dsa_cleanup(int procNumber) +{ + MemoryStatsDSHashEntry *entry; + + entry = dshash_find(MemoryStatsDsHash, &procNumber, true); + + Assert(MemoryStatsDsaArea != NULL); + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + entry->memstats_dsa_pointer = InvalidDsaPointer; + entry->stats_written = false; + entry->target_server_id = 0; + + dshash_release_lock(MemoryStatsDsHash, entry); +} + +/* + * Remove this process from the publishing process' + * client key slot, if the stats publishing process has failed to do so. + */ +static void +memstats_client_key_reset(int procNumber) +{ + LWLockAcquire(MemContextReportingClientKeysLock, LW_EXCLUSIVE); + + if (client_keys[procNumber] == MyProcNumber) + client_keys[procNumber] = -1; + LWLockRelease(MemContextReportingClientKeysLock); +} + +void +MemoryContextKeysShmemInit(void) +{ + bool found; + + client_keys = (int *) + ShmemInitStruct("MemoryContextKeys", + MemoryContextKeysShmemSize(), &found); + + if (!found) + MemSet(client_keys, -1, MemoryContextKeysShmemSize()); +} + +Size +MemoryContextKeysShmemSize(void) +{ + Size TotalProcs = 0; + + TotalProcs = add_size(TotalProcs, NUM_AUXILIARY_PROCS); + TotalProcs = add_size(TotalProcs, MaxBackends); + + return mul_size(TotalProcs, sizeof(int)); +} + +/* + * HandleGetMemoryContextInterrupt + * Handle receipt of an interrupt indicating a request to publish memory + * contexts statistics. + * + * All the actual work is deferred to ProcessGetMemoryContextInterrupt() as + * this cannot be performed in a signal handler. + */ +void +HandleGetMemoryContextInterrupt(void) +{ + InterruptPending = true; + PublishMemoryContextPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + +/* + * ProcessGetMemoryContextInterrupt + * Generate information about memory contexts used by the process. + * + * Performs a breadth first search on the memory context tree, thus parents + * statistics are reported before their children in the monitoring function + * output. + * + * Statistics for all the processes are shared via the same dynamic shared + * area. Individual statistics are tracked independently in per-process DSA + * pointers. These pointers are stored in a dshash table with key as requesting + * clients ProcNumber. + * + * We calculate maximum number of context's statistics that can be displayed + * using a pre-determined limit for memory available per process for this + * utility and maximum size of statistics for each context. The remaining + * context statistics if any are captured as a cumulative total at the end of + * individual context's statistics. + * + * If summary is true, we capture the level 1 and level 2 contexts statistics. + * For that we traverse the memory context tree recursively in depth first + * search manner to cover all the children of a parent context, to be able to + * display a cumulative total of memory consumption by a parent at level 2 and + * all its children. + */ +void +ProcessGetMemoryContextInterrupt(void) +{ + List *contexts; + HASHCTL ctl; + HTAB *context_id_lookup; + int context_id = 0; + MemoryStatsEntry *meminfo; + MemoryContextCounters stat; + int num_individual_stats = 0; + bool found; + MemoryStatsDSHashEntry *entry; + int clientProcNumber; + MemoryContext memstats_ctx = NULL; + MemoryContext oldcontext = NULL; + + PublishMemoryContextPending = false; + + /* + * Avoid performing any shared memory operations in aborted transaction, + * the caller will timeout and return NULL. + */ + if (IsAbortedTransactionBlockState()) + return; + + INJECTION_POINT("memcontext-server-wait", NULL); + + /* + * Retrieve the client key for publishing statistics and reset it to -1, + * so other clients can request memory statistics from this process. + * Return if the client_key is -1, which means the requesting client has + * timed out. + */ + LWLockAcquire(MemContextReportingClientKeysLock, LW_EXCLUSIVE); + if (client_keys[MyProcNumber] == -1) + { + LWLockRelease(MemContextReportingClientKeysLock); + return; + } + clientProcNumber = client_keys[MyProcNumber]; + client_keys[MyProcNumber] = -1; + LWLockRelease(MemContextReportingClientKeysLock); + + /* + * Create a new memory context which is not a part of TopMemoryContext + * tree. This context is used to allocate all memory in this function. + * This helps in keeping the memory allocation in this function to report + * memory consumption statistics separate. So that it does not affect the + * output of this function. + */ + memstats_ctx = AllocSetContextCreate((MemoryContext) NULL, + "publish_memory_context_statistics", + ALLOCSET_SMALL_SIZES); + oldcontext = MemoryContextSwitchTo(memstats_ctx); + + /* + * The hash table is used for constructing "path" column of the view, + * similar to its local backend counterpart. + */ + ctl.keysize = sizeof(MemoryContext); + ctl.entrysize = sizeof(MemoryContextId); + ctl.hcxt = CurrentMemoryContext; + + context_id_lookup = hash_create("pg_get_process_memory_contexts", + 256, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + /* List of contexts to process in the next round - start at the top. */ + contexts = list_make1(TopMemoryContext); + + /* + * The client process should have created the required DSA and DSHash + * table, so we can connect to these directly. The GetNamedXXX functions + * either return DSA/DSHash or error out (which we want to avoid as that + * would cause the current transaction to fail) so we don't need to check + * for NULL. An assertion is enough to catch in case this API should be + * changed at some point. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); + + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); + + Assert(MemoryStatsDsaArea != NULL && MemoryStatsDsHash != NULL); + + /* + * The entry lock is held by dshash_find_or_insert to protect writes to + * process specific memory. Two different processes publishing statistics + * do not block each other. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, &clientProcNumber, &found); + INJECTION_POINT("memcontext-server-injection", NULL); + + /* + * Check if the entry has been deleted due to calling process exiting, or + * if the caller has timed out waiting for us and have issued a request to + * another backend. + * + * Make sure that the client always deletes the entry after taking the + * required lock or this function may end up writing to unallocated memory. + */ + if (!found || entry->target_server_id != MyProcPid) + { + entry->stats_written = false; + + dshash_release_lock(MemoryStatsDsHash, entry); + + hash_destroy(context_id_lookup); + MemoryContextSwitchTo(oldcontext); + MemoryContextDelete(memstats_ctx); + + return; + } + + /* Should be allocated by a client backend that is requesting statistics */ + Assert(entry->memstats_dsa_pointer != InvalidDsaPointer); + meminfo = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + if (entry->summary) + { + int cxt_id = 0; + List *path = NIL; + MemoryContextId *contextid_entry; + + /* Copy TopMemoryContext statistics to DSA */ + memset(&stat, 0, sizeof(stat)); + (*TopMemoryContext->methods->stats) (TopMemoryContext, NULL, NULL, + &stat, true); + path = lcons_int(1, path); + PublishMemoryContext(meminfo, cxt_id, TopMemoryContext, path, stat, + 1); + + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &TopMemoryContext, + HASH_ENTER, &found); + Assert(!found); + + /* context id starts with 1 */ + contextid_entry->context_id = cxt_id + 1; + + /* + * Copy statistics for each of TopMemoryContext's children. This + * includes statistics of at most 100 children per node, with each + * child node limited to a depth of 100 in its subtree. + */ + for (MemoryContext c = TopMemoryContext->firstchild; c != NULL; + c = c->nextchild) + { + MemoryContextCounters grand_totals; + int num_contexts = 0; + + path = NIL; + memset(&grand_totals, 0, sizeof(grand_totals)); + + cxt_id++; + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &c, HASH_ENTER, &found); + Assert(!found); + contextid_entry->context_id = cxt_id + 1; + + MemoryContextStatsCounter(c, &grand_totals, &num_contexts); + + path = compute_context_path(c, context_id_lookup); + + PublishMemoryContext(meminfo, cxt_id, c, path, + grand_totals, num_contexts); + } + entry->total_stats = cxt_id + 1; + } + else + { + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + MemoryContextId *contextid_entry; + + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &cur, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = context_id + 1; + + /* + * Figure out the transient context_id of this context and each of + * its ancestors, to compute a path for this context. + */ + path = compute_context_path(cur, context_id_lookup); + + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + /* Account for saving one statistics slot for cumulative reporting */ + if (context_id < (MAX_MEMORY_CONTEXT_STATS_NUM - 1)) + { + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1); + } + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].totalspace += stat.totalspace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].nblocks += stat.nblocks; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freespace += stat.freespace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit per process is reached, write aggregate of the + * remaining statistics. + * + * We can store contexts from 0 to max_stats - 1. When context_id + * is greater than max_stats, we stop reporting individual + * statistics when context_id equals max_stats - 2. As we use + * max_stats - 1 array slot for reporting cumulative statistics or + * "Remaining Totals". + */ + if (context_id == (MAX_MEMORY_CONTEXT_STATS_NUM - 2)) + { + int namelen = strlen("Remaining Totals"); + + num_individual_stats = context_id + 1; + strlcpy(meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].name, + "Remaining Totals", namelen + 1); + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].ident[0] = '\0'; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].path[0] = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].type = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].levels = 0; + } + context_id++; + + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + contexts = lappend(contexts, c); + } + + /* + * Check if there are aggregated statistics or not in the result set. + * Statistics are individually reported when context_id <= max_stats, + * only if context_id > max_stats will there be aggregates. + */ + if (context_id <= MAX_MEMORY_CONTEXT_STATS_NUM) + { + entry->total_stats = context_id; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = 1; + } + + /* + * The number of contexts exceeded the space available, so report the + * number of aggregated memory contexts + */ + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = + context_id - num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for + * cumulative statistics. + */ + entry->total_stats = num_individual_stats + 1; + } + } + + entry->stats_written = true; + /* Notify waiting client backend and return */ + ConditionVariableSignal(&entry->memcxt_cv); + dshash_release_lock(MemoryStatsDsHash, entry); + hash_destroy(context_id_lookup); + + MemoryContextSwitchTo(oldcontext); + MemoryContextDelete(memstats_ctx); +} + +/* + * PublishMemoryContext + * + * Copy the memory context statistics of a single context to a DSA memory + */ +static void +PublishMemoryContext(MemoryStatsEntry *memcxt_info, int curr_id, + MemoryContext context, List *path, + MemoryContextCounters stat, int num_contexts) +{ + char *ident = unconstify(char *, context->ident); + char *name = unconstify(char *, context->name); + + /* + * To be consistent with logging output, we label dynahash contexts with + * just the hash table name as with MemoryContextStatsPrint(). + */ + if (context->ident && strncmp(context->name, "dynahash", 8) == 0) + { + name = unconstify(char *, context->ident); + ident = NULL; + } + + if (name != NULL) + { + int namelen = strlen(name); + + if (namelen >= MEMORY_CONTEXT_NAME_SHMEM_SIZE) + namelen = pg_mbcliplen(name, namelen, + MEMORY_CONTEXT_NAME_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].name, name, namelen + 1); + } + else + /* Clearing the array */ + memcxt_info[curr_id].name[0] = '\0'; + + /* Trim and copy the identifier if it is not set to NULL */ + if (ident != NULL) + { + int idlen = strlen(ident); + + /* + * Some identifiers such as SQL query string can be very long, + * truncate oversize identifiers. + */ + if (idlen >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + idlen = pg_mbcliplen(ident, idlen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].ident, ident, idlen + 1); + } + else + memcxt_info[curr_id].ident[0] = '\0'; + + /* Store the path */ + if (path == NIL) + memcxt_info[curr_id].path[0] = 0; + else + { + int levels = Min(list_length(path), MAX_PATH_DISPLAY_LENGTH); + + memcxt_info[curr_id].path_length = levels; + memcxt_info[curr_id].levels = list_length(path); + + foreach_int(i, path) + { + memcxt_info[curr_id].path[foreach_current_index(i)] = i; + if (--levels == 0) + break; + } + } + memcxt_info[curr_id].type = context->type; + memcxt_info[curr_id].totalspace = stat.totalspace; + memcxt_info[curr_id].nblocks = stat.nblocks; + memcxt_info[curr_id].freespace = stat.freespace; + memcxt_info[curr_id].freechunks = stat.freechunks; + memcxt_info[curr_id].num_agg_stats = num_contexts; +} + +void +AtProcExit_memstats_cleanup(int code, Datum arg) +{ + int idx = MyProcNumber; + MemoryStatsDSHashEntry *entry; + bool found; + + if (MemoryStatsDsHash != NULL) + { + entry = dshash_find_or_insert(MemoryStatsDsHash, &idx, &found); + + if (found) + { + if (MemoryStatsDsaArea != NULL && + DsaPointerIsValid(entry->memstats_dsa_pointer)) + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + } + dshash_delete_entry(MemoryStatsDsHash, entry); + } + LWLockAcquire(MemContextReportingClientKeysLock, LW_EXCLUSIVE); + client_keys[idx] = -1; + LWLockRelease(MemContextReportingClientKeysLock); +} diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index 36ad708b360..e246fc7ce1f 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -39,6 +39,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t PublishMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index b59e08605cc..ba5d3bf0d7a 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -669,6 +669,13 @@ BaseInit(void) * drop ephemeral slots, which in turn triggers stats reporting. */ ReplicationSlotInitialize(); + + /* + * The before shmem exit callback frees the DSA memory occupied by the + * latest memory context statistics that could be published by this proc + * if requested. + */ + before_shmem_exit(AtProcExit_memstats_cleanup, 0); } diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 073bdb35d2a..2a826cbef17 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -1011,6 +1011,37 @@ MemoryContextStatsInternal(MemoryContext context, int level, } } + +/* + * MemoryContextStatsCounter + * + * Accumulate statistics counts into *totals. totals should not be NULL. + * This involves a non-recursive tree traversal. + */ +void +MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts) +{ + int ichild = 1; + + Assert(totals != NULL); + context->methods->stats(context, NULL, NULL, totals, false); + + for (MemoryContext curr = context->firstchild; + curr != NULL; + curr = MemoryContextTraverseNext(curr, context)) + { + curr->methods->stats(curr, NULL, NULL, totals, false); + ichild++; + } + + /* + * Add the count of all the children contexts which are traversed + * including the parent. + */ + *num_contexts = ichild; +} + /* * MemoryContextStatsPrint * Print callback used by MemoryContextStatsInternal diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index dac40992cbc..3667bc2bf41 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8641,6 +8641,16 @@ prorettype => 'bool', proargtypes => 'int4', prosrc => 'pg_log_backend_memory_contexts' }, +# publishing memory contexts of the specified postgres process +{ oid => '2173', descr => 'publish memory contexts of the specified process', + proname => 'pg_get_process_memory_contexts', provolatile => 'v', + prorows => '100', proretset => 't', proparallel => 'u', + prorettype => 'record', proargtypes => 'int4 bool', + proallargtypes => '{int4,bool,text,text,text,int4,_int4,int8,int8,int8,int8,int8,int4}', + proargmodes => '{i,i,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{pid, summary, name, ident, type, level, path, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes, num_agg_contexts}', + prosrc => 'pg_get_process_memory_contexts' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index f16f35659b9..38e1e55a9a6 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -96,6 +96,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t PublishMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h index e94ebce95b9..6dd80689bbb 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -87,6 +87,7 @@ PG_LWLOCK(52, SerialControl) PG_LWLOCK(53, AioWorkerSubmissionQueue) PG_LWLOCK(54, WaitLSN) PG_LWLOCK(55, LogicalDecodingControl) +PG_LWLOCK(56, MemContextReportingClientKeys) /* * There also exist several built-in LWLock tranches. As with the predefined diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index 348fba53a93..00ccfcb22a4 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -35,6 +35,7 @@ typedef enum PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to send the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ PROCSIG_RECOVERY_CONFLICT, /* backend is blocking recovery, check * PGPROC->pendingRecoveryConflicts for the diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 11ab1717a16..40099036308 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -319,4 +319,11 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) return true; } +extern void ProcessGetMemoryContextInterrupt(void); +extern void HandleGetMemoryContextInterrupt(void); +extern void MemoryContextKeysShmemInit(void); +extern Size MemoryContextKeysShmemSize(void); +extern void MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts); +extern void AtProcExit_memstats_cleanup(int code, Datum arg); #endif /* MEMUTILS_H */ diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 3dd63fd88ed..c84e5adce16 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -233,3 +233,22 @@ select * from pg_timezone_abbrevs where abbrev = 'LMT'; LMT | @ 7 hours 52 mins 58 secs ago | f (1 row) +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; +NOTICE: (AllocSet,TopMemoryContext,) +NOTICE: (AllocSet,TopMemoryContext,) diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 004f9a70e00..d34d2b6211c 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -101,3 +101,21 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs; -- One specific case we can check without much fear of breakage -- is the historical local-mean-time value used for America/Los_Angeles. select * from pg_timezone_abbrevs where abbrev = 'LMT'; + +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 241945734ec..f90ff78a78b 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1709,6 +1709,8 @@ MemoryContextData MemoryContextId MemoryContextMethodID MemoryContextMethods +MemoryStatsEntry +MemoryStatsDSHashEntry MemoryStatsPrintFunc MergeAction MergeActionState -- 2.34.1 [application/octet-stream] v53-0002-Test-module-to-test-memory-context-reporting-wit.patch (10.1K, ../../CAH2L28sH3VoQDHE_kqgrmPMxUq7zdWzUWVB6kg-vu+c0_42MpA@mail.gmail.com/4-v53-0002-Test-module-to-test-memory-context-reporting-wit.patch) download | inline diff: From d7902195131dad7ee1d08be6213f31e65f754ed3 Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Fri, 28 Nov 2025 14:46:38 +0530 Subject: [PATCH 2/2] Test module to test memory context reporting with injection points --- src/test/modules/Makefile | 1 + .../test_memcontext_reporting/Makefile | 23 +++ .../t/001_memcontext_inj.pl | 185 ++++++++++++++++++ .../test_memcontext_reporting.c | 12 ++ .../test_memcontext_reporting.control | 4 + 5 files changed, 225 insertions(+) create mode 100644 src/test/modules/test_memcontext_reporting/Makefile create mode 100644 src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index 44c7163c1cd..8ce9598495c 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -34,6 +34,7 @@ SUBDIRS = \ test_json_parser \ test_lfind \ test_lwlock_tranches \ + test_memcontext_reporting \ test_misc \ test_oat_hooks \ test_parser \ diff --git a/src/test/modules/test_memcontext_reporting/Makefile b/src/test/modules/test_memcontext_reporting/Makefile new file mode 100644 index 00000000000..2ce169ffa69 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/Makefile @@ -0,0 +1,23 @@ +# src/test/modules/test_memcontext_reporting/Makefile + +TAP_TESTS = 1 + +EXTRA_INSTALL = src/test/modules/injection_points + +export enable_injection_points +MODULE_big = test_memcontext_reporting +OBJS = \ + $(WIN32RES) \ + test_memcontext_reporting.o +PGFILEDESC = "test_memcontext_reporting - test code for memory context reporting" + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_memcontext_reporting +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl new file mode 100644 index 00000000000..947059ceece --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl @@ -0,0 +1,185 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test suite for testing memory context statistics reporting + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} +my $psql_err; +my $psql_out; +# Create and start a cluster with one node +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->append_conf( + 'postgresql.conf', + qq[ +log_statement = none +restart_after_crash = false +]); +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); + +# Attaching to a client process's injection point that throws an error +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-client-injection', 'error');" +); + +my $pid = $node->safe_psql('postgres', + "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); + +# Client should have thrown error +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); +like($psql_err, + qr/error triggered for injection point memcontext-client-injection/); + +# Query the same process after detaching the injection point, using some other +# client and it should succeed. +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-client-injection');"); +my $topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name eq 'TopMemoryContext'); + +# Attaching to a target process injection point that throws an error +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-server-injection', 'error');" +); + +# Server process throws an error, which causes client to time out +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); + +like($psql_err, + qr/NOTICE: request for memory context statistics for PID $pid timed out/); +# Query the same process after detaching the injection point, using some other +# client and it should succeed. +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-injection');"); +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name eq 'TopMemoryContext'); + +# Test that two concurrent requests to the same process results in a warning +# for one of those + +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-client-injection', 'wait');"); +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-server-wait', 'wait');"); +my $psql_session1 = $node->background_psql('postgres'); + +# We use query_until so that we can execute additional commands without having +# to wait for this command to complete, since it will pause at injection +# points. + +$psql_session1->query_until( + qr//, + qq( + SELECT pg_get_process_memory_contexts($pid, true); +)); +$node->wait_for_event('client backend', 'memcontext-client-injection'); +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); +ok($psql_err =~ + /WARNING: server process $pid is processing previous request/); +# Wake the client up. +$node->safe_psql('postgres', + "SELECT injection_points_wakeup('memcontext-client-injection');"); + +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-client-injection');"); +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-wait');"); + +# Test the client process exiting with timeout does not break the server +# process + +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-server-wait', 'wait');"); +# Following client query times out, returning NULL as output +$node->psql( + 'postgres', + qq(select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';), + stdout => \$psql_out); +is($psql_out, '', 'Query returns null'); +# Wakeup the server process up and detach the injection point. +$node->safe_psql('postgres', + "SELECT injection_points_wakeup('memcontext-server-wait');"); +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-wait');"); +# Query the same server process again and it should succeed. +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name eq 'TopMemoryContext'); + +# Test if the monitoring works fine when server restarts after client backend crashes. +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-client-injection', 'test_memcontext_reporting', 'crash', NULL);" +); + +# Client will crash +$node->psql( + 'postgres', + qq(select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';), + stderr => \$psql_err); +like($psql_err, + qr/WARNING: terminating connection because of crash of another server process|server closed the connection unexpectedly|connection to server was lost|could not send data to server/ +); + +# Wait till server restarts +$node->restart; +$node->poll_query_until('postgres', "SELECT 1;", '1'); + +# Querying memory stats should succeed after server start +$pid = $node->safe_psql('postgres', + "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name eq 'TopMemoryContext'); + +# Test if the monitoring works fine when server restarts after server process crashes. +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-server-injection', 'test_memcontext_reporting', 'crash', NULL);" +); + +# Server process will crash +$node->psql( + 'postgres', + qq(select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';), + stderr => \$psql_err); +like($psql_err, + qr/WARNING: terminating connection because of crash of another server process|server closed the connection unexpectedly|connection to server was lost|could not send data to server/ +); + +# Wait till server restarts +$node->restart; +$node->poll_query_until('postgres', "SELECT 1;", '1'); + +# Querying memory stats should succeed after server start +$pid = $node->safe_psql('postgres', + "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name eq 'TopMemoryContext'); + +done_testing(); diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c new file mode 100644 index 00000000000..d641f3616dc --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c @@ -0,0 +1,12 @@ +#include "postgres.h" +#include "funcapi.h" + +PG_MODULE_MAGIC; + +extern PGDLLEXPORT void crash(const char *name, const void *private_data, void *arg); + +void +crash(const char *name, const void *private_data, void *arg) +{ + abort(); +} diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control new file mode 100644 index 00000000000..48b501682d5 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control @@ -0,0 +1,4 @@ +comment = 'Test code for memcontext reporting' +default_version = '1.0' +module_pathname = '$libdir/test_memcontext_reporting' +relocatable = true -- 2.34.1 ^ permalink raw reply [nested|flat] 52+ messages in thread
* Re: Enhancing Memory Context Statistics Reporting @ 2026-03-05 22:31 Rahila Syed <[email protected]> parent: Rahila Syed <[email protected]> 0 siblings, 0 replies; 52+ messages in thread From: Rahila Syed @ 2026-03-05 22:31 UTC (permalink / raw) To: Daniel Gustafsson <[email protected]>; +Cc: pgsql-hackers; torikoshia <[email protected]>; Robert Haas <[email protected]>; Chao Li <[email protected]> Hi All, The email below provides a summary of the changes implemented since the previous update on December 18th. 1. I tested and verified that when ProcessGetMemoryContextInterrupt() returns with error from processes that don't start a transaction, it doesn't affect system integrity. In auxiliary processes like ioworker, archiver, and wal_receiver, the ERROR causes the process to exit, which ensures proper resource and lock release. In other cases, cleanup and release are managed by error recovery logic, allowing processes to continue running smoothly. 2. I verified that the only resources acquired in ProcessGetMemoryContextInterrupt are DSMs, which remain attached until the process exits. During proc_exit, the release/detach of DSM runs via dsm_backend_shutdown(). 3.The error reporting level in the interrupt handler has been adjusted from ERROR to NOTICE to prevent unnecessary errors from being raised in interrupted transactions due to minor issues. 4. Aborted transaction check has been added to allow returning without processing the interrupt if it is called from an aborted transaction. 5. DSHash key type has been changed from string to integer, now using procNumber directly. This eliminates string formatting overhead. 6. Timeout messaging has been changed from silently returning empty row to issuing a NOTICE and returning an empty row. 7. Documentation has been to GetNamedDSA() and GetNamedDSHash(), warning that errors thrown from these functions can propagate to any transaction calling the CFI function 8. Subtracted elapsed time ((MEMORY_STATS_MAX_TIMEOUT * 1000) - elapsed_time) from timeout passed to ConditionVariableTimedSleep so the total wait doesn't exceed the intended timeout in case of spurious wakeups. 9. Fixed a race condition bug by ensuring ConditionVariableSignal is called before dshash_release_lock Additionally, I included couple of assertions, renamed variables and functions to enhance clarity and consistency with the existing style, and made improvements to the documentation and tests. Please find attached updated and rebased patches. Thank you, Rahila Syed On Tue, Feb 24, 2026 at 5:27 PM Rahila Syed <[email protected]> wrote: > Hi Daniel, > > Thank you for the review. All the changes suggested in the v52comments.diff > are incorporated in the attached patches. > > +#Server should have thrown error >> +$node->psql( >> + 'postgres', >> + qq(select pg_get_process_memory_contexts($pid, true);), >> + stderr => \$psql_err); >> >> This test doesn't validate that the server actually errored does it? >> (There is >> no proposed fix in the attached.) >> >> > This has been fixed by adding a check for the error returned by the above > command. > While at it, I also added another crash test to the file, This is similar > to the existing > test for a client backend crash, but in this scenario, it crashes the > server process > instead. > > Thank you, > Rahila Syed > Attachments: [application/octet-stream] v54-0002-Test-module-to-test-memory-context-reporting-wit.patch (10.4K, ../../CAH2L28tU+aC1+OR5S1ATxHPeyuGp5JAfBVcs+7r-h_N2JRaxUw@mail.gmail.com/3-v54-0002-Test-module-to-test-memory-context-reporting-wit.patch) download | inline diff: From d6d4022de834e714820e85f928d2e734197d994a Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Fri, 28 Nov 2025 14:46:38 +0530 Subject: [PATCH 2/2] Test module to test memory context reporting with injection points --- src/test/modules/Makefile | 1 + src/test/modules/meson.build | 1 + .../test_memcontext_reporting/Makefile | 23 +++ .../t/001_memcontext_inj.pl | 185 ++++++++++++++++++ .../test_memcontext_reporting.c | 12 ++ .../test_memcontext_reporting.control | 1 + 6 files changed, 223 insertions(+) create mode 100644 src/test/modules/test_memcontext_reporting/Makefile create mode 100644 src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c create mode 100644 src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control diff --git a/src/test/modules/Makefile b/src/test/modules/Makefile index 4ac5c84db43..4cb276e40f0 100644 --- a/src/test/modules/Makefile +++ b/src/test/modules/Makefile @@ -35,6 +35,7 @@ SUBDIRS = \ test_json_parser \ test_lfind \ test_lwlock_tranches \ + test_memcontext_reporting \ test_misc \ test_oat_hooks \ test_parser \ diff --git a/src/test/modules/meson.build b/src/test/modules/meson.build index e2b3eef4136..e785ea7e1f9 100644 --- a/src/test/modules/meson.build +++ b/src/test/modules/meson.build @@ -36,6 +36,7 @@ subdir('test_integerset') subdir('test_json_parser') subdir('test_lfind') subdir('test_lwlock_tranches') +subdir('test_memcontext_reporting') subdir('test_misc') subdir('test_oat_hooks') subdir('test_parser') diff --git a/src/test/modules/test_memcontext_reporting/Makefile b/src/test/modules/test_memcontext_reporting/Makefile new file mode 100644 index 00000000000..2ce169ffa69 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/Makefile @@ -0,0 +1,23 @@ +# src/test/modules/test_memcontext_reporting/Makefile + +TAP_TESTS = 1 + +EXTRA_INSTALL = src/test/modules/injection_points + +export enable_injection_points +MODULE_big = test_memcontext_reporting +OBJS = \ + $(WIN32RES) \ + test_memcontext_reporting.o +PGFILEDESC = "test_memcontext_reporting - test code for memory context reporting" + +ifdef USE_PGXS +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) +else +subdir = src/test/modules/test_memcontext_reporting +top_builddir = ../../../.. +include $(top_builddir)/src/Makefile.global +include $(top_srcdir)/contrib/contrib-global.mk +endif diff --git a/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl new file mode 100644 index 00000000000..e2e4d197e2b --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/t/001_memcontext_inj.pl @@ -0,0 +1,185 @@ +# Copyright (c) 2026, PostgreSQL Global Development Group + +# Test suite for testing memory context statistics reporting + +use strict; +use warnings FATAL => 'all'; + +use PostgreSQL::Test::Cluster; +use PostgreSQL::Test::Utils; +use Test::More; + +if ($ENV{enable_injection_points} ne 'yes') +{ + plan skip_all => 'Injection points not supported by this build'; +} +my $psql_err; +my $psql_out; +# Create and start a cluster with one node +my $node = PostgreSQL::Test::Cluster->new('main'); +$node->init; +$node->append_conf( + 'postgresql.conf', + qq[ +log_statement = none +restart_after_crash = false +]); +$node->start; +$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;'); + +# Attaching to a client process's injection point that throws an error +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-client-injection', 'error');" +); + +my $pid = $node->safe_psql('postgres', + "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); + +# Client should have thrown error +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); +like($psql_err, + qr/error triggered for injection point memcontext-client-injection/); + +# Query the same process after detaching the injection point, using some other +# client and it should succeed. +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-client-injection');"); +my $topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name eq 'TopMemoryContext'); + +# Attaching to a target process injection point that throws an error +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-server-injection', 'error');" +); + +# Server process throws an error, which causes client to time out +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); + +like($psql_err, + qr/NOTICE: request for memory context statistics to PID $pid timed out/); +# Query the same process after detaching the injection point, using some other +# client and it should succeed. +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-injection');"); +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name eq 'TopMemoryContext'); + +# Test that two concurrent requests to the same process results in a warning +# for one of those + +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-client-injection', 'wait');"); +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-server-wait', 'wait');"); +my $psql_session1 = $node->background_psql('postgres'); + +# We use query_until so that we can execute additional commands without having +# to wait for this command to complete, since it will pause at injection +# points. + +$psql_session1->query_until( + qr//, + qq( + SELECT pg_get_process_memory_contexts($pid, true); +)); +$node->wait_for_event('client backend', 'memcontext-client-injection'); +$node->psql( + 'postgres', + qq(select pg_get_process_memory_contexts($pid, true);), + stderr => \$psql_err); +ok($psql_err =~ + /WARNING: server process $pid is processing previous request/); +# Wake the client up. +$node->safe_psql('postgres', + "SELECT injection_points_wakeup('memcontext-client-injection');"); + +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-client-injection');"); +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-wait');"); + +# Test the client process exiting with timeout does not break the server +# process + +$node->safe_psql('postgres', + "SELECT injection_points_attach('memcontext-server-wait', 'wait');"); +# Following client query times out, returning NULL as output +$node->psql( + 'postgres', + qq(select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';), + stdout => \$psql_out); +is($psql_out, '', 'Query returns null'); +# Wakeup the server process up and detach the injection point. +$node->safe_psql('postgres', + "SELECT injection_points_wakeup('memcontext-server-wait');"); +$node->safe_psql('postgres', + "select injection_points_detach('memcontext-server-wait');"); +# Query the same server process again and it should succeed. +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name eq 'TopMemoryContext'); + +# Test if the monitoring works fine when server restarts after client backend crashes. +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-client-injection', 'test_memcontext_reporting', 'crash', NULL);" +); + +# Client will crash +$node->psql( + 'postgres', + qq(select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';), + stderr => \$psql_err); +like($psql_err, + qr/WARNING: terminating connection because of crash of another server process|server closed the connection unexpectedly|connection to server was lost|could not send data to server/ +); + +# Wait till server restarts +$node->restart; +$node->poll_query_until('postgres', "SELECT 1;", '1'); + +# Querying memory stats should succeed after server start +$pid = $node->safe_psql('postgres', + "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name eq 'TopMemoryContext'); + +# Test if the monitoring works fine when server restarts after server process crashes. +$node->safe_psql('postgres', + "select injection_points_attach('memcontext-server-injection', 'test_memcontext_reporting', 'crash', NULL);" +); + +# Server process will crash +$node->psql( + 'postgres', + qq(select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';), + stderr => \$psql_err); +like($psql_err, + qr/WARNING: terminating connection because of crash of another server process|server closed the connection unexpectedly|connection to server was lost|could not send data to server/ +); + +# Wait till server restarts +$node->restart; +$node->poll_query_until('postgres', "SELECT 1;", '1'); + +# Querying memory stats should succeed after server start +$pid = $node->safe_psql('postgres', + "SELECT pid from pg_stat_activity where backend_type='checkpointer'"); +$topcontext_name = $node->safe_psql('postgres', + "select name from pg_get_process_memory_contexts($pid, true) where path = '{1}';" +); +ok($topcontext_name eq 'TopMemoryContext'); + +done_testing(); diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c new file mode 100644 index 00000000000..d641f3616dc --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.c @@ -0,0 +1,12 @@ +#include "postgres.h" +#include "funcapi.h" + +PG_MODULE_MAGIC; + +extern PGDLLEXPORT void crash(const char *name, const void *private_data, void *arg); + +void +crash(const char *name, const void *private_data, void *arg) +{ + abort(); +} diff --git a/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control new file mode 100644 index 00000000000..10c3786d193 --- /dev/null +++ b/src/test/modules/test_memcontext_reporting/test_memcontext_reporting.control @@ -0,0 +1 @@ +comment = 'Test code for memcontext reporting' -- 2.34.1 [application/octet-stream] v54-0001-Add-function-to-report-memory-context-statistics.patch (60.3K, ../../CAH2L28tU+aC1+OR5S1ATxHPeyuGp5JAfBVcs+7r-h_N2JRaxUw@mail.gmail.com/4-v54-0001-Add-function-to-report-memory-context-statistics.patch) download | inline diff: From 836a1b255418a23be3378a826a06bb1c8248ec96 Mon Sep 17 00:00:00 2001 From: Rahila Syed <[email protected]> Date: Thu, 27 Nov 2025 14:39:43 +0530 Subject: [PATCH 1/2] Add function to report memory context statistics This adds a function for retrieving memory context statistics and information from backends as well as auxiliary processes. The intended usecase is cluster debugging when under memory pressure or unanticipated memory usage characteristics. When calling the function it sends a signal to the specified process to submit statistics regarding its memory contexts into dynamic shared memory. Each memory context is returned in detail, followed by a cumulative total in case the number of contexts exceed the max allocated amount of shared memory. Each process is limited to use at most 1Mb memory for this. A summary can also be explicitly requested by the user, this will return the TopMemoryContext and a cumulative total of all lower contexts. In order to not block on busy processes, we have hardcoded the number of seconds during which to retry before timing out. In the case where no statistics are published within the set timeout, NULL is returned --- doc/src/sgml/func/func-admin.sgml | 160 +++ src/backend/catalog/system_functions.sql | 14 + src/backend/postmaster/autovacuum.c | 4 + src/backend/postmaster/checkpointer.c | 4 + src/backend/postmaster/interrupt.c | 4 + src/backend/postmaster/pgarch.c | 4 + src/backend/postmaster/startup.c | 4 + src/backend/postmaster/walsummarizer.c | 4 + src/backend/storage/ipc/dsm_registry.c | 8 +- src/backend/storage/ipc/ipci.c | 3 + src/backend/storage/ipc/procsignal.c | 3 + src/backend/tcop/postgres.c | 3 + .../utils/activity/wait_event_names.txt | 2 + src/backend/utils/adt/mcxtfuncs.c | 1025 ++++++++++++++++- src/backend/utils/init/globals.c | 1 + src/backend/utils/init/postinit.c | 7 + src/backend/utils/mmgr/mcxt.c | 31 + src/include/catalog/pg_proc.dat | 10 + src/include/miscadmin.h | 1 + src/include/storage/lwlocklist.h | 1 + src/include/storage/procsignal.h | 1 + src/include/utils/memutils.h | 7 + src/test/regress/expected/sysviews.out | 19 + src/test/regress/sql/sysviews.sql | 18 + src/tools/pgindent/typedefs.list | 2 + 25 files changed, 1318 insertions(+), 22 deletions(-) diff --git a/doc/src/sgml/func/func-admin.sgml b/doc/src/sgml/func/func-admin.sgml index 17319f9f969..06bdfc8ad12 100644 --- a/doc/src/sgml/func/func-admin.sgml +++ b/doc/src/sgml/func/func-admin.sgml @@ -251,6 +251,133 @@ <literal>false</literal> is returned. </para></entry> </row> + + <row> + <entry role="func_table_entry"><para role="func_signature"> + <indexterm> + <primary>pg_get_process_memory_contexts</primary> + </indexterm> + <function>pg_get_process_memory_contexts</function> ( <parameter>pid</parameter> <type>integer</type> <optional>,<parameter>summary</parameter> <type>boolean</type> <literal>DEFAULT</literal> <literal>false</literal></optional> ) + <returnvalue>setof record</returnvalue> + ( <parameter>name</parameter> <type>text</type>, + <parameter>ident</parameter> <type>text</type>, + <parameter>type</parameter> <type>text</type>, + <parameter>level</parameter> <type>integer</type>, + <parameter>path</parameter> <type>integer[]</type>, + <parameter>total_bytes</parameter> <type>bigint</type>, + <parameter>total_nblocks</parameter> <type>bigint</type>, + <parameter>free_bytes</parameter> <type>bigint</type>, + <parameter>free_chunks</parameter> <type>bigint</type>, + <parameter>used_bytes</parameter> <type>bigint</type>, + <parameter>num_agg_contexts</parameter> <type>integer</type> ) + </para> + <para> + This function handles requests to display the memory contexts of a + <productname>PostgreSQL</productname> process with the specified + process ID. The function can be used to send requests to + <glossterm linkend="glossary-backend">backends</glossterm> as well as + <glossterm linkend="glossary-auxiliary-proc">auxiliary processes</glossterm>. + If the process does not respond with memory contexts statistics in 5 + seconds, the function emits a <literal>NOTICE</literal> with the message + <literal>request for memory context statistics to PID %d timed out</literal>, + and then returns NULL. + </para> + <para> + After receiving memory context statistics from the target process, it + returns the results as one row per context. If all the contexts don't + fit within the pre-determined size limit, the remaining context + statistics are aggregated and a cumulative total is displayed. The + <literal>num_agg_contexts</literal> column indicates the number of + contexts aggregated in the displayed statistics. + </para> + <para> + The returned record contains extended statistics per each memory + context: + <itemizedlist spacing="compact"> + <listitem> + <para> + <parameter>name</parameter> - The name of the memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>ident</parameter> - Memory context ID (if any). + </para> + </listitem> + <listitem> + <para> + <parameter>type</parameter> - The type of memory context, possible + values are: AllocSet, Generation, Slab and Bump. + </para> + </listitem> + <listitem> + <para> + <parameter>level</parameter> - The level in the tree of the current + memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>path</parameter> - Memory contexts are organized in a + tree model with TopMemoryContext as the root, and all other memory + contexts as nodes in the tree. The <parameter>path</parameter> + displays the path from the root to the current memory context + The path that is printed will be limited to 100 nodes counting from + the TopMemoryContext, to preserve memory during reporting. + </para> + </listitem> + <listitem> + <para> + <parameter>total_bytes</parameter> - The total number of bytes + allocated to this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>total_nblocks</parameter> - The total number of blocks + used for the allocated memory. + </para> + </listitem> + <listitem> + <para> + <parameter>free_bytes</parameter> - The amount of free memory in + this memory context. + </para> + </listitem> + <listitem> + <para> + <parameter>free_chunks</parameter> - The number of chunks that + <parameter>free_bytes</parameter> corresponds to. + </para> + </listitem> + <listitem> + <para> + <parameter>used_bytes</parameter> - The total number of bytes + currently occupied. + </para> + </listitem> + <listitem> + <para> + <parameter>num_agg_contexts</parameter> - The number of memory + contexts aggregated in the displayed statistics. Statistics are aggregated + when the internal reporting buffer (about 1 MB) is filled.<literal>1</literal> + indicates that context statistics are displayed separately. + </para> + </listitem> + </itemizedlist> + </para> + <para> + When <parameter>summary</parameter> is <literal>true</literal>, statistics + for memory contexts at levels 1 and 2 are displayed, with level 1 + representing the root node (i.e., <literal>TopMemoryContext</literal>). + Statistics for contexts on level 2 and below are aggregates of all + child contexts' statistics, where <literal>num_agg_contexts</literal> + indicate the number of aggregated child contexts. When + <parameter>summary</parameter> is <literal>false</literal> (the default), + <literal>the num_agg_contexts</literal> value is <literal>1</literal>, + indicating that individual statistics are being displayed. + </para></entry> + </row> </tbody> </tgroup> </table> @@ -302,6 +429,39 @@ LOG: Grand total: 1651920 bytes in 201 blocks; 622360 free (88 chunks); 1029560 because it may generate a large number of log messages. </para> + <para> + <function>pg_get_process_memory_contexts</function> can be used to request + memory contexts statistics of any <productname>PostgreSQL</productname> + process. For example: +<programlisting> +postgres=# SELECT * FROM pg_get_process_memory_contexts( + (SELECT pid FROM pg_stat_activity + WHERE backend_type = 'checkpointer'), + false) LIMIT 1; +-[ RECORD 1 ]----+------------------------------ +name | TopMemoryContext +ident | +type | AllocSet +level | 1 +path | {1} +total_bytes | 90304 +total_nblocks | 3 +free_bytes | 2880 +free_chunks | 1 +used_bytes | 87424 +num_agg_contexts | 1 +</programlisting> + <note> + <para> + While <function>pg_get_process_memory_contexts</function> can be used to + query memory contexts of the local backend, + <structname>pg_backend_memory_contexts</structname> + (see <xref linkend="view-pg-backend-memory-contexts"/> for more details) + will be less resource intensive when only the local backend is of interest. + </para> + </note> + </para> + </sect2> <sect2 id="functions-admin-backup"> diff --git a/src/backend/catalog/system_functions.sql b/src/backend/catalog/system_functions.sql index 69699f8830a..b62944cee87 100644 --- a/src/backend/catalog/system_functions.sql +++ b/src/backend/catalog/system_functions.sql @@ -378,6 +378,17 @@ BEGIN ATOMIC END; +CREATE OR REPLACE FUNCTION + pg_get_process_memory_contexts(IN pid integer, IN summary boolean DEFAULT false, + OUT name text, OUT ident text, OUT type text, OUT level integer, + OUT path integer[], OUT total_bytes bigint, OUT total_nblocks bigint, + OUT free_bytes bigint, OUT free_chunks bigint, OUT used_bytes bigint, + OUT num_agg_contexts integer) +RETURNS SETOF RECORD +LANGUAGE INTERNAL +STRICT VOLATILE PARALLEL UNSAFE +AS 'pg_get_process_memory_contexts'; + -- -- The default permissions for functions mean that anyone can execute them. -- A number of functions shouldn't be executable by just anyone, but rather @@ -503,6 +514,7 @@ REVOKE EXECUTE ON FUNCTION pg_ls_logicalmapdir() FROM PUBLIC; REVOKE EXECUTE ON FUNCTION pg_ls_replslotdir(text) FROM PUBLIC; +REVOKE EXECUTE ON FUNCTION pg_get_process_memory_contexts(integer, boolean) FROM PUBLIC; -- -- We also set up some things as accessible to standard roles. -- @@ -529,6 +541,8 @@ GRANT EXECUTE ON FUNCTION pg_current_logfile() TO pg_monitor; GRANT EXECUTE ON FUNCTION pg_current_logfile(text) TO pg_monitor; +GRANT EXECUTE ON FUNCTION pg_get_process_memory_contexts(integer, boolean) TO pg_read_all_stats; + GRANT pg_read_all_settings TO pg_monitor; GRANT pg_read_all_stats TO pg_monitor; diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c index 6fde740465f..3c24119c43a 100644 --- a/src/backend/postmaster/autovacuum.c +++ b/src/backend/postmaster/autovacuum.c @@ -787,6 +787,10 @@ ProcessAutoVacLauncherInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + /* Process sinval catchup interrupts that happened while sleeping */ ProcessCatchupInterrupt(); } diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c index e03c19123bc..d54193f8890 100644 --- a/src/backend/postmaster/checkpointer.c +++ b/src/backend/postmaster/checkpointer.c @@ -684,6 +684,10 @@ ProcessCheckpointerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/interrupt.c b/src/backend/postmaster/interrupt.c index a2c0ff012c5..cb18ce80893 100644 --- a/src/backend/postmaster/interrupt.c +++ b/src/backend/postmaster/interrupt.c @@ -48,6 +48,10 @@ ProcessMainLoopInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/postmaster/pgarch.c b/src/backend/postmaster/pgarch.c index 82731e452fc..e13a40f1427 100644 --- a/src/backend/postmaster/pgarch.c +++ b/src/backend/postmaster/pgarch.c @@ -870,6 +870,10 @@ ProcessPgArchInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ConfigReloadPending) { char *archiveLib = pstrdup(XLogArchiveLibrary); diff --git a/src/backend/postmaster/startup.c b/src/backend/postmaster/startup.c index cdbe53dd262..1d6d8f22d20 100644 --- a/src/backend/postmaster/startup.c +++ b/src/backend/postmaster/startup.c @@ -192,6 +192,10 @@ ProcessStartupProcInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } diff --git a/src/backend/postmaster/walsummarizer.c b/src/backend/postmaster/walsummarizer.c index 742137edad6..d5a3cb13a4c 100644 --- a/src/backend/postmaster/walsummarizer.c +++ b/src/backend/postmaster/walsummarizer.c @@ -875,6 +875,10 @@ ProcessWalSummarizerInterrupts(void) /* Perform logging of memory contexts of this process */ if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + + /* Publish memory contexts of this process */ + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); } /* diff --git a/src/backend/storage/ipc/dsm_registry.c b/src/backend/storage/ipc/dsm_registry.c index 068c1577b12..4810babac1b 100644 --- a/src/backend/storage/ipc/dsm_registry.c +++ b/src/backend/storage/ipc/dsm_registry.c @@ -270,7 +270,9 @@ GetNamedDSMSegment(const char *name, size_t size, * This routine returns a pointer to the DSA. A new LWLock tranche ID will be * generated if needed. Note that the lock tranche will be registered with the * provided name. Also note that this should be called at most once for a - * given DSA in each backend. + * given DSA in each backend. It's best to minimize the number of errors thrown + * in this function since it's called from a CFI function. An error here could + * lead to error in any transaction that calls the CFI function. */ dsa_area * GetNamedDSA(const char *name, bool *found) @@ -351,7 +353,9 @@ GetNamedDSA(const char *name, bool *found) * params is ignored; a new LWLock tranche ID will be generated if needed. * Note that the lock tranche will be registered with the provided name. Also * note that this should be called at most once for a given table in each - * backend. + * backend. It's best to minimize the number of errors thrown in this function + * since it's called from a CFI function. An error here could lead to error in + * any transaction that calls the CFI function. */ dshash_table * GetNamedDSHash(const char *name, const dshash_parameters *params, bool *found) diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c index 1f7e933d500..b0176613987 100644 --- a/src/backend/storage/ipc/ipci.c +++ b/src/backend/storage/ipc/ipci.c @@ -52,6 +52,7 @@ #include "storage/sinvaladt.h" #include "utils/guc.h" #include "utils/injection_point.h" +#include "utils/memutils.h" /* GUCs */ int shared_memory_type = DEFAULT_SHARED_MEMORY_TYPE; @@ -141,6 +142,7 @@ CalculateShmemSize(void) size = add_size(size, AioShmemSize()); size = add_size(size, WaitLSNShmemSize()); size = add_size(size, LogicalDecodingCtlShmemSize()); + size = add_size(size, MemoryContextKeysShmemSize()); /* include additional requested shmem from preload libraries */ size = add_size(size, total_addin_request); @@ -328,6 +330,7 @@ CreateOrAttachShmemStructs(void) AioShmemInit(); WaitLSNShmemInit(); LogicalDecodingCtlShmemInit(); + MemoryContextKeysShmemInit(); } /* diff --git a/src/backend/storage/ipc/procsignal.c b/src/backend/storage/ipc/procsignal.c index d47d180a32f..227d6333632 100644 --- a/src/backend/storage/ipc/procsignal.c +++ b/src/backend/storage/ipc/procsignal.c @@ -696,6 +696,9 @@ procsignal_sigusr1_handler(SIGNAL_ARGS) if (CheckProcSignal(PROCSIG_LOG_MEMORY_CONTEXT)) HandleLogMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_GET_MEMORY_CONTEXT)) + HandleGetMemoryContextInterrupt(); + if (CheckProcSignal(PROCSIG_PARALLEL_APPLY_MESSAGE)) HandleParallelApplyMessageInterrupt(); diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c index d01a09dd0c4..9e0d289a604 100644 --- a/src/backend/tcop/postgres.c +++ b/src/backend/tcop/postgres.c @@ -3573,6 +3573,9 @@ ProcessInterrupts(void) if (LogMemoryContextPending) ProcessLogMemoryContextInterrupt(); + if (PublishMemoryContextPending) + ProcessGetMemoryContextInterrupt(); + if (ParallelApplyMessagePending) ProcessParallelApplyMessages(); } diff --git a/src/backend/utils/activity/wait_event_names.txt b/src/backend/utils/activity/wait_event_names.txt index 4aa864fe3c3..3e1dc4225b2 100644 --- a/src/backend/utils/activity/wait_event_names.txt +++ b/src/backend/utils/activity/wait_event_names.txt @@ -163,6 +163,7 @@ WAL_RECEIVER_EXIT "Waiting for the WAL receiver to exit." WAL_RECEIVER_WAIT_START "Waiting for startup process to send initial data for streaming replication." WAL_SUMMARY_READY "Waiting for a new WAL summary to be generated." XACT_GROUP_UPDATE "Waiting for the group leader to update transaction status at transaction end." +MEM_CXT_PUBLISH "Waiting for a process to publish memory information." ABI_compatibility: @@ -366,6 +367,7 @@ SerialControl "Waiting to read or update shared <filename>pg_serial</filename> s AioWorkerSubmissionQueue "Waiting to access AIO worker submission queue." WaitLSN "Waiting to read or update shared Wait-for-LSN state." LogicalDecodingControl "Waiting to read or update logical decoding status information." +MemContextReportingClientKeys "Waiting for another process to complete reading or writing the memory reporting keys." # # END OF PREDEFINED LWLOCKS (DO NOT CHANGE THIS LINE) diff --git a/src/backend/utils/adt/mcxtfuncs.c b/src/backend/utils/adt/mcxtfuncs.c index c7f7b8bc2dd..f0bc634965f 100644 --- a/src/backend/utils/adt/mcxtfuncs.c +++ b/src/backend/utils/adt/mcxtfuncs.c @@ -15,25 +15,128 @@ #include "postgres.h" +#include "access/xact.h" #include "funcapi.h" #include "mb/pg_wchar.h" +#include "storage/dsm_registry.h" #include "storage/proc.h" #include "storage/procarray.h" #include "storage/procsignal.h" #include "utils/array.h" #include "utils/builtins.h" #include "utils/hsearch.h" +#include "utils/injection_point.h" +#include "utils/memutils.h" +#include "utils/wait_event_types.h" + +/* + * Memory Context reporting size limits. + */ + +/* Max length of context name and ident, to keep it consistent + * with ProcessLogMemoryContext() + */ +#define MEMORY_CONTEXT_IDENT_SHMEM_SIZE 100 +#define MEMORY_CONTEXT_NAME_SHMEM_SIZE 100 +#define MAX_PATH_DISPLAY_LENGTH 100 + +/* Maximum size (in bytes) of DSA area per process */ +#define MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND ((size_t) (1 * 1024 * 1024)) + +/* + * Maximum number of memory context statistics is calculated by dividing + * max memory allocated per backend with maximum size per context statistics. + * The identifier and name are statically allocated arrays of size 100 bytes. + * The path depth is limited to 100 like for memory context logging. + */ +#define MAX_MEMORY_CONTEXT_STATS_NUM MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND / (sizeof(MemoryStatsEntry)) + +/* + * Size of dshash key. The key is a uint32 rendered as a string, 10 chars + * plus space for a NULL terminator can hold all the values. + */ +#define CLIENT_KEY_SIZE (10 + 1) + +/* Dynamic shared memory state for reporting statistics per context */ +typedef struct MemoryStatsEntry +{ + char name[MEMORY_CONTEXT_NAME_SHMEM_SIZE]; + char ident[MEMORY_CONTEXT_IDENT_SHMEM_SIZE]; + int path[MAX_PATH_DISPLAY_LENGTH]; + NodeTag type; + int path_length; + int levels; + int64 totalspace; + int64 nblocks; + int64 freespace; + int64 freechunks; + int num_agg_stats; +} MemoryStatsEntry; + +/* + * Per backend dynamic shared hash entry for memory context statistics + * reporting. + */ +typedef struct MemoryStatsDSHashEntry +{ + ProcNumber client_key; + ConditionVariable memcxt_cv; + bool stats_written; + int target_server_id; + int total_stats; + bool summary; + dsa_pointer memstats_dsa_pointer; +} MemoryStatsDSHashEntry; + +static const dshash_parameters memctx_dsh_params = { + sizeof(ProcNumber), + sizeof(MemoryStatsDSHashEntry), + dshash_memcmp, + dshash_memhash, + dshash_memcpy +}; + +/* + * These are used for reporting memory context statistics of a process. + */ + +/* Array to store the keys of MemoryStatsDsHash */ +static int *client_keys = NULL; + +/* + * Table to store pointers to DSA memory containing memory statistics and other + * metadata. There is one entry per client backend request, keyed by ProcNumber + * of the client obtained from client_keys array above. + */ +static dshash_table *MemoryStatsDsHash = NULL; + +/* + * Dsa area which stores the actual memory context + * statistics. + */ +static dsa_area *MemoryStatsDsaArea = NULL; + +static void memstats_dsa_cleanup(int ProcNumber); +static void memstats_client_key_reset(int ProcNumber); +static void PublishMemoryContext(MemoryStatsEntry *memcxt_info, + int curr_id, MemoryContext context, + List *path, + MemoryContextCounters stat, + int num_contexts); /* ---------- * The max bytes for showing identifiers of MemoryContext. + * This is used by pg_get_backend_memory_context - view used for local backend. * ---------- */ #define MEMORY_CONTEXT_IDENT_DISPLAY_SIZE 1024 +/* Timeout in seconds */ +#define MEMORY_STATS_MAX_TIMEOUT 5 + /* * MemoryContextId - * Used for storage of transient identifiers for - * pg_get_backend_memory_contexts. + * Used for storage of transient identifiers for memory context reporting */ typedef struct MemoryContextId { @@ -63,6 +166,67 @@ int_list_to_array(const List *list) return PointerGetDatum(result_array); } +/* + * context_type_to_string + * Returns a textual representation of a context type + * + * This should cover the same types as MemoryContextIsValid. + */ +static const char * +context_type_to_string(NodeTag type) +{ + const char *context_type; + + switch (type) + { + case T_AllocSetContext: + context_type = "AllocSet"; + break; + case T_GenerationContext: + context_type = "Generation"; + break; + case T_SlabContext: + context_type = "Slab"; + break; + case T_BumpContext: + context_type = "Bump"; + break; + default: + context_type = "???"; + break; + } + return context_type; +} + +/* + * compute_context_path + * + * Append the transient context_id of this context and each of its ancestors + * to a list, in order to compute a path. + */ +static List * +compute_context_path(MemoryContext c, HTAB *context_id_lookup) +{ + bool found; + List *path = NIL; + MemoryContext cur_context; + + for (cur_context = c; cur_context != NULL; cur_context = cur_context->parent) + { + MemoryContextId *cur_entry; + + cur_entry = hash_search(context_id_lookup, &cur_context, HASH_FIND, &found); + + if (!found) + { + elog(NOTICE, "hash table corrupted, can't construct path value"); + return NIL; + } + path = lcons_int(cur_entry->context_id, path); + } + return path; +} + /* * PutMemoryContextsStatsTupleStore * Add details for the given MemoryContext to 'tupstore'. @@ -144,24 +308,7 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore, else nulls[1] = true; - switch (context->type) - { - case T_AllocSetContext: - type = "AllocSet"; - break; - case T_GenerationContext: - type = "Generation"; - break; - case T_SlabContext: - type = "Slab"; - break; - case T_BumpContext: - type = "Bump"; - break; - default: - type = "???"; - break; - } + type = context_type_to_string(context->type); values[2] = CStringGetTextDatum(type); values[3] = Int32GetDatum(list_length(path)); /* level */ @@ -306,3 +453,841 @@ pg_log_backend_memory_contexts(PG_FUNCTION_ARGS) PG_RETURN_BOOL(true); } + +/* + * pg_get_process_memory_contexts + * Signal a backend or an auxiliary process to send its memory contexts, + * wait for the results and display them. + * + * By default, only superusers or users with ROLE_PG_READ_ALL_STATS are allowed + * to signal a process to return the memory contexts. Additional roles can be + * permitted with GRANT. + * + * On receipt of this signal, a backend or an auxiliary process sets the flag + * in the signal handler, which causes the next CHECK_FOR_INTERRUPTS() + * or process-specific interrupt handler to copy the memory context details + * to a dynamic shared memory space. + * + * We have defined a limit on DSA memory that could be allocated per process - + * if the process has more memory contexts than what can fit in the allocated + * size, the excess contexts are summarized and represented as cumulative total + * at the end of the buffer. + * + * After sending the signal, wait on a condition variable. The publishing + * backend, after copying the data to shared memory, sends a signal on that + * condition variable. There is one condition variable per client process. + * Once the condition variable is signalled, check if the latest memory context + * information is available and display. + * + * If the publishing backend does not respond before the condition variable + * times out, which is set to a predefined value MEMORY_STATS_MAX_TIMEOUT, give + * up and return NULL. + */ +Datum +pg_get_process_memory_contexts(PG_FUNCTION_ARGS) +{ + int pid = PG_GETARG_INT32(0); + bool summary = PG_GETARG_BOOL(1); + PGPROC *proc; + ProcNumber client_procno = MyProcNumber; + ProcNumber server_procno; + bool proc_is_aux = false; + ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo; + MemoryStatsEntry *memcxt_info; + MemoryStatsDSHashEntry *entry; + bool found; + TimestampTz start_timestamp; + + /* + * See if the process with given pid is a backend or an auxiliary process + * and remember the type for when we requery the process later. + */ + proc = BackendPidGetProc(pid); + if (proc == NULL) + { + proc = AuxiliaryPidGetProc(pid); + proc_is_aux = true; + } + + /* + * BackendPidGetProc() and AuxiliaryPidGetProc() return NULL if the pid + * isn't valid; this is however not a problem and leave with a WARNING. + * See comment in pg_log_backend_memory_contexts for a discussion on this. + */ + if (proc == NULL) + { + ereport(WARNING, + errmsg("PID %d is not a PostgreSQL server process", pid)); + PG_RETURN_NULL(); + } + + server_procno = GetNumberFromPGProc(proc); + + /* + * First, check if the server process slot is occupied; if so, exit early. + * An occupied slot indicates another client backend is already requesting + * statistics from this server process. We should not set the slot yet, + * even if it appears empty, until we are prepared to signal the server + * process. This is because if the following DSA APIs throw an error, the + * server slot may not be reset, leading to it being incorrectly marked as + * occupied and blocking further memory context statistics queries to the + * same process. + */ + LWLockAcquire(MemContextReportingClientKeysLock, LW_EXCLUSIVE); + if (client_keys[server_procno] != -1) + { + LWLockRelease(MemContextReportingClientKeysLock); + ereport(WARNING, + errmsg("server process %d is processing previous request", + pid)); + PG_RETURN_NULL(); + } + LWLockRelease(MemContextReportingClientKeysLock); + + /* + * Create a DSA to allocate memory for copying memory contexts statistics. + * Allocate the memory in the DSA and send DSA pointer to the server + * process for storing the context statistics. If number of contexts + * exceed a predefined limit (1MB), a cumulative total is stored for such + * contexts. + * + * The DSA is created once for the lifetime of the server, and only + * attached in subsequent calls. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); + + /* + * The DSA pointers containing statistics for each client are stored in a + * dshash table. In addition to DSA pointer, each entry in this table also + * contains information about the statistics, condition variable for + * signalling between client and the server and miscellaneous data + * specific to a request. There is one entry per client request in the + * hash table. + */ + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); + + /* + * Insert an entry for this client in DSHASH table the first time this + * function is called. This entry is deleted when the process exits in + * before_shmem_exit call. + * + * dshash_find_or_insert locks the entry to prevent the publisher from + * reading before client has updated the entry. + */ + entry = dshash_find_or_insert(MemoryStatsDsHash, &client_procno, &found); + if (!found) + { + entry->stats_written = false; + ConditionVariableInit(&entry->memcxt_cv); + } + + /* + * Allocate 1MB of memory for the backend to publish its statistics on + * every call to this function. The memory is freed at the end of the + * function. + */ + entry->memstats_dsa_pointer = + dsa_allocate0(MemoryStatsDsaArea, MEMORY_CONTEXT_REPORT_MAX_PER_BACKEND); + + /* + * Specify whether a summary of statistics is requested, before signalling + * the server. + */ + entry->summary = summary; + + /* + * Indicate which server process statistics are being requested from. If + * this client times out before the last requested process can publish its + * statistics, it may send a new request to another server process. Since + * the previous server was notified, it might attempt to read the same + * client entry and respond incorrectly with its statistics. By storing + * the server ID in the client entry, we prevent any previously signalled + * server process from writing its statistics in the space meant for the + * newly requested process. + */ + entry->target_server_id = pid; + dshash_release_lock(MemoryStatsDsHash, entry); + + /* + * Check if the publishing process slot is empty and store this clients + * key i.e its procNumber. This informs the publishing process that it is + * supposed to write statistics in the hash entry corresponding to this + * client. + */ + LWLockAcquire(MemContextReportingClientKeysLock, LW_EXCLUSIVE); + if (client_keys[server_procno] == -1) + client_keys[server_procno] = MyProcNumber; + else + { + LWLockRelease(MemContextReportingClientKeysLock); + memstats_dsa_cleanup(client_procno); + ereport(WARNING, + errmsg("server process %d is processing previous request", + pid)); + PG_RETURN_NULL(); + } + LWLockRelease(MemContextReportingClientKeysLock); + + /* + * Send a signal to a PostgreSQL process, informing it we want it to + * produce information about its memory contexts. + */ + if (SendProcSignal(pid, PROCSIG_GET_MEMORY_CONTEXT, server_procno) < 0) + { + memstats_dsa_cleanup(client_procno); + memstats_client_key_reset(server_procno); + ereport(WARNING, + errmsg("could not send signal to process %d: %m", + pid)); + PG_RETURN_NULL(); + } + start_timestamp = GetCurrentTimestamp(); + + while (1) + { + long elapsed_time; + + INJECTION_POINT("memcontext-client-injection", NULL); + + elapsed_time = TimestampDifferenceMilliseconds(start_timestamp, + GetCurrentTimestamp()); + /* Return if we have already exceeded the timeout */ + if (elapsed_time >= MEMORY_STATS_MAX_TIMEOUT * 1000) + { + memstats_dsa_cleanup(client_procno); + memstats_client_key_reset(server_procno); + ConditionVariableCancelSleep(); + ereport(NOTICE, + errmsg("request for memory context statistics to PID %d timed out", + pid)); + PG_RETURN_NULL(); + } + + /* + * Recheck the state of the backend before sleeping on the condition + * variable to ensure the process is still alive. Only check the + * relevant process type based on the earlier PID check. + */ + if (proc_is_aux) + proc = AuxiliaryPidGetProc(pid); + else + proc = BackendPidGetProc(pid); + + /* + * The target server process ending during memory context processing + * is not an error. + */ + if (proc == NULL) + { + memstats_dsa_cleanup(client_procno); + memstats_client_key_reset(server_procno); + ConditionVariableCancelSleep(); + ereport(WARNING, + errmsg("PID %d is no longer a PostgreSQL server process", + pid)); + PG_RETURN_NULL(); + } + + /* + * Wait for at most MEMORY_STATS_MAX_TIMEOUT seconds, if we have been + * woken up spuriously we subtract the time already slept. If no + * statistics are available within the allowed time then return no + * rows and emit a NOTICE. The timer is defined in milliseconds since + * that's what the condition variable sleep uses. + */ + if (ConditionVariableTimedSleep(&entry->memcxt_cv, + (MEMORY_STATS_MAX_TIMEOUT * 1000) - elapsed_time, + WAIT_EVENT_MEM_CXT_PUBLISH)) + { + /* Timeout has expired, return NULL */ + memstats_dsa_cleanup(client_procno); + memstats_client_key_reset(server_procno); + ConditionVariableCancelSleep(); + ereport(NOTICE, + errmsg("request for memory context statistics to PID %d timed out", + pid)); + PG_RETURN_NULL(); + } + entry = dshash_find_or_insert(MemoryStatsDsHash, &client_procno, &found); + Assert(found); + + memcxt_info = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + /* + * We expect to come out of sleep when the requested process has + * finished publishing the statistics, verified using a boolean + * stats_written. + * + * Make sure that the statistics are actually written by checking that + * the name of the context is not NULL. This is done to ensure that + * the subsequent waits for statistics do not return spuriously if the + * previous call to the function ended in error and thus could not + * clear the stats_written flag. + */ + if (entry->stats_written && memcxt_info[0].name[0] != '\0') + break; + + dshash_release_lock(MemoryStatsDsHash, entry); + + } + + InitMaterializedSRF(fcinfo, 0); + + /* + * Backend has finished publishing the stats, project them. + */ +#define PG_GET_PROCESS_MEMORY_CONTEXTS_COLS 11 + for (int i = 0; i < entry->total_stats; i++) + { + ArrayType *path_array; + Datum values[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + bool nulls[PG_GET_PROCESS_MEMORY_CONTEXTS_COLS]; + Datum *path_datum = NULL; + + memset(values, 0, sizeof(values)); + memset(nulls, 0, sizeof(nulls)); + + Assert(memcxt_info[i].name[0] != '\0'); + values[0] = CStringGetTextDatum(memcxt_info[i].name); + + if (memcxt_info[i].ident[0] != '\0') + values[1] = CStringGetTextDatum(memcxt_info[i].ident); + else + nulls[1] = true; + + values[2] = CStringGetTextDatum(context_type_to_string(memcxt_info[i].type)); + + if (memcxt_info[i].levels != 0) + values[3] = Int32GetDatum(memcxt_info[i].levels); + else + nulls[3] = true; + + if (memcxt_info[i].path[0] != 0) + { + int path_length; + + path_length = memcxt_info[i].path_length; + path_datum = (Datum *) palloc(path_length * sizeof(Datum)); + + for (int j = 0; j < path_length; j++) + path_datum[j] = Int32GetDatum(memcxt_info[i].path[j]); + path_array = construct_array_builtin(path_datum, + path_length, + INT4OID); + values[4] = PointerGetDatum(path_array); + } + else + nulls[4] = true; + + values[5] = Int64GetDatum(memcxt_info[i].totalspace); + values[6] = Int64GetDatum(memcxt_info[i].nblocks); + values[7] = Int64GetDatum(memcxt_info[i].freespace); + values[8] = Int64GetDatum(memcxt_info[i].freechunks); + values[9] = Int64GetDatum(memcxt_info[i].totalspace - + memcxt_info[i].freespace); + values[10] = Int32GetDatum(memcxt_info[i].num_agg_stats); + + tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, + values, nulls); + } + dshash_release_lock(MemoryStatsDsHash, entry); + memstats_dsa_cleanup(client_procno); + + ConditionVariableCancelSleep(); + + PG_RETURN_NULL(); +} + +/* + * This function is executed before returning from the + * pg_get_process_memory_contexts() function. + * It releases the DSA memory allocated for storing the memory statistics + * retrieved by that function. The caller must ensure that the LWLock for + * MemoryStatsDsHash is not already held. + */ +static void +memstats_dsa_cleanup(int procNumber) +{ + MemoryStatsDSHashEntry *entry; + + entry = dshash_find(MemoryStatsDsHash, &procNumber, true); + + if (entry == NULL) + return; + + Assert(MemoryStatsDsaArea != NULL); + if (DsaPointerIsValid(entry->memstats_dsa_pointer)) + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + entry->memstats_dsa_pointer = InvalidDsaPointer; + entry->stats_written = false; + entry->target_server_id = 0; + + dshash_release_lock(MemoryStatsDsHash, entry); +} + +/* + * Remove this process from the publishing process' + * client key slot, if the stats publishing process has failed to do so. + */ +static void +memstats_client_key_reset(int procNumber) +{ + LWLockAcquire(MemContextReportingClientKeysLock, LW_EXCLUSIVE); + + if (client_keys[procNumber] == MyProcNumber) + client_keys[procNumber] = -1; + LWLockRelease(MemContextReportingClientKeysLock); +} + +void +MemoryContextKeysShmemInit(void) +{ + bool found; + + client_keys = (int *) + ShmemInitStruct("MemoryContextKeys", + MemoryContextKeysShmemSize(), &found); + + if (!found) + MemSet(client_keys, -1, MemoryContextKeysShmemSize()); +} + +Size +MemoryContextKeysShmemSize(void) +{ + Size TotalProcs = 0; + + TotalProcs = add_size(TotalProcs, NUM_AUXILIARY_PROCS); + TotalProcs = add_size(TotalProcs, MaxBackends); + + return mul_size(TotalProcs, sizeof(int)); +} + +/* + * HandleGetMemoryContextInterrupt + * Handle receipt of an interrupt indicating a request to publish memory + * contexts statistics. + * + * All the actual work is deferred to ProcessGetMemoryContextInterrupt() as + * this cannot be performed in a signal handler. + */ +void +HandleGetMemoryContextInterrupt(void) +{ + InterruptPending = true; + PublishMemoryContextPending = true; + /* latch will be set by procsignal_sigusr1_handler */ +} + +/* + * ProcessGetMemoryContextInterrupt + * Generate information about memory contexts used by the process. + * + * Performs a breadth first search on the memory context tree, thus parents + * statistics are reported before their children in the monitoring function + * output. + * + * Statistics for all the processes are shared via the same dynamic shared + * area. Individual statistics are tracked independently in per-process DSA + * pointers. These pointers are stored in a dshash table with key as requesting + * clients ProcNumber. + * + * We calculate maximum number of context's statistics that can be displayed + * using a pre-determined limit for memory available per process for this + * utility and maximum size of statistics for each context. The remaining + * context statistics if any are captured as a cumulative total at the end of + * individual context's statistics. + * + * If summary is true, we capture the level 1 and level 2 contexts statistics. + * For that we traverse the memory context tree recursively in depth first + * search manner to cover all the children of a parent context, to be able to + * display a cumulative total of memory consumption by a parent at level 2 and + * all its children. + */ +void +ProcessGetMemoryContextInterrupt(void) +{ + List *contexts; + HASHCTL ctl; + HTAB *context_id_lookup; + int context_id = 0; + MemoryStatsEntry *meminfo; + MemoryContextCounters stat; + int num_individual_stats = 0; + bool found; + MemoryStatsDSHashEntry *entry; + int clientProcNumber; + MemoryContext memstats_ctx = NULL; + MemoryContext oldcontext = NULL; + + PublishMemoryContextPending = false; + + /* + * Avoid performing any shared memory operations in aborted transaction, + * the caller will timeout and return NULL. + */ + if (IsAbortedTransactionBlockState()) + return; + + INJECTION_POINT("memcontext-server-wait", NULL); + + /* + * Retrieve the client key for publishing statistics and reset it to -1, + * so other clients can request memory statistics from this process. + * Return if the client_key is -1, which means the requesting client has + * timed out. + */ + LWLockAcquire(MemContextReportingClientKeysLock, LW_EXCLUSIVE); + if (client_keys[MyProcNumber] == -1) + { + LWLockRelease(MemContextReportingClientKeysLock); + return; + } + clientProcNumber = client_keys[MyProcNumber]; + client_keys[MyProcNumber] = -1; + LWLockRelease(MemContextReportingClientKeysLock); + + /* + * Create a new memory context which is not a part of TopMemoryContext + * tree. This context is used to allocate all memory in this function. + * This helps in keeping the memory allocation in this function to report + * memory consumption statistics separate. So that it does not affect the + * output of this function. + */ + memstats_ctx = AllocSetContextCreate((MemoryContext) NULL, + "publish_memory_context_statistics", + ALLOCSET_SMALL_SIZES); + oldcontext = MemoryContextSwitchTo(memstats_ctx); + + /* + * The hash table is used for constructing "path" column of the view, + * similar to its local backend counterpart. + */ + ctl.keysize = sizeof(MemoryContext); + ctl.entrysize = sizeof(MemoryContextId); + ctl.hcxt = CurrentMemoryContext; + + context_id_lookup = hash_create("pg_get_process_memory_contexts", + 256, + &ctl, + HASH_ELEM | HASH_BLOBS | HASH_CONTEXT); + + /* List of contexts to process in the next round - start at the top. */ + contexts = list_make1(TopMemoryContext); + + /* + * The client process should have created the required DSA and DSHash + * table, so we can connect to these directly. The GetNamedXXX functions + * either return DSA/DSHash or error out (which we want to avoid as that + * would cause the current transaction to fail) so we don't need to check + * for NULL. An assertion is enough to catch in case this API should be + * changed at some point. + */ + if (MemoryStatsDsaArea == NULL) + MemoryStatsDsaArea = GetNamedDSA("memory_context_statistics_dsa", + &found); + + if (MemoryStatsDsHash == NULL) + MemoryStatsDsHash = GetNamedDSHash("memory_context_statistics_dshash", + &memctx_dsh_params, &found); + + Assert(MemoryStatsDsaArea != NULL && MemoryStatsDsHash != NULL); + + /* + * The entry lock is held by dshash_find to prevent the client backend to + * delete or read from the entry before we finish writing to it. Two + * different backends requesting statistics do not block each other. + */ + entry = dshash_find(MemoryStatsDsHash, &clientProcNumber, true); + + /* + * Check if the entry has been deleted due to calling process exiting, or + * if the caller has timed out waiting for us and have issued a request to + * another backend. + * + * Make sure that the client always deletes the entry after taking the + * required lock or this function may end up writing to unallocated + * memory. + */ + if (entry == NULL || entry->target_server_id != MyProcPid) + { + dshash_release_lock(MemoryStatsDsHash, entry); + + hash_destroy(context_id_lookup); + MemoryContextSwitchTo(oldcontext); + MemoryContextDelete(memstats_ctx); + + return; + } + + /* Should be allocated by a client backend that is requesting statistics */ + Assert(entry->memstats_dsa_pointer != InvalidDsaPointer); + meminfo = (MemoryStatsEntry *) + dsa_get_address(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + + INJECTION_POINT("memcontext-server-injection", NULL); + if (entry->summary) + { + int cxt_id = 0; + List *path = NIL; + MemoryContextId *contextid_entry; + + /* Copy TopMemoryContext statistics to DSA */ + memset(&stat, 0, sizeof(stat)); + (*TopMemoryContext->methods->stats) (TopMemoryContext, NULL, NULL, + &stat, true); + path = lcons_int(1, path); + PublishMemoryContext(meminfo, cxt_id, TopMemoryContext, path, stat, + 1); + + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &TopMemoryContext, + HASH_ENTER, &found); + Assert(!found); + + /* context id starts with 1 */ + contextid_entry->context_id = cxt_id + 1; + + /* + * Copy statistics for each of TopMemoryContext's children. This + * includes statistics of at most 100 children per node, with each + * child node limited to a depth of 100 in its subtree. + */ + for (MemoryContext c = TopMemoryContext->firstchild; c != NULL; + c = c->nextchild) + { + MemoryContextCounters grand_totals; + int num_contexts = 0; + + path = NIL; + memset(&grand_totals, 0, sizeof(grand_totals)); + + cxt_id++; + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &c, HASH_ENTER, &found); + Assert(!found); + contextid_entry->context_id = cxt_id + 1; + + MemoryContextStatsCounter(c, &grand_totals, &num_contexts); + + path = compute_context_path(c, context_id_lookup); + + PublishMemoryContext(meminfo, cxt_id, c, path, + grand_totals, num_contexts); + } + entry->total_stats = cxt_id + 1; + } + else + { + foreach_ptr(MemoryContextData, cur, contexts) + { + List *path = NIL; + MemoryContextId *contextid_entry; + + contextid_entry = (MemoryContextId *) hash_search(context_id_lookup, + &cur, + HASH_ENTER, &found); + Assert(!found); + + /* + * context id starts with 1 + */ + contextid_entry->context_id = context_id + 1; + + /* + * Figure out the transient context_id of this context and each of + * its ancestors, to compute a path for this context. + */ + path = compute_context_path(cur, context_id_lookup); + + /* Examine the context stats */ + memset(&stat, 0, sizeof(stat)); + (*cur->methods->stats) (cur, NULL, NULL, &stat, true); + + /* Account for saving one statistics slot for cumulative reporting */ + if (context_id < (MAX_MEMORY_CONTEXT_STATS_NUM - 1)) + { + /* Copy statistics to DSA memory */ + PublishMemoryContext(meminfo, context_id, cur, path, stat, 1); + } + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].totalspace += stat.totalspace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].nblocks += stat.nblocks; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freespace += stat.freespace; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].freechunks += stat.freechunks; + } + + /* + * DSA max limit per process is reached, write aggregate of the + * remaining statistics. + * + * We can store contexts from 0 to max_stats - 1. When context_id + * is greater than max_stats, we stop reporting individual + * statistics when context_id equals max_stats - 2. As we use + * max_stats - 1 array slot for reporting cumulative statistics or + * "Remaining Totals". + */ + if (context_id == (MAX_MEMORY_CONTEXT_STATS_NUM - 2)) + { + int namelen = strlen("Remaining Totals"); + + num_individual_stats = context_id + 1; + strlcpy(meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].name, + "Remaining Totals", namelen + 1); + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].ident[0] = '\0'; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].path[0] = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].type = 0; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].levels = 0; + } + context_id++; + + for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild) + contexts = lappend(contexts, c); + } + + /* + * Check if there are aggregated statistics or not in the result set. + * Statistics are individually reported when context_id <= max_stats, + * only if context_id > max_stats will there be aggregates. + */ + if (context_id <= MAX_MEMORY_CONTEXT_STATS_NUM) + { + entry->total_stats = context_id; + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = 1; + } + + /* + * The number of contexts exceeded the space available, so report the + * number of aggregated memory contexts + */ + else + { + meminfo[MAX_MEMORY_CONTEXT_STATS_NUM - 1].num_agg_stats = + context_id - num_individual_stats; + + /* + * Total stats equals num_individual_stats + 1 record for + * cumulative statistics. + */ + entry->total_stats = num_individual_stats + 1; + } + } + + entry->stats_written = true; + /* Notify waiting client backend and return */ + ConditionVariableSignal(&entry->memcxt_cv); + dshash_release_lock(MemoryStatsDsHash, entry); + hash_destroy(context_id_lookup); + + MemoryContextSwitchTo(oldcontext); + MemoryContextDelete(memstats_ctx); +} + +/* + * PublishMemoryContext + * + * Copy the memory context statistics of a single context to a DSA memory + */ +static void +PublishMemoryContext(MemoryStatsEntry *memcxt_info, int curr_id, + MemoryContext context, List *path, + MemoryContextCounters stat, int num_contexts) +{ + char *ident = unconstify(char *, context->ident); + char *name = unconstify(char *, context->name); + + /* + * To be consistent with logging output, we label dynahash contexts with + * just the hash table name as with MemoryContextStatsPrint(). + */ + if (context->ident && strcmp(context->name, "dynahash") == 0) + { + name = unconstify(char *, context->ident); + ident = NULL; + } + + if (name != NULL) + { + int namelen = strlen(name); + + if (namelen >= MEMORY_CONTEXT_NAME_SHMEM_SIZE) + namelen = pg_mbcliplen(name, namelen, + MEMORY_CONTEXT_NAME_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].name, name, namelen + 1); + } + else + /* Clearing the array */ + memcxt_info[curr_id].name[0] = '\0'; + + /* Trim and copy the identifier if it is not set to NULL */ + if (ident != NULL) + { + int idlen = strlen(ident); + + /* + * Some identifiers such as SQL query string can be very long, + * truncate oversize identifiers. + */ + if (idlen >= MEMORY_CONTEXT_IDENT_SHMEM_SIZE) + idlen = pg_mbcliplen(ident, idlen, + MEMORY_CONTEXT_IDENT_SHMEM_SIZE - 1); + + strlcpy(memcxt_info[curr_id].ident, ident, idlen + 1); + } + else + memcxt_info[curr_id].ident[0] = '\0'; + + /* Store the path */ + if (path == NIL) + memcxt_info[curr_id].path[0] = 0; + else + { + int levels = Min(list_length(path), MAX_PATH_DISPLAY_LENGTH); + + memcxt_info[curr_id].path_length = levels; + memcxt_info[curr_id].levels = list_length(path); + + foreach_int(i, path) + { + memcxt_info[curr_id].path[foreach_current_index(i)] = i; + if (--levels == 0) + break; + } + } + memcxt_info[curr_id].type = context->type; + memcxt_info[curr_id].totalspace = stat.totalspace; + memcxt_info[curr_id].nblocks = stat.nblocks; + memcxt_info[curr_id].freespace = stat.freespace; + memcxt_info[curr_id].freechunks = stat.freechunks; + memcxt_info[curr_id].num_agg_stats = num_contexts; +} + +void +AtProcExit_memstats_cleanup(int code, Datum arg) +{ + int idx = MyProcNumber; + MemoryStatsDSHashEntry *entry; + + if (MemoryStatsDsHash != NULL) + { + entry = dshash_find(MemoryStatsDsHash, &idx, true); + + if (entry != NULL) + { + if (MemoryStatsDsaArea != NULL && + DsaPointerIsValid(entry->memstats_dsa_pointer)) + dsa_free(MemoryStatsDsaArea, entry->memstats_dsa_pointer); + dshash_delete_entry(MemoryStatsDsHash, entry); + } + } + LWLockAcquire(MemContextReportingClientKeysLock, LW_EXCLUSIVE); + client_keys[idx] = -1; + LWLockRelease(MemContextReportingClientKeysLock); +} diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c index 36ad708b360..e246fc7ce1f 100644 --- a/src/backend/utils/init/globals.c +++ b/src/backend/utils/init/globals.c @@ -39,6 +39,7 @@ volatile sig_atomic_t TransactionTimeoutPending = false; volatile sig_atomic_t IdleSessionTimeoutPending = false; volatile sig_atomic_t ProcSignalBarrierPending = false; volatile sig_atomic_t LogMemoryContextPending = false; +volatile sig_atomic_t PublishMemoryContextPending = false; volatile sig_atomic_t IdleStatsUpdateTimeoutPending = false; volatile uint32 InterruptHoldoffCount = 0; volatile uint32 QueryCancelHoldoffCount = 0; diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c index b59e08605cc..ba5d3bf0d7a 100644 --- a/src/backend/utils/init/postinit.c +++ b/src/backend/utils/init/postinit.c @@ -669,6 +669,13 @@ BaseInit(void) * drop ephemeral slots, which in turn triggers stats reporting. */ ReplicationSlotInitialize(); + + /* + * The before shmem exit callback frees the DSA memory occupied by the + * latest memory context statistics that could be published by this proc + * if requested. + */ + before_shmem_exit(AtProcExit_memstats_cleanup, 0); } diff --git a/src/backend/utils/mmgr/mcxt.c b/src/backend/utils/mmgr/mcxt.c index 073bdb35d2a..9780a5b090c 100644 --- a/src/backend/utils/mmgr/mcxt.c +++ b/src/backend/utils/mmgr/mcxt.c @@ -1011,6 +1011,37 @@ MemoryContextStatsInternal(MemoryContext context, int level, } } + +/* + * MemoryContextStatsCounter + * + * Accumulate statistics counts into *totals. totals should not be NULL. + * This involves a non-recursive tree traversal. + */ +void +MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts) +{ + int ichild = 1; + + Assert(totals != NULL && num_contexts != NULL); + context->methods->stats(context, NULL, NULL, totals, false); + + for (MemoryContext curr = context->firstchild; + curr != NULL; + curr = MemoryContextTraverseNext(curr, context)) + { + curr->methods->stats(curr, NULL, NULL, totals, false); + ichild++; + } + + /* + * Add the count of all the children contexts which are traversed + * including the parent. + */ + *num_contexts = ichild; +} + /* * MemoryContextStatsPrint * Print callback used by MemoryContextStatsInternal diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat index dac40992cbc..3667bc2bf41 100644 --- a/src/include/catalog/pg_proc.dat +++ b/src/include/catalog/pg_proc.dat @@ -8641,6 +8641,16 @@ prorettype => 'bool', proargtypes => 'int4', prosrc => 'pg_log_backend_memory_contexts' }, +# publishing memory contexts of the specified postgres process +{ oid => '2173', descr => 'publish memory contexts of the specified process', + proname => 'pg_get_process_memory_contexts', provolatile => 'v', + prorows => '100', proretset => 't', proparallel => 'u', + prorettype => 'record', proargtypes => 'int4 bool', + proallargtypes => '{int4,bool,text,text,text,int4,_int4,int8,int8,int8,int8,int8,int4}', + proargmodes => '{i,i,o,o,o,o,o,o,o,o,o,o,o}', + proargnames => '{pid, summary, name, ident, type, level, path, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes, num_agg_contexts}', + prosrc => 'pg_get_process_memory_contexts' }, + # non-persistent series generator { oid => '1066', descr => 'non-persistent series generator', proname => 'generate_series', prorows => '1000', diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h index f16f35659b9..38e1e55a9a6 100644 --- a/src/include/miscadmin.h +++ b/src/include/miscadmin.h @@ -96,6 +96,7 @@ extern PGDLLIMPORT volatile sig_atomic_t IdleSessionTimeoutPending; extern PGDLLIMPORT volatile sig_atomic_t ProcSignalBarrierPending; extern PGDLLIMPORT volatile sig_atomic_t LogMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t IdleStatsUpdateTimeoutPending; +extern PGDLLIMPORT volatile sig_atomic_t PublishMemoryContextPending; extern PGDLLIMPORT volatile sig_atomic_t CheckClientConnectionPending; extern PGDLLIMPORT volatile sig_atomic_t ClientConnectionLost; diff --git a/src/include/storage/lwlocklist.h b/src/include/storage/lwlocklist.h index e94ebce95b9..6dd80689bbb 100644 --- a/src/include/storage/lwlocklist.h +++ b/src/include/storage/lwlocklist.h @@ -87,6 +87,7 @@ PG_LWLOCK(52, SerialControl) PG_LWLOCK(53, AioWorkerSubmissionQueue) PG_LWLOCK(54, WaitLSN) PG_LWLOCK(55, LogicalDecodingControl) +PG_LWLOCK(56, MemContextReportingClientKeys) /* * There also exist several built-in LWLock tranches. As with the predefined diff --git a/src/include/storage/procsignal.h b/src/include/storage/procsignal.h index 348fba53a93..00ccfcb22a4 100644 --- a/src/include/storage/procsignal.h +++ b/src/include/storage/procsignal.h @@ -35,6 +35,7 @@ typedef enum PROCSIG_WALSND_INIT_STOPPING, /* ask walsenders to prepare for shutdown */ PROCSIG_BARRIER, /* global barrier interrupt */ PROCSIG_LOG_MEMORY_CONTEXT, /* ask backend to log the memory contexts */ + PROCSIG_GET_MEMORY_CONTEXT, /* ask backend to send the memory contexts */ PROCSIG_PARALLEL_APPLY_MESSAGE, /* Message from parallel apply workers */ PROCSIG_RECOVERY_CONFLICT, /* backend is blocking recovery, check * PGPROC->pendingRecoveryConflicts for the diff --git a/src/include/utils/memutils.h b/src/include/utils/memutils.h index 11ab1717a16..40099036308 100644 --- a/src/include/utils/memutils.h +++ b/src/include/utils/memutils.h @@ -319,4 +319,11 @@ pg_memory_is_all_zeros(const void *ptr, size_t len) return true; } +extern void ProcessGetMemoryContextInterrupt(void); +extern void HandleGetMemoryContextInterrupt(void); +extern void MemoryContextKeysShmemInit(void); +extern Size MemoryContextKeysShmemSize(void); +extern void MemoryContextStatsCounter(MemoryContext context, MemoryContextCounters *totals, + int *num_contexts); +extern void AtProcExit_memstats_cleanup(int code, Datum arg); #endif /* MEMUTILS_H */ diff --git a/src/test/regress/expected/sysviews.out b/src/test/regress/expected/sysviews.out index 3dd63fd88ed..c84e5adce16 100644 --- a/src/test/regress/expected/sysviews.out +++ b/src/test/regress/expected/sysviews.out @@ -233,3 +233,22 @@ select * from pg_timezone_abbrevs where abbrev = 'LMT'; LMT | @ 7 hours 52 mins 58 secs ago | f (1 row) +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; +NOTICE: (AllocSet,TopMemoryContext,) +NOTICE: (AllocSet,TopMemoryContext,) diff --git a/src/test/regress/sql/sysviews.sql b/src/test/regress/sql/sysviews.sql index 004f9a70e00..d34d2b6211c 100644 --- a/src/test/regress/sql/sysviews.sql +++ b/src/test/regress/sql/sysviews.sql @@ -101,3 +101,21 @@ select count(distinct utc_offset) >= 24 as ok from pg_timezone_abbrevs; -- One specific case we can check without much fear of breakage -- is the historical local-mean-time value used for America/Los_Angeles. select * from pg_timezone_abbrevs where abbrev = 'LMT'; + +DO $$ +DECLARE + bg_writer_pid int; + r RECORD; +BEGIN + SELECT pid from pg_stat_activity where backend_type='background writer' + INTO bg_writer_pid; + + select type, name, ident + from pg_get_process_memory_contexts(bg_writer_pid, false) + where path = '{1}' into r; + RAISE NOTICE '%', r; + select type, name, ident + from pg_get_process_memory_contexts(pg_backend_pid(), false) + where path = '{1}' into r; + RAISE NOTICE '%', r; +END $$; diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list index 77e3c04144e..a0370d1bb80 100644 --- a/src/tools/pgindent/typedefs.list +++ b/src/tools/pgindent/typedefs.list @@ -1710,6 +1710,8 @@ MemoryContextData MemoryContextId MemoryContextMethodID MemoryContextMethods +MemoryStatsEntry +MemoryStatsDSHashEntry MemoryStatsPrintFunc MergeAction MergeActionState -- 2.34.1 ^ permalink raw reply [nested|flat] 52+ messages in thread
end of thread, other threads:[~2026-03-05 22:31 UTC | newest] Thread overview: 52+ messages (download: mbox mbox.gz follow: Atom feed) -- links below jump to the message on this page -- 2021-12-17 15:35 [PATCH v8 4/4] Move the double-plus "Size" columns to the right Justin Pryzby <[email protected]> 2025-03-13 13:56 Re: Enhancing Memory Context Statistics Reporting Rahila Syed <[email protected]> 2025-03-15 08:40 ` Re: Enhancing Memory Context Statistics Reporting Alexander Korotkov <[email protected]> 2025-03-17 07:52 ` Re: Enhancing Memory Context Statistics Reporting Rahila Syed <[email protected]> 2025-03-17 08:36 ` Re: Enhancing Memory Context Statistics Reporting Ashutosh Bapat <[email protected]> 2025-03-20 07:39 ` Re: Enhancing Memory Context Statistics Reporting Rahila Syed <[email protected]> 2025-03-25 14:14 ` Re: Enhancing Memory Context Statistics Reporting Daniel Gustafsson <[email protected]> 2025-03-26 10:34 ` Re: Enhancing Memory Context Statistics Reporting Rahila Syed <[email protected]> 2025-04-02 21:44 ` Re: Enhancing Memory Context Statistics Reporting Daniel Gustafsson <[email protected]> 2025-04-07 13:41 ` Re: Enhancing Memory Context Statistics Reporting Daniel Gustafsson <[email protected]> 2025-04-07 16:38 ` Re: Enhancing Memory Context Statistics Reporting Andres Freund <[email protected]> 2025-04-08 00:03 ` Re: Enhancing Memory Context Statistics Reporting Andres Freund <[email protected]> 2025-04-08 08:03 ` Re: Enhancing Memory Context Statistics Reporting Daniel Gustafsson <[email protected]> 2025-04-30 10:43 ` Re: Enhancing Memory Context Statistics Reporting Daniel Gustafsson <[email protected]> 2025-07-11 15:31 ` Re: Enhancing Memory Context Statistics Reporting Rahila Syed <[email protected]> 2025-07-29 13:40 ` Re: Enhancing Memory Context Statistics Reporting Rahila Syed <[email protected]> 2025-08-08 09:26 ` Re: Enhancing Memory Context Statistics Reporting Rahila Syed <[email protected]> 2025-08-12 00:34 ` Re: Enhancing Memory Context Statistics Reporting torikoshia <[email protected]> 2025-08-13 22:35 ` Re: Enhancing Memory Context Statistics Reporting Rahila Syed <[email protected]> 2025-08-18 13:32 ` Re: Enhancing Memory Context Statistics Reporting torikoshia <[email protected]> 2025-08-19 21:42 ` Re: Enhancing Memory Context Statistics Reporting Rahila Syed <[email protected]> 2025-10-07 00:51 ` Re: Enhancing Memory Context Statistics Reporting torikoshia <[email protected]> 2025-10-09 08:43 ` Re: Enhancing Memory Context Statistics Reporting Rahila Syed <[email protected]> 2025-10-14 09:00 ` Re: Enhancing Memory Context Statistics Reporting torikoshia <[email protected]> 2025-10-28 05:36 ` Re: Enhancing Memory Context Statistics Reporting Rahila Syed <[email protected]> 2025-11-07 22:55 ` Re: Enhancing Memory Context Statistics Reporting Rahila Syed <[email protected]> 2025-11-20 09:56 ` Re: Enhancing Memory Context Statistics Reporting Daniel Gustafsson <[email protected]> 2025-11-25 07:20 ` Re: Enhancing Memory Context Statistics Reporting Rahila Syed <[email protected]> 2025-11-28 09:22 ` Re: Enhancing Memory Context Statistics Reporting Rahila Syed <[email protected]> 2025-12-08 01:26 ` Re: Enhancing Memory Context Statistics Reporting torikoshia <[email protected]> 2025-12-12 09:46 ` Re: Enhancing Memory Context Statistics Reporting Rahila Syed <[email protected]> 2025-12-08 09:43 ` Re: Enhancing Memory Context Statistics Reporting Daniel Gustafsson <[email protected]> 2025-12-12 11:04 ` Re: Enhancing Memory Context Statistics Reporting Rahila Syed <[email protected]> 2025-12-18 19:54 ` Re: Enhancing Memory Context Statistics Reporting Rahila Syed <[email protected]> 2025-12-29 08:56 ` Re: Enhancing Memory Context Statistics Reporting Rahila Syed <[email protected]> 2026-01-09 11:22 ` Re: Enhancing Memory Context Statistics Reporting Rahila Syed <[email protected]> 2026-01-13 19:31 ` Re: Enhancing Memory Context Statistics Reporting Robert Haas <[email protected]> 2026-01-13 20:26 ` Re: Enhancing Memory Context Statistics Reporting Andres Freund <[email protected]> 2026-01-13 21:11 ` Re: Enhancing Memory Context Statistics Reporting Robert Haas <[email protected]> 2026-01-13 21:47 ` Re: Enhancing Memory Context Statistics Reporting Andres Freund <[email protected]> 2026-01-14 13:26 ` Re: Enhancing Memory Context Statistics Reporting Robert Haas <[email protected]> 2026-01-14 13:32 ` Re: Enhancing Memory Context Statistics Reporting Andres Freund <[email protected]> 2026-01-19 17:31 ` Re: Enhancing Memory Context Statistics Reporting Rahila Syed <[email protected]> 2026-01-19 18:02 ` Re: Enhancing Memory Context Statistics Reporting Robert Haas <[email protected]> 2026-02-02 12:28 ` Re: Enhancing Memory Context Statistics Reporting Rahila Syed <[email protected]> 2026-02-10 00:03 ` Re: Enhancing Memory Context Statistics Reporting Rahila Syed <[email protected]> 2026-02-10 04:50 ` Re: Enhancing Memory Context Statistics Reporting Chao Li <[email protected]> 2026-02-15 12:40 ` Re: Enhancing Memory Context Statistics Reporting Rahila Syed <[email protected]> 2026-02-20 23:13 ` Re: Enhancing Memory Context Statistics Reporting Rahila Syed <[email protected]> 2026-02-23 09:58 ` Re: Enhancing Memory Context Statistics Reporting Daniel Gustafsson <[email protected]> 2026-02-24 11:57 ` Re: Enhancing Memory Context Statistics Reporting Rahila Syed <[email protected]> 2026-03-05 22:31 ` Re: Enhancing Memory Context Statistics Reporting Rahila Syed <[email protected]>
This inbox is served by agora; see mirroring instructions for how to clone and mirror all data and code used for this inbox