pg.ddx.io pgsql-hackers@postgresql.org mailing list archive
help / color / mirror / Atom feedAdding locks statistics
61+ messages / 8 participants
[nested] [flat]
* Adding locks statistics
@ 2025-08-01 09:49 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
0 siblings, 1 reply; 61+ messages in thread
From: Bertrand Drouvot @ 2025-08-01 09:49 UTC (permalink / raw)
To: pgsql-hackers@lists.postgresql.org
Hi hackers,
Please find attached a patch to add a new view (namely pg_stat_lock) that provides
lock statistics.
It’s output is like the following:
postgres=# select * from pg_stat_lock;
locktype | requests | waits | timeouts | deadlock_timeouts | deadlocks | fastpath | stats_reset
------------------+----------+-------+----------+-------------------+-----------+----------+-------------------------------
relation | 612775 | 1 | 0 | 0 | 0 | 531115 | 2025-08-01 09:18:26.476275+00
extend | 3128 | 0 | 0 | 0 | 0 | 0 | 2025-08-01 09:18:26.476275+00
frozenid | 11 | 0 | 0 | 0 | 0 | 0 | 2025-08-01 09:18:26.476275+00
page | 1 | 0 | 0 | 0 | 0 | 0 | 2025-08-01 09:18:26.476275+00
tuple | 3613 | 0 | 0 | 0 | 0 | 0 | 2025-08-01 09:18:26.476275+00
transactionid | 6130 | 0 | 0 | 0 | 0 | 0 | 2025-08-01 09:18:26.476275+00
virtualxid | 15390 | 0 | 0 | 0 | 0 | 15390 | 2025-08-01 09:18:26.476275+00
spectoken | 12 | 0 | 0 | 0 | 0 | 0 | 2025-08-01 09:18:26.476275+00
object | 8393 | 0 | 0 | 0 | 0 | 0 | 2025-08-01 09:18:26.476275+00
userlock | 0 | 0 | 0 | 0 | 0 | 0 | 2025-08-01 09:18:26.476275+00
advisory | 44 | 0 | 0 | 0 | 0 | 0 | 2025-08-01 09:18:26.476275+00
applytransaction | 0 | 0 | 0 | 0 | 0 | 0 | 2025-08-01 09:18:26.476275+00
It means that it provides historical trends of locks usage per lock type.
It can be used for example for:
1. checking if "waits" is close to "requests". Then it means you usually have to
wait before acquiring the lock, which means you may have a concurrency issue.
2. lock_timeout and deadlock_timeout tuning (lock_timeout is visible only in the
logs if log_min_error_statement is set appropriately).
3. checking the "requests"/"fastpath" ratio to see if "max_locks_per_transaction"
needs tuning (see c4d5cb71d2).
If any points need more details, it might be a good idea to start sampling pg_locks.
The patch is made of 2 sub-patches:
0001 - It adds a new stat kind PGSTAT_KIND_LOCK for the lock statistics.
This new statistic kind is a fixed one because its key is the lock type
so that we know its size is LOCKTAG_LAST_TYPE + 1.
This statistic kind records the following counters:
- requests: Number of requests for this lock type.
- waits: Number of times requests for this lock type had to wait.
- timeouts: Number of times requests for this lock type had to wait longer than
lock_timeout.
- deadlock_timeouts: Number of times requests for this lock type had to wait longer
than deadlock_timeout.
- deadlocks: Number of times a deadlock occurred on this lock type.
- fastpath: Number of times this lock type was taken via fast path.
No extra details is added (like the ones, i.e relation oid, database oid, we
can find in pg_locks). The idea is to provide an idea on what the locking
behaviour looks like.
Those new counters are incremented outside of the wait events code path,
as suggested in [1].
There are no major design choices, it relies on the current statistics machinery.
0002 - It adds the pg_stat_lock view
It also adds documentation and some tests.
Remarks:
- maybe we could add some metrics related to the lock duration (we have some hints
thanks to the timeout ounters though)
- if this is merged, a next step could be to record those metrics per backend
[1]: https://www.postgresql.org/message-id/CA%2BTgmobptuUWo7X5zcQrWKh22qeAn4eL%2B%3Dwtb8_ajCOR%2B7_tcw%40...
Looking forward to your feedback,
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
Attachments:
[text/x-diff] v1-0001-Add-lock-statistics.patch (16.1K, ../../aIyNxBWFCybgBZBS@ip-10-97-1-34.eu-west-3.compute.internal/2-v1-0001-Add-lock-statistics.patch)
download | inline diff:
From 18a9a9ca8fefa25c1afcf676765576346c3e7fe7 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Tue, 29 Jul 2025 08:36:35 +0000
Subject: [PATCH v1 1/2] Add lock statistics
Adding a new stat kind PGSTAT_KIND_LOCK for the lock statistics.
This new statistic kind is a fixed one because its key is the lock type
so that we know its size is LOCKTAG_LAST_TYPE + 1.
This statistic kind records the following counters:
requests
waits
timeouts
deadlock_timeouts
deadlocks
fastpath
No extra details is added (like the ones, i.e relation oid, database oid, we
can find in pg_locks). The idea is to provide an idea on what the locking
behaviour looks like.
XXX: Bump stat file format
---
src/backend/storage/lmgr/lock.c | 18 +++
src/backend/storage/lmgr/proc.c | 3 +
src/backend/tcop/postgres.c | 8 ++
src/backend/utils/activity/Makefile | 1 +
src/backend/utils/activity/meson.build | 1 +
src/backend/utils/activity/pgstat.c | 18 +++
src/backend/utils/activity/pgstat_lock.c | 168 +++++++++++++++++++++++
src/include/pgstat.h | 34 +++++
src/include/utils/pgstat_internal.h | 21 +++
src/include/utils/pgstat_kind.h | 5 +-
src/tools/pgindent/typedefs.list | 4 +
11 files changed, 279 insertions(+), 2 deletions(-)
13.3% src/backend/storage/lmgr/
62.5% src/backend/utils/activity/
8.3% src/include/utils/
12.1% src/include/
3.6% src/
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index 62f3471448e..e42919d132b 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -45,6 +45,7 @@
#include "storage/spin.h"
#include "storage/standby.h"
#include "utils/memutils.h"
+#include "utils/pgstat_internal.h"
#include "utils/ps_status.h"
#include "utils/resowner.h"
@@ -870,6 +871,9 @@ LockAcquireExtended(const LOCKTAG *locktag,
lockMethodTable->lockModeNames[lockmode]),
errhint("Only RowExclusiveLock or less can be acquired on database objects during recovery.")));
+ /* Increment the lock statistics requests counter */
+ pgstat_count_lock_requests(locktag->locktag_type);
+
#ifdef LOCK_DEBUG
if (LOCK_DEBUG_ENABLED(locktag))
elog(LOG, "LockAcquire: lock [%u,%u] %s",
@@ -1201,6 +1205,8 @@ LockAcquireExtended(const LOCKTAG *locktag,
}
else
{
+ /* Increment the lock statistics deadlocks counter */
+ pgstat_count_lock_deadlocks(locallock->tag.lock.locktag_type);
DeadLockReport();
/* DeadLockReport() will not return */
}
@@ -1212,6 +1218,8 @@ LockAcquireExtended(const LOCKTAG *locktag,
*/
if (waitResult == PROC_WAIT_STATUS_WAITING)
{
+ /* Increment the lock statistics waits counter */
+ pgstat_count_lock_waits(locktag->locktag_type);
Assert(!dontWait);
PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock);
LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode);
@@ -1232,6 +1240,8 @@ LockAcquireExtended(const LOCKTAG *locktag,
* now.
*/
Assert(!dontWait);
+ /* Increment the lock statistics deadlocks counter */
+ pgstat_count_lock_deadlocks(locallock->tag.lock.locktag_type);
DeadLockReport();
/* DeadLockReport() will not return */
}
@@ -2767,6 +2777,8 @@ FastPathGrantRelationLock(Oid relid, LOCKMODE lockmode)
{
Assert(!FAST_PATH_CHECK_LOCKMODE(MyProc, f, lockmode));
FAST_PATH_SET_LOCKMODE(MyProc, f, lockmode);
+ /* Increment the lock statistics fastpath counter */
+ pgstat_count_lock_fastpath(LOCKTAG_RELATION);
return true;
}
}
@@ -2776,6 +2788,8 @@ FastPathGrantRelationLock(Oid relid, LOCKMODE lockmode)
{
MyProc->fpRelId[unused_slot] = relid;
FAST_PATH_SET_LOCKMODE(MyProc, unused_slot, lockmode);
+ /* Increment the lock statistics fastpath counter */
+ pgstat_count_lock_fastpath(LOCKTAG_RELATION);
++FastPathLocalUseCounts[group];
return true;
}
@@ -4600,6 +4614,10 @@ VirtualXactLockTableInsert(VirtualTransactionId vxid)
MyProc->fpVXIDLock = true;
MyProc->fpLocalTransactionId = vxid.localTransactionId;
+ /* Increment the lock statistics requests and fastpath counters */
+ pgstat_count_lock_requests(LOCKTAG_VIRTUALTRANSACTION);
+ pgstat_count_lock_fastpath(LOCKTAG_VIRTUALTRANSACTION);
+
LWLockRelease(&MyProc->fpInfoLock);
}
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index e9ef0fbfe32..dd930fb9bd4 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -1456,9 +1456,12 @@ ProcSleep(LOCALLOCK *locallock)
/* check for deadlocks first, as that's probably log-worthy */
if (got_deadlock_timeout)
{
+ /* Increment the lock statistics deadlock_timeouts counter */
+ pgstat_count_lock_deadlock_timeouts(locallock->tag.lock.locktag_type);
CheckDeadLock();
got_deadlock_timeout = false;
}
+
CHECK_FOR_INTERRUPTS();
}
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 0cecd464902..df02719ad63 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3433,6 +3433,14 @@ ProcessInterrupts(void)
if (lock_timeout_occurred)
{
+ LOCALLOCK *lockAwaited;
+
+ lockAwaited = GetAwaitedLock();
+
+ /* Increment the lock statistics timeouts counter */
+ if (lockAwaited)
+ pgstat_count_lock_timeouts(lockAwaited->tag.lock.locktag_type);
+
LockErrorCleanup();
ereport(ERROR,
(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
diff --git a/src/backend/utils/activity/Makefile b/src/backend/utils/activity/Makefile
index 9c2443e1ecd..4293d0f9efd 100644
--- a/src/backend/utils/activity/Makefile
+++ b/src/backend/utils/activity/Makefile
@@ -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 \
diff --git a/src/backend/utils/activity/meson.build b/src/backend/utils/activity/meson.build
index d8e56b49c24..3d3d7b67b5f 100644
--- a/src/backend/utils/activity/meson.build
+++ b/src/backend/utils/activity/meson.build
@@ -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',
diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index 6bc91ce0dad..d78dc5e3d46 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -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
@@ -446,6 +447,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
index 00000000000..fb4ed5c7362
--- /dev/null
+++ b/src/backend/utils/activity/pgstat_lock.c
@@ -0,0 +1,168 @@
+/* -------------------------------------------------------------------------
+ *
+ * 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-2025, 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;
+
+/*
+ * 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(requests);
+ LOCKSTAT_ACC(waits);
+ LOCKSTAT_ACC(timeouts);
+ LOCKSTAT_ACC(deadlock_timeouts);
+ LOCKSTAT_ACC(deadlocks);
+ LOCKSTAT_ACC(fastpath);
+#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);
+ }
+}
+
+#define PGSTAT_COUNT_LOCK_FUNC(stat) \
+void \
+CppConcat(pgstat_count_lock_,stat)(uint8 locktag_type) \
+{ \
+ Assert(locktag_type <= LOCKTAG_LAST_TYPE); \
+ PendingLockStats.stats[locktag_type].stat++; \
+ have_lockstats = true; \
+ pgstat_report_fixed = true; \
+}
+
+/* pgstat_count_lock_requests */
+PGSTAT_COUNT_LOCK_FUNC(requests)
+
+/* pgstat_count_lock_waits */
+PGSTAT_COUNT_LOCK_FUNC(waits)
+
+/* pgstat_count_lock_timeouts */
+PGSTAT_COUNT_LOCK_FUNC(timeouts)
+
+/* pgstat_count_lock_deadlock_timeouts */
+PGSTAT_COUNT_LOCK_FUNC(deadlock_timeouts)
+
+/* pgstat_count_lock_deadlocks */
+PGSTAT_COUNT_LOCK_FUNC(deadlocks)
+
+/* pgstat_count_lock_fastpath */
+PGSTAT_COUNT_LOCK_FUNC(fastpath)
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 202bd2d5ace..7893f7311ae 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/lock.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"
@@ -339,6 +340,27 @@ typedef struct PgStat_IO
PgStat_BktypeIO stats[BACKEND_NUM_TYPES];
} PgStat_IO;
+typedef struct PgStat_LockEntry
+{
+ PgStat_Counter requests;
+ PgStat_Counter waits;
+ PgStat_Counter timeouts;
+ PgStat_Counter deadlock_timeouts;
+ PgStat_Counter deadlocks;
+ PgStat_Counter fastpath;
+} 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;
@@ -603,6 +625,18 @@ 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_requests(uint8 locktag_type);
+extern void pgstat_count_lock_waits(uint8 locktag_type);
+extern void pgstat_count_lock_timeouts(uint8 locktag_type);
+extern void pgstat_count_lock_deadlock_timeouts(uint8 locktag_type);
+extern void pgstat_count_lock_deadlocks(uint8 locktag_type);
+extern void pgstat_count_lock_fastpath(uint8 locktag_type);
+
/*
* Functions in pgstat_database.c
*/
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 6cf00008f63..182feec1235 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -396,6 +396,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 */
@@ -492,6 +502,7 @@ typedef struct PgStat_ShmemControl
PgStatShared_BgWriter bgwriter;
PgStatShared_Checkpointer checkpointer;
PgStatShared_IO io;
+ PgStatShared_Lock lock;
PgStatShared_SLRU slru;
PgStatShared_Wal wal;
@@ -524,6 +535,8 @@ typedef struct PgStat_Snapshot
PgStat_IO io;
+ PgStat_Lock lock;
+
PgStat_SLRUStats slru[SLRU_NUM_ELEMENTS];
PgStat_WalStats wal;
@@ -673,6 +686,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
diff --git a/src/include/utils/pgstat_kind.h b/src/include/utils/pgstat_kind.h
index eb5f0b3ae6d..f8e95f55533 100644
--- a/src/include/utils/pgstat_kind.h
+++ b/src/include/utils/pgstat_kind.h
@@ -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/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index e6f2e93b2d6..6456d53b13f 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2218,6 +2218,7 @@ PgStatShared_HashEntry
PgStatShared_IO
PgStatShared_InjectionPoint
PgStatShared_InjectionPointFixed
+PgStatShared_Lock
PgStatShared_Relation
PgStatShared_ReplSlot
PgStatShared_SLRU
@@ -2240,8 +2241,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
--
2.34.1
[text/x-diff] v1-0002-Add-the-pg_stat_lock-view.patch (17.1K, ../../aIyNxBWFCybgBZBS@ip-10-97-1-34.eu-west-3.compute.internal/3-v1-0002-Add-the-pg_stat_lock-view.patch)
download | inline diff:
From 6949485ab3951b6a6497f3e312b6d16ec8983210 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Thu, 31 Jul 2025 09:35:31 +0000
Subject: [PATCH v1 2/2] Add the pg_stat_lock view
This new view reports lock statistics.
This commit also adds documentation and a few tests.
XXX: Bump catversion
---
doc/src/sgml/monitoring.sgml | 151 ++++++++++++++++++
src/backend/catalog/system_views.sql | 12 ++
src/backend/utils/activity/pgstat_lock.c | 8 +
src/backend/utils/adt/pgstatfuncs.c | 39 +++++
src/include/catalog/pg_proc.dat | 9 ++
src/include/pgstat.h | 1 +
src/test/isolation/expected/deadlock-hard.out | 20 ++-
src/test/isolation/specs/deadlock-hard.spec | 5 +-
src/test/regress/expected/advisory_lock.out | 18 +++
src/test/regress/expected/rules.out | 9 ++
src/test/regress/sql/advisory_lock.sql | 4 +
11 files changed, 274 insertions(+), 2 deletions(-)
51.5% doc/src/sgml/
3.3% src/backend/catalog/
14.3% src/backend/utils/adt/
5.7% src/include/catalog/
8.4% src/test/isolation/expected/
5.9% src/test/isolation/specs/
6.7% src/test/regress/expected/
3.8% src/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 823afe1b30b..0d18124f239 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -493,6 +493,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
@@ -3027,6 +3036,142 @@ 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:
+ <literal>relation</literal>,
+ <literal>extend</literal>,
+ <literal>frozenid</literal>,
+ <literal>page</literal>,
+ <literal>tuple</literal>,
+ <literal>transactionid</literal>,
+ <literal>virtualxid</literal>,
+ <literal>spectoken</literal>,
+ <literal>object</literal>,
+ <literal>userlock</literal>,
+ <literal>advisory</literal>, or
+ <literal>applytransaction</literal>.
+ (See also <xref linkend="wait-event-lock-table"/>.)
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>requests</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of requests for this lock type.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>waits</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of times requests for this lock type had to wait.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>timeouts</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of times requests for this lock type had to wait longer
+ than <varname>lock_timeout</varname>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>deadlock_timeouts</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of times requests for this lock type had to wait longer
+ than <varname>deadlock_timeout</varname>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>deadlocks</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of times a deadlock occurred on this lock type.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>fastpath</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of times this lock type was taken via fast path.
+ </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>
@@ -5044,6 +5189,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
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index f6eca09ee15..d493a00c544 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -950,6 +950,18 @@ 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.requests,
+ l.waits,
+ l.timeouts,
+ l.deadlock_timeouts,
+ l.deadlocks,
+ l.fastpath,
+ l.stats_reset
+ FROM pg_stat_get_lock() l;
+
CREATE VIEW pg_stat_wal_receiver AS
SELECT
s.pid,
diff --git a/src/backend/utils/activity/pgstat_lock.c b/src/backend/utils/activity/pgstat_lock.c
index fb4ed5c7362..73bdeb5ff66 100644
--- a/src/backend/utils/activity/pgstat_lock.c
+++ b/src/backend/utils/activity/pgstat_lock.c
@@ -21,6 +21,14 @@
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()
*/
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 1c12ddbae49..5e63f2d2a38 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1696,6 +1696,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 8
+ 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];
+
+ locktypename = LockTagTypeNames[lcktype];
+
+ values[0] = CStringGetTextDatum(locktypename);
+ values[1] = Int64GetDatum(lck_stats->requests);
+ values[2] = Int64GetDatum(lck_stats->waits);
+ values[3] = Int64GetDatum(lck_stats->timeouts);
+ values[4] = Int64GetDatum(lck_stats->deadlock_timeouts);
+ values[5] = Int64GetDatum(lck_stats->deadlocks);
+ values[6] = Int64GetDatum(lck_stats->fastpath);
+ values[7] = TimestampTzGetDatum(lock_stats->stat_reset_timestamp);
+
+ tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+ }
+
+ return (Datum) 0;
+}
+
/*
* Returns statistics of SLRU caches.
*/
@@ -1880,6 +1916,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);
@@ -1897,6 +1934,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)
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 3ee8fed7e53..c5faff30317 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6001,6 +6001,15 @@
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,int8,int8,int8,timestamptz}',
+ proargmodes => '{o,o,o,o,o,o,o,o}',
+ proargnames => '{locktype,requests,waits,timeouts,deadlock_timeouts,deadlocks,fastpath,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',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 7893f7311ae..e653bce4aab 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -636,6 +636,7 @@ extern void pgstat_count_lock_timeouts(uint8 locktag_type);
extern void pgstat_count_lock_deadlock_timeouts(uint8 locktag_type);
extern void pgstat_count_lock_deadlocks(uint8 locktag_type);
extern void pgstat_count_lock_fastpath(uint8 locktag_type);
+extern PgStat_Lock *pgstat_fetch_stat_lock(void);
/*
* Functions in pgstat_database.c
diff --git a/src/test/isolation/expected/deadlock-hard.out b/src/test/isolation/expected/deadlock-hard.out
index 460653f2b86..ff448da5299 100644
--- a/src/test/isolation/expected/deadlock-hard.out
+++ b/src/test/isolation/expected/deadlock-hard.out
@@ -1,6 +1,12 @@
Parsed test spec with 8 sessions
-starting permutation: s1a1 s2a2 s3a3 s4a4 s5a5 s6a6 s7a7 s8a8 s1a2 s2a3 s3a4 s4a5 s5a6 s6a7 s7a8 s8a1 s8c s7c s6c s5c s4c s3c s2c s1c
+starting permutation: s1rl s1a1 s2a2 s3a3 s4a4 s5a5 s6a6 s7a7 s8a8 s1a2 s2a3 s3a4 s4a5 s5a6 s6a7 s7a8 s8a1 s8c s8f s7c s6c s5c s4c s3c s2c s1c s1sl
+step s1rl: SELECT pg_stat_reset_shared('lock');
+pg_stat_reset_shared
+--------------------
+
+(1 row)
+
step s1a1: LOCK TABLE a1;
step s2a2: LOCK TABLE a2;
step s3a3: LOCK TABLE a3;
@@ -21,6 +27,12 @@ step s8a1: <... completed>
ERROR: deadlock detected
step s7a8: <... completed>
step s8c: COMMIT;
+step s8f: SELECT pg_stat_force_next_flush();
+pg_stat_force_next_flush
+------------------------
+
+(1 row)
+
step s7c: COMMIT;
step s6a7: <... completed>
step s6c: COMMIT;
@@ -34,3 +46,9 @@ step s2a3: <... completed>
step s2c: COMMIT;
step s1a2: <... completed>
step s1c: COMMIT;
+step s1sl: SELECT deadlocks > 0, deadlock_timeouts > 0 FROM pg_stat_lock WHERE locktype = 'relation';
+?column?|?column?
+--------+--------
+t |t
+(1 row)
+
diff --git a/src/test/isolation/specs/deadlock-hard.spec b/src/test/isolation/specs/deadlock-hard.spec
index 60bedca237a..6e8330662b4 100644
--- a/src/test/isolation/specs/deadlock-hard.spec
+++ b/src/test/isolation/specs/deadlock-hard.spec
@@ -25,6 +25,8 @@ setup { BEGIN; SET deadlock_timeout = '100s'; }
step s1a1 { LOCK TABLE a1; }
step s1a2 { LOCK TABLE a2; }
step s1c { COMMIT; }
+step s1sl { SELECT deadlocks > 0, deadlock_timeouts > 0 FROM pg_stat_lock WHERE locktype = 'relation'; }
+step s1rl { SELECT pg_stat_reset_shared('lock'); }
session s2
setup { BEGIN; SET deadlock_timeout = '100s'; }
@@ -67,6 +69,7 @@ setup { BEGIN; SET deadlock_timeout = '10ms'; }
step s8a8 { LOCK TABLE a8; }
step s8a1 { LOCK TABLE a1; }
step s8c { COMMIT; }
+step s8f { SELECT pg_stat_force_next_flush(); }
# Note: when s8a1 detects the deadlock and fails, s7a8 is released, making
# it timing-dependent which query completion is received first by the tester.
@@ -76,4 +79,4 @@ step s8c { COMMIT; }
# dummy blocking mark to s8a1 to ensure it will be reported as "waiting"
# regardless of that.
-permutation s1a1 s2a2 s3a3 s4a4 s5a5 s6a6 s7a7 s8a8 s1a2 s2a3 s3a4 s4a5 s5a6 s6a7 s7a8(s8a1) s8a1(*) s8c s7c s6c s5c s4c s3c s2c s1c
+permutation s1rl s1a1 s2a2 s3a3 s4a4 s5a5 s6a6 s7a7 s8a8 s1a2 s2a3 s3a4 s4a5 s5a6 s6a7 s7a8(s8a1) s8a1(*) s8c s8f s7c s6c s5c s4c s3c s2c s1c s1sl
diff --git a/src/test/regress/expected/advisory_lock.out b/src/test/regress/expected/advisory_lock.out
index 02e07765ac2..fdaa1756ba4 100644
--- a/src/test/regress/expected/advisory_lock.out
+++ b/src/test/regress/expected/advisory_lock.out
@@ -2,6 +2,12 @@
-- ADVISORY LOCKS
--
SELECT oid AS datoid FROM pg_database WHERE datname = current_database() \gset
+SELECT pg_stat_reset_shared('lock');
+ pg_stat_reset_shared
+----------------------
+
+(1 row)
+
BEGIN;
SELECT
pg_advisory_xact_lock(1), pg_advisory_xact_lock_shared(2),
@@ -48,6 +54,12 @@ WARNING: you don't own a lock of type ShareLock
f | f | f | f
(1 row)
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
-- automatically release xact locks at commit
COMMIT;
SELECT count(*) FROM pg_locks WHERE locktype = 'advisory' AND database = :datoid;
@@ -56,6 +68,12 @@ SELECT count(*) FROM pg_locks WHERE locktype = 'advisory' AND database = :datoid
0
(1 row)
+SELECT requests FROM pg_stat_lock WHERE locktype = 'advisory';
+ requests
+----------
+ 4
+(1 row)
+
BEGIN;
-- holding both session and xact locks on the same objects, xact first
SELECT
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index dce8c672b40..c790164ea82 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1938,6 +1938,15 @@ 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,
+ requests,
+ waits,
+ timeouts,
+ deadlock_timeouts,
+ deadlocks,
+ fastpath,
+ stats_reset
+ FROM pg_stat_get_lock() l(locktype, requests, waits, timeouts, deadlock_timeouts, deadlocks, fastpath, stats_reset);
pg_stat_progress_analyze| SELECT s.pid,
s.datid,
d.datname,
diff --git a/src/test/regress/sql/advisory_lock.sql b/src/test/regress/sql/advisory_lock.sql
index 8513ab8e98f..f1bff60fd37 100644
--- a/src/test/regress/sql/advisory_lock.sql
+++ b/src/test/regress/sql/advisory_lock.sql
@@ -4,6 +4,8 @@
SELECT oid AS datoid FROM pg_database WHERE datname = current_database() \gset
+SELECT pg_stat_reset_shared('lock');
+
BEGIN;
SELECT
@@ -26,12 +28,14 @@ SELECT
pg_advisory_unlock(1), pg_advisory_unlock_shared(2),
pg_advisory_unlock(1, 1), pg_advisory_unlock_shared(2, 2);
+SELECT pg_stat_force_next_flush();
-- automatically release xact locks at commit
COMMIT;
SELECT count(*) FROM pg_locks WHERE locktype = 'advisory' AND database = :datoid;
+SELECT requests FROM pg_stat_lock WHERE locktype = 'advisory';
BEGIN;
--
2.34.1
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2025-08-11 17:54 Greg Sabino Mullane <htamfids@gmail.com>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
0 siblings, 2 replies; 61+ messages in thread
From: Greg Sabino Mullane @ 2025-08-11 17:54 UTC (permalink / raw)
To: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org
Great idea. +1. Here is a quick overall review to get things started.
Meta:
patch did not apply via "git apply". Also has carriage returns (e.g. DOS
file), and some errant whitespace. Seems to pass pgindent, though.
Name:
I think the name would read better as pg_stat_locks, especially as it
returns multiple rows.
Docs: seem good. Needs a section on how to reset via
SELECT pg_stat_reset_shared('lock');
Also plural better here ('locks')
Code:
+ * Copyright (c) 2021-2025, PostgreSQL Global Development Group
If a new file, we can simply say 2025?
+ LWLock locks[LOCKTAG_LAST_TYPE + 1];
+ PgStat_Lock stats;
Adding a lock to the system that counts locks! :) (just found amusing, not
a comment)
-#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
Why not just add LOCK as #13?
What about the overhead of maintaining this system? I know it's overall
very lightweight, but from my testing, the relation locktype in particular
is very, very busy. Busier than I realized until I saw it in this useful
view!
Cheers,
Greg
--
Crunchy Data - https://www.crunchydata.com
Enterprise Postgres Software Products & Tech Support
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2025-08-11 21:53 Jeff Davis <pgsql@j-davis.com>
parent: Greg Sabino Mullane <htamfids@gmail.com>
1 sibling, 1 reply; 61+ messages in thread
From: Jeff Davis @ 2025-08-11 21:53 UTC (permalink / raw)
To: Greg Sabino Mullane <htamfids@gmail.com>; Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org
On Mon, 2025-08-11 at 13:54 -0400, Greg Sabino Mullane wrote:
> Great idea. +1. Here is a quick overall review to get things started.
Can you describe your use case? I'd like to understand whether this is
useful for users, hackers, or both.
Regards,
Jeff Davis
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2025-08-11 23:44 Michael Paquier <michael@paquier.xyz>
parent: Jeff Davis <pgsql@j-davis.com>
0 siblings, 2 replies; 61+ messages in thread
From: Michael Paquier @ 2025-08-11 23:44 UTC (permalink / raw)
To: Jeff Davis <pgsql@j-davis.com>; +Cc: Greg Sabino Mullane <htamfids@gmail.com>; Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; pgsql-hackers@lists.postgresql.org
On Mon, Aug 11, 2025 at 02:53:58PM -0700, Jeff Davis wrote:
> On Mon, 2025-08-11 at 13:54 -0400, Greg Sabino Mullane wrote:
> > Great idea. +1. Here is a quick overall review to get things started.
>
> Can you describe your use case? I'd like to understand whether this is
> useful for users, hackers, or both.
This is a DBA feature, so the questions I'd ask myself are basically:
- Is there any decision-making where these numbers would help? These
decisions would shape in tweaking the configuration of the server or
the application to as we move from a "bad" number trend to a "good"
number trend.
- What would be good numbers? In this case, most likely a threshold
reached over a certain period of time.
- Would these new stats overlap with similar statistics gathered in
the system, creating duplication and bloat in the pgstats for no real
gain?
Looking at the patch, the data gathered is this, and I don't think
that we have metrics gathered in the system to get an idea of the
contention in this area, for timeouts and deadlocks:
+ PgStat_Counter requests;
+ PgStat_Counter waits;
+ PgStat_Counter timeouts;
+ PgStat_Counter deadlock_timeouts;
+ PgStat_Counter deadlocks;
+ PgStat_Counter fastpath;
Isn't the "deadlock_timeout" one an overlap between timeout and
deadlock? Having three counters to track two concepts seems strange
to me.
Regarding the fastpath locking, you'd want to optimize the fastpath to
be close to the number of requests. If you had these numbers, one
thing that I could see a DBA do is tune max_locks_per_transaction,
which is what influences the per-backend limit of fast-path locks,
with FastPathLockGroupsPerBackend.
Using static counters for the pending data is going to be necessary
when these are called in critical sections, which is I guess why
you've done it this way, right?
--
Michael
Attachments:
[application/pgp-signature] signature.asc (832B, ../../aJqAei83uRWV93bM@paquier.xyz/2-signature.asc)
download
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2025-08-11 23:49 Tom Lane <tgl@sss.pgh.pa.us>
parent: Michael Paquier <michael@paquier.xyz>
1 sibling, 1 reply; 61+ messages in thread
From: Tom Lane @ 2025-08-11 23:49 UTC (permalink / raw)
To: Michael Paquier <michael@paquier.xyz>; +Cc: Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; pgsql-hackers@lists.postgresql.org
Michael Paquier <michael@paquier.xyz> writes:
> On Mon, Aug 11, 2025 at 02:53:58PM -0700, Jeff Davis wrote:
>> Can you describe your use case? I'd like to understand whether this is
>> useful for users, hackers, or both.
> This is a DBA feature, so the questions I'd ask myself are basically:
> - Is there any decision-making where these numbers would help? These
> decisions would shape in tweaking the configuration of the server or
> the application to as we move from a "bad" number trend to a "good"
> number trend.
> - What would be good numbers? In this case, most likely a threshold
> reached over a certain period of time.
> - Would these new stats overlap with similar statistics gathered in
> the system, creating duplication and bloat in the pgstats for no real
> gain?
I'm also wondering why slicing the numbers in this particular way
(i.e., aggregating by locktype) is a helpful way to look at the data.
Maybe it's just what you want, but that's not obvious to me.
regards, tom lane
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2025-08-12 09:32 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Greg Sabino Mullane <htamfids@gmail.com>
1 sibling, 1 reply; 61+ messages in thread
From: Bertrand Drouvot @ 2025-08-12 09:32 UTC (permalink / raw)
To: Greg Sabino Mullane <htamfids@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org
Hi,
On Mon, Aug 11, 2025 at 01:54:38PM -0400, Greg Sabino Mullane wrote:
> Great idea. +1. Here is a quick overall review to get things started.
Thanks for looking at it!
> Meta:
> patch did not apply via "git apply". Also has carriage returns (e.g. DOS
> file), and some errant whitespace.
Interesting, I'm not seeing those issues on my side when applying with "git am".
> Name:
> I think the name would read better as pg_stat_locks, especially as it
> returns multiple rows.
I considered pg_stat_locks, but chose the singular form to be consistent with
pg_stat_database, pg_stat_subscription, and friends.
> Docs: seem good. Needs a section on how to reset via
> SELECT pg_stat_reset_shared('lock');
That's already included in v1:
"
@@ -5055,6 +5200,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>
"
Do you think that this needs any additions or changes?
> Also plural better here ('locks')
I think having pg_stat_<XXX> and XXX being the same option in " pg_stat_reset_shared()"
makes sense. It's how it has been done so far, that's why I went with 'lock'.
> Code:
>
> + * Copyright (c) 2021-2025, PostgreSQL Global Development Group
>
> If a new file, we can simply say 2025?
I'm not sure that matters that much, see [1] and we can look at some files added
in 2025 for examples:
src/backend/storage/aio/aio_io.c
src/backend/access/nbtree/nbtpreprocesskeys.c
where 2025 is not "alone".
> + LWLock locks[LOCKTAG_LAST_TYPE + 1];
> + PgStat_Lock stats;
>
> Adding a lock to the system that counts locks! :) (just found amusing, not
> a comment)
;-)
> -#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
>
> Why not just add LOCK as #13?
Because it looks like that they are ordered by alphabetical order.
> What about the overhead of maintaining this system? I know it's overall
> very lightweight, but from my testing, the relation locktype in particular
> is very, very busy.
The counter increments do call a function generated that way:
"
#define PGSTAT_COUNT_LOCK_FUNC(stat) \
void \
CppConcat(pgstat_count_lock_,stat)(uint8 locktag_type) \
{ \
Assert(locktag_type <= LOCKTAG_LAST_TYPE); \
PendingLockStats.stats[locktag_type].stat++; \
have_lockstats = true; \
pgstat_report_fixed = true; \
}
"
So, it's pretty lightweight as you said (given that PendingLockStats is
not shared and just local to the backend that increments the counter).
There could be some contention when the pending stats are flushed but that's
the same for all the stats kinds.
We could do better, though, and avoid the function calls by creating macros instead
of functions. That would mean PendingLockStats to be visible to the outside
world but:
- that's not the direction that has been taken recently (for example, see the
"kept here to avoid exposing PendingBackendStats to the outside world" comment in
pgstat_backend.c).
- I'm not sure that's worth for this particular case and code paths.
That said, if you (and/or others) have strong concerns about performance penalties,
I could study this area more in depth.
> Busier than I realized until I saw it in this useful
> view!
Thanks! Did you observe some noticeable performance penalties?
[1]: https://www.postgresql.org/message-id/202501211750.xf5j6thdkppy%40alvherre.pgsql
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2025-08-12 09:37 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Michael Paquier <michael@paquier.xyz>
1 sibling, 1 reply; 61+ messages in thread
From: Bertrand Drouvot @ 2025-08-12 09:37 UTC (permalink / raw)
To: Michael Paquier <michael@paquier.xyz>; +Cc: Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi,
On Tue, Aug 12, 2025 at 08:44:58AM +0900, Michael Paquier wrote:
> On Mon, Aug 11, 2025 at 02:53:58PM -0700, Jeff Davis wrote:
> > On Mon, 2025-08-11 at 13:54 -0400, Greg Sabino Mullane wrote:
> > > Great idea. +1. Here is a quick overall review to get things started.
> >
> > Can you describe your use case? I'd like to understand whether this is
> > useful for users, hackers, or both.
Thanks for looking at it!
I provided a few examples in the initial email ([1]):
"
It can be used for example for:
1. checking if "waits" is close to "requests". Then it means you usually have to
wait before acquiring the lock, which means you may have a concurrency issue.
2. lock_timeout and deadlock_timeout tuning (lock_timeout is visible only in the
logs if log_min_error_statement is set appropriately).
3. checking the "requests"/"fastpath" ratio to see if "max_locks_per_transaction"
needs tuning (see c4d5cb71d2).
"
Do these seem like useful use cases?
> - Is there any decision-making where these numbers would help? These
> decisions would shape in tweaking the configuration of the server or
> the application to as we move from a "bad" number trend to a "good"
> number trend.
Right, I think that could help for lock_timeout and deadlock_timeout tuning.
> - What would be good numbers? In this case, most likely a threshold
> reached over a certain period of time.
Also I think it's more a matter of ratio: waits/requests and requests/fastpath
for example.
> - Would these new stats overlap with similar statistics gathered in
> the system, creating duplication and bloat in the pgstats for no real
> gain?
I don't think there is currently stats that overlap with those.
> Looking at the patch, the data gathered is this, and I don't think
> that we have metrics gathered in the system to get an idea of the
> contention in this area, for timeouts and deadlocks:
> + PgStat_Counter requests;
> + PgStat_Counter waits;
> + PgStat_Counter timeouts;
> + PgStat_Counter deadlock_timeouts;
> + PgStat_Counter deadlocks;
> + PgStat_Counter fastpath;
>
> Isn't the "deadlock_timeout" one an overlap between timeout and
> deadlock?
I don't think so because:
- deadlock_timeout and lock_timeout are 2 distincts GUCs
- you could reach the deadlock_timeout without actually producing a deadlock
> Regarding the fastpath locking, you'd want to optimize the fastpath to
> be close to the number of requests. If you had these numbers, one
> thing that I could see a DBA do is tune max_locks_per_transaction,
> which is what influences the per-backend limit of fast-path locks,
> with FastPathLockGroupsPerBackend.
Exactly.
> Using static counters for the pending data is going to be necessary
> when these are called in critical sections, which is I guess why
> you've done it this way, right?
Yes and there is no need to over complicate this stuff as the max size is
known: LOCKTAG_LAST_TYPE + 1.
[1]: https://www.postgresql.org/message-id/aIyNxBWFCybgBZBS%40ip-10-97-1-34.eu-west-3.compute.internal
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2025-08-12 09:49 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Tom Lane <tgl@sss.pgh.pa.us>
0 siblings, 0 replies; 61+ messages in thread
From: Bertrand Drouvot @ 2025-08-12 09:49 UTC (permalink / raw)
To: Tom Lane <tgl@sss.pgh.pa.us>; +Cc: Michael Paquier <michael@paquier.xyz>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi,
On Mon, Aug 11, 2025 at 07:49:45PM -0400, Tom Lane wrote:
> Michael Paquier <michael@paquier.xyz> writes:
> > On Mon, Aug 11, 2025 at 02:53:58PM -0700, Jeff Davis wrote:
> >> Can you describe your use case? I'd like to understand whether this is
> >> useful for users, hackers, or both.
>
> > This is a DBA feature, so the questions I'd ask myself are basically:
> > - Is there any decision-making where these numbers would help? These
> > decisions would shape in tweaking the configuration of the server or
> > the application to as we move from a "bad" number trend to a "good"
> > number trend.
> > - What would be good numbers? In this case, most likely a threshold
> > reached over a certain period of time.
> > - Would these new stats overlap with similar statistics gathered in
> > the system, creating duplication and bloat in the pgstats for no real
> > gain?
>
> I'm also wondering why slicing the numbers in this particular way
> (i.e., aggregating by locktype) is a helpful way to look at the data.
> Maybe it's just what you want, but that's not obvious to me.
Thanks for providing your thoughts!
I thought it was more natural to aggregate by locktype because:
- I think that matches how they are categorized in the doc (from a "wait event"
point of view i.e "Wait Events of Type Lock").
- It provides a natural drill-down path, spot issues by locktype in the stats and
then query pg_locks for specific objects when needed.
Does that make sense to you?
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2025-08-12 12:54 Greg Sabino Mullane <htamfids@gmail.com>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
0 siblings, 1 reply; 61+ messages in thread
From: Greg Sabino Mullane @ 2025-08-12 12:54 UTC (permalink / raw)
To: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org
On Tue, Aug 12, 2025 at 5:32 AM Bertrand Drouvot <
bertranddrouvot.pg@gmail.com> wrote:
> I considered pg_stat_locks, but chose the singular form to be consistent
> with
> pg_stat_database, pg_stat_subscription, and friends.
>
Counter-examples: pg_stat_statements, pg_stat_subscription_stats. Our names
are not consistent. :) It just feels a better fit to have a plural name for
a table tracking aggregates of multiple types of objects.
(Ignore my git complaint, was obviously half-asleep when I wrote that).
> Docs: seem good. Needs a section on how to reset via
> > SELECT pg_stat_reset_shared('lock');
>
I meant something closer to the actual description of the view. Having it
buried in the pg_stat_reset_shared section is not intuitive for people
looking up the view in the docs.
> Because it looks like that they are ordered by alphabetical order.
>
That makes sense.
> - I'm not sure that's worth for this particular case and code paths.
>
Will let others opine on that.
Thanks! Did you observe some noticeable performance penalties?
>
No, but I did not give it any particularly heavy testing. More of a idle
thought when pulling up a brand new system and seeing the thousands of
locks for the tiniest bit of database action.
Cheers,
Greg
--
Crunchy Data - https://www.crunchydata.com
Enterprise Postgres Software Products & Tech Support
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2025-08-12 15:42 Jeff Davis <pgsql@j-davis.com>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
0 siblings, 1 reply; 61+ messages in thread
From: Jeff Davis @ 2025-08-12 15:42 UTC (permalink / raw)
To: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; Michael Paquier <michael@paquier.xyz>; +Cc: Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
On Tue, 2025-08-12 at 09:37 +0000, Bertrand Drouvot wrote:
> It can be used for example for:
>
> 1. checking if "waits" is close to "requests". Then it means you
> usually have to
> wait before acquiring the lock, which means you may have a
> concurrency issue.
>
> 2. lock_timeout and deadlock_timeout tuning (lock_timeout is visible
> only in the
> logs if log_min_error_statement is set appropriately).
>
> 3. checking the "requests"/"fastpath" ratio to see if
> "max_locks_per_transaction"
> needs tuning (see c4d5cb71d2).
> "
>
> Do these seem like useful use cases?
Those seem plausibly useful, but I don't recall needing that exact
information myself, and I wanted to hear more from others.
For instance, a view could be helpful to diagnose concurrency issues,
but I think that's worth discussing in more detail to see what kinds of
issues it can help with and how it complements other approaches. I
suspect when we get into the details, different people (or different
situations) would want slightly different information out of that view.
Regards,
Jeff Davis
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2025-08-13 03:20 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Greg Sabino Mullane <htamfids@gmail.com>
0 siblings, 0 replies; 61+ messages in thread
From: Bertrand Drouvot @ 2025-08-13 03:20 UTC (permalink / raw)
To: Greg Sabino Mullane <htamfids@gmail.com>; +Cc: pgsql-hackers@lists.postgresql.org
Hi,
On Tue, Aug 12, 2025 at 08:54:07AM -0400, Greg Sabino Mullane wrote:
> On Tue, Aug 12, 2025 at 5:32 AM Bertrand Drouvot <
> bertranddrouvot.pg@gmail.com> wrote:
>
> > Docs: seem good. Needs a section on how to reset via
> > > SELECT pg_stat_reset_shared('lock');
> >
>
> I meant something closer to the actual description of the view. Having it
> buried in the pg_stat_reset_shared section is not intuitive for people
> looking up the view in the docs.
I agree that we could add this extra information in the view documentation.
But none of pg_stat_archiver, pg_stat_bgwriter, pg_stat_checkpointer, pg_stat_io,
pg_stat_slru and pg_stat_wal have done so. The only exception is pg_stat_recovery_prefetch
and for a good reason as not all of its fields are reset.
While I think that's probably a good idea, I think it's worth a dedicated
thread (so that the discussion takes into account the other views mentioned
above).
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2025-08-13 03:55 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Jeff Davis <pgsql@j-davis.com>
0 siblings, 1 reply; 61+ messages in thread
From: Bertrand Drouvot @ 2025-08-13 03:55 UTC (permalink / raw)
To: Jeff Davis <pgsql@j-davis.com>; +Cc: Michael Paquier <michael@paquier.xyz>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi,
On Tue, Aug 12, 2025 at 08:42:48AM -0700, Jeff Davis wrote:
> On Tue, 2025-08-12 at 09:37 +0000, Bertrand Drouvot wrote:
> > It can be used for example for:
> >
> > 1. checking if "waits" is close to "requests". Then it means you
> > usually have to
> > wait before acquiring the lock, which means you may have a
> > concurrency issue.
> >
> > 2. lock_timeout and deadlock_timeout tuning (lock_timeout is visible
> > only in the
> > logs if log_min_error_statement is set appropriately).
> >
> > 3. checking the "requests"/"fastpath" ratio to see if
> > "max_locks_per_transaction"
> > needs tuning (see c4d5cb71d2).
> > "
> >
> > Do these seem like useful use cases?
>
> Those seem plausibly useful,
Thanks for sharing your thoughts about the use cases above.
> and I wanted to hear more from others.
> For instance, a view could be helpful to diagnose concurrency issues,
> but I think that's worth discussing in more detail to see what kinds of
> issues it can help with and how it complements other approaches. I
> suspect when we get into the details, different people (or different
> situations) would want slightly different information out of that view.
Fully agree, getting input from others definitely helps validate the approach
and see if the current design needs some changes.
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2025-12-15 16:48 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
0 siblings, 1 reply; 61+ messages in thread
From: Bertrand Drouvot @ 2025-12-15 16:48 UTC (permalink / raw)
To: Jeff Davis <pgsql@j-davis.com>; +Cc: Michael Paquier <michael@paquier.xyz>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi,
On Wed, Aug 13, 2025 at 03:55:41AM +0000, Bertrand Drouvot wrote:
> Fully agree, getting input from others definitely helps validate the approach
> and see if the current design needs some changes.
PFA a mandatory rebase.
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
Attachments:
[text/x-diff] v2-0001-Add-lock-statistics.patch (16.1K, ../../aUA7ymkrh%2FUpkJ6O@ip-10-97-1-34.eu-west-3.compute.internal/2-v2-0001-Add-lock-statistics.patch)
download | inline diff:
From 035c0ea87479c5207fc6f04c50276c2195d814c1 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Tue, 29 Jul 2025 08:36:35 +0000
Subject: [PATCH v2 1/2] Add lock statistics
Adding a new stat kind PGSTAT_KIND_LOCK for the lock statistics.
This new statistic kind is a fixed one because its key is the lock type
so that we know its size is LOCKTAG_LAST_TYPE + 1.
This statistic kind records the following counters:
requests
waits
timeouts
deadlock_timeouts
deadlocks
fastpath
No extra details is added (like the ones, i.e relation oid, database oid, we
can find in pg_locks). The idea is to provide an idea on what the locking
behaviour looks like.
XXX: Bump stat file format
---
src/backend/storage/lmgr/lock.c | 18 +++
src/backend/storage/lmgr/proc.c | 3 +
src/backend/tcop/postgres.c | 8 ++
src/backend/utils/activity/Makefile | 1 +
src/backend/utils/activity/meson.build | 1 +
src/backend/utils/activity/pgstat.c | 18 +++
src/backend/utils/activity/pgstat_lock.c | 168 +++++++++++++++++++++++
src/include/pgstat.h | 34 +++++
src/include/utils/pgstat_internal.h | 21 +++
src/include/utils/pgstat_kind.h | 5 +-
src/tools/pgindent/typedefs.list | 4 +
11 files changed, 279 insertions(+), 2 deletions(-)
13.3% src/backend/storage/lmgr/
62.5% src/backend/utils/activity/
8.3% src/include/utils/
12.1% src/include/
3.6% src/
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index 9015ba3caf7..bb766511db3 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -45,6 +45,7 @@
#include "storage/spin.h"
#include "storage/standby.h"
#include "utils/memutils.h"
+#include "utils/pgstat_internal.h"
#include "utils/ps_status.h"
#include "utils/resowner.h"
@@ -871,6 +872,9 @@ LockAcquireExtended(const LOCKTAG *locktag,
lockMethodTable->lockModeNames[lockmode]),
errhint("Only RowExclusiveLock or less can be acquired on database objects during recovery.")));
+ /* Increment the lock statistics requests counter */
+ pgstat_count_lock_requests(locktag->locktag_type);
+
#ifdef LOCK_DEBUG
if (LOCK_DEBUG_ENABLED(locktag))
elog(LOG, "LockAcquire: lock [%u,%u] %s",
@@ -1202,6 +1206,8 @@ LockAcquireExtended(const LOCKTAG *locktag,
}
else
{
+ /* Increment the lock statistics deadlocks counter */
+ pgstat_count_lock_deadlocks(locallock->tag.lock.locktag_type);
DeadLockReport();
/* DeadLockReport() will not return */
}
@@ -1213,6 +1219,8 @@ LockAcquireExtended(const LOCKTAG *locktag,
*/
if (waitResult == PROC_WAIT_STATUS_WAITING)
{
+ /* Increment the lock statistics waits counter */
+ pgstat_count_lock_waits(locktag->locktag_type);
Assert(!dontWait);
PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock);
LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode);
@@ -1233,6 +1241,8 @@ LockAcquireExtended(const LOCKTAG *locktag,
* now.
*/
Assert(!dontWait);
+ /* Increment the lock statistics deadlocks counter */
+ pgstat_count_lock_deadlocks(locallock->tag.lock.locktag_type);
DeadLockReport();
/* DeadLockReport() will not return */
}
@@ -2799,6 +2809,8 @@ FastPathGrantRelationLock(Oid relid, LOCKMODE lockmode)
{
Assert(!FAST_PATH_CHECK_LOCKMODE(MyProc, f, lockmode));
FAST_PATH_SET_LOCKMODE(MyProc, f, lockmode);
+ /* Increment the lock statistics fastpath counter */
+ pgstat_count_lock_fastpath(LOCKTAG_RELATION);
return true;
}
}
@@ -2808,6 +2820,8 @@ FastPathGrantRelationLock(Oid relid, LOCKMODE lockmode)
{
MyProc->fpRelId[unused_slot] = relid;
FAST_PATH_SET_LOCKMODE(MyProc, unused_slot, lockmode);
+ /* Increment the lock statistics fastpath counter */
+ pgstat_count_lock_fastpath(LOCKTAG_RELATION);
++FastPathLocalUseCounts[group];
return true;
}
@@ -4630,6 +4644,10 @@ VirtualXactLockTableInsert(VirtualTransactionId vxid)
MyProc->fpVXIDLock = true;
MyProc->fpLocalTransactionId = vxid.localTransactionId;
+ /* Increment the lock statistics requests and fastpath counters */
+ pgstat_count_lock_requests(LOCKTAG_VIRTUALTRANSACTION);
+ pgstat_count_lock_fastpath(LOCKTAG_VIRTUALTRANSACTION);
+
LWLockRelease(&MyProc->fpInfoLock);
}
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index ebc3f4ca457..778497ebf98 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -1466,9 +1466,12 @@ ProcSleep(LOCALLOCK *locallock)
/* check for deadlocks first, as that's probably log-worthy */
if (got_deadlock_timeout)
{
+ /* Increment the lock statistics deadlock_timeouts counter */
+ pgstat_count_lock_deadlock_timeouts(locallock->tag.lock.locktag_type);
CheckDeadLock();
got_deadlock_timeout = false;
}
+
CHECK_FOR_INTERRUPTS();
}
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 7dd75a490aa..5a892763060 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3438,6 +3438,14 @@ ProcessInterrupts(void)
if (lock_timeout_occurred)
{
+ LOCALLOCK *lockAwaited;
+
+ lockAwaited = GetAwaitedLock();
+
+ /* Increment the lock statistics timeouts counter */
+ if (lockAwaited)
+ pgstat_count_lock_timeouts(lockAwaited->tag.lock.locktag_type);
+
LockErrorCleanup();
ereport(ERROR,
(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
diff --git a/src/backend/utils/activity/Makefile b/src/backend/utils/activity/Makefile
index 9c2443e1ecd..4293d0f9efd 100644
--- a/src/backend/utils/activity/Makefile
+++ b/src/backend/utils/activity/Makefile
@@ -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 \
diff --git a/src/backend/utils/activity/meson.build b/src/backend/utils/activity/meson.build
index d8e56b49c24..3d3d7b67b5f 100644
--- a/src/backend/utils/activity/meson.build
+++ b/src/backend/utils/activity/meson.build
@@ -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',
diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index f317c6e8e90..fc4d59fc240 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -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
index 00000000000..fb4ed5c7362
--- /dev/null
+++ b/src/backend/utils/activity/pgstat_lock.c
@@ -0,0 +1,168 @@
+/* -------------------------------------------------------------------------
+ *
+ * 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-2025, 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;
+
+/*
+ * 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(requests);
+ LOCKSTAT_ACC(waits);
+ LOCKSTAT_ACC(timeouts);
+ LOCKSTAT_ACC(deadlock_timeouts);
+ LOCKSTAT_ACC(deadlocks);
+ LOCKSTAT_ACC(fastpath);
+#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);
+ }
+}
+
+#define PGSTAT_COUNT_LOCK_FUNC(stat) \
+void \
+CppConcat(pgstat_count_lock_,stat)(uint8 locktag_type) \
+{ \
+ Assert(locktag_type <= LOCKTAG_LAST_TYPE); \
+ PendingLockStats.stats[locktag_type].stat++; \
+ have_lockstats = true; \
+ pgstat_report_fixed = true; \
+}
+
+/* pgstat_count_lock_requests */
+PGSTAT_COUNT_LOCK_FUNC(requests)
+
+/* pgstat_count_lock_waits */
+PGSTAT_COUNT_LOCK_FUNC(waits)
+
+/* pgstat_count_lock_timeouts */
+PGSTAT_COUNT_LOCK_FUNC(timeouts)
+
+/* pgstat_count_lock_deadlock_timeouts */
+PGSTAT_COUNT_LOCK_FUNC(deadlock_timeouts)
+
+/* pgstat_count_lock_deadlocks */
+PGSTAT_COUNT_LOCK_FUNC(deadlocks)
+
+/* pgstat_count_lock_fastpath */
+PGSTAT_COUNT_LOCK_FUNC(fastpath)
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index f23dd5870da..913517ba612 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -17,6 +17,7 @@
#include "postmaster/pgarch.h" /* for MAX_XFN_CHARS */
#include "replication/conflict.h"
#include "replication/worker_internal.h"
+#include "storage/lock.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"
@@ -342,6 +343,27 @@ typedef struct PgStat_IO
PgStat_BktypeIO stats[BACKEND_NUM_TYPES];
} PgStat_IO;
+typedef struct PgStat_LockEntry
+{
+ PgStat_Counter requests;
+ PgStat_Counter waits;
+ PgStat_Counter timeouts;
+ PgStat_Counter deadlock_timeouts;
+ PgStat_Counter deadlocks;
+ PgStat_Counter fastpath;
+} 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;
@@ -614,6 +636,18 @@ 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_requests(uint8 locktag_type);
+extern void pgstat_count_lock_waits(uint8 locktag_type);
+extern void pgstat_count_lock_timeouts(uint8 locktag_type);
+extern void pgstat_count_lock_deadlock_timeouts(uint8 locktag_type);
+extern void pgstat_count_lock_deadlocks(uint8 locktag_type);
+extern void pgstat_count_lock_fastpath(uint8 locktag_type);
+
/*
* Functions in pgstat_database.c
*/
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 5c1ce4d3d6a..6762e45cc0c 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -463,6 +463,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 */
@@ -569,6 +579,7 @@ typedef struct PgStat_ShmemControl
PgStatShared_BgWriter bgwriter;
PgStatShared_Checkpointer checkpointer;
PgStatShared_IO io;
+ PgStatShared_Lock lock;
PgStatShared_SLRU slru;
PgStatShared_Wal wal;
@@ -601,6 +612,8 @@ typedef struct PgStat_Snapshot
PgStat_IO io;
+ PgStat_Lock lock;
+
PgStat_SLRUStats slru[SLRU_NUM_ELEMENTS];
PgStat_WalStats wal;
@@ -751,6 +764,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
diff --git a/src/include/utils/pgstat_kind.h b/src/include/utils/pgstat_kind.h
index eb5f0b3ae6d..f8e95f55533 100644
--- a/src/include/utils/pgstat_kind.h
+++ b/src/include/utils/pgstat_kind.h
@@ -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/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 3451538565e..ad813bafc3e 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2244,6 +2244,7 @@ PgStatShared_Database
PgStatShared_Function
PgStatShared_HashEntry
PgStatShared_IO
+PgStatShared_Lock
PgStatShared_Relation
PgStatShared_ReplSlot
PgStatShared_SLRU
@@ -2266,8 +2267,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
--
2.34.1
[text/x-diff] v2-0002-Add-the-pg_stat_lock-view.patch (17.1K, ../../aUA7ymkrh%2FUpkJ6O@ip-10-97-1-34.eu-west-3.compute.internal/3-v2-0002-Add-the-pg_stat_lock-view.patch)
download | inline diff:
From ccdab5fd2fc59b0449230fa73dcdf3ccb6ec04fe Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Thu, 31 Jul 2025 09:35:31 +0000
Subject: [PATCH v2 2/2] Add the pg_stat_lock view
This new view reports lock statistics.
This commit also adds documentation and a few tests.
XXX: Bump catversion
---
doc/src/sgml/monitoring.sgml | 151 ++++++++++++++++++
src/backend/catalog/system_views.sql | 12 ++
src/backend/utils/activity/pgstat_lock.c | 8 +
src/backend/utils/adt/pgstatfuncs.c | 39 +++++
src/include/catalog/pg_proc.dat | 9 ++
src/include/pgstat.h | 1 +
src/test/isolation/expected/deadlock-hard.out | 20 ++-
src/test/isolation/specs/deadlock-hard.spec | 5 +-
src/test/regress/expected/advisory_lock.out | 18 +++
src/test/regress/expected/rules.out | 9 ++
src/test/regress/sql/advisory_lock.sql | 4 +
11 files changed, 274 insertions(+), 2 deletions(-)
51.5% doc/src/sgml/
3.3% src/backend/catalog/
14.3% src/backend/utils/adt/
5.7% src/include/catalog/
8.4% src/test/isolation/expected/
5.9% src/test/isolation/specs/
6.7% src/test/regress/expected/
3.8% src/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 817fd9f4ca7..3c327093543 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -493,6 +493,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
@@ -3087,6 +3096,142 @@ 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:
+ <literal>relation</literal>,
+ <literal>extend</literal>,
+ <literal>frozenid</literal>,
+ <literal>page</literal>,
+ <literal>tuple</literal>,
+ <literal>transactionid</literal>,
+ <literal>virtualxid</literal>,
+ <literal>spectoken</literal>,
+ <literal>object</literal>,
+ <literal>userlock</literal>,
+ <literal>advisory</literal>, or
+ <literal>applytransaction</literal>.
+ (See also <xref linkend="wait-event-lock-table"/>.)
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>requests</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of requests for this lock type.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>waits</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of times requests for this lock type had to wait.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>timeouts</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of times requests for this lock type had to wait longer
+ than <varname>lock_timeout</varname>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>deadlock_timeouts</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of times requests for this lock type had to wait longer
+ than <varname>deadlock_timeout</varname>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>deadlocks</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of times a deadlock occurred on this lock type.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>fastpath</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of times this lock type was taken via fast path.
+ </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>
@@ -5158,6 +5303,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
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 0a0f95f6bb9..43969783069 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -964,6 +964,18 @@ 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.requests,
+ l.waits,
+ l.timeouts,
+ l.deadlock_timeouts,
+ l.deadlocks,
+ l.fastpath,
+ l.stats_reset
+ FROM pg_stat_get_lock() l;
+
CREATE VIEW pg_stat_wal_receiver AS
SELECT
s.pid,
diff --git a/src/backend/utils/activity/pgstat_lock.c b/src/backend/utils/activity/pgstat_lock.c
index fb4ed5c7362..73bdeb5ff66 100644
--- a/src/backend/utils/activity/pgstat_lock.c
+++ b/src/backend/utils/activity/pgstat_lock.c
@@ -21,6 +21,14 @@
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()
*/
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index ef6fffe60b9..602228263a9 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1725,6 +1725,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 8
+ 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];
+
+ locktypename = LockTagTypeNames[lcktype];
+
+ values[0] = CStringGetTextDatum(locktypename);
+ values[1] = Int64GetDatum(lck_stats->requests);
+ values[2] = Int64GetDatum(lck_stats->waits);
+ values[3] = Int64GetDatum(lck_stats->timeouts);
+ values[4] = Int64GetDatum(lck_stats->deadlock_timeouts);
+ values[5] = Int64GetDatum(lck_stats->deadlocks);
+ values[6] = Int64GetDatum(lck_stats->fastpath);
+ values[7] = TimestampTzGetDatum(lock_stats->stat_reset_timestamp);
+
+ tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+ }
+
+ return (Datum) 0;
+}
+
/*
* Returns statistics of SLRU caches.
*/
@@ -1909,6 +1945,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);
@@ -1926,6 +1963,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)
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fd9448ec7b9..bb466f9e5dd 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6017,6 +6017,15 @@
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,int8,int8,int8,timestamptz}',
+ proargmodes => '{o,o,o,o,o,o,o,o}',
+ proargnames => '{locktype,requests,waits,timeouts,deadlock_timeouts,deadlocks,fastpath,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',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 913517ba612..38a9a7a9976 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -647,6 +647,7 @@ extern void pgstat_count_lock_timeouts(uint8 locktag_type);
extern void pgstat_count_lock_deadlock_timeouts(uint8 locktag_type);
extern void pgstat_count_lock_deadlocks(uint8 locktag_type);
extern void pgstat_count_lock_fastpath(uint8 locktag_type);
+extern PgStat_Lock *pgstat_fetch_stat_lock(void);
/*
* Functions in pgstat_database.c
diff --git a/src/test/isolation/expected/deadlock-hard.out b/src/test/isolation/expected/deadlock-hard.out
index 460653f2b86..ff448da5299 100644
--- a/src/test/isolation/expected/deadlock-hard.out
+++ b/src/test/isolation/expected/deadlock-hard.out
@@ -1,6 +1,12 @@
Parsed test spec with 8 sessions
-starting permutation: s1a1 s2a2 s3a3 s4a4 s5a5 s6a6 s7a7 s8a8 s1a2 s2a3 s3a4 s4a5 s5a6 s6a7 s7a8 s8a1 s8c s7c s6c s5c s4c s3c s2c s1c
+starting permutation: s1rl s1a1 s2a2 s3a3 s4a4 s5a5 s6a6 s7a7 s8a8 s1a2 s2a3 s3a4 s4a5 s5a6 s6a7 s7a8 s8a1 s8c s8f s7c s6c s5c s4c s3c s2c s1c s1sl
+step s1rl: SELECT pg_stat_reset_shared('lock');
+pg_stat_reset_shared
+--------------------
+
+(1 row)
+
step s1a1: LOCK TABLE a1;
step s2a2: LOCK TABLE a2;
step s3a3: LOCK TABLE a3;
@@ -21,6 +27,12 @@ step s8a1: <... completed>
ERROR: deadlock detected
step s7a8: <... completed>
step s8c: COMMIT;
+step s8f: SELECT pg_stat_force_next_flush();
+pg_stat_force_next_flush
+------------------------
+
+(1 row)
+
step s7c: COMMIT;
step s6a7: <... completed>
step s6c: COMMIT;
@@ -34,3 +46,9 @@ step s2a3: <... completed>
step s2c: COMMIT;
step s1a2: <... completed>
step s1c: COMMIT;
+step s1sl: SELECT deadlocks > 0, deadlock_timeouts > 0 FROM pg_stat_lock WHERE locktype = 'relation';
+?column?|?column?
+--------+--------
+t |t
+(1 row)
+
diff --git a/src/test/isolation/specs/deadlock-hard.spec b/src/test/isolation/specs/deadlock-hard.spec
index 60bedca237a..6e8330662b4 100644
--- a/src/test/isolation/specs/deadlock-hard.spec
+++ b/src/test/isolation/specs/deadlock-hard.spec
@@ -25,6 +25,8 @@ setup { BEGIN; SET deadlock_timeout = '100s'; }
step s1a1 { LOCK TABLE a1; }
step s1a2 { LOCK TABLE a2; }
step s1c { COMMIT; }
+step s1sl { SELECT deadlocks > 0, deadlock_timeouts > 0 FROM pg_stat_lock WHERE locktype = 'relation'; }
+step s1rl { SELECT pg_stat_reset_shared('lock'); }
session s2
setup { BEGIN; SET deadlock_timeout = '100s'; }
@@ -67,6 +69,7 @@ setup { BEGIN; SET deadlock_timeout = '10ms'; }
step s8a8 { LOCK TABLE a8; }
step s8a1 { LOCK TABLE a1; }
step s8c { COMMIT; }
+step s8f { SELECT pg_stat_force_next_flush(); }
# Note: when s8a1 detects the deadlock and fails, s7a8 is released, making
# it timing-dependent which query completion is received first by the tester.
@@ -76,4 +79,4 @@ step s8c { COMMIT; }
# dummy blocking mark to s8a1 to ensure it will be reported as "waiting"
# regardless of that.
-permutation s1a1 s2a2 s3a3 s4a4 s5a5 s6a6 s7a7 s8a8 s1a2 s2a3 s3a4 s4a5 s5a6 s6a7 s7a8(s8a1) s8a1(*) s8c s7c s6c s5c s4c s3c s2c s1c
+permutation s1rl s1a1 s2a2 s3a3 s4a4 s5a5 s6a6 s7a7 s8a8 s1a2 s2a3 s3a4 s4a5 s5a6 s6a7 s7a8(s8a1) s8a1(*) s8c s8f s7c s6c s5c s4c s3c s2c s1c s1sl
diff --git a/src/test/regress/expected/advisory_lock.out b/src/test/regress/expected/advisory_lock.out
index 02e07765ac2..fdaa1756ba4 100644
--- a/src/test/regress/expected/advisory_lock.out
+++ b/src/test/regress/expected/advisory_lock.out
@@ -2,6 +2,12 @@
-- ADVISORY LOCKS
--
SELECT oid AS datoid FROM pg_database WHERE datname = current_database() \gset
+SELECT pg_stat_reset_shared('lock');
+ pg_stat_reset_shared
+----------------------
+
+(1 row)
+
BEGIN;
SELECT
pg_advisory_xact_lock(1), pg_advisory_xact_lock_shared(2),
@@ -48,6 +54,12 @@ WARNING: you don't own a lock of type ShareLock
f | f | f | f
(1 row)
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
-- automatically release xact locks at commit
COMMIT;
SELECT count(*) FROM pg_locks WHERE locktype = 'advisory' AND database = :datoid;
@@ -56,6 +68,12 @@ SELECT count(*) FROM pg_locks WHERE locktype = 'advisory' AND database = :datoid
0
(1 row)
+SELECT requests FROM pg_stat_lock WHERE locktype = 'advisory';
+ requests
+----------
+ 4
+(1 row)
+
BEGIN;
-- holding both session and xact locks on the same objects, xact first
SELECT
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 4286c266e17..304d9f72ed5 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1949,6 +1949,15 @@ 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,
+ requests,
+ waits,
+ timeouts,
+ deadlock_timeouts,
+ deadlocks,
+ fastpath,
+ stats_reset
+ FROM pg_stat_get_lock() l(locktype, requests, waits, timeouts, deadlock_timeouts, deadlocks, fastpath, stats_reset);
pg_stat_progress_analyze| SELECT s.pid,
s.datid,
d.datname,
diff --git a/src/test/regress/sql/advisory_lock.sql b/src/test/regress/sql/advisory_lock.sql
index 8513ab8e98f..f1bff60fd37 100644
--- a/src/test/regress/sql/advisory_lock.sql
+++ b/src/test/regress/sql/advisory_lock.sql
@@ -4,6 +4,8 @@
SELECT oid AS datoid FROM pg_database WHERE datname = current_database() \gset
+SELECT pg_stat_reset_shared('lock');
+
BEGIN;
SELECT
@@ -26,12 +28,14 @@ SELECT
pg_advisory_unlock(1), pg_advisory_unlock_shared(2),
pg_advisory_unlock(1, 1), pg_advisory_unlock_shared(2, 2);
+SELECT pg_stat_force_next_flush();
-- automatically release xact locks at commit
COMMIT;
SELECT count(*) FROM pg_locks WHERE locktype = 'advisory' AND database = :datoid;
+SELECT requests FROM pg_stat_lock WHERE locktype = 'advisory';
BEGIN;
--
2.34.1
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-02-10 07:30 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
0 siblings, 1 reply; 61+ messages in thread
From: Bertrand Drouvot @ 2026-02-10 07:30 UTC (permalink / raw)
To: Jeff Davis <pgsql@j-davis.com>; +Cc: Michael Paquier <michael@paquier.xyz>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi,
On Mon, Dec 15, 2025 at 04:48:10PM +0000, Bertrand Drouvot wrote:
> Hi,
>
> On Wed, Aug 13, 2025 at 03:55:41AM +0000, Bertrand Drouvot wrote:
> > Fully agree, getting input from others definitely helps validate the approach
> > and see if the current design needs some changes.
>
> PFA a mandatory rebase.
New rebase due to 73d60ac385a.
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
Attachments:
[text/x-diff] v3-0001-Add-lock-statistics.patch (16.1K, ../../aYreqqn3meYqPm1A@ip-10-97-1-34.eu-west-3.compute.internal/2-v3-0001-Add-lock-statistics.patch)
download | inline diff:
From 70c5c5bbf85d7e5aa94b1cf0c2499d9ff6dbf527 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Tue, 29 Jul 2025 08:36:35 +0000
Subject: [PATCH v3 1/2] Add lock statistics
Adding a new stat kind PGSTAT_KIND_LOCK for the lock statistics.
This new statistic kind is a fixed one because its key is the lock type
so that we know its size is LOCKTAG_LAST_TYPE + 1.
This statistic kind records the following counters:
requests
waits
timeouts
deadlock_timeouts
deadlocks
fastpath
No extra details is added (like the ones, i.e relation oid, database oid, we
can find in pg_locks). The idea is to provide an idea on what the locking
behaviour looks like.
XXX: Bump stat file format
---
src/backend/storage/lmgr/lock.c | 18 +++
src/backend/storage/lmgr/proc.c | 2 +
src/backend/tcop/postgres.c | 8 ++
src/backend/utils/activity/Makefile | 1 +
src/backend/utils/activity/meson.build | 1 +
src/backend/utils/activity/pgstat.c | 18 +++
src/backend/utils/activity/pgstat_lock.c | 168 +++++++++++++++++++++++
src/include/pgstat.h | 34 +++++
src/include/utils/pgstat_internal.h | 21 +++
src/include/utils/pgstat_kind.h | 5 +-
src/tools/pgindent/typedefs.list | 4 +
11 files changed, 278 insertions(+), 2 deletions(-)
13.3% src/backend/storage/lmgr/
62.5% src/backend/utils/activity/
8.3% src/include/utils/
12.1% src/include/
3.6% src/
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index 7f0cd784f79..bda257a3543 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -45,6 +45,7 @@
#include "storage/spin.h"
#include "storage/standby.h"
#include "utils/memutils.h"
+#include "utils/pgstat_internal.h"
#include "utils/ps_status.h"
#include "utils/resowner.h"
@@ -871,6 +872,9 @@ LockAcquireExtended(const LOCKTAG *locktag,
lockMethodTable->lockModeNames[lockmode]),
errhint("Only RowExclusiveLock or less can be acquired on database objects during recovery.")));
+ /* Increment the lock statistics requests counter */
+ pgstat_count_lock_requests(locktag->locktag_type);
+
#ifdef LOCK_DEBUG
if (LOCK_DEBUG_ENABLED(locktag))
elog(LOG, "LockAcquire: lock [%u,%u] %s",
@@ -1202,6 +1206,8 @@ LockAcquireExtended(const LOCKTAG *locktag,
}
else
{
+ /* Increment the lock statistics deadlocks counter */
+ pgstat_count_lock_deadlocks(locallock->tag.lock.locktag_type);
DeadLockReport();
/* DeadLockReport() will not return */
}
@@ -1213,6 +1219,8 @@ LockAcquireExtended(const LOCKTAG *locktag,
*/
if (waitResult == PROC_WAIT_STATUS_WAITING)
{
+ /* Increment the lock statistics waits counter */
+ pgstat_count_lock_waits(locktag->locktag_type);
Assert(!dontWait);
PROCLOCK_PRINT("LockAcquire: sleeping on lock", proclock);
LOCK_PRINT("LockAcquire: sleeping on lock", lock, lockmode);
@@ -1233,6 +1241,8 @@ LockAcquireExtended(const LOCKTAG *locktag,
* now.
*/
Assert(!dontWait);
+ /* Increment the lock statistics deadlocks counter */
+ pgstat_count_lock_deadlocks(locallock->tag.lock.locktag_type);
DeadLockReport();
/* DeadLockReport() will not return */
}
@@ -2799,6 +2809,8 @@ FastPathGrantRelationLock(Oid relid, LOCKMODE lockmode)
{
Assert(!FAST_PATH_CHECK_LOCKMODE(MyProc, f, lockmode));
FAST_PATH_SET_LOCKMODE(MyProc, f, lockmode);
+ /* Increment the lock statistics fastpath counter */
+ pgstat_count_lock_fastpath(LOCKTAG_RELATION);
return true;
}
}
@@ -2808,6 +2820,8 @@ FastPathGrantRelationLock(Oid relid, LOCKMODE lockmode)
{
MyProc->fpRelId[unused_slot] = relid;
FAST_PATH_SET_LOCKMODE(MyProc, unused_slot, lockmode);
+ /* Increment the lock statistics fastpath counter */
+ pgstat_count_lock_fastpath(LOCKTAG_RELATION);
++FastPathLocalUseCounts[group];
return true;
}
@@ -4630,6 +4644,10 @@ VirtualXactLockTableInsert(VirtualTransactionId vxid)
MyProc->fpVXIDLock = true;
MyProc->fpLocalTransactionId = vxid.localTransactionId;
+ /* Increment the lock statistics requests and fastpath counters */
+ pgstat_count_lock_requests(LOCKTAG_VIRTUALTRANSACTION);
+ pgstat_count_lock_fastpath(LOCKTAG_VIRTUALTRANSACTION);
+
LWLockRelease(&MyProc->fpInfoLock);
}
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 8560a903bc8..2b3e1dfea5c 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -1460,6 +1460,8 @@ ProcSleep(LOCALLOCK *locallock)
/* check for deadlocks first, as that's probably log-worthy */
if (got_deadlock_timeout)
{
+ /* Increment the lock statistics deadlock_timeouts counter */
+ pgstat_count_lock_deadlock_timeouts(locallock->tag.lock.locktag_type);
deadlock_state = CheckDeadLock();
got_deadlock_timeout = false;
}
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 02e9aaa6bca..ed8b2e27666 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3417,6 +3417,14 @@ ProcessInterrupts(void)
if (lock_timeout_occurred)
{
+ LOCALLOCK *lockAwaited;
+
+ lockAwaited = GetAwaitedLock();
+
+ /* Increment the lock statistics timeouts counter */
+ if (lockAwaited)
+ pgstat_count_lock_timeouts(lockAwaited->tag.lock.locktag_type);
+
LockErrorCleanup();
ereport(ERROR,
(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
diff --git a/src/backend/utils/activity/Makefile b/src/backend/utils/activity/Makefile
index c37bfb350bb..ca3ef89bf59 100644
--- a/src/backend/utils/activity/Makefile
+++ b/src/backend/utils/activity/Makefile
@@ -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 \
diff --git a/src/backend/utils/activity/meson.build b/src/backend/utils/activity/meson.build
index 53bd5a246ca..1aa7ece5290 100644
--- a/src/backend/utils/activity/meson.build
+++ b/src/backend/utils/activity/meson.build
@@ -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',
diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index 11bb71cad5a..eb8ccbaa628 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -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
index 00000000000..fb4ed5c7362
--- /dev/null
+++ b/src/backend/utils/activity/pgstat_lock.c
@@ -0,0 +1,168 @@
+/* -------------------------------------------------------------------------
+ *
+ * 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-2025, 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;
+
+/*
+ * 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(requests);
+ LOCKSTAT_ACC(waits);
+ LOCKSTAT_ACC(timeouts);
+ LOCKSTAT_ACC(deadlock_timeouts);
+ LOCKSTAT_ACC(deadlocks);
+ LOCKSTAT_ACC(fastpath);
+#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);
+ }
+}
+
+#define PGSTAT_COUNT_LOCK_FUNC(stat) \
+void \
+CppConcat(pgstat_count_lock_,stat)(uint8 locktag_type) \
+{ \
+ Assert(locktag_type <= LOCKTAG_LAST_TYPE); \
+ PendingLockStats.stats[locktag_type].stat++; \
+ have_lockstats = true; \
+ pgstat_report_fixed = true; \
+}
+
+/* pgstat_count_lock_requests */
+PGSTAT_COUNT_LOCK_FUNC(requests)
+
+/* pgstat_count_lock_waits */
+PGSTAT_COUNT_LOCK_FUNC(waits)
+
+/* pgstat_count_lock_timeouts */
+PGSTAT_COUNT_LOCK_FUNC(timeouts)
+
+/* pgstat_count_lock_deadlock_timeouts */
+PGSTAT_COUNT_LOCK_FUNC(deadlock_timeouts)
+
+/* pgstat_count_lock_deadlocks */
+PGSTAT_COUNT_LOCK_FUNC(deadlocks)
+
+/* pgstat_count_lock_fastpath */
+PGSTAT_COUNT_LOCK_FUNC(fastpath)
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index fff7ecc2533..cbe226b9a07 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -17,6 +17,7 @@
#include "postmaster/pgarch.h" /* for MAX_XFN_CHARS */
#include "replication/conflict.h"
#include "replication/worker_internal.h"
+#include "storage/lock.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"
@@ -342,6 +343,27 @@ typedef struct PgStat_IO
PgStat_BktypeIO stats[BACKEND_NUM_TYPES];
} PgStat_IO;
+typedef struct PgStat_LockEntry
+{
+ PgStat_Counter requests;
+ PgStat_Counter waits;
+ PgStat_Counter timeouts;
+ PgStat_Counter deadlock_timeouts;
+ PgStat_Counter deadlocks;
+ PgStat_Counter fastpath;
+} 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;
@@ -614,6 +636,18 @@ 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_requests(uint8 locktag_type);
+extern void pgstat_count_lock_waits(uint8 locktag_type);
+extern void pgstat_count_lock_timeouts(uint8 locktag_type);
+extern void pgstat_count_lock_deadlock_timeouts(uint8 locktag_type);
+extern void pgstat_count_lock_deadlocks(uint8 locktag_type);
+extern void pgstat_count_lock_fastpath(uint8 locktag_type);
+
/*
* Functions in pgstat_database.c
*/
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 9b8fbae00ed..97704421a92 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -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
diff --git a/src/include/utils/pgstat_kind.h b/src/include/utils/pgstat_kind.h
index c30b6235623..2d78a029683 100644
--- a/src/include/utils/pgstat_kind.h
+++ b/src/include/utils/pgstat_kind.h
@@ -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/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 7619845fba9..7386db6e6a4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2253,6 +2253,7 @@ PgStatShared_Database
PgStatShared_Function
PgStatShared_HashEntry
PgStatShared_IO
+PgStatShared_Lock
PgStatShared_Relation
PgStatShared_ReplSlot
PgStatShared_SLRU
@@ -2275,8 +2276,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
--
2.34.1
[text/x-diff] v3-0002-Add-the-pg_stat_lock-view.patch (17.1K, ../../aYreqqn3meYqPm1A@ip-10-97-1-34.eu-west-3.compute.internal/3-v3-0002-Add-the-pg_stat_lock-view.patch)
download | inline diff:
From c03efb1c8cbaaac8177348d1c4df19a0092afec7 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Thu, 31 Jul 2025 09:35:31 +0000
Subject: [PATCH v3 2/2] Add the pg_stat_lock view
This new view reports lock statistics.
This commit also adds documentation and a few tests.
XXX: Bump catversion
---
doc/src/sgml/monitoring.sgml | 151 ++++++++++++++++++
src/backend/catalog/system_views.sql | 12 ++
src/backend/utils/activity/pgstat_lock.c | 8 +
src/backend/utils/adt/pgstatfuncs.c | 39 +++++
src/include/catalog/pg_proc.dat | 9 ++
src/include/pgstat.h | 1 +
src/test/isolation/expected/deadlock-hard.out | 20 ++-
src/test/isolation/specs/deadlock-hard.spec | 5 +-
src/test/regress/expected/advisory_lock.out | 18 +++
src/test/regress/expected/rules.out | 9 ++
src/test/regress/sql/advisory_lock.sql | 4 +
11 files changed, 274 insertions(+), 2 deletions(-)
51.5% doc/src/sgml/
3.3% src/backend/catalog/
14.3% src/backend/utils/adt/
5.7% src/include/catalog/
8.4% src/test/isolation/expected/
5.9% src/test/isolation/specs/
6.7% src/test/regress/expected/
3.8% src/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index b77d189a500..927b62cf649 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -493,6 +493,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
@@ -3124,6 +3133,142 @@ 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:
+ <literal>relation</literal>,
+ <literal>extend</literal>,
+ <literal>frozenid</literal>,
+ <literal>page</literal>,
+ <literal>tuple</literal>,
+ <literal>transactionid</literal>,
+ <literal>virtualxid</literal>,
+ <literal>spectoken</literal>,
+ <literal>object</literal>,
+ <literal>userlock</literal>,
+ <literal>advisory</literal>, or
+ <literal>applytransaction</literal>.
+ (See also <xref linkend="wait-event-lock-table"/>.)
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>requests</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of requests for this lock type.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>waits</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of times requests for this lock type had to wait.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>timeouts</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of times requests for this lock type had to wait longer
+ than <varname>lock_timeout</varname>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>deadlock_timeouts</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of times requests for this lock type had to wait longer
+ than <varname>deadlock_timeout</varname>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>deadlocks</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of times a deadlock occurred on this lock type.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>fastpath</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of times this lock type was taken via fast path.
+ </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>
@@ -5195,6 +5340,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
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 1ea8f1faa9e..e30c25ced31 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -985,6 +985,18 @@ 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.requests,
+ l.waits,
+ l.timeouts,
+ l.deadlock_timeouts,
+ l.deadlocks,
+ l.fastpath,
+ l.stats_reset
+ FROM pg_stat_get_lock() l;
+
CREATE VIEW pg_stat_wal_receiver AS
SELECT
s.pid,
diff --git a/src/backend/utils/activity/pgstat_lock.c b/src/backend/utils/activity/pgstat_lock.c
index fb4ed5c7362..73bdeb5ff66 100644
--- a/src/backend/utils/activity/pgstat_lock.c
+++ b/src/backend/utils/activity/pgstat_lock.c
@@ -21,6 +21,14 @@
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()
*/
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 73ca0bb0b7f..4ca4a3f3b9b 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1725,6 +1725,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 8
+ 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];
+
+ locktypename = LockTagTypeNames[lcktype];
+
+ values[0] = CStringGetTextDatum(locktypename);
+ values[1] = Int64GetDatum(lck_stats->requests);
+ values[2] = Int64GetDatum(lck_stats->waits);
+ values[3] = Int64GetDatum(lck_stats->timeouts);
+ values[4] = Int64GetDatum(lck_stats->deadlock_timeouts);
+ values[5] = Int64GetDatum(lck_stats->deadlocks);
+ values[6] = Int64GetDatum(lck_stats->fastpath);
+ values[7] = TimestampTzGetDatum(lock_stats->stat_reset_timestamp);
+
+ tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+ }
+
+ return (Datum) 0;
+}
+
/*
* Returns statistics of SLRU caches.
*/
@@ -1909,6 +1945,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);
@@ -1926,6 +1963,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)
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 83f6501df38..a0c47f6888c 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6026,6 +6026,15 @@
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,int8,int8,int8,timestamptz}',
+ proargmodes => '{o,o,o,o,o,o,o,o}',
+ proargnames => '{locktype,requests,waits,timeouts,deadlock_timeouts,deadlocks,fastpath,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',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index cbe226b9a07..c16c3335755 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -647,6 +647,7 @@ extern void pgstat_count_lock_timeouts(uint8 locktag_type);
extern void pgstat_count_lock_deadlock_timeouts(uint8 locktag_type);
extern void pgstat_count_lock_deadlocks(uint8 locktag_type);
extern void pgstat_count_lock_fastpath(uint8 locktag_type);
+extern PgStat_Lock *pgstat_fetch_stat_lock(void);
/*
* Functions in pgstat_database.c
diff --git a/src/test/isolation/expected/deadlock-hard.out b/src/test/isolation/expected/deadlock-hard.out
index 460653f2b86..ff448da5299 100644
--- a/src/test/isolation/expected/deadlock-hard.out
+++ b/src/test/isolation/expected/deadlock-hard.out
@@ -1,6 +1,12 @@
Parsed test spec with 8 sessions
-starting permutation: s1a1 s2a2 s3a3 s4a4 s5a5 s6a6 s7a7 s8a8 s1a2 s2a3 s3a4 s4a5 s5a6 s6a7 s7a8 s8a1 s8c s7c s6c s5c s4c s3c s2c s1c
+starting permutation: s1rl s1a1 s2a2 s3a3 s4a4 s5a5 s6a6 s7a7 s8a8 s1a2 s2a3 s3a4 s4a5 s5a6 s6a7 s7a8 s8a1 s8c s8f s7c s6c s5c s4c s3c s2c s1c s1sl
+step s1rl: SELECT pg_stat_reset_shared('lock');
+pg_stat_reset_shared
+--------------------
+
+(1 row)
+
step s1a1: LOCK TABLE a1;
step s2a2: LOCK TABLE a2;
step s3a3: LOCK TABLE a3;
@@ -21,6 +27,12 @@ step s8a1: <... completed>
ERROR: deadlock detected
step s7a8: <... completed>
step s8c: COMMIT;
+step s8f: SELECT pg_stat_force_next_flush();
+pg_stat_force_next_flush
+------------------------
+
+(1 row)
+
step s7c: COMMIT;
step s6a7: <... completed>
step s6c: COMMIT;
@@ -34,3 +46,9 @@ step s2a3: <... completed>
step s2c: COMMIT;
step s1a2: <... completed>
step s1c: COMMIT;
+step s1sl: SELECT deadlocks > 0, deadlock_timeouts > 0 FROM pg_stat_lock WHERE locktype = 'relation';
+?column?|?column?
+--------+--------
+t |t
+(1 row)
+
diff --git a/src/test/isolation/specs/deadlock-hard.spec b/src/test/isolation/specs/deadlock-hard.spec
index 60bedca237a..6e8330662b4 100644
--- a/src/test/isolation/specs/deadlock-hard.spec
+++ b/src/test/isolation/specs/deadlock-hard.spec
@@ -25,6 +25,8 @@ setup { BEGIN; SET deadlock_timeout = '100s'; }
step s1a1 { LOCK TABLE a1; }
step s1a2 { LOCK TABLE a2; }
step s1c { COMMIT; }
+step s1sl { SELECT deadlocks > 0, deadlock_timeouts > 0 FROM pg_stat_lock WHERE locktype = 'relation'; }
+step s1rl { SELECT pg_stat_reset_shared('lock'); }
session s2
setup { BEGIN; SET deadlock_timeout = '100s'; }
@@ -67,6 +69,7 @@ setup { BEGIN; SET deadlock_timeout = '10ms'; }
step s8a8 { LOCK TABLE a8; }
step s8a1 { LOCK TABLE a1; }
step s8c { COMMIT; }
+step s8f { SELECT pg_stat_force_next_flush(); }
# Note: when s8a1 detects the deadlock and fails, s7a8 is released, making
# it timing-dependent which query completion is received first by the tester.
@@ -76,4 +79,4 @@ step s8c { COMMIT; }
# dummy blocking mark to s8a1 to ensure it will be reported as "waiting"
# regardless of that.
-permutation s1a1 s2a2 s3a3 s4a4 s5a5 s6a6 s7a7 s8a8 s1a2 s2a3 s3a4 s4a5 s5a6 s6a7 s7a8(s8a1) s8a1(*) s8c s7c s6c s5c s4c s3c s2c s1c
+permutation s1rl s1a1 s2a2 s3a3 s4a4 s5a5 s6a6 s7a7 s8a8 s1a2 s2a3 s3a4 s4a5 s5a6 s6a7 s7a8(s8a1) s8a1(*) s8c s8f s7c s6c s5c s4c s3c s2c s1c s1sl
diff --git a/src/test/regress/expected/advisory_lock.out b/src/test/regress/expected/advisory_lock.out
index 02e07765ac2..fdaa1756ba4 100644
--- a/src/test/regress/expected/advisory_lock.out
+++ b/src/test/regress/expected/advisory_lock.out
@@ -2,6 +2,12 @@
-- ADVISORY LOCKS
--
SELECT oid AS datoid FROM pg_database WHERE datname = current_database() \gset
+SELECT pg_stat_reset_shared('lock');
+ pg_stat_reset_shared
+----------------------
+
+(1 row)
+
BEGIN;
SELECT
pg_advisory_xact_lock(1), pg_advisory_xact_lock_shared(2),
@@ -48,6 +54,12 @@ WARNING: you don't own a lock of type ShareLock
f | f | f | f
(1 row)
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
-- automatically release xact locks at commit
COMMIT;
SELECT count(*) FROM pg_locks WHERE locktype = 'advisory' AND database = :datoid;
@@ -56,6 +68,12 @@ SELECT count(*) FROM pg_locks WHERE locktype = 'advisory' AND database = :datoid
0
(1 row)
+SELECT requests FROM pg_stat_lock WHERE locktype = 'advisory';
+ requests
+----------
+ 4
+(1 row)
+
BEGIN;
-- holding both session and xact locks on the same objects, xact first
SELECT
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index f9bc213e5a1..8ee2e4cbea1 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1951,6 +1951,15 @@ 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,
+ requests,
+ waits,
+ timeouts,
+ deadlock_timeouts,
+ deadlocks,
+ fastpath,
+ stats_reset
+ FROM pg_stat_get_lock() l(locktype, requests, waits, timeouts, deadlock_timeouts, deadlocks, fastpath, stats_reset);
pg_stat_progress_analyze| SELECT s.pid,
s.datid,
d.datname,
diff --git a/src/test/regress/sql/advisory_lock.sql b/src/test/regress/sql/advisory_lock.sql
index 8513ab8e98f..f1bff60fd37 100644
--- a/src/test/regress/sql/advisory_lock.sql
+++ b/src/test/regress/sql/advisory_lock.sql
@@ -4,6 +4,8 @@
SELECT oid AS datoid FROM pg_database WHERE datname = current_database() \gset
+SELECT pg_stat_reset_shared('lock');
+
BEGIN;
SELECT
@@ -26,12 +28,14 @@ SELECT
pg_advisory_unlock(1), pg_advisory_unlock_shared(2),
pg_advisory_unlock(1, 1), pg_advisory_unlock_shared(2, 2);
+SELECT pg_stat_force_next_flush();
-- automatically release xact locks at commit
COMMIT;
SELECT count(*) FROM pg_locks WHERE locktype = 'advisory' AND database = :datoid;
+SELECT requests FROM pg_stat_lock WHERE locktype = 'advisory';
BEGIN;
--
2.34.1
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-02-13 07:36 Michael Paquier <michael@paquier.xyz>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
0 siblings, 1 reply; 61+ messages in thread
From: Michael Paquier @ 2026-02-13 07:36 UTC (permalink / raw)
To: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
On Tue, Feb 10, 2026 at 07:30:50AM +0000, Bertrand Drouvot wrote:
> New rebase due to 73d60ac385a.
I have been looking at this patch, and can get behind the data
gathered here in terms of being able to tune things, but not all. See
below for the details of my reasoning.
max_locks_per_xact is a PGC_POSTMASTER, so backend-level stats with
this data would not be relevant for its tuning. However, something
else can be said about deadlock_timeout, where one could rely on the
data gathered by this view to set it on a backend basis, particularly
if the load pattern is divided into a subsets of connections (say few
backend see a lot of the deadlock_timeout, for example). Same
argument for lock_timeout, which is user-settable.
As the set of data gathered, I think that I'm OK with timeouts (for
lock_timeout), deadlock_timeouts (for deadlock_timeout), fastpath (for
max_locks_per_xact), that can all be compared with the number of
requests.
Regarding "deadlocks" and "waits", these two are less useful than the
three others because not really actionable. They would become much
more relevant if and only if we know the distribution of the deadlocks
not only for the lock types, but for the objects involved, especially
if the activity is diluted across many objects. So these have less
value IMO because they are not really actionable in the system. The
other three can be directly tuned based on the GUCs we have.
So my suggestion for the moment would be to be more frugal (yeah I
know, sorry..) and limit ourselves to four fields: deadlock_timeout,
requests, fastpath and timeouts. Three fields to compare with
requests, one for each GUC.
Regarding the implementation, you are right to use a fixed-sized stats
kind for the job. I can see a lot of code has been copy-pasted from
pgstat_io.c, then slightly adjusted to fit into the picture. That's
fine here, it makes the implementation straight-forward to read.
Regarding the documentation, listing all the values for locktype is a
recipe for rot. I'd suggest to remove the list instead, with only a
link referring to pg_locks to avoid the duplication.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (832B, ../../aY7UmTIiBScP4rAq@paquier.xyz/2-signature.asc)
download
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-02-13 10:24 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Michael Paquier <michael@paquier.xyz>
0 siblings, 1 reply; 61+ messages in thread
From: Bertrand Drouvot @ 2026-02-13 10:24 UTC (permalink / raw)
To: Michael Paquier <michael@paquier.xyz>; +Cc: Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi,
On Fri, Feb 13, 2026 at 04:36:57PM +0900, Michael Paquier wrote:
> On Tue, Feb 10, 2026 at 07:30:50AM +0000, Bertrand Drouvot wrote:
> > New rebase due to 73d60ac385a.
>
> I have been looking at this patch, and can get behind the data
> gathered here in terms of being able to tune things
Thanks!
>
> So my suggestion for the moment would be to be more frugal (yeah I
> know, sorry..) and limit ourselves to four fields: deadlock_timeout,
> requests, fastpath and timeouts. Three fields to compare with
> requests, one for each GUC.
That's fine by me. We could still add the others in the future if we feel the
need. Done that way in the attached.
> Regarding the implementation, you are right to use a fixed-sized stats
> kind for the job. I can see a lot of code has been copy-pasted from
> pgstat_io.c, then slightly adjusted to fit into the picture. That's
> fine here, it makes the implementation straight-forward to read.
Yeah, no need to reinvent the wheel.
> Regarding the documentation, listing all the values for locktype is a
> recipe for rot. I'd suggest to remove the list instead, with only a
> link referring to pg_locks to avoid the duplication.
Makes sense, done that way. Makes me think that we could do the same for pg_locks
and just link it to the Wait Events of Type Lock? (Table 27.11.)
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
Attachments:
[text/x-diff] v4-0001-Add-lock-statistics.patch (14.9K, ../../aY779Gu3rQwJ4Erq@ip-10-97-1-34.eu-west-3.compute.internal/2-v4-0001-Add-lock-statistics.patch)
download | inline diff:
From 7e7f2cecfedce71c5aa68ac6843a003540ee33e2 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Tue, 29 Jul 2025 08:36:35 +0000
Subject: [PATCH v4 1/2] Add lock statistics
Adding a new stat kind PGSTAT_KIND_LOCK for the lock statistics.
This new statistic kind is a fixed one because its key is the lock type
so that we know its size is LOCKTAG_LAST_TYPE + 1.
This statistic kind records the following counters:
requests
timeouts
deadlock_timeouts
fastpath
These counters enable tuning of lock related GUCs (max_locks_per_transaction,
lock_timeout, and deadlock_timeout).
No extra details is added (like the ones, i.e relation oid, database oid, we
can find in pg_locks). The idea is to provide an idea on what the locking
behaviour looks like.
XXX: Bump stat file format
---
src/backend/storage/lmgr/lock.c | 12 ++
src/backend/storage/lmgr/proc.c | 2 +
src/backend/tcop/postgres.c | 8 ++
src/backend/utils/activity/Makefile | 1 +
src/backend/utils/activity/meson.build | 1 +
src/backend/utils/activity/pgstat.c | 18 +++
src/backend/utils/activity/pgstat_lock.c | 160 +++++++++++++++++++++++
src/include/pgstat.h | 30 +++++
src/include/utils/pgstat_internal.h | 21 +++
src/include/utils/pgstat_kind.h | 5 +-
src/tools/pgindent/typedefs.list | 4 +
11 files changed, 260 insertions(+), 2 deletions(-)
9.7% src/backend/storage/lmgr/
3.0% src/backend/tcop/
66.2% src/backend/utils/activity/
9.1% src/include/utils/
10.9% src/include/
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index e1168ad3837..30537ccfade 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -45,6 +45,7 @@
#include "storage/spin.h"
#include "storage/standby.h"
#include "utils/memutils.h"
+#include "utils/pgstat_internal.h"
#include "utils/ps_status.h"
#include "utils/resowner.h"
@@ -871,6 +872,9 @@ LockAcquireExtended(const LOCKTAG *locktag,
lockMethodTable->lockModeNames[lockmode]),
errhint("Only RowExclusiveLock or less can be acquired on database objects during recovery.")));
+ /* Increment the lock statistics requests counter */
+ pgstat_count_lock_requests(locktag->locktag_type);
+
#ifdef LOCK_DEBUG
if (LOCK_DEBUG_ENABLED(locktag))
elog(LOG, "LockAcquire: lock [%u,%u] %s",
@@ -2799,6 +2803,8 @@ FastPathGrantRelationLock(Oid relid, LOCKMODE lockmode)
{
Assert(!FAST_PATH_CHECK_LOCKMODE(MyProc, f, lockmode));
FAST_PATH_SET_LOCKMODE(MyProc, f, lockmode);
+ /* Increment the lock statistics fastpath counter */
+ pgstat_count_lock_fastpath(LOCKTAG_RELATION);
return true;
}
}
@@ -2808,6 +2814,8 @@ FastPathGrantRelationLock(Oid relid, LOCKMODE lockmode)
{
MyProc->fpRelId[unused_slot] = relid;
FAST_PATH_SET_LOCKMODE(MyProc, unused_slot, lockmode);
+ /* Increment the lock statistics fastpath counter */
+ pgstat_count_lock_fastpath(LOCKTAG_RELATION);
++FastPathLocalUseCounts[group];
return true;
}
@@ -4629,6 +4637,10 @@ VirtualXactLockTableInsert(VirtualTransactionId vxid)
MyProc->fpVXIDLock = true;
MyProc->fpLocalTransactionId = vxid.localTransactionId;
+ /* Increment the lock statistics requests and fastpath counters */
+ pgstat_count_lock_requests(LOCKTAG_VIRTUALTRANSACTION);
+ pgstat_count_lock_fastpath(LOCKTAG_VIRTUALTRANSACTION);
+
LWLockRelease(&MyProc->fpInfoLock);
}
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index fd8318bdf3d..5eee8d1f69d 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -1447,6 +1447,8 @@ ProcSleep(LOCALLOCK *locallock)
/* check for deadlocks first, as that's probably log-worthy */
if (got_deadlock_timeout)
{
+ /* Increment the lock statistics deadlock_timeouts counter */
+ pgstat_count_lock_deadlock_timeouts(locallock->tag.lock.locktag_type);
deadlock_state = CheckDeadLock();
got_deadlock_timeout = false;
}
diff --git a/src/backend/tcop/postgres.c b/src/backend/tcop/postgres.c
index 21de158adbb..eadb43a283d 100644
--- a/src/backend/tcop/postgres.c
+++ b/src/backend/tcop/postgres.c
@@ -3472,6 +3472,14 @@ ProcessInterrupts(void)
if (lock_timeout_occurred)
{
+ LOCALLOCK *lockAwaited;
+
+ lockAwaited = GetAwaitedLock();
+
+ /* Increment the lock statistics timeouts counter */
+ if (lockAwaited)
+ pgstat_count_lock_timeouts(lockAwaited->tag.lock.locktag_type);
+
LockErrorCleanup();
ereport(ERROR,
(errcode(ERRCODE_LOCK_NOT_AVAILABLE),
diff --git a/src/backend/utils/activity/Makefile b/src/backend/utils/activity/Makefile
index c37bfb350bb..ca3ef89bf59 100644
--- a/src/backend/utils/activity/Makefile
+++ b/src/backend/utils/activity/Makefile
@@ -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 \
diff --git a/src/backend/utils/activity/meson.build b/src/backend/utils/activity/meson.build
index 53bd5a246ca..1aa7ece5290 100644
--- a/src/backend/utils/activity/meson.build
+++ b/src/backend/utils/activity/meson.build
@@ -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',
diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index 11bb71cad5a..eb8ccbaa628 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -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
index 00000000000..110a370b875
--- /dev/null
+++ b/src/backend/utils/activity/pgstat_lock.c
@@ -0,0 +1,160 @@
+/* -------------------------------------------------------------------------
+ *
+ * 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-2025, 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;
+
+/*
+ * 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(requests);
+ LOCKSTAT_ACC(timeouts);
+ LOCKSTAT_ACC(deadlock_timeouts);
+ LOCKSTAT_ACC(fastpath);
+#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);
+ }
+}
+
+#define PGSTAT_COUNT_LOCK_FUNC(stat) \
+void \
+CppConcat(pgstat_count_lock_,stat)(uint8 locktag_type) \
+{ \
+ Assert(locktag_type <= LOCKTAG_LAST_TYPE); \
+ PendingLockStats.stats[locktag_type].stat++; \
+ have_lockstats = true; \
+ pgstat_report_fixed = true; \
+}
+
+/* pgstat_count_lock_requests */
+PGSTAT_COUNT_LOCK_FUNC(requests)
+
+/* pgstat_count_lock_timeouts */
+PGSTAT_COUNT_LOCK_FUNC(timeouts)
+
+/* pgstat_count_lock_deadlock_timeouts */
+PGSTAT_COUNT_LOCK_FUNC(deadlock_timeouts)
+
+/* pgstat_count_lock_fastpath */
+PGSTAT_COUNT_LOCK_FUNC(fastpath)
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index fff7ecc2533..8460e2db159 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -17,6 +17,7 @@
#include "postmaster/pgarch.h" /* for MAX_XFN_CHARS */
#include "replication/conflict.h"
#include "replication/worker_internal.h"
+#include "storage/lock.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"
@@ -342,6 +343,25 @@ typedef struct PgStat_IO
PgStat_BktypeIO stats[BACKEND_NUM_TYPES];
} PgStat_IO;
+typedef struct PgStat_LockEntry
+{
+ PgStat_Counter requests;
+ PgStat_Counter timeouts;
+ PgStat_Counter deadlock_timeouts;
+ PgStat_Counter fastpath;
+} 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;
@@ -614,6 +634,16 @@ 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_requests(uint8 locktag_type);
+extern void pgstat_count_lock_timeouts(uint8 locktag_type);
+extern void pgstat_count_lock_deadlock_timeouts(uint8 locktag_type);
+extern void pgstat_count_lock_fastpath(uint8 locktag_type);
+
/*
* Functions in pgstat_database.c
*/
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 9b8fbae00ed..97704421a92 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -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
diff --git a/src/include/utils/pgstat_kind.h b/src/include/utils/pgstat_kind.h
index c30b6235623..2d78a029683 100644
--- a/src/include/utils/pgstat_kind.h
+++ b/src/include/utils/pgstat_kind.h
@@ -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/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 6e2d876a40f..cd3688524af 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2255,6 +2255,7 @@ PgStatShared_Database
PgStatShared_Function
PgStatShared_HashEntry
PgStatShared_IO
+PgStatShared_Lock
PgStatShared_Relation
PgStatShared_ReplSlot
PgStatShared_SLRU
@@ -2277,8 +2278,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
--
2.34.1
[text/x-diff] v4-0002-Add-the-pg_stat_lock-view.patch (15.9K, ../../aY779Gu3rQwJ4Erq@ip-10-97-1-34.eu-west-3.compute.internal/3-v4-0002-Add-the-pg_stat_lock-view.patch)
download | inline diff:
From f7b2a14597830286b08bc8a78722c96fc73cafd6 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Thu, 31 Jul 2025 09:35:31 +0000
Subject: [PATCH v4 2/2] Add the pg_stat_lock view
This new view reports lock statistics.
This commit also adds documentation and a few tests.
XXX: Bump catversion
---
doc/src/sgml/monitoring.sgml | 117 ++++++++++++++++++
src/backend/catalog/system_views.sql | 10 ++
src/backend/utils/activity/pgstat_lock.c | 8 ++
src/backend/utils/adt/pgstatfuncs.c | 40 ++++++
src/include/catalog/pg_proc.dat | 9 ++
src/include/pgstat.h | 1 +
src/test/isolation/expected/deadlock-hard.out | 20 ++-
src/test/isolation/specs/deadlock-hard.spec | 5 +-
src/test/regress/expected/advisory_lock.out | 18 +++
src/test/regress/expected/rules.out | 7 ++
src/test/regress/sql/advisory_lock.sql | 4 +
11 files changed, 237 insertions(+), 2 deletions(-)
46.2% doc/src/sgml/
3.2% src/backend/catalog/
16.3% src/backend/utils/adt/
6.2% src/include/catalog/
9.3% src/test/isolation/expected/
6.7% src/test/isolation/specs/
7.3% src/test/regress/expected/
4.4% src/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index b77d189a500..1cba84c1542 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -493,6 +493,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
@@ -3124,6 +3133,108 @@ 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_stat_lock</structname></link> for details.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>requests</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of requests for this lock type.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>timeouts</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of times requests for this lock type had to wait longer
+ than <varname>lock_timeout</varname>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>deadlock_timeouts</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of times requests for this lock type had to wait longer
+ than <varname>deadlock_timeout</varname>.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>fastpath</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of times this lock type was taken via fast path.
+ </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>
@@ -5195,6 +5306,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
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 1ea8f1faa9e..a51ae67de05 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -985,6 +985,16 @@ 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.requests,
+ l.timeouts,
+ l.deadlock_timeouts,
+ l.fastpath,
+ l.stats_reset
+ FROM pg_stat_get_lock() l;
+
CREATE VIEW pg_stat_wal_receiver AS
SELECT
s.pid,
diff --git a/src/backend/utils/activity/pgstat_lock.c b/src/backend/utils/activity/pgstat_lock.c
index 110a370b875..e26e24c260c 100644
--- a/src/backend/utils/activity/pgstat_lock.c
+++ b/src/backend/utils/activity/pgstat_lock.c
@@ -21,6 +21,14 @@
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()
*/
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index b1df96e7b0b..6a699da8bf0 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1737,6 +1737,43 @@ 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 6
+ 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->requests);
+ values[i++] = Int64GetDatum(lck_stats->timeouts);
+ values[i++] = Int64GetDatum(lck_stats->deadlock_timeouts);
+ values[i++] = Int64GetDatum(lck_stats->fastpath);
+ 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.
*/
@@ -1921,6 +1958,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);
@@ -1938,6 +1976,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)
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 83f6501df38..e208ebdbbc3 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6026,6 +6026,15 @@
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,int8,timestamptz}',
+ proargmodes => '{o,o,o,o,o,o}',
+ proargnames => '{locktype,requests,timeouts,deadlock_timeouts,fastpath,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',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8460e2db159..b06a261572b 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -643,6 +643,7 @@ extern void pgstat_count_lock_requests(uint8 locktag_type);
extern void pgstat_count_lock_timeouts(uint8 locktag_type);
extern void pgstat_count_lock_deadlock_timeouts(uint8 locktag_type);
extern void pgstat_count_lock_fastpath(uint8 locktag_type);
+extern PgStat_Lock *pgstat_fetch_stat_lock(void);
/*
* Functions in pgstat_database.c
diff --git a/src/test/isolation/expected/deadlock-hard.out b/src/test/isolation/expected/deadlock-hard.out
index 460653f2b86..5ae5a8bf953 100644
--- a/src/test/isolation/expected/deadlock-hard.out
+++ b/src/test/isolation/expected/deadlock-hard.out
@@ -1,6 +1,12 @@
Parsed test spec with 8 sessions
-starting permutation: s1a1 s2a2 s3a3 s4a4 s5a5 s6a6 s7a7 s8a8 s1a2 s2a3 s3a4 s4a5 s5a6 s6a7 s7a8 s8a1 s8c s7c s6c s5c s4c s3c s2c s1c
+starting permutation: s1rl s1a1 s2a2 s3a3 s4a4 s5a5 s6a6 s7a7 s8a8 s1a2 s2a3 s3a4 s4a5 s5a6 s6a7 s7a8 s8a1 s8c s8f s7c s6c s5c s4c s3c s2c s1c s1sl
+step s1rl: SELECT pg_stat_reset_shared('lock');
+pg_stat_reset_shared
+--------------------
+
+(1 row)
+
step s1a1: LOCK TABLE a1;
step s2a2: LOCK TABLE a2;
step s3a3: LOCK TABLE a3;
@@ -21,6 +27,12 @@ step s8a1: <... completed>
ERROR: deadlock detected
step s7a8: <... completed>
step s8c: COMMIT;
+step s8f: SELECT pg_stat_force_next_flush();
+pg_stat_force_next_flush
+------------------------
+
+(1 row)
+
step s7c: COMMIT;
step s6a7: <... completed>
step s6c: COMMIT;
@@ -34,3 +46,9 @@ step s2a3: <... completed>
step s2c: COMMIT;
step s1a2: <... completed>
step s1c: COMMIT;
+step s1sl: SELECT deadlock_timeouts > 0 FROM pg_stat_lock WHERE locktype = 'relation';
+?column?
+--------
+t
+(1 row)
+
diff --git a/src/test/isolation/specs/deadlock-hard.spec b/src/test/isolation/specs/deadlock-hard.spec
index 60bedca237a..d9ed99836e3 100644
--- a/src/test/isolation/specs/deadlock-hard.spec
+++ b/src/test/isolation/specs/deadlock-hard.spec
@@ -25,6 +25,8 @@ setup { BEGIN; SET deadlock_timeout = '100s'; }
step s1a1 { LOCK TABLE a1; }
step s1a2 { LOCK TABLE a2; }
step s1c { COMMIT; }
+step s1sl { SELECT deadlock_timeouts > 0 FROM pg_stat_lock WHERE locktype = 'relation'; }
+step s1rl { SELECT pg_stat_reset_shared('lock'); }
session s2
setup { BEGIN; SET deadlock_timeout = '100s'; }
@@ -67,6 +69,7 @@ setup { BEGIN; SET deadlock_timeout = '10ms'; }
step s8a8 { LOCK TABLE a8; }
step s8a1 { LOCK TABLE a1; }
step s8c { COMMIT; }
+step s8f { SELECT pg_stat_force_next_flush(); }
# Note: when s8a1 detects the deadlock and fails, s7a8 is released, making
# it timing-dependent which query completion is received first by the tester.
@@ -76,4 +79,4 @@ step s8c { COMMIT; }
# dummy blocking mark to s8a1 to ensure it will be reported as "waiting"
# regardless of that.
-permutation s1a1 s2a2 s3a3 s4a4 s5a5 s6a6 s7a7 s8a8 s1a2 s2a3 s3a4 s4a5 s5a6 s6a7 s7a8(s8a1) s8a1(*) s8c s7c s6c s5c s4c s3c s2c s1c
+permutation s1rl s1a1 s2a2 s3a3 s4a4 s5a5 s6a6 s7a7 s8a8 s1a2 s2a3 s3a4 s4a5 s5a6 s6a7 s7a8(s8a1) s8a1(*) s8c s8f s7c s6c s5c s4c s3c s2c s1c s1sl
diff --git a/src/test/regress/expected/advisory_lock.out b/src/test/regress/expected/advisory_lock.out
index 02e07765ac2..fdaa1756ba4 100644
--- a/src/test/regress/expected/advisory_lock.out
+++ b/src/test/regress/expected/advisory_lock.out
@@ -2,6 +2,12 @@
-- ADVISORY LOCKS
--
SELECT oid AS datoid FROM pg_database WHERE datname = current_database() \gset
+SELECT pg_stat_reset_shared('lock');
+ pg_stat_reset_shared
+----------------------
+
+(1 row)
+
BEGIN;
SELECT
pg_advisory_xact_lock(1), pg_advisory_xact_lock_shared(2),
@@ -48,6 +54,12 @@ WARNING: you don't own a lock of type ShareLock
f | f | f | f
(1 row)
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
-- automatically release xact locks at commit
COMMIT;
SELECT count(*) FROM pg_locks WHERE locktype = 'advisory' AND database = :datoid;
@@ -56,6 +68,12 @@ SELECT count(*) FROM pg_locks WHERE locktype = 'advisory' AND database = :datoid
0
(1 row)
+SELECT requests FROM pg_stat_lock WHERE locktype = 'advisory';
+ requests
+----------
+ 4
+(1 row)
+
BEGIN;
-- holding both session and xact locks on the same objects, xact first
SELECT
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 78a37d9fc8f..80999465baf 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1951,6 +1951,13 @@ 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,
+ requests,
+ timeouts,
+ deadlock_timeouts,
+ fastpath,
+ stats_reset
+ FROM pg_stat_get_lock() l(locktype, requests, timeouts, deadlock_timeouts, fastpath, stats_reset);
pg_stat_progress_analyze| SELECT s.pid,
s.datid,
d.datname,
diff --git a/src/test/regress/sql/advisory_lock.sql b/src/test/regress/sql/advisory_lock.sql
index 8513ab8e98f..f1bff60fd37 100644
--- a/src/test/regress/sql/advisory_lock.sql
+++ b/src/test/regress/sql/advisory_lock.sql
@@ -4,6 +4,8 @@
SELECT oid AS datoid FROM pg_database WHERE datname = current_database() \gset
+SELECT pg_stat_reset_shared('lock');
+
BEGIN;
SELECT
@@ -26,12 +28,14 @@ SELECT
pg_advisory_unlock(1), pg_advisory_unlock_shared(2),
pg_advisory_unlock(1, 1), pg_advisory_unlock_shared(2, 2);
+SELECT pg_stat_force_next_flush();
-- automatically release xact locks at commit
COMMIT;
SELECT count(*) FROM pg_locks WHERE locktype = 'advisory' AND database = :datoid;
+SELECT requests FROM pg_stat_lock WHERE locktype = 'advisory';
BEGIN;
--
2.34.1
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-02-13 21:13 Andres Freund <andres@anarazel.de>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
0 siblings, 2 replies; 61+ messages in thread
From: Andres Freund @ 2026-02-13 21:13 UTC (permalink / raw)
To: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: Michael Paquier <michael@paquier.xyz>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi,
On 2026-02-13 10:24:52 +0000, Bertrand Drouvot wrote:
> On Fri, Feb 13, 2026 at 04:36:57PM +0900, Michael Paquier wrote:
> > On Tue, Feb 10, 2026 at 07:30:50AM +0000, Bertrand Drouvot wrote:
> > > New rebase due to 73d60ac385a.
> > So my suggestion for the moment would be to be more frugal (yeah I
> > know, sorry..) and limit ourselves to four fields: deadlock_timeout,
> > requests, fastpath and timeouts. Three fields to compare with
> > requests, one for each GUC.
>
> That's fine by me. We could still add the others in the future if we feel the
> need. Done that way in the attached.
I'm not sure that it's unproblematic to add multiple pgstat count calls to
every lock acquisition, particularly if it's a fastpath acquisition or a
virtualxid lock. Notably these are external function calls, not just
increments of a counter in an inline function.
I also don't really know what one would do with some of the information?
What does the number of virtualxid lock acquisitions tell you that the numbers
of transactions doesn't already tell you in a more understandable way?
What does it tell you that the deadlock checker ran N times? It notably
doesn't count deadlocks, it counts how often we checked for deadlocks.
The percentage of fastpath locks also seems not really informative, because
that could be because we ran out of space for fastpath locks, or because a
lock mode that's ineligible for fastpath locks was used.
What I would actually count is the amount of time waiting for locks, that
seems vastly more useful than the number of acquisitions. We already do a
GetCurrentTimestamp() inside the timer activations for deadlock timeout, we
probably can figure out a way to reuse that to reduce the increase in overhead
due to timing. We could also just count the wait time after the deadlock
check has run.
> @@ -17,6 +17,7 @@
> #include "postmaster/pgarch.h" /* for MAX_XFN_CHARS */
> #include "replication/conflict.h"
> #include "replication/worker_internal.h"
> +#include "storage/lock.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"
> @@ -342,6 +343,25 @@ typedef struct PgStat_IO
> PgStat_BktypeIO stats[BACKEND_NUM_TYPES];
> } PgStat_IO;
I don't like the amount of headers this addition will indirectly include in a
lot of places.
I'm also pretty unhappy about an include of access/transam.h recently having
been added. And worker_internal.h quite obviously, given the name, has no
business being included here, and also includes a lot more. Grrr. I'll start
a thread.
Greetings,
Andres Freund
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-02-16 04:18 Michael Paquier <michael@paquier.xyz>
parent: Andres Freund <andres@anarazel.de>
1 sibling, 0 replies; 61+ messages in thread
From: Michael Paquier @ 2026-02-16 04:18 UTC (permalink / raw)
To: Andres Freund <andres@anarazel.de>; +Cc: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
On Fri, Feb 13, 2026 at 04:13:39PM -0500, Andres Freund wrote:
> I'm not sure that it's unproblematic to add multiple pgstat count calls to
> every lock acquisition, particularly if it's a fastpath acquisition or a
> virtualxid lock. Notably these are external function calls, not just
> increments of a counter in an inline function.
Right. I am not completely sure if this is really free, either,
particularly for a fastpath lock that we want to be.. Faster.
> What I would actually count is the amount of time waiting for locks, that
> seems vastly more useful than the number of acquisitions. We already do a
> GetCurrentTimestamp() inside the timer activations for deadlock timeout, we
> probably can figure out a way to reuse that to reduce the increase in overhead
> due to timing. We could also just count the wait time after the deadlock
> check has run.
That sounds like an interesting suggestion, yes. That's not going to
be bounded by performance if we know that we are already waiting.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (832B, ../../aZKamOyj4IHcSDZ4@paquier.xyz/2-signature.asc)
download
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-02-16 10:10 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Andres Freund <andres@anarazel.de>
1 sibling, 1 reply; 61+ messages in thread
From: Bertrand Drouvot @ 2026-02-16 10:10 UTC (permalink / raw)
To: Andres Freund <andres@anarazel.de>; +Cc: Michael Paquier <michael@paquier.xyz>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi,
On Fri, Feb 13, 2026 at 04:13:39PM -0500, Andres Freund wrote:
> Hi,
>
> On 2026-02-13 10:24:52 +0000, Bertrand Drouvot wrote:
> >
> > That's fine by me. We could still add the others in the future if we feel the
> > need. Done that way in the attached.
>
> I'm not sure that it's unproblematic to add multiple pgstat count calls to
> every lock acquisition, particularly if it's a fastpath acquisition or a
> virtualxid lock. Notably these are external function calls, not just
> increments of a counter in an inline function.
Yeah, we could create inline functions instead.
> I also don't really know what one would do with some of the information?
>
> What does the number of virtualxid lock acquisitions tell you that the numbers
> of transactions doesn't already tell you in a more understandable way?
I agree not that much, except being able to compute a transactions/virtualxid
ratio or such. Also the idea was to report the same as pg_locks so that one
could start sampling pg_locks if he needs more details.
> What does it tell you that the deadlock checker ran N times? It notably
> doesn't count deadlocks, it counts how often we checked for deadlocks.
>
> The percentage of fastpath locks also seems not really informative, because
> that could be because we ran out of space for fastpath locks, or because a
> lock mode that's ineligible for fastpath locks was used.
Right, maybe we could add an "exceed fastpath slots" or such counter?
> What I would actually count is the amount of time waiting for locks, that
> seems vastly more useful than the number of acquisitions. We already do a
> GetCurrentTimestamp() inside the timer activations for deadlock timeout, we
> probably can figure out a way to reuse that to reduce the increase in overhead
> due to timing. We could also just count the wait time after the deadlock
> check has run.
Yeah, providing the wait_time would be great. Just to be sure, are you suggesting
to remove all the fields (i.e requests, timeouts, deadlock_timeouts and fastpath)
and just add a wait_time field instead? I think that keeping requests would make
sense to be able to get the average wait time per request.
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-02-16 21:00 Andres Freund <andres@anarazel.de>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
0 siblings, 1 reply; 61+ messages in thread
From: Andres Freund @ 2026-02-16 21:00 UTC (permalink / raw)
To: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: Michael Paquier <michael@paquier.xyz>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi,
On 2026-02-16 10:10:21 +0000, Bertrand Drouvot wrote:
> On Fri, Feb 13, 2026 at 04:13:39PM -0500, Andres Freund wrote:
> > On 2026-02-13 10:24:52 +0000, Bertrand Drouvot wrote:
> > >
> > > That's fine by me. We could still add the others in the future if we feel the
> > > need. Done that way in the attached.
> >
> > I'm not sure that it's unproblematic to add multiple pgstat count calls to
> > every lock acquisition, particularly if it's a fastpath acquisition or a
> > virtualxid lock. Notably these are external function calls, not just
> > increments of a counter in an inline function.
>
> Yeah, we could create inline functions instead.
I think unless you have a heck of a lot more of a usecase than "it's
information", counting every lock acqusition seems just worth ti.
> > I also don't really know what one would do with some of the information?
> >
> > What does the number of virtualxid lock acquisitions tell you that the numbers
> > of transactions doesn't already tell you in a more understandable way?
>
> I agree not that much, except being able to compute a transactions/virtualxid
> ratio or such. Also the idea was to report the same as pg_locks so that one
> could start sampling pg_locks if he needs more details.
I don't think that's worth adding overhead to something extremely frequently
happening.
> > What does it tell you that the deadlock checker ran N times? It notably
> > doesn't count deadlocks, it counts how often we checked for deadlocks.
> >
> > The percentage of fastpath locks also seems not really informative, because
> > that could be because we ran out of space for fastpath locks, or because a
> > lock mode that's ineligible for fastpath locks was used.
>
> Right, maybe we could add an "exceed fastpath slots" or such counter?
That I could get behind.
> > What I would actually count is the amount of time waiting for locks, that
> > seems vastly more useful than the number of acquisitions. We already do a
> > GetCurrentTimestamp() inside the timer activations for deadlock timeout, we
> > probably can figure out a way to reuse that to reduce the increase in overhead
> > due to timing. We could also just count the wait time after the deadlock
> > check has run.
>
> Yeah, providing the wait_time would be great. Just to be sure, are you suggesting
> to remove all the fields (i.e requests, timeouts, deadlock_timeouts and fastpath)
> and just add a wait_time field instead?
Well, I'd maybe make it waits, wait_time and perhaps fastpath exceeded.
> I think that keeping requests would make sense to be able to get the average
> wait time per request.
I don't think I'd request for that (as that would require counting in the
normal case), I'd use the number of waits.
I don't think you have provided any actual motivation for why it's worth the
expense to track the number of lock requests. Just trackings stats that
nobody has a usecase for that are not free to collect makes no sense.
Greetings,
Andres Freund
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-02-17 16:33 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Andres Freund <andres@anarazel.de>
0 siblings, 1 reply; 61+ messages in thread
From: Bertrand Drouvot @ 2026-02-17 16:33 UTC (permalink / raw)
To: Andres Freund <andres@anarazel.de>; +Cc: Michael Paquier <michael@paquier.xyz>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi,
On Mon, Feb 16, 2026 at 04:00:56PM -0500, Andres Freund wrote:
> Hi,
>
> On 2026-02-16 10:10:21 +0000, Bertrand Drouvot wrote:
> > Yeah, providing the wait_time would be great. Just to be sure, are you suggesting
> > to remove all the fields (i.e requests, timeouts, deadlock_timeouts and fastpath)
> > and just add a wait_time field instead?
>
> Well, I'd maybe make it waits, wait_time and perhaps fastpath exceeded.
Okay, done that way in the attached. To avoid overhead due to timing as much as
possible, the patch simply relies on log_lock_waits and deadlock_timeout. It means
that it relies on the existing code, and increments waits and wait_time only if
log_lock_waits is on and if the session waited longer than deadlock_timeout.
I did not want to dissociate the waits and wait_time increments so that their
ratio could still make sense.
That sounds like a good compromise, thoughts?
> > I think that keeping requests would make sense to be able to get the average
> > wait time per request.
>
> I don't think I'd request for that (as that would require counting in the
> normal case), I'd use the number of waits.
Yeah, I meant to say waits and not requests.
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
Attachments:
[text/x-diff] v5-0001-Add-lock-statistics.patch (15.7K, ../../aZSYcsRmqh7grqJb@ip-10-97-1-34.eu-west-3.compute.internal/2-v5-0001-Add-lock-statistics.patch)
download | inline diff:
From b33d6101c912d1d6ce77cf5d95161ba0b6c10874 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Tue, 29 Jul 2025 08:36:35 +0000
Subject: [PATCH v5 1/2] Add lock statistics
Adding a new stat kind PGSTAT_KIND_LOCK for the lock statistics.
This new statistic kind is a fixed one because its key is the lock type
so that we know its size is LOCKTAG_LAST_TYPE + 1.
This statistic kind records the following counters:
waits
wait_time
fastpath_exceeded
The waits and wait_time counters are incremented if log_lock_waits is on and
the session waited longer than deadlock_timeout to acquire the lock.
No extra details is added (like the ones, i.e relation oid, database oid, we
can find in pg_locks). The idea is to provide an idea on what the locking
behaviour looks like.
XXX: Bump stat file format
---
src/backend/storage/lmgr/lock.c | 58 ++++----
src/backend/storage/lmgr/proc.c | 6 +
src/backend/utils/activity/Makefile | 1 +
src/backend/utils/activity/meson.build | 1 +
src/backend/utils/activity/pgstat.c | 18 +++
src/backend/utils/activity/pgstat_lock.c | 162 +++++++++++++++++++++++
src/include/pgstat.h | 28 ++++
src/include/utils/pgstat_internal.h | 21 +++
src/include/utils/pgstat_kind.h | 5 +-
src/tools/pgindent/typedefs.list | 4 +
10 files changed, 276 insertions(+), 28 deletions(-)
28.9% src/backend/storage/lmgr/
54.5% src/backend/utils/activity/
7.4% src/include/utils/
8.2% src/include/
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index e1168ad3837..8a0d970b26b 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -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"
@@ -984,37 +985,42 @@ LockAcquireExtended(const LOCKTAG *locktag,
* lock type on a relation we have already locked using the fast-path, but
* for now we don't worry about that case either.
*/
- if (EligibleForRelationFastPath(locktag, lockmode) &&
- FastPathLocalUseCounts[FAST_PATH_REL_GROUP(locktag->locktag_field2)] < FP_LOCK_SLOTS_PER_GROUP)
+ if (EligibleForRelationFastPath(locktag, lockmode))
{
- uint32 fasthashcode = FastPathStrongLockHashPartition(hashcode);
- bool acquired;
-
- /*
- * LWLockAcquire acts as a memory sequencing point, so it's safe to
- * assume that any strong locker whose increment to
- * FastPathStrongRelationLocks->counts becomes visible after we test
- * it has yet to begin to transfer fast-path locks.
- */
- LWLockAcquire(&MyProc->fpInfoLock, LW_EXCLUSIVE);
- if (FastPathStrongRelationLocks->count[fasthashcode] != 0)
- acquired = false;
- else
- acquired = FastPathGrantRelationLock(locktag->locktag_field2,
- lockmode);
- LWLockRelease(&MyProc->fpInfoLock);
- if (acquired)
+ if (FastPathLocalUseCounts[FAST_PATH_REL_GROUP(locktag->locktag_field2)] <
+ FP_LOCK_SLOTS_PER_GROUP)
{
+ uint32 fasthashcode = FastPathStrongLockHashPartition(hashcode);
+ bool acquired;
+
/*
- * The locallock might contain stale pointers to some old shared
- * objects; we MUST reset these to null before considering the
- * lock to be acquired via fast-path.
+ * LWLockAcquire acts as a memory sequencing point, so it's safe
+ * to assume that any strong locker whose increment to
+ * FastPathStrongRelationLocks->counts becomes visible after we
+ * test it has yet to begin to transfer fast-path locks.
*/
- locallock->lock = NULL;
- locallock->proclock = NULL;
- GrantLockLocal(locallock, owner);
- return LOCKACQUIRE_OK;
+ LWLockAcquire(&MyProc->fpInfoLock, LW_EXCLUSIVE);
+ if (FastPathStrongRelationLocks->count[fasthashcode] != 0)
+ acquired = false;
+ else
+ acquired = FastPathGrantRelationLock(locktag->locktag_field2,
+ lockmode);
+ LWLockRelease(&MyProc->fpInfoLock);
+ if (acquired)
+ {
+ /*
+ * The locallock might contain stale pointers to some old
+ * shared objects; we MUST reset these to null before
+ * considering the lock to be acquired via fast-path.
+ */
+ locallock->lock = NULL;
+ locallock->proclock = NULL;
+ GrantLockLocal(locallock, owner);
+ return LOCKACQUIRE_OK;
+ }
}
+ else
+ pgstat_count_lock_fastpath_exceeded(locallock->tag.lock.locktag_type);
}
/*
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index fd8318bdf3d..81d20e7ae76 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -1609,9 +1609,15 @@ ProcSleep(LOCALLOCK *locallock)
"Processes holding the lock: %s. Wait queue: %s.",
lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
else if (myWaitStatus == PROC_WAIT_STATUS_OK)
+ {
+ /* Increment the lock statistics counters */
+ pgstat_count_lock_waits(locallock->tag.lock.locktag_type);
+ pgstat_count_lock_wait_time(locallock->tag.lock.locktag_type, msecs);
+
ereport(LOG,
(errmsg("process %d acquired %s on %s after %ld.%03d ms",
MyProcPid, modename, buf.data, msecs, usecs)));
+ }
else
{
Assert(myWaitStatus == PROC_WAIT_STATUS_ERROR);
diff --git a/src/backend/utils/activity/Makefile b/src/backend/utils/activity/Makefile
index c37bfb350bb..ca3ef89bf59 100644
--- a/src/backend/utils/activity/Makefile
+++ b/src/backend/utils/activity/Makefile
@@ -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 \
diff --git a/src/backend/utils/activity/meson.build b/src/backend/utils/activity/meson.build
index 53bd5a246ca..1aa7ece5290 100644
--- a/src/backend/utils/activity/meson.build
+++ b/src/backend/utils/activity/meson.build
@@ -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',
diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index 11bb71cad5a..eb8ccbaa628 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -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
index 00000000000..7af4bc6d018
--- /dev/null
+++ b/src/backend/utils/activity/pgstat_lock.c
@@ -0,0 +1,162 @@
+/* -------------------------------------------------------------------------
+ *
+ * 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-2025, 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;
+
+/*
+ * 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);
+ }
+}
+
+#define PGSTAT_COUNT_LOCK_FUNC(stat) \
+void \
+CppConcat(pgstat_count_lock_,stat)(uint8 locktag_type) \
+{ \
+ Assert(locktag_type <= LOCKTAG_LAST_TYPE); \
+ PendingLockStats.stats[locktag_type].stat++; \
+ have_lockstats = true; \
+ pgstat_report_fixed = true; \
+}
+
+/* pgstat_count_lock_waits */
+PGSTAT_COUNT_LOCK_FUNC(waits)
+
+/* pgstat_count_lock_fastpath_exceeded */
+PGSTAT_COUNT_LOCK_FUNC(fastpath_exceeded)
+
+void
+pgstat_count_lock_wait_time(uint8 locktag_type, long msecs)
+{
+ Assert(locktag_type <= LOCKTAG_LAST_TYPE);
+ PendingLockStats.stats[locktag_type].wait_time += (PgStat_Counter) msecs;
+ have_lockstats = true;
+ pgstat_report_fixed = true;
+}
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index fff7ecc2533..dfbead474b1 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -17,6 +17,7 @@
#include "postmaster/pgarch.h" /* for MAX_XFN_CHARS */
#include "replication/conflict.h"
#include "replication/worker_internal.h"
+#include "storage/lock.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"
@@ -342,6 +343,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;
@@ -614,6 +633,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_waits(uint8 locktag_type);
+extern void pgstat_count_lock_fastpath_exceeded(uint8 locktag_type);
+extern void pgstat_count_lock_wait_time(uint8 locktag_type, long msecs);
+
/*
* Functions in pgstat_database.c
*/
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 9b8fbae00ed..97704421a92 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -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
diff --git a/src/include/utils/pgstat_kind.h b/src/include/utils/pgstat_kind.h
index c30b6235623..2d78a029683 100644
--- a/src/include/utils/pgstat_kind.h
+++ b/src/include/utils/pgstat_kind.h
@@ -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/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 241945734ec..0432862061b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2255,6 +2255,7 @@ PgStatShared_Database
PgStatShared_Function
PgStatShared_HashEntry
PgStatShared_IO
+PgStatShared_Lock
PgStatShared_Relation
PgStatShared_ReplSlot
PgStatShared_SLRU
@@ -2277,8 +2278,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
--
2.34.1
[text/x-diff] v5-0002-Add-the-pg_stat_lock-view.patch (26.6K, ../../aZSYcsRmqh7grqJb@ip-10-97-1-34.eu-west-3.compute.internal/3-v5-0002-Add-the-pg_stat_lock-view.patch)
download | inline diff:
From 85f170e565302f73e97cbbc5d7e20eeae206c6e9 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Thu, 31 Jul 2025 09:35:31 +0000
Subject: [PATCH v5 2/2] Add the pg_stat_lock view
This new view reports lock statistics.
Note that it does not omit combinations which do not make sense (as pg_locks
does).
Also wait_time is reported as bigint as the deadlock_timeout default value is
1s.
This commit also adds documentation and a few tests.
XXX: Bump catversion
---
doc/src/sgml/monitoring.sgml | 113 +++++++++++++++++
src/backend/catalog/system_views.sql | 9 ++
src/backend/utils/activity/pgstat_lock.c | 8 ++
src/backend/utils/adt/pgstatfuncs.c | 39 ++++++
src/include/catalog/pg_proc.dat | 9 ++
src/include/pgstat.h | 1 +
src/test/isolation/expected/stats.out | 147 +++++++++++++++++++++++
src/test/isolation/expected/stats_1.out | 147 +++++++++++++++++++++++
src/test/isolation/specs/stats.spec | 76 ++++++++++++
src/test/regress/expected/rules.out | 6 +
src/test/regress/expected/stats.out | 42 +++++++
src/test/regress/sql/stats.sql | 30 +++++
12 files changed, 627 insertions(+)
18.8% doc/src/sgml/
5.9% src/backend/utils/adt/
49.4% src/test/isolation/expected/
11.3% src/test/isolation/specs/
6.0% src/test/regress/expected/
4.1% src/test/regress/sql/
4.3% src/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index b77d189a500..3a196bc305c 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -493,6 +493,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
@@ -3124,6 +3133,104 @@ 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 <xref linkend="guc-log-lock-waits"/>
+ is enabled and 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 <xref linkend="guc-log-lock-waits"/> is enabled and
+ 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. You may want to increase
+ <xref linkend="guc-max-locks-per-transaction"/> if you feel this counter
+ is too high.
+ </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>
@@ -5195,6 +5302,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
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 1ea8f1faa9e..22efeb713e4 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -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,
diff --git a/src/backend/utils/activity/pgstat_lock.c b/src/backend/utils/activity/pgstat_lock.c
index 7af4bc6d018..cd4399b5ef6 100644
--- a/src/backend/utils/activity/pgstat_lock.c
+++ b/src/backend/utils/activity/pgstat_lock.c
@@ -21,6 +21,14 @@
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()
*/
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index b1df96e7b0b..27ca2d8f07c 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1737,6 +1737,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.
*/
@@ -1921,6 +1957,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);
@@ -1938,6 +1975,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)
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 83f6501df38..c224e842e32 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6026,6 +6026,15 @@
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',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index dfbead474b1..175e431ed81 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -641,6 +641,7 @@ extern void pgstat_lock_flush(bool nowait);
extern void pgstat_count_lock_waits(uint8 locktag_type);
extern void pgstat_count_lock_fastpath_exceeded(uint8 locktag_type);
extern void pgstat_count_lock_wait_time(uint8 locktag_type, long msecs);
+extern PgStat_Lock *pgstat_fetch_stat_lock(void);
/*
* Functions in pgstat_database.c
diff --git a/src/test/isolation/expected/stats.out b/src/test/isolation/expected/stats.out
index cfad309ccf3..d3cb8715d95 100644
--- a/src/test/isolation/expected/stats.out
+++ b/src/test/isolation/expected/stats.out
@@ -3751,3 +3751,150 @@ test_stat_func| 1|t |t
(1 row)
step s1_commit: COMMIT;
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits s2_set_deadlock_timeout s2_set_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 s1_set_log_lock_waits: SET log_lock_waits = on;
+step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
+step s2_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'relation';
+?column?|?column?
+--------+--------
+t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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 s1_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'transactionid';
+?column?|?column?
+--------+--------
+t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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 s1_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'advisory';
+?column?|?column?
+--------+--------
+t |t
+(1 row)
+
diff --git a/src/test/isolation/expected/stats_1.out b/src/test/isolation/expected/stats_1.out
index e1d937784cb..89d2e4cc6b4 100644
--- a/src/test/isolation/expected/stats_1.out
+++ b/src/test/isolation/expected/stats_1.out
@@ -3775,3 +3775,150 @@ test_stat_func| 1|t |t
(1 row)
step s1_commit: COMMIT;
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits s2_set_deadlock_timeout s2_set_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 s1_set_log_lock_waits: SET log_lock_waits = on;
+step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
+step s2_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'relation';
+?column?|?column?
+--------+--------
+t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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 s1_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'transactionid';
+?column?|?column?
+--------+--------
+t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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 s1_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'advisory';
+?column?|?column?
+--------+--------
+t |t
+(1 row)
+
diff --git a/src/test/isolation/specs/stats.spec b/src/test/isolation/specs/stats.spec
index da16710da0f..c600fc11157 100644
--- a/src/test/isolation/specs/stats.spec
+++ b/src/test/isolation/specs/stats.spec
@@ -130,6 +130,14 @@ step s1_slru_check_stats {
WHERE before.stat = 'blks_zeroed';
}
+# Lock stats steps
+step s1_set_deadlock_timeout { SET deadlock_timeout = '10ms'; }
+step s1_set_log_lock_waits { SET log_lock_waits = on; }
+step s1_reset_stat_lock { SELECT pg_stat_reset_shared('lock'); }
+step s1_sleep { SELECT pg_sleep(0.5); }
+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'; }
@@ -164,6 +172,15 @@ step s2_big_notify { SELECT pg_notify('stats_test_use',
repeat(i::text, current_setting('block_size')::int / 2)) FROM generate_series(1, 3) g(i);
}
+# Lock stats steps
+step s2_set_deadlock_timeout { SET deadlock_timeout = '10ms'; }
+step s2_set_log_lock_waits { SET log_lock_waits = on; }
+step s2_report_stat_lock_relation { SELECT waits > 0, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'relation'; }
+step s2_report_stat_lock_transactionid { SELECT waits > 0, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'transactionid'; }
+step s2_report_stat_lock_advisory { SELECT waits > 0, wait_time > 500 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); }
######################
# Function stats tests
@@ -765,3 +782,62 @@ permutation
s1_clear_snapshot
s1_func_stats
s1_commit
+
+######################
+# Lock stats tests
+######################
+
+# relation lock
+
+permutation
+ s1_set_deadlock_timeout
+ s1_reset_stat_lock
+ s1_set_log_lock_waits
+ s2_set_deadlock_timeout
+ s2_set_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
+
+# transaction lock
+
+permutation
+ s1_set_deadlock_timeout
+ s1_reset_stat_lock
+ s1_set_log_lock_waits
+ 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
+ s1_set_log_lock_waits
+ 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
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 78a37d9fc8f..c013fbebb88 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1951,6 +1951,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,
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index cd00f35bf7a..ddb016e73d2 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -1910,4 +1910,46 @@ 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 70 partitions (exceeds default number of slots)
+DO $$
+BEGIN
+ FOR i IN 1..70 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
diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql
index 8768e0f27fd..fac7d734758 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -944,4 +944,34 @@ 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 70 partitions (exceeds default number of slots)
+DO $$
+BEGIN
+ FOR i IN 1..70 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
--
2.34.1
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-02-19 04:06 Michael Paquier <michael@paquier.xyz>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
0 siblings, 2 replies; 61+ messages in thread
From: Michael Paquier @ 2026-02-19 04:06 UTC (permalink / raw)
To: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: Andres Freund <andres@anarazel.de>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
On Tue, Feb 17, 2026 at 04:33:54PM +0000, Bertrand Drouvot wrote:
> Okay, done that way in the attached. To avoid overhead due to timing as much as
> possible, the patch simply relies on log_lock_waits and deadlock_timeout. It means
> that it relies on the existing code, and increments waits and wait_time only if
> log_lock_waits is on and if the session waited longer than deadlock_timeout.
>
> I did not want to dissociate the waits and wait_time increments so that their
> ratio could still make sense.
>
> That sounds like a good compromise, thoughts?
else if (myWaitStatus == PROC_WAIT_STATUS_OK)
+ {
+ /* Increment the lock statistics counters */
+ pgstat_count_lock_waits(locallock->tag.lock.locktag_type);
+ pgstat_count_lock_wait_time(locallock->tag.lock.locktag_type, msecs);
Not sure that it makes much sense to me to rely on log_lock_waits
being enabled to decide if this count and this time are aggregated.
The log information and the stats gathering are two separate things.
Wouldn't it make more sense to call pgstat_count_lock_waits() outside
of this code path, when we know myWaitStatus?
While relying on the time calculating for the logs data is a good
idea, it seems to me that we should have a separate GUC to enable this
number, like a new track_lock_timings? If track_lock_timings or
log_lock_waits is enabled, we should calculate the time difference.
All these decisions also depends on what deadlock_state holds on top
of myWaitStatus, I guess..
--
Michael
Attachments:
[application/pgp-signature] signature.asc (832B, ../../aZaMXK90sRjSQBJP@paquier.xyz/2-signature.asc)
download
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-02-19 12:03 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Michael Paquier <michael@paquier.xyz>
1 sibling, 0 replies; 61+ messages in thread
From: Bertrand Drouvot @ 2026-02-19 12:03 UTC (permalink / raw)
To: Michael Paquier <michael@paquier.xyz>; +Cc: Andres Freund <andres@anarazel.de>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi,
On Thu, Feb 19, 2026 at 01:06:52PM +0900, Michael Paquier wrote:
> On Tue, Feb 17, 2026 at 04:33:54PM +0000, Bertrand Drouvot wrote:
> > Okay, done that way in the attached. To avoid overhead due to timing as much as
> > possible, the patch simply relies on log_lock_waits and deadlock_timeout. It means
> > that it relies on the existing code, and increments waits and wait_time only if
> > log_lock_waits is on and if the session waited longer than deadlock_timeout.
> >
> > I did not want to dissociate the waits and wait_time increments so that their
> > ratio could still make sense.
> >
> > That sounds like a good compromise, thoughts?
>
> else if (myWaitStatus == PROC_WAIT_STATUS_OK)
> + {
> + /* Increment the lock statistics counters */
> + pgstat_count_lock_waits(locallock->tag.lock.locktag_type);
> + pgstat_count_lock_wait_time(locallock->tag.lock.locktag_type, msecs);
>
> Not sure that it makes much sense to me to rely on log_lock_waits
> being enabled to decide if this count and this time are aggregated.
> The log information and the stats gathering are two separate things.
> Wouldn't it make more sense to call pgstat_count_lock_waits() outside
> of this code path, when we know myWaitStatus?
> While relying on the time calculating for the logs data is a good
> idea, it seems to me that we should have a separate GUC to enable this
> number, like a new track_lock_timings? If track_lock_timings or
> log_lock_waits is enabled, we should calculate the time difference.
> All these decisions also depends on what deadlock_state holds on top
> of myWaitStatus, I guess..
The idea was to avoid adding a new GUC and I did not want to increment the
waits independently of the wait time (so that wait time/waits could make
sense).
That said, your point of view also makes (more) sense, so in the attached:
- adds a new GUC (namely track_lock_timing)
- tracks the wait_time if the GUC is on and the session waited longer than
deadlock_timeout
- when wait_time is incremented, then a new timed_waits counter is also
incremented (so that wait_time / timed_waits makes sense)
- waits is incremented unconditionally
Note that due to the new GUC behavior (wait_time incremented only if we waited
longer than deadlock_timeout), then it is on by default (same idea as for
2aac62be8cb).
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
Attachments:
[text/x-diff] v6-0001-Add-lock-statistics.patch (26.5K, ../../aZb8FC%2FBlMuZ4CYM@ip-10-97-1-34.eu-west-3.compute.internal/2-v6-0001-Add-lock-statistics.patch)
download | inline diff:
From 0be6d3ac906aad862bcaa4a62fe07423e054f65c Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Tue, 29 Jul 2025 08:36:35 +0000
Subject: [PATCH v6 1/2] Add lock statistics
Adding a new stat kind PGSTAT_KIND_LOCK for the lock statistics.
This new statistic kind is a fixed one because its key is the lock type
so that we know its size is LOCKTAG_LAST_TYPE + 1.
This statistic kind records the following counters:
waits
wait_time
timed_waits
fastpath_exceeded
A new GUC (track_lock_timing) is added and defaults to on. If on, timed_waits and
wait_time counters are incremented if the session waited longer than deadlock_timeout
to acquire the lock. It's on by default, as this is the same idea as 2aac62be8cb.
waits is incremented unconditionally.
fastpath_exceeded is incremented when the lock can not be acquired via fast path
because the fast path slot limit was exceeded.
No extra details is added (like the ones, i.e relation oid, database oid, we
can find in pg_locks). The idea is to provide an idea on what the locking
behaviour looks like.
XXX: Bump stat file format
---
doc/src/sgml/config.sgml | 17 ++
src/backend/storage/lmgr/lock.c | 58 ++++---
src/backend/storage/lmgr/proc.c | 164 ++++++++++--------
src/backend/utils/activity/Makefile | 1 +
src/backend/utils/activity/meson.build | 1 +
src/backend/utils/activity/pgstat.c | 18 ++
src/backend/utils/activity/pgstat_lock.c | 164 ++++++++++++++++++
src/backend/utils/misc/guc_parameters.dat | 6 +
src/backend/utils/misc/postgresql.conf.sample | 1 +
src/include/pgstat.h | 30 ++++
src/include/utils/pgstat_internal.h | 21 +++
src/include/utils/pgstat_kind.h | 5 +-
src/tools/pgindent/typedefs.list | 4 +
13 files changed, 390 insertions(+), 100 deletions(-)
4.2% doc/src/sgml/
56.0% src/backend/storage/lmgr/
29.0% src/backend/utils/activity/
3.8% src/include/utils/
4.7% src/include/
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index faf0bdb62aa..0e868fb03e4 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8844,6 +8844,23 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</listitem>
</varlistentry>
+ <varlistentry id="guc-track-lock-timing" xreflabel="track_lock_timing">
+ <term><varname>track_lock_timing</varname> (<type>boolean</type>)
+ <indexterm>
+ <primary><varname>track_lock_timing</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Enables timing of lock waits. This parameter is on by default, as it tracks
+ only the timings for successful acquisitions that waited longer than
+ <xref linkend="guc-deadlock-timeout"/>.
+ Only superusers and users with the appropriate <literal>SET</literal>
+ privilege can change this setting.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-track-wal-io-timing" xreflabel="track_wal_io_timing">
<term><varname>track_wal_io_timing</varname> (<type>boolean</type>)
<indexterm>
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index e1168ad3837..8a0d970b26b 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -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"
@@ -984,37 +985,42 @@ LockAcquireExtended(const LOCKTAG *locktag,
* lock type on a relation we have already locked using the fast-path, but
* for now we don't worry about that case either.
*/
- if (EligibleForRelationFastPath(locktag, lockmode) &&
- FastPathLocalUseCounts[FAST_PATH_REL_GROUP(locktag->locktag_field2)] < FP_LOCK_SLOTS_PER_GROUP)
+ if (EligibleForRelationFastPath(locktag, lockmode))
{
- uint32 fasthashcode = FastPathStrongLockHashPartition(hashcode);
- bool acquired;
-
- /*
- * LWLockAcquire acts as a memory sequencing point, so it's safe to
- * assume that any strong locker whose increment to
- * FastPathStrongRelationLocks->counts becomes visible after we test
- * it has yet to begin to transfer fast-path locks.
- */
- LWLockAcquire(&MyProc->fpInfoLock, LW_EXCLUSIVE);
- if (FastPathStrongRelationLocks->count[fasthashcode] != 0)
- acquired = false;
- else
- acquired = FastPathGrantRelationLock(locktag->locktag_field2,
- lockmode);
- LWLockRelease(&MyProc->fpInfoLock);
- if (acquired)
+ if (FastPathLocalUseCounts[FAST_PATH_REL_GROUP(locktag->locktag_field2)] <
+ FP_LOCK_SLOTS_PER_GROUP)
{
+ uint32 fasthashcode = FastPathStrongLockHashPartition(hashcode);
+ bool acquired;
+
/*
- * The locallock might contain stale pointers to some old shared
- * objects; we MUST reset these to null before considering the
- * lock to be acquired via fast-path.
+ * LWLockAcquire acts as a memory sequencing point, so it's safe
+ * to assume that any strong locker whose increment to
+ * FastPathStrongRelationLocks->counts becomes visible after we
+ * test it has yet to begin to transfer fast-path locks.
*/
- locallock->lock = NULL;
- locallock->proclock = NULL;
- GrantLockLocal(locallock, owner);
- return LOCKACQUIRE_OK;
+ LWLockAcquire(&MyProc->fpInfoLock, LW_EXCLUSIVE);
+ if (FastPathStrongRelationLocks->count[fasthashcode] != 0)
+ acquired = false;
+ else
+ acquired = FastPathGrantRelationLock(locktag->locktag_field2,
+ lockmode);
+ LWLockRelease(&MyProc->fpInfoLock);
+ if (acquired)
+ {
+ /*
+ * The locallock might contain stale pointers to some old
+ * shared objects; we MUST reset these to null before
+ * considering the lock to be acquired via fast-path.
+ */
+ locallock->lock = NULL;
+ locallock->proclock = NULL;
+ GrantLockLocal(locallock, owner);
+ return LOCKACQUIRE_OK;
+ }
}
+ else
+ pgstat_count_lock_fastpath_exceeded(locallock->tag.lock.locktag_type);
}
/*
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index fd8318bdf3d..2cfd060ee5f 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -62,6 +62,7 @@ int IdleInTransactionSessionTimeout = 0;
int TransactionTimeout = 0;
int IdleSessionTimeout = 0;
bool log_lock_waits = true;
+bool track_lock_timing = true;
/* Pointer to this process's PGPROC struct, if any */
PGPROC *MyProc = NULL;
@@ -1545,93 +1546,110 @@ ProcSleep(LOCALLOCK *locallock)
/*
* If awoken after the deadlock check interrupt has run, and
- * log_lock_waits is on, then report about the wait.
+ * log_lock_waits or track_lock_timing is on, then report or track
+ * about the wait.
*/
- if (log_lock_waits && deadlock_state != DS_NOT_YET_CHECKED)
+ if ((log_lock_waits || track_lock_timing) &&
+ deadlock_state != DS_NOT_YET_CHECKED)
{
- StringInfoData buf,
- lock_waiters_sbuf,
- lock_holders_sbuf;
- const char *modename;
long secs;
int usecs;
long msecs;
- int lockHoldersNum = 0;
- initStringInfo(&buf);
- initStringInfo(&lock_waiters_sbuf);
- initStringInfo(&lock_holders_sbuf);
-
- DescribeLockTag(&buf, &locallock->tag.lock);
- modename = GetLockmodeName(locallock->tag.lock.locktag_lockmethodid,
- lockmode);
TimestampDifference(get_timeout_start_time(DEADLOCK_TIMEOUT),
GetCurrentTimestamp(),
&secs, &usecs);
msecs = secs * 1000 + usecs / 1000;
usecs = usecs % 1000;
- /* Gather a list of all lock holders and waiters */
- LWLockAcquire(partitionLock, LW_SHARED);
- GetLockHoldersAndWaiters(locallock, &lock_holders_sbuf,
- &lock_waiters_sbuf, &lockHoldersNum);
- LWLockRelease(partitionLock);
-
- if (deadlock_state == DS_SOFT_DEADLOCK)
- ereport(LOG,
- (errmsg("process %d avoided deadlock for %s on %s by rearranging queue order after %ld.%03d ms",
- MyProcPid, modename, buf.data, msecs, usecs),
- (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
- "Processes holding the lock: %s. Wait queue: %s.",
- lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
- else if (deadlock_state == DS_HARD_DEADLOCK)
- {
- /*
- * This message is a bit redundant with the error that will be
- * reported subsequently, but in some cases the error report
- * might not make it to the log (eg, if it's caught by an
- * exception handler), and we want to ensure all long-wait
- * events get logged.
- */
- ereport(LOG,
- (errmsg("process %d detected deadlock while waiting for %s on %s after %ld.%03d ms",
- MyProcPid, modename, buf.data, msecs, usecs),
- (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
- "Processes holding the lock: %s. Wait queue: %s.",
- lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
- }
+ /* Collect wait time stats if track_lock_timing is enabled */
+ if (track_lock_timing && myWaitStatus == PROC_WAIT_STATUS_OK)
+ pgstat_count_lock_timed_wait(locallock->tag.lock.locktag_type,
+ msecs);
- if (myWaitStatus == PROC_WAIT_STATUS_WAITING)
- ereport(LOG,
- (errmsg("process %d still waiting for %s on %s after %ld.%03d ms",
- MyProcPid, modename, buf.data, msecs, usecs),
- (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
- "Processes holding the lock: %s. Wait queue: %s.",
- lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
- else if (myWaitStatus == PROC_WAIT_STATUS_OK)
- ereport(LOG,
- (errmsg("process %d acquired %s on %s after %ld.%03d ms",
- MyProcPid, modename, buf.data, msecs, usecs)));
- else
+ if (log_lock_waits)
{
- Assert(myWaitStatus == PROC_WAIT_STATUS_ERROR);
-
- /*
- * Currently, the deadlock checker always kicks its own
- * process, which means that we'll only see
- * PROC_WAIT_STATUS_ERROR when deadlock_state ==
- * DS_HARD_DEADLOCK, and there's no need to print redundant
- * messages. But for completeness and future-proofing, print
- * a message if it looks like someone else kicked us off the
- * lock.
- */
- if (deadlock_state != DS_HARD_DEADLOCK)
+ StringInfoData buf,
+ lock_waiters_sbuf,
+ lock_holders_sbuf;
+ const char *modename;
+ int lockHoldersNum = 0;
+
+ initStringInfo(&buf);
+ initStringInfo(&lock_waiters_sbuf);
+ initStringInfo(&lock_holders_sbuf);
+
+ DescribeLockTag(&buf, &locallock->tag.lock);
+ modename = GetLockmodeName(locallock->tag.lock.locktag_lockmethodid,
+ lockmode);
+
+ /* Gather a list of all lock holders and waiters */
+ LWLockAcquire(partitionLock, LW_SHARED);
+ GetLockHoldersAndWaiters(locallock, &lock_holders_sbuf,
+ &lock_waiters_sbuf, &lockHoldersNum);
+ LWLockRelease(partitionLock);
+
+ if (deadlock_state == DS_SOFT_DEADLOCK)
+ ereport(LOG,
+ (errmsg("process %d avoided deadlock for %s on %s by rearranging queue order after %ld.%03d ms",
+ MyProcPid, modename, buf.data, msecs, usecs),
+ (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
+ "Processes holding the lock: %s. Wait queue: %s.",
+ lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
+ else if (deadlock_state == DS_HARD_DEADLOCK)
+ {
+ /*
+ * This message is a bit redundant with the error that
+ * will be reported subsequently, but in some cases the
+ * error report might not make it to the log (eg, if it's
+ * caught by an exception handler), and we want to ensure
+ * all long-wait events get logged.
+ */
ereport(LOG,
- (errmsg("process %d failed to acquire %s on %s after %ld.%03d ms",
+ (errmsg("process %d detected deadlock while waiting for %s on %s after %ld.%03d ms",
MyProcPid, modename, buf.data, msecs, usecs),
(errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
"Processes holding the lock: %s. Wait queue: %s.",
lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
+ }
+
+ if (myWaitStatus == PROC_WAIT_STATUS_WAITING)
+ ereport(LOG,
+ (errmsg("process %d still waiting for %s on %s after %ld.%03d ms",
+ MyProcPid, modename, buf.data, msecs, usecs),
+ (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
+ "Processes holding the lock: %s. Wait queue: %s.",
+ lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
+
+ else if (myWaitStatus == PROC_WAIT_STATUS_OK)
+ ereport(LOG,
+ (errmsg("process %d acquired %s on %s after %ld.%03d ms",
+ MyProcPid, modename, buf.data, msecs, usecs)));
+ else
+ {
+ Assert(myWaitStatus == PROC_WAIT_STATUS_ERROR);
+
+ /*
+ * Currently, the deadlock checker always kicks its own
+ * process, which means that we'll only see
+ * PROC_WAIT_STATUS_ERROR when deadlock_state ==
+ * DS_HARD_DEADLOCK, and there's no need to print
+ * redundant messages. But for completeness and
+ * future-proofing, print a message if it looks like
+ * someone else kicked us off the lock.
+ */
+ if (deadlock_state != DS_HARD_DEADLOCK)
+ ereport(LOG,
+ (errmsg("process %d failed to acquire %s on %s after %ld.%03d ms",
+ MyProcPid, modename, buf.data, msecs, usecs),
+ (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
+ "Processes holding the lock: %s. Wait queue: %s.",
+ lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
+ }
+
+ pfree(buf.data);
+ pfree(lock_holders_sbuf.data);
+ pfree(lock_waiters_sbuf.data);
}
/*
@@ -1639,13 +1657,15 @@ ProcSleep(LOCALLOCK *locallock)
* state so we don't print the above messages again.
*/
deadlock_state = DS_NO_DEADLOCK;
-
- pfree(buf.data);
- pfree(lock_holders_sbuf.data);
- pfree(lock_waiters_sbuf.data);
}
} while (myWaitStatus == PROC_WAIT_STATUS_WAITING);
+ /*
+ * Count lock waits unconditionally, regardless of log_lock_waits or
+ * track_lock_timing.
+ */
+ pgstat_count_lock_waits(locallock->tag.lock.locktag_type);
+
/*
* Disable the timers, if they are still running. As in LockErrorCleanup,
* we must preserve the LOCK_TIMEOUT indicator flag: if a lock timeout has
diff --git a/src/backend/utils/activity/Makefile b/src/backend/utils/activity/Makefile
index c37bfb350bb..ca3ef89bf59 100644
--- a/src/backend/utils/activity/Makefile
+++ b/src/backend/utils/activity/Makefile
@@ -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 \
diff --git a/src/backend/utils/activity/meson.build b/src/backend/utils/activity/meson.build
index 53bd5a246ca..1aa7ece5290 100644
--- a/src/backend/utils/activity/meson.build
+++ b/src/backend/utils/activity/meson.build
@@ -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',
diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index 11bb71cad5a..eb8ccbaa628 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -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
index 00000000000..b410f376d49
--- /dev/null
+++ b/src/backend/utils/activity/pgstat_lock.c
@@ -0,0 +1,164 @@
+/* -------------------------------------------------------------------------
+ *
+ * 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-2025, 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;
+
+/*
+ * 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(timed_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);
+ }
+}
+
+#define PGSTAT_COUNT_LOCK_FUNC(stat) \
+void \
+CppConcat(pgstat_count_lock_,stat)(uint8 locktag_type) \
+{ \
+ Assert(locktag_type <= LOCKTAG_LAST_TYPE); \
+ PendingLockStats.stats[locktag_type].stat++; \
+ have_lockstats = true; \
+ pgstat_report_fixed = true; \
+}
+
+/* pgstat_count_lock_waits */
+PGSTAT_COUNT_LOCK_FUNC(waits)
+
+/* pgstat_count_lock_fastpath_exceeded */
+PGSTAT_COUNT_LOCK_FUNC(fastpath_exceeded)
+
+void
+pgstat_count_lock_timed_wait(uint8 locktag_type, long msecs)
+{
+ Assert(locktag_type <= LOCKTAG_LAST_TYPE);
+ PendingLockStats.stats[locktag_type].timed_waits++;
+ PendingLockStats.stats[locktag_type].wait_time += (PgStat_Counter) msecs;
+ have_lockstats = true;
+ pgstat_report_fixed = true;
+}
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 271c033952e..2da402691d6 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -3110,6 +3110,12 @@
boot_val => 'false',
},
+{ name => 'track_lock_timing', type => 'bool', context => 'PGC_SUSET', group => 'STATS_CUMULATIVE',
+ short_desc => 'Collects timing statistics for lock acquisition.',
+ variable => 'track_lock_timing',
+ boot_val => 'true',
+},
+
{ name => 'track_wal_io_timing', type => 'bool', context => 'PGC_SUSET', group => 'STATS_CUMULATIVE',
short_desc => 'Collects timing statistics for WAL I/O activity.',
variable => 'track_wal_io_timing',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index f938cc65a3a..8a3a704aaa5 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -685,6 +685,7 @@
#track_counts = on
#track_cost_delay_timing = off
#track_io_timing = off
+#track_lock_timing = on
#track_wal_io_timing = off
#track_functions = none # none, pl, all
#stats_fetch_consistency = cache # cache, none, snapshot
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index fff7ecc2533..615ccf540e6 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -17,6 +17,7 @@
#include "postmaster/pgarch.h" /* for MAX_XFN_CHARS */
#include "replication/conflict.h"
#include "replication/worker_internal.h"
+#include "storage/lock.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"
@@ -342,6 +343,25 @@ typedef struct PgStat_IO
PgStat_BktypeIO stats[BACKEND_NUM_TYPES];
} PgStat_IO;
+typedef struct PgStat_LockEntry
+{
+ PgStat_Counter waits;
+ PgStat_Counter timed_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;
@@ -614,6 +634,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_waits(uint8 locktag_type);
+extern void pgstat_count_lock_fastpath_exceeded(uint8 locktag_type);
+extern void pgstat_count_lock_timed_wait(uint8 locktag_type, long msecs);
+
/*
* Functions in pgstat_database.c
*/
@@ -812,6 +841,7 @@ extern PgStat_WalStats *pgstat_fetch_stat_wal(void);
extern PGDLLIMPORT bool pgstat_track_counts;
extern PGDLLIMPORT int pgstat_track_functions;
extern PGDLLIMPORT int pgstat_fetch_consistency;
+extern PGDLLIMPORT bool track_lock_timing;
/*
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 9b8fbae00ed..97704421a92 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -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
diff --git a/src/include/utils/pgstat_kind.h b/src/include/utils/pgstat_kind.h
index c30b6235623..2d78a029683 100644
--- a/src/include/utils/pgstat_kind.h
+++ b/src/include/utils/pgstat_kind.h
@@ -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/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 241945734ec..0432862061b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2255,6 +2255,7 @@ PgStatShared_Database
PgStatShared_Function
PgStatShared_HashEntry
PgStatShared_IO
+PgStatShared_Lock
PgStatShared_Relation
PgStatShared_ReplSlot
PgStatShared_SLRU
@@ -2277,8 +2278,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
--
2.34.1
[text/x-diff] v6-0002-Add-the-pg_stat_lock-view.patch (31.8K, ../../aZb8FC%2FBlMuZ4CYM@ip-10-97-1-34.eu-west-3.compute.internal/3-v6-0002-Add-the-pg_stat_lock-view.patch)
download | inline diff:
From 3c34ed26df79b3bdf09df6ace6b81e886cd8a052 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Thu, 19 Feb 2026 10:19:12 +0000
Subject: [PATCH v6 2/2] Add the pg_stat_lock view
This new view reports lock statistics.
Note that it does not omit combinations which do not make sense (as pg_locks
does).
Also wait_time is reported as bigint as the deadlock_timeout default value is
1s.
This commit also adds documentation and a few tests.
XXX: Bump catversion
---
doc/src/sgml/config.sgml | 4 +-
doc/src/sgml/monitoring.sgml | 125 +++++++++++++++
src/backend/catalog/system_views.sql | 10 ++
src/backend/utils/activity/pgstat_lock.c | 8 +
src/backend/utils/adt/pgstatfuncs.c | 40 +++++
src/include/catalog/pg_proc.dat | 9 ++
src/include/pgstat.h | 1 +
src/test/isolation/expected/stats.out | 189 +++++++++++++++++++++++
src/test/isolation/expected/stats_1.out | 189 +++++++++++++++++++++++
src/test/isolation/specs/stats.spec | 95 ++++++++++++
src/test/regress/expected/rules.out | 7 +
src/test/regress/expected/stats.out | 48 ++++++
src/test/regress/sql/stats.sql | 36 +++++
13 files changed, 760 insertions(+), 1 deletion(-)
17.5% doc/src/sgml/
4.9% src/backend/utils/adt/
53.2% src/test/isolation/expected/
11.1% src/test/isolation/specs/
5.5% src/test/regress/expected/
3.8% src/test/regress/sql/
3.6% src/
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 0e868fb03e4..b3c353d6095 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8854,7 +8854,9 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
<para>
Enables timing of lock waits. This parameter is on by default, as it tracks
only the timings for successful acquisitions that waited longer than
- <xref linkend="guc-deadlock-timeout"/>.
+ <xref linkend="guc-deadlock-timeout"/>. Lock timing information is
+ displayed in the <link linkend="monitoring-pg-stat-lock-view">
+ <structname>pg_stat_lock</structname></link> view.
Only superusers and users with the appropriate <literal>SET</literal>
privilege can change this setting.
</para>
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index b77d189a500..277275fdcb5 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -493,6 +493,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
@@ -3124,6 +3133,116 @@ 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.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>timed_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 <xref linkend="guc-track-lock-timing"/>
+ is enabled and 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 <xref linkend="guc-track-lock-timing"/> is enabled and
+ 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. You may want to increase
+ <xref linkend="guc-max-locks-per-transaction"/> if you feel this counter
+ is too high.
+ </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>
@@ -5195,6 +5314,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
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 1ea8f1faa9e..dcf12a202e0 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -985,6 +985,16 @@ 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.timed_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,
diff --git a/src/backend/utils/activity/pgstat_lock.c b/src/backend/utils/activity/pgstat_lock.c
index b410f376d49..451fba4f8d9 100644
--- a/src/backend/utils/activity/pgstat_lock.c
+++ b/src/backend/utils/activity/pgstat_lock.c
@@ -21,6 +21,14 @@
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()
*/
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index b1df96e7b0b..3b7cbdb82be 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1737,6 +1737,43 @@ 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 6
+ 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->timed_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.
*/
@@ -1921,6 +1958,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);
@@ -1938,6 +1976,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)
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index dac40992cbc..3cd497a8e1f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6027,6 +6027,15 @@
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,int8,timestamptz}',
+ proargmodes => '{o,o,o,o,o,o}',
+ proargnames => '{locktype,waits,timed_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',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 615ccf540e6..39441d9698a 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -642,6 +642,7 @@ extern void pgstat_lock_flush(bool nowait);
extern void pgstat_count_lock_waits(uint8 locktag_type);
extern void pgstat_count_lock_fastpath_exceeded(uint8 locktag_type);
extern void pgstat_count_lock_timed_wait(uint8 locktag_type, long msecs);
+extern PgStat_Lock *pgstat_fetch_stat_lock(void);
/*
* Functions in pgstat_database.c
diff --git a/src/test/isolation/expected/stats.out b/src/test/isolation/expected/stats.out
index cfad309ccf3..f9cd78f03b3 100644
--- a/src/test/isolation/expected/stats.out
+++ b/src/test/isolation/expected/stats.out
@@ -3751,3 +3751,192 @@ test_stat_func| 1|t |t
(1 row)
step s1_commit: COMMIT;
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_set_track_lock_timing 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 s1_set_track_lock_timing: SET track_lock_timing = on;
+step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
+step s2_set_track_lock_timing: SET track_lock_timing = on;
+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.5);
+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, timed_waits = waits, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'relation';
+?column?|?column?|?column?
+--------+--------+--------
+t |t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_set_track_lock_timing 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 s1_set_track_lock_timing: SET track_lock_timing = on;
+step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
+step s2_set_track_lock_timing: SET track_lock_timing = 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.5);
+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, timed_waits = waits, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'transactionid';
+?column?|?column?|?column?
+--------+--------+--------
+t |t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_set_track_lock_timing 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 s1_set_track_lock_timing: SET track_lock_timing = on;
+step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
+step s2_set_track_lock_timing: SET track_lock_timing = 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.5);
+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, timed_waits = waits, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'advisory';
+?column?|?column?|?column?
+--------+--------+--------
+t |t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_unset_track_lock_timing 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 s1_set_track_lock_timing: SET track_lock_timing = on;
+step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
+step s2_unset_track_lock_timing: SET track_lock_timing = 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.5);
+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, timed_waits = waits, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'relation';
+?column?|?column?|?column?
+--------+--------+--------
+t |f |f
+(1 row)
+
diff --git a/src/test/isolation/expected/stats_1.out b/src/test/isolation/expected/stats_1.out
index e1d937784cb..955f6ff5ec0 100644
--- a/src/test/isolation/expected/stats_1.out
+++ b/src/test/isolation/expected/stats_1.out
@@ -3775,3 +3775,192 @@ test_stat_func| 1|t |t
(1 row)
step s1_commit: COMMIT;
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_set_track_lock_timing 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 s1_set_track_lock_timing: SET track_lock_timing = on;
+step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
+step s2_set_track_lock_timing: SET track_lock_timing = on;
+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.5);
+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, timed_waits = waits, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'relation';
+?column?|?column?|?column?
+--------+--------+--------
+t |t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_set_track_lock_timing 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 s1_set_track_lock_timing: SET track_lock_timing = on;
+step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
+step s2_set_track_lock_timing: SET track_lock_timing = 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.5);
+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, timed_waits = waits, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'transactionid';
+?column?|?column?|?column?
+--------+--------+--------
+t |t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_set_track_lock_timing 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 s1_set_track_lock_timing: SET track_lock_timing = on;
+step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
+step s2_set_track_lock_timing: SET track_lock_timing = 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.5);
+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, timed_waits = waits, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'advisory';
+?column?|?column?|?column?
+--------+--------+--------
+t |t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_unset_track_lock_timing 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 s1_set_track_lock_timing: SET track_lock_timing = on;
+step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
+step s2_unset_track_lock_timing: SET track_lock_timing = 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.5);
+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, timed_waits = waits, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'relation';
+?column?|?column?|?column?
+--------+--------+--------
+t |f |f
+(1 row)
+
diff --git a/src/test/isolation/specs/stats.spec b/src/test/isolation/specs/stats.spec
index da16710da0f..7c8e2a9214e 100644
--- a/src/test/isolation/specs/stats.spec
+++ b/src/test/isolation/specs/stats.spec
@@ -130,6 +130,14 @@ step s1_slru_check_stats {
WHERE before.stat = 'blks_zeroed';
}
+# Lock stats steps
+step s1_set_deadlock_timeout { SET deadlock_timeout = '10ms'; }
+step s1_set_track_lock_timing { SET track_lock_timing = on; }
+step s1_reset_stat_lock { SELECT pg_stat_reset_shared('lock'); }
+step s1_sleep { SELECT pg_sleep(0.5); }
+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'; }
@@ -164,6 +172,16 @@ step s2_big_notify { SELECT pg_notify('stats_test_use',
repeat(i::text, current_setting('block_size')::int / 2)) FROM generate_series(1, 3) g(i);
}
+# Lock stats steps
+step s2_set_deadlock_timeout { SET deadlock_timeout = '10ms'; }
+step s2_set_track_lock_timing { SET track_lock_timing = on; }
+step s2_unset_track_lock_timing { SET track_lock_timing = off; }
+step s2_report_stat_lock_relation { SELECT waits > 0, timed_waits = waits, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'relation'; }
+step s2_report_stat_lock_transactionid { SELECT waits > 0, timed_waits = waits, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'transactionid'; }
+step s2_report_stat_lock_advisory { SELECT waits > 0, timed_waits = waits, wait_time > 500 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); }
######################
# Function stats tests
@@ -765,3 +783,80 @@ permutation
s1_clear_snapshot
s1_func_stats
s1_commit
+
+######################
+# Lock stats tests
+######################
+
+# relation lock
+
+permutation
+ s1_set_deadlock_timeout
+ s1_reset_stat_lock
+ s1_set_track_lock_timing
+ s2_set_deadlock_timeout
+ s2_set_track_lock_timing
+ 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
+ s1_set_track_lock_timing
+ s2_set_deadlock_timeout
+ s2_set_track_lock_timing
+ 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
+ s1_set_track_lock_timing
+ s2_set_deadlock_timeout
+ s2_set_track_lock_timing
+ 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 track_lock_timing behaves correctly
+
+permutation
+ s1_set_deadlock_timeout
+ s1_reset_stat_lock
+ s1_set_track_lock_timing
+ s2_set_deadlock_timeout
+ s2_unset_track_lock_timing
+ s1_begin
+ s1_lock_relation
+ s2_begin
+ s2_ff
+ s2_lock_relation
+ s1_sleep
+ s1_commit
+ s2_commit
+ s2_report_stat_lock_relation
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 78a37d9fc8f..2fcb48df6c9 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1951,6 +1951,13 @@ 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,
+ timed_waits,
+ wait_time,
+ fastpath_exceeded,
+ stats_reset
+ FROM pg_stat_get_lock() l(locktype, waits, timed_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 cd00f35bf7a..93de87747d4 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -1910,4 +1910,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
diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql
index 8768e0f27fd..db9948eedb8 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -944,4 +944,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
--
2.34.1
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-02-19 18:27 Andres Freund <andres@anarazel.de>
parent: Michael Paquier <michael@paquier.xyz>
1 sibling, 1 reply; 61+ messages in thread
From: Andres Freund @ 2026-02-19 18:27 UTC (permalink / raw)
To: Michael Paquier <michael@paquier.xyz>; +Cc: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi,
On 2026-02-19 13:06:52 +0900, Michael Paquier wrote:
> On Tue, Feb 17, 2026 at 04:33:54PM +0000, Bertrand Drouvot wrote:
> > Okay, done that way in the attached. To avoid overhead due to timing as much as
> > possible, the patch simply relies on log_lock_waits and deadlock_timeout. It means
> > that it relies on the existing code, and increments waits and wait_time only if
> > log_lock_waits is on and if the session waited longer than deadlock_timeout.
> >
> > I did not want to dissociate the waits and wait_time increments so that their
> > ratio could still make sense.
> >
> > That sounds like a good compromise, thoughts?
>
> else if (myWaitStatus == PROC_WAIT_STATUS_OK)
> + {
> + /* Increment the lock statistics counters */
> + pgstat_count_lock_waits(locallock->tag.lock.locktag_type);
> + pgstat_count_lock_wait_time(locallock->tag.lock.locktag_type, msecs);
Why do two external function calls? The function calls are at least as
expensive as the work inside them, so doing this separately adds a fair bit of
overhead.
> Not sure that it makes much sense to me to rely on log_lock_waits
> being enabled to decide if this count and this time are aggregated.
> The log information and the stats gathering are two separate things.
> Wouldn't it make more sense to call pgstat_count_lock_waits() outside
> of this code path, when we know myWaitStatus?
IDK, it doesn't seem unreasonable to not duplicate work. If the delay is very
short it's probably also not that interesting to track, but I guess that's
debatable.
> While relying on the time calculating for the logs data is a good
> idea, it seems to me that we should have a separate GUC to enable this
> number, like a new track_lock_timings? If track_lock_timings or
> log_lock_waits is enabled, we should calculate the time difference.
> All these decisions also depends on what deadlock_state holds on top
> of myWaitStatus, I guess..
I don't think it's worth having a separate GUC to track this. The realistic
number of calls in a certain timespan to this is way way lower than something
like track_io_timing, track_wal_io_timing or such. So I don't think we need an
opt-out here like for those. If we eventually can reduce the overhead of the
other track_* GUCs, we should remove them too, but I think that's further out.
Greetings,
Andres Freund
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-02-20 06:38 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Andres Freund <andres@anarazel.de>
0 siblings, 1 reply; 61+ messages in thread
From: Bertrand Drouvot @ 2026-02-20 06:38 UTC (permalink / raw)
To: Andres Freund <andres@anarazel.de>; +Cc: Michael Paquier <michael@paquier.xyz>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi,
On Thu, Feb 19, 2026 at 01:27:10PM -0500, Andres Freund wrote:
> Hi,
>
> On 2026-02-19 13:06:52 +0900, Michael Paquier wrote:
> > On Tue, Feb 17, 2026 at 04:33:54PM +0000, Bertrand Drouvot wrote:
> > > Okay, done that way in the attached. To avoid overhead due to timing as much as
> > > possible, the patch simply relies on log_lock_waits and deadlock_timeout. It means
> > > that it relies on the existing code, and increments waits and wait_time only if
> > > log_lock_waits is on and if the session waited longer than deadlock_timeout.
> > >
> > > I did not want to dissociate the waits and wait_time increments so that their
> > > ratio could still make sense.
> > >
> > > That sounds like a good compromise, thoughts?
> >
> > else if (myWaitStatus == PROC_WAIT_STATUS_OK)
> > + {
> > + /* Increment the lock statistics counters */
> > + pgstat_count_lock_waits(locallock->tag.lock.locktag_type);
> > + pgstat_count_lock_wait_time(locallock->tag.lock.locktag_type, msecs);
>
> Why do two external function calls? The function calls are at least as
> expensive as the work inside them, so doing this separately adds a fair bit of
> overhead.
Yeah, I also realized that and that was changed in v6 (you were looking at v5
to make this comment).
> > Not sure that it makes much sense to me to rely on log_lock_waits
> > being enabled to decide if this count and this time are aggregated.
> > The log information and the stats gathering are two separate things.
> > Wouldn't it make more sense to call pgstat_count_lock_waits() outside
> > of this code path, when we know myWaitStatus?
>
> IDK, it doesn't seem unreasonable to not duplicate work.
I don't think that the new GUC is introducing duplicate work (see 0003 attached),
that said it's introducing extra complexity in ProcSleep().
> If the delay is very
> short it's probably also not that interesting to track, but I guess that's
> debatable.
v6 was introducing timed_waits so that we have:
waits
timed_waits
wait_time
fastpath_exceeded
timed_waits and wait_time were incremented together and waits was incremented
unconditionally. I like the idea of being able to track the numbers of waits
whatever the value of log_lock_waits (or the new track_lock_timing) is. Also
one could compare waits vs timed_waits.
It's still done that way in the attached.
> I don't think it's worth having a separate GUC to track this. The realistic
> number of calls in a certain timespan to this is way way lower than something
> like track_io_timing, track_wal_io_timing or such. So I don't think we need an
> opt-out here like for those. If we eventually can reduce the overhead of the
> other track_* GUCs, we should remove them too, but I think that's further out.
Yeah, OTOH having a dedicated GUC for a clear separation of duties also makes
sense.
I don't have a strong opinion on it, but in the attached 0003 is adding
the new GUC. So that we can see what having a new GUC implies in ProcSleep() and
we can just get rid of 0003 if we think the GUC is not worth the extra complexity.
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
Attachments:
[text/x-diff] v7-0001-Add-lock-statistics.patch (16.3K, ../../aZgBTyzxWmPJL0Tt@ip-10-97-1-34.eu-west-3.compute.internal/2-v7-0001-Add-lock-statistics.patch)
download | inline diff:
From 1e506de67d19bcc98ad41bc2681aed07aaa5d9e8 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Tue, 29 Jul 2025 08:36:35 +0000
Subject: [PATCH v7 1/3] Add lock statistics
Adding a new stat kind PGSTAT_KIND_LOCK for the lock statistics.
This new statistic kind is a fixed one because its key is the lock type
so that we know its size is LOCKTAG_LAST_TYPE + 1.
This statistic kind records the following counters:
waits
timed_waits
wait_time
fastpath_exceeded
The timed_waits and wait_time counters are incremented if log_lock_waits is on
and the session waited longer than deadlock_timeout to acquire the lock.
waits is incremented unconditionally.
fastpath_exceeded is incremented when the lock can not be acquired via fast path
because the fast path slot limit was exceeded.
No extra details is added (like the ones, i.e relation oid, database oid, we
can find in pg_locks). The idea is to provide an idea on what the locking
behaviour looks like.
XXX: Bump stat file format
---
src/backend/storage/lmgr/lock.c | 58 ++++----
src/backend/storage/lmgr/proc.c | 9 ++
src/backend/utils/activity/Makefile | 1 +
src/backend/utils/activity/meson.build | 1 +
src/backend/utils/activity/pgstat.c | 18 +++
src/backend/utils/activity/pgstat_lock.c | 164 +++++++++++++++++++++++
src/include/pgstat.h | 29 ++++
src/include/utils/pgstat_internal.h | 21 +++
src/include/utils/pgstat_kind.h | 5 +-
src/tools/pgindent/typedefs.list | 4 +
10 files changed, 282 insertions(+), 28 deletions(-)
29.3% src/backend/storage/lmgr/
54.2% src/backend/utils/activity/
7.2% src/include/utils/
8.4% src/include/
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index e1168ad3837..8a0d970b26b 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -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"
@@ -984,37 +985,42 @@ LockAcquireExtended(const LOCKTAG *locktag,
* lock type on a relation we have already locked using the fast-path, but
* for now we don't worry about that case either.
*/
- if (EligibleForRelationFastPath(locktag, lockmode) &&
- FastPathLocalUseCounts[FAST_PATH_REL_GROUP(locktag->locktag_field2)] < FP_LOCK_SLOTS_PER_GROUP)
+ if (EligibleForRelationFastPath(locktag, lockmode))
{
- uint32 fasthashcode = FastPathStrongLockHashPartition(hashcode);
- bool acquired;
-
- /*
- * LWLockAcquire acts as a memory sequencing point, so it's safe to
- * assume that any strong locker whose increment to
- * FastPathStrongRelationLocks->counts becomes visible after we test
- * it has yet to begin to transfer fast-path locks.
- */
- LWLockAcquire(&MyProc->fpInfoLock, LW_EXCLUSIVE);
- if (FastPathStrongRelationLocks->count[fasthashcode] != 0)
- acquired = false;
- else
- acquired = FastPathGrantRelationLock(locktag->locktag_field2,
- lockmode);
- LWLockRelease(&MyProc->fpInfoLock);
- if (acquired)
+ if (FastPathLocalUseCounts[FAST_PATH_REL_GROUP(locktag->locktag_field2)] <
+ FP_LOCK_SLOTS_PER_GROUP)
{
+ uint32 fasthashcode = FastPathStrongLockHashPartition(hashcode);
+ bool acquired;
+
/*
- * The locallock might contain stale pointers to some old shared
- * objects; we MUST reset these to null before considering the
- * lock to be acquired via fast-path.
+ * LWLockAcquire acts as a memory sequencing point, so it's safe
+ * to assume that any strong locker whose increment to
+ * FastPathStrongRelationLocks->counts becomes visible after we
+ * test it has yet to begin to transfer fast-path locks.
*/
- locallock->lock = NULL;
- locallock->proclock = NULL;
- GrantLockLocal(locallock, owner);
- return LOCKACQUIRE_OK;
+ LWLockAcquire(&MyProc->fpInfoLock, LW_EXCLUSIVE);
+ if (FastPathStrongRelationLocks->count[fasthashcode] != 0)
+ acquired = false;
+ else
+ acquired = FastPathGrantRelationLock(locktag->locktag_field2,
+ lockmode);
+ LWLockRelease(&MyProc->fpInfoLock);
+ if (acquired)
+ {
+ /*
+ * The locallock might contain stale pointers to some old
+ * shared objects; we MUST reset these to null before
+ * considering the lock to be acquired via fast-path.
+ */
+ locallock->lock = NULL;
+ locallock->proclock = NULL;
+ GrantLockLocal(locallock, owner);
+ return LOCKACQUIRE_OK;
+ }
}
+ else
+ pgstat_count_lock_fastpath_exceeded(locallock->tag.lock.locktag_type);
}
/*
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index fd8318bdf3d..bee4a3c2b89 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -1609,9 +1609,15 @@ ProcSleep(LOCALLOCK *locallock)
"Processes holding the lock: %s. Wait queue: %s.",
lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
else if (myWaitStatus == PROC_WAIT_STATUS_OK)
+ {
+ /* Increment the timed lock statistics counters */
+ pgstat_count_lock_timed_wait(locallock->tag.lock.locktag_type,
+ msecs);
+
ereport(LOG,
(errmsg("process %d acquired %s on %s after %ld.%03d ms",
MyProcPid, modename, buf.data, msecs, usecs)));
+ }
else
{
Assert(myWaitStatus == PROC_WAIT_STATUS_ERROR);
@@ -1646,6 +1652,9 @@ ProcSleep(LOCALLOCK *locallock)
}
} while (myWaitStatus == PROC_WAIT_STATUS_WAITING);
+ /* Count lock waits unconditionally, regardless of log_lock_waits */
+ pgstat_count_lock_waits(locallock->tag.lock.locktag_type);
+
/*
* Disable the timers, if they are still running. As in LockErrorCleanup,
* we must preserve the LOCK_TIMEOUT indicator flag: if a lock timeout has
diff --git a/src/backend/utils/activity/Makefile b/src/backend/utils/activity/Makefile
index c37bfb350bb..ca3ef89bf59 100644
--- a/src/backend/utils/activity/Makefile
+++ b/src/backend/utils/activity/Makefile
@@ -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 \
diff --git a/src/backend/utils/activity/meson.build b/src/backend/utils/activity/meson.build
index 53bd5a246ca..1aa7ece5290 100644
--- a/src/backend/utils/activity/meson.build
+++ b/src/backend/utils/activity/meson.build
@@ -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',
diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index 11bb71cad5a..eb8ccbaa628 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -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
index 00000000000..b410f376d49
--- /dev/null
+++ b/src/backend/utils/activity/pgstat_lock.c
@@ -0,0 +1,164 @@
+/* -------------------------------------------------------------------------
+ *
+ * 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-2025, 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;
+
+/*
+ * 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(timed_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);
+ }
+}
+
+#define PGSTAT_COUNT_LOCK_FUNC(stat) \
+void \
+CppConcat(pgstat_count_lock_,stat)(uint8 locktag_type) \
+{ \
+ Assert(locktag_type <= LOCKTAG_LAST_TYPE); \
+ PendingLockStats.stats[locktag_type].stat++; \
+ have_lockstats = true; \
+ pgstat_report_fixed = true; \
+}
+
+/* pgstat_count_lock_waits */
+PGSTAT_COUNT_LOCK_FUNC(waits)
+
+/* pgstat_count_lock_fastpath_exceeded */
+PGSTAT_COUNT_LOCK_FUNC(fastpath_exceeded)
+
+void
+pgstat_count_lock_timed_wait(uint8 locktag_type, long msecs)
+{
+ Assert(locktag_type <= LOCKTAG_LAST_TYPE);
+ PendingLockStats.stats[locktag_type].timed_waits++;
+ PendingLockStats.stats[locktag_type].wait_time += (PgStat_Counter) msecs;
+ have_lockstats = true;
+ pgstat_report_fixed = true;
+}
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 9bb777c3d5a..9c4508d5544 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -16,6 +16,7 @@
#include "portability/instr_time.h"
#include "postmaster/pgarch.h" /* for MAX_XFN_CHARS */
#include "replication/conflict.h"
+#include "storage/lock.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"
@@ -341,6 +342,25 @@ typedef struct PgStat_IO
PgStat_BktypeIO stats[BACKEND_NUM_TYPES];
} PgStat_IO;
+typedef struct PgStat_LockEntry
+{
+ PgStat_Counter waits;
+ PgStat_Counter timed_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;
@@ -613,6 +633,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_waits(uint8 locktag_type);
+extern void pgstat_count_lock_fastpath_exceeded(uint8 locktag_type);
+extern void pgstat_count_lock_timed_wait(uint8 locktag_type, long msecs);
+
/*
* Functions in pgstat_database.c
*/
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 9b8fbae00ed..97704421a92 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -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
diff --git a/src/include/utils/pgstat_kind.h b/src/include/utils/pgstat_kind.h
index c30b6235623..2d78a029683 100644
--- a/src/include/utils/pgstat_kind.h
+++ b/src/include/utils/pgstat_kind.h
@@ -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/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 241945734ec..0432862061b 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2255,6 +2255,7 @@ PgStatShared_Database
PgStatShared_Function
PgStatShared_HashEntry
PgStatShared_IO
+PgStatShared_Lock
PgStatShared_Relation
PgStatShared_ReplSlot
PgStatShared_SLRU
@@ -2277,8 +2278,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
--
2.34.1
[text/x-diff] v7-0002-Add-the-pg_stat_lock-view.patch (30.8K, ../../aZgBTyzxWmPJL0Tt@ip-10-97-1-34.eu-west-3.compute.internal/3-v7-0002-Add-the-pg_stat_lock-view.patch)
download | inline diff:
From 9ed0b32bdf025f2abeb4848d70c76de184e0a214 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Thu, 31 Jul 2025 09:35:31 +0000
Subject: [PATCH v7 2/3] Add the pg_stat_lock view
This new view reports lock statistics.
Note that it does not omit combinations which do not make sense (as pg_locks
does).
Also wait_time is reported as bigint as the deadlock_timeout default value is
1s.
This commit also adds documentation and a few tests.
XXX: Bump catversion
---
doc/src/sgml/monitoring.sgml | 125 +++++++++++++++
src/backend/catalog/system_views.sql | 10 ++
src/backend/utils/activity/pgstat_lock.c | 8 +
src/backend/utils/adt/pgstatfuncs.c | 40 +++++
src/include/catalog/pg_proc.dat | 9 ++
src/include/pgstat.h | 1 +
src/test/isolation/expected/stats.out | 189 +++++++++++++++++++++++
src/test/isolation/expected/stats_1.out | 189 +++++++++++++++++++++++
src/test/isolation/specs/stats.spec | 95 ++++++++++++
src/test/regress/expected/rules.out | 7 +
src/test/regress/expected/stats.out | 48 ++++++
src/test/regress/sql/stats.sql | 36 +++++
12 files changed, 757 insertions(+)
16.7% doc/src/sgml/
5.0% src/backend/utils/adt/
53.6% src/test/isolation/expected/
11.1% src/test/isolation/specs/
5.6% src/test/regress/expected/
3.9% src/test/regress/sql/
3.7% src/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index b77d189a500..cfcb8189418 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -493,6 +493,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
@@ -3124,6 +3133,116 @@ 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.
+ </para>
+ </entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry">
+ <para role="column_definition">
+ <structfield>timed_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 <xref linkend="guc-log-lock-waits"/>
+ is enabled and 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 <xref linkend="guc-log-lock-waits"/> is enabled and
+ 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. You may want to increase
+ <xref linkend="guc-max-locks-per-transaction"/> if you feel this counter
+ is too high.
+ </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>
@@ -5195,6 +5314,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
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 1ea8f1faa9e..dcf12a202e0 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -985,6 +985,16 @@ 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.timed_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,
diff --git a/src/backend/utils/activity/pgstat_lock.c b/src/backend/utils/activity/pgstat_lock.c
index b410f376d49..451fba4f8d9 100644
--- a/src/backend/utils/activity/pgstat_lock.c
+++ b/src/backend/utils/activity/pgstat_lock.c
@@ -21,6 +21,14 @@
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()
*/
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index b1df96e7b0b..3b7cbdb82be 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1737,6 +1737,43 @@ 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 6
+ 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->timed_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.
*/
@@ -1921,6 +1958,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);
@@ -1938,6 +1976,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)
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index dac40992cbc..3cd497a8e1f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6027,6 +6027,15 @@
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,int8,timestamptz}',
+ proargmodes => '{o,o,o,o,o,o}',
+ proargnames => '{locktype,waits,timed_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',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 9c4508d5544..a4cc006794d 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -641,6 +641,7 @@ extern void pgstat_lock_flush(bool nowait);
extern void pgstat_count_lock_waits(uint8 locktag_type);
extern void pgstat_count_lock_fastpath_exceeded(uint8 locktag_type);
extern void pgstat_count_lock_timed_wait(uint8 locktag_type, long msecs);
+extern PgStat_Lock *pgstat_fetch_stat_lock(void);
/*
* Functions in pgstat_database.c
diff --git a/src/test/isolation/expected/stats.out b/src/test/isolation/expected/stats.out
index cfad309ccf3..86d70a0705c 100644
--- a/src/test/isolation/expected/stats.out
+++ b/src/test/isolation/expected/stats.out
@@ -3751,3 +3751,192 @@ test_stat_func| 1|t |t
(1 row)
step s1_commit: COMMIT;
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits s2_set_deadlock_timeout s2_set_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 s1_set_log_lock_waits: SET log_lock_waits = on;
+step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
+step s2_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, timed_waits = waits, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'relation';
+?column?|?column?|?column?
+--------+--------+--------
+t |t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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 s1_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, timed_waits = waits, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'transactionid';
+?column?|?column?|?column?
+--------+--------+--------
+t |t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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 s1_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, timed_waits = waits, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'advisory';
+?column?|?column?|?column?
+--------+--------+--------
+t |t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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 s1_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, timed_waits = waits, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'relation';
+?column?|?column?|?column?
+--------+--------+--------
+t |f |f
+(1 row)
+
diff --git a/src/test/isolation/expected/stats_1.out b/src/test/isolation/expected/stats_1.out
index e1d937784cb..4e4c68b4525 100644
--- a/src/test/isolation/expected/stats_1.out
+++ b/src/test/isolation/expected/stats_1.out
@@ -3775,3 +3775,192 @@ test_stat_func| 1|t |t
(1 row)
step s1_commit: COMMIT;
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits s2_set_deadlock_timeout s2_set_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 s1_set_log_lock_waits: SET log_lock_waits = on;
+step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
+step s2_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, timed_waits = waits, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'relation';
+?column?|?column?|?column?
+--------+--------+--------
+t |t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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 s1_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, timed_waits = waits, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'transactionid';
+?column?|?column?|?column?
+--------+--------+--------
+t |t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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 s1_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, timed_waits = waits, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'advisory';
+?column?|?column?|?column?
+--------+--------+--------
+t |t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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 s1_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, timed_waits = waits, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'relation';
+?column?|?column?|?column?
+--------+--------+--------
+t |f |f
+(1 row)
+
diff --git a/src/test/isolation/specs/stats.spec b/src/test/isolation/specs/stats.spec
index da16710da0f..d4b53545198 100644
--- a/src/test/isolation/specs/stats.spec
+++ b/src/test/isolation/specs/stats.spec
@@ -130,6 +130,14 @@ step s1_slru_check_stats {
WHERE before.stat = 'blks_zeroed';
}
+# Lock stats steps
+step s1_set_deadlock_timeout { SET deadlock_timeout = '10ms'; }
+step s1_set_log_lock_waits { SET log_lock_waits = on; }
+step s1_reset_stat_lock { SELECT pg_stat_reset_shared('lock'); }
+step s1_sleep { SELECT pg_sleep(0.5); }
+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'; }
@@ -164,6 +172,16 @@ step s2_big_notify { SELECT pg_notify('stats_test_use',
repeat(i::text, current_setting('block_size')::int / 2)) FROM generate_series(1, 3) g(i);
}
+# Lock stats steps
+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, timed_waits = waits, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'relation'; }
+step s2_report_stat_lock_transactionid { SELECT waits > 0, timed_waits = waits, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'transactionid'; }
+step s2_report_stat_lock_advisory { SELECT waits > 0, timed_waits = waits, wait_time > 500 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); }
######################
# Function stats tests
@@ -765,3 +783,80 @@ permutation
s1_clear_snapshot
s1_func_stats
s1_commit
+
+######################
+# Lock stats tests
+######################
+
+# relation lock
+
+permutation
+ s1_set_deadlock_timeout
+ s1_reset_stat_lock
+ s1_set_log_lock_waits
+ s2_set_deadlock_timeout
+ s2_set_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
+
+# transaction lock
+
+permutation
+ s1_set_deadlock_timeout
+ s1_reset_stat_lock
+ s1_set_log_lock_waits
+ 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
+ s1_set_log_lock_waits
+ 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 behaves correctly
+
+permutation
+ s1_set_deadlock_timeout
+ s1_reset_stat_lock
+ s1_set_log_lock_waits
+ 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
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 78a37d9fc8f..2fcb48df6c9 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1951,6 +1951,13 @@ 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,
+ timed_waits,
+ wait_time,
+ fastpath_exceeded,
+ stats_reset
+ FROM pg_stat_get_lock() l(locktype, waits, timed_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 cd00f35bf7a..93de87747d4 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -1910,4 +1910,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
diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql
index 8768e0f27fd..db9948eedb8 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -944,4 +944,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
--
2.34.1
[text/x-diff] v7-0003-Introduce-a-new-track_lock_timing-GUC.patch (26.0K, ../../aZgBTyzxWmPJL0Tt@ip-10-97-1-34.eu-west-3.compute.internal/4-v7-0003-Introduce-a-new-track_lock_timing-GUC.patch)
download | inline diff:
From 0ecc6d129ae387e8cafb56a033a9b49edc0b12f0 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Fri, 20 Feb 2026 06:13:28 +0000
Subject: [PATCH v7 3/3] Introduce a new track_lock_timing GUC
A new GUC (track_lock_timing) is added and defaults to on. If on, timed_waits and
wait_time counters are incremented if the session waited longer than deadlock_timeout
to acquire the lock. It's on by default, as this is the same idea as 2aac62be8cb.
---
doc/src/sgml/config.sgml | 19 ++
doc/src/sgml/monitoring.sgml | 4 +-
src/backend/storage/lmgr/proc.c | 165 ++++++++++--------
src/backend/utils/misc/guc_parameters.dat | 6 +
src/backend/utils/misc/postgresql.conf.sample | 1 +
src/include/pgstat.h | 1 +
src/test/isolation/expected/stats.out | 24 +--
src/test/isolation/expected/stats_1.out | 24 +--
src/test/isolation/specs/stats.spec | 24 +--
9 files changed, 153 insertions(+), 115 deletions(-)
7.5% doc/src/sgml/
48.4% src/backend/storage/lmgr/
35.8% src/test/isolation/expected/
6.0% src/test/isolation/specs/
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 20dbcaeb3ee..1e6778576ea 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8841,6 +8841,25 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</listitem>
</varlistentry>
+ <varlistentry id="guc-track-lock-timing" xreflabel="track_lock_timing">
+ <term><varname>track_lock_timing</varname> (<type>boolean</type>)
+ <indexterm>
+ <primary><varname>track_lock_timing</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Enables timing of lock waits. This parameter is on by default, as it tracks
+ only the timings for successful acquisitions that waited longer than
+ <xref linkend="guc-deadlock-timeout"/>. Lock timing information is
+ displayed in the <link linkend="monitoring-pg-stat-lock-view">
+ <structname>pg_stat_lock</structname></link> view.
+ Only superusers and users with the appropriate <literal>SET</literal>
+ privilege can change this setting.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-track-wal-io-timing" xreflabel="track_wal_io_timing">
<term><varname>track_wal_io_timing</varname> (<type>boolean</type>)
<indexterm>
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index cfcb8189418..277275fdcb5 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -3193,7 +3193,7 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para>
<para>
Number of times a lock of this type had to wait because of a
- conflicting lock. Only incremented when <xref linkend="guc-log-lock-waits"/>
+ conflicting lock. Only incremented when <xref linkend="guc-track-lock-timing"/>
is enabled and the lock was successfully acquired after waiting longer
than <xref linkend="guc-deadlock-timeout"/>.
</para>
@@ -3207,7 +3207,7 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para>
<para>
Total time spent waiting for locks of this type, in milliseconds.
- Only incremented when <xref linkend="guc-log-lock-waits"/> is enabled and
+ Only incremented when <xref linkend="guc-track-lock-timing"/> is enabled and
the lock was successfully acquired after waiting longer than
<xref linkend="guc-deadlock-timeout"/>.
</para>
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index bee4a3c2b89..18e42959e96 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -62,6 +62,7 @@ int IdleInTransactionSessionTimeout = 0;
int TransactionTimeout = 0;
int IdleSessionTimeout = 0;
bool log_lock_waits = true;
+bool track_lock_timing = true;
/* Pointer to this process's PGPROC struct, if any */
PGPROC *MyProc = NULL;
@@ -1545,99 +1546,110 @@ ProcSleep(LOCALLOCK *locallock)
/*
* If awoken after the deadlock check interrupt has run, and
- * log_lock_waits is on, then report about the wait.
+ * log_lock_waits or track_lock_timing is on, then report or track
+ * about the wait.
*/
- if (log_lock_waits && deadlock_state != DS_NOT_YET_CHECKED)
+ if ((log_lock_waits || track_lock_timing) &&
+ deadlock_state != DS_NOT_YET_CHECKED)
{
- StringInfoData buf,
- lock_waiters_sbuf,
- lock_holders_sbuf;
- const char *modename;
long secs;
int usecs;
long msecs;
- int lockHoldersNum = 0;
- initStringInfo(&buf);
- initStringInfo(&lock_waiters_sbuf);
- initStringInfo(&lock_holders_sbuf);
-
- DescribeLockTag(&buf, &locallock->tag.lock);
- modename = GetLockmodeName(locallock->tag.lock.locktag_lockmethodid,
- lockmode);
TimestampDifference(get_timeout_start_time(DEADLOCK_TIMEOUT),
GetCurrentTimestamp(),
&secs, &usecs);
msecs = secs * 1000 + usecs / 1000;
usecs = usecs % 1000;
- /* Gather a list of all lock holders and waiters */
- LWLockAcquire(partitionLock, LW_SHARED);
- GetLockHoldersAndWaiters(locallock, &lock_holders_sbuf,
- &lock_waiters_sbuf, &lockHoldersNum);
- LWLockRelease(partitionLock);
-
- if (deadlock_state == DS_SOFT_DEADLOCK)
- ereport(LOG,
- (errmsg("process %d avoided deadlock for %s on %s by rearranging queue order after %ld.%03d ms",
- MyProcPid, modename, buf.data, msecs, usecs),
- (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
- "Processes holding the lock: %s. Wait queue: %s.",
- lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
- else if (deadlock_state == DS_HARD_DEADLOCK)
- {
- /*
- * This message is a bit redundant with the error that will be
- * reported subsequently, but in some cases the error report
- * might not make it to the log (eg, if it's caught by an
- * exception handler), and we want to ensure all long-wait
- * events get logged.
- */
- ereport(LOG,
- (errmsg("process %d detected deadlock while waiting for %s on %s after %ld.%03d ms",
- MyProcPid, modename, buf.data, msecs, usecs),
- (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
- "Processes holding the lock: %s. Wait queue: %s.",
- lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
- }
-
- if (myWaitStatus == PROC_WAIT_STATUS_WAITING)
- ereport(LOG,
- (errmsg("process %d still waiting for %s on %s after %ld.%03d ms",
- MyProcPid, modename, buf.data, msecs, usecs),
- (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
- "Processes holding the lock: %s. Wait queue: %s.",
- lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
- else if (myWaitStatus == PROC_WAIT_STATUS_OK)
- {
- /* Increment the timed lock statistics counters */
+ /* Increment the timed lock statistics counters */
+ if (track_lock_timing && myWaitStatus == PROC_WAIT_STATUS_OK)
pgstat_count_lock_timed_wait(locallock->tag.lock.locktag_type,
msecs);
- ereport(LOG,
- (errmsg("process %d acquired %s on %s after %ld.%03d ms",
- MyProcPid, modename, buf.data, msecs, usecs)));
- }
- else
+ if (log_lock_waits)
{
- Assert(myWaitStatus == PROC_WAIT_STATUS_ERROR);
-
- /*
- * Currently, the deadlock checker always kicks its own
- * process, which means that we'll only see
- * PROC_WAIT_STATUS_ERROR when deadlock_state ==
- * DS_HARD_DEADLOCK, and there's no need to print redundant
- * messages. But for completeness and future-proofing, print
- * a message if it looks like someone else kicked us off the
- * lock.
- */
- if (deadlock_state != DS_HARD_DEADLOCK)
+ StringInfoData buf,
+ lock_waiters_sbuf,
+ lock_holders_sbuf;
+ const char *modename;
+ int lockHoldersNum = 0;
+
+ initStringInfo(&buf);
+ initStringInfo(&lock_waiters_sbuf);
+ initStringInfo(&lock_holders_sbuf);
+
+ DescribeLockTag(&buf, &locallock->tag.lock);
+ modename = GetLockmodeName(locallock->tag.lock.locktag_lockmethodid,
+ lockmode);
+
+ /* Gather a list of all lock holders and waiters */
+ LWLockAcquire(partitionLock, LW_SHARED);
+ GetLockHoldersAndWaiters(locallock, &lock_holders_sbuf,
+ &lock_waiters_sbuf, &lockHoldersNum);
+ LWLockRelease(partitionLock);
+
+ if (deadlock_state == DS_SOFT_DEADLOCK)
ereport(LOG,
- (errmsg("process %d failed to acquire %s on %s after %ld.%03d ms",
+ (errmsg("process %d avoided deadlock for %s on %s by rearranging queue order after %ld.%03d ms",
MyProcPid, modename, buf.data, msecs, usecs),
(errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
"Processes holding the lock: %s. Wait queue: %s.",
lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
+ else if (deadlock_state == DS_HARD_DEADLOCK)
+ {
+ /*
+ * This message is a bit redundant with the error that
+ * will be reported subsequently, but in some cases the
+ * error report might not make it to the log (eg, if it's
+ * caught by an exception handler), and we want to ensure
+ * all long-wait events get logged.
+ */
+ ereport(LOG,
+ (errmsg("process %d detected deadlock while waiting for %s on %s after %ld.%03d ms",
+ MyProcPid, modename, buf.data, msecs, usecs),
+ (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
+ "Processes holding the lock: %s. Wait queue: %s.",
+ lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
+ }
+
+ if (myWaitStatus == PROC_WAIT_STATUS_WAITING)
+ ereport(LOG,
+ (errmsg("process %d still waiting for %s on %s after %ld.%03d ms",
+ MyProcPid, modename, buf.data, msecs, usecs),
+ (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
+ "Processes holding the lock: %s. Wait queue: %s.",
+ lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
+
+ else if (myWaitStatus == PROC_WAIT_STATUS_OK)
+ ereport(LOG,
+ (errmsg("process %d acquired %s on %s after %ld.%03d ms",
+ MyProcPid, modename, buf.data, msecs, usecs)));
+ else
+ {
+ Assert(myWaitStatus == PROC_WAIT_STATUS_ERROR);
+
+ /*
+ * Currently, the deadlock checker always kicks its own
+ * process, which means that we'll only see
+ * PROC_WAIT_STATUS_ERROR when deadlock_state ==
+ * DS_HARD_DEADLOCK, and there's no need to print
+ * redundant messages. But for completeness and
+ * future-proofing, print a message if it looks like
+ * someone else kicked us off the lock.
+ */
+ if (deadlock_state != DS_HARD_DEADLOCK)
+ ereport(LOG,
+ (errmsg("process %d failed to acquire %s on %s after %ld.%03d ms",
+ MyProcPid, modename, buf.data, msecs, usecs),
+ (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
+ "Processes holding the lock: %s. Wait queue: %s.",
+ lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
+ }
+
+ pfree(buf.data);
+ pfree(lock_holders_sbuf.data);
+ pfree(lock_waiters_sbuf.data);
}
/*
@@ -1645,14 +1657,13 @@ ProcSleep(LOCALLOCK *locallock)
* state so we don't print the above messages again.
*/
deadlock_state = DS_NO_DEADLOCK;
-
- pfree(buf.data);
- pfree(lock_holders_sbuf.data);
- pfree(lock_waiters_sbuf.data);
}
} while (myWaitStatus == PROC_WAIT_STATUS_WAITING);
- /* Count lock waits unconditionally, regardless of log_lock_waits */
+ /*
+ * Count lock waits unconditionally, regardless of log_lock_waits or
+ * track_lock_timing.
+ */
pgstat_count_lock_waits(locallock->tag.lock.locktag_type);
/*
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 9507778415d..c21e6c6ea4e 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -3110,6 +3110,12 @@
boot_val => 'false',
},
+{ name => 'track_lock_timing', type => 'bool', context => 'PGC_SUSET', group => 'STATS_CUMULATIVE',
+ short_desc => 'Collects timing statistics for lock acquisition.',
+ variable => 'track_lock_timing',
+ boot_val => 'true',
+},
+
{ name => 'track_wal_io_timing', type => 'bool', context => 'PGC_SUSET', group => 'STATS_CUMULATIVE',
short_desc => 'Collects timing statistics for WAL I/O activity.',
variable => 'track_wal_io_timing',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index f938cc65a3a..8a3a704aaa5 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -685,6 +685,7 @@
#track_counts = on
#track_cost_delay_timing = off
#track_io_timing = off
+#track_lock_timing = on
#track_wal_io_timing = off
#track_functions = none # none, pl, all
#stats_fetch_consistency = cache # cache, none, snapshot
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index a4cc006794d..62a9dc1b35e 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -840,6 +840,7 @@ extern PgStat_WalStats *pgstat_fetch_stat_wal(void);
extern PGDLLIMPORT bool pgstat_track_counts;
extern PGDLLIMPORT int pgstat_track_functions;
extern PGDLLIMPORT int pgstat_fetch_consistency;
+extern PGDLLIMPORT bool track_lock_timing;
/*
diff --git a/src/test/isolation/expected/stats.out b/src/test/isolation/expected/stats.out
index 86d70a0705c..f9cd78f03b3 100644
--- a/src/test/isolation/expected/stats.out
+++ b/src/test/isolation/expected/stats.out
@@ -3752,7 +3752,7 @@ test_stat_func| 1|t |t
step s1_commit: COMMIT;
-starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits s2_set_deadlock_timeout s2_set_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
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_set_track_lock_timing 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
------------------------
@@ -3765,9 +3765,9 @@ pg_stat_reset_shared
(1 row)
-step s1_set_log_lock_waits: SET log_lock_waits = on;
+step s1_set_track_lock_timing: SET track_lock_timing = on;
step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
-step s2_set_log_lock_waits: SET log_lock_waits = on;
+step s2_set_track_lock_timing: SET track_lock_timing = on;
step s1_begin: BEGIN;
step s1_lock_relation: LOCK TABLE test_stat_tab;
step s2_begin: BEGIN;
@@ -3794,7 +3794,7 @@ t |t |t
(1 row)
-starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_set_track_lock_timing 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
------------------------
@@ -3807,9 +3807,9 @@ pg_stat_reset_shared
(1 row)
-step s1_set_log_lock_waits: SET log_lock_waits = on;
+step s1_set_track_lock_timing: SET track_lock_timing = on;
step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
-step s2_set_log_lock_waits: SET log_lock_waits = on;
+step s2_set_track_lock_timing: SET track_lock_timing = 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';
@@ -3837,7 +3837,7 @@ t |t |t
(1 row)
-starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_set_track_lock_timing 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
------------------------
@@ -3850,9 +3850,9 @@ pg_stat_reset_shared
(1 row)
-step s1_set_log_lock_waits: SET log_lock_waits = on;
+step s1_set_track_lock_timing: SET track_lock_timing = on;
step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
-step s2_set_log_lock_waits: SET log_lock_waits = on;
+step s2_set_track_lock_timing: SET track_lock_timing = on;
step s1_lock_advisory_lock: SELECT pg_advisory_lock(1);
pg_advisory_lock
----------------
@@ -3899,7 +3899,7 @@ t |t |t
(1 row)
-starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_unset_track_lock_timing 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
------------------------
@@ -3912,9 +3912,9 @@ pg_stat_reset_shared
(1 row)
-step s1_set_log_lock_waits: SET log_lock_waits = on;
+step s1_set_track_lock_timing: SET track_lock_timing = on;
step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
-step s2_unset_log_lock_waits: SET log_lock_waits = off;
+step s2_unset_track_lock_timing: SET track_lock_timing = off;
step s1_begin: BEGIN;
step s1_lock_relation: LOCK TABLE test_stat_tab;
step s2_begin: BEGIN;
diff --git a/src/test/isolation/expected/stats_1.out b/src/test/isolation/expected/stats_1.out
index 4e4c68b4525..955f6ff5ec0 100644
--- a/src/test/isolation/expected/stats_1.out
+++ b/src/test/isolation/expected/stats_1.out
@@ -3776,7 +3776,7 @@ test_stat_func| 1|t |t
step s1_commit: COMMIT;
-starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits s2_set_deadlock_timeout s2_set_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
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_set_track_lock_timing 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
------------------------
@@ -3789,9 +3789,9 @@ pg_stat_reset_shared
(1 row)
-step s1_set_log_lock_waits: SET log_lock_waits = on;
+step s1_set_track_lock_timing: SET track_lock_timing = on;
step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
-step s2_set_log_lock_waits: SET log_lock_waits = on;
+step s2_set_track_lock_timing: SET track_lock_timing = on;
step s1_begin: BEGIN;
step s1_lock_relation: LOCK TABLE test_stat_tab;
step s2_begin: BEGIN;
@@ -3818,7 +3818,7 @@ t |t |t
(1 row)
-starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_set_track_lock_timing 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
------------------------
@@ -3831,9 +3831,9 @@ pg_stat_reset_shared
(1 row)
-step s1_set_log_lock_waits: SET log_lock_waits = on;
+step s1_set_track_lock_timing: SET track_lock_timing = on;
step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
-step s2_set_log_lock_waits: SET log_lock_waits = on;
+step s2_set_track_lock_timing: SET track_lock_timing = 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';
@@ -3861,7 +3861,7 @@ t |t |t
(1 row)
-starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_set_track_lock_timing 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
------------------------
@@ -3874,9 +3874,9 @@ pg_stat_reset_shared
(1 row)
-step s1_set_log_lock_waits: SET log_lock_waits = on;
+step s1_set_track_lock_timing: SET track_lock_timing = on;
step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
-step s2_set_log_lock_waits: SET log_lock_waits = on;
+step s2_set_track_lock_timing: SET track_lock_timing = on;
step s1_lock_advisory_lock: SELECT pg_advisory_lock(1);
pg_advisory_lock
----------------
@@ -3923,7 +3923,7 @@ t |t |t
(1 row)
-starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_unset_track_lock_timing 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
------------------------
@@ -3936,9 +3936,9 @@ pg_stat_reset_shared
(1 row)
-step s1_set_log_lock_waits: SET log_lock_waits = on;
+step s1_set_track_lock_timing: SET track_lock_timing = on;
step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
-step s2_unset_log_lock_waits: SET log_lock_waits = off;
+step s2_unset_track_lock_timing: SET track_lock_timing = off;
step s1_begin: BEGIN;
step s1_lock_relation: LOCK TABLE test_stat_tab;
step s2_begin: BEGIN;
diff --git a/src/test/isolation/specs/stats.spec b/src/test/isolation/specs/stats.spec
index d4b53545198..7c8e2a9214e 100644
--- a/src/test/isolation/specs/stats.spec
+++ b/src/test/isolation/specs/stats.spec
@@ -132,7 +132,7 @@ step s1_slru_check_stats {
# Lock stats steps
step s1_set_deadlock_timeout { SET deadlock_timeout = '10ms'; }
-step s1_set_log_lock_waits { SET log_lock_waits = on; }
+step s1_set_track_lock_timing { SET track_lock_timing = on; }
step s1_reset_stat_lock { SELECT pg_stat_reset_shared('lock'); }
step s1_sleep { SELECT pg_sleep(0.5); }
step s1_lock_relation { LOCK TABLE test_stat_tab; }
@@ -174,8 +174,8 @@ step s2_big_notify { SELECT pg_notify('stats_test_use',
# Lock stats steps
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_set_track_lock_timing { SET track_lock_timing = on; }
+step s2_unset_track_lock_timing { SET track_lock_timing = off; }
step s2_report_stat_lock_relation { SELECT waits > 0, timed_waits = waits, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'relation'; }
step s2_report_stat_lock_transactionid { SELECT waits > 0, timed_waits = waits, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'transactionid'; }
step s2_report_stat_lock_advisory { SELECT waits > 0, timed_waits = waits, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'advisory'; }
@@ -793,9 +793,9 @@ permutation
permutation
s1_set_deadlock_timeout
s1_reset_stat_lock
- s1_set_log_lock_waits
+ s1_set_track_lock_timing
s2_set_deadlock_timeout
- s2_set_log_lock_waits
+ s2_set_track_lock_timing
s1_begin
s1_lock_relation
s2_begin
@@ -811,9 +811,9 @@ permutation
permutation
s1_set_deadlock_timeout
s1_reset_stat_lock
- s1_set_log_lock_waits
+ s1_set_track_lock_timing
s2_set_deadlock_timeout
- s2_set_log_lock_waits
+ s2_set_track_lock_timing
s1_table_insert
s1_begin
s1_table_update_k1
@@ -830,9 +830,9 @@ permutation
permutation
s1_set_deadlock_timeout
s1_reset_stat_lock
- s1_set_log_lock_waits
+ s1_set_track_lock_timing
s2_set_deadlock_timeout
- s2_set_log_lock_waits
+ s2_set_track_lock_timing
s1_lock_advisory_lock
s2_begin
s2_ff
@@ -843,14 +843,14 @@ permutation
s2_commit
s2_report_stat_lock_advisory
-# Ensure log_lock_waits behaves correctly
+# Ensure track_lock_timing behaves correctly
permutation
s1_set_deadlock_timeout
s1_reset_stat_lock
- s1_set_log_lock_waits
+ s1_set_track_lock_timing
s2_set_deadlock_timeout
- s2_unset_log_lock_waits
+ s2_unset_track_lock_timing
s1_begin
s1_lock_relation
s2_begin
--
2.34.1
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-02-20 16:02 Andres Freund <andres@anarazel.de>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
0 siblings, 1 reply; 61+ messages in thread
From: Andres Freund @ 2026-02-20 16:02 UTC (permalink / raw)
To: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: Michael Paquier <michael@paquier.xyz>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi,
On 2026-02-20 06:38:07 +0000, Bertrand Drouvot wrote:
> > If the delay is very
> > short it's probably also not that interesting to track, but I guess that's
> > debatable.
>
> v6 was introducing timed_waits so that we have:
>
> waits
> timed_waits
> wait_time
> fastpath_exceeded
>
> timed_waits and wait_time were incremented together and waits was incremented
> unconditionally. I like the idea of being able to track the numbers of waits
> whatever the value of log_lock_waits (or the new track_lock_timing) is. Also
> one could compare waits vs timed_waits.
How could a user benefit from that split? To me this is pointless number
gathering that wastes resources and confuses users.
Seriously, youre introducing stats left and right, you really need to stop and
first carefully think about what those stats could possibly be useful
for. Before writing a patch implementing the stats.
Greetings,
Andres Freund
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-02-20 17:26 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Andres Freund <andres@anarazel.de>
0 siblings, 1 reply; 61+ messages in thread
From: Bertrand Drouvot @ 2026-02-20 17:26 UTC (permalink / raw)
To: Andres Freund <andres@anarazel.de>; +Cc: Michael Paquier <michael@paquier.xyz>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi,
On Fri, Feb 20, 2026 at 11:02:49AM -0500, Andres Freund wrote:
> Hi,
>
> On 2026-02-20 06:38:07 +0000, Bertrand Drouvot wrote:
> > > If the delay is very
> > > short it's probably also not that interesting to track, but I guess that's
> > > debatable.
> >
> > v6 was introducing timed_waits so that we have:
> >
> > waits
> > timed_waits
> > wait_time
> > fastpath_exceeded
> >
> > timed_waits and wait_time were incremented together and waits was incremented
> > unconditionally. I like the idea of being able to track the numbers of waits
> > whatever the value of log_lock_waits (or the new track_lock_timing) is. Also
> > one could compare waits vs timed_waits.
>
> How could a user benefit from that split? To me this is pointless number
> gathering that wastes resources and confuses users.
I was thinking that could be useful to know the distribution between "long" waits
(greater than the deadlock timeout) among all the waits.
If the vast majority are long waits that may indicate that the application is
misbehaving (as opposed to a tiny percentage of long waits).
I was also thinking to bring those stats per-backend (as a next step) and that
could also probably be more useful (distribution per host for example, thanks to
joining with pg_stat_activity).
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-02-27 05:14 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
0 siblings, 1 reply; 61+ messages in thread
From: Bertrand Drouvot @ 2026-02-27 05:14 UTC (permalink / raw)
To: Andres Freund <andres@anarazel.de>; +Cc: Michael Paquier <michael@paquier.xyz>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi,
On Fri, Feb 20, 2026 at 05:26:37PM +0000, Bertrand Drouvot wrote:
> Hi,
>
> On Fri, Feb 20, 2026 at 11:02:49AM -0500, Andres Freund wrote:
> > Hi,
> >
> > How could a user benefit from that split? To me this is pointless number
> > gathering that wastes resources and confuses users.
>
> I was thinking that could be useful to know the distribution between "long" waits
> (greater than the deadlock timeout) among all the waits.
>
> If the vast majority are long waits that may indicate that the application is
> misbehaving (as opposed to a tiny percentage of long waits).
>
> I was also thinking to bring those stats per-backend (as a next step) and that
> could also probably be more useful (distribution per host for example, thanks to
> joining with pg_stat_activity).
As it seems that I'm the only one thinking that this split could be useful, I'm
removing it in the attached. We can still split later on if we have requests from
the field.
So, we're back to what we were discussing before the split. As in v7, 0003 is
adding the new GUC. So that we can see what having a new GUC implies in ProcSleep()
and we can just get rid of 0003 if we think the GUC is not worth the extra complexity
(I don't have a strong opinion on it but tempted to think that the extra GUC is
not worth it).
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
Attachments:
[text/x-diff] v8-0001-Add-lock-statistics.patch (15.6K, ../../aaEoN1XexvGiFvYp@ip-10-97-1-34.eu-west-3.compute.internal/2-v8-0001-Add-lock-statistics.patch)
download | inline diff:
From bae7e839d430dbea957dea316038b9bc52688170 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Tue, 29 Jul 2025 08:36:35 +0000
Subject: [PATCH v8 1/3] Add lock statistics
Adding a new stat kind PGSTAT_KIND_LOCK for the lock statistics.
This new statistic kind is a fixed one because its key is the lock type
so that we know its size is LOCKTAG_LAST_TYPE + 1.
This statistic kind records the following counters:
waits
wait_time
fastpath_exceeded
The waits and wait_time counters are incremented if log_lock_waits is on
and the session waited longer than deadlock_timeout to acquire the lock.
fastpath_exceeded is incremented when the lock can not be acquired via fast path
because the fast path slot limit was exceeded.
No extra details is added (like the ones, i.e relation oid, database oid, we
can find in pg_locks). The idea is to provide an idea on what the locking
behaviour looks like.
XXX: Bump stat file format
---
src/backend/storage/lmgr/lock.c | 58 ++++----
src/backend/storage/lmgr/proc.c | 6 +
src/backend/utils/activity/Makefile | 1 +
src/backend/utils/activity/meson.build | 1 +
src/backend/utils/activity/pgstat.c | 18 +++
src/backend/utils/activity/pgstat_lock.c | 160 +++++++++++++++++++++++
src/include/pgstat.h | 27 ++++
src/include/utils/pgstat_internal.h | 21 +++
src/include/utils/pgstat_kind.h | 5 +-
src/tools/pgindent/typedefs.list | 4 +
10 files changed, 273 insertions(+), 28 deletions(-)
28.7% src/backend/storage/lmgr/
55.2% src/backend/utils/activity/
7.5% src/include/utils/
7.6% src/include/
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index d930c66cdbd..b2d597bfd1e 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -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"
@@ -984,37 +985,42 @@ LockAcquireExtended(const LOCKTAG *locktag,
* lock type on a relation we have already locked using the fast-path, but
* for now we don't worry about that case either.
*/
- if (EligibleForRelationFastPath(locktag, lockmode) &&
- FastPathLocalUseCounts[FAST_PATH_REL_GROUP(locktag->locktag_field2)] < FP_LOCK_SLOTS_PER_GROUP)
+ if (EligibleForRelationFastPath(locktag, lockmode))
{
- uint32 fasthashcode = FastPathStrongLockHashPartition(hashcode);
- bool acquired;
-
- /*
- * LWLockAcquire acts as a memory sequencing point, so it's safe to
- * assume that any strong locker whose increment to
- * FastPathStrongRelationLocks->counts becomes visible after we test
- * it has yet to begin to transfer fast-path locks.
- */
- LWLockAcquire(&MyProc->fpInfoLock, LW_EXCLUSIVE);
- if (FastPathStrongRelationLocks->count[fasthashcode] != 0)
- acquired = false;
- else
- acquired = FastPathGrantRelationLock(locktag->locktag_field2,
- lockmode);
- LWLockRelease(&MyProc->fpInfoLock);
- if (acquired)
+ if (FastPathLocalUseCounts[FAST_PATH_REL_GROUP(locktag->locktag_field2)] <
+ FP_LOCK_SLOTS_PER_GROUP)
{
+ uint32 fasthashcode = FastPathStrongLockHashPartition(hashcode);
+ bool acquired;
+
/*
- * The locallock might contain stale pointers to some old shared
- * objects; we MUST reset these to null before considering the
- * lock to be acquired via fast-path.
+ * LWLockAcquire acts as a memory sequencing point, so it's safe
+ * to assume that any strong locker whose increment to
+ * FastPathStrongRelationLocks->counts becomes visible after we
+ * test it has yet to begin to transfer fast-path locks.
*/
- locallock->lock = NULL;
- locallock->proclock = NULL;
- GrantLockLocal(locallock, owner);
- return LOCKACQUIRE_OK;
+ LWLockAcquire(&MyProc->fpInfoLock, LW_EXCLUSIVE);
+ if (FastPathStrongRelationLocks->count[fasthashcode] != 0)
+ acquired = false;
+ else
+ acquired = FastPathGrantRelationLock(locktag->locktag_field2,
+ lockmode);
+ LWLockRelease(&MyProc->fpInfoLock);
+ if (acquired)
+ {
+ /*
+ * The locallock might contain stale pointers to some old
+ * shared objects; we MUST reset these to null before
+ * considering the lock to be acquired via fast-path.
+ */
+ locallock->lock = NULL;
+ locallock->proclock = NULL;
+ GrantLockLocal(locallock, owner);
+ return LOCKACQUIRE_OK;
+ }
}
+ else
+ pgstat_count_lock_fastpath_exceeded(locallock->tag.lock.locktag_type);
}
/*
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 771b006b522..e38c8820103 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -1611,9 +1611,15 @@ ProcSleep(LOCALLOCK *locallock)
"Processes holding the lock: %s. Wait queue: %s.",
lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
else if (myWaitStatus == PROC_WAIT_STATUS_OK)
+ {
+ /* Increment the lock statistics counters */
+ pgstat_count_lock_waits(locallock->tag.lock.locktag_type,
+ msecs);
+
ereport(LOG,
(errmsg("process %d acquired %s on %s after %ld.%03d ms",
MyProcPid, modename, buf.data, msecs, usecs)));
+ }
else
{
Assert(myWaitStatus == PROC_WAIT_STATUS_ERROR);
diff --git a/src/backend/utils/activity/Makefile b/src/backend/utils/activity/Makefile
index c37bfb350bb..ca3ef89bf59 100644
--- a/src/backend/utils/activity/Makefile
+++ b/src/backend/utils/activity/Makefile
@@ -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 \
diff --git a/src/backend/utils/activity/meson.build b/src/backend/utils/activity/meson.build
index 53bd5a246ca..1aa7ece5290 100644
--- a/src/backend/utils/activity/meson.build
+++ b/src/backend/utils/activity/meson.build
@@ -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',
diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index 11bb71cad5a..eb8ccbaa628 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -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
index 00000000000..91a53a6013a
--- /dev/null
+++ b/src/backend/utils/activity/pgstat_lock.c
@@ -0,0 +1,160 @@
+/* -------------------------------------------------------------------------
+ *
+ * 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-2025, 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;
+
+/*
+ * 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);
+ }
+}
+
+#define PGSTAT_COUNT_LOCK_FUNC(stat) \
+void \
+CppConcat(pgstat_count_lock_,stat)(uint8 locktag_type) \
+{ \
+ Assert(locktag_type <= LOCKTAG_LAST_TYPE); \
+ PendingLockStats.stats[locktag_type].stat++; \
+ have_lockstats = true; \
+ pgstat_report_fixed = true; \
+}
+
+/* pgstat_count_lock_fastpath_exceeded */
+PGSTAT_COUNT_LOCK_FUNC(fastpath_exceeded)
+
+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;
+}
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 0e9d2b4c623..f563132bf7b 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/lock.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"
@@ -346,6 +347,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;
@@ -618,6 +637,14 @@ 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);
+
/*
* Functions in pgstat_database.c
*/
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 9b8fbae00ed..97704421a92 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -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
diff --git a/src/include/utils/pgstat_kind.h b/src/include/utils/pgstat_kind.h
index c30b6235623..2d78a029683 100644
--- a/src/include/utils/pgstat_kind.h
+++ b/src/include/utils/pgstat_kind.h
@@ -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/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 1a89ef94bec..b4de923667f 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2256,6 +2256,7 @@ PgStatShared_Database
PgStatShared_Function
PgStatShared_HashEntry
PgStatShared_IO
+PgStatShared_Lock
PgStatShared_Relation
PgStatShared_ReplSlot
PgStatShared_SLRU
@@ -2278,8 +2279,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
--
2.34.1
[text/x-diff] v8-0002-Add-the-pg_stat_lock-view.patch (29.9K, ../../aaEoN1XexvGiFvYp@ip-10-97-1-34.eu-west-3.compute.internal/3-v8-0002-Add-the-pg_stat_lock-view.patch)
download | inline diff:
From e71db2d88480f5a1a300c8c73b4428bfe5d5d877 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Thu, 31 Jul 2025 09:35:31 +0000
Subject: [PATCH v8 2/3] Add the pg_stat_lock view
This new view reports lock statistics.
Note that it does not omit combinations which do not make sense (as pg_locks
does).
Also wait_time is reported as bigint as the deadlock_timeout default value is
1s.
This commit also adds documentation and a few tests.
XXX: Bump catversion
---
doc/src/sgml/monitoring.sgml | 113 ++++++++++++++
src/backend/catalog/system_views.sql | 9 ++
src/backend/utils/activity/pgstat_lock.c | 8 +
src/backend/utils/adt/pgstatfuncs.c | 39 +++++
src/include/catalog/pg_proc.dat | 9 ++
src/include/pgstat.h | 1 +
src/test/isolation/expected/stats.out | 189 +++++++++++++++++++++++
src/test/isolation/expected/stats_1.out | 189 +++++++++++++++++++++++
src/test/isolation/specs/stats.spec | 95 ++++++++++++
src/test/regress/expected/rules.out | 6 +
src/test/regress/expected/stats.out | 48 ++++++
src/test/regress/sql/stats.sql | 36 +++++
12 files changed, 742 insertions(+)
15.9% doc/src/sgml/
5.0% src/backend/utils/adt/
54.1% src/test/isolation/expected/
11.3% src/test/isolation/specs/
5.7% src/test/regress/expected/
4.0% src/test/regress/sql/
3.6% src/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index b77d189a500..3a196bc305c 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -493,6 +493,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
@@ -3124,6 +3133,104 @@ 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 <xref linkend="guc-log-lock-waits"/>
+ is enabled and 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 <xref linkend="guc-log-lock-waits"/> is enabled and
+ 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. You may want to increase
+ <xref linkend="guc-max-locks-per-transaction"/> if you feel this counter
+ is too high.
+ </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>
@@ -5195,6 +5302,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
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 1ea8f1faa9e..22efeb713e4 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -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,
diff --git a/src/backend/utils/activity/pgstat_lock.c b/src/backend/utils/activity/pgstat_lock.c
index 91a53a6013a..954fa51c3e0 100644
--- a/src/backend/utils/activity/pgstat_lock.c
+++ b/src/backend/utils/activity/pgstat_lock.c
@@ -21,6 +21,14 @@
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()
*/
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index b1df96e7b0b..27ca2d8f07c 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1737,6 +1737,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.
*/
@@ -1921,6 +1957,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);
@@ -1938,6 +1975,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)
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index dac40992cbc..daeb45704c6 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6027,6 +6027,15 @@
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',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index f563132bf7b..1c261e2c7a8 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -644,6 +644,7 @@ 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 PgStat_Lock *pgstat_fetch_stat_lock(void);
/*
* Functions in pgstat_database.c
diff --git a/src/test/isolation/expected/stats.out b/src/test/isolation/expected/stats.out
index cfad309ccf3..3cae3052e40 100644
--- a/src/test/isolation/expected/stats.out
+++ b/src/test/isolation/expected/stats.out
@@ -3751,3 +3751,192 @@ test_stat_func| 1|t |t
(1 row)
step s1_commit: COMMIT;
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits s2_set_deadlock_timeout s2_set_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 s1_set_log_lock_waits: SET log_lock_waits = on;
+step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
+step s2_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'relation';
+?column?|?column?
+--------+--------
+t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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 s1_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'transactionid';
+?column?|?column?
+--------+--------
+t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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 s1_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'advisory';
+?column?|?column?
+--------+--------
+t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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 s1_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'relation';
+?column?|?column?
+--------+--------
+f |f
+(1 row)
+
diff --git a/src/test/isolation/expected/stats_1.out b/src/test/isolation/expected/stats_1.out
index e1d937784cb..ea4fd97a9a5 100644
--- a/src/test/isolation/expected/stats_1.out
+++ b/src/test/isolation/expected/stats_1.out
@@ -3775,3 +3775,192 @@ test_stat_func| 1|t |t
(1 row)
step s1_commit: COMMIT;
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits s2_set_deadlock_timeout s2_set_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 s1_set_log_lock_waits: SET log_lock_waits = on;
+step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
+step s2_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'relation';
+?column?|?column?
+--------+--------
+t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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 s1_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'transactionid';
+?column?|?column?
+--------+--------
+t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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 s1_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'advisory';
+?column?|?column?
+--------+--------
+t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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 s1_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'relation';
+?column?|?column?
+--------+--------
+f |f
+(1 row)
+
diff --git a/src/test/isolation/specs/stats.spec b/src/test/isolation/specs/stats.spec
index da16710da0f..42be68c545f 100644
--- a/src/test/isolation/specs/stats.spec
+++ b/src/test/isolation/specs/stats.spec
@@ -130,6 +130,14 @@ step s1_slru_check_stats {
WHERE before.stat = 'blks_zeroed';
}
+# Lock stats steps
+step s1_set_deadlock_timeout { SET deadlock_timeout = '10ms'; }
+step s1_set_log_lock_waits { SET log_lock_waits = on; }
+step s1_reset_stat_lock { SELECT pg_stat_reset_shared('lock'); }
+step s1_sleep { SELECT pg_sleep(0.5); }
+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'; }
@@ -164,6 +172,16 @@ step s2_big_notify { SELECT pg_notify('stats_test_use',
repeat(i::text, current_setting('block_size')::int / 2)) FROM generate_series(1, 3) g(i);
}
+# Lock stats steps
+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, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'relation'; }
+step s2_report_stat_lock_transactionid { SELECT waits > 0, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'transactionid'; }
+step s2_report_stat_lock_advisory { SELECT waits > 0, wait_time > 500 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); }
######################
# Function stats tests
@@ -765,3 +783,80 @@ permutation
s1_clear_snapshot
s1_func_stats
s1_commit
+
+######################
+# Lock stats tests
+######################
+
+# relation lock
+
+permutation
+ s1_set_deadlock_timeout
+ s1_reset_stat_lock
+ s1_set_log_lock_waits
+ s2_set_deadlock_timeout
+ s2_set_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
+
+# transaction lock
+
+permutation
+ s1_set_deadlock_timeout
+ s1_reset_stat_lock
+ s1_set_log_lock_waits
+ 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
+ s1_set_log_lock_waits
+ 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 behaves correctly
+
+permutation
+ s1_set_deadlock_timeout
+ s1_reset_stat_lock
+ s1_set_log_lock_waits
+ 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
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 78a37d9fc8f..c013fbebb88 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1951,6 +1951,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,
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index cd00f35bf7a..93de87747d4 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -1910,4 +1910,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
diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql
index 8768e0f27fd..db9948eedb8 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -944,4 +944,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
--
2.34.1
[text/x-diff] v8-0003-Introduce-a-new-track_lock_timing-GUC.patch (25.6K, ../../aaEoN1XexvGiFvYp@ip-10-97-1-34.eu-west-3.compute.internal/4-v8-0003-Introduce-a-new-track_lock_timing-GUC.patch)
download | inline diff:
From 1a7dfa836fa8c3c67b6423c587ebba2edf823cdf Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Fri, 20 Feb 2026 06:13:28 +0000
Subject: [PATCH v8 3/3] Introduce a new track_lock_timing GUC
A new GUC (track_lock_timing) is added and defaults to on. If on, waits and
wait_time counters are incremented if the session waited longer than deadlock_timeout
to acquire the lock. It's on by default, as this is the same idea as 2aac62be8cb.
---
doc/src/sgml/config.sgml | 19 +++
doc/src/sgml/monitoring.sgml | 4 +-
src/backend/storage/lmgr/proc.c | 160 +++++++++---------
src/backend/utils/misc/guc_parameters.dat | 6 +
src/backend/utils/misc/postgresql.conf.sample | 1 +
src/include/pgstat.h | 1 +
src/test/isolation/expected/stats.out | 24 +--
src/test/isolation/expected/stats_1.out | 24 +--
src/test/isolation/specs/stats.spec | 24 +--
9 files changed, 149 insertions(+), 114 deletions(-)
7.6% doc/src/sgml/
47.7% src/backend/storage/lmgr/
36.3% src/test/isolation/expected/
6.1% src/test/isolation/specs/
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index f670e2d4c31..1d3acb697da 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8842,6 +8842,25 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</listitem>
</varlistentry>
+ <varlistentry id="guc-track-lock-timing" xreflabel="track_lock_timing">
+ <term><varname>track_lock_timing</varname> (<type>boolean</type>)
+ <indexterm>
+ <primary><varname>track_lock_timing</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Enables timing of lock waits. This parameter is on by default, as it tracks
+ only the timings for successful acquisitions that waited longer than
+ <xref linkend="guc-deadlock-timeout"/>. Lock timing information is
+ displayed in the <link linkend="monitoring-pg-stat-lock-view">
+ <structname>pg_stat_lock</structname></link> view.
+ Only superusers and users with the appropriate <literal>SET</literal>
+ privilege can change this setting.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-track-wal-io-timing" xreflabel="track_wal_io_timing">
<term><varname>track_wal_io_timing</varname> (<type>boolean</type>)
<indexterm>
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 3a196bc305c..75c3e036860 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -3181,7 +3181,7 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para>
<para>
Number of times a lock of this type had to wait because of a
- conflicting lock. Only incremented when <xref linkend="guc-log-lock-waits"/>
+ conflicting lock. Only incremented when <xref linkend="guc-track-lock-timing"/>
is enabled and the lock was successfully acquired after waiting longer
than <xref linkend="guc-deadlock-timeout"/>.
</para>
@@ -3195,7 +3195,7 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para>
<para>
Total time spent waiting for locks of this type, in milliseconds.
- Only incremented when <xref linkend="guc-log-lock-waits"/> is enabled and
+ Only incremented when <xref linkend="guc-track-lock-timing"/> is enabled and
the lock was successfully acquired after waiting longer than
<xref linkend="guc-deadlock-timeout"/>.
</para>
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index e38c8820103..7681f87f078 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -62,6 +62,7 @@ int IdleInTransactionSessionTimeout = 0;
int TransactionTimeout = 0;
int IdleSessionTimeout = 0;
bool log_lock_waits = true;
+bool track_lock_timing = true;
/* Pointer to this process's PGPROC struct, if any */
PGPROC *MyProc = NULL;
@@ -1547,99 +1548,110 @@ ProcSleep(LOCALLOCK *locallock)
/*
* If awoken after the deadlock check interrupt has run, and
- * log_lock_waits is on, then report about the wait.
+ * log_lock_waits or track_lock_timing is on, then report or track
+ * about the wait.
*/
- if (log_lock_waits && deadlock_state != DS_NOT_YET_CHECKED)
+ if ((log_lock_waits || track_lock_timing) &&
+ deadlock_state != DS_NOT_YET_CHECKED)
{
- StringInfoData buf,
- lock_waiters_sbuf,
- lock_holders_sbuf;
- const char *modename;
long secs;
int usecs;
long msecs;
- int lockHoldersNum = 0;
- initStringInfo(&buf);
- initStringInfo(&lock_waiters_sbuf);
- initStringInfo(&lock_holders_sbuf);
-
- DescribeLockTag(&buf, &locallock->tag.lock);
- modename = GetLockmodeName(locallock->tag.lock.locktag_lockmethodid,
- lockmode);
TimestampDifference(get_timeout_start_time(DEADLOCK_TIMEOUT),
GetCurrentTimestamp(),
&secs, &usecs);
msecs = secs * 1000 + usecs / 1000;
usecs = usecs % 1000;
- /* Gather a list of all lock holders and waiters */
- LWLockAcquire(partitionLock, LW_SHARED);
- GetLockHoldersAndWaiters(locallock, &lock_holders_sbuf,
- &lock_waiters_sbuf, &lockHoldersNum);
- LWLockRelease(partitionLock);
-
- if (deadlock_state == DS_SOFT_DEADLOCK)
- ereport(LOG,
- (errmsg("process %d avoided deadlock for %s on %s by rearranging queue order after %ld.%03d ms",
- MyProcPid, modename, buf.data, msecs, usecs),
- (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
- "Processes holding the lock: %s. Wait queue: %s.",
- lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
- else if (deadlock_state == DS_HARD_DEADLOCK)
- {
- /*
- * This message is a bit redundant with the error that will be
- * reported subsequently, but in some cases the error report
- * might not make it to the log (eg, if it's caught by an
- * exception handler), and we want to ensure all long-wait
- * events get logged.
- */
- ereport(LOG,
- (errmsg("process %d detected deadlock while waiting for %s on %s after %ld.%03d ms",
- MyProcPid, modename, buf.data, msecs, usecs),
- (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
- "Processes holding the lock: %s. Wait queue: %s.",
- lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
- }
-
- if (myWaitStatus == PROC_WAIT_STATUS_WAITING)
- ereport(LOG,
- (errmsg("process %d still waiting for %s on %s after %ld.%03d ms",
- MyProcPid, modename, buf.data, msecs, usecs),
- (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
- "Processes holding the lock: %s. Wait queue: %s.",
- lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
- else if (myWaitStatus == PROC_WAIT_STATUS_OK)
- {
- /* Increment the lock statistics counters */
+ /* Increment the lock statistics counters */
+ if (track_lock_timing && myWaitStatus == PROC_WAIT_STATUS_OK)
pgstat_count_lock_waits(locallock->tag.lock.locktag_type,
msecs);
- ereport(LOG,
- (errmsg("process %d acquired %s on %s after %ld.%03d ms",
- MyProcPid, modename, buf.data, msecs, usecs)));
- }
- else
+ if (log_lock_waits)
{
- Assert(myWaitStatus == PROC_WAIT_STATUS_ERROR);
-
- /*
- * Currently, the deadlock checker always kicks its own
- * process, which means that we'll only see
- * PROC_WAIT_STATUS_ERROR when deadlock_state ==
- * DS_HARD_DEADLOCK, and there's no need to print redundant
- * messages. But for completeness and future-proofing, print
- * a message if it looks like someone else kicked us off the
- * lock.
- */
- if (deadlock_state != DS_HARD_DEADLOCK)
+ StringInfoData buf,
+ lock_waiters_sbuf,
+ lock_holders_sbuf;
+ const char *modename;
+ int lockHoldersNum = 0;
+
+ initStringInfo(&buf);
+ initStringInfo(&lock_waiters_sbuf);
+ initStringInfo(&lock_holders_sbuf);
+
+ DescribeLockTag(&buf, &locallock->tag.lock);
+ modename = GetLockmodeName(locallock->tag.lock.locktag_lockmethodid,
+ lockmode);
+
+ /* Gather a list of all lock holders and waiters */
+ LWLockAcquire(partitionLock, LW_SHARED);
+ GetLockHoldersAndWaiters(locallock, &lock_holders_sbuf,
+ &lock_waiters_sbuf, &lockHoldersNum);
+ LWLockRelease(partitionLock);
+
+ if (deadlock_state == DS_SOFT_DEADLOCK)
+ ereport(LOG,
+ (errmsg("process %d avoided deadlock for %s on %s by rearranging queue order after %ld.%03d ms",
+ MyProcPid, modename, buf.data, msecs, usecs),
+ (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
+ "Processes holding the lock: %s. Wait queue: %s.",
+ lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
+ else if (deadlock_state == DS_HARD_DEADLOCK)
+ {
+ /*
+ * This message is a bit redundant with the error that
+ * will be reported subsequently, but in some cases the
+ * error report might not make it to the log (eg, if it's
+ * caught by an exception handler), and we want to ensure
+ * all long-wait events get logged.
+ */
ereport(LOG,
- (errmsg("process %d failed to acquire %s on %s after %ld.%03d ms",
+ (errmsg("process %d detected deadlock while waiting for %s on %s after %ld.%03d ms",
MyProcPid, modename, buf.data, msecs, usecs),
(errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
"Processes holding the lock: %s. Wait queue: %s.",
lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
+ }
+
+ if (myWaitStatus == PROC_WAIT_STATUS_WAITING)
+ ereport(LOG,
+ (errmsg("process %d still waiting for %s on %s after %ld.%03d ms",
+ MyProcPid, modename, buf.data, msecs, usecs),
+ (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
+ "Processes holding the lock: %s. Wait queue: %s.",
+ lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
+
+ else if (myWaitStatus == PROC_WAIT_STATUS_OK)
+ ereport(LOG,
+ (errmsg("process %d acquired %s on %s after %ld.%03d ms",
+ MyProcPid, modename, buf.data, msecs, usecs)));
+ else
+ {
+ Assert(myWaitStatus == PROC_WAIT_STATUS_ERROR);
+
+ /*
+ * Currently, the deadlock checker always kicks its own
+ * process, which means that we'll only see
+ * PROC_WAIT_STATUS_ERROR when deadlock_state ==
+ * DS_HARD_DEADLOCK, and there's no need to print
+ * redundant messages. But for completeness and
+ * future-proofing, print a message if it looks like
+ * someone else kicked us off the lock.
+ */
+ if (deadlock_state != DS_HARD_DEADLOCK)
+ ereport(LOG,
+ (errmsg("process %d failed to acquire %s on %s after %ld.%03d ms",
+ MyProcPid, modename, buf.data, msecs, usecs),
+ (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
+ "Processes holding the lock: %s. Wait queue: %s.",
+ lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
+ }
+
+ pfree(buf.data);
+ pfree(lock_holders_sbuf.data);
+ pfree(lock_waiters_sbuf.data);
}
/*
@@ -1647,10 +1659,6 @@ ProcSleep(LOCALLOCK *locallock)
* state so we don't print the above messages again.
*/
deadlock_state = DS_NO_DEADLOCK;
-
- pfree(buf.data);
- pfree(lock_holders_sbuf.data);
- pfree(lock_waiters_sbuf.data);
}
} while (myWaitStatus == PROC_WAIT_STATUS_WAITING);
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index 9507778415d..c21e6c6ea4e 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -3110,6 +3110,12 @@
boot_val => 'false',
},
+{ name => 'track_lock_timing', type => 'bool', context => 'PGC_SUSET', group => 'STATS_CUMULATIVE',
+ short_desc => 'Collects timing statistics for lock acquisition.',
+ variable => 'track_lock_timing',
+ boot_val => 'true',
+},
+
{ name => 'track_wal_io_timing', type => 'bool', context => 'PGC_SUSET', group => 'STATS_CUMULATIVE',
short_desc => 'Collects timing statistics for WAL I/O activity.',
variable => 'track_wal_io_timing',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index f938cc65a3a..8a3a704aaa5 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -685,6 +685,7 @@
#track_counts = on
#track_cost_delay_timing = off
#track_io_timing = off
+#track_lock_timing = on
#track_wal_io_timing = off
#track_functions = none # none, pl, all
#stats_fetch_consistency = cache # cache, none, snapshot
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 1c261e2c7a8..1ba7d98d06b 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -843,6 +843,7 @@ extern PgStat_WalStats *pgstat_fetch_stat_wal(void);
extern PGDLLIMPORT bool pgstat_track_counts;
extern PGDLLIMPORT int pgstat_track_functions;
extern PGDLLIMPORT int pgstat_fetch_consistency;
+extern PGDLLIMPORT bool track_lock_timing;
/*
diff --git a/src/test/isolation/expected/stats.out b/src/test/isolation/expected/stats.out
index 3cae3052e40..4f7ad061549 100644
--- a/src/test/isolation/expected/stats.out
+++ b/src/test/isolation/expected/stats.out
@@ -3752,7 +3752,7 @@ test_stat_func| 1|t |t
step s1_commit: COMMIT;
-starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits s2_set_deadlock_timeout s2_set_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
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_set_track_lock_timing 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
------------------------
@@ -3765,9 +3765,9 @@ pg_stat_reset_shared
(1 row)
-step s1_set_log_lock_waits: SET log_lock_waits = on;
+step s1_set_track_lock_timing: SET track_lock_timing = on;
step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
-step s2_set_log_lock_waits: SET log_lock_waits = on;
+step s2_set_track_lock_timing: SET track_lock_timing = on;
step s1_begin: BEGIN;
step s1_lock_relation: LOCK TABLE test_stat_tab;
step s2_begin: BEGIN;
@@ -3794,7 +3794,7 @@ t |t
(1 row)
-starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_set_track_lock_timing 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
------------------------
@@ -3807,9 +3807,9 @@ pg_stat_reset_shared
(1 row)
-step s1_set_log_lock_waits: SET log_lock_waits = on;
+step s1_set_track_lock_timing: SET track_lock_timing = on;
step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
-step s2_set_log_lock_waits: SET log_lock_waits = on;
+step s2_set_track_lock_timing: SET track_lock_timing = 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';
@@ -3837,7 +3837,7 @@ t |t
(1 row)
-starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_set_track_lock_timing 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
------------------------
@@ -3850,9 +3850,9 @@ pg_stat_reset_shared
(1 row)
-step s1_set_log_lock_waits: SET log_lock_waits = on;
+step s1_set_track_lock_timing: SET track_lock_timing = on;
step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
-step s2_set_log_lock_waits: SET log_lock_waits = on;
+step s2_set_track_lock_timing: SET track_lock_timing = on;
step s1_lock_advisory_lock: SELECT pg_advisory_lock(1);
pg_advisory_lock
----------------
@@ -3899,7 +3899,7 @@ t |t
(1 row)
-starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_unset_track_lock_timing 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
------------------------
@@ -3912,9 +3912,9 @@ pg_stat_reset_shared
(1 row)
-step s1_set_log_lock_waits: SET log_lock_waits = on;
+step s1_set_track_lock_timing: SET track_lock_timing = on;
step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
-step s2_unset_log_lock_waits: SET log_lock_waits = off;
+step s2_unset_track_lock_timing: SET track_lock_timing = off;
step s1_begin: BEGIN;
step s1_lock_relation: LOCK TABLE test_stat_tab;
step s2_begin: BEGIN;
diff --git a/src/test/isolation/expected/stats_1.out b/src/test/isolation/expected/stats_1.out
index ea4fd97a9a5..e1a60d41bad 100644
--- a/src/test/isolation/expected/stats_1.out
+++ b/src/test/isolation/expected/stats_1.out
@@ -3776,7 +3776,7 @@ test_stat_func| 1|t |t
step s1_commit: COMMIT;
-starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits s2_set_deadlock_timeout s2_set_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
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_set_track_lock_timing 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
------------------------
@@ -3789,9 +3789,9 @@ pg_stat_reset_shared
(1 row)
-step s1_set_log_lock_waits: SET log_lock_waits = on;
+step s1_set_track_lock_timing: SET track_lock_timing = on;
step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
-step s2_set_log_lock_waits: SET log_lock_waits = on;
+step s2_set_track_lock_timing: SET track_lock_timing = on;
step s1_begin: BEGIN;
step s1_lock_relation: LOCK TABLE test_stat_tab;
step s2_begin: BEGIN;
@@ -3818,7 +3818,7 @@ t |t
(1 row)
-starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_set_track_lock_timing 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
------------------------
@@ -3831,9 +3831,9 @@ pg_stat_reset_shared
(1 row)
-step s1_set_log_lock_waits: SET log_lock_waits = on;
+step s1_set_track_lock_timing: SET track_lock_timing = on;
step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
-step s2_set_log_lock_waits: SET log_lock_waits = on;
+step s2_set_track_lock_timing: SET track_lock_timing = 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';
@@ -3861,7 +3861,7 @@ t |t
(1 row)
-starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_set_track_lock_timing 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
------------------------
@@ -3874,9 +3874,9 @@ pg_stat_reset_shared
(1 row)
-step s1_set_log_lock_waits: SET log_lock_waits = on;
+step s1_set_track_lock_timing: SET track_lock_timing = on;
step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
-step s2_set_log_lock_waits: SET log_lock_waits = on;
+step s2_set_track_lock_timing: SET track_lock_timing = on;
step s1_lock_advisory_lock: SELECT pg_advisory_lock(1);
pg_advisory_lock
----------------
@@ -3923,7 +3923,7 @@ t |t
(1 row)
-starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_unset_track_lock_timing 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
------------------------
@@ -3936,9 +3936,9 @@ pg_stat_reset_shared
(1 row)
-step s1_set_log_lock_waits: SET log_lock_waits = on;
+step s1_set_track_lock_timing: SET track_lock_timing = on;
step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
-step s2_unset_log_lock_waits: SET log_lock_waits = off;
+step s2_unset_track_lock_timing: SET track_lock_timing = off;
step s1_begin: BEGIN;
step s1_lock_relation: LOCK TABLE test_stat_tab;
step s2_begin: BEGIN;
diff --git a/src/test/isolation/specs/stats.spec b/src/test/isolation/specs/stats.spec
index 42be68c545f..81b45a801d9 100644
--- a/src/test/isolation/specs/stats.spec
+++ b/src/test/isolation/specs/stats.spec
@@ -132,7 +132,7 @@ step s1_slru_check_stats {
# Lock stats steps
step s1_set_deadlock_timeout { SET deadlock_timeout = '10ms'; }
-step s1_set_log_lock_waits { SET log_lock_waits = on; }
+step s1_set_track_lock_timing { SET track_lock_timing = on; }
step s1_reset_stat_lock { SELECT pg_stat_reset_shared('lock'); }
step s1_sleep { SELECT pg_sleep(0.5); }
step s1_lock_relation { LOCK TABLE test_stat_tab; }
@@ -174,8 +174,8 @@ step s2_big_notify { SELECT pg_notify('stats_test_use',
# Lock stats steps
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_set_track_lock_timing { SET track_lock_timing = on; }
+step s2_unset_track_lock_timing { SET track_lock_timing = off; }
step s2_report_stat_lock_relation { SELECT waits > 0, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'relation'; }
step s2_report_stat_lock_transactionid { SELECT waits > 0, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'transactionid'; }
step s2_report_stat_lock_advisory { SELECT waits > 0, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'advisory'; }
@@ -793,9 +793,9 @@ permutation
permutation
s1_set_deadlock_timeout
s1_reset_stat_lock
- s1_set_log_lock_waits
+ s1_set_track_lock_timing
s2_set_deadlock_timeout
- s2_set_log_lock_waits
+ s2_set_track_lock_timing
s1_begin
s1_lock_relation
s2_begin
@@ -811,9 +811,9 @@ permutation
permutation
s1_set_deadlock_timeout
s1_reset_stat_lock
- s1_set_log_lock_waits
+ s1_set_track_lock_timing
s2_set_deadlock_timeout
- s2_set_log_lock_waits
+ s2_set_track_lock_timing
s1_table_insert
s1_begin
s1_table_update_k1
@@ -830,9 +830,9 @@ permutation
permutation
s1_set_deadlock_timeout
s1_reset_stat_lock
- s1_set_log_lock_waits
+ s1_set_track_lock_timing
s2_set_deadlock_timeout
- s2_set_log_lock_waits
+ s2_set_track_lock_timing
s1_lock_advisory_lock
s2_begin
s2_ff
@@ -843,14 +843,14 @@ permutation
s2_commit
s2_report_stat_lock_advisory
-# Ensure log_lock_waits behaves correctly
+# Ensure track_lock_timing behaves correctly
permutation
s1_set_deadlock_timeout
s1_reset_stat_lock
- s1_set_log_lock_waits
+ s1_set_track_lock_timing
s2_set_deadlock_timeout
- s2_unset_log_lock_waits
+ s2_unset_track_lock_timing
s1_begin
s1_lock_relation
s2_begin
--
2.34.1
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-03-18 04:36 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
0 siblings, 1 reply; 61+ messages in thread
From: Bertrand Drouvot @ 2026-03-18 04:36 UTC (permalink / raw)
To: Andres Freund <andres@anarazel.de>; +Cc: Michael Paquier <michael@paquier.xyz>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi,
On Fri, Feb 27, 2026 at 05:14:31AM +0000, Bertrand Drouvot wrote:
> Hi,
>
> On Fri, Feb 20, 2026 at 05:26:37PM +0000, Bertrand Drouvot wrote:
> > Hi,
> >
> > On Fri, Feb 20, 2026 at 11:02:49AM -0500, Andres Freund wrote:
> > > Hi,
> > >
> > > How could a user benefit from that split? To me this is pointless number
> > > gathering that wastes resources and confuses users.
> >
> > I was thinking that could be useful to know the distribution between "long" waits
> > (greater than the deadlock timeout) among all the waits.
> >
> > If the vast majority are long waits that may indicate that the application is
> > misbehaving (as opposed to a tiny percentage of long waits).
> >
> > I was also thinking to bring those stats per-backend (as a next step) and that
> > could also probably be more useful (distribution per host for example, thanks to
> > joining with pg_stat_activity).
>
> As it seems that I'm the only one thinking that this split could be useful, I'm
> removing it in the attached. We can still split later on if we have requests from
> the field.
>
> So, we're back to what we were discussing before the split. As in v7, 0003 is
> adding the new GUC. So that we can see what having a new GUC implies in ProcSleep()
> and we can just get rid of 0003 if we think the GUC is not worth the extra complexity
> (I don't have a strong opinion on it but tempted to think that the extra GUC is
> not worth it).
PFA, a rebase due to fd6ecbfa75ff.
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
Attachments:
[text/x-diff] v9-0001-Add-lock-statistics.patch (15.5K, ../../abortN3ngqMcaBJ7@ip-10-97-1-34.eu-west-3.compute.internal/2-v9-0001-Add-lock-statistics.patch)
download | inline diff:
From 0052bdf7c8f710b9388430e28f8184064238e054 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Tue, 29 Jul 2025 08:36:35 +0000
Subject: [PATCH v9 1/3] Add lock statistics
Adding a new stat kind PGSTAT_KIND_LOCK for the lock statistics.
This new statistic kind is a fixed one because its key is the lock type
so that we know its size is LOCKTAG_LAST_TYPE + 1.
This statistic kind records the following counters:
waits
wait_time
fastpath_exceeded
The waits and wait_time counters are incremented if log_lock_waits is on
and the session waited longer than deadlock_timeout to acquire the lock.
fastpath_exceeded is incremented when the lock can not be acquired via fast path
because the fast path slot limit was exceeded.
No extra details is added (like the ones, i.e relation oid, database oid, we
can find in pg_locks). The idea is to provide an idea on what the locking
behaviour looks like.
XXX: Bump stat file format
---
src/backend/storage/lmgr/lock.c | 58 ++++----
src/backend/storage/lmgr/proc.c | 5 +
src/backend/utils/activity/Makefile | 1 +
src/backend/utils/activity/meson.build | 1 +
src/backend/utils/activity/pgstat.c | 18 +++
src/backend/utils/activity/pgstat_lock.c | 160 +++++++++++++++++++++++
src/include/pgstat.h | 27 ++++
src/include/utils/pgstat_internal.h | 21 +++
src/include/utils/pgstat_kind.h | 5 +-
src/tools/pgindent/typedefs.list | 4 +
10 files changed, 272 insertions(+), 28 deletions(-)
28.6% src/backend/storage/lmgr/
55.2% src/backend/utils/activity/
7.5% src/include/utils/
7.7% src/include/
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index d930c66cdbd..b2d597bfd1e 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -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"
@@ -984,37 +985,42 @@ LockAcquireExtended(const LOCKTAG *locktag,
* lock type on a relation we have already locked using the fast-path, but
* for now we don't worry about that case either.
*/
- if (EligibleForRelationFastPath(locktag, lockmode) &&
- FastPathLocalUseCounts[FAST_PATH_REL_GROUP(locktag->locktag_field2)] < FP_LOCK_SLOTS_PER_GROUP)
+ if (EligibleForRelationFastPath(locktag, lockmode))
{
- uint32 fasthashcode = FastPathStrongLockHashPartition(hashcode);
- bool acquired;
-
- /*
- * LWLockAcquire acts as a memory sequencing point, so it's safe to
- * assume that any strong locker whose increment to
- * FastPathStrongRelationLocks->counts becomes visible after we test
- * it has yet to begin to transfer fast-path locks.
- */
- LWLockAcquire(&MyProc->fpInfoLock, LW_EXCLUSIVE);
- if (FastPathStrongRelationLocks->count[fasthashcode] != 0)
- acquired = false;
- else
- acquired = FastPathGrantRelationLock(locktag->locktag_field2,
- lockmode);
- LWLockRelease(&MyProc->fpInfoLock);
- if (acquired)
+ if (FastPathLocalUseCounts[FAST_PATH_REL_GROUP(locktag->locktag_field2)] <
+ FP_LOCK_SLOTS_PER_GROUP)
{
+ uint32 fasthashcode = FastPathStrongLockHashPartition(hashcode);
+ bool acquired;
+
/*
- * The locallock might contain stale pointers to some old shared
- * objects; we MUST reset these to null before considering the
- * lock to be acquired via fast-path.
+ * LWLockAcquire acts as a memory sequencing point, so it's safe
+ * to assume that any strong locker whose increment to
+ * FastPathStrongRelationLocks->counts becomes visible after we
+ * test it has yet to begin to transfer fast-path locks.
*/
- locallock->lock = NULL;
- locallock->proclock = NULL;
- GrantLockLocal(locallock, owner);
- return LOCKACQUIRE_OK;
+ LWLockAcquire(&MyProc->fpInfoLock, LW_EXCLUSIVE);
+ if (FastPathStrongRelationLocks->count[fasthashcode] != 0)
+ acquired = false;
+ else
+ acquired = FastPathGrantRelationLock(locktag->locktag_field2,
+ lockmode);
+ LWLockRelease(&MyProc->fpInfoLock);
+ if (acquired)
+ {
+ /*
+ * The locallock might contain stale pointers to some old
+ * shared objects; we MUST reset these to null before
+ * considering the lock to be acquired via fast-path.
+ */
+ locallock->lock = NULL;
+ locallock->proclock = NULL;
+ GrantLockLocal(locallock, owner);
+ return LOCKACQUIRE_OK;
+ }
}
+ else
+ pgstat_count_lock_fastpath_exceeded(locallock->tag.lock.locktag_type);
}
/*
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 8f5ce0e2a8a..263dc88b735 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -1631,9 +1631,14 @@ ProcSleep(LOCALLOCK *locallock)
}
}
else if (myWaitStatus == PROC_WAIT_STATUS_OK)
+ {
+ /* Increment the lock statistics counters */
+ pgstat_count_lock_waits(locallock->tag.lock.locktag_type, msecs);
+
ereport(LOG,
(errmsg("process %d acquired %s on %s after %ld.%03d ms",
MyProcPid, modename, buf.data, msecs, usecs)));
+ }
else
{
Assert(myWaitStatus == PROC_WAIT_STATUS_ERROR);
diff --git a/src/backend/utils/activity/Makefile b/src/backend/utils/activity/Makefile
index c37bfb350bb..ca3ef89bf59 100644
--- a/src/backend/utils/activity/Makefile
+++ b/src/backend/utils/activity/Makefile
@@ -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 \
diff --git a/src/backend/utils/activity/meson.build b/src/backend/utils/activity/meson.build
index 53bd5a246ca..1aa7ece5290 100644
--- a/src/backend/utils/activity/meson.build
+++ b/src/backend/utils/activity/meson.build
@@ -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',
diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index 11bb71cad5a..eb8ccbaa628 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -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
index 00000000000..91a53a6013a
--- /dev/null
+++ b/src/backend/utils/activity/pgstat_lock.c
@@ -0,0 +1,160 @@
+/* -------------------------------------------------------------------------
+ *
+ * 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-2025, 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;
+
+/*
+ * 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);
+ }
+}
+
+#define PGSTAT_COUNT_LOCK_FUNC(stat) \
+void \
+CppConcat(pgstat_count_lock_,stat)(uint8 locktag_type) \
+{ \
+ Assert(locktag_type <= LOCKTAG_LAST_TYPE); \
+ PendingLockStats.stats[locktag_type].stat++; \
+ have_lockstats = true; \
+ pgstat_report_fixed = true; \
+}
+
+/* pgstat_count_lock_fastpath_exceeded */
+PGSTAT_COUNT_LOCK_FUNC(fastpath_exceeded)
+
+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;
+}
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 216b93492ba..4a28ea99705 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/lock.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"
@@ -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,14 @@ 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);
+
/*
* Functions in pgstat_database.c
*/
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 9b8fbae00ed..97704421a92 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -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
diff --git a/src/include/utils/pgstat_kind.h b/src/include/utils/pgstat_kind.h
index c30b6235623..2d78a029683 100644
--- a/src/include/utils/pgstat_kind.h
+++ b/src/include/utils/pgstat_kind.h
@@ -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/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 52f8603a7be..c6d497603f2 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2274,6 +2274,7 @@ PgStatShared_Database
PgStatShared_Function
PgStatShared_HashEntry
PgStatShared_IO
+PgStatShared_Lock
PgStatShared_Relation
PgStatShared_ReplSlot
PgStatShared_SLRU
@@ -2296,8 +2297,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
--
2.34.1
[text/x-diff] v9-0002-Add-the-pg_stat_lock-view.patch (29.9K, ../../abortN3ngqMcaBJ7@ip-10-97-1-34.eu-west-3.compute.internal/3-v9-0002-Add-the-pg_stat_lock-view.patch)
download | inline diff:
From 75c23d2098f55aca5f3baed35c65bfb2016b5741 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Thu, 31 Jul 2025 09:35:31 +0000
Subject: [PATCH v9 2/3] Add the pg_stat_lock view
This new view reports lock statistics.
Note that it does not omit combinations which do not make sense (as pg_locks
does).
Also wait_time is reported as bigint as the deadlock_timeout default value is
1s.
This commit also adds documentation and a few tests.
XXX: Bump catversion
---
doc/src/sgml/monitoring.sgml | 113 ++++++++++++++
src/backend/catalog/system_views.sql | 9 ++
src/backend/utils/activity/pgstat_lock.c | 8 +
src/backend/utils/adt/pgstatfuncs.c | 39 +++++
src/include/catalog/pg_proc.dat | 9 ++
src/include/pgstat.h | 1 +
src/test/isolation/expected/stats.out | 189 +++++++++++++++++++++++
src/test/isolation/expected/stats_1.out | 189 +++++++++++++++++++++++
src/test/isolation/specs/stats.spec | 95 ++++++++++++
src/test/regress/expected/rules.out | 6 +
src/test/regress/expected/stats.out | 48 ++++++
src/test/regress/sql/stats.sql | 36 +++++
12 files changed, 742 insertions(+)
15.9% doc/src/sgml/
5.0% src/backend/utils/adt/
54.1% src/test/isolation/expected/
11.3% src/test/isolation/specs/
5.7% src/test/regress/expected/
4.0% src/test/regress/sql/
3.6% src/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 462019a972c..221ee28cc7b 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -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,104 @@ 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 <xref linkend="guc-log-lock-waits"/>
+ is enabled and 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 <xref linkend="guc-log-lock-waits"/> is enabled and
+ 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. You may want to increase
+ <xref linkend="guc-max-locks-per-transaction"/> if you feel this counter
+ is too high.
+ </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 +5479,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
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index f1ed7b58f13..e54018004db 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -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,
diff --git a/src/backend/utils/activity/pgstat_lock.c b/src/backend/utils/activity/pgstat_lock.c
index 91a53a6013a..954fa51c3e0 100644
--- a/src/backend/utils/activity/pgstat_lock.c
+++ b/src/backend/utils/activity/pgstat_lock.c
@@ -21,6 +21,14 @@
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()
*/
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 5f907335990..9185a8e6b83 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -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)
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fc8d82665b8..3e2c43e9d1b 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6041,6 +6041,15 @@
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',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 4a28ea99705..06e2638e788 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -643,6 +643,7 @@ 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 PgStat_Lock *pgstat_fetch_stat_lock(void);
/*
* Functions in pgstat_database.c
diff --git a/src/test/isolation/expected/stats.out b/src/test/isolation/expected/stats.out
index cfad309ccf3..3cae3052e40 100644
--- a/src/test/isolation/expected/stats.out
+++ b/src/test/isolation/expected/stats.out
@@ -3751,3 +3751,192 @@ test_stat_func| 1|t |t
(1 row)
step s1_commit: COMMIT;
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits s2_set_deadlock_timeout s2_set_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 s1_set_log_lock_waits: SET log_lock_waits = on;
+step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
+step s2_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'relation';
+?column?|?column?
+--------+--------
+t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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 s1_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'transactionid';
+?column?|?column?
+--------+--------
+t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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 s1_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'advisory';
+?column?|?column?
+--------+--------
+t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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 s1_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'relation';
+?column?|?column?
+--------+--------
+f |f
+(1 row)
+
diff --git a/src/test/isolation/expected/stats_1.out b/src/test/isolation/expected/stats_1.out
index e1d937784cb..ea4fd97a9a5 100644
--- a/src/test/isolation/expected/stats_1.out
+++ b/src/test/isolation/expected/stats_1.out
@@ -3775,3 +3775,192 @@ test_stat_func| 1|t |t
(1 row)
step s1_commit: COMMIT;
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits s2_set_deadlock_timeout s2_set_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 s1_set_log_lock_waits: SET log_lock_waits = on;
+step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
+step s2_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'relation';
+?column?|?column?
+--------+--------
+t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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 s1_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'transactionid';
+?column?|?column?
+--------+--------
+t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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 s1_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'advisory';
+?column?|?column?
+--------+--------
+t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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 s1_set_log_lock_waits: SET log_lock_waits = on;
+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.5);
+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, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'relation';
+?column?|?column?
+--------+--------
+f |f
+(1 row)
+
diff --git a/src/test/isolation/specs/stats.spec b/src/test/isolation/specs/stats.spec
index da16710da0f..42be68c545f 100644
--- a/src/test/isolation/specs/stats.spec
+++ b/src/test/isolation/specs/stats.spec
@@ -130,6 +130,14 @@ step s1_slru_check_stats {
WHERE before.stat = 'blks_zeroed';
}
+# Lock stats steps
+step s1_set_deadlock_timeout { SET deadlock_timeout = '10ms'; }
+step s1_set_log_lock_waits { SET log_lock_waits = on; }
+step s1_reset_stat_lock { SELECT pg_stat_reset_shared('lock'); }
+step s1_sleep { SELECT pg_sleep(0.5); }
+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'; }
@@ -164,6 +172,16 @@ step s2_big_notify { SELECT pg_notify('stats_test_use',
repeat(i::text, current_setting('block_size')::int / 2)) FROM generate_series(1, 3) g(i);
}
+# Lock stats steps
+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, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'relation'; }
+step s2_report_stat_lock_transactionid { SELECT waits > 0, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'transactionid'; }
+step s2_report_stat_lock_advisory { SELECT waits > 0, wait_time > 500 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); }
######################
# Function stats tests
@@ -765,3 +783,80 @@ permutation
s1_clear_snapshot
s1_func_stats
s1_commit
+
+######################
+# Lock stats tests
+######################
+
+# relation lock
+
+permutation
+ s1_set_deadlock_timeout
+ s1_reset_stat_lock
+ s1_set_log_lock_waits
+ s2_set_deadlock_timeout
+ s2_set_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
+
+# transaction lock
+
+permutation
+ s1_set_deadlock_timeout
+ s1_reset_stat_lock
+ s1_set_log_lock_waits
+ 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
+ s1_set_log_lock_waits
+ 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 behaves correctly
+
+permutation
+ s1_set_deadlock_timeout
+ s1_reset_stat_lock
+ s1_set_log_lock_waits
+ 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
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 32bea58db2c..2b3cf6d8569 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -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,
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index b99462bf946..ea7f7846895 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -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
diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql
index 941222cf0be..65d8968c83e 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -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
--
2.34.1
[text/x-diff] v9-0003-Introduce-a-new-track_lock_timing-GUC.patch (26.6K, ../../abortN3ngqMcaBJ7@ip-10-97-1-34.eu-west-3.compute.internal/4-v9-0003-Introduce-a-new-track_lock_timing-GUC.patch)
download | inline diff:
From 7d7e3c41b82a65add59ee778c6e5dbe3779f5193 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Fri, 20 Feb 2026 06:13:28 +0000
Subject: [PATCH v9 3/3] Introduce a new track_lock_timing GUC
A new GUC (track_lock_timing) is added and defaults to on. If on, waits and
wait_time counters are incremented if the session waited longer than deadlock_timeout
to acquire the lock. It's on by default, as this is the same idea as 2aac62be8cb.
---
doc/src/sgml/config.sgml | 19 ++
doc/src/sgml/monitoring.sgml | 4 +-
src/backend/storage/lmgr/proc.c | 185 +++++++++---------
src/backend/utils/misc/guc_parameters.dat | 6 +
src/backend/utils/misc/postgresql.conf.sample | 1 +
src/include/pgstat.h | 1 +
src/test/isolation/expected/stats.out | 24 +--
src/test/isolation/expected/stats_1.out | 24 +--
src/test/isolation/specs/stats.spec | 24 +--
9 files changed, 161 insertions(+), 127 deletions(-)
7.3% doc/src/sgml/
50.0% src/backend/storage/lmgr/
34.7% src/test/isolation/expected/
5.8% src/test/isolation/specs/
diff --git a/doc/src/sgml/config.sgml b/doc/src/sgml/config.sgml
index 8cdd826fbd3..69938ba3cdc 100644
--- a/doc/src/sgml/config.sgml
+++ b/doc/src/sgml/config.sgml
@@ -8857,6 +8857,25 @@ COPY postgres_log FROM '/full/path/to/logfile.csv' WITH csv;
</listitem>
</varlistentry>
+ <varlistentry id="guc-track-lock-timing" xreflabel="track_lock_timing">
+ <term><varname>track_lock_timing</varname> (<type>boolean</type>)
+ <indexterm>
+ <primary><varname>track_lock_timing</varname> configuration parameter</primary>
+ </indexterm>
+ </term>
+ <listitem>
+ <para>
+ Enables timing of lock waits. This parameter is on by default, as it tracks
+ only the timings for successful acquisitions that waited longer than
+ <xref linkend="guc-deadlock-timeout"/>. Lock timing information is
+ displayed in the <link linkend="monitoring-pg-stat-lock-view">
+ <structname>pg_stat_lock</structname></link> view.
+ Only superusers and users with the appropriate <literal>SET</literal>
+ privilege can change this setting.
+ </para>
+ </listitem>
+ </varlistentry>
+
<varlistentry id="guc-track-wal-io-timing" xreflabel="track_wal_io_timing">
<term><varname>track_wal_io_timing</varname> (<type>boolean</type>)
<indexterm>
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 221ee28cc7b..6ae9cc7635d 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -3340,7 +3340,7 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para>
<para>
Number of times a lock of this type had to wait because of a
- conflicting lock. Only incremented when <xref linkend="guc-log-lock-waits"/>
+ conflicting lock. Only incremented when <xref linkend="guc-track-lock-timing"/>
is enabled and the lock was successfully acquired after waiting longer
than <xref linkend="guc-deadlock-timeout"/>.
</para>
@@ -3354,7 +3354,7 @@ description | Waiting for a newly initialized WAL file to reach durable storage
</para>
<para>
Total time spent waiting for locks of this type, in milliseconds.
- Only incremented when <xref linkend="guc-log-lock-waits"/> is enabled and
+ Only incremented when <xref linkend="guc-track-lock-timing"/> is enabled and
the lock was successfully acquired after waiting longer than
<xref linkend="guc-deadlock-timeout"/>.
</para>
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 263dc88b735..e9b7b5fd4c9 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -63,6 +63,7 @@ int IdleInTransactionSessionTimeout = 0;
int TransactionTimeout = 0;
int IdleSessionTimeout = 0;
bool log_lock_waits = true;
+bool track_lock_timing = true;
/* Pointer to this process's PGPROC struct, if any */
PGPROC *MyProc = NULL;
@@ -1550,115 +1551,125 @@ ProcSleep(LOCALLOCK *locallock)
/*
* If awoken after the deadlock check interrupt has run, and
- * log_lock_waits is on, then report about the wait.
+ * log_lock_waits or track_lock_timing is on, then report or track
+ * about the wait.
*/
- if (log_lock_waits && deadlock_state != DS_NOT_YET_CHECKED)
+ if ((log_lock_waits || track_lock_timing) &&
+ deadlock_state != DS_NOT_YET_CHECKED)
{
- StringInfoData buf,
- lock_waiters_sbuf,
- lock_holders_sbuf;
- const char *modename;
long secs;
int usecs;
long msecs;
- int lockHoldersNum = 0;
- initStringInfo(&buf);
- initStringInfo(&lock_waiters_sbuf);
- initStringInfo(&lock_holders_sbuf);
-
- DescribeLockTag(&buf, &locallock->tag.lock);
- modename = GetLockmodeName(locallock->tag.lock.locktag_lockmethodid,
- lockmode);
TimestampDifference(get_timeout_start_time(DEADLOCK_TIMEOUT),
GetCurrentTimestamp(),
&secs, &usecs);
msecs = secs * 1000 + usecs / 1000;
usecs = usecs % 1000;
- /* Gather a list of all lock holders and waiters */
- LWLockAcquire(partitionLock, LW_SHARED);
- GetLockHoldersAndWaiters(locallock, &lock_holders_sbuf,
- &lock_waiters_sbuf, &lockHoldersNum);
- LWLockRelease(partitionLock);
-
- if (deadlock_state == DS_SOFT_DEADLOCK)
- ereport(LOG,
- (errmsg("process %d avoided deadlock for %s on %s by rearranging queue order after %ld.%03d ms",
- MyProcPid, modename, buf.data, msecs, usecs),
- (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
- "Processes holding the lock: %s. Wait queue: %s.",
- lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
- else if (deadlock_state == DS_HARD_DEADLOCK)
- {
- /*
- * This message is a bit redundant with the error that will be
- * reported subsequently, but in some cases the error report
- * might not make it to the log (eg, if it's caught by an
- * exception handler), and we want to ensure all long-wait
- * events get logged.
- */
- ereport(LOG,
- (errmsg("process %d detected deadlock while waiting for %s on %s after %ld.%03d ms",
- MyProcPid, modename, buf.data, msecs, usecs),
- (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
- "Processes holding the lock: %s. Wait queue: %s.",
- lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
- }
+ /* Increment the lock statistics counters */
+ if (track_lock_timing && myWaitStatus == PROC_WAIT_STATUS_OK)
+ pgstat_count_lock_waits(locallock->tag.lock.locktag_type, msecs);
- if (myWaitStatus == PROC_WAIT_STATUS_WAITING)
+ if (log_lock_waits)
{
- /*
- * Guard the "still waiting on lock" log message so it is
- * reported at most once while waiting for the lock.
- *
- * Without this guard, the message can be emitted whenever the
- * lock-wait sleep is interrupted (for example by SIGHUP for
- * config reload or by client_connection_check_interval). For
- * example, if client_connection_check_interval is set very
- * low (e.g., 100 ms), the message could be logged repeatedly,
- * flooding the log and making it difficult to use.
- */
- if (!logged_lock_wait)
- {
+ StringInfoData buf,
+ lock_waiters_sbuf,
+ lock_holders_sbuf;
+ const char *modename;
+ int lockHoldersNum = 0;
+
+ initStringInfo(&buf);
+ initStringInfo(&lock_waiters_sbuf);
+ initStringInfo(&lock_holders_sbuf);
+
+ DescribeLockTag(&buf, &locallock->tag.lock);
+ modename = GetLockmodeName(locallock->tag.lock.locktag_lockmethodid,
+ lockmode);
+
+ /* Gather a list of all lock holders and waiters */
+ LWLockAcquire(partitionLock, LW_SHARED);
+ GetLockHoldersAndWaiters(locallock, &lock_holders_sbuf,
+ &lock_waiters_sbuf, &lockHoldersNum);
+ LWLockRelease(partitionLock);
+
+ if (deadlock_state == DS_SOFT_DEADLOCK)
ereport(LOG,
- (errmsg("process %d still waiting for %s on %s after %ld.%03d ms",
+ (errmsg("process %d avoided deadlock for %s on %s by rearranging queue order after %ld.%03d ms",
MyProcPid, modename, buf.data, msecs, usecs),
(errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
"Processes holding the lock: %s. Wait queue: %s.",
lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
- logged_lock_wait = true;
- }
- }
- else if (myWaitStatus == PROC_WAIT_STATUS_OK)
- {
- /* Increment the lock statistics counters */
- pgstat_count_lock_waits(locallock->tag.lock.locktag_type, msecs);
-
- ereport(LOG,
- (errmsg("process %d acquired %s on %s after %ld.%03d ms",
- MyProcPid, modename, buf.data, msecs, usecs)));
- }
- else
- {
- Assert(myWaitStatus == PROC_WAIT_STATUS_ERROR);
-
- /*
- * Currently, the deadlock checker always kicks its own
- * process, which means that we'll only see
- * PROC_WAIT_STATUS_ERROR when deadlock_state ==
- * DS_HARD_DEADLOCK, and there's no need to print redundant
- * messages. But for completeness and future-proofing, print
- * a message if it looks like someone else kicked us off the
- * lock.
- */
- if (deadlock_state != DS_HARD_DEADLOCK)
+ else if (deadlock_state == DS_HARD_DEADLOCK)
+ {
+ /*
+ * This message is a bit redundant with the error that
+ * will be reported subsequently, but in some cases the
+ * error report might not make it to the log (eg, if it's
+ * caught by an exception handler), and we want to ensure
+ * all long-wait events get logged.
+ */
ereport(LOG,
- (errmsg("process %d failed to acquire %s on %s after %ld.%03d ms",
+ (errmsg("process %d detected deadlock while waiting for %s on %s after %ld.%03d ms",
MyProcPid, modename, buf.data, msecs, usecs),
(errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
"Processes holding the lock: %s. Wait queue: %s.",
lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
+ }
+ if (myWaitStatus == PROC_WAIT_STATUS_WAITING)
+ {
+ /*
+ * Guard the "still waiting on lock" log message so it is
+ * reported at most once while waiting for the lock.
+ *
+ * Without this guard, the message can be emitted whenever
+ * the lock-wait sleep is interrupted (for example by
+ * SIGHUP for config reload or by
+ * client_connection_check_interval). For example, if
+ * client_connection_check_interval is set very low (e.g.,
+ * 100 ms), the message could be logged repeatedly,
+ * flooding the log and making it difficult to use.
+ */
+ if (!logged_lock_wait)
+ {
+ ereport(LOG,
+ (errmsg("process %d still waiting for %s on %s after %ld.%03d ms",
+ MyProcPid, modename, buf.data, msecs, usecs),
+ (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
+ "Processes holding the lock: %s. Wait queue: %s.",
+ lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
+ logged_lock_wait = true;
+ }
+ }
+ else if (myWaitStatus == PROC_WAIT_STATUS_OK)
+ ereport(LOG,
+ (errmsg("process %d acquired %s on %s after %ld.%03d ms",
+ MyProcPid, modename, buf.data, msecs, usecs)));
+ else
+ {
+ Assert(myWaitStatus == PROC_WAIT_STATUS_ERROR);
+
+ /*
+ * Currently, the deadlock checker always kicks its own
+ * process, which means that we'll only see
+ * PROC_WAIT_STATUS_ERROR when deadlock_state ==
+ * DS_HARD_DEADLOCK, and there's no need to print
+ * redundant messages. But for completeness and
+ * future-proofing, print a message if it looks like
+ * someone else kicked us off the lock.
+ */
+ if (deadlock_state != DS_HARD_DEADLOCK)
+ ereport(LOG,
+ (errmsg("process %d failed to acquire %s on %s after %ld.%03d ms",
+ MyProcPid, modename, buf.data, msecs, usecs),
+ (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
+ "Processes holding the lock: %s. Wait queue: %s.",
+ lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
+ }
+
+ pfree(buf.data);
+ pfree(lock_holders_sbuf.data);
+ pfree(lock_waiters_sbuf.data);
}
/*
@@ -1666,10 +1677,6 @@ ProcSleep(LOCALLOCK *locallock)
* state so we don't print the above messages again.
*/
deadlock_state = DS_NO_DEADLOCK;
-
- pfree(buf.data);
- pfree(lock_holders_sbuf.data);
- pfree(lock_waiters_sbuf.data);
}
} while (myWaitStatus == PROC_WAIT_STATUS_WAITING);
diff --git a/src/backend/utils/misc/guc_parameters.dat b/src/backend/utils/misc/guc_parameters.dat
index a5a0edf2534..7055dc88e0d 100644
--- a/src/backend/utils/misc/guc_parameters.dat
+++ b/src/backend/utils/misc/guc_parameters.dat
@@ -3110,6 +3110,12 @@
boot_val => 'false',
},
+{ name => 'track_lock_timing', type => 'bool', context => 'PGC_SUSET', group => 'STATS_CUMULATIVE',
+ short_desc => 'Collects timing statistics for lock acquisition.',
+ variable => 'track_lock_timing',
+ boot_val => 'true',
+},
+
{ name => 'track_wal_io_timing', type => 'bool', context => 'PGC_SUSET', group => 'STATS_CUMULATIVE',
short_desc => 'Collects timing statistics for WAL I/O activity.',
variable => 'track_wal_io_timing',
diff --git a/src/backend/utils/misc/postgresql.conf.sample b/src/backend/utils/misc/postgresql.conf.sample
index e686d88afc4..5d057608fe1 100644
--- a/src/backend/utils/misc/postgresql.conf.sample
+++ b/src/backend/utils/misc/postgresql.conf.sample
@@ -685,6 +685,7 @@
#track_counts = on
#track_cost_delay_timing = off
#track_io_timing = off
+#track_lock_timing = on
#track_wal_io_timing = off
#track_functions = none # none, pl, all
#stats_fetch_consistency = cache # cache, none, snapshot
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 06e2638e788..95006dc139c 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -842,6 +842,7 @@ extern PgStat_WalStats *pgstat_fetch_stat_wal(void);
extern PGDLLIMPORT bool pgstat_track_counts;
extern PGDLLIMPORT int pgstat_track_functions;
extern PGDLLIMPORT int pgstat_fetch_consistency;
+extern PGDLLIMPORT bool track_lock_timing;
/*
diff --git a/src/test/isolation/expected/stats.out b/src/test/isolation/expected/stats.out
index 3cae3052e40..4f7ad061549 100644
--- a/src/test/isolation/expected/stats.out
+++ b/src/test/isolation/expected/stats.out
@@ -3752,7 +3752,7 @@ test_stat_func| 1|t |t
step s1_commit: COMMIT;
-starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits s2_set_deadlock_timeout s2_set_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
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_set_track_lock_timing 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
------------------------
@@ -3765,9 +3765,9 @@ pg_stat_reset_shared
(1 row)
-step s1_set_log_lock_waits: SET log_lock_waits = on;
+step s1_set_track_lock_timing: SET track_lock_timing = on;
step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
-step s2_set_log_lock_waits: SET log_lock_waits = on;
+step s2_set_track_lock_timing: SET track_lock_timing = on;
step s1_begin: BEGIN;
step s1_lock_relation: LOCK TABLE test_stat_tab;
step s2_begin: BEGIN;
@@ -3794,7 +3794,7 @@ t |t
(1 row)
-starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_set_track_lock_timing 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
------------------------
@@ -3807,9 +3807,9 @@ pg_stat_reset_shared
(1 row)
-step s1_set_log_lock_waits: SET log_lock_waits = on;
+step s1_set_track_lock_timing: SET track_lock_timing = on;
step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
-step s2_set_log_lock_waits: SET log_lock_waits = on;
+step s2_set_track_lock_timing: SET track_lock_timing = 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';
@@ -3837,7 +3837,7 @@ t |t
(1 row)
-starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_set_track_lock_timing 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
------------------------
@@ -3850,9 +3850,9 @@ pg_stat_reset_shared
(1 row)
-step s1_set_log_lock_waits: SET log_lock_waits = on;
+step s1_set_track_lock_timing: SET track_lock_timing = on;
step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
-step s2_set_log_lock_waits: SET log_lock_waits = on;
+step s2_set_track_lock_timing: SET track_lock_timing = on;
step s1_lock_advisory_lock: SELECT pg_advisory_lock(1);
pg_advisory_lock
----------------
@@ -3899,7 +3899,7 @@ t |t
(1 row)
-starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_unset_track_lock_timing 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
------------------------
@@ -3912,9 +3912,9 @@ pg_stat_reset_shared
(1 row)
-step s1_set_log_lock_waits: SET log_lock_waits = on;
+step s1_set_track_lock_timing: SET track_lock_timing = on;
step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
-step s2_unset_log_lock_waits: SET log_lock_waits = off;
+step s2_unset_track_lock_timing: SET track_lock_timing = off;
step s1_begin: BEGIN;
step s1_lock_relation: LOCK TABLE test_stat_tab;
step s2_begin: BEGIN;
diff --git a/src/test/isolation/expected/stats_1.out b/src/test/isolation/expected/stats_1.out
index ea4fd97a9a5..e1a60d41bad 100644
--- a/src/test/isolation/expected/stats_1.out
+++ b/src/test/isolation/expected/stats_1.out
@@ -3776,7 +3776,7 @@ test_stat_func| 1|t |t
step s1_commit: COMMIT;
-starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits s2_set_deadlock_timeout s2_set_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
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_set_track_lock_timing 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
------------------------
@@ -3789,9 +3789,9 @@ pg_stat_reset_shared
(1 row)
-step s1_set_log_lock_waits: SET log_lock_waits = on;
+step s1_set_track_lock_timing: SET track_lock_timing = on;
step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
-step s2_set_log_lock_waits: SET log_lock_waits = on;
+step s2_set_track_lock_timing: SET track_lock_timing = on;
step s1_begin: BEGIN;
step s1_lock_relation: LOCK TABLE test_stat_tab;
step s2_begin: BEGIN;
@@ -3818,7 +3818,7 @@ t |t
(1 row)
-starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_set_track_lock_timing 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
------------------------
@@ -3831,9 +3831,9 @@ pg_stat_reset_shared
(1 row)
-step s1_set_log_lock_waits: SET log_lock_waits = on;
+step s1_set_track_lock_timing: SET track_lock_timing = on;
step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
-step s2_set_log_lock_waits: SET log_lock_waits = on;
+step s2_set_track_lock_timing: SET track_lock_timing = 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';
@@ -3861,7 +3861,7 @@ t |t
(1 row)
-starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_set_track_lock_timing 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
------------------------
@@ -3874,9 +3874,9 @@ pg_stat_reset_shared
(1 row)
-step s1_set_log_lock_waits: SET log_lock_waits = on;
+step s1_set_track_lock_timing: SET track_lock_timing = on;
step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
-step s2_set_log_lock_waits: SET log_lock_waits = on;
+step s2_set_track_lock_timing: SET track_lock_timing = on;
step s1_lock_advisory_lock: SELECT pg_advisory_lock(1);
pg_advisory_lock
----------------
@@ -3923,7 +3923,7 @@ t |t
(1 row)
-starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_track_lock_timing s2_set_deadlock_timeout s2_unset_track_lock_timing 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
------------------------
@@ -3936,9 +3936,9 @@ pg_stat_reset_shared
(1 row)
-step s1_set_log_lock_waits: SET log_lock_waits = on;
+step s1_set_track_lock_timing: SET track_lock_timing = on;
step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
-step s2_unset_log_lock_waits: SET log_lock_waits = off;
+step s2_unset_track_lock_timing: SET track_lock_timing = off;
step s1_begin: BEGIN;
step s1_lock_relation: LOCK TABLE test_stat_tab;
step s2_begin: BEGIN;
diff --git a/src/test/isolation/specs/stats.spec b/src/test/isolation/specs/stats.spec
index 42be68c545f..81b45a801d9 100644
--- a/src/test/isolation/specs/stats.spec
+++ b/src/test/isolation/specs/stats.spec
@@ -132,7 +132,7 @@ step s1_slru_check_stats {
# Lock stats steps
step s1_set_deadlock_timeout { SET deadlock_timeout = '10ms'; }
-step s1_set_log_lock_waits { SET log_lock_waits = on; }
+step s1_set_track_lock_timing { SET track_lock_timing = on; }
step s1_reset_stat_lock { SELECT pg_stat_reset_shared('lock'); }
step s1_sleep { SELECT pg_sleep(0.5); }
step s1_lock_relation { LOCK TABLE test_stat_tab; }
@@ -174,8 +174,8 @@ step s2_big_notify { SELECT pg_notify('stats_test_use',
# Lock stats steps
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_set_track_lock_timing { SET track_lock_timing = on; }
+step s2_unset_track_lock_timing { SET track_lock_timing = off; }
step s2_report_stat_lock_relation { SELECT waits > 0, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'relation'; }
step s2_report_stat_lock_transactionid { SELECT waits > 0, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'transactionid'; }
step s2_report_stat_lock_advisory { SELECT waits > 0, wait_time > 500 FROM pg_stat_lock WHERE locktype = 'advisory'; }
@@ -793,9 +793,9 @@ permutation
permutation
s1_set_deadlock_timeout
s1_reset_stat_lock
- s1_set_log_lock_waits
+ s1_set_track_lock_timing
s2_set_deadlock_timeout
- s2_set_log_lock_waits
+ s2_set_track_lock_timing
s1_begin
s1_lock_relation
s2_begin
@@ -811,9 +811,9 @@ permutation
permutation
s1_set_deadlock_timeout
s1_reset_stat_lock
- s1_set_log_lock_waits
+ s1_set_track_lock_timing
s2_set_deadlock_timeout
- s2_set_log_lock_waits
+ s2_set_track_lock_timing
s1_table_insert
s1_begin
s1_table_update_k1
@@ -830,9 +830,9 @@ permutation
permutation
s1_set_deadlock_timeout
s1_reset_stat_lock
- s1_set_log_lock_waits
+ s1_set_track_lock_timing
s2_set_deadlock_timeout
- s2_set_log_lock_waits
+ s2_set_track_lock_timing
s1_lock_advisory_lock
s2_begin
s2_ff
@@ -843,14 +843,14 @@ permutation
s2_commit
s2_report_stat_lock_advisory
-# Ensure log_lock_waits behaves correctly
+# Ensure track_lock_timing behaves correctly
permutation
s1_set_deadlock_timeout
s1_reset_stat_lock
- s1_set_log_lock_waits
+ s1_set_track_lock_timing
s2_set_deadlock_timeout
- s2_unset_log_lock_waits
+ s2_unset_track_lock_timing
s1_begin
s1_lock_relation
s2_begin
--
2.34.1
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-03-18 08:15 Michael Paquier <michael@paquier.xyz>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
0 siblings, 1 reply; 61+ messages in thread
From: Michael Paquier @ 2026-03-18 08:15 UTC (permalink / raw)
To: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: Andres Freund <andres@anarazel.de>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
On Wed, Mar 18, 2026 at 04:36:04AM +0000, Bertrand Drouvot wrote:
>> So, we're back to what we were discussing before the split. As in v7, 0003 is
>> adding the new GUC. So that we can see what having a new GUC implies in ProcSleep()
>> and we can just get rid of 0003 if we think the GUC is not worth the extra complexity
>> (I don't have a strong opinion on it but tempted to think that the extra GUC is
>> not worth it).
>
> PFA, a rebase due to fd6ecbfa75ff.
Looking again at this patch, all the fields that you are adding are in
non-critical paths, so it looks fine by me to begin with this data
set. We may want to document that for future readers of the code to
not add counter increments in the fast code paths, where performance
could matter.
Let's also drop 0003 with the GUC. log_lock_waits is enabled by
default and we are in a wait path which would not be
performance-critical.
Regarding the isolation test, the new permutations add 4 pg_sleep()
calls at 500ms each, making the stats test longer. It also looks like
the outputs are the same for the two alternate expected files? Do you
think that it could be possible to move these tests to a new file,
perhaps cutting a bit the sleeps to make it faster?
--
Michael
Attachments:
[application/pgp-signature] signature.asc (832B, ../../abpfH33mR4Mp9dtO@paquier.xyz/2-signature.asc)
download
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-03-18 14:51 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Michael Paquier <michael@paquier.xyz>
0 siblings, 1 reply; 61+ messages in thread
From: Bertrand Drouvot @ 2026-03-18 14:51 UTC (permalink / raw)
To: Michael Paquier <michael@paquier.xyz>; +Cc: Andres Freund <andres@anarazel.de>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi,
On Wed, Mar 18, 2026 at 05:15:27PM +0900, Michael Paquier wrote:
> On Wed, Mar 18, 2026 at 04:36:04AM +0000, Bertrand Drouvot wrote:
> >> So, we're back to what we were discussing before the split. As in v7, 0003 is
> >> adding the new GUC. So that we can see what having a new GUC implies in ProcSleep()
> >> and we can just get rid of 0003 if we think the GUC is not worth the extra complexity
> >> (I don't have a strong opinion on it but tempted to think that the extra GUC is
> >> not worth it).
> >
> > PFA, a rebase due to fd6ecbfa75ff.
>
> Looking again at this patch, all the fields that you are adding are in
> non-critical paths, so it looks fine by me to begin with this data
> set.
Thanks for looking at it!
> We may want to document that for future readers of the code to
> not add counter increments in the fast code paths, where performance
> could matter.
Yeah, added a few words in the callers and on top of the function definitions.
> Let's also drop 0003 with the GUC. log_lock_waits is enabled by
> default and we are in a wait path which would not be
> performance-critical.
Yeah, that was also my vote.
> Regarding the isolation test, the new permutations add 4 pg_sleep()
> calls at 500ms each, making the stats test longer. It also looks like
> the outputs are the same for the two alternate expected files? Do you
> think that it could be possible to move these tests to a new file,
> perhaps cutting a bit the sleeps to make it faster?
This is done that way in the attached, so that we don't need the extra output in
the _1.out file and the test time is reduced (since the deadlock timeout is set
to 10ms in the test, I changed the sleep time to 50ms (I did not want to be very
close to 10ms)).
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
Attachments:
[text/x-diff] v10-0001-Add-lock-statistics.patch (16.3K, ../../abq71eL7qIdWAxMD@ip-10-97-1-34.eu-west-3.compute.internal/2-v10-0001-Add-lock-statistics.patch)
download | inline diff:
From 42bc5f58427df5f888e78c6b4f65f7b636f2473c Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Tue, 29 Jul 2025 08:36:35 +0000
Subject: [PATCH v10 1/2] Add lock statistics
Adding a new stat kind PGSTAT_KIND_LOCK for the lock statistics.
This new statistic kind is a fixed one because its key is the lock type
so that we know its size is LOCKTAG_LAST_TYPE + 1.
This statistic kind records the following counters:
waits
wait_time
fastpath_exceeded
The waits and wait_time counters are incremented if log_lock_waits is on
and the session waited longer than deadlock_timeout to acquire the lock.
fastpath_exceeded is incremented when the lock can not be acquired via fast path
because the fast path slot limit was exceeded.
No extra details is added (like the ones, i.e relation oid, database oid, we
can find in pg_locks). The idea is to provide an idea on what the locking
behaviour looks like.
XXX: Bump stat file format
Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reviewed-by: Andres Freund <andres@anarazel.de>
Discussion: https://postgr.es/m/aIyNxBWFCybgBZBS%40ip-10-97-1-34.eu-west-3.compute.internal
---
src/backend/storage/lmgr/lock.c | 61 ++++----
src/backend/storage/lmgr/proc.c | 10 ++
src/backend/utils/activity/Makefile | 1 +
src/backend/utils/activity/meson.build | 1 +
src/backend/utils/activity/pgstat.c | 18 +++
src/backend/utils/activity/pgstat_lock.c | 170 +++++++++++++++++++++++
src/include/pgstat.h | 27 ++++
src/include/utils/pgstat_internal.h | 21 +++
src/include/utils/pgstat_kind.h | 5 +-
src/tools/pgindent/typedefs.list | 4 +
10 files changed, 292 insertions(+), 26 deletions(-)
30.6% src/backend/storage/lmgr/
54.4% src/backend/utils/activity/
7.0% src/include/utils/
7.1% src/include/
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index d930c66cdbd..81319cfff2a 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -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"
@@ -984,36 +985,48 @@ LockAcquireExtended(const LOCKTAG *locktag,
* lock type on a relation we have already locked using the fast-path, but
* for now we don't worry about that case either.
*/
- if (EligibleForRelationFastPath(locktag, lockmode) &&
- FastPathLocalUseCounts[FAST_PATH_REL_GROUP(locktag->locktag_field2)] < FP_LOCK_SLOTS_PER_GROUP)
+ if (EligibleForRelationFastPath(locktag, lockmode))
{
- uint32 fasthashcode = FastPathStrongLockHashPartition(hashcode);
- bool acquired;
+ if (FastPathLocalUseCounts[FAST_PATH_REL_GROUP(locktag->locktag_field2)] <
+ FP_LOCK_SLOTS_PER_GROUP)
+ {
+ uint32 fasthashcode = FastPathStrongLockHashPartition(hashcode);
+ bool acquired;
- /*
- * LWLockAcquire acts as a memory sequencing point, so it's safe to
- * assume that any strong locker whose increment to
- * FastPathStrongRelationLocks->counts becomes visible after we test
- * it has yet to begin to transfer fast-path locks.
- */
- LWLockAcquire(&MyProc->fpInfoLock, LW_EXCLUSIVE);
- if (FastPathStrongRelationLocks->count[fasthashcode] != 0)
- acquired = false;
+ /*
+ * LWLockAcquire acts as a memory sequencing point, so it's safe
+ * to assume that any strong locker whose increment to
+ * FastPathStrongRelationLocks->counts becomes visible after we
+ * test it has yet to begin to transfer fast-path locks.
+ */
+ LWLockAcquire(&MyProc->fpInfoLock, LW_EXCLUSIVE);
+ if (FastPathStrongRelationLocks->count[fasthashcode] != 0)
+ acquired = false;
+ else
+ acquired = FastPathGrantRelationLock(locktag->locktag_field2,
+ lockmode);
+ LWLockRelease(&MyProc->fpInfoLock);
+ if (acquired)
+ {
+ /*
+ * The locallock might contain stale pointers to some old
+ * shared objects; we MUST reset these to null before
+ * considering the lock to be acquired via fast-path.
+ */
+ locallock->lock = NULL;
+ locallock->proclock = NULL;
+ GrantLockLocal(locallock, owner);
+ return LOCKACQUIRE_OK;
+ }
+ }
else
- acquired = FastPathGrantRelationLock(locktag->locktag_field2,
- lockmode);
- LWLockRelease(&MyProc->fpInfoLock);
- if (acquired)
{
/*
- * The locallock might contain stale pointers to some old shared
- * objects; we MUST reset these to null before considering the
- * lock to be acquired via fast-path.
+ * Increment the lock statistics counter. That's OK from an
+ * overhead point of view as we are outside the fastpath code
+ * path.
*/
- locallock->lock = NULL;
- locallock->proclock = NULL;
- GrantLockLocal(locallock, owner);
- return LOCKACQUIRE_OK;
+ pgstat_count_lock_fastpath_exceeded(locallock->tag.lock.locktag_type);
}
}
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 8f5ce0e2a8a..3ffb565dc28 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -1631,9 +1631,19 @@ ProcSleep(LOCALLOCK *locallock)
}
}
else if (myWaitStatus == PROC_WAIT_STATUS_OK)
+ {
+ /*
+ * Increment the lock statistics counters. That's OK from an
+ * overhead point of view as we are already stuck behind a
+ * lock for more than the deadlock timeout and we can reuse
+ * its wait time computation here.
+ */
+ pgstat_count_lock_waits(locallock->tag.lock.locktag_type, msecs);
+
ereport(LOG,
(errmsg("process %d acquired %s on %s after %ld.%03d ms",
MyProcPid, modename, buf.data, msecs, usecs)));
+ }
else
{
Assert(myWaitStatus == PROC_WAIT_STATUS_ERROR);
diff --git a/src/backend/utils/activity/Makefile b/src/backend/utils/activity/Makefile
index c37bfb350bb..ca3ef89bf59 100644
--- a/src/backend/utils/activity/Makefile
+++ b/src/backend/utils/activity/Makefile
@@ -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 \
diff --git a/src/backend/utils/activity/meson.build b/src/backend/utils/activity/meson.build
index 53bd5a246ca..1aa7ece5290 100644
--- a/src/backend/utils/activity/meson.build
+++ b/src/backend/utils/activity/meson.build
@@ -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',
diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index 11bb71cad5a..eb8ccbaa628 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -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
index 00000000000..9ec61f2b2ec
--- /dev/null
+++ b/src/backend/utils/activity/pgstat_lock.c
@@ -0,0 +1,170 @@
+/* -------------------------------------------------------------------------
+ *
+ * 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-2025, 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;
+
+/*
+ * 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 a statistic per lock type
+ *
+ * Don't call this function for every lock acquisition and in hot path.
+ */
+#define PGSTAT_COUNT_LOCK_FUNC(stat) \
+void \
+CppConcat(pgstat_count_lock_,stat)(uint8 locktag_type) \
+{ \
+ Assert(locktag_type <= LOCKTAG_LAST_TYPE); \
+ PendingLockStats.stats[locktag_type].stat++; \
+ have_lockstats = true; \
+ pgstat_report_fixed = true; \
+}
+
+/* pgstat_count_lock_fastpath_exceeded */
+PGSTAT_COUNT_LOCK_FUNC(fastpath_exceeded)
+
+/*
+ * Increment the number of waits and wait time per lock type
+ *
+ * Don't call this function for every lock acquisition and in hot path.
+ */
+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;
+}
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 216b93492ba..4a28ea99705 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/lock.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"
@@ -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,14 @@ 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);
+
/*
* Functions in pgstat_database.c
*/
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 9b8fbae00ed..97704421a92 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -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
diff --git a/src/include/utils/pgstat_kind.h b/src/include/utils/pgstat_kind.h
index c30b6235623..2d78a029683 100644
--- a/src/include/utils/pgstat_kind.h
+++ b/src/include/utils/pgstat_kind.h
@@ -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/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 174e2798443..3ee504ef6e4 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2276,6 +2276,7 @@ PgStatShared_Database
PgStatShared_Function
PgStatShared_HashEntry
PgStatShared_IO
+PgStatShared_Lock
PgStatShared_Relation
PgStatShared_ReplSlot
PgStatShared_SLRU
@@ -2298,8 +2299,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
--
2.34.1
[text/x-diff] v10-0002-Add-the-pg_stat_lock-view.patch (24.4K, ../../abq71eL7qIdWAxMD@ip-10-97-1-34.eu-west-3.compute.internal/3-v10-0002-Add-the-pg_stat_lock-view.patch)
download | inline diff:
From 235f702180e6ef64e1c6170a624057c91f9fe76a Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Thu, 31 Jul 2025 09:35:31 +0000
Subject: [PATCH v10 2/2] Add the pg_stat_lock view
This new view reports lock statistics.
Note that it does not omit combinations which do not make sense (as pg_locks
does).
Also wait_time is reported as bigint as the deadlock_timeout default value is
1s.
This commit also adds documentation and a few tests.
XXX: Bump catversion
Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reviewed-by: Andres Freund <andres@anarazel.de>
Discussion: https://postgr.es/m/aIyNxBWFCybgBZBS%40ip-10-97-1-34.eu-west-3.compute.internal
---
doc/src/sgml/monitoring.sgml | 113 ++++++++++++
src/backend/catalog/system_views.sql | 9 +
src/backend/utils/activity/pgstat_lock.c | 8 +
src/backend/utils/adt/pgstatfuncs.c | 39 +++++
src/include/catalog/pg_proc.dat | 9 +
src/include/pgstat.h | 1 +
src/test/isolation/expected/lock-stats.out | 190 +++++++++++++++++++++
src/test/isolation/isolation_schedule | 1 +
src/test/isolation/specs/lock-stats.spec | 118 +++++++++++++
src/test/regress/expected/rules.out | 6 +
src/test/regress/expected/stats.out | 48 ++++++
src/test/regress/sql/stats.sql | 36 ++++
12 files changed, 578 insertions(+)
20.8% doc/src/sgml/
6.5% src/backend/utils/adt/
35.5% src/test/isolation/expected/
19.1% src/test/isolation/specs/
7.5% src/test/regress/expected/
5.3% src/test/regress/sql/
4.9% src/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 462019a972c..221ee28cc7b 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -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,104 @@ 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 <xref linkend="guc-log-lock-waits"/>
+ is enabled and 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 <xref linkend="guc-log-lock-waits"/> is enabled and
+ 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. You may want to increase
+ <xref linkend="guc-max-locks-per-transaction"/> if you feel this counter
+ is too high.
+ </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 +5479,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
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index f1ed7b58f13..e54018004db 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -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,
diff --git a/src/backend/utils/activity/pgstat_lock.c b/src/backend/utils/activity/pgstat_lock.c
index 9ec61f2b2ec..eccb12b0b68 100644
--- a/src/backend/utils/activity/pgstat_lock.c
+++ b/src/backend/utils/activity/pgstat_lock.c
@@ -21,6 +21,14 @@
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()
*/
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 5f907335990..9185a8e6b83 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -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)
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fc8d82665b8..3e2c43e9d1b 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6041,6 +6041,15 @@
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',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 4a28ea99705..06e2638e788 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -643,6 +643,7 @@ 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 PgStat_Lock *pgstat_fetch_stat_lock(void);
/*
* Functions in pgstat_database.c
diff --git a/src/test/isolation/expected/lock-stats.out b/src/test/isolation/expected/lock-stats.out
new file mode 100644
index 00000000000..fa9e141dc20
--- /dev/null
+++ b/src/test/isolation/expected/lock-stats.out
@@ -0,0 +1,190 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits s2_set_deadlock_timeout s2_set_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 s1_set_log_lock_waits: SET log_lock_waits = on;
+step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
+step s2_set_log_lock_waits: SET log_lock_waits = on;
+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, wait_time > 50 FROM pg_stat_lock WHERE locktype = 'relation';
+?column?|?column?
+--------+--------
+t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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 s1_set_log_lock_waits: SET log_lock_waits = on;
+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, wait_time > 50 FROM pg_stat_lock WHERE locktype = 'transactionid';
+?column?|?column?
+--------+--------
+t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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 s1_set_log_lock_waits: SET log_lock_waits = on;
+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, wait_time > 50 FROM pg_stat_lock WHERE locktype = 'advisory';
+?column?|?column?
+--------+--------
+t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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 s1_set_log_lock_waits: SET log_lock_waits = on;
+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, wait_time > 50 FROM pg_stat_lock WHERE locktype = 'relation';
+?column?|?column?
+--------+--------
+f |f
+(1 row)
+
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 4e466580cd4..2cc86849c9a 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -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
index 00000000000..2c5c91791e9
--- /dev/null
+++ b/src/test/isolation/specs/lock-stats.spec
@@ -0,0 +1,118 @@
+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_set_log_lock_waits { SET log_lock_waits = on; }
+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, wait_time > 50 FROM pg_stat_lock WHERE locktype = 'relation'; }
+step s2_report_stat_lock_transactionid { SELECT waits > 0, wait_time > 50 FROM pg_stat_lock WHERE locktype = 'transactionid'; }
+step s2_report_stat_lock_advisory { SELECT waits > 0, wait_time > 50 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
+ s1_set_log_lock_waits
+ s2_set_deadlock_timeout
+ s2_set_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
+
+# transaction lock
+
+permutation
+ s1_set_deadlock_timeout
+ s1_reset_stat_lock
+ s1_set_log_lock_waits
+ 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
+ s1_set_log_lock_waits
+ 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 behaves correctly
+
+permutation
+ s1_set_deadlock_timeout
+ s1_reset_stat_lock
+ s1_set_log_lock_waits
+ 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
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 32bea58db2c..2b3cf6d8569 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -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,
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index b99462bf946..ea7f7846895 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -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
diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql
index 941222cf0be..65d8968c83e 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -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
--
2.34.1
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-03-19 07:01 Michael Paquier <michael@paquier.xyz>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
0 siblings, 1 reply; 61+ messages in thread
From: Michael Paquier @ 2026-03-19 07:01 UTC (permalink / raw)
To: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: Andres Freund <andres@anarazel.de>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
On Wed, Mar 18, 2026 at 02:51:01PM +0000, Bertrand Drouvot wrote:
> This is done that way in the attached, so that we don't need the extra output in
> the _1.out file and the test time is reduced (since the deadlock timeout is set
> to 10ms in the test, I changed the sleep time to 50ms (I did not want to be very
> close to 10ms)).
> + <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 <xref linkend="guc-log-lock-waits"/>
> + is enabled and the lock was successfully acquired after waiting longer
> + than <xref linkend="guc-deadlock-timeout"/>.
> + </para>
> + </entry>
It does not make much sense to me to decide that the counter is
incremented if a GUC related to a control of the logs generated is
enabled. It's a fact that log_lock_waits is enabled by default these
days, hence we will be able to get the time calculation for free for
most deployments, but it seems inconsistent to me to not count this
information if the GUC is disabled. We should increment the counter
and aggregate the time spent on the wait all the time, no?
+ * Copyright (c) 2021-2025, PostgreSQL Global Development Group
Incorrect date at the top of pgstat_lock.c.
storage/lock.h is included in pgstat.h as LOCKTAG_LAST_TYPE is wanted
for the new lock stats structure. That would pull in a lot of header
data into pgstat.h. How about creating a new header that splits a
portion of lock.h into a new file? LockTagType, LOCKTAG_LAST_TYPE,
LockTagTypeNames at least and perhaps a few other things? Or we could
just limit ourselves to a locktag.h with these three, then include the
new locktag.h in pgstat.h.
It would be nice to document at the top of the new spec file what this
test is here for, and perhaps name it stats-lock instead?
--
Michael
Attachments:
[application/pgp-signature] signature.asc (832B, ../../abufUya2oK-_PJ3E@paquier.xyz/2-signature.asc)
download
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-03-19 12:25 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Michael Paquier <michael@paquier.xyz>
0 siblings, 1 reply; 61+ messages in thread
From: Bertrand Drouvot @ 2026-03-19 12:25 UTC (permalink / raw)
To: Michael Paquier <michael@paquier.xyz>; +Cc: Andres Freund <andres@anarazel.de>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi,
On Thu, Mar 19, 2026 at 04:01:39PM +0900, Michael Paquier wrote:
> On Wed, Mar 18, 2026 at 02:51:01PM +0000, Bertrand Drouvot wrote:
> > This is done that way in the attached, so that we don't need the extra output in
> > the _1.out file and the test time is reduced (since the deadlock timeout is set
> > to 10ms in the test, I changed the sleep time to 50ms (I did not want to be very
> > close to 10ms)).
>
> > + <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 <xref linkend="guc-log-lock-waits"/>
> > + is enabled and the lock was successfully acquired after waiting longer
> > + than <xref linkend="guc-deadlock-timeout"/>.
> > + </para>
> > + </entry>
>
> It does not make much sense to me to decide that the counter is
> incremented if a GUC related to a control of the logs generated is
> enabled. It's a fact that log_lock_waits is enabled by default these
> days, hence we will be able to get the time calculation for free for
> most deployments, but it seems inconsistent to me to not count this
> information if the GUC is disabled. We should increment the counter
> and aggregate the time spent on the wait all the time, no?
Yeah that's another way to think about it. In my mind the GUC was a "protection"
to be able to avoid the extra GetCurrentTimestamp() call. But since it's done
only if we waited longer than the deadlock timeout that's also fine to call
GetCurrentTimestamp() even if log_lock_waits is off. Done that way in the
attached.
>
> + * Copyright (c) 2021-2025, PostgreSQL Global Development Group
>
> Incorrect date at the top of pgstat_lock.c.
Yeah, so replaced 2025 with 2026. I did not change 2021 because it's mainly copied
from pgstat_io.c and I also think that's not that important ([1]).
> storage/lock.h is included in pgstat.h as LOCKTAG_LAST_TYPE is wanted
> for the new lock stats structure. That would pull in a lot of header
> data into pgstat.h. How about creating a new header that splits a
> portion of lock.h into a new file? LockTagType, LOCKTAG_LAST_TYPE,
> LockTagTypeNames at least and perhaps a few other things? Or we could
> just limit ourselves to a locktag.h with these three, then include the
> new locktag.h in pgstat.h.
That's a good idea! I only moved LockTagType, LOCKTAG_LAST_TYPE, LockTagTypeNames
and LOCKTAG into a new locktag.h. I chose not to move the SET_LOCKTAG_* macros
because then we'd also need to move DEFAULT_LOCKMETHOD and USER_LOCKMETHOD that
I think are better suited in lock.h.
I did not check if there are any other files that could benefit of using
locktag.h instead of lock.h but that's something I'll do and open a dedicated
if any (once locktag.h is in the tree).
> It would be nice to document at the top of the new spec file what this
> test is here for,
Yeah, I copied stats.spec which has no comments but better to add some. Done in
the new version.
> and perhaps name it stats-lock instead?
Not sure as we already have multixact-stats.
[1]: https://postgr.es/m/202501211750.xf5j6thdkppy%40alvherre.pgsql
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
Attachments:
[text/x-diff] v11-0001-Add-storage-locktag.h.patch (5.8K, ../../abvrRZo52Yx9ZzWQ@ip-10-97-1-34.eu-west-3.compute.internal/2-v11-0001-Add-storage-locktag.h.patch)
download | inline diff:
From d46f85045e7d20f5fead3d1afdd508a3ad270b8d Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Thu, 19 Mar 2026 09:50:06 +0000
Subject: [PATCH v11 1/3] Add storage/locktag.h
Move LockTagType, LOCKTAG_LAST_TYPE, LockTagTypeNames and LOCKTAG out
of storage/lock.h into a new storage/locktag.h header. This will avoid
pulling in the heavyweight storage/lock.h header from pgstat.h in a following
commit.
Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Discussion: https://postgr.es/m/abufUya2oK-_PJ3E%40paquier.xyz
---
src/include/storage/lock.h | 47 +-------------------------
src/include/storage/locktag.h | 63 +++++++++++++++++++++++++++++++++++
2 files changed, 64 insertions(+), 46 deletions(-)
100.0% src/include/storage/
diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h
index 55ffaa5e4a5..e72d94bba49 100644
--- a/src/include/storage/lock.h
+++ b/src/include/storage/lock.h
@@ -21,6 +21,7 @@
#include "access/transam.h"
#include "lib/ilist.h"
#include "storage/lockdefs.h"
+#include "storage/locktag.h"
#include "storage/lwlock.h"
#include "storage/procnumber.h"
#include "storage/shmem.h"
@@ -127,52 +128,6 @@ typedef uint16 LOCKMETHODID;
#define DEFAULT_LOCKMETHOD 1
#define USER_LOCKMETHOD 2
-/*
- * LOCKTAG is the key information needed to look up a LOCK item in the
- * lock hashtable. A LOCKTAG value uniquely identifies a lockable object.
- *
- * The LockTagType enum defines the different kinds of objects we can lock.
- * We can handle up to 256 different LockTagTypes.
- */
-typedef enum LockTagType
-{
- LOCKTAG_RELATION, /* whole relation */
- LOCKTAG_RELATION_EXTEND, /* the right to extend a relation */
- LOCKTAG_DATABASE_FROZEN_IDS, /* pg_database.datfrozenxid */
- LOCKTAG_PAGE, /* one page of a relation */
- LOCKTAG_TUPLE, /* one physical tuple */
- LOCKTAG_TRANSACTION, /* transaction (for waiting for xact done) */
- LOCKTAG_VIRTUALTRANSACTION, /* virtual transaction (ditto) */
- LOCKTAG_SPECULATIVE_TOKEN, /* speculative insertion Xid and token */
- LOCKTAG_OBJECT, /* non-relation database object */
- LOCKTAG_USERLOCK, /* reserved for old contrib/userlock code */
- LOCKTAG_ADVISORY, /* advisory user locks */
- LOCKTAG_APPLY_TRANSACTION, /* transaction being applied on a logical
- * replication subscriber */
-} LockTagType;
-
-#define LOCKTAG_LAST_TYPE LOCKTAG_APPLY_TRANSACTION
-
-extern PGDLLIMPORT const char *const LockTagTypeNames[];
-
-/*
- * The LOCKTAG struct is defined with malice aforethought to fit into 16
- * bytes with no padding. Note that this would need adjustment if we were
- * to widen Oid, BlockNumber, or TransactionId to more than 32 bits.
- *
- * We include lockmethodid in the locktag so that a single hash table in
- * shared memory can store locks of different lockmethods.
- */
-typedef struct LOCKTAG
-{
- uint32 locktag_field1; /* a 32-bit ID field */
- uint32 locktag_field2; /* a 32-bit ID field */
- uint32 locktag_field3; /* a 32-bit ID field */
- uint16 locktag_field4; /* a 16-bit ID field */
- uint8 locktag_type; /* see enum LockTagType */
- uint8 locktag_lockmethodid; /* lockmethod indicator */
-} LOCKTAG;
-
/*
* These macros define how we map logical IDs of lockable objects into
* the physical fields of LOCKTAG. Use these to set up LOCKTAG values,
diff --git a/src/include/storage/locktag.h b/src/include/storage/locktag.h
new file mode 100644
index 00000000000..22ad9c22798
--- /dev/null
+++ b/src/include/storage/locktag.h
@@ -0,0 +1,63 @@
+/*-------------------------------------------------------------------------
+ *
+ * locktag.h
+ * POSTGRES LOCKTAG to look up a LOCK item in the lock hashtable.
+ *
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ * Portions Copyright (c) 1994, Regents of the University of California
+ *
+ * src/include/storage/locktag.h
+ *
+ *-------------------------------------------------------------------------
+ */
+#ifndef LOCKTAG_H_
+#define LOCKTAG_H_
+
+/*
+ * LOCKTAG is the key information needed to look up a LOCK item in the
+ * lock hashtable. A LOCKTAG value uniquely identifies a lockable object.
+ *
+ * The LockTagType enum defines the different kinds of objects we can lock.
+ * We can handle up to 256 different LockTagTypes.
+ */
+typedef enum LockTagType
+{
+ LOCKTAG_RELATION, /* whole relation */
+ LOCKTAG_RELATION_EXTEND, /* the right to extend a relation */
+ LOCKTAG_DATABASE_FROZEN_IDS, /* pg_database.datfrozenxid */
+ LOCKTAG_PAGE, /* one page of a relation */
+ LOCKTAG_TUPLE, /* one physical tuple */
+ LOCKTAG_TRANSACTION, /* transaction (for waiting for xact done) */
+ LOCKTAG_VIRTUALTRANSACTION, /* virtual transaction (ditto) */
+ LOCKTAG_SPECULATIVE_TOKEN, /* speculative insertion Xid and token */
+ LOCKTAG_OBJECT, /* non-relation database object */
+ LOCKTAG_USERLOCK, /* reserved for old contrib/userlock code */
+ LOCKTAG_ADVISORY, /* advisory user locks */
+ LOCKTAG_APPLY_TRANSACTION, /* transaction being applied on a logical
+ * replication subscriber */
+} LockTagType;
+
+#define LOCKTAG_LAST_TYPE LOCKTAG_APPLY_TRANSACTION
+
+extern PGDLLIMPORT const char *const LockTagTypeNames[];
+
+/*
+ * The LOCKTAG struct is defined with malice aforethought to fit into 16
+ * bytes with no padding. Note that this would need adjustment if we were
+ * to widen Oid, BlockNumber, or TransactionId to more than 32 bits.
+ *
+ * We include lockmethodid in the locktag so that a single hash table in
+ * shared memory can store locks of different lockmethods.
+ */
+typedef struct LOCKTAG
+{
+ uint32 locktag_field1; /* a 32-bit ID field */
+ uint32 locktag_field2; /* a 32-bit ID field */
+ uint32 locktag_field3; /* a 32-bit ID field */
+ uint16 locktag_field4; /* a 16-bit ID field */
+ uint8 locktag_type; /* see enum LockTagType */
+ uint8 locktag_lockmethodid; /* lockmethod indicator */
+} LOCKTAG;
+
+#endif /* LOCKTAG_H_ */
--
2.34.1
[text/x-diff] v11-0002-Add-lock-statistics.patch (24.5K, ../../abvrRZo52Yx9ZzWQ@ip-10-97-1-34.eu-west-3.compute.internal/3-v11-0002-Add-lock-statistics.patch)
download | inline diff:
From 48c398db3600cc40e3e75cfd767afa90696472b3 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Tue, 29 Jul 2025 08:36:35 +0000
Subject: [PATCH v11 2/3] Add lock statistics
Adding a new stat kind PGSTAT_KIND_LOCK for the lock statistics.
This new statistic kind is a fixed one because its key is the lock type
so that we know its size is LOCKTAG_LAST_TYPE + 1.
This statistic kind records the following counters:
waits
wait_time
fastpath_exceeded
The waits and wait_time counters are incremented if the session waited longer
than deadlock_timeout to acquire the lock.
fastpath_exceeded is incremented when the lock can not be acquired via fast path
because the fast path slot limit was exceeded.
No extra details is added (like the ones, i.e relation oid, database oid, we
can find in pg_locks). The idea is to provide an idea on what the locking
behaviour looks like.
XXX: Bump stat file format
Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reviewed-by: Andres Freund <andres@anarazel.de>
Discussion: https://postgr.es/m/aIyNxBWFCybgBZBS%40ip-10-97-1-34.eu-west-3.compute.internal
---
src/backend/storage/lmgr/lock.c | 61 +++++---
src/backend/storage/lmgr/proc.c | 188 ++++++++++++-----------
src/backend/utils/activity/Makefile | 1 +
src/backend/utils/activity/meson.build | 1 +
src/backend/utils/activity/pgstat.c | 18 +++
src/backend/utils/activity/pgstat_lock.c | 170 ++++++++++++++++++++
src/include/pgstat.h | 27 ++++
src/include/utils/pgstat_internal.h | 21 +++
src/include/utils/pgstat_kind.h | 5 +-
src/tools/pgindent/typedefs.list | 4 +
10 files changed, 384 insertions(+), 112 deletions(-)
61.8% src/backend/storage/lmgr/
29.9% src/backend/utils/activity/
3.8% src/include/utils/
3.9% src/include/
diff --git a/src/backend/storage/lmgr/lock.c b/src/backend/storage/lmgr/lock.c
index d930c66cdbd..81319cfff2a 100644
--- a/src/backend/storage/lmgr/lock.c
+++ b/src/backend/storage/lmgr/lock.c
@@ -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"
@@ -984,36 +985,48 @@ LockAcquireExtended(const LOCKTAG *locktag,
* lock type on a relation we have already locked using the fast-path, but
* for now we don't worry about that case either.
*/
- if (EligibleForRelationFastPath(locktag, lockmode) &&
- FastPathLocalUseCounts[FAST_PATH_REL_GROUP(locktag->locktag_field2)] < FP_LOCK_SLOTS_PER_GROUP)
+ if (EligibleForRelationFastPath(locktag, lockmode))
{
- uint32 fasthashcode = FastPathStrongLockHashPartition(hashcode);
- bool acquired;
+ if (FastPathLocalUseCounts[FAST_PATH_REL_GROUP(locktag->locktag_field2)] <
+ FP_LOCK_SLOTS_PER_GROUP)
+ {
+ uint32 fasthashcode = FastPathStrongLockHashPartition(hashcode);
+ bool acquired;
- /*
- * LWLockAcquire acts as a memory sequencing point, so it's safe to
- * assume that any strong locker whose increment to
- * FastPathStrongRelationLocks->counts becomes visible after we test
- * it has yet to begin to transfer fast-path locks.
- */
- LWLockAcquire(&MyProc->fpInfoLock, LW_EXCLUSIVE);
- if (FastPathStrongRelationLocks->count[fasthashcode] != 0)
- acquired = false;
+ /*
+ * LWLockAcquire acts as a memory sequencing point, so it's safe
+ * to assume that any strong locker whose increment to
+ * FastPathStrongRelationLocks->counts becomes visible after we
+ * test it has yet to begin to transfer fast-path locks.
+ */
+ LWLockAcquire(&MyProc->fpInfoLock, LW_EXCLUSIVE);
+ if (FastPathStrongRelationLocks->count[fasthashcode] != 0)
+ acquired = false;
+ else
+ acquired = FastPathGrantRelationLock(locktag->locktag_field2,
+ lockmode);
+ LWLockRelease(&MyProc->fpInfoLock);
+ if (acquired)
+ {
+ /*
+ * The locallock might contain stale pointers to some old
+ * shared objects; we MUST reset these to null before
+ * considering the lock to be acquired via fast-path.
+ */
+ locallock->lock = NULL;
+ locallock->proclock = NULL;
+ GrantLockLocal(locallock, owner);
+ return LOCKACQUIRE_OK;
+ }
+ }
else
- acquired = FastPathGrantRelationLock(locktag->locktag_field2,
- lockmode);
- LWLockRelease(&MyProc->fpInfoLock);
- if (acquired)
{
/*
- * The locallock might contain stale pointers to some old shared
- * objects; we MUST reset these to null before considering the
- * lock to be acquired via fast-path.
+ * Increment the lock statistics counter. That's OK from an
+ * overhead point of view as we are outside the fastpath code
+ * path.
*/
- locallock->lock = NULL;
- locallock->proclock = NULL;
- GrantLockLocal(locallock, owner);
- return LOCKACQUIRE_OK;
+ pgstat_count_lock_fastpath_exceeded(locallock->tag.lock.locktag_type);
}
}
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 8f5ce0e2a8a..72456151aa3 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -1549,122 +1549,138 @@ 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 (log_lock_waits && deadlock_state != DS_NOT_YET_CHECKED)
+ if (deadlock_state != DS_NOT_YET_CHECKED)
{
- StringInfoData buf,
- lock_waiters_sbuf,
- lock_holders_sbuf;
- const char *modename;
long secs;
int usecs;
long msecs;
- int lockHoldersNum = 0;
- initStringInfo(&buf);
- initStringInfo(&lock_waiters_sbuf);
- initStringInfo(&lock_holders_sbuf);
-
- DescribeLockTag(&buf, &locallock->tag.lock);
- modename = GetLockmodeName(locallock->tag.lock.locktag_lockmethodid,
- lockmode);
TimestampDifference(get_timeout_start_time(DEADLOCK_TIMEOUT),
GetCurrentTimestamp(),
&secs, &usecs);
msecs = secs * 1000 + usecs / 1000;
usecs = usecs % 1000;
- /* Gather a list of all lock holders and waiters */
- LWLockAcquire(partitionLock, LW_SHARED);
- GetLockHoldersAndWaiters(locallock, &lock_holders_sbuf,
- &lock_waiters_sbuf, &lockHoldersNum);
- LWLockRelease(partitionLock);
-
- if (deadlock_state == DS_SOFT_DEADLOCK)
- ereport(LOG,
- (errmsg("process %d avoided deadlock for %s on %s by rearranging queue order after %ld.%03d ms",
- MyProcPid, modename, buf.data, msecs, usecs),
- (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
- "Processes holding the lock: %s. Wait queue: %s.",
- lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
- else if (deadlock_state == DS_HARD_DEADLOCK)
- {
- /*
- * This message is a bit redundant with the error that will be
- * reported subsequently, but in some cases the error report
- * might not make it to the log (eg, if it's caught by an
- * exception handler), and we want to ensure all long-wait
- * events get logged.
- */
- ereport(LOG,
- (errmsg("process %d detected deadlock while waiting for %s on %s after %ld.%03d ms",
- MyProcPid, modename, buf.data, msecs, usecs),
- (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
- "Processes holding the lock: %s. Wait queue: %s.",
- lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
- }
+ /*
+ * Increment the lock statistics counters. That's OK from an
+ * overhead point of view as we are already stuck behind a lock
+ * for more than the deadlock timeout and we can reuse its wait
+ * time computation here.
+ */
+ if (myWaitStatus == PROC_WAIT_STATUS_OK)
+ pgstat_count_lock_waits(locallock->tag.lock.locktag_type, msecs);
- if (myWaitStatus == PROC_WAIT_STATUS_WAITING)
+ if (log_lock_waits)
{
- /*
- * Guard the "still waiting on lock" log message so it is
- * reported at most once while waiting for the lock.
- *
- * Without this guard, the message can be emitted whenever the
- * lock-wait sleep is interrupted (for example by SIGHUP for
- * config reload or by client_connection_check_interval). For
- * example, if client_connection_check_interval is set very
- * low (e.g., 100 ms), the message could be logged repeatedly,
- * flooding the log and making it difficult to use.
- */
- if (!logged_lock_wait)
- {
+ StringInfoData buf,
+ lock_waiters_sbuf,
+ lock_holders_sbuf;
+ const char *modename;
+ int lockHoldersNum = 0;
+
+ initStringInfo(&buf);
+ initStringInfo(&lock_waiters_sbuf);
+ initStringInfo(&lock_holders_sbuf);
+
+ DescribeLockTag(&buf, &locallock->tag.lock);
+ modename = GetLockmodeName(locallock->tag.lock.locktag_lockmethodid,
+ lockmode);
+
+ /* Gather a list of all lock holders and waiters */
+ LWLockAcquire(partitionLock, LW_SHARED);
+ GetLockHoldersAndWaiters(locallock, &lock_holders_sbuf,
+ &lock_waiters_sbuf, &lockHoldersNum);
+ LWLockRelease(partitionLock);
+
+ if (deadlock_state == DS_SOFT_DEADLOCK)
ereport(LOG,
- (errmsg("process %d still waiting for %s on %s after %ld.%03d ms",
+ (errmsg("process %d avoided deadlock for %s on %s by rearranging queue order after %ld.%03d ms",
MyProcPid, modename, buf.data, msecs, usecs),
(errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
"Processes holding the lock: %s. Wait queue: %s.",
lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
- logged_lock_wait = true;
- }
- }
- else if (myWaitStatus == PROC_WAIT_STATUS_OK)
- ereport(LOG,
- (errmsg("process %d acquired %s on %s after %ld.%03d ms",
- MyProcPid, modename, buf.data, msecs, usecs)));
- else
- {
- Assert(myWaitStatus == PROC_WAIT_STATUS_ERROR);
-
- /*
- * Currently, the deadlock checker always kicks its own
- * process, which means that we'll only see
- * PROC_WAIT_STATUS_ERROR when deadlock_state ==
- * DS_HARD_DEADLOCK, and there's no need to print redundant
- * messages. But for completeness and future-proofing, print
- * a message if it looks like someone else kicked us off the
- * lock.
- */
- if (deadlock_state != DS_HARD_DEADLOCK)
+ else if (deadlock_state == DS_HARD_DEADLOCK)
+ {
+ /*
+ * This message is a bit redundant with the error that
+ * will be reported subsequently, but in some cases the
+ * error report might not make it to the log (eg, if it's
+ * caught by an exception handler), and we want to ensure
+ * all long-wait events get logged.
+ */
ereport(LOG,
- (errmsg("process %d failed to acquire %s on %s after %ld.%03d ms",
+ (errmsg("process %d detected deadlock while waiting for %s on %s after %ld.%03d ms",
MyProcPid, modename, buf.data, msecs, usecs),
(errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
"Processes holding the lock: %s. Wait queue: %s.",
lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
+ }
+
+ if (myWaitStatus == PROC_WAIT_STATUS_WAITING)
+ {
+ /*
+ * Guard the "still waiting on lock" log message so it is
+ * reported at most once while waiting for the lock.
+ *
+ * Without this guard, the message can be emitted whenever
+ * the lock-wait sleep is interrupted (for example by
+ * SIGHUP for config reload or by
+ * client_connection_check_interval). For example, if
+ * client_connection_check_interval is set very low (e.g.,
+ * 100 ms), the message could be logged repeatedly,
+ * flooding the log and making it difficult to use.
+ */
+ if (!logged_lock_wait)
+ {
+ ereport(LOG,
+ (errmsg("process %d still waiting for %s on %s after %ld.%03d ms",
+ MyProcPid, modename, buf.data, msecs, usecs),
+ (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
+ "Processes holding the lock: %s. Wait queue: %s.",
+ lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
+ logged_lock_wait = true;
+ }
+ }
+ else if (myWaitStatus == PROC_WAIT_STATUS_OK)
+ ereport(LOG,
+ (errmsg("process %d acquired %s on %s after %ld.%03d ms",
+ MyProcPid, modename, buf.data, msecs, usecs)));
+ else
+ {
+ Assert(myWaitStatus == PROC_WAIT_STATUS_ERROR);
+
+ /*
+ * Currently, the deadlock checker always kicks its own
+ * process, which means that we'll only see
+ * PROC_WAIT_STATUS_ERROR when deadlock_state ==
+ * DS_HARD_DEADLOCK, and there's no need to print
+ * redundant messages. But for completeness and
+ * future-proofing, print a message if it looks like
+ * someone else kicked us off the lock.
+ */
+ if (deadlock_state != DS_HARD_DEADLOCK)
+ ereport(LOG,
+ (errmsg("process %d failed to acquire %s on %s after %ld.%03d ms",
+ MyProcPid, modename, buf.data, msecs, usecs),
+ (errdetail_log_plural("Process holding the lock: %s. Wait queue: %s.",
+ "Processes holding the lock: %s. Wait queue: %s.",
+ lockHoldersNum, lock_holders_sbuf.data, lock_waiters_sbuf.data))));
+ }
+ pfree(buf.data);
+ pfree(lock_holders_sbuf.data);
+ pfree(lock_waiters_sbuf.data);
}
/*
* At this point we might still need to wait for the lock. Reset
- * state so we don't print the above messages again.
+ * state so we don't print the above messages again if
+ * log_lock_waits is on.
*/
deadlock_state = DS_NO_DEADLOCK;
-
- pfree(buf.data);
- pfree(lock_holders_sbuf.data);
- pfree(lock_waiters_sbuf.data);
}
} while (myWaitStatus == PROC_WAIT_STATUS_WAITING);
diff --git a/src/backend/utils/activity/Makefile b/src/backend/utils/activity/Makefile
index c37bfb350bb..ca3ef89bf59 100644
--- a/src/backend/utils/activity/Makefile
+++ b/src/backend/utils/activity/Makefile
@@ -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 \
diff --git a/src/backend/utils/activity/meson.build b/src/backend/utils/activity/meson.build
index 53bd5a246ca..1aa7ece5290 100644
--- a/src/backend/utils/activity/meson.build
+++ b/src/backend/utils/activity/meson.build
@@ -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',
diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index 11bb71cad5a..eb8ccbaa628 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -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
index 00000000000..fe330eb99b1
--- /dev/null
+++ b/src/backend/utils/activity/pgstat_lock.c
@@ -0,0 +1,170 @@
+/* -------------------------------------------------------------------------
+ *
+ * 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;
+
+/*
+ * 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 a statistic per lock type
+ *
+ * Don't call this function for every lock acquisition and in hot path.
+ */
+#define PGSTAT_COUNT_LOCK_FUNC(stat) \
+void \
+CppConcat(pgstat_count_lock_,stat)(uint8 locktag_type) \
+{ \
+ Assert(locktag_type <= LOCKTAG_LAST_TYPE); \
+ PendingLockStats.stats[locktag_type].stat++; \
+ have_lockstats = true; \
+ pgstat_report_fixed = true; \
+}
+
+/* pgstat_count_lock_fastpath_exceeded */
+PGSTAT_COUNT_LOCK_FUNC(fastpath_exceeded)
+
+/*
+ * Increment the number of waits and wait time per lock type
+ *
+ * Don't call this function for every lock acquisition and in hot path.
+ */
+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;
+}
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 216b93492ba..553106025da 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/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"
@@ -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,14 @@ 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);
+
/*
* Functions in pgstat_database.c
*/
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 9b8fbae00ed..97704421a92 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -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
diff --git a/src/include/utils/pgstat_kind.h b/src/include/utils/pgstat_kind.h
index c30b6235623..2d78a029683 100644
--- a/src/include/utils/pgstat_kind.h
+++ b/src/include/utils/pgstat_kind.h
@@ -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/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 4673eca9cd6..cf00072a9b2 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2276,6 +2276,7 @@ PgStatShared_Database
PgStatShared_Function
PgStatShared_HashEntry
PgStatShared_IO
+PgStatShared_Lock
PgStatShared_Relation
PgStatShared_ReplSlot
PgStatShared_SLRU
@@ -2298,8 +2299,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
--
2.34.1
[text/x-diff] v11-0003-Add-the-pg_stat_lock-view.patch (24.6K, ../../abvrRZo52Yx9ZzWQ@ip-10-97-1-34.eu-west-3.compute.internal/4-v11-0003-Add-the-pg_stat_lock-view.patch)
download | inline diff:
From fa264ed2fd5d7ee44f50cd6e3f906b2cc9c13eb3 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Thu, 31 Jul 2025 09:35:31 +0000
Subject: [PATCH v11 3/3] Add the pg_stat_lock view
This new view reports lock statistics.
Note that it does not omit combinations which do not make sense (as pg_locks
does).
Also wait_time is reported as bigint as the deadlock_timeout default value is
1s.
This commit also adds documentation and a few tests.
XXX: Bump catversion
Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reviewed-by: Andres Freund <andres@anarazel.de>
Discussion: https://postgr.es/m/aIyNxBWFCybgBZBS%40ip-10-97-1-34.eu-west-3.compute.internal
---
doc/src/sgml/monitoring.sgml | 111 ++++++++++++
src/backend/catalog/system_views.sql | 9 +
src/backend/utils/activity/pgstat_lock.c | 8 +
src/backend/utils/adt/pgstatfuncs.c | 39 +++++
src/include/catalog/pg_proc.dat | 9 +
src/include/pgstat.h | 1 +
src/test/isolation/expected/lock-stats.out | 190 +++++++++++++++++++++
src/test/isolation/isolation_schedule | 1 +
src/test/isolation/specs/lock-stats.spec | 125 ++++++++++++++
src/test/regress/expected/rules.out | 6 +
src/test/regress/expected/stats.out | 48 ++++++
src/test/regress/sql/stats.sql | 36 ++++
12 files changed, 583 insertions(+)
19.9% doc/src/sgml/
6.4% src/backend/utils/adt/
35.1% src/test/isolation/expected/
20.8% src/test/isolation/specs/
7.4% src/test/regress/expected/
5.2% src/test/regress/sql/
4.8% src/
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 462019a972c..d590ecad541 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -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,102 @@ 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. You may want to increase
+ <xref linkend="guc-max-locks-per-transaction"/> if you feel this counter
+ is too high.
+ </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 +5477,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
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index f1ed7b58f13..e54018004db 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -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,
diff --git a/src/backend/utils/activity/pgstat_lock.c b/src/backend/utils/activity/pgstat_lock.c
index fe330eb99b1..76749e4724d 100644
--- a/src/backend/utils/activity/pgstat_lock.c
+++ b/src/backend/utils/activity/pgstat_lock.c
@@ -21,6 +21,14 @@
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()
*/
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 5f907335990..9185a8e6b83 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -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)
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index fc8d82665b8..3e2c43e9d1b 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -6041,6 +6041,15 @@
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',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 553106025da..dadf8b1c1ed 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -643,6 +643,7 @@ 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 PgStat_Lock *pgstat_fetch_stat_lock(void);
/*
* Functions in pgstat_database.c
diff --git a/src/test/isolation/expected/lock-stats.out b/src/test/isolation/expected/lock-stats.out
new file mode 100644
index 00000000000..556c210a28b
--- /dev/null
+++ b/src/test/isolation/expected/lock-stats.out
@@ -0,0 +1,190 @@
+Parsed test spec with 2 sessions
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits s2_set_deadlock_timeout s2_set_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 s1_set_log_lock_waits: SET log_lock_waits = on;
+step s2_set_deadlock_timeout: SET deadlock_timeout = '10ms';
+step s2_set_log_lock_waits: SET log_lock_waits = on;
+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, wait_time > 50 FROM pg_stat_lock WHERE locktype = 'relation';
+?column?|?column?
+--------+--------
+t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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 s1_set_log_lock_waits: SET log_lock_waits = on;
+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, wait_time > 50 FROM pg_stat_lock WHERE locktype = 'transactionid';
+?column?|?column?
+--------+--------
+t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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 s1_set_log_lock_waits: SET log_lock_waits = on;
+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, wait_time > 50 FROM pg_stat_lock WHERE locktype = 'advisory';
+?column?|?column?
+--------+--------
+t |t
+(1 row)
+
+
+starting permutation: s1_set_deadlock_timeout s1_reset_stat_lock s1_set_log_lock_waits 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 s1_set_log_lock_waits: SET log_lock_waits = on;
+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, wait_time > 50 FROM pg_stat_lock WHERE locktype = 'relation';
+?column?|?column?
+--------+--------
+t |t
+(1 row)
+
diff --git a/src/test/isolation/isolation_schedule b/src/test/isolation/isolation_schedule
index 4e466580cd4..2cc86849c9a 100644
--- a/src/test/isolation/isolation_schedule
+++ b/src/test/isolation/isolation_schedule
@@ -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
index 00000000000..e4e66a9fb4d
--- /dev/null
+++ b/src/test/isolation/specs/lock-stats.spec
@@ -0,0 +1,125 @@
+# Test for the lock statistics
+#
+# This test creates multiple locking situations when a session (s2) has to
+# wait on a lock for longer than the deadlock timeout. The first permutations
+# each test a dedicated lock type. The last permutation ensures 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_set_log_lock_waits { SET log_lock_waits = on; }
+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, wait_time > 50 FROM pg_stat_lock WHERE locktype = 'relation'; }
+step s2_report_stat_lock_transactionid { SELECT waits > 0, wait_time > 50 FROM pg_stat_lock WHERE locktype = 'transactionid'; }
+step s2_report_stat_lock_advisory { SELECT waits > 0, wait_time > 50 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
+ s1_set_log_lock_waits
+ s2_set_deadlock_timeout
+ s2_set_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
+
+# transaction lock
+
+permutation
+ s1_set_deadlock_timeout
+ s1_reset_stat_lock
+ s1_set_log_lock_waits
+ 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
+ s1_set_log_lock_waits
+ 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
+ s1_set_log_lock_waits
+ 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
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 32bea58db2c..2b3cf6d8569 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -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,
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index b99462bf946..ea7f7846895 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -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
diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql
index 941222cf0be..65d8968c83e 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -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
--
2.34.1
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-03-21 05:55 Michael Paquier <michael@paquier.xyz>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
0 siblings, 2 replies; 61+ messages in thread
From: Michael Paquier @ 2026-03-21 05:55 UTC (permalink / raw)
To: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: Andres Freund <andres@anarazel.de>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
On Thu, Mar 19, 2026 at 12:25:41PM +0000, Bertrand Drouvot wrote:
> That's a good idea! I only moved LockTagType, LOCKTAG_LAST_TYPE, LockTagTypeNames
> and LOCKTAG into a new locktag.h. I chose not to move the SET_LOCKTAG_* macros
> because then we'd also need to move DEFAULT_LOCKMETHOD and USER_LOCKMETHOD that
> I think are better suited in lock.h.
While looking at the full set of declarations, I've come to disagree
to that: by moving the macros, we also do not need to know about the
internal field names of LOCKTAG across multiple headers.
> I did not check if there are any other files that could benefit of using
> locktag.h instead of lock.h but that's something I'll do and open a dedicated
> if any (once locktag.h is in the tree).
I have checked after that, and did not spot an area (except your patch
of course). And applied this part.
> Not sure as we already have multixact-stats.
Ah, right. I did not notice this one. This existing name sounds OK
by me, then.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (832B, ../../ab4y1UQUsSS7bSVd@paquier.xyz/2-signature.asc)
download
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-03-21 20:01 Álvaro Herrera <alvherre@kurilemu.de>
parent: Michael Paquier <michael@paquier.xyz>
1 sibling, 1 reply; 61+ messages in thread
From: Álvaro Herrera @ 2026-03-21 20:01 UTC (permalink / raw)
To: Michael Paquier <michael@paquier.xyz>; Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: Andres Freund <andres@anarazel.de>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; L. pgsql-hackers <pgsql-hackers@lists.postgresql.org>
On 2026-03-21, Michael Paquier wrote:
> On Thu, Mar 19, 2026 at 12:25:41PM +0000, Bertrand Drouvot wrote:
>> I did not check if there are any other files that could benefit of using
>> locktag.h instead of lock.h but that's something I'll do and open a dedicated
>> if any (once locktag.h is in the tree).
>
> I have checked after that, and did not spot an area (except your patch
> of course). And applied this part.
I checked this, and found a couple of headers that can benefit from a removal, as shown in the attached patches.
A special case (not modified here) is proc.h. It seems to me that lock.h _could_ be removed from there with some effort, but the amount of .c files that would benefit seems to me not large enough to justify the number of contortions needed. Others could disagree though.
--
Álvaro Herrera
Attachments:
[text/x-patch] 0001-procarray.h-does-not-need-storage-lock.h-for-anythin.patch (741B, ../../e38089eb-3619-4434-b888-397175c4eaa9@app.fastmail.com/2-0001-procarray.h-does-not-need-storage-lock.h-for-anythin.patch)
download | inline diff:
From aa6155106b7f5dc697df54dc4e4aeb5c6419ec92 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@kurilemu.de>
Date: Sat, 21 Mar 2026 20:33:10 +0100
Subject: [PATCH 1/4] procarray.h does not need storage/lock.h for anything
No fallout from this change. Weird
---
src/include/storage/procarray.h | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/include/storage/procarray.h b/src/include/storage/procarray.h
index c5ab1574fe3..abdf021e66e 100644
--- a/src/include/storage/procarray.h
+++ b/src/include/storage/procarray.h
@@ -14,7 +14,6 @@
#ifndef PROCARRAY_H
#define PROCARRAY_H
-#include "storage/lock.h"
#include "storage/standby.h"
#include "utils/relcache.h"
#include "utils/snapshot.h"
--
2.47.3
[text/x-patch] 0002-lmgr.h-doesn-t-need-the-full-lock.h.patch (1.7K, ../../e38089eb-3619-4434-b888-397175c4eaa9@app.fastmail.com/3-0002-lmgr.h-doesn-t-need-the-full-lock.h.patch)
download | inline diff:
From c09489b4164d0d8c7942ca20a9fb645a1d68ea7f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@kurilemu.de>
Date: Sat, 21 Mar 2026 20:35:51 +0100
Subject: [PATCH 2/4] lmgr.h doesn't need the full lock.h
---
src/backend/utils/activity/wait_event.c | 6 ++++--
src/backend/utils/cache/syscache.c | 1 +
src/include/storage/lmgr.h | 2 +-
3 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index aca2c8fc742..c7e2d825120 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -22,9 +22,11 @@
*/
#include "postgres.h"
-#include "storage/lmgr.h" /* for GetLockNameFromTagType */
-#include "storage/lwlock.h" /* for GetLWLockIdentifier */
+#include "storage/lmgr.h"
+#include "storage/lwlock.h"
+#include "storage/shmem.h"
#include "storage/spin.h"
+#include "utils/hsearch.h"
#include "utils/wait_event.h"
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 007a9a15d71..f4233f9e31a 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -32,6 +32,7 @@
#include "lib/qunique.h"
#include "miscadmin.h"
#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/catcache.h"
#include "utils/inval.h"
#include "utils/lsyscache.h"
diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h
index 74a398ffc00..2a985ce5e15 100644
--- a/src/include/storage/lmgr.h
+++ b/src/include/storage/lmgr.h
@@ -16,7 +16,7 @@
#include "lib/stringinfo.h"
#include "storage/itemptr.h"
-#include "storage/lock.h"
+#include "storage/locktag.h"
#include "utils/rel.h"
--
2.47.3
[text/x-patch] 0003-namespace.h-doesn-t-need-lock.h-only-lockdefs.h.patch (2.9K, ../../e38089eb-3619-4434-b888-397175c4eaa9@app.fastmail.com/4-0003-namespace.h-doesn-t-need-lock.h-only-lockdefs.h.patch)
download | inline diff:
From 35b09d1a4e4e3e65c0754c88098259974c9a1973 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@kurilemu.de>
Date: Sat, 21 Mar 2026 20:49:41 +0100
Subject: [PATCH 3/4] namespace.h doesn't need lock.h, only lockdefs.h
---
src/backend/access/common/relation.c | 1 +
src/backend/commands/conversioncmds.c | 1 +
src/backend/commands/discard.c | 1 +
src/backend/parser/parse_oper.c | 1 +
src/backend/utils/cache/ts_cache.c | 1 +
src/include/catalog/namespace.h | 2 +-
6 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/src/backend/access/common/relation.c b/src/backend/access/common/relation.c
index 92a6e37d8bd..2e3a37c9272 100644
--- a/src/backend/access/common/relation.c
+++ b/src/backend/access/common/relation.c
@@ -24,6 +24,7 @@
#include "access/xact.h"
#include "catalog/namespace.h"
#include "pgstat.h"
+#include "storage/lock.h"
#include "storage/lmgr.h"
#include "utils/inval.h"
#include "utils/syscache.h"
diff --git a/src/backend/commands/conversioncmds.c b/src/backend/commands/conversioncmds.c
index 61aa8bb9fd1..5f2022d3072 100644
--- a/src/backend/commands/conversioncmds.c
+++ b/src/backend/commands/conversioncmds.c
@@ -19,6 +19,7 @@
#include "catalog/pg_proc.h"
#include "catalog/pg_type.h"
#include "commands/conversioncmds.h"
+#include "fmgr.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
#include "parser/parse_func.h"
diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c
index 7b5520b9abe..17d172df076 100644
--- a/src/backend/commands/discard.c
+++ b/src/backend/commands/discard.c
@@ -19,6 +19,7 @@
#include "commands/discard.h"
#include "commands/prepare.h"
#include "commands/sequence.h"
+#include "storage/lock.h"
#include "utils/guc.h"
#include "utils/portal.h"
diff --git a/src/backend/parser/parse_oper.c b/src/backend/parser/parse_oper.c
index a6b402f2d7b..2f218c1ab8b 100644
--- a/src/backend/parser/parse_oper.c
+++ b/src/backend/parser/parse_oper.c
@@ -25,6 +25,7 @@
#include "parser/parse_oper.h"
#include "parser/parse_type.h"
#include "utils/builtins.h"
+#include "utils/hsearch.h"
#include "utils/inval.h"
#include "utils/lsyscache.h"
#include "utils/syscache.h"
diff --git a/src/backend/utils/cache/ts_cache.c b/src/backend/utils/cache/ts_cache.c
index 744c8e71d71..9e29f1386b0 100644
--- a/src/backend/utils/cache/ts_cache.c
+++ b/src/backend/utils/cache/ts_cache.c
@@ -44,6 +44,7 @@
#include "utils/catcache.h"
#include "utils/fmgroids.h"
#include "utils/guc_hooks.h"
+#include "utils/hsearch.h"
#include "utils/inval.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h
index 1a25973685c..9453a3e4932 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -15,7 +15,7 @@
#define NAMESPACE_H
#include "nodes/primnodes.h"
-#include "storage/lock.h"
+#include "storage/lockdefs.h"
#include "storage/procnumber.h"
--
2.47.3
[text/x-patch] 0004-vacuum.h-doesn-t-need-lock.h.patch (1.6K, ../../e38089eb-3619-4434-b888-397175c4eaa9@app.fastmail.com/5-0004-vacuum.h-doesn-t-need-lock.h.patch)
download | inline diff:
From 13fc9223da3fab2cc958e302c00175a6e0c516c8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@kurilemu.de>
Date: Sat, 21 Mar 2026 20:53:54 +0100
Subject: [PATCH 4/4] vacuum.h doesn't need lock.h
---
src/backend/access/nbtree/nbtree.c | 1 +
src/backend/catalog/pg_subscription.c | 1 +
src/include/commands/vacuum.h | 1 -
3 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index aed74590cf4..6d870e4ebe7 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -30,6 +30,7 @@
#include "storage/indexfsm.h"
#include "storage/ipc.h"
#include "storage/lmgr.h"
+#include "storage/lwlock.h"
#include "storage/read_stream.h"
#include "utils/datum.h"
#include "utils/fmgrprotos.h"
diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c
index 3673d4f0bc1..1d140c513d0 100644
--- a/src/backend/catalog/pg_subscription.c
+++ b/src/backend/catalog/pg_subscription.c
@@ -26,6 +26,7 @@
#include "foreign/foreign.h"
#include "miscadmin.h"
#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/acl.h"
#include "utils/array.h"
#include "utils/builtins.h"
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 953a506181e..1f45bca015c 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -23,7 +23,6 @@
#include "catalog/pg_type.h"
#include "parser/parse_node.h"
#include "storage/buf.h"
-#include "storage/lock.h"
#include "utils/relcache.h"
/*
--
2.47.3
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-03-22 19:37 Álvaro Herrera <alvherre@kurilemu.de>
parent: Álvaro Herrera <alvherre@kurilemu.de>
0 siblings, 1 reply; 61+ messages in thread
From: Álvaro Herrera @ 2026-03-22 19:37 UTC (permalink / raw)
To: Michael Paquier <michael@paquier.xyz>; Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: Andres Freund <andres@anarazel.de>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; L. pgsql-hackers <pgsql-hackers@lists.postgresql.org>
On 2026-Mar-21, Álvaro Herrera wrote:
> I checked this, and found a couple of headers that can benefit from a
> removal, as shown in the attached patches.
I looked again and found some more; the first 14 patches attached here
do so. A couple of them have no fallout to speak of, which I find
really surprising; for the majority we just need a couple of extra
includes somewhere or a typedef or two. I unleashed CI on it to see
what would happen,
https://cirrus-ci.com/build/5522804717649920
The 15th one removes the inclusion of clog.h in proc.h. Not really
related to this thread, but I noticed while looking at proc.h for
removal of lock.h.
> A special case (not modified here) is proc.h. It seems to me that
> lock.h _could_ be removed from there with some effort, but the amount
> of .c files that would benefit seems to me not large enough to justify
> the number of contortions needed. Others could disagree though.
I looked at it again while doing the above and came to the same
conclusion. In order to make it work we'd need half a dozen typedefs,
and it doesn't really look very pretty.
--
Álvaro Herrera 48°01'N 7°57'E — https://www.EnterpriseDB.com/
Syntax error: function hell() needs an argument.
Please choose what hell you want to involve.
Attachments:
[text/x-diff] v2-0001-procarray.h-does-not-need-storage-lock.h-for-anyt.patch (745B, ../../202603221856.iwlhitt6dxxx@alvherre.pgsql/2-v2-0001-procarray.h-does-not-need-storage-lock.h-for-anyt.patch)
download | inline diff:
From fa3addb3e59e1e816b86fc8fdf80ebbc4cc2b119 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@kurilemu.de>
Date: Sat, 21 Mar 2026 20:33:10 +0100
Subject: [PATCH v2 01/15] procarray.h does not need storage/lock.h for
anything
No fallout from this change. Weird
---
src/include/storage/procarray.h | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/include/storage/procarray.h b/src/include/storage/procarray.h
index c5ab1574fe3..abdf021e66e 100644
--- a/src/include/storage/procarray.h
+++ b/src/include/storage/procarray.h
@@ -14,7 +14,6 @@
#ifndef PROCARRAY_H
#define PROCARRAY_H
-#include "storage/lock.h"
#include "storage/standby.h"
#include "utils/relcache.h"
#include "utils/snapshot.h"
--
2.47.3
[text/x-diff] v2-0002-lmgr.h-doesn-t-need-the-full-lock.h.patch (1.7K, ../../202603221856.iwlhitt6dxxx@alvherre.pgsql/3-v2-0002-lmgr.h-doesn-t-need-the-full-lock.h.patch)
download | inline diff:
From 2a3617412f169efca8a044ad03daa047c8427cbd Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@kurilemu.de>
Date: Sat, 21 Mar 2026 20:35:51 +0100
Subject: [PATCH v2 02/15] lmgr.h doesn't need the full lock.h
---
src/backend/utils/activity/wait_event.c | 6 ++++--
src/backend/utils/cache/syscache.c | 1 +
src/include/storage/lmgr.h | 2 +-
3 files changed, 6 insertions(+), 3 deletions(-)
diff --git a/src/backend/utils/activity/wait_event.c b/src/backend/utils/activity/wait_event.c
index aca2c8fc742..c7e2d825120 100644
--- a/src/backend/utils/activity/wait_event.c
+++ b/src/backend/utils/activity/wait_event.c
@@ -22,9 +22,11 @@
*/
#include "postgres.h"
-#include "storage/lmgr.h" /* for GetLockNameFromTagType */
-#include "storage/lwlock.h" /* for GetLWLockIdentifier */
+#include "storage/lmgr.h"
+#include "storage/lwlock.h"
+#include "storage/shmem.h"
#include "storage/spin.h"
+#include "utils/hsearch.h"
#include "utils/wait_event.h"
diff --git a/src/backend/utils/cache/syscache.c b/src/backend/utils/cache/syscache.c
index 007a9a15d71..f4233f9e31a 100644
--- a/src/backend/utils/cache/syscache.c
+++ b/src/backend/utils/cache/syscache.c
@@ -32,6 +32,7 @@
#include "lib/qunique.h"
#include "miscadmin.h"
#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/catcache.h"
#include "utils/inval.h"
#include "utils/lsyscache.h"
diff --git a/src/include/storage/lmgr.h b/src/include/storage/lmgr.h
index 74a398ffc00..2a985ce5e15 100644
--- a/src/include/storage/lmgr.h
+++ b/src/include/storage/lmgr.h
@@ -16,7 +16,7 @@
#include "lib/stringinfo.h"
#include "storage/itemptr.h"
-#include "storage/lock.h"
+#include "storage/locktag.h"
#include "utils/rel.h"
--
2.47.3
[text/x-diff] v2-0003-namespace.h-doesn-t-need-lock.h-only-lockdefs.h.patch (2.9K, ../../202603221856.iwlhitt6dxxx@alvherre.pgsql/4-v2-0003-namespace.h-doesn-t-need-lock.h-only-lockdefs.h.patch)
download | inline diff:
From 59bab523687d21ff8732c453693e45054b0e3def Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@kurilemu.de>
Date: Sat, 21 Mar 2026 20:49:41 +0100
Subject: [PATCH v2 03/15] namespace.h doesn't need lock.h, only lockdefs.h
---
src/backend/access/common/relation.c | 1 +
src/backend/commands/conversioncmds.c | 1 +
src/backend/commands/discard.c | 1 +
src/backend/parser/parse_oper.c | 1 +
src/backend/utils/cache/ts_cache.c | 1 +
src/include/catalog/namespace.h | 2 +-
6 files changed, 6 insertions(+), 1 deletion(-)
diff --git a/src/backend/access/common/relation.c b/src/backend/access/common/relation.c
index 92a6e37d8bd..2e3a37c9272 100644
--- a/src/backend/access/common/relation.c
+++ b/src/backend/access/common/relation.c
@@ -24,6 +24,7 @@
#include "access/xact.h"
#include "catalog/namespace.h"
#include "pgstat.h"
+#include "storage/lock.h"
#include "storage/lmgr.h"
#include "utils/inval.h"
#include "utils/syscache.h"
diff --git a/src/backend/commands/conversioncmds.c b/src/backend/commands/conversioncmds.c
index 61aa8bb9fd1..5f2022d3072 100644
--- a/src/backend/commands/conversioncmds.c
+++ b/src/backend/commands/conversioncmds.c
@@ -19,6 +19,7 @@
#include "catalog/pg_proc.h"
#include "catalog/pg_type.h"
#include "commands/conversioncmds.h"
+#include "fmgr.h"
#include "mb/pg_wchar.h"
#include "miscadmin.h"
#include "parser/parse_func.h"
diff --git a/src/backend/commands/discard.c b/src/backend/commands/discard.c
index 7b5520b9abe..17d172df076 100644
--- a/src/backend/commands/discard.c
+++ b/src/backend/commands/discard.c
@@ -19,6 +19,7 @@
#include "commands/discard.h"
#include "commands/prepare.h"
#include "commands/sequence.h"
+#include "storage/lock.h"
#include "utils/guc.h"
#include "utils/portal.h"
diff --git a/src/backend/parser/parse_oper.c b/src/backend/parser/parse_oper.c
index a6b402f2d7b..2f218c1ab8b 100644
--- a/src/backend/parser/parse_oper.c
+++ b/src/backend/parser/parse_oper.c
@@ -25,6 +25,7 @@
#include "parser/parse_oper.h"
#include "parser/parse_type.h"
#include "utils/builtins.h"
+#include "utils/hsearch.h"
#include "utils/inval.h"
#include "utils/lsyscache.h"
#include "utils/syscache.h"
diff --git a/src/backend/utils/cache/ts_cache.c b/src/backend/utils/cache/ts_cache.c
index 744c8e71d71..9e29f1386b0 100644
--- a/src/backend/utils/cache/ts_cache.c
+++ b/src/backend/utils/cache/ts_cache.c
@@ -44,6 +44,7 @@
#include "utils/catcache.h"
#include "utils/fmgroids.h"
#include "utils/guc_hooks.h"
+#include "utils/hsearch.h"
#include "utils/inval.h"
#include "utils/lsyscache.h"
#include "utils/memutils.h"
diff --git a/src/include/catalog/namespace.h b/src/include/catalog/namespace.h
index 1a25973685c..9453a3e4932 100644
--- a/src/include/catalog/namespace.h
+++ b/src/include/catalog/namespace.h
@@ -15,7 +15,7 @@
#define NAMESPACE_H
#include "nodes/primnodes.h"
-#include "storage/lock.h"
+#include "storage/lockdefs.h"
#include "storage/procnumber.h"
--
2.47.3
[text/x-diff] v2-0004-vacuum.h-doesn-t-need-lock.h.patch (1.6K, ../../202603221856.iwlhitt6dxxx@alvherre.pgsql/5-v2-0004-vacuum.h-doesn-t-need-lock.h.patch)
download | inline diff:
From 605e2b8866607a1f3d79bda89d93678c91af6848 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@kurilemu.de>
Date: Sat, 21 Mar 2026 20:53:54 +0100
Subject: [PATCH v2 04/15] vacuum.h doesn't need lock.h
---
src/backend/access/nbtree/nbtree.c | 1 +
src/backend/catalog/pg_subscription.c | 1 +
src/include/commands/vacuum.h | 1 -
3 files changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/backend/access/nbtree/nbtree.c b/src/backend/access/nbtree/nbtree.c
index aed74590cf4..6d870e4ebe7 100644
--- a/src/backend/access/nbtree/nbtree.c
+++ b/src/backend/access/nbtree/nbtree.c
@@ -30,6 +30,7 @@
#include "storage/indexfsm.h"
#include "storage/ipc.h"
#include "storage/lmgr.h"
+#include "storage/lwlock.h"
#include "storage/read_stream.h"
#include "utils/datum.h"
#include "utils/fmgrprotos.h"
diff --git a/src/backend/catalog/pg_subscription.c b/src/backend/catalog/pg_subscription.c
index 3673d4f0bc1..1d140c513d0 100644
--- a/src/backend/catalog/pg_subscription.c
+++ b/src/backend/catalog/pg_subscription.c
@@ -26,6 +26,7 @@
#include "foreign/foreign.h"
#include "miscadmin.h"
#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "utils/acl.h"
#include "utils/array.h"
#include "utils/builtins.h"
diff --git a/src/include/commands/vacuum.h b/src/include/commands/vacuum.h
index 953a506181e..1f45bca015c 100644
--- a/src/include/commands/vacuum.h
+++ b/src/include/commands/vacuum.h
@@ -23,7 +23,6 @@
#include "catalog/pg_type.h"
#include "parser/parse_node.h"
#include "storage/buf.h"
-#include "storage/lock.h"
#include "utils/relcache.h"
/*
--
2.47.3
[text/x-diff] v2-0005-standby.h-doesn-t-need-the-full-lock.h.patch (1.9K, ../../202603221856.iwlhitt6dxxx@alvherre.pgsql/6-v2-0005-standby.h-doesn-t-need-the-full-lock.h.patch)
download | inline diff:
From 30faaa8fe40d1f95480de3ad00b9576fd4ea7e1d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@kurilemu.de>
Date: Sun, 22 Mar 2026 17:52:12 +0100
Subject: [PATCH v2 05/15] standby.h doesn't need the full lock.h
---
contrib/amcheck/verify_heapam.c | 1 +
src/include/storage/lock.h | 2 +-
src/include/storage/standby.h | 5 ++++-
3 files changed, 6 insertions(+), 2 deletions(-)
diff --git a/contrib/amcheck/verify_heapam.c b/contrib/amcheck/verify_heapam.c
index ada782f98f5..20ff58aa782 100644
--- a/contrib/amcheck/verify_heapam.c
+++ b/contrib/amcheck/verify_heapam.c
@@ -24,6 +24,7 @@
#include "funcapi.h"
#include "miscadmin.h"
#include "storage/bufmgr.h"
+#include "storage/lwlock.h"
#include "storage/procarray.h"
#include "storage/read_stream.h"
#include "utils/builtins.h"
diff --git a/src/include/storage/lock.h b/src/include/storage/lock.h
index 9505701a805..fa68e6ecece 100644
--- a/src/include/storage/lock.h
+++ b/src/include/storage/lock.h
@@ -59,7 +59,7 @@ extern PGDLLIMPORT bool Debug_deadlocks;
* coding errors from trying to use struct assignment with it; instead use
* GET_VXID_FROM_PGPROC().
*/
-typedef struct
+typedef struct VirtualTransactionId
{
ProcNumber procNumber; /* proc number of the PGPROC */
LocalTransactionId localTransactionId; /* lxid from PGPROC */
diff --git a/src/include/storage/standby.h b/src/include/storage/standby.h
index c63a4f2cc6a..6a314c693cd 100644
--- a/src/include/storage/standby.h
+++ b/src/include/storage/standby.h
@@ -15,10 +15,13 @@
#define STANDBY_H
#include "datatype/timestamp.h"
-#include "storage/lock.h"
+#include "storage/locktag.h"
#include "storage/relfilelocator.h"
#include "storage/standbydefs.h"
+typedef struct PGPROC PGPROC;
+typedef struct VirtualTransactionId VirtualTransactionId;
+
/* User-settable GUC parameters */
extern PGDLLIMPORT int max_standby_archive_delay;
extern PGDLLIMPORT int max_standby_streaming_delay;
--
2.47.3
[text/x-diff] v2-0006-tablecmds.h-doesn-t-need-the-full-lock.h.patch (833B, ../../202603221856.iwlhitt6dxxx@alvherre.pgsql/7-v2-0006-tablecmds.h-doesn-t-need-the-full-lock.h.patch)
download | inline diff:
From 1c8324331616a58bd9a14f77cc0bdd68c8e79239 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@kurilemu.de>
Date: Sun, 22 Mar 2026 17:53:31 +0100
Subject: [PATCH v2 06/15] tablecmds.h doesn't need the full lock.h
---
src/include/commands/tablecmds.h | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/include/commands/tablecmds.h b/src/include/commands/tablecmds.h
index ed38b311ddf..c3d8518cb62 100644
--- a/src/include/commands/tablecmds.h
+++ b/src/include/commands/tablecmds.h
@@ -18,7 +18,7 @@
#include "catalog/dependency.h"
#include "catalog/objectaddress.h"
#include "nodes/parsenodes.h"
-#include "storage/lock.h"
+#include "storage/lockdefs.h"
#include "utils/relcache.h"
typedef struct AlterTableUtilityContext AlterTableUtilityContext; /* avoid including
--
2.47.3
[text/x-diff] v2-0007-reloptions.h-doesn-t-need-lock.h-at-all.patch (2.6K, ../../202603221856.iwlhitt6dxxx@alvherre.pgsql/8-v2-0007-reloptions.h-doesn-t-need-lock.h-at-all.patch)
download | inline diff:
From d75283b1aaa586bc2c4965bca5adba5097ebe8db Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@kurilemu.de>
Date: Sun, 22 Mar 2026 17:56:44 +0100
Subject: [PATCH v2 07/15] reloptions.h doesn't need lock.h at all
---
src/backend/access/common/reloptions.c | 1 +
src/backend/access/nbtree/nbtutils.c | 1 +
src/backend/commands/tablespace.c | 1 +
src/backend/utils/cache/relcache.c | 1 +
src/include/access/reloptions.h | 1 -
5 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/backend/access/common/reloptions.c b/src/backend/access/common/reloptions.c
index 237ab8d0ed9..a6002ae9b07 100644
--- a/src/backend/access/common/reloptions.c
+++ b/src/backend/access/common/reloptions.c
@@ -28,6 +28,7 @@
#include "commands/defrem.h"
#include "commands/tablespace.h"
#include "nodes/makefuncs.h"
+#include "storage/lock.h"
#include "utils/array.h"
#include "utils/attoptcache.h"
#include "utils/builtins.h"
diff --git a/src/backend/access/nbtree/nbtutils.c b/src/backend/access/nbtree/nbtutils.c
index 9b091858997..732bc750c9e 100644
--- a/src/backend/access/nbtree/nbtutils.c
+++ b/src/backend/access/nbtree/nbtutils.c
@@ -24,6 +24,7 @@
#include "common/int.h"
#include "lib/qunique.h"
#include "miscadmin.h"
+#include "storage/lwlock.h"
#include "utils/datum.h"
#include "utils/lsyscache.h"
#include "utils/rel.h"
diff --git a/src/backend/commands/tablespace.c b/src/backend/commands/tablespace.c
index ed2a93a09db..d91fcf0facf 100644
--- a/src/backend/commands/tablespace.c
+++ b/src/backend/commands/tablespace.c
@@ -70,6 +70,7 @@
#include "miscadmin.h"
#include "postmaster/bgwriter.h"
#include "storage/fd.h"
+#include "storage/lwlock.h"
#include "storage/procsignal.h"
#include "storage/standby.h"
#include "utils/acl.h"
diff --git a/src/backend/utils/cache/relcache.c b/src/backend/utils/cache/relcache.c
index 3a4f19e8d58..e19f0d3e51c 100644
--- a/src/backend/utils/cache/relcache.c
+++ b/src/backend/utils/cache/relcache.c
@@ -77,6 +77,7 @@
#include "rewrite/rowsecurity.h"
#include "storage/fd.h"
#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "storage/smgr.h"
#include "utils/array.h"
#include "utils/builtins.h"
diff --git a/src/include/access/reloptions.h b/src/include/access/reloptions.h
index 0bd17b30ca7..c228147420a 100644
--- a/src/include/access/reloptions.h
+++ b/src/include/access/reloptions.h
@@ -23,7 +23,6 @@
#include "access/htup.h"
#include "access/tupdesc.h"
#include "nodes/pg_list.h"
-#include "storage/lock.h"
/* types supported by reloptions */
typedef enum relopt_type
--
2.47.3
[text/x-diff] v2-0008-predicate.h-almost-doesn-t-need-lock.h.patch (1.8K, ../../202603221856.iwlhitt6dxxx@alvherre.pgsql/9-v2-0008-predicate.h-almost-doesn-t-need-lock.h.patch)
download | inline diff:
From 7fa919aceab47698cf50e3b3a01ec17199f93a7e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@kurilemu.de>
Date: Sun, 22 Mar 2026 17:59:33 +0100
Subject: [PATCH v2 08/15] predicate.h almost doesn't need lock.h
---
src/backend/access/heap/heapam_handler.c | 1 +
src/backend/access/index/indexam.c | 1 +
src/include/storage/predicate.h | 7 ++++++-
3 files changed, 8 insertions(+), 1 deletion(-)
diff --git a/src/backend/access/heap/heapam_handler.c b/src/backend/access/heap/heapam_handler.c
index 253a735b6c1..d40878928e1 100644
--- a/src/backend/access/heap/heapam_handler.c
+++ b/src/backend/access/heap/heapam_handler.c
@@ -40,6 +40,7 @@
#include "storage/bufmgr.h"
#include "storage/bufpage.h"
#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "storage/predicate.h"
#include "storage/procarray.h"
#include "storage/smgr.h"
diff --git a/src/backend/access/index/indexam.c b/src/backend/access/index/indexam.c
index 5eb7e99ad3e..fbfc33159eb 100644
--- a/src/backend/access/index/indexam.c
+++ b/src/backend/access/index/indexam.c
@@ -53,6 +53,7 @@
#include "nodes/execnodes.h"
#include "pgstat.h"
#include "storage/lmgr.h"
+#include "storage/lock.h"
#include "storage/predicate.h"
#include "utils/ruleutils.h"
#include "utils/snapmgr.h"
diff --git a/src/include/storage/predicate.h b/src/include/storage/predicate.h
index 607d7d84596..a5ac55b8f7e 100644
--- a/src/include/storage/predicate.h
+++ b/src/include/storage/predicate.h
@@ -14,11 +14,16 @@
#ifndef PREDICATE_H
#define PREDICATE_H
+#include "access/transam.h"
#include "storage/itemptr.h"
-#include "storage/lock.h"
#include "utils/relcache.h"
#include "utils/snapshot.h"
+/*
+ * forward references in this file
+ */
+typedef struct VirtualTransactionId VirtualTransactionId;
+
/*
* GUC variables
--
2.47.3
[text/x-diff] v2-0009-twophase.h-can-mostly-do-without-lock.h.patch (881B, ../../202603221856.iwlhitt6dxxx@alvherre.pgsql/10-v2-0009-twophase.h-can-mostly-do-without-lock.h.patch)
download | inline diff:
From 55903301eb5fccb5ce0fd2260a8d123e72c16951 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@kurilemu.de>
Date: Sun, 22 Mar 2026 19:29:21 +0100
Subject: [PATCH v2 09/15] twophase.h can mostly do without lock.h
---
src/include/access/twophase.h | 7 ++++++-
1 file changed, 6 insertions(+), 1 deletion(-)
diff --git a/src/include/access/twophase.h b/src/include/access/twophase.h
index e312514ba87..b762170a041 100644
--- a/src/include/access/twophase.h
+++ b/src/include/access/twophase.h
@@ -17,7 +17,12 @@
#include "access/xact.h"
#include "access/xlogdefs.h"
#include "datatype/timestamp.h"
-#include "storage/lock.h"
+
+/*
+ * forward references in this file
+ */
+typedef struct VirtualTransactionId VirtualTransactionId;
+typedef struct PGPROC PGPROC;
/*
* GlobalTransactionData is defined in twophase.c; other places have no
--
2.47.3
[text/x-diff] v2-0010-sinvaladt.h-doesn-t-need-lock.h-at-all.patch (640B, ../../202603221856.iwlhitt6dxxx@alvherre.pgsql/11-v2-0010-sinvaladt.h-doesn-t-need-lock.h-at-all.patch)
download | inline diff:
From c43b71fdd3c18d804b23bb227d8b559091be010d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@kurilemu.de>
Date: Sun, 22 Mar 2026 19:30:40 +0100
Subject: [PATCH v2 10/15] sinvaladt.h doesn't need lock.h at all
---
src/include/storage/sinvaladt.h | 1 -
1 file changed, 1 deletion(-)
diff --git a/src/include/storage/sinvaladt.h b/src/include/storage/sinvaladt.h
index a1694500a85..122dbcdf19f 100644
--- a/src/include/storage/sinvaladt.h
+++ b/src/include/storage/sinvaladt.h
@@ -22,7 +22,6 @@
#ifndef SINVALADT_H
#define SINVALADT_H
-#include "storage/lock.h"
#include "storage/sinval.h"
/*
--
2.47.3
[text/x-diff] v2-0011-replication-worker_internal.h-doesn-t-need-lock.h.patch (1.4K, ../../202603221856.iwlhitt6dxxx@alvherre.pgsql/12-v2-0011-replication-worker_internal.h-doesn-t-need-lock.h.patch)
download | inline diff:
From be0d5febb3865ce0204ec4fb53f24bde9955bf6f Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@kurilemu.de>
Date: Sun, 22 Mar 2026 19:32:01 +0100
Subject: [PATCH v2 11/15] replication/worker_internal.h doesn't need lock.h at
all
---
src/backend/replication/logical/sequencesync.c | 1 +
src/include/replication/worker_internal.h | 1 -
2 files changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/backend/replication/logical/sequencesync.c b/src/backend/replication/logical/sequencesync.c
index 9c92fddd624..a4fb6783ba9 100644
--- a/src/backend/replication/logical/sequencesync.c
+++ b/src/backend/replication/logical/sequencesync.c
@@ -60,6 +60,7 @@
#include "postmaster/interrupt.h"
#include "replication/logicalworker.h"
#include "replication/worker_internal.h"
+#include "storage/lwlock.h"
#include "utils/acl.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
diff --git a/src/include/replication/worker_internal.h b/src/include/replication/worker_internal.h
index 4ecbdcfadac..745b7d9e969 100644
--- a/src/include/replication/worker_internal.h
+++ b/src/include/replication/worker_internal.h
@@ -20,7 +20,6 @@
#include "replication/walreceiver.h"
#include "storage/buffile.h"
#include "storage/fileset.h"
-#include "storage/lock.h"
#include "storage/shm_mq.h"
#include "storage/shm_toc.h"
#include "storage/spin.h"
--
2.47.3
[text/x-diff] v2-0012-cluster.h-can-do-with-just-lockdefs.h.patch (701B, ../../202603221856.iwlhitt6dxxx@alvherre.pgsql/13-v2-0012-cluster.h-can-do-with-just-lockdefs.h.patch)
download | inline diff:
From 230bfb7ffbb241b561d02ae2055227c640ac2ae3 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@kurilemu.de>
Date: Sun, 22 Mar 2026 19:33:09 +0100
Subject: [PATCH v2 12/15] cluster.h can do with just lockdefs.h
---
src/include/commands/cluster.h | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/include/commands/cluster.h b/src/include/commands/cluster.h
index 28741988478..c79cd2d0e19 100644
--- a/src/include/commands/cluster.h
+++ b/src/include/commands/cluster.h
@@ -15,7 +15,7 @@
#include "nodes/parsenodes.h"
#include "parser/parse_node.h"
-#include "storage/lock.h"
+#include "storage/lockdefs.h"
#include "utils/relcache.h"
--
2.47.3
[text/x-diff] v2-0013-toasting.h-only-needs-lockdefs.h.patch (672B, ../../202603221856.iwlhitt6dxxx@alvherre.pgsql/14-v2-0013-toasting.h-only-needs-lockdefs.h.patch)
download | inline diff:
From e063f8e6cfcef798c9873d1fe7b2a4fd29c0892c Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@kurilemu.de>
Date: Sun, 22 Mar 2026 19:33:48 +0100
Subject: [PATCH v2 13/15] toasting.h only needs lockdefs.h
---
src/include/catalog/toasting.h | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/include/catalog/toasting.h b/src/include/catalog/toasting.h
index 7e0a9f8e9bf..0bc61a8fee9 100644
--- a/src/include/catalog/toasting.h
+++ b/src/include/catalog/toasting.h
@@ -14,7 +14,7 @@
#ifndef TOASTING_H
#define TOASTING_H
-#include "storage/lock.h"
+#include "storage/lockdefs.h"
/*
* toasting.c prototypes
--
2.47.3
[text/x-diff] v2-0014-pg_inherits.h-doesn-t-need-the-full-lock.h.patch (1.6K, ../../202603221856.iwlhitt6dxxx@alvherre.pgsql/15-v2-0014-pg_inherits.h-doesn-t-need-the-full-lock.h.patch)
download | inline diff:
From a081c57217d6eb3beb418debc31bd016132da4a8 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@kurilemu.de>
Date: Sun, 22 Mar 2026 19:39:18 +0100
Subject: [PATCH v2 14/15] pg_inherits.h doesn't need the full lock.h
---
src/backend/catalog/pg_inherits.c | 1 +
src/backend/optimizer/util/inherit.c | 2 ++
src/include/catalog/pg_inherits.h | 2 +-
3 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/backend/catalog/pg_inherits.c b/src/backend/catalog/pg_inherits.c
index cef80633157..4b9802aafcc 100644
--- a/src/backend/catalog/pg_inherits.c
+++ b/src/backend/catalog/pg_inherits.c
@@ -28,6 +28,7 @@
#include "storage/lmgr.h"
#include "utils/builtins.h"
#include "utils/fmgroids.h"
+#include "utils/hsearch.h"
#include "utils/snapmgr.h"
#include "utils/syscache.h"
diff --git a/src/backend/optimizer/util/inherit.c b/src/backend/optimizer/util/inherit.c
index 8d623bf136d..6a7b9edff3f 100644
--- a/src/backend/optimizer/util/inherit.c
+++ b/src/backend/optimizer/util/inherit.c
@@ -14,6 +14,8 @@
*/
#include "postgres.h"
+#include <limits.h>
+
#include "access/sysattr.h"
#include "access/table.h"
#include "catalog/partition.h"
diff --git a/src/include/catalog/pg_inherits.h b/src/include/catalog/pg_inherits.h
index 7a36241d7b3..cc874abaabb 100644
--- a/src/include/catalog/pg_inherits.h
+++ b/src/include/catalog/pg_inherits.h
@@ -22,7 +22,7 @@
#include "catalog/pg_inherits_d.h" /* IWYU pragma: export */
#include "nodes/pg_list.h"
-#include "storage/lock.h"
+#include "storage/lockdefs.h"
/* ----------------
* pg_inherits definition. cpp turns this into
--
2.47.3
[text/x-diff] v2-0015-avoid-including-clog.h-in-proc.h.patch (1.4K, ../../202603221856.iwlhitt6dxxx@alvherre.pgsql/16-v2-0015-avoid-including-clog.h-in-proc.h.patch)
download | inline diff:
From e7f84cac2e77a4aa8d1c9b878aabf09e754494ea Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=C3=81lvaro=20Herrera?= <alvherre@kurilemu.de>
Date: Sun, 22 Mar 2026 19:42:23 +0100
Subject: [PATCH v2 15/15] avoid including clog.h in proc.h
---
src/backend/storage/lmgr/proc.c | 1 +
src/include/storage/proc.h | 4 +++-
2 files changed, 4 insertions(+), 1 deletion(-)
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 8f5ce0e2a8a..1d389cf60ec 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -33,6 +33,7 @@
#include <unistd.h>
#include <sys/time.h>
+#include "access/clog.h"
#include "access/transam.h"
#include "access/twophase.h"
#include "access/xlogutils.h"
diff --git a/src/include/storage/proc.h b/src/include/storage/proc.h
index bf3094f0f7d..1dad125706e 100644
--- a/src/include/storage/proc.h
+++ b/src/include/storage/proc.h
@@ -14,7 +14,6 @@
#ifndef _PROC_H_
#define _PROC_H_
-#include "access/clog.h"
#include "access/xlogdefs.h"
#include "lib/ilist.h"
#include "miscadmin.h"
@@ -24,6 +23,9 @@
#include "storage/proclist_types.h"
#include "storage/procnumber.h"
+/* Avoid including clog.h here */
+typedef int XidStatus;
+
/*
* Each backend advertises up to PGPROC_MAX_CACHED_SUBXIDS TransactionIds
* for non-aborted subtransactions of its current top transaction. These
--
2.47.3
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-03-23 08:18 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Álvaro Herrera <alvherre@kurilemu.de>
0 siblings, 2 replies; 61+ messages in thread
From: Bertrand Drouvot @ 2026-03-23 08:18 UTC (permalink / raw)
To: Álvaro Herrera <alvherre@kurilemu.de>; +Cc: Michael Paquier <michael@paquier.xyz>; Andres Freund <andres@anarazel.de>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; L. pgsql-hackers <pgsql-hackers@lists.postgresql.org>
Hi,
On Sun, Mar 22, 2026 at 08:37:47PM +0100, Álvaro Herrera wrote:
> On 2026-Mar-21, Álvaro Herrera wrote:
>
> > I checked this, and found a couple of headers that can benefit from a
> > removal, as shown in the attached patches.
>
> I looked again and found some more; the first 14 patches attached here
> do so. A couple of them have no fallout to speak of, which I find
> really surprising; for the majority we just need a couple of extra
> includes somewhere or a typedef or two. I unleashed CI on it to see
> what would happen,
> https://cirrus-ci.com/build/5522804717649920
Thanks for looking at it!
I looked at the C files changes that now need to include lock.h (or other ones)
directly:
verify_heapam.c: lock.h was indirectly included through procarray.h
heapam_handler.c: lock.h was indirectly included through heapam.h -> tableam.h
-> vacuum.h
relation.c: lock.h was indirectly included through namespace.h
reloptions.c: lock.h was indirectly included through reloptions.h
indexam.c: lock.h was indirectly included through reloptions.h
relcache.c: lock.h was indirectly included through reloptions.h
syscache.c: lock.h was indirectly included through lmgr.h
discard.c: lock.h was indirectly included through namespace.h
pg_subscription.c: lock.h was indirectly included through heapam.h -> tableam.h
-> vacuum.h
nbtree.c: lock.h was indirectly included through vacuum.h
nbtutils.c: lock.h was indirectly included through reloptions.h
pg_inherits.c: lock.h was indirectly included through pg_inherits.h
inherit.c: lock.h was indirectly included through pg_inherits.h
conversioncmds.c: lock.h was indirectly included parse_func.h -> namespace.h
tablespace.c: lock.h was indirectly included through heapam.h -> tableam.h
-> vacuum.h
parse_oper.c: lock.h was indirectly included through parse_func.h -> namespace.h
sequencesync.c: lock.h was indirectly included through worker_internal.h
ts_cache.c: lock.h was indirectly included through namespace.h
So the changes done in your patches make sense to me.
I have 2 comments:
1/ wait_event.c
-#include "storage/lmgr.h" /* for GetLockNameFromTagType */
-#include "storage/lwlock.h" /* for GetLWLockIdentifier */
+#include "storage/lmgr.h"
+#include "storage/lwlock.h"
+#include "storage/shmem.h"
#include "storage/spin.h"
+#include "utils/hsearch.h"
hsearch.h is already included into shmem.h so its direct include is not needed.
That said wait_event.c needs it so including it directly might make sense just from
a coding "style" point of view (given that it is harmless as it is protected by
ifndef HSEARCH_H).
2/ Not directly linked to your patches
It looks like that aio_funcs.c does not need lock.h (reported by include-what-you-use).
If we remove its direct include, it's still indirectly included through proc.h
though. But I think that removing its direct include makes sense as it's not
needed at all.
If we still need to discuss about those lock.h related changes, maybe it's
worth creating a dedicated thread?
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-03-24 03:15 Michael Paquier <michael@paquier.xyz>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
1 sibling, 0 replies; 61+ messages in thread
From: Michael Paquier @ 2026-03-24 03:15 UTC (permalink / raw)
To: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: Álvaro Herrera <alvherre@kurilemu.de>; Andres Freund <andres@anarazel.de>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; L. pgsql-hackers <pgsql-hackers@lists.postgresql.org>
On Mon, Mar 23, 2026 at 08:18:05AM +0000, Bertrand Drouvot wrote:
> It looks like that aio_funcs.c does not need lock.h (reported by include-what-you-use).
> If we remove its direct include, it's still indirectly included through proc.h
> though. But I think that removing its direct include makes sense as it's not
> needed at all.
>
> If we still need to discuss about those lock.h related changes, maybe it's
> worth creating a dedicated thread?
This patch set is related to an entirely different topic than what we
are discussing, so yes, a different thread would be adapted.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (832B, ../../acIBuaf0SXRkbPfx@paquier.xyz/2-signature.asc)
download
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-03-24 07:02 Michael Paquier <michael@paquier.xyz>
parent: Michael Paquier <michael@paquier.xyz>
1 sibling, 2 replies; 61+ messages in thread
From: Michael Paquier @ 2026-03-24 07:02 UTC (permalink / raw)
To: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: Andres Freund <andres@anarazel.de>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
On Sat, Mar 21, 2026 at 02:55:33PM +0900, Michael Paquier wrote:
> On Thu, Mar 19, 2026 at 12:25:41PM +0000, Bertrand Drouvot wrote:
>> I did not check if there are any other files that could benefit of using
>> locktag.h instead of lock.h but that's something I'll do and open a dedicated
>> if any (once locktag.h is in the tree).
>
> I have checked after that, and did not spot an area (except your patch
> of course). And applied this part.
The main patch has some churn in proc.c and lock.c, moving some code
blocks while the main focus of the patch is to add the two pgstat()
calls to report some data, so I have moved this part into its own
commit, and applied it. One thing to not in lock.c: we will calculate
the time difference of the wait even if log_lock_waits is disabled,
without the lock stats part. As this GUC is enabled by default, it
does not matter much in practice, I guess, and it matters even less
with the main patch merged.
The implementation of the main patch is close enough to pgstat_io.c
that your logic is a no-brainer (timestamp protected by the first
LWLock, each field incremented depending on locktags, etc.). Instead
of a boolean flag tracking if some stats have been set, we could also
have used an approach like the checkpointer stats with is_all_zeros on
the pending data, perhaps? At the end, with two incrementation
routines, I've let the code as-is.
Another thing I am not completely sure is if the sleep time of the
isolation tests is long enough. I have tested things with
CLOBBER_CACHE_ALWAYS to make the setup more sensitive to timings but
could not get it to fail. We'll know soon enough if the buildfarm
complains.
After a few more tweaks here and there (code, comments, some
beautification), done.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (832B, ../../acI3H5aucJNS2w78@paquier.xyz/2-signature.asc)
download
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-03-24 16:16 Álvaro Herrera <alvherre@kurilemu.de>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
1 sibling, 1 reply; 61+ messages in thread
From: Álvaro Herrera @ 2026-03-24 16:16 UTC (permalink / raw)
To: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: Michael Paquier <michael@paquier.xyz>; Andres Freund <andres@anarazel.de>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; L. pgsql-hackers <pgsql-hackers@lists.postgresql.org>
Hello,
On 2026-Mar-23, Bertrand Drouvot wrote:
> So the changes done in your patches make sense to me.
>
> I have 2 comments:
>
> 1/ wait_event.c
>
> -#include "storage/lmgr.h" /* for GetLockNameFromTagType */
> -#include "storage/lwlock.h" /* for GetLWLockIdentifier */
> +#include "storage/lmgr.h"
> +#include "storage/lwlock.h"
> +#include "storage/shmem.h"
> #include "storage/spin.h"
> +#include "utils/hsearch.h"
>
> hsearch.h is already included into shmem.h so its direct include is not needed.
> That said wait_event.c needs it so including it directly might make sense just from
> a coding "style" point of view (given that it is harmless as it is protected by
> ifndef HSEARCH_H).
Hmm, right -- I don't need to add both. Done that way. I also made
some alphabetical sorts and pushed this part.
> 2/ Not directly linked to your patches
>
> It looks like that aio_funcs.c does not need lock.h (reported by include-what-you-use).
> If we remove its direct include, it's still indirectly included through proc.h
> though. But I think that removing its direct include makes sense as it's not
> needed at all.
I'm not opposed to somebody else making this change, if they want, but I
think there's little practical impact.
--
Álvaro Herrera Breisgau, Deutschland — https://www.EnterpriseDB.com/
"En las profundidades de nuestro inconsciente hay una obsesiva necesidad
de un universo lógico y coherente. Pero el universo real se halla siempre
un paso más allá de la lógica" (Irulan)
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-03-24 20:09 Andres Freund <andres@anarazel.de>
parent: Michael Paquier <michael@paquier.xyz>
1 sibling, 1 reply; 61+ messages in thread
From: Andres Freund @ 2026-03-24 20:09 UTC (permalink / raw)
To: Michael Paquier <michael@paquier.xyz>; +Cc: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi,
On 2026-03-24 16:02:55 +0900, Michael Paquier wrote:
> Another thing I am not completely sure is if the sleep time of the
> isolation tests is long enough. I have tested things with
> CLOBBER_CACHE_ALWAYS to make the setup more sensitive to timings but
> could not get it to fail. We'll know soon enough if the buildfarm
> complains.
>
> After a few more tweaks here and there (code, comments, some
> beautification), done.
The test is extremely unstable on windows. On CI 10/16 runs since the test in
failed due to it, afaict.
I don't see how a test with a timeout setting that's anywhere remotely close
to 10ms could be expected to be stable.
Also, anything that requires short sleeps (like pg_sleep(0.05);) is extremely
likely to be a long time test stability hazard. It's a huge "test smell" to
me, to the point that I think every single sleep in a test needs a comment
explaining why that one use of sleep is correct, and that comment better be
signed in blood.
Greetings,
Andres Freund
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-03-24 23:46 Michael Paquier <michael@paquier.xyz>
parent: Andres Freund <andres@anarazel.de>
0 siblings, 1 reply; 61+ messages in thread
From: Michael Paquier @ 2026-03-24 23:46 UTC (permalink / raw)
To: Andres Freund <andres@anarazel.de>; +Cc: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
On Tue, Mar 24, 2026 at 04:09:37PM -0400, Andres Freund wrote:
> The test is extremely unstable on windows. On CI 10/16 runs since the test in
> failed due to it, afaict.
I am not surprised by that. Windows is good in catching race
conditions.
> I don't see how a test with a timeout setting that's anywhere remotely close
> to 10ms could be expected to be stable.
Well, the low value of deadlock_timeout is not the problem. The
shorter the better to make the test go faster with more deadlock
checks. What the buildfarm is telling is that we do not have the time
to process the deadlock_timeout request, and that we would need to pay
the cost of longer sleep if coded this way. This is going to cost in
runtime on fast machines where the CheckDeadLock() call would happen
quickly. And on slow machines, we don't have the guarantee that the
sleep would be long enough to process the deadlock request.
This test would be better if rewritten as a TAP test, I guess, with a
NOTICE injection point before the CheckDeadLock() call in ProcSleep()
to make sure that the second session processes the deadlock timeout
request while it is waiting on its lock to be acquired. One trickier
part is that we only care about the deadlock_timeout in s2, because we
want to measure the wait it has waited until the lock could be
acquired, meaning that we should make s1 use a large deadlock_timeout
to avoid interferences with a global injpoint.
I don't have the credits to test that in the CI for this month,
unfortunately, and this creates noise in the CI for the work of other
folks in this release cycle, so I am going to remove this test for
now.
> Also, anything that requires short sleeps (like pg_sleep(0.05);) is extremely
> likely to be a long time test stability hazard. It's a huge "test smell" to
> me, to the point that I think every single sleep in a test needs a comment
> explaining why that one use of sleep is correct, and that comment better be
> signed in blood.
Yep, agreed.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (832B, ../../acMiT69EEGKHWGM5@paquier.xyz/2-signature.asc)
download
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-03-25 03:15 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Michael Paquier <michael@paquier.xyz>
0 siblings, 1 reply; 61+ messages in thread
From: Bertrand Drouvot @ 2026-03-25 03:15 UTC (permalink / raw)
To: Michael Paquier <michael@paquier.xyz>; +Cc: Andres Freund <andres@anarazel.de>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi,
On Wed, Mar 25, 2026 at 08:46:23AM +0900, Michael Paquier wrote:
> On Tue, Mar 24, 2026 at 04:09:37PM -0400, Andres Freund wrote:
> > The test is extremely unstable on windows. On CI 10/16 runs since the test in
> > failed due to it, afaict.
Thanks for the warning!
> I am not surprised by that. Windows is good in catching race
> conditions.
>
> > I don't see how a test with a timeout setting that's anywhere remotely close
> > to 10ms could be expected to be stable.
>
> Well, the low value of deadlock_timeout is not the problem.
Yeah, there are other tests making use of a 10ms deadlock timeout.
> This test would be better if rewritten as a TAP test, I guess, with a
> NOTICE injection point before the CheckDeadLock() call in ProcSleep()
> to make sure that the second session processes the deadlock timeout
> request while it is waiting on its lock to be acquired. One trickier
> part is that we only care about the deadlock_timeout in s2, because we
> want to measure the wait it has waited until the lock could be
> acquired, meaning that we should make s1 use a large deadlock_timeout
> to avoid interferences with a global injpoint.
>
> I don't have the credits to test that in the CI for this month,
> unfortunately, and this creates noise in the CI for the work of other
> folks in this release cycle, so I am going to remove this test for
> now.
Thanks for the test removal. I created an open item for v19 so that we
don't forget to re-add a new version of the tests. I'll work on it.
[1]: https://wiki.postgresql.org/wiki/PostgreSQL_19_Open_Items
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-03-25 03:32 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Álvaro Herrera <alvherre@kurilemu.de>
0 siblings, 0 replies; 61+ messages in thread
From: Bertrand Drouvot @ 2026-03-25 03:32 UTC (permalink / raw)
To: Álvaro Herrera <alvherre@kurilemu.de>; +Cc: Michael Paquier <michael@paquier.xyz>; Andres Freund <andres@anarazel.de>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; L. pgsql-hackers <pgsql-hackers@lists.postgresql.org>
Hi,
On Tue, Mar 24, 2026 at 05:16:11PM +0100, Álvaro Herrera wrote:
> Hello,
>
> On 2026-Mar-23, Bertrand Drouvot wrote:
>
> > 2/ Not directly linked to your patches
> >
> > It looks like that aio_funcs.c does not need lock.h (reported by include-what-you-use).
> > If we remove its direct include, it's still indirectly included through proc.h
> > though. But I think that removing its direct include makes sense as it's not
> > needed at all.
>
> I'm not opposed to somebody else making this change, if they want, but I
> think there's little practical impact.
yeah, not sure it deserves its own commit... I was just mentioning here in case
you want to change that in passing while working on lock.h includes.
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-03-25 05:25 Michael Paquier <michael@paquier.xyz>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
0 siblings, 2 replies; 61+ messages in thread
From: Michael Paquier @ 2026-03-25 05:25 UTC (permalink / raw)
To: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: Andres Freund <andres@anarazel.de>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
On Wed, Mar 25, 2026 at 03:15:19AM +0000, Bertrand Drouvot wrote:
> Thanks for the test removal. I created an open item for v19 so that we
> don't forget to re-add a new version of the tests. I'll work on it.
>
> [1]: https://wiki.postgresql.org/wiki/PostgreSQL_19_Open_Items
Thanks for adding that. We are most likely going to need the help of
the CI here. The buildfarm has been fortunately (unfortunately?)
stable on this one, and that would make for less noise overall.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (832B, ../../acNxrWn2bvDARnC3@paquier.xyz/2-signature.asc)
download
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-03-25 07:24 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Michael Paquier <michael@paquier.xyz>
1 sibling, 0 replies; 61+ messages in thread
From: Bertrand Drouvot @ 2026-03-25 07:24 UTC (permalink / raw)
To: Michael Paquier <michael@paquier.xyz>; +Cc: Andres Freund <andres@anarazel.de>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi,
On Wed, Mar 25, 2026 at 02:25:01PM +0900, Michael Paquier wrote:
> On Wed, Mar 25, 2026 at 03:15:19AM +0000, Bertrand Drouvot wrote:
> > Thanks for the test removal. I created an open item for v19 so that we
> > don't forget to re-add a new version of the tests. I'll work on it.
> >
> > [1]: https://wiki.postgresql.org/wiki/PostgreSQL_19_Open_Items
>
> Thanks for adding that. We are most likely going to need the help of
> the CI here.
Yeah. FWIW I always use the CI before submiting patches. This one was
green and did not catch the failure:
https://github.com/bdrouvot/postgres/runs/67730691634
Would probably had to be launched multiple times to catch it (as Andres mentioned
that 10/16 runs failed).
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-03-25 19:54 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Michael Paquier <michael@paquier.xyz>
1 sibling, 1 reply; 61+ messages in thread
From: Bertrand Drouvot @ 2026-03-25 19:54 UTC (permalink / raw)
To: Michael Paquier <michael@paquier.xyz>; +Cc: Andres Freund <andres@anarazel.de>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi,
On Wed, Mar 25, 2026 at 02:25:01PM +0900, Michael Paquier wrote:
> On Wed, Mar 25, 2026 at 03:15:19AM +0000, Bertrand Drouvot wrote:
> > Thanks for the test removal. I created an open item for v19 so that we
> > don't forget to re-add a new version of the tests. I'll work on it.
> >
> > [1]: https://wiki.postgresql.org/wiki/PostgreSQL_19_Open_Items
>
> Thanks for adding that. We are most likely going to need the help of
> the CI here. The buildfarm has been fortunately (unfortunately?)
> stable on this one, and that would make for less noise overall.
PFA a new version of the tests using an injection point. This new injection
point is created in ProcSleep() when we know that the deadlock timeout fired.
The patch adds a new query_until_stderr() subroutine in BackgroundPsql.pm:
It does the same as query_until() except that it is waiting for a desired
stderr (and not stdout). Thanks to it the session can wait until it gets the
injection point notice.
I think that looks clearer that way than using the logfile to look for the
notice (should log_min_messages be set to an appropriate value).
If you like the idea, maybe we could introduce query_until_stderr() in a separate
commit. If you don't, then we could switch to looking in the server logfile
instead of the session stderr.
Then the new tests follow this workflow:
- session 1 holds a lock
- session 2 attaches to the new injection point with the notice action
- session 2 is blocked by session 1 and waits until the injection point notice
is received
- session 1 releases its lock, session 2 commits
- pg_stat_lock is polled until we get the counters for the lock type or die
with a timeout
That way there is no sleep at all. Once we know that session 2 has waited longer
than the deadlock timeout (thanks to the new injection point notice) then we
can release the locks and poll pg_stat_lock to get the updated stats.
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
Attachments:
[text/x-diff] v1-0001-Add-new-tests-for-lock-stats.patch (9.3K, ../../acQ9gnE84897xHfQ@ip-10-97-1-34.eu-west-3.compute.internal/2-v1-0001-Add-new-tests-for-lock-stats.patch)
download | inline diff:
From b25a8d4201e20ec457685502420db22805c584ac Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Wed, 25 Mar 2026 09:28:39 +0000
Subject: [PATCH v1] Add new tests for lock stats
commit 7c64d56fd97 removed isolation test lock-stats because it was unstable in
the CI for Windows, at least. This commit creates a new test for the lock stats
using an injection point. This new injection point is created in ProcSleep() when
we know that the deadlock timeout fired.
This commit adds a new query_until_stderr() subroutine in BackgroundPsql.pm.
It does the same as query_until() except that it is waiting for a desired
stderr (and not stdout). Thanks to it the session can wait until it gets the
injection point notice.
Then the new tests follow this workflow:
- session 1 holds a lock
- session 2 attaches to the new injection point with the notice action
- session 2 is blocked by session 1 and waits until the injection point notice
is received
- session 1 releases its lock, session 2 commits
- pg_stat_lock is polled until we get the counters for the lock type or die
with a timeout
That way there is no sleep at all. Once we know that session 2 has waited longer
than the deadlock timeout (thanks to the new injection point notice) then we
can poll pg_stat_lock to get the updated stats.
Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Discussion: https://postgr.es/m/acNTR1lLHwQJ0o%2BP%40ip-10-97-1-34.eu-west-3.compute.internal
---
src/backend/storage/lmgr/proc.c | 2 +
src/test/modules/test_misc/meson.build | 1 +
.../modules/test_misc/t/011_lock_stats.pl | 205 ++++++++++++++++++
.../perl/PostgreSQL/Test/BackgroundPsql.pm | 33 +++
4 files changed, 241 insertions(+)
82.8% src/test/modules/test_misc/t/
15.0% src/test/perl/PostgreSQL/Test/
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 5c47cf13473..b857a10354f 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -52,6 +52,7 @@
#include "storage/procsignal.h"
#include "storage/spin.h"
#include "storage/standby.h"
+#include "utils/injection_point.h"
#include "utils/timeout.h"
#include "utils/timestamp.h"
#include "utils/wait_event.h"
@@ -1560,6 +1561,7 @@ ProcSleep(LOCALLOCK *locallock)
int usecs;
long msecs;
+ INJECTION_POINT("deadlock-timeout-fired", NULL);
TimestampDifference(get_timeout_start_time(DEADLOCK_TIMEOUT),
GetCurrentTimestamp(),
&secs, &usecs);
diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build
index 6e8db1621a7..1b25d98f7f3 100644
--- a/src/test/modules/test_misc/meson.build
+++ b/src/test/modules/test_misc/meson.build
@@ -19,6 +19,7 @@ tests += {
't/008_replslot_single_user.pl',
't/009_log_temp_files.pl',
't/010_index_concurrently_upsert.pl',
+ 't/011_lock_stats.pl',
],
# The injection points are cluster-wide, so disable installcheck
'runningcheck': false,
diff --git a/src/test/modules/test_misc/t/011_lock_stats.pl b/src/test/modules/test_misc/t/011_lock_stats.pl
new file mode 100644
index 00000000000..c693a2ed268
--- /dev/null
+++ b/src/test/modules/test_misc/t/011_lock_stats.pl
@@ -0,0 +1,205 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# 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 tests each test a
+# dedicated lock type.
+# The last one checks that log_lock_waits has no impact on the statistics
+# counters.
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+plan skip_all => 'Injection points not supported by this build'
+ unless $ENV{enable_injection_points} eq 'yes';
+
+my $deadlock_timeout = 10;
+my $s1;
+my $s2;
+my $node;
+
+# Setup the 2 sessions
+sub setup_sessions
+{
+ $s1 = $node->background_psql('postgres');
+ $s2 = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Setup injection points for the waiting session
+ $s2->query_safe(q[
+ SELECT injection_points_set_local();
+ SELECT injection_points_attach('deadlock-timeout-fired', 'notice');
+ ]);
+}
+
+# Fetch waits and wait_time from pg_stat_lock for a given lock type
+# until they reached expected values: at least one wait and waiting longer
+# than the deadlock_timeout.
+sub wait_for_pg_stat_lock
+{
+ my ($node, $lock_type) = @_;
+
+ $node->poll_query_until(
+ 'postgres', qq[
+ SELECT waits > 0 AND wait_time >= $deadlock_timeout
+ FROM pg_stat_lock
+ WHERE locktype = '$lock_type';
+ ]) or die "Timed out waiting for pg_stat_lock for $lock_type"
+}
+
+# Node initialization
+$node = PostgreSQL::Test::Cluster->new('node');
+$node->init();
+$node->append_conf('postgresql.conf', "deadlock_timeout = ${deadlock_timeout}ms");
+$node->start();
+
+# Check if the extension injection_points is available
+plan skip_all => 'Extension injection_points not installed'
+ unless $node->check_extension('injection_points');
+
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+
+$node->safe_psql(
+ 'postgres', q[
+CREATE TABLE test_stat_tab(key text not null, value int);
+INSERT INTO test_stat_tab(key, value) VALUES('k0', 1);
+]);
+
+############################################################################
+
+####### Relation lock
+
+setup_sessions();
+
+$s1->query_safe(
+ q[
+SELECT pg_stat_reset_shared('lock');
+BEGIN;
+LOCK TABLE test_stat_tab;
+]);
+
+# s2 blocks
+$s2->query_until_stderr(qr/deadlock-timeout-fired/, q(
+BEGIN;
+SELECT pg_stat_force_next_flush();
+LOCK TABLE test_stat_tab;
+));
+
+# deadlock_timeout fired, now commit in s1 and s2
+$s1->query_safe(q(COMMIT));
+$s2->query(q(COMMIT));
+
+# check that pg_stat_lock has been updated
+wait_for_pg_stat_lock($node, 'relation');
+ok(1, "Lock stats ok for relation");
+
+# close sessions
+$s1->quit;
+$s2->quit;
+
+####### transaction lock
+
+setup_sessions();
+
+$s1->query_safe(
+ q[
+SELECT pg_stat_reset_shared('lock');
+INSERT INTO test_stat_tab(key, value) VALUES('k1', 1), ('k2', 1), ('k3', 1);
+BEGIN;
+UPDATE test_stat_tab SET value = value + 1 WHERE key = 'k1';
+]);
+
+# s2 blocks
+$s2->query_until_stderr(qr/deadlock-timeout-fired/, q(
+SET log_lock_waits = on;
+BEGIN;
+SELECT pg_stat_force_next_flush();
+UPDATE test_stat_tab SET value = value + 1 WHERE key = 'k1';
+));
+
+# deadlock_timeout fired, now commit in s1 and s2
+$s1->query_safe(q(COMMIT));
+$s2->query(q(COMMIT));
+
+# check that pg_stat_lock has been updated
+wait_for_pg_stat_lock($node, 'transactionid');
+ok(1, "Lock stats ok for transactionid");
+
+# Close sessions
+$s1->quit;
+$s2->quit;
+
+####### advisory lock
+
+setup_sessions();
+
+$s1->query_safe(
+ q[
+SELECT pg_stat_reset_shared('lock');
+SELECT pg_advisory_lock(1);
+]);
+
+# s2 blocks
+$s2->query_until_stderr(qr/deadlock-timeout-fired/, q(
+SET log_lock_waits = on;
+BEGIN;
+SELECT pg_stat_force_next_flush();
+SELECT pg_advisory_lock(1);
+));
+
+# deadlock_timeout fired, now unlock and commit s2
+$s1->query_safe(q(SELECT pg_advisory_unlock(1)));
+$s2->query(
+ q[
+SELECT pg_advisory_unlock(1);
+COMMIT;
+]);
+
+# check that pg_stat_lock has been updated
+wait_for_pg_stat_lock($node, 'advisory');
+ok(1, "Lock stats ok for advisory");
+
+# Close sessions
+$s1->quit;
+$s2->quit;
+
+####### Ensure log_lock_waits has no impact
+
+setup_sessions();
+
+$s1->query_safe(
+ q[
+SELECT pg_stat_reset_shared('lock');
+BEGIN;
+LOCK TABLE test_stat_tab;
+]);
+
+# s2 blocks
+$s2->query_until_stderr(qr/deadlock-timeout-fired/, q(
+SET log_lock_waits = off;
+BEGIN;
+SELECT pg_stat_force_next_flush();
+LOCK TABLE test_stat_tab;
+));
+
+# deadlock_timeout fired, now commit in s1 and s2
+$s1->query_safe(q(COMMIT));
+$s2->query(q(COMMIT));
+
+# check that pg_stat_lock has been updated
+wait_for_pg_stat_lock($node, 'relation');
+ok(1, "log_lock_waits has no impact on Lock stats");
+
+# close sessions
+$s1->quit;
+$s2->quit;
+
+# cleanup
+$node->safe_psql('postgres', q[DROP TABLE test_stat_tab;]);
+
+done_testing();
diff --git a/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm b/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
index c6ff2dbde4c..2c8dc3f66b7 100644
--- a/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
+++ b/src/test/perl/PostgreSQL/Test/BackgroundPsql.pm
@@ -357,6 +357,39 @@ sub query_until
=pod
+=item $session->query_until_stderr(until, query)
+
+Issue C<query> and wait for C<until> appearing in stderr rather than stdout.
+C<query> needs to end with newline and semicolon (if applicable, interactive
+psql input may not require it) for psql to process the input. This can be useful
+when using injection points with a notice action.
+
+=cut
+
+sub query_until_stderr
+{
+ my ($self, $until, $query) = @_;
+ my $ret;
+ local $Test::Builder::Level = $Test::Builder::Level + 1;
+
+ $self->{timeout}->start() if (defined($self->{query_timer_restart}));
+ $self->{stdin} .= $query;
+
+ pump_until($self->{run}, $self->{timeout}, \$self->{stderr}, $until);
+
+ die "psql query timed out" if $self->{timeout}->is_expired;
+
+ $ret = $self->{stdout};
+
+ # clear out output and stderr for the next query
+ $self->{stdout} = '';
+ $self->{stderr} = '';
+
+ return $ret;
+}
+
+=pod
+
=item $session->set_query_timer_restart()
Configures the timer to be restarted before each query such that the defined
--
2.34.1
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-03-25 23:06 Michael Paquier <michael@paquier.xyz>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
0 siblings, 1 reply; 61+ messages in thread
From: Michael Paquier @ 2026-03-25 23:06 UTC (permalink / raw)
To: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: Andres Freund <andres@anarazel.de>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
On Wed, Mar 25, 2026 at 07:54:42PM +0000, Bertrand Drouvot wrote:
> If you like the idea, maybe we could introduce query_until_stderr() in a separate
> commit. If you don't, then we could switch to looking in the server logfile
> instead of the session stderr.
I like the patch, but I happen to not like my initial idea of relying
on a NOTICE in an injection point combined with your new API in
BackgroundPsql because we can already achieve the same with a wait
injection point and use BackgroundPsql::query_until() with an \echo to
detect that the command is blocked.
The updated version attached uses this method (edited quickly your
commit message). Like your patch there is no need for hardcoded
sleeps and the CI's first impressions are actually good, but I am
going to need more runs to gain more confidence. Note I should be
able to do something here in 10 days or so. If you could confirm the
stability on your side for the time being with more runs, that would
help, of course.
--
Michael
From fd56d27596834eb019460bffb038aebfa0fa94a4 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 26 Mar 2026 08:03:31 +0900
Subject: [PATCH v2] Add tests for lock stats, take two
commit 7c64d56fd97 removed isolation test lock-stats because it was
unstable in the CI for Windows, at least. This commit creates a new
test for the lock stats using an injection point. This new injection
point is created in ProcSleep() when we know that the deadlock timeout
fired.
Then the new tests follow this workflow:
- session 1 holds a lock
- session 2 attaches to the new injection point with the wait action.
- session 2 is blocked by session 1 and waits until the injection point
wait is reached.
- session 1 releases its lock, session 2 commits
- pg_stat_lock is polled until we get the counters for the lock type or
die with a timeout.
That way there is no sleep at all. Once we know that session 2 has
waited longer the deadlock_timeout (thanks to the new injection point
wait) then we can poll pg_stat_lock to get the updated stats.
Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Discussion: https://postgr.es/m/acNTR1lLHwQJ0o%2BP%40ip-10-97-1-34.eu-west-3.compute.internal
---
src/backend/storage/lmgr/proc.c | 2 +
src/test/modules/test_misc/meson.build | 1 +
.../modules/test_misc/t/011_lock_stats.pl | 249 ++++++++++++++++++
3 files changed, 252 insertions(+)
create mode 100644 src/test/modules/test_misc/t/011_lock_stats.pl
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 5c47cf13473e..b857a10354f7 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -52,6 +52,7 @@
#include "storage/procsignal.h"
#include "storage/spin.h"
#include "storage/standby.h"
+#include "utils/injection_point.h"
#include "utils/timeout.h"
#include "utils/timestamp.h"
#include "utils/wait_event.h"
@@ -1560,6 +1561,7 @@ ProcSleep(LOCALLOCK *locallock)
int usecs;
long msecs;
+ INJECTION_POINT("deadlock-timeout-fired", NULL);
TimestampDifference(get_timeout_start_time(DEADLOCK_TIMEOUT),
GetCurrentTimestamp(),
&secs, &usecs);
diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build
index 6e8db1621a74..1b25d98f7f33 100644
--- a/src/test/modules/test_misc/meson.build
+++ b/src/test/modules/test_misc/meson.build
@@ -19,6 +19,7 @@ tests += {
't/008_replslot_single_user.pl',
't/009_log_temp_files.pl',
't/010_index_concurrently_upsert.pl',
+ 't/011_lock_stats.pl',
],
# The injection points are cluster-wide, so disable installcheck
'runningcheck': false,
diff --git a/src/test/modules/test_misc/t/011_lock_stats.pl b/src/test/modules/test_misc/t/011_lock_stats.pl
new file mode 100644
index 000000000000..4281e842c981
--- /dev/null
+++ b/src/test/modules/test_misc/t/011_lock_stats.pl
@@ -0,0 +1,249 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# 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 tests each test a
+# dedicated lock type.
+# The last one checks that log_lock_waits has no impact on the statistics
+# counters.
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+plan skip_all => 'Injection points not supported by this build'
+ unless $ENV{enable_injection_points} eq 'yes';
+
+my $deadlock_timeout = 10;
+my $s1;
+my $s2;
+my $node;
+
+# Setup the 2 sessions
+sub setup_sessions
+{
+ $s1 = $node->background_psql('postgres');
+ $s2 = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Setup injection points for the waiting session
+ $s2->query_safe(q[
+ SELECT injection_points_set_local();
+ SELECT injection_points_attach('deadlock-timeout-fired', 'wait');
+ ]);
+}
+
+# Fetch waits and wait_time from pg_stat_lock for a given lock type
+# until they reached expected values: at least one wait and waiting longer
+# than the deadlock_timeout.
+sub wait_for_pg_stat_lock
+{
+ my ($node, $lock_type) = @_;
+
+ $node->poll_query_until(
+ 'postgres', qq[
+ SELECT waits > 0 AND wait_time >= $deadlock_timeout
+ FROM pg_stat_lock
+ WHERE locktype = '$lock_type';
+ ]) or die "Timed out waiting for pg_stat_lock for $lock_type"
+}
+
+# Convenience wrapper to wait for a point, then detach it.
+sub wait_and_detach
+{
+ my ($node, $point_name) = @_;
+
+ $node->wait_for_event('client backend', $point_name);
+ $node->safe_psql('postgres',
+ "SELECT injection_points_detach('$point_name');");
+ $node->safe_psql('postgres',
+ "SELECT injection_points_wakeup('$point_name');");
+}
+
+# Node initialization
+$node = PostgreSQL::Test::Cluster->new('node');
+$node->init();
+$node->append_conf('postgresql.conf', "deadlock_timeout = ${deadlock_timeout}ms");
+$node->start();
+
+# Check if the extension injection_points is available
+plan skip_all => 'Extension injection_points not installed'
+ unless $node->check_extension('injection_points');
+
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+
+$node->safe_psql(
+ 'postgres', q[
+CREATE TABLE test_stat_tab(key text not null, value int);
+INSERT INTO test_stat_tab(key, value) VALUES('k0', 1);
+]);
+
+############################################################################
+
+####### Relation lock
+
+setup_sessions();
+
+$s1->query_safe(
+ q[
+SELECT pg_stat_reset_shared('lock');
+BEGIN;
+LOCK TABLE test_stat_tab;
+]);
+
+# s2 setup
+$s2->query_safe(
+ q[
+BEGIN;
+SELECT pg_stat_force_next_flush();
+]);
+# s2 blocks on LOCK.
+$s2->query_until(
+ qr/lock_s2/, q[
+\echo lock_s2
+LOCK TABLE test_stat_tab;
+]);
+
+wait_and_detach($node, 'deadlock-timeout-fired');
+
+# deadlock_timeout fired, now commit in s1 and s2
+$s1->query_safe(q(COMMIT));
+$s2->query_safe(q(COMMIT));
+
+# check that pg_stat_lock has been updated
+wait_for_pg_stat_lock($node, 'relation');
+ok(1, "Lock stats ok for relation");
+
+# close sessions
+$s1->quit;
+$s2->quit;
+
+####### transaction lock
+
+setup_sessions();
+
+$s1->query_safe(
+ q[
+SELECT pg_stat_reset_shared('lock');
+INSERT INTO test_stat_tab(key, value) VALUES('k1', 1), ('k2', 1), ('k3', 1);
+BEGIN;
+UPDATE test_stat_tab SET value = value + 1 WHERE key = 'k1';
+]);
+
+# s2 setup
+$s2->query_safe(
+ q[
+SET log_lock_waits = on;
+BEGIN;
+SELECT pg_stat_force_next_flush();
+]);
+# s2 blocks here on UPDATE
+$s2->query_until(
+ qr/lock_s2/, q[
+\echo lock_s2
+UPDATE test_stat_tab SET value = value + 1 WHERE key = 'k1';
+]);
+
+wait_and_detach($node, 'deadlock-timeout-fired');
+
+# deadlock_timeout fired, now commit in s1 and s2
+$s1->query_safe(q(COMMIT));
+$s2->query_safe(q(COMMIT));
+
+# check that pg_stat_lock has been updated
+wait_for_pg_stat_lock($node, 'transactionid');
+ok(1, "Lock stats ok for transactionid");
+
+# Close sessions
+$s1->quit;
+$s2->quit;
+
+####### advisory lock
+
+setup_sessions();
+
+$s1->query_safe(
+ q[
+SELECT pg_stat_reset_shared('lock');
+SELECT pg_advisory_lock(1);
+]);
+
+# s2 setup
+$s2->query_safe(
+ q[
+SET log_lock_waits = on;
+BEGIN;
+SELECT pg_stat_force_next_flush();
+]);
+# s2 blocks on the advisory lock.
+$s2->query_until(
+ qr/lock_s2/, q[
+\echo lock_s2
+SELECT pg_advisory_lock(1);
+]);
+
+wait_and_detach($node, 'deadlock-timeout-fired');
+
+# deadlock_timeout fired, now unlock and commit s2
+$s1->query_safe(q(SELECT pg_advisory_unlock(1)));
+$s2->query_safe(
+ q[
+SELECT pg_advisory_unlock(1);
+COMMIT;
+]);
+
+# check that pg_stat_lock has been updated
+wait_for_pg_stat_lock($node, 'advisory');
+ok(1, "Lock stats ok for advisory");
+
+# Close sessions
+$s1->quit;
+$s2->quit;
+
+####### Ensure log_lock_waits has no impact
+
+setup_sessions();
+
+$s1->query_safe(
+ q[
+SELECT pg_stat_reset_shared('lock');
+BEGIN;
+LOCK TABLE test_stat_tab;
+]);
+
+# s2 setup
+$s2->query_safe(
+ q[
+SET log_lock_waits = off;
+BEGIN;
+SELECT pg_stat_force_next_flush();
+]);
+# s2 blocks on LOCK.
+$s2->query_until(
+ qr/lock_s2/, q[
+\echo lock_s2
+LOCK TABLE test_stat_tab;
+]);
+
+wait_and_detach($node, 'deadlock-timeout-fired');
+
+# deadlock_timeout fired, now commit in s1 and s2
+$s1->query_safe(q(COMMIT));
+$s2->query_safe(q(COMMIT));
+
+# check that pg_stat_lock has been updated
+wait_for_pg_stat_lock($node, 'relation');
+ok(1, "log_lock_waits has no impact on Lock stats");
+
+# close sessions
+$s1->quit;
+$s2->quit;
+
+# cleanup
+$node->safe_psql('postgres', q[DROP TABLE test_stat_tab;]);
+
+done_testing();
--
2.53.0
Attachments:
[text/plain] v2-0001-Add-tests-for-lock-stats-take-two.patch (8.4K, ../../acRqbLtT-aJDRSRl@paquier.xyz/2-v2-0001-Add-tests-for-lock-stats-take-two.patch)
download | inline diff:
From fd56d27596834eb019460bffb038aebfa0fa94a4 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 26 Mar 2026 08:03:31 +0900
Subject: [PATCH v2] Add tests for lock stats, take two
commit 7c64d56fd97 removed isolation test lock-stats because it was
unstable in the CI for Windows, at least. This commit creates a new
test for the lock stats using an injection point. This new injection
point is created in ProcSleep() when we know that the deadlock timeout
fired.
Then the new tests follow this workflow:
- session 1 holds a lock
- session 2 attaches to the new injection point with the wait action.
- session 2 is blocked by session 1 and waits until the injection point
wait is reached.
- session 1 releases its lock, session 2 commits
- pg_stat_lock is polled until we get the counters for the lock type or
die with a timeout.
That way there is no sleep at all. Once we know that session 2 has
waited longer the deadlock_timeout (thanks to the new injection point
wait) then we can poll pg_stat_lock to get the updated stats.
Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Discussion: https://postgr.es/m/acNTR1lLHwQJ0o%2BP%40ip-10-97-1-34.eu-west-3.compute.internal
---
src/backend/storage/lmgr/proc.c | 2 +
src/test/modules/test_misc/meson.build | 1 +
.../modules/test_misc/t/011_lock_stats.pl | 249 ++++++++++++++++++
3 files changed, 252 insertions(+)
create mode 100644 src/test/modules/test_misc/t/011_lock_stats.pl
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 5c47cf13473e..b857a10354f7 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -52,6 +52,7 @@
#include "storage/procsignal.h"
#include "storage/spin.h"
#include "storage/standby.h"
+#include "utils/injection_point.h"
#include "utils/timeout.h"
#include "utils/timestamp.h"
#include "utils/wait_event.h"
@@ -1560,6 +1561,7 @@ ProcSleep(LOCALLOCK *locallock)
int usecs;
long msecs;
+ INJECTION_POINT("deadlock-timeout-fired", NULL);
TimestampDifference(get_timeout_start_time(DEADLOCK_TIMEOUT),
GetCurrentTimestamp(),
&secs, &usecs);
diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build
index 6e8db1621a74..1b25d98f7f33 100644
--- a/src/test/modules/test_misc/meson.build
+++ b/src/test/modules/test_misc/meson.build
@@ -19,6 +19,7 @@ tests += {
't/008_replslot_single_user.pl',
't/009_log_temp_files.pl',
't/010_index_concurrently_upsert.pl',
+ 't/011_lock_stats.pl',
],
# The injection points are cluster-wide, so disable installcheck
'runningcheck': false,
diff --git a/src/test/modules/test_misc/t/011_lock_stats.pl b/src/test/modules/test_misc/t/011_lock_stats.pl
new file mode 100644
index 000000000000..4281e842c981
--- /dev/null
+++ b/src/test/modules/test_misc/t/011_lock_stats.pl
@@ -0,0 +1,249 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# 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 tests each test a
+# dedicated lock type.
+# The last one checks that log_lock_waits has no impact on the statistics
+# counters.
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+plan skip_all => 'Injection points not supported by this build'
+ unless $ENV{enable_injection_points} eq 'yes';
+
+my $deadlock_timeout = 10;
+my $s1;
+my $s2;
+my $node;
+
+# Setup the 2 sessions
+sub setup_sessions
+{
+ $s1 = $node->background_psql('postgres');
+ $s2 = $node->background_psql('postgres', on_error_stop => 0);
+
+ # Setup injection points for the waiting session
+ $s2->query_safe(q[
+ SELECT injection_points_set_local();
+ SELECT injection_points_attach('deadlock-timeout-fired', 'wait');
+ ]);
+}
+
+# Fetch waits and wait_time from pg_stat_lock for a given lock type
+# until they reached expected values: at least one wait and waiting longer
+# than the deadlock_timeout.
+sub wait_for_pg_stat_lock
+{
+ my ($node, $lock_type) = @_;
+
+ $node->poll_query_until(
+ 'postgres', qq[
+ SELECT waits > 0 AND wait_time >= $deadlock_timeout
+ FROM pg_stat_lock
+ WHERE locktype = '$lock_type';
+ ]) or die "Timed out waiting for pg_stat_lock for $lock_type"
+}
+
+# Convenience wrapper to wait for a point, then detach it.
+sub wait_and_detach
+{
+ my ($node, $point_name) = @_;
+
+ $node->wait_for_event('client backend', $point_name);
+ $node->safe_psql('postgres',
+ "SELECT injection_points_detach('$point_name');");
+ $node->safe_psql('postgres',
+ "SELECT injection_points_wakeup('$point_name');");
+}
+
+# Node initialization
+$node = PostgreSQL::Test::Cluster->new('node');
+$node->init();
+$node->append_conf('postgresql.conf', "deadlock_timeout = ${deadlock_timeout}ms");
+$node->start();
+
+# Check if the extension injection_points is available
+plan skip_all => 'Extension injection_points not installed'
+ unless $node->check_extension('injection_points');
+
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+
+$node->safe_psql(
+ 'postgres', q[
+CREATE TABLE test_stat_tab(key text not null, value int);
+INSERT INTO test_stat_tab(key, value) VALUES('k0', 1);
+]);
+
+############################################################################
+
+####### Relation lock
+
+setup_sessions();
+
+$s1->query_safe(
+ q[
+SELECT pg_stat_reset_shared('lock');
+BEGIN;
+LOCK TABLE test_stat_tab;
+]);
+
+# s2 setup
+$s2->query_safe(
+ q[
+BEGIN;
+SELECT pg_stat_force_next_flush();
+]);
+# s2 blocks on LOCK.
+$s2->query_until(
+ qr/lock_s2/, q[
+\echo lock_s2
+LOCK TABLE test_stat_tab;
+]);
+
+wait_and_detach($node, 'deadlock-timeout-fired');
+
+# deadlock_timeout fired, now commit in s1 and s2
+$s1->query_safe(q(COMMIT));
+$s2->query_safe(q(COMMIT));
+
+# check that pg_stat_lock has been updated
+wait_for_pg_stat_lock($node, 'relation');
+ok(1, "Lock stats ok for relation");
+
+# close sessions
+$s1->quit;
+$s2->quit;
+
+####### transaction lock
+
+setup_sessions();
+
+$s1->query_safe(
+ q[
+SELECT pg_stat_reset_shared('lock');
+INSERT INTO test_stat_tab(key, value) VALUES('k1', 1), ('k2', 1), ('k3', 1);
+BEGIN;
+UPDATE test_stat_tab SET value = value + 1 WHERE key = 'k1';
+]);
+
+# s2 setup
+$s2->query_safe(
+ q[
+SET log_lock_waits = on;
+BEGIN;
+SELECT pg_stat_force_next_flush();
+]);
+# s2 blocks here on UPDATE
+$s2->query_until(
+ qr/lock_s2/, q[
+\echo lock_s2
+UPDATE test_stat_tab SET value = value + 1 WHERE key = 'k1';
+]);
+
+wait_and_detach($node, 'deadlock-timeout-fired');
+
+# deadlock_timeout fired, now commit in s1 and s2
+$s1->query_safe(q(COMMIT));
+$s2->query_safe(q(COMMIT));
+
+# check that pg_stat_lock has been updated
+wait_for_pg_stat_lock($node, 'transactionid');
+ok(1, "Lock stats ok for transactionid");
+
+# Close sessions
+$s1->quit;
+$s2->quit;
+
+####### advisory lock
+
+setup_sessions();
+
+$s1->query_safe(
+ q[
+SELECT pg_stat_reset_shared('lock');
+SELECT pg_advisory_lock(1);
+]);
+
+# s2 setup
+$s2->query_safe(
+ q[
+SET log_lock_waits = on;
+BEGIN;
+SELECT pg_stat_force_next_flush();
+]);
+# s2 blocks on the advisory lock.
+$s2->query_until(
+ qr/lock_s2/, q[
+\echo lock_s2
+SELECT pg_advisory_lock(1);
+]);
+
+wait_and_detach($node, 'deadlock-timeout-fired');
+
+# deadlock_timeout fired, now unlock and commit s2
+$s1->query_safe(q(SELECT pg_advisory_unlock(1)));
+$s2->query_safe(
+ q[
+SELECT pg_advisory_unlock(1);
+COMMIT;
+]);
+
+# check that pg_stat_lock has been updated
+wait_for_pg_stat_lock($node, 'advisory');
+ok(1, "Lock stats ok for advisory");
+
+# Close sessions
+$s1->quit;
+$s2->quit;
+
+####### Ensure log_lock_waits has no impact
+
+setup_sessions();
+
+$s1->query_safe(
+ q[
+SELECT pg_stat_reset_shared('lock');
+BEGIN;
+LOCK TABLE test_stat_tab;
+]);
+
+# s2 setup
+$s2->query_safe(
+ q[
+SET log_lock_waits = off;
+BEGIN;
+SELECT pg_stat_force_next_flush();
+]);
+# s2 blocks on LOCK.
+$s2->query_until(
+ qr/lock_s2/, q[
+\echo lock_s2
+LOCK TABLE test_stat_tab;
+]);
+
+wait_and_detach($node, 'deadlock-timeout-fired');
+
+# deadlock_timeout fired, now commit in s1 and s2
+$s1->query_safe(q(COMMIT));
+$s2->query_safe(q(COMMIT));
+
+# check that pg_stat_lock has been updated
+wait_for_pg_stat_lock($node, 'relation');
+ok(1, "log_lock_waits has no impact on Lock stats");
+
+# close sessions
+$s1->quit;
+$s2->quit;
+
+# cleanup
+$node->safe_psql('postgres', q[DROP TABLE test_stat_tab;]);
+
+done_testing();
--
2.53.0
[application/pgp-signature] signature.asc (832B, ../../acRqbLtT-aJDRSRl@paquier.xyz/3-signature.asc)
download
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-03-26 04:05 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Michael Paquier <michael@paquier.xyz>
0 siblings, 1 reply; 61+ messages in thread
From: Bertrand Drouvot @ 2026-03-26 04:05 UTC (permalink / raw)
To: Michael Paquier <michael@paquier.xyz>; +Cc: Andres Freund <andres@anarazel.de>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi,
On Thu, Mar 26, 2026 at 08:06:20AM +0900, Michael Paquier wrote:
>
> I like the patch, but I happen to not like my initial idea of relying
> on a NOTICE in an injection point combined with your new API in
> BackgroundPsql because we can already achieve the same with a wait
> injection point and use BackgroundPsql::query_until() with an \echo to
> detect that the command is blocked.
Yeah that works too.
> The updated version attached uses this method (edited quickly your
> commit message). Like your patch there is no need for hardcoded
> sleeps and the CI's first impressions are actually good,
Same here.
> but I am
> going to need more runs to gain more confidence. Note I should be
> able to do something here in 10 days or so. If you could confirm the
> stability on your side for the time being with more runs, that would
> help, of course.
With wait + echo we don't need s2 to "on_error_stop => 0" anymore. I changed
that in the attached. I'll run more CI tests during those 10 days. Let's sync
up when you'll be about to push it.
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
Attachments:
[text/x-diff] v3-0001-Add-tests-for-lock-stats-take-two.patch (8.4K, ../../acSwlC46%2FWhYZuy5@ip-10-97-1-34.eu-west-3.compute.internal/2-v3-0001-Add-tests-for-lock-stats-take-two.patch)
download | inline diff:
From c7324797651c965dce529a9052d2ff3bc379c278 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Thu, 26 Mar 2026 08:03:31 +0900
Subject: [PATCH v3] Add tests for lock stats, take two
commit 7c64d56fd97 removed isolation test lock-stats because it was
unstable in the CI for Windows, at least. This commit creates a new
test for the lock stats using an injection point. This new injection
point is created in ProcSleep() when we know that the deadlock timeout
fired.
Then the new tests follow this workflow:
- session 1 holds a lock
- session 2 attaches to the new injection point with the wait action
- session 2 is blocked by session 1 and waits until the injection point
wait is reached
- session 1 releases its lock, session 2 commits
- pg_stat_lock is polled until we get the counters for the lock type or
die with a timeout.
That way there is no sleep at all. Once we know that session 2 has
waited longer the deadlock_timeout (thanks to the new injection point
wait) then we can poll pg_stat_lock to get the updated stats.
Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Discussion: https://postgr.es/m/acNTR1lLHwQJ0o%2BP%40ip-10-97-1-34.eu-west-3.compute.internal
---
src/backend/storage/lmgr/proc.c | 2 +
src/test/modules/test_misc/meson.build | 1 +
.../modules/test_misc/t/011_lock_stats.pl | 249 ++++++++++++++++++
3 files changed, 252 insertions(+)
97.8% src/test/modules/test_misc/t/
diff --git a/src/backend/storage/lmgr/proc.c b/src/backend/storage/lmgr/proc.c
index 5c47cf13473..b857a10354f 100644
--- a/src/backend/storage/lmgr/proc.c
+++ b/src/backend/storage/lmgr/proc.c
@@ -52,6 +52,7 @@
#include "storage/procsignal.h"
#include "storage/spin.h"
#include "storage/standby.h"
+#include "utils/injection_point.h"
#include "utils/timeout.h"
#include "utils/timestamp.h"
#include "utils/wait_event.h"
@@ -1560,6 +1561,7 @@ ProcSleep(LOCALLOCK *locallock)
int usecs;
long msecs;
+ INJECTION_POINT("deadlock-timeout-fired", NULL);
TimestampDifference(get_timeout_start_time(DEADLOCK_TIMEOUT),
GetCurrentTimestamp(),
&secs, &usecs);
diff --git a/src/test/modules/test_misc/meson.build b/src/test/modules/test_misc/meson.build
index 6e8db1621a7..1b25d98f7f3 100644
--- a/src/test/modules/test_misc/meson.build
+++ b/src/test/modules/test_misc/meson.build
@@ -19,6 +19,7 @@ tests += {
't/008_replslot_single_user.pl',
't/009_log_temp_files.pl',
't/010_index_concurrently_upsert.pl',
+ 't/011_lock_stats.pl',
],
# The injection points are cluster-wide, so disable installcheck
'runningcheck': false,
diff --git a/src/test/modules/test_misc/t/011_lock_stats.pl b/src/test/modules/test_misc/t/011_lock_stats.pl
new file mode 100644
index 00000000000..829d7e3f5b9
--- /dev/null
+++ b/src/test/modules/test_misc/t/011_lock_stats.pl
@@ -0,0 +1,249 @@
+
+# Copyright (c) 2026, PostgreSQL Global Development Group
+
+# 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 tests each test a
+# dedicated lock type.
+# The last one checks that log_lock_waits has no impact on the statistics
+# counters.
+
+use strict;
+use warnings FATAL => 'all';
+
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+plan skip_all => 'Injection points not supported by this build'
+ unless $ENV{enable_injection_points} eq 'yes';
+
+my $deadlock_timeout = 10;
+my $s1;
+my $s2;
+my $node;
+
+# Setup the 2 sessions
+sub setup_sessions
+{
+ $s1 = $node->background_psql('postgres');
+ $s2 = $node->background_psql('postgres');
+
+ # Setup injection points for the waiting session
+ $s2->query_safe(q[
+ SELECT injection_points_set_local();
+ SELECT injection_points_attach('deadlock-timeout-fired', 'wait');
+ ]);
+}
+
+# Fetch waits and wait_time from pg_stat_lock for a given lock type
+# until they reached expected values: at least one wait and waiting longer
+# than the deadlock_timeout.
+sub wait_for_pg_stat_lock
+{
+ my ($node, $lock_type) = @_;
+
+ $node->poll_query_until(
+ 'postgres', qq[
+ SELECT waits > 0 AND wait_time >= $deadlock_timeout
+ FROM pg_stat_lock
+ WHERE locktype = '$lock_type';
+ ]) or die "Timed out waiting for pg_stat_lock for $lock_type"
+}
+
+# Convenience wrapper to wait for a point, then detach it.
+sub wait_and_detach
+{
+ my ($node, $point_name) = @_;
+
+ $node->wait_for_event('client backend', $point_name);
+ $node->safe_psql('postgres',
+ "SELECT injection_points_detach('$point_name');");
+ $node->safe_psql('postgres',
+ "SELECT injection_points_wakeup('$point_name');");
+}
+
+# Node initialization
+$node = PostgreSQL::Test::Cluster->new('node');
+$node->init();
+$node->append_conf('postgresql.conf', "deadlock_timeout = ${deadlock_timeout}ms");
+$node->start();
+
+# Check if the extension injection_points is available
+plan skip_all => 'Extension injection_points not installed'
+ unless $node->check_extension('injection_points');
+
+$node->safe_psql('postgres', 'CREATE EXTENSION injection_points;');
+
+$node->safe_psql(
+ 'postgres', q[
+CREATE TABLE test_stat_tab(key text not null, value int);
+INSERT INTO test_stat_tab(key, value) VALUES('k0', 1);
+]);
+
+############################################################################
+
+####### Relation lock
+
+setup_sessions();
+
+$s1->query_safe(
+ q[
+SELECT pg_stat_reset_shared('lock');
+BEGIN;
+LOCK TABLE test_stat_tab;
+]);
+
+# s2 setup
+$s2->query_safe(
+ q[
+BEGIN;
+SELECT pg_stat_force_next_flush();
+]);
+# s2 blocks on LOCK.
+$s2->query_until(
+ qr/lock_s2/, q[
+\echo lock_s2
+LOCK TABLE test_stat_tab;
+]);
+
+wait_and_detach($node, 'deadlock-timeout-fired');
+
+# deadlock_timeout fired, now commit in s1 and s2
+$s1->query_safe(q(COMMIT));
+$s2->query_safe(q(COMMIT));
+
+# check that pg_stat_lock has been updated
+wait_for_pg_stat_lock($node, 'relation');
+ok(1, "Lock stats ok for relation");
+
+# close sessions
+$s1->quit;
+$s2->quit;
+
+####### transaction lock
+
+setup_sessions();
+
+$s1->query_safe(
+ q[
+SELECT pg_stat_reset_shared('lock');
+INSERT INTO test_stat_tab(key, value) VALUES('k1', 1), ('k2', 1), ('k3', 1);
+BEGIN;
+UPDATE test_stat_tab SET value = value + 1 WHERE key = 'k1';
+]);
+
+# s2 setup
+$s2->query_safe(
+ q[
+SET log_lock_waits = on;
+BEGIN;
+SELECT pg_stat_force_next_flush();
+]);
+# s2 blocks here on UPDATE
+$s2->query_until(
+ qr/lock_s2/, q[
+\echo lock_s2
+UPDATE test_stat_tab SET value = value + 1 WHERE key = 'k1';
+]);
+
+wait_and_detach($node, 'deadlock-timeout-fired');
+
+# deadlock_timeout fired, now commit in s1 and s2
+$s1->query_safe(q(COMMIT));
+$s2->query_safe(q(COMMIT));
+
+# check that pg_stat_lock has been updated
+wait_for_pg_stat_lock($node, 'transactionid');
+ok(1, "Lock stats ok for transactionid");
+
+# Close sessions
+$s1->quit;
+$s2->quit;
+
+####### advisory lock
+
+setup_sessions();
+
+$s1->query_safe(
+ q[
+SELECT pg_stat_reset_shared('lock');
+SELECT pg_advisory_lock(1);
+]);
+
+# s2 setup
+$s2->query_safe(
+ q[
+SET log_lock_waits = on;
+BEGIN;
+SELECT pg_stat_force_next_flush();
+]);
+# s2 blocks on the advisory lock.
+$s2->query_until(
+ qr/lock_s2/, q[
+\echo lock_s2
+SELECT pg_advisory_lock(1);
+]);
+
+wait_and_detach($node, 'deadlock-timeout-fired');
+
+# deadlock_timeout fired, now unlock and commit s2
+$s1->query_safe(q(SELECT pg_advisory_unlock(1)));
+$s2->query_safe(
+ q[
+SELECT pg_advisory_unlock(1);
+COMMIT;
+]);
+
+# check that pg_stat_lock has been updated
+wait_for_pg_stat_lock($node, 'advisory');
+ok(1, "Lock stats ok for advisory");
+
+# Close sessions
+$s1->quit;
+$s2->quit;
+
+####### Ensure log_lock_waits has no impact
+
+setup_sessions();
+
+$s1->query_safe(
+ q[
+SELECT pg_stat_reset_shared('lock');
+BEGIN;
+LOCK TABLE test_stat_tab;
+]);
+
+# s2 setup
+$s2->query_safe(
+ q[
+SET log_lock_waits = off;
+BEGIN;
+SELECT pg_stat_force_next_flush();
+]);
+# s2 blocks on LOCK.
+$s2->query_until(
+ qr/lock_s2/, q[
+\echo lock_s2
+LOCK TABLE test_stat_tab;
+]);
+
+wait_and_detach($node, 'deadlock-timeout-fired');
+
+# deadlock_timeout fired, now commit in s1 and s2
+$s1->query_safe(q(COMMIT));
+$s2->query_safe(q(COMMIT));
+
+# check that pg_stat_lock has been updated
+wait_for_pg_stat_lock($node, 'relation');
+ok(1, "log_lock_waits has no impact on Lock stats");
+
+# close sessions
+$s1->quit;
+$s2->quit;
+
+# cleanup
+$node->safe_psql('postgres', q[DROP TABLE test_stat_tab;]);
+
+done_testing();
--
2.34.1
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-03-26 06:53 Michael Paquier <michael@paquier.xyz>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
0 siblings, 1 reply; 61+ messages in thread
From: Michael Paquier @ 2026-03-26 06:53 UTC (permalink / raw)
To: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: Andres Freund <andres@anarazel.de>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
On Thu, Mar 26, 2026 at 04:05:40AM +0000, Bertrand Drouvot wrote:
> With wait + echo we don't need s2 to "on_error_stop => 0" anymore. I changed
> that in the attached.
Indeed.
> I'll run more CI tests during those 10 days. Let's sync
> up when you'll be about to push it.
.
Thanks.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (832B, ../../acTX8bbXh9lgOvZL@paquier.xyz/2-signature.asc)
download
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-03-30 16:11 Tomas Vondra <tomas@vondra.me>
parent: Michael Paquier <michael@paquier.xyz>
1 sibling, 1 reply; 61+ messages in thread
From: Tomas Vondra @ 2026-03-30 16:11 UTC (permalink / raw)
To: Michael Paquier <michael@paquier.xyz>; Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: Andres Freund <andres@anarazel.de>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi,
Isn't pgstat_lock_flush_cb a bit broken with nowait=true? It'll skip
flushing stats for that particular lock type, but then it'll happily
reset the pending stats anyway, forgetting the stats.
AFAIK it should keep the pending stats, and flush them sometime lager,
when the lock is not contended. That's what the other flush callbacks
do, at least. This probably means it needs to reset the entries one by
one, not the whole struct at once.
TBH I'm rather skeptical about having one lock per entry. Sure, it
allows two backends to write different entries concurrently. But is it
actually worth it? With nowait=true it might even be cheaper to try with
a single lock, and AFAICS that's the case where it matters.
I wouldn't be surprised if this behaved quite poorly with contended
cases, because the backends will be accessing the locks in exactly the
same order and synchronize. So if one lock is contended, won't that
"synchronize" the backends, making the other locks contended too?
Has anyone tested it actually improves the behavior? I only quickly
skimmed the thread, I might have missed it ...
If per-entry locks help, maybe a good micro-optimization would be to
check if there even is anything to sync before acquiring the lock. I
mean, if (waits==0), why obtain the lock?
regards
--
Tomas Vondra
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-03-31 07:10 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Tomas Vondra <tomas@vondra.me>
0 siblings, 1 reply; 61+ messages in thread
From: Bertrand Drouvot @ 2026-03-31 07:10 UTC (permalink / raw)
To: Tomas Vondra <tomas@vondra.me>; +Cc: Michael Paquier <michael@paquier.xyz>; Andres Freund <andres@anarazel.de>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi,
On Mon, Mar 30, 2026 at 06:11:17PM +0200, Tomas Vondra wrote:
> Hi,
>
> Isn't pgstat_lock_flush_cb a bit broken with nowait=true? It'll skip
> flushing stats for that particular lock type, but then it'll happily
> reset the pending stats anyway, forgetting the stats.
>
> AFAIK it should keep the pending stats, and flush them sometime lager,
> when the lock is not contended. That's what the other flush callbacks
> do, at least. This probably means it needs to reset the entries one by
> one, not the whole struct at once.
Oh right, it's currently misbehaving, thanks for the warning!
> TBH I'm rather skeptical about having one lock per entry. Sure, it
> allows two backends to write different entries concurrently. But is it
> actually worth it? With nowait=true it might even be cheaper to try with
> a single lock, and AFAICS that's the case where it matters.
>
> I wouldn't be surprised if this behaved quite poorly with contended
> cases, because the backends will be accessing the locks in exactly the
> same order and synchronize. So if one lock is contended, won't that
> "synchronize" the backends, making the other locks contended too?
>
> Has anyone tested it actually improves the behavior? I only quickly
> skimmed the thread, I might have missed it ...
>
I just did a few tests, with a per entry lock version fixed (to avoid the bug
mentioned above) and with a single lock.
The test is this one:
Setup:
deadlock_timeout set to 1ms.
CREATE TABLE t1(id int primary key, v int);
CREATE TABLE t2(id int primary key, v int);
INSERT INTO t1 SELECT i, 0 FROM generate_series(1,100) i;
INSERT INTO t2 SELECT i, 0 FROM generate_series(1,100) i;
test.sql:
\set id1 random(1, 100)
\set id2 random(1, 100)
BEGIN;
SELECT pg_advisory_xact_lock(:id1);
UPDATE t1 SET v=v+1 WHERE id=:id1;
UPDATE t2 SET v=v+1 WHERE id=:id2;
END;
Launched that way:
pgbench -c 32 -j 32 -T60 -f test.sql
One run produces, something like:
postgres=# select locktype, waits, wait_time from pg_stat_lock where waits > 0;
locktype | waits | wait_time
---------------+--------+-----------
tuple | 5058 | 5092
transactionid | 78287 | 79269
advisory | 105005 | 177253
(3 rows)
With one lock per entry, the avg (the test has been run 5 times) tps is 12099.
With one single lock, the avg (the test has been run 5 times) tps is 12118.
The difference looks like noise so that one lock per entry does not show
improved performance.
Also both kind of tests produce this perf profile:
0.00% 0.00% postgres postgres [.] pgstat_lock_flush_cb
So, I'm tempted to say that one lock per entry adds complexity without observable
performance benefit. Also one single lock matches more naturally the intent of the
nowait path and I would also not be surprised if one lock per entry behaves
worse in some cases.
So, PFA a patch to move to a single lock instead.
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
Attachments:
[text/x-diff] v1-0001-lock-statistics-replace-per-lock-type-locks-with-.patch (5.7K, ../../actze31XhGYXNNQg@ip-10-97-1-34.eu-west-3.compute.internal/2-v1-0001-lock-statistics-replace-per-lock-type-locks-with-.patch)
download | inline diff:
From 12eb6bb593fd660ff1283474a694b674b30d1e99 Mon Sep 17 00:00:00 2001
From: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Date: Mon, 30 Mar 2026 17:16:08 +0000
Subject: [PATCH v1] lock statistics: replace per lock type locks with a single
lock
Currently there is one LWLock per lock type entry: this adds complexity without
observable performance benefit.
Plus there is a bug with nowait=true: a backend iterating over all
entries could successfully flush some entries while skipping others due
to contention, then unconditionally reset PendingLockStats and clear
have_lockstats, losing the skipped entries permanently.
This commit replaces the per entry locks with a single lock protecting all
entries and the reset timestamp.
This makes the nowait path correct: either the lock is acquired and everything
is flushed, or it is not acquired and nothing is modified. This also better
matches the intent of the nowait path.
As a result, the pgstat_lock_init_shmem_cb(), pgstat_lock_reset_all_cb() and
pgstat_lock_snapshot_cb() callbacks are also simplified.
Author: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
Reported-by: Tomas Vondra <tomas@vondra.me>
Discussion: https://postgr.es/m/1af63e6d-16d5-4d5b-9b03-11472ef1adf9%40vondra.me
---
src/backend/utils/activity/pgstat_lock.c | 86 ++++++++----------------
src/include/utils/pgstat_internal.h | 6 +-
2 files changed, 32 insertions(+), 60 deletions(-)
92.0% src/backend/utils/activity/
7.9% src/include/utils/
diff --git a/src/backend/utils/activity/pgstat_lock.c b/src/backend/utils/activity/pgstat_lock.c
index 041f521ce73..aec64f8fb4b 100644
--- a/src/backend/utils/activity/pgstat_lock.c
+++ b/src/backend/utils/activity/pgstat_lock.c
@@ -50,99 +50,71 @@ pgstat_lock_flush(bool nowait)
bool
pgstat_lock_flush_cb(bool nowait)
{
- LWLock *lcktype_lock;
- PgStat_LockEntry *lck_shstats;
- bool lock_not_acquired = false;
+ LWLock *lckstat_lock;
+ PgStatShared_Lock *shstats;
if (!have_lockstats)
return false;
+ shstats = &pgStatLocal.shmem->lock;
+ lckstat_lock = &shstats->lock;
+
+ if (!nowait)
+ LWLockAcquire(lckstat_lock, LW_EXCLUSIVE);
+ else if (!LWLockConditionalAcquire(lckstat_lock, LW_EXCLUSIVE))
+ return true;
+
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)
+ (shstats->stats.stats[i].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));
+ LWLockRelease(lckstat_lock);
+ memset(&PendingLockStats, 0, sizeof(PendingLockStats));
have_lockstats = false;
- return lock_not_acquired;
+ return false;
}
-
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);
+ LWLockInitialize(&stat_shmem->lock, 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];
+ LWLock *lckstat_lock = &pgStatLocal.shmem->lock.lock;
- LWLockAcquire(lcktype_lock, LW_EXCLUSIVE);
+ LWLockAcquire(lckstat_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;
+ pgStatLocal.shmem->lock.stats.stat_reset_timestamp = ts;
- memset(lck_shstats, 0, sizeof(*lck_shstats));
- LWLockRelease(lcktype_lock);
- }
+ memset(pgStatLocal.shmem->lock.stats.stats, 0,
+ sizeof(pgStatLocal.shmem->lock.stats.stats));
+
+ LWLockRelease(lckstat_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);
- }
+ LWLock *lckstat_lock = &pgStatLocal.shmem->lock.lock;
+
+ LWLockAcquire(lckstat_lock, LW_SHARED);
+
+ pgStatLocal.snapshot.lock = pgStatLocal.shmem->lock.stats;
+
+ LWLockRelease(lckstat_lock);
}
/*
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 97704421a92..8ae3fbcba46 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -467,10 +467,10 @@ typedef struct PgStatShared_IO
typedef struct PgStatShared_Lock
{
/*
- * locks[i] protects stats.stats[i]. locks[0] also protects
- * stats.stat_reset_timestamp.
+ * single lock protecting all entries as well as
+ * stats->stat_reset_timestamp.
*/
- LWLock locks[LOCKTAG_LAST_TYPE + 1];
+ LWLock lock;
PgStat_Lock stats;
} PgStatShared_Lock;
--
2.34.1
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-04-06 00:05 Michael Paquier <michael@paquier.xyz>
parent: Michael Paquier <michael@paquier.xyz>
0 siblings, 1 reply; 61+ messages in thread
From: Michael Paquier @ 2026-04-06 00:05 UTC (permalink / raw)
To: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: Andres Freund <andres@anarazel.de>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
On Thu, Mar 26, 2026 at 03:53:37PM +0900, Michael Paquier wrote:
> On Thu, Mar 26, 2026 at 04:05:40AM +0000, Bertrand Drouvot wrote:
>> With wait + echo we don't need s2 to "on_error_stop => 0" anymore. I changed
>> that in the attached.
>
> Indeed.
Applied without the on_error_stop, after a few more runs in the CI,
that have all passed. (Looking now at the rest.)
--
Michael
Attachments:
[application/pgp-signature] signature.asc (832B, ../../adL4u897hLceqYmc@paquier.xyz/2-signature.asc)
download
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-04-06 05:11 Michael Paquier <michael@paquier.xyz>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
0 siblings, 1 reply; 61+ messages in thread
From: Michael Paquier @ 2026-04-06 05:11 UTC (permalink / raw)
To: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: Tomas Vondra <tomas@vondra.me>; Andres Freund <andres@anarazel.de>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
On Tue, Mar 31, 2026 at 07:10:51AM +0000, Bertrand Drouvot wrote:
> On Mon, Mar 30, 2026 at 06:11:17PM +0200, Tomas Vondra wrote:
>> Isn't pgstat_lock_flush_cb a bit broken with nowait=true? It'll skip
>> flushing stats for that particular lock type, but then it'll happily
>> reset the pending stats anyway, forgetting the stats.
>>
>> AFAIK it should keep the pending stats, and flush them sometime lager,
>> when the lock is not contended. That's what the other flush callbacks
>> do, at least. This probably means it needs to reset the entries one by
>> one, not the whole struct at once.
>
> Oh right, it's currently misbehaving, thanks for the warning!
Not misbehaving, mistaken. This would create loss of stats data that
should have been flushed. I should not have missed that, sorry about
that. A single-lock approach is also something we do for SLRU and WAL
data, and these are much hotter.
At the exception of one comment that could be simplified, the code
removed is correct, so addressed this one as well.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (832B, ../../adNAec0VtP5J4q7m@paquier.xyz/2-signature.asc)
download
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-04-06 05:22 Andres Freund <andres@anarazel.de>
parent: Michael Paquier <michael@paquier.xyz>
0 siblings, 1 reply; 61+ messages in thread
From: Andres Freund @ 2026-04-06 05:22 UTC (permalink / raw)
To: Michael Paquier <michael@paquier.xyz>; +Cc: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi,
On 2026-04-06 09:05:15 +0900, Michael Paquier wrote:
> On Thu, Mar 26, 2026 at 03:53:37PM +0900, Michael Paquier wrote:
> > On Thu, Mar 26, 2026 at 04:05:40AM +0000, Bertrand Drouvot wrote:
> >> With wait + echo we don't need s2 to "on_error_stop => 0" anymore. I changed
> >> that in the attached.
> >
> > Indeed.
>
> Applied without the on_error_stop, after a few more runs in the CI,
> that have all passed. (Looking now at the rest.)
https://cirrus-ci.com/task/6253659454963712
https://api.cirrus-ci.com/v1/artifact/task/6253659454963712/testrun/build/testrun/test_misc/011_lock...
[04:33:31.756](0.067s) # die: error running SQL: 'psql:<stdin>:1: ERROR: could not detach injection point "deadlock-timeout-fired"'
Greetings,
Andres Freund
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-04-06 06:19 Michael Paquier <michael@paquier.xyz>
parent: Andres Freund <andres@anarazel.de>
0 siblings, 1 reply; 61+ messages in thread
From: Michael Paquier @ 2026-04-06 06:19 UTC (permalink / raw)
To: Andres Freund <andres@anarazel.de>; +Cc: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
On Mon, Apr 06, 2026 at 01:22:23AM -0400, Andres Freund wrote:
> https://cirrus-ci.com/task/6253659454963712
> https://api.cirrus-ci.com/v1/artifact/task/6253659454963712/testrun/build/testrun/test_misc/011_lock...
>
> [04:33:31.756](0.067s) # die: error running SQL: 'psql:<stdin>:1: ERROR: could not detach injection point "deadlock-timeout-fired"'
I got a way to reproduce the same error pattern with the following
trick:
--- a/src/backend/utils/misc/injection_point.c
+++ b/src/backend/utils/misc/injection_point.c
@@ -357,6 +357,8 @@ InjectionPointDetach(const char *name)
int idx;
int max_inuse;
+ pg_usleep(5 * 1000000L);
+
LWLockAcquire(InjectionPointLock, LW_EXCLUSIVE);
Now looking at it, and for the reason why 010 for concurrent indexes
does not complain..
--
Michael
Attachments:
[application/pgp-signature] signature.asc (832B, ../../adNQjbY7QvWe_W4m@paquier.xyz/2-signature.asc)
download
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-04-06 06:34 Michael Paquier <michael@paquier.xyz>
parent: Michael Paquier <michael@paquier.xyz>
0 siblings, 1 reply; 61+ messages in thread
From: Michael Paquier @ 2026-04-06 06:34 UTC (permalink / raw)
To: Andres Freund <andres@anarazel.de>; +Cc: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
On Mon, Apr 06, 2026 at 03:19:57PM +0900, Michael Paquier wrote:
> Now looking at it, and for the reason why 010 for concurrent indexes
> does not complain..
This one was a simple puzzle: there was a race condition between the
detach done by a local point and the wait/detach sequence. As we want
a detach, dropping the local point is proving to work here.
I am going to do a few more runs to gain some more confidence.
Bertrand, could you confirm please?
--
Michael
From 684a26ad148a6d3d4261a633978702f76d7ee537 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Mon, 6 Apr 2026 15:34:03 +0900
Subject: [PATCH] Fix detach timing problem in lock stats test
---
src/test/modules/test_misc/t/011_lock_stats.pl | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/src/test/modules/test_misc/t/011_lock_stats.pl b/src/test/modules/test_misc/t/011_lock_stats.pl
index 58a0046a52c3..908ade55a05f 100644
--- a/src/test/modules/test_misc/t/011_lock_stats.pl
+++ b/src/test/modules/test_misc/t/011_lock_stats.pl
@@ -31,9 +31,8 @@ sub setup_sessions
$s2 = $node->background_psql('postgres');
# Setup injection points for the waiting session
- $s2->query_safe(
- q[
- SELECT injection_points_set_local();
+ $s2->query_until(qr/attaching_injection_point/, q[
+ \echo attaching_injection_point
SELECT injection_points_attach('deadlock-timeout-fired', 'wait');
]);
}
@@ -59,10 +58,11 @@ sub wait_and_detach
my ($node, $point_name) = @_;
$node->wait_for_event('client backend', $point_name);
- $node->safe_psql('postgres',
- "SELECT injection_points_detach('$point_name');");
- $node->safe_psql('postgres',
- "SELECT injection_points_wakeup('$point_name');");
+ $node->safe_psql(
+ 'postgres', qq[
+SELECT injection_points_detach('$point_name');
+SELECT injection_points_wakeup('$point_name');
+]);
}
# Node initialization
--
2.53.0
Attachments:
[text/plain] 0001-Fix-detach-timing-problem-in-lock-stats-test.patch (1.4K, ../../adNUBGqGK2YB1azG@paquier.xyz/2-0001-Fix-detach-timing-problem-in-lock-stats-test.patch)
download | inline diff:
From 684a26ad148a6d3d4261a633978702f76d7ee537 Mon Sep 17 00:00:00 2001
From: Michael Paquier <michael@paquier.xyz>
Date: Mon, 6 Apr 2026 15:34:03 +0900
Subject: [PATCH] Fix detach timing problem in lock stats test
---
src/test/modules/test_misc/t/011_lock_stats.pl | 14 +++++++-------
1 file changed, 7 insertions(+), 7 deletions(-)
diff --git a/src/test/modules/test_misc/t/011_lock_stats.pl b/src/test/modules/test_misc/t/011_lock_stats.pl
index 58a0046a52c3..908ade55a05f 100644
--- a/src/test/modules/test_misc/t/011_lock_stats.pl
+++ b/src/test/modules/test_misc/t/011_lock_stats.pl
@@ -31,9 +31,8 @@ sub setup_sessions
$s2 = $node->background_psql('postgres');
# Setup injection points for the waiting session
- $s2->query_safe(
- q[
- SELECT injection_points_set_local();
+ $s2->query_until(qr/attaching_injection_point/, q[
+ \echo attaching_injection_point
SELECT injection_points_attach('deadlock-timeout-fired', 'wait');
]);
}
@@ -59,10 +58,11 @@ sub wait_and_detach
my ($node, $point_name) = @_;
$node->wait_for_event('client backend', $point_name);
- $node->safe_psql('postgres',
- "SELECT injection_points_detach('$point_name');");
- $node->safe_psql('postgres',
- "SELECT injection_points_wakeup('$point_name');");
+ $node->safe_psql(
+ 'postgres', qq[
+SELECT injection_points_detach('$point_name');
+SELECT injection_points_wakeup('$point_name');
+]);
}
# Node initialization
--
2.53.0
[application/pgp-signature] signature.asc (832B, ../../adNUBGqGK2YB1azG@paquier.xyz/3-signature.asc)
download
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-04-07 04:21 Michael Paquier <michael@paquier.xyz>
parent: Michael Paquier <michael@paquier.xyz>
0 siblings, 1 reply; 61+ messages in thread
From: Michael Paquier @ 2026-04-07 04:21 UTC (permalink / raw)
To: Andres Freund <andres@anarazel.de>; +Cc: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
On Mon, Apr 06, 2026 at 03:34:44PM +0900, Michael Paquier wrote:
> This one was a simple puzzle: there was a race condition between the
> detach done by a local point and the wait/detach sequence. As we want
> a detach, dropping the local point is proving to work here.
>
> I am going to do a few more runs to gain some more confidence.
Done a total of 5 runs (or 6 actually), and fixed it.
> Bertrand, could you confirm please?
That's of course always welcome. I'll keep an eye on the CI and the
buildfarm.
--
Michael
Attachments:
[application/pgp-signature] signature.asc (832B, ../../adSGU67l6NQWg_m7@paquier.xyz/2-signature.asc)
download
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-04-07 06:01 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Michael Paquier <michael@paquier.xyz>
0 siblings, 1 reply; 61+ messages in thread
From: Bertrand Drouvot @ 2026-04-07 06:01 UTC (permalink / raw)
To: Michael Paquier <michael@paquier.xyz>; +Cc: Andres Freund <andres@anarazel.de>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi,
On Tue, Apr 07, 2026 at 01:21:39PM +0900, Michael Paquier wrote:
> On Mon, Apr 06, 2026 at 03:34:44PM +0900, Michael Paquier wrote:
> > This one was a simple puzzle: there was a race condition between the
> > detach done by a local point and the wait/detach sequence. As we want
> > a detach, dropping the local point is proving to work here.
> >
> > I am going to do a few more runs to gain some more confidence.
>
> Done a total of 5 runs (or 6 actually), and fixed it.
>
> > Bertrand, could you confirm please?
>
> That's of course always welcome. I'll keep an eye on the CI and the
> buildfarm.
That looks to work, thanks! But I was wondering if this new version is not
introducing a new race: the injection point is not local anymore so it could be
that another process reach the new injection point. That said, even if this is
the case I think we're ok since s2 is using "query_until" so we could say that
"at least" s2 reached the injection point. The new version does not ensure that
"only" s2 reached the injection point but I think that's safe.
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-04-07 06:02 Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
parent: Michael Paquier <michael@paquier.xyz>
0 siblings, 0 replies; 61+ messages in thread
From: Bertrand Drouvot @ 2026-04-07 06:02 UTC (permalink / raw)
To: Michael Paquier <michael@paquier.xyz>; +Cc: Tomas Vondra <tomas@vondra.me>; Andres Freund <andres@anarazel.de>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
Hi,
On Mon, Apr 06, 2026 at 02:11:21PM +0900, Michael Paquier wrote:
> On Tue, Mar 31, 2026 at 07:10:51AM +0000, Bertrand Drouvot wrote:
> > On Mon, Mar 30, 2026 at 06:11:17PM +0200, Tomas Vondra wrote:
> >> Isn't pgstat_lock_flush_cb a bit broken with nowait=true? It'll skip
> >> flushing stats for that particular lock type, but then it'll happily
> >> reset the pending stats anyway, forgetting the stats.
> >>
> >> AFAIK it should keep the pending stats, and flush them sometime lager,
> >> when the lock is not contended. That's what the other flush callbacks
> >> do, at least. This probably means it needs to reset the entries one by
> >> one, not the whole struct at once.
> >
> > Oh right, it's currently misbehaving, thanks for the warning!
>
> I should not have missed that,
So do I...
Regards,
--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com
^ permalink raw reply [nested|flat] 61+ messages in thread
* Re: Adding locks statistics
@ 2026-04-07 23:30 Michael Paquier <michael@paquier.xyz>
parent: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
0 siblings, 0 replies; 61+ messages in thread
From: Michael Paquier @ 2026-04-07 23:30 UTC (permalink / raw)
To: Bertrand Drouvot <bertranddrouvot.pg@gmail.com>; +Cc: Andres Freund <andres@anarazel.de>; Jeff Davis <pgsql@j-davis.com>; Greg Sabino Mullane <htamfids@gmail.com>; pgsql-hackers@lists.postgresql.org
On Tue, Apr 07, 2026 at 06:01:08AM +0000, Bertrand Drouvot wrote:
> That looks to work, thanks! But I was wondering if this new version is not
> introducing a new race: the injection point is not local anymore so it could be
> that another process reach the new injection point. That said, even if this is
> the case I think we're ok since s2 is using "query_until" so we could say that
> "at least" s2 reached the injection point. The new version does not ensure that
> "only" s2 reached the injection point but I think that's safe.
Yes, the lookups based on pg_stat_activity should be enough, I hope.
From what I can see, the buildfarm is silent this morning for this
test, as much as [1] in the CI, so I'd like to think that we are done
here. Again, I'm hoping so.
[1]: https://cfbot.cputube.org/highlights/all.html
--
Michael
Attachments:
[application/pgp-signature] signature.asc (832B, ../../adWTnO4ulh1WDIGa@paquier.xyz/2-signature.asc)
download
^ permalink raw reply [nested|flat] 61+ messages in thread
end of thread, other threads:[~2026-04-07 23:30 UTC | newest]
Thread overview: 61+ messages (download: mbox mbox.gz follow: Atom feed)
-- links below jump to the message on this page --
2025-08-01 09:49 Adding locks statistics Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2025-08-11 17:54 ` Greg Sabino Mullane <htamfids@gmail.com>
2025-08-11 21:53 ` Jeff Davis <pgsql@j-davis.com>
2025-08-11 23:44 ` Michael Paquier <michael@paquier.xyz>
2025-08-11 23:49 ` Tom Lane <tgl@sss.pgh.pa.us>
2025-08-12 09:49 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2025-08-12 09:37 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2025-08-12 15:42 ` Jeff Davis <pgsql@j-davis.com>
2025-08-13 03:55 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2025-12-15 16:48 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-02-10 07:30 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-02-13 07:36 ` Michael Paquier <michael@paquier.xyz>
2026-02-13 10:24 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-02-13 21:13 ` Andres Freund <andres@anarazel.de>
2026-02-16 04:18 ` Michael Paquier <michael@paquier.xyz>
2026-02-16 10:10 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-02-16 21:00 ` Andres Freund <andres@anarazel.de>
2026-02-17 16:33 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-02-19 04:06 ` Michael Paquier <michael@paquier.xyz>
2026-02-19 12:03 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-02-19 18:27 ` Andres Freund <andres@anarazel.de>
2026-02-20 06:38 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-02-20 16:02 ` Andres Freund <andres@anarazel.de>
2026-02-20 17:26 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-02-27 05:14 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-03-18 04:36 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-03-18 08:15 ` Michael Paquier <michael@paquier.xyz>
2026-03-18 14:51 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-03-19 07:01 ` Michael Paquier <michael@paquier.xyz>
2026-03-19 12:25 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-03-21 05:55 ` Michael Paquier <michael@paquier.xyz>
2026-03-21 20:01 ` Álvaro Herrera <alvherre@kurilemu.de>
2026-03-22 19:37 ` Álvaro Herrera <alvherre@kurilemu.de>
2026-03-23 08:18 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-03-24 03:15 ` Michael Paquier <michael@paquier.xyz>
2026-03-24 16:16 ` Álvaro Herrera <alvherre@kurilemu.de>
2026-03-25 03:32 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-03-24 07:02 ` Michael Paquier <michael@paquier.xyz>
2026-03-24 20:09 ` Andres Freund <andres@anarazel.de>
2026-03-24 23:46 ` Michael Paquier <michael@paquier.xyz>
2026-03-25 03:15 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-03-25 05:25 ` Michael Paquier <michael@paquier.xyz>
2026-03-25 07:24 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-03-25 19:54 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-03-25 23:06 ` Michael Paquier <michael@paquier.xyz>
2026-03-26 04:05 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-03-26 06:53 ` Michael Paquier <michael@paquier.xyz>
2026-04-06 00:05 ` Michael Paquier <michael@paquier.xyz>
2026-04-06 05:22 ` Andres Freund <andres@anarazel.de>
2026-04-06 06:19 ` Michael Paquier <michael@paquier.xyz>
2026-04-06 06:34 ` Michael Paquier <michael@paquier.xyz>
2026-04-07 04:21 ` Michael Paquier <michael@paquier.xyz>
2026-04-07 06:01 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-04-07 23:30 ` Michael Paquier <michael@paquier.xyz>
2026-03-30 16:11 ` Tomas Vondra <tomas@vondra.me>
2026-03-31 07:10 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2026-04-06 05:11 ` Michael Paquier <michael@paquier.xyz>
2026-04-07 06:02 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2025-08-12 09:32 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
2025-08-12 12:54 ` Greg Sabino Mullane <htamfids@gmail.com>
2025-08-13 03:20 ` Bertrand Drouvot <bertranddrouvot.pg@gmail.com>
This inbox is served by DDX for PostgreSQL; see mirroring instructions
for how to clone and mirror all data and code used for this inbox