From a503285e012de12539df384d615675c1e48e5cfd Mon Sep 17 00:00:00 2001 From: Melanie Plageman Date: Wed, 25 Feb 2026 16:48:19 -0500 Subject: [PATCH v40 02/12] Add pruning fast path for all-visible and all-frozen pages Because of the SKIP_PAGES_THRESHOLD optimization or a stale prune XID, heap_page_prune_and_freeze() can be invoked for pages with no pruning or freezing work. To avoid this, if a page is already all-frozen or it is all-visible and no freezing will be attempted, we exit early. We can't exit early if vacuum passed DISABLE_PAGE_SKIPPING, though. Author: Melanie Plageman Reviewed-by: Chao Li Reviewed-by: Kirill Reshke Discussion: https://postgr.es/m/bqc4kh5midfn44gnjiqez3bjqv4zogydguvdn446riw45jcf3y%404ez66il7ebvk --- src/backend/access/heap/pruneheap.c | 92 +++++++++++++++++++++++++++- src/backend/access/heap/vacuumlazy.c | 10 +++ src/include/access/heapam.h | 1 + 3 files changed, 102 insertions(+), 1 deletion(-) diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c index 52cafb23c6b..bf740c37f3d 100644 --- a/src/backend/access/heap/pruneheap.c +++ b/src/backend/access/heap/pruneheap.c @@ -129,6 +129,12 @@ typedef struct /* Bits in the vmbuffer for this heap page */ uint8 vmbits; + /* + * True if the page can bypass full page inspection during pruning and + * freezing based on its visibility map status and the caller's options. + */ + bool fast_path; + /*------------------------------------------------------- * Information about what was done * @@ -184,6 +190,7 @@ static void prune_freeze_setup(PruneFreezeParams *params, PruneFreezeResult *presult, PruneState *prstate); static void heap_fix_vm_corruption(PruneState *prstate, OffsetNumber offnum); +static void heap_page_bypass_prune_freeze(PruneState *prstate, PruneFreezeResult *presult); static void prune_freeze_plan(PruneState *prstate, OffsetNumber *off_loc); static HTSV_Result heap_prune_satisfies_vacuum(PruneState *prstate, @@ -312,7 +319,7 @@ heap_page_prune_opt(Relation relation, Buffer buffer, Buffer *vmbuffer) * cannot safely determine that during on-access pruning with the * current implementation. */ - params.options = 0; + params.options = HEAP_PAGE_PRUNE_ALLOW_FAST_PATH; heap_page_prune_and_freeze(¶ms, &presult, &dummy_off_loc, NULL, NULL); @@ -381,6 +388,16 @@ prune_freeze_setup(PruneFreezeParams *params, prstate->block, &prstate->vmbuffer); + /* + * If the page is already all-frozen, or already all-visible when freezing + * is not being attempted, we can skip pruning and freezing entirely. + * Callers must opt in by setting HEAP_PAGE_PRUNE_ALLOW_FAST_PATH. + */ + prstate->fast_path = ((prstate->vmbits & VISIBILITYMAP_ALL_FROZEN) || + ((prstate->vmbits & VISIBILITYMAP_ALL_VISIBLE) && + !prstate->attempt_freeze)) && + (params->options & HEAP_PAGE_PRUNE_ALLOW_FAST_PATH); + /* * Our strategy is to scan the page and make lists of items to change, * then apply the changes within a critical section. This keeps as much @@ -882,6 +899,68 @@ heap_fix_vm_corruption(PruneState *prstate, OffsetNumber offnum) prstate->vmbits = 0; } +/* + * If the page is already all-frozen, or already all-visible and freezing + * is not being attempted, there is no remaining work and we can bypass the + * expensive overhead of heap_page_prune_and_freeze(). + * + * This can happen when the page has a stale prune hint, or if VACUUM is + * scanning an already all-frozen page due to SKIP_PAGES_THRESHOLD. + * + * The caller must already have examined the visibility map and saved the + * status for the page's VM bits in prstate->vmbits. Caller must hold a + * content lock on the heap page since it will examine line pointers. + * + * Before calling heap_page_bypass_prune_freeze(), the caller should first + * check for and fix any discrepancy between the page-level visibility hint + * and the visibility map. Otherwise, the fast path will always prevent us + * from getting them in sync. Note that if there are tuples on the page that + * are not visible to all but the VM is incorrectly marked + * all-visible/all-frozen, we will not get the chance to fix that corruption + * when using the fast path. + */ +static void +heap_page_bypass_prune_freeze(PruneState *prstate, PruneFreezeResult *presult) +{ + OffsetNumber maxoff = PageGetMaxOffsetNumber(prstate->page); + Page page = prstate->page; + + Assert(prstate->vmbits & VISIBILITYMAP_ALL_FROZEN || + (prstate->vmbits & VISIBILITYMAP_ALL_VISIBLE && + !prstate->attempt_freeze)); + + /* We'll fill in presult for the caller */ + memset(presult, 0, sizeof(PruneFreezeResult)); + + presult->vmbits = prstate->vmbits; + + /* Clear any stale prune hint */ + if (TransactionIdIsValid(PageGetPruneXid(page))) + { + PageClearPrunable(page); + MarkBufferDirtyHint(prstate->buffer, true); + } + + if (PageIsEmpty(page)) + return; + + presult->hastup = true; + + /* + * Since the page is all-visible, a count of the normal ItemIds on the + * page should be sufficient for vacuum's live tuple count. + */ + for (OffsetNumber off = FirstOffsetNumber; + off <= maxoff; + off = OffsetNumberNext(off)) + { + if (ItemIdIsNormal(PageGetItemId(page, off))) + prstate->live_tuples++; + } + + presult->live_tuples = prstate->live_tuples; +} + /* * Prune and repair fragmentation and potentially freeze tuples on the * specified page. @@ -945,6 +1024,17 @@ heap_page_prune_and_freeze(PruneFreezeParams *params, !PageIsAllVisible(prstate.page)) heap_fix_vm_corruption(&prstate, InvalidOffsetNumber); + /* + * If the visibility map status allows it, bypass pruning and freezing + * entirely. This must be done after fixing any discrepancy between the + * page-level visibility hint and the VM. + */ + if (prstate.fast_path) + { + heap_page_bypass_prune_freeze(&prstate, presult); + return; + } + /* * Examine all line pointers and tuple visibility information to determine * which line pointers should change state and which tuples may be frozen. diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c index 957322648ca..ad7a3290821 100644 --- a/src/backend/access/heap/vacuumlazy.c +++ b/src/backend/access/heap/vacuumlazy.c @@ -2019,6 +2019,16 @@ lazy_scan_prune(LVRelState *vacrel, if (vacrel->nindexes == 0) params.options |= HEAP_PAGE_PRUNE_MARK_UNUSED_NOW; + /* + * Allow skipping full inspection of pages that the VM indicates are + * already all-frozen (which may be scanned due to SKIP_PAGES_THRESHOLD). + * However, if DISABLE_PAGE_SKIPPING was specified, we can't trust the VM, + * so we must examine the page to make sure it is truly all-frozen and fix + * it otherwise. + */ + if (vacrel->skipwithvm) + params.options |= HEAP_PAGE_PRUNE_ALLOW_FAST_PATH; + heap_page_prune_and_freeze(¶ms, &presult, &vacrel->offnum, diff --git a/src/include/access/heapam.h b/src/include/access/heapam.h index c649e5f1980..0b571d7089f 100644 --- a/src/include/access/heapam.h +++ b/src/include/access/heapam.h @@ -42,6 +42,7 @@ /* "options" flag bits for heap_page_prune_and_freeze */ #define HEAP_PAGE_PRUNE_MARK_UNUSED_NOW (1 << 0) #define HEAP_PAGE_PRUNE_FREEZE (1 << 1) +#define HEAP_PAGE_PRUNE_ALLOW_FAST_PATH (1 << 2) typedef struct BulkInsertStateData *BulkInsertState; typedef struct GlobalVisState GlobalVisState; -- 2.43.0