From 4d9d3ef3e6870228304a478a06f75ecd629f2db1 Mon Sep 17 00:00:00 2001
From: Peter Geoghegan <pg@bowt.ie>
Date: Tue, 30 Mar 2021 19:43:06 -0700
Subject: [PATCH v10 4/5] Truncate line pointer array during VACUUM.

Truncate each heap page's line pointer array when a contiguous group of
LP_UNUSED item pointers appears at the end of the array.  This happens
during VACUUM's second pass over the heap.  In practice most affected
LP_UNUSED line pointers are truncated away at the same point that VACUUM
marks them LP_UNUSED (from LP_DEAD).

This is particularly helpful with queue-like workloads that have
successive related range DELETEs and multi-row INSERT queries.  VACUUM
can reclaim all of the space on each page, making it more likely that
space utilization will be stable over time (a lower heap fill factor is
still essential, though).

Author: Matthias van de Meent <boekewurm+postgres@gmail.com>
Author: Peter Geoghegan <pg@bowt.ie>
Discussion: https://postgr.es/m/CAEze2WjgaQc55Y5f5CQd3L=eS5CZcff2Obxp=O6pto8-f0hC4w@mail.gmail.com
---
 src/include/storage/bufpage.h        |  1 +
 src/backend/access/heap/heapam.c     | 15 +++--
 src/backend/access/heap/pruneheap.c  |  1 +
 src/backend/access/heap/vacuumlazy.c | 16 +++++-
 src/backend/storage/page/bufpage.c   | 82 +++++++++++++++++++++++++++-
 5 files changed, 107 insertions(+), 8 deletions(-)

diff --git a/src/include/storage/bufpage.h b/src/include/storage/bufpage.h
index 359b749f7f..c86ccdaf60 100644
--- a/src/include/storage/bufpage.h
+++ b/src/include/storage/bufpage.h
@@ -441,6 +441,7 @@ extern Page PageGetTempPageCopy(Page page);
 extern Page PageGetTempPageCopySpecial(Page page);
 extern void PageRestoreTempPage(Page tempPage, Page oldPage);
 extern void PageRepairFragmentation(Page page);
+extern void PageTruncateLinePointerArray(Page page);
 extern Size PageGetFreeSpace(Page page);
 extern Size PageGetFreeSpaceForMultipleTuples(Page page, int ntups);
 extern Size PageGetExactFreeSpace(Page page);
diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c
index 9cbc161d7a..db84c6f882 100644
--- a/src/backend/access/heap/heapam.c
+++ b/src/backend/access/heap/heapam.c
@@ -635,8 +635,15 @@ heapgettup(HeapScanDesc scan,
 		}
 		else
 		{
+			/* 
+			 * The previous returned tuple may have been vacuumed since the
+			 * previous scan when we use a non-MVCC snapshot, so we must
+			 * re-establish the lineoff <= PageGetMaxOffsetNumber(dp)
+			 * invariant
+			 */
 			lineoff =			/* previous offnum */
-				OffsetNumberPrev(ItemPointerGetOffsetNumber(&(tuple->t_self)));
+				Min(lines,
+					OffsetNumberPrev(ItemPointerGetOffsetNumber(&(tuple->t_self))));
 		}
 		/* page and lineoff now reference the physically previous tid */
 
@@ -8556,10 +8563,8 @@ heap_xlog_vacuum(XLogReaderState *record)
 			ItemIdSetUnused(lp);
 		}
 
-		/*
-		 * Update the page's hint bit about whether it has free pointers
-		 */
-		PageSetHasFreeLinePointers(page);
+		/* Attempt to truncate line pointer array now */
+		PageTruncateLinePointerArray(page);
 
 		PageSetLSN(page, lsn);
 		MarkBufferDirty(buffer);
diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c
index f75502ca2c..3c8dc0af18 100644
--- a/src/backend/access/heap/pruneheap.c
+++ b/src/backend/access/heap/pruneheap.c
@@ -962,6 +962,7 @@ heap_get_root_tuples(Page page, OffsetNumber *root_offsets)
 		 */
 		for (;;)
 		{
+			Assert(OffsetNumberIsValid(nextoffnum) && nextoffnum <= maxoff);
 			lp = PageGetItemId(page, nextoffnum);
 
 			/* Check for broken chains */
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index a0db90b43f..c7bb0b1f23 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -1448,7 +1448,11 @@ lazy_scan_heap(LVRelState *vacrel, VacuumParams *params, bool aggressive)
 		if (prunestate.has_lpdead_items && vacrel->do_index_vacuuming)
 		{
 			/*
-			 * Wait until lazy_vacuum_heap_rel() to save free space.
+			 * Wait until lazy_vacuum_heap_rel() to save free space.  This
+			 * doesn't just save us some cycles; it also allows us to record
+			 * any additional free space that lazy_vacuum_heap_page() will
+			 * make available in cases where it's possible to truncate the
+			 * page's line pointer array.
 			 *
 			 * Note that the one-pass (no indexes) case is only supposed to
 			 * make it this far when there were no LP_DEAD items during
@@ -2053,6 +2057,13 @@ lazy_vacuum_all_indexes(LVRelState *vacrel)
  * Pages that never had lazy_scan_prune record LP_DEAD items are not visited
  * at all.
  *
+ * We may also be able to truncate the line pointer array of the heap pages we
+ * visit.  If there is a contiguous group of LP_UNUSED items at the end of the
+ * array, it can be reclaimed as free space.  These LP_UNUSED items usually
+ * start out as LP_DEAD items recorded by lazy_scan_prune (we set items from
+ * each page to LP_UNUSED, and then consider if it's possible to truncate the
+ * page's line pointer array).
+ *
  * Note: the reason for doing this as a second pass is we cannot remove the
  * tuples until we've removed their index entries, and we want to process
  * index entry removal in batches as large as possible.
@@ -2195,7 +2206,8 @@ lazy_vacuum_heap_page(LVRelState *vacrel, BlockNumber blkno, Buffer buffer,
 
 	Assert(uncnt > 0);
 
-	PageSetHasFreeLinePointers(page);
+	/* Attempt to truncate line pointer array now */
+	PageTruncateLinePointerArray(page);
 
 	/*
 	 * Mark buffer dirty before we write WAL.
diff --git a/src/backend/storage/page/bufpage.c b/src/backend/storage/page/bufpage.c
index 5d5989c2f5..6e820cd675 100644
--- a/src/backend/storage/page/bufpage.c
+++ b/src/backend/storage/page/bufpage.c
@@ -676,9 +676,10 @@ compactify_tuples(itemIdCompact itemidbase, int nitems, Page page, bool presorte
  * PageRepairFragmentation
  *
  * Frees fragmented space on a page.
- * It doesn't remove unused line pointers! Please don't change this.
  *
  * This routine is usable for heap pages only, but see PageIndexMultiDelete.
+ * It never removes unused line pointers, though PageTruncateLinePointerArray
+ * will do so at the point that VACUUM sets LP_DEAD items to LP_UNUSED.
  *
  * Caller had better have a super-exclusive lock on page's buffer.  As a side
  * effect the page's PD_HAS_FREE_LINES hint bit will be set or unset as
@@ -784,6 +785,85 @@ PageRepairFragmentation(Page page)
 		PageClearHasFreeLinePointers(page);
 }
 
+/*
+ * PageTruncateLinePointerArray
+ *
+ * Removes unused line pointers at the end of the line pointer array.
+ *
+ * This routine is usable for heap pages only.  It is called by VACUUM during
+ * its second pass over the heap.  We expect that there will be at least one
+ * LP_UNUSED line pointer on the page (if VACUUM didn't have an LP_DEAD item
+ * to set LP_UNUSED on the page then it wouldn't have visited the page).
+ *
+ * Caller can have either an exclusive lock or a super-exclusive lock on
+ * page's buffer.  As a side effect the page's PD_HAS_FREE_LINES hint bit will
+ * be set as needed.
+ */
+void
+PageTruncateLinePointerArray(Page page)
+{
+	PageHeader	phdr = (PageHeader) page;
+	ItemId		lp;
+	bool		truncating = true;
+	bool		sethint = false;
+	int			nline,
+				nunusedend;
+
+	/*
+	 * Scan original line pointer array backwards to determine how to far to
+	 * truncate to.  Note that we avoid truncating the line pointer to 0 items
+	 * in all cases.
+	 */
+	nline = PageGetMaxOffsetNumber(page);
+	nunusedend = 0;
+
+	for (int i = nline; i >= FirstOffsetNumber; i--)
+	{
+		lp = PageGetItemId(page, i);
+
+		if (truncating && i > FirstOffsetNumber)
+		{
+			/*
+			 * Still counting which line pointers from the end of the array
+			 * can be truncated away.
+			 *
+			 * If this is another LP_UNUSED line pointer (or the first), count
+			 * it among those we'll truncate.  Otherwise stop considering
+			 * further LP_UNUSED line pointers for truncation, but continue
+			 * with scan of array in any case.
+			 */
+			if (!ItemIdIsUsed(lp))
+				nunusedend++;
+			else
+				truncating = false;
+		}
+		else
+		{
+			if (!ItemIdIsUsed(lp))
+			{
+				/*
+				 * This is an unused line pointer that we won't be truncating
+				 * away -- so there is at least one.  Set hint on page.
+				 */
+				sethint = true;
+				break;
+			}
+		}
+	}
+
+	Assert(nline > nunusedend);
+	if (nunusedend > 0)
+		phdr->pd_lower -= sizeof(ItemIdData) * nunusedend;
+	else
+		Assert(sethint);
+
+	/* Set hint bit for PageAddItemExtended */
+	if (sethint)
+		PageSetHasFreeLinePointers(page);
+	else
+		PageClearHasFreeLinePointers(page);
+}
+
 /*
  * PageGetFreeSpace
  *		Returns the size of the free (allocatable) space on a page,
-- 
2.27.0

