Add path column to pg_backend_memory_contexts view
authorDavid Rowley <[email protected]>
Thu, 25 Jul 2024 03:03:28 +0000 (15:03 +1200)
committerDavid Rowley <[email protected]>
Thu, 25 Jul 2024 03:03:28 +0000 (15:03 +1200)
"path" provides a reliable method of determining the parent/child
relationships between memory contexts.  Previously this could be done in
a non-reliable way by writing a recursive query and joining the "parent"
and "name" columns.  This wasn't reliable as the names were not unique,
which could result in joining to the wrong parent.

To make this reliable, "path" stores an array of numerical identifiers
starting with the identifier for TopLevelMemoryContext.  It contains an
element for each intermediate parent between that and the current context.

Incompatibility: Here we also adjust the "level" column to make it
1-based rather than 0-based.  A 1-based level provides a convenient way
to access elements in the "path" array. e.g. path[level] gives the
identifier for the current context.

Identifiers are not stable across multiple evaluations of the view.  In
an attempt to make these more stable for ad-hoc queries, the identifiers
are assigned breadth-first.  Contexts closer to TopLevelMemoryContext
are less likely to change between queries and during queries.

Author: Melih Mutlu <[email protected]>
Discussion: https://postgr.es/m/CAGPVpCThLyOsj3e_gYEvLoHkr5w=tadDiN_=z2OwsK3VJppeBA@mail.gmail.com
Reviewed-by: Andres Freund, Stephen Frost, Atsushi Torikoshi,
Reviewed-by: Michael Paquier, Robert Haas, David Rowley
doc/src/sgml/system-views.sgml
src/backend/utils/adt/mcxtfuncs.c
src/include/catalog/catversion.h
src/include/catalog/pg_proc.dat
src/include/nodes/memnodes.h
src/test/regress/expected/rules.out
src/test/regress/expected/sysviews.out
src/test/regress/sql/sysviews.sql

index bdc34cf94e87b07e6e293f63e44f9f150f0173c3..a0b692bf1e9345045f6a94d212e32bc4e203e7d8 100644 (file)
        <structfield>level</structfield> <type>int4</type>
       </para>
       <para>
-       Distance from TopMemoryContext in context tree
+       The 1-based level of the context in the memory context hierarchy. The
+       level of a context also shows the position of that context in the
+       <structfield>path</structfield> column.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>path</structfield> <type>int4[]</type>
+      </para>
+      <para>
+       Array of transient numerical identifiers to describe the memory
+       context hierarchy. The first element is for
+       <literal>TopMemoryContext</literal>, subsequent elements contain
+       intermediate parents and the final element contains the identifier for
+       the current context.
       </para></entry>
      </row>
 
    read only by superusers or roles with the privileges of the
    <literal>pg_read_all_stats</literal> role.
   </para>
+
+  <para>
+   Since memory contexts are created and destroyed during the running of a
+   query, the identifiers stored in the <structfield>path</structfield> column
+   can be unstable between multiple invocations of the view in the same query.
+   The example below demonstrates an effective usage of this column and
+   calculates the total number of bytes used by
+   <literal>CacheMemoryContext</literal> and all of its children:
+
+<programlisting>
+WITH memory_contexts AS (
+    SELECT * FROM pg_backend_memory_contexts
+)
+SELECT sum(c1.total_bytes)
+FROM memory_contexts c1, memory_contexts c2
+WHERE c2.name = 'CacheMemoryContext'
+AND c1.path[c2.level] = c2.path[c2.level];
+</programlisting>
+
+   The <link linkend="queries-with">Common Table Expression</link> is used
+   to ensure the context IDs in the <structfield>path</structfield> column
+   match between both evaluations of the view.
+  </para>
  </sect1>
 
  <sect1 id="view-pg-config">
index 108594148480850bd9dfacb4731dfa7d9a6984fd..199e68c1ae508f5c52b4a6bc1aabc43159e4e00f 100644 (file)
@@ -19,7 +19,9 @@
 #include "mb/pg_wchar.h"
 #include "storage/proc.h"
 #include "storage/procarray.h"
+#include "utils/array.h"
 #include "utils/builtins.h"
+#include "utils/hsearch.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;
+
+/*
+ * get_memory_context_name_and_ident
+ *     Populate *name and *ident from the name and ident from 'context'.
+ */
+static void
+get_memory_context_name_and_ident(MemoryContext context, const char **const name,
+                                 const char **const ident)
+{
+   *name = context->name;
+   *ident = context->ident;
+
+   /*
+    * To be consistent with logging output, we label dynahash contexts with
+    * just the hash table name as with MemoryContextStatsPrint().
+    */
+   if (ident && strcmp(*name, "dynahash") == 0)
+   {
+       *name = *ident;
+       *ident = NULL;
+   }
+}
+
+/*
+ * int_list_to_array
+ *     Convert an IntList to an array of INT4OIDs.
+ */
+static Datum
+int_list_to_array(const List *list)
+{
+   Datum      *datum_array;
+   int         length;
+   ArrayType  *result_array;
+
+   length = list_length(list);
+   datum_array = (Datum *) palloc(length * sizeof(Datum));
+
+   foreach_int(i, list)
+       datum_array[foreach_current_index(i)] = Int32GetDatum(i);
+
+   result_array = construct_array_builtin(datum_array, length, INT4OID);
+
+   return PointerGetDatum(result_array);
+}
+
 /*
  * PutMemoryContextsStatsTupleStore
- *     One recursion level for pg_get_backend_memory_contexts.
+ *     Add details for the given MemoryContext to 'tupstore'.
  */
 static void
 PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore,
                                 TupleDesc tupdesc, MemoryContext context,
-                                const char *parent, int level)
+                                HTAB *context_id_lookup)
 {
-#define PG_GET_BACKEND_MEMORY_CONTEXTS_COLS    10
+#define PG_GET_BACKEND_MEMORY_CONTEXTS_COLS    11
 
    Datum       values[PG_GET_BACKEND_MEMORY_CONTEXTS_COLS];
    bool        nulls[PG_GET_BACKEND_MEMORY_CONTEXTS_COLS];
    MemoryContextCounters stat;
-   MemoryContext child;
+   List       *path = NIL;
    const char *name;
    const char *ident;
    const char *type;
 
    Assert(MemoryContextIsValid(context));
 
-   name = context->name;
-   ident = context->ident;
-
    /*
-    * To be consistent with logging output, we label dynahash contexts with
-    * just the hash table name as with MemoryContextStatsPrint().
+    * Figure out the transient context_id of this context and each of its
+    * ancestors.
     */
-   if (ident && strcmp(name, "dynahash") == 0)
+   for (MemoryContext cur = context; cur != NULL; cur = cur->parent)
    {
-       name = ident;
-       ident = NULL;
+       MemoryContextId *entry;
+       bool        found;
+
+       entry = hash_search(context_id_lookup, &cur, HASH_FIND, &found);
+
+       if (!found)
+           elog(ERROR, "hash table corrupted");
+       path = lcons_int(entry->context_id, path);
    }
 
    /* Examine the context itself */
    memset(&stat, 0, sizeof(stat));
-   (*context->methods->stats) (context, NULL, (void *) &level, &stat, true);
+   (*context->methods->stats) (context, NULL, NULL, &stat, true);
 
    memset(values, 0, sizeof(values));
    memset(nulls, 0, sizeof(nulls));
 
+   get_memory_context_name_and_ident(context, &name, &ident);
+
    if (name)
        values[0] = CStringGetTextDatum(name);
    else
@@ -92,8 +154,15 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore,
    else
        nulls[1] = true;
 
-   if (parent)
-       values[2] = CStringGetTextDatum(parent);
+   if (context->parent)
+   {
+       const char *parent_name,
+                  *parent_ident;
+
+       get_memory_context_name_and_ident(context->parent, &parent_name,
+                                         &parent_ident);
+       values[2] = CStringGetTextDatum(parent_name);
+   }
    else
        nulls[2] = true;
 
@@ -117,19 +186,16 @@ PutMemoryContextsStatsTupleStore(Tuplestorestate *tupstore,
    }
 
    values[3] = CStringGetTextDatum(type);
-   values[4] = Int32GetDatum(level);
-   values[5] = Int64GetDatum(stat.totalspace);
-   values[6] = Int64GetDatum(stat.nblocks);
-   values[7] = Int64GetDatum(stat.freespace);
-   values[8] = Int64GetDatum(stat.freechunks);
-   values[9] = Int64GetDatum(stat.totalspace - stat.freespace);
-   tuplestore_putvalues(tupstore, tupdesc, values, nulls);
+   values[4] = Int32GetDatum(list_length(path));   /* level */
+   values[5] = int_list_to_array(path);
+   values[6] = Int64GetDatum(stat.totalspace);
+   values[7] = Int64GetDatum(stat.nblocks);
+   values[8] = Int64GetDatum(stat.freespace);
+   values[9] = Int64GetDatum(stat.freechunks);
+   values[10] = Int64GetDatum(stat.totalspace - stat.freespace);
 
-   for (child = context->firstchild; child != NULL; child = child->nextchild)
-   {
-       PutMemoryContextsStatsTupleStore(tupstore, tupdesc,
-                                        child, name, level + 1);
-   }
+   tuplestore_putvalues(tupstore, tupdesc, values, nulls);
+   list_free(path);
 }
 
 /*
@@ -140,10 +206,66 @@ Datum
 pg_get_backend_memory_contexts(PG_FUNCTION_ARGS)
 {
    ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+   int         context_id;
+   List       *contexts;
+   HASHCTL     ctl;
+   HTAB       *context_id_lookup;
+
+   ctl.keysize = sizeof(MemoryContext);
+   ctl.entrysize = sizeof(MemoryContextId);
+   ctl.hcxt = CurrentMemoryContext;
+
+   context_id_lookup = hash_create("pg_get_backend_memory_contexts",
+                                   256,
+                                   &ctl,
+                                   HASH_ELEM | HASH_BLOBS | HASH_CONTEXT);
 
    InitMaterializedSRF(fcinfo, 0);
-   PutMemoryContextsStatsTupleStore(rsinfo->setResult, rsinfo->setDesc,
-                                    TopMemoryContext, NULL, 0);
+
+   /*
+    * Here we use a non-recursive algorithm to visit all MemoryContexts
+    * starting with TopMemoryContext.  The reason we avoid using a recursive
+    * algorithm is because we want to assign the context_id breadth-first.
+    * I.e. all contexts at level 1 are assigned IDs before contexts at level
+    * 2.  Because contexts closer to TopMemoryContext are less likely to
+    * change, this makes the assigned context_id more stable.  Otherwise, if
+    * the first child of TopMemoryContext obtained an additional grandchild,
+    * the context_id for the second child of TopMemoryContext would change.
+    */
+   contexts = list_make1(TopMemoryContext);
+
+   /* TopMemoryContext will always have a context_id of 1 */
+   context_id = 1;
+
+   foreach_ptr(MemoryContextData, cur, contexts)
+   {
+       MemoryContextId *entry;
+       bool        found;
+
+       /*
+        * Record the context_id that we've assigned to each MemoryContext.
+        * 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->context_id = context_id++;
+       Assert(!found);
+
+       PutMemoryContextsStatsTupleStore(rsinfo->setResult,
+                                        rsinfo->setDesc,
+                                        cur,
+                                        context_id_lookup);
+
+       /*
+        * Append all children onto the contexts list so they're processed by
+        * subsequent iterations.
+        */
+       for (MemoryContext c = cur->firstchild; c != NULL; c = c->nextchild)
+           contexts = lappend(contexts, c);
+   }
+
+   hash_destroy(context_id_lookup);
 
    return (Datum) 0;
 }
index 3254e7ab9293934406b85c16e3ff978130843cdb..4c88ea1034f62bfe8e4e64e2a8afadbf2e447078 100644 (file)
@@ -57,6 +57,6 @@
  */
 
 /*                         yyyymmddN */
-#define CATALOG_VERSION_NO 202407111
+#define CATALOG_VERSION_NO 202407251
 
 #endif
index 73d9cf85826c99cd22f4804d3c250a4568fdb1c0..d14a94b9873467a69d07ce96774b285536a97e44 100644 (file)
   proname => 'pg_get_backend_memory_contexts', prorows => '100',
   proretset => 't', provolatile => 'v', proparallel => 'r',
   prorettype => 'record', proargtypes => '',
-  proallargtypes => '{text,text,text,text,int4,int8,int8,int8,int8,int8}',
-  proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
-  proargnames => '{name, ident, parent, type, level, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes}',
+  proallargtypes => '{text,text,text,text,int4,_int4,int8,int8,int8,int8,int8}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{name, ident, parent, type, level, path, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes}',
   prosrc => 'pg_get_backend_memory_contexts' },
 
 # logging memory contexts of the specified backend
index c4c9fd3e3e1bb279e515c7d741a7ea43aad25aec..addfbb1ccdf5e75aa6d5076b5533485cf3555cb5 100644 (file)
@@ -128,8 +128,8 @@ typedef struct MemoryContextData
    MemoryContext firstchild;   /* head of linked list of children */
    MemoryContext prevchild;    /* previous child of same parent */
    MemoryContext nextchild;    /* next child of same parent */
-   const char *name;           /* context name (just for debugging) */
-   const char *ident;          /* context ID if any (just for debugging) */
+   const char *name;           /* context name */
+   const char *ident;          /* context ID if any */
    MemoryContextCallback *reset_cbs;   /* list of reset/delete callbacks */
 } MemoryContextData;
 
index 4c789279e5e633b52225b84e5c1b63502b02b3de..52012806699aab91edb6c5bd05cad81d4020cca2 100644 (file)
@@ -1308,12 +1308,13 @@ pg_backend_memory_contexts| SELECT name,
     parent,
     type,
     level,
+    path,
     total_bytes,
     total_nblocks,
     free_bytes,
     free_chunks,
     used_bytes
-   FROM pg_get_backend_memory_contexts() pg_get_backend_memory_contexts(name, ident, parent, type, level, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes);
+   FROM pg_get_backend_memory_contexts() pg_get_backend_memory_contexts(name, ident, parent, type, level, path, total_bytes, total_nblocks, free_bytes, free_chunks, used_bytes);
 pg_config| SELECT name,
     setting
    FROM pg_config() pg_config(name, setting);
index 729620de13c8027556509e792c099cc16d67246f..84b3b64b49073528d12439968808156df214ee15 100644 (file)
@@ -22,10 +22,10 @@ select count(*) >= 0 as ok from pg_available_extensions;
 -- The entire output of pg_backend_memory_contexts is not stable,
 -- we test only the existence and basic condition of TopMemoryContext.
 select type, name, ident, parent, level, total_bytes >= free_bytes
-  from pg_backend_memory_contexts where level = 0;
+  from pg_backend_memory_contexts where level = 1;
    type   |       name       | ident | parent | level | ?column? 
 ----------+------------------+-------+--------+-------+----------
- AllocSet | TopMemoryContext |       |        |     0 | t
+ AllocSet | TopMemoryContext |       |        |     1 | t
 (1 row)
 
 -- We can exercise some MemoryContext type stats functions.  Most of the
@@ -51,6 +51,20 @@ from pg_backend_memory_contexts where name = 'Caller tuples';
 (1 row)
 
 rollback;
+-- Further sanity checks on pg_backend_memory_contexts.  We expect
+-- CacheMemoryContext to have multiple children.  Ensure that's the case.
+with contexts as (
+  select * from pg_backend_memory_contexts
+)
+select count(*) > 1
+from contexts c1, contexts c2
+where c2.name = 'CacheMemoryContext'
+and c1.path[c2.level] = c2.path[c2.level];
+ ?column? 
+----------
+ t
+(1 row)
+
 -- At introduction, pg_config had 23 entries; it may grow
 select count(*) > 20 as ok from pg_config;
  ok 
index 7edac2fde148c5f8dbaf0a4e16a5aae83be3ea7d..15e2a9e741729cfbf0c903acc10558d2ba81ad54 100644 (file)
@@ -15,7 +15,7 @@ select count(*) >= 0 as ok from pg_available_extensions;
 -- The entire output of pg_backend_memory_contexts is not stable,
 -- we test only the existence and basic condition of TopMemoryContext.
 select type, name, ident, parent, level, total_bytes >= free_bytes
-  from pg_backend_memory_contexts where level = 0;
+  from pg_backend_memory_contexts where level = 1;
 
 -- We can exercise some MemoryContext type stats functions.  Most of the
 -- column values are too platform-dependant to display.
@@ -32,6 +32,16 @@ select type, name, parent, total_bytes > 0, total_nblocks, free_bytes > 0, free_
 from pg_backend_memory_contexts where name = 'Caller tuples';
 rollback;
 
+-- Further sanity checks on pg_backend_memory_contexts.  We expect
+-- CacheMemoryContext to have multiple children.  Ensure that's the case.
+with contexts as (
+  select * from pg_backend_memory_contexts
+)
+select count(*) > 1
+from contexts c1, contexts c2
+where c2.name = 'CacheMemoryContext'
+and c1.path[c2.level] = c2.path[c2.level];
+
 -- At introduction, pg_config had 23 entries; it may grow
 select count(*) > 20 as ok from pg_config;