From 836c406337edda3a7e440d020d7d5a229f7e1099 Mon Sep 17 00:00:00 2001
From: Thomas Munro <thomas.munro@gmail.com>
Date: Tue, 18 Mar 2025 22:27:36 +1300
Subject: [PATCH 1/2] Teach Parallel Bitmap Heap Scan to combine I/O.

As 56788d21 did for parallel sequential scans, make TIDBitmap preserve
locality when giving block numbers to parallel workers, instead of
generating a stream of little islands with gaps in between.

Now it will fetch at least 8 block numbers from the shared bitmap at
once and then more if required keep opportunities to combine I/O.  The
exception is at the start of the scan, following a common pattern for
such things.

XXX Not done: ramp down at the end of the scan, for fair parallel work
allocation.

Even when no I/O is involved, it didn't seem ideal to hand out single
blocks under an exclusive lock.

XXX But 8 was chosen arbitrarily.

Discussion: https://postgr.es/m/CAAKRu_YhYy6Te5ZNiijtayr6mM-7O0B3UXm7mFOCN8U%3DiFgoxg%40mail.gmail.com
---
 src/backend/nodes/tidbitmap.c | 228 ++++++++++++++++++++++++----------
 1 file changed, 161 insertions(+), 67 deletions(-)

diff --git a/src/backend/nodes/tidbitmap.c b/src/backend/nodes/tidbitmap.c
index 41031aa8f2f..d9e6a957de4 100644
--- a/src/backend/nodes/tidbitmap.c
+++ b/src/backend/nodes/tidbitmap.c
@@ -44,6 +44,7 @@
 #include "common/int.h"
 #include "nodes/bitmapset.h"
 #include "nodes/tidbitmap.h"
+#include "storage/bufmgr.h"
 #include "storage/lwlock.h"
 #include "utils/dsa.h"
 
@@ -74,6 +75,14 @@
 /* number of active words for a lossy chunk: */
 #define WORDS_PER_CHUNK  ((PAGES_PER_CHUNK - 1) / BITS_PER_BITMAPWORD + 1)
 
+/*
+ * The number of blocks to fetch at once when iterating through a shared
+ * bitmap in a parallel query to reduce lock contention.  The number actually
+ * fetched may be higher if trailing sequential neighbors are included to
+ * encourage I/O combining.
+ */
+#define TBM_SHARED_FETCH 8
+
 /*
  * The hashtable entries are represented by this data structure.  For
  * an exact page, blockno is the page number and bit k of the bitmap
@@ -212,6 +221,12 @@ struct TBMSharedIterator
 	PTEntryArray *ptbase;		/* pagetable element array */
 	PTIterationArray *ptpages;	/* sorted exact page index list */
 	PTIterationArray *ptchunks; /* sorted lossy page index list */
+
+	/* Results fetched from shared memory in bulk, ready to be consumed. */
+	int			result_index;
+	int			result_count;
+	int			result_capacity;
+	TBMIterateResult results[FLEXIBLE_ARRAY_MEMBER];
 };
 
 /* Local function prototypes */
@@ -1046,6 +1061,34 @@ tbm_private_iterate(TBMPrivateIterator *iterator, TBMIterateResult *tbmres)
 	return false;
 }
 
+/*
+ * Decide whether to claim a candidate block for the current batch or stop
+ * here without including it.
+ */
+static inline bool
+tbm_shared_iterate_include_block_p(TBMSharedIterator *iterator,
+								   BlockNumber candidate_blockno,
+								   BlockNumber *next_blockno,
+								   int *seq_count)
+{
+	/* Track the length of the current sequential cluster of blocks. */
+	if (candidate_blockno == *next_blockno)
+		*seq_count += 1;
+	else
+		*seq_count = 1;
+	*next_blockno = candidate_blockno + 1;
+
+	/* If we haven't reached the target batch size yet, then definitely. */
+	if (iterator->result_count < TBM_SHARED_FETCH)
+		return true;
+
+	/*
+	 * We're prepared to go bigger, but only to avoid an arbitrary break in a
+	 * potential combined I/O.
+	 */
+	return *seq_count > 1 && *seq_count <= io_combine_limit;
+}
+
 /*
  *	tbm_shared_iterate - scan through next page of a TIDBitmap
  *
@@ -1056,87 +1099,128 @@ tbm_private_iterate(TBMPrivateIterator *iterator, TBMIterateResult *tbmres)
 bool
 tbm_shared_iterate(TBMSharedIterator *iterator, TBMIterateResult *tbmres)
 {
-	TBMSharedIteratorState *istate = iterator->state;
-	PagetableEntry *ptbase = NULL;
-	int		   *idxpages = NULL;
-	int		   *idxchunks = NULL;
-
-	if (iterator->ptbase != NULL)
-		ptbase = iterator->ptbase->ptentry;
-	if (iterator->ptpages != NULL)
-		idxpages = iterator->ptpages->index;
-	if (iterator->ptchunks != NULL)
-		idxchunks = iterator->ptchunks->index;
+	TBMSharedIteratorState *istate;
+	TBMIterateResult *result;
+	PagetableEntry *ptbase;
+	int		   *idxpages;
+	int		   *idxchunks;
+	int			max_results;
+	int			seq_count;
+	BlockNumber next_blockno;
+
+	/* Return results fetched earlier until they run out. */
+	if (iterator->result_index < iterator->result_count)
+	{
+		*tbmres = iterator->results[iterator->result_index++];
+		return true;
+	}
+
+	/* Prepare to fetch a new batch of results from shared memory. */
+	istate = iterator->state;
+	ptbase = iterator->ptbase->ptentry;
+	idxpages = iterator->ptpages ? iterator->ptpages->index : NULL;
+	idxchunks = iterator->ptchunks ? iterator->ptchunks->index : NULL;
+
+	/* Ramp up from size 1 in case there is a small LIMIT. */
+	if (iterator->result_count == 0)
+		max_results = 1;		/* first time caller */
+	else if (iterator->result_count < iterator->result_capacity / 2)
+		max_results = iterator->result_count * 2;	/* ramp up */
+	else
+		max_results = iterator->result_capacity;	/* saturated */
+
+	/* Write first result in caller's object directly. */
+	result = tbmres;
+	iterator->result_index = 1;
+	iterator->result_count = 0;
+	next_blockno = InvalidBlockNumber;
+	seq_count = 0;
 
 	/* Acquire the LWLock before accessing the shared members */
 	LWLockAcquire(&istate->lock, LW_EXCLUSIVE);
 
-	/*
-	 * If lossy chunk pages remain, make sure we've advanced schunkptr/
-	 * schunkbit to the next set bit.
-	 */
-	while (istate->schunkptr < istate->nchunks)
+	do
 	{
-		PagetableEntry *chunk = &ptbase[idxchunks[istate->schunkptr]];
-		int			schunkbit = istate->schunkbit;
-
-		tbm_advance_schunkbit(chunk, &schunkbit);
-		if (schunkbit < PAGES_PER_CHUNK)
+		/*
+		 * If lossy chunk pages remain, make sure we've advanced schunkptr/
+		 * schunkbit to the next set bit.
+		 */
+		while (istate->schunkptr < istate->nchunks)
 		{
-			istate->schunkbit = schunkbit;
-			break;
-		}
-		/* advance to next chunk */
-		istate->schunkptr++;
-		istate->schunkbit = 0;
-	}
+			PagetableEntry *chunk = &ptbase[idxchunks[istate->schunkptr]];
+			int			schunkbit = istate->schunkbit;
 
-	/*
-	 * If both chunk and per-page data remain, must output the numerically
-	 * earlier page.
-	 */
-	if (istate->schunkptr < istate->nchunks)
-	{
-		PagetableEntry *chunk = &ptbase[idxchunks[istate->schunkptr]];
-		BlockNumber chunk_blockno;
-
-		chunk_blockno = chunk->blockno + istate->schunkbit;
+			tbm_advance_schunkbit(chunk, &schunkbit);
+			if (schunkbit < PAGES_PER_CHUNK)
+			{
+				istate->schunkbit = schunkbit;
+				break;
+			}
+			/* advance to next chunk */
+			istate->schunkptr++;
+			istate->schunkbit = 0;
+		}
 
-		if (istate->spageptr >= istate->npages ||
-			chunk_blockno < ptbase[idxpages[istate->spageptr]].blockno)
+		/*
+		 * If both chunk and per-page data remain, must output the numerically
+		 * earlier page.
+		 */
+		if (istate->schunkptr < istate->nchunks)
 		{
-			/* Return a lossy page indicator from the chunk */
-			tbmres->blockno = chunk_blockno;
-			tbmres->lossy = true;
-			tbmres->recheck = true;
-			tbmres->internal_page = NULL;
-			istate->schunkbit++;
+			PagetableEntry *chunk = &ptbase[idxchunks[istate->schunkptr]];
+			BlockNumber chunk_blockno;
 
-			LWLockRelease(&istate->lock);
-			return true;
-		}
-	}
-
-	if (istate->spageptr < istate->npages)
-	{
-		PagetableEntry *page = &ptbase[idxpages[istate->spageptr]];
+			chunk_blockno = chunk->blockno + istate->schunkbit;
 
-		tbmres->internal_page = page;
-		tbmres->blockno = page->blockno;
-		tbmres->lossy = false;
-		tbmres->recheck = page->recheck;
-		istate->spageptr++;
+			if (istate->spageptr >= istate->npages ||
+				chunk_blockno < ptbase[idxpages[istate->spageptr]].blockno)
+			{
+				if (!tbm_shared_iterate_include_block_p(iterator,
+														chunk_blockno,
+														&next_blockno,
+														&seq_count))
+					break;
+
+				/* Return a lossy page indicator from the chunk */
+				result->blockno = chunk_blockno;
+				result->lossy = true;
+				result->recheck = true;
+				result->internal_page = NULL;
+				istate->schunkbit++;
+				result = &iterator->results[++iterator->result_count];
+				continue;
+			}
+		}
 
-		LWLockRelease(&istate->lock);
+		if (istate->spageptr < istate->npages)
+		{
+			PagetableEntry *page = &ptbase[idxpages[istate->spageptr]];
+
+			if (!tbm_shared_iterate_include_block_p(iterator,
+													page->blockno,
+													&next_blockno,
+													&seq_count))
+				break;
+
+			result->internal_page = page;
+			result->blockno = page->blockno;
+			result->lossy = false;
+			result->recheck = page->recheck;
+			istate->spageptr++;
+			result = &iterator->results[++iterator->result_count];
+			continue;
+		}
 
-		return true;
+		/* Nothing more in the bitmap. */
+		Assert(istate->spageptr == istate->npages);
+		Assert(istate->schunkptr == istate->nchunks);
+		break;
 	}
+	while (iterator->result_count < max_results);
 
 	LWLockRelease(&istate->lock);
 
-	/* Nothing more in the bitmap */
-	tbmres->blockno = InvalidBlockNumber;
-	return false;
+	return iterator->result_count > 0;
 }
 
 /*
@@ -1466,12 +1550,22 @@ tbm_attach_shared_iterate(dsa_area *dsa, dsa_pointer dp)
 {
 	TBMSharedIterator *iterator;
 	TBMSharedIteratorState *istate;
+	int			result_capacity;
 
 	/*
-	 * Create the TBMSharedIterator struct, with enough trailing space to
-	 * serve the needs of the TBMIterateResult sub-struct.
+	 * How many blocks to fetch from the shared bitmap at a time, allowing
+	 * extra space for trailing sequential blocks.
+	 */
+	result_capacity = TBM_SHARED_FETCH + io_combine_limit - 1;
+
+	/*
+	 * Create the TBMSharedIterator struct, with enough trailing space for the
+	 * above.
 	 */
-	iterator = (TBMSharedIterator *) palloc0(sizeof(TBMSharedIterator));
+	iterator = (TBMSharedIterator *)
+		palloc0(offsetof(TBMSharedIterator, results) +
+				sizeof(iterator->results[0]) * result_capacity);
+	iterator->result_capacity = result_capacity;
 
 	istate = (TBMSharedIteratorState *) dsa_get_address(dsa, dp);
 
-- 
2.39.5

