0007-Extended-vacuum-statistics-timing-metrics-and-failsa.patch
text/plain
Filename: 0007-Extended-vacuum-statistics-timing-metrics-and-failsa.patch
Type: text/plain
Part: 6
Message:
Re: Vacuum statistics
Patch
Same data as JSON:
GET /api/v1/attachments/:id/patch
the parsed metadata as JSON — format, series position, per-file stats; never the diff bytes.
API reference →
Format: format-patch
Series: patch 0007
Subject: Extended vacuum statistics: timing metrics and failsafe for tables, indexes and database
| File | + | − |
|---|---|---|
| doc/src/sgml/system-views.sgml | 112 | 0 |
| src/backend/access/heap/vacuumlazy.c | 28 | 0 |
| src/backend/catalog/system_views.sql | 20 | 3 |
| src/backend/utils/activity/pgstat_vacuum.c | 6 | 0 |
| src/backend/utils/adt/pgstatfuncs.c | 20 | 3 |
| src/include/catalog/pg_proc.dat | 9 | 9 |
| src/include/pgstat.h | 9 | 0 |
| src/test/modules/xid_wraparound/meson.build | 1 | 0 |
| src/test/modules/xid_wraparound/t/005_vacuum_stats_failsafe.pl | 66 | 0 |
| src/test/regress/expected/rules.out | 20 | 6 |
| src/test/regress/expected/vacuum_stats.out | 54 | 8 |
| src/test/regress/sql/vacuum_stats.sql | 40 | 2 |
From 42dffa8754bc2a554bec5a9662f5afdbb379d6f2 Mon Sep 17 00:00:00 2001
From: Alena Rybakina <alenka.rybakina@gmail.com>
Date: Tue, 16 Jun 2026 10:53:03 +0300
Subject: [PATCH 7/8] Extended vacuum statistics: timing metrics and failsafe
for tables, indexes and database
Expose the timing counters and the wraparound failsafe count in
pg_stat_vacuum_tables, pg_stat_vacuum_indexes and pg_stat_vacuum_database,
with documentation and regression coverage:
blk_read_time time spent reading blocks (with track_io_timing)
blk_write_time time spent writing blocks (with track_io_timing)
delay_time time spent in vacuum cost-based delay
total_time total wall-clock time of the vacuum
wraparound_failsafe number of vacuums that engaged the wraparound
failsafe (tables expose it as a per-relation flag,
the database aggregate as a count)
total_time is always positive; the regression test also exercises the
positive delay_time path with a dedicated cost-delayed vacuum. The positive
wraparound_failsafe path requires reaching the failsafe XID age and is covered
by a separate TAP test under src/test/modules/xid_wraparound.
---
doc/src/sgml/system-views.sgml | 112 ++++++++++++++++++
src/backend/access/heap/vacuumlazy.c | 28 +++++
src/backend/catalog/system_views.sql | 23 +++-
src/backend/utils/activity/pgstat_vacuum.c | 6 +
src/backend/utils/adt/pgstatfuncs.c | 23 +++-
src/include/catalog/pg_proc.dat | 18 +--
src/include/pgstat.h | 9 ++
src/test/modules/xid_wraparound/meson.build | 1 +
.../t/005_vacuum_stats_failsafe.pl | 66 +++++++++++
src/test/regress/expected/rules.out | 26 +++-
src/test/regress/expected/vacuum_stats.out | 62 ++++++++--
src/test/regress/sql/vacuum_stats.sql | 42 ++++++-
12 files changed, 385 insertions(+), 31 deletions(-)
create mode 100644 src/test/modules/xid_wraparound/t/005_vacuum_stats_failsafe.pl
diff --git a/doc/src/sgml/system-views.sgml b/doc/src/sgml/system-views.sgml
index 8e80db3e03..0443cbad40 100644
--- a/doc/src/sgml/system-views.sgml
+++ b/doc/src/sgml/system-views.sgml
@@ -5964,6 +5964,46 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of this relation's blocks found in shared buffers by the vacuum.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>blk_read_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Time spent reading blocks by the vacuum, in milliseconds (when <varname>track_io_timing</varname> is enabled).
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>blk_write_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Time spent writing (flushing) blocks by the vacuum, in milliseconds; remains zero if no flushes occurred.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>delay_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Total time the vacuum spent sleeping in vacuum delay points, in milliseconds.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>total_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Total wall-clock time of the vacuum, in milliseconds, including time spent waiting for I/O and locks.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>wraparound_failsafe</structfield> <type>integer</type>
+ </para>
+ <para>
+ Number of times the failsafe mechanism was triggered to prevent transaction ID wraparound during the vacuum.
+ </para></entry>
+ </row>
</tbody>
</tgroup>
</table>
@@ -6103,6 +6143,38 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of block hits within this index during the vacuum.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>blk_read_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Time spent reading index blocks, in milliseconds (if <varname>track_io_timing</varname> is enabled).
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>blk_write_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Time spent writing index blocks, in milliseconds (if <varname>track_io_timing</varname> is enabled).
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>delay_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Time spent in vacuum cost-based delay while processing this index, in milliseconds.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>total_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Total time spent vacuuming this index, in milliseconds.
+ </para></entry>
+ </row>
</tbody>
</tgroup>
</table>
@@ -6193,6 +6265,46 @@ SELECT * FROM pg_locks pl LEFT JOIN pg_prepared_xacts ppx
Number of shared buffer blocks written out by vacuum operations in this database.
</para></entry>
</row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>wraparound_failsafe</structfield> <type>integer</type>
+ </para>
+ <para>
+ Number of vacuum operations in this database that engaged the wraparound failsafe mechanism.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>blk_read_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Time spent reading blocks by vacuum operations in this database, in milliseconds (if <varname>track_io_timing</varname> is enabled).
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>blk_write_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Time spent writing blocks by vacuum operations in this database, in milliseconds (if <varname>track_io_timing</varname> is enabled).
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>delay_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Time spent in vacuum cost-based delay by vacuum operations in this database, in milliseconds.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>total_time</structfield> <type>double precision</type>
+ </para>
+ <para>
+ Total time spent by vacuum operations in this database, in milliseconds.
+ </para></entry>
+ </row>
</tbody>
</tgroup>
</table>
diff --git a/src/backend/access/heap/vacuumlazy.c b/src/backend/access/heap/vacuumlazy.c
index fdd70653e3..997eabf1ec 100644
--- a/src/backend/access/heap/vacuumlazy.c
+++ b/src/backend/access/heap/vacuumlazy.c
@@ -552,6 +552,9 @@ extvac_stats_end(Relation rel, LVExtStatCounters * counters,
PgStat_CommonCounts * report)
{
BufferUsage bufusage;
+ TimestampTz endtime;
+ long secs;
+ int usecs;
if (!pgstat_track_vacuum_statistics)
return;
@@ -562,6 +565,9 @@ extvac_stats_end(Relation rel, LVExtStatCounters * counters,
memset(&bufusage, 0, sizeof(BufferUsage));
BufferUsageAccumDiff(&bufusage, &pgBufferUsage, &counters->bufusage);
+ endtime = GetCurrentTimestamp();
+ TimestampDifference(counters->starttime, endtime, &secs, &usecs);
+
/*
* Fill additional statistics on a vacuum processing operation.
*/
@@ -570,6 +576,14 @@ extvac_stats_end(Relation rel, LVExtStatCounters * counters,
report->total_blks_dirtied += bufusage.local_blks_dirtied + bufusage.shared_blks_dirtied;
report->total_blks_written += bufusage.shared_blks_written;
+ report->blk_read_time += INSTR_TIME_GET_MILLISEC(bufusage.local_blk_read_time);
+ report->blk_read_time += INSTR_TIME_GET_MILLISEC(bufusage.shared_blk_read_time);
+ report->blk_write_time += INSTR_TIME_GET_MILLISEC(bufusage.local_blk_write_time);
+ report->blk_write_time += INSTR_TIME_GET_MILLISEC(bufusage.shared_blk_write_time);
+ report->delay_time += VacuumDelayTime - counters->VacuumDelayTime;
+
+ report->total_time += secs * 1000. + usecs / 1000.;
+
if (!rel->pgstat_info || !pgstat_track_counts)
/*
@@ -667,21 +681,31 @@ accumulate_heap_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCount
extVacStats->table.recently_dead_tuples = vacrel->recently_dead_tuples;
extVacStats->table.missed_dead_tuples = vacrel->missed_dead_tuples;
extVacStats->table.missed_dead_pages = vacrel->missed_dead_pages;
+ extVacStats->common.wraparound_failsafe_count = vacrel->wraparound_failsafe_count;
+ extVacStats->common.blk_read_time -= vacrel->extVacReportIdx.common.blk_read_time;
+ extVacStats->common.blk_write_time -= vacrel->extVacReportIdx.common.blk_write_time;
extVacStats->common.total_blks_dirtied -= vacrel->extVacReportIdx.common.total_blks_dirtied;
extVacStats->common.total_blks_hit -= vacrel->extVacReportIdx.common.total_blks_hit;
extVacStats->common.total_blks_read -= vacrel->extVacReportIdx.common.total_blks_read;
extVacStats->common.total_blks_written -= vacrel->extVacReportIdx.common.total_blks_written;
+
+ extVacStats->common.total_time -= vacrel->extVacReportIdx.common.total_time;
+ extVacStats->common.delay_time -= vacrel->extVacReportIdx.common.delay_time;
}
static void
accumulate_idxs_vacuum_statistics(LVRelState *vacrel, PgStat_VacuumRelationCounts * extVacIdxStats)
{
/* Fill heap-specific extended stats fields */
+ vacrel->extVacReportIdx.common.blk_read_time += extVacIdxStats->common.blk_read_time;
+ vacrel->extVacReportIdx.common.blk_write_time += extVacIdxStats->common.blk_write_time;
vacrel->extVacReportIdx.common.total_blks_dirtied += extVacIdxStats->common.total_blks_dirtied;
vacrel->extVacReportIdx.common.total_blks_hit += extVacIdxStats->common.total_blks_hit;
vacrel->extVacReportIdx.common.total_blks_read += extVacIdxStats->common.total_blks_read;
vacrel->extVacReportIdx.common.total_blks_written += extVacIdxStats->common.total_blks_written;
+ vacrel->extVacReportIdx.common.delay_time += extVacIdxStats->common.delay_time;
+ vacrel->extVacReportIdx.common.total_time += extVacIdxStats->common.total_time;
}
/*
@@ -706,6 +730,10 @@ extvac_accumulate_idx_report(PgStat_VacuumRelationCounts * dst,
dst->common.total_blks_written += src->common.total_blks_written;
dst->common.blks_fetched += src->common.blks_fetched;
dst->common.blks_hit += src->common.blks_hit;
+ dst->common.blk_read_time += src->common.blk_read_time;
+ dst->common.blk_write_time += src->common.blk_write_time;
+ dst->common.delay_time += src->common.delay_time;
+ dst->common.total_time += src->common.total_time;
dst->common.tuples_deleted += src->common.tuples_deleted;
dst->index.pages_deleted += src->index.pages_deleted;
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e7d4165844..11135733c2 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1581,7 +1581,12 @@ CREATE VIEW pg_stat_vacuum_tables AS
S.total_blks_dirtied AS total_blks_dirtied,
S.total_blks_written AS total_blks_written,
S.rel_blks_read AS rel_blks_read,
- S.rel_blks_hit AS rel_blks_hit
+ S.rel_blks_hit AS rel_blks_hit,
+ S.blk_read_time AS blk_read_time,
+ S.blk_write_time AS blk_write_time,
+ S.delay_time AS delay_time,
+ S.total_time AS total_time,
+ S.wraparound_failsafe AS wraparound_failsafe
FROM pg_class C JOIN
pg_namespace N ON N.oid = C.relnamespace,
@@ -1605,7 +1610,12 @@ CREATE VIEW pg_stat_vacuum_indexes AS
S.total_blks_written AS total_blks_written,
S.rel_blks_read AS rel_blks_read,
- S.rel_blks_hit AS rel_blks_hit
+ S.rel_blks_hit AS rel_blks_hit,
+
+ S.blk_read_time AS blk_read_time,
+ S.blk_write_time AS blk_write_time,
+ S.delay_time AS delay_time,
+ S.total_time AS total_time
FROM
pg_class C JOIN
pg_index X ON C.oid = X.indrelid JOIN
@@ -1624,7 +1634,14 @@ CREATE VIEW pg_stat_vacuum_database AS
S.db_blks_read AS db_blks_read,
S.db_blks_hit AS db_blks_hit,
S.total_blks_dirtied AS total_blks_dirtied,
- S.total_blks_written AS total_blks_written
+ S.total_blks_written AS total_blks_written,
+
+ S.wraparound_failsafe AS wraparound_failsafe,
+
+ S.blk_read_time AS blk_read_time,
+ S.blk_write_time AS blk_write_time,
+ S.delay_time AS delay_time,
+ S.total_time AS total_time
FROM
pg_database D,
LATERAL pg_stat_get_vacuum_database(D.oid) S;
diff --git a/src/backend/utils/activity/pgstat_vacuum.c b/src/backend/utils/activity/pgstat_vacuum.c
index 9b1eaf0f4a..d0e2eea258 100644
--- a/src/backend/utils/activity/pgstat_vacuum.c
+++ b/src/backend/utils/activity/pgstat_vacuum.c
@@ -46,7 +46,13 @@ pgstat_accumulate_common(PgStat_CommonCounts *dst, const PgStat_CommonCounts *sr
ACCUMULATE_FIELD(blks_fetched);
ACCUMULATE_FIELD(blks_hit);
+ ACCUMULATE_FIELD(blk_read_time);
+ ACCUMULATE_FIELD(blk_write_time);
+ ACCUMULATE_FIELD(delay_time);
+ ACCUMULATE_FIELD(total_time);
+
ACCUMULATE_FIELD(tuples_deleted);
+ ACCUMULATE_FIELD(wraparound_failsafe_count);
}
/*
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 4f0be66621..75ebff6d68 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -2374,7 +2374,7 @@ pg_stat_have_stats(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 17
+#define PG_STAT_GET_VACUUM_TABLES_STATS_COLS 22
Oid relid = PG_GETARG_OID(0);
PgStat_VacuumRelationCounts *extvacuum;
@@ -2416,6 +2416,11 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
values[i++] = Int64GetDatum(extvacuum->common.total_blks_written);
values[i++] = Int64GetDatum(extvacuum->common.blks_fetched - extvacuum->common.blks_hit);
values[i++] = Int64GetDatum(extvacuum->common.blks_hit);
+ values[i++] = Float8GetDatum(extvacuum->common.blk_read_time);
+ values[i++] = Float8GetDatum(extvacuum->common.blk_write_time);
+ values[i++] = Float8GetDatum(extvacuum->common.delay_time);
+ values[i++] = Float8GetDatum(extvacuum->common.total_time);
+ values[i++] = Int32GetDatum(extvacuum->common.wraparound_failsafe_count);
Assert(i == PG_STAT_GET_VACUUM_TABLES_STATS_COLS);
@@ -2429,7 +2434,7 @@ pg_stat_get_vacuum_tables(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS 9
+#define PG_STAT_GET_VACUUM_INDEX_STATS_COLS 13
Oid relid = PG_GETARG_OID(0);
PgStat_VacuumRelationCounts *extvacuum;
@@ -2465,6 +2470,11 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
values[i++] = Int64GetDatum(extvacuum->common.blks_fetched - extvacuum->common.blks_hit);
values[i++] = Int64GetDatum(extvacuum->common.blks_hit);
+ values[i++] = Float8GetDatum(extvacuum->common.blk_read_time);
+ values[i++] = Float8GetDatum(extvacuum->common.blk_write_time);
+ values[i++] = Float8GetDatum(extvacuum->common.delay_time);
+ values[i++] = Float8GetDatum(extvacuum->common.total_time);
+
Assert(i == PG_STAT_GET_VACUUM_INDEX_STATS_COLS);
/* Returns the record as Datum */
@@ -2477,7 +2487,7 @@ pg_stat_get_vacuum_indexes(PG_FUNCTION_ARGS)
Datum
pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
{
-#define PG_STAT_GET_VACUUM_DATABASE_STATS_COLS 6
+#define PG_STAT_GET_VACUUM_DATABASE_STATS_COLS 11
Oid dbid = PG_GETARG_OID(0);
PgStat_VacuumDBCounts *extvacuum;
@@ -2505,6 +2515,13 @@ pg_stat_get_vacuum_database(PG_FUNCTION_ARGS)
values[i++] = Int64GetDatum(extvacuum->common.total_blks_dirtied);
values[i++] = Int64GetDatum(extvacuum->common.total_blks_written);
+ values[i++] = Int32GetDatum(extvacuum->common.wraparound_failsafe_count);
+
+ values[i++] = Float8GetDatum(extvacuum->common.blk_read_time);
+ values[i++] = Float8GetDatum(extvacuum->common.blk_write_time);
+ values[i++] = Float8GetDatum(extvacuum->common.delay_time);
+ values[i++] = Float8GetDatum(extvacuum->common.total_time);
+
Assert(i == PG_STAT_GET_VACUUM_DATABASE_STATS_COLS);
/* Returns the record as Datum */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 14f070a941..75d6e2b329 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12643,9 +12643,9 @@
proname => 'pg_stat_get_vacuum_tables', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8}',
- proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
- proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples,vm_new_frozen_pages,vm_new_visible_pages,vm_new_visible_frozen_pages,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,rel_blks_read,rel_blks_hit}',
+ proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,int8,float8,float8,float8,float8,int4}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{reloid,relid,pages_scanned,pages_removed,tuples_deleted,tuples_frozen,recently_dead_tuples,missed_dead_pages,missed_dead_tuples,vm_new_frozen_pages,vm_new_visible_pages,vm_new_visible_frozen_pages,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,rel_blks_read,rel_blks_hit,blk_read_time,blk_write_time,delay_time,total_time,wraparound_failsafe}',
prosrc => 'pg_stat_get_vacuum_tables' }
# oid8 related functions
@@ -12718,17 +12718,17 @@
proname => 'pg_stat_get_vacuum_indexes', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8}',
- proargmodes => '{i,o,o,o,o,o,o,o,o,o}',
- proargnames => '{reloid,relid,pages_deleted,tuples_deleted,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,rel_blks_read,rel_blks_hit}',
+ proallargtypes => '{oid,oid,int8,int8,int8,int8,int8,int8,int8,int8,float8,float8,float8,float8}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{reloid,relid,pages_deleted,tuples_deleted,total_blks_read,total_blks_hit,total_blks_dirtied,total_blks_written,rel_blks_read,rel_blks_hit,blk_read_time,blk_write_time,delay_time,total_time}',
prosrc => 'pg_stat_get_vacuum_indexes' },
{ oid => '8005',
descr => 'pg_stat_get_vacuum_database returns vacuum stats values for database',
proname => 'pg_stat_get_vacuum_database', prorows => 1000, provolatile => 's', prorettype => 'record', proisstrict => 'f',
proretset => 't',
proargtypes => 'oid',
- proallargtypes => '{oid,oid,int4,int8,int8,int8,int8}',
- proargmodes => '{i,o,o,o,o,o,o}',
- proargnames => '{dbid,dboid,errors,db_blks_read,db_blks_hit,total_blks_dirtied,total_blks_written}',
+ proallargtypes => '{oid,oid,int4,int8,int8,int8,int8,int4,float8,float8,float8,float8}',
+ proargmodes => '{i,o,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{dbid,dboid,errors,db_blks_read,db_blks_hit,total_blks_dirtied,total_blks_written,wraparound_failsafe,blk_read_time,blk_write_time,delay_time,total_time}',
prosrc => 'pg_stat_get_vacuum_database' },
]
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index de38949d12..bfc2995389 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -185,8 +185,17 @@ typedef struct PgStat_CommonCounts
int64 blks_fetched;
int64 blks_hit;
+ /* Time */
+ double blk_read_time;
+ double blk_write_time;
+ double delay_time;
+ double total_time;
+
/* tuples */
int64 tuples_deleted;
+
+ /* failsafe */
+ int32 wraparound_failsafe_count;
} PgStat_CommonCounts;
/* ----------
diff --git a/src/test/modules/xid_wraparound/meson.build b/src/test/modules/xid_wraparound/meson.build
index 97ce670f9a..9224e59d1d 100644
--- a/src/test/modules/xid_wraparound/meson.build
+++ b/src/test/modules/xid_wraparound/meson.build
@@ -31,6 +31,7 @@ tests += {
't/002_limits.pl',
't/003_wraparounds.pl',
't/004_notify_freeze.pl',
+ 't/005_vacuum_stats_failsafe.pl',
],
},
}
diff --git a/src/test/modules/xid_wraparound/t/005_vacuum_stats_failsafe.pl b/src/test/modules/xid_wraparound/t/005_vacuum_stats_failsafe.pl
new file mode 100644
index 0000000000..eff77fe876
--- /dev/null
+++ b/src/test/modules/xid_wraparound/t/005_vacuum_stats_failsafe.pl
@@ -0,0 +1,66 @@
+# Copyright (c) 2024-2026, PostgreSQL Global Development Group
+
+# Test that the wraparound failsafe counter exposed by the extended vacuum
+# statistics views advances when a VACUUM engages the wraparound failsafe.
+#
+# The failsafe only triggers once a relation's age exceeds
+# max(vacuum_failsafe_age, autovacuum_freeze_max_age * 1.05), which cannot be
+# reached by an ordinary regression test. Here we lower
+# autovacuum_freeze_max_age to its minimum and use the xid_wraparound
+# extension to burn enough transaction IDs to age the table past that
+# threshold, then check that the wraparound_failsafe counters advance.
+
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+
+if (!$ENV{PG_TEST_EXTRA} || $ENV{PG_TEST_EXTRA} !~ /\bxid_wraparound\b/)
+{
+ plan skip_all => "test xid_wraparound not enabled in PG_TEST_EXTRA";
+}
+
+my $node = PostgreSQL::Test::Cluster->new('main');
+$node->init;
+$node->append_conf(
+ 'postgresql.conf', qq[
+autovacuum = off
+track_vacuum_statistics = on
+# Lower the wraparound-failsafe threshold as far as possible so that a modest
+# number of consumed XIDs is enough to engage the failsafe.
+autovacuum_freeze_max_age = 100000
+vacuum_failsafe_age = 0
+]);
+$node->start;
+$node->safe_psql('postgres', 'CREATE EXTENSION xid_wraparound');
+
+# A table whose relfrozenxid age we will push past the failsafe threshold.
+$node->safe_psql(
+ 'postgres', qq[
+CREATE TABLE fs_tab (id int) WITH (autovacuum_enabled = off);
+INSERT INTO fs_tab SELECT generate_series(1, 1000);
+]);
+
+# Advance the XID counter well past the failsafe threshold.
+$node->safe_psql('postgres', 'SELECT consume_xids(200000)');
+
+# This VACUUM must engage the wraparound failsafe.
+$node->safe_psql('postgres', 'VACUUM fs_tab');
+
+# The per-table view records that the failsafe was engaged for this relation.
+my $tab = $node->safe_psql(
+ 'postgres', qq[
+SELECT wraparound_failsafe > 0
+ FROM pg_stat_vacuum_tables WHERE relname = 'fs_tab']);
+is($tab, 't', 'wraparound_failsafe advanced in pg_stat_vacuum_tables');
+
+# The per-database aggregate counts the failsafe as well.
+my $db = $node->safe_psql(
+ 'postgres', qq[
+SELECT wraparound_failsafe > 0
+ FROM pg_stat_vacuum_database WHERE dbname = current_database()]);
+is($db, 't', 'wraparound_failsafe advanced in pg_stat_vacuum_database');
+
+$node->stop;
+done_testing();
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 0b28e2ffc3..952392f66d 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2427,9 +2427,14 @@ pg_stat_vacuum_database| SELECT d.oid AS dboid,
s.db_blks_read,
s.db_blks_hit,
s.total_blks_dirtied,
- s.total_blks_written
+ s.total_blks_written,
+ s.wraparound_failsafe,
+ s.blk_read_time,
+ s.blk_write_time,
+ s.delay_time,
+ s.total_time
FROM pg_database d,
- LATERAL pg_stat_get_vacuum_database(d.oid) s(dboid, errors, db_blks_read, db_blks_hit, total_blks_dirtied, total_blks_written);
+ LATERAL pg_stat_get_vacuum_database(d.oid) s(dboid, errors, db_blks_read, db_blks_hit, total_blks_dirtied, total_blks_written, wraparound_failsafe, blk_read_time, blk_write_time, delay_time, total_time);
pg_stat_vacuum_indexes| SELECT c.oid AS relid,
i.oid AS indexrelid,
n.nspname AS schemaname,
@@ -2442,12 +2447,16 @@ pg_stat_vacuum_indexes| SELECT c.oid AS relid,
s.total_blks_dirtied,
s.total_blks_written,
s.rel_blks_read,
- s.rel_blks_hit
+ s.rel_blks_hit,
+ s.blk_read_time,
+ s.blk_write_time,
+ s.delay_time,
+ s.total_time
FROM (((pg_class c
JOIN pg_index x ON ((c.oid = x.indrelid)))
JOIN pg_class i ON ((i.oid = x.indexrelid)))
LEFT JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
- LATERAL pg_stat_get_vacuum_indexes(i.oid) s(relid, pages_deleted, tuples_deleted, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, rel_blks_read, rel_blks_hit)
+ LATERAL pg_stat_get_vacuum_indexes(i.oid) s(relid, pages_deleted, tuples_deleted, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, rel_blks_read, rel_blks_hit, blk_read_time, blk_write_time, delay_time, total_time)
WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
c.relname,
@@ -2467,10 +2476,15 @@ pg_stat_vacuum_tables| SELECT n.nspname AS schemaname,
s.total_blks_dirtied,
s.total_blks_written,
s.rel_blks_read,
- s.rel_blks_hit
+ s.rel_blks_hit,
+ s.blk_read_time,
+ s.blk_write_time,
+ s.delay_time,
+ s.total_time,
+ s.wraparound_failsafe
FROM (pg_class c
JOIN pg_namespace n ON ((n.oid = c.relnamespace))),
- LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples, vm_new_frozen_pages, vm_new_visible_pages, vm_new_visible_frozen_pages, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, rel_blks_read, rel_blks_hit)
+ LATERAL pg_stat_get_vacuum_tables(c.oid) s(relid, pages_scanned, pages_removed, tuples_deleted, tuples_frozen, recently_dead_tuples, missed_dead_pages, missed_dead_tuples, vm_new_frozen_pages, vm_new_visible_pages, vm_new_visible_frozen_pages, total_blks_read, total_blks_hit, total_blks_dirtied, total_blks_written, rel_blks_read, rel_blks_hit, blk_read_time, blk_write_time, delay_time, total_time, wraparound_failsafe)
WHERE (c.relkind = ANY (ARRAY['r'::"char", 't'::"char", 'm'::"char"]));
pg_stat_wal| SELECT wal_records,
wal_fpi,
diff --git a/src/test/regress/expected/vacuum_stats.out b/src/test/regress/expected/vacuum_stats.out
index bf2fbc7626..b5bae99946 100644
--- a/src/test/regress/expected/vacuum_stats.out
+++ b/src/test/regress/expected/vacuum_stats.out
@@ -124,6 +124,43 @@ SELECT rel_blks_read >= 0 AS rel_blks_read,
t | t
(1 row)
+-- timing metrics and failsafe. The vacuum always takes some wall-clock time
+-- (total_time > 0) and does not engage the wraparound failsafe under normal
+-- conditions (wraparound_failsafe = 0). blk_read_time/blk_write_time are only
+-- non-zero when track_io_timing is enabled, and delay_time only when a vacuum
+-- cost delay is configured, so those are merely checked for being
+-- non-negative; the positive delay_time path is exercised separately below.
+SELECT blk_read_time >= 0 AS blk_read_time,
+ blk_write_time >= 0 AS blk_write_time,
+ delay_time >= 0 AS delay_time,
+ total_time > 0 AS total_time,
+ wraparound_failsafe = 0 AS wraparound_failsafe
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+ blk_read_time | blk_write_time | delay_time | total_time | wraparound_failsafe
+---------------+----------------+------------+------------+---------------------
+ t | t | t | t | t
+(1 row)
+
+-- delay path: with a vacuum cost delay configured, a vacuum that accrues cost
+-- sleeps, so delay_time advances.
+CREATE TABLE vacstat_delay (id int PRIMARY KEY, v text)
+ WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vacstat_delay SELECT g, repeat('x', 100) FROM generate_series(1, 3000) g;
+DELETE FROM vacstat_delay WHERE id % 2 = 0;
+SET vacuum_cost_delay = '1ms';
+SET vacuum_cost_limit = 1;
+VACUUM vacstat_delay;
+RESET vacuum_cost_delay;
+RESET vacuum_cost_limit;
+SELECT delay_time > 0 AS delay_time,
+ total_time > 0 AS total_time
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_delay';
+ delay_time | total_time
+------------+------------
+ t | t
+(1 row)
+
+DROP TABLE vacstat_delay;
-- per-index view: the primary key index is processed by the same VACUUM.
-- No btree leaf empties out (interleaved deletions), so pages_deleted = 0,
-- while every index entry for a removed heap tuple is deleted. The index is
@@ -137,11 +174,15 @@ SELECT indexrelname,
total_blks_dirtied >= 0 AS total_blks_dirtied,
total_blks_written >= 0 AS total_blks_written,
rel_blks_read >= 0 AS rel_blks_read,
- rel_blks_hit > 0 AS rel_blks_hit
+ rel_blks_hit > 0 AS rel_blks_hit,
+ blk_read_time >= 0 AS blk_read_time,
+ blk_write_time >= 0 AS blk_write_time,
+ delay_time >= 0 AS delay_time,
+ total_time > 0 AS total_time
FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_t' ORDER BY indexrelname;
- indexrelname | pages_deleted | tuples_deleted | total_blks_read | total_blks_hit | total_blks_dirtied | total_blks_written | rel_blks_read | rel_blks_hit
-----------------+---------------+----------------+-----------------+----------------+--------------------+--------------------+---------------+--------------
- vacstat_t_pkey | t | t | t | t | t | t | t | t
+ indexrelname | pages_deleted | tuples_deleted | total_blks_read | total_blks_hit | total_blks_dirtied | total_blks_written | rel_blks_read | rel_blks_hit | blk_read_time | blk_write_time | delay_time | total_time
+----------------+---------------+----------------+-----------------+----------------+--------------------+--------------------+---------------+--------------+---------------+----------------+------------+------------
+ vacstat_t_pkey | t | t | t | t | t | t | t | t | t | t | t | t
(1 row)
-- index page-deletion path: deleting a contiguous key range empties whole
@@ -170,10 +211,15 @@ SELECT errors = 0 AS errors,
db_blks_read >= 0 AS db_blks_read,
db_blks_hit > 0 AS db_blks_hit,
total_blks_dirtied >= 0 AS total_blks_dirtied,
- total_blks_written >= 0 AS total_blks_written
+ total_blks_written >= 0 AS total_blks_written,
+ wraparound_failsafe = 0 AS wraparound_failsafe,
+ blk_read_time >= 0 AS blk_read_time,
+ blk_write_time >= 0 AS blk_write_time,
+ delay_time >= 0 AS delay_time,
+ total_time > 0 AS total_time
FROM pg_stat_vacuum_database WHERE dbname = current_database();
- errors | db_blks_read | db_blks_hit | total_blks_dirtied | total_blks_written
---------+--------------+-------------+--------------------+--------------------
- t | t | t | t | t
+ errors | db_blks_read | db_blks_hit | total_blks_dirtied | total_blks_written | wraparound_failsafe | blk_read_time | blk_write_time | delay_time | total_time
+--------+--------------+-------------+--------------------+--------------------+---------------------+---------------+----------------+------------+------------
+ t | t | t | t | t | t | t | t | t | t
(1 row)
diff --git a/src/test/regress/sql/vacuum_stats.sql b/src/test/regress/sql/vacuum_stats.sql
index 49ed3b4063..ee517d6f44 100644
--- a/src/test/regress/sql/vacuum_stats.sql
+++ b/src/test/regress/sql/vacuum_stats.sql
@@ -94,6 +94,35 @@ SELECT rel_blks_read >= 0 AS rel_blks_read,
rel_blks_hit > 0 AS rel_blks_hit
FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+-- timing metrics and failsafe. The vacuum always takes some wall-clock time
+-- (total_time > 0) and does not engage the wraparound failsafe under normal
+-- conditions (wraparound_failsafe = 0). blk_read_time/blk_write_time are only
+-- non-zero when track_io_timing is enabled, and delay_time only when a vacuum
+-- cost delay is configured, so those are merely checked for being
+-- non-negative; the positive delay_time path is exercised separately below.
+SELECT blk_read_time >= 0 AS blk_read_time,
+ blk_write_time >= 0 AS blk_write_time,
+ delay_time >= 0 AS delay_time,
+ total_time > 0 AS total_time,
+ wraparound_failsafe = 0 AS wraparound_failsafe
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_t';
+
+-- delay path: with a vacuum cost delay configured, a vacuum that accrues cost
+-- sleeps, so delay_time advances.
+CREATE TABLE vacstat_delay (id int PRIMARY KEY, v text)
+ WITH (autovacuum_enabled = off, fillfactor = 10);
+INSERT INTO vacstat_delay SELECT g, repeat('x', 100) FROM generate_series(1, 3000) g;
+DELETE FROM vacstat_delay WHERE id % 2 = 0;
+SET vacuum_cost_delay = '1ms';
+SET vacuum_cost_limit = 1;
+VACUUM vacstat_delay;
+RESET vacuum_cost_delay;
+RESET vacuum_cost_limit;
+SELECT delay_time > 0 AS delay_time,
+ total_time > 0 AS total_time
+ FROM pg_stat_vacuum_tables WHERE relname = 'vacstat_delay';
+DROP TABLE vacstat_delay;
+
-- per-index view: the primary key index is processed by the same VACUUM.
-- No btree leaf empties out (interleaved deletions), so pages_deleted = 0,
-- while every index entry for a removed heap tuple is deleted. The index is
@@ -107,7 +136,11 @@ SELECT indexrelname,
total_blks_dirtied >= 0 AS total_blks_dirtied,
total_blks_written >= 0 AS total_blks_written,
rel_blks_read >= 0 AS rel_blks_read,
- rel_blks_hit > 0 AS rel_blks_hit
+ rel_blks_hit > 0 AS rel_blks_hit,
+ blk_read_time >= 0 AS blk_read_time,
+ blk_write_time >= 0 AS blk_write_time,
+ delay_time >= 0 AS delay_time,
+ total_time > 0 AS total_time
FROM pg_stat_vacuum_indexes WHERE relname = 'vacstat_t' ORDER BY indexrelname;
-- index page-deletion path: deleting a contiguous key range empties whole
@@ -132,5 +165,10 @@ SELECT errors = 0 AS errors,
db_blks_read >= 0 AS db_blks_read,
db_blks_hit > 0 AS db_blks_hit,
total_blks_dirtied >= 0 AS total_blks_dirtied,
- total_blks_written >= 0 AS total_blks_written
+ total_blks_written >= 0 AS total_blks_written,
+ wraparound_failsafe = 0 AS wraparound_failsafe,
+ blk_read_time >= 0 AS blk_read_time,
+ blk_write_time >= 0 AS blk_write_time,
+ delay_time >= 0 AS delay_time,
+ total_time > 0 AS total_time
FROM pg_stat_vacuum_database WHERE dbname = current_database();
--
2.39.5 (Apple Git-154)