]> git.ipfire.org Git - thirdparty/postgresql.git/commitdiff
Add support for lock statistics in pgstats
authorMichael Paquier <michael@paquier.xyz>
Tue, 24 Mar 2026 06:32:09 +0000 (15:32 +0900)
committerMichael Paquier <michael@paquier.xyz>
Tue, 24 Mar 2026 06:32:09 +0000 (15:32 +0900)
This commit adds a new stats kind, called PGSTAT_KIND_LOCK, implementing
statistics for lock tags, as reported by pg_locks.  The implementation
is fixed-sized, as the data is caped based on the number of lock tags in
LockTagType.

The new statistics kind records the following fields, providing insight
regarding lock behavior, while avoiding impact on performance-critical
code paths (such as fast-path lock acquisition):
- waits and wait_time: respectively track the number of times a lock
required waiting and the total time spent acquiring it.  These metrics
are only collected once a lock is successfully acquired and after
deadlock_timeout has been exceeded.
fastpath_exceeded: counts how often a lock could not be acquired via
the fast path due to the max_locks_per_transaction slot limits.

A new view called pg_stat_lock can be used to access this data, coupled
with a SQL function called pg_stat_get_lock().

Bump stat file format PGSTAT_FILE_FORMAT_ID.
Bump catalog version.

Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reviewed-by: Andres Freund <andres@anarazel.de>
Reviewed-by: Michael Paquier <michael@paquier.xyz>
Discussion: https://postgr.es/m/aIyNxBWFCybgBZBS%40ip-10-97-1-34.eu-west-3.compute.internal

21 files changed:
doc/src/sgml/monitoring.sgml
src/backend/catalog/system_views.sql
src/backend/storage/lmgr/lock.c
src/backend/storage/lmgr/proc.c
src/backend/utils/activity/Makefile
src/backend/utils/activity/meson.build
src/backend/utils/activity/pgstat.c
src/backend/utils/activity/pgstat_lock.c [new file with mode: 0644]
src/backend/utils/adt/pgstatfuncs.c
src/include/catalog/catversion.h
src/include/catalog/pg_proc.dat
src/include/pgstat.h
src/include/utils/pgstat_internal.h
src/include/utils/pgstat_kind.h
src/test/isolation/expected/lock-stats.out [new file with mode: 0644]
src/test/isolation/isolation_schedule
src/test/isolation/specs/lock-stats.spec [new file with mode: 0644]
src/test/regress/expected/rules.out
src/test/regress/expected/stats.out
src/test/regress/sql/stats.sql
src/tools/pgindent/typedefs.list

index 462019a972c64e4f5f4d998488ae989e8592ae91..bb75ed1069b7d779436b0b6705ff8134c9695ef4 100644 (file)
@@ -509,6 +509,15 @@ postgres   27093  0.0  0.0  30096  2752 ?        Ss   11:34   0:00 postgres: ser
      </entry>
      </row>
 
+     <row>
+      <entry><structname>pg_stat_lock</structname><indexterm><primary>pg_stat_lock</primary></indexterm></entry>
+      <entry>
+       One row for each lock type, containing cluster-wide locks statistics.
+       See <link linkend="monitoring-pg-stat-lock-view">
+       <structname>pg_stat_lock</structname></link> for details.
+     </entry>
+     </row>
+
      <row>
       <entry><structname>pg_stat_replication_slots</structname><indexterm><primary>pg_stat_replication_slots</primary></indexterm></entry>
       <entry>One row per replication slot, showing statistics about the
@@ -3283,6 +3292,101 @@ description | Waiting for a newly initialized WAL file to reach durable storage
 
  </sect2>
 
+
+ <sect2 id="monitoring-pg-stat-lock-view">
+  <title><structname>pg_stat_lock</structname></title>
+
+  <indexterm>
+   <primary>pg_stat_lock</primary>
+  </indexterm>
+
+  <para>
+   The <structname>pg_stat_lock</structname> view will contain one row for each
+   lock type, showing cluster-wide locks statistics.
+  </para>
+
+  <table id="pg-stat-lock-view" xreflabel="pg_stat_lock">
+   <title><structname>pg_stat_lock</structname> View</title>
+   <tgroup cols="1">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry">
+       <para role="column_definition">
+        Column Type
+       </para>
+       <para>
+        Description
+       </para>
+      </entry>
+     </row>
+    </thead>
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry">
+       <para role="column_definition">
+        <structfield>locktype</structfield> <type>text</type>
+       </para>
+       <para>
+        Type of the lockable object. See <link linkend="view-pg-locks">
+        <structname>pg_locks</structname></link> for details.
+       </para>
+      </entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry">
+       <para role="column_definition">
+        <structfield>waits</structfield> <type>bigint</type>
+       </para>
+       <para>
+        Number of times a lock of this type had to wait because of a
+        conflicting lock. Only incremented when the lock was successfully
+        acquired after waiting longer than <xref linkend="guc-deadlock-timeout"/>.
+       </para>
+      </entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry">
+       <para role="column_definition">
+        <structfield>wait_time</structfield> <type>bigint</type>
+       </para>
+       <para>
+        Total time spent waiting for locks of this type, in milliseconds.
+        Only incremented when the lock was successfully acquired after waiting
+        longer than <xref linkend="guc-deadlock-timeout"/>.
+       </para>
+      </entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry">
+       <para role="column_definition">
+        <structfield>fastpath_exceeded</structfield> <type>bigint</type>
+       </para>
+       <para>
+        Number of times a lock of this type could not be acquired via fast path
+        because the fast path slot limit was exceeded. Increasing
+        <xref linkend="guc-max-locks-per-transaction"/> can reduce this number.
+       </para>
+      </entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry">
+       <para role="column_definition">
+        <structfield>stats_reset</structfield> <type>timestamp with time zone</type>
+       </para>
+       <para>
+        Time at which these statistics were last reset.
+       </para>
+      </entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+ </sect2>
+
  <sect2 id="monitoring-pg-stat-bgwriter-view">
   <title><structname>pg_stat_bgwriter</structname></title>
 
@@ -5372,6 +5476,12 @@ description | Waiting for a newly initialized WAL file to reach durable storage
           <structname>pg_stat_io</structname> view.
          </para>
         </listitem>
+        <listitem>
+         <para>
+          <literal>lock</literal>: Reset all the counters shown in the
+          <structname>pg_stat_lock</structname> view.
+         </para>
+        </listitem>
         <listitem>
          <para>
           <literal>recovery_prefetch</literal>: Reset all the counters shown in
index f1ed7b58f13ba5b5405b4c2afbb0c72b7d167fb7..e54018004db1f1ab8304ea9a5345ab8e2c677be5 100644 (file)
@@ -985,6 +985,15 @@ CREATE VIEW pg_stat_slru AS
             s.stats_reset
     FROM pg_stat_get_slru() s;
 
+CREATE VIEW pg_stat_lock AS
+    SELECT
+            l.locktype,
+            l.waits,
+            l.wait_time,
+            l.fastpath_exceeded,
+            l.stats_reset
+    FROM pg_stat_get_lock() l;
+
 CREATE VIEW pg_stat_wal_receiver AS
     SELECT
             s.pid,
index f875e434ee10804f81c537366ed17f4d0454a43a..234643e4dd7a335c0468b8af66559da5d1cbba75 100644 (file)
@@ -39,6 +39,7 @@
 #include "access/xlogutils.h"
 #include "miscadmin.h"
 #include "pg_trace.h"
+#include "pgstat.h"
 #include "storage/lmgr.h"
 #include "storage/proc.h"
 #include "storage/procarray.h"
@@ -1018,6 +1019,14 @@ LockAcquireExtended(const LOCKTAG *locktag,
                                return LOCKACQUIRE_OK;
                        }
                }
+               else
+               {
+                       /*
+                        * Increment the lock statistics counter if lock could not be
+                        * acquired via the fast-path.
+                        */
+                       pgstat_count_lock_fastpath_exceeded(locallock->tag.lock.locktag_type);
+               }
        }
 
        /*
index a2ec2ce6d72446f96d907c7e0d74e64ec08c9583..ec41c86f390d10e6636dfaf4e702b76d01b2e61a 100644 (file)
@@ -1549,8 +1549,9 @@ ProcSleep(LOCALLOCK *locallock)
                }
 
                /*
-                * If awoken after the deadlock check interrupt has run, and
-                * log_lock_waits is on, then report about the wait.
+                * If awoken after the deadlock check interrupt has run, increment the
+                * lock statistics counters and if log_lock_waits is on, then report
+                * about the wait.
                 */
                if (deadlock_state != DS_NOT_YET_CHECKED)
                {
@@ -1564,6 +1565,10 @@ ProcSleep(LOCALLOCK *locallock)
                        msecs = secs * 1000 + usecs / 1000;
                        usecs = usecs % 1000;
 
+                       /* Increment the lock statistics counters if done waiting. */
+                       if (myWaitStatus == PROC_WAIT_STATUS_OK)
+                               pgstat_count_lock_waits(locallock->tag.lock.locktag_type, msecs);
+
                        if (log_lock_waits)
                        {
                                StringInfoData buf,
index c37bfb350bbc5e3d7339fcdf594305ba9e6365f2..ca3ef89bf5997e5a7a1165384ef8f1e0d5bef4e8 100644 (file)
@@ -26,6 +26,7 @@ OBJS = \
        pgstat_database.o \
        pgstat_function.o \
        pgstat_io.o \
+       pgstat_lock.o \
        pgstat_relation.o \
        pgstat_replslot.o \
        pgstat_shmem.o \
index 53bd5a246cabe67f7c067f163f0b5a2796ff42c5..1aa7ece52908cb22acc53ed6ef0502bfb997b230 100644 (file)
@@ -11,6 +11,7 @@ backend_sources += files(
   'pgstat_database.c',
   'pgstat_function.c',
   'pgstat_io.c',
+  'pgstat_lock.c',
   'pgstat_relation.c',
   'pgstat_replslot.c',
   'pgstat_shmem.c',
index 11bb71cad5ad665abd4c1453006e3209d665db16..eb8ccbaa62858473e6c5955132f83be4235fa58a 100644 (file)
@@ -83,6 +83,7 @@
  * - pgstat_database.c
  * - pgstat_function.c
  * - pgstat_io.c
+ * - pgstat_lock.c
  * - pgstat_relation.c
  * - pgstat_replslot.c
  * - pgstat_slru.c
@@ -448,6 +449,23 @@ static const PgStat_KindInfo pgstat_kind_builtin_infos[PGSTAT_KIND_BUILTIN_SIZE]
                .snapshot_cb = pgstat_io_snapshot_cb,
        },
 
+       [PGSTAT_KIND_LOCK] = {
+               .name = "lock",
+
+               .fixed_amount = true,
+               .write_to_file = true,
+
+               .snapshot_ctl_off = offsetof(PgStat_Snapshot, lock),
+               .shared_ctl_off = offsetof(PgStat_ShmemControl, lock),
+               .shared_data_off = offsetof(PgStatShared_Lock, stats),
+               .shared_data_len = sizeof(((PgStatShared_Lock *) 0)->stats),
+
+               .flush_static_cb = pgstat_lock_flush_cb,
+               .init_shmem_cb = pgstat_lock_init_shmem_cb,
+               .reset_all_cb = pgstat_lock_reset_all_cb,
+               .snapshot_cb = pgstat_lock_snapshot_cb,
+       },
+
        [PGSTAT_KIND_SLRU] = {
                .name = "slru",
 
diff --git a/src/backend/utils/activity/pgstat_lock.c b/src/backend/utils/activity/pgstat_lock.c
new file mode 100644 (file)
index 0000000..041f521
--- /dev/null
@@ -0,0 +1,178 @@
+/* -------------------------------------------------------------------------
+ *
+ * pgstat_lock.c
+ *       Implementation of lock statistics.
+ *
+ * This file contains the implementation of lock statistics.  It is kept
+ * separate from pgstat.c to enforce the line between the statistics
+ * access / storage implementation and the details about individual types
+ * of statistics.
+ *
+ * Copyright (c) 2021-2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *       src/backend/utils/activity/pgstat_lock.c
+ * -------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "utils/pgstat_internal.h"
+
+static PgStat_PendingLock PendingLockStats;
+static bool have_lockstats = false;
+
+PgStat_Lock *
+pgstat_fetch_stat_lock(void)
+{
+       pgstat_snapshot_fixed(PGSTAT_KIND_LOCK);
+
+       return &pgStatLocal.snapshot.lock;
+}
+
+/*
+ * Simpler wrapper of pgstat_lock_flush_cb()
+ */
+void
+pgstat_lock_flush(bool nowait)
+{
+       (void) pgstat_lock_flush_cb(nowait);
+}
+
+/*
+ * Flush out locally pending lock statistics
+ *
+ * If no stats have been recorded, this function returns false.
+ *
+ * If nowait is true, this function returns true if the lock could not be
+ * acquired. Otherwise, return false.
+ */
+bool
+pgstat_lock_flush_cb(bool nowait)
+{
+       LWLock     *lcktype_lock;
+       PgStat_LockEntry *lck_shstats;
+       bool            lock_not_acquired = false;
+
+       if (!have_lockstats)
+               return false;
+
+       for (int i = 0; i <= LOCKTAG_LAST_TYPE; i++)
+       {
+               lcktype_lock = &pgStatLocal.shmem->lock.locks[i];
+               lck_shstats =
+                       &pgStatLocal.shmem->lock.stats.stats[i];
+
+               if (!nowait)
+                       LWLockAcquire(lcktype_lock, LW_EXCLUSIVE);
+               else if (!LWLockConditionalAcquire(lcktype_lock, LW_EXCLUSIVE))
+               {
+                       lock_not_acquired = true;
+                       continue;
+               }
+
+#define LOCKSTAT_ACC(fld) \
+       (lck_shstats->fld += PendingLockStats.stats[i].fld)
+               LOCKSTAT_ACC(waits);
+               LOCKSTAT_ACC(wait_time);
+               LOCKSTAT_ACC(fastpath_exceeded);
+#undef LOCKSTAT_ACC
+
+               LWLockRelease(lcktype_lock);
+       }
+
+       memset(&PendingLockStats, 0, sizeof(PendingLockStats));
+
+       have_lockstats = false;
+
+       return lock_not_acquired;
+}
+
+
+void
+pgstat_lock_init_shmem_cb(void *stats)
+{
+       PgStatShared_Lock *stat_shmem = (PgStatShared_Lock *) stats;
+
+       for (int i = 0; i <= LOCKTAG_LAST_TYPE; i++)
+               LWLockInitialize(&stat_shmem->locks[i], LWTRANCHE_PGSTATS_DATA);
+}
+
+void
+pgstat_lock_reset_all_cb(TimestampTz ts)
+{
+       for (int i = 0; i <= LOCKTAG_LAST_TYPE; i++)
+       {
+               LWLock     *lcktype_lock = &pgStatLocal.shmem->lock.locks[i];
+               PgStat_LockEntry *lck_shstats = &pgStatLocal.shmem->lock.stats.stats[i];
+
+               LWLockAcquire(lcktype_lock, LW_EXCLUSIVE);
+
+               /*
+                * Use the lock in the first lock type PgStat_LockEntry to protect the
+                * reset timestamp as well.
+                */
+               if (i == 0)
+                       pgStatLocal.shmem->lock.stats.stat_reset_timestamp = ts;
+
+               memset(lck_shstats, 0, sizeof(*lck_shstats));
+               LWLockRelease(lcktype_lock);
+       }
+}
+
+void
+pgstat_lock_snapshot_cb(void)
+{
+       for (int i = 0; i <= LOCKTAG_LAST_TYPE; i++)
+       {
+               LWLock     *lcktype_lock = &pgStatLocal.shmem->lock.locks[i];
+               PgStat_LockEntry *lck_shstats = &pgStatLocal.shmem->lock.stats.stats[i];
+               PgStat_LockEntry *lck_snap = &pgStatLocal.snapshot.lock.stats[i];
+
+               LWLockAcquire(lcktype_lock, LW_SHARED);
+
+               /*
+                * Use the lock in the first lock type PgStat_LockEntry to protect the
+                * reset timestamp as well.
+                */
+               if (i == 0)
+                       pgStatLocal.snapshot.lock.stat_reset_timestamp =
+                               pgStatLocal.shmem->lock.stats.stat_reset_timestamp;
+
+               /* using struct assignment due to better type safety */
+               *lck_snap = *lck_shstats;
+               LWLockRelease(lcktype_lock);
+       }
+}
+
+/*
+ * Increment counter for lock not acquired with the fast-path, per lock
+ * type, due to the fast-path slot limit reached.
+ *
+ * Note: This function should not be called in performance-sensitive paths,
+ * like lock acquisitions.
+ */
+void
+pgstat_count_lock_fastpath_exceeded(uint8 locktag_type)
+{
+       Assert(locktag_type <= LOCKTAG_LAST_TYPE);
+       PendingLockStats.stats[locktag_type].fastpath_exceeded++;
+       have_lockstats = true;
+       pgstat_report_fixed = true;
+}
+
+/*
+ * Increment the number of waits and wait time, per lock type.
+ *
+ * Note: This function should not be called in performance-sensitive paths,
+ * like lock acquisitions.
+ */
+void
+pgstat_count_lock_waits(uint8 locktag_type, long msecs)
+{
+       Assert(locktag_type <= LOCKTAG_LAST_TYPE);
+       PendingLockStats.stats[locktag_type].waits++;
+       PendingLockStats.stats[locktag_type].wait_time += (PgStat_Counter) msecs;
+       have_lockstats = true;
+       pgstat_report_fixed = true;
+}
index 5f9073359903bfdbb5888e7790cb164b38531ff7..9185a8e6b83def5dab688598611f39f1e401dd74 100644 (file)
@@ -1741,6 +1741,42 @@ pg_stat_get_wal(PG_FUNCTION_ARGS)
                                                                        wal_stats->stat_reset_timestamp));
 }
 
+Datum
+pg_stat_get_lock(PG_FUNCTION_ARGS)
+{
+#define PG_STAT_LOCK_COLS      5
+       ReturnSetInfo *rsinfo;
+       PgStat_Lock *lock_stats;
+
+       InitMaterializedSRF(fcinfo, 0);
+       rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+
+       lock_stats = pgstat_fetch_stat_lock();
+
+       for (int lcktype = 0; lcktype <= LOCKTAG_LAST_TYPE; lcktype++)
+       {
+               const char *locktypename;
+               Datum           values[PG_STAT_LOCK_COLS] = {0};
+               bool            nulls[PG_STAT_LOCK_COLS] = {0};
+               PgStat_LockEntry *lck_stats = &lock_stats->stats[lcktype];
+               int                     i = 0;
+
+               locktypename = LockTagTypeNames[lcktype];
+
+               values[i++] = CStringGetTextDatum(locktypename);
+               values[i++] = Int64GetDatum(lck_stats->waits);
+               values[i++] = Int64GetDatum(lck_stats->wait_time);
+               values[i++] = Int64GetDatum(lck_stats->fastpath_exceeded);
+               values[i] = TimestampTzGetDatum(lock_stats->stat_reset_timestamp);
+
+               Assert(i + 1 == PG_STAT_LOCK_COLS);
+
+               tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+       }
+
+       return (Datum) 0;
+}
+
 /*
  * Returns statistics of SLRU caches.
  */
@@ -1925,6 +1961,7 @@ pg_stat_reset_shared(PG_FUNCTION_ARGS)
                pgstat_reset_of_kind(PGSTAT_KIND_BGWRITER);
                pgstat_reset_of_kind(PGSTAT_KIND_CHECKPOINTER);
                pgstat_reset_of_kind(PGSTAT_KIND_IO);
+               pgstat_reset_of_kind(PGSTAT_KIND_LOCK);
                XLogPrefetchResetStats();
                pgstat_reset_of_kind(PGSTAT_KIND_SLRU);
                pgstat_reset_of_kind(PGSTAT_KIND_WAL);
@@ -1942,6 +1979,8 @@ pg_stat_reset_shared(PG_FUNCTION_ARGS)
                pgstat_reset_of_kind(PGSTAT_KIND_CHECKPOINTER);
        else if (strcmp(target, "io") == 0)
                pgstat_reset_of_kind(PGSTAT_KIND_IO);
+       else if (strcmp(target, "lock") == 0)
+               pgstat_reset_of_kind(PGSTAT_KIND_LOCK);
        else if (strcmp(target, "recovery_prefetch") == 0)
                XLogPrefetchResetStats();
        else if (strcmp(target, "slru") == 0)
index 420850293f8b337c5e6d72a9c8991445e918a3fe..4d9ccc5789c4d2825bdd0062d2e19b483c628a91 100644 (file)
@@ -57,6 +57,6 @@
  */
 
 /*                                                     yyyymmddN */
-#define CATALOG_VERSION_NO     202603201
+#define CATALOG_VERSION_NO     202603241
 
 #endif
index 84e7adde0e5b5b2c21734b3b83d7b3a14a73c576..0118e970ddafebfdccb6edec10666c9181206996 100644 (file)
   proargnames => '{backend_type,object,context,reads,read_bytes,read_time,writes,write_bytes,write_time,writebacks,writeback_time,extends,extend_bytes,extend_time,hits,evictions,reuses,fsyncs,fsync_time,stats_reset}',
   prosrc => 'pg_stat_get_io' },
 
+{ oid => '9375', descr => 'statistics: per lock type statistics',
+  proname => 'pg_stat_get_lock', prorows => '10', proretset => 't',
+  provolatile => 'v', proparallel => 'r', prorettype => 'record',
+  proargtypes => '', proallargtypes => '{text,int8,int8,int8,timestamptz}',
+  proargmodes => '{o,o,o,o,o}',
+  proargnames => '{locktype,waits,wait_time,fastpath_exceeded,stats_reset}',
+  prosrc => 'pg_stat_get_lock' },
+
 { oid => '6386', descr => 'statistics: backend IO statistics',
   proname => 'pg_stat_get_backend_io', prorows => '5', proretset => 't',
   provolatile => 'v', proparallel => 'r', prorettype => 'record',
index 216b93492ba6a77b1d9935b5af05ae253fb875ff..8e3549c3752547c7a210789d219d07c093186d20 100644 (file)
@@ -15,6 +15,7 @@
 #include "portability/instr_time.h"
 #include "postmaster/pgarch.h" /* for MAX_XFN_CHARS */
 #include "replication/conflict.h"
+#include "storage/locktag.h"
 #include "utils/backend_progress.h" /* for backward compatibility */   /* IWYU pragma: export */
 #include "utils/backend_status.h"      /* for backward compatibility */        /* IWYU pragma: export */
 #include "utils/pgstat_kind.h"
@@ -217,7 +218,7 @@ typedef struct PgStat_TableXactStatus
  * ------------------------------------------------------------
  */
 
-#define PGSTAT_FILE_FORMAT_ID  0x01A5BCBB
+#define PGSTAT_FILE_FORMAT_ID  0x01A5BCBC
 
 typedef struct PgStat_ArchiverStats
 {
@@ -345,6 +346,24 @@ typedef struct PgStat_IO
        PgStat_BktypeIO stats[BACKEND_NUM_TYPES];
 } PgStat_IO;
 
+typedef struct PgStat_LockEntry
+{
+       PgStat_Counter waits;
+       PgStat_Counter wait_time;       /* time in milliseconds */
+       PgStat_Counter fastpath_exceeded;
+} PgStat_LockEntry;
+
+typedef struct PgStat_PendingLock
+{
+       PgStat_LockEntry stats[LOCKTAG_LAST_TYPE + 1];
+} PgStat_PendingLock;
+
+typedef struct PgStat_Lock
+{
+       TimestampTz stat_reset_timestamp;
+       PgStat_LockEntry stats[LOCKTAG_LAST_TYPE + 1];
+} PgStat_Lock;
+
 typedef struct PgStat_StatDBEntry
 {
        PgStat_Counter xact_commit;
@@ -617,6 +636,15 @@ extern bool pgstat_tracks_io_op(BackendType bktype, IOObject io_object,
                                                                IOContext io_context, IOOp io_op);
 
 
+/*
+ * Functions in pgstat_lock.c
+ */
+
+extern void pgstat_lock_flush(bool nowait);
+extern void pgstat_count_lock_fastpath_exceeded(uint8 locktag_type);
+extern void pgstat_count_lock_waits(uint8 locktag_type, long msecs);
+extern PgStat_Lock *pgstat_fetch_stat_lock(void);
+
 /*
  * Functions in pgstat_database.c
  */
index 9b8fbae00ed5770622a892a98ceaf69ae778d130..97704421a92d8e354b3e7c033f6fdfd1545ccc6a 100644 (file)
@@ -464,6 +464,16 @@ typedef struct PgStatShared_IO
        PgStat_IO       stats;
 } PgStatShared_IO;
 
+typedef struct PgStatShared_Lock
+{
+       /*
+        * locks[i] protects stats.stats[i]. locks[0] also protects
+        * stats.stat_reset_timestamp.
+        */
+       LWLock          locks[LOCKTAG_LAST_TYPE + 1];
+       PgStat_Lock stats;
+} PgStatShared_Lock;
+
 typedef struct PgStatShared_SLRU
 {
        /* lock protects ->stats */
@@ -570,6 +580,7 @@ typedef struct PgStat_ShmemControl
        PgStatShared_BgWriter bgwriter;
        PgStatShared_Checkpointer checkpointer;
        PgStatShared_IO io;
+       PgStatShared_Lock lock;
        PgStatShared_SLRU slru;
        PgStatShared_Wal wal;
 
@@ -602,6 +613,8 @@ typedef struct PgStat_Snapshot
 
        PgStat_IO       io;
 
+       PgStat_Lock lock;
+
        PgStat_SLRUStats slru[SLRU_NUM_ELEMENTS];
 
        PgStat_WalStats wal;
@@ -752,6 +765,14 @@ extern void pgstat_io_init_shmem_cb(void *stats);
 extern void pgstat_io_reset_all_cb(TimestampTz ts);
 extern void pgstat_io_snapshot_cb(void);
 
+/*
+ * Functions in pgstat_lock.c
+ */
+
+extern bool pgstat_lock_flush_cb(bool nowait);
+extern void pgstat_lock_init_shmem_cb(void *stats);
+extern void pgstat_lock_reset_all_cb(TimestampTz ts);
+extern void pgstat_lock_snapshot_cb(void);
 
 /*
  * Functions in pgstat_relation.c
index c30b623562300b3279a3e81dd90c7a1780dd80f4..2d78a029683476a251d1d190d22730bc4814d1a5 100644 (file)
@@ -36,8 +36,9 @@
 #define PGSTAT_KIND_BGWRITER   8
 #define PGSTAT_KIND_CHECKPOINTER       9
 #define PGSTAT_KIND_IO 10
-#define PGSTAT_KIND_SLRU       11
-#define PGSTAT_KIND_WAL        12
+#define PGSTAT_KIND_LOCK       11
+#define PGSTAT_KIND_SLRU       12
+#define PGSTAT_KIND_WAL        13
 
 #define PGSTAT_KIND_BUILTIN_MIN PGSTAT_KIND_DATABASE
 #define PGSTAT_KIND_BUILTIN_MAX PGSTAT_KIND_WAL
diff --git a/src/test/isolation/expected/lock-stats.out b/src/test/isolation/expected/lock-stats.out
new file mode 100644 (file)
index 0000000..160f0c9
--- /dev/null
@@ -0,0 +1,197 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s2_set_deadlock_timeout s1_begin s1_lock_relation s2_begin s2_ff s2_lock_relation s1_sleep s1_commit s2_commit s2_report_stat_lock_relation
+pg_stat_force_next_flush
+------------------------
+                        
+(1 row)
+
+step s1_set_deadlock_timeout: SET deadlock_timeout = '10ms';
+step s1_reset_stat_lock: SELECT pg_stat_reset_shared('lock');
+pg_stat_reset_shared
+--------------------
+                    
+(1 row)
+
+step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
+step s1_begin: BEGIN;
+step s1_lock_relation: LOCK TABLE test_stat_tab;
+step s2_begin: BEGIN;
+step s2_ff: SELECT pg_stat_force_next_flush();
+pg_stat_force_next_flush
+------------------------
+                        
+(1 row)
+
+step s2_lock_relation: LOCK TABLE test_stat_tab; <waiting ...>
+step s1_sleep: SELECT pg_sleep(0.05);
+pg_sleep
+--------
+        
+(1 row)
+
+step s1_commit: COMMIT;
+step s2_lock_relation: <... completed>
+step s2_commit: COMMIT;
+step s2_report_stat_lock_relation: 
+  SELECT waits > 0 AS has_waits, wait_time > 50 AS has_wait_time
+    FROM pg_stat_lock WHERE locktype = 'relation';
+
+has_waits|has_wait_time
+---------+-------------
+t        |t            
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s2_set_deadlock_timeout s2_set_log_lock_waits s1_table_insert s1_begin s1_table_update_k1 s2_begin s2_ff s2_table_update_k1 s1_sleep s1_commit s2_commit s2_report_stat_lock_transactionid
+pg_stat_force_next_flush
+------------------------
+                        
+(1 row)
+
+step s1_set_deadlock_timeout: SET deadlock_timeout = '10ms';
+step s1_reset_stat_lock: SELECT pg_stat_reset_shared('lock');
+pg_stat_reset_shared
+--------------------
+                    
+(1 row)
+
+step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
+step s2_set_log_lock_waits: SET log_lock_waits = on;
+step s1_table_insert: INSERT INTO test_stat_tab(key, value) VALUES('k1', 1), ('k2', 1), ('k3', 1);
+step s1_begin: BEGIN;
+step s1_table_update_k1: UPDATE test_stat_tab SET value = value + 1 WHERE key = 'k1';
+step s2_begin: BEGIN;
+step s2_ff: SELECT pg_stat_force_next_flush();
+pg_stat_force_next_flush
+------------------------
+                        
+(1 row)
+
+step s2_table_update_k1: UPDATE test_stat_tab SET value = value + 1 WHERE key = 'k1'; <waiting ...>
+step s1_sleep: SELECT pg_sleep(0.05);
+pg_sleep
+--------
+        
+(1 row)
+
+step s1_commit: COMMIT;
+step s2_table_update_k1: <... completed>
+step s2_commit: COMMIT;
+step s2_report_stat_lock_transactionid: 
+  SELECT waits > 0 AS has_waits, wait_time > 50 AS has_wait_time
+    FROM pg_stat_lock WHERE locktype = 'transactionid';
+
+has_waits|has_wait_time
+---------+-------------
+t        |t            
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s2_set_deadlock_timeout s2_set_log_lock_waits s1_lock_advisory_lock s2_begin s2_ff s2_lock_advisory_lock s1_sleep s1_lock_advisory_unlock s2_lock_advisory_unlock s2_commit s2_report_stat_lock_advisory
+pg_stat_force_next_flush
+------------------------
+                        
+(1 row)
+
+step s1_set_deadlock_timeout: SET deadlock_timeout = '10ms';
+step s1_reset_stat_lock: SELECT pg_stat_reset_shared('lock');
+pg_stat_reset_shared
+--------------------
+                    
+(1 row)
+
+step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
+step s2_set_log_lock_waits: SET log_lock_waits = on;
+step s1_lock_advisory_lock: SELECT pg_advisory_lock(1);
+pg_advisory_lock
+----------------
+                
+(1 row)
+
+step s2_begin: BEGIN;
+step s2_ff: SELECT pg_stat_force_next_flush();
+pg_stat_force_next_flush
+------------------------
+                        
+(1 row)
+
+step s2_lock_advisory_lock: SELECT pg_advisory_lock(1); <waiting ...>
+step s1_sleep: SELECT pg_sleep(0.05);
+pg_sleep
+--------
+        
+(1 row)
+
+step s1_lock_advisory_unlock: SELECT pg_advisory_unlock(1);
+pg_advisory_unlock
+------------------
+t                 
+(1 row)
+
+step s2_lock_advisory_lock: <... completed>
+pg_advisory_lock
+----------------
+                
+(1 row)
+
+step s2_lock_advisory_unlock: SELECT pg_advisory_unlock(1);
+pg_advisory_unlock
+------------------
+t                 
+(1 row)
+
+step s2_commit: COMMIT;
+step s2_report_stat_lock_advisory: 
+  SELECT waits > 0 AS has_waits, wait_time > 50 AS has_wait_time
+    FROM pg_stat_lock WHERE locktype = 'advisory';
+
+has_waits|has_wait_time
+---------+-------------
+t        |t            
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s2_set_deadlock_timeout s2_unset_log_lock_waits s1_begin s1_lock_relation s2_begin s2_ff s2_lock_relation s1_sleep s1_commit s2_commit s2_report_stat_lock_relation
+pg_stat_force_next_flush
+------------------------
+                        
+(1 row)
+
+step s1_set_deadlock_timeout: SET deadlock_timeout = '10ms';
+step s1_reset_stat_lock: SELECT pg_stat_reset_shared('lock');
+pg_stat_reset_shared
+--------------------
+                    
+(1 row)
+
+step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
+step s2_unset_log_lock_waits: SET log_lock_waits = off;
+step s1_begin: BEGIN;
+step s1_lock_relation: LOCK TABLE test_stat_tab;
+step s2_begin: BEGIN;
+step s2_ff: SELECT pg_stat_force_next_flush();
+pg_stat_force_next_flush
+------------------------
+                        
+(1 row)
+
+step s2_lock_relation: LOCK TABLE test_stat_tab; <waiting ...>
+step s1_sleep: SELECT pg_sleep(0.05);
+pg_sleep
+--------
+        
+(1 row)
+
+step s1_commit: COMMIT;
+step s2_lock_relation: <... completed>
+step s2_commit: COMMIT;
+step s2_report_stat_lock_relation: 
+  SELECT waits > 0 AS has_waits, wait_time > 50 AS has_wait_time
+    FROM pg_stat_lock WHERE locktype = 'relation';
+
+has_waits|has_wait_time
+---------+-------------
+t        |t            
+(1 row)
+
index 4e466580cd4d8dfa9301c21c2eb721b22526d021..2cc86849c9aa26422c84dc1cb262ae1fda7a44a9 100644 (file)
@@ -43,6 +43,7 @@ test: eval-plan-qual-trigger
 test: inplace-inval
 test: intra-grant-inplace
 test: intra-grant-inplace-db
+test: lock-stats
 test: lock-update-delete
 test: lock-update-traversal
 test: inherit-temp
diff --git a/src/test/isolation/specs/lock-stats.spec b/src/test/isolation/specs/lock-stats.spec
new file mode 100644 (file)
index 0000000..e688642
--- /dev/null
@@ -0,0 +1,128 @@
+# Test for the lock statistics
+#
+# This test creates multiple locking situations when a session (s2) has to
+# wait on a lock for longer than deadlock_timeout.  The first permutations
+# test various lock tags.  The last permutation checks that log_lock_waits
+# has no impact on the statistics counters.
+
+setup
+{
+    CREATE TABLE test_stat_tab(key text not null, value int);
+    INSERT INTO test_stat_tab(key, value) VALUES('k0', 1);
+    SELECT pg_stat_force_next_flush();
+}
+
+teardown
+{
+    DROP TABLE IF EXISTS test_stat_tab;
+}
+
+session s1
+setup { SET stats_fetch_consistency = 'none'; }
+step s1_begin { BEGIN; }
+step s1_commit { COMMIT; }
+step s1_table_insert { INSERT INTO test_stat_tab(key, value) VALUES('k1', 1), ('k2', 1), ('k3', 1);}
+step s1_table_update_k1 { UPDATE test_stat_tab SET value = value + 1 WHERE key = 'k1';}
+step s1_set_deadlock_timeout { SET deadlock_timeout = '10ms'; }
+step s1_reset_stat_lock { SELECT pg_stat_reset_shared('lock'); }
+step s1_sleep { SELECT pg_sleep(0.05); }
+step s1_lock_relation { LOCK TABLE test_stat_tab; }
+step s1_lock_advisory_lock { SELECT pg_advisory_lock(1); }
+step s1_lock_advisory_unlock { SELECT pg_advisory_unlock(1); }
+
+session s2
+setup { SET stats_fetch_consistency = 'none'; }
+step s2_begin { BEGIN; }
+step s2_commit { COMMIT; }
+step s2_ff { SELECT pg_stat_force_next_flush(); }
+step s2_table_update_k1 { UPDATE test_stat_tab SET value = value + 1 WHERE key = 'k1';}
+step s2_set_deadlock_timeout { SET deadlock_timeout = '10ms'; }
+step s2_set_log_lock_waits { SET log_lock_waits = on; }
+step s2_unset_log_lock_waits { SET log_lock_waits = off; }
+step s2_report_stat_lock_relation {
+  SELECT waits > 0 AS has_waits, wait_time > 50 AS has_wait_time
+    FROM pg_stat_lock WHERE locktype = 'relation';
+}
+step s2_report_stat_lock_transactionid {
+  SELECT waits > 0 AS has_waits, wait_time > 50 AS has_wait_time
+    FROM pg_stat_lock WHERE locktype = 'transactionid';
+}
+step s2_report_stat_lock_advisory {
+  SELECT waits > 0 AS has_waits, wait_time > 50 AS has_wait_time
+    FROM pg_stat_lock WHERE locktype = 'advisory';
+}
+step s2_lock_relation { LOCK TABLE test_stat_tab; }
+step s2_lock_advisory_lock { SELECT pg_advisory_lock(1); }
+step s2_lock_advisory_unlock { SELECT pg_advisory_unlock(1); }
+
+######################
+# Lock stats tests
+######################
+
+# relation lock
+
+permutation
+  s1_set_deadlock_timeout
+  s1_reset_stat_lock
+  s2_set_deadlock_timeout
+  s1_begin
+  s1_lock_relation
+  s2_begin
+  s2_ff
+  s2_lock_relation
+  s1_sleep
+  s1_commit
+  s2_commit
+  s2_report_stat_lock_relation
+
+# transaction lock
+
+permutation
+  s1_set_deadlock_timeout
+  s1_reset_stat_lock
+  s2_set_deadlock_timeout
+  s2_set_log_lock_waits
+  s1_table_insert
+  s1_begin
+  s1_table_update_k1
+  s2_begin
+  s2_ff
+  s2_table_update_k1
+  s1_sleep
+  s1_commit
+  s2_commit
+  s2_report_stat_lock_transactionid
+
+# advisory lock
+
+permutation
+  s1_set_deadlock_timeout
+  s1_reset_stat_lock
+  s2_set_deadlock_timeout
+  s2_set_log_lock_waits
+  s1_lock_advisory_lock
+  s2_begin
+  s2_ff
+  s2_lock_advisory_lock
+  s1_sleep
+  s1_lock_advisory_unlock
+  s2_lock_advisory_unlock
+  s2_commit
+  s2_report_stat_lock_advisory
+
+# Ensure log_lock_waits has no impact
+
+permutation
+  s1_set_deadlock_timeout
+  s1_reset_stat_lock
+  s2_set_deadlock_timeout
+  s2_unset_log_lock_waits
+  s1_begin
+  s1_lock_relation
+  s2_begin
+  s2_ff
+  s2_lock_relation
+  s1_sleep
+  s1_commit
+  s2_commit
+  s2_report_stat_lock_relation
index 32bea58db2ccfb3d6cc8f92dc6414041fd6263cf..2b3cf6d85695f1a8c9d037e8b2badea19029eef0 100644 (file)
@@ -1952,6 +1952,12 @@ pg_stat_io| SELECT backend_type,
     fsync_time,
     stats_reset
    FROM pg_stat_get_io() b(backend_type, object, context, reads, read_bytes, read_time, writes, write_bytes, write_time, writebacks, writeback_time, extends, extend_bytes, extend_time, hits, evictions, reuses, fsyncs, fsync_time, stats_reset);
+pg_stat_lock| SELECT locktype,
+    waits,
+    wait_time,
+    fastpath_exceeded,
+    stats_reset
+   FROM pg_stat_get_lock() l(locktype, waits, wait_time, fastpath_exceeded, stats_reset);
 pg_stat_progress_analyze| SELECT s.pid,
     s.datid,
     d.datname,
index b99462bf94641392b46a37761c1c1998d0acfaa0..ea7f784689593f7c9688b06d4485a07247daeacf 100644 (file)
@@ -1958,4 +1958,52 @@ SELECT * FROM check_estimated_rows('SELECT * FROM table_fillfactor');
 (1 row)
 
 DROP TABLE table_fillfactor;
+-- Test fastpath_exceeded stat
+CREATE TABLE part_test (id int) PARTITION BY RANGE (id);
+SELECT pg_stat_reset_shared('lock');
+ pg_stat_reset_shared 
+----------------------
+(1 row)
+
+-- Create partitions (exceeds number of slots)
+DO $$
+DECLARE
+  max_locks int;
+BEGIN
+  SELECT setting::int INTO max_locks
+  FROM pg_settings
+  WHERE name = 'max_locks_per_transaction';
+
+  FOR i IN 1..(max_locks + 10) LOOP
+    EXECUTE format(
+      'CREATE TABLE part_test_%s PARTITION OF part_test
+       FOR VALUES FROM (%s) TO (%s)',
+      i, (i-1)*1000, i*1000
+    );
+  END LOOP;
+END;
+$$;
+SELECT fastpath_exceeded AS fastpath_exceeded_before FROM pg_stat_lock WHERE locktype = 'relation' \gset
+-- Needs a lock on each partition
+SELECT count(*) FROM part_test;
+ count 
+-------
+     0
+(1 row)
+
+-- Ensure pending stats are flushed
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush 
+--------------------------
+(1 row)
+
+SELECT fastpath_exceeded > :fastpath_exceeded_before FROM pg_stat_lock WHERE locktype = 'relation';
+ ?column? 
+----------
+ t
+(1 row)
+
+DROP TABLE part_test;
 -- End of Stats Test
index 941222cf0bec64656c17f0222bd0e5aefbaa606f..65d8968c83e78909b90e38711ea9c0a3f0f776d1 100644 (file)
@@ -964,4 +964,40 @@ SELECT * FROM check_estimated_rows('SELECT * FROM table_fillfactor');
 
 DROP TABLE table_fillfactor;
 
+-- Test fastpath_exceeded stat
+CREATE TABLE part_test (id int) PARTITION BY RANGE (id);
+
+SELECT pg_stat_reset_shared('lock');
+
+-- Create partitions (exceeds number of slots)
+DO $$
+DECLARE
+  max_locks int;
+BEGIN
+  SELECT setting::int INTO max_locks
+  FROM pg_settings
+  WHERE name = 'max_locks_per_transaction';
+
+  FOR i IN 1..(max_locks + 10) LOOP
+    EXECUTE format(
+      'CREATE TABLE part_test_%s PARTITION OF part_test
+       FOR VALUES FROM (%s) TO (%s)',
+      i, (i-1)*1000, i*1000
+    );
+  END LOOP;
+END;
+$$;
+
+SELECT fastpath_exceeded AS fastpath_exceeded_before FROM pg_stat_lock WHERE locktype = 'relation' \gset
+
+-- Needs a lock on each partition
+SELECT count(*) FROM part_test;
+
+-- Ensure pending stats are flushed
+SELECT pg_stat_force_next_flush();
+
+SELECT fastpath_exceeded > :fastpath_exceeded_before FROM pg_stat_lock WHERE locktype = 'relation';
+
+DROP TABLE part_test;
+
 -- End of Stats Test
index 0c07c945f05cd9fdb690e71682afff21782546fd..8df23840e57c122aa4ab397857f5a8c6851edc29 100644 (file)
@@ -2282,6 +2282,7 @@ PgStatShared_Database
 PgStatShared_Function
 PgStatShared_HashEntry
 PgStatShared_IO
+PgStatShared_Lock
 PgStatShared_Relation
 PgStatShared_ReplSlot
 PgStatShared_SLRU
@@ -2304,8 +2305,11 @@ PgStat_HashKey
 PgStat_IO
 PgStat_KindInfo
 PgStat_LocalState
+PgStat_Lock
+PgStat_LockEntry
 PgStat_PendingDroppedStatsItem
 PgStat_PendingIO
+PgStat_PendingLock
 PgStat_SLRUStats
 PgStat_ShmemControl
 PgStat_Snapshot