Hello,

attached you find a patch that adds a new GUC:

prepared_statement_limit:

        Specifies the maximum amount of memory used in each session to cache         parsed-and-rewritten queries and execution plans. This affects the maximum memory         a backend threads will reserve when many prepared statements are used.         The default value of 0 disables this setting, but it is recommended to set this         value to a bit lower than the maximum memory a backend worker thread should reserve
        permanently.

If the GUC is configured after each save of a CachedPlanSource, or after creating a CachedPlan from it, the function EnforcePreparedStatementLimit is called now. It checks the mem usage of the existing saved CachedPlanSources and invalidates the query_list and the gplan if available until the memory limit is met again.

CachedPlanSource are removed-and-tailadded in the saved_plan_list everytime GetCachedPlan is called on them so it can be used as a LRU list.

I also reworked ResetPlanCache, PlanCacheRelCallback and PlanCacheObjectCallback a bit so when a CachedPlanSource is invalidated the query_list is not only marked as invalid but it is also fully released to free memory here.

Regards,
Daniel Migowski

PS@Konstantin: This patch also includes the CachedPlanMemoryUsage function you like, maybe you like the review the patch for me?



diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index cdc30fa..2da4ba8 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -1702,6 +1702,24 @@ include_dir 'conf.d'
       </listitem>
      </varlistentry>
 
+     <varlistentry id="guc-prepared-statement-limit" 
xreflabel="prepared_statement_limit">
+      <term><varname>prepared_statement_limit</varname> (<type>integer</type>)
+      <indexterm>
+       <primary><varname>prepared_statement_limit</varname> configuration 
parameter</primary>
+      </indexterm>
+      </term>
+      <listitem>
+       <para>
+        Specifies the maximum amount of memory used in each session to cache 
+        parsed-and-rewritten queries and execution plans. This affects the 
maximum memory
+        a backend threads will reserve when many prepared statements are used.
+        The default value of 0 disables this setting, but it is recommended to 
set this
+        value to a bit lower than the maximum memory a backend worker thread 
should reserve
+        permanently. 
+       </para>
+      </listitem>
+     </varlistentry>
+
      <varlistentry id="guc-max-stack-depth" xreflabel="max_stack_depth">
       <term><varname>max_stack_depth</varname> (<type>integer</type>)
       <indexterm>
diff --git a/src/backend/utils/cache/plancache.c 
b/src/backend/utils/cache/plancache.c
index abc3062..dbeb5a2 100644
--- a/src/backend/utils/cache/plancache.c
+++ b/src/backend/utils/cache/plancache.c
@@ -88,6 +88,9 @@
  * those that are in long-lived storage and are examined for sinval events).
  * We use a dlist instead of separate List cells so that we can guarantee
  * to save a CachedPlanSource without error.
+ *
+ * This list is used as a LRU list for prepared statements when a
+ * prepared_statement_limit is configured.
  */
 static dlist_head saved_plan_list = DLIST_STATIC_INIT(saved_plan_list);
 
@@ -97,6 +100,7 @@ static dlist_head saved_plan_list = 
DLIST_STATIC_INIT(saved_plan_list);
 static dlist_head cached_expression_list = 
DLIST_STATIC_INIT(cached_expression_list);
 
 static void ReleaseGenericPlan(CachedPlanSource *plansource);
+static void ReleaseQueryList(CachedPlanSource *plansource);
 static List *RevalidateCachedQuery(CachedPlanSource *plansource,
                                                                   
QueryEnvironment *queryEnv);
 static bool CheckCachedPlan(CachedPlanSource *plansource);
@@ -114,6 +118,7 @@ static TupleDesc PlanCacheComputeResultDesc(List 
*stmt_list);
 static void PlanCacheRelCallback(Datum arg, Oid relid);
 static void PlanCacheObjectCallback(Datum arg, int cacheid, uint32 hashvalue);
 static void PlanCacheSysCallback(Datum arg, int cacheid, uint32 hashvalue);
+static void EnforcePreparedStatementLimit(CachedPlanSource* ignoreThis);
 
 /* GUC parameter */
 int                    plan_cache_mode;
@@ -482,6 +487,11 @@ SaveCachedPlan(CachedPlanSource *plansource)
        dlist_push_tail(&saved_plan_list, &plansource->node);
 
        plansource->is_saved = true;
+
+       if( prep_statement_limit > 0 ) {
+               /* Clean up statements when mem limit is hit */
+               EnforcePreparedStatementLimit(plansource);
+       }
 }
 
 /*
@@ -536,6 +546,31 @@ ReleaseGenericPlan(CachedPlanSource *plansource)
 }
 
 /*
+ * ReleaseQueryList: release a CachedPlanSource's query list, relationOids,
+ * invalItems and search_path.
+ */
+static void
+ReleaseQueryList(CachedPlanSource *plansource)
+{
+       if (plansource->query_list)
+       {
+               plansource->is_valid = false;
+               plansource->query_list = NIL;
+               plansource->relationOids = NIL;
+               plansource->invalItems = NIL;
+               plansource->search_path = NULL;
+
+               if (plansource->query_context)
+               {
+                       MemoryContext qcxt = plansource->query_context;
+
+                       plansource->query_context = NULL;
+                       MemoryContextDelete(qcxt);
+               }
+       }
+}
+
+/*
  * RevalidateCachedQuery: ensure validity of analyzed-and-rewritten query tree.
  *
  * What we do here is re-acquire locks and redo parse analysis if necessary.
@@ -626,27 +661,9 @@ RevalidateCachedQuery(CachedPlanSource *plansource,
        /*
         * Discard the no-longer-useful query tree.  (Note: we don't want to do
         * this any earlier, else we'd not have been able to release locks
-        * correctly in the race condition case.)
+        * correctly in the race condition case.) Also discard query_context
         */
-       plansource->is_valid = false;
-       plansource->query_list = NIL;
-       plansource->relationOids = NIL;
-       plansource->invalItems = NIL;
-       plansource->search_path = NULL;
-
-       /*
-        * Free the query_context.  We don't really expect MemoryContextDelete 
to
-        * fail, but just in case, make sure the CachedPlanSource is left in a
-        * reasonably sane state.  (The generic plan won't get unlinked yet, but
-        * that's acceptable.)
-        */
-       if (plansource->query_context)
-       {
-               MemoryContext qcxt = plansource->query_context;
-
-               plansource->query_context = NULL;
-               MemoryContextDelete(qcxt);
-       }
+       ReleaseQueryList(plansource);
 
        /* Drop the generic plan reference if any */
        ReleaseGenericPlan(plansource);
@@ -1141,6 +1158,7 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo 
boundParams,
        CachedPlan *plan = NULL;
        List       *qlist;
        bool            customplan;
+       bool        newplan = false;
 
        /* Assert caller is doing things in a sane order */
        Assert(plansource->magic == CACHEDPLANSOURCE_MAGIC);
@@ -1172,6 +1190,7 @@ GetCachedPlan(CachedPlanSource *plansource, ParamListInfo 
boundParams,
                        /* Link the new generic plan into the plansource */
                        plansource->gplan = plan;
                        plan->refcount++;
+                       newplan = true;
                        /* Immediately reparent into appropriate context */
                        if (plansource->is_saved)
                        {
@@ -1241,6 +1260,18 @@ GetCachedPlan(CachedPlanSource *plansource, 
ParamListInfo boundParams,
                plan->is_saved = true;
        }
 
+       if( plansource->is_saved && prep_statement_limit > 0 ) {
+               /* move CachedPlanSource to tail of list (use it like a LRU 
list).
+               * Todo: Make this more efficient / a single op */
+               dlist_delete(&plansource->node);
+               dlist_push_tail(&saved_plan_list, &plansource->node);
+
+               if( newplan ) {
+                       /* Clean up statements when mem limit is hit */
+                       EnforcePreparedStatementLimit(plansource);
+               }
+       }
+
        return plan;
 }
 
@@ -1530,6 +1561,181 @@ FreeCachedExpression(CachedExpression *cexpr)
 }
 
 /*
+ * CachedPlanMemUsage: Return memory used by the CachedPlanSource
+ *
+ * Returns the malloced memory used by the two MemoryContexts in
+ * CachedPlanSource and (if available) the MemoryContext in the generic plan.
+ * Does not care for the free memory in those MemoryContexts because it is very
+ * unlikely that it is reused for anythink else anymore and can be considered
+ * dead memory anyway. Also the size of the CachedPlanSource struct is added.
+ *
+ * This function is used only for the pg_prepared_statements view to allow
+ * client applications to monitor memory used by prepared statements and to
+ * selects candidates for eviction in memory contraint environments with
+ * automatic preparation of often called queries.
+ */
+Size
+CachedPlanMemoryUsage(CachedPlanSource *plan)
+{
+       MemoryContextCounters counters;
+       MemoryContext context;
+       counters.totalspace = 0;
+
+       context = plan->context;
+       context->methods->stats(context,NULL,NULL,&counters);
+
+       if( plan->query_context )
+       {
+               context = plan->query_context;
+               context->methods->stats(context,NULL,NULL,&counters);
+       }
+
+       if( plan->gplan )
+       {
+               context = plan->gplan->context;
+               context->methods->stats(context,NULL,NULL,&counters);
+       }
+
+       return counters.totalspace;
+}
+
+/*
+ * AllCachedPlansMemUsage: Return memory used by the saved CachedPlanSources
+ *
+ * If this exceeds a configurable treshhold, Other CachedPlans are invalidated
+ * until enought memory has been freed.
+ */
+static Size
+AllCachedPlansMemUsage()
+{
+
+       dlist_iter      iter;
+       Size        size = 0;
+
+       dlist_foreach(iter, &saved_plan_list)
+       {
+               CachedPlanSource *plansource = dlist_container(CachedPlanSource,
+                                                                               
                           node, iter.cur);
+               Assert(plansource->magic == CACHEDPLANSOURCE_MAGIC);
+               size += CachedPlanMemoryUsage(plansource);
+       }
+
+       return size;
+}
+
+/*
+ * CachedPlanFreeableMemory: Return memory freed when query_list and gplan
+ * would be released.
+ */
+static Size
+CachedPlanFreeableMemory(CachedPlanSource *plan)
+{
+       MemoryContextCounters counters;
+       MemoryContext context;
+       counters.totalspace = 0;
+
+       if( plan->query_context )
+       {
+               context = plan->query_context;
+               
context->methods->stats(plan->query_context,NULL,NULL,&counters);
+       }
+
+       if( plan->gplan )
+       {
+               context = plan->gplan->context;
+               context->methods->stats(context,NULL,NULL,&counters);
+       }
+
+       return counters.totalspace;
+}
+
+/*
+ * Tries to enforce the preparedStatementLimit. Does nothing when there is
+ *
+ */
+static void
+EnforcePreparedStatementLimit(CachedPlanSource* ignoreThis)
+{
+       Size currSize;
+       Size limit = prep_statement_limit<<10;
+       dlist_iter      iter;
+
+       // If GUC prepared_statement_limit is not set, there is nothing to do 
here.
+       if( prep_statement_limit <= 0 )
+               return;
+
+       // Check the current memory usage
+       currSize = AllCachedPlansMemUsage();
+
+       // If we use less than that we can exit
+       if( currSize <= limit )
+               return;
+
+       // Iterate over all cached plans and invalidate them until the limit is
+       // reached but skip an eventually plan that we want to use NOW.
+       dlist_foreach(iter, &saved_plan_list)
+       {
+               CachedPlanSource *plansource = dlist_container(CachedPlanSource,
+                                                                               
                           node, iter.cur);
+               ListCell   *lc;
+               Size       freeable;
+
+               Assert(plansource->magic == CACHEDPLANSOURCE_MAGIC);
+
+               /* Skip one we might want to ignore */
+               if( ignoreThis == plansource )
+                       continue;
+
+               /* No work if it's already invalidated */
+               if (!plansource->is_valid)
+                       continue;
+
+               /*
+                * We *must not* mark transaction control statements as invalid,
+                * particularly not ROLLBACK, because they may need to be 
executed in
+                * aborted transactions when we can't revalidate them (cf bug 
#5269).
+                */
+               if (IsTransactionStmtPlan(plansource))
+                       continue;
+
+               /* No work if we can't do anything about it */
+               freeable = CachedPlanFreeableMemory(plansource);
+               if (freeable == 0)
+                       continue;
+
+               /*
+                * In general there is no point in invalidating utility 
statements
+                * since they have no plans anyway.  So invalidate it only if it
+                * contains at least one non-utility statement, or contains a 
utility
+                * statement that contains a pre-analyzed query (which could 
have
+                * dependencies.)
+                */
+               foreach(lc, plansource->query_list)
+               {
+                       Query      *query = lfirst_node(Query, lc);
+
+                       if (query->commandType != CMD_UTILITY ||
+                               UtilityContainsQuery(query->utilityStmt))
+                       {
+                               /* non-utility statement, so invalidate */
+                               ReleaseQueryList(plansource);
+                               /* no need to look further */
+                               break;
+                       }
+               }
+
+               /* Release generic plan if any */
+               ReleaseGenericPlan(plansource);
+
+               /* Stop when enough it released */
+               currSize -= freeable;
+               if( currSize <= limit )
+                       break;
+       }
+}
+
+
+/*
  * QueryListGetPrimaryStmt
  *             Get the "primary" stmt within a list, ie, the one marked 
canSetTag.
  *
@@ -1787,9 +1993,9 @@ PlanCacheRelCallback(Datum arg, Oid relid)
                        list_member_oid(plansource->relationOids, relid))
                {
                        /* Invalidate the querytree and generic plan */
-                       plansource->is_valid = false;
-                       if (plansource->gplan)
-                               plansource->gplan->is_valid = false;
+                       ReleaseQueryList(plansource);
+                       /* Release generic plan if any */
+                       ReleaseGenericPlan(plansource);
                }
 
                /*
@@ -1810,7 +2016,7 @@ PlanCacheRelCallback(Datum arg, Oid relid)
                                        
list_member_oid(plannedstmt->relationOids, relid))
                                {
                                        /* Invalidate the generic plan only */
-                                       plansource->gplan->is_valid = false;
+                                       ReleaseGenericPlan(plansource);
                                        break;          /* out of stmt_list 
scan */
                                }
                        }
@@ -1878,9 +2084,9 @@ PlanCacheObjectCallback(Datum arg, int cacheid, uint32 
hashvalue)
                                item->hashValue == hashvalue)
                        {
                                /* Invalidate the querytree and generic plan */
-                               plansource->is_valid = false;
-                               if (plansource->gplan)
-                                       plansource->gplan->is_valid = false;
+                               ReleaseQueryList(plansource);
+                               /* Release generic plan if any */
+                               ReleaseGenericPlan(plansource);
                                break;
                        }
                }
@@ -1908,11 +2114,11 @@ PlanCacheObjectCallback(Datum arg, int cacheid, uint32 
hashvalue)
                                                item->hashValue == hashvalue)
                                        {
                                                /* Invalidate the generic plan 
only */
-                                               plansource->gplan->is_valid = 
false;
+                                               ReleaseGenericPlan(plansource);
                                                break;  /* out of invalItems 
scan */
                                        }
                                }
-                               if (!plansource->gplan->is_valid)
+                               if (!plansource->gplan)
                                        break;          /* out of stmt_list 
scan */
                        }
                }
@@ -2002,9 +2208,9 @@ ResetPlanCache(void)
                                UtilityContainsQuery(query->utilityStmt))
                        {
                                /* non-utility statement, so invalidate */
-                               plansource->is_valid = false;
-                               if (plansource->gplan)
-                                       plansource->gplan->is_valid = false;
+                               ReleaseQueryList(plansource);
+                               /* Release generic plan if any */
+                               ReleaseGenericPlan(plansource);
                                /* no need to look further */
                                break;
                        }
diff --git a/src/backend/utils/init/globals.c b/src/backend/utils/init/globals.c
index 3bf96de..ce1257f 100644
--- a/src/backend/utils/init/globals.c
+++ b/src/backend/utils/init/globals.c
@@ -120,6 +120,7 @@ bool                enableFsync = true;
 bool           allowSystemTableMods = false;
 int                    work_mem = 1024;
 int                    maintenance_work_mem = 16384;
+int         prep_statement_limit = 0;
 int                    max_parallel_maintenance_workers = 2;
 
 /*
diff --git a/src/backend/utils/misc/guc.c b/src/backend/utils/misc/guc.c
index eb78522..7c7615a 100644
--- a/src/backend/utils/misc/guc.c
+++ b/src/backend/utils/misc/guc.c
@@ -2351,6 +2351,19 @@ static struct config_int ConfigureNamesInt[] =
                NULL, NULL, NULL
        },
 
+       {
+               {"prepared_statement_limit", PGC_POSTMASTER, RESOURCES_MEM,
+                       gettext_noop("Limits the memory used to cache plans of 
prepared statements, per backend."),
+                       gettext_noop("This much memory can be used by prepared 
statements to cache "
+                                                "parsed-and-rewritten queries 
and execution plans. If the limit is reached "
+                                                "unused prepared statements 
loose their plans until they are executed again."),
+                       GUC_UNIT_KB
+               },
+               &prep_statement_limit,
+               0, 0, MAX_KILOBYTES,
+               NULL, NULL, NULL
+       },
+
 #ifdef LOCK_DEBUG
        {
                {"trace_lock_oidmin", PGC_SUSET, DEVELOPER_OPTIONS,
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 61a24c2..5819e32 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -244,6 +244,7 @@ extern bool enableFsync;
 extern PGDLLIMPORT bool allowSystemTableMods;
 extern PGDLLIMPORT int work_mem;
 extern PGDLLIMPORT int maintenance_work_mem;
+extern PGDLLIMPORT int prep_statement_limit;
 extern PGDLLIMPORT int max_parallel_maintenance_workers;
 
 extern int     VacuumCostPageHit;
diff --git a/src/include/utils/plancache.h b/src/include/utils/plancache.h
index de2555e..2d3fc34 100644
--- a/src/include/utils/plancache.h
+++ b/src/include/utils/plancache.h
@@ -219,6 +219,8 @@ extern CachedPlan *GetCachedPlan(CachedPlanSource 
*plansource,
                                                                 
QueryEnvironment *queryEnv);
 extern void ReleaseCachedPlan(CachedPlan *plan, bool useResOwner);
 
+extern Size CachedPlanMemoryUsage(CachedPlanSource *plansource);
+
 extern CachedExpression *GetCachedExpression(Node *expr);
 extern void FreeCachedExpression(CachedExpression *cexpr);
 

Reply via email to