From 441f7134d73d3f97585817eb0cbd77c555fdbb2c Mon Sep 17 00:00:00 2001
From: Matthias van de Meent <boekewurm+postgres@gmail.com>
Date: Tue, 15 Feb 2022 19:39:41 +0100
Subject: [PATCH v8] Improve application of line pointer array truncation in
 heapam.

This will allow reuse of what is effectively free space for data as well as
new line pointers, instead of keeping it reserved for line pointers only.

It further improves efficiency of HeapAM's local updates by not requiring a
2nd run of VACUUM to come by before we can reuse line pointers left over from
temporary high amounts of HOT activity.

Additionally, it improves the current application of 2nd VACUUM pass line
pointer truncation truncate down to 0 line pointers; improving page
applicability for PageIsEmpty()-based optimizations and making now-empty heap
pages applicable again for MaxHeapTupleSize-sized tuple insertions on 32-bit
systems.
---
 src/backend/access/heap/vacuumlazy.c |  7 ++++-
 src/backend/storage/page/bufpage.c   | 44 +++++++++++++++++++++-------
 2 files changed, 40 insertions(+), 11 deletions(-)

diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index 9c88b9bd71..71d8a0eb27 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -1582,7 +1582,6 @@ lazy_scan_prune(LVRelState *vacrel,
 
 	Assert(BufferGetBlockNumber(buf) == blkno);
 
-	maxoff = PageGetMaxOffsetNumber(page);
 
 retry:
 
@@ -1606,6 +1605,12 @@ retry:
 									 InvalidTransactionId, 0, &nnewlpdead,
 									 &vacrel->offnum);
 
+	/*
+	 * Get maxoff after each heap_page_prune, as the maxoff might have gone
+	 * down due to removed trailing LP_UNUSED items.
+	 */
+	maxoff = PageGetMaxOffsetNumber(page);
+
 	/*
 	 * Now scan the page to collect LP_DEAD items and check for tuples
 	 * requiring freezing among remaining tuples with storage
diff --git a/src/backend/storage/page/bufpage.c b/src/backend/storage/page/bufpage.c
index 147ba4d923..f6722cb008 100644
--- a/src/backend/storage/page/bufpage.c
+++ b/src/backend/storage/page/bufpage.c
@@ -688,10 +688,19 @@ compactify_tuples(itemIdCompact itemidbase, int nitems, Page page, bool presorte
  *
  * This routine is usable for heap pages only, but see PageIndexMultiDelete.
  *
- * Never removes unused line pointers.  PageTruncateLinePointerArray can
- * safely remove some unused line pointers.  It ought to be safe for this
- * routine to free unused line pointers in roughly the same way, but it's not
- * clear that that would be beneficial.
+ * This routine removes unused trailing line pointers. See also
+ * PageTruncateLinePointerArray for further rationale.
+ *
+ * Although this routine is not called from contexts that have removed root
+ * line pointers, the line pointers removed in this function generally
+ * originate from temporary, but long, HOT chains. Once the HOT versions are
+ * removed, the trailing line pointers are not needed, and so we can free
+ * them. Not doing so would mean that this space can only be reused after
+ * a second phase of vacuum, which may not arrive until after we've evicted
+ * an updated tuple from the page due to space constraints.
+ *
+ * The computational cost of applying the truncation is very cheap, so
+ * there's little reason not to do it whenever we repair fragmentation.
  *
  * PageTruncateLinePointerArray is only called during VACUUM's second pass
  * over the heap.  Any unused line pointers that it sees are likely to have
@@ -718,6 +727,7 @@ PageRepairFragmentation(Page page)
 	int			nline,
 				nstorage,
 				nunused;
+	OffsetNumber lastUsed = InvalidOffsetNumber;
 	int			i;
 	Size		totallen;
 	bool		presorted = true;	/* For now */
@@ -751,6 +761,7 @@ PageRepairFragmentation(Page page)
 		lp = PageGetItemId(page, i);
 		if (ItemIdIsUsed(lp))
 		{
+			lastUsed = i;
 			if (ItemIdHasStorage(lp))
 			{
 				itemidptr->offsetindex = i - 1;
@@ -798,6 +809,24 @@ PageRepairFragmentation(Page page)
 		compactify_tuples(itemidbase, nstorage, page, presorted);
 	}
 
+	/* The last line pointer is not the last used line pointer */
+	if (lastUsed != nline)
+	{
+		int nunusedend = nline - lastUsed;
+
+		Assert(nunused >= nunusedend && nunusedend > 0);
+
+		/* remove trailing unused line pointers from the count */
+		nunused -= nunusedend;
+		/* truncate the line pointer array */
+		((PageHeader) page)->pd_lower -= (sizeof(ItemIdData) * nunusedend);
+
+#ifdef CLOBBER_FREED_MEMORY
+		memset((char *) page + ((PageHeader) page)->pd_lower, 0x7F,
+			   sizeof(ItemIdData) * nunusedend);
+#endif
+	}
+
 	/* Set hint bit for PageAddItemExtended */
 	if (nunused > 0)
 		PageSetHasFreeLinePointers(page);
@@ -815,11 +844,6 @@ PageRepairFragmentation(Page page)
  * pointer on the page (if VACUUM didn't have an LP_DEAD item on the page that
  * it just set to LP_UNUSED then it should not call here).
  *
- * We avoid truncating the line pointer array to 0 items, if necessary by
- * leaving behind a single remaining LP_UNUSED item.  This is a little
- * arbitrary, but it seems like a good idea to avoid leaving a PageIsEmpty()
- * page behind.
- *
  * Caller can have either an exclusive lock or a full cleanup lock on page's
  * buffer.  The page's PD_HAS_FREE_LINES hint bit will be set or unset based
  * on whether or not we leave behind any remaining LP_UNUSED items.
@@ -837,7 +861,7 @@ PageTruncateLinePointerArray(Page page)
 	{
 		ItemId		lp = PageGetItemId(page, i);
 
-		if (!countdone && i > FirstOffsetNumber)
+		if (!countdone)
 		{
 			/*
 			 * Still determining which line pointers from the end of the array
-- 
2.30.2

