v34-0004-Add-system-view-tracking-IO-ops-per-backend-type.patch
text/x-patch
Filename: v34-0004-Add-system-view-tracking-IO-ops-per-backend-type.patch
Type: text/x-patch
Part: 3
Patch
Format: format-patch
Series: patch v34-0004
Subject: Add system view tracking IO ops per backend type
| File | + | − |
|---|---|---|
| doc/src/sgml/monitoring.sgml | 328 | 2 |
| src/backend/catalog/system_views.sql | 17 | 0 |
| src/backend/utils/adt/pgstatfuncs.c | 153 | 0 |
| src/include/catalog/pg_proc.dat | 9 | 0 |
| src/test/regress/expected/rules.out | 14 | 0 |
| src/test/regress/expected/stats.out | 242 | 0 |
| src/test/regress/sql/stats.sql | 131 | 0 |
From 1de196bad4aba06beb32ba6487145d765794a7c5 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <melanieplageman@gmail.com>
Date: Thu, 6 Oct 2022 12:24:42 -0400
Subject: [PATCH v34 4/5] Add system view tracking IO ops per backend type
Add pg_stat_io, a system view which tracks the number of IOOps (freelist
acquisitions, evictions, reuses, rejections, repossessions, reads,
writes, extends, and fsyncs) done through each IOContext (shared
buffers, local buffers, and buffers reserved by a BufferAccessStrategy)
by each type of backend (e.g. client backend, checkpointer).
Some BackendTypes do not accumulate IO operations statistics and will
not be included in the view.
Some IOContexts are not used by some BackendTypes and will not be in the
view. For example, checkpointer does not use a BufferAccessStrategy
(currently), so there will be no rows for BufferAccessStrategy
IOContexts for checkpointer.
Some IOOps are invalid in combination with certain IOContexts. Those
cells will be NULL in the view to distinguish between 0 observed IOOps
of that type and an invalid combination. For example, local buffers are
not fsynced so cells for all BackendTypes for IOCONTEXT_LOCAL and
IOOP_FSYNC will be NULL.
Some BackendTypes never perform certain IOOps. Those cells will also be
NULL in the view. For example, bgwriter should not perform reads.
View stats are populated with statistics incremented when a backend
performs an IO Operation and maintained by the cumulative statistics
subsystem.
Each row of the view shows stats for a particular BackendType and
IOContext combination (e.g. shared buffer accesses by checkpointer) and
each column in the view is the total number of IO Operations done (e.g.
writes).
So a cell in the view would be, for example, the number of shared
buffers written by checkpointer since the last stats reset.
In anticipation of tracking WAL IO and non-block-oriented IO (such as
temporary file IO), the "unit" column specifies the unit of the "read",
"written", and "extended" columns for a given row.
Note that some of the cells in the view are redundant with fields in
pg_stat_bgwriter (e.g. buffers_backend), however these have been kept in
pg_stat_bgwriter for backwards compatibility. Deriving the redundant
pg_stat_bgwriter stats from the IO operations stats structures was also
problematic due to the separate reset targets for 'bgwriter' and 'io'.
Suggested by Andres Freund
Author: Melanie Plageman <melanieplageman@gmail.com>
Reviewed-by: Andres Freund <andres@anarazel.de>
Reviewed-by: Justin Pryzby <pryzby@telsasoft.com>
Reviewed-by: Kyotaro Horiguchi <horikyota.ntt@gmail.com>
Reviewed-by: Maciek Sakrejda <m.sakrejda@gmail.com>
Reviewed-by: Lukas Fittl <lukas@fittl.com>
Discussion: https://www.postgresql.org/message-id/flat/20200124195226.lth52iydq2n2uilq%40alap3.anarazel.de
---
doc/src/sgml/monitoring.sgml | 330 ++++++++++++++++++++++++++-
src/backend/catalog/system_views.sql | 17 ++
src/backend/utils/adt/pgstatfuncs.c | 153 +++++++++++++
src/include/catalog/pg_proc.dat | 9 +
src/test/regress/expected/rules.out | 14 ++
src/test/regress/expected/stats.out | 242 ++++++++++++++++++++
src/test/regress/sql/stats.sql | 131 +++++++++++
7 files changed, 894 insertions(+), 2 deletions(-)
diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 698f274341..e144bb0c35 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -448,6 +448,15 @@ postgres 27093 0.0 0.0 30096 2752 ? Ss 11:34 0:00 postgres: ser
</entry>
</row>
+ <row>
+ <entry><structname>pg_stat_io</structname><indexterm><primary>pg_stat_io</primary></indexterm></entry>
+ <entry>A row for each IO Context for each backend type showing
+ statistics about backend IO operations. See
+ <link linkend="monitoring-pg-stat-io-view">
+ <structname>pg_stat_io</structname></link> for details.
+ </entry>
+ </row>
+
<row>
<entry><structname>pg_stat_wal</structname><indexterm><primary>pg_stat_wal</primary></indexterm></entry>
<entry>One row only, showing statistics about WAL activity. See
@@ -3600,13 +3609,12 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
<structfield>stats_reset</structfield> <type>timestamp with time zone</type>
</para>
<para>
- Time at which these statistics were last reset
+ Time at which these statistics were last reset.
</para></entry>
</row>
</tbody>
</tgroup>
</table>
-
<para>
Normally, WAL files are archived in order, oldest to newest, but that is
not guaranteed, and does not hold under special circumstances like when
@@ -3615,7 +3623,325 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
<structfield>last_archived_wal</structfield> have also been successfully
archived.
</para>
+ </sect2>
+
+ <sect2 id="monitoring-pg-stat-io-view">
+ <title><structname>pg_stat_io</structname></title>
+
+ <indexterm>
+ <primary>pg_stat_io</primary>
+ </indexterm>
+
+ <para>
+ The <structname>pg_stat_io</structname> view has a row for each backend type
+ and IO context containing global data for the cluster on IO operations done
+ by that backend type in that IO context. Currently only a subset of IO
+ operations are tracked here. WAL IO, IO on temporary files, and some forms
+ of IO outside of shared buffers (such as when building indexes or moving a
+ table from one tablespace to another) could be added in the future.
+ </para>
+
+ <table id="pg-stat-io-view" xreflabel="pg_stat_io">
+ <title><structname>pg_stat_io</structname> View</title>
+ <tgroup cols="1">
+ <thead>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ Column Type
+ </para>
+ <para>
+ Description
+ </para></entry>
+ </row>
+ </thead>
+ <tbody>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>backend_type</structfield> <type>text</type>
+ </para>
+ <para>
+ Type of backend (e.g. background worker, autovacuum worker).
+ See <link linkend="monitoring-pg-stat-activity-view">
+ <structname>pg_stat_activity</structname></link> for more information on
+ <varname>backend_type</varname>s.
+ </para></entry>
+ </row>
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>io_context</structfield> <type>text</type>
+ </para>
+ <para>
+ IO Context used. This refers to the context or location of an IO
+ operation. <literal>shared</literal> refers to shared buffers, the
+ primary buffer pool for relation data. <literal>local</literal> refers
+ to process-local memory used for temporary tables.
+ <literal>vacuum</literal> refers to memory reserved for use during
+ vacuumming and analyzing. <literal>bulkread</literal> refers to memory
+ reserved for use during bulk read operations.
+ <literal>bulkwrite</literal> refers to memory reserved for use during
+ bulk write operations. These last three <varname>io_context</varname>s
+ are counted separately because the autovacuum daemon, explicit
+ <command>VACUUM</command>, explicit <command>ANALYZE</command>, many
+ bulk reads, and many bulk writes use a fixed amount of memory, acquiring
+ the equivalent number of shared buffers and reusing them circularly to
+ avoid occupying an undue portion of the main shared buffer pool.
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>read</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Reads by this <varname>backend_type</varname> into buffers in this
+ <varname>io_context</varname>.
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>written</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Writes of data in this <varname>io_context</varname> written out by this
+ <varname>backend_type</varname>. Note that the values of
+ <varname>written</varname> for <varname>backend_type</varname>
+ <literal>background writer</literal> and <varname>backend_type</varname>
+ <literal>checkpointer</literal> are equivalent to the values of
+ <varname>buffers_clean</varname> and
+ <varname>buffers_checkpoint</varname>, respectively, in <link
+ linkend="monitoring-pg-stat-bgwriter-view">
+ <structname>pg_stat_bgwriter</structname></link>. Also, the sum of
+ <varname>written</varname> and <varname>extended</varname> in this view
+ for <varname>backend_type</varname>s <literal>client backend</literal>,
+ <literal>autovacuum worker</literal>, <literal>background
+ worker</literal>, and <literal>walsender</literal> in
+ <varname>io_context</varname>s <literal>shared</literal>,
+ <literal>bulkread</literal>, <literal>bulkwrite</literal>, and
+ <literal>vacuum</literal> is equivalent to
+ <varname>buffers_backend</varname> in
+ <structname>pg_stat_bgwriter</structname>.
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>extended</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Extends of relations done by this <varname>backend_type</varname> in
+ order to write data in this <varname>io_context</varname>.
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>unit</structfield> <type>text</type>
+ </para>
+ <para>
+ The unit of IO read, written, or extended. For block-oriented IO of
+ relation data, reads, writes, and extends are done in
+ <varname>block_size</varname> units. Thus, the <varname>unit</varname>
+ column will be the value of the build-time parameter
+ <symbol>BLCKSZ</symbol> represented in kilobytes. Future values could
+ include those derived from <symbol>XLOG_BLCKSZ</symbol>, once WAL IO is
+ tracked in this view and <quote>bytes</quote>, once non-block-oriented IO
+ (e.g. temporary file IO) is tracked here.
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>freelist_acquired</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of unoccupied shared or local buffers this
+ <varname>backend_type</varname> has acquired for use as a buffer
+ in this <varname>io_context</varname>.
+
+ A freelist of shared buffers which have yet to be used or whose contents
+ have been invalidated and have yet to be replaced with other contents is
+ maintained in shared memory.
+
+ Acquisition of a buffer from the freelist constitutes a <quote>cache
+ miss</quote>.
+
+ <varname>freelist_acquired</varname> in <varname>io_context</varname>
+ <literal>shared</literal> counts the acquisitions of an unoccupied
+ shared buffer from the freelist.
+
+ Given a working set close to or larger than the size of shared buffers,
+ once the database has warmed up initially, fewer
+ <varname>freelist_acquired</varname> are expected. Instead, data
+ resident in shared buffers will need to be evicted in order to read in
+ non-resident data. This will be manifest as increased
+ <varname>evicted</varname> count in <varname>io_context</varname>
+ <literal>shared</literal>.
+
+ <varname>freelist_acquired</varname> in <varname>io_context</varname>
+ <literal>vacuum</literal>, <literal>bulkread</literal>, and
+ <literal>bulkwrite</literal> counts the number of times unoccupied
+ shared buffers were acquired from the freelist and added to the
+ fixed-size strategy ring buffer. Shared buffers are added to the
+ strategy ring lazily. If the current buffer in the ring is pinned or in
+ use by another backend, it may be replaced with a new shared buffer. In
+ <varname>io_context</varname> <literal>bulkread</literal>, existing
+ dirty buffers in the ring requirng flush are
+ <varname>rejected</varname>. If all of the buffers in the strategy ring
+ have been <varname>rejected</varname>, a new shared buffer will be added
+ to the ring and will be counted as <varname>freelist_acquired</varname>
+ if the replacement buffer is procured from the freelist.
+
+ <varname>freelist_acquired</varname> in <varname>io_context</varname>
+ <literal>local</literal> counts the number of local buffers allocated.
+ Local buffers are allocated lazily.
+ </para></entry>
+ </row>
+
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>evicted</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of times a <varname>backend_type</varname> has evicted a block
+ from a shared or local buffer in order to reuse the buffer in this
+ <varname>io_context</varname>.
+
+ <varname>evicted</varname> and <varname>freelist_acquired</varname>
+ together constitute the number of cache misses for local or shared
+ buffers. A large number of <varname>evicted</varname> buffers after the
+ database has initially warmed up could indicate that shared buffers is
+ too small and should be set to a larger value.
+
+ <varname>evicted</varname> in <varname>io_context</varname>
+ <literal>shared</literal> counts the eviction of a block from a shared
+ buffer so that it can be replaced with another block, also in shared
+ buffers.
+
+ If shared buffers are available on the freelist, then no eviction is
+ necessary and the acquisition of the required buffer is counted as a
+ <literal>freelist_acquired</literal> operation in the
+ <varname>io_context</varname> <literal>shared</literal>.
+
+ <varname>evicted</varname> in <varname>io_context</varname>
+ <literal>vacuum</literal>, <literal>bulkread</literal>, and
+ <literal>bulkwrite</literal> counts the number of times occupied shared
+ buffers were added to the fixed-size strategy ring buffer, causing the
+ buffer contents to be evicted. If the current buffer in the ring is
+ pinned or in use by another backend, it may be replaced by a new shared
+ buffer. If this shared buffer contains valid data, that block must be
+ evicted and will count as <varname>evicted</varname>.
+
+ In <varname>io_context</varname> <literal>bulkread</literal>, existing
+ dirty buffers in the ring requirng flush are
+ <varname>rejected</varname>. If all of the buffers in the strategy ring
+ have been <varname>rejected</varname>, a new shared buffer will be added
+ to the ring. If the new shared buffer is occupied, its contents will
+ need to be evicted.
+
+ Seeing a large number of <varname>evicted</varname> in strategy
+ <varname>io_context</varname>s can provide insight into primary working
+ set cache misses.
+
+ <varname>evicted</varname> in <varname>io_context</varname>
+ <literal>local</literal> counts the eviction of a block of data from an
+ existing local buffer in order to replace it with another block, also in
+ local buffers.
+
+ This is in contrast with <varname>freelist_acquired</varname> operations
+ in <varname>io_context</varname> <literal>local</literal> which are
+ counted the first time that the memory for a local buffer is allocated.
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>reused</structfield> <type>bigint</type>
+ </para>
+ <para>
+ The number of times an existing buffer in the strategy ring was reused
+ as part of an operation in the <literal>bulkread</literal>,
+ <literal>bulkwrite</literal>, and <literal>vacuum</literal>
+ <varname>io_context</varname>s. This is equivalent to
+ <varname>evicted</varname> for shared buffers in
+ <varname>io_context</varname> <literal>shared</literal>, as the contents
+ of the buffer are <quote>evicted</quote> but refers to the case when the
+ eviction target is already a member of the strategy ring.
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>rejected</structfield> <type>bigint</type>
+ </para>
+ <para>
+ The number of times a <literal>bulkread</literal> found the current
+ buffer in the fixed-size strategy ring dirty and requiring flush.
+ <quote>Rejecting</quote> the buffer effectively removes it from the
+ strategy ring buffer allowing the slot in the ring to be replaced in the
+ future with a new shared buffer. A high number of
+ <literal>bulkread</literal> rejections can indicate a need for more
+ frequent vacuuming or more aggressive autovacuum settings, as buffers are
+ dirtied during a bulkread operation when updating the hint bit or when
+ performing on-access pruning.
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>repossessed</structfield> <type>bigint</type>
+ </para>
+ <para>
+ The number of times a buffer in the fixed-size ring buffer used by
+ operations in the <literal>bulkread</literal>,
+ <literal>bulkwrite</literal>, and <literal>vacuum</literal>
+ <varname>io_context</varname>s was removed from that ring buffer because
+ it was pinned or in use by another backend and thus could not have its
+ tenant block evicted so it could be reused. Once removed from the
+ strategy ring, this buffer is a <quote>normal</quote> shared buffer
+ again. A high number of repossessions is a sign of contention for the
+ blocks operated on by the strategy operation.
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>files_synced</structfield> <type>bigint</type>
+ </para>
+ <para>
+ Number of files fsynced by this <varname>backend_type</varname> for the
+ purpose of persisting data dirtied in this
+ <varname>io_context</varname>. <literal>fsyncs</literal> are done at
+ segment boundaries so <varname>unit</varname> does not apply to the
+ <varname>files_synced</varname> column. <literal>fsyncs</literal> done
+ by backends in order to persist data written in
+ <varname>io_context</varname> <literal>vacuum</literal>,
+ <varname>io_context</varname> <literal>bulkread</literal>, or
+ <varname>io_context</varname> <literal>bulkwrite</literal> are counted
+ as an <varname>io_context</varname> <literal>shared</literal>
+ <literal>fsync</literal>. Note that the sum of
+ <varname>files_synced</varname> for all <varname>io_context</varname>
+ <literal>shared</literal> for all <varname>backend_type</varname>s
+ except <literal>checkpointer</literal> is equivalent to
+ <varname>buffers_backend_fsync</varname> in <link
+ linkend="monitoring-pg-stat-bgwriter-view">
+ <structname>pg_stat_bgwriter</structname></link>.
+ </para></entry>
+ </row>
+
+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>stats_reset</structfield> <type>timestamp with time zone</type>
+ </para>
+ <para>
+ Time at which these statistics were last reset.
+ </para></entry>
+ </row>
+ </tbody>
+ </tgroup>
+ </table>
</sect2>
<sect2 id="monitoring-pg-stat-bgwriter-view">
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 2d8104b090..f851cc9aac 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1117,6 +1117,23 @@ CREATE VIEW pg_stat_bgwriter AS
pg_stat_get_buf_alloc() AS buffers_alloc,
pg_stat_get_bgwriter_stat_reset_time() AS stats_reset;
+CREATE VIEW pg_stat_io AS
+SELECT
+ b.backend_type,
+ b.io_context,
+ b.read,
+ b.written,
+ b.extended,
+ b.unit,
+ b.freelist_acquired,
+ b.evicted,
+ b.reused,
+ b.rejected,
+ b.repossessed,
+ b.files_synced,
+ b.stats_reset
+FROM pg_stat_get_io() b;
+
CREATE VIEW pg_stat_wal AS
SELECT
w.wal_records,
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index b783af130c..74ee9f61d0 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -29,6 +29,7 @@
#include "storage/procarray.h"
#include "utils/acl.h"
#include "utils/builtins.h"
+#include "utils/guc.h"
#include "utils/inet.h"
#include "utils/timestamp.h"
@@ -1725,6 +1726,158 @@ pg_stat_get_buf_alloc(PG_FUNCTION_ARGS)
PG_RETURN_INT64(pgstat_fetch_stat_bgwriter()->buf_alloc);
}
+/*
+* When adding a new column to the pg_stat_io view, add a new enum value
+* here above IO_NUM_COLUMNS.
+*/
+typedef enum io_stat_col
+{
+ IO_COL_BACKEND_TYPE,
+ IO_COL_IO_CONTEXT,
+ IO_COL_READS,
+ IO_COL_WRITES,
+ IO_COL_EXTENDS,
+ IO_COL_UNIT,
+ IO_COL_FREELIST_ACQUISITIONS,
+ IO_COL_EVICTIONS,
+ IO_COL_REUSES,
+ IO_COL_REJECTIONS,
+ IO_COL_REPOSSESSIONS,
+ IO_COL_FSYNCS,
+ IO_COL_RESET_TIME,
+ IO_NUM_COLUMNS,
+} io_stat_col;
+
+/*
+ * When adding a new IOOp, add a new io_stat_col and add a case to this
+ * function returning the corresponding io_stat_col.
+ */
+static io_stat_col
+pgstat_io_op_get_index(IOOp io_op)
+{
+ switch (io_op)
+ {
+ case IOOP_EVICT:
+ return IO_COL_EVICTIONS;
+ case IOOP_FREELIST_ACQUIRE:
+ return IO_COL_FREELIST_ACQUISITIONS;
+ case IOOP_READ:
+ return IO_COL_READS;
+ case IOOP_REUSE:
+ return IO_COL_REUSES;
+ case IOOP_REJECT:
+ return IO_COL_REJECTIONS;
+ case IOOP_REPOSSESS:
+ return IO_COL_REPOSSESSIONS;
+ case IOOP_WRITE:
+ return IO_COL_WRITES;
+ case IOOP_EXTEND:
+ return IO_COL_EXTENDS;
+ case IOOP_FSYNC:
+ return IO_COL_FSYNCS;
+ }
+
+ elog(ERROR, "unrecognized IOOp value: %d", io_op);
+}
+
+Datum
+pg_stat_get_io(PG_FUNCTION_ARGS)
+{
+ PgStat_BackendIOContextOps *backends_io_stats;
+ ReturnSetInfo *rsinfo;
+ Datum reset_time;
+
+ InitMaterializedSRF(fcinfo, 0);
+ rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+
+ backends_io_stats = pgstat_fetch_backend_io_context_ops();
+
+ reset_time = TimestampTzGetDatum(backends_io_stats->stat_reset_timestamp);
+
+ for (int bktype = 0; bktype < BACKEND_NUM_TYPES; bktype++)
+ {
+ Datum bktype_desc = CStringGetTextDatum(GetBackendTypeDesc((BackendType) bktype));
+ bool expect_backend_stats = true;
+ PgStat_IOContextOps *io_context_ops = &backends_io_stats->stats[bktype];
+
+ /*
+ * For those BackendTypes without IO Operation stats, skip
+ * representing them in the view altogether.
+ */
+ expect_backend_stats = pgstat_io_op_stats_collected((BackendType)
+ bktype);
+
+ for (int io_context = 0; io_context < IOCONTEXT_NUM_TYPES; io_context++)
+ {
+ PgStat_IOOpCounters *counters = &io_context_ops->data[io_context];
+ const char *io_context_str = pgstat_io_context_desc(io_context);
+
+ /*
+ * Hard-code this to blocks until we have non-block-oriented IO
+ * represented in the view as well
+ */
+ int unit = GUC_UNIT_BLOCKS;
+ const char *unit_name = get_config_unit_name(unit);
+
+
+ Datum values[IO_NUM_COLUMNS] = {0};
+ bool nulls[IO_NUM_COLUMNS] = {0};
+
+ /*
+ * Given that unit is hard-coded to GUC_UNIT_BLOCKS, unit_name
+ * should not be NULL.
+ */
+ Assert(unit_name);
+
+ /*
+ * Some combinations of IOContext and BackendType are not valid
+ * for any type of IOOp. In such cases, omit the entire row from
+ * the view.
+ */
+ if (!expect_backend_stats ||
+ !pgstat_bktype_io_context_valid((BackendType) bktype,
+ (IOContext) io_context))
+ {
+ pgstat_io_context_ops_assert_zero(counters);
+ continue;
+ }
+
+ values[IO_COL_BACKEND_TYPE] = bktype_desc;
+ values[IO_COL_IO_CONTEXT] = CStringGetTextDatum(io_context_str);
+ values[IO_COL_READS] = Int64GetDatum(counters->reads);
+ values[IO_COL_WRITES] = Int64GetDatum(counters->writes);
+ values[IO_COL_EXTENDS] = Int64GetDatum(counters->extends);
+ values[IO_COL_UNIT] = CStringGetTextDatum(unit_name);
+ values[IO_COL_FREELIST_ACQUISITIONS] = Int64GetDatum(counters->freelist_acquisitions);
+ values[IO_COL_EVICTIONS] = Int64GetDatum(counters->evictions);
+ values[IO_COL_REUSES] = Int64GetDatum(counters->reuses);
+ values[IO_COL_REJECTIONS] = Int64GetDatum(counters->rejections);
+ values[IO_COL_REPOSSESSIONS] = Int64GetDatum(counters->repossessions);
+ values[IO_COL_FSYNCS] = Int64GetDatum(counters->fsyncs);
+ values[IO_COL_RESET_TIME] = TimestampTzGetDatum(reset_time);
+
+ /*
+ * Some combinations of BackendType and IOOp and of IOContext and
+ * IOOp are not valid. Set these cells in the view NULL and assert
+ * that these stats are zero as expected.
+ */
+ for (int io_op = 0; io_op < IOOP_NUM_TYPES; io_op++)
+ {
+ if (!(pgstat_io_op_valid((BackendType) bktype, (IOContext)
+ io_context, (IOOp) io_op)))
+ {
+ pgstat_io_op_assert_zero(counters, (IOOp) io_op);
+ nulls[pgstat_io_op_get_index((IOOp) io_op)] = true;
+ }
+ }
+
+ tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+ }
+ }
+
+ return (Datum) 0;
+}
+
/*
* Returns statistics of WAL activity
*/
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 62a5b8e655..2d07595677 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5653,6 +5653,15 @@
proname => 'pg_stat_get_buf_alloc', provolatile => 's', proparallel => 'r',
prorettype => 'int8', proargtypes => '', prosrc => 'pg_stat_get_buf_alloc' },
+{ oid => '8459', descr => 'statistics: per backend type IO statistics',
+ proname => 'pg_stat_get_io', provolatile => 'v',
+ prorows => '30', proretset => 't',
+ proparallel => 'r', prorettype => 'record', proargtypes => '',
+ proallargtypes => '{text,text,int8,int8,int8,text,int8,int8,int8,int8,int8,int8,timestamptz}',
+ proargmodes => '{o,o,o,o,o,o,o,o,o,o,o,o,o}',
+ proargnames => '{backend_type,io_context,read,written,extended,unit,freelist_acquired,evicted,reused,rejected,repossessed,files_synced,stats_reset}',
+ prosrc => 'pg_stat_get_io' },
+
{ oid => '1136', descr => 'statistics: information about WAL activity',
proname => 'pg_stat_get_wal', proisstrict => 'f', provolatile => 's',
proparallel => 'r', prorettype => 'record', proargtypes => '',
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index bfcd8ac9a0..c8cf3d3fa8 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1871,6 +1871,20 @@ pg_stat_gssapi| SELECT s.pid,
s.gss_enc AS encrypted
FROM pg_stat_get_activity(NULL::integer) s(datid, pid, usesysid, application_name, state, query, wait_event_type, wait_event, xact_start, query_start, backend_start, state_change, client_addr, client_hostname, client_port, backend_xid, backend_xmin, backend_type, ssl, sslversion, sslcipher, sslbits, ssl_client_dn, ssl_client_serial, ssl_issuer_dn, gss_auth, gss_princ, gss_enc, leader_pid, query_id)
WHERE (s.client_port IS NOT NULL);
+pg_stat_io| SELECT b.backend_type,
+ b.io_context,
+ b.read,
+ b.written,
+ b.extended,
+ b.unit,
+ b.freelist_acquired,
+ b.evicted,
+ b.reused,
+ b.rejected,
+ b.repossessed,
+ b.files_synced,
+ b.stats_reset
+ FROM pg_stat_get_io() b(backend_type, io_context, read, written, extended, unit, freelist_acquired, evicted, reused, rejected, repossessed, files_synced, stats_reset);
pg_stat_progress_analyze| SELECT s.pid,
s.datid,
d.datname,
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index 257a6a9da9..8a95002d61 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -1120,4 +1120,246 @@ SELECT pg_stat_get_subscription_stats(NULL);
(1 row)
+-- Test that the following operations are tracked in pg_stat_io:
+-- - acquisitions of shared buffers from the freelist or by evicting another block
+-- - reads of target blocks into shared buffers
+-- - writes of shared buffers
+-- - extends of relations using shared buffers
+-- - fsyncs done to ensure the durability of data dirtying shared buffers
+-- Consider both buffers acquired from the freelist as well as those acquired
+-- by evicting another block because we cannot be sure the state of shared
+-- buffers at the point the test is run.
+SELECT sum(evicted) + sum(freelist_acquired) AS io_sum_shared_acq_before FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT sum(read) AS io_sum_shared_reads_before FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT sum(written) AS io_sum_shared_writes_before FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT sum(extended) AS io_sum_shared_extends_before FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT sum(files_synced) AS io_sum_shared_fsyncs_before FROM pg_stat_io WHERE io_context = 'shared' \gset
+-- Create a regular table and insert some data to generate IOCONTEXT_SHARED
+-- evicted or freelist acquisitions and extends.
+CREATE TABLE test_io_shared(a int);
+INSERT INTO test_io_shared SELECT i FROM generate_series(1,100)i;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+-- After a checkpoint, there should be some additional IOCONTEXT_SHARED writes
+-- and fsyncs.
+-- The second checkpoint ensures that stats from the first checkpoint have been
+-- reported and protects against any potential races amongst the table
+-- creation, a possible timing-triggered checkpoint, and the explicit
+-- checkpoint in the test.
+CHECKPOINT;
+CHECKPOINT;
+SELECT sum(evicted) + sum(freelist_acquired) AS io_sum_shared_acq_after FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT sum(written) AS io_sum_shared_writes_after FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT sum(extended) AS io_sum_shared_extends_after FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT sum(files_synced) AS io_sum_shared_fsyncs_after FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT :io_sum_shared_acq_after > :io_sum_shared_acq_before;
+ ?column?
+----------
+ t
+(1 row)
+
+SELECT :io_sum_shared_writes_after > :io_sum_shared_writes_before;
+ ?column?
+----------
+ t
+(1 row)
+
+SELECT :io_sum_shared_extends_after > :io_sum_shared_extends_before;
+ ?column?
+----------
+ t
+(1 row)
+
+SELECT current_setting('fsync') = 'off' OR :io_sum_shared_fsyncs_after > :io_sum_shared_fsyncs_before;
+ ?column?
+----------
+ t
+(1 row)
+
+-- Change the tablespace so that the table is rewritten directly, then SELECT
+-- from it to cause it to be read back into shared buffers.
+SET allow_in_place_tablespaces = true;
+CREATE TABLESPACE test_io_shared_stats_tblspc LOCATION '';
+ALTER TABLE test_io_shared SET TABLESPACE test_io_shared_stats_tblspc;
+SELECT COUNT(*) FROM test_io_shared;
+ count
+-------
+ 100
+(1 row)
+
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+SELECT sum(read) AS io_sum_shared_reads_after FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT :io_sum_shared_reads_after > :io_sum_shared_reads_before;
+ ?column?
+----------
+ t
+(1 row)
+
+DROP TABLE test_io_shared;
+DROP TABLESPACE test_io_shared_stats_tblspc;
+-- Test that the follow IOCONTEXT_LOCAL IOOps are tracked in pg_stat_io:
+-- - initial allocation and subsequent usage of local buffers
+-- - reads of temporary table blocks into local buffers
+-- - writes of local buffers
+-- - extends of temporary tables
+-- Set temp_buffers to a low value so that we can trigger writes with fewer
+-- inserted tuples. Do so in a new session in case temporary tables have been
+-- accessed by previous tests in this session.
+\c
+SET temp_buffers TO '1MB';
+CREATE TEMPORARY TABLE test_io_local(a int, b TEXT);
+SELECT sum(evicted) + sum(freelist_acquired) AS io_sum_local_acq_before FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT sum(read) AS io_sum_local_reads_before FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT sum(written) AS io_sum_local_writes_before FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT sum(extended) AS io_sum_local_extends_before FROM pg_stat_io WHERE io_context = 'local' \gset
+-- Insert enough values that we need to reuse and write out dirty local
+-- buffers.
+INSERT INTO test_io_local SELECT generate_series(1, 8000) as id, repeat('a', 100);
+-- Read in evicted buffers.
+SELECT COUNT(*) FROM test_io_local;
+ count
+-------
+ 8000
+(1 row)
+
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+SELECT sum(evicted) + sum(freelist_acquired) AS io_sum_local_acq_after FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT sum(read) AS io_sum_local_reads_after FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT sum(written) AS io_sum_local_writes_after FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT sum(extended) AS io_sum_local_extends_after FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT :io_sum_local_acq_after > :io_sum_local_acq_before;
+ ?column?
+----------
+ t
+(1 row)
+
+SELECT :io_sum_local_reads_after > :io_sum_local_reads_before;
+ ?column?
+----------
+ t
+(1 row)
+
+SELECT :io_sum_local_writes_after > :io_sum_local_writes_before;
+ ?column?
+----------
+ t
+(1 row)
+
+SELECT :io_sum_local_extends_after > :io_sum_local_extends_before;
+ ?column?
+----------
+ t
+(1 row)
+
+RESET temp_buffers;
+-- Test that reuse of strategy buffers and reads of blocks into these reused
+-- buffers while VACUUMing are tracked in pg_stat_io.
+-- Set wal_skip_threshold smaller than the expected size of
+-- test_io_vac_strategy so that, even if wal_level is minimal, VACUUM FULL will
+-- fsync the newly rewritten test_io_vac_strategy instead of writing it to WAL.
+-- Writing it to WAL will result in the newly written relation pages being in
+-- shared buffers -- preventing us from testing BAS_VACUUM BufferAccessStrategy
+-- evictions/freelist acqusitions and reads.
+SET wal_skip_threshold = '1 kB';
+SELECT sum(evicted) + sum(freelist_acquired) AS io_sum_vac_strategy_acq_before FROM pg_stat_io WHERE io_context = 'vacuum' \gset
+SELECT sum(reused) AS io_sum_vac_strategy_reuses_before FROM pg_stat_io WHERE io_context = 'vacuum' \gset
+SELECT sum(read) AS io_sum_vac_strategy_reads_before FROM pg_stat_io WHERE io_context = 'vacuum' \gset
+CREATE TABLE test_io_vac_strategy(a int, b int) WITH (autovacuum_enabled = 'false');
+INSERT INTO test_io_vac_strategy SELECT i, i from generate_series(1, 8000)i;
+-- Ensure that the next VACUUM will need to perform IO by rewriting the table
+-- first with VACUUM (FULL).
+VACUUM (FULL) test_io_vac_strategy;
+VACUUM (PARALLEL 0) test_io_vac_strategy;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+SELECT sum(evicted) + sum(freelist_acquired) AS io_sum_vac_strategy_acq_after FROM pg_stat_io WHERE io_context = 'vacuum' \gset
+SELECT sum(reused) AS io_sum_vac_strategy_reuses_after FROM pg_stat_io WHERE io_context = 'vacuum' \gset
+SELECT sum(read) AS io_sum_vac_strategy_reads_after FROM pg_stat_io WHERE io_context = 'vacuum' \gset
+SELECT :io_sum_vac_strategy_acq_after > :io_sum_vac_strategy_acq_before;
+ ?column?
+----------
+ t
+(1 row)
+
+SELECT :io_sum_vac_strategy_reads_after > :io_sum_vac_strategy_reads_before;
+ ?column?
+----------
+ t
+(1 row)
+
+SELECT :io_sum_vac_strategy_reuses_after > :io_sum_vac_strategy_reuses_before;
+ ?column?
+----------
+ t
+(1 row)
+
+RESET wal_skip_threshold;
+-- Test that extends done by a CTAS, which uses a BAS_BULKWRITE
+-- BufferAccessStrategy, are tracked in pg_stat_io.
+SELECT sum(extended) AS io_sum_bulkwrite_strategy_extends_before FROM pg_stat_io WHERE io_context = 'bulkwrite' \gset
+CREATE TABLE test_io_bulkwrite_strategy AS SELECT i FROM generate_series(1,100)i;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+SELECT sum(extended) AS io_sum_bulkwrite_strategy_extends_after FROM pg_stat_io WHERE io_context = 'bulkwrite' \gset
+SELECT :io_sum_bulkwrite_strategy_extends_after > :io_sum_bulkwrite_strategy_extends_before;
+ ?column?
+----------
+ t
+(1 row)
+
+-- Test that reads of blocks into reused strategy buffers during database
+-- creation, which uses a BAS_BULKREAD BufferAccessStrategy, are tracked in
+-- pg_stat_io.
+SELECT sum(read) AS io_sum_bulkread_strategy_reads_before FROM pg_stat_io WHERE io_context = 'bulkread' \gset
+CREATE DATABASE test_io_bulkread_strategy_db;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush
+--------------------------
+
+(1 row)
+
+SELECT sum(read) AS io_sum_bulkread_strategy_reads_after FROM pg_stat_io WHERE io_context = 'bulkread' \gset
+SELECT :io_sum_bulkread_strategy_reads_after > :io_sum_bulkread_strategy_reads_before;
+ ?column?
+----------
+ t
+(1 row)
+
+-- Test IO stats reset
+SELECT sum(evicted) + sum(freelist_acquired) + sum(reused) + sum(extended) + sum(files_synced) + sum(read) + sum(written) AS io_stats_pre_reset FROM pg_stat_io \gset
+SELECT pg_stat_reset_shared('io');
+ pg_stat_reset_shared
+----------------------
+
+(1 row)
+
+SELECT sum(evicted) + sum(freelist_acquired) + sum(reused) + sum(extended) + sum(files_synced) + sum(read) + sum(written) AS io_stats_post_reset FROM pg_stat_io \gset
+SELECT :io_stats_post_reset < :io_stats_pre_reset;
+ ?column?
+----------
+ t
+(1 row)
+
-- End of Stats Test
diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql
index f6270f7bad..2b8ca91088 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -535,4 +535,135 @@ SELECT pg_stat_get_replication_slot(NULL);
SELECT pg_stat_get_subscription_stats(NULL);
+-- Test that the following operations are tracked in pg_stat_io:
+-- - acquisitions of shared buffers from the freelist or by evicting another block
+-- - reads of target blocks into shared buffers
+-- - writes of shared buffers
+-- - extends of relations using shared buffers
+-- - fsyncs done to ensure the durability of data dirtying shared buffers
+
+-- Consider both buffers acquired from the freelist as well as those acquired
+-- by evicting another block because we cannot be sure the state of shared
+-- buffers at the point the test is run.
+SELECT sum(evicted) + sum(freelist_acquired) AS io_sum_shared_acq_before FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT sum(read) AS io_sum_shared_reads_before FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT sum(written) AS io_sum_shared_writes_before FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT sum(extended) AS io_sum_shared_extends_before FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT sum(files_synced) AS io_sum_shared_fsyncs_before FROM pg_stat_io WHERE io_context = 'shared' \gset
+-- Create a regular table and insert some data to generate IOCONTEXT_SHARED
+-- evicted or freelist acquisitions and extends.
+CREATE TABLE test_io_shared(a int);
+INSERT INTO test_io_shared SELECT i FROM generate_series(1,100)i;
+SELECT pg_stat_force_next_flush();
+-- After a checkpoint, there should be some additional IOCONTEXT_SHARED writes
+-- and fsyncs.
+-- The second checkpoint ensures that stats from the first checkpoint have been
+-- reported and protects against any potential races amongst the table
+-- creation, a possible timing-triggered checkpoint, and the explicit
+-- checkpoint in the test.
+CHECKPOINT;
+CHECKPOINT;
+SELECT sum(evicted) + sum(freelist_acquired) AS io_sum_shared_acq_after FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT sum(written) AS io_sum_shared_writes_after FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT sum(extended) AS io_sum_shared_extends_after FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT sum(files_synced) AS io_sum_shared_fsyncs_after FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT :io_sum_shared_acq_after > :io_sum_shared_acq_before;
+SELECT :io_sum_shared_writes_after > :io_sum_shared_writes_before;
+SELECT :io_sum_shared_extends_after > :io_sum_shared_extends_before;
+SELECT current_setting('fsync') = 'off' OR :io_sum_shared_fsyncs_after > :io_sum_shared_fsyncs_before;
+-- Change the tablespace so that the table is rewritten directly, then SELECT
+-- from it to cause it to be read back into shared buffers.
+SET allow_in_place_tablespaces = true;
+CREATE TABLESPACE test_io_shared_stats_tblspc LOCATION '';
+ALTER TABLE test_io_shared SET TABLESPACE test_io_shared_stats_tblspc;
+SELECT COUNT(*) FROM test_io_shared;
+SELECT pg_stat_force_next_flush();
+SELECT sum(read) AS io_sum_shared_reads_after FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT :io_sum_shared_reads_after > :io_sum_shared_reads_before;
+DROP TABLE test_io_shared;
+DROP TABLESPACE test_io_shared_stats_tblspc;
+
+-- Test that the follow IOCONTEXT_LOCAL IOOps are tracked in pg_stat_io:
+-- - initial allocation and subsequent usage of local buffers
+-- - reads of temporary table blocks into local buffers
+-- - writes of local buffers
+-- - extends of temporary tables
+
+-- Set temp_buffers to a low value so that we can trigger writes with fewer
+-- inserted tuples. Do so in a new session in case temporary tables have been
+-- accessed by previous tests in this session.
+\c
+SET temp_buffers TO '1MB';
+CREATE TEMPORARY TABLE test_io_local(a int, b TEXT);
+SELECT sum(evicted) + sum(freelist_acquired) AS io_sum_local_acq_before FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT sum(read) AS io_sum_local_reads_before FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT sum(written) AS io_sum_local_writes_before FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT sum(extended) AS io_sum_local_extends_before FROM pg_stat_io WHERE io_context = 'local' \gset
+-- Insert enough values that we need to reuse and write out dirty local
+-- buffers.
+INSERT INTO test_io_local SELECT generate_series(1, 8000) as id, repeat('a', 100);
+-- Read in evicted buffers.
+SELECT COUNT(*) FROM test_io_local;
+SELECT pg_stat_force_next_flush();
+SELECT sum(evicted) + sum(freelist_acquired) AS io_sum_local_acq_after FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT sum(read) AS io_sum_local_reads_after FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT sum(written) AS io_sum_local_writes_after FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT sum(extended) AS io_sum_local_extends_after FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT :io_sum_local_acq_after > :io_sum_local_acq_before;
+SELECT :io_sum_local_reads_after > :io_sum_local_reads_before;
+SELECT :io_sum_local_writes_after > :io_sum_local_writes_before;
+SELECT :io_sum_local_extends_after > :io_sum_local_extends_before;
+RESET temp_buffers;
+
+-- Test that reuse of strategy buffers and reads of blocks into these reused
+-- buffers while VACUUMing are tracked in pg_stat_io.
+
+-- Set wal_skip_threshold smaller than the expected size of
+-- test_io_vac_strategy so that, even if wal_level is minimal, VACUUM FULL will
+-- fsync the newly rewritten test_io_vac_strategy instead of writing it to WAL.
+-- Writing it to WAL will result in the newly written relation pages being in
+-- shared buffers -- preventing us from testing BAS_VACUUM BufferAccessStrategy
+-- evictions/freelist acqusitions and reads.
+SET wal_skip_threshold = '1 kB';
+SELECT sum(evicted) + sum(freelist_acquired) AS io_sum_vac_strategy_acq_before FROM pg_stat_io WHERE io_context = 'vacuum' \gset
+SELECT sum(reused) AS io_sum_vac_strategy_reuses_before FROM pg_stat_io WHERE io_context = 'vacuum' \gset
+SELECT sum(read) AS io_sum_vac_strategy_reads_before FROM pg_stat_io WHERE io_context = 'vacuum' \gset
+CREATE TABLE test_io_vac_strategy(a int, b int) WITH (autovacuum_enabled = 'false');
+INSERT INTO test_io_vac_strategy SELECT i, i from generate_series(1, 8000)i;
+-- Ensure that the next VACUUM will need to perform IO by rewriting the table
+-- first with VACUUM (FULL).
+VACUUM (FULL) test_io_vac_strategy;
+VACUUM (PARALLEL 0) test_io_vac_strategy;
+SELECT pg_stat_force_next_flush();
+SELECT sum(evicted) + sum(freelist_acquired) AS io_sum_vac_strategy_acq_after FROM pg_stat_io WHERE io_context = 'vacuum' \gset
+SELECT sum(reused) AS io_sum_vac_strategy_reuses_after FROM pg_stat_io WHERE io_context = 'vacuum' \gset
+SELECT sum(read) AS io_sum_vac_strategy_reads_after FROM pg_stat_io WHERE io_context = 'vacuum' \gset
+SELECT :io_sum_vac_strategy_acq_after > :io_sum_vac_strategy_acq_before;
+SELECT :io_sum_vac_strategy_reads_after > :io_sum_vac_strategy_reads_before;
+SELECT :io_sum_vac_strategy_reuses_after > :io_sum_vac_strategy_reuses_before;
+RESET wal_skip_threshold;
+
+-- Test that extends done by a CTAS, which uses a BAS_BULKWRITE
+-- BufferAccessStrategy, are tracked in pg_stat_io.
+SELECT sum(extended) AS io_sum_bulkwrite_strategy_extends_before FROM pg_stat_io WHERE io_context = 'bulkwrite' \gset
+CREATE TABLE test_io_bulkwrite_strategy AS SELECT i FROM generate_series(1,100)i;
+SELECT pg_stat_force_next_flush();
+SELECT sum(extended) AS io_sum_bulkwrite_strategy_extends_after FROM pg_stat_io WHERE io_context = 'bulkwrite' \gset
+SELECT :io_sum_bulkwrite_strategy_extends_after > :io_sum_bulkwrite_strategy_extends_before;
+
+-- Test that reads of blocks into reused strategy buffers during database
+-- creation, which uses a BAS_BULKREAD BufferAccessStrategy, are tracked in
+-- pg_stat_io.
+SELECT sum(read) AS io_sum_bulkread_strategy_reads_before FROM pg_stat_io WHERE io_context = 'bulkread' \gset
+CREATE DATABASE test_io_bulkread_strategy_db;
+SELECT pg_stat_force_next_flush();
+SELECT sum(read) AS io_sum_bulkread_strategy_reads_after FROM pg_stat_io WHERE io_context = 'bulkread' \gset
+SELECT :io_sum_bulkread_strategy_reads_after > :io_sum_bulkread_strategy_reads_before;
+
+-- Test IO stats reset
+SELECT sum(evicted) + sum(freelist_acquired) + sum(reused) + sum(extended) + sum(files_synced) + sum(read) + sum(written) AS io_stats_pre_reset FROM pg_stat_io \gset
+SELECT pg_stat_reset_shared('io');
+SELECT sum(evicted) + sum(freelist_acquired) + sum(reused) + sum(extended) + sum(files_synced) + sum(read) + sum(written) AS io_stats_post_reset FROM pg_stat_io \gset
+SELECT :io_stats_post_reset < :io_stats_pre_reset;
+
-- End of Stats Test
--
2.34.1