From 3824f5d8801144848a8531eb7f7f6e86a73397f8 Mon Sep 17 00:00:00 2001
From: Greg Burd <greg@burd.me>
Date: Sun, 11 Oct 2026 17:44:37 +0000
Subject: [PATCH v20261011 07/12] clock-sweep: let the background writer spend
 chances when the sweep starves

The background writer already walks ahead of the clock hand writing dirty
buffers, so that an allocating backend finds clean victims instead of waiting
on a write.  It can help the same way with the other half of the problem.  A
buffer it passes that is unpinned but still holds its second chance is one the
hand will have to spend a visit on; spending it here means the hand finds a
candidate instead.

Do that only while the sweep is actually starving.  StrategySweepClaims()
counts allocations that gave up on the second-chance rule because they could
not find a candidate, and the writer compares successive readings: while the
count is rising it spends chances as it scans, and while it is flat it leaves
the rule alone.  On a workload that never starves the sweep this changes
nothing.

A buffer whose chance is spent this way counts toward the writer's
reusable-buffer target, because a candidate is exactly what the caller was
short of.  The buffer still survives the cycle, just as it would have if the
hand had been the one to pass it.  The checkpointer passes false: it writes to
a deadline and has no interest in replacement.
---
 src/backend/storage/buffer/bufmgr.c | 45 ++++++++++++++++++++++++++---
 1 file changed, 41 insertions(+), 4 deletions(-)

diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 6aecbd7094a..2410ed60acd 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -636,7 +636,7 @@ static void PinBuffer_Locked(BufferDesc *buf);
 static void UnpinBuffer(BufferDesc *buf);
 static void UnpinBufferNoOwner(BufferDesc *buf);
 static void BufferSync(int flags);
-static int	SyncOneBuffer(int buf_id, bool skip_recently_used,
+static int	SyncOneBuffer(int buf_id, bool skip_recently_used, bool spend_chance,
 						  WritebackContext *wb_context);
 static void WaitIO(BufferDesc *buf);
 static void AbortBufferIO(Buffer buffer);
@@ -3795,7 +3795,7 @@ BufferSync(int flags)
 		 */
 		if (pg_atomic_read_u64(&bufHdr->state) & BM_CHECKPOINT_NEEDED)
 		{
-			if (SyncOneBuffer(buf_id, false, &wb_context) & BUF_WRITTEN)
+			if (SyncOneBuffer(buf_id, false, false, &wb_context) & BUF_WRITTEN)
 			{
 				TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
 				PendingCheckpointerStats.buffers_written++;
@@ -3899,6 +3899,11 @@ BgBufferSync(WritebackContext *wb_context)
 	int			num_to_scan;
 	int			num_written;
 	int			reusable_buffers;
+	bool		spend_chance;
+	uint64		sweep_claims;
+
+	/* Claim count as of our previous cycle, to get a rate rather than a total */
+	static uint64 prev_sweep_claims = 0;
 
 	/* Variables for final smoothed_density update */
 	long		new_strategy_delta;
@@ -4080,11 +4085,23 @@ BgBufferSync(WritebackContext *wb_context)
 	num_written = 0;
 	reusable_buffers = reusable_buffers_est;
 
+	/*
+	 * Decide whether to spend second chances as we scan.  StrategySweepClaims()
+	 * counts allocations that gave up on the second-chance rule because the
+	 * sweep could not find a candidate; while that count is rising the sweep is
+	 * starving and the cheapest help we can give is to make candidates ahead of
+	 * the hand.  While it is flat we leave the rule alone, so this costs
+	 * nothing on a workload that does not need it.
+	 */
+	sweep_claims = StrategySweepClaims();
+	spend_chance = (sweep_claims > prev_sweep_claims);
+	prev_sweep_claims = sweep_claims;
+
 	/* Execute the LRU scan */
 	while (num_to_scan > 0 && reusable_buffers < upcoming_alloc_est)
 	{
 		int			sync_state = SyncOneBuffer(next_to_clean, true,
-											   wb_context);
+											   spend_chance, wb_context);
 
 		if (++next_to_clean >= NBuffers)
 		{
@@ -4147,6 +4164,11 @@ BgBufferSync(WritebackContext *wb_context)
 /*
  * SyncOneBuffer -- process a single buffer during syncing.
  *
+ * If spend_chance is true, an unpinned buffer that still has its second
+ * chance has it spent here, which makes it reclaimable by the next pass of
+ * the clock hand.  The caller sets this only while the sweep is starving for
+ * candidates; see BgBufferSync().
+ *
  * If skip_recently_used is true, we don't write currently-pinned buffers, nor
  * buffers marked recently used, as these are not replacement candidates.
  *
@@ -4159,7 +4181,8 @@ BgBufferSync(WritebackContext *wb_context)
  * after locking it, but we don't care all that much.)
  */
 static int
-SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
+SyncOneBuffer(int buf_id, bool skip_recently_used, bool spend_chance,
+			  WritebackContext *wb_context)
 {
 	BufferDesc *bufHdr = GetBufferDescriptor(buf_id);
 	int			result = 0;
@@ -4186,6 +4209,20 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
 	{
 		result |= BUF_REUSABLE;
 	}
+	else if (spend_chance &&
+			 BUF_STATE_GET_REFCOUNT(buf_state) == 0 &&
+			 BUF_STATE_HAS_SECOND_CHANCE(buf_state))
+	{
+		/*
+		 * Unpinned, but it still has its chance.  Spend it here so the next
+		 * pass of the hand can reclaim the buffer, and count it toward our
+		 * target: we have made a candidate, which is what the caller is short
+		 * of.  The buffer survives this cycle either way, exactly as it would
+		 * have if the hand had passed it.
+		 */
+		UnlockBufHdrExt(bufHdr, buf_state, 0, BM_SECOND_CHANCE, 0);
+		return result | BUF_REUSABLE;
+	}
 	else if (skip_recently_used)
 	{
 		/* Caller told us not to write recently-used buffers */
-- 
2.56.0

