From b66212de1d046dea5bbc238a6f26f1d8d9f712a7 Mon Sep 17 00:00:00 2001 From: Melanie Plageman Date: Tue, 26 Mar 2024 09:37:46 -0400 Subject: [PATCH v8 12/22] Merge prune and freeze records When both pruning and freezing is done, this means a single, combined WAL record is emitted for both operations. This will reduce the number of WAL records emitted. When there are only tuples to freeze present, we can avoid taking a full cleanup lock when replaying the record. --- src/backend/access/heap/heapam.c | 2 - src/backend/access/heap/pruneheap.c | 215 +++++++++++++++------------- 2 files changed, 114 insertions(+), 103 deletions(-) diff --git a/src/backend/access/heap/heapam.c b/src/backend/access/heap/heapam.c index 41c1c7d286f..aefc0be0dd3 100644 --- a/src/backend/access/heap/heapam.c +++ b/src/backend/access/heap/heapam.c @@ -6836,8 +6836,6 @@ heap_freeze_prepared_tuples(Buffer buffer, HeapTupleFreeze *tuples, int ntuples) htup = (HeapTupleHeader) PageGetItem(page, itemid); heap_execute_freeze_tuple(htup, frz); } - - MarkBufferDirty(buffer); } /* diff --git a/src/backend/access/heap/pruneheap.c b/src/backend/access/heap/pruneheap.c index 8914d4bf5c8..db8a182a197 100644 --- a/src/backend/access/heap/pruneheap.c +++ b/src/backend/access/heap/pruneheap.c @@ -249,9 +249,8 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer, bool do_freeze; bool all_visible_except_removable; bool do_prune; - bool whole_page_freezable; + bool do_hint; bool hint_bit_fpi; - bool prune_fpi = false; int64 fpi_before = pgWalUsage.wal_fpi; /* @@ -464,10 +463,9 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer, /* * If checksums are enabled, heap_prune_satisfies_vacuum() may have caused - * an FPI to be emitted. Then reset fpi_before for no prune case. + * an FPI to be emitted. */ hint_bit_fpi = fpi_before != pgWalUsage.wal_fpi; - fpi_before = pgWalUsage.wal_fpi; /* * For vacuum, if the whole page will become frozen, we consider @@ -517,16 +515,16 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer, prstate.ndead > 0 || prstate.nunused > 0; + /* Record number of newly-set-LP_DEAD items for caller */ + presult->nnewlpdead = prstate.ndead; + /* - * Only incur overhead of checking if we will do an FPI if we might use - * the information. + * Even if we don't prune anything, if we found a new value for the + * pd_prune_xid field or the page was marked full, we will update the hint + * bit. */ - if (do_prune && pagefrz) - prune_fpi = XLogCheckBufferNeedsBackup(buffer); - - /* Is the whole page freezable? And is there something to freeze */ - whole_page_freezable = all_visible_except_removable && - presult->all_frozen; + do_hint = ((PageHeader) page)->pd_prune_xid != prstate.new_prune_xid || + PageIsFull(page); /* * Freeze the page when heap_prepare_freeze_tuple indicates that at least @@ -539,46 +537,57 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer, * opportunistic freeze heuristic must be improved; however, for now, try * to approximate it. */ - do_freeze = pagefrz && - (pagefrz->freeze_required || - (whole_page_freezable && presult->nfrozen > 0 && (prune_fpi || hint_bit_fpi))); - if (do_freeze) + do_freeze = false; + if (pagefrz) { - heap_pre_freeze_checks(buffer, prstate.frozen, presult->nfrozen); + /* Is the whole page freezable? And is there something to freeze? */ + bool whole_page_freezable = all_visible_except_removable && + presult->all_frozen; - /* - * We can use the visibility_cutoff_xid as our cutoff for conflicts - * when the whole page is eligible to become all-frozen in the VM once - * we're done with it. Otherwise we generate a conservative cutoff by - * stepping back from OldestXmin. This avoids false conflicts when - * hot_standby_feedback is in use. - */ - if (all_visible_except_removable && presult->all_frozen) - frz_conflict_horizon = visibility_cutoff_xid; - else + if (pagefrz->freeze_required) + do_freeze = true; + else if (whole_page_freezable && presult->nfrozen > 0) { - /* Avoids false conflicts when hot_standby_feedback in use */ - frz_conflict_horizon = pagefrz->cutoffs->OldestXmin; - TransactionIdRetreat(frz_conflict_horizon); + /* + * Freezing would make the page all-frozen. In this case, we will + * freeze if we have already emitted an FPI or will do so anyway. + * Be sure only to incur the overhead of checking if we will do an + * FPI if we may use that information. + */ + if (hint_bit_fpi || + ((do_prune || do_hint) && XLogCheckBufferNeedsBackup(buffer))) + { + do_freeze = true; + } } } - /* Any error while applying the changes is critical */ - START_CRIT_SECTION(); + /* + * Validate the tuples we are considering freezing. We do this even if + * pruning and hint bit setting have not emitted an FPI so far because we + * still may emit an FPI while setting the page hint bit later. But we + * want to avoid doing the pre-freeze checks in a critical section. + */ + if (do_freeze) + heap_pre_freeze_checks(buffer, prstate.frozen, presult->nfrozen); - /* Have we found any prunable items? */ - if (do_prune) + if (!do_freeze && (!pagefrz || !presult->all_frozen || presult->nfrozen > 0)) { /* - * Apply the planned item changes, then repair page fragmentation, and - * update the page's hint bit about whether it has free line pointers. + * If we will neither freeze tuples on the page nor set the page all + * frozen in the visibility map, the page is not all-frozen and there + * will be no newly frozen tuples. */ - heap_page_prune_execute(buffer, false, - prstate.redirected, prstate.nredirected, - prstate.nowdead, prstate.ndead, - prstate.nowunused, prstate.nunused); + presult->all_frozen = false; + presult->nfrozen = 0; /* avoid miscounts in instrumentation */ + } + + /* Any error while applying the changes is critical */ + START_CRIT_SECTION(); + if (do_hint) + { /* * Update the page's pd_prune_xid field to either zero, or the lowest * XID of any soon-prunable tuple. @@ -586,12 +595,52 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer, ((PageHeader) page)->pd_prune_xid = prstate.new_prune_xid; /* - * Also clear the "page is full" flag, since there's no point in - * repeating the prune/defrag process until something else happens to - * the page. + * Clear the "page is full" flag if it is set since there's no point + * in repeating the prune/defrag process until something else happens + * to the page. */ PageClearFull(page); + /* + * We only needed to update pd_prune_xid and clear the page-is-full + * hint bit, this is a non-WAL-logged hint. If we will also freeze or + * prune the page, we will mark the buffer dirty below. + */ + if (!do_freeze && !do_prune) + MarkBufferDirtyHint(buffer, true); + } + + if (do_prune || do_freeze) + { + /* Apply the planned item changes, then repair page fragmentation. */ + if (do_prune) + { + heap_page_prune_execute(buffer, false, + prstate.redirected, prstate.nredirected, + prstate.nowdead, prstate.ndead, + prstate.nowunused, prstate.nunused); + } + + if (do_freeze) + { + /* + * We can use the visibility_cutoff_xid as our cutoff for + * conflicts when the whole page is eligible to become all-frozen + * in the VM once we're done with it. Otherwise we generate a + * conservative cutoff by stepping back from OldestXmin. This + * avoids false conflicts when hot_standby_feedback is in use. + */ + if (all_visible_except_removable && presult->all_frozen) + frz_conflict_horizon = visibility_cutoff_xid; + else + { + /* Avoids false conflicts when hot_standby_feedback in use */ + frz_conflict_horizon = pagefrz->cutoffs->OldestXmin; + TransactionIdRetreat(frz_conflict_horizon); + } + heap_freeze_prepared_tuples(buffer, prstate.frozen, presult->nfrozen); + } + MarkBufferDirty(buffer); /* @@ -599,72 +648,35 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer, */ if (RelationNeedsWAL(relation)) { + /* + * The snapshotConflictHorizon for the whole record should be the + * most conservative of all the horizons calculated for any of the + * possible modifications. If this record will prune tuples, any + * transactions on the standby older than the youngest xmax of the + * most recently removed tuple this record will prune will + * conflict. If this record will freeze tuples, any transactions + * on the standby with xids older than the youngest tuple this + * record will freeze will conflict. + */ + TransactionId conflict_xid; + + if (TransactionIdFollows(frz_conflict_horizon, prstate.latest_xid_removed)) + conflict_xid = frz_conflict_horizon; + else + conflict_xid = prstate.latest_xid_removed; + log_heap_prune_and_freeze(relation, buffer, - prstate.latest_xid_removed, + conflict_xid, true, reason, - NULL, 0, + prstate.frozen, presult->nfrozen, prstate.redirected, prstate.nredirected, prstate.nowdead, prstate.ndead, prstate.nowunused, prstate.nunused); } } - else - { - /* - * If we didn't prune anything, but have found a new value for the - * pd_prune_xid field, update it and mark the buffer dirty. This is - * treated as a non-WAL-logged hint. - * - * Also clear the "page is full" flag if it is set, since there's no - * point in repeating the prune/defrag process until something else - * happens to the page. - */ - if (((PageHeader) page)->pd_prune_xid != prstate.new_prune_xid || - PageIsFull(page)) - { - ((PageHeader) page)->pd_prune_xid = prstate.new_prune_xid; - PageClearFull(page); - MarkBufferDirtyHint(buffer, true); - } - } END_CRIT_SECTION(); - /* Record number of newly-set-LP_DEAD items for caller */ - presult->nnewlpdead = prstate.ndead; - - if (do_freeze) - { - START_CRIT_SECTION(); - - Assert(presult->nfrozen > 0); - - heap_freeze_prepared_tuples(buffer, prstate.frozen, presult->nfrozen); - - MarkBufferDirty(buffer); - - /* Now WAL-log freezing if necessary */ - if (RelationNeedsWAL(relation)) - log_heap_prune_and_freeze(relation, buffer, - frz_conflict_horizon, false, reason, - prstate.frozen, presult->nfrozen, - NULL, 0, /* redirected */ - NULL, 0, /* dead */ - NULL, 0); /* unused */ - - END_CRIT_SECTION(); - } - else if (!pagefrz || !presult->all_frozen || presult->nfrozen > 0) - { - /* - * If we will neither freeze tuples on the page nor set the page all - * frozen in the visibility map, the page is not all frozen and there - * will be no newly frozen tuples. - */ - presult->all_frozen = false; - presult->nfrozen = 0; /* avoid miscounts in instrumentation */ - } - /* * For callers planning to update the visibility map, the conflict horizon * for that record must be the newest xmin on the page. However, if the @@ -681,9 +693,10 @@ heap_page_prune_and_freeze(Relation relation, Buffer buffer, * tuples on the page, if we will set the page all-frozen in the * visibility map, we can advance relfrozenxid and relminmxid to the * values in pagefrz->FreezePageRelfrozenXid and - * pagefrz->FreezePageRelminMxid. + * pagefrz->FreezePageRelminMxid. MFIXME: which one should be pick if + * presult->nfrozen == 0 and presult->all_frozen = True. */ - if (presult->all_frozen || presult->nfrozen > 0) + if (presult->nfrozen > 0) { presult->new_relfrozenxid = pagefrz->FreezePageRelfrozenXid; presult->new_relminmxid = pagefrz->FreezePageRelminMxid; -- 2.39.2