From 7e2fe005525d59c4def5ac0f1060d3d4bfa43351 Mon Sep 17 00:00:00 2001
From: Alexandre Felipe <o.alexandre.felipe@gmail.com>
Date: Mon, 17 Aug 2026 18:19:43 +0100
Subject: [PATCH 1/5] Benchmark

This adds a module for benchmarking LWLocks in a tight loop.
This time I am structuring the benchmark module as a generic one that
could be extended in the future.

The idea is that under the module microbench a contributor that trying
to do a low level benchmark could write a new component there and
easily get pooled measurements exposed via SQL.

microbench-head-1.0.sql implements a standard processing helping to
understand not only the average performance but also rank statistics.

e.g.
     op      |  avg  | min  | [q1  | med  | q3]  |  max  | std
-------------+-------+------+------+------+------+-------+------
 spin-lock   | 10.07 | 9.11 | 9.77 | 9.77 | 9.77 | 19.21 | 1.20
 LWLock-ex   |  8.26 | 7.16 | 7.81 | 8.13 | 8.14 | 17.25 | 0.74
 LWLock-sh   |  8.18 | 7.16 | 7.81 | 7.81 | 8.14 | 10.74 | 0.72
 LWLock-cond |  6.81 | 6.18 | 6.84 | 6.84 | 6.84 | 34.18 | 0.95
 nop         |  1.00 | 0.32 | 0.98 | 0.98 | 0.98 |  1.95 | 0.11
(5 rows)

In this particular exmaple, spin-lock is the LockBufHdr and
UnlockBufHdr, LWLock-ex and LWLock-sh represents LWLockAcquire
followed by LWLockRelease, in exclusive and shared mode, respectively.
LWLock-cond is a conditional shared lock followed by a LWLockRelease.
nop is just reading the lwlock pointer in the loop and gives a
baseline.

Where statistics of average times of each measurement are computed as
follows:
avg: average time per
min: above 0% of the data (obviously)
q1: end of first quartile, above 25%
med: short for median, above 50%
q2: end of third quartile, above 75%
max: above 100% of the data (obviously)
std: standard deviation, measures how dispersed is the data.

To reduce the impact of the time measurement itself one can
repeat the operation a large number of times in a loop between
two timer observations. One looses the individual measurements
but the averages are still going to reflect to some extent the
variability of the operation itself.
---
 src/test/modules/microbench/Makefile          |  61 ++++++++
 src/test/modules/microbench/lwlock/bench.c    | 123 ++++++++++++++++
 .../modules/microbench/lwlock/install.sql     |  10 ++
 src/test/modules/microbench/lwlock/query.sql  |   8 ++
 src/test/modules/microbench/meson.build       |  33 +++++
 .../microbench/microbench-head--1.0.sql       |  44 ++++++
 .../modules/microbench/microbench.control     |   4 +
 src/test/modules/microbench/randomize.h       |  23 +++
 .../modules/microbench/scripts/run-test.sh    | 135 ++++++++++++++++++
 src/test/modules/microbench/timing-magic.h    |  30 ++++
 10 files changed, 471 insertions(+)
 create mode 100644 src/test/modules/microbench/Makefile
 create mode 100644 src/test/modules/microbench/lwlock/bench.c
 create mode 100644 src/test/modules/microbench/lwlock/install.sql
 create mode 100644 src/test/modules/microbench/lwlock/query.sql
 create mode 100644 src/test/modules/microbench/meson.build
 create mode 100644 src/test/modules/microbench/microbench-head--1.0.sql
 create mode 100644 src/test/modules/microbench/microbench.control
 create mode 100644 src/test/modules/microbench/randomize.h
 create mode 100755 src/test/modules/microbench/scripts/run-test.sh
 create mode 100644 src/test/modules/microbench/timing-magic.h

diff --git a/src/test/modules/microbench/Makefile b/src/test/modules/microbench/Makefile
new file mode 100644
index 00000000000..4cdbe4ff0c7
--- /dev/null
+++ b/src/test/modules/microbench/Makefile
@@ -0,0 +1,61 @@
+# src/test/modules/microbench/Makefile
+
+PGFILEDESC = "microbench - suite of functions for micro-benchmarks"
+
+EXTENSION = microbench
+MODULE_big = microbench
+
+# One subdir per benchmark: <name>/bench.c, <name>/install.sql, <name>/query.sql
+MICROBENCH_TESTS := $(sort $(patsubst %/bench.c,%,$(wildcard */bench.c)))
+SQL_FRAGMENTS := $(addsuffix /install.sql,$(MICROBENCH_TESTS))
+
+MICROBENCH_SQL_HEAD = microbench-head--1.0.sql
+MICROBENCH_SQL_BUILT = microbench--1.0.sql
+
+OBJS = \
+	$(WIN32RES) \
+	$(addsuffix /bench.o,$(MICROBENCH_TESTS))
+
+# Generated extension script; must be DATA_built so all/install depend on it.
+DATA_built = $(MICROBENCH_SQL_BUILT)
+
+ifdef USE_PGXS
+PG_CONFIG = pg_config
+PGXS := $(shell $(PG_CONFIG) --pgxs)
+include $(PGXS)
+else
+subdir = src/test/modules/microbench
+top_builddir = ../../../..
+include $(top_builddir)/src/Makefile.global
+include $(top_srcdir)/contrib/contrib-global.mk
+
+# Link the module against an explicit postgres binary when requested, e.g.
+# make install MICROBENCH_POSTGRES=/path/to/bin/postgres
+ifdef MICROBENCH_POSTGRES
+BE_DLLLIBS := -bundle_loader $(MICROBENCH_POSTGRES)
+endif
+endif
+
+$(MICROBENCH_SQL_BUILT): $(addprefix $(srcdir)/,$(MICROBENCH_SQL_HEAD)) \
+		$(addprefix $(srcdir)/,$(SQL_FRAGMENTS))
+	cat $^ > $@
+
+# Incremental compile of a single benchmark folder, e.g. make lwlock
+$(MICROBENCH_TESTS): %: %/bench.o
+
+%/bench.o: %/bench.c randomize.h timing-magic.h
+	$(COMPILE.c) -I. -o $@ $<
+
+.PHONY: run tests list $(MICROBENCH_TESTS)
+
+tests: $(MICROBENCH_TESTS)
+
+list:
+	@echo $(MICROBENCH_TESTS)
+
+run:
+ifndef TEST
+	$(error TEST is required, e.g. make run TEST=lwlock)
+endif
+	@test -d '$(TEST)' || (echo "unknown test: $(TEST)" && exit 1)
+	$(SHELL) '$(srcdir)/scripts/run-test.sh' '$(TEST)'
diff --git a/src/test/modules/microbench/lwlock/bench.c b/src/test/modules/microbench/lwlock/bench.c
new file mode 100644
index 00000000000..33d75770a0d
--- /dev/null
+++ b/src/test/modules/microbench/lwlock/bench.c
@@ -0,0 +1,123 @@
+/*-------------------------------------------------------------------------
+ *
+ * lwlock/bench.c
+ *		Micro-benchmark of LWLock acquire/release paths.
+ *
+ * IDENTIFICATION
+ *	  src/test/modules/microbench/lwlock/bench.c
+ *
+ *-------------------------------------------------------------------------
+ */
+#include "postgres.h"
+
+#include "fmgr.h"
+#include "funcapi.h"
+#include "portability/instr_time.h"
+#include "storage/buf_internals.h"
+#include "storage/lwlock.h"
+#include "utils/builtins.h"
+#include "utils/tuplestore.h"
+
+#include "randomize.h"
+#include "timing-magic.h"
+
+PG_MODULE_MAGIC;
+
+PG_FUNCTION_INFO_V1(bench_lwlock);
+
+/*
+ * bench_lwlock(n, rounds, randomize) -> SETOF
+ *     (op text, avg_ns float8, batch_size int8, id int8)
+ */
+Datum
+bench_lwlock(PG_FUNCTION_ARGS)
+{
+	int64			n = PG_GETARG_INT64(0);
+	int64			rounds = PG_ARGISNULL(1) ? 1 : PG_GETARG_INT64(1);
+	bool			randomize = PG_ARGISNULL(2) ? true : PG_GETARG_BOOL(2);
+	ReturnSetInfo  *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+	LWLock		  **locks;
+	volatile int64	sink PG_USED_FOR_ASSERTS_ONLY = 0;
+
+	if (n <= 0 || rounds <= 0)
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("n and rounds must be positive")));
+
+	if (n > NUM_BUFFER_PARTITIONS)
+		n = NUM_BUFFER_PARTITIONS;
+
+	InitMaterializedSRF(fcinfo, 0);
+
+	locks = palloc(sizeof(LWLock *) * n);
+	if (randomize)
+	{
+		pg_prng_state rng;
+
+		pg_prng_seed(&rng, 0xDA7ABA5E);
+		for (int i = 0; i < n; i++)
+			locks[i] = BufMappingPartitionLock(i);
+		shuffle_pointers(&rng, (void **) locks, (int) n);
+	}
+	else
+	{
+		for (int i = 0; i < n; i++)
+			locks[i] = BufMappingPartitionLock(0);
+	}
+
+	if (!timing_initialized)
+		pg_initialize_timing();
+
+	for (int64 r = 0; r < rounds; r++)
+	{
+		INIT_TIMING_SCOPE();
+		{
+			BufferDesc *buf_desc = GetBufferDescriptor(1);
+			BEGIN_TIMING("spin-lock", n)
+			{
+				LockBufHdr(buf_desc);
+				UnlockBufHdr(buf_desc);
+			}
+			END_TIMING;
+		}
+
+		BEGIN_TIMING("LWLock-ex", n)
+		{
+			LWLock	   *lock = locks[i];
+
+			LWLockAcquire(lock, LW_EXCLUSIVE);
+			LWLockRelease(lock);
+		}
+		END_TIMING;
+
+		BEGIN_TIMING("LWLock-sh", n)
+		{
+			LWLock	   *lock = locks[i];
+
+			LWLockAcquire(lock, LW_SHARED);
+			LWLockRelease(lock);
+		}
+		END_TIMING;
+
+		BEGIN_TIMING("LWLock-cond", n)
+		{
+			LWLock	   *lock = locks[i];
+
+			if (!LWLockConditionalAcquire(lock, LW_SHARED))
+				elog(ERROR, "Failed to acquire lock");
+			LWLockRelease(lock);
+		}
+		END_TIMING;
+
+		BEGIN_TIMING("nop", n)
+		{
+			LWLock	   *lock = locks[i];
+
+			sink += (int64) (uintptr_t) lock;
+		}
+		END_TIMING;
+	}
+
+	(void) sink;
+	return (Datum) 0;
+}
diff --git a/src/test/modules/microbench/lwlock/install.sql b/src/test/modules/microbench/lwlock/install.sql
new file mode 100644
index 00000000000..9bf234837b1
--- /dev/null
+++ b/src/test/modules/microbench/lwlock/install.sql
@@ -0,0 +1,10 @@
+CREATE FUNCTION bench_lwlock(
+	IN n int8,
+	IN rounds int8 DEFAULT 1,
+	IN random bool DEFAULT true
+)
+RETURNS SETOF microbench_sample
+AS 'MODULE_PATHNAME', 'bench_lwlock'
+LANGUAGE C;
+
+REVOKE ALL ON FUNCTION bench_lwlock(int8, int8, bool) FROM PUBLIC;
diff --git a/src/test/modules/microbench/lwlock/query.sql b/src/test/modules/microbench/lwlock/query.sql
new file mode 100644
index 00000000000..16c1fd1dfe4
--- /dev/null
+++ b/src/test/modules/microbench/lwlock/query.sql
@@ -0,0 +1,8 @@
+-- lwlock micro-benchmark query
+
+\pset format aligned
+
+SELECT * FROM format_microbench(
+	(SELECT array_agg(s ORDER BY s.id)
+	 FROM bench_lwlock(:'n'::int8, :'rounds'::int8, true) AS s)
+);
diff --git a/src/test/modules/microbench/meson.build b/src/test/modules/microbench/meson.build
new file mode 100644
index 00000000000..9836640a3df
--- /dev/null
+++ b/src/test/modules/microbench/meson.build
@@ -0,0 +1,33 @@
+# Copyright (c) 2024-2026, PostgreSQL Global Development Group
+
+microbench_tests = [
+  'lwlock',
+]
+
+microbench_sources = files(
+  '@0@/bench.c'.format(test) for test in microbench_tests
+)
+
+if host_system == 'windows'
+  microbench_sources += rc_lib_gen.process(win32ver_rc, extra_args: [
+    '--NAME', 'microbench',
+    '--FILEDESC', 'microbench - micro-benchmark suite',])
+endif
+
+microbench_sql = custom_target('microbench--1.0.sql',
+  output: 'microbench--1.0.sql',
+  input: files('microbench--1.0.sql.head') + files(
+    '@0@/install.sql'.format(test) for test in microbench_tests
+  ),
+  command: [find_program('cat'), '@INPUT@'],
+  build_by_default: true,
+)
+
+microbench = shared_module('microbench',
+  microbench_sources,
+  kwargs: pg_test_mod_args,
+)
+test_install_libs += microbench
+
+test_install_data += microbench_sql
+test_install_data += files('microbench.control')
diff --git a/src/test/modules/microbench/microbench-head--1.0.sql b/src/test/modules/microbench/microbench-head--1.0.sql
new file mode 100644
index 00000000000..48c5143843a
--- /dev/null
+++ b/src/test/modules/microbench/microbench-head--1.0.sql
@@ -0,0 +1,44 @@
+/* src/test/modules/microbench/microbench--1.0.sql */
+/* Generated from microbench--1.0.sql.head and per-test install.sql files. */
+
+-- complain if script is sourced in psql, rather than via CREATE EXTENSION
+\echo Use "CREATE EXTENSION microbench" to load this file. \quit
+
+CREATE DOMAIN microbench_format AS numeric(15, 2);
+
+CREATE TYPE microbench_sample AS (
+  op         text,
+  avg_ns     float8,
+  batch_size int8,
+  id         int8
+);
+
+CREATE TYPE microbench_stats AS (
+  op            text,
+  avg           microbench_format,
+  min           microbench_format,
+  q1            microbench_format,
+  med           microbench_format,
+  q3            microbench_format,
+  max           microbench_format,
+  std           microbench_format
+);
+
+CREATE FUNCTION format_microbench(samples microbench_sample[])
+RETURNS SETOF microbench_stats
+LANGUAGE sql
+STABLE
+AS $$
+  SELECT
+    s.op,
+    avg(s.avg_ns),
+    min(s.avg_ns),
+    percentile_cont(0.25) WITHIN GROUP (ORDER BY s.avg_ns),
+    percentile_cont(0.50) WITHIN GROUP (ORDER BY s.avg_ns),
+    percentile_cont(0.75) WITHIN GROUP (ORDER BY s.avg_ns),
+    max(s.avg_ns),
+    stddev(s.avg_ns)
+  FROM unnest(samples) AS s
+  GROUP BY s.op
+  ORDER BY min(s.id);
+$$;
diff --git a/src/test/modules/microbench/microbench.control b/src/test/modules/microbench/microbench.control
new file mode 100644
index 00000000000..16aaf002517
--- /dev/null
+++ b/src/test/modules/microbench/microbench.control
@@ -0,0 +1,4 @@
+comment = 'micro-benchmarks for LWLock acquire/release paths'
+default_version = '1.0'
+module_pathname = '$libdir/microbench'
+relocatable = true
diff --git a/src/test/modules/microbench/randomize.h b/src/test/modules/microbench/randomize.h
new file mode 100644
index 00000000000..e6eb3e32542
--- /dev/null
+++ b/src/test/modules/microbench/randomize.h
@@ -0,0 +1,23 @@
+#ifndef MICROBENCH_RANDOMIZE_H
+#define MICROBENCH_RANDOMIZE_H
+
+#include "common/pg_prng.h"
+
+/*
+ * Fisher-Yates shuffle of a pointer array.
+ * https://en.wikipedia.org/wiki/Fisher-Yates_shuffle
+ */
+static inline void
+shuffle_pointers(pg_prng_state *rng, void **ptrs, int count)
+{
+	for (int i = count - 1; i > 0; i--)
+	{
+		int			k = (int) pg_prng_int64_range(rng, 0, i);
+		void	   *tmp = ptrs[i];
+
+		ptrs[i] = ptrs[k];
+		ptrs[k] = tmp;
+	}
+}
+
+#endif
diff --git a/src/test/modules/microbench/scripts/run-test.sh b/src/test/modules/microbench/scripts/run-test.sh
new file mode 100755
index 00000000000..ee9e29d7b49
--- /dev/null
+++ b/src/test/modules/microbench/scripts/run-test.sh
@@ -0,0 +1,135 @@
+#!/usr/bin/env bash
+#
+# Run one microbench test folder.
+#
+# Usage: run-test.sh TEST
+# Env:   TOP_BUILDDIR, PG_CONFIG, MICROBENCH_PORT (default 55432),
+#        MICROBENCH_N (default 128), MICROBENCH_ROUNDS (default 1000)
+#
+set -euo pipefail
+
+TEST=${1:?usage: run-test.sh TEST}
+
+SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
+MODULE_DIR=$(cd "$SCRIPT_DIR/.." && pwd)
+TOP_BUILDDIR=${TOP_BUILDDIR:-$(cd "$MODULE_DIR/../../../.." && pwd)}
+PORT=${MICROBENCH_PORT:-55432}
+LOGDIR="$MODULE_DIR/.tmp_check/log"
+DATADIR="$MODULE_DIR/.tmp_check/data"
+N=${MICROBENCH_N:-128}
+ROUNDS=${MICROBENCH_ROUNDS:-1000}
+
+log() { printf '%s\n' "$*" >&2; }
+
+test -f "$MODULE_DIR/$TEST/query.sql" || {
+	log "missing $MODULE_DIR/$TEST/query.sql"
+	exit 1
+}
+pick_pg_config() {
+	if [[ -n "${PG_CONFIG:-}" && -x "$PG_CONFIG" ]]; then
+		printf '%s\n' "$PG_CONFIG"
+		return
+	fi
+
+	local makefile_global="$TOP_BUILDDIR/src/Makefile.global"
+	if [[ -f "$makefile_global" ]]; then
+		local prefix candidate
+
+		prefix=$(sed -n 's/^prefix := //p' "$makefile_global" | head -1)
+		if [[ -n "$prefix" ]]; then
+			candidate="$prefix/bin/pg_config"
+			if [[ -x "$candidate" ]]; then
+				printf '%s\n' "$candidate"
+				return
+			fi
+			log "configured prefix $prefix has no executable pg_config at $candidate"
+		fi
+	fi
+
+	return 1
+}
+
+PG_CONFIG=$(pick_pg_config) || {
+	log "no PostgreSQL install found; set PG_CONFIG or run configure && make install at repo root"
+	exit 1
+}
+
+BINDIR=$("$PG_CONFIG" --bindir)
+LIBDIR=$("$PG_CONFIG" --libdir)
+log "==> using $($PG_CONFIG --version) [$BINDIR]"
+
+log "==> building and installing microbench..."
+make -C "$MODULE_DIR" MICROBENCH_POSTGRES="$BINDIR/postgres" install
+
+export PATH="$BINDIR:$PATH"
+case "$(uname -s)" in
+	Darwin) export DYLD_LIBRARY_PATH="$LIBDIR:${DYLD_LIBRARY_PATH:-}" ;;
+	*) export LD_LIBRARY_PATH="$LIBDIR:${LD_LIBRARY_PATH:-}" ;;
+esac
+
+mkdir -p "$LOGDIR"
+
+postgres_build_id() {
+	# Re-init when the installed postgres binary changes (catalog bumps, rebuilds).
+	printf '%s:%s' "$("$BINDIR/postgres" --version)" \
+		"$(shasum -a 256 "$BINDIR/postgres" | awk '{print $1}')"
+}
+
+ensure_datadir() {
+	local build_id stamp_file="$DATADIR/.microbench_build_id"
+
+	build_id=$(postgres_build_id)
+	if [[ -f "$DATADIR/PG_VERSION" && -f "$stamp_file" && "$(cat "$stamp_file")" == "$build_id" ]]; then
+		return 0
+	fi
+
+	if [[ -f "$DATADIR/PG_VERSION" ]]; then
+		log "==> stale datadir (postgres rebuilt); re-initdb..."
+	else
+		log "==> initdb..."
+	fi
+
+	rm -rf "$DATADIR"
+	"$BINDIR/initdb" -D "$DATADIR" --auth trust --no-sync --no-instructions -N \
+		>"$LOGDIR/initdb.log" 2>&1
+	printf '%s\n' "$build_id" > "$stamp_file"
+}
+
+ensure_datadir
+
+cleanup() {
+	if "$BINDIR/pg_ctl" -D "$DATADIR" status >/dev/null 2>&1; then
+		"$BINDIR/pg_ctl" -D "$DATADIR" stop -m fast >>"$LOGDIR/pg_ctl.log" 2>&1 || true
+	fi
+}
+trap cleanup EXIT
+
+if ! "$BINDIR/pg_ctl" -D "$DATADIR" status >/dev/null 2>&1; then
+	log "==> starting postgres on port $PORT..."
+	if ! "$BINDIR/pg_ctl" -D "$DATADIR" -l "$LOGDIR/postgres.log" \
+		-o "-p $PORT -F -h '' -c shared_buffers=128MB" start \
+		>>"$LOGDIR/pg_ctl.log" 2>&1; then
+		if grep -q 'incompatible with server' "$LOGDIR/postgres.log"; then
+			log "==> postgres rejected datadir; re-initdb..."
+			rm -rf "$DATADIR"
+			ensure_datadir
+			"$BINDIR/pg_ctl" -D "$DATADIR" -l "$LOGDIR/postgres.log" \
+				-o "-p $PORT -F -h '' -c shared_buffers=128MB" start \
+				>>"$LOGDIR/pg_ctl.log" 2>&1
+		else
+			log "pg_ctl start failed; see $LOGDIR/postgres.log and $LOGDIR/pg_ctl.log"
+			exit 1
+		fi
+	fi
+fi
+
+log "==> CREATE EXTENSION microbench"
+: >"$LOGDIR/psql.log"
+"$BINDIR/psql" -v ON_ERROR_STOP=1 -p "$PORT" -d postgres \
+	-c "DROP EXTENSION IF EXISTS microbench CASCADE; CREATE EXTENSION microbench;" \
+	>>"$LOGDIR/psql.log" 2>&1
+
+log "==> running $TEST/query.sql (n=$N rounds=$ROUNDS)"
+"$BINDIR/psql" -v ON_ERROR_STOP=1 -p "$PORT" -d postgres \
+	-v n="$N" -v rounds="$ROUNDS" \
+	-f "$MODULE_DIR/$TEST/query.sql"
diff --git a/src/test/modules/microbench/timing-magic.h b/src/test/modules/microbench/timing-magic.h
new file mode 100644
index 00000000000..54318841f64
--- /dev/null
+++ b/src/test/modules/microbench/timing-magic.h
@@ -0,0 +1,30 @@
+#ifndef MICROBENCH_TIMING_MAGIC_H
+#define MICROBENCH_TIMING_MAGIC_H
+
+#include "portability/instr_time.h"
+
+#define INIT_TIMING_SCOPE() \
+	int64 timing_operation_id = 0
+
+#define BEGIN_TIMING(name, n) \
+	do { \
+		instr_time t0, t1, dt; \
+		Datum values[4]; \
+		bool nulls[4] = {0}; \
+		values[0] = CStringGetTextDatum(name); \
+		INSTR_TIME_SET_CURRENT_FAST(t0); \
+		for (int64 i = 0; i < (n); ++i) \
+		{
+
+#define END_TIMING \
+		} \
+		INSTR_TIME_SET_CURRENT_FAST(t1); \
+		INSTR_TIME_SET_ZERO(dt); \
+		INSTR_TIME_ACCUM_DIFF(dt, t1, t0); \
+		values[1] = Float8GetDatum((double) INSTR_TIME_GET_NANOSEC(dt) / (double) (n)); \
+		values[2] = Int64GetDatum(n); \
+		values[3] = Int64GetDatum(++timing_operation_id); \
+		tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls); \
+	} while (0)
+
+#endif
-- 
2.53.0

