From 28719d74ca686eed979bae657fada4ed726fd3fa Mon Sep 17 00:00:00 2001
From: TatsuyaKawata <kawatatatsuya0913@gmail.com>
Date: Sun, 31 May 2026 17:27:05 +0900
Subject: [PATCH v1] pg_stat_lock: add blocker mode dimension

Extend pg_stat_lock to track wait statistics per (locktype, blocker_mode)
instead of per locktype only.  This allows the view to distinguish, e.g.,
waits caused by VACUUM (ShareUpdateExclusiveLock) from waits caused by
DDL (AccessExclusiveLock), which is otherwise only possible by parsing
log_lock_waits output.

The blocker_mode is captured at the moment the requester joins the wait
queue, under the lock partition LWLock.  The selection rule prefers a
currently-held conflicting mode (strongest first) over a queued waiter
mode, so the recorded blocker reflects an actual current holder when
one exists and falls back to a queued waiter only when the wait is
purely due to queue priority.
---
 src/backend/catalog/system_views.sql     |  1 +
 src/backend/storage/lmgr/lock.c          | 46 +++++++++++++++++++++++-
 src/backend/storage/lmgr/proc.c          |  3 +-
 src/backend/utils/activity/pgstat_lock.c | 37 ++++++++++++-------
 src/backend/utils/adt/pgstatfuncs.c      | 44 +++++++++++++++--------
 src/include/catalog/catversion.h         |  2 +-
 src/include/catalog/pg_proc.dat          | 10 +++---
 src/include/pgstat.h                     | 14 +++++---
 src/include/storage/lock.h               |  4 +++
 src/test/regress/expected/rules.out      |  3 +-
 src/test/regress/expected/stats.out      |  6 ++--
 src/test/regress/sql/stats.sql           |  6 ++--
 12 files changed, 133 insertions(+), 43 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 73a1c1c4670..7d3d2d8ef8d 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1006,6 +1006,7 @@ CREATE VIEW pg_stat_slru AS
 CREATE VIEW pg_stat_lock AS
     SELECT
             l.locktype,
+            l.mode,
             l.waits,
             l.wait_time,
             l.fastpath_exceeded,
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index 8d246ed5a4e..147c63cbd31 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -1021,7 +1021,7 @@ LockAcquireExtended(const LOCKTAG *locktag,
 			 * 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);
+			pgstat_count_lock_fastpath_exceeded(locallock->tag.lock.locktag_type, lockmode);
 		}
 	}
 
@@ -1113,6 +1113,50 @@ LockAcquireExtended(const LOCKTAG *locktag,
 	}
 	else
 	{
+		/*
+		 * Take a snapshot of the conflicting lock mode that is causing us
+		 * to wait, for pg_stat_lock attribution.  Prefer modes that are
+		 * currently held over those that are merely queued ahead of us, so
+		 * that the recorded blocker reflects an actual current holder when
+		 * one exists.  Walk from the strongest mode down so that the first
+		 * match is the strongest conflicting mode.
+		 *
+		 * This is done under the partition lock so the snapshot is
+		 * consistent.  If JoinWaitQueue() ends up not making us wait, the
+		 * snapshot will simply not be read by pgstat_count_lock_waits().
+		 */
+		LOCKMASK	conflict_mask = lockMethodTable->conflictTab[lockmode];
+		LOCKMODE	blocker = 0;
+
+		/* Phase 1: prefer an actual current holder. */
+		for (LOCKMODE i = MaxLockMode; i >= 1; i--)
+		{
+			if ((conflict_mask & LOCKBIT_ON(i)) && lock->granted[i] > 0)
+			{
+				blocker = i;
+				break;
+			}
+		}
+
+		/*
+		 * Phase 2: if no held mode conflicts, the wait is caused only by
+		 * queue priority against another waiter.  Attribute to the
+		 * strongest queued mode that conflicts with us.
+		 */
+		if (blocker == 0)
+		{
+			for (LOCKMODE i = MaxLockMode; i >= 1; i--)
+			{
+				if ((conflict_mask & LOCKBIT_ON(i)) &&
+					(lock->waitMask & LOCKBIT_ON(i)))
+				{
+					blocker = i;
+					break;
+				}
+			}
+		}
+		locallock->blocker_mode = blocker;
+
 		/*
 		 * Join the lock's wait queue.  We call this even in the dontWait
 		 * case, because JoinWaitQueue() may discover that we can acquire the
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 6fa9de33e1c..5ca89f9fc45 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -1613,7 +1613,8 @@ ProcSleep(LOCALLOCK *locallock)
 
 			/* Increment the lock statistics counters if done waiting. */
 			if (myWaitStatus == PROC_WAIT_STATUS_OK)
-				pgstat_count_lock_waits(locallock->tag.lock.locktag_type, msecs);
+				pgstat_count_lock_waits(locallock->tag.lock.locktag_type,
+										locallock->blocker_mode, msecs);
 
 			if (log_lock_waits)
 			{
diff --git a/src/backend/utils/activity/pgstat_lock.c b/src/backend/utils/activity/pgstat_lock.c
index aec64f8fb4b..89c7c7b741e 100644
--- a/src/backend/utils/activity/pgstat_lock.c
+++ b/src/backend/utils/activity/pgstat_lock.c
@@ -66,12 +66,15 @@ pgstat_lock_flush_cb(bool nowait)
 
 	for (int i = 0; i <= LOCKTAG_LAST_TYPE; i++)
 	{
+		for (int j = 0; j <= MaxLockMode; j++)
+		{
 #define LOCKSTAT_ACC(fld) \
-	(shstats->stats.stats[i].fld += PendingLockStats.stats[i].fld)
-		LOCKSTAT_ACC(waits);
-		LOCKSTAT_ACC(wait_time);
-		LOCKSTAT_ACC(fastpath_exceeded);
+		(shstats->stats.stats[i][j].fld += PendingLockStats.stats[i][j].fld)
+			LOCKSTAT_ACC(waits);
+			LOCKSTAT_ACC(wait_time);
+			LOCKSTAT_ACC(fastpath_exceeded);
 #undef LOCKSTAT_ACC
+		}
 	}
 
 	LWLockRelease(lckstat_lock);
@@ -118,33 +121,43 @@ pgstat_lock_snapshot_cb(void)
 }
 
 /*
- * Increment counter for lock not acquired with the fast-path, per lock
- * type, due to the fast-path slot limit reached.
+ * Increment counter for lock not acquired with the fast-path, per
+ * (lock type, mode), due to the fast-path slot limit reached.
+ *
+ * The "mode" dimension is the requested lock mode (fast-path only
+ * applies to weak modes on relations); there is no "blocker" concept
+ * for slot exhaustion.
  *
  * Note: This function should not be called in performance-sensitive paths,
  * like lock acquisitions.
  */
 void
-pgstat_count_lock_fastpath_exceeded(uint8 locktag_type)
+pgstat_count_lock_fastpath_exceeded(uint8 locktag_type, LOCKMODE lockmode)
 {
 	Assert(locktag_type <= LOCKTAG_LAST_TYPE);
-	PendingLockStats.stats[locktag_type].fastpath_exceeded++;
+	Assert(lockmode > 0 && lockmode <= MaxLockMode);
+	PendingLockStats.stats[locktag_type][lockmode].fastpath_exceeded++;
 	have_lockstats = true;
 	pgstat_report_fixed = true;
 }
 
 /*
- * Increment the number of waits and wait time, per lock type.
+ * Increment the number of waits and wait time, per (lock type, mode).
+ *
+ * The "mode" dimension is the mode the wait should be attributed to.
+ * Callers typically pass the strongest conflicting lock mode captured
+ * at queue join time (see LockAcquireExtended()).
  *
  * Note: This function should not be called in performance-sensitive paths,
  * like lock acquisitions.
  */
 void
-pgstat_count_lock_waits(uint8 locktag_type, long msecs)
+pgstat_count_lock_waits(uint8 locktag_type, LOCKMODE lockmode, long msecs)
 {
 	Assert(locktag_type <= LOCKTAG_LAST_TYPE);
-	PendingLockStats.stats[locktag_type].waits++;
-	PendingLockStats.stats[locktag_type].wait_time += (PgStat_Counter) msecs;
+	Assert(lockmode > 0 && lockmode <= MaxLockMode);
+	PendingLockStats.stats[locktag_type][lockmode].waits++;
+	PendingLockStats.stats[locktag_type][lockmode].wait_time += (PgStat_Counter) msecs;
 	have_lockstats = true;
 	pgstat_report_fixed = true;
 }
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 6f9c9c72de5..3c819082f4f 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1740,7 +1740,7 @@ pg_stat_get_wal(PG_FUNCTION_ARGS)
 Datum
 pg_stat_get_lock(PG_FUNCTION_ARGS)
 {
-#define PG_STAT_LOCK_COLS	5
+#define PG_STAT_LOCK_COLS	6
 	ReturnSetInfo *rsinfo;
 	PgStat_Lock *lock_stats;
 
@@ -1751,23 +1751,39 @@ pg_stat_get_lock(PG_FUNCTION_ARGS)
 
 	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;
+		const char *locktypename = LockTagTypeNames[lcktype];
 
-		locktypename = LockTagTypeNames[lcktype];
+		for (LOCKMODE mode = 1; mode <= MaxLockMode; mode++)
+		{
+			Datum		values[PG_STAT_LOCK_COLS] = {0};
+			bool		nulls[PG_STAT_LOCK_COLS] = {0};
+			PgStat_LockEntry *lck_stats = &lock_stats->stats[lcktype][mode];
+			int			i = 0;
 
-		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);
+			/*
+			 * Skip cells with no recorded activity to keep the view
+			 * sparse.  Combinations of (locktype, mode) that never occur
+			 * in practice (e.g. AccessExclusiveLock on transactionid)
+			 * would otherwise appear as noise rows.
+			 */
+			if (lck_stats->waits == 0 &&
+				lck_stats->wait_time == 0 &&
+				lck_stats->fastpath_exceeded == 0)
+				continue;
 
-		Assert(i + 1 == PG_STAT_LOCK_COLS);
+			values[i++] = CStringGetTextDatum(locktypename);
+			values[i++] = CStringGetTextDatum(GetLockmodeName(DEFAULT_LOCKMETHOD,
+															  mode));
+			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);
 
-		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+			Assert(i + 1 == PG_STAT_LOCK_COLS);
+
+			tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc,
+								 values, nulls);
+		}
 	}
 
 	return (Datum) 0;
diff --git a/src/include/catalog/catversion.h b/src/include/catalog/catversion.h
index a1416260abc..9716574efec 100644
--- a/src/include/catalog/catversion.h
+++ b/src/include/catalog/catversion.h
@@ -57,6 +57,6 @@
  */
 
 /*							yyyymmddN */
-#define CATALOG_VERSION_NO	202605131
+#define CATALOG_VERSION_NO	202605311
 
 #endif
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index be157a5fbe9..43aa5777395 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6061,12 +6061,12 @@
   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 => '6509', descr => 'statistics: per lock type statistics',
-  proname => 'pg_stat_get_lock', prorows => '10', proretset => 't',
+{ oid => '6509', descr => 'statistics: per (lock type, blocker mode) statistics',
+  proname => 'pg_stat_get_lock', prorows => '40', 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}',
+  proargtypes => '', proallargtypes => '{text,text,int8,int8,int8,timestamptz}',
+  proargmodes => '{o,o,o,o,o,o}',
+  proargnames => '{locktype,mode,waits,wait_time,fastpath_exceeded,stats_reset}',
   prosrc => 'pg_stat_get_lock' },
 
 { oid => '6386', descr => 'statistics: backend IO statistics',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index dfa2e837638..2e21d03fbeb 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -15,6 +15,7 @@
 #include "portability/instr_time.h"
 #include "postmaster/pgarch.h"	/* for MAX_XFN_CHARS */
 #include "replication/conflict.h"
+#include "storage/lockdefs.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 */
@@ -353,15 +354,20 @@ typedef struct PgStat_LockEntry
 	PgStat_Counter fastpath_exceeded;
 } PgStat_LockEntry;
 
+/*
+ * Lock statistics are tracked per (locktag type, lock mode) pair.
+ * The interpretation of "mode" depends on the caller (see
+ * pgstat_count_lock_waits() and pgstat_count_lock_fastpath_exceeded()).
+ */
 typedef struct PgStat_PendingLock
 {
-	PgStat_LockEntry stats[LOCKTAG_LAST_TYPE + 1];
+	PgStat_LockEntry stats[LOCKTAG_LAST_TYPE + 1][MaxLockMode + 1];
 } PgStat_PendingLock;
 
 typedef struct PgStat_Lock
 {
 	TimestampTz stat_reset_timestamp;
-	PgStat_LockEntry stats[LOCKTAG_LAST_TYPE + 1];
+	PgStat_LockEntry stats[LOCKTAG_LAST_TYPE + 1][MaxLockMode + 1];
 } PgStat_Lock;
 
 typedef struct PgStat_StatDBEntry
@@ -637,8 +643,8 @@ extern bool pgstat_tracks_io_op(BackendType bktype, IOObject io_object,
  */
 
 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 void pgstat_count_lock_fastpath_exceeded(uint8 locktag_type, LOCKMODE lockmode);
+extern void pgstat_count_lock_waits(uint8 locktag_type, LOCKMODE lockmode, long msecs);
 extern PgStat_Lock *pgstat_fetch_stat_lock(void);
 
 /*
diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h
index ee3cb1dc203..ce8890f02e6 100644
--- a/src/include/storage/lock.h
+++ b/src/include/storage/lock.h
@@ -269,6 +269,10 @@ typedef struct LOCALLOCK
 	LOCALLOCKOWNER *lockOwners; /* dynamically resizable array */
 	bool		holdsStrongLockCount;	/* bumped FastPathStrongRelationLocks */
 	bool		lockCleared;	/* we read all sinval msgs for lock */
+	LOCKMODE	blocker_mode;	/* snapshot of the conflicting lock mode that
+								 * caused us to wait when we last joined the
+								 * wait queue. 0 if not currently waiting /
+								 * never waited. */
 } LOCALLOCK;
 
 #define LOCALLOCK_LOCKMETHOD(llock) ((llock).tag.lock.locktag_lockmethodid)
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index a65a5bf0c4f..39a3b4e20a5 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1968,11 +1968,12 @@ pg_stat_io| SELECT backend_type,
     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,
+    mode,
     waits,
     wait_time,
     fastpath_exceeded,
     stats_reset
-   FROM pg_stat_get_lock() l(locktype, waits, wait_time, fastpath_exceeded, stats_reset);
+   FROM pg_stat_get_lock() l(locktype, mode, waits, wait_time, fastpath_exceeded, stats_reset);
 pg_stat_progress_analyze| SELECT s.pid,
     s.datid,
     d.datname,
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index bbb1db3c433..fe4644e2dee 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -2018,7 +2018,8 @@ BEGIN
   END LOOP;
 END;
 $$;
-SELECT fastpath_exceeded AS fastpath_exceeded_before FROM pg_stat_lock WHERE locktype = 'relation' \gset
+SELECT COALESCE(sum(fastpath_exceeded), 0) AS fastpath_exceeded_before
+  FROM pg_stat_lock WHERE locktype = 'relation' \gset
 -- Needs a lock on each partition
 SELECT count(*) FROM part_test;
  count 
@@ -2033,7 +2034,8 @@ SELECT pg_stat_force_next_flush();
  
 (1 row)
 
-SELECT fastpath_exceeded > :fastpath_exceeded_before FROM pg_stat_lock WHERE locktype = 'relation';
+SELECT COALESCE(sum(fastpath_exceeded), 0) > :fastpath_exceeded_before
+  FROM pg_stat_lock WHERE locktype = 'relation';
  ?column? 
 ----------
  t
diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql
index 610fd21fae4..29dab94bb9a 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -996,7 +996,8 @@ BEGIN
 END;
 $$;
 
-SELECT fastpath_exceeded AS fastpath_exceeded_before FROM pg_stat_lock WHERE locktype = 'relation' \gset
+SELECT COALESCE(sum(fastpath_exceeded), 0) AS fastpath_exceeded_before
+  FROM pg_stat_lock WHERE locktype = 'relation' \gset
 
 -- Needs a lock on each partition
 SELECT count(*) FROM part_test;
@@ -1004,7 +1005,8 @@ 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';
+SELECT COALESCE(sum(fastpath_exceeded), 0) > :fastpath_exceeded_before
+  FROM pg_stat_lock WHERE locktype = 'relation';
 
 DROP TABLE part_test;
 
-- 
2.34.1

