Subject: pg_stat_bgwriter.buffers_backend is pretty meaningless (and more?)

From: Andres Freund <[email-masked]>
Date: Fri, 24 Jan 2020 11:52:26 -0800

Hi,

Currently pg_stat_bgwriter.buffers_backend is pretty useless to gauge
whether backends are doing writes they shouldn't do. That's because it
counts things that are either unavoidably or unlikely doable by other
parts of the system (checkpointer, bgwriter).

In particular extending the file can not currently be done by any
another type of process, yet is counted. When using a buffer access
strategy it is also very likely that writes have to be done by the
'dirtying' backend itself, as the buffer will be reused soon after (when
not previously in s_b that is).

Additionally pg_stat_bgwriter.buffers_backend also counts writes done by
autovacuum et al.

I think it'd make sense to at least split buffers_backend into
buffers_backend_extend,
buffers_backend_write,
buffers_backend_write_strat

but it could also be worthwhile to expand it into
buffers_backend_extend,
buffers_{backend,checkpoint,bgwriter,autovacuum}_write
buffers_{backend,autovacuum}_write_stat

Possibly by internally, in contrast to SQL level, having just counter
arrays indexed by backend types.

It's also noteworthy that buffers_backend is accounted in an absurd
manner. One might think that writes are accounted from backend -> shared
memory or such. But instead it works like this:

1) backend flushes buffer in bufmgr.c, accounts for backend *write time*
2) mdwrite writes and  registers a sync request, which forwards the sync request to checkpointer
3) ForwardSyncRequest(), when not called by bgwriter, increments CheckpointerShmem->num_backend_writes
4) checkpointer, whenever doing AbsorbSyncRequests(), moves
   CheckpointerShmem->num_backend_writes to
   BgWriterStats.m_buf_written_backend (local memory!)
5) Occasionally it calls pgstat_send_bgwriter(), which sends the data to
   pgstat (which bgwriter also does)
6) Which then updates the shared memory used by the display functions

Worthwhile to note that backend buffer read/write *time* is accounted
differently. That's done via pgstat_send_tabstat().

I think there's very little excuse for the indirection via checkpointer,
besides architectually being weird, it actually requires that we
continue to wake up checkpointer over and over instead of optimizing how
and when we submit fsync requests.

As far as I can tell we're also simply not accounting at all for writes
done outside of shared buffers. All writes done directly through
smgrwrite()/extend() aren't accounted anywhere as far as I can tell.

I think we also count things as writes that aren't writes: mdtruncate()
is AFAICT counted as one backend write for each segment. Which seems
weird to me.

Lastly, I don't understand what the point of sending fixed size stats,
like the stuff underlying pg_stat_bgwriter, through pgstats IPC. While
I don't like it's architecture, we obviously need something like pgstat
to handle variable amounts of stats (database, table level etc
stats). But that doesn't at all apply to these types of global stats.

Greetings,

Andres Freund

---

From: Magnus Hagander <[email-masked]>
Date: Sat, 25 Jan 2020 15:43:41 +0100

> not previously in s_b that is).

Yeah. That's quite annoying.

> buffers_{backend,autovacuum}_write_stat

Given that these are individual global counters, I don't really see
any reason not to expand it to the bigger set of counters. It's easy
enough to add them up together later if needed.

> is AFAICT counted as one backend write for each segment. Which seems

It's at least slightly weird :) Might it be worth counting truncate
events separately?

> stats). But that doesn't at all apply to these types of global stats.

That part has annoyed me as well a few times. +1 for just moving that
into a global shared memory. Given that we don't really care about
things being in sync between those different counters *or* if we loose
a bit of data (which the stats collector is designed to do), we could
even do that without a lock?

-- 
 Magnus Hagander
 Me: https://www.hagander.net/
 Work: https://www.redpill-linpro.com/

---

From: Andres Freund <[email-masked]>
Date: Sat, 25 Jan 2020 16:44:01 -0800

Hi,

> enough to add them up together later if needed.

Are you agreeing to
buffers_{backend,checkpoint,bgwriter,autovacuum}_write
or are you suggesting further ones?

> It's at least slightly weird :) Might it be worth counting truncate

Is that really something interesting? Feels like it'd have to be done at
a higher level to be useful. E.g. the truncate done by TRUNCATE (when in
same xact as creation) and VACUUM are quite different. I think it'd be
better to just not include it.

> even do that without a lock?

I don't think we'd quite want to do it without any (single counter)
synchronization - high concurrency setups would be pretty likely to
loose values that way. I suspect the best would be to have a struct in
shared memory that contains the potential counters for each potential
process. And then sum them up when actually wanting the concrete
value. That way we avoid unnecessary contention, in contrast to having a
single shared memory value for each(which would just pingpong between
different sockets and store buffers).  There's a few details like how
exactly to implement resetting the counters, but ...

Thanks,

Andres Freund

---

From: Magnus Hagander <[email-masked]>
Date: Sun, 26 Jan 2020 16:20:03 +0100

> or are you suggesting further ones?

The former.

> better to just not include it.

Yeah, you're probably right. it certainly makes very little sense
where it is now.

> exactly to implement resetting the counters, but ...

Right. Each process gets to do their own write, but still in shared
memory. But do you need to lock them when reading them (for the
summary)? That's the part where I figured you could just read and
summarize them, and accept the possible loss.

---

From: Andres Freund <[email-masked]>
Date: Sun, 26 Jan 2020 12:22:03 -0800

Hi,

> summarize them, and accept the possible loss.

Oh, yea, I'd not lock for that. On nearly all machines aligned 64bit
integers can be read / written without a danger of torn values, and I
don't think we need perfect cross counter accuracy. To deal with the few
platforms without 64bit "single copy atomicity", we can just use
pg_atomic_read/write_u64. These days (e8fdbd58fe) they automatically
fall back to using locked operations for those platforms.  So I don't
think there's actually a danger of loss.

Obviously we could also use atomic ops to increment the value, but I'd
rather not add all those atomic operations, even if it's on uncontended
cachelines. It'd allow us to reset the backend values more easily by
just swapping in a 0, which we can't do if the backend increments
non-atomically. But I think we could instead just have one global "bias"
value to implement resets (by subtracting that from the summarized
value, and storing the current sum when resetting). Or use the new
global barrier to trigger a reset. Or something similar.

Greetings,

Andres Freund

---

From: Kyotaro Horiguchi <[email-masked]>
Date: Mon, 27 Jan 2020 13:20:09 +0900 (JST)

Hello.

At Sun, 26 Jan 2020 12:22:03 -0800, Andres Freund <[email-masked]> wrote in 
> Hi,

I feel the same on the specific issues brought in upthread.

> global barrier to trigger a reset. Or something similar.

Fixed or global stats are suitable for the startar of shared-memory
stats collector. In the case of buffers_*_write, the global stats
entry for each process needs just 8 bytes plus matbe extra 8 bytes for
the bias value.  I'm not sure how many counters like this there are,
but is such size of footprint acceptatble?  (Each backend already uses
the same amount of local memory for pgstat use, though.)

Anyway I will do something like that as a trial, maybe by adding a
member in PgBackendStatus and one global-shared for the bial value.

      int64       st_progress_param[PGSTAT_NUM_PROGRESS_PARAM];
+     PgBackendStatsCounters counters;
  } PgBackendStatus;

regards.

-- 
Kyotaro Horiguchi
NTT Open Source Software Center

---

From: Melanie Plageman <[email-masked]>
Date: Mon, 12 Apr 2021 19:49:36 -0700

> +     PgBackendStatsCounters counters;

So, I took a stab at implementing this in PgBackendStatus. The attached
patch is not quite on top of current master, so, alas, don't try and
apply it. I went to rebase today and realized I needed to make some
changes in light of e1025044cd4, however, I wanted to share this WIP so
that I could pose a few questions that I imagine will still be relevant
after I rewrite the patch.

I removed buffers_backend and buffers_backend_fsync from
pg_stat_bgwriter and have created a new view which tracks
  - number of shared buffers the checkpointer and bgwriter write out
  - number of shared buffers a regular backend is forced to flush
  - number of extends done by a regular backend through shared buffers
  - number of buffers flushed by a backend or autovacuum using a
    BufferAccessStrategy which, were they not to use this strategy,
    could perhaps have been avoided if a clean shared buffer was
    available
  - number of fsyncs done by a backend which could have been done by
    checkpointer if sync queue had not been full

This view currently does only track writes and extends that go through
shared buffers and fsyncs of shared buffers (which, AFAIK are the only
things fsync'd though the SyncRequest machinery currently).

BufferAlloc() and SyncOneBuffer() are the main points at which the
tracking is done. I can definitely expand this, but, I want to make sure
that we are tracking the right kind of information.

num_backend_writes and num_backend_fsync were intended (though they were
not accurate) to count buffers that backends had to end up writing
themselves and fsyncs that backends had to end up doing themselves which
could have been avoided with a different configuration (or, I suppose, a
different workload/different data, etc). That is, they were meant to
tell you if checkpointer and bgwriter were keeping up and/or if the
size of shared buffers was adequate.

In implementing this counting per backend, it is easy for all types of
backends to keep track of the number of writes, extends, fsyncs, and
strategy writes they are doing. So, as recommended upthread, I have
added columns in the view for the number of writes for checkpointer and
bgwriter and others. Thus, this view becomes more than just stats on
"avoidable I/O done by backends".

So, my question is, does it makes sense to track all extends -- those to
extend the fsm and visimap and when making a new relation or index? Is
that information useful? If so, is it different than the extends done
through shared buffers? Should it be tracked separately?

Also, if we care about all of the extends, then it seems a bit annoying
to pepper the counting all over the place when it really just needs to
be done when smgrextend() — even though maybe a stats function doesn't
belong in that API.

Another question I have is, should the number of extends be for every
single block extended or should we try to track the initiation of a set
of extends (all of those added in RelationAddExtraBlocks(), in this
case)?

When it comes to fsync counting, I only count the fsyncs counted by the
previous code — that is fsyncs done by backends themselves when the
checkpointer sync request queue was full.
I did the counting in the same place in checkpointer code -- in
ForwardSyncRequest() -- partially because there did not seem to be
another good place to do it since register_dirty_segment() returns void
(thought about having it return a bool to indicate if it fsync'd it or
if it registered the fsync because that seemed alright, but mdextend(),
mdwrite() etc, also return NULL) so there is no way to propagate the
information back up to the bufmgr that the process had to do its own
fsync, so, that means that I would have to muck with the md.c API. and,
since the checkpointer is the one processing these sync requests anyway,
it actually seems okay to do it in the checkpointer code.

I'm not counting fsyncs that are "unavoidable" in the sense that they
couldn't be avoided by changing settings/workload etc -- like those done
when building an index, creating a table/rewriting a table/copying a
table -- is it useful to count these? It seems like it makes the number
of "avoidable fsyncs by backends" less useful if we count the others.
Also, should we count how many fsyncs checkpointer has done (have to
check if there is already a stat for that)? Is that useful in this
context?

Of course, this view, when grown, will begin to overlap with pg_statio,
which is another consideration. What is its identity? I would find
"avoidable I/O" either avoidable entirely or avoidable for that
particular type of process, to be useful.

Or maybe, it should have a more expansive mandate. Maybe it would be
useful to aggregate some of the info from pg_stat_statements at a higher
level -- like maybe shared_blks_read counted across many statements for
a period of time/context in which we expected the relation in shared
buffers becomes potentially interesting.

As for the way I have recorded strategy writes -- it is quite inelegant,
but, I wanted to make sure that I only counted a strategy write as one
in which the backend wrote out the dirty buffer from its strategy ring
but did not check if there was any clean buffer in shared buffers more
generally (so, it is *potentially* an avoidable write). I'm not sure if
this distinction is useful to anyone. I haven't done enough with
BufferAccessStrategies to know what I'd want to know about them when
developing or using Postgres. However, if I don't need to be so careful,
it will make the code much simpler (though, I'm sure I can improve the
code regardless).

As for the implementation of the counters themselves, I appreciate that
it isn't very nice to have a bunch of random members in PgBackendStatus
to count all of these write, extends, fsyncs. I considered if I could
add params that were used for all command types to st_progress_param but
I haven't looked into it yet. Alternatively, I could create an array
just for these kind of stats in PgBackendStatus. Though, I imagine that
I should take a look at the changes that have been made recently to this
area and at the shared memory stats patch.

Oh, also, there should be a way to reset the stats, especially if we add
more extends and fsyncs that happen at the time of relation/index
creation. I, at least, would find it useful to see these numbers once
the database is at some kind of steady state.

Oh and src/test/regress/sql/stats.sql will fail and, of course, I don't
intend to add that SELECT from the view to regress, it was just for
testing purposes to make sure the view was working.

-- Melanie

---

From: Andres Freund <[email-masked]>
Date: Thu, 15 Apr 2021 16:59:54 -0700

Hi,

On 2021-04-12 19:49:36 -0700, Melanie Plageman wrote:
> So, I took a stab at implementing this in PgBackendStatus.

Cool!

> The attached patch is not quite on top of current master, so, alas,
> don't try and apply it. I went to rebase today and realized I needed
> to make some changes in light of e1025044cd4, however, I wanted to
> share this WIP so that I could pose a few questions that I imagine
> will still be relevant after I rewrite the patch.
>

I wonder if leaving buffers_alloc in pg_stat_bgwriter makes sense after
this? I'm tempted to move that to pg_stat_buffers or such...

I'm not quite convinced by having separate columns for checkpointer,
bgwriter, etc. That doesn't seem to scale all that well. What if we
instead made it a view that has one row for each BackendType?

> through shared buffers? Should it be tracked separately?

I don't fully understand what you mean with "extends done through shared
buffers"?

> of extends (all of those added in RelationAddExtraBlocks(), in this

I think it should be 8k blocks, i.e. RelationAddExtraBlocks() should be
tracked as many individual extends. It's implemented that way, but more
importantly, it should be in BLCKSZ units. If we later add some actually
batched operations, we can have separate stats for that.

> particular type of process, to be useful.

I think it's fine to overlap with pg_statio_* - those are for individual
objects, so it seems to be expected to overlap with coarser stats.

> buffers becomes potentially interesting.

Let's do something more basic first...

Greetings,

Andres Freund

---

From: Melanie Plageman <[email-masked]>
Date: Fri, 4 Jun 2021 17:12:43 -0400

> > So, I took a stab at implementing this in PgBackendStatus.

Just a note on v2 of the patch -- the diff for the changes I made to
pgstatfuncs.c is pretty atrocious and hard to read. I tried using a
different diff algorithm, to no avail.

> > will still be relevant after I rewrite the patch.

Regarding the refactor done in e1025044cd4:
Most of the functions I've added access variables in PgBackendStatus, so
I put most of them in backend_status.h/c. However, technically, these
are stats which are aggregated over time, which e1025044cd4 says should
go in pgstat.c/h. I could move some of it, but I hadn't tried to do so,
as it made a few things inconvenient, and, I wasn't sure if it was the
right thing to do anyway.

> this? I'm tempted to move that to pg_stat_buffers or such...
I've gone ahead and moved buffers_alloc out of pg_stat_bgwriter and into
pg_stat_buffer_actions (I've renamed it from pg_stat_buffers_written).

> instead made it a view that has one row for each BackendType?
I've changed the view to have one row for each backend type for which we
would like to report stats and one column for each buffer action type.

To make the code easier to write, I record buffer actions for all
backend types -- even if we don't have any buffer actions we care about
for that backend type. I thought it was okay because when I actually
aggregate the counters across backends, I only do so for the backend
types we care about -- thus there shouldn't be much accessing of shared
memory by multiple different processes.

Also, I copy-pasted most of the code in pg_stat_get_buffer_actions() to
set up the result tuplestore from pg_stat_get_activity() without totally
understanding all the parts of it, so I'm not sure if all of it is
required here.

> I don't fully understand what you mean with "extends done through shared
By "extends done through shared buffers", I just mean when an extend of
a relation is done and the data that will be written to the new block is
written into a shared buffer (as opposed to a local one or local memory
or a strategy buffer).

Random note:
I added a length member to the BackendType enum (BACKEND_NUM_TYPES),
which led to this compiler warning:

    miscinit.c: In function ‘GetBackendTypeDesc’:
    miscinit.c:236:2: warning: enumeration value ‘BACKEND_NUM_TYPES’ not
handled in switch [-Wswitch]
      236 |  switch (backendType)
          |  ^~~~~~

I tried using pg_attribute_unused() for BACKEND_NUM_TYPES, but, it
didn't seem to have the desired effect. As such, I just threw a case
into GetBackendTypeDesc() which does nothing (as opposed to erroring
out), since the backendDesc already is initialized to "unknown process
type", erroring out doesn't seem to be expected.

- Melanie
From 8bfacd947641554c6f1e8a24df4950e915c2c264 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Fri, 4 Jun 2021 17:07:48 -0400
Subject: [PATCH v2] Add system view tracking shared buffer actions

Add a system view which tracks
- number of shared buffers the checkpointer and bgwriter write out
- number of shared buffers a regular backend is forced to flush
- number of extends done by a regular backend through shared buffers
- number of buffers flushed by a backend or autovacuum using a
  BufferAccessStrategy which, were they not to use this strategy, could
  perhaps have been avoided if a clean shared buffer was available
- number of fsyncs done by a backend which could have been done by
  checkpointer if sync queue had not been full
- number of buffers allocated by a regular backend or autovacuum worker
  for either a new block or an existing block of a relation which is not
  currently in a buffer

All of these stats which were in the system view pg_stat_bgwriter have
been removed from that view.

All backends, on exit, will update a shared memory array with the
buffers they wrote or extended.

When the view is queried, add all live backend's statuses
to the totals in the shared memory array and return that as the full
total.

Each row of the view is for a particular backend type and each column is
the number of a particular kind of buffer action taken by the various
backends.

TODO:
- Some kind of test?
- Docs change
---
 src/backend/catalog/system_views.sql        |  14 +-
 src/backend/postmaster/checkpointer.c       |  27 +---
 src/backend/postmaster/pgstat.c             |   3 -
 src/backend/storage/buffer/bufmgr.c         |  73 +++++++++-
 src/backend/storage/buffer/freelist.c       |  37 +++--
 src/backend/storage/ipc/ipci.c              |   1 +
 src/backend/utils/activity/backend_status.c | 144 ++++++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c         |  53 +++++--
 src/backend/utils/init/miscinit.c           |   2 +
 src/include/catalog/pg_proc.dat             |  21 ++-
 src/include/miscadmin.h                     |  12 ++
 src/include/pgstat.h                        |   6 -
 src/include/storage/buf_internals.h         |   3 +
 src/include/utils/backend_status.h          |   9 +-
 src/test/regress/expected/rules.out         |  10 +-
 src/test/regress/sql/stats.sql              |   1 +
 16 files changed, 341 insertions(+), 75 deletions(-)

diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 999d984068..e77ed42352 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1067,9 +1067,6 @@ CREATE VIEW pg_stat_bgwriter AS
         pg_stat_get_bgwriter_buf_written_checkpoints() AS buffers_checkpoint,
         pg_stat_get_bgwriter_buf_written_clean() AS buffers_clean,
         pg_stat_get_bgwriter_maxwritten_clean() AS maxwritten_clean,
-        pg_stat_get_buf_written_backend() AS buffers_backend,
-        pg_stat_get_buf_fsync_backend() AS buffers_backend_fsync,
-        pg_stat_get_buf_alloc() AS buffers_alloc,
         pg_stat_get_bgwriter_stat_reset_time() AS stats_reset;
 
 CREATE VIEW pg_stat_wal AS
@@ -1085,6 +1082,17 @@ CREATE VIEW pg_stat_wal AS
         w.stats_reset
     FROM pg_stat_get_wal() w;
 
+CREATE VIEW pg_stat_buffer_actions AS
+SELECT
+       b.backend_type,
+       b.buffers_alloc,
+       b.buffers_extend,
+       b.buffers_fsync,
+       b.buffers_write,
+       b.buffers_write_strat
+FROM pg_stat_get_buffer_actions() b;
+
+
 CREATE VIEW pg_stat_progress_analyze AS
     SELECT
         S.pid AS pid, S.datid AS datid, D.datname AS datname,
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 75a95f3de7..8eecd35965 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -93,17 +93,8 @@
  * requesting backends since the last checkpoint start.  The flags are
  * chosen so that OR'ing is the correct way to combine multiple requests.
  *
- * num_backend_writes is used to count the number of buffer writes performed
- * by user backend processes.  This counter should be wide enough that it
- * can't overflow during a single processing cycle.  num_backend_fsync
- * counts the subset of those writes that also had to do their own fsync,
- * because the checkpointer failed to absorb their request.
- *
  * The requests array holds fsync requests sent by backends and not yet
  * absorbed by the checkpointer.
- *
- * Unlike the checkpoint fields, num_backend_writes, num_backend_fsync, and
- * the requests fields are protected by CheckpointerCommLock.
  *----------
  */
 typedef struct
@@ -127,9 +118,6 @@ typedef struct
 	ConditionVariable start_cv; /* signaled when ckpt_started advances */
 	ConditionVariable done_cv;	/* signaled when ckpt_done advances */
 
-	uint32		num_backend_writes; /* counts user backend buffer writes */
-	uint32		num_backend_fsync;	/* counts user backend fsync calls */
-
 	int			num_requests;	/* current # of requests */
 	int			max_requests;	/* allocated array size */
 	CheckpointerRequest requests[FLEXIBLE_ARRAY_MEMBER];
@@ -1092,10 +1080,6 @@ ForwardSyncRequest(const FileTag *ftag, SyncRequestType type)
 
 	LWLockAcquire(CheckpointerCommLock, LW_EXCLUSIVE);
 
-	/* Count all backend writes regardless of if they fit in the queue */
-	if (!AmBackgroundWriterProcess())
-		CheckpointerShmem->num_backend_writes++;
-
 	/*
 	 * If the checkpointer isn't running or the request queue is full, the
 	 * backend will have to perform its own fsync request.  But before forcing
@@ -1109,8 +1093,10 @@ ForwardSyncRequest(const FileTag *ftag, SyncRequestType type)
 		 * Count the subset of writes where backends have to do their own
 		 * fsync
 		 */
+		/* TODO: should we count fsyncs for all types of procs? */
 		if (!AmBackgroundWriterProcess())
-			CheckpointerShmem->num_backend_fsync++;
+			pgstat_increment_buffer_action(BA_Fsync);
+
 		LWLockRelease(CheckpointerCommLock);
 		return false;
 	}
@@ -1267,13 +1253,6 @@ AbsorbSyncRequests(void)
 
 	LWLockAcquire(CheckpointerCommLock, LW_EXCLUSIVE);
 
-	/* Transfer stats counts into pending pgstats message */
-	BgWriterStats.m_buf_written_backend += CheckpointerShmem->num_backend_writes;
-	BgWriterStats.m_buf_fsync_backend += CheckpointerShmem->num_backend_fsync;
-
-	CheckpointerShmem->num_backend_writes = 0;
-	CheckpointerShmem->num_backend_fsync = 0;
-
 	/*
 	 * We try to avoid holding the lock for a long time by copying the request
 	 * array, and processing the requests after releasing the lock.
diff --git a/src/backend/postmaster/pgstat.c b/src/backend/postmaster/pgstat.c
index b0d07c0e0b..7396202da2 100644
--- a/src/backend/postmaster/pgstat.c
+++ b/src/backend/postmaster/pgstat.c
@@ -5352,9 +5352,6 @@ pgstat_recv_bgwriter(PgStat_MsgBgWriter *msg, int len)
 	globalStats.buf_written_checkpoints += msg->m_buf_written_checkpoints;
 	globalStats.buf_written_clean += msg->m_buf_written_clean;
 	globalStats.maxwritten_clean += msg->m_maxwritten_clean;
-	globalStats.buf_written_backend += msg->m_buf_written_backend;
-	globalStats.buf_fsync_backend += msg->m_buf_fsync_backend;
-	globalStats.buf_alloc += msg->m_buf_alloc;
 }
 
 /* ----------
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 4b296a22c4..4c753c1e02 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -969,6 +969,11 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 
 	if (isExtend)
 	{
+		/*
+		 * Extends counted here are only those that go through shared buffers
+		 */
+		pgstat_increment_buffer_action(BA_Extend);
+
 		/* new buffers are zero-filled */
 		MemSet((char *) bufBlock, 0, BLCKSZ);
 		/* don't set checksum for all-zero page */
@@ -1235,11 +1240,60 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 					if (XLogNeedsFlush(lsn) &&
 						StrategyRejectBuffer(strategy, buf))
 					{
+						/*
+						 * Unset the strat write flag, as we will not be writing
+						 * this particular buffer from our ring out and may end
+						 * up having to find a buffer from main shared buffers,
+						 * which, if it is dirty, we may have to write out, which
+						 * could have been prevented by checkpointing and background
+						 * writing
+						 */
+						StrategyUnChooseBufferFromRing(strategy);
+
 						/* Drop lock/pin and loop around for another buffer */
 						LWLockRelease(BufferDescriptorGetContentLock(buf));
 						UnpinBuffer(buf, true);
 						continue;
 					}
+
+					/*
+					 * TODO: there is certainly a better way to write this
+					 * logic
+					 */
+
+					/*
+					 * The dirty buffer that will be written out was selected
+					 * from the ring and we did not bother checking the
+					 * freelist or doing a clock sweep to look for a clean
+					 * buffer to use, thus, this write will be counted as a
+					 * strategy write -- one that may be unnecessary without a
+					 * strategy
+					 */
+					if (StrategyIsBufferFromRing(strategy))
+					{
+						pgstat_increment_buffer_action(BA_Write_Strat);
+					}
+
+						/*
+						 * If the dirty buffer was one we grabbed from the
+						 * freelist or through a clock sweep, it could have been
+						 * written out by bgwriter or checkpointer, thus, we will
+						 * count it as a regular write
+						 */
+					else
+						pgstat_increment_buffer_action(BA_Write);
+				}
+				else
+				{
+					/*
+					 * If strategy is NULL, we could only be doing a write.
+					 * Extend operations will be counted in smgrextend. That
+					 * is separate I/O than any flushing of dirty buffers. If
+					 * we add more Backend Access Types, perhaps we will need
+					 * additional checks here
+					 */
+					pgstat_increment_buffer_action(BA_Write);
+
 				}
 
 				/* OK, do the I/O */
@@ -2252,9 +2306,6 @@ BgBufferSync(WritebackContext *wb_context)
 	 */
 	strategy_buf_id = StrategySyncStart(&strategy_passes, &recent_alloc);
 
-	/* Report buffer alloc counts to pgstat */
-	BgWriterStats.m_buf_alloc += recent_alloc;
-
 	/*
 	 * If we're not running the LRU scan, just stop after doing the stats
 	 * stuff.  We mark the saved state invalid so that we can recover sanely
@@ -2549,6 +2600,8 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
 	 * Pin it, share-lock it, write it.  (FlushBuffer will do nothing if the
 	 * buffer is clean by the time we've locked it.)
 	 */
+	pgstat_increment_buffer_action(BA_Write);
+
 	PinBuffer_Locked(bufHdr);
 	LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
 
@@ -2901,6 +2954,20 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln)
 	/*
 	 * bufToWrite is either the shared buffer or a copy, as appropriate.
 	 */
+
+	/*
+	 * TODO: consider that if we did not need to distinguish between a buffer
+	 * flushed that was grabbed from the ring buffer and written out as part
+	 * of a strategy which was not from main Shared Buffers (and thus
+	 * preventable by bgwriter or checkpointer), then we could move all calls
+	 * to pgstat_increment_buffer_action() here except for the one for
+	 * extends, which would remain in ReadBuffer_common() before smgrextend()
+	 * (unless we decide to start counting other extends). That includes the
+	 * call to count buffers written by bgwriter and checkpointer which go
+	 * through FlushBuffer() but not BufferAlloc(). That would make it
+	 * simpler. Perhaps instead we can find somewhere else to indicate that
+	 * the buffer is from the ring of buffers to reuse.
+	 */
 	smgrwrite(reln,
 			  buf->tag.forkNum,
 			  buf->tag.blockNum,
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index 6be80476db..523b024992 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -19,6 +19,7 @@
 #include "storage/buf_internals.h"
 #include "storage/bufmgr.h"
 #include "storage/proc.h"
+#include "utils/backend_status.h"
 
 #define INT_ACCESS_ONCE(var)	((int)(*((volatile int *)&(var))))
 
@@ -51,7 +52,6 @@ typedef struct
 	 * overflow during a single bgwriter cycle.
 	 */
 	uint32		completePasses; /* Complete cycles of the clock sweep */
-	pg_atomic_uint32 numBufferAllocs;	/* Buffers allocated since last reset */
 
 	/*
 	 * Bgworker process to be notified upon activity or -1 if none. See
@@ -86,6 +86,13 @@ typedef struct BufferAccessStrategyData
 	 * ring already.
 	 */
 	bool		current_was_in_ring;
+	/*
+	 * If we could chose a buffer from this list and we end up having to write
+	 * it out because it is dirty when we actually could have found a clean
+	 * buffer in either the freelist or through doing a clock sweep of shared
+	 * buffers, this flag will indicate that
+	 */
+	bool		chose_buffer_in_ring;
 
 	/*
 	 * Array of buffer numbers.  InvalidBuffer (that is, zero) indicates we
@@ -213,7 +220,10 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
 	{
 		buf = GetBufferFromRing(strategy, buf_state);
 		if (buf != NULL)
+		{
+			StrategyChooseBufferBufferFromRing(strategy);
 			return buf;
+		}
 	}
 
 	/*
@@ -247,7 +257,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
 	 * the rate of buffer consumption.  Note that buffers recycled by a
 	 * strategy object are intentionally not counted here.
 	 */
-	pg_atomic_fetch_add_u32(&StrategyControl->numBufferAllocs, 1);
+	pgstat_increment_buffer_action(BA_Alloc);
 
 	/*
 	 * First check, without acquiring the lock, whether there's buffers in the
@@ -411,11 +421,6 @@ StrategySyncStart(uint32 *complete_passes, uint32 *num_buf_alloc)
 		 */
 		*complete_passes += nextVictimBuffer / NBuffers;
 	}
-
-	if (num_buf_alloc)
-	{
-		*num_buf_alloc = pg_atomic_exchange_u32(&StrategyControl->numBufferAllocs, 0);
-	}
 	SpinLockRelease(&StrategyControl->buffer_strategy_lock);
 	return result;
 }
@@ -517,7 +522,6 @@ StrategyInitialize(bool init)
 
 		/* Clear statistics */
 		StrategyControl->completePasses = 0;
-		pg_atomic_init_u32(&StrategyControl->numBufferAllocs, 0);
 
 		/* No pending notification */
 		StrategyControl->bgwprocno = -1;
@@ -702,3 +706,20 @@ StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf)
 
 	return true;
 }
+void
+StrategyUnChooseBufferFromRing(BufferAccessStrategy strategy)
+{
+	strategy->chose_buffer_in_ring = false;
+}
+
+void
+StrategyChooseBufferBufferFromRing(BufferAccessStrategy strategy)
+{
+	strategy->chose_buffer_in_ring = true;
+}
+
+bool
+StrategyIsBufferFromRing(BufferAccessStrategy strategy)
+{
+	return strategy->chose_buffer_in_ring;
+}
diff --git a/src/backend/storage/ipc/ipci.c b/src/backend/storage/ipc/ipci.c
index 3e4ec53a97..c662853423 100644
--- a/src/backend/storage/ipc/ipci.c
+++ b/src/backend/storage/ipc/ipci.c
@@ -240,6 +240,7 @@ CreateSharedMemoryAndSemaphores(void)
 		InitProcGlobal();
 	CreateSharedProcArray();
 	CreateSharedBackendStatus();
+	CreateBufferActionStatsCounters();
 	TwoPhaseShmemInit();
 	BackgroundWorkerShmemInit();
 
diff --git a/src/backend/utils/activity/backend_status.c b/src/backend/utils/activity/backend_status.c
index 2901f9f5a9..97dac2d41f 100644
--- a/src/backend/utils/activity/backend_status.c
+++ b/src/backend/utils/activity/backend_status.c
@@ -55,6 +55,7 @@ static char *BackendAppnameBuffer = NULL;
 static char *BackendClientHostnameBuffer = NULL;
 static char *BackendActivityBuffer = NULL;
 static Size BackendActivityBufferSize = 0;
+static int *BufferActionStatsArray    = NULL;
 #ifdef USE_SSL
 static PgBackendSSLStatus *BackendSslStatusBuffer = NULL;
 #endif
@@ -75,6 +76,7 @@ static MemoryContext backendStatusSnapContext;
 static void pgstat_beshutdown_hook(int code, Datum arg);
 static void pgstat_read_current_status(void);
 static void pgstat_setup_backend_status_context(void);
+static void pgstat_record_dead_backend_buffer_actions(void);

 /*
@@ -236,6 +238,22 @@ CreateSharedBackendStatus(void)
 #endif
 }
 
+void
+CreateBufferActionStatsCounters(void)
+{
+	bool		found;
+	Size		size = 0;
+	int length;
+
+	length = BACKEND_NUM_TYPES * BUFFER_ACTION_NUM_TYPES;
+	size                   = mul_size(sizeof(int), length);
+	BufferActionStatsArray = (int *)
+		ShmemInitStruct("Buffer actions taken by each backend type", size, &found);
+	if (!found)
+		MemSet(BufferActionStatsArray, 0, size);
+}
+
+
 /*
  * Initialize pgstats backend activity state, and set up our on-proc-exit
  * hook.  Called from InitPostgres and AuxiliaryProcessMain. For auxiliary
@@ -399,6 +417,11 @@ pgstat_bestart(void)
 	lbeentry.st_progress_command = PROGRESS_COMMAND_INVALID;
 	lbeentry.st_progress_command_target = InvalidOid;
 	lbeentry.st_query_id = UINT64CONST(0);
+	lbeentry.num_allocs = 0;
+	lbeentry.num_extends = 0;
+	lbeentry.num_fsyncs = 0;
+	lbeentry.num_writes = 0;
+	lbeentry.num_writes_strat = 0;
 
 	/*
 	 * we don't zero st_progress_param here to save cycles; nobody should
@@ -469,6 +492,16 @@ pgstat_beshutdown_hook(int code, Datum arg)
 	beentry->st_procpid = 0;	/* mark invalid */
 
 	PGSTAT_END_WRITE_ACTIVITY(beentry);
+
+	/*
+	 * Because the stats tracking shared buffers written and extended do not
+	 * go through the stats collector, it didn't make sense to add them to
+	 * pgstat_report_stat() At least the DatabaseId should be valid. Otherwise
+	 * we can't be sure that the members were zero-initialized (TODO: is that
+	 * true?)
+	 */
+	if (OidIsValid(MyDatabaseId))
+		pgstat_record_dead_backend_buffer_actions();
 }
 
 /*
@@ -1041,6 +1074,117 @@ pgstat_get_my_query_id(void)
 	 */
 	return MyBEEntry->st_query_id;
 }
+void
+pgstat_increment_buffer_action(BufferActionType ba_type)
+{
+	volatile PgBackendStatus *beentry   = MyBEEntry;
+
+	if (!beentry || !pgstat_track_activities)
+		return;
+
+	PGSTAT_BEGIN_WRITE_ACTIVITY(beentry);
+	if (ba_type == BA_Alloc)
+		beentry->num_allocs++;
+	else if (ba_type == BA_Extend)
+		beentry->num_extends++;
+	else if (ba_type == BA_Fsync)
+		beentry->num_fsyncs++;
+	else if (ba_type == BA_Write)
+		beentry->num_writes++;
+	else if (ba_type == BA_Write_Strat)
+		beentry->num_writes_strat++;
+	PGSTAT_END_WRITE_ACTIVITY(beentry);
+}
+
+/*
+ * Called for a single backend at the time of death to persist its I/O stats
+ */
+void
+pgstat_record_dead_backend_buffer_actions(void)
+{
+	BackendType bt;
+	volatile	PgBackendStatus *beentry = MyBEEntry;
+
+	if (beentry->st_procpid != 0)
+		return;
+	bt = beentry->st_backendType;
+
+	for (;;)
+	{
+		int			before_changecount;
+		int			after_changecount;
+
+		pgstat_begin_read_activity(beentry, before_changecount);
+		BufferActionStatsArray[(bt * BUFFER_ACTION_NUM_TYPES) + BA_Alloc] += beentry->num_allocs;
+		BufferActionStatsArray[(bt * BUFFER_ACTION_NUM_TYPES) + BA_Extend] += beentry->num_extends;
+		BufferActionStatsArray[(bt * BUFFER_ACTION_NUM_TYPES) + BA_Fsync] += beentry->num_fsyncs;
+		BufferActionStatsArray[(bt * BUFFER_ACTION_NUM_TYPES) + BA_Write] += beentry->num_writes;
+		BufferActionStatsArray[(bt * BUFFER_ACTION_NUM_TYPES) + BA_Write_Strat] += beentry->num_writes_strat;
+		pgstat_end_read_activity(beentry, after_changecount);
+
+		if (pgstat_read_activity_complete(before_changecount, after_changecount))
+			break;
+
+		/* Make sure we can break out of loop if stuck... */
+		CHECK_FOR_INTERRUPTS();
+	}
+}
+
+/*
+ * Fill the provided values array with the accumulated counts of buffer actions
+ * taken by all backends of type backend_type (input parameter), both alive and
+ * dead. This is currently only used by pg_stat_get_buffer_actions() to create
+ * the rows in the pg_stat_buffer_actions system view.
+ */
+void
+pgstat_recount_all_buffer_actions(BackendType backend_type, Datum * values)
+{
+	int			beid;
+	int			tot_backends = pgstat_fetch_stat_numbackends();
+
+	/*
+	 * Add stats from all exited backends
+	 *
+	 * TODO: I thought maybe it is okay to just access this lock-free since it
+	 * is only written to when a process dies in
+	 * pgstat_record_dead_backend_buffer_actions() and is read at the time of
+	 * querying the view with the stats. It's okay if we don't have 100%
+	 * up-to-date stats. However, I was wondering about torn values and
+	 * platforms without 64bit "single copy atomicity"
+	 *
+	 * Because the values array is datums and
+	 * BufferActionStatsArray is int64s, can't do a simple memcpy
+	 *
+	 */
+	values[BA_Alloc] = BufferActionStatsArray[(backend_type * BUFFER_ACTION_NUM_TYPES) + BA_Alloc];
+	values[BA_Extend] = BufferActionStatsArray[(backend_type * BUFFER_ACTION_NUM_TYPES) + BA_Extend];
+	values[BA_Fsync] = BufferActionStatsArray[(backend_type * BUFFER_ACTION_NUM_TYPES) + BA_Fsync];
+	values[BA_Write] = BufferActionStatsArray[(backend_type * BUFFER_ACTION_NUM_TYPES) + BA_Write];
+	values[BA_Write_Strat] = BufferActionStatsArray[(backend_type * BUFFER_ACTION_NUM_TYPES) + BA_Write_Strat];
+
+	/*
+	 * Loop through all live backends and count their buffer actions
+	 */
+	// TODO: is there a more efficient to do this, since we will potentially loop
+	//  through all backends for each backend type
+	for (beid = 1; beid <= tot_backends; beid++)
+	{
+		BackendType bt;
+		PgBackendStatus *beentry = pgstat_fetch_stat_beentry(beid);
+
+		if (beentry->st_procpid == 0)
+			continue;
+		bt = beentry->st_backendType;
+		if (bt != backend_type)
+			continue;
+
+		values[BA_Alloc] += beentry->num_allocs;
+		values[BA_Extend] += beentry->num_extends;
+		values[BA_Fsync] += beentry->num_fsyncs;
+		values[BA_Write] += beentry->num_writes;
+		values[BA_Write_Strat] += beentry->num_writes_strat;
+	}
+}

 /* ----------
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 14056f5347..e0dade2c52 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1780,21 +1780,50 @@ pg_stat_get_bgwriter_stat_reset_time(PG_FUNCTION_ARGS)
 }
 
 Datum
-pg_stat_get_buf_written_backend(PG_FUNCTION_ARGS)
+pg_stat_get_buffer_actions(PG_FUNCTION_ARGS)
 {
-	PG_RETURN_INT64(pgstat_fetch_global()->buf_written_backend);
-}
+	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+	TupleDesc	tupdesc;
+	Tuplestorestate *tupstore;
+	MemoryContext per_query_ctx;
+	MemoryContext oldcontext;
 
-Datum
-pg_stat_get_buf_fsync_backend(PG_FUNCTION_ARGS)
-{
-	PG_RETURN_INT64(pgstat_fetch_global()->buf_fsync_backend);
-}
+	/* Build a tuple descriptor for our result type */
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
 
-Datum
-pg_stat_get_buf_alloc(PG_FUNCTION_ARGS)
-{
-	PG_RETURN_INT64(pgstat_fetch_global()->buf_alloc);
+	per_query_ctx = rsinfo->econtext->ecxt_per_query_memory;
+	oldcontext = MemoryContextSwitchTo(per_query_ctx);
+
+	tupstore = tuplestore_begin_heap(true, false, work_mem);
+	rsinfo->returnMode = SFRM_Materialize;
+	rsinfo->setResult = tupstore;
+	rsinfo->setDesc = tupdesc;
+
+	MemoryContextSwitchTo(oldcontext);
+	for (size_t i = 0; i < BACKEND_NUM_TYPES; i++)
+	{
+		/*
+		 * Currently, the only supported backend types for stats are the following.
+		 * If this were to change, pg_proc.dat would need to be changed as well
+		 * to reflect the new expected number of rows.
+		 */
+		Datum values[BUFFER_ACTION_NUM_TYPES];
+		bool nulls[BUFFER_ACTION_NUM_TYPES];
+		if (!(i == B_BG_WRITER || i == B_CHECKPOINTER || i == B_AUTOVAC_WORKER || i == B_BACKEND))
+			continue;
+
+		MemSet(values, 0, sizeof(values));
+		MemSet(nulls, 0, sizeof(nulls));
+
+		values[0] = CStringGetTextDatum(GetBackendTypeDesc(i));
+		pgstat_recount_all_buffer_actions(i, values);
+		tuplestore_putvalues(tupstore, tupdesc, values, nulls);
+	}
+	/* clean up and return the tuplestore */
+	tuplestore_donestoring(tupstore);
+
+	return (Datum) 0;
 }
 
 /*
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 8b73850d0d..d0923407ff 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -277,6 +277,8 @@ GetBackendTypeDesc(BackendType backendType)
 		case B_LOGGER:
 			backendDesc = "logger";
 			break;
+		case BACKEND_NUM_TYPES:
+			break;
 	}
 
 	return backendDesc;
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index acbcae4607..7fc3711d52 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5561,18 +5561,15 @@
   proname => 'pg_stat_get_checkpoint_sync_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => '',
   prosrc => 'pg_stat_get_checkpoint_sync_time' },
-{ oid => '2775', descr => 'statistics: number of buffers written by backends',
-  proname => 'pg_stat_get_buf_written_backend', provolatile => 's',
-  proparallel => 'r', prorettype => 'int8', proargtypes => '',
-  prosrc => 'pg_stat_get_buf_written_backend' },
-{ oid => '3063',
-  descr => 'statistics: number of backend buffer writes that did their own fsync',
-  proname => 'pg_stat_get_buf_fsync_backend', provolatile => 's',
-  proparallel => 'r', prorettype => 'int8', proargtypes => '',
-  prosrc => 'pg_stat_get_buf_fsync_backend' },
-{ oid => '2859', descr => 'statistics: number of buffer allocations',
-  proname => 'pg_stat_get_buf_alloc', provolatile => 's', proparallel => 'r',
-  prorettype => 'int8', proargtypes => '', prosrc => 'pg_stat_get_buf_alloc' },
+
+  { oid => '8459', descr => 'statistics: counts of buffer actions taken by each backend type',
+  proname => 'pg_stat_get_buffer_actions', provolatile => 's', proisstrict => 'f',
+  prorows => '4', proretset => 't',
+  proparallel => 'r', prorettype => 'record', proargtypes => '',
+  proallargtypes => '{text,int8,int8,int8,int8,int8}',
+  proargmodes => '{o,o,o,o,o,o}',
+  proargnames => '{backend_type,buffers_alloc,buffers_extend,buffers_fsync,buffers_write,buffers_write_strat}',
+  prosrc => 'pg_stat_get_buffer_actions' },
 
 { oid => '1136', descr => 'statistics: information about WAL activity',
   proname => 'pg_stat_get_wal', proisstrict => 'f', provolatile => 's',
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 4dc343cbc5..b31369bd7d 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -336,8 +336,20 @@ typedef enum BackendType
 	B_ARCHIVER,
 	B_STATS_COLLECTOR,
 	B_LOGGER,
+	BACKEND_NUM_TYPES,
 } BackendType;
 
+typedef enum BufferActionType
+{
+	BA_Invalid = 0,
+	BA_Alloc,
+	BA_Extend,
+	BA_Fsync,
+	BA_Write,
+	BA_Write_Strat,
+	BUFFER_ACTION_NUM_TYPES,
+}			BufferActionType;
+
 extern BackendType MyBackendType;
 
 extern const char *GetBackendTypeDesc(BackendType backendType);
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 9612c0a6c2..9d0c2a5e1f 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -475,9 +475,6 @@ typedef struct PgStat_MsgBgWriter
 	PgStat_Counter m_buf_written_checkpoints;
 	PgStat_Counter m_buf_written_clean;
 	PgStat_Counter m_maxwritten_clean;
-	PgStat_Counter m_buf_written_backend;
-	PgStat_Counter m_buf_fsync_backend;
-	PgStat_Counter m_buf_alloc;
 	PgStat_Counter m_checkpoint_write_time; /* times in milliseconds */
 	PgStat_Counter m_checkpoint_sync_time;
 } PgStat_MsgBgWriter;
@@ -854,9 +851,6 @@ typedef struct PgStat_GlobalStats
 	PgStat_Counter buf_written_checkpoints;
 	PgStat_Counter buf_written_clean;
 	PgStat_Counter maxwritten_clean;
-	PgStat_Counter buf_written_backend;
-	PgStat_Counter buf_fsync_backend;
-	PgStat_Counter buf_alloc;
 	TimestampTz stat_reset_timestamp;
 } PgStat_GlobalStats;
 
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index 33fcaf5c9a..2bec2cee45 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -314,6 +314,9 @@ extern BufferDesc *StrategyGetBuffer(BufferAccessStrategy strategy,
 extern void StrategyFreeBuffer(BufferDesc *buf);
 extern bool StrategyRejectBuffer(BufferAccessStrategy strategy,
 								 BufferDesc *buf);
+extern void StrategyUnChooseBufferFromRing(BufferAccessStrategy strategy);
+extern void StrategyChooseBufferBufferFromRing(BufferAccessStrategy strategy);
+extern bool StrategyIsBufferFromRing(BufferAccessStrategy strategy);
 
 extern int	StrategySyncStart(uint32 *complete_passes, uint32 *num_buf_alloc);
 extern void StrategyNotifyBgWriter(int bgwprocno);
diff --git a/src/include/utils/backend_status.h b/src/include/utils/backend_status.h
index 8042b817df..4b35b3fe15 100644
--- a/src/include/utils/backend_status.h
+++ b/src/include/utils/backend_status.h
@@ -168,6 +168,11 @@ typedef struct PgBackendStatus
 
 	/* query identifier, optionally computed using post_parse_analyze_hook */
 	uint64		st_query_id;
+	int num_allocs;
+	int num_extends;
+	int num_fsyncs;
+	int num_writes;
+	int num_writes_strat;
 } PgBackendStatus;

@@ -282,7 +287,7 @@ extern PGDLLIMPORT PgBackendStatus *MyBEEntry;
  */
 extern Size BackendStatusShmemSize(void);
 extern void CreateSharedBackendStatus(void);
-
+extern void CreateBufferActionStatsCounters(void);
 
 /* ----------
  * Functions called from backends
@@ -305,6 +310,8 @@ extern const char *pgstat_get_backend_current_activity(int pid, bool checkUser);
 extern const char *pgstat_get_crashed_backend_activity(int pid, char *buffer,
 													   int buflen);
 extern uint64 pgstat_get_my_query_id(void);
+extern void pgstat_increment_buffer_action(BufferActionType ba_type);
+extern void pgstat_recount_all_buffer_actions(BackendType backend_type, Datum *values);

 /* ----------
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index e5ab11275d..609ccf3b7b 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1824,10 +1824,14 @@ pg_stat_bgwriter| SELECT pg_stat_get_bgwriter_timed_checkpoints() AS checkpoints
     pg_stat_get_bgwriter_buf_written_checkpoints() AS buffers_checkpoint,
     pg_stat_get_bgwriter_buf_written_clean() AS buffers_clean,
     pg_stat_get_bgwriter_maxwritten_clean() AS maxwritten_clean,
-    pg_stat_get_buf_written_backend() AS buffers_backend,
-    pg_stat_get_buf_fsync_backend() AS buffers_backend_fsync,
-    pg_stat_get_buf_alloc() AS buffers_alloc,
     pg_stat_get_bgwriter_stat_reset_time() AS stats_reset;
+pg_stat_buffer_actions| SELECT b.backend_type,
+    b.buffers_alloc,
+    b.buffers_extend,
+    b.buffers_fsync,
+    b.buffers_write,
+    b.buffers_write_strat
+   FROM pg_stat_get_buffer_actions() b(backend_type, buffers_alloc, buffers_extend, buffers_fsync, buffers_write, buffers_write_strat);
 pg_stat_database| SELECT d.oid AS datid,
     d.datname,
         CASE
diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql
index feaaee6326..fb4b613d4b 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -176,4 +176,5 @@ FROM prevstats AS pr;
 
 DROP TABLE trunc_stats_test, trunc_stats_test1, trunc_stats_test2, trunc_stats_test3, trunc_stats_test4;
 DROP TABLE prevstats;
+SELECT * FROM pg_stat_buffer_actions;
 -- End of Stats Test
-- 
2.27.0

---

From: Alvaro Herrera <[email-masked]>
Date: Fri, 4 Jun 2021 17:52:17 -0400

> it will make the code much simpler (though, I'm sure I can improve the

I was bitten last year by REFRESH MATERIALIZED VIEW counting its writes
via buffers_backend, and I was very surprised/confused about it.  So it
seems definitely worthwhile to count writes via strategy separately.
For a DBA tuning the server configuration it is very useful.

The main thing is to *not* let these writes end up regular
buffers_backend (or whatever you call these now).  I didn't read your
patch, but the way you have described it seems okay to me.

-- 
Álvaro Herrera                            39°49'30"S 73°17'W

---

From: Melanie Plageman <[email-masked]>
Date: Mon, 2 Aug 2021 18:25:56 -0400

> patch, but the way you have described it seems okay to me.

Thanks for the feedback!

I agree it makes sense to count strategy writes separately.

I thought about this some more, and I don't know if it makes sense to
only count "avoidable" strategy writes.

This would mean that a backend writing out a buffer from the strategy
ring when no clean shared buffers (as well as no clean strategy buffers)
are available would not count that write as a strategy write (even
though it is writing out a buffer from its strategy ring). But, it
obviously doesn't make sense to count it as a regular buffer being
written out. So, I plan to change this code.

On another note, I've updated the patch with more correct concurrency
control control mechanisms (had some data races and other problems
before). Now, I am using atomics for the buffer action counters, though
the code includes several #TODO questions around the correctness of what
I have now too.

I also wrapped the buffer action types in a struct to make them easier
to work with.

The most substantial missing piece of the patch right now is persisting
the data across reboots.

The two places in the code I can see to persist the buffer action stats
data are:
1) using the stats collector code (like in
pgstat_read/write_statsfiles()
2) using a before_shmem_exit() hook which writes the data structure to a
file and then read from it when making the shared memory array initially

It feels a bit weird to me to wedge the buffer actions stats into the
stats collector code--since the stats collector isn't receiving and
aggregating the buffer action stats.

Also, I'm unsure how writing the buffer action stats out in
pgstat_write_statsfiles() will work, since I think that backends can
update their buffer action stats after we would have already persisted
the data from the BufferActionStatsArray -- causing us to lose those
updates.

And, I don't think I can use pgstat_read_statsfiles() since the
BufferActionStatsArray should have the data from the file as soon as the
view containing the buffer action stats can be queried. Thus, it seems
like I would need to read the file while initializing the array in
CreateBufferActionStatsCounters().

I am registering the patch for September commitfest but plan to update
the stats persistence before then (and docs, etc).

-- Melanie
From 2753bf0dc3ff54a515bc0729b51ef56b6715a703 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Mon, 2 Aug 2021 17:56:07 -0400
Subject: [PATCH v3] Add system view tracking shared buffer actions

TODO:
- Some kind of test?
- Docs change
---
 src/backend/catalog/system_views.sql        |  14 ++-
 src/backend/postmaster/checkpointer.c       |  27 +---
 src/backend/postmaster/pgstat.c             |   3 -
 src/backend/storage/buffer/bufmgr.c         |  73 ++++++++++-
 src/backend/storage/buffer/freelist.c       |  37 ++++--
 src/backend/storage/ipc/ipci.c              |   1 +
 src/backend/utils/activity/backend_status.c | 131 ++++++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c         |  55 ++++++--
 src/backend/utils/init/miscinit.c           |   2 +
 src/include/catalog/pg_proc.dat             |  21 ++--
 src/include/miscadmin.h                     |  12 ++
 src/include/pgstat.h                        |   6 -
 src/include/storage/buf_internals.h         |   3 +
 src/include/utils/backend_status.h          |  16 ++-
 src/test/regress/expected/rules.out         |  10 +-
 src/test/regress/sql/stats.sql              |   1 +
 16 files changed, 337 insertions(+), 75 deletions(-)

index 55f6e3711d..96cac0a74e 100644

index bc9ac7ccfa..cbe4889fb6 100644
@@ -90,17 +90,8 @@
@@ -124,9 +115,6 @@ typedef struct
 
@@ -1089,10 +1077,6 @@ ForwardSyncRequest(const FileTag *ftag, SyncRequestType type)

@@ -1106,8 +1090,10 @@ ForwardSyncRequest(const FileTag *ftag, SyncRequestType type)
@@ -1264,13 +1250,6 @@ AbsorbSyncRequests(void)

index 11702f2a80..03d8e13c3a 100644
 
 /* ----------
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 33d99f604a..3bfbb48b1f 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -963,6 +963,11 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 
 	if (isExtend)
 	{
+		/*
@@ -1229,11 +1234,60 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 
 				/* OK, do the I/O */
@@ -2246,9 +2300,6 @@ BgBufferSync(WritebackContext *wb_context)
 	 */
 	strategy_buf_id = StrategySyncStart(&strategy_passes, &recent_alloc);
 
@@ -2543,6 +2594,8 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
 
@@ -2895,6 +2948,20 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln)
 	/*

 
 	/*
 
 	/*
 
 	/*
 
 	/*
 
 		/* Clear statistics */

 	return true;
 }
+void
 
index 2901f9f5a9..ec1a7d4c3a 100644
+static PgBackendBufferActionStats *BufferActionStatsArray    = NULL;

 /*
@@ -236,6 +238,35 @@ CreateSharedBackendStatus(void)
 #endif
 }
 
+void
+CreateBufferActionStatsCounters(void)
+{
+	bool		found;
+	Size		size;
+	int i;
+	PgBackendBufferActionStats *ba_stats;
+
+	size = BACKEND_NUM_TYPES * sizeof(PgBackendBufferActionStats);
+	BufferActionStatsArray = (PgBackendBufferActionStats *)
+		ShmemInitStruct("Buffer actions taken by each backend type", size, &found);
+	if (!found)
+		MemSet(BufferActionStatsArray, 0, size);
+
+	// TODO: do I want a lock on this while initializing the members?
+	ba_stats = BufferActionStatsArray;
+	for (i = 1; i < BACKEND_NUM_TYPES; i++)
+	{
+		pg_atomic_init_u64(&ba_stats->allocs, 0);
+		pg_atomic_init_u64(&ba_stats->extends, 0);
+		pg_atomic_init_u64(&ba_stats->fsyncs, 0);
+		pg_atomic_init_u64(&ba_stats->writes, 0);
+		pg_atomic_init_u64(&ba_stats->writes_strat, 0);
+
+		ba_stats++;
+	}
+}
+
+
 /*
  * Initialize pgstats backend activity state, and set up our on-proc-exit
  * hook.  Called from InitPostgres and AuxiliaryProcessMain. For auxiliary
@@ -399,6 +430,11 @@ pgstat_bestart(void)
 	lbeentry.st_progress_command = PROGRESS_COMMAND_INVALID;
 	lbeentry.st_progress_command_target = InvalidOid;
 	lbeentry.st_query_id = UINT64CONST(0);
+	pg_atomic_init_u64(&lbeentry.buffer_action_stats.allocs, 0);
+	pg_atomic_init_u64(&lbeentry.buffer_action_stats.extends, 0);
+	pg_atomic_init_u64(&lbeentry.buffer_action_stats.fsyncs, 0);
+	pg_atomic_init_u64(&lbeentry.buffer_action_stats.writes, 0);
+	pg_atomic_init_u64(&lbeentry.buffer_action_stats.writes_strat, 0);
 
 	/*
 	 * we don't zero st_progress_param here to save cycles; nobody should
@@ -469,6 +505,16 @@ pgstat_beshutdown_hook(int code, Datum arg)
 	beentry->st_procpid = 0;	/* mark invalid */

 /*
@@ -1041,6 +1087,91 @@ pgstat_get_my_query_id(void)
 	 */
 	return MyBEEntry->st_query_id;
 }
+void
+pgstat_increment_buffer_action(BufferActionType ba_type)
+{
+	volatile PgBackendStatus *beentry   = MyBEEntry;
+
+	if (!beentry || !pgstat_track_activities)
+		return;
+
+	if (ba_type == BA_Alloc)
+		pg_atomic_add_fetch_u64(&beentry->buffer_action_stats.allocs, 1);
+	else if (ba_type == BA_Extend)
+		pg_atomic_add_fetch_u64(&beentry->buffer_action_stats.extends, 1);
+	else if (ba_type == BA_Fsync)
+		pg_atomic_add_fetch_u64(&beentry->buffer_action_stats.fsyncs, 1);
+	else if (ba_type == BA_Write)
+		pg_atomic_add_fetch_u64(&beentry->buffer_action_stats.writes, 1);
+	else if (ba_type == BA_Write_Strat)
+		pg_atomic_add_fetch_u64(&beentry->buffer_action_stats.writes_strat, 1);
+}
+
+/*
+ * Called for a single backend at the time of death to persist its I/O stats
+ */
+void
+pgstat_record_dead_backend_buffer_actions(void)
+{
+	volatile PgBackendBufferActionStats *ba_stats;
+	volatile	PgBackendStatus *beentry = MyBEEntry;
+
+	if (beentry->st_procpid != 0)
+		return;
+
+	// TODO: is this correct? could there be a data race? do I need a lock?
+	ba_stats = &BufferActionStatsArray[beentry->st_backendType];
+	pg_atomic_add_fetch_u64(&ba_stats->allocs, pg_atomic_read_u64(&beentry->buffer_action_stats.allocs));
+	pg_atomic_add_fetch_u64(&ba_stats->extends, pg_atomic_read_u64(&beentry->buffer_action_stats.extends));
+	pg_atomic_add_fetch_u64(&ba_stats->fsyncs, pg_atomic_read_u64(&beentry->buffer_action_stats.fsyncs));
+	pg_atomic_add_fetch_u64(&ba_stats->writes, pg_atomic_read_u64(&beentry->buffer_action_stats.writes));
+	pg_atomic_add_fetch_u64(&ba_stats->writes_strat, pg_atomic_read_u64(&beentry->buffer_action_stats.writes_strat));
+}
+
+/*
+ * Fill the provided values array with the accumulated counts of buffer actions
+ * taken by all backends of type backend_type (input parameter), both alive and
+ * dead. This is currently only used by pg_stat_get_buffer_actions() to create
+ * the rows in the pg_stat_buffer_actions system view.
+ */
+void
+pgstat_recount_all_buffer_actions(BackendType backend_type, Datum *values)
+{
+	int			i;
+	volatile PgBackendStatus *beentry;
+
+	/*
+	 * Add stats from all exited backends
+	 */
+	values[BA_Alloc] = pg_atomic_read_u64(&BufferActionStatsArray[backend_type].allocs);
+	values[BA_Extend] = pg_atomic_read_u64(&BufferActionStatsArray[backend_type].extends);
+	values[BA_Fsync] = pg_atomic_read_u64(&BufferActionStatsArray[backend_type].fsyncs);
+	values[BA_Write] = pg_atomic_read_u64(&BufferActionStatsArray[backend_type].writes);
+	values[BA_Write_Strat] = pg_atomic_read_u64(&BufferActionStatsArray[backend_type].writes_strat);
+
+	/*
+	 * Loop through all live backends and count their buffer actions
+	 */
+	// TODO: see note in pg_stat_get_buffer_actions() about inefficiency of this method
+
+	beentry = BackendStatusArray;
+	for (i = 1; i <= MaxBackends; i++)
+	{
+		/* Don't count dead backends. They should already be counted */
+		if (beentry->st_procpid == 0)
+			continue;
+		if (beentry->st_backendType != backend_type)
+			continue;
+
+		values[BA_Alloc] += pg_atomic_read_u64(&beentry->buffer_action_stats.allocs);
+		values[BA_Extend] += pg_atomic_read_u64(&beentry->buffer_action_stats.extends);
+		values[BA_Fsync] += pg_atomic_read_u64(&beentry->buffer_action_stats.fsyncs);
+		values[BA_Write] += pg_atomic_read_u64(&beentry->buffer_action_stats.writes);
+		values[BA_Write_Strat] += pg_atomic_read_u64(&beentry->buffer_action_stats.writes_strat);
+
+		beentry++;
+	}
+}

 /* ----------
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index f0e09eae4d..ce4d97e5a4 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1780,21 +1780,52 @@ pg_stat_get_bgwriter_stat_reset_time(PG_FUNCTION_ARGS)
 }
 
 Datum
 
-Datum
 
-Datum
+	// TODO: doing the loop like this means we will loop through all backends up to BACKEND_NUM_TYPES times
+	// could preallocate the values arrays and then loop through the backends once, filling in the appropriate values array
 
 /*
 
 	return backendDesc;
index 8cd0252082..3d3a0eea3f 100644
@@ -5565,18 +5565,15 @@
 
index 68d840d699..24d2943d9c 100644

 
 
 extern int	StrategySyncStart(uint32 *complete_passes, uint32 *num_buf_alloc);
 extern void StrategyNotifyBgWriter(int bgwprocno);
diff --git a/src/include/utils/backend_status.h b/src/include/utils/backend_status.h
index 8042b817df..f5360b5aff 100644
--- a/src/include/utils/backend_status.h
+++ b/src/include/utils/backend_status.h
@@ -13,6 +13,7 @@
 #include "datatype/timestamp.h"
 #include "libpq/pqcomm.h"
 #include "miscadmin.h"			/* for BackendType */
+#include "port/atomics.h"
 #include "utils/backend_progress.h"

@@ -79,6 +80,15 @@ typedef struct PgBackendGSSStatus
 
 } PgBackendGSSStatus;
 
+typedef struct PgBackendBufferActionStats
+{
+	pg_atomic_uint64 allocs;
+	pg_atomic_uint64 extends;
+	pg_atomic_uint64 fsyncs;
+	pg_atomic_uint64 writes;
+	pg_atomic_uint64 writes_strat;
+} PgBackendBufferActionStats;
+
 
 /* ----------
  * PgBackendStatus
@@ -168,6 +178,8 @@ typedef struct PgBackendStatus
 
 	/* query identifier, optionally computed using post_parse_analyze_hook */
 	uint64		st_query_id;
+	// TODO: do its members need to be atomics when in the PgBackendStatus since only this backend will write to them?
+	PgBackendBufferActionStats buffer_action_stats;
 } PgBackendStatus;

@@ -282,7 +294,7 @@ extern PGDLLIMPORT PgBackendStatus *MyBEEntry;
  */
 extern Size BackendStatusShmemSize(void);
 extern void CreateSharedBackendStatus(void);
-
+extern void CreateBufferActionStatsCounters(void);
 
 /* ----------
@@ -305,6 +317,8 @@ extern const char *pgstat_get_backend_current_activity(int pid, bool checkUser);

 /* ----------

---

From: Andres Freund <[email-masked]>
Date: Tue, 3 Aug 2021 11:12:58 -0700

Hi,

> written out. So, I plan to change this code.

What do you mean with "no clean shared buffers ... are available"?

> file and then read from it when making the shared memory array initially

I think it's pretty clear that we should go for 1. Having two mechanisms for
persisting stats data is a bad idea.

> the data from the BufferActionStatsArray -- causing us to lose those

I was thinking it'd work differently. Whenever a connection ends, it reports
its data up to pgstats.c (otherwise we'd loose those stats). By the time
shutdown happens, they all need to have already have reported their stats - so
we don't need to do anything to get the data to pgstats.c during shutdown
time.

> CreateBufferActionStatsCounters().

Why would backends need to read that data back?

>          pg_stat_get_bgwriter_stat_reset_time() AS stats_reset;

Material for a separate patch, not this. But if we're going to break
monitoring queries anyway, I think we should consider also renaming
maxwritten_clean (and perhaps a few others), because nobody understands what
that is supposed to mean.

> +			pgstat_increment_buffer_action(BA_Fsync);

Yes, I think that'd make sense. Now that we can disambiguate the different
types of syncs between procs, I don't see a point of having a process-type
filter here. We just loose data...

>  						LWLockRelease(BufferDescriptorGetContentLock(buf));

Could we combine this with StrategyRejectBuffer()? It seems a bit wasteful to
have two function calls into freelist.c when the second happens exactly when
the first returns true?

> +						pgstat_increment_buffer_action(BA_Write);

It seems this would be better solved by having an "bool *from_ring" or
GetBufferSource* parameter to StrategyGetBuffer().

> +	 * the buffer is from the ring of buffers to reuse.

Can we just add a parameter to FlushBuffer indicating what the source of the
write is?

>  	SpinLockRelease(&StrategyControl->buffer_strategy_lock);

Hm. Isn't bgwriter using the *num_buf_alloc value to pace its activity? I
suspect this patch shouldn't get rid of numBufferAllocs at the same time as
overhauling the stats stuff. Perhaps we don't need both - but it's not obvious
that that's the case / how we can make that work.

> +		pg_atomic_add_fetch_u64(&beentry->buffer_action_stats.writes_strat, 1);

I don't think we want to use atomic increments here - they're *slow*. And
there only ever can be a single writer to a backend's stats. So just doing
something like
    pg_atomic_write_u64(&var, pg_atomic_read_u64(&var) + 1)
should do the trick.

> +	pg_atomic_add_fetch_u64(&ba_stats->writes_strat, pg_atomic_read_u64(&beentry->buffer_action_stats.writes_strat));

I don't see a race, FWIW.

This is where I propose that we instead report the values up to the stats
collector, instead of having a separate array that we need to persist

> +		values[BA_Write_Strat] += pg_atomic_read_u64(&beentry->buffer_action_stats.writes_strat);

It seems to make a bit more sense to have this sum up the stats for all
backend types at once.

> +		bool nulls[BUFFER_ACTION_NUM_TYPES];

Ah ;)

Greetings,

Andres Freund

---

From: Melanie Plageman <[email-masked]>
Date: Wed, 11 Aug 2021 16:11:34 -0400

> What do you mean with "no clean shared buffers ... are available"?

I think I was talking about the scenario in which a backend using a
strategy does not find a clean buffer in the strategy ring and goes to
look in the freelist for a clean shared buffer and doesn't find one.

I was probably talking in circles up there. I think the current
patch counts the right writes in the right way, though.

> persisting stats data is a bad idea.

New version uses the stats collector.

> we don't need to do anything to get the data to pgstats.c during shutdown

When you say "whenever a connection ends", what part of the code are you
referring to specifically?

Also, when you say "shutdown", do you mean a backend shutting down or
all backends shutting down (including postmaster) -- like pg_ctl stop?

> Why would backends need to read that data back?

To get totals across restarts, but, doesn't matter now that I am using
stats collector.

> that is supposed to mean.

Do you mean I shouldn't remove anything from the pg_stat_bgwriter view?

> filter here. We just loose data...

Done

> GetBufferSource* parameter to StrategyGetBuffer().

I've addressed both of these in the new version.

> Can we just add a parameter to FlushBuffer indicating what the source of the

I just noticed this comment now, so I'll address that in the next
version. I rebased today and noticed merge conflicts, so, it looks like
v5 will be on its way soon anyway.

> that that's the case / how we can make that work.

I initially meant to add a function to the patch like
pg_stat_get_buffer_actions() but which took a BufferActionType and
BackendType as parameters and returned a single value which is the
number of buffer action types of that type for that type of backend.

let's say I defined it like this:
uint64
  pg_stat_get_backend_buffer_actions_stats(BackendType backend_type,
                                          BufferActionType ba_type)

Then, I intended to use that in StrategySyncStart() to set num_buf_alloc
by subtracting the value of StrategyControl->numBufferAllocs from the
value returned by pg_stat_get_backend_buffer_actions_stats(B_BG_WRITER,
BA_Alloc), val, then adding that value, val, to
StrategyControl->numBufferAllocs.

I think that would have the same behavior as current, though I'm not
sure if the performance would end up being better or worse. It wouldn't
be atomically incrementing StrategyControl->numBufferAllocs, but it
would do a few additional atomic operations in StrategySyncStart() than
before. Also, we would do all the work done by
pg_stat_get_buffer_actions() in StrategySyncStart().

But that is called comparatively infrequently, right?

>     pg_atomic_write_u64(&var, pg_atomic_read_u64(&var) + 1)

Done

> collector, instead of having a separate array that we need to persist

Changed

> It seems to make a bit more sense to have this sum up the stats for all

Changed.

> > +             bool nulls[BUFFER_ACTION_NUM_TYPES];

I just went ahead and made a row for each backend type.

- Melanie
From ab751bdbc96c8c52a341d9ced3f9e1fe929e2010 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Mon, 2 Aug 2021 17:56:07 -0400
Subject: [PATCH v4] Add system view tracking shared buffer actions

TODO:
- Some kind of test?
- Docs change
---
 src/backend/catalog/system_views.sql        | 14 +++-
 src/backend/postmaster/checkpointer.c       | 27 +-----
 src/backend/postmaster/pgstat.c             | 40 ++++++++-
 src/backend/storage/buffer/bufmgr.c         | 30 +++++--
 src/backend/storage/buffer/freelist.c       | 16 +++-
 src/backend/utils/activity/backend_status.c | 62 ++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c         | 91 ++++++++++++++++++---
 src/backend/utils/init/miscinit.c           |  2 +
 src/include/catalog/pg_proc.dat             | 21 ++---
 src/include/miscadmin.h                     | 12 +++
 src/include/pgstat.h                        | 26 ++++--
 src/include/storage/buf_internals.h         |  4 +-
 src/include/utils/backend_status.h          | 15 +++-
 src/test/regress/expected/rules.out         | 10 ++-
 src/test/regress/sql/stats.sql              |  1 +
 15 files changed, 297 insertions(+), 74 deletions(-)

 
index bc9ac7ccfa..db1c6c45c2 100644

 
@@ -1106,8 +1090,8 @@ ForwardSyncRequest(const FileTag *ftag, SyncRequestType type)
@@ -1264,13 +1248,6 @@ AbsorbSyncRequests(void)

-	/* Transfer stats counts into pending pgstats message */
-	BgWriterStats.m_buf_written_backend += CheckpointerShmem->num_backend_writes;
-	BgWriterStats.m_buf_fsync_backend += CheckpointerShmem->num_backend_fsync;
-
-	CheckpointerShmem->num_backend_writes = 0;
-	CheckpointerShmem->num_backend_fsync = 0;
-
 	/*
 	 * We try to avoid holding the lock for a long time by copying the request
 	 * array, and processing the requests after releasing the lock.
diff --git a/src/backend/postmaster/pgstat.c b/src/backend/postmaster/pgstat.c
index 11702f2a80..0db6cd0587 100644
--- a/src/backend/postmaster/pgstat.c
+++ b/src/backend/postmaster/pgstat.c
@@ -129,6 +129,7 @@ char	   *pgstat_stat_tmpname = NULL;
  * without needing to copy things around.  We assume these init to zeroes.
  */
 PgStat_MsgBgWriter BgWriterStats;
+PgStat_MsgBufferActions BufferActionsStats;
 PgStat_MsgWal WalStats;
 
 /*
@@ -348,6 +349,7 @@ static void pgstat_recv_analyze(PgStat_MsgAnalyze *msg, int len);
 static void pgstat_recv_anl_ancestors(PgStat_MsgAnlAncestors *msg, int len);
 static void pgstat_recv_archiver(PgStat_MsgArchiver *msg, int len);
 static void pgstat_recv_bgwriter(PgStat_MsgBgWriter *msg, int len);
+static void pgstat_recv_buffer_actions(PgStat_MsgBufferActions *msg, int len);
 static void pgstat_recv_wal(PgStat_MsgWal *msg, int len);
 static void pgstat_recv_slru(PgStat_MsgSLRU *msg, int len);
 static void pgstat_recv_funcstat(PgStat_MsgFuncstat *msg, int len);
@@ -3040,6 +3042,16 @@ pgstat_send_bgwriter(void)
 	MemSet(&BgWriterStats, 0, sizeof(BgWriterStats));
 }
 
+void
+pgstat_send_buffer_actions(void)
+{
+	pgstat_setheader(&BufferActionsStats.m_hdr, PGSTAT_MTYPE_BUFFER_ACTIONS);
+	pgstat_send(&BufferActionsStats, sizeof(BufferActionsStats));
+
+	// TODO: not needed because backends only call this when exiting?
+	MemSet(&BufferActionsStats, 0, sizeof(BufferActionsStats));
+}
+
 /* ----------
  * pgstat_send_wal() -
  *
@@ -3382,6 +3394,10 @@ PgstatCollectorMain(int argc, char *argv[])
 					pgstat_recv_bgwriter(&msg.msg_bgwriter, len);
 					break;
 
+				case PGSTAT_MTYPE_BUFFER_ACTIONS:
+					pgstat_recv_buffer_actions(&msg.msg_buffer_actions, len);
+					break;
+
 				case PGSTAT_MTYPE_WAL:
 					pgstat_recv_wal(&msg.msg_wal, len);
 					break;
@@ -4056,6 +4072,8 @@ pgstat_read_statsfiles(Oid onlydb, bool permanent, bool deep)
 		goto done;
 	}
 
+
+
 	/*
 	 * We found an existing collector stats file. Read it and put all the
 	 * hashtable entries into place.
@@ -5352,9 +5370,25 @@ pgstat_recv_bgwriter(PgStat_MsgBgWriter *msg, int len)
 	globalStats.buf_written_checkpoints += msg->m_buf_written_checkpoints;
 	globalStats.buf_written_clean += msg->m_buf_written_clean;
 	globalStats.maxwritten_clean += msg->m_maxwritten_clean;
-	globalStats.buf_written_backend += msg->m_buf_written_backend;
-	globalStats.buf_fsync_backend += msg->m_buf_fsync_backend;
-	globalStats.buf_alloc += msg->m_buf_alloc;
+}
+
+
+static void
+pgstat_recv_buffer_actions(PgStat_MsgBufferActions *msg, int len)
+{
+	globalStats.buffer_actions[msg->backend_type].backend_type = msg->backend_type;
+	globalStats.buffer_actions[msg->backend_type].allocs += msg->allocs;
+	globalStats.buffer_actions[msg->backend_type].extends += msg->extends;
+	globalStats.buffer_actions[msg->backend_type].fsyncs += msg->fsyncs;
+	globalStats.buffer_actions[msg->backend_type].writes += msg->writes;
+	globalStats.buffer_actions[msg->backend_type].writes_strat += msg->writes_strat;
+
+}
+
+PgStat_MsgBufferActions *
+pgstat_get_buffer_action_stats(BackendType backend_type)
+{
+	return &globalStats.buffer_actions[backend_type];
 }
 
 /* ----------
index 33d99f604a..8bfdf848a4 100644
 
 	if (isExtend)
 	{
+		/*
+		 * Extends counted here are only those that go through shared buffers
+		 */
+		pgstat_increment_buffer_action(BA_Extend);
+
 		/* new buffers are zero-filled */
 		MemSet((char *) bufBlock, 0, BLCKSZ);
 		/* don't set checksum for all-zero page */
@@ -1163,6 +1168,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	/* Loop here in case we have to try another victim buffer */
 	for (;;)
 	{
+		bool from_ring = false;
 		/*
 		 * Ensure, while the spinlock's not yet held, that there's a free
 		 * refcount entry.
@@ -1173,7 +1179,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 		 * Select a victim buffer.  The buffer is returned with its header
 		 * spinlock still held!
 		 */
-		buf = StrategyGetBuffer(strategy, &buf_state);
+		buf = StrategyGetBuffer(strategy, &buf_state, &from_ring);
 
 		Assert(BUF_STATE_GET_REFCOUNT(buf_state) == 0);
 
@@ -1210,6 +1216,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 			if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf),
 										 LW_SHARED))
 			{
+				BufferActionType buffer_action;
 				/*
 				 * If using a nondefault strategy, and writing the buffer
 				 * would require a WAL flush, let the strategy decide whether
@@ -1227,7 +1234,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 					UnlockBufHdr(buf, buf_state);
 
 					if (XLogNeedsFlush(lsn) &&
-						StrategyRejectBuffer(strategy, buf))
+						StrategyRejectBuffer(strategy, buf, &from_ring))
 					{
 						/* Drop lock/pin and loop around for another buffer */
 						LWLockRelease(BufferDescriptorGetContentLock(buf));
@@ -1236,6 +1243,20 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 					}
 				}
 
+				/*
+				 * When a strategy is in use, if the dirty buffer was selected
+				 * from the strategy ring and we did not bother checking the
+				 * freelist or doing a clock sweep to look for a clean shared
+				 * buffer to use, the write will be counted as a strategy
+				 * write. However, if the dirty buffer was obtained from the
+				 * freelist or a clock sweep, it is counted as a regular write.
+				 * When a strategy is not in use, at this point, the write can
+				 * only be a "regular" write of a dirty buffer.
+				 */
+
+				buffer_action = from_ring ? BA_Write_Strat : BA_Write;
+				pgstat_increment_buffer_action(buffer_action);
+
 				/* OK, do the I/O */
 				TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum,
 														  smgr->smgr_rnode.node.spcNode,
@@ -2246,9 +2267,6 @@ BgBufferSync(WritebackContext *wb_context)
 	 */
 	strategy_buf_id = StrategySyncStart(&strategy_passes, &recent_alloc);
 
@@ -2543,6 +2561,8 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
 
index 6be80476db..e8a8d9f788 100644

@@ -198,7 +199,7 @@ have_free_buffer(void)
  *	return the buffer with the buffer header spinlock still held.
  */
 BufferDesc *
-StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
+StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state, bool *from_ring)
 {
 	BufferDesc *buf;
 	int			bgwprocno;
@@ -213,7 +214,10 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
 	{
 		buf = GetBufferFromRing(strategy, buf_state);
 		if (buf != NULL)
+		{
+			*from_ring = true;
 			return buf;
+		}
 	}
 
 	/*
@@ -247,6 +251,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
 	 * the rate of buffer consumption.  Note that buffers recycled by a
 	 * strategy object are intentionally not counted here.
 	 */
+	pgstat_increment_buffer_action(BA_Alloc);
 	pg_atomic_fetch_add_u32(&StrategyControl->numBufferAllocs, 1);
 
 	/*
@@ -683,7 +688,7 @@ AddBufferToRing(BufferAccessStrategy strategy, BufferDesc *buf)
  * if this buffer should be written and re-used.
  */
 bool
-StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf)
+StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf, bool *from_ring)
 {
 	/* We only do this in bulkread mode */
 	if (strategy->btype != BAS_BULKREAD)
@@ -700,5 +705,12 @@ StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf)
 	 */
 	strategy->buffers[strategy->current] = InvalidBuffer;
 
+	/*
+	 * Since we will not be writing out a dirty buffer from the ring, set
+	 * from_ring to false so that we do not count this write as a "strategy
+	 * write" and can do proper bookkeeping for pg_stat_buffer_actions.
+	 */
+	*from_ring = false;
+
 	return true;
 }
diff --git a/src/backend/utils/activity/backend_status.c b/src/backend/utils/activity/backend_status.c
index 2901f9f5a9..d720c73e70 100644
--- a/src/backend/utils/activity/backend_status.c
+++ b/src/backend/utils/activity/backend_status.c
@@ -75,6 +75,7 @@ static MemoryContext backendStatusSnapContext;
 static void pgstat_beshutdown_hook(int code, Datum arg);
 static void pgstat_read_current_status(void);
 static void pgstat_setup_backend_status_context(void);
+static void pgstat_record_dead_backend_buffer_actions(void);

 /*
@@ -399,6 +400,11 @@ pgstat_bestart(void)
 
 	/*
 	 * we don't zero st_progress_param here to save cycles; nobody should
@@ -469,6 +475,11 @@ pgstat_beshutdown_hook(int code, Datum arg)
 	beentry->st_procpid = 0;	/* mark invalid */
 
 	PGSTAT_END_WRITE_ACTIVITY(beentry);
+
+	// TODO: should this go in pgstat_report_stat() instead
+	// TODO: should this check be here? Is it possible that members were zero-initialized if database ID is not valid?
+	if (OidIsValid(MyDatabaseId))
+		pgstat_record_dead_backend_buffer_actions();
 }
 
 /*
@@ -1041,6 +1052,57 @@ pgstat_get_my_query_id(void)
 	 */
 	return MyBEEntry->st_query_id;
 }
+void
+pgstat_increment_buffer_action(BufferActionType ba_type)
+{
+	volatile PgBackendStatus *beentry   = MyBEEntry;
+
+	if (!beentry || !pgstat_track_activities)
+		return;
+
+	if (ba_type == BA_Alloc)
+		pg_atomic_write_u64(&beentry->buffer_action_stats.allocs, pg_atomic_read_u64(&beentry->buffer_action_stats.allocs) + 1);
+	else if (ba_type == BA_Extend)
+		pg_atomic_write_u64(&beentry->buffer_action_stats.extends, pg_atomic_read_u64(&beentry->buffer_action_stats.extends) + 1);
+	else if (ba_type == BA_Fsync)
+		pg_atomic_write_u64(&beentry->buffer_action_stats.fsyncs, pg_atomic_read_u64(&beentry->buffer_action_stats.fsyncs) + 1);
+	else if (ba_type == BA_Write)
+		pg_atomic_write_u64(&beentry->buffer_action_stats.writes, pg_atomic_read_u64(&beentry->buffer_action_stats.writes) + 1);
+	else if (ba_type == BA_Write_Strat)
+		pg_atomic_write_u64(&beentry->buffer_action_stats.writes_strat, pg_atomic_read_u64(&beentry->buffer_action_stats.writes_strat) + 1);
+
+}
+
+/*
+ * Called for a single backend at the time of death to persist its I/O stats
+ */
+void
+pgstat_record_dead_backend_buffer_actions(void)
+{
+	volatile	PgBackendStatus *beentry = MyBEEntry;
+
+	if (beentry->st_procpid != 0)
+		return;
+
+	// TODO: should I add this or just set it -- seems like it would only happen once -
+	BufferActionsStats.backend_type = beentry->st_backendType;
+	BufferActionsStats.allocs = pg_atomic_read_u64(&beentry->buffer_action_stats.allocs);
+	BufferActionsStats.extends = pg_atomic_read_u64(&beentry->buffer_action_stats.extends);
+	BufferActionsStats.fsyncs = pg_atomic_read_u64(&beentry->buffer_action_stats.fsyncs);
+	BufferActionsStats.writes = pg_atomic_read_u64(&beentry->buffer_action_stats.writes);
+	BufferActionsStats.writes_strat = pg_atomic_read_u64(&beentry->buffer_action_stats.writes_strat);
+	pgstat_send_buffer_actions();
+}
+
+// TODO: this is clearly no good, but I'm not sure if I have to/want to/can use
+// the below pgstat_fetch_stat_beentry and doing the loop that is in
+// pg_stat_get_buffer_actions() into this file will likely mean having to pass a
+// two-dimensional array as a parameter which is unappealing to me
+volatile PgBackendStatus *
+pgstat_access_backend_status_array(void)
+{
+	return BackendStatusArray;
+}

 /* ----------
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index f0e09eae4d..163679f60b 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1780,21 +1780,88 @@ pg_stat_get_bgwriter_stat_reset_time(PG_FUNCTION_ARGS)
 }
 
 Datum
-pg_stat_get_buf_written_backend(PG_FUNCTION_ARGS)
+pg_stat_get_buffer_actions(PG_FUNCTION_ARGS)
 {
-	PG_RETURN_INT64(pgstat_fetch_global()->buf_written_backend);
-}
+	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+	TupleDesc	tupdesc;
+	Tuplestorestate *tupstore;
+	MemoryContext per_query_ctx;
+	MemoryContext oldcontext;
+	PgStat_MsgBufferActions *buffer_actions;
+	int i;
+	volatile PgBackendStatus *beentry;
+	Datum all_values[BACKEND_NUM_TYPES][BUFFER_ACTION_NUM_TYPES];
+	bool all_nulls[BACKEND_NUM_TYPES][BUFFER_ACTION_NUM_TYPES];
 
-Datum
 
-Datum
-pg_stat_get_buf_alloc(PG_FUNCTION_ARGS)
-{
-	PG_RETURN_INT64(pgstat_fetch_global()->buf_alloc);
+	per_query_ctx = rsinfo->econtext->ecxt_per_query_memory;
+	oldcontext = MemoryContextSwitchTo(per_query_ctx);
+
+	tupstore = tuplestore_begin_heap(true, false, work_mem);
+	rsinfo->returnMode = SFRM_Materialize;
+	rsinfo->setResult = tupstore;
+	rsinfo->setDesc = tupdesc;
+
+	MemoryContextSwitchTo(oldcontext);
+	pgstat_fetch_global();
+	for (i = 1; i < BACKEND_NUM_TYPES; i++)
+	{
+		Datum *values = all_values[i];
+		bool *nulls = all_nulls[i];
+
+		MemSet(values, 0, sizeof(Datum[BUFFER_ACTION_NUM_TYPES]));
+		MemSet(nulls, 0, sizeof(Datum[BUFFER_ACTION_NUM_TYPES]));
+
+		values[0] = CStringGetTextDatum(GetBackendTypeDesc(i));
+		/*
+		* Add stats from all exited backends
+		*/
+		buffer_actions = pgstat_get_buffer_action_stats(i);
+
+		values[BA_Alloc] += buffer_actions->allocs;
+		values[BA_Extend] += buffer_actions->extends;
+		values[BA_Fsync] += buffer_actions->fsyncs;
+		values[BA_Write] += buffer_actions->writes;
+		values[BA_Write_Strat] += buffer_actions->writes_strat;
+	}
+
+	/*
+	 * Loop through all live backends and count their buffer actions
+	 */
+
+	beentry = pgstat_access_backend_status_array();
+	for (i = 0; i <= MaxBackends; i++)
+	{
+		Datum *values;
+		beentry++;
+		/* Don't count dead backends. They should already be counted */
+		if (beentry->st_procpid == 0)
+			continue;
+		values = all_values[beentry->st_backendType];
+
+
+		values[BA_Alloc] += pg_atomic_read_u64(&beentry->buffer_action_stats.allocs);
+		values[BA_Extend] += pg_atomic_read_u64(&beentry->buffer_action_stats.extends);
+		values[BA_Fsync] += pg_atomic_read_u64(&beentry->buffer_action_stats.fsyncs);
+		values[BA_Write] += pg_atomic_read_u64(&beentry->buffer_action_stats.writes);
+		values[BA_Write_Strat] += pg_atomic_read_u64(&beentry->buffer_action_stats.writes_strat);
+
+	}
+
+	for (i = 1; i < BACKEND_NUM_TYPES; i++)
+	{
+		Datum *values = all_values[i];
+		bool *nulls = all_nulls[i];
+		tuplestore_putvalues(tupstore, tupdesc, values, nulls);
+	}
+
+	/* clean up and return the tuplestore */
+	tuplestore_donestoring(tupstore);
+
+	return (Datum) 0;
 }
 
 /*
 
 	return backendDesc;
index 8cd0252082..32257dcde8 100644
+  prorows => '13', proretset => 't',
 
index 68d840d699..74b18dad0f 100644

 extern const char *GetBackendTypeDesc(BackendType backendType);
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 9612c0a6c2..ee545a9d63 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -72,6 +72,7 @@ typedef enum StatMsgType
 	PGSTAT_MTYPE_ANL_ANCESTORS,
 	PGSTAT_MTYPE_ARCHIVER,
 	PGSTAT_MTYPE_BGWRITER,
+	PGSTAT_MTYPE_BUFFER_ACTIONS,
 	PGSTAT_MTYPE_WAL,
 	PGSTAT_MTYPE_SLRU,
 	PGSTAT_MTYPE_FUNCSTAT,
@@ -475,13 +476,22 @@ typedef struct PgStat_MsgBgWriter
 	PgStat_Counter m_buf_written_checkpoints;
 	PgStat_Counter m_buf_written_clean;
 	PgStat_Counter m_maxwritten_clean;
-	PgStat_Counter m_buf_written_backend;
-	PgStat_Counter m_buf_fsync_backend;
-	PgStat_Counter m_buf_alloc;
 	PgStat_Counter m_checkpoint_write_time; /* times in milliseconds */
 	PgStat_Counter m_checkpoint_sync_time;
 } PgStat_MsgBgWriter;
 
+typedef struct PgStat_MsgBufferActions
+{
+	PgStat_MsgHdr m_hdr;
+
+	BackendType backend_type;
+	uint64 allocs;
+	uint64 extends;
+	uint64 fsyncs;
+	uint64 writes;
+	uint64 writes_strat;
+} PgStat_MsgBufferActions;
+
 /* ----------
  * PgStat_MsgWal			Sent by backends and background processes to update WAL statistics.
  * ----------
@@ -700,6 +710,7 @@ typedef union PgStat_Msg
 	PgStat_MsgAnlAncestors msg_anl_ancestors;
 	PgStat_MsgArchiver msg_archiver;
 	PgStat_MsgBgWriter msg_bgwriter;
+	PgStat_MsgBufferActions msg_buffer_actions;
 	PgStat_MsgWal msg_wal;
 	PgStat_MsgSLRU msg_slru;
 	PgStat_MsgFuncstat msg_funcstat;
@@ -854,9 +865,7 @@ typedef struct PgStat_GlobalStats
 	PgStat_Counter buf_written_checkpoints;
 	PgStat_Counter buf_written_clean;
 	PgStat_Counter maxwritten_clean;
-	PgStat_Counter buf_written_backend;
-	PgStat_Counter buf_fsync_backend;
-	PgStat_Counter buf_alloc;
+	PgStat_MsgBufferActions buffer_actions[BACKEND_NUM_TYPES];
 	TimestampTz stat_reset_timestamp;
 } PgStat_GlobalStats;
 
@@ -941,6 +950,8 @@ extern char *pgstat_stat_filename;
  */
 extern PgStat_MsgBgWriter BgWriterStats;
 
+extern PgStat_MsgBufferActions BufferActionsStats;
+
 /*
  * WAL statistics counter is updated by backends and background processes
  */
@@ -1091,6 +1102,9 @@ extern void pgstat_twophase_postabort(TransactionId xid, uint16 info,
 
 extern void pgstat_send_archiver(const char *xlog, bool failed);
 extern void pgstat_send_bgwriter(void);
+extern void pgstat_send_buffer_actions(void);
+
+extern PgStat_MsgBufferActions * pgstat_get_buffer_action_stats(BackendType backend_type);
 extern void pgstat_send_wal(bool force);
 
 /* ----------
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index 33fcaf5c9a..7e385135db 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -310,10 +310,10 @@ extern void ScheduleBufferTagForWriteback(WritebackContext *context, BufferTag *
 
 /* freelist.c */
 extern BufferDesc *StrategyGetBuffer(BufferAccessStrategy strategy,
-									 uint32 *buf_state);
+									 uint32 *buf_state, bool *from_ring);
 extern void StrategyFreeBuffer(BufferDesc *buf);
 extern bool StrategyRejectBuffer(BufferAccessStrategy strategy,
-								 BufferDesc *buf);
+								 BufferDesc *buf, bool *from_ring);
 
index 8042b817df..0aeac79184 100644

 } PgBackendGSSStatus;

 /* ----------
  * PgBackendStatus
@@ -168,6 +178,7 @@ typedef struct PgBackendStatus

@@ -282,7 +293,7 @@ extern PGDLLIMPORT PgBackendStatus *MyBEEntry;
  */
 extern Size BackendStatusShmemSize(void);
 extern void CreateSharedBackendStatus(void);
-
+extern void CreateBufferActionStatsCounters(void);
 
 /* ----------
@@ -305,7 +316,9 @@ extern const char *pgstat_get_backend_current_activity(int pid, bool checkUser);
 
+extern volatile PgBackendStatus *pgstat_access_backend_status_array(void);
 
 /* ----------
  * Support functions for the SQL-callable functions to

---

From: Melanie Plageman <[email-masked]>
Date: Wed, 11 Aug 2021 18:00:40 -0400

> v5 will be on its way soon anyway.

Actually, after moving the code around like you suggested, calling
pgstat_increment_buffer_action() before smgrwrite() in FlushBuffer() and
using a parameter to indicate if it is a strategy write or not would
only save us one other call to pgstat_increment_buffer_action() -- the
one in SyncOneBuffer(). We would end up moving the one in BufferAlloc()
to FlushBuffer() and removing the one in SyncOneBuffer().
Do you think it is still worth it?

Rebased v5 attached.
From c5feb44585e1e073927c8d45230aa2eabc178c7e Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Wed, 11 Aug 2021 17:49:08 -0400
Subject: [PATCH v5] Add system view tracking shared buffer actions

TODO:
- Some kind of test?
- Docs change
---
 src/backend/catalog/system_views.sql        | 13 ++-
 src/backend/postmaster/checkpointer.c       | 27 +------
 src/backend/postmaster/pgstat.c             | 43 +++++++++-
 src/backend/storage/buffer/bufmgr.c         | 30 +++++--
 src/backend/storage/buffer/freelist.c       | 17 +++-
 src/backend/utils/activity/backend_status.c | 57 +++++++++++++
 src/backend/utils/adt/pgstatfuncs.c         | 90 ++++++++++++++++++---
 src/backend/utils/init/miscinit.c           |  2 +
 src/include/catalog/pg_proc.dat             | 22 +++--
 src/include/miscadmin.h                     | 13 +++
 src/include/pgstat.h                        | 24 ++++--
 src/include/storage/buf_internals.h         |  4 +-
 src/include/utils/backend_status.h          | 14 ++++
 src/test/regress/expected/rules.out         | 10 ++-
 src/test/regress/sql/stats.sql              |  1 +
 15 files changed, 293 insertions(+), 74 deletions(-)

index 55f6e3711d..f51e7938fc 100644
@@ -1067,11 +1067,18 @@ CREATE VIEW pg_stat_bgwriter AS
 
         w.wal_records,
index be7366379d..fca78fa4ef 100644
@@ -90,17 +90,9 @@
@@ -124,9 +116,6 @@ typedef struct
 
@@ -1085,10 +1074,6 @@ ForwardSyncRequest(const FileTag *ftag, SyncRequestType type)

@@ -1102,8 +1087,7 @@ ForwardSyncRequest(const FileTag *ftag, SyncRequestType type)
@@ -1260,15 +1244,6 @@ AbsorbSyncRequests(void)

-	/* Transfer stats counts into pending pgstats message */
-	PendingCheckpointerStats.m_buf_written_backend
-		+= CheckpointerShmem->num_backend_writes;
-	PendingCheckpointerStats.m_buf_fsync_backend
-		+= CheckpointerShmem->num_backend_fsync;
-
-	CheckpointerShmem->num_backend_writes = 0;
-	CheckpointerShmem->num_backend_fsync = 0;
-
 	/*
 	 * We try to avoid holding the lock for a long time by copying the request
 	 * array, and processing the requests after releasing the lock.
diff --git a/src/backend/postmaster/pgstat.c b/src/backend/postmaster/pgstat.c
index 1b54ef74eb..04a6fec18e 100644
--- a/src/backend/postmaster/pgstat.c
+++ b/src/backend/postmaster/pgstat.c
@@ -70,6 +70,7 @@
 #include "utils/rel.h"
 #include "utils/snapmgr.h"
 #include "utils/timestamp.h"
+#include "utils/backend_status.h"
 
 /* ----------
  * Timer definitions.
@@ -128,6 +129,7 @@ char	   *pgstat_stat_tmpname = NULL;
  * Stored directly in a stats message structure so they can be sent
  * without needing to copy things around.  We assume these init to zeroes.
  */
+PgStat_MsgBufferActions BufferActionsStats;
 PgStat_MsgBgWriter PendingBgWriterStats;
 PgStat_MsgCheckpointer PendingCheckpointerStats;
 PgStat_MsgWal WalStats;
@@ -360,6 +362,7 @@ static void pgstat_recv_anl_ancestors(PgStat_MsgAnlAncestors *msg, int len);
 static void pgstat_recv_archiver(PgStat_MsgArchiver *msg, int len);
 static void pgstat_recv_bgwriter(PgStat_MsgBgWriter *msg, int len);
 static void pgstat_recv_checkpointer(PgStat_MsgCheckpointer *msg, int len);
+static void pgstat_recv_buffer_actions(PgStat_MsgBufferActions *msg, int len);
 static void pgstat_recv_wal(PgStat_MsgWal *msg, int len);
 static void pgstat_recv_slru(PgStat_MsgSLRU *msg, int len);
 static void pgstat_recv_funcstat(PgStat_MsgFuncstat *msg, int len);
@@ -866,6 +869,8 @@ pgstat_report_stat(bool disconnect)
 
 	pgstat_assert_is_up();
 
+
+
 	/*
 	 * Don't expend a clock check if nothing to do.
 	 *
@@ -971,6 +976,10 @@ pgstat_report_stat(bool disconnect)
 	/* Now, send function statistics */
 	pgstat_send_funcstats();
 
+	// TODO: not really sure if this is the right place to do this
+	if (pgstat_record_dying_backend_buffer_actions())
+		pgstat_send_buffer_actions();
+
 	/* Send WAL statistics */
 	pgstat_send_wal(true);
 
@@ -3137,6 +3146,16 @@ pgstat_send_checkpointer(void)
 	MemSet(&PendingCheckpointerStats, 0, sizeof(PendingCheckpointerStats));
 }
 
+void
+pgstat_send_buffer_actions(void)
+{
+	pgstat_setheader(&BufferActionsStats.m_hdr, PGSTAT_MTYPE_BUFFER_ACTIONS);
+	pgstat_send(&BufferActionsStats, sizeof(BufferActionsStats));
+
+	// TODO: not needed because backends only call this when exiting?
+	MemSet(&BufferActionsStats, 0, sizeof(BufferActionsStats));
+}
+
 /* ----------
  * pgstat_send_wal() -
  *
@@ -3483,6 +3502,10 @@ PgstatCollectorMain(int argc, char *argv[])
 					pgstat_recv_checkpointer(&msg.msg_checkpointer, len);
 					break;
 
+				case PGSTAT_MTYPE_BUFFER_ACTIONS:
+					pgstat_recv_buffer_actions(&msg.msg_buffer_actions, len);
+					break;
+
 				case PGSTAT_MTYPE_WAL:
 					pgstat_recv_wal(&msg.msg_wal, len);
 					break;
@@ -5465,7 +5488,6 @@ pgstat_recv_bgwriter(PgStat_MsgBgWriter *msg, int len)
 {
 	globalStats.bgwriter.buf_written_clean += msg->m_buf_written_clean;
 	globalStats.bgwriter.maxwritten_clean += msg->m_maxwritten_clean;
-	globalStats.bgwriter.buf_alloc += msg->m_buf_alloc;
 }
 
 /* ----------
@@ -5482,8 +5504,23 @@ pgstat_recv_checkpointer(PgStat_MsgCheckpointer *msg, int len)
 	globalStats.checkpointer.checkpoint_write_time += msg->m_checkpoint_write_time;
 	globalStats.checkpointer.checkpoint_sync_time += msg->m_checkpoint_sync_time;
 	globalStats.checkpointer.buf_written_checkpoints += msg->m_buf_written_checkpoints;
-	globalStats.checkpointer.buf_written_backend += msg->m_buf_written_backend;
-	globalStats.checkpointer.buf_fsync_backend += msg->m_buf_fsync_backend;
+}
+
+static void
+pgstat_recv_buffer_actions(PgStat_MsgBufferActions *msg, int len)
+{
+	globalStats.buffer_actions[msg->backend_type].backend_type = msg->backend_type;
+	globalStats.buffer_actions[msg->backend_type].allocs += msg->allocs;
+	globalStats.buffer_actions[msg->backend_type].extends += msg->extends;
+	globalStats.buffer_actions[msg->backend_type].fsyncs += msg->fsyncs;
+	globalStats.buffer_actions[msg->backend_type].writes += msg->writes;
+	globalStats.buffer_actions[msg->backend_type].writes_strat += msg->writes_strat;
+}
+
+PgStat_MsgBufferActions *
+pgstat_get_buffer_action_stats(BackendType backend_type)
+{
+	return &globalStats.buffer_actions[backend_type];
 }
 
 /* ----------
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 3b485de067..74f88a918e 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -963,6 +963,10 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 
 	if (isExtend)
 	{
+		/*
@@ -1163,6 +1167,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
@@ -1173,7 +1178,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,

@@ -1210,6 +1215,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 			if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf),
 										 LW_SHARED))
 			{
+				BufferActionType buffer_action;
 				/*
 				 * If using a nondefault strategy, and writing the buffer
 				 * would require a WAL flush, let the strategy decide whether
@@ -1227,7 +1233,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 					UnlockBufHdr(buf, buf_state);
 
@@ -1236,6 +1242,21 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 					}
 				}
 
+				/*
 
-	PendingBgWriterStats.m_buf_alloc += recent_alloc;
 
index 6be80476db..20bba546fe 100644

 
 	/*
 
 	/*
@@ -700,5 +705,13 @@ StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf)
 
+	/*
index 2901f9f5a9..b2dbc4dc28 100644
@@ -399,6 +399,11 @@ pgstat_bestart(void)
 
 	/*
 	 * we don't zero st_progress_param here to save cycles; nobody should
@@ -459,6 +464,7 @@ pgstat_beshutdown_hook(int code, Datum arg)
 {
 	volatile PgBackendStatus *beentry = MyBEEntry;
 
+
 	/*
 	 * Clear my status entry, following the protocol of bumping st_changecount
 	 * before and after.  We use a volatile pointer here to ensure the
@@ -1042,6 +1048,57 @@ pgstat_get_my_query_id(void)
 	return MyBEEntry->st_query_id;
 }
 
+void
+pgstat_record_dying_backend_buffer_actions(void)
+	if (!beentry)
+		return false;
+	return true;
 
 /* ----------
  * pgstat_fetch_stat_beentry() -
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index ff5aedc99c..17d3fa942d 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1779,21 +1779,87 @@ pg_stat_get_bgwriter_stat_reset_time(PG_FUNCTION_ARGS)
 }
 
 Datum
-	PG_RETURN_INT64(pgstat_fetch_stat_checkpointer()->buf_written_backend);
 
-Datum
-	PG_RETURN_INT64(pgstat_fetch_stat_checkpointer()->buf_fsync_backend);
 
-Datum
-	PG_RETURN_INT64(pgstat_fetch_stat_bgwriter()->buf_alloc);
 
 /*
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 88801374b5..cbeaa9ab94 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -294,6 +294,8 @@ GetBackendTypeDesc(BackendType backendType)
 		case B_LOGGER:
 			backendDesc = "logger";
 			break;
+		case BACKEND_NUM_TYPES:
+			break;
 	}
 
 	return backendDesc;
index b603700ed9..1f1a97ba48 100644
@@ -5629,18 +5629,16 @@
+{ oid => '8459', descr => 'statistics: counts of buffer actions taken by each backend type',
 
 { oid => '1136', descr => 'statistics: information about WAL activity',
   proname => 'pg_stat_get_wal', proisstrict => 'f', provolatile => 's',
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 2e2e9a364a..03d5e464a9 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -336,8 +336,21 @@ typedef enum BackendType
 	B_ARCHIVER,
 	B_STATS_COLLECTOR,
 	B_LOGGER,
+	BACKEND_NUM_TYPES,
 } BackendType;

 extern const char *GetBackendTypeDesc(BackendType backendType);
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 2068a68a5f..1948c0bc04 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -73,6 +73,7 @@ typedef enum StatMsgType
 	PGSTAT_MTYPE_ARCHIVER,
 	PGSTAT_MTYPE_BGWRITER,
 	PGSTAT_MTYPE_CHECKPOINTER,
+	PGSTAT_MTYPE_BUFFER_ACTIONS,
 	PGSTAT_MTYPE_WAL,
 	PGSTAT_MTYPE_SLRU,
 	PGSTAT_MTYPE_FUNCSTAT,
@@ -473,7 +474,6 @@ typedef struct PgStat_MsgBgWriter

 /* ----------
@@ -487,12 +487,22 @@ typedef struct PgStat_MsgCheckpointer
 	PgStat_Counter m_timed_checkpoints;
 	PgStat_Counter m_requested_checkpoints;
 	PgStat_Counter m_buf_written_checkpoints;
-	PgStat_Counter m_buf_written_backend;
-	PgStat_Counter m_buf_fsync_backend;
 	PgStat_Counter m_checkpoint_write_time; /* times in milliseconds */
 	PgStat_Counter m_checkpoint_sync_time;
 } PgStat_MsgCheckpointer;
 
@@ -712,6 +722,7 @@ typedef union PgStat_Msg
 	PgStat_MsgCheckpointer msg_checkpointer;
@@ -860,7 +871,6 @@ typedef struct PgStat_BgWriterStats
 } PgStat_BgWriterStats;
 
@@ -875,8 +885,6 @@ typedef struct PgStat_CheckpointerStats
 	PgStat_Counter checkpoint_write_time;	/* times in milliseconds */
 	PgStat_Counter checkpoint_sync_time;
 	PgStat_Counter buf_written_checkpoints;
-	PgStat_Counter buf_written_backend;
-	PgStat_Counter buf_fsync_backend;
 } PgStat_CheckpointerStats;
 
 /*
@@ -888,6 +896,7 @@ typedef struct PgStat_GlobalStats
 
 	PgStat_CheckpointerStats checkpointer;
 	PgStat_BgWriterStats bgwriter;
+	PgStat_MsgBufferActions buffer_actions[BACKEND_NUM_TYPES];
 } PgStat_GlobalStats;
 
 /*
@@ -977,6 +986,7 @@ extern PgStat_MsgBgWriter PendingBgWriterStats;
  */
 extern PgStat_MsgCheckpointer PendingCheckpointerStats;
 
+extern PgStat_MsgBufferActions BufferActionsStats;
 /*
  * WAL statistics counter is updated by backends and background processes
  */
@@ -1128,6 +1138,8 @@ extern void pgstat_twophase_postabort(TransactionId xid, uint16 info,
 extern void pgstat_send_archiver(const char *xlog, bool failed);
 extern void pgstat_send_bgwriter(void);
 extern void pgstat_send_checkpointer(void);
+extern void pgstat_send_buffer_actions(void);
+extern PgStat_MsgBufferActions * pgstat_get_buffer_action_stats(BackendType backend_type);
 extern void pgstat_send_wal(bool force);
 
 /* ----------
 
 /* freelist.c */
 
index 8042b817df..ecb1076ffd 100644

@@ -79,6 +80,14 @@ typedef struct PgBackendGSSStatus
 
 } PgBackendGSSStatus;

 /* ----------
  * PgBackendStatus
@@ -168,6 +177,7 @@ typedef struct PgBackendStatus

@@ -306,6 +316,10 @@ extern const char *pgstat_get_crashed_backend_activity(int pid, char *buffer,
 													   int buflen);
 extern uint64 pgstat_get_my_query_id(void);
 
+extern void pgstat_increment_buffer_action(BufferActionType ba_type);
+extern volatile PgBackendStatus *pgstat_access_backend_status_array(void);
+extern bool pgstat_record_dying_backend_buffer_actions(void);
+
 
 /* ----------

---

From: Andres Freund <[email-masked]>
Date: Fri, 13 Aug 2021 03:08:11 -0700

Hi,

> referring to specifically?

pgstat_beshutdown_hook()

> all backends shutting down (including postmaster) -- like pg_ctl stop?

Admittedly our language is very imprecise around this :(. What I meant
is that backends would report their own stats up to the stats collector
when the connection ends (in pgstat_beshutdown_hook()). That means that
when the whole server (pgstat and then postmaster, potentially via
pg_ctl stop) shuts down, all the per-connection stats have already been
reported up to pgstat.

> Do you mean I shouldn't remove anything from the pg_stat_bgwriter view?

No - I just meant that now that we're breaking pg_stat_bgwriter queries,
we should also rename the columns to be easier to understand. But that
it should be a separate patch / commit...

> StrategyControl->numBufferAllocs.

I don't think you could restrict this to B_BG_WRITER? The whole point of
this logic is that bgwriter uses the stats for *all* backends to get the
"usage rate" for buffers, which it then uses to control how many buffers
to clean.

> pg_stat_get_buffer_actions() in StrategySyncStart().

I think it'd be better to separate changing the bgwriter pacing logic
(and thus numBufferAllocs) from changing the stats reporting.

> But that is called comparatively infrequently, right?

Depending on the workload not that rarely. I'm afraid this might be a
bit too expensive. It's possible we can work around that however.

Greetings,

Andres Freund

---

From: Melanie Plageman <[email-masked]>
Date: Tue, 7 Sep 2021 13:16:28 -0700

> it should be a separate patch / commit...

I separated the removal of some redundant stats from pg_stat_bgwriter
into a different commit but haven't removed or clarified any additional
columns in pg_stat_bgwriter.

> bit too expensive. It's possible we can work around that however.

I've restored StrategyControl->numBuffersAlloc.

Attached is v6 of the patchset.

I have made several small updates to the patch, including user docs
updates, comment clarifications, various changes related to how
structures are initialized, code simplications, small details like
alphabetizing of #includes, etc.

Below are details on the remaining TODOs and open questions for this
patch and why I haven't done them yet:

1) performance testing (initial tests done, but need to do some further
investigation before sharing)

2) stats_reset
Because pg_stat_buffer_actions fields were added to the globalStats
structure, they get reset when the target RESET_BGWRITER is reset.
Depending on whether or not these commits remove columns from the
pg_stat_bgwriter view, I would approach adding stats_reset to
pg_stat_buffer_actions differently. If removing all of pg_stat_bgwriter,
I would just rename the target to apply to pg_stat_buffer_actions. If
not removing all of pg_stat_bgwriter, I would add a new target for
pg_stat_buffer_actions to reset those stats and then either remove them
from globalStats or MemSet() only the relevant parts of the struct in
pgstat_recv_resetsharedcounter().
I haven't done this yet because I want to get input on what should
happen to pg_stat_bgwriter first (all of it goes, all of it stays, some
goes, etc).

3) what to count
Currently, the patch counts allocs, extends, fsyncs and writes of shared
buffers and writes done when using a buffer access strategy. So, it is a
mix of mostly shared buffers and a few non-shared buffers. I am
wondering if it makes sense to also count extends with smgrextend()
other than those using shared buffers--for example when building an
index or when extending the free space map or visibility map. For
fsyncs, the patch does not count checkpointer fsyncs or fsyncs done from
XLogWrite().
On a related note, depending on what the view counts, the name
buffer_actions may or may not be too general.

I also feel like the BackendType B_BACKEND is a bit confusing when we
are tracking buffer actions for different backend types -- this name
makes it seem like other types of backends are not backends.

I'm not sure what the view should track and can see arguments for
excluding certain extends or separating them into another stat. I
haven't made the changes because I am looking for other peoples'
opinions.

4) Adding some sort of protection against regressions when code is added
that adds additional buffer actions but doesn't count them -- more
likely if we are counting all users of smgrextend() but not doing the
counter incrementing there.

I'm not sure how I would even do this, so, that's why I haven't done it.

5) It seems like the code to create a tuplestore used by various stats
functions like pg_stat_get_progress_info(), pg_stat_get_activity, and
pg_stat_get_slru could be refactored into a helper function since it is
quite redundant (maybe returning a ReturnSetInfo).

I haven't done this because I wasn't sure if it was a good idea, and, if
it is, if I should do it in a separate commit.

6) Cleaning up of commit message, running pgindent, and, eventually,
catalog bump (waiting until the patch is done to do this).

7) Additional testing to ensure all codepaths added are hit (one-off
testing, not added to regression test suite). I am waiting to do this
until all of the types of buffer actions that will be done are
finalized.

- Melanie

---

From: Melanie Plageman <[email-masked]>
Date: Wed, 8 Sep 2021 18:28:38 -0700

> pg_ctl stop) shuts down, all the per-connection stats have already been

So, I realized that the patch has a problem. I added the code to send
buffer actions stats to the stats collector
(pgstat_send_buffer_actions()) to pgstat_report_stat() and this isn't
getting called when all types of backends exit.

I originally thought to add pgstat_send_buffer_actions() to
pgstat_beshutdown_hook() (as suggested), but, this is called after
pgstat_shutdown_hook(), so, we aren't able to send stats to the stats
collector at that time. (pgstat_shutdown_hook() sets pgstat_is_shutdown
to true and then in pgstat_beshutdown_hook() (called after), if we call
pgstat_send_buffer_actions(), it calls pgstat_send() which calls
pgstat_assert_is_up() which trips when pgstat_is_shutdown is true.)

After calling pgstat_send_buffer_actions() from pgstat_report_stat(), it
seems to miss checkpointer stats entirely. I did find that if I
sprinkled pgstat_send_buffer_actions() around in the various places that
pgstat_send_checkpointer() is called, I could get checkpointer stats
(see attached patch, capture_checkpointer_buffer_actions.patch), but,
that seems a little bit haphazard since pgstat_send_buffer_actions() is
supposed to capture stats for all backend types. Is there somewhere else
I can call it that is exercised by all backend types before
pgstat_shutdown_hook() is called but after they would have finished any
relevant buffer actions?

- Melanie

---

From: Melanie Plageman <[email-masked]>
Date: Fri, 10 Sep 2021 18:16:28 -0400

> pgstat_shutdown_hook() is called but after they would have finished any

I realized that putting these additional calls in checkpointer code and
not clearing out PgBackendStatus counters for buffer actions results in
a lot of duplicate stats. I was wondering if
pgstat_send_buffer_actions() is needed, however, in
HandleCheckpointerInterrupts() before the proc_exit().

It does seem like additional calls to pgstat_send_buffer_actions()
shouldn't be needed since most processes register
pgstat_shutdown_hook(). However, since MyDatabaseId isn't valid for the
auxiliary processes, even though the pgstat_shutdown_hook() is
registered from BaseInit(), pgstat_report_stat() never gets called for
them, so their stats aren't persisted using the current method.

It seems like the best solution to persisting all processes' stats would
be to have all processes register pgstat_shutdown_hook() and to still
call pgstat_report_stat() even if MyDatabaseId is not valid if the
process is not a regular backend (I assume that it is only a problem
that MyDatabaseId is InvalidOid for backends that have had it set to a
valid oid at some point). For the stats that rely on database OID,
perhaps those can be reported based on whether or not MyDatabaseId is
valid from within pgstat_report_stat().

I also realized that I am not collecting stats from live auxiliary
processes in pg_stat_get_buffer_actions(). I need to change the loop to
for (i = 0; i <= MaxBackends + NUM_AUXPROCTYPES; i++) to actually get
stats from live auxiliary processes when querying the view.

On an unrelated note, I am planning to remove buffers_clean and
buffers_checkpoint from the pg_stat_bgwriter view since those are also
redundant. When I was removing them, I noticed that buffers_checkpoint
and buffers_clean count buffers as having been written even when
FlushBuffer() "does nothing" because someone else wrote out the dirty
buffer before the bgwriter or checkpointer had a chance to do it. This
seems like it would result in an incorrect count. Am I missing
something?

- Melanie

---

From: Melanie Plageman <[email-masked]>
Date: Mon, 13 Sep 2021 17:46:02 -0400

Hi,

I've attached the v7 patch set.

Changes from v6:
- removed unnecessary global variable BufferActionsStats
- fixed the loop condition in pg_stat_get_buffer_actions()
- updated some comments
- removed buffers_checkpoint and buffers_clean from pg_stat_bgwriter
  view (now pg_stat_bgwriter view is mainly checkpointer statistics,
  which isn't great)
- instead of calling pgstat_send_buffer_actions() in
  pgstat_report_stat(), I renamed pgstat_send_buffer_actions() to
  pgstat_report_buffers() and call it directly from
  pgstat_shutdown_hook() for all types of processes (including processes
  with invalid MyDatabaseId [like auxiliary processes])

I began changing the code to add the stats reset timestamp to the
pg_stat_buffer_actions view, but, I realized that it will be kind of
distracting to have every row for every backend type have a stats reset
timestamp (since it will be the same timestamp over and over). If,
however, you could reset buffer stats for each backend type
individually, then, I could see having it. Otherwise, we could add a
function like pg_stat_get_stats_reset_time(viewname) where viewname
would be pg_stat_buffer_actions in our case. Though, maybe that is
annoying and not very usable--I'm not sure.

I also think it makes sense to rename the pg_stat_buffer_actions view to
pg_stat_buffers and to name the columns using both the buffer action
type and buffer type -- e.g. shared, strategy, local. This leaves open
the possibility of counting buffer actions done on other non-shared
buffers -- like those done while building indexes or those using local
buffers. The third patch in the set does this (I wanted to see if it
made sense before fixing it up into the first patch in the set).

This naming convention (BufferType_BufferActionType) made me think that
it might make sense to have two enumerations: one being the current
BufferActionType (which could also be called BufferAccessType though
that might get confusing with BufferAccessStrategyType and buffer access
strategies in general) and the other being BufferType (which would be
one of shared, local, index, etc).

I attached a patch with the outline of this idea
(buffer_type_enum_addition.patch). It doesn't work because
pg_stat_get_buffer_actions() uses the BufferActionType as an index into
the values array returned. If I wanted to use a combination of the two
enums as an indexing mechanism (BufferActionType and BufferType), we
would end up with a tuple having every combination of the two
enums--some of which aren't valid. It might not make sense to implement
this. I do think it is useful to think of these stats as a combination
of a buffer action and a type of buffer.

- Melanie
From 17bf27ad0a6ae54a6a898e96c630d36867e9d943 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 2 Sep 2021 11:33:59 -0400
Subject: [PATCH v7 1/3] Add system view tracking shared buffer actions

All backends increment a counter in their PgBackendStatus when
performing one of these buffer actions. On exit, backends send these
stats to the stats collector to be persisted.

When pg_stat_buffer_actions view is queried, add all live backend's
statuses to the saved stats kept by the stats collector (since the last
stats reset) and return that as the total.

TODO:
- Some kind of test to protect against regressions in counting these
  (and remove unstable pg_stats test)
- stats reset refactor
- when finished, catalog bump
- pgindent
---
 doc/src/sgml/monitoring.sgml                | 94 +++++++++++++++++++++
 src/backend/catalog/system_views.sql        | 10 +++
 src/backend/postmaster/checkpointer.c       |  1 +
 src/backend/postmaster/pgstat.c             | 72 +++++++++++++++-
 src/backend/storage/buffer/bufmgr.c         | 27 +++++-
 src/backend/storage/buffer/freelist.c       | 22 ++++-
 src/backend/utils/activity/backend_status.c | 51 ++++++++++-
 src/backend/utils/adt/pgstatfuncs.c         | 93 ++++++++++++++++++++
 src/backend/utils/init/miscinit.c           |  2 +
 src/include/catalog/pg_proc.dat             |  9 ++
 src/include/miscadmin.h                     | 13 +++
 src/include/pgstat.h                        | 17 ++++
 src/include/storage/buf_internals.h         |  4 +-
 src/include/utils/backend_status.h          | 13 +++
 src/test/regress/expected/rules.out         |  7 ++
 src/test/regress/sql/stats.sql              |  1 +
 16 files changed, 426 insertions(+), 10 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 2281ba120f..edd19368be 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -444,6 +444,15 @@ postgres   27093  0.0  0.0  30096  2752 ?        Ss   11:34   0:00 postgres: ser
      </entry>
      </row>
 
+     <row>
+      <entry><structname>pg_stat_buffer_actions</structname><indexterm><primary>pg_stat_buffer_actions</primary></indexterm></entry>
+      <entry>One row for each backend type showing statistics about
+      backend buffer activity. See
+       <link linkend="monitoring-pg-stat-buffer-actions-view">
+       <structname>pg_stat_buffer_actions</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
@@ -3478,6 +3487,91 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
 
  </sect2>
 
+ <sect2 id="monitoring-pg-stat-buffer-actions-view">
+  <title><structname>pg_stat_buffer_actions</structname></title>
+
+  <indexterm>
+   <primary>pg_stat_buffer_actions</primary>
+  </indexterm>
+
+  <para>
+   The <structname>pg_stat_buffer_actions</structname> view has a row for each
+   backend type, containing global data for the cluster for that backend type.
+  </para>
+
+  <table id="pg-stat-buffer-actions-view" xreflabel="pg_stat_buffer_actions">
+   <title><structname>pg_stat_buffer_actions</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).
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>buffers_alloc</structfield> <type>integer</type>
+      </para>
+      <para>
+       Number of buffers allocated.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>buffers_extend</structfield> <type>integer</type>
+      </para>
+      <para>
+       Number of buffers extended.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>buffers_fsync</structfield> <type>integer</type>
+      </para>
+      <para>
+       Number of buffers fsynced. TODO: is this only shared buffers?
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>buffers_write</structfield> <type>integer</type>
+      </para>
+      <para>
+       Number of buffers written.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>buffers_write_strat</structfield> <type>integer</type>
+      </para>
+      <para>
+       Number of buffers written as part of a buffer access strategy.
+      </para></entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+
+ </sect2>
+
  <sect2 id="monitoring-pg-stat-wal-view">
    <title><structname>pg_stat_wal</structname></title>
 
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 55f6e3711d..7ba54d1119 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1072,6 +1072,16 @@ 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_buffer_actions AS
+SELECT
+       b.backend_type,
+       b.buffers_alloc,
+       b.buffers_extend,
+       b.buffers_fsync,
+       b.buffers_write,
+       b.buffers_write_strat
+FROM pg_stat_get_buffer_actions() b;
+
 CREATE VIEW pg_stat_wal AS
     SELECT
         w.wal_records,
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index be7366379d..23f2ffccd9 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -1104,6 +1104,7 @@ ForwardSyncRequest(const FileTag *ftag, SyncRequestType type)
 		 */
 		if (!AmBackgroundWriterProcess())
 			CheckpointerShmem->num_backend_fsync++;
+		pgstat_increment_buffer_action(BA_Fsync);
 		LWLockRelease(CheckpointerCommLock);
 		return false;
 	}
diff --git a/src/backend/postmaster/pgstat.c b/src/backend/postmaster/pgstat.c
index 3450a10129..75db7a1995 100644
--- a/src/backend/postmaster/pgstat.c
+++ b/src/backend/postmaster/pgstat.c
@@ -63,6 +63,7 @@
 #include "storage/pg_shmem.h"
 #include "storage/proc.h"
 #include "storage/procsignal.h"
+#include "utils/backend_status.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
 #include "utils/memutils.h"
@@ -124,9 +125,12 @@ char	   *pgstat_stat_filename = NULL;
 char	   *pgstat_stat_tmpname = NULL;
 
 /*
- * BgWriter and WAL global statistics counters.
- * Stored directly in a stats message structure so they can be sent
- * without needing to copy things around.  We assume these init to zeroes.
+ * BgWriter, Checkpointer, WAL, and I/O global statistics counters. I/O global
+ * statistics on various buffer actions are tracked in PgBackendStatus while a
+ * backend is alive and then sent to stats collector before a backend exits in
+ * a PgStat_MsgBufferActions.
+ * All others are stored directly in a stats message structure so they can be
+ * sent without needing to copy things around.  We assume these init to zeroes.
  */
 PgStat_MsgBgWriter PendingBgWriterStats;
 PgStat_MsgCheckpointer PendingCheckpointerStats;
@@ -359,6 +363,7 @@ static void pgstat_recv_analyze(PgStat_MsgAnalyze *msg, int len);
 static void pgstat_recv_archiver(PgStat_MsgArchiver *msg, int len);
 static void pgstat_recv_bgwriter(PgStat_MsgBgWriter *msg, int len);
 static void pgstat_recv_checkpointer(PgStat_MsgCheckpointer *msg, int len);
+static void pgstat_recv_buffer_actions(PgStat_MsgBufferActions *msg, int len);
 static void pgstat_recv_wal(PgStat_MsgWal *msg, int len);
 static void pgstat_recv_slru(PgStat_MsgSLRU *msg, int len);
 static void pgstat_recv_funcstat(PgStat_MsgFuncstat *msg, int len);
@@ -970,6 +975,7 @@ pgstat_report_stat(bool disconnect)
 	/* Now, send function statistics */
 	pgstat_send_funcstats();
 
+
 
@@ -2903,6 +2909,13 @@ static void
 pgstat_shutdown_hook(int code, Datum arg)
 {
 	Assert(!pgstat_is_shutdown);
+	/*
+	 * Only need to send stats on buffer actions when a process exits, as
+	 * pg_stat_get_buffer_actions() will read from live backends'
+	 * PgBackendStatus and then sum this with totals from exited backends
+	 * persisted by the stats collector.
+	 */
+	pgstat_report_buffers();
 
 	/*
 	 * If we got as far as discovering our own database ID, we can report what
@@ -3085,6 +3098,31 @@ pgstat_send_checkpointer(void)
 	MemSet(&PendingCheckpointerStats, 0, sizeof(PendingCheckpointerStats));
 }
 
+/*
+ * Called for a single backend at the time of death to send its I/O stats to
+ * the stats collector so that they may be persisted.
+ */
+void
+pgstat_report_buffers(void)
+{
+	PgStat_MsgBufferActions buffer_actions_stats;
+	volatile	PgBackendStatus *beentry = MyBEEntry;
+	if (!beentry)
+		return;
+
+	buffer_actions_stats = (PgStat_MsgBufferActions) {
+		.backend_type = beentry->st_backendType,
+		.allocs = pg_atomic_read_u64(&beentry->buffer_action_stats.allocs),
+		.extends = pg_atomic_read_u64(&beentry->buffer_action_stats.extends),
+		.fsyncs = pg_atomic_read_u64(&beentry->buffer_action_stats.fsyncs),
+		.writes = pg_atomic_read_u64(&beentry->buffer_action_stats.writes),
+		.writes_strat = pg_atomic_read_u64(&beentry->buffer_action_stats.writes_strat)
+	};
+
+	pgstat_setheader(&buffer_actions_stats.m_hdr, PGSTAT_MTYPE_BUFFER_ACTIONS);
+	pgstat_send(&buffer_actions_stats, sizeof(buffer_actions_stats));
+}
+
 /* ----------
  * pgstat_send_wal() -
  *
@@ -3427,6 +3465,10 @@ PgstatCollectorMain(int argc, char *argv[])
 					pgstat_recv_checkpointer(&msg.msg_checkpointer, len);
 					break;
 
+				case PGSTAT_MTYPE_BUFFER_ACTIONS:
+					pgstat_recv_buffer_actions(&msg.msg_buffer_actions, len);
+					break;
+
 				case PGSTAT_MTYPE_WAL:
 					pgstat_recv_wal(&msg.msg_wal, len);
 					break;
@@ -5406,6 +5448,30 @@ pgstat_recv_checkpointer(PgStat_MsgCheckpointer *msg, int len)
 	globalStats.checkpointer.buf_fsync_backend += msg->m_buf_fsync_backend;
 }
 
+static void
+pgstat_recv_buffer_actions(PgStat_MsgBufferActions *msg, int len)
+{
+	/*
+	 * No users will likely need PgStat_MsgBufferActions->backend_type when
+	 * accessing it from globalStats since its place in the
+	 * globalStats.buffer_actions array indicates backend_type. However,
+	 * leaving it undefined seemed like an invitation for unnecessary future
+	 * bugs.
+	 */
+	globalStats.buffer_actions[msg->backend_type].backend_type = msg->backend_type;
+	globalStats.buffer_actions[msg->backend_type].allocs += msg->allocs;
+	globalStats.buffer_actions[msg->backend_type].extends += msg->extends;
+	globalStats.buffer_actions[msg->backend_type].fsyncs += msg->fsyncs;
+	globalStats.buffer_actions[msg->backend_type].writes += msg->writes;
+	globalStats.buffer_actions[msg->backend_type].writes_strat += msg->writes_strat;
+}
+
+const PgStat_MsgBufferActions *
+pgstat_get_buffer_action_stats(BackendType backend_type)
+{
+	return &globalStats.buffer_actions[backend_type];
+}
+
 /* ----------
  * pgstat_recv_wal() -
  *
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index bc1753ae91..ef83c576b0 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -963,6 +963,10 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 
 	if (isExtend)
 	{
+		/*
+		bool from_ring;

 
 
+				/*
@@ -2543,6 +2564,8 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
 
index 6be80476db..17b76e9c2c 100644

@@ -198,7 +199,7 @@ have_free_buffer(void)
  *	return the buffer with the buffer header spinlock still held.
  */
 BufferDesc *
-StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
+StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state, bool *from_ring)
 {
 	BufferDesc *buf;
 	int			bgwprocno;
@@ -212,6 +213,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
 	if (strategy != NULL)
 	{
 		buf = GetBufferFromRing(strategy, buf_state);
+		*from_ring = buf == NULL ? false : true;
 		if (buf != NULL)
 			return buf;
 	}
@@ -247,6 +249,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
 	 * the rate of buffer consumption.  Note that buffers recycled by a
 	 * strategy object are intentionally not counted here.
 	 */
+	pgstat_increment_buffer_action(BA_Alloc);
 	pg_atomic_fetch_add_u32(&StrategyControl->numBufferAllocs, 1);
 
 	/*
@@ -683,8 +686,14 @@ AddBufferToRing(BufferAccessStrategy strategy, BufferDesc *buf)
  * if this buffer should be written and re-used.
  */
 bool
-StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf)
+StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf, bool *from_ring)
 {
+	/*
+	 * If we decide to use the dirty buffer selected by StrategyGetBuffer, then
+	 * ensure that we count it as such in pg_stat_buffer_actions view.
+	 */
+	*from_ring = true;
+
 	/* We only do this in bulkread mode */
 	if (strategy->btype != BAS_BULKREAD)
 		return false;
@@ -700,5 +709,14 @@ StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf)
 	 */
 	strategy->buffers[strategy->current] = InvalidBuffer;
 
+	/*
+	 * Since we will not be writing out a dirty buffer from the ring, set
+	 * from_ring to false so that the caller does not count this write as a
+	 * "strategy write" and can do proper bookkeeping for
+	 * pg_stat_buffer_actions.
+	 */
+	*from_ring = false;
+
+
 	return true;
 }
diff --git a/src/backend/utils/activity/backend_status.c b/src/backend/utils/activity/backend_status.c
index e19c4506ef..f8f914ac7e 100644
--- a/src/backend/utils/activity/backend_status.c
+++ b/src/backend/utils/activity/backend_status.c
@@ -279,7 +279,7 @@ pgstat_beinit(void)
  * pgstat_bestart() -
  *
  *	Initialize this backend's entry in the PgBackendStatus array.
- *	Called from InitPostgres.
+ *	Called from InitPostgres and AuxiliaryProcessMain
  *
  *	Apart from auxiliary processes, MyBackendId, MyDatabaseId,
  *	session userid, and application_name must be set for a
@@ -399,6 +399,11 @@ pgstat_bestart(void)
 	lbeentry.st_progress_command = PROGRESS_COMMAND_INVALID;
 	lbeentry.st_progress_command_target = InvalidOid;
 	lbeentry.st_query_id = UINT64CONST(0);
+	pg_atomic_init_u64(&lbeentry.buffer_action_stats.allocs, 0);
+	pg_atomic_init_u64(&lbeentry.buffer_action_stats.extends, 0);
+	pg_atomic_init_u64(&lbeentry.buffer_action_stats.fsyncs, 0);
+	pg_atomic_init_u64(&lbeentry.buffer_action_stats.writes, 0);
+	pg_atomic_init_u64(&lbeentry.buffer_action_stats.writes_strat, 0);
 
 	/*
 	 * we don't zero st_progress_param here to save cycles; nobody should
@@ -1045,6 +1050,50 @@ pgstat_get_my_query_id(void)
 	return MyBEEntry->st_query_id;
 }
 
+volatile PgBackendStatus *
+pgstat_access_backend_status_array(void)
+{
+	return BackendStatusArray;
+}
+
+void
+pgstat_increment_buffer_action(BufferActionType ba_type)
+{
+	volatile PgBackendStatus *beentry   = MyBEEntry;
+
+	if (!beentry || !pgstat_track_activities)
+		return;
+
+	switch (ba_type)
+	{
+		case BA_Alloc:
+			pg_atomic_write_u64(&beentry->buffer_action_stats.allocs,
+					pg_atomic_read_u64(&beentry->buffer_action_stats.allocs) + 1);
+			break;
+		case BA_Extend:
+			pg_atomic_write_u64(&beentry->buffer_action_stats.extends,
+					pg_atomic_read_u64(&beentry->buffer_action_stats.extends) + 1);
+			break;
+		case BA_Fsync:
+			pg_atomic_write_u64(&beentry->buffer_action_stats.fsyncs,
+					pg_atomic_read_u64(&beentry->buffer_action_stats.fsyncs) + 1);
+			break;
+		case BA_Write:
+			pg_atomic_write_u64(&beentry->buffer_action_stats.writes,
+					pg_atomic_read_u64(&beentry->buffer_action_stats.writes) + 1);
+			break;
+		case BA_Write_Strat:
+			pg_atomic_write_u64(&beentry->buffer_action_stats.writes_strat,
+					pg_atomic_read_u64(&beentry->buffer_action_stats.writes_strat) + 1);
+			break;
+		default:
+			ereport(LOG,
+					(errmsg(
+							"Statistics on Buffer Action Type, %d, are not currently collected.",
+							ba_type)));
+	}
+
+}
 
 /* ----------
  * pgstat_fetch_stat_beentry() -
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index ff5aedc99c..4ba492c121 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1796,6 +1796,99 @@ pg_stat_get_buf_alloc(PG_FUNCTION_ARGS)
 	PG_RETURN_INT64(pgstat_fetch_stat_bgwriter()->buf_alloc);
 }
 
+Datum
+pg_stat_get_buffer_actions(PG_FUNCTION_ARGS)
+{
+	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+	TupleDesc	tupdesc;
+	Tuplestorestate *tupstore;
+	MemoryContext per_query_ctx;
+	MemoryContext oldcontext;
+	const PgStat_MsgBufferActions *buffer_actions;
+	int i;
+	volatile PgBackendStatus *beentry;
+	Datum all_values[BACKEND_NUM_TYPES][BUFFER_ACTION_NUM_TYPES];
+	bool all_nulls[BACKEND_NUM_TYPES][BUFFER_ACTION_NUM_TYPES];
+
+	/* check to see if caller supports us returning a tuplestore */
+	if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("set-valued function called in context that cannot accept a set")));
+	if (!(rsinfo->allowedModes & SFRM_Materialize))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("materialize mode required, but it is not allowed in this context")));
+
+	/* Build a tuple descriptor for our result type */
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	per_query_ctx = rsinfo->econtext->ecxt_per_query_memory;
+	oldcontext = MemoryContextSwitchTo(per_query_ctx);
+
+	tupstore = tuplestore_begin_heap(true, false, work_mem);
+	rsinfo->returnMode = SFRM_Materialize;
+	rsinfo->setResult = tupstore;
+	rsinfo->setDesc = tupdesc;
+
+	MemoryContextSwitchTo(oldcontext);
+
+	MemSet(all_values, 0, sizeof(Datum[BACKEND_NUM_TYPES][BUFFER_ACTION_NUM_TYPES]));
+	MemSet(all_nulls, 0, sizeof(bool[BACKEND_NUM_TYPES][BUFFER_ACTION_NUM_TYPES]));
+
+	/* Add stats from all exited backends */
+	pgstat_fetch_global();
+	/* 0 is not a valid BackendType */
+	for (i = 1; i < BACKEND_NUM_TYPES; i++)
+	{
+		Datum *values = all_values[i];
+
+		values[0] = CStringGetTextDatum(GetBackendTypeDesc(i));
+		buffer_actions = pgstat_get_buffer_action_stats(i);
+
+		values[BA_Alloc] += buffer_actions->allocs;
+		values[BA_Extend] += buffer_actions->extends;
+		values[BA_Fsync] += buffer_actions->fsyncs;
+		values[BA_Write] += buffer_actions->writes;
+		values[BA_Write_Strat] += buffer_actions->writes_strat;
+	}
+
+	/*
+	 * Loop through all live backends and count their buffer actions
+	 */
+	beentry = pgstat_access_backend_status_array();
+	for (i = 0; i <= MaxBackends + NUM_AUXPROCTYPES; i++)
+	{
+		Datum *values;
+		beentry++;
+		/* Don't count dead backends. They should already be counted */
+		if (beentry->st_procpid == 0)
+			continue;
+		values = all_values[beentry->st_backendType];
+
+
+		values[BA_Alloc] += pg_atomic_read_u64(&beentry->buffer_action_stats.allocs);
+		values[BA_Extend] += pg_atomic_read_u64(&beentry->buffer_action_stats.extends);
+		values[BA_Fsync] += pg_atomic_read_u64(&beentry->buffer_action_stats.fsyncs);
+		values[BA_Write] += pg_atomic_read_u64(&beentry->buffer_action_stats.writes);
+		values[BA_Write_Strat] += pg_atomic_read_u64(&beentry->buffer_action_stats.writes_strat);
+
+	}
+
+	for (i = 1; i < BACKEND_NUM_TYPES; i++)
+	{
+		Datum *values = all_values[i];
+		bool *nulls = all_nulls[i];
+		tuplestore_putvalues(tupstore, tupdesc, values, nulls);
+	}
+
+	/* clean up and return the tuplestore */
+	tuplestore_donestoring(tupstore);
+
+	return (Datum) 0;
+}
+
 /*
  * Returns statistics of WAL activity
  */
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 88801374b5..cbeaa9ab94 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -294,6 +294,8 @@ GetBackendTypeDesc(BackendType backendType)
 		case B_LOGGER:
 			backendDesc = "logger";
 			break;
+		case BACKEND_NUM_TYPES:
+			break;
 	}
 
 	return backendDesc;
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index d068d6532e..ee3c11db06 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5642,6 +5642,15 @@
   proname => 'pg_stat_get_buf_alloc', provolatile => 's', proparallel => 'r',
   prorettype => 'int8', proargtypes => '', prosrc => 'pg_stat_get_buf_alloc' },
 
   proparallel => 'r', prorettype => 'record', proargtypes => '',

index 509849c7ff..21f5f24e8c 100644
@@ -473,6 +474,18 @@ typedef struct PgStat_MsgCheckpointer
 
@@ -691,6 +704,7 @@ typedef union PgStat_Msg
@@ -866,6 +880,7 @@ typedef struct PgStat_GlobalStats

 /*
@@ -1105,6 +1120,8 @@ extern void pgstat_twophase_postabort(TransactionId xid, uint16 info,
 extern void pgstat_send_archiver(const char *xlog, bool failed);
 extern void pgstat_send_bgwriter(void);
 extern void pgstat_send_checkpointer(void);
+extern void pgstat_report_buffers(void);
+extern const PgStat_MsgBufferActions * pgstat_get_buffer_action_stats(BackendType backend_type);
 extern void pgstat_send_wal(bool force);
 
 /* ----------
 
 /* freelist.c */
 
index 8042b817df..c23b74b4a6 100644

 } PgBackendGSSStatus;

 /* ----------
  * PgBackendStatus

@@ -306,6 +316,9 @@ extern const char *pgstat_get_crashed_backend_activity(int pid, char *buffer,
 													   int buflen);
 extern uint64 pgstat_get_my_query_id(void);

 /* ----------
  * Support functions for the SQL-callable functions to
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2fa00a3c29..5c5445bcd7 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1828,6 +1828,13 @@ pg_stat_bgwriter| SELECT pg_stat_get_bgwriter_timed_checkpoints() AS checkpoints
     pg_stat_get_buf_fsync_backend() AS buffers_backend_fsync,
     pg_stat_get_buf_alloc() AS buffers_alloc,
     pg_stat_get_bgwriter_stat_reset_time() AS stats_reset;
+pg_stat_buffer_actions| SELECT b.backend_type,
+    b.buffers_alloc,
+    b.buffers_extend,
+    b.buffers_fsync,
+    b.buffers_write,
+    b.buffers_write_strat
+   FROM pg_stat_get_buffer_actions() b(backend_type, buffers_alloc, buffers_extend, buffers_fsync, buffers_write, buffers_write_strat);
 pg_stat_database| SELECT d.oid AS datid,
     d.datname,
         CASE
diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql
index feaaee6326..fb4b613d4b 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -176,4 +176,5 @@ FROM prevstats AS pr;

From f853ecf1a44984158b2a06705c2f5d01bbace47e Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 2 Sep 2021 11:47:41 -0400
Subject: [PATCH v7 2/3] Remove superfluous bgwriter stats

Remove stats from pg_stat_bgwriter which are now more clearly expressed
in pg_stat_buffer_actions.
---
 doc/src/sgml/monitoring.sgml          | 47 ---------------------------
 src/backend/catalog/system_views.sql  |  5 ---
 src/backend/postmaster/checkpointer.c | 26 ---------------
 src/backend/postmaster/pgstat.c       |  5 ---
 src/backend/storage/buffer/bufmgr.c   |  6 ----
 src/backend/utils/adt/pgstatfuncs.c   | 30 -----------------
 src/include/catalog/pg_proc.dat       | 22 -------------
 src/include/pgstat.h                  | 10 ------
 src/test/regress/expected/rules.out   |  5 ---
 9 files changed, 156 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index edd19368be..96258ada18 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -3416,24 +3416,6 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
       </para></entry>
      </row>
 
-     <row>
-      <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>buffers_checkpoint</structfield> <type>bigint</type>
-      </para>
-      <para>
-       Number of buffers written during checkpoints
-      </para></entry>
-     </row>
-
-     <row>
-      <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>buffers_clean</structfield> <type>bigint</type>
-      </para>
-      <para>
-       Number of buffers written by the background writer
-      </para></entry>
-     </row>
-
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>maxwritten_clean</structfield> <type>bigint</type>
@@ -3444,35 +3426,6 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
       </para></entry>
      </row>
 
-     <row>
-      <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>buffers_backend</structfield> <type>bigint</type>
-      </para>
-      <para>
-       Number of buffers written directly by a backend
-      </para></entry>
-     </row>
-
-     <row>
-      <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>buffers_backend_fsync</structfield> <type>bigint</type>
-      </para>
-      <para>
-       Number of times a backend had to execute its own
-       <function>fsync</function> call (normally the background writer handles those
-       even when the backend does its own write)
-      </para></entry>
-     </row>
-
-     <row>
-      <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>buffers_alloc</structfield> <type>bigint</type>
-      </para>
-      <para>
-       Number of buffers allocated
-      </para></entry>
-     </row>
-
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>stats_reset</structfield> <type>timestamp with time zone</type>
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 7ba54d1119..a5d7972687 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1064,12 +1064,7 @@ CREATE VIEW pg_stat_bgwriter AS
         pg_stat_get_bgwriter_requested_checkpoints() AS checkpoints_req,
         pg_stat_get_checkpoint_write_time() AS checkpoint_write_time,
         pg_stat_get_checkpoint_sync_time() AS checkpoint_sync_time,
-        pg_stat_get_bgwriter_buf_written_checkpoints() AS buffers_checkpoint,
-        pg_stat_get_bgwriter_buf_written_clean() AS buffers_clean,
         pg_stat_get_bgwriter_maxwritten_clean() AS maxwritten_clean,
-        pg_stat_get_buf_written_backend() AS buffers_backend,
-        pg_stat_get_buf_fsync_backend() AS buffers_backend_fsync,
-        pg_stat_get_buf_alloc() AS buffers_alloc,
         pg_stat_get_bgwriter_stat_reset_time() AS stats_reset;
 
 CREATE VIEW pg_stat_buffer_actions AS
index 23f2ffccd9..fca78fa4ef 100644

 
-	/* Count all backend writes regardless of if they fit in the queue */
-	if (!AmBackgroundWriterProcess())
-		CheckpointerShmem->num_backend_writes++;
-
 	/*
 	 * If the checkpointer isn't running or the request queue is full, the
 	 * backend will have to perform its own fsync request.  But before forcing
@@ -1102,8 +1087,6 @@ ForwardSyncRequest(const FileTag *ftag, SyncRequestType type)
 		 * Count the subset of writes where backends have to do their own
 		 * fsync
 		 */
-		if (!AmBackgroundWriterProcess())
-			CheckpointerShmem->num_backend_fsync++;
 		pgstat_increment_buffer_action(BA_Fsync);
 		LWLockRelease(CheckpointerCommLock);
 		return false;
@@ -1261,15 +1244,6 @@ AbsorbSyncRequests(void)

-	/* Transfer stats counts into pending pgstats message */
-	PendingCheckpointerStats.m_buf_written_backend
-		+= CheckpointerShmem->num_backend_writes;
-	PendingCheckpointerStats.m_buf_fsync_backend
-		+= CheckpointerShmem->num_backend_fsync;
-
-	CheckpointerShmem->num_backend_writes = 0;
-	CheckpointerShmem->num_backend_fsync = 0;
-
 	/*
 	 * We try to avoid holding the lock for a long time by copying the request
 	 * array, and processing the requests after releasing the lock.
diff --git a/src/backend/postmaster/pgstat.c b/src/backend/postmaster/pgstat.c
index 75db7a1995..43e88f488f 100644
--- a/src/backend/postmaster/pgstat.c
+++ b/src/backend/postmaster/pgstat.c
@@ -5425,9 +5425,7 @@ pgstat_recv_archiver(PgStat_MsgArchiver *msg, int len)
 static void
 pgstat_recv_bgwriter(PgStat_MsgBgWriter *msg, int len)
 {
-	globalStats.bgwriter.buf_written_clean += msg->m_buf_written_clean;
 	globalStats.bgwriter.maxwritten_clean += msg->m_maxwritten_clean;
-	globalStats.bgwriter.buf_alloc += msg->m_buf_alloc;
 }
 
 /* ----------
@@ -5443,9 +5441,6 @@ pgstat_recv_checkpointer(PgStat_MsgCheckpointer *msg, int len)
 	globalStats.checkpointer.requested_checkpoints += msg->m_requested_checkpoints;
 	globalStats.checkpointer.checkpoint_write_time += msg->m_checkpoint_write_time;
 	globalStats.checkpointer.checkpoint_sync_time += msg->m_checkpoint_sync_time;
-	globalStats.checkpointer.buf_written_checkpoints += msg->m_buf_written_checkpoints;
-	globalStats.checkpointer.buf_written_backend += msg->m_buf_written_backend;
-	globalStats.checkpointer.buf_fsync_backend += msg->m_buf_fsync_backend;
 }
 
 static void
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index ef83c576b0..ff219038e2 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -2158,7 +2158,6 @@ BufferSync(int flags)
 			if (SyncOneBuffer(buf_id, false, &wb_context) & BUF_WRITTEN)
 			{
 				TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
-				PendingCheckpointerStats.m_buf_written_checkpoints++;
 				num_written++;
 			}
 		}
@@ -2267,9 +2266,6 @@ BgBufferSync(WritebackContext *wb_context)
 	 */
 	strategy_buf_id = StrategySyncStart(&strategy_passes, &recent_alloc);
 
@@ -2466,8 +2462,6 @@ BgBufferSync(WritebackContext *wb_context)
 			reusable_buffers++;
 	}
 
-	PendingBgWriterStats.m_buf_written_clean += num_written;
-
 #ifdef BGW_DEBUG
 	elog(DEBUG1, "bgwriter: recent_alloc=%u smoothed=%.2f delta=%ld ahead=%d density=%.2f reusable_est=%d upcoming_est=%d scanned=%d wrote=%d reusable=%d",
 		 recent_alloc, smoothed_alloc, strategy_delta, bufs_ahead,
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 4ba492c121..e373c20525 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1738,18 +1738,6 @@ pg_stat_get_bgwriter_requested_checkpoints(PG_FUNCTION_ARGS)
 	PG_RETURN_INT64(pgstat_fetch_stat_checkpointer()->requested_checkpoints);
 }
 
-Datum
-pg_stat_get_bgwriter_buf_written_checkpoints(PG_FUNCTION_ARGS)
-{
-	PG_RETURN_INT64(pgstat_fetch_stat_checkpointer()->buf_written_checkpoints);
-}
-
-Datum
-pg_stat_get_bgwriter_buf_written_clean(PG_FUNCTION_ARGS)
-{
-	PG_RETURN_INT64(pgstat_fetch_stat_bgwriter()->buf_written_clean);
-}
-
 Datum
 pg_stat_get_bgwriter_maxwritten_clean(PG_FUNCTION_ARGS)
 {
@@ -1778,24 +1766,6 @@ pg_stat_get_bgwriter_stat_reset_time(PG_FUNCTION_ARGS)
 	PG_RETURN_TIMESTAMPTZ(pgstat_fetch_stat_bgwriter()->stat_reset_timestamp);
 }
 
-Datum
-pg_stat_get_buf_written_backend(PG_FUNCTION_ARGS)
-{
-	PG_RETURN_INT64(pgstat_fetch_stat_checkpointer()->buf_written_backend);
-}
-
-Datum
-pg_stat_get_buf_fsync_backend(PG_FUNCTION_ARGS)
-{
-	PG_RETURN_INT64(pgstat_fetch_stat_checkpointer()->buf_fsync_backend);
-}
-
-Datum
-pg_stat_get_buf_alloc(PG_FUNCTION_ARGS)
-{
-	PG_RETURN_INT64(pgstat_fetch_stat_bgwriter()->buf_alloc);
-}
-
 Datum
 pg_stat_get_buffer_actions(PG_FUNCTION_ARGS)
 {
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index ee3c11db06..afab94ca96 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5600,16 +5600,6 @@
   proname => 'pg_stat_get_bgwriter_requested_checkpoints', provolatile => 's',
   proparallel => 'r', prorettype => 'int8', proargtypes => '',
   prosrc => 'pg_stat_get_bgwriter_requested_checkpoints' },
-{ oid => '2771',
-  descr => 'statistics: number of buffers written by the bgwriter during checkpoints',
-  proname => 'pg_stat_get_bgwriter_buf_written_checkpoints', provolatile => 's',
-  proparallel => 'r', prorettype => 'int8', proargtypes => '',
-  prosrc => 'pg_stat_get_bgwriter_buf_written_checkpoints' },
-{ oid => '2772',
-  descr => 'statistics: number of buffers written by the bgwriter for cleaning dirty buffers',
-  proname => 'pg_stat_get_bgwriter_buf_written_clean', provolatile => 's',
-  proparallel => 'r', prorettype => 'int8', proargtypes => '',
-  prosrc => 'pg_stat_get_bgwriter_buf_written_clean' },
 { oid => '2773',
   descr => 'statistics: number of times the bgwriter stopped processing when it had written too many buffers while cleaning',
   proname => 'pg_stat_get_bgwriter_maxwritten_clean', provolatile => 's',
@@ -5629,18 +5619,6 @@
   proname => 'pg_stat_get_checkpoint_sync_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => '',
   prosrc => 'pg_stat_get_checkpoint_sync_time' },
-{ oid => '2775', descr => 'statistics: number of buffers written by backends',
-  proname => 'pg_stat_get_buf_written_backend', provolatile => 's',
-  proparallel => 'r', prorettype => 'int8', proargtypes => '',
-  prosrc => 'pg_stat_get_buf_written_backend' },
-{ oid => '3063',
-  descr => 'statistics: number of backend buffer writes that did their own fsync',
-  proname => 'pg_stat_get_buf_fsync_backend', provolatile => 's',
-  proparallel => 'r', prorettype => 'int8', proargtypes => '',
-  prosrc => 'pg_stat_get_buf_fsync_backend' },
-{ oid => '2859', descr => 'statistics: number of buffer allocations',
-  proname => 'pg_stat_get_buf_alloc', provolatile => 's', proparallel => 'r',
-  prorettype => 'int8', proargtypes => '', prosrc => 'pg_stat_get_buf_alloc' },
 
 { oid => '8459', descr => 'statistics: counts of buffer actions taken by each backend type',
   proname => 'pg_stat_get_buffer_actions', provolatile => 's', proisstrict => 'f',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 21f5f24e8c..7c79995a2b 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -452,9 +452,7 @@ typedef struct PgStat_MsgBgWriter
 {
 	PgStat_MsgHdr m_hdr;
 
-	PgStat_Counter m_buf_written_clean;
 	PgStat_Counter m_maxwritten_clean;
-	PgStat_Counter m_buf_alloc;
 } PgStat_MsgBgWriter;
 
 /* ----------
@@ -467,9 +465,6 @@ typedef struct PgStat_MsgCheckpointer
 
 	PgStat_Counter m_timed_checkpoints;
 	PgStat_Counter m_requested_checkpoints;
-	PgStat_Counter m_buf_written_checkpoints;
-	PgStat_Counter m_buf_written_backend;
-	PgStat_Counter m_buf_fsync_backend;
 	PgStat_Counter m_checkpoint_write_time; /* times in milliseconds */
 	PgStat_Counter m_checkpoint_sync_time;
 } PgStat_MsgCheckpointer;
@@ -850,9 +845,7 @@ typedef struct PgStat_ArchiverStats
  */
 typedef struct PgStat_BgWriterStats
 {
-	PgStat_Counter buf_written_clean;
 	PgStat_Counter maxwritten_clean;
-	PgStat_Counter buf_alloc;
 	TimestampTz stat_reset_timestamp;
 } PgStat_BgWriterStats;
 
@@ -866,9 +859,6 @@ typedef struct PgStat_CheckpointerStats
 	PgStat_Counter requested_checkpoints;
 	PgStat_Counter checkpoint_write_time;	/* times in milliseconds */
 	PgStat_Counter checkpoint_sync_time;
-	PgStat_Counter buf_written_checkpoints;
-	PgStat_Counter buf_written_backend;
-	PgStat_Counter buf_fsync_backend;
 } PgStat_CheckpointerStats;
 
 /*
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 5c5445bcd7..f88c060370 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1821,12 +1821,7 @@ pg_stat_bgwriter| SELECT pg_stat_get_bgwriter_timed_checkpoints() AS checkpoints
     pg_stat_get_bgwriter_requested_checkpoints() AS checkpoints_req,
     pg_stat_get_checkpoint_write_time() AS checkpoint_write_time,
     pg_stat_get_checkpoint_sync_time() AS checkpoint_sync_time,
-    pg_stat_get_bgwriter_buf_written_checkpoints() AS buffers_checkpoint,
-    pg_stat_get_bgwriter_buf_written_clean() AS buffers_clean,
     pg_stat_get_bgwriter_maxwritten_clean() AS maxwritten_clean,
-    pg_stat_get_buf_written_backend() AS buffers_backend,
-    pg_stat_get_buf_fsync_backend() AS buffers_backend_fsync,
-    pg_stat_get_buf_alloc() AS buffers_alloc,
     pg_stat_get_bgwriter_stat_reset_time() AS stats_reset;
 pg_stat_buffer_actions| SELECT b.backend_type,
     b.buffers_alloc,
-- 
2.27.0

From c60cf6ec100aae062cbaa9cab1639eb077bca6ed Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Mon, 13 Sep 2021 16:29:50 -0400
Subject: [PATCH v7 3/3] Rename pg_stat_buffer_actions to pg_stat_buffers

Also, rename all members of view to allow for future expansion of buffer
types covered.
---
 doc/src/sgml/monitoring.sgml          | 32 +++++++++++++--------------
 src/backend/catalog/system_views.sql  | 12 +++++-----
 src/backend/storage/buffer/freelist.c |  4 ++--
 src/include/catalog/pg_proc.dat       |  2 +-
 src/test/regress/expected/rules.out   | 14 ++++++------
 src/test/regress/sql/stats.sql        |  2 +-
 6 files changed, 33 insertions(+), 33 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 96258ada18..1ee46694f0 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -445,11 +445,11 @@ postgres   27093  0.0  0.0  30096  2752 ?        Ss   11:34   0:00 postgres: ser
      </row>
 
      <row>
-      <entry><structname>pg_stat_buffer_actions</structname><indexterm><primary>pg_stat_buffer_actions</primary></indexterm></entry>
+      <entry><structname>pg_stat_buffers</structname><indexterm><primary>pg_stat_buffers</primary></indexterm></entry>
       <entry>One row for each backend type showing statistics about
       backend buffer activity. See
        <link linkend="monitoring-pg-stat-buffer-actions-view">
-       <structname>pg_stat_buffer_actions</structname></link> for details.
+       <structname>pg_stat_buffers</structname></link> for details.
      </entry>
      </row>
 
@@ -3441,19 +3441,19 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
  </sect2>
 
  <sect2 id="monitoring-pg-stat-buffer-actions-view">
-  <title><structname>pg_stat_buffer_actions</structname></title>
+  <title><structname>pg_stat_buffers</structname></title>
 
   <indexterm>
-   <primary>pg_stat_buffer_actions</primary>
+   <primary>pg_stat_buffers</primary>
   </indexterm>
 
   <para>
-   The <structname>pg_stat_buffer_actions</structname> view has a row for each
+   The <structname>pg_stat_buffers</structname> view has a row for each
    backend type, containing global data for the cluster for that backend type.
   </para>
 
-  <table id="pg-stat-buffer-actions-view" xreflabel="pg_stat_buffer_actions">
-   <title><structname>pg_stat_buffer_actions</structname> View</title>
+  <table id="pg-stat-buffer-actions-view" xreflabel="pg_stat_buffers">
+   <title><structname>pg_stat_buffers</structname> View</title>
    <tgroup cols="1">
     <thead>
      <row>
@@ -3477,43 +3477,43 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
 
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>buffers_alloc</structfield> <type>integer</type>
+       <structfield>shared_buffers_alloc</structfield> <type>integer</type>
       </para>
       <para>
-       Number of buffers allocated.
+       Number of shared buffers allocated.
       </para></entry>
      </row>
 
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>buffers_extend</structfield> <type>integer</type>
+       <structfield>shared_buffers_extend</structfield> <type>integer</type>
       </para>
       <para>
-       Number of buffers extended.
+       Number of shared buffers extended.
       </para></entry>
      </row>
 
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>buffers_fsync</structfield> <type>integer</type>
+       <structfield>shared_buffers_fsync</structfield> <type>integer</type>
       </para>
       <para>
-       Number of buffers fsynced. TODO: is this only shared buffers?
+       Number of shared buffers fsynced.
       </para></entry>
      </row>
 
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>buffers_write</structfield> <type>integer</type>
+       <structfield>shared_buffers_write</structfield> <type>integer</type>
       </para>
       <para>
-       Number of buffers written.
+       Number of shared buffers written.
       </para></entry>
      </row>
 
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>buffers_write_strat</structfield> <type>integer</type>
+       <structfield>strategy_buffers_write</structfield> <type>integer</type>
       </para>
       <para>
        Number of buffers written as part of a buffer access strategy.
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index a5d7972687..ff668fb256 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1067,14 +1067,14 @@ CREATE VIEW pg_stat_bgwriter AS
         pg_stat_get_bgwriter_maxwritten_clean() AS maxwritten_clean,
         pg_stat_get_bgwriter_stat_reset_time() AS stats_reset;
 
-CREATE VIEW pg_stat_buffer_actions AS
+CREATE VIEW pg_stat_buffers AS
 SELECT
        b.backend_type,
-       b.buffers_alloc,
-       b.buffers_extend,
-       b.buffers_fsync,
-       b.buffers_write,
-       b.buffers_write_strat
+       b.shared_buffers_alloc,
+       b.shared_buffers_extend,
+       b.shared_buffers_fsync,
+       b.shared_buffers_write,
+       b.strategy_buffers_write
 FROM pg_stat_get_buffer_actions() b;
 
 CREATE VIEW pg_stat_wal AS
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index 17b76e9c2c..c85ec3eec0 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -690,7 +690,7 @@ StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf, bool *from_
 {
 	/*
 	 * If we decide to use the dirty buffer selected by StrategyGetBuffer, then
-	 * ensure that we count it as such in pg_stat_buffer_actions view.
+	 * ensure that we count it as such in pg_stat_buffers view.
 	 */
 	*from_ring = true;
 
@@ -713,7 +713,7 @@ StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf, bool *from_
 	 * Since we will not be writing out a dirty buffer from the ring, set
 	 * from_ring to false so that the caller does not count this write as a
 	 * "strategy write" and can do proper bookkeeping for
-	 * pg_stat_buffer_actions.
+	 * pg_stat_buffers.
 	 */
 	*from_ring = false;
 
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index afab94ca96..02d161aa5c 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5626,7 +5626,7 @@
   proparallel => 'r', prorettype => 'record', proargtypes => '',
   proallargtypes => '{text,int8,int8,int8,int8,int8}',
   proargmodes => '{o,o,o,o,o,o}',
-  proargnames => '{backend_type,buffers_alloc,buffers_extend,buffers_fsync,buffers_write,buffers_write_strat}',
+  proargnames => '{backend_type,shared_buffers_alloc,shared_buffers_extend,shared_buffers_fsync,shared_buffers_write,strategy_buffers_write}',
   prosrc => 'pg_stat_get_buffer_actions' },
 
 { oid => '1136', descr => 'statistics: information about WAL activity',
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index f88c060370..8a61ef93e1 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1823,13 +1823,13 @@ pg_stat_bgwriter| SELECT pg_stat_get_bgwriter_timed_checkpoints() AS checkpoints
     pg_stat_get_checkpoint_sync_time() AS checkpoint_sync_time,
     pg_stat_get_bgwriter_maxwritten_clean() AS maxwritten_clean,
     pg_stat_get_bgwriter_stat_reset_time() AS stats_reset;
-pg_stat_buffer_actions| SELECT b.backend_type,
-    b.buffers_alloc,
-    b.buffers_extend,
-    b.buffers_fsync,
-    b.buffers_write,
-    b.buffers_write_strat
-   FROM pg_stat_get_buffer_actions() b(backend_type, buffers_alloc, buffers_extend, buffers_fsync, buffers_write, buffers_write_strat);
+pg_stat_buffers| SELECT b.backend_type,
+    b.shared_buffers_alloc,
+    b.shared_buffers_extend,
+    b.shared_buffers_fsync,
+    b.shared_buffers_write,
+    b.strategy_buffers_write
+   FROM pg_stat_get_buffer_actions() b(backend_type, shared_buffers_alloc, shared_buffers_extend, shared_buffers_fsync, shared_buffers_write, strategy_buffers_write);
 pg_stat_database| SELECT d.oid AS datid,
     d.datname,
         CASE
diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql
index fb4b613d4b..e908ac2591 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -176,5 +176,5 @@ FROM prevstats AS pr;
 
 DROP TABLE trunc_stats_test, trunc_stats_test1, trunc_stats_test2, trunc_stats_test3, trunc_stats_test4;
 DROP TABLE prevstats;
-SELECT * FROM pg_stat_buffer_actions;
+SELECT * FROM pg_stat_buffers;
 -- End of Stats Test
-- 
2.27.0

diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index e52599dc75..1b201ce829 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -1090,7 +1090,7 @@ ForwardSyncRequest(const FileTag *ftag, SyncRequestType type)
 		 * Count the subset of writes where backends have to do their own
 		 * fsync
 		 */
-		pgstat_increment_buffer_action(BA_Fsync);
+		pgstat_increment_buffer_action(BT_Shared, BA_Fsync);
 		LWLockRelease(CheckpointerCommLock);
 		return false;
 	}
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index ff219038e2..0aa1ba8830 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -966,7 +966,7 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 		/*
 		 * Extends counted here are only those that go through shared buffers
 		 */
-		pgstat_increment_buffer_action(BA_Extend);
+		pgstat_increment_buffer_action(BT_Shared, BA_Extend);
 		/* new buffers are zero-filled */
 		MemSet((char *) bufBlock, 0, BLCKSZ);
 		/* don't set checksum for all-zero page */
@@ -1215,7 +1215,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 			if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf),
 										 LW_SHARED))
 			{
-				BufferActionType buffer_action;
+				BufferType buffer_type;
 				/*
 				 * If using a nondefault strategy, and writing the buffer
 				 * would require a WAL flush, let the strategy decide whether
@@ -1253,8 +1253,8 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 				 * only be a "regular" write of a dirty buffer.
 				 */
 
-				buffer_action = from_ring ? BA_Write_Strat : BA_Write;
-				pgstat_increment_buffer_action(buffer_action);
+				buffer_type = from_ring ? BT_Strategy : BT_Shared;
+				pgstat_increment_buffer_action(buffer_type, BA_Write);

 				/* OK, do the I/O */
@@ -2559,7 +2559,7 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
 	 * buffer is clean by the time we've locked it.)
 	 */
 
-	pgstat_increment_buffer_action(BA_Write);
+	pgstat_increment_buffer_action(BT_Shared, BA_Write);
 	PinBuffer_Locked(bufHdr);
 	LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
 
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index 17b76e9c2c..fd95cba478 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -249,7 +249,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state, bool *from_r
 	 * the rate of buffer consumption.  Note that buffers recycled by a
 	 * strategy object are intentionally not counted here.
 	 */
-	pgstat_increment_buffer_action(BA_Alloc);
+	pgstat_increment_buffer_action(BT_Shared, BA_Alloc);
 	pg_atomic_fetch_add_u32(&StrategyControl->numBufferAllocs, 1);
 
 	/*
diff --git a/src/backend/utils/activity/backend_status.c b/src/backend/utils/activity/backend_status.c
index f8f914ac7e..a0833a733b 100644
--- a/src/backend/utils/activity/backend_status.c
+++ b/src/backend/utils/activity/backend_status.c
@@ -1057,14 +1057,14 @@ pgstat_access_backend_status_array(void)
 }
 
 void
-pgstat_increment_buffer_action(BufferActionType ba_type)
+pgstat_increment_buffer_action(BufferType buffer_type, BufferActionType buffer_action_type)
 {
 	volatile PgBackendStatus *beentry   = MyBEEntry;
 
 	if (!beentry || !pgstat_track_activities)
 		return;
 
-	switch (ba_type)
+	switch (buffer_action_type)
 	{
 		case BA_Alloc:
 			pg_atomic_write_u64(&beentry->buffer_action_stats.allocs,
@@ -1079,18 +1079,18 @@ pgstat_increment_buffer_action(BufferActionType ba_type)
 					pg_atomic_read_u64(&beentry->buffer_action_stats.fsyncs) + 1);
 			break;
 		case BA_Write:
-			pg_atomic_write_u64(&beentry->buffer_action_stats.writes,
-					pg_atomic_read_u64(&beentry->buffer_action_stats.writes) + 1);
-			break;
-		case BA_Write_Strat:
-			pg_atomic_write_u64(&beentry->buffer_action_stats.writes_strat,
-					pg_atomic_read_u64(&beentry->buffer_action_stats.writes_strat) + 1);
+			if (buffer_type == BT_Strategy)
+				pg_atomic_write_u64(&beentry->buffer_action_stats.writes_strat,
+						pg_atomic_read_u64(&beentry->buffer_action_stats.writes_strat) + 1);
+			else if (buffer_type == BT_Shared)
+				pg_atomic_write_u64(&beentry->buffer_action_stats.writes,
+						pg_atomic_read_u64(&beentry->buffer_action_stats.writes) + 1);
 			break;
 		default:
 			ereport(LOG,
 					(errmsg(
 							"Statistics on Buffer Action Type, %d, are not currently collected.",
-							ba_type)));
+							buffer_action_type)));
 	}
 
 }
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 03d5e464a9..b6885a5d66 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -339,6 +339,15 @@ typedef enum BackendType
 	BACKEND_NUM_TYPES,
 } BackendType;
 
+typedef enum BufferType
+{
+	BT_Shared,
+	BT_Strategy,
+	BT_Local,
+	BUFFER_NUM_TYPES,
+} BufferType;
+
+// TODO: should BufferAction be BufferAccess? See BufferAccessStrategyType
 typedef enum BufferActionType
 {
 	BA_Invalid = 0,
@@ -346,7 +355,6 @@ typedef enum BufferActionType
 	BA_Extend,
 	BA_Fsync,
 	BA_Write,
-	BA_Write_Strat,
 	BUFFER_ACTION_NUM_TYPES,
 }	BufferActionType;
 
diff --git a/src/include/utils/backend_status.h b/src/include/utils/backend_status.h
index c23b74b4a6..fb1334401e 100644
--- a/src/include/utils/backend_status.h
+++ b/src/include/utils/backend_status.h
@@ -317,7 +317,7 @@ extern const char *pgstat_get_crashed_backend_activity(int pid, char *buffer,
 extern uint64 pgstat_get_my_query_id(void);
 
 extern volatile PgBackendStatus *pgstat_access_backend_status_array(void);
-extern void pgstat_increment_buffer_action(BufferActionType ba_type);
+extern void pgstat_increment_buffer_action(BufferType buffer_type, BufferActionType buffer_action_type);

 /* ----------

---

From: Alvaro Herrera <[email-masked]>
Date: Tue, 14 Sep 2021 22:30:02 -0300

Hello Melanie

> made sense before fixing it up into the first patch in the set).

What do you think of the idea of having the "shared/strategy/local"
attribute be a column?  So you'd have up to three rows per buffer action
type.  Users wishing to see an aggregate can just aggregate them, just
like they'd do with pg_buffercache.  I think that leads to an easy

> of a buffer action and a type of buffer.

Does that seem sensible?

(It's weird to have enum values that are there just to indicate what's
the maximum value.  I think that sort of thing is better done by having
a "#define LAST_THING" that takes the last valid value from the enum.
That would free you from having to handle the last value in switch
blocks, for example.  LAST_OCLASS in dependency.h is a precedent on this.)

-- 
Álvaro Herrera              Valdivia, Chile  —  https://www.EnterpriseDB.com/
"That sort of implies that there are Emacs keystrokes which aren't obscure.
I've been using it daily for 2 years now and have yet to discover any key
sequence which makes any sense."                        (Paul Thomas)

---

From: Melanie Plageman <[email-masked]>
Date: Thu, 23 Sep 2021 17:05:07 -0400

> decision with regards to this point:

I have rewritten the code to implement this.

> blocks, for example.  LAST_OCLASS in dependency.h is a precedent on this.)

I have made this change.

The attached v8 patchset is rewritten to add in an additional dimension
-- buffer type. Now, a backend keeps track of how many buffers of a
particular type (e.g. shared, local) it has accessed in a particular way
(e.g. alloc, write). It also changes the naming of various structures
and the view members.

Previously, stats reset did not work since it did not consider live
backends' counters. Now, the reset message includes the current live
backends' counters to be tracked by the stats collector and used when
the view is queried.

The reset message is one of the areas in which I still need to do some
work -- I shoved the array of PgBufferAccesses into the existing reset
message used for checkpointer, bgwriter, etc. Before making a new type
of message, I would like feedback from a reviewer about the approach.

There are various TODOs in the code which are actually questions for the
reviewer. Once I have some feedback, it will be easier to address these
items.

There a few other items which may be material for other commits that
I would also like to do:
1) write wrapper functions for smgr* functions which count buffer
accesses of the appropriate type. I wasn't sure if these should
literally just take all the parameters that the smgr* functions take +
buffer type. Once these exist, there will be less possibility for
regressions in which new code is added using smgr* functions without
counting this buffer activity. Once I add these, I was going to go
through and replace existing calls to smgr* functions and thereby start
counting currently uncounted buffer type accesses (direct, local, etc).

2) Separate checkpointer and bgwriter into two views and add additional
stats to the bgwriter view.

3) Consider adding a helper function to pgstatfuncs.c to help create the
tuplestore. These functions all have quite a few lines which are exactly
the same, and I thought it might be nice to do something about that:
  pg_stat_get_progress_info(PG_FUNCTION_ARGS)
  pg_stat_get_activity(PG_FUNCTION_ARGS)
  pg_stat_get_buffers_accesses(PG_FUNCTION_ARGS)
  pg_stat_get_slru(PG_FUNCTION_ARGS)
  pg_stat_get_progress_info(PG_FUNCTION_ARGS)
I can imagine a function that takes a Datums array, a nulls array, and a
ResultSetInfo and then makes the tuplestore -- though I think that will
use more memory. Perhaps we could make a macro which does the initial
error checking (checking if caller supports returning a tuplestore)? I'm
not sure if there is something meaningful here, but I thought I would
ask.

Finally, I haven't removed the test in pg_stats and haven't done a final
pass for comment clarity, alphabetization, etc on this version.

- Melanie
From 479fdfc53d1a6ee9943bdc580884866e99497673 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 2 Sep 2021 11:47:41 -0400
Subject: [PATCH v8 2/2] Remove superfluous bgwriter stats

in pg_stat_buffers.

TODO:
- make pg_stat_checkpointer view and move relevant stats into it
- add additional stats to pg_stat_bgwriter
---
 doc/src/sgml/monitoring.sgml          | 47 ---------------------------
 src/backend/catalog/system_views.sql  |  6 +---
 src/backend/postmaster/checkpointer.c | 26 ---------------
 src/backend/postmaster/pgstat.c       |  5 ---
 src/backend/storage/buffer/bufmgr.c   |  6 ----
 src/backend/utils/adt/pgstatfuncs.c   | 30 -----------------
 src/include/catalog/pg_proc.dat       | 22 -------------
 src/include/pgstat.h                  | 10 ------
 src/test/regress/expected/rules.out   |  5 ---
 9 files changed, 1 insertion(+), 156 deletions(-)

index 60627c692a..08772652ac 100644
 
-     <row>
 
-     <row>
index 30280d520b..c45c261f4b 100644
@@ -1058,18 +1058,14 @@ CREATE VIEW pg_stat_archiver AS
         s.stats_reset
     FROM pg_stat_get_archiver() s;
 
+-- TODO: make separate pg_stat_checkpointer view
 CREATE VIEW pg_stat_bgwriter AS
     SELECT
         pg_stat_get_bgwriter_timed_checkpoints() AS checkpoints_timed,
         pg_stat_get_bgwriter_requested_checkpoints() AS checkpoints_req,
         pg_stat_get_checkpoint_write_time() AS checkpoint_write_time,
         pg_stat_get_checkpoint_sync_time() AS checkpoint_sync_time,
-        pg_stat_get_bgwriter_buf_written_checkpoints() AS buffers_checkpoint,
-        pg_stat_get_bgwriter_buf_written_clean() AS buffers_clean,
         pg_stat_get_bgwriter_maxwritten_clean() AS maxwritten_clean,
-        pg_stat_get_buf_written_backend() AS buffers_backend,
-        pg_stat_get_buf_fsync_backend() AS buffers_backend_fsync,
-        pg_stat_get_buf_alloc() AS buffers_alloc,
         pg_stat_get_bgwriter_stat_reset_time() AS stats_reset;
 
 CREATE VIEW pg_stat_buffers AS
index fe6bd15506..6d3a8da948 100644
 
@@ -1087,10 +1076,6 @@ ForwardSyncRequest(const FileTag *ftag, SyncRequestType type)

-	/* Count all backend writes regardless of if they fit in the queue */
-	if (!AmBackgroundWriterProcess())
-		CheckpointerShmem->num_backend_writes++;
-
 	/*
 	 * If the checkpointer isn't running or the request queue is full, the
 	 * backend will have to perform its own fsync request.  But before forcing
@@ -1104,8 +1089,6 @@ ForwardSyncRequest(const FileTag *ftag, SyncRequestType type)
 		 * Count the subset of writes where backends have to do their own
 		 * fsync
 		 */
-		if (!AmBackgroundWriterProcess())
-			CheckpointerShmem->num_backend_fsync++;
 		pgstat_increment_buffer_access_type(BA_Fsync, Buf_Shared);
 		LWLockRelease(CheckpointerCommLock);
 		return false;
@@ -1263,15 +1246,6 @@ AbsorbSyncRequests(void)

index 40f646b7c6..849a9be702 100644
@@ -5586,9 +5586,7 @@ pgstat_recv_archiver(PgStat_MsgArchiver *msg, int len)
 
 /* ----------
@@ -5604,9 +5602,6 @@ pgstat_recv_checkpointer(PgStat_MsgCheckpointer *msg, int len)
 
 static void
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 135b2d3925..f3c0fd96b3 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -2177,7 +2177,6 @@ BufferSync(int flags)
 			if (SyncOneBuffer(buf_id, false, &wb_context) & BUF_WRITTEN)
 			{
 				TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
-				PendingCheckpointerStats.m_buf_written_checkpoints++;
 				num_written++;
 			}
 		}
@@ -2286,9 +2285,6 @@ BgBufferSync(WritebackContext *wb_context)
 	 */
 	strategy_buf_id = StrategySyncStart(&strategy_passes, &recent_alloc);
 
@@ -2485,8 +2481,6 @@ BgBufferSync(WritebackContext *wb_context)
 			reusable_buffers++;
 	}
 
index d4e0ce4143..94a4500ead 100644
 
-Datum
 
-Datum
 pg_stat_get_buffers_accesses(PG_FUNCTION_ARGS)
index 54661e2b5f..02f624c18c 100644
 
 { oid => '8459', descr => 'statistics: counts of various types of accesses of buffers done by each backend type',
   proname => 'pg_stat_get_buffers_accesses', provolatile => 's', proisstrict => 'f',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index c97b6897cf..9b4217cbbb 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -489,9 +489,7 @@ typedef struct PgStat_MsgBgWriter
 {
 	PgStat_MsgHdr m_hdr;

 /* ----------
@@ -504,9 +502,6 @@ typedef struct PgStat_MsgCheckpointer
 
@@ -884,9 +879,7 @@ typedef struct PgStat_ArchiverStats
 
@@ -900,9 +893,6 @@ typedef struct PgStat_CheckpointerStats
 
 /*
index 9172b0fcd2..ac2f7cf61e 100644
 pg_stat_buffers| SELECT b.backend_type,
     b.buffer_type,
-- 
2.27.0

From 156f8c96ac9bab619c55a072624e7ee4dfa82c79 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 2 Sep 2021 11:33:59 -0400
Subject: [PATCH v8 1/2] Add system view tracking accesses to buffers

Add pg_stat_buffers, a system view which tracks the number of buffers of
a particular type (e.g. shared, local) allocated, written, fsync'd, and
extended by each backend type.

Some of these should always be zero. For example, a checkpointer backend
will not use a BufferAccessStrategy (currently), so buffer type
"strategy" for checkpointer will be 0 for all buffer access types
(alloc, write, fsync, and extend).

All backends increment a counter in their PgBackendStatus when
performing a buffer access. On exit, backends send these stats to the
stats collector to be persisted.

When stats are reset, the reset message includes the current values of
all the live backends' buffer access counters. When receiving this
message, the stats collector will 1) save these reset values in an array
of "resets" and 2) zero out the exited backends' saved buffer access
counters. This is required for accurate stats after a reset without
writing to other backends' PgBackendStatus.

When the pg_stat_buffers view is queried, sum live backends' stats with
saved stats from exited backends and subtract saved reset stats,
returning the total.

Each row of the view is for a particular backend type and a particular
buffer type (e.g. shared buffer accesses by checkpointer) and each
column in the view is the total number of buffers of each kind of buffer
access (e.g. written). So a cell in the view would be, for example, the
number of shared buffers written by checkpointer since the last stats
reset.

Note that this commit does not add code to increment buffer accesses for
all types of buffers. It includes all possible combinations in the stats
view but doesn't populate all of them.

TODO:
- TODOs in code (which are questions for the reviewer)
- Wrappers for smgr funcs to protect against regressions and cover other
  buffer types
- Consider helper func in pgstatfuncs.c to refactor out some of the
  redundant tuplestore creation code from pg_stat_get_progress_info,
  pg_stat_get_activity, etc
- Remove pg_stats test I added
- Additional polish for comments, check ordering of function
  definitions, datatypes, etc
- When finished, catalog bump
- pgindent
---
 doc/src/sgml/monitoring.sgml                | 116 +++++++++++++++-
 src/backend/catalog/system_views.sql        |  11 ++
 src/backend/postmaster/checkpointer.c       |   3 +
 src/backend/postmaster/pgstat.c             | 138 +++++++++++++++++++-
 src/backend/storage/buffer/bufmgr.c         |  37 +++++-
 src/backend/storage/buffer/freelist.c       |  22 +++-
 src/backend/utils/activity/backend_status.c |  53 +++++++-
 src/backend/utils/adt/pgstatfuncs.c         | 137 +++++++++++++++++++
 src/backend/utils/init/miscinit.c           |  25 ++++
 src/include/access/xlog.h                   |   1 +
 src/include/catalog/pg_proc.dat             |   9 ++
 src/include/miscadmin.h                     |  23 ++++
 src/include/pgstat.h                        |  41 +++++-
 src/include/storage/buf_internals.h         |   4 +-
 src/include/utils/backend_status.h          |  15 +++
 src/test/regress/expected/rules.out         |   8 ++
 src/test/regress/sql/stats.sql              |   6 +
 17 files changed, 631 insertions(+), 18 deletions(-)

index 2281ba120f..60627c692a 100644
 
+     <row>
+      <entry><structname>pg_stat_buffers</structname><indexterm><primary>pg_stat_buffers</primary></indexterm></entry>
+      <entry>A row for each buffer type for each backend type showing
+      statistics about backend buffer activity. See
+       <link linkend="monitoring-pg-stat-buffers-view">
+       <structname>pg_stat_buffers</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
@@ -3478,6 +3487,101 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
 
  </sect2>
 
+ <sect2 id="monitoring-pg-stat-buffers-view">
+  <title><structname>pg_stat_buffers</structname></title>
+
+  <indexterm>
+   <primary>pg_stat_buffers</primary>
+  </indexterm>
+
+  <para>
+   The <structname>pg_stat_buffers</structname> view has a row for each buffer
+   type for each backend type, containing global data for the cluster for that
+   backend and buffer type.
+  </para>
+
+  <table id="pg-stat-buffer-actions-view" xreflabel="pg_stat_buffers">
+   <title><structname>pg_stat_buffers</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).
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>buffer_type</structfield> <type>text</type>
+      </para>
+      <para>
+      Type of buffer accessed (e.g. shared).
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>alloc</structfield> <type>integer</type>
+      </para>
+      <para>
+       Number of buffers allocated.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>extend</structfield> <type>integer</type>
+      </para>
+      <para>
+       Number of buffers extended.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>fsync</structfield> <type>integer</type>
+      </para>
+      <para>
+       Number of buffers fsynced.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>write</structfield> <type>integer</type>
+      </para>
+      <para>
+       Number of buffers written.
+      </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-wal-view">
    <title><structname>pg_stat_wal</structname></title>
 
@@ -5074,12 +5178,14 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
        </para>
        <para>
         Resets some cluster-wide statistics counters to zero, depending on the
-        argument.  The argument can be <literal>bgwriter</literal> to reset
-        all the counters shown in
-        the <structname>pg_stat_bgwriter</structname>
+        argument.  The argument can be <literal>bgwriter</literal> to reset all
+        the counters shown in the <structname>pg_stat_bgwriter</structname>
         view, <literal>archiver</literal> to reset all the counters shown in
-        the <structname>pg_stat_archiver</structname> view or <literal>wal</literal>
-        to reset all the counters shown in the <structname>pg_stat_wal</structname> view.
+        the <structname>pg_stat_archiver</structname> view,
+        <literal>wal</literal> to reset all the counters shown in the
+        <structname>pg_stat_wal</structname> view, or
+        <literal>buffers</literal> to reset all the counters shown in the
+        <structname>pg_stat_buffers</structname> view.
        </para>
        <para>
         This function is restricted to superusers by default, but other users
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 55f6e3711d..30280d520b 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1072,6 +1072,17 @@ 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_buffers AS
+SELECT
+       b.backend_type,
+       b.buffer_type,
+       b.alloc,
+       b.extend,
+       b.fsync,
+       b.write,
+       b.stats_reset
+FROM pg_stat_get_buffers_accesses() b;
+
 CREATE VIEW pg_stat_wal AS
     SELECT
         w.wal_records,
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index be7366379d..fe6bd15506 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -577,6 +577,8 @@ HandleCheckpointerInterrupts(void)
 		ShutdownXLOG(0, 0);
 		pgstat_send_checkpointer();
 		pgstat_send_wal(true);
+		// TODO: is this needed
+		pgstat_report_buffers();
 
 		/* Normal exit from the checkpointer is here */
 		proc_exit(0);			/* done */
@@ -1104,6 +1106,7 @@ ForwardSyncRequest(const FileTag *ftag, SyncRequestType type)
 		 */
 		if (!AmBackgroundWriterProcess())
 			CheckpointerShmem->num_backend_fsync++;
+		pgstat_increment_buffer_access_type(BA_Fsync, Buf_Shared);
 		LWLockRelease(CheckpointerCommLock);
 		return false;
 	}
diff --git a/src/backend/postmaster/pgstat.c b/src/backend/postmaster/pgstat.c
index b7d0fbaefd..40f646b7c6 100644
--- a/src/backend/postmaster/pgstat.c
+++ b/src/backend/postmaster/pgstat.c
@@ -63,6 +63,7 @@
 #include "storage/pg_shmem.h"
 #include "storage/proc.h"
 #include "storage/procsignal.h"
+#include "utils/backend_status.h"
 #include "utils/builtins.h"
 #include "utils/guc.h"
 #include "utils/memutils.h"
@@ -124,9 +125,12 @@ char	   *pgstat_stat_filename = NULL;
 char	   *pgstat_stat_tmpname = NULL;
 
 /*
@@ -362,6 +366,7 @@ static void pgstat_recv_analyze(PgStat_MsgAnalyze *msg, int len);
+static void pgstat_recv_buffer_type_accesses(PgStat_MsgBufferTypeAccesses *msg, int len);
@@ -974,6 +979,7 @@ pgstat_report_stat(bool disconnect)
 
+
 
@@ -1452,6 +1458,42 @@ pgstat_reset_shared_counters(const char *target)
 		msg.m_resettarget = RESET_ARCHIVER;
 	else if (strcmp(target, "bgwriter") == 0)
 		msg.m_resettarget = RESET_BGWRITER;
+	else if (strcmp(target, "buffers") == 0) {
+		int i, buffer_type;
+		volatile	PgBackendStatus *beentry;
+
+		memset(msg.resets, 0, sizeof(PgStat_MsgBufferTypeAccesses) * BACKEND_NUM_TYPES);
+
+		msg.m_resettarget = RESET_BUFFERS;
+
+		beentry = pgstat_access_backend_status_array();
+		/*
+		 * Loop through live backends and capture reset values
+		 */
+		for (i = 0; i < MaxBackends + NUM_AUXPROCTYPES; i++)
+		{
+			PgBufferAccesses *live_accesses;
+			PgStatBufferAccesses *reset_accesses;
+			beentry++;
+			/* Don't count dead backends. They should already be counted */
+			if (beentry->st_procpid == 0)
+				continue;
+
+			live_accesses = (PgBufferAccesses *) beentry->buffer_access_stats;
+			reset_accesses = msg.resets[beentry->st_backendType].buffer_type_accesses;
+
+			for (buffer_type = 0; buffer_type < BUFFER_NUM_TYPES; buffer_type++)
+			{
+				uint64 live_allocs = pg_atomic_read_u64(&live_accesses->allocs);
+				reset_accesses->allocs = live_allocs;
+				reset_accesses->extends = pg_atomic_read_u64(&live_accesses->extends);
+				reset_accesses->fsyncs = pg_atomic_read_u64(&live_accesses->fsyncs);
+				reset_accesses->writes = pg_atomic_read_u64(&live_accesses->writes);
+				reset_accesses++;
+				live_accesses++;
+			}
+		}
+	}
 	else if (strcmp(target, "wal") == 0)
 		msg.m_resettarget = RESET_WAL;
 	else
@@ -2998,6 +3040,13 @@ static void
 pgstat_shutdown_hook(int code, Datum arg)
 {
 	Assert(!pgstat_is_shutdown);
+	/*
+	 * Only need to send stats on buffer accesses when a process exits, as
+	 * pg_stat_get_buffers() will read from live backends' PgBackendStatus and
+	 * then sum this with totals from exited backends persisted by the stats
+	 * collector.
+	 */
+	pgstat_report_buffers();
 
 	/*
 	 * If we got as far as discovering our own database ID, we can report what
@@ -3180,6 +3229,43 @@ pgstat_send_checkpointer(void)
 	MemSet(&PendingCheckpointerStats, 0, sizeof(PendingCheckpointerStats));
 }
 
+/*
+ * Called for a single backend at the time of death to send its I/O stats to
+ * the stats collector so that they may be persisted.
+ */
+void
+pgstat_report_buffers(void)
+{
+	PgStat_MsgBufferTypeAccesses msg;
+	PgBufferAccesses *src_accesses;
+	PgStatBufferAccesses *dest_accesses;
+	int buffer_type;
+
+	volatile	PgBackendStatus *beentry = MyBEEntry;
+
+	if (!beentry)
+		return;
+
+	MemSet(&msg, 0, sizeof(msg));
+	msg.backend_type = beentry->st_backendType;
+
+	src_accesses = (PgBufferAccesses *) &beentry->buffer_access_stats;
+	dest_accesses = msg.buffer_type_accesses;
+
+	for (buffer_type = 0; buffer_type < BUFFER_NUM_TYPES; buffer_type++)
+	{
+		dest_accesses->allocs += pg_atomic_read_u64(&src_accesses->allocs);
+		dest_accesses->extends += pg_atomic_read_u64(&src_accesses->extends);
+		dest_accesses->fsyncs += pg_atomic_read_u64(&src_accesses->fsyncs);
+		dest_accesses->writes += pg_atomic_read_u64(&src_accesses->writes);
+		dest_accesses++;
+		src_accesses++;
+	}
+
+	pgstat_setheader(&msg.m_hdr, PGSTAT_MTYPE_BUFFER_ACTIONS);
+	pgstat_send(&msg, sizeof(msg));
+}
+
 /* ----------
  * pgstat_send_wal() -
  *
@@ -3522,6 +3608,10 @@ PgstatCollectorMain(int argc, char *argv[])
 					pgstat_recv_checkpointer(&msg.msg_checkpointer, len);
 					break;
 
+				case PGSTAT_MTYPE_BUFFER_ACTIONS:
+					pgstat_recv_buffer_type_accesses(&msg.msg_buffer_accesses, len);
+					break;
+
 				case PGSTAT_MTYPE_WAL:
 					pgstat_recv_wal(&msg.msg_wal, len);
 					break;
@@ -5222,9 +5312,16 @@ pgstat_recv_resetsharedcounter(PgStat_MsgResetsharedcounter *msg, int len)
 	if (msg->m_resettarget == RESET_BGWRITER)
 	{
 		/* Reset the global, bgwriter and checkpointer statistics for the cluster. */
-		memset(&globalStats, 0, sizeof(globalStats));
+		memset(&globalStats.checkpointer, 0, sizeof(globalStats.checkpointer));
+		memset(&globalStats.bgwriter, 0, sizeof(globalStats.bgwriter));
 		globalStats.bgwriter.stat_reset_timestamp = GetCurrentTimestamp();
 	}
+	else if (msg->m_resettarget == RESET_BUFFERS)
+	{
+		memset(&globalStats.buffers.accesses, 0, sizeof(globalStats.buffers.accesses));
+		memcpy(globalStats.buffers.resets, msg->resets, sizeof(msg->resets));
+		globalStats.buffers.stat_reset_timestamp = GetCurrentTimestamp();
+	}
 	else if (msg->m_resettarget == RESET_ARCHIVER)
 	{
 		/* Reset the archiver statistics for the cluster. */
@@ -5512,6 +5609,39 @@ pgstat_recv_checkpointer(PgStat_MsgCheckpointer *msg, int len)
 	globalStats.checkpointer.buf_fsync_backend += msg->m_buf_fsync_backend;
 }
 
+static void
+pgstat_recv_buffer_type_accesses(PgStat_MsgBufferTypeAccesses *msg, int len)
+{
+	int buffer_type;
+	PgStatBufferAccesses *src_buffer_accesses = msg->buffer_type_accesses;
+	PgStatBufferAccesses *dest_buffer_accesses = globalStats.buffers.accesses[msg->backend_type].buffer_type_accesses;
+
+	/*
+	 * No users will likely need PgStat_MsgBufferTypeAccesses->backend_type
+	 * when accessing it from globalStats since its place in the
+	 * globalStats.buffers.accesses array indicates backend_type. However,
+	 * leaving it undefined seemed like an invitation for unnecessary future
+	 * bugs.
+	 */
+	globalStats.buffers.accesses[msg->backend_type].backend_type = msg->backend_type;
+
+	for (buffer_type = 0; buffer_type < BUFFER_NUM_TYPES; buffer_type++)
+	{
+		PgStatBufferAccesses *src = &src_buffer_accesses[buffer_type];
+		PgStatBufferAccesses *dest = &dest_buffer_accesses[buffer_type];
+		dest->allocs += src->allocs;
+		dest->extends += src->extends;
+		dest->fsyncs += src->fsyncs;
+		dest->writes += src->writes;
+	}
+}
+
+PgStat_BackendAccesses *
+pgstat_get_global_buffers_stats(void)
+{
+	return &globalStats.buffers;
+}
+
 /* ----------
  * pgstat_recv_wal() -
  *
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index e88e4e918b..135b2d3925 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -972,6 +972,7 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 
 	if (isExtend)
 	{
+		pgstat_increment_buffer_access_type(BA_Extend, Buf_Shared);
 		/* new buffers are zero-filled */
 		MemSet((char *) bufBlock, 0, BLCKSZ);
 		/* don't set checksum for all-zero page */
@@ -1172,6 +1173,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	/* Loop here in case we have to try another victim buffer */
 	for (;;)
 	{
+		bool from_ring;
 		/*
 		 * Ensure, while the spinlock's not yet held, that there's a free
 		 * refcount entry.
@@ -1182,7 +1184,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 		 * Select a victim buffer.  The buffer is returned with its header
 		 * spinlock still held!
 		 */
-		buf = StrategyGetBuffer(strategy, &buf_state);
+		buf = StrategyGetBuffer(strategy, &buf_state, &from_ring);

@@ -1236,7 +1238,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 					UnlockBufHdr(buf, buf_state);
 
@@ -1245,6 +1247,23 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 					}
 				}
 
+				/*
+				 * When a strategy is in use, if the dirty buffer was selected
+				 * from the strategy ring and we did not bother checking the
+				 * freelist or doing a clock sweep to look for a clean shared
+				 * buffer to use, the write will be counted as a strategy
+				 * write. However, if the dirty buffer was obtained from the
+				 * freelist or a clock sweep, it is counted as a regular write.
+				 * When a strategy is not in use, at this point, the write can
+				 * only be a "regular" write of a dirty buffer.
+				 */
+
+				if (from_ring)
+					pgstat_increment_buffer_access_type(BA_Write, Buf_Strategy);
+				else
+					pgstat_increment_buffer_access_type(BA_Write, Buf_Shared);
+
+
 				/* OK, do the I/O */
 				TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum,
 														  smgr->smgr_rnode.node.spcNode,
@@ -2143,6 +2162,18 @@ BufferSync(int flags)
 		 */
 		if (pg_atomic_read_u32(&bufHdr->state) & BM_CHECKPOINT_NEEDED)
 		{
+			// TODO: could remove BUF_WRITTEN and instead check buffers written
+			// for this backend before starting BufferSync() and after
+			// finishing. It seems like BUF_WRITTEN it is only used for
+			// CheckpointStats.ckpt_bufs_written -- which seems redundant with
+			// the new PgBufferAccesses -- and for
+			// TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN - which can perhaps be done
+			// another way?
+			// BgBufferSync() has a similar local variable, num_written, which
+			// it uses in a similar way and also seems like it could be
+			// refactored to use PgBufferAccesses.
+			// Then, SyncOneBuffer() could only return a bool representing
+			// whether or not the buffer is reusable.
 			if (SyncOneBuffer(buf_id, false, &wb_context) & BUF_WRITTEN)
 			{
 				TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
@@ -2552,6 +2583,8 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
 	 * Pin it, share-lock it, write it.  (FlushBuffer will do nothing if the
 	 * buffer is clean by the time we've locked it.)
 	 */
+
+	pgstat_increment_buffer_access_type(BA_Write, Buf_Shared);
 	PinBuffer_Locked(bufHdr);
 	LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
 
index 6be80476db..866cdd3911 100644

+	pgstat_increment_buffer_access_type(BA_Alloc, Buf_Shared);
 
 	/*
 
+	/*
+	 * Since we will not be writing out a dirty buffer from the ring, set
+	 * from_ring to false so that the caller does not count this write as a
+	 * "strategy write" and can do proper bookkeeping for
+	 * pg_stat_buffers.
+	 */
+	*from_ring = false;
+
+
 	return true;
 }
diff --git a/src/backend/utils/activity/backend_status.c b/src/backend/utils/activity/backend_status.c
index 7229598822..50e7081a16 100644
--- a/src/backend/utils/activity/backend_status.c
+++ b/src/backend/utils/activity/backend_status.c
@@ -279,7 +279,7 @@ pgstat_beinit(void)
  * pgstat_bestart() -
  *
  *	Initialize this backend's entry in the PgBackendStatus array.
- *	Called from InitPostgres.
+ *	Called from InitPostgres and AuxiliaryProcessMain
  *
  *	Apart from auxiliary processes, MyBackendId, MyDatabaseId,
  *	session userid, and application_name must be set for a
@@ -293,6 +293,7 @@ pgstat_bestart(void)
 {
 	volatile PgBackendStatus *vbeentry = MyBEEntry;
 	PgBackendStatus lbeentry;
+	int buffer_type;
 #ifdef USE_SSL
 	PgBackendSSLStatus lsslstatus;
 #endif
@@ -399,6 +400,17 @@ pgstat_bestart(void)
 	lbeentry.st_progress_command = PROGRESS_COMMAND_INVALID;
 	lbeentry.st_progress_command_target = InvalidOid;
 	lbeentry.st_query_id = UINT64CONST(0);
+	for (buffer_type = 0; buffer_type < BUFFER_NUM_TYPES; buffer_type++)
+	{
+		// TODO: is it okay stylistically that I do this loop differently
+		// elsewhere (e.g. increment the accesses variable vs using buffer_type
+		// as an index into the array)?
+		PgBufferAccesses *accesses = &lbeentry.buffer_access_stats[buffer_type];
+		pg_atomic_init_u64(&accesses->allocs, 0);
+		pg_atomic_init_u64(&accesses->extends, 0);
+		pg_atomic_init_u64(&accesses->fsyncs, 0);
+		pg_atomic_init_u64(&accesses->writes, 0);
+	}
 
 	/*
 	 * we don't zero st_progress_param here to save cycles; nobody should
@@ -1045,6 +1057,45 @@ pgstat_get_my_query_id(void)
 	return MyBEEntry->st_query_id;
 }
 
+PgBackendStatus *
+pgstat_access_backend_status_array(void)
+{
+	return BackendStatusArray;
+}
+
+// TODO: is there a way to inline this in a header file for performance reasons?
+// not sure with the static function calls in it
+void
+pgstat_increment_buffer_access_type(BufferAccessType ba_type, BufferType buf_type)
+{
+	PgBufferAccesses *accesses;
+	PgBackendStatus *beentry   = MyBEEntry;
+
+	// TODO: do I need to check pgstat_track_activities?
+	if (!beentry || !pgstat_track_activities)
+		return;
+
+	accesses = &beentry->buffer_access_stats[buf_type];
+	switch (ba_type)
+	{
+		case BA_Alloc:
+			pg_atomic_write_u64(&accesses->allocs,
+					pg_atomic_read_u64(&accesses->allocs) + 1);
+			break;
+		case BA_Extend:
+			pg_atomic_write_u64(&accesses->extends,
+					pg_atomic_read_u64(&accesses->extends) + 1);
+			break;
+		case BA_Fsync:
+			pg_atomic_write_u64(&accesses->fsyncs,
+					pg_atomic_read_u64(&accesses->fsyncs) + 1);
+			break;
+		case BA_Write:
+			pg_atomic_write_u64(&accesses->writes,
+					pg_atomic_read_u64(&accesses->writes) + 1);
+			break;
+	}
+}
 
 /* ----------
  * pgstat_fetch_stat_beentry() -
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index ff5aedc99c..d4e0ce4143 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1796,6 +1796,143 @@ pg_stat_get_buf_alloc(PG_FUNCTION_ARGS)
 	PG_RETURN_INT64(pgstat_fetch_stat_bgwriter()->buf_alloc);
 }
 
+Datum
+pg_stat_get_buffers_accesses(PG_FUNCTION_ARGS)
+{
+#define NROWS ((BACKEND_NUM_TYPES - 1) * BUFFER_NUM_TYPES)
+	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+	TupleDesc	tupdesc;
+	Tuplestorestate *tupstore;
+	MemoryContext per_query_ctx;
+	MemoryContext oldcontext;
+	PgStat_BackendAccesses *backend_accesses;
+	int buffer_type;
+	int backend_type;
+	Datum reset_time;
+	int i;
+	// TODO: does this need to be volatile
+	volatile PgBackendStatus *beentry;
+
+	enum {
+		COLUMN_BACKEND_TYPE,
+		COLUMN_BUFFER_TYPE,
+		COLUMN_ALLOCS,
+		COLUMN_EXTENDS,
+		COLUMN_FSYNCS,
+		COLUMN_WRITES,
+		COLUMN_RESET_TIME,
+		COLUMN_LENGTH,
+	};
+
+	Datum all_values[NROWS][COLUMN_LENGTH];
+	bool all_nulls[NROWS][COLUMN_LENGTH];
+
+	/* check to see if caller supports us returning a tuplestore */
+	if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("set-valued function called in context that cannot accept a set")));
+	if (!(rsinfo->allowedModes & SFRM_Materialize))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("materialize mode required, but it is not allowed in this context")));
+
+	/* Build a tuple descriptor for our result type */
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	per_query_ctx = rsinfo->econtext->ecxt_per_query_memory;
+	oldcontext = MemoryContextSwitchTo(per_query_ctx);
+
+	tupstore = tuplestore_begin_heap(true, false, work_mem);
+	rsinfo->returnMode = SFRM_Materialize;
+	rsinfo->setResult = tupstore;
+	rsinfo->setDesc = tupdesc;
+
+	MemoryContextSwitchTo(oldcontext);
+
+	memset(all_values, 0, sizeof(all_values));
+	memset(all_nulls, 0, sizeof(all_nulls));
+
+	/*
+	 * Loop through all live backends and count their buffer accesses for each
+	 * buffer type
+	 */
+	beentry = pgstat_access_backend_status_array();
+	for (i = 0; i < MaxBackends + NUM_AUXPROCTYPES; i++)
+	{
+		PgBufferAccesses *buffer_accesses;
+		beentry++;
+		/* Don't count dead backends. They should already be counted */
+		if (beentry->st_procpid == 0)
+			continue;
+
+		buffer_accesses = (PgBufferAccesses *) beentry->buffer_access_stats;
+
+		for (buffer_type = 0; buffer_type < BUFFER_NUM_TYPES; buffer_type++)
+		{
+			int rownum = (beentry->st_backendType - 1) * BUFFER_NUM_TYPES + buffer_type;
+			Datum *values = all_values[rownum];
+
+			/*
+			 * COLUMN_RESET_TIME, COLUMN_BACKEND_TYPE, and COLUMN_BUFFER_TYPE
+			 * will all be set when looping through exited backends array
+			 */
+			values[COLUMN_ALLOCS] += pg_atomic_read_u64(&buffer_accesses->allocs);
+			values[COLUMN_EXTENDS] += pg_atomic_read_u64(&buffer_accesses->extends);
+			values[COLUMN_FSYNCS] += pg_atomic_read_u64(&buffer_accesses->fsyncs);
+			values[COLUMN_WRITES] += pg_atomic_read_u64(&buffer_accesses->writes);
+			buffer_accesses++;
+		}
+	}
+
+	/* Add stats from all exited backends */
+	pgstat_fetch_global();
+	backend_accesses = pgstat_get_global_buffers_stats();
+
+	reset_time = TimestampTzGetDatum(backend_accesses->stat_reset_timestamp);
+
+	/* 0 is not a valid BackendType */
+	for (backend_type = 1; backend_type < BACKEND_NUM_TYPES; backend_type++)
+	{
+		PgStatBufferAccesses *buffer_accesses = backend_accesses->accesses[backend_type].buffer_type_accesses;
+		PgStatBufferAccesses *resets = backend_accesses->resets[backend_type].buffer_type_accesses;
+
+		Datum backend_type_desc = CStringGetTextDatum(GetBackendTypeDesc(backend_type));
+
+		for (buffer_type = 0; buffer_type < BUFFER_NUM_TYPES; buffer_type++)
+		{
+			/*
+			 * Subtract 1 from backend_type to avoid having rows for B_INVALID
+			 * BackendType
+			 */
+			Datum *values = all_values[(backend_type - 1) * BUFFER_NUM_TYPES + buffer_type];
+
+			values[COLUMN_BACKEND_TYPE] = backend_type_desc;
+			values[COLUMN_BUFFER_TYPE] = CStringGetTextDatum(GetBufferTypeDesc(buffer_type));
+			values[COLUMN_ALLOCS] = values[COLUMN_ALLOCS] + buffer_accesses->allocs - resets->allocs;
+			values[COLUMN_EXTENDS] = values[COLUMN_EXTENDS] + buffer_accesses->extends - resets->extends;
+			values[COLUMN_FSYNCS] = values[COLUMN_FSYNCS] + buffer_accesses->fsyncs - resets->fsyncs;
+			values[COLUMN_WRITES] = values[COLUMN_WRITES] + buffer_accesses->writes - resets->writes;
+			values[COLUMN_RESET_TIME] = reset_time;
+			buffer_accesses++;
+			resets++;
+		}
+	}
+
+	for (i = 0; i < NROWS; i++)
+	{
+		Datum *values = all_values[i];
+		bool *nulls = all_nulls[i];
+		tuplestore_putvalues(tupstore, tupdesc, values, nulls);
+	}
+
+	/* clean up and return the tuplestore */
+	tuplestore_donestoring(tupstore);
+
+	return (Datum) 0;
+}
+
 /*
  * Returns statistics of WAL activity
  */
diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 88801374b5..50b50d00ce 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -299,6 +299,31 @@ GetBackendTypeDesc(BackendType backendType)
 	return backendDesc;
 }
 
+const char *
+GetBufferTypeDesc(BufferType bufferType)
+{
+	const char *bufferDesc = "unknown buffer type";
+
+	switch (bufferType)
+	{
+		case Buf_Direct:
+			bufferDesc = "direct";
+			break;
+		case Buf_Local:
+			bufferDesc = "local";
+			break;
+		case Buf_Shared:
+			bufferDesc = "shared";
+			break;
+		case Buf_Strategy:
+			bufferDesc = "strategy";
+			break;
+	}
+
+	return bufferDesc;
+}
+
+
 /* ----------------------------------------------------------------
  *				database path / name support stuff
  * ----------------------------------------------------------------
diff --git a/src/include/access/xlog.h b/src/include/access/xlog.h
index 5e2c94a05f..58575889de 100644
--- a/src/include/access/xlog.h
+++ b/src/include/access/xlog.h
@@ -224,6 +224,7 @@ typedef struct CheckpointStatsData
 	TimestampTz ckpt_sync_end_t;	/* end of fsyncs */
 	TimestampTz ckpt_end_t;		/* end of checkpoint */
 
+	// TODO: is this now redundant with checkpointer's backend writes buffers counter?
 	int			ckpt_bufs_written;	/* # of buffers written */
 
 	int			ckpt_segs_added;	/* # of new xlog segments created */
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index d068d6532e..54661e2b5f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5642,6 +5642,15 @@
   proname => 'pg_stat_get_buf_alloc', provolatile => 's', proparallel => 'r',
   prorettype => 'int8', proargtypes => '', prosrc => 'pg_stat_get_buf_alloc' },
 
+{ oid => '8459', descr => 'statistics: counts of various types of accesses of buffers done by each backend type',
+  proname => 'pg_stat_get_buffers_accesses', provolatile => 's', proisstrict => 'f',
+  prorows => '52', proretset => 't',
+  proparallel => 'r', prorettype => 'record', proargtypes => '',
+  proallargtypes => '{text,text,int8,int8,int8,int8,timestamptz}',
+  proargmodes => '{o,o,o,o,o,o,o}',
+  proargnames => '{backend_type,buffer_type,alloc,extend,fsync,write,stats_reset}',
+  prosrc => 'pg_stat_get_buffers_accesses' },
+
 { 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/include/miscadmin.h b/src/include/miscadmin.h
index 90a3016065..266d32835c 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -338,9 +338,32 @@ typedef enum BackendType
 	B_LOGGER,
 } BackendType;
 
+#define BACKEND_NUM_TYPES (B_LOGGER + 1)
+
+typedef enum BufferAccessType
+{
+	BA_Alloc,
+	BA_Extend,
+	BA_Fsync,
+	BA_Write,
+}	BufferAccessType;
+
+#define BUFFER_ACCESS_NUM_TYPES (BA_Write + 1)
+
+typedef enum BufferType
+{
+	Buf_Direct,
+	Buf_Local,
+	Buf_Shared,
+	Buf_Strategy,
+} BufferType;
+
+#define BUFFER_NUM_TYPES (Buf_Strategy + 1)
+
 extern BackendType MyBackendType;
 
 extern const char *GetBackendTypeDesc(BackendType backendType);
+extern const char * GetBufferTypeDesc(BufferType bufferType);
 
 extern void SetDatabasePath(const char *path);
 extern void checkDataDir(void);
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index bcd3588ea2..c97b6897cf 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -72,6 +72,7 @@ typedef enum StatMsgType
 	PGSTAT_MTYPE_ARCHIVER,
 	PGSTAT_MTYPE_BGWRITER,
 	PGSTAT_MTYPE_CHECKPOINTER,
+	PGSTAT_MTYPE_BUFFER_ACTIONS,
 	PGSTAT_MTYPE_WAL,
 	PGSTAT_MTYPE_SLRU,
 	PGSTAT_MTYPE_FUNCSTAT,
@@ -138,6 +139,7 @@ typedef enum PgStat_Shared_Reset_Target
 {
 	RESET_ARCHIVER,
 	RESET_BGWRITER,
+	RESET_BUFFERS,
 	RESET_WAL
 } PgStat_Shared_Reset_Target;
 
@@ -224,7 +226,9 @@ typedef struct PgStat_MsgHdr
  * platforms, but we're being conservative here.)
  * ----------
  */
-#define PGSTAT_MAX_MSG_SIZE 1000
+// TODO: how sketchy is this? What can I do instead? The array of counters for
+// reset message is 2kB, I think
+#define PGSTAT_MAX_MSG_SIZE 3000
 #define PGSTAT_MSG_PAYLOAD	(PGSTAT_MAX_MSG_SIZE - sizeof(PgStat_MsgHdr))

@@ -342,6 +346,30 @@ typedef struct PgStat_MsgResetcounter
 	Oid			m_databaseid;
 } PgStat_MsgResetcounter;
 
+// TODO: should these be PgStat_Counters? kind of want them unsigned though...
+typedef struct PgStatBufferAccesses
+{
+	uint64 allocs;
+	uint64 extends;
+	uint64 fsyncs;
+	uint64 writes;
+} PgStatBufferAccesses;
+
+typedef struct PgStat_MsgBufferTypeAccesses
+{
+	PgStat_MsgHdr m_hdr;
+
+	BackendType backend_type;
+	PgStatBufferAccesses buffer_type_accesses[BUFFER_NUM_TYPES];
+} PgStat_MsgBufferTypeAccesses;
+
+typedef struct PgStat_BackendAccesses
+{
+	TimestampTz stat_reset_timestamp;
+	PgStat_MsgBufferTypeAccesses accesses[BACKEND_NUM_TYPES];
+	PgStat_MsgBufferTypeAccesses resets[BACKEND_NUM_TYPES];
+} PgStat_BackendAccesses;
+
 /* ----------
  * PgStat_MsgResetsharedcounter Sent by the backend to tell the collector
  *								to reset a shared counter
@@ -351,6 +379,10 @@ typedef struct PgStat_MsgResetsharedcounter
 {
 	PgStat_MsgHdr m_hdr;
 	PgStat_Shared_Reset_Target m_resettarget;
+	// TODO: Should I make a new type of reset message that is only used for
+	// resetting pg_stat_buffers stats and includes this resets array?
+	// Is it okay that we are mixing query and command messages?
+	PgStat_MsgBufferTypeAccesses resets[BACKEND_NUM_TYPES];
 } PgStat_MsgResetsharedcounter;
 
 /* ----------
@@ -479,6 +511,8 @@ typedef struct PgStat_MsgCheckpointer
 	PgStat_Counter m_checkpoint_sync_time;
 } PgStat_MsgCheckpointer;
 
+
+
 /* ----------
  * PgStat_MsgWal			Sent by backends and background processes to update WAL statistics.
  * ----------
@@ -703,6 +737,7 @@ typedef union PgStat_Msg
 	PgStat_MsgArchiver msg_archiver;
 	PgStat_MsgBgWriter msg_bgwriter;
 	PgStat_MsgCheckpointer msg_checkpointer;
+	PgStat_MsgBufferTypeAccesses msg_buffer_accesses;
 	PgStat_MsgWal msg_wal;
 	PgStat_MsgSLRU msg_slru;
 	PgStat_MsgFuncstat msg_funcstat;
@@ -879,6 +914,7 @@ typedef struct PgStat_GlobalStats
 
 	PgStat_CheckpointerStats checkpointer;
 	PgStat_BgWriterStats bgwriter;
+	PgStat_BackendAccesses buffers;
 } PgStat_GlobalStats;
 
 /*
@@ -946,7 +982,6 @@ typedef struct PgStat_FunctionCallUsage
 	instr_time	f_start;
 } PgStat_FunctionCallUsage;
 
-
 /* ----------
  * GUC parameters
  * ----------
@@ -1119,6 +1154,8 @@ extern void pgstat_twophase_postabort(TransactionId xid, uint16 info,
 extern void pgstat_send_archiver(const char *xlog, bool failed);
 extern void pgstat_send_bgwriter(void);
 extern void pgstat_send_checkpointer(void);
+extern void pgstat_report_buffers(void);
+extern PgStat_BackendAccesses * pgstat_get_global_buffers_stats(void);
 extern void pgstat_send_wal(bool force);
 
 /* ----------
 
 /* freelist.c */
 
index 8042b817df..30b7103831 100644

@@ -79,6 +80,16 @@ typedef struct PgBackendGSSStatus
 
 } PgBackendGSSStatus;
 
+// TODO: I'd like to move this elsewhere since it doesn't seem related to backend_status, but, I'm not sure where
+// I tried miscadmin.h but couldn't figure out what to include to get the Postgres atomic types and link correctly
+// Should it be elsewhere?
+typedef struct PgBufferAccesses
+{
+	pg_atomic_uint64 allocs;
+	pg_atomic_uint64 extends;
+	pg_atomic_uint64 fsyncs;
+	pg_atomic_uint64 writes;
+} PgBufferAccesses;
 
 /* ----------
  * PgBackendStatus
@@ -168,6 +179,7 @@ typedef struct PgBackendStatus
 
 	/* query identifier, optionally computed using post_parse_analyze_hook */
 	uint64		st_query_id;
+	PgBufferAccesses buffer_access_stats[BUFFER_NUM_TYPES];
 } PgBackendStatus;

@@ -306,6 +318,9 @@ extern const char *pgstat_get_crashed_backend_activity(int pid, char *buffer,
 													   int buflen);
 extern uint64 pgstat_get_my_query_id(void);
 
+extern PgBackendStatus *pgstat_access_backend_status_array(void);
+extern void pgstat_increment_buffer_access_type(BufferAccessType ba_type, BufferType buf_type);
+
 
 /* ----------
  * Support functions for the SQL-callable functions to
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2fa00a3c29..9172b0fcd2 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1828,6 +1828,14 @@ pg_stat_bgwriter| SELECT pg_stat_get_bgwriter_timed_checkpoints() AS checkpoints
     pg_stat_get_buf_fsync_backend() AS buffers_backend_fsync,
     pg_stat_get_buf_alloc() AS buffers_alloc,
     pg_stat_get_bgwriter_stat_reset_time() AS stats_reset;
+pg_stat_buffers| SELECT b.backend_type,
+    b.buffer_type,
+    b.alloc,
+    b.extend,
+    b.fsync,
+    b.write,
+    b.stats_reset
+   FROM pg_stat_get_buffers_accesses() b(backend_type, buffer_type, alloc, extend, fsync, write, stats_reset);
 pg_stat_database| SELECT d.oid AS datid,
     d.datname,
         CASE
diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql
index feaaee6326..c132717e08 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -176,4 +176,10 @@ FROM prevstats AS pr;
 
 DROP TABLE trunc_stats_test, trunc_stats_test1, trunc_stats_test2, trunc_stats_test3, trunc_stats_test4;
 DROP TABLE prevstats;
+SELECT * FROM pg_stat_buffers;
+SELECT pg_stat_reset_shared('buffers');
+-- TODO: Is it okay that it takes a second for the message to be received, so I
+-- can't query the view right away and see it reset?
+SELECT pg_sleep(2);
+SELECT * FROM pg_stat_buffers;
 -- End of Stats Test
-- 
2.27.0

---

From: Melanie Plageman <[email-masked]>
Date: Fri, 24 Sep 2021 17:58:48 -0400

> pass for comment clarity, alphabetization, etc on this version.

I have addressed almost all of the issues mentioned above in v9.
The only remaining TODOs are described in the commit message.
most critical one is that the reset message doesn't work.
From 9747484ad0b6f1fe97f98cfb681fa117982dfb2f Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 2 Sep 2021 11:47:41 -0400
Subject: [PATCH v9 3/3] Remove superfluous bgwriter stats

TODO:

-     <row>
 
-     <row>

index c0c4122fd5..829f52cc8f 100644

 

index 903d4df911..b8c17f8e7f 100644
@@ -5569,9 +5569,7 @@ pgstat_recv_archiver(PgStat_MsgArchiver *msg, int len)
 
 /* ----------
@@ -5587,9 +5585,6 @@ pgstat_recv_checkpointer(PgStat_MsgCheckpointer *msg, int len)
 
 static void
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 9832d35b90..997fff9f3f 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -2165,7 +2165,6 @@ BufferSync(int flags)
 			if (SyncOneBuffer(buf_id, false, &wb_context) & BUF_WRITTEN)
 			{
 				TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
-				PendingCheckpointerStats.m_buf_written_checkpoints++;
 				num_written++;
 			}
 		}
@@ -2274,9 +2273,6 @@ BgBufferSync(WritebackContext *wb_context)
 	 */
 	strategy_buf_id = StrategySyncStart(&strategy_passes, &recent_alloc);
 
@@ -2473,8 +2469,6 @@ BgBufferSync(WritebackContext *wb_context)
 			reusable_buffers++;
 	}
 
index 477caf2536..998625d490 100644
 
-Datum
 
-Datum
 
 { oid => '8459', descr => 'statistics: counts of various types of accesses of buffers done by each backend type',
   proname => 'pg_stat_get_buffers_accesses', provolatile => 's', proisstrict => 'f',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index b73265ab13..9a3ffc9ee4 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -486,9 +486,7 @@ typedef struct PgStat_MsgBgWriter
 {
 	PgStat_MsgHdr m_hdr;

 /* ----------
@@ -501,9 +499,6 @@ typedef struct PgStat_MsgCheckpointer
 
@@ -879,9 +874,7 @@ typedef struct PgStat_ArchiverStats
 
@@ -895,9 +888,6 @@ typedef struct PgStat_CheckpointerStats
 
 /*

From b0a24e0cd0115f5bfb15a69693ce205a9dca841e Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Fri, 24 Sep 2021 17:39:12 -0400
Subject: [PATCH v9 1/3] Allow bootstrap process to beinit

---
 src/backend/utils/init/postinit.c | 3 +--
 1 file changed, 1 insertion(+), 2 deletions(-)

diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 78bc64671e..fba5864172 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -670,8 +670,7 @@ InitPostgres(const char *in_dbname, Oid dboid, const char *username,
 	EnablePortalManager();
 
 	/* Initialize status reporting */
-	if (!bootstrap)
-		pgstat_beinit();
+	pgstat_beinit();
 
 	/*
 	 * Load relcache entries for the shared system catalogs.  This must create
-- 
2.27.0

From f924873f296fa4691a41f38b4b3509d08ee3b62d Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 2 Sep 2021 11:33:59 -0400
Subject: [PATCH v9 2/3] Add system view tracking accesses to buffers

TODO:
- pgstat reset message too large -- needs to be fixed
- Wrappers for smgr funcs to protect against regressions and cover other
  buffer types
- Consider helper func in pgstatfuncs.c to refactor out some of the
  redundant tuplestore creation code from pg_stat_get_progress_info,
  pg_stat_get_activity, etc
- Remove pg_stats test I added
- current code TODOs are mostly about adding comments
- When finished, catalog bump
- pgindent
---
 doc/src/sgml/monitoring.sgml                | 116 ++++++++++++++++-
 src/backend/catalog/system_views.sql        |  11 ++
 src/backend/postmaster/checkpointer.c       |   1 +
 src/backend/postmaster/pgstat.c             | 115 ++++++++++++++++-
 src/backend/storage/buffer/bufmgr.c         |  25 +++-
 src/backend/storage/buffer/freelist.c       |  22 +++-
 src/backend/utils/activity/backend_status.c |  49 ++++++-
 src/backend/utils/adt/pgstatfuncs.c         | 136 ++++++++++++++++++++
 src/backend/utils/init/miscinit.c           |  25 ++++
 src/include/catalog/pg_proc.dat             |   9 ++
 src/include/miscadmin.h                     |  23 ++++
 src/include/pgstat.h                        |  35 ++++-
 src/include/storage/buf_internals.h         |   4 +-
 src/include/utils/backend_status.h          |  44 +++++++
 src/test/regress/expected/rules.out         |   8 ++
 src/test/regress/sql/stats.sql              |   4 +
 16 files changed, 610 insertions(+), 17 deletions(-)

+     <row>
 
  </sect2>

 
index be7366379d..c0c4122fd5 100644
index b7d0fbaefd..903d4df911 100644
@@ -124,9 +124,12 @@ char	   *pgstat_stat_filename = NULL;
 
 /*
@@ -362,6 +365,7 @@ static void pgstat_recv_analyze(PgStat_MsgAnalyze *msg, int len);
@@ -974,6 +978,7 @@ pgstat_report_stat(bool disconnect)
 
+
 
@@ -1452,6 +1457,13 @@ pgstat_reset_shared_counters(const char *target)
 		msg.m_resettarget = RESET_ARCHIVER;
 	else if (strcmp(target, "bgwriter") == 0)
 		msg.m_resettarget = RESET_BGWRITER;
+	else if (strcmp(target, "buffers") == 0) {
+		memset(msg.resets, 0, sizeof(PgStat_MsgBufferTypeAccesses) * BACKEND_NUM_TYPES);
+
+		msg.m_resettarget = RESET_BUFFERS;
+
+		pgstat_report_live_backend_accesses(msg.resets);
+	}
 	else if (strcmp(target, "wal") == 0)
 		msg.m_resettarget = RESET_WAL;
 	else
@@ -2760,6 +2772,15 @@ pgstat_twophase_postabort(TransactionId xid, uint16 info,
 		rec->tuples_inserted + rec->tuples_updated;
 }
 
+// TODO: add comment?
+PgStat_BackendAccesses *
+pgstat_fetch_exited_backend_buffers(void)
+{
+	backend_read_statsfile();
+
+	return &globalStats.buffers;
+}
+
 
 /* ----------
  * pgstat_fetch_stat_dbentry() -
@@ -2998,6 +3019,13 @@ static void
 pgstat_shutdown_hook(int code, Datum arg)
 {
 	Assert(!pgstat_is_shutdown);
+	/*
+	 * Only need to send stats on buffer accesses when a process exits, as
+	 * pg_stat_get_buffers() will read from live backends' PgBackendStatus and
+	 * then sum this with totals from exited backends persisted by the stats
+	 * collector.
+	 */
+	pgstat_send_buffers();
 
 	/*
 	 * If we got as far as discovering our own database ID, we can report what
@@ -3148,6 +3176,47 @@ pgstat_send_bgwriter(void)
 	MemSet(&PendingBgWriterStats, 0, sizeof(PendingBgWriterStats));
 }
 
+/* ----------
+ * pgstat_send_buffers() -
+ *
+ *		Before exiting, a backend sends its buffer access statistics to the
+ *		collector so that they may be persisted
+ * ----------
+ */
+void
+pgstat_send_buffers(void)
+{
+	PgStat_MsgBufferTypeAccesses msg;
+	PgBufferAccesses *src_accesses;
+	PgStatBufferAccesses *dest_accesses;
+	int buffer_type;
+
+	PgBackendStatus *beentry = MyBEEntry;
+
+	if (!beentry)
+		return;
+
+	MemSet(&msg, 0, sizeof(msg));
+	msg.backend_type = beentry->st_backendType;
+
+	src_accesses = (PgBufferAccesses *) &beentry->buffer_access_stats;
+	dest_accesses = msg.buffer_type_accesses;
+
+	for (buffer_type = 0; buffer_type < BUFFER_NUM_TYPES; buffer_type++)
+	{
+		dest_accesses->allocs += pg_atomic_read_u64(&src_accesses->allocs);
+		dest_accesses->extends += pg_atomic_read_u64(&src_accesses->extends);
+		dest_accesses->fsyncs += pg_atomic_read_u64(&src_accesses->fsyncs);
+		dest_accesses->writes += pg_atomic_read_u64(&src_accesses->writes);
+		dest_accesses++;
+		src_accesses++;
+	}
+
+	pgstat_setheader(&msg.m_hdr, PGSTAT_MTYPE_BUFFER_ACTIONS);
+	pgstat_send(&msg, sizeof(msg));
+}
+
+
 /* ----------
  * pgstat_send_checkpointer() -
  *
@@ -3522,6 +3591,10 @@ PgstatCollectorMain(int argc, char *argv[])
 					pgstat_recv_checkpointer(&msg.msg_checkpointer, len);
 					break;
 
@@ -5222,9 +5295,16 @@ pgstat_recv_resetsharedcounter(PgStat_MsgResetsharedcounter *msg, int len)
@@ -5512,6 +5592,33 @@ pgstat_recv_checkpointer(PgStat_MsgCheckpointer *msg, int len)
 
+static void
index e88e4e918b..9832d35b90 100644
 
 	if (isExtend)
 	{

 
 
+				/*
@@ -2552,6 +2571,8 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)

 
 
 	/*
 
+	/*
index 7229598822..0581dce8b9 100644
@@ -399,6 +400,14 @@ pgstat_bestart(void)
 
 	/*
 	 * we don't zero st_progress_param here to save cycles; nobody should
@@ -621,6 +630,38 @@ pgstat_report_activity(BackendState state, const char *cmd_str)
 	PGSTAT_END_WRITE_ACTIVITY(beentry);
 }
 
+// TODO: function comment
+void pgstat_report_live_backend_accesses(PgStat_MsgBufferTypeAccesses *backend_accesses)
+{
+	int i, buffer_type;
+	PgBackendStatus *beentry = BackendStatusArray;
+	/*
+		* Loop through live backends and capture reset values
+		*/
+	for (i = 0; i < MaxBackends + NUM_AUXPROCTYPES; i++)
+	{
+		PgBufferAccesses *live_accesses;
+		PgStatBufferAccesses *buffer_accesses;
+		beentry++;
+		/* Don't count dead backends. They should already be counted */
+		if (beentry->st_procpid == 0)
+			continue;
+
+		live_accesses = (PgBufferAccesses *) beentry->buffer_access_stats;
+		buffer_accesses = backend_accesses[beentry->st_backendType].buffer_type_accesses;
+
+		for (buffer_type = 0; buffer_type < BUFFER_NUM_TYPES; buffer_type++)
+		{
+			buffer_accesses->allocs = pg_atomic_read_u64(&live_accesses->allocs);
+			buffer_accesses->extends = pg_atomic_read_u64(&live_accesses->extends);
+			buffer_accesses->fsyncs = pg_atomic_read_u64(&live_accesses->fsyncs);
+			buffer_accesses->writes = pg_atomic_read_u64(&live_accesses->writes);
+			buffer_accesses++;
+			live_accesses++;
+		}
+	}
+}
+
 /* --------
  * pgstat_report_query_id() -
  *
@@ -1046,6 +1087,12 @@ pgstat_get_my_query_id(void)
 }

+PgBackendStatus *
+pgstat_fetch_backend_statuses(void)
+{
+	return BackendStatusArray;
+}
+
 /* ----------
  * pgstat_fetch_stat_beentry() -
  *
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index ff5aedc99c..477caf2536 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1796,6 +1796,142 @@ pg_stat_get_buf_alloc(PG_FUNCTION_ARGS)
 	PG_RETURN_INT64(pgstat_fetch_stat_bgwriter()->buf_alloc);
 }
 
+Datum
+	PgBackendStatus *beentry;
+	beentry = pgstat_fetch_backend_statuses();
+		buffer_accesses = beentry->buffer_access_stats;
+	backend_accesses = pgstat_fetch_exited_backend_buffers();
 
+const char *

 
 
index bcd3588ea2..b73265ab13 100644

@@ -331,6 +335,30 @@ typedef struct PgStat_MsgDropdb
 } PgStat_MsgDropdb;

+// TODO: add comment
+	PgStat_Counter allocs;
+	PgStat_Counter extends;
+	PgStat_Counter fsyncs;
+	PgStat_Counter writes;
  * PgStat_MsgResetcounter		Sent by the backend to tell the collector
  *								to reset counters
@@ -351,6 +379,7 @@ typedef struct PgStat_MsgResetsharedcounter
 
 /* ----------
@@ -703,6 +732,7 @@ typedef union PgStat_Msg
 	PgStat_MsgArchiver msg_archiver;
 	PgStat_MsgBgWriter msg_bgwriter;
 	PgStat_MsgCheckpointer msg_checkpointer;
+	PgStat_MsgBufferTypeAccesses msg_buffer_accesses;
 	PgStat_MsgWal msg_wal;
 	PgStat_MsgSLRU msg_slru;
 	PgStat_MsgFuncstat msg_funcstat;
@@ -879,6 +909,7 @@ typedef struct PgStat_GlobalStats

 /*
@@ -1118,6 +1149,7 @@ extern void pgstat_twophase_postabort(TransactionId xid, uint16 info,
 
+extern void pgstat_send_buffers(void);
 
@@ -1126,6 +1158,7 @@ extern void pgstat_send_wal(bool force);
  * generate the pgstat* views.
  * ----------
  */
+extern PgStat_BackendAccesses * pgstat_fetch_exited_backend_buffers(void);
 extern PgStat_StatDBEntry *pgstat_fetch_stat_dbentry(Oid dbid);
 extern PgStat_StatTabEntry *pgstat_fetch_stat_tabentry(Oid relid);
 extern PgStat_StatFuncEntry *pgstat_fetch_stat_funcentry(Oid funcid);
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index 33fcaf5c9a..7e385135db 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -310,10 +310,10 @@ extern void ScheduleBufferTagForWriteback(WritebackContext *context, BufferTag *
 
 /* freelist.c */
 
index 8042b817df..0364779978 100644

@@ -37,6 +38,15 @@ typedef enum BackendState
  * ----------
  */
 
+// TODO: add a comment
  * PgBackendSSLStatus

@@ -296,7 +307,39 @@ extern void pgstat_bestart(void);
 extern void pgstat_clear_backend_activity_snapshot(void);
 
 /* Activity reporting functions */
+typedef struct PgStat_MsgBufferTypeAccesses PgStat_MsgBufferTypeAccesses;
+
+static inline void
+pgstat_increment_buffer_access_type(BufferAccessType ba_type, BufferType buf_type)
+{
+	PgBufferAccesses *accesses;
+	PgBackendStatus *beentry   = MyBEEntry;
+
+	Assert(beentry);
+
+	accesses = &beentry->buffer_access_stats[buf_type];
+	switch (ba_type)
+	{
+		case BA_Alloc:
+			pg_atomic_write_u64(&accesses->allocs,
+					pg_atomic_read_u64(&accesses->allocs) + 1);
+			break;
+		case BA_Extend:
+			pg_atomic_write_u64(&accesses->extends,
+					pg_atomic_read_u64(&accesses->extends) + 1);
+			break;
+		case BA_Fsync:
+			pg_atomic_write_u64(&accesses->fsyncs,
+					pg_atomic_read_u64(&accesses->fsyncs) + 1);
+			break;
+		case BA_Write:
+			pg_atomic_write_u64(&accesses->writes,
+					pg_atomic_read_u64(&accesses->writes) + 1);
+			break;
+	}
+}
 extern void pgstat_report_activity(BackendState state, const char *cmd_str);
+extern void pgstat_report_live_backend_accesses(PgStat_MsgBufferTypeAccesses *backend_accesses);
 extern void pgstat_report_query_id(uint64 query_id, bool force);
 extern void pgstat_report_tempfile(size_t filesize);
 extern void pgstat_report_appname(const char *appname);
@@ -312,6 +355,7 @@ extern uint64 pgstat_get_my_query_id(void);
  * generate the pgstat* views.
  * ----------
  */
+extern PgBackendStatus *pgstat_fetch_backend_statuses(void);
 extern int	pgstat_fetch_stat_numbackends(void);
 extern PgBackendStatus *pgstat_fetch_stat_beentry(int beid);
 extern LocalPgBackendStatus *pgstat_fetch_stat_local_beentry(int beid);
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2fa00a3c29..9172b0fcd2 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1828,6 +1828,14 @@ pg_stat_bgwriter| SELECT pg_stat_get_bgwriter_timed_checkpoints() AS checkpoints
     pg_stat_get_buf_fsync_backend() AS buffers_backend_fsync,
     pg_stat_get_buf_alloc() AS buffers_alloc,
     pg_stat_get_bgwriter_stat_reset_time() AS stats_reset;
+pg_stat_buffers| SELECT b.backend_type,
+    b.buffer_type,
+    b.alloc,
+    b.extend,
+    b.fsync,
+    b.write,
+    b.stats_reset
+   FROM pg_stat_get_buffers_accesses() b(backend_type, buffer_type, alloc, extend, fsync, write, stats_reset);
 pg_stat_database| SELECT d.oid AS datid,
     d.datname,
         CASE
diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql
index feaaee6326..4ad672b35a 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -176,4 +176,8 @@ FROM prevstats AS pr;

---

From: Melanie Plageman <[email-masked]>
Date: Mon, 27 Sep 2021 14:58:53 -0400

> most critical one is that the reset message doesn't work.

v10 is attached with updated comments and some limited code refactoring
From 566a5e6194acf6e670f9c1836d4296cf43b7ffcc Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 2 Sep 2021 11:47:41 -0400
Subject: [PATCH v10 3/3] Remove superfluous bgwriter stats

TODO:

-     <row>
 
-     <row>

 

 
 
index 0df7c3ceb6..7a1a5d22ed 100644
@@ -5585,9 +5585,7 @@ pgstat_recv_archiver(PgStat_MsgArchiver *msg, int len)
 
 /* ----------
@@ -5603,9 +5601,6 @@ pgstat_recv_checkpointer(PgStat_MsgCheckpointer *msg, int len)
 
 static void

 
-Datum
 
-Datum
 
 { oid => '8459', descr => 'statistics: counts of various types of accesses of buffers done by each backend type',
   proname => 'pg_stat_get_buffers_accesses', provolatile => 's', proisstrict => 'f',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 38b2167380..fb074d7094 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -501,9 +501,7 @@ typedef struct PgStat_MsgBgWriter
 {
 	PgStat_MsgHdr m_hdr;

 /* ----------
@@ -516,9 +514,6 @@ typedef struct PgStat_MsgCheckpointer
 
@@ -894,9 +889,7 @@ typedef struct PgStat_ArchiverStats
 
@@ -910,9 +903,6 @@ typedef struct PgStat_CheckpointerStats
 
 /*

From b0a24e0cd0115f5bfb15a69693ce205a9dca841e Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Fri, 24 Sep 2021 17:39:12 -0400
Subject: [PATCH v10 1/3] Allow bootstrap process to beinit

---

 
 	/*

From d8fb7a3b83a5ec322201cf8b02b49e36e58fe26d Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 2 Sep 2021 11:33:59 -0400
Subject: [PATCH v10 2/3] Add system view tracking accesses to buffers

TODO:
- pgstat reset message too large -- needs to be fixed
- Wrappers for smgr funcs to protect against regressions and cover other
  buffer types
- Consider helper func in pgstatfuncs.c to refactor out some of the
  redundant tuplestore creation code from pg_stat_get_progress_info,
  pg_stat_get_activity, etc
- Remove pg_stats test I added
- When finished, catalog bump
- pgindent
---
 doc/src/sgml/monitoring.sgml                | 116 ++++++++++++++++-
 src/backend/catalog/system_views.sql        |  11 ++
 src/backend/postmaster/checkpointer.c       |   1 +
 src/backend/postmaster/pgstat.c             | 131 ++++++++++++++++++-
 src/backend/storage/buffer/bufmgr.c         |  25 +++-
 src/backend/storage/buffer/freelist.c       |  22 +++-
 src/backend/utils/activity/backend_status.c |  43 ++++++-
 src/backend/utils/adt/pgstatfuncs.c         | 136 ++++++++++++++++++++
 src/backend/utils/init/miscinit.c           |  25 ++++
 src/include/catalog/pg_proc.dat             |   9 ++
 src/include/miscadmin.h                     |  23 ++++
 src/include/pgstat.h                        |  52 +++++++-
 src/include/storage/buf_internals.h         |   4 +-
 src/include/utils/backend_status.h          |  46 +++++++
 src/test/regress/expected/rules.out         |   8 ++
 src/test/regress/sql/stats.sql              |   4 +
 16 files changed, 639 insertions(+), 17 deletions(-)

+     <row>
 
  </sect2>

 
index b7d0fbaefd..0df7c3ceb6 100644
 
 /*
 
+
 
@@ -2760,6 +2772,20 @@ pgstat_twophase_postabort(TransactionId xid, uint16 info,
 
+/*
+ *
+ *	Support function for SQL-callable pgstat* functions. Returns a pointer to
+ *	the BackendAccesses structure tracking buffer access statistics for both
+ *	exited backends and reset arithmetic.
+ */
+PgStat_BackendAccesses *
+pgstat_fetch_exited_backend_buffers(void)
+{
+	backend_read_statsfile();
+
+	return &globalStats.buffers;
+}
+
 
 /* ----------
@@ -2998,6 +3024,13 @@ static void
 
 	/*
 	 * If we got as far as discovering our own database ID, we can report what
@@ -3092,6 +3125,29 @@ pgstat_send(void *msg, int len)
 #endif
 }
 
+/*
+ * Add live buffer access stats for all buffer types (e.g. shared, local) to
+ * those in the equivalent stats structure for exited backends. Note that this
+ * adds and doesn't set, so the destination buffer access stats should be
+ * zeroed out by the caller initially. This would commonly be used to transfer
+ * all buffer access stats for all buffer types for a particular backend type
+ * to the pgstats structure.
+ */
+void pgstat_add_buffer_type_accesses(PgStatBufferAccesses *dest, PgBufferAccesses *src, int buffer_num_types)
+{
+	int buffer_type;
+	for (buffer_type = 0; buffer_type < buffer_num_types; buffer_type++)
+	{
+		dest->allocs += pg_atomic_read_u64(&src->allocs);
+		dest->extends += pg_atomic_read_u64(&src->extends);
+		dest->fsyncs += pg_atomic_read_u64(&src->fsyncs);
+		dest->writes += pg_atomic_read_u64(&src->writes);
+		dest++;
+		src++;
+	}
+
+}
+
 /* ----------
  * pgstat_send_archiver() -
  *
@@ -3148,6 +3204,35 @@ pgstat_send_bgwriter(void)
 	MemSet(&PendingBgWriterStats, 0, sizeof(PendingBgWriterStats));
 }
 
+/* ----------
+	pgstat_add_buffer_type_accesses(msg.buffer_type_accesses,
+			(PgBufferAccesses *) &beentry->buffer_access_stats,
+			BUFFER_NUM_TYPES);
@@ -3522,6 +3607,10 @@ PgstatCollectorMain(int argc, char *argv[])
 
@@ -5222,9 +5311,16 @@ pgstat_recv_resetsharedcounter(PgStat_MsgResetsharedcounter *msg, int len)
@@ -5512,6 +5608,33 @@ pgstat_recv_checkpointer(PgStat_MsgCheckpointer *msg, int len)
 
+static void
 
 	if (isExtend)
 	{

 
 
+				/*

 
 
 	/*
 
+	/*
index 7229598822..6f02b624bb 100644
 
 	/*
 	 * we don't zero st_progress_param here to save cycles; nobody should
@@ -621,6 +630,32 @@ pgstat_report_activity(BackendState state, const char *cmd_str)
 	PGSTAT_END_WRITE_ACTIVITY(beentry);
 }
 
+/*
+ * Iterate through BackendStatusArray and capture live backends' buffer access
+ * stats, adding them to that backend type's member of the backend_accesses
+ * structure.
+ */
+void pgstat_report_live_backend_accesses(PgStat_MsgBufferTypeAccesses *backend_accesses)
+{
+	int i;
+	PgBackendStatus *beentry = BackendStatusArray;
+	/*
+	 * Loop through live backends and capture reset values
+	 */
+	for (i = 0; i < MaxBackends + NUM_AUXPROCTYPES; i++)
+	{
+		beentry++;
+		/* Don't count dead backends. They should already be counted */
+		if (beentry->st_procpid == 0)
+			continue;
+
+		pgstat_add_buffer_type_accesses(backend_accesses[beentry->st_backendType].buffer_type_accesses,
+				(PgBufferAccesses *) beentry->buffer_access_stats,
+				BUFFER_NUM_TYPES);
+
+	}
+}
+
 /* --------
  * pgstat_report_query_id() -
  *
@@ -1046,6 +1081,12 @@ pgstat_get_my_query_id(void)
 }

+PgBackendStatus *
 
+Datum
 
+const char *

 
 
index bcd3588ea2..38b2167380 100644

@@ -331,6 +335,45 @@ typedef struct PgStat_MsgDropdb
 } PgStat_MsgDropdb;

+/*
+ * Structure for counting all types of buffer accesses in the stats collector
+ * It has no message header, so, it must be used within a
+ * PgStat_MsgBufferTypeAccesses when being sent to the stats collector.
+ */
+typedef struct PgStatBufferAccesses
+{
+	PgStat_Counter allocs;
+	PgStat_Counter extends;
+	PgStat_Counter fsyncs;
+	PgStat_Counter writes;
+} PgStatBufferAccesses;
+
+/*
+ * Sent by a backend to the stats collector to report all buffer accesses of
+ * all types of buffers for a given type of a backend. This will happen when
+ * the backend exits or when stats are reset.
+ */
+typedef struct PgStat_MsgBufferTypeAccesses
+{
+	PgStat_MsgHdr m_hdr;
+
+	BackendType backend_type;
+	PgStatBufferAccesses buffer_type_accesses[BUFFER_NUM_TYPES];
+} PgStat_MsgBufferTypeAccesses;
+
+/*
+ * Structure used by stats collector to keep track of all types of exited
+ * backends' buffer accesses for all types of buffers as well as all stats from
+ * live backends at the time of stats reset. resets is populated using a reset
+ * message sent to the stats collector.
+ */
+typedef struct PgStat_BackendAccesses
+{
+	TimestampTz stat_reset_timestamp;
+	PgStat_MsgBufferTypeAccesses accesses[BACKEND_NUM_TYPES];
+	PgStat_MsgBufferTypeAccesses resets[BACKEND_NUM_TYPES];
+} PgStat_BackendAccesses;
+
 /* ----------
  * PgStat_MsgResetcounter		Sent by the backend to tell the collector
  *								to reset counters
@@ -351,6 +394,7 @@ typedef struct PgStat_MsgResetsharedcounter
 {
 	PgStat_MsgHdr m_hdr;
 	PgStat_Shared_Reset_Target m_resettarget;
+	PgStat_MsgBufferTypeAccesses resets[BACKEND_NUM_TYPES];
 } PgStat_MsgResetsharedcounter;
 
 /* ----------
@@ -703,6 +747,7 @@ typedef union PgStat_Msg
 	PgStat_MsgArchiver msg_archiver;
 	PgStat_MsgBgWriter msg_bgwriter;
 	PgStat_MsgCheckpointer msg_checkpointer;
+	PgStat_MsgBufferTypeAccesses msg_buffer_accesses;
 	PgStat_MsgWal msg_wal;
 	PgStat_MsgSLRU msg_slru;
 	PgStat_MsgFuncstat msg_funcstat;
@@ -879,6 +924,7 @@ typedef struct PgStat_GlobalStats

 /*
@@ -1116,8 +1162,11 @@ extern void pgstat_twophase_postcommit(TransactionId xid, uint16 info,
 extern void pgstat_twophase_postabort(TransactionId xid, uint16 info,
 									  void *recdata, uint32 len);
 
+extern void pgstat_add_buffer_type_accesses(PgStatBufferAccesses *dest,
+		PgBufferAccesses *src, int buffer_num_types);
 extern void pgstat_send_archiver(const char *xlog, bool failed);
 extern void pgstat_send_bgwriter(void);
+extern void pgstat_send_buffers(void);
 extern void pgstat_send_checkpointer(void);
 extern void pgstat_send_wal(bool force);
 
@@ -1126,6 +1175,7 @@ extern void pgstat_send_wal(bool force);
 
 /* freelist.c */
 
index 8042b817df..c910cb6206 100644

@@ -37,6 +38,17 @@ typedef enum BackendState
  * ----------
  */
 
+/*
+ * Structure for counting all types of buffer accesses for a live backend.
+ */
+typedef struct PgBufferAccesses
+{
+	pg_atomic_uint64 allocs;
+	pg_atomic_uint64 extends;
+	pg_atomic_uint64 fsyncs;
+	pg_atomic_uint64 writes;
+} PgBufferAccesses;
+
 /*
  * PgBackendSSLStatus
  *
@@ -168,6 +180,7 @@ typedef struct PgBackendStatus

@@ -296,7 +309,39 @@ extern void pgstat_bestart(void);
 extern void pgstat_clear_backend_activity_snapshot(void);
 
@@ -312,6 +357,7 @@ extern uint64 pgstat_get_my_query_id(void);

---

From: Melanie Plageman <[email-masked]>
Date: Wed, 29 Sep 2021 16:46:07 -0400

> v10 is attached with updated comments and some limited code refactoring

v11 has fixed the oversize message issue by sending a reset message for
each backend type. Now, we will call GetCurrentTimestamp
BACKEND_NUM_TYPES times, so maybe I should add some kind of flag to the
reset message that indicates the first message so that all the "do once"
things can be done at that point.

I've also fixed a few style/cosmetic issues and updated the commit
message with a link to the thread [1] where I proposed smgrwrite() and
smgrextend() wrappers (which is where I propose to call
pgstat_incremement_buffer_access_type() for unbuffered writes and
extends).

- Melanie

[1] https://www.postgresql.org/message-id/CAAKRu_aw72w70X1P%3Dba20K8iGUvSkyz7Yk03wPPh3f9WgmcJ3g%40mail.gmail.com
From b2023adb80f2920081a6f35c19e0276d38ae3a15 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Wed, 29 Sep 2021 15:44:51 -0400
Subject: [PATCH v11 3/3] Remove superfluous bgwriter stats

TODO:

index 75753c3339..5852c45246 100644
 
-     <row>
 
-     <row>

 

 
 
index 683be10430..9048154515 100644
@@ -5601,9 +5601,7 @@ pgstat_recv_archiver(PgStat_MsgArchiver *msg, int len)
 
 /* ----------
@@ -5619,9 +5617,6 @@ pgstat_recv_checkpointer(PgStat_MsgCheckpointer *msg, int len)
 
 static void
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 0bbb1e2458..72fb5de664 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -2164,7 +2164,6 @@ BufferSync(int flags)
 			if (SyncOneBuffer(buf_id, false, &wb_context) & BUF_WRITTEN)
 			{
 				TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
-				PendingCheckpointerStats.m_buf_written_checkpoints++;
 				num_written++;
 			}
 		}
@@ -2273,9 +2272,6 @@ BgBufferSync(WritebackContext *wb_context)
 	 */
 	strategy_buf_id = StrategySyncStart(&strategy_passes, &recent_alloc);
 
@@ -2472,8 +2468,6 @@ BgBufferSync(WritebackContext *wb_context)
 			reusable_buffers++;
 	}
 
index 184968d99c..bf98f19db1 100644
 
-Datum
 
-Datum
 
 { oid => '8459', descr => 'statistics: counts of various types of accesses of buffers done by each backend type',
   proname => 'pg_stat_get_buffers_accesses', provolatile => 's', proisstrict => 'f',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index d3f51f1fd8..246ab05f76 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -499,9 +499,7 @@ typedef struct PgStat_MsgBgWriter
 {
 	PgStat_MsgHdr m_hdr;

 /* ----------
@@ -514,9 +512,6 @@ typedef struct PgStat_MsgCheckpointer
 
@@ -892,9 +887,7 @@ typedef struct PgStat_ArchiverStats
 
@@ -908,9 +901,6 @@ typedef struct PgStat_CheckpointerStats
 
 /*

From 454b63a963a46a6a3541ed5627d985e09c640446 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Wed, 29 Sep 2021 15:39:45 -0400
Subject: [PATCH v11 2/3] Add system view tracking accesses to buffers

When stats are reset, the backend sending the reset message will loop
through and collect all of the live backends' buffer access counters,
sending a reset message for each backend type containing its buffer
access stats. When receiving this message, the stats collector will 1)
save these reset values in an array of "resets" and 2) zero out the
exited backends' saved buffer access counters. This is required for
accurate stats after a reset without writing to other backends'
PgBackendStatus.

A separate proposed patch [1] which would add wrappers for smgrwrite()
and extend() would provide a good location to call
pgstat_increment_buffer_access_type() for unbuffered IO and avoid
regressions for future users of these functions.

TODO:

[1] https://www.postgresql.org/message-id/CAAKRu_aw72w70X1P%3Dba20K8iGUvSkyz7Yk03wPPh3f9WgmcJ3g%40mail.gmail.com
---
 doc/src/sgml/monitoring.sgml                | 116 ++++++++++++++-
 src/backend/catalog/system_views.sql        |  11 ++
 src/backend/postmaster/checkpointer.c       |   1 +
 src/backend/postmaster/pgstat.c             | 149 +++++++++++++++++++-
 src/backend/storage/buffer/bufmgr.c         |  24 +++-
 src/backend/storage/buffer/freelist.c       |  24 +++-
 src/backend/utils/activity/backend_status.c |  44 +++++-
 src/backend/utils/adt/pgstatfuncs.c         | 140 ++++++++++++++++++
 src/backend/utils/init/miscinit.c           |  19 +++
 src/include/catalog/pg_proc.dat             |   9 ++
 src/include/miscadmin.h                     |  23 +++
 src/include/pgstat.h                        |  48 +++++++
 src/include/storage/buf_internals.h         |   4 +-
 src/include/utils/backend_status.h          |  46 ++++++
 src/test/regress/expected/rules.out         |   8 ++
 src/test/regress/sql/stats.sql              |   4 +
 16 files changed, 652 insertions(+), 18 deletions(-)

index 2cd8920645..75753c3339 100644
 
+     <row>
 
  </sect2>

 
index b7d0fbaefd..683be10430 100644
 
 /*
 
+
 
@@ -1452,6 +1457,8 @@ pgstat_reset_shared_counters(const char *target)
 		msg.m_resettarget = RESET_ARCHIVER;
 	else if (strcmp(target, "bgwriter") == 0)
 		msg.m_resettarget = RESET_BGWRITER;
+	else if (strcmp(target, "buffers") == 0)
+		msg.m_resettarget = RESET_BUFFERS;
 	else if (strcmp(target, "wal") == 0)
 		msg.m_resettarget = RESET_WAL;
 	else
@@ -1461,7 +1468,23 @@ pgstat_reset_shared_counters(const char *target)
 				 errhint("Target must be \"archiver\", \"bgwriter\", or \"wal\".")));
 
 	pgstat_setheader(&msg.m_hdr, PGSTAT_MTYPE_RESETSHAREDCOUNTER);
-	pgstat_send(&msg, sizeof(msg));
+
+	if (msg.m_resettarget == RESET_BUFFERS)
+	{
+		int backend_type;
+		PgStat_MsgBufferTypeAccesses accesses[BACKEND_NUM_TYPES];
+		memset(accesses, 0, sizeof(accesses));
+		pgstat_report_live_backend_accesses(accesses);
+
+		for (backend_type = 1; backend_type < BACKEND_NUM_TYPES; backend_type++)
+		{
+			memcpy(&msg.backend_resets, &accesses[backend_type], sizeof(msg.backend_resets));
+			pgstat_send(&msg, sizeof(msg));
+		}
+	}
+	else
+		pgstat_send(&msg, sizeof(msg));
+
 }
 
 /* ----------
@@ -2760,6 +2783,20 @@ pgstat_twophase_postabort(TransactionId xid, uint16 info,
 		rec->tuples_inserted + rec->tuples_updated;
 }
 
+/*
+ *
 
 /* ----------
@@ -2998,6 +3035,13 @@ static void
 
 	/*
 	 * If we got as far as discovering our own database ID, we can report what
@@ -3092,6 +3136,29 @@ pgstat_send(void *msg, int len)
 #endif
 }
 
+/*
@@ -3148,6 +3215,32 @@ pgstat_send_bgwriter(void)
 
+/*
+	memset(&msg, 0, sizeof(msg));
@@ -3522,6 +3615,10 @@ PgstatCollectorMain(int argc, char *argv[])
 
+				case PGSTAT_MTYPE_BUFFER_ACTIONS:
+					pgstat_recv_buffer_type_accesses(&msg.msg_buffer_accesses, len);
+					break;
+
 				case PGSTAT_MTYPE_WAL:
 					pgstat_recv_wal(&msg.msg_wal, len);
 					break;
@@ -5222,9 +5319,24 @@ pgstat_recv_resetsharedcounter(PgStat_MsgResetsharedcounter *msg, int len)
 	if (msg->m_resettarget == RESET_BGWRITER)
 	{
 		/* Reset the global, bgwriter and checkpointer statistics for the cluster. */
-		memset(&globalStats, 0, sizeof(globalStats));
+		memset(&globalStats.checkpointer, 0, sizeof(globalStats.checkpointer));
+		memset(&globalStats.bgwriter, 0, sizeof(globalStats.bgwriter));
 		globalStats.bgwriter.stat_reset_timestamp = GetCurrentTimestamp();
 	}
+	else if (msg->m_resettarget == RESET_BUFFERS)
+	{
+		BackendType backend_type = msg->backend_resets.backend_type;
+		// TODO: this only needs to be done once, but I am doing it for every backend_type
+		// should I add a way to tell if this is the first of the RESET_BUFFERS messages?
+		memset(&globalStats.buffers.accesses, 0, sizeof(globalStats.buffers.accesses));
+
+		memcpy(&globalStats.buffers.resets[backend_type],
+				&msg->backend_resets, sizeof(msg->backend_resets));
+		// TODO: likewise this should only be done once -- especially because
+		// it isn't cheap. Should I add something to the message to indicate it
+		// is the first message when it is of type RESET_BUFFERS?
+		globalStats.buffers.stat_reset_timestamp = GetCurrentTimestamp();
+	}
 	else if (msg->m_resettarget == RESET_ARCHIVER)
 	{
 		/* Reset the archiver statistics for the cluster. */
@@ -5512,6 +5624,33 @@ pgstat_recv_checkpointer(PgStat_MsgCheckpointer *msg, int len)
 	globalStats.checkpointer.buf_fsync_backend += msg->m_buf_fsync_backend;
 }
 
+static void
index e88e4e918b..0bbb1e2458 100644
 
 	if (isExtend)
 	{

@@ -1219,6 +1221,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 			if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf),
 										 LW_SHARED))
 			{
+				BufferType buftype;
 				/*
 				 * If using a nondefault strategy, and writing the buffer
 				 * would require a WAL flush, let the strategy decide whether
@@ -1236,7 +1239,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 					UnlockBufHdr(buf, buf_state);
 
@@ -1245,6 +1248,21 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 					}
 				}
 
+				/*
+				buftype = from_ring ? Buf_Strategy : Buf_Shared;
+				pgstat_increment_buffer_access_type(BA_Write, buftype);
@@ -2552,6 +2570,8 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
 
index 6be80476db..386cad1a36 100644

@@ -198,7 +199,7 @@ have_free_buffer(void)
  *	return the buffer with the buffer header spinlock still held.
  */
 BufferDesc *
-StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
+StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state, bool *from_ring)
 {
 	BufferDesc *buf;
 	int			bgwprocno;
@@ -212,7 +213,8 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
 	if (strategy != NULL)
 	{
 		buf = GetBufferFromRing(strategy, buf_state);
-		if (buf != NULL)
+		*from_ring = buf != NULL;
+		if (*from_ring)
 			return buf;
 	}

 	/*
 
+	/*
index 7229598822..0bfa22264f 100644
 
 	/*
 	 * we don't zero st_progress_param here to save cycles; nobody should
@@ -621,6 +630,33 @@ pgstat_report_activity(BackendState state, const char *cmd_str)
 	PGSTAT_END_WRITE_ACTIVITY(beentry);
 }
 
+/*
+		backend_accesses[beentry->st_backendType].backend_type = beentry->st_backendType;
@@ -1046,6 +1082,12 @@ pgstat_get_my_query_id(void)
 }

+PgBackendStatus *
+pgstat_fetch_backend_statuses(void)
+{
+	return BackendStatusArray;
+}
+
 /* ----------
  * pgstat_fetch_stat_beentry() -
  *
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index ff5aedc99c..184968d99c 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1796,6 +1796,146 @@ pg_stat_get_buf_alloc(PG_FUNCTION_ARGS)
 	PG_RETURN_INT64(pgstat_fetch_stat_bgwriter()->buf_alloc);
 }
 
+Datum
+	 * When adding a new column to the pg_stat_buffers view, add a new enum
+	 * value here above COLUMN_LENGTH.
index 88801374b5..0922bc2c81 100644
@@ -299,6 +299,25 @@ GetBackendTypeDesc(BackendType backendType)
 	return backendDesc;
 }
 
+const char *
+			return "direct";
+			return "local";
+			return "shared";
+			return "strategy";
+	return "unknown buffer type";

 
 
index bcd3588ea2..d3f51f1fd8 100644
 
@@ -331,6 +333,45 @@ typedef struct PgStat_MsgDropdb
 } PgStat_MsgDropdb;

+/*
@@ -351,6 +392,7 @@ typedef struct PgStat_MsgResetsharedcounter
+	PgStat_MsgBufferTypeAccesses backend_resets;
 
 /* ----------
@@ -703,6 +745,7 @@ typedef union PgStat_Msg
 	PgStat_MsgArchiver msg_archiver;
 	PgStat_MsgBgWriter msg_bgwriter;
 	PgStat_MsgCheckpointer msg_checkpointer;
+	PgStat_MsgBufferTypeAccesses msg_buffer_accesses;
 	PgStat_MsgWal msg_wal;
 	PgStat_MsgSLRU msg_slru;
 	PgStat_MsgFuncstat msg_funcstat;
@@ -879,6 +922,7 @@ typedef struct PgStat_GlobalStats

 /*
@@ -1116,8 +1160,11 @@ extern void pgstat_twophase_postcommit(TransactionId xid, uint16 info,
 extern void pgstat_twophase_postabort(TransactionId xid, uint16 info,
 									  void *recdata, uint32 len);

@@ -1126,6 +1173,7 @@ extern void pgstat_send_wal(bool force);
 
 /* freelist.c */

 
+/*

 

From 1b27d12692bce68d5c7acb57d5f3debcf54cbbae Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Fri, 24 Sep 2021 17:39:12 -0400
Subject: [PATCH v11 1/3] Allow bootstrap process to beinit

---

 
 	/*

---

From: Melanie Plageman <[email-masked]>
Date: Thu, 30 Sep 2021 17:16:34 -0400

> [1] https://www.postgresql.org/message-id/CAAKRu_aw72w70X1P%3Dba20K8iGUvSkyz7Yk03wPPh3f9WgmcJ3g%40mail.gmail.com

v12 (attached) has various style and code clarity updates (it is
pgindented as well). I also added a new commit which creates a utility
function to make a tuplestore for views that need one in pgstatfuncs.c.

Having received some offlist feedback about the names BufferAccessType
and BufferType being confusing, I am planning to rename these variables
and all of the associated functions. I agree that BufferType and
BufferAccessType are confusing for the following reasons:
  - They sound similar.
  - They aren't very precise.
  - One of the types of buffers is not using a Postgres buffer.

So far, the proposed alternative is IO_Op or IOOp for BufferAccessType
and IOPath for BufferType.

- Melanie
From 5c3f382ba4eef310fc82b2b676029097eb99cd70 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Wed, 29 Sep 2021 15:44:51 -0400
Subject: [PATCH v12 4/4] Remove superfluous bgwriter stats

TODO:

-     <row>
 
-     <row>

index 931bdcaa59..712c878f1c 100644

 
 		pgstat_inc_buffer_access_type(BA_Fsync, Buf_Shared);

index 3673b34f50..f85e508689 100644
@@ -5611,9 +5611,7 @@ pgstat_recv_archiver(PgStat_MsgArchiver *msg, int len)
 
 /* ----------
@@ -5629,9 +5627,6 @@ pgstat_recv_checkpointer(PgStat_MsgCheckpointer *msg, int len)
 
 static void
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 58bf60425b..f69dbe38b8 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -2166,7 +2166,6 @@ BufferSync(int flags)
 			if (SyncOneBuffer(buf_id, false, &wb_context) & BUF_WRITTEN)
 			{
 				TRACE_POSTGRESQL_BUFFER_SYNC_WRITTEN(buf_id);
-				PendingCheckpointerStats.m_buf_written_checkpoints++;
 				num_written++;
 			}
 		}
@@ -2275,9 +2274,6 @@ BgBufferSync(WritebackContext *wb_context)
 	 */
 	strategy_buf_id = StrategySyncStart(&strategy_passes, &recent_alloc);
 
@@ -2474,8 +2470,6 @@ BgBufferSync(WritebackContext *wb_context)
 			reusable_buffers++;
 	}
 
-	PendingBgWriterStats.m_buf_written_clean += num_written;
-
 #ifdef BGW_DEBUG
 	elog(DEBUG1, "bgwriter: recent_alloc=%u smoothed=%.2f delta=%ld ahead=%d density=%.2f reusable_est=%d upcoming_est=%d scanned=%d wrote=%d reusable=%d",
 		 recent_alloc, smoothed_alloc, strategy_delta, bufs_ahead,
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 674a2167ec..cf2998514e 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1723,18 +1723,6 @@ pg_stat_get_bgwriter_requested_checkpoints(PG_FUNCTION_ARGS)
 	PG_RETURN_INT64(pgstat_fetch_stat_checkpointer()->requested_checkpoints);
 }
 
-Datum
@@ -1763,24 +1751,6 @@ pg_stat_get_bgwriter_stat_reset_time(PG_FUNCTION_ARGS)
 
-Datum
 
 { oid => '8459', descr => 'statistics: counts of various types of accesses of buffers done by each backend type',
   proname => 'pg_stat_get_buffers_accesses', provolatile => 's', proisstrict => 'f',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 2e3dfcc01d..02a9fff64e 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -505,9 +505,7 @@ typedef struct PgStat_MsgBgWriter
 {
 	PgStat_MsgHdr m_hdr;

 /* ----------
@@ -520,9 +518,6 @@ typedef struct PgStat_MsgCheckpointer
 
@@ -898,9 +893,7 @@ typedef struct PgStat_ArchiverStats
 
@@ -914,9 +907,6 @@ typedef struct PgStat_CheckpointerStats
 
 /*

From a709ddb30b2b747beb214f0b13cd1e1816094e6b Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 30 Sep 2021 16:16:22 -0400
Subject: [PATCH v12 2/4] Add utility to make tuplestores for pg stat views

Most of the steps to make a tuplestore for those pg_stat views requiring
one are the same. Consolidate them into a single helper function for
clarity and to avoid bugs.
---
 src/backend/utils/adt/pgstatfuncs.c | 129 ++++++++++------------------
 1 file changed, 44 insertions(+), 85 deletions(-)

diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index ff5aedc99c..513f5aecf6 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -36,6 +36,42 @@
 
 #define HAS_PGSTAT_PERMISSIONS(role)	 (is_member_of_role(GetUserId(), ROLE_PG_READ_ALL_STATS) || has_privs_of_role(GetUserId(), role))
 
+/*
+ * Helper function for views with multiple rows constructed from a tuplestore
+ */
+static Tuplestorestate *
+pg_stat_make_tuplestore(FunctionCallInfo fcinfo, TupleDesc *tupdesc)
+{
+	Tuplestorestate *tupstore;
+	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+	MemoryContext per_query_ctx;
+	MemoryContext oldcontext;
+
+	/* check to see if caller supports us returning a tuplestore */
+	if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("set-valued function called in context that cannot accept a set")));
+	if (!(rsinfo->allowedModes & SFRM_Materialize))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("materialize mode required, but it is not allowed in this context")));
+
+	/* Build a tuple descriptor for our result type */
+	if (get_call_result_type(fcinfo, NULL, tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	per_query_ctx = rsinfo->econtext->ecxt_per_query_memory;
+	oldcontext = MemoryContextSwitchTo(per_query_ctx);
+
+	tupstore = tuplestore_begin_heap(true, false, work_mem);
+	rsinfo->returnMode = SFRM_Materialize;
+	rsinfo->setResult = tupstore;
+	rsinfo->setDesc = *tupdesc;
+	MemoryContextSwitchTo(oldcontext);
+	return tupstore;
+}
+
 Datum
 pg_stat_get_numscans(PG_FUNCTION_ARGS)
 {
@@ -457,29 +493,13 @@ Datum
 pg_stat_get_progress_info(PG_FUNCTION_ARGS)
 {
 #define PG_STAT_GET_PROGRESS_COLS	PGSTAT_NUM_PROGRESS_PARAM + 3
-	int			num_backends = pgstat_fetch_stat_numbackends();
 	int			curr_backend;
-	char	   *cmd = text_to_cstring(PG_GETARG_TEXT_PP(0));
 	ProgressCommandType cmdtype;
 	TupleDesc	tupdesc;
-	Tuplestorestate *tupstore;
-	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
-	MemoryContext per_query_ctx;
-	MemoryContext oldcontext;
-
-	/* check to see if caller supports us returning a tuplestore */
-	if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo))
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("set-valued function called in context that cannot accept a set")));
-	if (!(rsinfo->allowedModes & SFRM_Materialize))
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("materialize mode required, but it is not allowed in this context")));
 
-	/* Build a tuple descriptor for our result type */
-	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
-		elog(ERROR, "return type must be a row type");
+	Tuplestorestate *tupstore = pg_stat_make_tuplestore(fcinfo, &tupdesc);
+	int			num_backends = pgstat_fetch_stat_numbackends();
+	char	   *cmd = text_to_cstring(PG_GETARG_TEXT_PP(0));
 
 	/* Translate command name into command type code. */
 	if (pg_strcasecmp(cmd, "VACUUM") == 0)
@@ -499,15 +519,6 @@ pg_stat_get_progress_info(PG_FUNCTION_ARGS)
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("invalid command name: \"%s\"", cmd)));
 
-	per_query_ctx = rsinfo->econtext->ecxt_per_query_memory;
-	oldcontext = MemoryContextSwitchTo(per_query_ctx);
-
-	tupstore = tuplestore_begin_heap(true, false, work_mem);
-	rsinfo->returnMode = SFRM_Materialize;
-	rsinfo->setResult = tupstore;
-	rsinfo->setDesc = tupdesc;
-	MemoryContextSwitchTo(oldcontext);
-
 	/* 1-based index */
 	for (curr_backend = 1; curr_backend <= num_backends; curr_backend++)
 	{
@@ -568,38 +579,12 @@ Datum
 pg_stat_get_activity(PG_FUNCTION_ARGS)
 {
 #define PG_STAT_GET_ACTIVITY_COLS	30
-	int			num_backends = pgstat_fetch_stat_numbackends();
-	int			curr_backend;
-	int			pid = PG_ARGISNULL(0) ? -1 : PG_GETARG_INT32(0);
-	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
 	TupleDesc	tupdesc;
-	Tuplestorestate *tupstore;
-	MemoryContext per_query_ctx;
-	MemoryContext oldcontext;
-
-	/* check to see if caller supports us returning a tuplestore */
-	if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo))
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("set-valued function called in context that cannot accept a set")));
-	if (!(rsinfo->allowedModes & SFRM_Materialize))
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("materialize mode required, but it is not allowed in this context")));
-
-	/* Build a tuple descriptor for our result type */
-	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
-		elog(ERROR, "return type must be a row type");
-
-	per_query_ctx = rsinfo->econtext->ecxt_per_query_memory;
-	oldcontext = MemoryContextSwitchTo(per_query_ctx);
-
-	tupstore = tuplestore_begin_heap(true, false, work_mem);
-	rsinfo->returnMode = SFRM_Materialize;
-	rsinfo->setResult = tupstore;
-	rsinfo->setDesc = tupdesc;
+	int			curr_backend;
 
-	MemoryContextSwitchTo(oldcontext);
+	int			num_backends = pgstat_fetch_stat_numbackends();
+	int			pid = PG_ARGISNULL(0) ? -1 : PG_GETARG_INT32(0);
+	Tuplestorestate *tupstore = pg_stat_make_tuplestore(fcinfo, &tupdesc);
 
 	/* 1-based index */
 	for (curr_backend = 1; curr_backend <= num_backends; curr_backend++)
@@ -1871,37 +1856,11 @@ Datum
 pg_stat_get_slru(PG_FUNCTION_ARGS)
 {
 #define PG_STAT_GET_SLRU_COLS	9
-	ReturnSetInfo *rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
 	TupleDesc	tupdesc;
-	Tuplestorestate *tupstore;
-	MemoryContext per_query_ctx;
-	MemoryContext oldcontext;
 	int			i;
 	PgStat_SLRUStats *stats;
 
-	/* check to see if caller supports us returning a tuplestore */
-	if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo))
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("set-valued function called in context that cannot accept a set")));
-	if (!(rsinfo->allowedModes & SFRM_Materialize))
-		ereport(ERROR,
-				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
-				 errmsg("materialize mode required, but it is not allowed in this context")));
-
-	/* Build a tuple descriptor for our result type */
-	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
-		elog(ERROR, "return type must be a row type");
-
-	per_query_ctx = rsinfo->econtext->ecxt_per_query_memory;
-	oldcontext = MemoryContextSwitchTo(per_query_ctx);
-
-	tupstore = tuplestore_begin_heap(true, false, work_mem);
-	rsinfo->returnMode = SFRM_Materialize;
-	rsinfo->setResult = tupstore;
-	rsinfo->setDesc = tupdesc;
-
-	MemoryContextSwitchTo(oldcontext);
+	Tuplestorestate *tupstore = pg_stat_make_tuplestore(fcinfo, &tupdesc);
 
 	/* request SLRU stats from the stat collector */
 	stats = pgstat_fetch_slru();
-- 
2.27.0

From 40c809ad1127322f3462e85be080c10534485f0d Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Fri, 24 Sep 2021 17:39:12 -0400
Subject: [PATCH v12 1/4] Allow bootstrap process to beinit

---

 
 	/*

From 397777ca5d1512a233d3f0ba8954b0a32421ad4f Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Wed, 29 Sep 2021 15:39:45 -0400
Subject: [PATCH v12 3/4] Add system view tracking accesses to buffers

TODO:

Discussion: https://www.postgresql.org/message-id/flat/20210415235954.qcypb4urtovzkat5%40alap3.anarazel.de#724d5cce4bcb587f9167b80a5824bc5c
---
 doc/src/sgml/monitoring.sgml                | 116 ++++++++++++++-
 src/backend/catalog/system_views.sql        |  11 ++
 src/backend/postmaster/checkpointer.c       |   1 +
 src/backend/postmaster/pgstat.c             | 153 +++++++++++++++++++-
 src/backend/storage/buffer/bufmgr.c         |  26 +++-
 src/backend/storage/buffer/freelist.c       |  23 ++-
 src/backend/utils/activity/backend_status.c |  64 +++++++-
 src/backend/utils/adt/pgstatfuncs.c         | 116 +++++++++++++++
 src/include/catalog/pg_proc.dat             |   9 ++
 src/include/miscadmin.h                     |   2 +
 src/include/pgstat.h                        |  54 +++++++
 src/include/storage/buf_internals.h         |   4 +-
 src/include/utils/backend_status.h          |  75 ++++++++++
 src/test/regress/expected/rules.out         |   8 +
 src/test/regress/sql/stats.sql              |   4 +
 15 files changed, 647 insertions(+), 19 deletions(-)

+     <row>
 
  </sect2>

 
index be7366379d..931bdcaa59 100644
+		pgstat_inc_buffer_access_type(BA_Fsync, Buf_Shared);
index b7d0fbaefd..3673b34f50 100644
 
 /*
+ * a PgStat_MsgBufferTypeAccesses.
 
+
 
@@ -1461,7 +1468,25 @@ pgstat_reset_shared_counters(const char *target)
 
 	pgstat_setheader(&msg.m_hdr, PGSTAT_MTYPE_RESETSHAREDCOUNTER);
-	pgstat_send(&msg, sizeof(msg));
+
+	if (msg.m_resettarget == RESET_BUFFERS)
+	{
+		int			backend_type;
+		PgStatBufferTypeAccesses accesses[BACKEND_NUM_TYPES];
+
+		memset(accesses, 0, sizeof(accesses));
+		pgstat_report_live_backend_accesses(accesses);
+
+		for (backend_type = 1; backend_type < BACKEND_NUM_TYPES; backend_type++)
+		{
+			msg.m_backend_resets.backend_type = backend_type;
+			memcpy(&msg.m_backend_resets.bta, &accesses[backend_type], sizeof(msg.m_backend_resets.bta));
+			pgstat_send(&msg, sizeof(msg));
+		}
+	}
+	else
+		pgstat_send(&msg, sizeof(msg));
+
 }
 
 /* ----------
@@ -2760,6 +2785,20 @@ pgstat_twophase_postabort(TransactionId xid, uint16 info,
 		rec->tuples_inserted + rec->tuples_updated;
 }
 
+/*
+ *
 
 /* ----------
  * pgstat_fetch_stat_dbentry() -
@@ -2999,6 +3038,14 @@ pgstat_shutdown_hook(int code, Datum arg)
 {
 	Assert(!pgstat_is_shutdown);
 
+	/*
+	 * Only need to send stats on buffer accesses when a process exits, as
+	 * pg_stat_get_buffers_accesses() will read from live backends'
+	 * PgBackendStatus and then sum this with totals from exited backends
+	 * persisted by the stats collector.
+	 */
+	pgstat_send_buffers();
+
 	/*
 	 * If we got as far as discovering our own database ID, we can report what
 	 * we did to the collector.  Otherwise, we'd be sending an invalid
@@ -3092,6 +3139,31 @@ pgstat_send(void *msg, int len)
 #endif
 }
 
+/*
+pgstat_add_buffer_type_accesses(PgStatBufferAccesses *dest, PgBufferAccesses *src, int buffer_num_types)
@@ -3148,6 +3220,32 @@ pgstat_send_bgwriter(void)
 
+/*
+	pgstat_add_buffer_type_accesses(msg.bta.bt_accesses,
@@ -3522,6 +3620,10 @@ PgstatCollectorMain(int argc, char *argv[])
 
+				case PGSTAT_MTYPE_BUFFER_ACTIONS:
+					pgstat_recv_buffer_type_accesses(&msg.msg_buffer_accesses, len);
+					break;
+
 				case PGSTAT_MTYPE_WAL:
 					pgstat_recv_wal(&msg.msg_wal, len);
 					break;
@@ -5221,10 +5323,30 @@ pgstat_recv_resetsharedcounter(PgStat_MsgResetsharedcounter *msg, int len)
 {
 	if (msg->m_resettarget == RESET_BGWRITER)
 	{
-		/* Reset the global, bgwriter and checkpointer statistics for the cluster. */
-		memset(&globalStats, 0, sizeof(globalStats));
+		/*
+		 * Reset the global, bgwriter and checkpointer statistics for the
+		 * cluster.
+		 */
+		memset(&globalStats.checkpointer, 0, sizeof(globalStats.checkpointer));
+		memset(&globalStats.bgwriter, 0, sizeof(globalStats.bgwriter));
 		globalStats.bgwriter.stat_reset_timestamp = GetCurrentTimestamp();
 	}
+	else if (msg->m_resettarget == RESET_BUFFERS)
+	{
+		BackendType backend_type = msg->m_backend_resets.backend_type;
+
+		/*
+		 * Though globalStats.buffers only needs to be reset once, doing so
+		 * for every message is less brittle and the extra cost is
+		 * irrelevant-- given how often stats are reset.
+		 */
+		memset(&globalStats.buffers.accesses, 0, sizeof(globalStats.buffers.accesses));
+		globalStats.buffers.stat_reset_timestamp = GetCurrentTimestamp();
+
+		memcpy(&globalStats.buffers.resets[backend_type],
+			   &msg->m_backend_resets.bta.bt_accesses, sizeof(msg->m_backend_resets.bta.bt_accesses));
+
+	}
 	else if (msg->m_resettarget == RESET_ARCHIVER)
 	{
 		/* Reset the archiver statistics for the cluster. */
@@ -5512,6 +5634,25 @@ pgstat_recv_checkpointer(PgStat_MsgCheckpointer *msg, int len)
 	globalStats.checkpointer.buf_fsync_backend += msg->m_buf_fsync_backend;
 }
 
+static void
+	PgStatBufferAccesses *src_buffer_accesses = msg->bta.bt_accesses;
+	PgStatBufferAccesses *dest_buffer_accesses = globalStats.buffers.accesses[msg->backend_type].bt_accesses;
index e88e4e918b..58bf60425b 100644
 
 	if (isExtend)
 	{
+		pgstat_inc_buffer_access_type(BA_Extend, Buf_Shared);
 		/* new buffers are zero-filled */
 		MemSet((char *) bufBlock, 0, BLCKSZ);
 		/* don't set checksum for all-zero page */
@@ -1172,6 +1173,8 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	/* Loop here in case we have to try another victim buffer */
 	for (;;)
 	{
+		bool		from_ring;
+
 		/*
 		 * Ensure, while the spinlock's not yet held, that there's a free
 		 * refcount entry.
@@ -1182,7 +1185,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 		 * Select a victim buffer.  The buffer is returned with its header
 		 * spinlock still held!
 		 */
-		buf = StrategyGetBuffer(strategy, &buf_state);
+		buf = StrategyGetBuffer(strategy, &buf_state, &from_ring);

@@ -1219,6 +1222,8 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 			if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf),
 										 LW_SHARED))
 			{
+				BufferType	buftype;
+
 				/*
 				 * If using a nondefault strategy, and writing the buffer
 				 * would require a WAL flush, let the strategy decide whether
@@ -1236,7 +1241,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 					UnlockBufHdr(buf, buf_state);
 
@@ -1245,6 +1250,21 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 					}
 				}
 
+				/*
+				 * When a strategy is in use, if the dirty buffer was selected
+				 * from the strategy ring and we did not bother checking the
+				 * freelist or doing a clock sweep to look for a clean shared
+				 * buffer to use, the write will be counted as a strategy
+				 * write. However, if the dirty buffer was obtained from the
+				 * freelist or a clock sweep, it is counted as a regular
+				 * write. When a strategy is not in use, at this point, the
+				 * write can only be a "regular" write of a dirty buffer.
+				 */
+
+				buftype = from_ring ? Buf_Strategy : Buf_Shared;
+				pgstat_inc_buffer_access_type(BA_Write, buftype);
+
+
 				/* OK, do the I/O */
 				TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum,
 														  smgr->smgr_rnode.node.spcNode,
@@ -2552,6 +2572,8 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
 	 * Pin it, share-lock it, write it.  (FlushBuffer will do nothing if the
 	 * buffer is clean by the time we've locked it.)
 	 */
+
+	pgstat_inc_buffer_access_type(BA_Write, Buf_Shared);
 	PinBuffer_Locked(bufHdr);
 	LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
 
index 6be80476db..574965212b 100644

 
+	pgstat_inc_buffer_access_type(BA_Alloc, Buf_Shared);
 
 	/*
@@ -683,8 +686,14 @@ AddBufferToRing(BufferAccessStrategy strategy, BufferDesc *buf)
  * if this buffer should be written and re-used.
  */
 bool
-StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf)
+StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf, bool *from_ring)
 {
+	/*
+	 * If we decide to use the dirty buffer selected by StrategyGetBuffer(),
+	 * then ensure that we count it as such in pg_stat_buffers view.
+	 */
+	*from_ring = true;
+
 	/* We only do this in bulkread mode */
 	if (strategy->btype != BAS_BULKREAD)
 		return false;
@@ -700,5 +709,13 @@ StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf)
 	 */
 	strategy->buffers[strategy->current] = InvalidBuffer;
 
+	/*
+	 * Since we will not be writing out a dirty buffer from the ring, set
+	 * from_ring to false so that the caller does not count this write as a
+	 * "strategy write" and can do proper bookkeeping for pg_stat_buffers.
+	 */
+	*from_ring = false;
+
+
 	return true;
 }
diff --git a/src/backend/utils/activity/backend_status.c b/src/backend/utils/activity/backend_status.c
index 7229598822..d02326423a 100644
--- a/src/backend/utils/activity/backend_status.c
+++ b/src/backend/utils/activity/backend_status.c
@@ -236,6 +236,24 @@ CreateSharedBackendStatus(void)
 #endif
 }
 
+const char *
@@ -279,7 +297,7 @@ pgstat_beinit(void)
@@ -293,6 +311,7 @@ pgstat_bestart(void)
@@ -399,6 +418,15 @@ pgstat_bestart(void)
 
 	/*
 	 * we don't zero st_progress_param here to save cycles; nobody should
@@ -621,6 +649,34 @@ pgstat_report_activity(BackendState state, const char *cmd_str)
 	PGSTAT_END_WRITE_ACTIVITY(beentry);
 }
 
+/*
+ * Iterate through BackendStatusArray and capture live backends' buffer access
+ * stats, adding them to that backend type's member of the backend_accesses
+ * structure.
+ */
+void
+pgstat_report_live_backend_accesses(PgStatBufferTypeAccesses *backend_accesses)
+{
+	int			i;
+	PgBackendStatus *beentry = BackendStatusArray;
+
+	/*
+	 * Loop through live backends and capture reset values
+	 */
+	for (i = 0; i < MaxBackends + NUM_AUXPROCTYPES; i++)
+	{
+		beentry++;
+		/* Don't count dead backends. They should already be counted */
+		if (beentry->st_procpid == 0)
+			continue;
+
+		pgstat_add_buffer_type_accesses(backend_accesses[beentry->st_backendType].bt_accesses,
+										(PgBufferAccesses *) beentry->buffer_access_stats,
+										BUFFER_NUM_TYPES);
+
+	}
+}
+
 /* --------
  * pgstat_report_query_id() -
  *
@@ -1046,6 +1102,12 @@ pgstat_get_my_query_id(void)
 }

+PgBackendStatus *
+pgstat_fetch_backend_statuses(void)
+{
+	return BackendStatusArray;
+}
+
 /* ----------
  * pgstat_fetch_stat_beentry() -
  *
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 513f5aecf6..674a2167ec 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1781,6 +1781,122 @@ pg_stat_get_buf_alloc(PG_FUNCTION_ARGS)
 	PG_RETURN_INT64(pgstat_fetch_stat_bgwriter()->buf_alloc);
 }
 
+Datum
+	int			buffer_type, backend_type;
+	Tuplestorestate *tupstore = pg_stat_make_tuplestore(fcinfo, &tupdesc);
+	enum
+		PgStatBufferAccesses *buffer_accesses = backend_accesses->accesses[backend_type].bt_accesses;
+		PgStatBufferAccesses *resets = backend_accesses->resets[backend_type].bt_accesses;
 
index 90a3016065..6785fb3813 100644
@@ -338,6 +338,8 @@ typedef enum BackendType
 	B_LOGGER,
 } BackendType;

index bcd3588ea2..2e3dfcc01d 100644
 
@@ -331,6 +333,51 @@ typedef struct PgStat_MsgDropdb
 } PgStat_MsgDropdb;

+/*
+ * Structure for counting all types of buffer accesses in the stats collector
+ */
+typedef struct PgStatBufferAccesses
+{
+	PgStat_Counter allocs;
+	PgStat_Counter extends;
+	PgStat_Counter fsyncs;
+	PgStat_Counter writes;
+} PgStatBufferAccesses;
+
+/*
+ * Structure for counting all buffer accesses of all types of buffers.
+ */
+typedef struct PgStatBufferTypeAccesses
+{
+	PgStatBufferAccesses bt_accesses[BUFFER_NUM_TYPES];
+} PgStatBufferTypeAccesses;
+
+/*
+ * Sent by a backend to the stats collector to report all buffer accesses of
+ * all types of buffers for a given type of a backend. This will happen when
+ * the backend exits or when stats are reset.
+ */
+typedef struct PgStat_MsgBufferTypeAccesses
+{
+	PgStat_MsgHdr m_hdr;
+
+	BackendType backend_type;
+	PgStatBufferTypeAccesses bta;
+} PgStat_MsgBufferTypeAccesses;
+
+/*
+ * Structure used by stats collector to keep track of all types of exited
+ * backends' buffer accesses for all types of buffers as well as all stats from
+ * live backends at the time of stats reset. resets is populated using a reset
+ * message sent to the stats collector.
+ */
+typedef struct PgStat_BackendAccesses
+{
+	TimestampTz stat_reset_timestamp;
+	PgStatBufferTypeAccesses accesses[BACKEND_NUM_TYPES];
+	PgStatBufferTypeAccesses resets[BACKEND_NUM_TYPES];
+} PgStat_BackendAccesses;
+
 /* ----------
  * PgStat_MsgResetcounter		Sent by the backend to tell the collector
  *								to reset counters
@@ -351,6 +398,7 @@ typedef struct PgStat_MsgResetsharedcounter
 {
 	PgStat_MsgHdr m_hdr;
 	PgStat_Shared_Reset_Target m_resettarget;
+	PgStat_MsgBufferTypeAccesses m_backend_resets;
 } PgStat_MsgResetsharedcounter;
 
 /* ----------
@@ -703,6 +751,7 @@ typedef union PgStat_Msg
 	PgStat_MsgArchiver msg_archiver;
 	PgStat_MsgBgWriter msg_bgwriter;
 	PgStat_MsgCheckpointer msg_checkpointer;
+	PgStat_MsgBufferTypeAccesses msg_buffer_accesses;
 	PgStat_MsgWal msg_wal;
 	PgStat_MsgSLRU msg_slru;
 	PgStat_MsgFuncstat msg_funcstat;
@@ -879,6 +928,7 @@ typedef struct PgStat_GlobalStats

 /*
@@ -1116,8 +1166,11 @@ extern void pgstat_twophase_postcommit(TransactionId xid, uint16 info,
 extern void pgstat_twophase_postabort(TransactionId xid, uint16 info,
 									  void *recdata, uint32 len);

@@ -1126,6 +1179,7 @@ extern void pgstat_send_wal(bool force);
+extern PgStat_BackendAccesses *pgstat_fetch_exited_backend_buffers(void);
 
 /* freelist.c */
 
index 8042b817df..eb134d82f1 100644

@@ -31,12 +32,48 @@ typedef enum BackendState
 	STATE_DISABLED
 } BackendState;
 
+/* ----------
+ * IO Stats reporting utility types
+ * ----------
+ */
+
+typedef enum BufferAccessType
+{
+	BA_Alloc,
+	BA_Extend,
+	BA_Fsync,
+	BA_Write,
+} BufferAccessType;
+
+#define BUFFER_ACCESS_NUM_TYPES (BA_Write + 1)
+
+typedef enum BufferType
+{
+	Buf_Direct,
+	Buf_Local,
+	Buf_Shared,
+	Buf_Strategy,
+} BufferType;
+
+#define BUFFER_NUM_TYPES (Buf_Strategy + 1)
+
 
 /* ----------
  * Shared-memory data structures
  * ----------
  */
 
+/*
@@ -168,6 +205,7 @@ typedef struct PgBackendStatus

@@ -289,6 +327,10 @@ extern void CreateSharedBackendStatus(void);
  * ----------
  */
 
+/* Utility functions */
+extern const char *GetBufferTypeDesc(BufferType bufferType);
+
+
 /* Initialization functions */
 extern void pgstat_beinit(void);
 extern void pgstat_bestart(void);
@@ -296,7 +338,39 @@ extern void pgstat_bestart(void);
 extern void pgstat_clear_backend_activity_snapshot(void);
 
+typedef struct PgStatBufferTypeAccesses PgStatBufferTypeAccesses;
+pgstat_inc_buffer_access_type(BufferAccessType ba_type, BufferType buf_type)
+extern void pgstat_report_live_backend_accesses(PgStatBufferTypeAccesses *backend_accesses);
@@ -312,6 +386,7 @@ extern uint64 pgstat_get_my_query_id(void);

---

From: Alvaro Herrera <[email-masked]>
Date: Thu, 30 Sep 2021 20:15:13 -0300

Can you say more about 0001?

-- 
Álvaro Herrera         PostgreSQL Developer  —  https://www.EnterpriseDB.com/
"Use it up, wear it out, make it do, or do without"

---

From: Melanie Plageman <[email-masked]>
Date: Fri, 1 Oct 2021 16:05:31 -0400

v13 (attached) contains several cosmetic updates and the full rename
(comments included) of BufferAccessType and BufferType.

> Can you say more about 0001?

The rationale for this patch was that it doesn't save much to avoid
initializing backend activity state in the bootstrap process and by
doing so, I don't have to do the check if (beentry) in pgstat_inc_ioop()
--which happens on most buffer accesses.
From ee11056ad25a095593ba2acc2dc8ff31f4ceb9ab Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Wed, 29 Sep 2021 15:44:51 -0400
Subject: [PATCH v13 4/4] Remove superfluous bgwriter stats

TODO:

index 6debc53ecc..f59ca8b993 100644
 
-     <row>
 
-     <row>
index 8e92b23edc..a27fe0c80c 100644

index 0d18e7f71a..8f2ef63ee5 100644

 
 		pgstat_inc_ioop(IOOP_FSYNC, IOPATH_SHARED);

index 27f4b6ce2f..fbec722a1f 100644
@@ -5608,9 +5608,7 @@ pgstat_recv_archiver(PgStat_MsgArchiver *msg, int len)
 
 /* ----------
@@ -5626,9 +5624,6 @@ pgstat_recv_checkpointer(PgStat_MsgCheckpointer *msg, int len)
 
 static void
index 504cf37ff9..b911dd9ce5 100644

index 5f4b15c9e1..557b2673c0 100644
 
-Datum
 
-Datum
 pg_stat_get_buffers(PG_FUNCTION_ARGS)
index bbdb07b222..a3cdbe1dbc 100644
 
 { oid => '8459', descr => 'statistics: counts of all IO operations done to all IO paths by each type of backend.',
   proname => 'pg_stat_get_buffers', provolatile => 's', proisstrict => 'f',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 94eee19b8e..8ff87a3f54 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -505,9 +505,7 @@ typedef struct PgStat_MsgBgWriter
 {
 	PgStat_MsgHdr m_hdr;

 /* ----------

 
 /*
index 5e5a0324ee..090a65cdb0 100644
     b.io_path,
-- 
2.27.0

From 40c809ad1127322f3462e85be080c10534485f0d Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Fri, 24 Sep 2021 17:39:12 -0400
Subject: [PATCH v13 1/4] Allow bootstrap process to beinit

---

 
 	/*

From a709ddb30b2b747beb214f0b13cd1e1816094e6b Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 30 Sep 2021 16:16:22 -0400
Subject: [PATCH v13 2/4] Add utility to make tuplestores for pg stat views

 
 
+/*

 

 	/* 1-based index */

From e9a5d2a021d429fdbb2daa58ab9d75a069f334d4 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Wed, 29 Sep 2021 15:39:45 -0400
Subject: [PATCH v13 3/4] Add system view tracking IO ops per backend type

Add pg_stat_buffers, a system view which tracks the number of IO
operations (allocs, writes, fsyncs, and extends) done through each IO
path (e.g. shared buffers, local buffers, unbuffered IO) by each type of
backend.

Some of these should always be zero. For example, checkpointer does not
use a BufferAccessStrategy (currently), so the "strategy" IO path for
checkpointer will be 0 for all IO operations (alloc, write, fsync, and
extend).

All backends increment a counter in their PgBackendStatus when
performing an IO operation. On exit, backends send these stats to the
stats collector to be persisted.

When stats are reset, the backend sending the reset message will loop
through and collect all of the live backends' IO op stats, sending a
reset message for each backend type containing these stats. When
receiving this message, the stats collector will 1) save these reset
values in an array of "resets" and 2) zero out the exited backends'
saved IO op counters. This is required for accurate stats after a reset
without writing to other backends' PgBackendStatuses.

When the pg_stat_buffers view is queried, one backend will sum live
backends' stats with saved stats from exited backends and subtract saved
reset stats, returning the total.

Each row of the view is stats for a particular backend type for a
particular IO path (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.

Note that this commit does not add code to increment IO ops for all IO
paths. It includes all possible combinations in the stats view but
doesn't populate all of them.

A separate proposed patch [1] which would add wrappers for smgrwrite()
and extend() would provide a good location to call pgstat_inc_ioop() for
unbuffered IO and avoid regressions for future users of these functions.

TODO:
- catalog bump

Discussion: https://www.postgresql.org/message-id/flat/20210415235954.qcypb4urtovzkat5%40alap3.anarazel.de#724d5cce4bcb587f9167b80a5824bc5c
---
 doc/src/sgml/monitoring.sgml                | 116 ++++++++++++++-
 src/backend/catalog/system_views.sql        |  11 ++
 src/backend/postmaster/checkpointer.c       |   1 +
 src/backend/postmaster/pgstat.c             | 151 +++++++++++++++++++-
 src/backend/storage/buffer/bufmgr.c         |  25 +++-
 src/backend/storage/buffer/freelist.c       |  23 ++-
 src/backend/utils/activity/backend_status.c |  64 ++++++++-
 src/backend/utils/adt/pgstatfuncs.c         | 120 ++++++++++++++++
 src/include/catalog/pg_proc.dat             |   9 ++
 src/include/miscadmin.h                     |   2 +
 src/include/pgstat.h                        |  54 +++++++
 src/include/storage/buf_internals.h         |   4 +-
 src/include/utils/backend_status.h          |  84 +++++++++++
 src/test/regress/expected/rules.out         |   8 ++
 14 files changed, 653 insertions(+), 19 deletions(-)

index 2cd8920645..6debc53ecc 100644
 
+     <row>
+      <entry><structname>pg_stat_buffers</structname><indexterm><primary>pg_stat_buffers</primary></indexterm></entry>
+      <entry>A row for each IO path for each backend type showing
+      statistics about backend IO operations. See
+       <link linkend="monitoring-pg-stat-buffers-view">
+       <structname>pg_stat_buffers</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
@@ -3478,6 +3487,101 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
 
  </sect2>
 
+ <sect2 id="monitoring-pg-stat-buffers-view">
+  <title><structname>pg_stat_buffers</structname></title>
+
+  <indexterm>
+   <primary>pg_stat_buffers</primary>
+  </indexterm>
+
+  <para>
+   The <structname>pg_stat_buffers</structname> view has a row for each backend
+   type for each possible IO path, containing global data for the cluster for
+   that backend and IO path.
+  </para>
+
+  <table id="pg-stat-buffers-view" xreflabel="pg_stat_buffers">
+   <title><structname>pg_stat_buffers</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).
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>io_path</structfield> <type>text</type>
+      </para>
+      <para>
+       IO path taken (e.g. shared buffers, direct).
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>alloc</structfield> <type>integer</type>
+      </para>
+      <para>
+       Number of buffers allocated.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>extend</structfield> <type>integer</type>
+      </para>
+      <para>
+       Number of buffers extended.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>fsync</structfield> <type>integer</type>
+      </para>
+      <para>
+       Number of buffers fsynced.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>write</structfield> <type>integer</type>
+      </para>
+      <para>
+       Number of buffers written.
+      </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-wal-view">
    <title><structname>pg_stat_wal</structname></title>
 
index 55f6e3711d..8e92b23edc 100644
 
+CREATE VIEW pg_stat_buffers AS
+SELECT
+       b.backend_type,
+       b.io_path,
+       b.alloc,
+       b.extend,
+       b.fsync,
+       b.write,
+       b.stats_reset
+FROM pg_stat_get_buffers() b;
+
 CREATE VIEW pg_stat_wal AS
     SELECT
         w.wal_records,
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index be7366379d..0d18e7f71a 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -1104,6 +1104,7 @@ ForwardSyncRequest(const FileTag *ftag, SyncRequestType type)
 		 */
 		if (!AmBackgroundWriterProcess())
 			CheckpointerShmem->num_backend_fsync++;
+		pgstat_inc_ioop(IOOP_FSYNC, IOPATH_SHARED);
 		LWLockRelease(CheckpointerCommLock);
 		return false;
 	}
diff --git a/src/backend/postmaster/pgstat.c b/src/backend/postmaster/pgstat.c
index b7d0fbaefd..27f4b6ce2f 100644
--- a/src/backend/postmaster/pgstat.c
+++ b/src/backend/postmaster/pgstat.c
@@ -124,9 +124,12 @@ char	   *pgstat_stat_filename = NULL;
 char	   *pgstat_stat_tmpname = NULL;
 
 /*
+ * statistics on various IO ops are tracked in PgBackendStatus while a backend
+ * is alive and then sent to stats collector before a backend exits in a
+ * PgStat_MsgIOPathOps.
+static void pgstat_recv_io_path_ops(PgStat_MsgIOPathOps *msg, int len);
@@ -1452,6 +1456,8 @@ pgstat_reset_shared_counters(const char *target)
@@ -1461,7 +1467,25 @@ pgstat_reset_shared_counters(const char *target)
 
 	pgstat_setheader(&msg.m_hdr, PGSTAT_MTYPE_RESETSHAREDCOUNTER);
-	pgstat_send(&msg, sizeof(msg));
+
+	if (msg.m_resettarget == RESET_BUFFERS)
+	{
+		int			backend_type;
+		PgStatIOPathOps ops[BACKEND_NUM_TYPES];
+
+		memset(ops, 0, sizeof(ops));
+		pgstat_report_live_backend_io_path_ops(ops);
+
+		for (backend_type = 1; backend_type < BACKEND_NUM_TYPES; backend_type++)
+		{
+			msg.m_backend_resets.backend_type = backend_type;
+			memcpy(&msg.m_backend_resets.iop, &ops[backend_type], sizeof(msg.m_backend_resets.iop));
+			pgstat_send(&msg, sizeof(msg));
+		}
+	}
+	else
+		pgstat_send(&msg, sizeof(msg));
+
 }
 
 /* ----------
@@ -2760,6 +2784,19 @@ pgstat_twophase_postabort(TransactionId xid, uint16 info,
 		rec->tuples_inserted + rec->tuples_updated;
 }
 
+/*
+ *	Support function for SQL-callable pgstat* functions. Returns a pointer to
+ *	the PgStat_BackendIOPathOps structure tracking IO op statistics for both
+ *	exited backends and reset arithmetic.
+ */
+PgStat_BackendIOPathOps *
+pgstat_fetch_exited_backend_buffers(void)
+{
+	backend_read_statsfile();
+
+	return &globalStats.buffers;
+}
+
 
 /* ----------
  * pgstat_fetch_stat_dbentry() -
@@ -2999,6 +3036,14 @@ pgstat_shutdown_hook(int code, Datum arg)
 {
 	Assert(!pgstat_is_shutdown);
 
+	/*
+	 * Only need to send stats on IO Ops for IO Paths when a process exits, as
+	 * pg_stat_get_buffers() will read from live backends' PgBackendStatus and
+	 * then sum this with totals from exited backends persisted by the stats
+	 * collector.
+	 */
+	pgstat_send_buffers();
+
 	/*
 	 * If we got as far as discovering our own database ID, we can report what
 	 * we did to the collector.  Otherwise, we'd be sending an invalid
@@ -3092,6 +3137,30 @@ pgstat_send(void *msg, int len)
 #endif
 }
 
+/*
+ * Add live IO Op stats for all IO Paths (e.g. shared, local) to those in the
+ * equivalent stats structure for exited backends. Note that this adds and
+ * doesn't set, so the destination stats structure should be zeroed out by the
+ * caller initially. This would commonly be used to transfer all IO Op stats
+ * for all IO Paths for a particular backend type to the pgstats structure.
+ */
+void
+pgstat_add_io_path_ops(PgStatIOOps *dest, IOOps *src, int io_path_num_types)
+{
+	int			io_path;
+
+	for (io_path = 0; io_path < io_path_num_types; io_path++)
+	{
+		dest->allocs += pg_atomic_read_u64(&src->allocs);
+		dest->extends += pg_atomic_read_u64(&src->extends);
+		dest->fsyncs += pg_atomic_read_u64(&src->fsyncs);
+		dest->writes += pg_atomic_read_u64(&src->writes);
+		dest++;
+		src++;
+	}
+
+}
+
 /* ----------
  * pgstat_send_archiver() -
  *
@@ -3148,6 +3217,32 @@ pgstat_send_bgwriter(void)
 	MemSet(&PendingBgWriterStats, 0, sizeof(PendingBgWriterStats));
 }
 
+/*
+ * Before exiting, a backend sends its IO op statistics to the collector so
+ * that they may be persisted.
+ */
+void
+pgstat_send_buffers(void)
+{
+	PgStat_MsgIOPathOps msg;
+
+	PgBackendStatus *beentry = MyBEEntry;
+
+	if (!beentry)
+		return;
+
+	memset(&msg, 0, sizeof(msg));
+	msg.backend_type = beentry->st_backendType;
+
+	pgstat_add_io_path_ops(msg.iop.io_path_ops,
+						   (IOOps *) &beentry->io_path_stats,
+						   IOPATH_NUM_TYPES);
+
+	pgstat_setheader(&msg.m_hdr, PGSTAT_MTYPE_IO_PATH_OPS);
+	pgstat_send(&msg, sizeof(msg));
+}
+
+
 /* ----------
  * pgstat_send_checkpointer() -
  *
@@ -3522,6 +3617,10 @@ PgstatCollectorMain(int argc, char *argv[])
 					pgstat_recv_checkpointer(&msg.msg_checkpointer, len);
 					break;
 
+				case PGSTAT_MTYPE_IO_PATH_OPS:
+					pgstat_recv_io_path_ops(&msg.msg_io_path_ops, len);
+					break;
+
 				case PGSTAT_MTYPE_WAL:
 					pgstat_recv_wal(&msg.msg_wal, len);
 					break;
@@ -5221,10 +5320,30 @@ pgstat_recv_resetsharedcounter(PgStat_MsgResetsharedcounter *msg, int len)
 {
 	if (msg->m_resettarget == RESET_BGWRITER)
 	{
-		/* Reset the global, bgwriter and checkpointer statistics for the cluster. */
-		memset(&globalStats, 0, sizeof(globalStats));
+		/*
+		 * Reset the global, bgwriter and checkpointer statistics for the
+		 * cluster.
+		 */
+		memset(&globalStats.checkpointer, 0, sizeof(globalStats.checkpointer));
+		memset(&globalStats.bgwriter, 0, sizeof(globalStats.bgwriter));
 		globalStats.bgwriter.stat_reset_timestamp = GetCurrentTimestamp();
 	}
+	else if (msg->m_resettarget == RESET_BUFFERS)
+	{
+		BackendType backend_type = msg->m_backend_resets.backend_type;
+
+		/*
+		 * Though globalStats.buffers only needs to be reset once, doing so
+		 * for every message is less brittle and the extra cost is irrelevant
+		 * given how often stats are reset.
+		 */
+		memset(&globalStats.buffers.ops, 0, sizeof(globalStats.buffers.ops));
+		globalStats.buffers.stat_reset_timestamp = GetCurrentTimestamp();
+
+		memcpy(&globalStats.buffers.resets[backend_type],
+			   &msg->m_backend_resets.iop.io_path_ops, sizeof(msg->m_backend_resets.iop.io_path_ops));
+
+	}
 	else if (msg->m_resettarget == RESET_ARCHIVER)
 	{
 		/* Reset the archiver statistics for the cluster. */
@@ -5512,6 +5631,26 @@ pgstat_recv_checkpointer(PgStat_MsgCheckpointer *msg, int len)
 	globalStats.checkpointer.buf_fsync_backend += msg->m_buf_fsync_backend;
 }
 
+static void
+pgstat_recv_io_path_ops(PgStat_MsgIOPathOps *msg, int len)
+{
+	int			io_path;
+	PgStatIOOps *src_io_path_ops = msg->iop.io_path_ops;
+	PgStatIOOps *dest_io_path_ops =
+	globalStats.buffers.ops[msg->backend_type].io_path_ops;
+
+	for (io_path = 0; io_path < IOPATH_NUM_TYPES; io_path++)
+	{
+		PgStatIOOps *src = &src_io_path_ops[io_path];
+		PgStatIOOps *dest = &dest_io_path_ops[io_path];
+
+		dest->allocs += src->allocs;
+		dest->extends += src->extends;
+		dest->fsyncs += src->fsyncs;
+		dest->writes += src->writes;
+	}
+}
+
 /* ----------
  * pgstat_recv_wal() -
  *
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index e88e4e918b..504cf37ff9 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -972,6 +972,7 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 
 	if (isExtend)
 	{
+		pgstat_inc_ioop(IOOP_EXTEND, IOPATH_SHARED);

+				IOPath		iopath;
 
@@ -1245,6 +1250,20 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 					}
 				}
 
+				/*
+				iopath = from_ring ? IOPATH_STRATEGY : IOPATH_SHARED;
+				pgstat_inc_ioop(IOOP_WRITE, iopath);
+	pgstat_inc_ioop(IOOP_WRITE, IOPATH_SHARED);
 
index 6be80476db..e2e1c3bf56 100644

 
+	pgstat_inc_ioop(IOOP_ALLOC, IOPATH_SHARED);
 
 	/*
 
+	/*
index 7229598822..f326297517 100644
 
+const char *
+GetIOPathDesc(IOPath io_path)
+{
+
+	switch (io_path)
+	{
+		case IOPATH_DIRECT:
+			return "direct";
+		case IOPATH_LOCAL:
+			return "local";
+		case IOPATH_SHARED:
+			return "shared";
+		case IOPATH_STRATEGY:
+			return "strategy";
+	}
+	return "unknown IO path";
+}
+
 /*
  * Initialize pgstats backend activity state, and set up our on-proc-exit
  * hook.  Called from InitPostgres and AuxiliaryProcessMain. For auxiliary
@@ -279,7 +297,7 @@ pgstat_beinit(void)
  * pgstat_bestart() -
  *
  *	Initialize this backend's entry in the PgBackendStatus array.
- *	Called from InitPostgres.
+ *	Called from InitPostgres and AuxiliaryProcessMain
  *
  *	Apart from auxiliary processes, MyBackendId, MyDatabaseId,
  *	session userid, and application_name must be set for a
@@ -293,6 +311,7 @@ pgstat_bestart(void)
 {
 	volatile PgBackendStatus *vbeentry = MyBEEntry;
 	PgBackendStatus lbeentry;
+	int			io_path;
 #ifdef USE_SSL
 	PgBackendSSLStatus lsslstatus;
 #endif
@@ -399,6 +418,15 @@ pgstat_bestart(void)
 	lbeentry.st_progress_command = PROGRESS_COMMAND_INVALID;
 	lbeentry.st_progress_command_target = InvalidOid;
 	lbeentry.st_query_id = UINT64CONST(0);
+	for (io_path = 0; io_path < IOPATH_NUM_TYPES; io_path++)
+	{
+		IOOps	   *io_ops = &lbeentry.io_path_stats[io_path];
+
+		pg_atomic_init_u64(&io_ops->allocs, 0);
+		pg_atomic_init_u64(&io_ops->extends, 0);
+		pg_atomic_init_u64(&io_ops->fsyncs, 0);
+		pg_atomic_init_u64(&io_ops->writes, 0);
+	}
 
 	/*
 
+/*
+ * Iterate through BackendStatusArray and capture live backends' stats on IO
+ * Ops for all IO Paths, adding them to that backend type's member of the
+ * backend_io_path_ops structure.
+ */
+void
+pgstat_report_live_backend_io_path_ops(PgStatIOPathOps *backend_io_path_ops)
+{
+	int			i;
+	PgBackendStatus *beentry = BackendStatusArray;
+
+	/*
+	 * Loop through live backends and capture reset values
+	 */
+	for (i = 0; i < MaxBackends + NUM_AUXPROCTYPES; i++)
+	{
+		beentry++;
+		/* Don't count dead backends. They should already be counted */
+		if (beentry->st_procpid == 0)
+			continue;
+
+		pgstat_add_io_path_ops(backend_io_path_ops[beentry->st_backendType].io_path_ops,
+							   (IOOps *) beentry->io_path_stats,
+							   IOPATH_NUM_TYPES);
+
+	}
+}
+
 /* --------
  * pgstat_report_query_id() -
  *
@@ -1046,6 +1102,12 @@ pgstat_get_my_query_id(void)
 }

+PgBackendStatus *
+pgstat_fetch_backend_statuses(void)
+{
+	return BackendStatusArray;
+}
+
 /* ----------
  * pgstat_fetch_stat_beentry() -
  *
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 513f5aecf6..5f4b15c9e1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1781,6 +1781,126 @@ pg_stat_get_buf_alloc(PG_FUNCTION_ARGS)
 	PG_RETURN_INT64(pgstat_fetch_stat_bgwriter()->buf_alloc);
 }
 
+Datum
+pg_stat_get_buffers(PG_FUNCTION_ARGS)
+{
+#define NROWS ((BACKEND_NUM_TYPES - 1) * IOPATH_NUM_TYPES)
+	PgStat_BackendIOPathOps *backend_io_path_ops;
+	int			i;
+	int			io_path,
+				backend_type;
+	Datum		reset_time;
+	PgBackendStatus *beentry;
+	TupleDesc	tupdesc;
+
+	Tuplestorestate *tupstore = pg_stat_make_tuplestore(fcinfo, &tupdesc);
+
+	/*
+	 * When adding a new column to the pg_stat_buffers view, add a new enum
+	 * value here above COLUMN_LENGTH.
+	 */
+	enum
+	{
+		COLUMN_BACKEND_TYPE,
+		COLUMN_IO_PATH,
+		COLUMN_ALLOCS,
+		COLUMN_EXTENDS,
+		COLUMN_FSYNCS,
+		COLUMN_WRITES,
+		COLUMN_RESET_TIME,
+		COLUMN_LENGTH,
+	};
+
+	Datum		all_values[NROWS][COLUMN_LENGTH];
+	bool		all_nulls[NROWS][COLUMN_LENGTH];
+
+	memset(all_values, 0, sizeof(all_values));
+	memset(all_nulls, 0, sizeof(all_nulls));
+
+	/*
+	 * Loop through all live backends and count their IO Ops for each IO Path
+	 */
+	beentry = pgstat_fetch_backend_statuses();
+
+	for (i = 0; i < MaxBackends + NUM_AUXPROCTYPES; i++)
+	{
+		IOOps	   *io_ops;
+
+		beentry++;
+		/* Don't count dead backends. They should already be counted */
+		if (beentry->st_procpid == 0)
+			continue;
+
+		io_ops = beentry->io_path_stats;
+
+		for (io_path = 0; io_path < IOPATH_NUM_TYPES; io_path++)
+		{
+			/*
+			 * Subtract 1 from backend_type to avoid having rows for B_INVALID
+			 * BackendType
+			 */
+			int			rownum = (beentry->st_backendType - 1) * IOPATH_NUM_TYPES + io_path;
+			Datum	   *values = all_values[rownum];
+
+			/*
+			 * COLUMN_RESET_TIME, COLUMN_BACKEND_TYPE, and COLUMN_IO_PATH will
+			 * all be set when looping through exited backends array
+			 */
+			values[COLUMN_ALLOCS] += pg_atomic_read_u64(&io_ops->allocs);
+			values[COLUMN_EXTENDS] += pg_atomic_read_u64(&io_ops->extends);
+			values[COLUMN_FSYNCS] += pg_atomic_read_u64(&io_ops->fsyncs);
+			values[COLUMN_WRITES] += pg_atomic_read_u64(&io_ops->writes);
+			io_ops++;
+		}
+	}
+
+	/* Add stats from all exited backends */
+	backend_io_path_ops = pgstat_fetch_exited_backend_buffers();
+
+	reset_time = TimestampTzGetDatum(backend_io_path_ops->stat_reset_timestamp);
+
+	/* 0 is not a valid BackendType */
+	for (backend_type = 1; backend_type < BACKEND_NUM_TYPES; backend_type++)
+	{
+		PgStatIOOps *io_ops = backend_io_path_ops->ops[backend_type].io_path_ops;
+		PgStatIOOps *resets = backend_io_path_ops->resets[backend_type].io_path_ops;
+
+		Datum		backend_type_desc = CStringGetTextDatum(GetBackendTypeDesc(backend_type));
+
+		for (io_path = 0; io_path < IOPATH_NUM_TYPES; io_path++)
+		{
+			/*
+			 * Subtract 1 from backend_type to avoid having rows for B_INVALID
+			 * BackendType
+			 */
+			Datum	   *values = all_values[(backend_type - 1) * IOPATH_NUM_TYPES + io_path];
+
+			values[COLUMN_BACKEND_TYPE] = backend_type_desc;
+			values[COLUMN_IO_PATH] = CStringGetTextDatum(GetIOPathDesc(io_path));
+			values[COLUMN_ALLOCS] = values[COLUMN_ALLOCS] + io_ops->allocs - resets->allocs;
+			values[COLUMN_EXTENDS] = values[COLUMN_EXTENDS] + io_ops->extends - resets->extends;
+			values[COLUMN_FSYNCS] = values[COLUMN_FSYNCS] + io_ops->fsyncs - resets->fsyncs;
+			values[COLUMN_WRITES] = values[COLUMN_WRITES] + io_ops->writes - resets->writes;
+			values[COLUMN_RESET_TIME] = reset_time;
+			io_ops++;
+			resets++;
+		}
+	}
+
+	for (i = 0; i < NROWS; i++)
+	{
+		Datum	   *values = all_values[i];
+		bool	   *nulls = all_nulls[i];
+
+		tuplestore_putvalues(tupstore, tupdesc, values, nulls);
+	}
+
+	/* clean up and return the tuplestore */
+	tuplestore_donestoring(tupstore);
+
+	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 d068d6532e..bbdb07b222 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5642,6 +5642,15 @@
   proname => 'pg_stat_get_buf_alloc', provolatile => 's', proparallel => 'r',
   prorettype => 'int8', proargtypes => '', prosrc => 'pg_stat_get_buf_alloc' },
 
+{ oid => '8459', descr => 'statistics: counts of all IO operations done to all IO paths by each type of backend.',
+  proname => 'pg_stat_get_buffers', provolatile => 's', proisstrict => 'f',
+  prorows => '52', proretset => 't',
+  proparallel => 'r', prorettype => 'record', proargtypes => '',
+  proallargtypes => '{text,text,int8,int8,int8,int8,timestamptz}',
+  proargmodes => '{o,o,o,o,o,o,o}',
+  proargnames => '{backend_type,io_path,alloc,extend,fsync,write,stats_reset}',
+  prosrc => 'pg_stat_get_buffers' },
+
 { 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/include/miscadmin.h b/src/include/miscadmin.h
index 90a3016065..6785fb3813 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -338,6 +338,8 @@ typedef enum BackendType
 	B_LOGGER,
 } BackendType;

index bcd3588ea2..94eee19b8e 100644
+	PGSTAT_MTYPE_IO_PATH_OPS,

+/*
+ * Structure for counting all types of IO ops in the stats collector
+ */
+typedef struct PgStatIOOps
+{
+	PgStat_Counter allocs;
+	PgStat_Counter extends;
+	PgStat_Counter fsyncs;
+	PgStat_Counter writes;
+} PgStatIOOps;
+
+/*
+ * Structure for counting all IO ops on all types of buffers.
+ */
+typedef struct PgStatIOPathOps
+{
+	PgStatIOOps io_path_ops[IOPATH_NUM_TYPES];
+} PgStatIOPathOps;
+
+/*
+ * Sent by a backend to the stats collector to report all IO Ops for all IO
+ * Paths for a given type of a backend. This will happen when the backend exits
+ * or when stats are reset.
+ */
+typedef struct PgStat_MsgIOPathOps
+{
+	PgStat_MsgHdr m_hdr;
+
+	BackendType backend_type;
+	PgStatIOPathOps iop;
+} PgStat_MsgIOPathOps;
+
+/*
+ * Structure used by stats collector to keep track of all types of exited
+ * backends' IO Ops for all IO Paths as well as all stats from live backends at
+ * the time of stats reset. resets is populated using a reset message sent to
+ * the stats collector.
+ */
+typedef struct PgStat_BackendIOPathOps
+{
+	TimestampTz stat_reset_timestamp;
+	PgStatIOPathOps ops[BACKEND_NUM_TYPES];
+	PgStatIOPathOps resets[BACKEND_NUM_TYPES];
+} PgStat_BackendIOPathOps;
+
 /* ----------
  * PgStat_MsgResetcounter		Sent by the backend to tell the collector
  *								to reset counters
@@ -351,6 +398,7 @@ typedef struct PgStat_MsgResetsharedcounter
 {
 	PgStat_MsgHdr m_hdr;
 	PgStat_Shared_Reset_Target m_resettarget;
+	PgStat_MsgIOPathOps m_backend_resets;
 } PgStat_MsgResetsharedcounter;
 
 /* ----------
+	PgStat_MsgIOPathOps msg_io_path_ops;
 
 	PgStat_CheckpointerStats checkpointer;
 	PgStat_BgWriterStats bgwriter;
+	PgStat_BackendIOPathOps buffers;
 } PgStat_GlobalStats;
 
 /*
 
+extern void pgstat_add_io_path_ops(PgStatIOOps *dest,
+								   IOOps *src, int io_path_num_types);
 extern void pgstat_send_archiver(const char *xlog, bool failed);
 extern void pgstat_send_bgwriter(void);
+extern void pgstat_send_buffers(void);
 extern void pgstat_send_checkpointer(void);
 extern void pgstat_send_wal(bool force);
 
+extern PgStat_BackendIOPathOps *pgstat_fetch_exited_backend_buffers(void);
 
 /* freelist.c */
 
index 8042b817df..419de72591 100644

+/* ----------
+ * IO Stats reporting utility types
+ * ----------
+ */
+
+typedef enum IOOp
+{
+	IOOP_ALLOC,
+	IOOP_EXTEND,
+	IOOP_FSYNC,
+	IOOP_WRITE,
+} IOOp;
+
+#define IOOP_NUM_TYPES (IOOP_WRITE + 1)
+
+typedef enum IOPath
+{
+	IOPATH_DIRECT,
+	IOPATH_LOCAL,
+	IOPATH_SHARED,
+	IOPATH_STRATEGY,
+} IOPath;
+
+#define IOPATH_NUM_TYPES (IOPATH_STRATEGY + 1)
+
 
 /* ----------
 
+/*
+ * Structure for counting all types of IOOps for a live backend.
+ */
+typedef struct IOOps
+{
+	pg_atomic_uint64 allocs;
+	pg_atomic_uint64 extends;
+	pg_atomic_uint64 fsyncs;
+	pg_atomic_uint64 writes;
+} IOOps;
+
 /*
  * PgBackendSSLStatus
  *
@@ -168,6 +205,16 @@ typedef struct PgBackendStatus
 
 	/* query identifier, optionally computed using post_parse_analyze_hook */
 	uint64		st_query_id;
+
+	/*
+	 * Stats on all IO Ops for all IO Paths for this backend. When the
+	 * pg_stat_buffers view is queried and when stats are reset, one backend
+	 * will read io_path_stats from all live backends and combine them with
+	 * io_path_stats from exited backends for each backend type. When this
+	 * backend exits, it will send io_path_stats to the stats collector to be
+	 * persisted.
+	 */
+	IOOps		io_path_stats[IOPATH_NUM_TYPES];
 } PgBackendStatus;

@@ -289,6 +336,10 @@ extern void CreateSharedBackendStatus(void);
  * ----------
  */
 
+/* Utility functions */
+extern const char *GetIOPathDesc(IOPath io_path);
+
+
 /* Initialization functions */
 extern void pgstat_beinit(void);
 extern void pgstat_bestart(void);
@@ -296,7 +347,39 @@ extern void pgstat_bestart(void);
 extern void pgstat_clear_backend_activity_snapshot(void);
 
 /* Activity reporting functions */
+typedef struct PgStatIOPathOps PgStatIOPathOps;
+
+static inline void
+pgstat_inc_ioop(IOOp io_op, IOPath io_path)
+{
+	IOOps	   *io_ops;
+	PgBackendStatus *beentry = MyBEEntry;
+
+	Assert(beentry);
+
+	io_ops = &beentry->io_path_stats[io_path];
+	switch (io_op)
+	{
+		case IOOP_ALLOC:
+			pg_atomic_write_u64(&io_ops->allocs,
+								pg_atomic_read_u64(&io_ops->allocs) + 1);
+			break;
+		case IOOP_EXTEND:
+			pg_atomic_write_u64(&io_ops->extends,
+								pg_atomic_read_u64(&io_ops->extends) + 1);
+			break;
+		case IOOP_FSYNC:
+			pg_atomic_write_u64(&io_ops->fsyncs,
+								pg_atomic_read_u64(&io_ops->fsyncs) + 1);
+			break;
+		case IOOP_WRITE:
+			pg_atomic_write_u64(&io_ops->writes,
+								pg_atomic_read_u64(&io_ops->writes) + 1);
+			break;
+	}
+}
 extern void pgstat_report_activity(BackendState state, const char *cmd_str);
+extern void pgstat_report_live_backend_io_path_ops(PgStatIOPathOps *backend_io_path_ops);
 extern void pgstat_report_query_id(uint64 query_id, bool force);
 extern void pgstat_report_tempfile(size_t filesize);
 extern void pgstat_report_appname(const char *appname);
@@ -312,6 +395,7 @@ extern uint64 pgstat_get_my_query_id(void);
  * generate the pgstat* views.
  * ----------
  */
+extern PgBackendStatus *pgstat_fetch_backend_statuses(void);
 extern int	pgstat_fetch_stat_numbackends(void);
 extern PgBackendStatus *pgstat_fetch_stat_beentry(int beid);
 extern LocalPgBackendStatus *pgstat_fetch_stat_local_beentry(int beid);
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2fa00a3c29..5e5a0324ee 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1828,6 +1828,14 @@ pg_stat_bgwriter| SELECT pg_stat_get_bgwriter_timed_checkpoints() AS checkpoints
     pg_stat_get_buf_fsync_backend() AS buffers_backend_fsync,
     pg_stat_get_buf_alloc() AS buffers_alloc,
     pg_stat_get_bgwriter_stat_reset_time() AS stats_reset;
+pg_stat_buffers| SELECT b.backend_type,
+    b.io_path,
+    b.alloc,
+    b.extend,
+    b.fsync,
+    b.write,
+    b.stats_reset
+   FROM pg_stat_get_buffers() b(backend_type, io_path, alloc, extend, fsync, write, stats_reset);
 pg_stat_database| SELECT d.oid AS datid,
     d.datname,
         CASE
-- 
2.27.0

---

From: Andres Freund <[email-masked]>
Date: Fri, 8 Oct 2021 10:56:20 -0700

Hi,

>  	 * Load relcache entries for the shared system catalogs.  This must create

I think it's good to remove more and more of these !bootstrap cases - they
really make it harder to understand the state of the system at various
points. Optimizing for the rarely executed bootstrap mode at the cost of
checks in very common codepaths...

> +++ b/src/backend/utils/adt/pgstatfuncs.c

Is pgstattuple the best place for this helper? It's not really pgstatfuncs
specific...

It also looks vaguely familiar - I wonder if we have a helper roughly like
this somewhere else already...

>  		LWLockRelease(CheckpointerCommLock);

ISTM this doens't need to happen while holding CheckpointerCommLock?

> +		pgstat_send(&msg, sizeof(msg));

I'd perhaps put this in a small helper function.

I think it might be nicer to move pgstat_beshutdown_hook() to be a
before_shmem_exit(), and do this in there.

> + * for all IO Paths for a particular backend type to the pgstats structure.

This seems a bit odd. Why not zero it in here? Perhaps it also should be
called something like _sum_ instead of _add_?

> +pgstat_add_io_path_ops(PgStatIOOps *dest, IOOps *src, int io_path_num_types)

Why is io_path_num_types a parameter?

> +		dest->writes += src->writes;

Could this, with a bit of finessing, use pgstat_add_io_path_ops()?

> +++ b/src/backend/storage/buffer/bufmgr.c

What about writes originating in like FlushRelationBuffers()?

> +	 * then ensure that we count it as such in pg_stat_buffers view.

Absolutely minor nitpick: Somehow it feelsoff to talk about the view here.

> +	return BackendStatusArray;

Hm, not sure this adds much?

> +			int			rownum = (beentry->st_backendType - 1) * IOPATH_NUM_TYPES + io_path;

Perhaps worth wrapping this in a macro or inline function? It's repeated and nontrivial.

> +	backend_io_path_ops = pgstat_fetch_exited_backend_buffers();

It's probably *not* worth it, but I do wonder if we should do the addition on the SQL
level, and actually have two functions, one returning data for exited
backends, and one for currently connected ones.

> +								pg_atomic_read_u64(&io_ops->writes) + 1);

IIRC Thomas Munro had a patch adding a nonatomic_add or such
somewhere. Perhaps in the recovery readahead thread? Might be worth using
instead?

Greetings,

Andres Freund

---

From: Melanie Plageman <[email-masked]>
Date: Mon, 11 Oct 2021 16:48:01 -0400

> checks in very common codepaths...

What scope do you suggest for this patch set? A single patch which does
this in more locations (remove !bootstrap) or should I remove this patch
from the patchset?

> this somewhere else already...

I don't see a function which is specifically a utility to make a
tuplestore. Looking at the callers of tuplestore_begin_heap(), I notice
very similar code to the function I added in pg_tablespace_databases()
in utils/adt/misc.c, pg_stop_backup_v2() in xlogfuncs.c,
pg_event_trigger_dropped_objects() and pg_event_trigger_ddl_commands in
event_tigger.c, pg_available_extensions in extension.c, etc.

Do you think it makes sense to refactor this code out of all of these
places? If so, where would such a utility function belong?

> ISTM this doens't need to happen while holding CheckpointerCommLock?

Fixed in attached updates. I only attached the diff from my previous version.

> I'd perhaps put this in a small helper function.

Done.

> before_shmem_exit(), and do this in there.

Not really sure the correct way to do this. A cursory attempt to do so
failed because ShutdownXLOG() is also registered as a
before_shmem_exit() and ends up being called after
pgstat_beshutdown_hook(). pgstat_beshutdown_hook() zeroes out
PgBackendStatus, ShutdownXLOG() initiates a checkpoint, and during a
checkpoint, the checkpointer increments IO op counter for writes in its
PgBackendStatus.

> called something like _sum_ instead of _add_?

I wanted to be able to use the function both when it was setting the
values and when it needed to add to the values (which are the two
current callers). I have changed the name from add -> sum.

> Why is io_path_num_types a parameter?

I imagined that maybe another caller would want to only add some IO path
types and still use the function, but I think it is more confusing than
anything else so I've changed it.

> Could this, with a bit of finessing, use pgstat_add_io_path_ops()?

I didn't really see a good way to do this -- given that
pgstat_add_io_path_ops() adds IOOps members to PgStatIOOps members --
which requires a pg_atomic_read_u64() and pgstat_recv_io_path_ops adds
PgStatIOOps to PgStatIOOps which doesn't require pg_atomic_read_u64().
Maybe I could pass a flag which, based on the type, either does or
doesn't use pg_atomic_read_u64 to access the value? But that seems worse
to me.

> What about writes originating in like FlushRelationBuffers()?

Yes, I have made IOPath a parameter to FlushBuffer() so that it can
distinguish between strategy buffer writes and shared buffer writes and
then pushed pgstat_inc_ioop() into FlushBuffer().

> Absolutely minor nitpick: Somehow it feelsoff to talk about the view here.

Fixed.

> Hm, not sure this adds much?

Is there a better way to access the whole BackendStatusArray from within
pgstatfuncs.c?

> Perhaps worth wrapping this in a macro or inline function? It's repeated and nontrivial.

Done.

> backends, and one for currently connected ones.

It would be easy enough to implement. I would defer to others on whether
or not this would be useful. My use case for pg_stat_buffers() is to see
what backends' IO during a benchmark or test workload. For that, I reset
the stats before and then query pg_stat_buffers after running the
benchmark. I don't know if I would use exited and live stats
individually. In a real workload, I could see using
pg_stat_buffers live and exited to see if the workload causing lots of
backends to do their own writes is ongoing. Though a given workload may
be composed of lots of different queries, with backends exiting
throughout.

> somewhere. Perhaps in the recovery readahead thread? Might be worth using

I've added Thomas' function in a separate commit. I looked for a better
place to add it (I was thinking somewhere in src/backend/utils/misc) but
couldn't find anywhere that made sense.

I also added a call to pgstat_inc_ioop() in ProcessSyncRequests() to capture
when the checkpointer does fsyncs.

I also added pgstat_inc_ioop() calls to callers of smgrwrite() flushing local
buffers. I don't know if that is desirable or not in this patch. They could be
removed if wrappers for smgrwrite() go in and pgstat_inc_ioop() can be called
from within those wrappers.

- Melanie
From 977cd26ee8b489772b99de46330c9492a1839b6d Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Mon, 11 Oct 2021 13:43:50 -0400
Subject: [PATCH 2/2] updates

---
 src/backend/postmaster/checkpointer.c       |   2 +-
 src/backend/postmaster/pgstat.c             | 104 +++++++++++---------
 src/backend/storage/buffer/bufmgr.c         |  25 +++--
 src/backend/storage/buffer/freelist.c       |   6 +-
 src/backend/storage/buffer/localbuf.c       |   3 +
 src/backend/storage/sync/sync.c             |   1 +
 src/backend/utils/activity/backend_status.c |   5 +-
 src/backend/utils/adt/pgstatfuncs.c         |  61 ++++++------
 src/include/pgstat.h                        |   3 +-
 src/include/utils/backend_status.h          |  12 +--
 10 files changed, 119 insertions(+), 103 deletions(-)

diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 8f2ef63ee5..dec325e40e 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -1083,12 +1083,12 @@ ForwardSyncRequest(const FileTag *ftag, SyncRequestType type)
 		(CheckpointerShmem->num_requests >= CheckpointerShmem->max_requests &&
 		 !CompactCheckpointerRequestQueue()))
 	{
+		LWLockRelease(CheckpointerCommLock);
 		/*
 		 * Count the subset of writes where backends have to do their own
 		 * fsync
 		 */
 		pgstat_inc_ioop(IOOP_FSYNC, IOPATH_SHARED);
-		LWLockRelease(CheckpointerCommLock);
 		return false;
 	}
 
diff --git a/src/backend/postmaster/pgstat.c b/src/backend/postmaster/pgstat.c
index fbec722a1f..e0762444af 100644
--- a/src/backend/postmaster/pgstat.c
+++ b/src/backend/postmaster/pgstat.c
@@ -1435,6 +1435,28 @@ pgstat_reset_counters(void)
 	pgstat_send(&msg, sizeof(msg));
 }
 
+/*
+ * Helper function to collect and send live backends' current IO operations
+ * stats counters when a stats reset is initiated so that they may be deducted
+ * from future totals.
+ */
+static void
+pgstat_send_buffers_reset(PgStat_MsgResetsharedcounter *msg)
+{
+		int			backend_type;
+		PgStatIOPathOps ops[BACKEND_NUM_TYPES];
+
+		memset(ops, 0, sizeof(ops));
+		pgstat_report_live_backend_io_path_ops(ops);
+
+		for (backend_type = 1; backend_type < BACKEND_NUM_TYPES; backend_type++)
+		{
+			msg->m_backend_resets.backend_type = backend_type;
+			memcpy(&msg->m_backend_resets.iop, &ops[backend_type], sizeof(msg->m_backend_resets.iop));
+			pgstat_send(msg, sizeof(PgStat_MsgResetsharedcounter));
+		}
+}
+
 /* ----------
  * pgstat_reset_shared_counters() -
  *
@@ -1452,12 +1474,17 @@ pgstat_reset_shared_counters(const char *target)
 	if (pgStatSock == PGINVALID_SOCKET)
 		return;
 
-	if (strcmp(target, "archiver") == 0)
+	pgstat_setheader(&msg.m_hdr, PGSTAT_MTYPE_RESETSHAREDCOUNTER);
+	if (strcmp(target, "buffers") == 0)
+	{
+		msg.m_resettarget = RESET_BUFFERS;
+		pgstat_send_buffers_reset(&msg);
+		return;
+	}
+	else if (strcmp(target, "archiver") == 0)
 		msg.m_resettarget = RESET_ARCHIVER;
 	else if (strcmp(target, "bgwriter") == 0)
 		msg.m_resettarget = RESET_BGWRITER;
-	else if (strcmp(target, "buffers") == 0)
-		msg.m_resettarget = RESET_BUFFERS;
 	else if (strcmp(target, "wal") == 0)
 		msg.m_resettarget = RESET_WAL;
 	else
@@ -1466,25 +1493,8 @@ pgstat_reset_shared_counters(const char *target)
 				 errmsg("unrecognized reset target: \"%s\"", target),
 				 errhint("Target must be \"archiver\", \"bgwriter\", or \"wal\".")));
 
-	pgstat_setheader(&msg.m_hdr, PGSTAT_MTYPE_RESETSHAREDCOUNTER);
-
-	if (msg.m_resettarget == RESET_BUFFERS)
-	{
-		int			backend_type;
-		PgStatIOPathOps ops[BACKEND_NUM_TYPES];
 
-		memset(ops, 0, sizeof(ops));
-		pgstat_report_live_backend_io_path_ops(ops);
-
-		for (backend_type = 1; backend_type < BACKEND_NUM_TYPES; backend_type++)
-		{
-			msg.m_backend_resets.backend_type = backend_type;
-			memcpy(&msg.m_backend_resets.iop, &ops[backend_type], sizeof(msg.m_backend_resets.iop));
-			pgstat_send(&msg, sizeof(msg));
-		}
-	}
-	else
-		pgstat_send(&msg, sizeof(msg));
+	pgstat_send(&msg, sizeof(msg));
 
 }
 
@@ -3137,30 +3147,6 @@ pgstat_send(void *msg, int len)
 #endif
 }
 
-/*
- * Add live IO Op stats for all IO Paths (e.g. shared, local) to those in the
- * equivalent stats structure for exited backends. Note that this adds and
- * doesn't set, so the destination stats structure should be zeroed out by the
- * caller initially. This would commonly be used to transfer all IO Op stats
- * for all IO Paths for a particular backend type to the pgstats structure.
- */
-void
-pgstat_add_io_path_ops(PgStatIOOps *dest, IOOps *src, int io_path_num_types)
-{
-	int			io_path;
-
-	for (io_path = 0; io_path < io_path_num_types; io_path++)
-	{
-		dest->allocs += pg_atomic_read_u64(&src->allocs);
-		dest->extends += pg_atomic_read_u64(&src->extends);
-		dest->fsyncs += pg_atomic_read_u64(&src->fsyncs);
-		dest->writes += pg_atomic_read_u64(&src->writes);
-		dest++;
-		src++;
-	}
-
-}
-
 /* ----------
  * pgstat_send_archiver() -
  *
@@ -3234,9 +3220,8 @@ pgstat_send_buffers(void)
 	memset(&msg, 0, sizeof(msg));
 	msg.backend_type = beentry->st_backendType;
 
-	pgstat_add_io_path_ops(msg.iop.io_path_ops,
-						   (IOOps *) &beentry->io_path_stats,
-						   IOPATH_NUM_TYPES);
+	pgstat_sum_io_path_ops(msg.iop.io_path_ops,
+						   (IOOps *) &beentry->io_path_stats);
 
 	pgstat_setheader(&msg.m_hdr, PGSTAT_MTYPE_IO_PATH_OPS);
 	pgstat_send(&msg, sizeof(msg));
@@ -3407,6 +3392,29 @@ pgstat_send_slru(void)
 	}
 }
 
+/*
+ * Helper function to sum all live IO Op stats for all IO Paths (e.g. shared,
+ * local) to those in the equivalent stats structure for exited backends. Note
+ * that this adds and doesn't set, so the destination stats structure should be
+ * zeroed out by the caller initially. This would commonly be used to transfer
+ * all IO Op stats for all IO Paths for a particular backend type to the
+ * pgstats structure.
+ */
+void
+pgstat_sum_io_path_ops(PgStatIOOps *dest, IOOps *src)
+{
+	int			io_path;
+	for (io_path = 0; io_path < IOPATH_NUM_TYPES; io_path++)
+	{
+		dest->allocs += pg_atomic_read_u64(&src->allocs);
+		dest->extends += pg_atomic_read_u64(&src->extends);
+		dest->fsyncs += pg_atomic_read_u64(&src->fsyncs);
+		dest->writes += pg_atomic_read_u64(&src->writes);
+		dest++;
+		src++;
+	}
+
+}
 
 /* ----------
  * PgstatCollectorMain() -
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index b911dd9ce5..537c8dcadc 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -480,7 +480,7 @@ static BufferDesc *BufferAlloc(SMgrRelation smgr,
 							   BlockNumber blockNum,
 							   BufferAccessStrategy strategy,
 							   bool *foundPtr);
-static void FlushBuffer(BufferDesc *buf, SMgrRelation reln);
+static void FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOPath iopath);
 static void FindAndDropRelFileNodeBuffers(RelFileNode rnode,
 										  ForkNumber forkNum,
 										  BlockNumber nForkBlock,
@@ -1262,7 +1262,6 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 				 */
 
 				iopath = from_ring ? IOPATH_STRATEGY : IOPATH_SHARED;
-				pgstat_inc_ioop(IOOP_WRITE, iopath);
 
 				/* OK, do the I/O */
 				TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum,
@@ -1270,7 +1269,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 														  smgr->smgr_rnode.node.dbNode,
 														  smgr->smgr_rnode.node.relNode);
 
-				FlushBuffer(buf, NULL);
+				FlushBuffer(buf, NULL, iopath);
 				LWLockRelease(BufferDescriptorGetContentLock(buf));
 
 				ScheduleBufferTagForWriteback(&BackendWritebackContext,
@@ -2566,11 +2565,10 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
 	 * buffer is clean by the time we've locked it.)
 	 */
 
-	pgstat_inc_ioop(IOOP_WRITE, IOPATH_SHARED);
 	PinBuffer_Locked(bufHdr);
 	LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
 
-	FlushBuffer(bufHdr, NULL);
+	FlushBuffer(bufHdr, NULL, IOPATH_SHARED);
 
 	LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 
@@ -2818,9 +2816,12 @@ BufferGetTag(Buffer buffer, RelFileNode *rnode, ForkNumber *forknum,
  *
  * If the caller has an smgr reference for the buffer's relation, pass it
  * as the second parameter.  If not, pass NULL.
+ *
+ * IOPath will always be IOPATH_SHARED except when a buffer access strategy is
+ * used and the buffer being flushed is a buffer from the strategy ring.
  */
 static void
-FlushBuffer(BufferDesc *buf, SMgrRelation reln)
+FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOPath iopath)
 {
 	XLogRecPtr	recptr;
 	ErrorContextCallback errcallback;
@@ -2912,6 +2913,8 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln)
 			  bufToWrite,
 			  false);
 
+	pgstat_inc_ioop(IOOP_WRITE, iopath);
+
 	if (track_io_timing)
 	{
 		INSTR_TIME_SET_CURRENT(io_time);
@@ -3559,6 +3562,8 @@ FlushRelationBuffers(Relation rel)
 						  localpage,
 						  false);
 
+				pgstat_inc_ioop(IOOP_WRITE, IOPATH_LOCAL);
+
 				buf_state &= ~(BM_DIRTY | BM_JUST_DIRTIED);
 				pg_atomic_unlocked_write_u32(&bufHdr->state, buf_state);
 
@@ -3594,7 +3599,7 @@ FlushRelationBuffers(Relation rel)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, RelationGetSmgr(rel));
+			FlushBuffer(bufHdr, RelationGetSmgr(rel), IOPATH_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr, true);
 		}
@@ -3690,7 +3695,7 @@ FlushRelationsAllBuffers(SMgrRelation *smgrs, int nrels)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, srelent->srel);
+			FlushBuffer(bufHdr, srelent->srel, IOPATH_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr, true);
 		}
@@ -3746,7 +3751,7 @@ FlushDatabaseBuffers(Oid dbid)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, NULL);
+			FlushBuffer(bufHdr, NULL, IOPATH_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr, true);
 		}
@@ -3773,7 +3778,7 @@ FlushOneBuffer(Buffer buffer)
 
 	Assert(LWLockHeldByMe(BufferDescriptorGetContentLock(bufHdr)));
 
-	FlushBuffer(bufHdr, NULL);
+	FlushBuffer(bufHdr, NULL, IOPATH_SHARED);
 }
 
 /*
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index e2e1c3bf56..c7ca8d75aa 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -689,8 +689,8 @@ bool
 StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf, bool *from_ring)
 {
 	/*
-	 * If we decide to use the dirty buffer selected by StrategyGetBuffer(),
-	 * then ensure that we count it as such in pg_stat_buffers view.
+	 * Start by assuming that we will use the dirty buffer selected by
+	 * StrategyGetBuffer().
 	 */
 	*from_ring = true;
 
@@ -712,7 +712,7 @@ StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf, bool *from_
 	/*
 	 * Since we will not be writing out a dirty buffer from the ring, set
 	 * from_ring to false so that the caller does not count this write as a
-	 * "strategy write" and can do proper bookkeeping for pg_stat_buffers.
+	 * "strategy write" and can do proper bookkeeping.
 	 */
 	*from_ring = false;
 
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index 04b3558ea3..f396a2b68d 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -20,6 +20,7 @@
 #include "executor/instrument.h"
 #include "storage/buf_internals.h"
 #include "storage/bufmgr.h"
+#include "utils/backend_status.h"
 #include "utils/guc.h"
 #include "utils/memutils.h"
 #include "utils/resowner_private.h"
@@ -226,6 +227,8 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
 				  localpage,
 				  false);
 
+		pgstat_inc_ioop(IOOP_WRITE, IOPATH_LOCAL);
+
 		/* Mark not-dirty now in case we error out below */
 		buf_state &= ~BM_DIRTY;
 		pg_atomic_unlocked_write_u32(&bufHdr->state, buf_state);
diff --git a/src/backend/storage/sync/sync.c b/src/backend/storage/sync/sync.c
index 4a2ed414b0..8e5be66998 100644
--- a/src/backend/storage/sync/sync.c
+++ b/src/backend/storage/sync/sync.c
@@ -396,6 +396,7 @@ ProcessSyncRequests(void)
 					total_elapsed += elapsed;
 					processed++;
 
+					pgstat_inc_ioop(IOOP_FSYNC, IOPATH_SHARED);
 					if (log_checkpoints)
 						elog(DEBUG1, "checkpoint sync: number=%d file=%s time=%.3f ms",
 							 processed,
diff --git a/src/backend/utils/activity/backend_status.c b/src/backend/utils/activity/backend_status.c
index f326297517..f853ee6c1c 100644
--- a/src/backend/utils/activity/backend_status.c
+++ b/src/backend/utils/activity/backend_status.c
@@ -670,9 +670,8 @@ pgstat_report_live_backend_io_path_ops(PgStatIOPathOps *backend_io_path_ops)
 		if (beentry->st_procpid == 0)
 			continue;
 
-		pgstat_add_io_path_ops(backend_io_path_ops[beentry->st_backendType].io_path_ops,
-							   (IOOps *) beentry->io_path_stats,
-							   IOPATH_NUM_TYPES);
+		pgstat_sum_io_path_ops(backend_io_path_ops[beentry->st_backendType].io_path_ops,
+							   (IOOps *) beentry->io_path_stats);
 
 	}
 }
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 557b2673c0..d6ac325d63 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1751,10 +1751,40 @@ pg_stat_get_bgwriter_stat_reset_time(PG_FUNCTION_ARGS)
 	PG_RETURN_TIMESTAMPTZ(pgstat_fetch_stat_bgwriter()->stat_reset_timestamp);
 }
 
+/*
+* When adding a new column to the pg_stat_buffers view, add a new enum
+* value here above COLUMN_LENGTH.
+*/
+enum
+{
+	COLUMN_BACKEND_TYPE,
+	COLUMN_IO_PATH,
+	COLUMN_ALLOCS,
+	COLUMN_EXTENDS,
+	COLUMN_FSYNCS,
+	COLUMN_WRITES,
+	COLUMN_RESET_TIME,
+	COLUMN_LENGTH,
+};
+
+#define NROWS ((BACKEND_NUM_TYPES - 1) * IOPATH_NUM_TYPES)
+/*
+ * Helper function to get the correct row in the pg_stat_buffers view.
+ */
+static inline Datum *
+get_pg_stat_buffers_row(Datum all_values[NROWS][COLUMN_LENGTH], BackendType backend_type, IOPath io_path)
+{
+
+	/*
+	 * Subtract 1 from backend_type to avoid having rows for B_INVALID
+	 * BackendType
+	 */
+	return all_values[(backend_type - 1) * IOPATH_NUM_TYPES + io_path];
+}
+
 Datum
 pg_stat_get_buffers(PG_FUNCTION_ARGS)
 {
-#define NROWS ((BACKEND_NUM_TYPES - 1) * IOPATH_NUM_TYPES)
 	PgStat_BackendIOPathOps *backend_io_path_ops;
 	int			i;
 	int			io_path,
@@ -1765,22 +1795,6 @@ pg_stat_get_buffers(PG_FUNCTION_ARGS)
 
 	Tuplestorestate *tupstore = pg_stat_make_tuplestore(fcinfo, &tupdesc);
 
-	/*
-	 * When adding a new column to the pg_stat_buffers view, add a new enum
-	 * value here above COLUMN_LENGTH.
-	 */
-	enum
-	{
-		COLUMN_BACKEND_TYPE,
-		COLUMN_IO_PATH,
-		COLUMN_ALLOCS,
-		COLUMN_EXTENDS,
-		COLUMN_FSYNCS,
-		COLUMN_WRITES,
-		COLUMN_RESET_TIME,
-		COLUMN_LENGTH,
-	};
-
 	Datum		all_values[NROWS][COLUMN_LENGTH];
 	bool		all_nulls[NROWS][COLUMN_LENGTH];
 
@@ -1805,12 +1819,7 @@ pg_stat_get_buffers(PG_FUNCTION_ARGS)
 
 		for (io_path = 0; io_path < IOPATH_NUM_TYPES; io_path++)
 		{
-			/*
-			 * Subtract 1 from backend_type to avoid having rows for B_INVALID
-			 * BackendType
-			 */
-			int			rownum = (beentry->st_backendType - 1) * IOPATH_NUM_TYPES + io_path;
-			Datum	   *values = all_values[rownum];
+			Datum *values = get_pg_stat_buffers_row(all_values, beentry->st_backendType, io_path);
 
 			/*
 			 * COLUMN_RESET_TIME, COLUMN_BACKEND_TYPE, and COLUMN_IO_PATH will
@@ -1839,11 +1848,7 @@ pg_stat_get_buffers(PG_FUNCTION_ARGS)
 
 		for (io_path = 0; io_path < IOPATH_NUM_TYPES; io_path++)
 		{
-			/*
-			 * Subtract 1 from backend_type to avoid having rows for B_INVALID
-			 * BackendType
-			 */
-			Datum	   *values = all_values[(backend_type - 1) * IOPATH_NUM_TYPES + io_path];
+			Datum *values = get_pg_stat_buffers_row(all_values, backend_type, io_path);
 
 			values[COLUMN_BACKEND_TYPE] = backend_type_desc;
 			values[COLUMN_IO_PATH] = CStringGetTextDatum(GetIOPathDesc(io_path));
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8ff87a3f54..2d72933e90 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -1156,13 +1156,12 @@ extern void pgstat_twophase_postcommit(TransactionId xid, uint16 info,
 extern void pgstat_twophase_postabort(TransactionId xid, uint16 info,
 									  void *recdata, uint32 len);
 
-extern void pgstat_add_io_path_ops(PgStatIOOps *dest,
-								   IOOps *src, int io_path_num_types);
 extern void pgstat_send_archiver(const char *xlog, bool failed);
 extern void pgstat_send_bgwriter(void);
 extern void pgstat_send_buffers(void);
 extern void pgstat_send_checkpointer(void);
 extern void pgstat_send_wal(bool force);
+extern void pgstat_sum_io_path_ops(PgStatIOOps *dest, IOOps *src);
 
 /* ----------
  * Support functions for the SQL-callable functions to
diff --git a/src/include/utils/backend_status.h b/src/include/utils/backend_status.h
index c0149ce0de..f0392a07dc 100644
--- a/src/include/utils/backend_status.h
+++ b/src/include/utils/backend_status.h
@@ -371,20 +371,16 @@ pgstat_inc_ioop(IOOp io_op, IOPath io_path)
 	switch (io_op)
 	{
 		case IOOP_ALLOC:
-			pg_atomic_write_u64(&io_ops->allocs,
-								pg_atomic_read_u64(&io_ops->allocs) + 1);
+			inc_counter(&io_ops->allocs);
 			break;
 		case IOOP_EXTEND:
-			pg_atomic_write_u64(&io_ops->extends,
-								pg_atomic_read_u64(&io_ops->extends) + 1);
+			inc_counter(&io_ops->extends);
 			break;
 		case IOOP_FSYNC:
-			pg_atomic_write_u64(&io_ops->fsyncs,
-								pg_atomic_read_u64(&io_ops->fsyncs) + 1);
+			inc_counter(&io_ops->fsyncs);
 			break;
 		case IOOP_WRITE:
-			pg_atomic_write_u64(&io_ops->writes,
-								pg_atomic_read_u64(&io_ops->writes) + 1);
+			inc_counter(&io_ops->writes);
 			break;
 	}
 }
-- 
2.27.0

From 1f7a8a274aebf62e84cf7cbfdb95097ed55e7c14 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Mon, 11 Oct 2021 16:15:06 -0400
Subject: [PATCH 1/2] Read-only atomic's backend write function

For counters in shared memory which can be read by any backend but only
written to by one backend, an atomic is still needed to protect against
torn values, however, pg_atomic_fetch_add_u64() is overkill for
incrementing the counter. inc_counter() is a helper function which can
be used to increment these values safely but without unnecessary
overhead.

Author: Thomas Munro
---
 src/include/utils/backend_status.h | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/src/include/utils/backend_status.h b/src/include/utils/backend_status.h
index 419de72591..c0149ce0de 100644
--- a/src/include/utils/backend_status.h
+++ b/src/include/utils/backend_status.h
@@ -349,6 +349,16 @@ extern void pgstat_clear_backend_activity_snapshot(void);
 /* Activity reporting functions */
 typedef struct PgStatIOPathOps PgStatIOPathOps;
 
+/*
+ * On modern systems this is really just *counter++.  On some older systems
+ * there might be more to it, due to inability to read and write 64 bit values
+ * atomically.
+ */
+static inline void inc_counter(pg_atomic_uint64 *counter)
+{
+	pg_atomic_write_u64(counter, pg_atomic_read_u64(counter) + 1);
+}
+
 static inline void
 pgstat_inc_ioop(IOOp io_op, IOPath io_path)
 {
-- 
2.27.0

---

From: Andres Freund <[email-masked]>
Date: Tue, 19 Oct 2021 12:29:31 -0700

Hi,

> this in more locations (remove !bootstrap) or should I remove this patch

I think the scope is fine as-is.

> places?

Yes, I think it'd make sense. We have about 40 copies of this stuff, which is
fairly ridiculous.

> If so, where would such a utility function belong?

Not quite sure. src/backend/utils/fmgr/funcapi.c maybe? I suggest starting a
separate thread about that...

> checkpoint, the checkpointer increments IO op counter for writes in its

I think we'll really need to do a proper redesign of the shutdown callback
mechanism :(.

> doesn't use pg_atomic_read_u64 to access the value? But that seems worse

Yea, you're probably right, that's worse.

> Is there a better way to access the whole BackendStatusArray from within

Export the variable itself?

> couldn't find anywhere that made sense.

I think it should just live in atomics.h?

> from within those wrappers.

Makes sense to me to to have it here.

Greetings,

Andres Freund

---

From: Melanie Plageman <[email-masked]>
Date: Tue, 2 Nov 2021 15:26:52 -0400

v14 attached.

> separate thread about that...

done [1]. also, I dropped that commit from this patchset.

> I think we'll really need to do a proper redesign of the shutdown callback

I've left what I originally had, then.

> Export the variable itself?

done but wasn't sure about PGDLLIMPORT

> I think it should just live in atomics.h?

done

-- melanie

[1] https://www.postgresql.org/message-id/flat/CAAKRu_azyd1Z3W_r7Ou4sorTjRCs%2BPxeHw1CWJeXKofkE6TuZg%40mail.gmail.com
From 41242606a03aace906e307a38fc67c5cefcaec20 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Mon, 11 Oct 2021 16:15:06 -0400
Subject: [PATCH v14 2/4] Read-only atomic backend write function

Author: Thomas Munro
---
 src/include/port/atomics.h | 10 ++++++++++
 1 file changed, 10 insertions(+)

diff --git a/src/include/port/atomics.h b/src/include/port/atomics.h
index 856338f161..09a2575d6a 100644
--- a/src/include/port/atomics.h
+++ b/src/include/port/atomics.h
@@ -519,6 +519,16 @@ pg_atomic_sub_fetch_u64(volatile pg_atomic_uint64 *ptr, int64 sub_)
 	return pg_atomic_sub_fetch_u64_impl(ptr, sub_);
 }
 
+/*
 #undef INSIDE_ATOMICS_H
 
 #endif							/* ATOMICS_H */
-- 
2.30.2

From cfd8941958877a9bf3f5946e18aa098b1351d36f Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Wed, 29 Sep 2021 15:44:51 -0400
Subject: [PATCH v14 4/4] Remove superfluous bgwriter stats

TODO:

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 77b044343d..1aa94c5b19 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -3400,24 +3400,6 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
       </para></entry>
      </row>
 
-     <row>
@@ -3428,35 +3410,6 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
       </para></entry>
      </row>
 
-     <row>
index 86ca35121b..1e93937198 100644
@@ -1062,18 +1062,14 @@ CREATE VIEW pg_stat_archiver AS

index 1306b5238d..dec325e40e 100644

 
@@ -1103,8 +1088,6 @@ ForwardSyncRequest(const FileTag *ftag, SyncRequestType type)

index 355690d944..e0762444af 100644
@@ -5616,9 +5616,7 @@ pgstat_recv_archiver(PgStat_MsgArchiver *msg, int len)
 
 /* ----------
@@ -5634,9 +5632,6 @@ pgstat_recv_checkpointer(PgStat_MsgCheckpointer *msg, int len)
 
 static void
index 6926fc5742..67447f997a 100644

index 734079e233..641a891e34 100644
 
-Datum
-pg_stat_get_bgwriter_buf_written_checkpoints(PG_FUNCTION_ARGS)
-{
-	PG_RETURN_INT64(pgstat_fetch_stat_checkpointer()->buf_written_checkpoints);
-}
-
-Datum
-pg_stat_get_bgwriter_buf_written_clean(PG_FUNCTION_ARGS)
-{
-	PG_RETURN_INT64(pgstat_fetch_stat_bgwriter()->buf_written_clean);
-}
-
 Datum
 pg_stat_get_bgwriter_maxwritten_clean(PG_FUNCTION_ARGS)
 {
@@ -1809,24 +1797,6 @@ get_pg_stat_buffers_row(Datum all_values[NROWS][COLUMN_LENGTH], BackendType back
 	return all_values[(backend_type - 1) * IOPATH_NUM_TYPES + io_path];
 }
 
-Datum
 
index ac8aca2c61..2d72933e90 100644

 /* ----------

 
 /*
2.30.2

From 45b4d9bd16c37965eb73b74073121c985b230abf Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Fri, 24 Sep 2021 17:39:12 -0400
Subject: [PATCH v14 1/4] Allow bootstrap process to beinit

---

 
 	/*
2.30.2

From 2895819b15189495dba26fa5b3b91b8fb07f35ad Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Wed, 29 Sep 2021 15:39:45 -0400
Subject: [PATCH v14 3/4] Add system view tracking IO ops per backend type

TODO:
- catalog bump

Discussion: https://www.postgresql.org/message-id/flat/20210415235954.qcypb4urtovzkat5%40alap3.anarazel.de#724d5cce4bcb587f9167b80a5824bc5c
---
 doc/src/sgml/monitoring.sgml                | 116 +++++++++++++-
 src/backend/catalog/system_views.sql        |  11 ++
 src/backend/postmaster/checkpointer.c       |   3 +-
 src/backend/postmaster/pgstat.c             | 161 +++++++++++++++++++-
 src/backend/storage/buffer/bufmgr.c         |  46 ++++--
 src/backend/storage/buffer/freelist.c       |  23 ++-
 src/backend/storage/buffer/localbuf.c       |   3 +
 src/backend/storage/sync/sync.c             |   1 +
 src/backend/utils/activity/backend_status.c |  60 +++++++-
 src/backend/utils/adt/pgstatfuncs.c         | 152 ++++++++++++++++++
 src/include/catalog/pg_proc.dat             |   9 ++
 src/include/miscadmin.h                     |   2 +
 src/include/pgstat.h                        |  53 +++++++
 src/include/storage/buf_internals.h         |   4 +-
 src/include/utils/backend_status.h          |  80 ++++++++++
 src/test/regress/expected/rules.out         |   8 +
 16 files changed, 701 insertions(+), 31 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 3173ec2566..77b044343d 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -435,6 +435,15 @@ postgres   27093  0.0  0.0  30096  2752 ?        Ss   11:34   0:00 postgres: ser
      </entry>
      </row>
 
+     <row>
@@ -3462,6 +3471,101 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
 
  </sect2>

@@ -5058,12 +5162,14 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
        </para>
        <para>
index eb560955cd..86ca35121b 100644
@@ -1076,6 +1076,17 @@ CREATE VIEW pg_stat_bgwriter AS
 
index be7366379d..1306b5238d 100644
@@ -1098,13 +1098,14 @@ ForwardSyncRequest(const FileTag *ftag, SyncRequestType type)
 
index b7d0fbaefd..355690d944 100644
 
 /*
@@ -1431,6 +1435,28 @@ pgstat_reset_counters(void)
 
+/*
@@ -1448,7 +1474,14 @@ pgstat_reset_shared_counters(const char *target)
 
@@ -1460,8 +1493,9 @@ pgstat_reset_shared_counters(const char *target)

 /* ----------
@@ -2760,6 +2794,19 @@ pgstat_twophase_postabort(TransactionId xid, uint16 info,
 		rec->tuples_inserted + rec->tuples_updated;
 }
 
+/*
 
 /* ----------
  * pgstat_fetch_stat_dbentry() -
@@ -2999,6 +3046,14 @@ pgstat_shutdown_hook(int code, Datum arg)
 {
 	Assert(!pgstat_is_shutdown);
 
+	/*
@@ -3148,6 +3203,31 @@ pgstat_send_bgwriter(void)
 
+/*
@@ -3312,6 +3392,29 @@ pgstat_send_slru(void)
 	}
 }
 
+/*
 
 /* ----------
  * PgstatCollectorMain() -
@@ -3522,6 +3625,10 @@ PgstatCollectorMain(int argc, char *argv[])
 					pgstat_recv_checkpointer(&msg.msg_checkpointer, len);
 					break;
 
@@ -5221,10 +5328,30 @@ pgstat_recv_resetsharedcounter(PgStat_MsgResetsharedcounter *msg, int len)
@@ -5512,6 +5639,26 @@ pgstat_recv_checkpointer(PgStat_MsgCheckpointer *msg, int len)
 
+static void
index 08ebabfe96..6926fc5742 100644
 
 	if (isExtend)
 	{

 
@@ -1245,13 +1250,26 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 					}
 				}
 
+				/*

@@ -2552,10 +2570,11 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)

 
@@ -2803,9 +2822,12 @@ BufferGetTag(Buffer buffer, RelFileNode *rnode, ForkNumber *forknum,
  *
  * If the caller has an smgr reference for the buffer's relation, pass it
  * as the second parameter.  If not, pass NULL.
+ *
+ * IOPath will always be IOPATH_SHARED except when a buffer access strategy is
+ * used and the buffer being flushed is a buffer from the strategy ring.
  */
 static void
-FlushBuffer(BufferDesc *buf, SMgrRelation reln)
+FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOPath iopath)
 {
 	XLogRecPtr	recptr;
 	ErrorContextCallback errcallback;
@@ -2897,6 +2919,8 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln)
 			  bufToWrite,
 			  false);
 
+	pgstat_inc_ioop(IOOP_WRITE, iopath);
+
 	if (track_io_timing)
 	{
 		INSTR_TIME_SET_CURRENT(io_time);
@@ -3544,6 +3568,8 @@ FlushRelationBuffers(Relation rel)
 						  localpage,
 						  false);

@@ -3579,7 +3605,7 @@ FlushRelationBuffers(Relation rel)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, RelationGetSmgr(rel));
+			FlushBuffer(bufHdr, RelationGetSmgr(rel), IOPATH_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr, true);
 		}
@@ -3675,7 +3701,7 @@ FlushRelationsAllBuffers(SMgrRelation *smgrs, int nrels)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, srelent->srel);
+			FlushBuffer(bufHdr, srelent->srel, IOPATH_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr, true);
 		}
@@ -3731,7 +3757,7 @@ FlushDatabaseBuffers(Oid dbid)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, NULL);
+			FlushBuffer(bufHdr, NULL, IOPATH_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr, true);
 		}
@@ -3758,7 +3784,7 @@ FlushOneBuffer(Buffer buffer)

 
 /*
index 6be80476db..c7ca8d75aa 100644

 
 
 	/*
 
+	/*
 
+		pgstat_inc_ioop(IOOP_WRITE, IOPATH_LOCAL);
+
 		/* Mark not-dirty now in case we error out below */
 		buf_state &= ~BM_DIRTY;
 		pg_atomic_unlocked_write_u32(&bufHdr->state, buf_state);
diff --git a/src/backend/storage/sync/sync.c b/src/backend/storage/sync/sync.c
index d4083e8a56..3be06d5d5a 100644
--- a/src/backend/storage/sync/sync.c
+++ b/src/backend/storage/sync/sync.c
@@ -420,6 +420,7 @@ ProcessSyncRequests(void)
 					total_elapsed += elapsed;
 					processed++;
 
+					pgstat_inc_ioop(IOOP_FSYNC, IOPATH_SHARED);
 					if (log_checkpoints)
 						elog(DEBUG1, "checkpoint sync: number=%d file=%s time=%.3f ms",
 							 processed,
diff --git a/src/backend/utils/activity/backend_status.c b/src/backend/utils/activity/backend_status.c
index 7229598822..4662ea2e24 100644
--- a/src/backend/utils/activity/backend_status.c
+++ b/src/backend/utils/activity/backend_status.c
@@ -50,7 +50,7 @@ int			pgstat_track_activity_query_size = 1024;
 PgBackendStatus *MyBEEntry = NULL;

-static PgBackendStatus *BackendStatusArray = NULL;
+PgBackendStatus *BackendStatusArray = NULL;
 static char *BackendAppnameBuffer = NULL;
 static char *BackendClientHostnameBuffer = NULL;
 static char *BackendActivityBuffer = NULL;
@@ -236,6 +236,24 @@ CreateSharedBackendStatus(void)
 #endif
 }
 
+const char *
 
 	/*
 	 * we don't zero st_progress_param here to save cycles; nobody should
@@ -621,6 +649,33 @@ pgstat_report_activity(BackendState state, const char *cmd_str)
 	PGSTAT_END_WRITE_ACTIVITY(beentry);
 }
 
+/*
@@ -1045,7 +1100,6 @@ pgstat_get_my_query_id(void)
 
-
 /* ----------
  * pgstat_fetch_stat_beentry() -
  *
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index ff5aedc99c..734079e233 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1778,6 +1778,37 @@ pg_stat_get_bgwriter_stat_reset_time(PG_FUNCTION_ARGS)
 	PG_RETURN_TIMESTAMPTZ(pgstat_fetch_stat_bgwriter()->stat_reset_timestamp);
 }
 
+/*
 pg_stat_get_buf_written_backend(PG_FUNCTION_ARGS)
@@ -1796,6 +1827,127 @@ pg_stat_get_buf_alloc(PG_FUNCTION_ARGS)
 
+Datum
+	int			backend_type, io_path;
+	ReturnSetInfo *rsinfo;
+	rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;

 
index bcd3588ea2..ac8aca2c61 100644

+/*
 
 /* ----------

 /*
@@ -1118,14 +1168,17 @@ extern void pgstat_twophase_postabort(TransactionId xid, uint16 info,

 /* ----------
 
 /* freelist.c */
 
index 8042b817df..2b99195e7b 100644

+/* ----------
 
 /* ----------
 
+/*

@@ -274,6 +321,7 @@ extern PGDLLIMPORT int pgstat_track_activity_query_size;
  * ----------
  */
 extern PGDLLIMPORT PgBackendStatus *MyBEEntry;
+extern PGDLLIMPORT PgBackendStatus *BackendStatusArray;

 /* ----------
@@ -289,6 +337,10 @@ extern void CreateSharedBackendStatus(void);
  * ----------
  */
 
+/* Utility functions */
@@ -296,7 +348,35 @@ extern void pgstat_bestart(void);
 
2.30.2

---

From: Andres Freund <[email-masked]>
Date: Fri, 19 Nov 2021 08:49:52 -0800

Hi,

> Subject: [PATCH v14 1/4] Allow bootstrap process to beinit

Pushed.

> +	pg_atomic_write_u64(counter, pg_atomic_read_u64(counter) + 1);

Why is this using a completely different naming scheme from the rest of the
file?

>  16 files changed, 701 insertions(+), 31 deletions(-)

This is a pretty large change, I wonder if there's a way to make it a bit more
granular.

Greetings,

Andres Freund

---

From: Melanie Plageman <[email-masked]>
Date: Wed, 24 Nov 2021 16:19:20 -0500

> Why is this using a completely different naming scheme from the rest of the

It was what Thomas originally named it. Also, I noticed all the other
pg_atomic* in this file were wrappers around the same impl function, so
I thought maybe naming it this way would be confusing. I renamed it to
pg_atomic_inc_counter(), though maybe pg_atomic_readonly_write() would
be better?

> This is a pretty large change, I wonder if there's a way to make it a bit more

I have done this. See v15 patch set attached.

- Melanie

---

From: Justin Pryzby <[email-masked]>
Date: Wed, 24 Nov 2021 19:15:59 -0600

Thanks for working on this.  I was just trying to find something like
"pg_stat_checkpointer".

You wrote beentry++ at the start of two loops, but I think that's wrong; it
should be at the end, as in the rest of the file (or as a loop increment).
BackendStatusArray[0] is actually used (even though its backend has
backendId==1, not 0).  "MyBEEntry = &BackendStatusArray[MyBackendId - 1];"

You could put *_NUM_TYPES as the last value in these enums, like
NUM_AUXPROCTYPES, NUM_PMSIGNALS, and NUM_PROCSIGNALS:

There's extraneous blank lines in these functions:

+pgstat_sum_io_path_ops
+pgstat_report_live_backend_io_path_ops
+pgstat_recv_resetsharedcounter
+GetIOPathDesc
+StrategyRejectBuffer

This function is doubly-indented:

+pgstat_send_buffers_reset

As support for C99 is now required by postgres, variables can be declared as
part of various loops.

+       int                     io_path;

Rather than memset(), you could initialize msg like this.
PgStat_MsgIOPathOps msg = {0};

-- 
Justin

---

From: Justin Pryzby <[email-masked]>
Date: Fri, 26 Nov 2021 15:16:36 -0600

> +pgstat_recv_resetsharedcounter

+ an extra blank line pgstat_reset_shared_counters.

In 0005:

monitoring.sgml says that the columns in pg_stat_buffers are integers, but
they're actually bigint.

You're passing a constant randomAccess=true to tuplestore_begin_heap ;)

+Datum all_values[NROWS][COLUMN_LENGTH];

If you were to allocate this as an array, I think it could actually be 3-D:
Datum all_values[BACKEND_NUM_TYPES-1][IOPATH_NUM_TYPES][COLUMN_LENGTH];

But I don't know if this is portable across postgres' supported platforms; I
haven't seen any place which allocates a multidimensional array on the stack,
nor passes one to a function:

+static inline Datum *

Maybe the allocation half is okay (I think it's ~3kB), but it seems easier to
palloc the required amount than to research compiler behavior.

That function is only used as a one-line helper, and doesn't use
multidimensional array access anyway:

I think it'd be better as a macro, like (I think)
#define ROW(backend_type, io_path) all_values[NROWS*(backend_type-1)+io_path]

Maybe it should take the column type as a 3 arg.

The enum with COLUMN_LENGTH should be named.

Or maybe it should be removed, and the enum names moved to comments, like:

+                       /* backend_type */
+                       values[val++] = backend_type_desc;

+                       /* io_path */
+                       values[val++] = CStringGetTextDatum(GetIOPathDesc(io_path)); 

+                       /* allocs */
+                       values[val++] += io_ops->allocs - resets->allocs;
...

*Note the use of += and not =.

Also:
src/include/miscadmin.h:#define BACKEND_NUM_TYPES (B_LOGGER + 1)

I think it's wrong to say NUM_TYPES = B_LOGGER + 1 (which would suggest using
lessthan-or-equal instead of lessthan as you are).

Since the valid backend types start at 1 , the "count" of backend types is
currently B_LOGGER (13) - not 14.  I think you should remove the "+1" here.
Then NROWS (if it continued to exist at all) wouldn't need to subtract one.

-- 
Justin

---

From: Melanie Plageman <[email-masked]>
Date: Wed, 1 Dec 2021 16:59:44 -0500

Thanks for the review!

> backendId==1, not 0).  "MyBEEntry = &BackendStatusArray[MyBackendId - 1];"

I've fixed this in v16 which I will attach to the next email in the thread.

> +#define BACKEND_NUM_TYPES (B_LOGGER + 1)

I originally had it as you describe, but based on this feedback upthread

> blocks, for example.  LAST_OCLASS in dependency.h is a precedent on this.)

So, I changed it to use macros.

> There's extraneous blank lines in these functions:

Fixed

> +pgstat_report_live_backend_io_path_ops

I didn't see one here

> +pgstat_recv_resetsharedcounter

I didn't see one here

> +GetIOPathDesc

Fixed

> +StrategyRejectBuffer

Fixed

> +pgstat_send_buffers_reset

Fixed. Thanks for catching this.
I also ran pgindent and manually picked a few of the formatting fixes
that were relevant to code I added.

> +       for (io_path = 0; io_path < IOPATH_NUM_TYPES; io_path++)

Fixed this and all other occurrences in my code.

> +       memset(&msg, 0, sizeof(msg));

though changing the initialization to universal zero initialization
seems to be the correct way, I do get this compiler warning when I make
the change

pgstat.c:3212:29: warning: suggest braces around initialization of
subobject [-Wmissing-braces]
        PgStat_MsgIOPathOps msg = {0};
                                   ^
                                   {}
I have seem some comments online that say that this is a spurious
warning present with some versions of both gcc and clang when using
-Wmissing-braces to compile code with universal zero initialization, but
I'm not sure what I should do.

v16 attached in next message

- Melanie

---

From: Melanie Plageman <[email-masked]>
Date: Wed, 1 Dec 2021 17:00:14 -0500

v16 (also rebased) attached

Fixed

Fixed

Fixed

> Datum all_values[BACKEND_NUM_TYPES-1][IOPATH_NUM_TYPES][COLUMN_LENGTH];

I've changed this to a 3D array as you suggested and removed the NROWS
macro.

> palloc the required amount than to research compiler behavior.

I think passing it to the function is okay. The parameter type would be
adjusted from an array to a pointer.
I am not sure if the allocation on the stack in the body of
pg_stat_get_buffers is too large. (left as is for now)

> +       return all_values[(backend_type - 1) * IOPATH_NUM_TYPES + io_path];

with your suggested changes to a 3D array, it now does use multidimensional
array access

> #define ROW(backend_type, io_path) all_values[NROWS*(backend_type-1)+io_path]

If I am understanding the idea of the macro, it would change the call
site from this:

+Datum *values = get_pg_stat_buffers_row(all_values,
beentry->st_backendType, io_path);

+values[COLUMN_ALLOCS] += pg_atomic_read_u64(&io_ops->allocs);
+values[COLUMN_FSYNCS] += pg_atomic_read_u64(&io_ops->fsyncs);

to this:

+Datum *row =  ROW(beentry->st_backendType, io_path);

+row[COLUMN_ALLOCS] += pg_atomic_read_u64(&io_ops->allocs);
+row[COLUMN_FSYNCS] += pg_atomic_read_u64(&io_ops->fsyncs);

I usually prefer functions to macros, but I am fine with changing it.
(I did not change it in this version)
I have changed all the local variables from "values" to "row" which
I think is a bit clearer.

> Maybe it should take the column type as a 3 arg.

If I am understanding this idea, the call site would look like this now:
+CELL(beentry->st_backendType, io_path, COLUMN_FSYNCS) +=
pg_atomic_read_u64(&io_ops->fsyncs);
+CELL(beentry->st_backendType, io_path, COLUMN_ALLOCS) +=
pg_atomic_read_u64(&io_ops->allocs);

I don't like this as much. Since this code is inside of a loop, it kind
of makes sense to me that you get a row at the top of the loop and then
fill in all the cells in the row using that "row" variable.

> The enum with COLUMN_LENGTH should be named.

I only use the values in it, so it didn't need a name.

> +                       values[val++] += io_ops->allocs - resets->allocs;

I find it easier to understand with it in code instead of as a comment.

> *Note the use of += and not =.

Thanks for seeing this. I have changed this (to use +=).

> Then NROWS (if it continued to exist at all) wouldn't need to subtract one.

I think what I currently have is technically correct because I start at
1 when I am using it as a loop condition. I do waste a spot in the
arrays I allocate with BACKEND_NUM_TYPES size.

I was hesitant to make the value of BACKEND_NUM_TYPES == B_LOGGER
because it seems kind of weird to have it have the same value as the
B_LOGGER enum.

I am open to changing it. (I didn't change it in this v16).

- Melanie

---

From: Justin Pryzby <[email-masked]>
Date: Wed, 1 Dec 2021 17:59:15 -0600

> because it seems kind of weird to have it have the same value as the

I don't mean to say that the code is misbehaving - I mean "num_x" means "the
number of x's" - how many there are.  Since the first, valid backend type is 1,
and they're numbered consecutively and without duplicates, then "the number of
backend types" is the same as the value of the last one (B_LOGGER).  It's
confusing if there's a macro called BACKEND_NUM_TYPES which is greater than the
number of backend types.

Most loops say for (int i=0; i<NUM; ++i)
If it's 1-based, they say for (int i=1; i<=NUM; ++i)
You have two different loops like:

+       for (int i = 0; i < BACKEND_NUM_TYPES - 1 ; i++)
+       for (int backend_type = 1; backend_type < BACKEND_NUM_TYPES; backend_type++)

Both of these iterate over the correct number of backend types, but they both
*look* wrong, which isn't desirable.

-- 
Justin

---

From: Justin Pryzby <[email-masked]>
Date: Wed, 1 Dec 2021 21:31:45 -0600

> I originally had it as you describe, but based on this feedback upthread

I saw that after I made my suggestion.  Sorry for the noise.
Both ways already exist in postgres and seem to be acceptable.

> > +pgstat_recv_resetsharedcounter

=> The extra blank line is after the RESET_BUFFERS memset.

> +        * Reset the global, bgwriter and checkpointer statistics for the

The first comma in this comment was introduced in 1bc8e7b09, and seems to be
extraneous, since bgwriter and checkpointer are both global.  With the comma,
it looks like it should be memsetting 3 things.

> +               /* Don't count dead backends. They should already be counted */

Maybe this comment should say ".. they'll be added below"

> +                       row[COLUMN_BACKEND_TYPE] = backend_type_desc;
> +                       row[COLUMN_IO_PATH] = CStringGetTextDatum(GetIOPathDesc(io_path));
> +                       row[COLUMN_ALLOCS] += io_ops->allocs - resets->allocs;
> +                       row[COLUMN_EXTENDS] += io_ops->extends - resets->extends;
> +                       row[COLUMN_FSYNCS] += io_ops->fsyncs - resets->fsyncs;
> +                       row[COLUMN_WRITES] += io_ops->writes - resets->writes;
> +                       row[COLUMN_RESET_TIME] = reset_time;

It'd be clearer if RESET_TIME were set adjacent to BACKEND_TYPE and IO_PATH.

> pgstat.c:3212:29: warning: suggest braces around initialization of subobject [-Wmissing-braces]
> 

I think gcc is suggesting to write something like {{0}}, and I think the online
commentary you found is saying that the warning is a false positive.
So I think you should ignore my suggestion - it's not worth the bother.

This message needs to be updated:
	errhint("Target must be \"archiver\", \"bgwriter\", or \"wal\".")))

When I query the view, I see reset times as: 1999-12-31 18:00:00-06.
I guess it should be initialized like this one:
	globalStats.bgwriter.stat_reset_timestamp = ts

The cfbot shows failures now (I thought it was passing with the previous patch,
but I suppose I'm wrong.)

It looks like running recovery during single user mode hits this assertion.
TRAP: FailedAssertion("beentry", File: "../../../../src/include/utils/backend_status.h", Line: 359, PID: 3499)

-- 
Justin

---

From: Justin Pryzby <[email-masked]>
Date: Wed, 1 Dec 2021 22:06:53 -0600

> I've fixed this in v16 which I will attach to the next email in the thread.

I just noticed that since beentry++ is now at the end of the loop, it's being
missed when you "continue":

Also, I saw that pgindent messed up and added spaces after pointers in function
declarations, due to new typedefs not in typedefs.list:

-pgstat_send_buffers_reset(PgStat_MsgResetsharedcounter *msg)
+pgstat_send_buffers_reset(PgStat_MsgResetsharedcounter * msg)

-static inline void pg_atomic_inc_counter(pg_atomic_uint64 *counter)
+static inline void
+pg_atomic_inc_counter(pg_atomic_uint64 * counter)

-- 
Justin

---

From: Melanie Plageman <[email-masked]>
Date: Fri, 3 Dec 2021 15:02:24 -0500

Thanks again! I really appreciate the thorough review.

I have combined responses to all three of your emails below.
Let me know if it is more confusing to do it this way.

> *look* wrong, which isn't desirable.

I've changed this and added comments wherever I could to make it clear.
Whenever the parameter was of type BackendType, I tried to use the
correct (not adjusted by subtracting 1) number and wherever the type was
int and being used as an index into the array, I used the adjusted value
and added the idx suffix to make it clear that the number does not
reflect the actual BackendType:

Fixed.

Fixed.

Fixed.

> It'd be clearer if RESET_TIME were set adjacent to BACKEND_TYPE and IO_PATH.

If you mean just in the order here (not in the column order in the
view), then I have changed it as you recommended.

Done.

Done.

> TRAP: FailedAssertion("beentry", File: "../../../../src/include/utils/backend_status.h", Line: 359, PID: 3499)

Yes, thank you for catching this.
I have moved up pgstat_beinit and pgstat_bestart so that single user
mode process will also have PgBackendStatus. I also have to guard
against sending these stats to the collector since there is no room for
B_INVALID backendtype in the array of IO Op values.

With this change `make check-world` passes on my machine.

Fixed.

Fixed.

-- Melanie

---

From: Andres Freund <[email-masked]>
Date: Thu, 9 Dec 2021 11:17:52 -0800

Hi,

On 2021-12-03 15:02:24 -0500, Melanie Plageman wrote:
> From e0f7f3dfd60a68fa01f3c023bcdb69305ade3738 Mon Sep 17 00:00:00 2001
> Subject: [PATCH v17 1/7] Read-only atomic backend write function
> 
> incrementing the counter. pg_atomic_inc_counter() is a helper function
> which can be used to increment these values safely but without
> unnecessary overhead.
>
> Author: Thomas Munro
> ---
>  src/include/port/atomics.h | 11 +++++++++++
>  1 file changed, 11 insertions(+)
> 
> index 856338f161..39ffff24dd 100644
> @@ -519,6 +519,17 @@ pg_atomic_sub_fetch_u64(volatile pg_atomic_uint64 *ptr, int64 sub_)
> +pg_atomic_inc_counter(pg_atomic_uint64 *counter)
> +{

I wonder if it's worth putting something in the name indicating that this is
not actual atomic RMW operation. Perhaps adding _unlocked?

> From b0e193cfa08f0b8cf1be929f26fe38f06a39aeae Mon Sep 17 00:00:00 2001
> Date: Wed, 24 Nov 2021 10:32:56 -0500
> Subject: [PATCH v17 2/7] Add IO operation counters to PgBackendStatus
> 
> Add an array of counters in PgBackendStatus which count the buffers
> allocated, extended, fsynced, and written by a given backend. Each "IO
> Op" (alloc, fsync, extend, write) is counted per "IO Path" (direct,
> local, shared, or strategy). "local" and "shared" IO Path counters count
> operations on local and shared buffers. The "strategy" IO Path counts
> buffers alloc'd/written/read/fsync'd as part of a BufferAccessStrategy.
> The "direct" IO Path counts blocks of IO which are read, written, or
> fsync'd using smgrwrite/extend/immedsync directly (as opposed to through
> [Local]BufferAlloc()).
> 
> With this commit, all backends increment a counter in their
> PgBackendStatus when performing an IO operation. This is in preparation
> for future commits which will persist these stats upon backend exit and
> use the counters to provide observability of database IO operations.
> 
> Note that this commit does not add code to increment the "direct" path.
> A separate proposed patch [1] which would add wrappers for smgrwrite(),
> smgrextend(), and smgrimmedsync() would provide a good location to call
> pgstat_inc_ioop() for unbuffered IO and avoid regressions for future
> users of these functions.
>

On longer thread it's nice for committers to already have Reviewed-By: in the
commit message.

> index 7229598822..413cc605f8 100644
> @@ -399,6 +399,15 @@ pgstat_bestart(void)
> +	for (int io_path = 0; io_path < IOPATH_NUM_TYPES; io_path++)
> +	{

nit: I think we nearly always have a blank line before loops

> index 646126edee..93f1b4bcfc 100644
> @@ -623,6 +623,7 @@ InitPostgres(const char *in_dbname, Oid dboid, const char *username,
>  		RegisterTimeout(CLIENT_CONNECTION_CHECK_TIMEOUT, ClientCheckTimeoutHandler);
>  	}
>  
> +	pgstat_beinit();
>  	/*
>  	 * Initialize local process's access to XLOG.
>  	 */

nit: same with multi-line comments.

> @@ -649,6 +650,7 @@ InitPostgres(const char *in_dbname, Oid dboid, const char *username,
>  		 */
>  		CreateAuxProcessResourceOwner();
>  
> +		pgstat_bestart();
>  		StartupXLOG();
>  		/* Release (and warn about) any buffer pins leaked in StartupXLOG */
>  		ReleaseAuxProcessResources(true);
> @@ -676,7 +678,6 @@ InitPostgres(const char *in_dbname, Oid dboid, const char *username,
>  	EnablePortalManager();
>  

I'd like to see changes like moving this kind of thing around broken around
and committed separately. It's much easier to pinpoint breakage if the CF
breaks after moving just pgstat_beinit() around, rather than when committing
this considerably larger patch. And reordering subsystem initialization has
the habit of causing problems...

> [...]

To me IOop and IOOps sound to much alike - even though they're really kind of
separate things. s/IOOps/IOOpCounters/ maybe?

> @@ -3152,6 +3156,14 @@ pgstat_shutdown_hook(int code, Datum arg)
>  {
> +	 * Only need to send stats on IO Ops for IO Paths when a process exits.
> +	 * Users requiring IO Ops for both live and exited backends can read from
> +	 * live backends' PgBackendStatus and sum this with totals from exited
> +	 * backends persisted by the stats collector.
> +	 */
> +	pgstat_send_buffers();

Perhaps something like this comment belongs somewhere at the top of the file,
or in the header, or ...? It's a fairly central design piece, and it's not
obvious one would need to look in the shutdown hook for it?

> +	 * Though some backends with type B_INVALID (such as the single-user mode
> +	 * process) do initialize and increment IO operations stats, there is no
> +	 * spot in the array of IO operations for backends of type B_INVALID. As
> +	 * such, do not send these to the stats collector.
> +	 */
> +	if (!beentry || beentry->st_backendType == B_INVALID)
> +		return;

Why does single user mode use B_INVALID? That doesn't seem quite right.

> +	pgstat_send(&msg, sizeof(msg));

It seems worth having a path skipping sending the message if there was no IO?

> +	for (int io_path = 0; io_path < IOPATH_NUM_TYPES; io_path++)
> +	{

Sacriligeous, but I find io_path a harder to understand variable name for the
counter than i (or io_path_off or ...) ;)

> +	PgStatIOOps *src_io_path_ops;
> +	PgStatIOOps *dest_io_path_ops;
> +
> +	/*
> +	 * Subtract 1 from message's BackendType to get a valid index into the
> +	 * array of IO Ops which does not include an entry for B_INVALID
> +	 * BackendType.
> +	 */
> +	Assert(msg->backend_type > B_INVALID);

Probably worth also asserting the upper boundary?

> From f972ea87270feaed464a74fb6541ac04b4fc7d98 Mon Sep 17 00:00:00 2001
> Date: Wed, 24 Nov 2021 11:39:48 -0500
> Subject: [PATCH v17 4/7] Add "buffers" to pgstat_reset_shared_counters
> 
> Backends count IO operations for various IO paths in their PgBackendStatus.
> Upon exit, they send these counts to the stats collector. Prior to this commit,
> these IO Ops stats would have been reset when the target was "bgwriter".
> 
> With this commit, target "bgwriter" no longer will cause the IO operations
> stats to be reset, and the IO operations stats can be reset with new target,
> "buffers".
> ---
>  doc/src/sgml/monitoring.sgml                |  2 +-
>  src/backend/postmaster/pgstat.c             | 83 +++++++++++++++++++--
>  src/backend/utils/activity/backend_status.c | 29 +++++++
>  src/include/pgstat.h                        |  8 +-
>  src/include/utils/backend_status.h          |  2 +
>  5 files changed, 117 insertions(+), 7 deletions(-)
> 
> index 62f2a3332b..bda3eef309 100644
> @@ -3604,7 +3604,7 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
> -       Time at which these statistics were last reset
>      </tbody>

Hm?

Shouldn't this new reset target be documented?

> +	 * Iterate through the array of IO Ops for all IO Paths for each
> +	 * BackendType. Because the array does not include a spot for BackendType
> +	 * B_INVALID, add 1 to the index when setting backend_type so that there is
> +	 * no confusion as to the BackendType with which this reset message
> +	 * corresponds.
> +	 */
> +	for (int backend_type_idx = 0; backend_type_idx < BACKEND_NUM_TYPES; backend_type_idx++)
> +	{
> +		msg->m_backend_resets.backend_type = backend_type_idx + 1;
> +		memcpy(&msg->m_backend_resets.iop, &ops[backend_type_idx],
> +				sizeof(msg->m_backend_resets.iop));

Probably worth explaining why multiple messages are sent?

> @@ -5583,10 +5621,45 @@ pgstat_recv_resetsharedcounter(PgStat_MsgResetsharedcounter *msg, int len)
>  {
> +		 * Reset the global bgwriter and checkpointer statistics for the
> +		 * cluster.
> +		 */

Oh, is this a live bug?

> +		/*
> +		 * Subtract 1 from the BackendType to arrive at a valid index in the
> +		 * array, as it does not contain a spot for B_INVALID BackendType.
> +		 */

Instead of repeating a comment about +- 1 in a bunch of places, would it look
better to have two helper inline functions for this purpose?

> +* value here above COLUMN_LENGTH.

COLUMN_LENGTH seems like a fairly generic name...

> From 9f22da9041e1e1fbc0ef003f5f78f4e72274d438 Mon Sep 17 00:00:00 2001
> Date: Wed, 24 Nov 2021 12:20:10 -0500
> Subject: [PATCH v17 6/7] Remove superfluous bgwriter stats
> 

When do you think it makes sense to tackle these wrt committing some of the
patches?

> -	PendingBgWriterStats.m_buf_written_clean += num_written;

Isn't num_written unused now, unless tracepoints are enabled? I'd expect some
compilers to warn... Perhaps we should just remove information from the
tracepoint?

Greetings,

Andres Freund

---

From: Melanie Plageman <[email-masked]>
Date: Wed, 15 Dec 2021 16:40:27 -0500

v18 attached.

Done.

Done.

Done.

Done.

Done

Done.

> obvious one would need to look in the shutdown hook for it?

now in pgstat.h above the declaration of pgstat_send_buffers()

> Why does single user mode use B_INVALID? That doesn't seem quite right.

I think PgBackendStatus->st_backendType is set from MyBackendType which
isn't set for the single user mode process. What BackendType would you
expect to see?

> It seems worth having a path skipping sending the message if there was no IO?

Makes sense. I've updated pgstat_send_buffers() to do a loop after calling
pgstat_sum_io_path_ops() and check if it should skip sending.

I also thought about having pgstat_sum_io_path_ops() return a value to
indicate if everything was 0 -- which could be useful to future callers
potentially?

I didn't do this because I am not sure what the return value would be.
It could be a bool and be true if any IO was done and false if none was
done -- but that doesn't really make sense given the function's name it
would be called like
if (!pgstat_sum_io_path_ops())
  return
which I'm not sure is very clear

> counter than i (or io_path_off or ...) ;)

I've updated almost all my non-standard loop index variable names.

Done.

> Shouldn't this new reset target be documented?

It is in the commit adding the view. I didn't include it in this commit
because the pg_stat_buffers view doesn't exist yet, as of this commit,
and I thought it would be odd to mention it in the docs (in this
commit).
As an aside, I shouldn't have left this correction in this commit. I
moved it now to the other one.

Done.

> >               globalStats.bgwriter.stat_reset_timestamp = GetCurrentTimestamp();

I don't think it is a bug. globalStats only contained bgwriter and
checkpointer stats and those were all only displayed in
pg_stat_bgwriter(), so memsetting the whole thing seems fine.

Done.

Changed.

> When do you think it makes sense to tackle these wrt committing some of the

Well, the new stats are a superset of the old stats (no stats have been
removed that are not represented in the new or old views). So, I don't
see that as a blocker for committing these patches.

Since it is weird that pg_stat_bgwriter had mostly checkpointer stats,
I've edited this commit to rename that view to pg_stat_checkpointer.

I have not made a separate view just for maxwritten_clean (presumably
called pg_stat_bgwriter), but I would not be opposed to doing this if
you thought having a view with a single column isn't a problem (in the
event that we don't get around to adding more bgwriter stats right
away).

I noticed after changing the docs on the "bgwriter" target for
pg_stat_reset_shared to say "checkpointer", that it still said "bgwriter" in
  src/backend/po/ko.po
  src/backend/po/it.po
  ...
I presume these are automatically updated with some incantation, but I wasn't
sure what it was nor could I find documentation on this.

> compilers to warn... Perhaps we should just remove information from the

The local variable num_written is used in BgBufferSync() to determine
whether or not to increment maxwritten_clean which is still represented
in the view pg_stat_checkpointer (formerly pg_stat_bgwriter).

A local variable num_written is used in BufferSync() to increment
CheckpointStats.ckpt_bufs_written which is logged in LogCheckpointEnd(),
so I'm not sure that can be removed.

- Melanie
From 2d8ad99743e50c0dc021d116782fe8d63329fc31 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Wed, 24 Nov 2021 12:21:08 -0500
Subject: [PATCH v18 8/8] small comment correction

---
 src/backend/utils/activity/backend_status.c | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

index 08e3f8f167..9666c962b4 100644
@@ -296,7 +296,7 @@ pgstat_beinit(void)

From ebd21154bc83b02a31ce185bbfd2c51ad9c7d565 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Wed, 24 Nov 2021 11:39:48 -0500
Subject: [PATCH v18 5/8] Add "buffers" to pgstat_reset_shared_counters

Backends count IO operations for various IO paths in their
PgBackendStatus. Upon exit, they send these counts to the stats
collector. Prior to this commit, these IO operation stats would have
been reset when the target was "bgwriter".

With this commit, target "bgwriter" no longer will cause the IO
operations stats to be reset, and the IO operations stats can be reset
with new target, "buffers".

Author: Melanie Plageman <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/20200124195226.lth52iydq2n2uilq%40alap3.anarazel.de
---
 src/backend/postmaster/pgstat.c             | 77 +++++++++++++++++++--
 src/backend/utils/activity/backend_status.c | 27 ++++++++
 src/include/pgstat.h                        | 30 ++++++--
 src/include/utils/backend_status.h          |  2 +
 4 files changed, 125 insertions(+), 11 deletions(-)

diff --git a/src/backend/postmaster/pgstat.c b/src/backend/postmaster/pgstat.c
index 2102c0be98..8b8e3ccfcb 100644
--- a/src/backend/postmaster/pgstat.c
+++ b/src/backend/postmaster/pgstat.c
@@ -1545,6 +1545,36 @@ BackendType idx_get_backend_type(int idx)
 	return backend_type;
 }
 
+/*
+ * Helper function to collect and send live backends' current IO operations
+ * stats counters when a stats reset is initiated so that they may be deducted
+ * from future totals.
+ */
+static void
+pgstat_send_buffers_reset(PgStat_MsgResetsharedcounter *msg)
+{
+	PgStatIOPathOps ops[BACKEND_NUM_TYPES];
+
+	memset(ops, 0, sizeof(ops));
+	pgstat_report_live_backend_io_path_ops(ops);
+
+	/*
+	 * Iterate through the array of all IOOps for all IOPaths for each
+	 * BackendType.
+	 *
+	 * An individual message is sent for each backend type because sending all
+	 * IO operations in one message would exceed the PGSTAT_MAX_MSG_SIZE of
+	 * 1000.
+	 */
+	for (int i = 0; i < BACKEND_NUM_TYPES; i++)
+	{
+		msg->m_backend_resets.backend_type = idx_get_backend_type(i);
+		memcpy(&msg->m_backend_resets.iop, &ops[i],
+				sizeof(msg->m_backend_resets.iop));
+		pgstat_send(msg, sizeof(PgStat_MsgResetsharedcounter));
+	}
+}
+
 /* ----------
  * pgstat_reset_shared_counters() -
  *
@@ -1562,7 +1592,14 @@ pgstat_reset_shared_counters(const char *target)
 	if (pgStatSock == PGINVALID_SOCKET)
 		return;
 
-	if (strcmp(target, "archiver") == 0)
+	pgstat_setheader(&msg.m_hdr, PGSTAT_MTYPE_RESETSHAREDCOUNTER);
+	if (strcmp(target, "buffers") == 0)
+	{
+		msg.m_resettarget = RESET_BUFFERS;
+		pgstat_send_buffers_reset(&msg);
+		return;
+	}
+	else if (strcmp(target, "archiver") == 0)
 		msg.m_resettarget = RESET_ARCHIVER;
 	else if (strcmp(target, "bgwriter") == 0)
 		msg.m_resettarget = RESET_BGWRITER;
@@ -1572,9 +1609,10 @@ pgstat_reset_shared_counters(const char *target)
 		ereport(ERROR,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("unrecognized reset target: \"%s\"", target),
-				 errhint("Target must be \"archiver\", \"bgwriter\", or \"wal\".")));
+				 errhint(
+					 "Target must be \"archiver\", \"bgwriter\", \"buffers\", or \"wal\".")));
+

@@ -4464,6 +4502,7 @@ pgstat_read_statsfiles(Oid onlydb, bool permanent, bool deep)
 	 */
 	ts = GetCurrentTimestamp();
 	globalStats.bgwriter.stat_reset_timestamp = ts;
+	globalStats.buffers.stat_reset_timestamp = ts;
 	archiverStats.stat_reset_timestamp = ts;
 	walStats.stat_reset_timestamp = ts;
 
@@ -5629,10 +5668,38 @@ pgstat_recv_resetsharedcounter(PgStat_MsgResetsharedcounter *msg, int len)
 {
 	if (msg->m_resettarget == RESET_BGWRITER)
 	{
-		/* Reset the global, bgwriter and checkpointer statistics for the cluster. */
-		memset(&globalStats, 0, sizeof(globalStats));
+		/*
+		 * Reset the global bgwriter and checkpointer statistics for the
+		 * cluster.
+		 */
+		memset(&globalStats.checkpointer, 0, sizeof(globalStats.checkpointer));
+		memset(&globalStats.bgwriter, 0, sizeof(globalStats.bgwriter));
 		globalStats.bgwriter.stat_reset_timestamp = GetCurrentTimestamp();
 	}
+	else if (msg->m_resettarget == RESET_BUFFERS)
+	{
+		/*
+		 * Because the stats collector cannot write to live backends'
+		 * PgBackendStatuses, it maintains an array of "resets". The reset
+		 * message contains the current values of these counters for live
+		 * backends. The stats collector saves these in its "resets" array,
+		 * then zeroes out the exited backends' saved IO operations counters.
+		 * This is required to calculate an accurate total for each IO
+		 * operations counter post reset.
+		 */
+		BackendType backend_type = msg->m_backend_resets.backend_type;
+
+		/*
+		 * Though globalStats.buffers only needs to be reset once, doing so for
+		 * every message is less brittle and the extra cost is irrelevant given
+		 * how often stats are reset.
+		 */
+		memset(&globalStats.buffers.ops, 0, sizeof(globalStats.buffers.ops));
+		globalStats.buffers.stat_reset_timestamp = GetCurrentTimestamp();
+		memcpy(&globalStats.buffers.resets[backend_type_get_idx(backend_type)],
+				&msg->m_backend_resets.iop.io_path_ops,
+				sizeof(msg->m_backend_resets.iop.io_path_ops));
+	}
 	else if (msg->m_resettarget == RESET_ARCHIVER)
 	{
 		/* Reset the archiver statistics for the cluster. */
diff --git a/src/backend/utils/activity/backend_status.c b/src/backend/utils/activity/backend_status.c
index 44c9a6e1a6..9fb888f2ca 100644
--- a/src/backend/utils/activity/backend_status.c
+++ b/src/backend/utils/activity/backend_status.c
@@ -631,6 +631,33 @@ pgstat_report_activity(BackendState state, const char *cmd_str)
 	PGSTAT_END_WRITE_ACTIVITY(beentry);
 }
 
+/*
+ * Iterate through BackendStatusArray and capture live backends' stats on IOOps
+ * for all IOPaths, adding them to that backend type's member of the
+ * backend_io_path_ops structure.
+ */
+void
+pgstat_report_live_backend_io_path_ops(PgStatIOPathOps *backend_io_path_ops)
+{
+	PgBackendStatus *beentry = BackendStatusArray;
+
+	/*
+	 * Loop through live backends and capture reset values
+	 */
+	for (int i = 0; i < MaxBackends + NUM_AUXPROCTYPES; i++, beentry++)
+	{
+		int idx;
+
+		/* Don't count dead backends or those with type B_INVALID. */
+		if (beentry->st_procpid == 0 || beentry->st_backendType == B_INVALID)
+			continue;
+
+		idx = backend_type_get_idx(beentry->st_backendType);
+		pgstat_sum_io_path_ops(backend_io_path_ops[idx].io_path_ops,
+				(IOOpCounters *) beentry->io_path_stats);
+	}
+}
+
 /* --------
  * pgstat_report_query_id() -
  *
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 5c6ec5b9ad..c7c304c4d8 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -142,6 +142,7 @@ typedef enum PgStat_Shared_Reset_Target
 {
 	RESET_ARCHIVER,
 	RESET_BGWRITER,
+	RESET_BUFFERS,
 	RESET_WAL
 } PgStat_Shared_Reset_Target;
 
@@ -357,7 +358,8 @@ typedef struct PgStatIOPathOps
 
 /*
  * Sent by a backend to the stats collector to report all IOOps for all IOPaths
- * for a given type of a backend. This will happen when the backend exits.
+ * for a given type of a backend. This will happen when the backend exits or
+ * when stats are reset.
  */
 typedef struct PgStat_MsgIOPathOps
 {
@@ -369,15 +371,18 @@ typedef struct PgStat_MsgIOPathOps
 
 /*
  * Structure used by stats collector to keep track of all types of exited
- * backends' IO Ops for all IO Paths as well as all stats from live backends at
+ * backends' IOOps for all IOPaths as well as all stats from live backends at
  * the time of stats reset. resets is populated using a reset message sent to
  * the stats collector.
  */
 typedef struct PgStat_BackendIOPathOps
 {
+	TimestampTz stat_reset_timestamp;
 	PgStatIOPathOps ops[BACKEND_NUM_TYPES];
+	PgStatIOPathOps resets[BACKEND_NUM_TYPES];
 } PgStat_BackendIOPathOps;
 
+
 /* ----------
  * PgStat_MsgResetcounter		Sent by the backend to tell the collector
  *								to reset counters
@@ -389,15 +394,28 @@ typedef struct PgStat_MsgResetcounter
 	Oid			m_databaseid;
 } PgStat_MsgResetcounter;
 
-/* ----------
- * PgStat_MsgResetsharedcounter Sent by the backend to tell the collector
- *								to reset a shared counter
- * ----------
+/*
+ * Sent by the backend to tell the collector to reset a shared counter.
+ *
+ * In addition to the message header and reset target, the message also
+ * contains an array with all of the IO operations for all IO paths done by a
+ * particular backend type.
+ *
+ * This is needed because the IO operation stats for live backends cannot be
+ * safely modified by other processes. Therefore, to correctly calculate the
+ * total IO operations for a particular backend type after a reset, the balance
+ * of IO operations for live backends at the time of prior resets must be
+ * subtracted from the total IO operations.
+ *
+ * To satisfy this requirement the process initiating the reset will read the
+ * IO operations from live backends and send them to the stats collector which
+ * maintains an array of reset values.
  */
 typedef struct PgStat_MsgResetsharedcounter
 {
 	PgStat_MsgHdr m_hdr;
 	PgStat_Shared_Reset_Target m_resettarget;
+	PgStat_MsgIOPathOps m_backend_resets;
 } PgStat_MsgResetsharedcounter;
 
 /* ----------
diff --git a/src/include/utils/backend_status.h b/src/include/utils/backend_status.h
index 0ee450cda1..55e30022f6 100644
--- a/src/include/utils/backend_status.h
+++ b/src/include/utils/backend_status.h
@@ -342,6 +342,7 @@ extern void pgstat_bestart(void);
 extern void pgstat_clear_backend_activity_snapshot(void);

 static inline void
@@ -369,6 +370,7 @@ pgstat_inc_ioop(IOOp io_op, IOPath io_path)

From baae117915069f3807c67c98b649dbeab71fc47c Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Wed, 24 Nov 2021 12:20:10 -0500
Subject: [PATCH v18 7/8] Remove superfluous bgwriter stats

Remove stats from pg_stat_bgwriter which are now more clearly expressed
in pg_stat_buffers. Rename pg_stat_bgwriter to pg_stat_checkpointer
since most of the stats now concern checkpointer.

TODO:
- make pg_stat_bgwriter view again and move maxwritten_clean into it
- add additional stats to pg_stat_bgwriter
---
 doc/src/sgml/monitoring.sgml          | 69 +++++----------------------
 src/backend/catalog/system_views.sql  | 11 ++---
 src/backend/postmaster/checkpointer.c | 29 +----------
 src/backend/postmaster/pgstat.c       | 13 ++---
 src/backend/storage/buffer/bufmgr.c   |  6 ---
 src/backend/utils/adt/pgstatfuncs.c   | 34 +------------
 src/include/catalog/pg_proc.dat       | 34 +++----------
 src/include/pgstat.h                  | 12 +----
 src/test/regress/expected/rules.out   | 17 +++----
 9 files changed, 35 insertions(+), 190 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index b16952d439..412f0d2502 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -427,11 +427,11 @@ postgres   27093  0.0  0.0  30096  2752 ?        Ss   11:34   0:00 postgres: ser
      </row>
 
      <row>
-      <entry><structname>pg_stat_bgwriter</structname><indexterm><primary>pg_stat_bgwriter</primary></indexterm></entry>
+      <entry><structname>pg_stat_checkpointer</structname><indexterm><primary>pg_stat_checkpointer</primary></indexterm></entry>
       <entry>One row only, showing statistics about the
        background writer process's activity. See
-       <link linkend="monitoring-pg-stat-bgwriter-view">
-       <structname>pg_stat_bgwriter</structname></link> for details.
+       <link linkend="monitoring-pg-stat-checkpointer-view">
+       <structname>pg_stat_checkpointer</structname></link> for details.
      </entry>
      </row>
 
@@ -3485,20 +3485,20 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
 
  </sect2>
 
- <sect2 id="monitoring-pg-stat-bgwriter-view">
-  <title><structname>pg_stat_bgwriter</structname></title>
+ <sect2 id="monitoring-pg-stat-checkpointer-view">
+  <title><structname>pg_stat_checkpointer</structname></title>
 
   <indexterm>
-   <primary>pg_stat_bgwriter</primary>
+   <primary>pg_stat_checkpointer</primary>
   </indexterm>
 
   <para>
-   The <structname>pg_stat_bgwriter</structname> view will always have a
+   The <structname>pg_stat_checkpointer</structname> view will always have a
    single row, containing global data for the cluster.
   </para>
 
-  <table id="pg-stat-bgwriter-view" xreflabel="pg_stat_bgwriter">
-   <title><structname>pg_stat_bgwriter</structname> View</title>
+  <table id="pg-stat-checkpointer-view" xreflabel="pg_stat_checkpointer">
+   <title><structname>pg_stat_checkpointer</structname> View</title>
    <tgroup cols="1">
     <thead>
      <row>
@@ -3551,24 +3551,6 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
       </para></entry>
      </row>
 
-     <row>
@@ -3579,35 +3561,6 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
       </para></entry>
      </row>
 
-     <row>
-      <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>buffers_backend</structfield> <type>bigint</type>
-      </para>
-      <para>
-       Number of buffers written directly by a backend
-      </para></entry>
-     </row>
-
-     <row>
-      <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>buffers_backend_fsync</structfield> <type>bigint</type>
-      </para>
-      <para>
-       Number of times a backend had to execute its own
-       <function>fsync</function> call (normally the background writer handles those
-       even when the backend does its own write)
-      </para></entry>
-     </row>
-
-     <row>
-      <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>buffers_alloc</structfield> <type>bigint</type>
-      </para>
-      <para>
-       Number of buffers allocated
-      </para></entry>
-     </row>
-
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>stats_reset</structfield> <type>timestamp with time zone</type>
@@ -5313,9 +5266,9 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
        </para>
        <para>
         Resets some cluster-wide statistics counters to zero, depending on the
-        argument.  The argument can be <literal>bgwriter</literal> to reset
+        argument.  The argument can be <literal>checkpointer</literal> to reset
         all the counters shown in
-        the <structname>pg_stat_bgwriter</structname>
+        the <structname>pg_stat_checkpointer</structname>
         view, <literal>archiver</literal> to reset all the counters shown in
         the <structname>pg_stat_archiver</structname> view, <literal>wal</literal>
         to reset all the counters shown in the <structname>pg_stat_wal</structname> view, 
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e214d23056..6258c2770c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1062,18 +1062,13 @@ CREATE VIEW pg_stat_archiver AS
         s.stats_reset
     FROM pg_stat_get_archiver() s;
 
-CREATE VIEW pg_stat_bgwriter AS
+CREATE VIEW pg_stat_checkpointer AS
     SELECT
-        pg_stat_get_bgwriter_timed_checkpoints() AS checkpoints_timed,
-        pg_stat_get_bgwriter_requested_checkpoints() AS checkpoints_req,
+        pg_stat_get_timed_checkpoints() AS checkpoints_timed,
+        pg_stat_get_requested_checkpoints() AS checkpoints_req,
         pg_stat_get_checkpoint_write_time() AS checkpoint_write_time,
         pg_stat_get_checkpoint_sync_time() AS checkpoint_sync_time,
-        pg_stat_get_bgwriter_buf_written_checkpoints() AS buffers_checkpoint,
-        pg_stat_get_bgwriter_buf_written_clean() AS buffers_clean,
         pg_stat_get_bgwriter_maxwritten_clean() AS maxwritten_clean,
-        pg_stat_get_buf_written_backend() AS buffers_backend,
-        pg_stat_get_buf_fsync_backend() AS buffers_backend_fsync,
-        pg_stat_get_buf_alloc() AS buffers_alloc,
         pg_stat_get_bgwriter_stat_reset_time() AS stats_reset;
 
index 8440b2b802..b9c3745474 100644
 
@@ -1081,10 +1070,6 @@ ForwardSyncRequest(const FileTag *ftag, SyncRequestType type)

@@ -1094,13 +1079,12 @@ ForwardSyncRequest(const FileTag *ftag, SyncRequestType type)
@@ -1257,15 +1241,6 @@ AbsorbSyncRequests(void)

-	/* Transfer stats counts into pending pgstats message */
-	PendingCheckpointerStats.m_buf_written_backend
-		+= CheckpointerShmem->num_backend_writes;
-	PendingCheckpointerStats.m_buf_fsync_backend
-		+= CheckpointerShmem->num_backend_fsync;
-
-	CheckpointerShmem->num_backend_writes = 0;
-	CheckpointerShmem->num_backend_fsync = 0;
-
 	/*
 	 * We try to avoid holding the lock for a long time by copying the request
 	 * array, and processing the requests after releasing the lock.
diff --git a/src/backend/postmaster/pgstat.c b/src/backend/postmaster/pgstat.c
index 3545e197ce..13e46f497f 100644
--- a/src/backend/postmaster/pgstat.c
+++ b/src/backend/postmaster/pgstat.c
@@ -1601,8 +1601,8 @@ pgstat_reset_shared_counters(const char *target)
 	}
 	else if (strcmp(target, "archiver") == 0)
 		msg.m_resettarget = RESET_ARCHIVER;
-	else if (strcmp(target, "bgwriter") == 0)
-		msg.m_resettarget = RESET_BGWRITER;
+	else if (strcmp(target, "checkpointer") == 0)
+		msg.m_resettarget = RESET_CHECKPOINTER;
 	else if (strcmp(target, "wal") == 0)
 		msg.m_resettarget = RESET_WAL;
 	else
@@ -1610,7 +1610,7 @@ pgstat_reset_shared_counters(const char *target)
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("unrecognized reset target: \"%s\"", target),
 				 errhint(
-					 "Target must be \"archiver\", \"bgwriter\", \"buffers\", or \"wal\".")));
+					 "Target must be \"archiver\", \"checkpointer\", \"buffers\", or \"wal\".")));

 	pgstat_send(&msg, sizeof(msg));
@@ -5679,7 +5679,7 @@ pgstat_recv_resetcounter(PgStat_MsgResetcounter *msg, int len)
 static void
 pgstat_recv_resetsharedcounter(PgStat_MsgResetsharedcounter *msg, int len)
 {
-	if (msg->m_resettarget == RESET_BGWRITER)
+	if (msg->m_resettarget == RESET_CHECKPOINTER)
 	{
 		/*
 		 * Reset the global bgwriter and checkpointer statistics for the
@@ -5985,9 +5985,7 @@ pgstat_recv_archiver(PgStat_MsgArchiver *msg, int len)
 static void
 pgstat_recv_bgwriter(PgStat_MsgBgWriter *msg, int len)
 {
-	globalStats.bgwriter.buf_written_clean += msg->m_buf_written_clean;
 	globalStats.bgwriter.maxwritten_clean += msg->m_maxwritten_clean;
-	globalStats.bgwriter.buf_alloc += msg->m_buf_alloc;
 }
 
 /* ----------
@@ -6003,9 +6001,6 @@ pgstat_recv_checkpointer(PgStat_MsgCheckpointer *msg, int len)
 
 static void
index 949f1088b6..8120567de1 100644

-	PendingBgWriterStats.m_buf_written_clean += num_written;
-
 #ifdef BGW_DEBUG
 	elog(DEBUG1, "bgwriter: recent_alloc=%u smoothed=%.2f delta=%ld ahead=%d density=%.2f reusable_est=%d upcoming_est=%d scanned=%d wrote=%d reusable=%d",
 		 recent_alloc, smoothed_alloc, strategy_delta, bufs_ahead,
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 26730230b6..52500c8af4 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1727,29 +1727,17 @@ pg_stat_get_db_sessions_killed(PG_FUNCTION_ARGS)
 }
 
 Datum
-pg_stat_get_bgwriter_timed_checkpoints(PG_FUNCTION_ARGS)
+pg_stat_get_timed_checkpoints(PG_FUNCTION_ARGS)
 {
 	PG_RETURN_INT64(pgstat_fetch_stat_checkpointer()->timed_checkpoints);
 }
 
 Datum
-pg_stat_get_bgwriter_requested_checkpoints(PG_FUNCTION_ARGS)
+pg_stat_get_requested_checkpoints(PG_FUNCTION_ARGS)
 {
 	PG_RETURN_INT64(pgstat_fetch_stat_checkpointer()->requested_checkpoints);
 }
 
-Datum
 
-Datum
-pg_stat_get_buf_written_backend(PG_FUNCTION_ARGS)
-{
-	PG_RETURN_INT64(pgstat_fetch_stat_checkpointer()->buf_written_backend);
-}
-
-Datum
-pg_stat_get_buf_fsync_backend(PG_FUNCTION_ARGS)
-{
-	PG_RETURN_INT64(pgstat_fetch_stat_checkpointer()->buf_fsync_backend);
-}
-
-Datum
-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_buffers view, add a new enum
 * value here above BUFFERS_NUM_COLUMNS.
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 148208d242..293df23040 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5585,25 +5585,15 @@
   proargnames => '{archived_count,last_archived_wal,last_archived_time,failed_count,last_failed_wal,last_failed_time,stats_reset}',
   prosrc => 'pg_stat_get_archiver' },
 { oid => '2769',
-  descr => 'statistics: number of timed checkpoints started by the bgwriter',
-  proname => 'pg_stat_get_bgwriter_timed_checkpoints', provolatile => 's',
+  descr => 'statistics: number of scheduled checkpoints performed',
+  proname => 'pg_stat_get_timed_checkpoints', provolatile => 's',
   proparallel => 'r', prorettype => 'int8', proargtypes => '',
-  prosrc => 'pg_stat_get_bgwriter_timed_checkpoints' },
+  prosrc => 'pg_stat_get_timed_checkpoints' },
 { oid => '2770',
-  descr => 'statistics: number of backend requested checkpoints started by the bgwriter',
-  proname => 'pg_stat_get_bgwriter_requested_checkpoints', provolatile => 's',
+  descr => 'statistics: number of backend requested checkpoints performed',
+  proname => 'pg_stat_get_requested_checkpoints', provolatile => 's',
   proparallel => 'r', prorettype => 'int8', proargtypes => '',
-  prosrc => 'pg_stat_get_bgwriter_requested_checkpoints' },
-{ oid => '2771',
-  descr => 'statistics: number of buffers written by the bgwriter during checkpoints',
-  proname => 'pg_stat_get_bgwriter_buf_written_checkpoints', provolatile => 's',
-  proparallel => 'r', prorettype => 'int8', proargtypes => '',
-  prosrc => 'pg_stat_get_bgwriter_buf_written_checkpoints' },
-{ oid => '2772',
-  descr => 'statistics: number of buffers written by the bgwriter for cleaning dirty buffers',
-  proname => 'pg_stat_get_bgwriter_buf_written_clean', provolatile => 's',
-  proparallel => 'r', prorettype => 'int8', proargtypes => '',
-  prosrc => 'pg_stat_get_bgwriter_buf_written_clean' },
+  prosrc => 'pg_stat_get_requested_checkpoints' },
 { oid => '2773',
   descr => 'statistics: number of times the bgwriter stopped processing when it had written too many buffers while cleaning',
   proname => 'pg_stat_get_bgwriter_maxwritten_clean', provolatile => 's',
@@ -5623,18 +5613,6 @@
   proname => 'pg_stat_get_checkpoint_sync_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => '',
   prosrc => 'pg_stat_get_checkpoint_sync_time' },
-{ oid => '2775', descr => 'statistics: number of buffers written by backends',
-  proname => 'pg_stat_get_buf_written_backend', provolatile => 's',
-  proparallel => 'r', prorettype => 'int8', proargtypes => '',
-  prosrc => 'pg_stat_get_buf_written_backend' },
-{ oid => '3063',
-  descr => 'statistics: number of backend buffer writes that did their own fsync',
-  proname => 'pg_stat_get_buf_fsync_backend', provolatile => 's',
-  proparallel => 'r', prorettype => 'int8', proargtypes => '',
-  prosrc => 'pg_stat_get_buf_fsync_backend' },
-{ oid => '2859', descr => 'statistics: number of buffer allocations',
-  proname => 'pg_stat_get_buf_alloc', provolatile => 's', proparallel => 'r',
-  prorettype => 'int8', proargtypes => '', prosrc => 'pg_stat_get_buf_alloc' },
 
 { oid => '8459', descr => 'statistics: counts of all IO operations done to all IO paths by each type of backend.',
   proname => 'pg_stat_get_buffers', provolatile => 's', proisstrict => 'f',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e07584767..020cd4ca87 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -141,7 +141,7 @@ typedef struct PgStat_TableCounts
 typedef enum PgStat_Shared_Reset_Target
 {
 	RESET_ARCHIVER,
-	RESET_BGWRITER,
+	RESET_CHECKPOINTER,
 	RESET_BUFFERS,
 	RESET_WAL
 } PgStat_Shared_Reset_Target;
@@ -523,9 +523,7 @@ typedef struct PgStat_MsgBgWriter
 {
 	PgStat_MsgHdr m_hdr;

 /* ----------
@@ -538,9 +536,6 @@ typedef struct PgStat_MsgCheckpointer
 
@@ -971,9 +966,7 @@ typedef struct PgStat_ArchiverStats
 
@@ -987,9 +980,6 @@ typedef struct PgStat_CheckpointerStats
 
 /*
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 5869ce442f..13f48722c3 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1817,17 +1817,6 @@ pg_stat_archiver| SELECT s.archived_count,
     s.last_failed_time,
     s.stats_reset
    FROM pg_stat_get_archiver() s(archived_count, last_archived_wal, last_archived_time, failed_count, last_failed_wal, last_failed_time, stats_reset);
-pg_stat_bgwriter| SELECT pg_stat_get_bgwriter_timed_checkpoints() AS checkpoints_timed,
-    pg_stat_get_bgwriter_requested_checkpoints() AS checkpoints_req,
-    pg_stat_get_checkpoint_write_time() AS checkpoint_write_time,
-    pg_stat_get_checkpoint_sync_time() AS checkpoint_sync_time,
-    pg_stat_get_bgwriter_buf_written_checkpoints() AS buffers_checkpoint,
-    pg_stat_get_bgwriter_buf_written_clean() AS buffers_clean,
-    pg_stat_get_bgwriter_maxwritten_clean() AS maxwritten_clean,
-    pg_stat_get_buf_written_backend() AS buffers_backend,
-    pg_stat_get_buf_fsync_backend() AS buffers_backend_fsync,
-    pg_stat_get_buf_alloc() AS buffers_alloc,
-    pg_stat_get_bgwriter_stat_reset_time() AS stats_reset;
 pg_stat_buffers| SELECT b.backend_type,
     b.io_path,
     b.alloc,
@@ -1836,6 +1825,12 @@ pg_stat_buffers| SELECT b.backend_type,
     b.write,
     b.stats_reset
    FROM pg_stat_get_buffers() b(backend_type, io_path, alloc, extend, fsync, write, stats_reset);
+pg_stat_checkpointer| SELECT pg_stat_get_timed_checkpoints() AS checkpoints_timed,
+    pg_stat_get_requested_checkpoints() AS checkpoints_req,
+    pg_stat_get_checkpoint_write_time() AS checkpoint_write_time,
+    pg_stat_get_checkpoint_sync_time() AS checkpoint_sync_time,
+    pg_stat_get_bgwriter_maxwritten_clean() AS maxwritten_clean,
+    pg_stat_get_bgwriter_stat_reset_time() AS stats_reset;
 pg_stat_database| SELECT d.oid AS datid,
     d.datname,
         CASE
-- 
2.30.2

From e26f274748e3857515e476d43370df4376ca770c Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Wed, 24 Nov 2021 11:16:55 -0500
Subject: [PATCH v18 4/8] Send IO operations to stats collector

On exit, backends send the IO operations they have done on all IO Paths
to the stats collector. The stats collector adds these counts to its
existing counts stored in a global data structure it maintains and
persists.

PgStatIOOpCounters contains the same information as backend_status.h's
IOOpCounters, however IOOpCounters' members must be atomics and the
stats collector has no such requirement.

Suggested by Andres Freund

Author: Melanie Plageman <[email-masked]>
Reviewed-by: Justin Pryzby <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/20200124195226.lth52iydq2n2uilq%40alap3.anarazel.de
---
 src/backend/postmaster/pgstat.c    | 143 ++++++++++++++++++++++++++++-
 src/include/miscadmin.h            |   2 +
 src/include/pgstat.h               |  56 +++++++++++
 src/include/utils/backend_status.h |   4 +
 4 files changed, 202 insertions(+), 3 deletions(-)

diff --git a/src/backend/postmaster/pgstat.c b/src/backend/postmaster/pgstat.c
index 7264d2c727..2102c0be98 100644
--- a/src/backend/postmaster/pgstat.c
+++ b/src/backend/postmaster/pgstat.c
@@ -126,9 +126,12 @@ char	   *pgstat_stat_filename = NULL;
 char	   *pgstat_stat_tmpname = NULL;
 
 /*
- * BgWriter and WAL global statistics counters.
- * Stored directly in a stats message structure so they can be sent
- * without needing to copy things around.  We assume these init to zeroes.
+ * BgWriter, Checkpointer, WAL, and IO global statistics counters. IO global
+ * statistics on various IO operations are tracked in PgBackendStatus while a
+ * backend is alive and then sent to stats collector before a backend exits in
+ * a PgStat_MsgIOPathOps.
+ * All others are stored directly in a stats message structure so they can be
+ * sent without needing to copy things around. We assume these init to zeroes.
  */
 PgStat_MsgBgWriter PendingBgWriterStats;
 PgStat_MsgCheckpointer PendingCheckpointerStats;
@@ -369,6 +372,7 @@ static void pgstat_recv_analyze(PgStat_MsgAnalyze *msg, int len);
 static void pgstat_recv_archiver(PgStat_MsgArchiver *msg, int len);
 static void pgstat_recv_bgwriter(PgStat_MsgBgWriter *msg, int len);
 static void pgstat_recv_checkpointer(PgStat_MsgCheckpointer *msg, int len);
+static void pgstat_recv_io_path_ops(PgStat_MsgIOPathOps *msg, int len);
 static void pgstat_recv_wal(PgStat_MsgWal *msg, int len);
 static void pgstat_recv_slru(PgStat_MsgSLRU *msg, int len);
 static void pgstat_recv_funcstat(PgStat_MsgFuncstat *msg, int len);
@@ -1508,6 +1512,39 @@ pgstat_reset_counters(void)
 	pgstat_send(&msg, sizeof(msg));
 }
 
+/*
+ * When maintaining an array of information about all valid BackendTypes, in
+ * order to avoid wasting the 0th spot, use this helper to convert a valid
+ * BackendType to a valid location in the array (given that no spot is
+ * maintained for B_INVALID BackendType).
+ */
+int backend_type_get_idx(BackendType backend_type)
+{
+	/*
+	 * backend_type must be one of the valid backend types. If caller is
+	 * maintaining backend information in an array that includes B_INVALID,
+	 * this function is unnecessary.
+	 */
+	Assert(backend_type > B_INVALID && backend_type <= BACKEND_NUM_TYPES);
+	return backend_type - 1;
+}
+
+/*
+ * When using a value from an array of information about all valid
+ * BackendTypes, add 1 to the index before using it as a BackendType to adjust
+ * for not maintaining a spot for B_INVALID BackendType.
+ */
+BackendType idx_get_backend_type(int idx)
+{
+	int backend_type = idx + 1;
+	/*
+	 * If the array includes a spot for B_INVALID BackendType this function is
+	 * not required.
+	 */
+	Assert(backend_type > B_INVALID && backend_type <= BACKEND_NUM_TYPES);
+	return backend_type;
+}
+
 /* ----------
  * pgstat_reset_shared_counters() -
  *
@@ -3152,6 +3189,14 @@ pgstat_shutdown_hook(int code, Datum arg)
 {
 	Assert(!pgstat_is_shutdown);
 
+	/*
+	 * Only need to send stats on IOOps for IOPaths when a process exits. Users
+	 * requiring IOOps for both live and exited backends can read from live
+	 * backends' PgBackendStatus and sum this with totals from exited backends
+	 * persisted by the stats collector.
+	 */
+	pgstat_send_buffers();
+
 	/*
 	 * If we got as far as discovering our own database ID, we can report what
 	 * we did to the collector.  Otherwise, we'd be sending an invalid
@@ -3301,6 +3346,49 @@ pgstat_send_bgwriter(void)
 	MemSet(&PendingBgWriterStats, 0, sizeof(PendingBgWriterStats));
 }
 
+/*
+ * Before exiting, a backend sends its IO operations statistics to the
+ * collector so that they may be persisted.
+ */
+void
+pgstat_send_buffers(void)
+{
+	PgStatIOOpCounters *io_path_ops;
+	PgStat_MsgIOPathOps msg;
+
+	PgBackendStatus *beentry = MyBEEntry;
+	PgStat_Counter sum = 0;
+
+	if (!beentry || beentry->st_backendType == B_INVALID)
+		return;
+
+	memset(&msg, 0, sizeof(msg));
+	msg.backend_type = beentry->st_backendType;
+
+	io_path_ops = msg.iop.io_path_ops;
+	pgstat_sum_io_path_ops(io_path_ops, (IOOpCounters *)
+			&beentry->io_path_stats);
+
+	/*
+	 * Check if no IO was done. If so, don't bother sending anything to the
+	 * stats collector.
+	 */
+	for (int i = 0; i < IOPATH_NUM_TYPES; i++)
+	{
+		sum += io_path_ops[i].allocs;
+		sum += io_path_ops[i].extends;
+		sum += io_path_ops[i].fsyncs;
+		sum += io_path_ops[i].writes;
+	}
+
+	if (sum == 0)
+		return;
+
+	pgstat_setheader(&msg.m_hdr, PGSTAT_MTYPE_IO_PATH_OPS);
+	pgstat_send(&msg, sizeof(msg));
+}
+
+
 /* ----------
  * pgstat_send_checkpointer() -
  *
@@ -3483,6 +3571,29 @@ pgstat_send_subscription_purge(PgStat_MsgSubscriptionPurge *msg)
 	pgstat_send(msg, len);
 }
 
+/*
+ * Helper function to sum all IO operations stats for all IOPaths (e.g. shared,
+ * local) from live backends to those in the equivalent stats structure for
+ * exited backends.
+ * Note that this adds and doesn't set, so the destination stats structure
+ * should be zeroed out by the caller initially.
+ * This would commonly be used to transfer all IOOp stats for all IOPaths for a
+ * particular backend type to the pgstats structure.
+ */
+void
+pgstat_sum_io_path_ops(PgStatIOOpCounters *dest, IOOpCounters *src)
+{
+	for (int i = 0; i < IOPATH_NUM_TYPES; i++)
+	{
+		dest->allocs += pg_atomic_read_u64(&src->allocs);
+		dest->extends += pg_atomic_read_u64(&src->extends);
+		dest->fsyncs += pg_atomic_read_u64(&src->fsyncs);
+		dest->writes += pg_atomic_read_u64(&src->writes);
+		dest++;
+		src++;
+	}
+}
+
 /* ----------
  * PgstatCollectorMain() -
  *
@@ -3692,6 +3803,10 @@ PgstatCollectorMain(int argc, char *argv[])
 					pgstat_recv_checkpointer(&msg.msg_checkpointer, len);
 					break;
 
@@ -5813,6 +5928,28 @@ pgstat_recv_checkpointer(PgStat_MsgCheckpointer *msg, int len)
 
+static void
+pgstat_recv_io_path_ops(PgStat_MsgIOPathOps *msg, int len)
+{
+	PgStatIOOpCounters *src_io_path_ops;
+	PgStatIOOpCounters *dest_io_path_ops;
+
+	src_io_path_ops = msg->iop.io_path_ops;
+	dest_io_path_ops =
+		globalStats.buffers.ops[backend_type_get_idx(msg->backend_type)].io_path_ops;
+
+	for (int i = 0; i < IOPATH_NUM_TYPES; i++)
+	{
+		PgStatIOOpCounters *src = &src_io_path_ops[i];
+		PgStatIOOpCounters *dest = &dest_io_path_ops[i];
+
+		dest->allocs += src->allocs;
+		dest->extends += src->extends;
+		dest->fsyncs += src->fsyncs;
+		dest->writes += src->writes;
+	}
+}
+
 /* ----------
  * pgstat_recv_wal() -
  *
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 90a3016065..662170c72e 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -338,6 +338,8 @@ typedef enum BackendType
 	B_LOGGER,
 } BackendType;
 
+#define BACKEND_NUM_TYPES B_LOGGER
+
 extern BackendType MyBackendType;
 
 extern const char *GetBackendTypeDesc(BackendType backendType);
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 5b51b58e5a..5c6ec5b9ad 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -73,6 +73,7 @@ typedef enum StatMsgType
 	PGSTAT_MTYPE_ARCHIVER,
 	PGSTAT_MTYPE_BGWRITER,
 	PGSTAT_MTYPE_CHECKPOINTER,
+	PGSTAT_MTYPE_IO_PATH_OPS,
 	PGSTAT_MTYPE_WAL,
 	PGSTAT_MTYPE_SLRU,
 	PGSTAT_MTYPE_FUNCSTAT,
@@ -335,6 +336,48 @@ typedef struct PgStat_MsgDropdb
 } PgStat_MsgDropdb;

+/*
+ * Structure for counting all types of IOOps in the stats collector
+ */
+typedef struct PgStatIOOpCounters
+{
+	PgStat_Counter allocs;
+	PgStat_Counter extends;
+	PgStat_Counter fsyncs;
+	PgStat_Counter writes;
+} PgStatIOOpCounters;
+
+/*
+ * Structure for counting all IOOps on all types of IOPaths.
+ */
+typedef struct PgStatIOPathOps
+{
+	PgStatIOOpCounters io_path_ops[IOPATH_NUM_TYPES];
+} PgStatIOPathOps;
+
+/*
+ * Sent by a backend to the stats collector to report all IOOps for all IOPaths
+ * for a given type of a backend. This will happen when the backend exits.
+ */
+typedef struct PgStat_MsgIOPathOps
+{
+	PgStat_MsgHdr m_hdr;
+
+	BackendType backend_type;
+	PgStatIOPathOps iop;
+} PgStat_MsgIOPathOps;
+
+/*
+ * Structure used by stats collector to keep track of all types of exited
+ * backends' IO Ops for all IO Paths as well as all stats from live backends at
+ * the time of stats reset. resets is populated using a reset message sent to
+ * the stats collector.
+ */
+typedef struct PgStat_BackendIOPathOps
+{
+	PgStatIOPathOps ops[BACKEND_NUM_TYPES];
+} PgStat_BackendIOPathOps;
+
 /* ----------
  * PgStat_MsgResetcounter		Sent by the backend to tell the collector
  *								to reset counters
@@ -756,6 +799,7 @@ typedef union PgStat_Msg
 	PgStat_MsgArchiver msg_archiver;
 	PgStat_MsgBgWriter msg_bgwriter;
 	PgStat_MsgCheckpointer msg_checkpointer;
+	PgStat_MsgIOPathOps msg_io_path_ops;
 	PgStat_MsgWal msg_wal;
 	PgStat_MsgSLRU msg_slru;
 	PgStat_MsgFuncstat msg_funcstat;
@@ -939,6 +983,7 @@ typedef struct PgStat_GlobalStats

 /*
@@ -1215,8 +1260,19 @@ extern void pgstat_twophase_postabort(TransactionId xid, uint16 info,
 
 extern void pgstat_send_archiver(const char *xlog, bool failed);
 extern void pgstat_send_bgwriter(void);
+/*
+ * While some processes send some types of statistics to the collector at
+ * regular intervals (e.g. CheckpointerMain() calling
+ * pgstat_send_checkpointer()), IO operations stats are only sent by
+ * pgstat_send_buffers() when a process exits (in pgstat_shutdown_hook()). IO
+ * operations stats from live backends can be read from their PgBackendStatuses
+ * and, if desired, summed with totals from exited backends persisted by the
+ * stats collector.
+ */
+extern void pgstat_send_buffers(void);
 extern void pgstat_send_checkpointer(void);
 extern void pgstat_send_wal(bool force);
+extern void pgstat_sum_io_path_ops(PgStatIOOpCounters *dest, IOOpCounters *src);
 
 /* ----------
  * Support functions for the SQL-callable functions to
diff --git a/src/include/utils/backend_status.h b/src/include/utils/backend_status.h
index 56a0f25296..0ee450cda1 100644
--- a/src/include/utils/backend_status.h
+++ b/src/include/utils/backend_status.h
@@ -331,6 +331,10 @@ extern void CreateSharedBackendStatus(void);
  * ----------
  */
 
+/* Utility functions */
+extern int backend_type_get_idx(BackendType backend_type);
+extern BackendType idx_get_backend_type(int idx);
+
 /* Initialization functions */
 extern void pgstat_beinit(void);
 extern void pgstat_bestart(void);
-- 
2.30.2

From 015b4e3b25aea17c9f8143fce7bb8488cfa12511 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Wed, 24 Nov 2021 12:07:37 -0500
Subject: [PATCH v18 6/8] Add system view tracking IO ops per backend type

Some of these should always be zero. For example, checkpointer does not
use a BufferAccessStrategy (currently), so the "strategy" IO path for
checkpointer will be 0 for all IO operations (alloc, write, fsync, and
extend). All possible combinations of IO Path and IO Op are enumerated
in the view but not all are populated or even possible at this point.

Suggested by Andres Freund

Author: Melanie Plageman <[email-masked]>
Reviewed-by: Justin Pryzby <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/20200124195226.lth52iydq2n2uilq%40alap3.anarazel.de
---
 doc/src/sgml/monitoring.sgml                | 112 ++++++++++++++-
 src/backend/catalog/system_views.sql        |  11 ++
 src/backend/postmaster/pgstat.c             |  13 ++
 src/backend/utils/activity/backend_status.c |  19 ++-
 src/backend/utils/adt/pgstatfuncs.c         | 150 ++++++++++++++++++++
 src/include/catalog/pg_proc.dat             |   9 ++
 src/include/pgstat.h                        |   1 +
 src/include/utils/backend_status.h          |   2 +
 src/test/regress/expected/rules.out         |   8 ++
 9 files changed, 321 insertions(+), 4 deletions(-)

index 62f2a3332b..b16952d439 100644
 
+     <row>
+      <entry><structname>pg_stat_buffers</structname><indexterm><primary>pg_stat_buffers</primary></indexterm></entry>
+      <entry>A row for each IO path for each backend type showing
+      statistics about backend IO operations. See
+       <link linkend="monitoring-pg-stat-buffers-view">
+       <structname>pg_stat_buffers</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
@@ -3604,7 +3613,102 @@ 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>
+
+ </sect2>
+
+ <sect2 id="monitoring-pg-stat-buffers-view">
+  <title><structname>pg_stat_buffers</structname></title>
+
+  <indexterm>
+   <primary>pg_stat_buffers</primary>
+  </indexterm>
+
+  <para>
+   The <structname>pg_stat_buffers</structname> view has a row for each backend
+   type for each possible IO path, containing global data for the cluster for
+   that backend and IO path.
+  </para>
+
+  <table id="pg-stat-buffers-view" xreflabel="pg_stat_buffers">
+   <title><structname>pg_stat_buffers</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).
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>io_path</structfield> <type>text</type>
+      </para>
+      <para>
+       IO path taken (e.g. shared buffers, direct).
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>alloc</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of buffers allocated.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>extend</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of buffers extended.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>fsync</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of buffers fsynced.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>write</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of buffers written.
+      </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>
@@ -5213,8 +5317,10 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
         all the counters shown in
         the <structname>pg_stat_bgwriter</structname>
         view, <literal>archiver</literal> to reset all the counters shown in
-        the <structname>pg_stat_archiver</structname> view or <literal>wal</literal>
-        to reset all the counters shown in the <structname>pg_stat_wal</structname> view.
+        the <structname>pg_stat_archiver</structname> view, <literal>wal</literal>
+        to reset all the counters shown in the <structname>pg_stat_wal</structname> view, 
+        or <literal>buffers</literal> to reset all the counters shown in the
+        <structname>pg_stat_buffers</structname> view.
        </para>
        <para>
         This function is restricted to superusers by default, but other users
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 61b515cdb8..e214d23056 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1076,6 +1076,17 @@ 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_buffers AS
+SELECT
+       b.backend_type,
+       b.io_path,
+       b.alloc,
+       b.extend,
+       b.fsync,
+       b.write,
+       b.stats_reset
+FROM pg_stat_get_buffers() b;
+
 CREATE VIEW pg_stat_wal AS
     SELECT
         w.wal_records,
diff --git a/src/backend/postmaster/pgstat.c b/src/backend/postmaster/pgstat.c
index 8b8e3ccfcb..3545e197ce 100644
--- a/src/backend/postmaster/pgstat.c
+++ b/src/backend/postmaster/pgstat.c
@@ -2959,6 +2959,19 @@ pgstat_twophase_postabort(TransactionId xid, uint16 info,
 		rec->tuples_inserted + rec->tuples_updated;
 }
 
+/*
+ *	Support function for SQL-callable pgstat* functions. Returns a pointer to
+ *	the PgStat_BackendIOPathOps structure tracking IO operations statistics for
+ *	both exited backends and reset arithmetic.
+ */
+PgStat_BackendIOPathOps *
+pgstat_fetch_exited_backend_buffers(void)
+{
+	backend_read_statsfile();
+
+	return &globalStats.buffers;
+}
+
 
 /* ----------
index 9fb888f2ca..08e3f8f167 100644

@@ -236,6 +236,23 @@ CreateSharedBackendStatus(void)
 #endif
 }
 
+const char *
index f529c1561a..26730230b6 100644
@@ -1796,6 +1796,156 @@ pg_stat_get_buf_alloc(PG_FUNCTION_ARGS)
 
+/*
+* When adding a new column to the pg_stat_buffers view, add a new enum
+* value here above BUFFERS_NUM_COLUMNS.
+*/
+enum
+{
+	BUFFERS_COLUMN_BACKEND_TYPE,
+	BUFFERS_COLUMN_IO_PATH,
+	BUFFERS_COLUMN_ALLOCS,
+	BUFFERS_COLUMN_EXTENDS,
+	BUFFERS_COLUMN_FSYNCS,
+	BUFFERS_COLUMN_WRITES,
+	BUFFERS_COLUMN_RESET_TIME,
+	BUFFERS_NUM_COLUMNS,
+};
+
+/*
+ * Helper function to get the correct row in the pg_stat_buffers view.
+ */
+static inline Datum *
+get_pg_stat_buffers_row(Datum all_values[BACKEND_NUM_TYPES][IOPATH_NUM_TYPES][BUFFERS_NUM_COLUMNS],
+		BackendType backend_type, IOPath io_path)
+{
+	return all_values[backend_type_get_idx(backend_type)][io_path];
+}
+
+Datum
+pg_stat_get_buffers(PG_FUNCTION_ARGS)
+{
+	PgStat_BackendIOPathOps *backend_io_path_ops;
+	PgBackendStatus *beentry;
+	Datum		reset_time;
+
+	ReturnSetInfo *rsinfo;
+	TupleDesc	tupdesc;
+	Tuplestorestate *tupstore;
+	MemoryContext per_query_ctx;
+	MemoryContext oldcontext;
+
+	Datum		all_values[BACKEND_NUM_TYPES][IOPATH_NUM_TYPES][BUFFERS_NUM_COLUMNS];
+	bool		all_nulls[BACKEND_NUM_TYPES][IOPATH_NUM_TYPES][BUFFERS_NUM_COLUMNS];
+
+	rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+
+	/* check to see if caller supports us returning a tuplestore */
+	if (rsinfo == NULL || !IsA(rsinfo, ReturnSetInfo))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("set-valued function called in context that cannot accept a set")));
+	if (!(rsinfo->allowedModes & SFRM_Materialize))
+		ereport(ERROR,
+				(errcode(ERRCODE_FEATURE_NOT_SUPPORTED),
+				 errmsg("materialize mode required, but it is not allowed in this context")));
+
+	/* Build a tuple descriptor for our result type */
+	if (get_call_result_type(fcinfo, NULL, &tupdesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	per_query_ctx = rsinfo->econtext->ecxt_per_query_memory;
+	oldcontext = MemoryContextSwitchTo(per_query_ctx);
+	tupstore = tuplestore_begin_heap((bool) (rsinfo->allowedModes & SFRM_Materialize_Random),
+			false, work_mem);
+	rsinfo->returnMode = SFRM_Materialize;
+	rsinfo->setResult = tupstore;
+	rsinfo->setDesc = tupdesc;
+	MemoryContextSwitchTo(oldcontext);
+
+	memset(all_values, 0, sizeof(all_values));
+	memset(all_nulls, 0, sizeof(all_nulls));
+
+	 /* Loop through all live backends and count their IO Ops for each IO Path */
+	beentry = BackendStatusArray;
+
+	for (int i = 0; i < MaxBackends + NUM_AUXPROCTYPES; i++, beentry++)
+	{
+		IOOpCounters   *io_ops;
+
+		/*
+		 * Don't count dead backends. They will be added below There are no
+		 * rows in the view for BackendType B_INVALID, so skip those as well.
+		 */
+		if (beentry->st_procpid == 0 || beentry->st_backendType == B_INVALID)
+			continue;
+
+		io_ops = beentry->io_path_stats;
+
+		for (int i = 0; i < IOPATH_NUM_TYPES; i++)
+		{
+			Datum *row = get_pg_stat_buffers_row(all_values, beentry->st_backendType, i);
+
+			/*
+			 * BUFFERS_COLUMN_RESET_TIME, BUFFERS_COLUMN_BACKEND_TYPE, and
+			 * BUFFERS_COLUMN_IO_PATH will all be set when looping through
+			 * exited backends array
+			 */
+			row[BUFFERS_COLUMN_ALLOCS] += pg_atomic_read_u64(&io_ops->allocs);
+			row[BUFFERS_COLUMN_EXTENDS] += pg_atomic_read_u64(&io_ops->extends);
+			row[BUFFERS_COLUMN_FSYNCS] += pg_atomic_read_u64(&io_ops->fsyncs);
+			row[BUFFERS_COLUMN_WRITES] += pg_atomic_read_u64(&io_ops->writes);
+			io_ops++;
+		}
+	}
+
+	/* Add stats from all exited backends */
+	backend_io_path_ops = pgstat_fetch_exited_backend_buffers();
+
+	reset_time = TimestampTzGetDatum(backend_io_path_ops->stat_reset_timestamp);
+
+	for (int i = 0; i < BACKEND_NUM_TYPES; i++)
+	{
+		BackendType backend_type = idx_get_backend_type(i);
+
+		PgStatIOOpCounters *io_ops =
+			backend_io_path_ops->ops[i].io_path_ops;
+		PgStatIOOpCounters *resets =
+			backend_io_path_ops->resets[i].io_path_ops;
+
+		Datum		backend_type_desc =
+			CStringGetTextDatum(GetBackendTypeDesc(backend_type));
+
+		for (int j = 0; j < IOPATH_NUM_TYPES; j++)
+		{
+			Datum *row = get_pg_stat_buffers_row(all_values, backend_type, j);
+
+			row[BUFFERS_COLUMN_BACKEND_TYPE] = backend_type_desc;
+			row[BUFFERS_COLUMN_IO_PATH] = CStringGetTextDatum(GetIOPathDesc(j));
+			row[BUFFERS_COLUMN_RESET_TIME] = reset_time;
+			row[BUFFERS_COLUMN_ALLOCS] += io_ops->allocs - resets->allocs;
+			row[BUFFERS_COLUMN_EXTENDS] += io_ops->extends - resets->extends;
+			row[BUFFERS_COLUMN_FSYNCS] += io_ops->fsyncs - resets->fsyncs;
+			row[BUFFERS_COLUMN_WRITES] += io_ops->writes - resets->writes;
+			io_ops++;
+			resets++;
+		}
+	}
+
+	for (int i = 0; i < BACKEND_NUM_TYPES; i++)
+	{
+		for (int j = 0; j < IOPATH_NUM_TYPES; j++)
+		{
+			Datum	   *values = all_values[i][j];
+			bool	   *nulls = all_nulls[i][j];
+
+			tuplestore_putvalues(tupstore, tupdesc, 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 4d992dc224..148208d242 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5636,6 +5636,15 @@
   proname => 'pg_stat_get_buf_alloc', provolatile => 's', proparallel => 'r',
   prorettype => 'int8', proargtypes => '', prosrc => 'pg_stat_get_buf_alloc' },
 
index c7c304c4d8..8e07584767 100644
@@ -1297,6 +1297,7 @@ extern void pgstat_sum_io_path_ops(PgStatIOOpCounters *dest, IOOpCounters *src);
index 55e30022f6..a3e8a9f44e 100644
@@ -316,6 +316,7 @@ extern PGDLLIMPORT int pgstat_track_activity_query_size;

 /* ----------
@@ -334,6 +335,7 @@ extern void CreateSharedBackendStatus(void);
 /* Utility functions */
 extern int backend_type_get_idx(BackendType backend_type);
 extern BackendType idx_get_backend_type(int idx);
+extern const char *GetIOPathDesc(IOPath io_path);
 
index b58b062b10..5869ce442f 100644

From d6521c85854e7c8254930ebf5911e50204b9d3b5 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Wed, 24 Nov 2021 10:32:56 -0500
Subject: [PATCH v18 3/8] Add IO operation counters to PgBackendStatus

Note that this commit does not add code to increment the "direct" path.
A future patch adding wrappers for smgrwrite(), smgrextend(), and
smgrimmedsync() would provide a good location to call pgstat_inc_ioop()
for unbuffered IO and avoid regressions for future users of these
functions.

Author: Melanie Plageman <[email-masked]>
Reviewed-by: Justin Pryzby <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/20200124195226.lth52iydq2n2uilq%40alap3.anarazel.de
---
 src/backend/postmaster/checkpointer.c       |  1 +
 src/backend/storage/buffer/bufmgr.c         | 46 +++++++++++---
 src/backend/storage/buffer/freelist.c       | 22 ++++++-
 src/backend/storage/buffer/localbuf.c       |  3 +
 src/backend/storage/sync/sync.c             |  1 +
 src/backend/utils/activity/backend_status.c | 10 +++
 src/include/storage/buf_internals.h         |  4 +-
 src/include/utils/backend_status.h          | 68 +++++++++++++++++++++
 8 files changed, 140 insertions(+), 15 deletions(-)

diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 25a18b7a14..8440b2b802 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -1101,6 +1101,7 @@ ForwardSyncRequest(const FileTag *ftag, SyncRequestType type)
 		if (!AmBackgroundWriterProcess())
 			CheckpointerShmem->num_backend_fsync++;
 		LWLockRelease(CheckpointerCommLock);
+		pgstat_inc_ioop(IOOP_FSYNC, IOPATH_SHARED);
 		return false;
 	}
 
index 16de918e2e..949f1088b6 100644
 
 	if (isExtend)
 	{

 
 
+				/*

 

 
+	pgstat_inc_ioop(IOOP_WRITE, iopath);
+
 	if (track_io_timing)
 	{
 		INSTR_TIME_SET_CURRENT(io_time);
@@ -3533,6 +3557,8 @@ FlushRelationBuffers(Relation rel)
 						  localpage,
 						  false);

@@ -3568,7 +3594,7 @@ FlushRelationBuffers(Relation rel)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, RelationGetSmgr(rel));
+			FlushBuffer(bufHdr, RelationGetSmgr(rel), IOPATH_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr, true);
 		}
@@ -3664,7 +3690,7 @@ FlushRelationsAllBuffers(SMgrRelation *smgrs, int nrels)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, srelent->srel);
+			FlushBuffer(bufHdr, srelent->srel, IOPATH_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr, true);
 		}
@@ -3720,7 +3746,7 @@ FlushDatabaseBuffers(Oid dbid)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, NULL);
+			FlushBuffer(bufHdr, NULL, IOPATH_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr, true);
 		}
@@ -3747,7 +3773,7 @@ FlushOneBuffer(Buffer buffer)

 
 /*
index 6be80476db..45d73995b2 100644

 
 
 	/*
@@ -700,5 +709,12 @@ StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf)
 
+	/*

+					pgstat_inc_ioop(IOOP_FSYNC, IOPATH_SHARED);
 					if (log_checkpoints)
 						elog(DEBUG1, "checkpoint sync: number=%d file=%s time=%.3f ms",
 							 processed,
diff --git a/src/backend/utils/activity/backend_status.c b/src/backend/utils/activity/backend_status.c
index 7229598822..44c9a6e1a6 100644
--- a/src/backend/utils/activity/backend_status.c
+++ b/src/backend/utils/activity/backend_status.c
@@ -400,6 +400,16 @@ pgstat_bestart(void)
 	lbeentry.st_progress_command_target = InvalidOid;
 	lbeentry.st_query_id = UINT64CONST(0);
 
+	for (int i = 0; i < IOPATH_NUM_TYPES; i++)
+	{
+		IOOpCounters *io_ops = &lbeentry.io_path_stats[i];
+
+		pg_atomic_init_u64(&io_ops->allocs, 0);
+		pg_atomic_init_u64(&io_ops->extends, 0);
+		pg_atomic_init_u64(&io_ops->fsyncs, 0);
+		pg_atomic_init_u64(&io_ops->writes, 0);
+	}
+
 	/*
 	 * we don't zero st_progress_param here to save cycles; nobody should
 	 * examine it until st_progress_command has been set to something other
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index 33fcaf5c9a..7e385135db 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -310,10 +310,10 @@ extern void ScheduleBufferTagForWriteback(WritebackContext *context, BufferTag *
 
 /* freelist.c */
 
index 8042b817df..56a0f25296 100644

@@ -31,12 +32,47 @@ typedef enum BackendState
 	STATE_DISABLED
 } BackendState;
 
+/* ----------
 
 /* ----------
 
+/*
+ * Structure for counting all types of IOOp for a live backend.
+ */
+typedef struct IOOpCounters
+{
+	pg_atomic_uint64 allocs;
+	pg_atomic_uint64 extends;
+	pg_atomic_uint64 fsyncs;
+	pg_atomic_uint64 writes;
+} IOOpCounters;
+
 /*
  * PgBackendSSLStatus
  *
@@ -168,6 +204,12 @@ typedef struct PgBackendStatus
 
 	/* query identifier, optionally computed using post_parse_analyze_hook */
 	uint64		st_query_id;
+
+	/*
+	 * Stats on all IOOps for all IOPaths for this backend. These should be
+	 * incremented whenever an IO Operation is performed.
+	 */
+	IOOpCounters	io_path_stats[IOPATH_NUM_TYPES];
 } PgBackendStatus;

@@ -296,6 +338,32 @@ extern void pgstat_bestart(void);
 extern void pgstat_clear_backend_activity_snapshot(void);
 
 /* Activity reporting functions */
+
+static inline void
+pgstat_inc_ioop(IOOp io_op, IOPath io_path)
+{
+	IOOpCounters *io_ops;
+	PgBackendStatus *beentry = MyBEEntry;
+
+	Assert(beentry);
+
+	io_ops = &beentry->io_path_stats[io_path];
+	switch (io_op)
+	{
+		case IOOP_ALLOC:
+			pg_atomic_unlocked_inc_counter(&io_ops->allocs);
+			break;
+		case IOOP_EXTEND:
+			pg_atomic_unlocked_inc_counter(&io_ops->extends);
+			break;
+		case IOOP_FSYNC:
+			pg_atomic_unlocked_inc_counter(&io_ops->fsyncs);
+			break;
+		case IOOP_WRITE:
+			pg_atomic_unlocked_inc_counter(&io_ops->writes);
+			break;
+	}
+}
 extern void pgstat_report_activity(BackendState state, const char *cmd_str);
 extern void pgstat_report_query_id(uint64 query_id, bool force);
 extern void pgstat_report_tempfile(size_t filesize);
-- 
2.30.2

From 0a35d1bf2d70131b298adf0aa35349f13d8ad8aa Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Mon, 11 Oct 2021 16:15:06 -0400
Subject: [PATCH v18 1/8] Read-only atomic backend write function

Author: Thomas Munro <[email-masked]>
Reviewed-by: Melanie Plageman <[email-masked]>
Discussion: https://www.postgresql.org/message-id/CA%2BhUKGJ06d3h5JeOtAv4h52n0vG1jOPZxqMCn5FySJQUVZA32w%40mail.gmail.com
Discussion: https://www.postgresql.org/message-id/flat/20200124195226.lth52iydq2n2uilq%40alap3.anarazel.de
---
 src/include/port/atomics.h | 11 +++++++++++
 1 file changed, 11 insertions(+)

index 856338f161..dac9767b1c 100644
 
+/*
+ * On modern systems this is really just *counter++.  On some older systems
+ * there might be more to it, due to inability to read and write 64 bit values
+ * atomically.
+ */
+static inline void
+pg_atomic_unlocked_inc_counter(pg_atomic_uint64 *counter)
+{
+	pg_atomic_write_u64(counter, pg_atomic_read_u64(counter) + 1);
+}
+
 #undef INSIDE_ATOMICS_H
 
 #endif							/* ATOMICS_H */
-- 
2.30.2

From 316b073738e08d26db3da743d6da327b5bd1e390 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Tue, 14 Dec 2021 12:26:56 -0500
Subject: [PATCH v18 2/8] Move backend pgstat initialization earlier

Initialize pgstats subsystem earlier during process initialization so
that more process types have a backend activity state.

Conditionally initializing backend activity state in some types of
processes and not in others necessitates surprising special cases later.

This particular commit was motivated by single user mode missing a
backend activity state.

Author: Melanie Plageman <[email-masked]>
Discussion: https://www.postgresql.org/message-id/CAAKRu_aaq33UnG4TXq3S-OSXGWj1QGf0sU%2BECH4tNwGFNERkZA%40mail.gmail.com
---
 src/backend/utils/init/postinit.c | 7 +++----
 1 file changed, 3 insertions(+), 4 deletions(-)

diff --git a/src/backend/utils/init/postinit.c b/src/backend/utils/init/postinit.c
index 7292e51f7d..11f1fec17e 100644
--- a/src/backend/utils/init/postinit.c
+++ b/src/backend/utils/init/postinit.c
@@ -623,6 +623,8 @@ InitPostgres(const char *in_dbname, Oid dboid, const char *username,
 		RegisterTimeout(CLIENT_CONNECTION_CHECK_TIMEOUT, ClientCheckTimeoutHandler);
 	}
 
+	pgstat_beinit();
+
 	/*
 	 * If this is either a bootstrap process nor a standalone backend, start
 	 * up the XLOG machinery, and register to have it closed down at exit.
@@ -638,6 +640,7 @@ InitPostgres(const char *in_dbname, Oid dboid, const char *username,
 		 */
 		CreateAuxProcessResourceOwner();
 
+		pgstat_bestart();
 		StartupXLOG();
 		/* Release (and warn about) any buffer pins leaked in StartupXLOG */
 		ReleaseAuxProcessResources(true);
@@ -665,7 +668,6 @@ InitPostgres(const char *in_dbname, Oid dboid, const char *username,
 	EnablePortalManager();

 	/*
 	 * Load relcache entries for the shared system catalogs.  This must create
@@ -903,10 +905,7 @@ InitPostgres(const char *in_dbname, Oid dboid, const char *username,
 		 * transaction we started before returning.
 		 */
 		if (!bootstrap)
-		{
-			pgstat_bestart();
 			CommitTransactionCommand();
-		}
 		return;
 	}
 
-- 
2.30.2

---

From: Justin Pryzby <[email-masked]>
Date: Wed, 15 Dec 2021 16:38:52 -0600

> Let me know if it is more confusing to do it this way.

One email is better than three - I'm just not a model citizen ;)

Thanks for updating the patch.  I checked that all my previous review comments
were addressed (except for the part about passing the 3D array to a function -
I know that technically the pointer is being passed).

=> I think it'd be desirable for these to be either static functions (which
won't work for your needs) or macros, or inline functions in the header.

=> This should be added in alphabetical order.  Which is unimportant, but it
will also makes the patch 2 lines shorter.  The doc patch should also be in
order.

=> Missing a period.

-- 
Justin

---

From: Andres Freund <[email-masked]>
Date: Thu, 16 Dec 2021 12:18:37 -0800

Hi,

> isn't set for the single user mode process. What BackendType would you

Either B_BACKEND or something new like B_SINGLE_USER_BACKEND?

> which I'm not sure is very clear

Yea, I think it's ok to not do something fancier here for nwo.

> event that we don't get around to adding more bgwriter stats right

How about keeping old bgwriter values in place in the view , but generated
from the new stats stuff?

> sure what it was nor could I find documentation on this.

Yes, they are - and often some languages lag updating things.  There's a bit
of docs at https://www.postgresql.org/docs/devel/nls.html

Greetings,

Andres Freund

---

From: Alvaro Herrera <[email-masked]>
Date: Thu, 16 Dec 2021 18:24:44 -0300

> sure what it was nor could I find documentation on this.

Yes, feel free to ignore those files completely.  They are updated using
an external workflow that you don't need to concern yourself with.

-- 
Álvaro Herrera              Valdivia, Chile  —  https://www.EnterpriseDB.com/
"World domination is proceeding according to plan"        (Andrew Morton)

---

From: Melanie Plageman <[email-masked]>
Date: Tue, 21 Dec 2021 20:32:44 -0500

Combined responses to both Justin and Andres here.
v19 attached.

Done

> will also makes the patch 2 lines shorter.  The doc patch should also be in

Thanks for catching this.
I've corrected the order in most locations. The exception is in
pgstat_reset_shared_counters():

    if (strcmp(target, "buffers") == 0)
    {
        msg.m_resettarget = RESET_BUFFERS;
        pgstat_send_buffers_reset(&msg);
        return;
    }

Because "buffers" is a special case
which uses a different send function, I prefer to have it first.

Fixed.

> Either B_BACKEND or something new like B_SINGLE_USER_BACKEND?

I added B_STANDALONE_BACKEND and set it in InitStandaloneBackend() (as
opposed to in PostgresSingleUserMain()) so that the bootstrap process
could also use it.

> from the new stats stuff?

I tried this, but I actually don't think it is the right way to go. In
order to maintain the old view with the new source code, I had to add
new code to maintain a separate resets array just for the bgwriter view.
It adds some fiddly code that will be annoying to maintain (the reset
logic is confusing enough as is).
And, besides the implementation complexity, if a user resets
pg_stat_bgwriter and not pg_stat_buffers (or vice versa), they will
see totally different numbers for "buffers_backend" in pg_stat_bgwriter
than shared buffers written by B_BACKEND in pg_stat_buffers. I would
find that confusing.

Instead, what I did was create the separate pg_stat_checkpointer view
and move most of the old pg_stat_bgwriter stats over there.

Because that left us with a pg_stat_bgwriter view with one column, I
added a few stats to it which could later be expanded.

In pg_stat_bgwriter, I renamed "maxwritten_clean" to "rounds_hit_limit".
I added "rounds_cleaned_estimate" and "rounds_lapped_clock" which are
the other two exit conditions from the LRU scan loop in BgBufferSync().

There are other stats related to bgwriter that might be more interesting
(e.g. number of times bgwriter was woken up to clean, % of time bgwriter
spends in hibernation vs cleaning, etc); however the stats I ended up
adding were available in the same scope as maxwritten_clean and seemed
like a non-intrusive way to start building out pg_stat_bgwriter.

BgBufferSync() has a *lot* of local variables that are all getting
incremented and reset in a complicated way, so I'm not 100% sure that
the new stats I added are actually correct.

> of docs at https://www.postgresql.org/docs/devel/nls.html

I noticed that the po files for pgstat.c are not updated (the msgid I am
concerned with has the old line number in pgstat.c and the old message).
So, I tried running `make update-po`, but it didn't have the documented
effect:

"to be called if the messages in the program source have changed, in
order to merge the changes into the existing .po files"

No po.new files were created.

I can look into it more, though if it is part of an external workflow, as
Álvaro suggested, perhaps I shouldn't?

- Melanie
From 1900d7456f958d45c695ca2e848ba08d01df57de Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Wed, 24 Nov 2021 12:20:10 -0500
Subject: [PATCH v19 7/9] Remove superfluous bgwriter stats

- Remove stats from pg_stat_bgwriter which are now more clearly
  expressed in pg_stat_buffers.
- Move checkpointer related stats to new view, pg_stat_checkpointer.
---
 doc/src/sgml/monitoring.sgml          | 146 ++++++++++++++------------
 src/backend/catalog/system_views.sql  |  18 ++--
 src/backend/postmaster/checkpointer.c |  29 +----
 src/backend/postmaster/pgstat.c       |  32 +++---
 src/backend/storage/buffer/bufmgr.c   |   6 --
 src/backend/utils/adt/pgstatfuncs.c   |  44 ++------
 src/include/catalog/pg_proc.dat       |  47 +++------
 src/include/pgstat.h                  |  12 +--
 src/test/regress/expected/rules.out   |  16 ++-
 9 files changed, 136 insertions(+), 214 deletions(-)

index 40884dbc27..84c37151b8 100644
 
+     <row>
+      <entry><structname>pg_stat_checkpointer</structname><indexterm><primary>pg_stat_checkpointer</primary></indexterm></entry>
+      <entry>One row only, showing statistics about the
+       checkpointer process's activity. See
+       <link linkend="monitoring-pg-stat-checkpointer-view">
+       <structname>pg_stat_checkpointer</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
@@ -3514,97 +3523,106 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
     <tbody>
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>checkpoints_timed</structfield> <type>bigint</type>
+       <structfield>maxwritten_clean</structfield> <type>bigint</type>
       </para>
       <para>
-       Number of scheduled checkpoints that have been performed
+       Number of times the background writer stopped a cleaning
+       scan because it had written too many buffers
       </para></entry>
      </row>
 
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>checkpoints_req</structfield> <type>bigint</type>
+       <structfield>stats_reset</structfield> <type>timestamp with time zone</type>
       </para>
       <para>
-       Number of requested checkpoints that have been performed
+       Time at which these statistics were last reset.
       </para></entry>
      </row>
+    </tbody>
+   </tgroup>
+  </table>
 
-     <row>
-      <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>checkpoint_write_time</structfield> <type>double precision</type>
-      </para>
-      <para>
-       Total amount of time that has been spent in the portion of
-       checkpoint processing where files are written to disk, in milliseconds
-      </para></entry>
-     </row>
+ </sect2>
+
+ <sect2 id="monitoring-pg-stat-buffers-view">
+  <title><structname>pg_stat_buffers</structname></title>
+
+  <indexterm>
+   <primary>pg_stat_buffers</primary>
+  </indexterm>
 
+  <para>
+   The <structname>pg_stat_buffers</structname> view has a row for each backend
+   type for each possible IO path containing global data for the cluster for
+   that backend and IO path.
+  </para>
+
+  <table id="pg-stat-buffers-view" xreflabel="pg_stat_buffers">
+   <title><structname>pg_stat_buffers</structname> View</title>
+   <tgroup cols="1">
+    <thead>
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>checkpoint_sync_time</structfield> <type>double precision</type>
+       Column Type
       </para>
       <para>
-       Total amount of time that has been spent in the portion of
-       checkpoint processing where files are synchronized to disk, in
-       milliseconds
+       Description
       </para></entry>
      </row>
-
+    </thead>
+    <tbody>
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>buffers_checkpoint</structfield> <type>bigint</type>
+       <structfield>backend_type</structfield> <type>text</type>
       </para>
       <para>
-       Number of buffers written during checkpoints
+       Type of backend (e.g. background worker, autovacuum worker).
       </para></entry>
      </row>
 
      <row>
 
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>maxwritten_clean</structfield> <type>bigint</type>
+       <structfield>alloc</structfield> <type>bigint</type>
       </para>
       <para>
-       Number of times the background writer stopped a cleaning
-       scan because it had written too many buffers
+       Number of buffers allocated.
       </para></entry>
      </row>
 
      <row>
 
      <row>
 
      <row>
 
@@ -3622,21 +3640,20 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
 
  </sect2>
 
- <sect2 id="monitoring-pg-stat-buffers-view">
-  <title><structname>pg_stat_buffers</structname></title>
+ <sect2 id="monitoring-pg-stat-checkpointer-view">
+  <title><structname>pg_stat_checkpointer</structname></title>
 
   <indexterm>
-   <primary>pg_stat_buffers</primary>
+   <primary>pg_stat_checkpointer</primary>
   </indexterm>
 
   <para>
-   The <structname>pg_stat_buffers</structname> view has a row for each backend
-   type for each possible IO path containing global data for the cluster for
-   that backend and IO path.
+   The <structname>pg_stat_checkpointer</structname> view will always have a
+   single row, containing global data for the cluster.
   </para>
 
-  <table id="pg-stat-buffers-view" xreflabel="pg_stat_buffers">
-   <title><structname>pg_stat_buffers</structname> View</title>
+  <table id="pg-stat-checkpointer-view" xreflabel="pg_stat_checkpointer">
+   <title><structname>pg_stat_checkpointer</structname> View</title>
    <tgroup cols="1">
     <thead>
      <row>
@@ -3648,58 +3665,44 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
       </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).
-      </para></entry>
-     </row>
-
-     <row>
-      <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>io_path</structfield> <type>text</type>
-      </para>
-      <para>
-       IO path taken (e.g. shared buffers, direct).
-      </para></entry>
-     </row>
 
+    <tbody>
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>alloc</structfield> <type>bigint</type>
+       <structfield>checkpoints_timed</structfield> <type>bigint</type>
       </para>
       <para>
-       Number of buffers allocated.
+       Number of scheduled checkpoints that have been performed
       </para></entry>
      </row>
 
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>extend</structfield> <type>bigint</type>
+       <structfield>checkpoints_req</structfield> <type>bigint</type>
       </para>
       <para>
-       Number of buffers extended.
+       Number of requested checkpoints that have been performed
       </para></entry>
      </row>
 
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>fsync</structfield> <type>bigint</type>
+       <structfield>checkpoint_write_time</structfield> <type>double precision</type>
       </para>
       <para>
-       Number of buffers fsynced.
+       Total amount of time that has been spent in the portion of
+       checkpoint processing where files are written to disk, in milliseconds
       </para></entry>
      </row>
 
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>write</structfield> <type>bigint</type>
+       <structfield>checkpoint_sync_time</structfield> <type>double precision</type>
       </para>
       <para>
-       Number of buffers written.
+       Total amount of time that has been spent in the portion of
+       checkpoint processing where files are synchronized to disk, in
+       milliseconds
       </para></entry>
      </row>
 
@@ -5315,10 +5318,13 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
         Resets some cluster-wide statistics counters to zero, depending on the
         argument.  The argument can be <literal>archiver</literal> to reset all
         the counters shown in the <structname>pg_stat_archiver</structname>
-        view, <literal>bgwriter</literal> to reset all the counters shown in
-        the <structname>pg_stat_bgwriter</structname> view,
+        view, 
+        <literal>bgwriter</literal> to reset all the counters shown in the
+        <structname>pg_stat_bgwriter</structname> view,
         <literal>buffers</literal> to reset all the counters shown in the
-        <structname>pg_stat_buffers</structname> view, or
+        <structname>pg_stat_buffers</structname> view,
+        <literal>checkpointer</literal> to reset all the counters shown in the
+        <structname>pg_stat_checkpointer</structname> view, or
         <literal>wal</literal> to reset all the counters shown in the
         <structname>pg_stat_wal</structname> view.
        </para>
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e214d23056..0caf50421c 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1064,16 +1064,7 @@ CREATE VIEW pg_stat_archiver AS

 CREATE VIEW pg_stat_buffers AS
@@ -1087,6 +1078,15 @@ SELECT
        b.stats_reset
 FROM pg_stat_get_buffers() b;
 
+    SELECT

 

index 8ea024059e..a080ec78b4 100644
@@ -1568,6 +1568,8 @@ pgstat_reset_shared_counters(const char *target)
@@ -1575,7 +1577,8 @@ pgstat_reset_shared_counters(const char *target)
+					 "Target must be \"archiver\", \"bgwriter\", \"buffers\", "
+					 "\"checkpointer\", or \"wal\".")));
 
 	pgstat_send(&msg, sizeof(msg));
 }
@@ -5640,13 +5643,15 @@ pgstat_recv_resetcounter(PgStat_MsgResetcounter *msg, int len)
 static void
 pgstat_recv_resetsharedcounter(PgStat_MsgResetsharedcounter *msg, int len)
 {
-	if (msg->m_resettarget == RESET_BGWRITER)
+	if (msg->m_resettarget == RESET_ARCHIVER)
 	{
-		/*
-		 * Reset the global bgwriter and checkpointer statistics for the
-		 * cluster.
-		 */
-		memset(&globalStats.checkpointer, 0, sizeof(globalStats.checkpointer));
+		/* Reset the archiver statistics for the cluster. */
+		memset(&archiverStats, 0, sizeof(archiverStats));
+		archiverStats.stat_reset_timestamp = GetCurrentTimestamp();
+	}
+	else if (msg->m_resettarget == RESET_BGWRITER)
+	{
+		/* Reset the global bgwriter statistics for the cluster */
 		memset(&globalStats.bgwriter, 0, sizeof(globalStats.bgwriter));
 		globalStats.bgwriter.stat_reset_timestamp = GetCurrentTimestamp();
 	}
@@ -5674,11 +5679,11 @@ pgstat_recv_resetsharedcounter(PgStat_MsgResetsharedcounter *msg, int len)
 				&msg->m_backend_resets.iop.io_path_ops,
 				sizeof(msg->m_backend_resets.iop.io_path_ops));
 	}
-	else if (msg->m_resettarget == RESET_ARCHIVER)
+	else if (msg->m_resettarget == RESET_CHECKPOINTER)
 	{
-		/* Reset the archiver statistics for the cluster. */
-		memset(&archiverStats, 0, sizeof(archiverStats));
-		archiverStats.stat_reset_timestamp = GetCurrentTimestamp();
+		 /* Reset the global checkpointer statistics for the cluster */
+		memset(&globalStats.checkpointer, 0, sizeof(globalStats.checkpointer));
+		globalStats.checkpointer.stat_reset_timestamp = GetCurrentTimestamp();
 	}
 	else if (msg->m_resettarget == RESET_WAL)
 	{
@@ -5946,9 +5951,7 @@ pgstat_recv_archiver(PgStat_MsgArchiver *msg, int len)
 static void
 pgstat_recv_bgwriter(PgStat_MsgBgWriter *msg, int len)
 {
-	globalStats.bgwriter.buf_written_clean += msg->m_buf_written_clean;
 	globalStats.bgwriter.maxwritten_clean += msg->m_maxwritten_clean;
-	globalStats.bgwriter.buf_alloc += msg->m_buf_alloc;
 }
 
 /* ----------
@@ -5964,9 +5967,6 @@ pgstat_recv_checkpointer(PgStat_MsgCheckpointer *msg, int len)
 
 static void
index 528995e7fb..6ec15ea00e 100644

-	PendingBgWriterStats.m_buf_written_clean += num_written;
-
 #ifdef BGW_DEBUG
 	elog(DEBUG1, "bgwriter: recent_alloc=%u smoothed=%.2f delta=%ld ahead=%d density=%.2f reusable_est=%d upcoming_est=%d scanned=%d wrote=%d reusable=%d",
 		 recent_alloc, smoothed_alloc, strategy_delta, bufs_ahead,
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index c16dc3ba9a..6ee9705ea3 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1727,33 +1727,27 @@ pg_stat_get_db_sessions_killed(PG_FUNCTION_ARGS)
 }
 
 Datum
-pg_stat_get_bgwriter_timed_checkpoints(PG_FUNCTION_ARGS)
-{
-	PG_RETURN_INT64(pgstat_fetch_stat_checkpointer()->timed_checkpoints);
-}
-
-Datum
-pg_stat_get_bgwriter_requested_checkpoints(PG_FUNCTION_ARGS)
+pg_stat_get_bgwriter_maxwritten_clean(PG_FUNCTION_ARGS)
 {
-	PG_RETURN_INT64(pgstat_fetch_stat_checkpointer()->requested_checkpoints);
+	PG_RETURN_INT64(pgstat_fetch_stat_bgwriter()->maxwritten_clean);
 }
 
 Datum
-pg_stat_get_bgwriter_buf_written_checkpoints(PG_FUNCTION_ARGS)
+pg_stat_get_bgwriter_stat_reset_time(PG_FUNCTION_ARGS)
 {
-	PG_RETURN_INT64(pgstat_fetch_stat_checkpointer()->buf_written_checkpoints);
+	PG_RETURN_TIMESTAMPTZ(pgstat_fetch_stat_bgwriter()->stat_reset_timestamp);
 }
 
 Datum
-pg_stat_get_bgwriter_buf_written_clean(PG_FUNCTION_ARGS)
+pg_stat_get_timed_checkpoints(PG_FUNCTION_ARGS)
 {
-	PG_RETURN_INT64(pgstat_fetch_stat_bgwriter()->buf_written_clean);
+	PG_RETURN_INT64(pgstat_fetch_stat_checkpointer()->timed_checkpoints);
 }
 
 Datum
-pg_stat_get_bgwriter_maxwritten_clean(PG_FUNCTION_ARGS)
+pg_stat_get_requested_checkpoints(PG_FUNCTION_ARGS)
 {
-	PG_RETURN_INT64(pgstat_fetch_stat_bgwriter()->maxwritten_clean);
+	PG_RETURN_INT64(pgstat_fetch_stat_checkpointer()->requested_checkpoints);
 }
 
 Datum
@@ -1773,27 +1767,9 @@ pg_stat_get_checkpoint_sync_time(PG_FUNCTION_ARGS)
 }
 
 Datum
-pg_stat_get_bgwriter_stat_reset_time(PG_FUNCTION_ARGS)
-{
-	PG_RETURN_TIMESTAMPTZ(pgstat_fetch_stat_bgwriter()->stat_reset_timestamp);
-}
-
-Datum
-pg_stat_get_buf_written_backend(PG_FUNCTION_ARGS)
-{
-	PG_RETURN_INT64(pgstat_fetch_stat_checkpointer()->buf_written_backend);
-}
-
-Datum
-pg_stat_get_buf_fsync_backend(PG_FUNCTION_ARGS)
-{
-	PG_RETURN_INT64(pgstat_fetch_stat_checkpointer()->buf_fsync_backend);
-}
-
-Datum
-pg_stat_get_buf_alloc(PG_FUNCTION_ARGS)
+pg_stat_get_checkpointer_stat_reset_time(PG_FUNCTION_ARGS)
 {
-	PG_RETURN_INT64(pgstat_fetch_stat_bgwriter()->buf_alloc);
+	PG_RETURN_TIMESTAMPTZ(pgstat_fetch_stat_checkpointer()->stat_reset_timestamp);
 }
 
 /*
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 24d1fc29a2..d4911ada45 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5584,26 +5584,7 @@
   proargmodes => '{o,o,o,o,o,o,o}',
   proargnames => '{archived_count,last_archived_wal,last_archived_time,failed_count,last_failed_wal,last_failed_time,stats_reset}',
   prosrc => 'pg_stat_get_archiver' },
-{ oid => '2769',
-  descr => 'statistics: number of timed checkpoints started by the bgwriter',
-  proname => 'pg_stat_get_bgwriter_timed_checkpoints', provolatile => 's',
-  proparallel => 'r', prorettype => 'int8', proargtypes => '',
-  prosrc => 'pg_stat_get_bgwriter_timed_checkpoints' },
-{ oid => '2770',
-  descr => 'statistics: number of backend requested checkpoints started by the bgwriter',
-  proname => 'pg_stat_get_bgwriter_requested_checkpoints', provolatile => 's',
-  proparallel => 'r', prorettype => 'int8', proargtypes => '',
-  prosrc => 'pg_stat_get_bgwriter_requested_checkpoints' },
-{ oid => '2771',
-  descr => 'statistics: number of buffers written by the bgwriter during checkpoints',
-  proname => 'pg_stat_get_bgwriter_buf_written_checkpoints', provolatile => 's',
-  proparallel => 'r', prorettype => 'int8', proargtypes => '',
-  prosrc => 'pg_stat_get_bgwriter_buf_written_checkpoints' },
-{ oid => '2772',
-  descr => 'statistics: number of buffers written by the bgwriter for cleaning dirty buffers',
-  proname => 'pg_stat_get_bgwriter_buf_written_clean', provolatile => 's',
-  proparallel => 'r', prorettype => 'int8', proargtypes => '',
-  prosrc => 'pg_stat_get_bgwriter_buf_written_clean' },
+
 { oid => '2773',
   descr => 'statistics: number of times the bgwriter stopped processing when it had written too many buffers while cleaning',
   proname => 'pg_stat_get_bgwriter_maxwritten_clean', provolatile => 's',
@@ -5613,6 +5594,16 @@
   proname => 'pg_stat_get_bgwriter_stat_reset_time', provolatile => 's',
   proparallel => 'r', prorettype => 'timestamptz', proargtypes => '',
   prosrc => 'pg_stat_get_bgwriter_stat_reset_time' },
+{ oid => '2769',
+  descr => 'statistics: number of scheduled checkpoints performed',
+  proname => 'pg_stat_get_timed_checkpoints', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => '',
+  prosrc => 'pg_stat_get_timed_checkpoints' },
+{ oid => '2770',
+  descr => 'statistics: number of backend requested checkpoints performed',
+  proname => 'pg_stat_get_requested_checkpoints', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => '',
+  prosrc => 'pg_stat_get_requested_checkpoints' },
 { oid => '3160',
   descr => 'statistics: checkpoint time spent writing buffers to disk, in milliseconds',
   proname => 'pg_stat_get_checkpoint_write_time', provolatile => 's',
@@ -5623,18 +5614,10 @@
   proname => 'pg_stat_get_checkpoint_sync_time', provolatile => 's',
   proparallel => 'r', prorettype => 'float8', proargtypes => '',
   prosrc => 'pg_stat_get_checkpoint_sync_time' },
-{ oid => '2775', descr => 'statistics: number of buffers written by backends',
-  proname => 'pg_stat_get_buf_written_backend', provolatile => 's',
-  proparallel => 'r', prorettype => 'int8', proargtypes => '',
-  prosrc => 'pg_stat_get_buf_written_backend' },
-{ oid => '3063',
-  descr => 'statistics: number of backend buffer writes that did their own fsync',
-  proname => 'pg_stat_get_buf_fsync_backend', provolatile => 's',
-  proparallel => 'r', prorettype => 'int8', proargtypes => '',
-  prosrc => 'pg_stat_get_buf_fsync_backend' },
-{ oid => '2859', descr => 'statistics: number of buffer allocations',
-  proname => 'pg_stat_get_buf_alloc', provolatile => 's', proparallel => 'r',
-  prorettype => 'int8', proargtypes => '', prosrc => 'pg_stat_get_buf_alloc' },
+{ oid => '8460', descr => 'statistics: last reset for the checkpointer',
+  proname => 'pg_stat_get_checkpointer_stat_reset_time', provolatile => 's',
+  proparallel => 'r', prorettype => 'timestamptz', proargtypes => '',
+  prosrc => 'pg_stat_get_checkpointer_stat_reset_time' },
 
 { oid => '8459', descr => 'statistics: counts of all IO operations done through all IO paths by each type of backend.',
   proname => 'pg_stat_get_buffers', provolatile => 's', proisstrict => 'f',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 9369e4a408..36d981c9ea 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -143,6 +143,7 @@ typedef enum PgStat_Shared_Reset_Target
 	RESET_ARCHIVER,
 	RESET_BGWRITER,
 	RESET_BUFFERS,
+	RESET_CHECKPOINTER,
 	RESET_WAL
 } PgStat_Shared_Reset_Target;
 
@@ -523,9 +524,7 @@ typedef struct PgStat_MsgBgWriter
 {
 	PgStat_MsgHdr m_hdr;

 /* ----------
@@ -538,9 +537,6 @@ typedef struct PgStat_MsgCheckpointer
 
@@ -971,9 +967,7 @@ typedef struct PgStat_ArchiverStats
 
@@ -987,9 +981,7 @@ typedef struct PgStat_CheckpointerStats
 
 /*
index 5869ce442f..97af066c6b 100644
@@ -1817,16 +1817,7 @@ pg_stat_archiver| SELECT s.archived_count,
+pg_stat_bgwriter| SELECT pg_stat_get_bgwriter_maxwritten_clean() AS maxwritten_clean,
@@ -1836,6 +1827,11 @@ pg_stat_buffers| SELECT b.backend_type,

From 88cf6d9237f98b55d75947700d541afaeb7ee4ca Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Mon, 20 Dec 2021 16:48:06 -0500
Subject: [PATCH v19 9/9] Add stats to pg_stat_bgwriter

Rename pg_stat_bgwriter stat "maxwritten_clean" to "rounds_hit_limit".
This stat tracks the number of rounds in which the bgwriter hit the
limit imposed by bgwriter_lru_maxpages for the number of pages which are
allowed to be cleaned in a round.

Also, add two new additional stats to this view. The first,
"rounds_cleaned_estimate", is the number of rounds in which the bgwriter
was able to clean the number of buffers which it has estimated will be
allocated in the next cycle.

The second, "rounds_lapped_clock", is the number of rounds in which the
bgwriter lapped the freelist clock sweep scan.

Both of these are conditions to exit the LRU cleaning scan.
---
 doc/src/sgml/monitoring.sgml         | 22 +++++++++++++++++++++-
 src/backend/catalog/system_views.sql |  4 +++-
 src/backend/postmaster/pgstat.c      |  5 ++++-
 src/backend/storage/buffer/bufmgr.c  | 21 +++++++++++++++------
 src/backend/utils/adt/pgstatfuncs.c  | 16 ++++++++++++++--
 src/include/catalog/pg_proc.dat      | 15 +++++++++++++--
 src/include/pgstat.h                 |  8 ++++++--
 src/test/regress/expected/rules.out  |  4 +++-
 8 files changed, 79 insertions(+), 16 deletions(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 84c37151b8..e029a596c9 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -3523,7 +3523,17 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
     <tbody>
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
-       <structfield>maxwritten_clean</structfield> <type>bigint</type>
+       <structfield>rounds_cleaned_estimate</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of rounds in which the bgwriter cleaned the number of buffers it
+       estimated would be allocated in the next cycle. 
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>rounds_hit_limit</structfield> <type>bigint</type>
       </para>
       <para>
        Number of times the background writer stopped a cleaning
@@ -3531,6 +3541,16 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
       </para></entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>rounds_lapped_clock</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of rounds in which the bgwriter lapped the clock sweep scan while
+       cleaning buffers.
+      </para></entry>
+     </row>
+
      <row>
       <entry role="catalog_table_entry"><para role="column_definition">
        <structfield>stats_reset</structfield> <type>timestamp with time zone</type>
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index 0caf50421c..8ac1988d21 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1064,7 +1064,9 @@ CREATE VIEW pg_stat_archiver AS
 
 CREATE VIEW pg_stat_bgwriter AS
     SELECT
-        pg_stat_get_bgwriter_maxwritten_clean() AS maxwritten_clean,
+        pg_stat_get_bgwriter_rounds_cleaned_estimate() AS rounds_cleaned_estimate,
+        pg_stat_get_bgwriter_rounds_hit_limit() AS rounds_hit_limit,
+        pg_stat_get_bgwriter_rounds_lapped_clock() AS rounds_lapped_clock,
         pg_stat_get_bgwriter_stat_reset_time() AS stats_reset;
 
 CREATE VIEW pg_stat_buffers AS
diff --git a/src/backend/postmaster/pgstat.c b/src/backend/postmaster/pgstat.c
index a080ec78b4..a441216d25 100644
--- a/src/backend/postmaster/pgstat.c
+++ b/src/backend/postmaster/pgstat.c
@@ -5951,7 +5951,10 @@ pgstat_recv_archiver(PgStat_MsgArchiver *msg, int len)
 static void
 pgstat_recv_bgwriter(PgStat_MsgBgWriter *msg, int len)
 {
-	globalStats.bgwriter.maxwritten_clean += msg->m_maxwritten_clean;
+	globalStats.bgwriter.rounds_cleaned_estimate +=
+		msg->m_rounds_cleaned_estimate;
+	globalStats.bgwriter.rounds_hit_limit += msg->m_rounds_hit_limit;
+	globalStats.bgwriter.rounds_lapped_clock += msg->m_rounds_lapped_clock;
 }
 
 /* ----------
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 6ec15ea00e..5830b971d6 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -2454,20 +2454,29 @@ BgBufferSync(WritebackContext *wb_context)
 			next_to_clean = 0;
 			next_passes++;
 		}
-		num_to_scan--;
 
-		if (sync_state & BUF_WRITTEN)
+		if (--num_to_scan == 0)
+			PendingBgWriterStats.m_rounds_lapped_clock++;
+
+		if (sync_state & BUF_WRITTEN || sync_state & BUF_REUSABLE)
 		{
 			reusable_buffers++;
-			if (++num_written >= bgwriter_lru_maxpages)
+			if (reusable_buffers >= upcoming_alloc_est)
+				PendingBgWriterStats.m_rounds_cleaned_estimate++;
+		}
+
+		if (sync_state & BUF_WRITTEN)
+		{
+			num_written++;
+			if (num_written >= bgwriter_lru_maxpages)
 			{
-				PendingBgWriterStats.m_maxwritten_clean++;
+				PendingBgWriterStats.m_rounds_hit_limit++;
 				break;
 			}
 		}
-		else if (sync_state & BUF_REUSABLE)
-			reusable_buffers++;
+
 	}
+	
 
 #ifdef BGW_DEBUG
 	elog(DEBUG1, "bgwriter: recent_alloc=%u smoothed=%.2f delta=%ld ahead=%d density=%.2f reusable_est=%d upcoming_est=%d scanned=%d wrote=%d reusable=%d",
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 6ee9705ea3..eafcaf4abf 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1727,9 +1727,21 @@ pg_stat_get_db_sessions_killed(PG_FUNCTION_ARGS)
 }
 
 Datum
-pg_stat_get_bgwriter_maxwritten_clean(PG_FUNCTION_ARGS)
+pg_stat_get_bgwriter_rounds_cleaned_estimate(PG_FUNCTION_ARGS)
 {
-	PG_RETURN_INT64(pgstat_fetch_stat_bgwriter()->maxwritten_clean);
+	PG_RETURN_INT64(pgstat_fetch_stat_bgwriter()->rounds_cleaned_estimate);
+}
+
+Datum
+pg_stat_get_bgwriter_rounds_hit_limit(PG_FUNCTION_ARGS)
+{
+	PG_RETURN_INT64(pgstat_fetch_stat_bgwriter()->rounds_hit_limit);
+}
+
+Datum
+pg_stat_get_bgwriter_rounds_lapped_clock(PG_FUNCTION_ARGS)
+{
+	PG_RETURN_INT64(pgstat_fetch_stat_bgwriter()->rounds_lapped_clock);
 }
 
 Datum
index d4911ada45..64efbeaa7e 100644
@@ -5585,11 +5585,22 @@
 
+{ oid => '8461',
+  descr => 'statistics: number of times the bgwriter cleaned the number of buffers in a round estimated to be allocated in the next cycle.',
+  proname => 'pg_stat_get_bgwriter_rounds_cleaned_estimate', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => '',
+  prosrc => 'pg_stat_get_bgwriter_rounds_cleaned_estimate' },
 { oid => '2773',
   descr => 'statistics: number of times the bgwriter stopped processing when it had written too many buffers while cleaning',
-  proname => 'pg_stat_get_bgwriter_maxwritten_clean', provolatile => 's',
+  proname => 'pg_stat_get_bgwriter_rounds_hit_limit', provolatile => 's',
+  proparallel => 'r', prorettype => 'int8', proargtypes => '',
+  prosrc => 'pg_stat_get_bgwriter_rounds_hit_limit' },
+{ oid => '8462',
+  descr => 'statistics: number of times the bgwriter lapped the clock sweep scan while cleaning buffers during a round',
+  proname => 'pg_stat_get_bgwriter_rounds_lapped_clock', provolatile => 's',
   proparallel => 'r', prorettype => 'int8', proargtypes => '',
-  prosrc => 'pg_stat_get_bgwriter_maxwritten_clean' },
+  prosrc => 'pg_stat_get_bgwriter_rounds_lapped_clock' },
+
 { oid => '3075', descr => 'statistics: last reset for the bgwriter',
   proname => 'pg_stat_get_bgwriter_stat_reset_time', provolatile => 's',
   proparallel => 'r', prorettype => 'timestamptz', proargtypes => '',
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 36d981c9ea..89bf8c6b81 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -524,7 +524,9 @@ typedef struct PgStat_MsgBgWriter
 {
 	PgStat_MsgHdr m_hdr;
 
-	PgStat_Counter m_maxwritten_clean;
+	PgStat_Counter m_rounds_cleaned_estimate;
+	PgStat_Counter m_rounds_hit_limit;
+	PgStat_Counter m_rounds_lapped_clock;
 } PgStat_MsgBgWriter;
 
 /* ----------
@@ -967,7 +969,9 @@ typedef struct PgStat_ArchiverStats
  */
 typedef struct PgStat_BgWriterStats
 {
-	PgStat_Counter maxwritten_clean;
+	PgStat_Counter rounds_cleaned_estimate;
+	PgStat_Counter rounds_hit_limit;
+	PgStat_Counter rounds_lapped_clock;
 	TimestampTz stat_reset_timestamp;
 } PgStat_BgWriterStats;
 
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 97af066c6b..5373fed143 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1817,7 +1817,9 @@ pg_stat_archiver| SELECT s.archived_count,
     s.last_failed_time,
     s.stats_reset
    FROM pg_stat_get_archiver() s(archived_count, last_archived_wal, last_archived_time, failed_count, last_failed_wal, last_failed_time, stats_reset);
-pg_stat_bgwriter| SELECT pg_stat_get_bgwriter_maxwritten_clean() AS maxwritten_clean,
+pg_stat_bgwriter| SELECT pg_stat_get_bgwriter_rounds_cleaned_estimate() AS rounds_cleaned_estimate,
+    pg_stat_get_bgwriter_rounds_hit_limit() AS rounds_hit_limit,
+    pg_stat_get_bgwriter_rounds_lapped_clock() AS rounds_lapped_clock,
     pg_stat_get_bgwriter_stat_reset_time() AS stats_reset;
 pg_stat_buffers| SELECT b.backend_type,
     b.io_path,
-- 
2.30.2

From 26de96df4e4bad1a2fde28a42795c596b93dc57d Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Wed, 24 Nov 2021 12:21:08 -0500
Subject: [PATCH v19 8/9] small comment correction

Naming callers in function comment is brittle and unnecessary.
---
 src/backend/utils/activity/backend_status.c | 11 +++++------
 1 file changed, 5 insertions(+), 6 deletions(-)

diff --git a/src/backend/utils/activity/backend_status.c b/src/backend/utils/activity/backend_status.c
index 08e3f8f167..d508b819b1 100644
--- a/src/backend/utils/activity/backend_status.c
+++ b/src/backend/utils/activity/backend_status.c
@@ -254,11 +254,11 @@ GetIOPathDesc(IOPath io_path)
 }
 
 /*
- * Initialize pgstats backend activity state, and set up our on-proc-exit
- * hook.  Called from InitPostgres and AuxiliaryProcessMain. For auxiliary
- * process, MyBackendId is invalid. Otherwise, MyBackendId must be set, but we
- * must not have started any transaction yet (since the exit hook must run
- * after the last transaction exit).
+ * Initialize pgstats backend activity state, and set up our on-proc-exit hook.
+ *
+ * For auxiliary process, MyBackendId is invalid. Otherwise, MyBackendId must
+ * be set, but we must not have started any transaction yet (since the exit
+ * hook must run after the last transaction exit).
  *
  * NOTE: MyDatabaseId isn't set yet; so the shutdown hook has to be careful.
  */
@@ -296,7 +296,6 @@ pgstat_beinit(void)
  * pgstat_bestart() -
  *
  *	Initialize this backend's entry in the PgBackendStatus array.
- *	Called from InitPostgres.
  *
  *	Apart from auxiliary processes, MyBackendId, MyDatabaseId,
  *	session userid, and application_name must be set for a
-- 
2.30.2

From 23ed7f8a4dfed1638daeb2e9b7a60ac4929fd99f Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Wed, 24 Nov 2021 11:39:48 -0500
Subject: [PATCH v19 5/9] Add "buffers" to pgstat_reset_shared_counters

Author: Melanie Plageman <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/20200124195226.lth52iydq2n2uilq%40alap3.anarazel.de
---
 src/backend/postmaster/pgstat.c             | 75 +++++++++++++++++++--
 src/backend/utils/activity/backend_status.c | 27 ++++++++
 src/include/pgstat.h                        | 28 ++++++--
 src/include/utils/backend_status.h          |  2 +
 4 files changed, 123 insertions(+), 9 deletions(-)

diff --git a/src/backend/postmaster/pgstat.c b/src/backend/postmaster/pgstat.c
index 11db91f62b..2328b17bdf 100644
--- a/src/backend/postmaster/pgstat.c
+++ b/src/backend/postmaster/pgstat.c
@@ -1509,6 +1509,36 @@ pgstat_reset_counters(void)
 	pgstat_send(&msg, sizeof(msg));
 }
 
+/*
@@ -1526,6 +1556,14 @@ pgstat_reset_shared_counters(const char *target)
 
 	if (strcmp(target, "archiver") == 0)
@@ -1536,9 +1574,9 @@ pgstat_reset_shared_counters(const char *target)

@@ -4425,6 +4463,7 @@ pgstat_read_statsfiles(Oid onlydb, bool permanent, bool deep)
 	 */
 
@@ -5590,10 +5629,38 @@ pgstat_recv_resetsharedcounter(PgStat_MsgResetsharedcounter *msg, int len)
 {
 
+/*
index 23496afffc..08dea71537 100644

 /*
@@ -375,9 +377,12 @@ typedef struct PgStat_MsgIOPathOps
 
+
 /* ----------
 
-/* ----------
+ * To satisfy this requirement, the process initiating the reset will read the
+ * IO operations counters from live backends and send them to the stats
+ * collector which maintains an array of reset values.
 
 /* ----------
diff --git a/src/include/utils/backend_status.h b/src/include/utils/backend_status.h
index ae4597c5fe..92f00e1fce 100644
--- a/src/include/utils/backend_status.h
+++ b/src/include/utils/backend_status.h
@@ -375,6 +375,7 @@ extern void pgstat_bestart(void);
 extern void pgstat_clear_backend_activity_snapshot(void);

 static inline void
@@ -402,6 +403,7 @@ pgstat_inc_ioop(IOOp io_op, IOPath io_path)

From b85c02757fab93c841aaa58ebc08aaac4ad828bb Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Wed, 24 Nov 2021 12:07:37 -0500
Subject: [PATCH v19 6/9] Add system view tracking IO ops per backend type

Some of these should always be zero. For example, checkpointer does not
use a BufferAccessStrategy (currently), so the "strategy" IO Path for
checkpointer will be 0 for all IO operations (alloc, write, fsync, and
extend). All possible combinations of IOPath and IOOp are enumerated in
the view but not all are populated or even possible at this point.

All backends increment a counter in an array of IO stat counters in
their PgBackendStatus when performing an IO operation. On exit, backends
send these stats to the stats collector to be persisted.

particular IO Path (e.g. shared buffer accesses by checkpointer) and

Author: Melanie Plageman <[email-masked]>
Reviewed-by: Justin Pryzby <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/20200124195226.lth52iydq2n2uilq%40alap3.anarazel.de
---
 doc/src/sgml/monitoring.sgml                | 120 +++++++++++++++-
 src/backend/catalog/system_views.sql        |  11 ++
 src/backend/postmaster/pgstat.c             |  13 ++
 src/backend/utils/activity/backend_status.c |  19 ++-
 src/backend/utils/adt/pgstatfuncs.c         | 150 ++++++++++++++++++++
 src/include/catalog/pg_proc.dat             |   9 ++
 src/include/pgstat.h                        |   1 +
 src/include/utils/backend_status.h          |   1 +
 src/test/regress/expected/rules.out         |   8 ++
 9 files changed, 324 insertions(+), 8 deletions(-)

index 62f2a3332b..40884dbc27 100644
 
+     <row>
+   type for each possible IO path containing global data for the cluster for
@@ -5209,12 +5313,14 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
-        view, <literal>archiver</literal> to reset all the counters shown in
+        argument.  The argument can be <literal>archiver</literal> to reset all
+        the counters shown in the <structname>pg_stat_archiver</structname>
+        view, <literal>bgwriter</literal> to reset all the counters shown in
+        the <structname>pg_stat_bgwriter</structname> view,
+        <structname>pg_stat_buffers</structname> view, or
+        <structname>pg_stat_wal</structname> view.
 
+CREATE VIEW pg_stat_buffers AS
+SELECT
+       b.backend_type,
+       b.io_path,
+       b.alloc,
+       b.extend,
+       b.fsync,
+       b.write,
+       b.stats_reset
+FROM pg_stat_get_buffers() b;
+
 CREATE VIEW pg_stat_wal AS
     SELECT
         w.wal_records,
diff --git a/src/backend/postmaster/pgstat.c b/src/backend/postmaster/pgstat.c
index 2328b17bdf..8ea024059e 100644
--- a/src/backend/postmaster/pgstat.c
+++ b/src/backend/postmaster/pgstat.c
@@ -2923,6 +2923,19 @@ pgstat_twophase_postabort(TransactionId xid, uint16 info,
 		rec->tuples_inserted + rec->tuples_updated;
 }
 
+/*
 
 /* ----------

+const char *
index f529c1561a..c16dc3ba9a 100644
 
+/*
+		 * Don't count dead backends. They will be added below. There are no
index 4d992dc224..24d1fc29a2 100644
 
+{ oid => '8459', descr => 'statistics: counts of all IO operations done through all IO paths by each type of backend.',
index 08dea71537..9369e4a408 100644
index 92f00e1fce..ecd0668161 100644

 /* ----------

From b1cd3358991553b50a087e6c0b865d8733d82951 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Mon, 11 Oct 2021 16:15:06 -0400
Subject: [PATCH v19 1/9] Read-only atomic backend write function

For counters in shared memory which can be read by any backend but only
written to by one backend, an atomic is still needed to protect against
torn values; however, pg_atomic_fetch_add_u64() is overkill for
incrementing such counters. pg_atomic_unlocked_inc_counter() is a helper
function which can be used to increment these values safely without
unnecessary overhead.

index 856338f161..af30da32e5 100644
 
+/*
+ * On modern systems this is really just *counter++. On some older systems
+ * there might be more to it (due to an inability to read and write 64-bit
+ * values atomically).
+ */
+static inline void
+pg_atomic_unlocked_inc_counter(pg_atomic_uint64 *counter)
+{
+	pg_atomic_write_u64(counter, pg_atomic_read_u64(counter) + 1);
+}
+
 #undef INSIDE_ATOMICS_H
 
 #endif							/* ATOMICS_H */
-- 
2.30.2

From f11345e1bed8955ac335e3bc510d248590659dc1 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Tue, 14 Dec 2021 12:26:56 -0500
Subject: [PATCH v19 2/9] Move backend pgstat initialization earlier

Initialize the pgstats subsystem earlier during process initialization
so that more process types have a backend activity state
(PgBackendStatus).

Conditionally initializing backend activity state in some types of
processes and not in others necessitates surprising special cases in the
code.

This commit also adds a new BackendType for standalone backends,
B_STANDALONE_BACKEND (and alphabetizes the BackendTypes). Both the
bootstrap backend and single user mode backends will have BackendType
B_STANDALONE_BACKEND.

Author: Melanie Plageman <[email-masked]>
Discussion: https://www.postgresql.org/message-id/CAAKRu_aaq33UnG4TXq3S-OSXGWj1QGf0sU%2BECH4tNwGFNERkZA%40mail.gmail.com
---
 src/backend/utils/init/miscinit.c | 23 ++++++++++++++---------
 src/backend/utils/init/postinit.c |  7 +++----
 src/include/miscadmin.h           |  7 ++++---
 3 files changed, 21 insertions(+), 16 deletions(-)

diff --git a/src/backend/utils/init/miscinit.c b/src/backend/utils/init/miscinit.c
index 88801374b5..41d0b023cd 100644
--- a/src/backend/utils/init/miscinit.c
+++ b/src/backend/utils/init/miscinit.c
@@ -176,6 +176,8 @@ InitStandaloneProcess(const char *argv0)
 {
 	Assert(!IsPostmasterEnvironment);
 
+	MyBackendType = B_STANDALONE_BACKEND;
+
 	/*
 	 * Start our win32 signal implementation
 	 */
@@ -255,6 +257,9 @@ GetBackendTypeDesc(BackendType backendType)
 		case B_INVALID:
 			backendDesc = "not initialized";
 			break;
+		case B_ARCHIVER:
+			backendDesc = "archiver";
+			break;
 		case B_AUTOVAC_LAUNCHER:
 			backendDesc = "autovacuum launcher";
 			break;
@@ -273,9 +278,18 @@ GetBackendTypeDesc(BackendType backendType)
 		case B_CHECKPOINTER:
 			backendDesc = "checkpointer";
 			break;
+		case B_LOGGER:
+			backendDesc = "logger";
+			break;
+		case B_STANDALONE_BACKEND:
+			backendDesc = "standalone backend";
+			break;
 		case B_STARTUP:
 			backendDesc = "startup";
 			break;
+		case B_STATS_COLLECTOR:
+			backendDesc = "stats collector";
+			break;
 		case B_WAL_RECEIVER:
 			backendDesc = "walreceiver";
 			break;
@@ -285,15 +299,6 @@ GetBackendTypeDesc(BackendType backendType)
 		case B_WAL_WRITER:
 			backendDesc = "walwriter";
 			break;
-		case B_ARCHIVER:
-			backendDesc = "archiver";
-			break;
-		case B_STATS_COLLECTOR:
-			backendDesc = "stats collector";
-			break;
-		case B_LOGGER:
-			backendDesc = "logger";
-			break;
 	}
 
 	return backendDesc;
 
+	pgstat_beinit();
+
 	/*
 
+		pgstat_bestart();
 		StartupXLOG();

 	/*
 
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 90a3016065..1d688f9e51 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -323,19 +323,20 @@ extern void SwitchBackToLocalLatch(void);
 typedef enum BackendType
 {
 	B_INVALID = 0,
+	B_ARCHIVER,
 	B_AUTOVAC_LAUNCHER,
 	B_AUTOVAC_WORKER,
 	B_BACKEND,
 	B_BG_WORKER,
 	B_BG_WRITER,
 	B_CHECKPOINTER,
+	B_LOGGER,
+	B_STANDALONE_BACKEND,
 	B_STARTUP,
+	B_STATS_COLLECTOR,
 	B_WAL_RECEIVER,
 	B_WAL_SENDER,
 	B_WAL_WRITER,
-	B_ARCHIVER,
-	B_STATS_COLLECTOR,
-	B_LOGGER,
 } BackendType;

From 8588e4afc3e3869dc2478d53bfb970ea9a6dbd9a Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Wed, 24 Nov 2021 10:32:56 -0500
Subject: [PATCH v19 3/9] Add IO operation counters to PgBackendStatus

Add an array of counters to PgBackendStatus which count the buffers
allocated, extended, fsynced, and written by a given backend.

Each "IO Op" (alloc, fsync, extend, write) is counted per "IO Path"
(direct, local, shared, or strategy).

"local" and "shared" IO Path counters count operations on local and
shared buffers.

The "strategy" IO Path counts buffers alloc'd/written/read/fsync'd as
part of a BufferAccessStrategy.

With this commit, backends increment a counter in the array in their
PgBackendStatus when performing an IO operation.

Future patches will persist the IO stat counters from a backend's
PgBackendStatus upon backend exit and use the counters to provide
observability of database IO operations.

 src/backend/storage/buffer/bufmgr.c         | 47 +++++++++++---
 8 files changed, 141 insertions(+), 15 deletions(-)

index b4532948d3..528995e7fb 100644
 
 	if (isExtend)
 	{

 
@@ -1245,13 +1250,27 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 					}
 				}
 
+				/*
+				 * When a strategy is not in use, at this point the write can

@@ -2552,10 +2571,11 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)

 
@@ -2803,9 +2823,12 @@ BufferGetTag(Buffer buffer, RelFileNode *rnode, ForkNumber *forknum,
  *
  * If the caller has an smgr reference for the buffer's relation, pass it
  * as the second parameter.  If not, pass NULL.
+ *
+ * IOPath will always be IOPATH_SHARED except when a buffer access strategy is
+ * used and the buffer being flushed is a buffer from the strategy ring.
  */
 static void
-FlushBuffer(BufferDesc *buf, SMgrRelation reln)
+FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOPath iopath)
 {
 	XLogRecPtr	recptr;
 	ErrorContextCallback errcallback;
@@ -2897,6 +2920,8 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln)
 			  bufToWrite,
 			  false);
 
+	pgstat_inc_ioop(IOOP_WRITE, iopath);
+
 	if (track_io_timing)
 	{
 		INSTR_TIME_SET_CURRENT(io_time);
@@ -3533,6 +3558,8 @@ FlushRelationBuffers(Relation rel)
 						  localpage,
 						  false);

@@ -3568,7 +3595,7 @@ FlushRelationBuffers(Relation rel)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, RelationGetSmgr(rel));
+			FlushBuffer(bufHdr, RelationGetSmgr(rel), IOPATH_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr, true);
 		}
@@ -3664,7 +3691,7 @@ FlushRelationsAllBuffers(SMgrRelation *smgrs, int nrels)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, srelent->srel);
+			FlushBuffer(bufHdr, srelent->srel, IOPATH_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr, true);
 		}
@@ -3720,7 +3747,7 @@ FlushDatabaseBuffers(Oid dbid)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, NULL);
+			FlushBuffer(bufHdr, NULL, IOPATH_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr, true);
 		}
@@ -3747,7 +3774,7 @@ FlushOneBuffer(Buffer buffer)

 
 /*

 
 
 	/*
 
+	/*

 
index 7c6653311a..15f5724fa1 100644
 
 /* freelist.c */

 
+/* ----------
 
 /* ----------
 
+/*

 

From 7d02db46a4bd7f2bff5384abdd224bd749792a2d Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Wed, 24 Nov 2021 11:16:55 -0500
Subject: [PATCH v19 4/9] Send IO operations to stats collector

Author: Melanie Plageman <[email-masked]>
Reviewed-by: Justin Pryzby <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/20200124195226.lth52iydq2n2uilq%40alap3.anarazel.de
---
 src/backend/postmaster/pgstat.c    | 100 ++++++++++++++++++++++++++++-
 src/include/miscadmin.h            |   2 +
 src/include/pgstat.h               |  56 ++++++++++++++++
 src/include/utils/backend_status.h |  37 +++++++++++
 4 files changed, 194 insertions(+), 1 deletion(-)

diff --git a/src/backend/postmaster/pgstat.c b/src/backend/postmaster/pgstat.c
index 7264d2c727..11db91f62b 100644
--- a/src/backend/postmaster/pgstat.c
+++ b/src/backend/postmaster/pgstat.c
@@ -126,7 +126,7 @@ char	   *pgstat_stat_filename = NULL;
 char	   *pgstat_stat_tmpname = NULL;
 
 /*
- * BgWriter and WAL global statistics counters.
+ * BgWriter, Checkpointer, and WAL global statistics counters.
  * Stored directly in a stats message structure so they can be sent
  * without needing to copy things around.  We assume these init to zeroes.
  */
@@ -369,6 +369,7 @@ static void pgstat_recv_analyze(PgStat_MsgAnalyze *msg, int len);
 static void pgstat_recv_archiver(PgStat_MsgArchiver *msg, int len);
 static void pgstat_recv_bgwriter(PgStat_MsgBgWriter *msg, int len);
 static void pgstat_recv_checkpointer(PgStat_MsgCheckpointer *msg, int len);
+static void pgstat_recv_io_path_ops(PgStat_MsgIOPathOps *msg, int len);
 static void pgstat_recv_wal(PgStat_MsgWal *msg, int len);
 static void pgstat_recv_slru(PgStat_MsgSLRU *msg, int len);
 static void pgstat_recv_funcstat(PgStat_MsgFuncstat *msg, int len);
@@ -3152,6 +3153,14 @@ pgstat_shutdown_hook(int code, Datum arg)
 {
 	Assert(!pgstat_is_shutdown);
 
+	/*
+	 * Only need to send stats on IOOps for IOPaths when a process exits. Users
+	 * requiring IOOps for both live and exited backends can read from live
+	 * backends' PgBackendStatuses and sum this with totals from exited
+	 * backends persisted by the stats collector.
+	 */
+	pgstat_send_buffers();
+
 	/*
 	 * If we got as far as discovering our own database ID, we can report what
 	 * we did to the collector.  Otherwise, we'd be sending an invalid
@@ -3301,6 +3310,46 @@ pgstat_send_bgwriter(void)
 	MemSet(&PendingBgWriterStats, 0, sizeof(PendingBgWriterStats));
 }
 
+/*
+	/* If no IO was done, don't bother sending anything to the stats collector. */
@@ -3483,6 +3532,29 @@ pgstat_send_subscription_purge(PgStat_MsgSubscriptionPurge *msg)
 	pgstat_send(msg, len);
 }
 
+/*
+ * local) from live backends with those in the equivalent stats structure for
@@ -3692,6 +3764,10 @@ PgstatCollectorMain(int argc, char *argv[])
 
@@ -5813,6 +5889,28 @@ pgstat_recv_checkpointer(PgStat_MsgCheckpointer *msg, int len)
 
+static void
index 1d688f9e51..85fe522780 100644
@@ -339,6 +339,8 @@ typedef enum BackendType
 	B_WAL_WRITER,
 } BackendType;
 
+#define BACKEND_NUM_TYPES B_WAL_WRITER
+
 extern BackendType MyBackendType;
 
index 5b51b58e5a..23496afffc 100644

+/*

 /*

 /* ----------
  * Support functions for the SQL-callable functions to
diff --git a/src/include/utils/backend_status.h b/src/include/utils/backend_status.h
index 56a0f25296..ae4597c5fe 100644
--- a/src/include/utils/backend_status.h
+++ b/src/include/utils/backend_status.h
@@ -331,6 +331,43 @@ extern void CreateSharedBackendStatus(void);
  * ----------
  */
 
+/* Utility functions */
+
+/*
+static inline int backend_type_get_idx(BackendType backend_type)
+static inline BackendType idx_get_backend_type(int idx)

---

From: Melanie Plageman <[email-masked]>
Date: Thu, 30 Dec 2021 15:30:50 -0500

> than shared buffers written by B_BACKEND in pg_stat_buffers. I would

In a quick chat off-list, Andres suggested it might be okay to have a
single reset target for both the pg_stat_buffers view and legacy
pg_stat_bgwriter view. So, I am planning to share a new patchset which
has only the new "buffers" target which will also reset the legacy
pg_stat_bgwriter view.

I'll also remove the bgwriter stats I proposed and the
pg_stat_checkpointer view to keep things simple for now.

- Melanie

---

From: Melanie Plageman <[email-masked]>
Date: Tue, 5 Jul 2022 13:24:55 -0400

On Mon, Mar 21, 2022 at 8:15 PM Andres Freund <[email-masked]> wrote:

> Hi,
>
> On 2022-02-19 11:06:18 -0500, Melanie Plageman wrote:
> > v21 rebased with compile errors fixed is attached.
>
> This currently doesn't apply (mea culpa likely):
> http://cfbot.cputube.org/patch_37_3272.log
>
> Could you rebase? Marked as waiting-on-author for now.
>
>
>
Attached is the rebased/rewritten version of the pg_stat_buffers patch
which uses the cumulative stats system instead of stats collector.

I've moved to the model of backend-local pending stats which get
accumulated into shared memory by pgstat_report_stat().

It is worth noting that, with this method, other backends will no longer
have access to each other's individual IO operation statistics. An
argument could be made to keep the statistics in each backend in
PgBackendStatus before accumulating them to the cumulative stats system
so that they can be accessed at the per-backend level of detail.

There are two TODOs related to when pgstat_report_io_ops() should be
called. pgstat_report_io_ops() is meant for backends that will not
commonly call pgstat_report_stat(). I was unsure if it made sense for
BootstrapModeMain() to explicitly call pgstat_report_io_ops() and if
auto vacuum worker should call it explicitly and, if so, if it was the
right location to call it after do_autovacuum().

Archiver and syslogger do not increment or report IO operations.

I did not change pg_stat_bgwriter fields to derive from the IO
operations statistics structures since the reset targets differ.

Also, I added one test, but I'm not sure if it will be flakey. It tests
that the "writes" for checkpointer are tracked when data is inserted
into a table and then CHECKPOINT is explicitly invoked directly after. I
don't know if this will have a problem if the checkpointer is busy and
somehow the backend which dirtied the buffer is forced to write out its
own buffer, causing the test to potentially fail (even if the
checkpointer is doing other writes [causing it to be busy], it may not
do them in between the INSERT and the SELECT from pg_stat_buffers).

I am wondering how to add a non-flakey test. For regular backends, I
couldn't think of a way to suspend checkpointer to make them do their
own writes and fsyncs in the context of a regression or isolation test.
In fact for many of the dirty buffers it seems like it will be difficult
to keep bgwriter, checkpointer, and regular backends from competing and
sometimes causing test failures.

- Melanie

---

From: Andres Freund <[email-masked]>
Date: Wed, 6 Jul 2022 12:20:47 -0700

Hi,

On 2022-07-05 13:24:55 -0400, Melanie Plageman wrote:
> From 2d089e26236c55d1be5b93833baa0cf7667ba38d Mon Sep 17 00:00:00 2001
> Date: Tue, 28 Jun 2022 11:33:04 -0400
> Subject: [PATCH v22 1/3] Add BackendType for standalone backends
> 
> All backends should have a BackendType to enable statistics reporting
> per BackendType.
> 
> Add a new BackendType for standalone backends, B_STANDALONE_BACKEND (and
> alphabetize the BackendTypes). Both the bootstrap backend and single
> user mode backends will have BackendType B_STANDALONE_BACKEND.
> 
>  src/backend/utils/init/miscinit.c | 17 +++++++++++------
>  src/include/miscadmin.h           |  5 +++--
>  2 files changed, 14 insertions(+), 8 deletions(-)
> 
> index eb43b2c5e5..07e6db1a1c 100644

Hm. This is used for singleuser mode as well as bootstrap. Should we
split those? It's not like bootstrap mode really matters for stats, so
I'm inclined not to.

> @@ -375,6 +376,8 @@ BootstrapModeMain(int argc, char *argv[], bool check_only)
>  	 * out the initial relation mapping files.
>  	 */
>  	RelationMapFinishBootstrap();
> +	// TODO: should this be done for bootstrap?
> +	pgstat_report_io_ops();

Hm. Not particularly useful, but also not harmful. But we don't need an
explicit call, because it'll be done at process exit too. At least I
think, it could be that it's different for bootstrap.

> diff --git a/src/backend/postmaster/autovacuum.c b/src/backend/postmaster/autovacuum.c
> index 2e146aac93..e6dbb1c4bb 100644
> --- a/src/backend/postmaster/autovacuum.c
> +++ b/src/backend/postmaster/autovacuum.c
> @@ -1712,6 +1712,9 @@ AutoVacWorkerMain(int argc, char *argv[])
>  		recentXid = ReadNextTransactionId();
>  		recentMulti = ReadNextMultiXactId();
>  		do_autovacuum();
> +
> +		// TODO: should this be done more often somewhere in do_autovacuum()?
> +		pgstat_report_io_ops();
>  	}

Don't think you need all these calls before process exit - it'll happen
via pgstat_shutdown_hook().

IMO it'd be a good idea to add pgstat_report_io_ops() to
pgstat_report_vacuum()/analyze(), so that the stats for a longrunning
autovac worker get updated more regularly.

> diff --git a/src/backend/postmaster/bgwriter.c b/src/backend/postmaster/bgwriter.c
> index 91e6f6ea18..87e4b9e9bd 100644
> --- a/src/backend/postmaster/bgwriter.c
> +++ b/src/backend/postmaster/bgwriter.c
> @@ -242,6 +242,7 @@ BackgroundWriterMain(void)
>  
>  		/* Report pending statistics to the cumulative stats system */
>  		pgstat_report_bgwriter();
> +		pgstat_report_io_ops();
>  
>  		if (FirstCallSinceLastCheckpoint())
>  		{

How about moving the pgstat_report_io_ops() into
pgstat_report_bgwriter(), pgstat_report_autovacuum() etc? Seems
unnecessary to have multiple pgstat_* calls in these places.

> +/*
> + * Flush out locally pending IO Operation statistics entries
> + *
> + * If nowait is true, this function returns false on lock failure. Otherwise
> + * this function always returns true. Writer processes are mutually excluded
> + * using LWLock, but readers are expected to use change-count protocol to avoid
> + * interference with writers.
> + *
> + * If nowait is true, this function returns true if the lock could not be
> + * acquired. Otherwise return false.
> + *
> + */
> +bool
> +pgstat_flush_io_ops(bool nowait)
> +{
> +	PgStat_IOPathOps *dest_io_path_ops;
> +	PgStatShared_BackendIOPathOps *stats_shmem;
> +
> +	if (!have_ioopstats)
> +	stats_shmem = &pgStatLocal.shmem->io_ops;
> +
> +	if (!nowait)
> +		LWLockAcquire(&stats_shmem->lock, LW_EXCLUSIVE);
> +	else if (!LWLockConditionalAcquire(&stats_shmem->lock, LW_EXCLUSIVE))
> +		return true;

Wonder if it's worth making the lock specific to the backend type?

> +	dest_io_path_ops =
> +		&stats_shmem->stats[backend_type_get_idx(beentry->st_backendType)];
> +

This could be done before acquiring the lock, right?

> +void
> +pgstat_io_ops_snapshot_cb(void)
> +{
> +	PgStatShared_BackendIOPathOps *stats_shmem = &pgStatLocal.shmem->io_ops;
> +	PgStat_IOPathOps *snapshot_ops = pgStatLocal.snapshot.io_path_ops;
> +	PgStat_IOPathOps *reset_ops;
> +
> +	PgStat_IOPathOps *reset_offset = stats_shmem->reset_offset;
> +	PgStat_IOPathOps reset[BACKEND_NUM_TYPES];
> +
> +	pgstat_copy_changecounted_stats(snapshot_ops,
> +			&stats_shmem->stats, sizeof(stats_shmem->stats),
> +			&stats_shmem->changecount);

This doesn't make sense - with multiple writers you can't use the
changecount approach (and you don't in the flush part above).

> +	LWLockAcquire(&stats_shmem->lock, LW_SHARED);
> +	memcpy(&reset, reset_offset, sizeof(stats_shmem->stats));
> +	LWLockRelease(&stats_shmem->lock);

Which then also means that you don't need the reset offset stuff. It's
only there because with the changecount approach we can't take a lock to
reset the stats (since there is no lock). With a lock you can just reset
the shared state.

> +void
> +pgstat_count_io_op(IOOp io_op, IOPath io_path)
> +{
> +	PgStat_IOOpCounters *pending_counters = &pending_IOOpStats.data[io_path];
> +	PgStat_IOOpCounters *cumulative_counters =
> +			&cumulative_IOOpStats.data[io_path];

the pending_/cumultive_ prefix before an uppercase-first camelcase name
seems ugly...

> +	switch (io_op)
> +	{
> +		case IOOP_ALLOC:
> +			pending_counters->allocs++;
> +			cumulative_counters->allocs++;
> +			break;
> +		case IOOP_EXTEND:
> +			pending_counters->extends++;
> +			cumulative_counters->extends++;
> +			break;
> +		case IOOP_FSYNC:
> +			pending_counters->fsyncs++;
> +			cumulative_counters->fsyncs++;
> +			break;
> +		case IOOP_WRITE:
> +			pending_counters->writes++;
> +			cumulative_counters->writes++;
> +			break;
> +	}
> +
> +	have_ioopstats = true;
> +}

Doing two math ops / memory accesses every time seems off. Seems better
to maintain cumultive_counters whenever reporting stats, just before
zeroing pending_counters?

> +/*
> + * Report IO operation statistics
> + *
> + * This works in much the same way as pgstat_flush_io_ops() but is meant for
> + * BackendTypes like bgwriter for whom pgstat_report_stat() will not be called
> + * frequently enough to keep shared memory stats fresh.
> + * Backends not typically calling pgstat_report_stat() can invoke
> + * pgstat_report_io_ops() explicitly.
> + */
> +void
> +pgstat_report_io_ops(void)
> +{

This shouldn't be needed - the flush function above can be used.

> +	PgStat_IOPathOps *dest_io_path_ops;
> +	PgStatShared_BackendIOPathOps *stats_shmem;
> +
> +	Assert(!pgStatLocal.shmem->is_shutdown);
> +	pgstat_assert_is_up();
> +
> +	if (!have_ioopstats)
> +		return;
> +

Is there a case where this may be called where we have no beentry?

Why not just use MyBackendType?

> +	stats_shmem = &pgStatLocal.shmem->io_ops;
> +
> +	dest_io_path_ops =
> +		&stats_shmem->stats[backend_type_get_idx(beentry->st_backendType)];
> +
> +	pgstat_begin_changecount_write(&stats_shmem->changecount);

A mentioned before, the changecount stuff doesn't apply here. You need a
lock.

> +PgStat_IOPathOps *
> +pgstat_fetch_backend_io_path_ops(void)
> +{
> +	pgstat_snapshot_fixed(PGSTAT_KIND_IOOPS);
> +	return pgStatLocal.snapshot.io_path_ops;
> +}
> +
> +PgStat_Counter
> +pgstat_fetch_cumulative_io_ops(IOPath io_path, IOOp io_op)
> +{
> +	PgStat_IOOpCounters *counters = &cumulative_IOOpStats.data[io_path];
> +
> +	switch (io_op)
> +	{
> +		case IOOP_ALLOC:
> +			return counters->allocs;
> +		case IOOP_EXTEND:
> +			return counters->extends;
> +		case IOOP_FSYNC:
> +			return counters->fsyncs;
> +		case IOOP_WRITE:
> +			return counters->writes;
> +		default:
> +			elog(ERROR, "IO Operation %s for IO Path %s is undefined.",
> +					pgstat_io_op_desc(io_op), pgstat_io_path_desc(io_path));
> +	}
> +}

There's currently no user for this, right? Maybe let's just defer the
cumulative stuff until we need it?

> +const char *
> +pgstat_io_path_desc(IOPath io_path)
> +{
> +	const char *io_path_desc = "Unknown IO Path";
> +

This should be unreachable, right?

> From f2b5b75f5063702cbc3c64efdc1e7ef3cf1acdb4 Mon Sep 17 00:00:00 2001
> Date: Mon, 4 Jul 2022 15:44:17 -0400
> Subject: [PATCH v22 3/3] Add system view tracking IO ops per backend type

> path (e.g. shared buffers, local buffers, unbuffered IO) by each type of

I think I like pg_stat_io a bit better? Nearly everything in here seems
to fit better in that.

I guess we could split out buffers allocated, but that's actually
interesting in the context of the kind of IO too.

> @@ -3595,7 +3604,102 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i

Grammar critique time :)

> +FROM pg_stat_get_buffers() b;

Do we want to expose all data to all users? I guess pg_stat_bgwriter
does? But this does split things out a lot more...

> +		PgStat_IOOpCounters *counters = io_path_ops->data;
> +			CStringGetTextDatum(GetBackendTypeDesc(idx_get_backend_type(i)));
> +			/* const char *log_name = GetBackendTypeDesc(idx_get_backend_type(i)); */
> +
> +			Datum values[BUFFERS_NUM_COLUMNS];
> +			bool nulls[BUFFERS_NUM_COLUMNS];
> +			memset(values, 0, sizeof(values));
> +			memset(nulls, 0, sizeof(nulls));
> +
> +			values[BUFFERS_COLUMN_BACKEND_TYPE] = backend_type_desc;
> +			values[BUFFERS_COLUMN_IO_PATH] = CStringGetTextDatum(pgstat_io_path_desc(j));

Random musing: I wonder if we should start to use SQL level enums for
this kind of thing.

> + pg_stat_reset_shared 
> +----------------------
> +(1 row)
> +SELECT pg_stat_force_next_flush();
> + pg_stat_force_next_flush 
> +--------------------------
> + 
> +(1 row)
> +
> +SELECT write = 0 FROM pg_stat_buffers WHERE io_path = 'Shared' and backend_type = 'checkpointer';
> + ?column? 
> +----------
> + t
> +(1 row)

Don't think you can rely on that. The lookup of the view, functions
might have needed to load catalog data, which might have needed to evict
buffers.  I think you can do something more reliable by checking that
there's more written buffers after a checkpoint than before, or such.

Would be nice to have something testing that the ringbuffer stats stuff
does something sensible - that feels not entirely trivial.

Greetings,

Andres Freund

---

From: Melanie Plageman <[email-masked]>
Date: Mon, 11 Jul 2022 22:22:28 -0400

Hi,

In the attached patch set, I've added in missing IO operations for
certain IO Paths as well as enumerating in the commit message which IO
Paths and IO Operations are not currently counted and or not possible.

There is a TODO in HandleWalWriterInterrupts() about removing
pgstat_report_wal() since it is immediately before a proc_exit()

I was wondering if LocalBufferAlloc() should increment the counter or if
I should wait until GetLocalBufferStorage() to increment the counter.

I also realized that I am not differentiating between IOPATH_SHARED and
IOPATH_STRATEGY for IOOP_FSYNC. But, given that we don't know what type
of buffer we are fsync'ing by the time we call register_dirty_segment(),
I'm not sure how we would fix this.

On Wed, Jul 6, 2022 at 3:20 PM Andres Freund <[email-masked]> wrote:

> > Discussion:
> https://www.postgresql.org/message-id/CAAKRu_aaq33UnG4TXq3S-OSXGWj1QGf0sU%2BECH4tNwGFNERkZA%40mail.gmail.com
> > ---
I have no opinion currently.
It depends on how commonly you think developers might want separate
bootstrap and single user mode IO stats.

> think, it could be that it's different for bootstrap.
I've removed this and other occurrences which were before proc_exit()
(and thus redundant). (Though I did not explicitly check if it was
different for bootstrap.)

> autovac worker get updated more regularly.

noted and fixed.

> unnecessary to have multiple pgstat_* calls in these places.
noted and fixed.

> Wonder if it's worth making the lock specific to the backend type?

I've added another Lock into PgStat_IOPathOps so that each BackendType
can be locked separately. But, I've also kept the lock in
PgStatShared_BackendIOPathOps so that reset_all and snapshot could be
done easily.

>
>
> > +     dest_io_path_ops =
> > +
>  &stats_shmem->stats[backend_type_get_idx(beentry->st_backendType)];
> > +
>

Yes, I believe I have cleaned up all of this embarrassing mess. I use the
lock in PgStatShared_BackendIOPathOps for reset all and snapshot and the
locks in PgStat_IOPathOps for flush.

> zeroing pending_counters?

I've gone ahead and cut the cumulative counters concept.

Fixed.

Fixed.

>  &stats_shmem->stats[backend_type_get_idx(beentry->st_backendType)];
> > +

Fixed.

> cumulative stuff until we need it?

Removed.

> This should be unreachable, right?

Changed it to an error.

> interesting in the context of the kind of IO too.

changed it to pg_stat_io

> does? But this does split things out a lot more...
I didn't see another similar example limiting access.

> there's more written buffers after a checkpoint than before, or such.
Yes, per an off list suggestion by you, I have changed the tests to use a
sum of writes. I've also added a test for IOPATH_LOCAL and fixed some of
the missing calls to count IO Operations for IOPATH_LOCAL and
IOPATH_STRATEGY.

I struggled to come up with a way to test writes for a particular
type of backend are counted correctly since a dirty buffer could be
written out by another type of backend before the target BackendType has
a chance to write it out.

I also struggled to come up with a way to test IO operations for
background workers. I'm not sure of a way to deterministically have a
background worker do a particular kind of IO in a test scenario.

I'm not sure how to cause a strategy "extend" for testing.

> does something sensible - that feels not entirely trivial.
I've added a test to test that reused strategy buffers are counted as
allocs. I would like to add a test which checks that if a buffer in the
ring is pinned and thus not reused, that it is not counted as a strategy
alloc, but I found it challenging without a way to pause vacuuming, pin
a buffer, then resume vacuuming.

Thanks,
Melanie
From 9d8fdbcf8dde109e84b680c8160c0174574a2c05 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Tue, 28 Jun 2022 11:33:04 -0400
Subject: [PATCH v23 1/3] Add BackendType for standalone backends

 
@@ -273,6 +278,12 @@ GetBackendTypeDesc(BackendType backendType)
@@ -285,12 +296,6 @@ GetBackendTypeDesc(BackendType backendType)
 
 	return backendDesc;
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index ea9a56d395..5276bf25a1 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -316,18 +316,19 @@ extern void SwitchBackToLocalLatch(void);
 typedef enum BackendType
 {
 	B_INVALID = 0,
+	B_ARCHIVER,
 	B_AUTOVAC_LAUNCHER,
 	B_AUTOVAC_WORKER,
 	B_BACKEND,
 	B_BG_WORKER,
 	B_BG_WRITER,
 	B_CHECKPOINTER,
+	B_LOGGER,
+	B_STANDALONE_BACKEND,
 	B_STARTUP,
 	B_WAL_RECEIVER,
 	B_WAL_SENDER,
 	B_WAL_WRITER,
-	B_ARCHIVER,
-	B_LOGGER,
 } BackendType;
 
 extern PGDLLIMPORT BackendType MyBackendType;
-- 
2.34.1

From b20d5fcc16492b1934d9a8cc8144508505db5d6b Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Wed, 29 Jun 2022 18:37:42 -0400
Subject: [PATCH v23 2/3] Track IO operation statistics

Introduce "IOOp", an IO operation done by a backend, and "IOPath", the
location or type of IO done by a backend. For example, the checkpointer
may write a shared buffer out. This would be counted as an IOOp write on
an IOPath IOPATH_SHARED by BackendType "checkpointer".

Each IOOp (alloc, fsync, extend, write) is counted per IOPath
(direct, local, shared, or strategy) through a call to
pgstat_count_io_op().

The primary concern of these statistics is IO operations on data blocks
during the course of normal database operations. IO done by, for
example, the archiver or syslogger is not counted in these statistics.

IOPATH_LOCAL and IOPATH_SHARED IOPaths concern operations on local
and shared buffers.

The IOPATH_STRATEGY IOPath concerns buffers alloc'd/written/read/fsync'd
as part of a BufferAccessStrategy.

The IOPATH_DIRECT IOPath concerns blocks of IO which are read, written,
or fsync'd using smgrwrite/extend/immedsync directly (as opposed to
through [Local]BufferAlloc()).

Note that this commit does not add code to increment IOPATH_DIRECT. A
future patch adding wrappers for smgrwrite(), smgrextend(), and
smgrimmedsync() would provide a good location to call
pgstat_count_io_op() for unbuffered IO and avoid regressions for future
users of these functions.

IOOP_ALLOC is counted for IOPATH_SHARED and IOPATH_LOCAL whenever a
buffer is acquired through [Local]BufferAlloc(). IOOP_ALLOC is invalid
for IOPATH_DIRECT. IOOP_ALLOC for IOPATH_STRATEGY is counted whenever a
buffer already in the strategy ring is reused.

Stats on IOOps for all IOPaths for a backend are initially accumulated
locally.

Later they are flushed to shared memory and accumulated with those from
all other backends, exited and live.

Some BackendTypes will not execute pgstat_report_stat() and thus must
explicitly call pgstat_report_io_ops() in order to flush their backend
local IO operation statistics to shared memory.

Author: Melanie Plageman <[email-masked]>
Reviewed-by: Justin Pryzby <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/20200124195226.lth52iydq2n2uilq%40alap3.anarazel.de
---
 src/backend/bootstrap/bootstrap.c             |   1 +
 src/backend/postmaster/checkpointer.c         |   1 +
 src/backend/postmaster/walwriter.c            |   1 +
 src/backend/storage/buffer/bufmgr.c           |  53 ++++--
 src/backend/storage/buffer/freelist.c         |  33 +++-
 src/backend/storage/buffer/localbuf.c         |   5 +
 src/backend/storage/sync/sync.c               |   2 +
 src/backend/utils/activity/Makefile           |   1 +
 src/backend/utils/activity/pgstat.c           |  24 +++
 src/backend/utils/activity/pgstat_bgwriter.c  |   5 +
 .../utils/activity/pgstat_checkpointer.c      |   5 +
 src/backend/utils/activity/pgstat_database.c  |   5 +
 src/backend/utils/activity/pgstat_io_ops.c    | 168 ++++++++++++++++++
 src/backend/utils/activity/pgstat_relation.c  |  10 ++
 src/backend/utils/activity/pgstat_wal.c       |   5 +
 src/backend/utils/adt/pgstatfuncs.c           |   4 +-
 src/include/miscadmin.h                       |   2 +
 src/include/pgstat.h                          |  53 ++++++
 src/include/storage/buf_internals.h           |   4 +-
 src/include/utils/backend_status.h            |  34 ++++
 src/include/utils/pgstat_internal.h           |  17 ++
 21 files changed, 417 insertions(+), 16 deletions(-)
 create mode 100644 src/backend/utils/activity/pgstat_io_ops.c

diff --git a/src/backend/bootstrap/bootstrap.c b/src/backend/bootstrap/bootstrap.c
index 088556ab54..963b05321e 100644
--- a/src/backend/bootstrap/bootstrap.c
+++ b/src/backend/bootstrap/bootstrap.c
@@ -33,6 +33,7 @@
 #include "miscadmin.h"
 #include "nodes/makefuncs.h"
 #include "pg_getopt.h"
+#include "pgstat.h"
 #include "storage/bufmgr.h"
 #include "storage/bufpage.h"
 #include "storage/condition_variable.h"
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 5fc076fc14..a06331e1eb 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -1116,6 +1116,7 @@ ForwardSyncRequest(const FileTag *ftag, SyncRequestType type)
 		if (!AmBackgroundWriterProcess())
 			CheckpointerShmem->num_backend_fsync++;
 		LWLockRelease(CheckpointerCommLock);
+		pgstat_count_io_op(IOOP_FSYNC, IOPATH_SHARED);
 		return false;
 	}
 
diff --git a/src/backend/postmaster/walwriter.c b/src/backend/postmaster/walwriter.c
index e926f8c27c..64e58f17f6 100644
--- a/src/backend/postmaster/walwriter.c
+++ b/src/backend/postmaster/walwriter.c
@@ -301,6 +301,7 @@ HandleWalWriterInterrupts(void)
 		 * loop to avoid overloading the cumulative stats system, there may
 		 * exist unreported stats counters for the WAL writer.
 		 */
+		// TODO: This may not be needed also
 		pgstat_report_wal(true);
 
 		proc_exit(0);
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index e257ae23e4..be5fb1e5bf 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -482,7 +482,7 @@ static BufferDesc *BufferAlloc(SMgrRelation smgr,
 							   BlockNumber blockNum,
 							   BufferAccessStrategy strategy,
 							   bool *foundPtr);
-static void FlushBuffer(BufferDesc *buf, SMgrRelation reln);
+static void FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOPath iopath);
 static void FindAndDropRelFileLocatorBuffers(RelFileLocator rlocator,
 											 ForkNumber forkNum,
 											 BlockNumber nForkBlock,
@@ -980,6 +980,16 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 
 	if (isExtend)
 	{
+		IOPath io_path;
+
+		if (isLocalBuf)
+			io_path = IOPATH_LOCAL;
+		else if (strategy != NULL)
+			io_path = IOPATH_STRATEGY;
+		else
+			io_path = IOPATH_SHARED;
+
+		pgstat_count_io_op(IOOP_EXTEND, io_path);
 		/* new buffers are zero-filled */
 		MemSet((char *) bufBlock, 0, BLCKSZ);
 		/* don't set checksum for all-zero page */
@@ -1180,6 +1190,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	/* Loop here in case we have to try another victim buffer */
 	for (;;)
 	{
+		bool from_ring;
 		/*
 		 * Ensure, while the spinlock's not yet held, that there's a free
 		 * refcount entry.
@@ -1190,7 +1201,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 		 * Select a victim buffer.  The buffer is returned with its header
 		 * spinlock still held!
 		 */
-		buf = StrategyGetBuffer(strategy, &buf_state);
+		buf = StrategyGetBuffer(strategy, &buf_state, &from_ring);

@@ -1227,6 +1238,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 			if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf),
 										 LW_SHARED))
 			{
+				IOPath iopath;
 				/*
 				 * If using a nondefault strategy, and writing the buffer
 				 * would require a WAL flush, let the strategy decide whether
@@ -1244,7 +1256,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 					UnlockBufHdr(buf, buf_state);
 
@@ -1253,13 +1265,27 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 					}
 				}
 
+				/*
+				 * When a strategy is in use, if the dirty buffer was selected
+				 * from the strategy ring and we did not bother checking the
+				 * freelist or doing a clock sweep to look for a clean shared
+				 * buffer to use, the write will be counted as a strategy
+				 * write. However, if the dirty buffer was obtained from the
+				 * freelist or a clock sweep, it is counted as a regular write.
+				 *
+				 * When a strategy is not in use, at this point the write can
+				 * only be a "regular" write of a dirty buffer.
+				 */
+
+				iopath = from_ring ? IOPATH_STRATEGY : IOPATH_SHARED;
+
 				/* OK, do the I/O */
 				TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum,
 														  smgr->smgr_rlocator.locator.spcOid,
 														  smgr->smgr_rlocator.locator.dbOid,
 														  smgr->smgr_rlocator.locator.relNumber);

 				ScheduleBufferTagForWriteback(&BackendWritebackContext,
@@ -2563,7 +2589,7 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
 	PinBuffer_Locked(bufHdr);
 	LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);

 
@@ -2810,9 +2836,12 @@ BufferGetTag(Buffer buffer, RelFileLocator *rlocator, ForkNumber *forknum,
  *
  * If the caller has an smgr reference for the buffer's relation, pass it
  * as the second parameter.  If not, pass NULL.
+ *
+ * IOPath will always be IOPATH_SHARED except when a buffer access strategy is
+ * used and the buffer being flushed is a buffer from the strategy ring.
  */
 static void
-FlushBuffer(BufferDesc *buf, SMgrRelation reln)
+FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOPath iopath)
 {
 	XLogRecPtr	recptr;
 	ErrorContextCallback errcallback;
@@ -2892,6 +2921,8 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln)
 	 */
 	bufToWrite = PageSetChecksumCopy((Page) bufBlock, buf->tag.blockNum);
 
+	pgstat_count_io_op(IOOP_WRITE, iopath);
+
 	if (track_io_timing)
 		INSTR_TIME_SET_CURRENT(io_start);
 
@@ -3540,6 +3571,8 @@ FlushRelationBuffers(Relation rel)
 						  localpage,
 						  false);
 
+				pgstat_count_io_op(IOOP_WRITE, IOPATH_LOCAL);
+
 				buf_state &= ~(BM_DIRTY | BM_JUST_DIRTIED);
 				pg_atomic_unlocked_write_u32(&bufHdr->state, buf_state);
 
@@ -3575,7 +3608,7 @@ FlushRelationBuffers(Relation rel)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, RelationGetSmgr(rel));
+			FlushBuffer(bufHdr, RelationGetSmgr(rel), IOPATH_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr, true);
 		}
@@ -3670,7 +3703,7 @@ FlushRelationsAllBuffers(SMgrRelation *smgrs, int nrels)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, srelent->srel);
+			FlushBuffer(bufHdr, srelent->srel, IOPATH_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr, true);
 		}
@@ -3878,7 +3911,7 @@ FlushDatabaseBuffers(Oid dbid)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, NULL);
+			FlushBuffer(bufHdr, NULL, IOPATH_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr, true);
 		}
@@ -3905,7 +3938,7 @@ FlushOneBuffer(Buffer buffer)

 
 /*
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index 990e081aae..e042612c4a 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -15,6 +15,7 @@
  */
 #include "postgres.h"
 
+#include "pgstat.h"
 #include "port/atomics.h"
 #include "storage/buf_internals.h"
 #include "storage/bufmgr.h"
@@ -198,7 +199,7 @@ have_free_buffer(void)
  *	return the buffer with the buffer header spinlock still held.
  */
 BufferDesc *
-StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
+StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state, bool *from_ring)
 {
 	BufferDesc *buf;
 	int			bgwprocno;
@@ -212,8 +213,19 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
 	if (strategy != NULL)
 	{
 		buf = GetBufferFromRing(strategy, buf_state);
-		if (buf != NULL)
+		*from_ring = buf != NULL;
+		if (*from_ring)
+		{
+			/*
+			 * When a strategy is in use, reused buffers from the strategy ring
+			 * will be counted as allocations for the purposes of IO Operation
+			 * statistics tracking. However, even when a strategy is in use, if
+			 * a new buffer must be allocated from shared buffers and added to
+			 * the ring, this is counted as a IOPATH_SHARED allocation.
+			 */
+			pgstat_count_io_op(IOOP_ALLOC, IOPATH_STRATEGY);
 			return buf;
+		}
 	}
 
 	/*
@@ -247,6 +259,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
 	 * the rate of buffer consumption.  Note that buffers recycled by a
 	 * strategy object are intentionally not counted here.
 	 */
+	pgstat_count_io_op(IOOP_ALLOC, IOPATH_SHARED);
 	pg_atomic_fetch_add_u32(&StrategyControl->numBufferAllocs, 1);
 
 	/*
@@ -682,8 +695,15 @@ AddBufferToRing(BufferAccessStrategy strategy, BufferDesc *buf)
  * if this buffer should be written and re-used.
  */
 bool
-StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf)
+StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf, bool *from_ring)
 {
+
+	/*
+	 * Start by assuming that we will use the dirty buffer selected by
+	 * StrategyGetBuffer().
+	 */
+	*from_ring = true;
+
 	/* We only do this in bulkread mode */
 	if (strategy->btype != BAS_BULKREAD)
 		return false;
@@ -699,5 +719,12 @@ StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf)
 	 */
 	strategy->buffers[strategy->current] = InvalidBuffer;
 
+	/*
index 41a08076b3..e99e1f53ef 100644
@@ -15,6 +15,7 @@
  */
 #include "postgres.h"
 
+#include "pgstat.h"
 #include "access/parallel.h"
 #include "catalog/catalog.h"
 #include "executor/instrument.h"
@@ -123,6 +124,8 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
 	if (LocalBufHash == NULL)
 		InitLocalBuffers();
 
+	pgstat_count_io_op(IOOP_ALLOC, IOPATH_LOCAL);
+
 	/* See if the desired buffer already exists */
 	hresult = (LocalBufferLookupEnt *)
 		hash_search(LocalBufHash, (void *) &newTag, HASH_FIND, NULL);
@@ -226,6 +229,8 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
 				  localpage,
 				  false);
 
+		pgstat_count_io_op(IOOP_WRITE, IOPATH_LOCAL);
+
 		/* Mark not-dirty now in case we error out below */
 		buf_state &= ~BM_DIRTY;
 		pg_atomic_unlocked_write_u32(&bufHdr->state, buf_state);
diff --git a/src/backend/storage/sync/sync.c b/src/backend/storage/sync/sync.c
index e1fb631003..20e259edef 100644
--- a/src/backend/storage/sync/sync.c
+++ b/src/backend/storage/sync/sync.c
@@ -432,6 +432,8 @@ ProcessSyncRequests(void)
 					total_elapsed += elapsed;
 					processed++;
 
+					pgstat_count_io_op(IOOP_FSYNC, IOPATH_SHARED);
+
 					if (log_checkpoints)
 						elog(DEBUG1, "checkpoint sync: number=%d file=%s time=%.3f ms",
 							 processed,
diff --git a/src/backend/utils/activity/Makefile b/src/backend/utils/activity/Makefile
index a2e8507fd6..0098785089 100644
--- a/src/backend/utils/activity/Makefile
+++ b/src/backend/utils/activity/Makefile
@@ -22,6 +22,7 @@ OBJS = \
 	pgstat_checkpointer.o \
 	pgstat_database.o \
 	pgstat_function.o \
+	pgstat_io_ops.o \
 	pgstat_relation.o \
 	pgstat_replslot.o \
 	pgstat_shmem.o \
diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index 88e5dd1b2b..52924e64dd 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -359,6 +359,15 @@ static const PgStat_KindInfo pgstat_kind_infos[PGSTAT_NUM_KINDS] = {
 		.snapshot_cb = pgstat_checkpointer_snapshot_cb,
 	},
 
+	[PGSTAT_KIND_IOOPS] = {
+		.name = "io_ops",
+
+		.fixed_amount = true,
+
+		.reset_all_cb = pgstat_io_ops_reset_all_cb,
+		.snapshot_cb = pgstat_io_ops_snapshot_cb,
+	},
+
 	[PGSTAT_KIND_SLRU] = {
 		.name = "slru",
 
@@ -628,6 +637,9 @@ pgstat_report_stat(bool force)
 	/* flush database / relation / function / ... stats */
 	partial_flush |= pgstat_flush_pending_entries(nowait);
 
+	/* flush IO Operations stats */
+	partial_flush |= pgstat_flush_io_ops(nowait);
+
 	/* flush wal stats */
 	partial_flush |= pgstat_flush_wal(nowait);
 
@@ -1312,6 +1324,12 @@ pgstat_write_statsfile(void)
 	pgstat_build_snapshot_fixed(PGSTAT_KIND_CHECKPOINTER);
 	write_chunk_s(fpout, &pgStatLocal.snapshot.checkpointer);
 
+	/*
+	 * Write IO Operations stats struct
+	 */
+	pgstat_build_snapshot_fixed(PGSTAT_KIND_IOOPS);
+	write_chunk_s(fpout, &pgStatLocal.snapshot.io_path_ops);
+
 	/*
 	 * Write SLRU stats struct
 	 */
@@ -1486,6 +1504,12 @@ pgstat_read_statsfile(void)
 	if (!read_chunk_s(fpin, &shmem->checkpointer.stats))
 		goto error;
 
+	/*
+	 * Read IO Operations stats struct
+	 */
+	if (!read_chunk_s(fpin, &shmem->io_ops.stats))
+		goto error;
+
 	/*
 	 * Read SLRU stats struct
 	 */
diff --git a/src/backend/utils/activity/pgstat_bgwriter.c b/src/backend/utils/activity/pgstat_bgwriter.c
index fbb1edc527..d83df169db 100644
--- a/src/backend/utils/activity/pgstat_bgwriter.c
+++ b/src/backend/utils/activity/pgstat_bgwriter.c
@@ -56,6 +56,11 @@ pgstat_report_bgwriter(void)
 	 * Clear out the statistics buffer, so it can be re-used.
 	 */
 	MemSet(&PendingBgWriterStats, 0, sizeof(PendingBgWriterStats));
+
+	/*
+	 * Also report IO Operations statistics
+	 */
+	pgstat_flush_io_ops(false);
 }
 
 /*
diff --git a/src/backend/utils/activity/pgstat_checkpointer.c b/src/backend/utils/activity/pgstat_checkpointer.c
index af8d513e7b..668abecf90 100644
--- a/src/backend/utils/activity/pgstat_checkpointer.c
+++ b/src/backend/utils/activity/pgstat_checkpointer.c
@@ -62,6 +62,11 @@ pgstat_report_checkpointer(void)
 	 * Clear out the statistics buffer, so it can be re-used.
 	 */
 	MemSet(&PendingCheckpointerStats, 0, sizeof(PendingCheckpointerStats));
+
+	/*
+	 * Also report IO Operation statistics
+	 */
+	pgstat_flush_io_ops(false);
 }
 
 /*
diff --git a/src/backend/utils/activity/pgstat_database.c b/src/backend/utils/activity/pgstat_database.c
index d9275611f0..5fac75c8c6 100644
--- a/src/backend/utils/activity/pgstat_database.c
+++ b/src/backend/utils/activity/pgstat_database.c
@@ -72,6 +72,11 @@ pgstat_report_autovac(Oid dboid)
 	dbentry->stats.last_autovac_time = GetCurrentTimestamp();
 
 	pgstat_unlock_entry(entry_ref);
+
+	/*
+	 * Also report IO Operation statistics
+	 */
+	pgstat_flush_io_ops(false);
 }
 
 /*
diff --git a/src/backend/utils/activity/pgstat_io_ops.c b/src/backend/utils/activity/pgstat_io_ops.c
new file mode 100644
index 0000000000..abe288efc4
--- /dev/null
+++ b/src/backend/utils/activity/pgstat_io_ops.c
@@ -0,0 +1,168 @@
+/* -------------------------------------------------------------------------
+ *
+ * pgstat_io_ops.c
+ *	  Implementation of IO operation statistics.
+ *
+ * This file contains the implementation of IO operation statistics. It is kept
+ * separate from pgstat.c to enforce the line between the statistics access /
+ * storage implementation and the details about individual types of
+ * statistics.
+ *
+ * Copyright (c) 2001-2022, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/activity/pgstat_io_ops.c
+ * -------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "utils/pgstat_internal.h"
+
+static PgStat_IOPathOps pending_IOOpStats;
+bool have_ioopstats = false;
+
+
+/*
+ * Flush out locally pending IO Operation statistics entries
+ *
+ * If nowait is true, this function returns false on lock failure. Otherwise
+ * this function always returns true.
+ *
+ * If nowait is true, this function returns true if the lock could not be
+ * acquired. Otherwise return false.
+ */
+bool
+pgstat_flush_io_ops(bool nowait)
+{
+	PgStat_IOPathOps *stats_shmem;
+
+	if (!have_ioopstats)
+		return false;
+
+	stats_shmem =
+		&pgStatLocal.shmem->io_ops.stats[backend_type_get_idx(MyBackendType)];
+
+	if (!nowait)
+		LWLockAcquire(&stats_shmem->lock, LW_EXCLUSIVE);
+	else if (!LWLockConditionalAcquire(&stats_shmem->lock, LW_EXCLUSIVE))
+		return true;
+
+
+	for (int i = 0; i < IOPATH_NUM_TYPES; i++)
+	{
+		PgStat_IOOpCounters *sharedent = &stats_shmem->data[i];
+		PgStat_IOOpCounters *pendingent = &pending_IOOpStats.data[i];
+
+#define IO_OP_ACC(fld) sharedent->fld += pendingent->fld
+		IO_OP_ACC(allocs);
+		IO_OP_ACC(extends);
+		IO_OP_ACC(fsyncs);
+		IO_OP_ACC(writes);
+#undef IO_OP_ACC
+	}
+
+	LWLockRelease(&stats_shmem->lock);
+
+	MemSet(&pending_IOOpStats, 0, sizeof(pending_IOOpStats));
+
+	have_ioopstats = false;
+
+	return false;
+}
+
+void
+pgstat_io_ops_snapshot_cb(void)
+{
+	PgStatShared_BackendIOPathOps *stats_shmem = &pgStatLocal.shmem->io_ops;
+
+	LWLockAcquire(&stats_shmem->lock, LW_SHARED);
+
+	memcpy(pgStatLocal.snapshot.io_path_ops, &stats_shmem->stats,
+			sizeof(stats_shmem->stats));
+
+	LWLockRelease(&stats_shmem->lock);
+}
+
+void
+pgstat_io_ops_reset_all_cb(TimestampTz ts)
+{
+	PgStatShared_BackendIOPathOps *stats_shmem = &pgStatLocal.shmem->io_ops;
+
+	LWLockAcquire(&stats_shmem->lock, LW_EXCLUSIVE);
+
+	memset(&stats_shmem->stats, 0, sizeof(stats_shmem->stats));
+
+	for (int i = 0; i < BACKEND_NUM_TYPES; i++)
+		stats_shmem->stats[i].stat_reset_timestamp = ts;
+
+	LWLockRelease(&stats_shmem->lock);
+}
+
+void
+pgstat_count_io_op(IOOp io_op, IOPath io_path)
+{
+	PgStat_IOOpCounters *pending_counters = &pending_IOOpStats.data[io_path];
+
+	switch (io_op)
+	{
+		case IOOP_ALLOC:
+			pending_counters->allocs++;
+			break;
+		case IOOP_EXTEND:
+			pending_counters->extends++;
+			break;
+		case IOOP_FSYNC:
+			pending_counters->fsyncs++;
+			break;
+		case IOOP_WRITE:
+			pending_counters->writes++;
+			break;
+	}
+
+	have_ioopstats = true;
+}
+
+PgStat_IOPathOps *
+pgstat_fetch_backend_io_path_ops(void)
+{
+	pgstat_snapshot_fixed(PGSTAT_KIND_IOOPS);
+
+	return pgStatLocal.snapshot.io_path_ops;
+}
+
+const char *
+pgstat_io_path_desc(IOPath io_path)
+{
+	switch (io_path)
+	{
+		case IOPATH_DIRECT:
+			return "Direct";
+		case IOPATH_LOCAL:
+			return "Local";
+		case IOPATH_SHARED:
+			return "Shared";
+		case IOPATH_STRATEGY:
+			return "Strategy";
+	}
+
+	elog(ERROR, "Attempt to describe an unknown IOPath");
+}
+
+const char *
+pgstat_io_op_desc(IOOp io_op)
+{
+	switch (io_op)
+	{
+		case IOOP_ALLOC:
+			return "Alloc";
+		case IOOP_EXTEND:
+			return "Extend";
+		case IOOP_FSYNC:
+			return "Fsync";
+		case IOOP_WRITE:
+			return "Write";
+	}
+
+	elog(ERROR, "Attempt to describe an unknown IOOperation");
+}
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index a846d9ffb6..01ea45adf4 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -257,6 +257,11 @@ pgstat_report_vacuum(Oid tableoid, bool shared,
 	}
 
 	pgstat_unlock_entry(entry_ref);
+
+	/*
+	 * Also report IO Operations statistics
+	 */
+	pgstat_flush_io_ops(false);
 }
 
 /*
@@ -340,6 +345,11 @@ pgstat_report_analyze(Relation rel,
 	}
 
 	pgstat_unlock_entry(entry_ref);
+
+	/*
+	 * Also report IO Operations statistics
+	 */
+	pgstat_flush_io_ops(false);
 }
 
 /*
diff --git a/src/backend/utils/activity/pgstat_wal.c b/src/backend/utils/activity/pgstat_wal.c
index 5a878bd115..29b2e63e16 100644
--- a/src/backend/utils/activity/pgstat_wal.c
+++ b/src/backend/utils/activity/pgstat_wal.c
@@ -43,6 +43,11 @@ void
 pgstat_report_wal(bool force)
 {
 	pgstat_flush_wal(force);
+
+	/*
+	 * Also report IO Operation statistics
+	 */
+	pgstat_flush_io_ops(false);
 }
 
 /*
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 893690dad5..6259cc4f4c 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -2104,6 +2104,8 @@ pg_stat_reset_shared(PG_FUNCTION_ARGS)
 		pgstat_reset_of_kind(PGSTAT_KIND_BGWRITER);
 		pgstat_reset_of_kind(PGSTAT_KIND_CHECKPOINTER);
 	}
+	else if (strcmp(target, "io") == 0)
+		pgstat_reset_of_kind(PGSTAT_KIND_IOOPS);
 	else if (strcmp(target, "recovery_prefetch") == 0)
 		XLogPrefetchResetStats();
 	else if (strcmp(target, "wal") == 0)
@@ -2112,7 +2114,7 @@ pg_stat_reset_shared(PG_FUNCTION_ARGS)
 		ereport(ERROR,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("unrecognized reset target: \"%s\"", target),
-				 errhint("Target must be \"archiver\", \"bgwriter\", \"recovery_prefetch\", or \"wal\".")));
+				 errhint("Target must be \"archiver\", \"io\", \"bgwriter\", \"recovery_prefetch\", or \"wal\".")));
 
 	PG_RETURN_VOID();
 }
diff --git a/src/include/miscadmin.h b/src/include/miscadmin.h
index 5276bf25a1..61e95135f2 100644
--- a/src/include/miscadmin.h
+++ b/src/include/miscadmin.h
@@ -331,6 +331,8 @@ typedef enum BackendType
 	B_WAL_WRITER,
 } BackendType;
 
+#define BACKEND_NUM_TYPES B_WAL_WRITER
+
 extern PGDLLIMPORT BackendType MyBackendType;
 
 extern const char *GetBackendTypeDesc(BackendType backendType);
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index ac28f813b4..36a4b89a58 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -14,6 +14,7 @@
 #include "datatype/timestamp.h"
 #include "portability/instr_time.h"
 #include "postmaster/pgarch.h"	/* for MAX_XFN_CHARS */
+#include "storage/lwlock.h"
 #include "utils/backend_progress.h" /* for backward compatibility */
 #include "utils/backend_status.h"	/* for backward compatibility */
 #include "utils/relcache.h"
@@ -48,6 +49,7 @@ typedef enum PgStat_Kind
 	PGSTAT_KIND_ARCHIVER,
 	PGSTAT_KIND_BGWRITER,
 	PGSTAT_KIND_CHECKPOINTER,
+	PGSTAT_KIND_IOOPS,
 	PGSTAT_KIND_SLRU,
 	PGSTAT_KIND_WAL,
 } PgStat_Kind;
@@ -276,6 +278,45 @@ typedef struct PgStat_CheckpointerStats
 	PgStat_Counter buf_fsync_backend;
 } PgStat_CheckpointerStats;
 
+/*
+ * Types related to counting IO Operations for various IO Paths
+ */
+
+typedef enum IOOp
+{
+	IOOP_ALLOC,
+	IOOP_EXTEND,
+	IOOP_FSYNC,
+	IOOP_WRITE,
+} IOOp;
+
+#define IOOP_NUM_TYPES (IOOP_WRITE + 1)
+
+typedef enum IOPath
+{
+	IOPATH_DIRECT,
+	IOPATH_LOCAL,
+	IOPATH_SHARED,
+	IOPATH_STRATEGY,
+} IOPath;
+
+#define IOPATH_NUM_TYPES (IOPATH_STRATEGY + 1)
+
+typedef struct PgStat_IOOpCounters
+{
+	PgStat_Counter allocs;
+	PgStat_Counter extends;
+	PgStat_Counter fsyncs;
+	PgStat_Counter writes;
+} PgStat_IOOpCounters;
+
+typedef struct PgStat_IOPathOps
+{
+	LWLock		lock;
+	PgStat_IOOpCounters data[IOPATH_NUM_TYPES];
+	TimestampTz stat_reset_timestamp;
+} PgStat_IOPathOps;
+
 typedef struct PgStat_StatDBEntry
 {
 	PgStat_Counter n_xact_commit;
@@ -453,6 +494,18 @@ extern void pgstat_report_checkpointer(void);
 extern PgStat_CheckpointerStats *pgstat_fetch_stat_checkpointer(void);

+/*
+ * Functions in pgstat_io_ops.c
+ */
+
+extern void pgstat_count_io_op(IOOp io_op, IOPath io_path);
+extern bool pgstat_flush_io_ops(bool nowait);
+extern PgStat_IOPathOps *pgstat_fetch_backend_io_path_ops(void);
+extern PgStat_Counter pgstat_fetch_cumulative_io_ops(IOPath io_path, IOOp io_op);
+extern const char *pgstat_io_op_desc(IOOp io_op);
+extern const char *pgstat_io_path_desc(IOPath io_path);
+
+
 /*
  * Functions in pgstat_database.c
  */
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index aded5e8f7e..e35d82f050 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -310,10 +310,10 @@ extern void ScheduleBufferTagForWriteback(WritebackContext *context, BufferTag *
 
 /* freelist.c */
 
 extern int	StrategySyncStart(uint32 *complete_passes, uint32 *num_buf_alloc);
 extern void StrategyNotifyBgWriter(int bgwprocno);
diff --git a/src/include/utils/backend_status.h b/src/include/utils/backend_status.h
index 7403bca25e..49d062b1af 100644
--- a/src/include/utils/backend_status.h
+++ b/src/include/utils/backend_status.h
@@ -306,6 +306,40 @@ extern const char *pgstat_get_crashed_backend_activity(int pid, char *buffer,
 													   int buflen);
 extern uint64 pgstat_get_my_query_id(void);
 
+/* Utility functions */
+
+/*
 
 /* ----------
  * Support functions for the SQL-callable functions to
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 9303d05427..adffdd147d 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -329,6 +329,12 @@ typedef struct PgStatShared_Checkpointer
 	PgStat_CheckpointerStats reset_offset;
 } PgStatShared_Checkpointer;
 
+typedef struct PgStatShared_BackendIOPathOps
+{
+	LWLock lock;
+	PgStat_IOPathOps stats[BACKEND_NUM_TYPES];
+} PgStatShared_BackendIOPathOps;
+
 typedef struct PgStatShared_SLRU
 {
 	/* lock protects ->stats */
@@ -419,6 +425,7 @@ typedef struct PgStat_ShmemControl
 	PgStatShared_Archiver archiver;
 	PgStatShared_BgWriter bgwriter;
 	PgStatShared_Checkpointer checkpointer;
+	PgStatShared_BackendIOPathOps io_ops;
 	PgStatShared_SLRU slru;
 	PgStatShared_Wal wal;
 } PgStat_ShmemControl;
@@ -442,6 +449,8 @@ typedef struct PgStat_Snapshot

+	PgStat_IOPathOps io_path_ops[BACKEND_NUM_TYPES];
+
 	PgStat_SLRUStats slru[SLRU_NUM_ELEMENTS];
 
 	PgStat_WalStats wal;
@@ -549,6 +558,14 @@ extern void pgstat_database_reset_timestamp_cb(PgStatShared_Common *header, Time
 extern bool pgstat_function_flush_cb(PgStat_EntryRef *entry_ref, bool nowait);

+/*
+ * Functions in pgstat_io_ops.c
+ */
+
+extern void pgstat_io_ops_snapshot_cb(void);
+extern void pgstat_io_ops_reset_all_cb(TimestampTz ts);
+
+
 /*
  * Functions in pgstat_relation.c
  */
-- 
2.34.1

From 714fab745590b4ed6c1b9e220fb75c36ad5ab85d Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Mon, 4 Jul 2022 15:44:17 -0400
Subject: [PATCH v23 3/3] Add system view tracking IO ops per backend type

Add pg_stat_io, a system view which tracks the number of IOOp (allocs,
writes, fsyncs, and extends) done through each IOPath (e.g. shared
buffers, local buffers, unbuffered IO) by each type of backend.

Some of these should always be zero. For example, checkpointer does not
use a BufferAccessStrategy (currently), so the "strategy" IOPath for
checkpointer will be 0 for all IOOps. All possible combinations of
IOPath and IOOp are enumerated in the view but not all are populated or
even possible at this point.

View stats are fetched from statistics incremented when a backend
performs an IO Operation and maintained by the cumulative statistics
subsystem.

Each row of the view is stats for a particular BackendType for a
particular IOPath (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.

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'.

Author: Melanie Plageman <[email-masked]>
Reviewed-by: Justin Pryzby <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/20200124195226.lth52iydq2n2uilq%40alap3.anarazel.de
---
 doc/src/sgml/monitoring.sgml         | 108 ++++++++++++++++++++++++++-
 src/backend/catalog/system_views.sql |  11 +++
 src/backend/utils/adt/pgstatfuncs.c  |  66 ++++++++++++++++
 src/include/catalog/pg_proc.dat      |   9 +++
 src/test/regress/expected/rules.out  |   8 ++
 src/test/regress/expected/stats.out  |  59 +++++++++++++++
 src/test/regress/sql/stats.sql       |  34 +++++++++
 7 files changed, 294 insertions(+), 1 deletion(-)

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index 4549c2560e..775ecf2f21 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 path 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
@@ -3595,7 +3604,102 @@ 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>
+
+ </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 for each possible IO path containing global data for the cluster for
+   that backend and IO path.
+  </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).
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>io_path</structfield> <type>text</type>
+      </para>
+      <para>
+       IO path taken (e.g. shared buffers, direct).
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>alloc</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of buffers allocated.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>extend</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of blocks extended.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>fsync</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of blocks fsynced.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>write</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of blocks written.
+      </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>
@@ -5355,6 +5459,8 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
         the <structname>pg_stat_bgwriter</structname>
         view, <literal>archiver</literal> to reset all the counters shown in
         the <structname>pg_stat_archiver</structname> view,
+        <literal>io</literal> to reset all the counters shown in the
+        <structname>pg_stat_io</structname> view,
         <literal>wal</literal> to reset all the counters shown in the
         <structname>pg_stat_wal</structname> view or
         <literal>recovery_prefetch</literal> to reset all the counters shown
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index fedaed533b..b0b2d39e28 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1115,6 +1115,17 @@ 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_path,
+       b.alloc,
+       b.extend,
+       b.fsync,
+       b.write,
+       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 6259cc4f4c..30aff64860 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1739,6 +1739,72 @@ 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.
+*/
+enum
+{
+	IO_COLUMN_BACKEND_TYPE,
+	IO_COLUMN_IO_PATH,
+	IO_COLUMN_ALLOCS,
+	IO_COLUMN_EXTENDS,
+	IO_COLUMN_FSYNCS,
+	IO_COLUMN_WRITES,
+	IO_COLUMN_RESET_TIME,
+	IO_NUM_COLUMNS,
+};
+
+Datum
+pg_stat_get_io(PG_FUNCTION_ARGS)
+{
+	PgStat_IOPathOps *io_path_ops;
+	ReturnSetInfo *rsinfo;
+	Datum reset_time;
+
+	SetSingleFuncCall(fcinfo, 0);
+	io_path_ops = pgstat_fetch_backend_io_path_ops();
+	rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+
+	/*
+		* Currently it is not permitted to reset IO operation stats for individual
+		* IO Paths or individual BackendTypes. All IO Operation statistics are
+		* reset together. As such, it is easiest to reuse the first reset timestamp
+		* available.
+		*/
+	reset_time = TimestampTzGetDatum(io_path_ops->stat_reset_timestamp);
+
+	for (int i = 0; i < BACKEND_NUM_TYPES; i++)
+	{
+		PgStat_IOOpCounters *counters = io_path_ops->data;
+		Datum		backend_type_desc =
+			CStringGetTextDatum(GetBackendTypeDesc(idx_get_backend_type(i)));
+			/* const char *log_name = GetBackendTypeDesc(idx_get_backend_type(i)); */
+
+		for (int j = 0; j < IOPATH_NUM_TYPES; j++)
+		{
+			Datum values[IO_NUM_COLUMNS];
+			bool nulls[IO_NUM_COLUMNS];
+			memset(values, 0, sizeof(values));
+			memset(nulls, 0, sizeof(nulls));
+
+			values[IO_COLUMN_BACKEND_TYPE] = backend_type_desc;
+			values[IO_COLUMN_IO_PATH] = CStringGetTextDatum(pgstat_io_path_desc(j));
+			values[IO_COLUMN_RESET_TIME] = TimestampTzGetDatum(reset_time);
+			values[IO_COLUMN_ALLOCS] = Int64GetDatum(counters->allocs);
+			values[IO_COLUMN_EXTENDS] = Int64GetDatum(counters->extends);
+			values[IO_COLUMN_FSYNCS] = Int64GetDatum(counters->fsyncs);
+			values[IO_COLUMN_WRITES] = Int64GetDatum(counters->writes);
+
+			tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+			counters++;
+		}
+		io_path_ops++;
+	}
+
+	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 2e41f4d9e8..e9662fdc04 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5646,6 +5646,15 @@
   proname => 'pg_stat_get_buf_alloc', provolatile => 's', proparallel => 'r',
   prorettype => 'int8', proargtypes => '', prosrc => 'pg_stat_get_buf_alloc' },
 
+{ oid => '8459', descr => 'statistics: counts of all IO operations done through all IO paths by each type of backend.',
+  proname => 'pg_stat_get_io', provolatile => 's', proisstrict => 'f',
+  prorows => '52', proretset => 't',
+  proparallel => 'r', prorettype => 'record', proargtypes => '',
+  proallargtypes => '{text,text,int8,int8,int8,int8,timestamptz}',
+  proargmodes => '{o,o,o,o,o,o,o}',
+  proargnames => '{backend_type,io_path,alloc,extend,fsync,write,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 7ec3d2688f..3b05af9ac8 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1873,6 +1873,14 @@ 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_path,
+    b.alloc,
+    b.extend,
+    b.fsync,
+    b.write,
+    b.stats_reset
+   FROM pg_stat_get_io() b(backend_type, io_path, alloc, extend, fsync, write, 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 5b0ebf090f..6dade03b65 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -554,4 +554,63 @@ SELECT pg_stat_get_live_tuples(:drop_stats_test_subxact_oid);
 
 DROP TABLE trunc_stats_test, trunc_stats_test1, trunc_stats_test2, trunc_stats_test3, trunc_stats_test4;
 DROP TABLE prevstats;
+-- Test that writes to Shared Buffers are tracked in pg_stat_io
+SELECT sum(write) AS io_sum_shared_writes_before FROM pg_stat_io WHERE io_path = 'Shared' \gset
+CREATE TABLE test_io_shared_writes(a int);
+INSERT INTO test_io_shared_writes SELECT i FROM generate_series(1,100)i;
+CHECKPOINT;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush 
+--------------------------
+ 
+(1 row)
+
+SELECT sum(write) AS io_sum_shared_writes_after FROM pg_stat_io WHERE io_path = 'Shared' \gset
+SELECT :io_sum_shared_writes_after > :io_sum_shared_writes_before;
+ ?column? 
+----------
+ t
+(1 row)
+
+DROP TABLE test_io_shared_writes;
+-- Test that extends of temporary tables are tracked in pg_stat_io
+CREATE TEMPORARY TABLE test_io_local_extends(a int);
+SELECT sum(extend) AS io_sum_local_extends_before FROM pg_stat_io WHERE io_path = 'Local' \gset
+INSERT INTO test_io_local_extends VALUES(1);
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush 
+--------------------------
+ 
+(1 row)
+
+SELECT sum(extend) AS io_sum_local_extends_after FROM pg_stat_io WHERE io_path = 'Local' \gset
+SELECT :io_sum_local_extends_after > :io_sum_local_extends_before;
+ ?column? 
+----------
+ t
+(1 row)
+
+-- Test that, when using a Strategy, reusing buffers from the Strategy ring
+-- count as "Strategy" allocs.
+CREATE TABLE test_io_strategy_stats(a INT, b INT);
+ALTER TABLE test_io_strategy_stats SET (autovacuum_enabled = 'false');
+INSERT INTO test_io_strategy_stats SELECT i, i from generate_series(1,8000)i;
+-- Ensure that the next VACUUM will need to perform IO
+VACUUM (FULL) test_io_strategy_stats;
+SELECT sum(alloc) AS io_sum_strategy_allocs_before FROM pg_stat_io WHERE io_path = 'Strategy' \gset
+VACUUM (PARALLEL 0) test_io_strategy_stats;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush 
+--------------------------
+ 
+(1 row)
+
+SELECT sum(alloc) AS io_sum_strategy_allocs_after FROM pg_stat_io WHERE io_path = 'Strategy' \gset
+SELECT :io_sum_strategy_allocs_after > :io_sum_strategy_allocs_before;
+ ?column? 
+----------
+ t
+(1 row)
+
+DROP TABLE test_io_strategy_stats;
 -- End of Stats Test
diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql
index 3f3cf8fb56..fbd3977605 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -285,4 +285,38 @@ SELECT pg_stat_get_live_tuples(:drop_stats_test_subxact_oid);
 
 DROP TABLE trunc_stats_test, trunc_stats_test1, trunc_stats_test2, trunc_stats_test3, trunc_stats_test4;
 DROP TABLE prevstats;
+
+-- Test that writes to Shared Buffers are tracked in pg_stat_io
+SELECT sum(write) AS io_sum_shared_writes_before FROM pg_stat_io WHERE io_path = 'Shared' \gset
+CREATE TABLE test_io_shared_writes(a int);
+INSERT INTO test_io_shared_writes SELECT i FROM generate_series(1,100)i;
+CHECKPOINT;
+SELECT pg_stat_force_next_flush();
+SELECT sum(write) AS io_sum_shared_writes_after FROM pg_stat_io WHERE io_path = 'Shared' \gset
+SELECT :io_sum_shared_writes_after > :io_sum_shared_writes_before;
+DROP TABLE test_io_shared_writes;
+
+-- Test that extends of temporary tables are tracked in pg_stat_io
+CREATE TEMPORARY TABLE test_io_local_extends(a int);
+SELECT sum(extend) AS io_sum_local_extends_before FROM pg_stat_io WHERE io_path = 'Local' \gset
+INSERT INTO test_io_local_extends VALUES(1);
+SELECT pg_stat_force_next_flush();
+SELECT sum(extend) AS io_sum_local_extends_after FROM pg_stat_io WHERE io_path = 'Local' \gset
+SELECT :io_sum_local_extends_after > :io_sum_local_extends_before;
+
+-- Test that, when using a Strategy, reusing buffers from the Strategy ring
+-- count as "Strategy" allocs.
+CREATE TABLE test_io_strategy_stats(a INT, b INT);
+ALTER TABLE test_io_strategy_stats SET (autovacuum_enabled = 'false');
+INSERT INTO test_io_strategy_stats SELECT i, i from generate_series(1,8000)i;
+-- Ensure that the next VACUUM will need to perform IO
+VACUUM (FULL) test_io_strategy_stats;
+SELECT sum(alloc) AS io_sum_strategy_allocs_before FROM pg_stat_io WHERE io_path = 'Strategy' \gset
+VACUUM (PARALLEL 0) test_io_strategy_stats;
+SELECT pg_stat_force_next_flush();
+SELECT sum(alloc) AS io_sum_strategy_allocs_after FROM pg_stat_io WHERE io_path = 'Strategy' \gset
+SELECT :io_sum_strategy_allocs_after > :io_sum_strategy_allocs_before;
+DROP TABLE test_io_strategy_stats;
+
+
 -- End of Stats Test
-- 
2.34.1

---

From: Kyotaro Horiguchi <[email-masked]>
Date: Tue, 12 Jul 2022 17:06:21 +0900 (JST)

At Mon, 11 Jul 2022 22:22:28 -0400, Melanie Plageman <[email-masked]> wrote in 
> pgstat_report_wal() since it is immediately before a proc_exit()

Right. walwriter does that without needing the explicit call.

> I should wait until GetLocalBufferStorage() to increment the counter.

Depends on what "allocate" means. Different from shared buffers, local
buffers are taken from OS then allocated to page.  OS-allcoated pages
are restricted by num_temp_buffers so I think what we're interested in
is the count incremented by LocalBuferAlloc(). (And it is the parallel
of alloc for shared-buffers)

> I'm not sure how we would fix this.

I think there scarcely happens flush for strategy-loaded buffers.  If
that is sensible, IOOP_FSYNC would not make much sense for
IOPATH_STRATEGY.

> bootstrap and single user mode IO stats.

Regarding to stats, I don't think separating them makes much sense.

> different for bootstrap.)

pgstat_report_stat(true) is supposed to be called as needed via
before_shmem_hook so I think that's the right thing.

I think that the function comment also should mention this.

> PgStatShared_BackendIOPathOps so that reset_all and snapshot could be

Looks fine about the lock separation.
By the way, in the following line:

backend_type_get_idx(x) is actually (x - 1) plus assertion on the
value range. And the only use-case is here. There's an reverse
function and also used only at one place.

In this usage GetBackendTypeDesc() gracefully treats out-of-domain
values but idx_get_backend_type keenly kills the process for the
same. This is inconsistent.

My humbel opinion on this is we don't define the two functions and
replace the calls to them with (x +/- 1).  Addition to that, I think
we should not abort() by invalid backend types.  In that sense, I
wonder if we could use B_INVALIDth element for this purpose.

> locks in PgStat_IOPathOps for flush.

Looks fine, but I think pgstat_flush_io_ops() need more comments like
other pgstat_flush_* functions.

I'm not sure we need a separate reset timestamp for each backend type
but SLRU counter does the same thing..

> > This shouldn't be needed - the flush function above can be used.

The commit message of 0002 contains that name:p

I think we usually spell it as ("unrecognized IOPath value: %d", io_path).

> > interesting in the context of the kind of IO too.

A bit different thing, but I felt a little uneasy about some uses of
"pgstat_io_ops". IOOp looks like a neighbouring word of IOPath. On the
other hand, actually iopath is used as an attribute of io_ops in many
places.  Couldn't we be more consistent about the relationship between
the names?

IOOp   -> PgStat_IOOpType
IOPath -> PgStat_IOPath
PgStat_IOOpCOonters -> PgStat_IOCounters
PgStat_IOPathOps    -> PgStat_IO
pgstat_count_io_op  -> pgstat_count_io
...

(Better wordings are welcome.)

> I didn't see another similar example limiting access.

(The doc told me that) pg_buffercache view is restricted to
pg_monitor. But other activity-stats(aka stats collector:)-related
pg_stat_* views are not restricted to pg_monitor.

doc> pg_monitor	Read/execute various monitoring views and functions. 

Hmm....

> I'm not sure how to cause a strategy "extend" for testing.

I'm not sure what you are expecting, but for example, "create table t
as select generate_series(0, 99999)" increments Strategy-extend by
about 400.  (I'm surprised that autovac worker-shared-extend has
non-zero number)

> a buffer, then resume vacuuming.

===

If I'm not missing something, in BufferAlloc, when strategy is not
used and the victim is dirty, iopath is determined based on the
uninitialized from_ring.  It seems to me from_ring is equivalent to
strategy_current_was_in_ring.  And if StrategyGetBuffer has set
from_ring to false, StratetgyRejectBuffer may set it to true, which is
is wrong. The logic around there seems to need a rethink.

What can we read from the values separated to Shared and Strategy?

regards.

---

From: Melanie Plageman <[email-masked]>
Date: Tue, 12 Jul 2022 12:19:06 -0400

Thanks for the review!

> Right. walwriter does that without needing the explicit call.

I have deleted it.

> of alloc for shared-buffers)

I've left it in LocalBufferAlloc().

> that is sensible, IOOP_FSYNC would not make much sense for

Why would it be less likely for a backend to do its own fsync when
flushing a dirty strategy buffer than a regular dirty shared buffer?

> I think that the function comment also should mention this.

I've added comments at the top of all these functions.

> Looks fine about the lock separation.

Actually, I think it is not safe to use both of these locks. So for
picking one method, it is probably better to go with the locks in
PgStat_IOPathOps, it will be more efficient for flush (and not for
fetching and resetting), so that is probably the way to go, right?

>  &pgStatLocal.shmem->io_ops.stats[backend_type_get_idx(MyBackendType)];
>
>  CStringGetTextDatum(GetBackendTypeDesc(idx_get_backend_type(i)));
>

I think that GetBackendTypeDesc() should probably also error out for an
unknown value.

I would be open to not using the helper functions. I thought it would be
less error-prone, but since it is limited to the code in
pgstat_io_ops.c, it is probably okay. Let me think a bit more.

Could you explain more about what you mean about using B_INVALID
BackendType?

> but SLRU counter does the same thing..

Yes, I think for SLRU stats it is because you can reset individual SLRU
stats. Also there is no wrapper data structure to put it in. I could
keep it in PgStatShared_BackendIOPathOps since you have to reset all IO
operation stats at once, but I am thinking of getting rid of
PgStatShared_BackendIOPathOps since it is not needed if I only keep the
locks in PgStat_IOPathOps and make the global shared value an array of
PgStat_IOPathOps.

> The commit message of 0002 contains that name:p

Thanks! Fixed.

> I think we usually spell it as ("unrecognized IOPath value: %d", io_path).

I have changed to this.

> (Better wordings are welcome.)

Let me think about naming and make changes in the next version.

> What can we read from the values separated to Shared and Strategy?

I have changed this local variable to only be used for communicating if
the buffer which was not rejected by StrategyRejectBuffer() was from the
ring or not for the purposes of counting strategy writes. I could add an
accessor for this member (strategy->current_was_in_ring) if that makes
more sense? For strategy allocs, I just use
strategy->current_was_in_ring inside of StrategyGetBuffer() since this
has access to that member of the struct.

Currently, strategy allocs count only reuses of a strategy buffer (not
initial shared buffers which are added to the ring).
strategy writes count only the writing out of dirty buffers which are
already in the ring and are being reused.

Alternatively, we could also count as strategy allocs all those buffers
which are added to the ring and count as strategy writes  all those
shared buffers which are dirty when initially added to the ring.

- Melanie
From fdf6486b6a1546bb393aae7aa9e97604abc31d86 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Tue, 28 Jun 2022 11:33:04 -0400
Subject: [PATCH v24 1/3] Add BackendType for standalone backends

 
 
 	return backendDesc;

From 546b9a06e33e1bf6ab7c8de2d5db93e77f77c39c Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Mon, 4 Jul 2022 15:44:17 -0400
Subject: [PATCH v24 3/3] Add system view tracking IO ops per backend type

Reviewed-by: Justin Pryzby <[email-masked]>, Kyotaro Horiguchi <[email-masked]>

+     <row>

+/*

 

From 16016c4d1989ca154b4894912281d290e093639e Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Wed, 29 Jun 2022 18:37:42 -0400
Subject: [PATCH v24 2/3] Track IO operation statistics

Some BackendTypes will not execute pgstat_report_stat() and thus must
explicitly call pgstat_flush_io_ops() in order to flush their backend
local IO operation statistics to shared memory.

Author: Melanie Plageman <[email-masked]>
Reviewed-by: Justin Pryzby <[email-masked]>, Kyotaro Horiguchi <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/20200124195226.lth52iydq2n2uilq%40alap3.anarazel.de
---
 src/backend/bootstrap/bootstrap.c             |   1 +
 src/backend/postmaster/checkpointer.c         |   1 +
 src/backend/postmaster/walwriter.c            |  11 --
 src/backend/storage/buffer/bufmgr.c           |  51 +++++-
 src/backend/storage/buffer/freelist.c         |  48 ++++-
 src/backend/storage/buffer/localbuf.c         |   5 +
 src/backend/storage/sync/sync.c               |   2 +
 src/backend/utils/activity/Makefile           |   1 +
 src/backend/utils/activity/pgstat.c           |  24 +++
 src/backend/utils/activity/pgstat_bgwriter.c  |   7 +-
 .../utils/activity/pgstat_checkpointer.c      |   7 +-
 src/backend/utils/activity/pgstat_database.c  |   8 +-
 src/backend/utils/activity/pgstat_io_ops.c    | 168 ++++++++++++++++++
 src/backend/utils/activity/pgstat_relation.c  |  14 +-
 src/backend/utils/activity/pgstat_wal.c       |   4 +-
 src/backend/utils/adt/pgstatfuncs.c           |   4 +-
 src/include/miscadmin.h                       |   2 +
 src/include/pgstat.h                          |  53 ++++++
 src/include/storage/buf_internals.h           |   2 +-
 src/include/utils/backend_status.h            |  34 ++++
 src/include/utils/pgstat_internal.h           |  17 ++
 21 files changed, 432 insertions(+), 32 deletions(-)
 create mode 100644 src/backend/utils/activity/pgstat_io_ops.c

diff --git a/src/backend/postmaster/walwriter.c b/src/backend/postmaster/walwriter.c
index e926f8c27c..beb46dcb55 100644
--- a/src/backend/postmaster/walwriter.c
+++ b/src/backend/postmaster/walwriter.c
@@ -293,18 +293,7 @@ HandleWalWriterInterrupts(void)
 	}
 
 	if (ShutdownRequestPending)
-	{
-		/*
-		 * Force reporting remaining WAL statistics at process exit.
-		 *
-		 * Since pgstat_report_wal is invoked with 'force' is false in main
-		 * loop to avoid overloading the cumulative stats system, there may
-		 * exist unreported stats counters for the WAL writer.
-		 */
-		pgstat_report_wal(true);
-
 		proc_exit(0);
-	}
 
 	/* Perform logging of memory contexts of this process */
 	if (LogMemoryContextPending)
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index c7d7abcd73..846297f273 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -482,7 +482,7 @@ static BufferDesc *BufferAlloc(SMgrRelation smgr,
 							   BlockNumber blockNum,
 							   BufferAccessStrategy strategy,
 							   bool *foundPtr);
-static void FlushBuffer(BufferDesc *buf, SMgrRelation reln);
+static void FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOPath iopath);
 static void FindAndDropRelationBuffers(RelFileLocator rlocator,
 									   ForkNumber forkNum,
 									   BlockNumber nForkBlock,
@@ -980,6 +980,16 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 
 	if (isExtend)
 	{
+		IOPath io_path;
+
+		if (isLocalBuf)
+		bool write_from_ring = false;
 
+						StrategyRejectBuffer(strategy, buf, &write_from_ring))
 
+				/*
+				 * When a strategy is in use, if the target dirty buffer is an existing
+				 * strategy buffer being reused, count this as a strategy write for the
+				 * purposes of IO Operations statistics tracking.
+				 *
+				 * All dirty shared buffers upon first being added to the ring will be
+				 * counted as shared buffer writes.
+				 *
+				 * When a strategy is not in use, the write can only be a only be a
+				 * "regular" write of a dirty shared buffer.
+				 */
+
+				iopath = write_from_ring ? IOPATH_STRATEGY : IOPATH_SHARED;
+
 				/* OK, do the I/O */
 				TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum,
 														  smgr->smgr_rlocator.locator.spcOid,
 														  smgr->smgr_rlocator.locator.dbOid,
 														  smgr->smgr_rlocator.locator.relNumber);

 

 
 
@@ -3539,6 +3570,8 @@ FlushRelationBuffers(Relation rel)
 						  localpage,
 						  false);

@@ -3574,7 +3607,7 @@ FlushRelationBuffers(Relation rel)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, RelationGetSmgr(rel));
+			FlushBuffer(bufHdr, RelationGetSmgr(rel), IOPATH_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr, true);
 		}
@@ -3669,7 +3702,7 @@ FlushRelationsAllBuffers(SMgrRelation *smgrs, int nrels)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, srelent->srel);
+			FlushBuffer(bufHdr, srelent->srel, IOPATH_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr, true);
 		}
@@ -3877,7 +3910,7 @@ FlushDatabaseBuffers(Oid dbid)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, NULL);
+			FlushBuffer(bufHdr, NULL, IOPATH_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr, true);
 		}
@@ -3904,7 +3937,7 @@ FlushOneBuffer(Buffer buffer)

 
 /*
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index 990e081aae..8ab88341ce 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -15,6 +15,7 @@
  */
 #include "postgres.h"
 
+#include "pgstat.h"
@@ -212,8 +213,18 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
+		if (strategy->current_was_in_ring)
 
 	/*
@@ -247,6 +258,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
 
 	/*
@@ -682,16 +694,37 @@ AddBufferToRing(BufferAccessStrategy strategy, BufferDesc *buf)
  * if this buffer should be written and re-used.
  */
 bool
-StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf)
+StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf, bool *write_from_ring)
 {
-	/* We only do this in bulkread mode */
+
+	/*
+	 * We only reject reusing and writing out the strategy buffer this in
+	 * bulkread mode.
+	 */
 	if (strategy->btype != BAS_BULKREAD)
+	{
+		/*
+		 * If the buffer was from the ring and we are not rejecting it, consider it
+		 * a write of a strategy buffer.
+		 */
+		if (strategy->current_was_in_ring)
+			*write_from_ring = true;
 		return false;
+	}
 
-	/* Don't muck with behavior of normal buffer-replacement strategy */
+	/*
+	 * Don't muck with behavior of normal buffer-replacement strategy.
+	 * Though we are not rejecting this buffer, write_from_ring will remain false
+	 * because shared buffers that are added to the ring, either initially or as
+	 * part of an expansion, are not considered strategy writes for the purposes
+	 * of IO Operation statistics.
+	 */
 	if (!strategy->current_was_in_ring ||
 		strategy->buffers[strategy->current] != BufferDescriptorGetBuffer(buf))
+	{
+		*write_from_ring = false;
 		return false;
+	}
 
 	/*
 	 * Remove the dirty buffer from the ring; necessary to prevent infinite
@@ -699,5 +732,12 @@ StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf)
 	 */
 	strategy->buffers[strategy->current] = InvalidBuffer;
 
+	/*
+	 * Though caller should not use this flag since the buffer is being rejected
+	 * (and it should have been initialized to false anyway), set it here for
+	 * clarity.
+	 */
+	*write_from_ring = false;
+
 	return true;
 }
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index 9c038851d7..58738d6684 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -15,6 +15,7 @@
  */
 #include "postgres.h"
 
+#include "pgstat.h"

 

 

+	/*
 
+	/*
+	 * Read IO Operations stats struct
+	 */
+	if (!read_chunk_s(fpin, &shmem->io_ops.stats))
+		goto error;
+
 	/*
 	 * Read SLRU stats struct
 	 */
diff --git a/src/backend/utils/activity/pgstat_bgwriter.c b/src/backend/utils/activity/pgstat_bgwriter.c
index fbb1edc527..3d7f90a1b7 100644
--- a/src/backend/utils/activity/pgstat_bgwriter.c
+++ b/src/backend/utils/activity/pgstat_bgwriter.c
@@ -24,7 +24,7 @@ PgStat_BgWriterStats PendingBgWriterStats = {0};

 /*
- * Report bgwriter statistics
+ * Report bgwriter and IO Operation statistics
  */
 void
 pgstat_report_bgwriter(void)
@@ -56,6 +56,11 @@ pgstat_report_bgwriter(void)
 	 * Clear out the statistics buffer, so it can be re-used.
 	 */
 	MemSet(&PendingBgWriterStats, 0, sizeof(PendingBgWriterStats));
+
+	/*
+	 * Report IO Operations statistics
+	 */
+	pgstat_flush_io_ops(false);
 }
 
 /*
diff --git a/src/backend/utils/activity/pgstat_checkpointer.c b/src/backend/utils/activity/pgstat_checkpointer.c
index af8d513e7b..cfcf127210 100644
--- a/src/backend/utils/activity/pgstat_checkpointer.c
+++ b/src/backend/utils/activity/pgstat_checkpointer.c
@@ -24,7 +24,7 @@ PgStat_CheckpointerStats PendingCheckpointerStats = {0};

 /*
- * Report checkpointer statistics
+ * Report checkpointer and IO Operation statistics
  */
 void
 pgstat_report_checkpointer(void)
@@ -62,6 +62,11 @@ pgstat_report_checkpointer(void)
 	 * Clear out the statistics buffer, so it can be re-used.
 	 */
 	MemSet(&PendingCheckpointerStats, 0, sizeof(PendingCheckpointerStats));
+
+	/*
+	 * Report IO Operation statistics
+	 */
+	pgstat_flush_io_ops(false);
 }
 
 /*
diff --git a/src/backend/utils/activity/pgstat_database.c b/src/backend/utils/activity/pgstat_database.c
index d9275611f0..d3963f59d0 100644
--- a/src/backend/utils/activity/pgstat_database.c
+++ b/src/backend/utils/activity/pgstat_database.c
@@ -47,7 +47,8 @@ pgstat_drop_database(Oid databaseid)
 }
 
 /*
- * Called from autovacuum.c to report startup of an autovacuum process.
+ * Called from autovacuum.c to report startup of an autovacuum process and
+ * flush IO Operation statistics.
  * We are called before InitPostgres is done, so can't rely on MyDatabaseId;
  * the db OID must be passed in, instead.
  */
@@ -72,6 +73,11 @@ pgstat_report_autovac(Oid dboid)
 	dbentry->stats.last_autovac_time = GetCurrentTimestamp();
 
 	pgstat_unlock_entry(entry_ref);
+
+	/*
+	 * Report IO Operation statistics
+	 */
+	pgstat_flush_io_ops(false);
 }
 
 /*
index 0000000000..f037d2c7c5
+	memset(&pending_IOOpStats, 0, sizeof(pending_IOOpStats));
+	elog(ERROR, "unrecognized IOPath value: %d", io_path);
+	elog(ERROR, "unrecognized IOOp value: %d", io_op);
index a846d9ffb6..0f8048eaa9 100644
@@ -205,7 +205,7 @@ pgstat_drop_relation(Relation rel)
 }
 
 /*
- * Report that the table was just vacuumed.
+ * Report that the table was just vacuumed and flush IO Operation statistics.
  */
 void
 pgstat_report_vacuum(Oid tableoid, bool shared,
@@ -257,10 +257,15 @@ pgstat_report_vacuum(Oid tableoid, bool shared,
 	}
 
 	pgstat_unlock_entry(entry_ref);
+
+	/*
+	 * Report IO Operations statistics
+	 */
+	pgstat_flush_io_ops(false);
 }
 
 /*
- * Report that the table was just analyzed.
+ * Report that the table was just analyzed and flush IO Operation statistics.
  *
  * Caller must provide new live- and dead-tuples estimates, as well as a
  * flag indicating whether to reset the changes_since_analyze counter.
@@ -340,6 +345,11 @@ pgstat_report_analyze(Relation rel,
 	}
 
 	pgstat_unlock_entry(entry_ref);
+
+	/*
+	 * Report IO Operations statistics
+	 */
+	pgstat_flush_io_ops(false);
 }
 
 /*
diff --git a/src/backend/utils/activity/pgstat_wal.c b/src/backend/utils/activity/pgstat_wal.c
index 5a878bd115..9cac407b42 100644
--- a/src/backend/utils/activity/pgstat_wal.c
+++ b/src/backend/utils/activity/pgstat_wal.c
@@ -34,7 +34,7 @@ static WalUsage prevWalUsage;
 
 /*
  * Calculate how much WAL usage counters have increased and update
- * shared statistics.
+ * shared WAL and IO Operation statistics.
  *
  * Must be called by processes that generate WAL, that do not call
  * pgstat_report_stat(), like walwriter.
@@ -43,6 +43,8 @@ void
 pgstat_report_wal(bool force)
 {
 	pgstat_flush_wal(force);
+
+	pgstat_flush_io_ops(force);
 }
 
 /*
 
 	PG_RETURN_VOID();
 }

 
+/*

+/*
index 69e45900ba..b69c5f7e3c 100644
@@ -313,7 +313,7 @@ extern BufferDesc *StrategyGetBuffer(BufferAccessStrategy strategy,
 									 uint32 *buf_state);
+								 BufferDesc *buf, bool *write_from_ring);

+/* Utility functions */
+
+/*
 
 /* ----------

 
 
 	PgStat_WalStats wal;

+/*

---

From: Andres Freund <[email-masked]>
Date: Tue, 12 Jul 2022 10:01:31 -0700

Hi,

> flushing a dirty strategy buffer than a regular dirty shared buffer?

We really just don't expect a backend to do many segment fsyncs at
all. Otherwise there's something wrong with the forwarding mechanism.

It'd be different if we tracked WAL fsyncs more granularly - which would be
quite interesting - but that's something for another day^Wpatch.

> fetching and resetting), so that is probably the way to go, right?

I think it's good to just use one kind of lock, and efficiency of snapshotting
/ resetting is nearly irrelevant. But I don't see why it's not safe to use
both kinds of locks?

> locks in PgStat_IOPathOps and make the global shared value an array of

I'm strongly against introducing super granular reset timestamps. I think that
was a mistake for SLRU stats, but we can't fix that as easily.

> already in the ring and are being reused.

That seems right to me.

> shared buffers which are dirty when initially added to the ring.

I don't think that'd provide valuable information. The whole reason that
strategy writes are interesting is that they can lead to writing out data a
lot sooner than they would be written out without a strategy being used.

> an IOPath IOPATH_SHARED by BackendType "checkpointer".

I'm still not 100% happy with IOPath - seems a bit too easy to confuse with
the file path. What about 'origin'?

> (direct, local, shared, or strategy) through a call to

It seems we should track reads too - it's quite interesting to know whether
reads happened because of a strategy, for example. You do reference reads in a
later part of the commit message even :)

> example, the archiver or syslogger is not counted in these statistics.

We could extend this at a later stage, if we really want to. But I'm not sure
it's interesting or fully possible. E.g. the archiver's write are largely not
done by the archiver itself, but by a command (or module these days) it shells
out to.

> users of these functions.

Hm. Perhaps we should defer introducing IOPATH_DIRECT for now then?

> all other backends, exited and live.

Perhaps mention here that this later could be extended to make per-connection
stats visible?

> local IO operation statistics to shared memory.

Maybe add "flush ... during ongoing operation" or such? Because they'd all
flush at commit, IIRC.

Hm?

>  	if (LogMemoryContextPending)

Let's do this in a separate commit and get it out of the way...

> +			*write_from_ring = true;

Hm. This is set even if the buffer wasn't dirty? I guess we don't expect
StrategyRejectBuffer() to be called for clean buffers...

> +	pgstat_flush_io_ops(false);

Hm. I suspect this will always be zero - at this point we haven't connected to
a database, so there really can't have been much, if any, IO. I think I
suggested doing something here, but on a second look it really doesn't make
much sense.

Note that that's different from doing something in
pgstat_report_(vacuum|analyze) - clearly we've done something at that point.

> +	pgstat_flush_io_ops(false);

Think it'd be good to amend these comments to say that otherwise stats would
only get flushed after a multi-relatio autovacuum cycle is done / a
VACUUM/ANALYZE command processed all tables.  Perhaps add the comment to one
of the two functions, and just reference it in the other place?

> +	return backend_type - 1;

In function definitions (vs declarations) we put the 'static inline int' in a
separate line from the rest of the function signature.

> +	 * If the array includes a spot for B_INVALID BackendType this function is

The comments around this seem a bit over the top, but I also don't mind them
much.

> buffers, local buffers, unbuffered IO) by each type of backend.

Annoying question: pg_stat_io vs pg_statio? I'd not think of suggesting the
latter, except that we already have a bunch of views with that prefix.

> checkpointer will be 0 for all IOOps.

What do you think about returning NULL for the values that we except to never
be non-zero? Perhaps with an assert against non-zero values? Seems like it
might be helpful for understanding the view.

> +	IO_COLUMN_BACKEND_TYPE,

We typedef pretty much every enum so the enum can be referenced without the
'enum' prefix. I'd do that here, even if we don't need it.

Greetings,

Andres Freund

---

From: Andres Freund <[email-masked]>
Date: Tue, 12 Jul 2022 10:18:10 -0700

Hi,

> a chance to write it out.

I guess temp file writes would be reliably done by one backend... Don't have a
good idea otherwise.

> background worker do a particular kind of IO in a test scenario.

I think it's perfectly fine to not test that - for it to be broken we'd have
to somehow screw up setting the backend type. Everything else is the same as
other types of backends anyway.

If you *do* want to test it, you probably could use
SET parallel_leader_participation = false;
SET force_parallel_mode = 'regress';
SELECT something_triggering_io;

> I'm not sure how to cause a strategy "extend" for testing.

COPY into a table should work. But might be unattractive due to the size of of
the COPY ringbuffer.

> a buffer, then resume vacuuming.

Yea, that's probably too hard to make reliable to be worth it.

Greetings,

Andres Freund

---

From: Kyotaro Horiguchi <[email-masked]>
Date: Wed, 13 Jul 2022 11:00:07 +0900 (JST)

At Tue, 12 Jul 2022 12:19:06 -0400, Melanie Plageman <[email-masked]> wrote in 
> Could you explain more about what you mean about using B_INVALID

I imagined to use B_INVALID as a kind of "default" partition, which
accepts all unknown backend types. We can just ignore that values but
then we lose the clue for malfunction of stats machinery. I thought
that that backend-type as the sentinel for malfunctions.  Thus we can
emit logs instead.

I feel that the stats machinery shouldn't stop the server as possible,
or I think it is overreaction to abort for invalid values that can be
easily coped with.

regards.

---

From: Andres Freund <[email-masked]>
Date: Tue, 12 Jul 2022 19:18:22 -0700

Hi,

On 2022-07-13 11:00:07 +0900, Kyotaro Horiguchi wrote:
> accepts all unknown backend types.

There shouldn't be any unknown backend types. Something has gone wrong if we
get far without a backend type set.

> We can just ignore that values but then we lose the clue for malfunction of
> stats machinery. I thought that that backend-type as the sentinel for
> malfunctions.  Thus we can emit logs instead.
> 

I strongly disagree. That just ends up with hard to find bugs.

Greetings,

Andres Freund

---

From: Kyotaro Horiguchi <[email-masked]>
Date: Wed, 13 Jul 2022 11:41:40 +0900 (JST)

At Tue, 12 Jul 2022 19:18:22 -0700, Andres Freund <[email-masked]> wrote in 
> I strongly disagree. That just ends up with hard to find bugs.

I was not sure about the policy on that since, as Melanie (and I)
mentioned, GetBackendTypeDesc() is gracefully treating invalid values.

Since both of you are agreeing on this point, I'm fine with
Assert()ing assuming that GetBackendTypeDesc() (or other places
backend-type is handled) is modified to behave the same way.

regards.

---

From: Melanie Plageman <[email-masked]>
Date: Wed, 13 Jul 2022 13:14:52 -0400

Attached patch set is substantially different enough from previous
versions that I kept it as a new patch set.
Note that local buffer allocations are now correctly tracked.

> all. Otherwise there's something wrong with the forwarding mechanism.

When a dirty strategy buffer is written out, if pendingOps sync queue is
full and the backend has to fsync the segment itself instead of relying
on the checkpointer, this will show in the statistics as an IOOP_FSYNC
for IOPATH_SHARED not IOPATH_STRATEGY.
IOPATH_STRATEGY + IOOP_FSYNC will always be 0 for all BackendTypes.
Does this seem right?

> quite interesting - but that's something for another day^Wpatch.
I do have a question about this.
So, if we were to start tracking WAL IO would it fit within this
paradigm to have a new IOPATH_WAL for WAL or would it add a separate
dimension?

I was thinking that we might want to consider calling this view
pg_stat_io_data because we might want to have a separate view,
pg_stat_io_wal and then, maybe eventually, convert pg_stat_slru to
pg_stat_io_slru (or a subset of what is in pg_stat_slru).
And maybe then later add pg_stat_io_[archiver/other]

Is pg_stat_io_data a good name that gives us flexibility to
introduce views which expose per-backend IO operation stats (maybe that
goes in pg_stat_activity, though [or maybe not because it wouldn't
include exited backends?]) and per query IO operation stats?

I would like to add roughly the same additional columns to all of
these during AIO development (basically the columns from iostat):
- average block size (will usually be 8kB for pg_stat_io_data but won't
necessarily for the others)
- IOPS/BW
- avg read/write wait time
- demand rate/completion rate
- merges
- maybe queue depth

And I would like to be able to see all of these per query, per backend,
per relation, per BackendType, per IOPath, per SLRU type, etc.

Basically, what I'm asking is
1) what can we name the view to enable these future stats to exist with
the least confusing/wordy view names?
2) will the current view layout and column titles work with minimal
changes for future stats extensions like what I mention above?

> / resetting is nearly irrelevant. But I don't see why it's not safe to use
The way I implemented it was not safe because I didn't use both locks
when resetting the stats.

In this new version of the patch, I've done the following: In shared
memory I've put the lock in PgStatShared_IOPathOps -- the data structure
which contains an array of PgStat_IOOpCounters for all IOOp types for
all IOPaths. Thus, different BackendType + IOPath combinations can be
updated concurrently without contending for the same lock.

To make this work, I made two versions of the PgStat_IOPathOps -- one
that has the lock, PgStatShared_IOPathOps, and one without,
PgStat_IOPathOps, so that I can persist it to the stats file without
writing and reading the LWLock and can have a local and snapshot version
of the data structure without the lock.

This also necessitated two versions of the data structure wrapping
PgStat_IOPathOps, PgStat_BackendIOPathOps, which contains an array with
a PgStat_IOPathOps for each BackendType, and
PgStatShared_BackendIOPathOps, containing an array of
PgStatShared_IOPathOps.

> was a mistake for SLRU stats, but we can't fix that as easily.
Since all stats in pg_stat_io must be reset at the same time, I've put
the reset timestamp can in the PgStat[Shared]_BackendIOPathOps and
removed it from each PgStat[Shared]_IOPathOps.

> lot sooner than they would be written out without a strategy being used.
Then I agree that strategy writes should only count strategy buffers
that are written out in order to reuse the buffer (which is in lieu of
getting a new, potentially clean, shared buffer). This patch implements
that behavior.

However, for strategy allocs, it seems like we would want to count all
demand for buffers as part of a BufferAccessStrategy. So, that would
include allocating buffers to initially fill the ring, allocations of
new shared buffers after the ring was already full that are added to the
ring because all existing buffers in the ring are pinned, and buffers
already in the ring which are being reused.

This version of the patch only counts the third scenario as a strategy
allocation, but I think it would make more sense to count all three as
strategy allocs.

The downside of this behavior is that strategy allocs count different
scenarios than strategy writes, reads, and extends. But, I think that
this is okay.

I'll clarify it in the docs once there is a decision.

Also, note that, as stated above, there will never be any strategy
fsyncs (that is, IOPATH_STRATEGY + IOOP_FSYNC will always be 0) because
the code path starting with register_dirty_segment() which ends with a
regular backend doing its own fsync when pendingOps is full does not
know what the current IOPATH is and checkpointer does not use a
BufferAccessStrategy.

> the file path. What about 'origin'?
Enough has changed in this version of the patch that I decided to defer
renaming until some of the other issues are resolved.

> later part of the commit message even :)

I've added reads to what is counted.

> done by the archiver itself, but by a command (or module these days) it

I've added note of this to some of the comments and the commit message.
I also omit rows for these BackendTypes from the view. See my later
comment in this email for more detail on that.

> Hm. Perhaps we should defer introducing IOPATH_DIRECT for now then?
It's gone.

> Perhaps mention here that this later could be extended to make
Mentioned.

> Maybe add "flush ... during ongoing operation" or such? Because they'd all
Added.

> >  #include "storage/condition_variable.h"

Removed

> Let's do this in a separate commit and get it out of the way...
I've put it in a separate commit.

> StrategyRejectBuffer() to be called for clean buffers...
Yes, we do not expect it to be called for clean buffers.
I've added a comment about this assumption.

>
> >  /*
> > diff --git a/src/backend/utils/activity/pgstat_database.c
> b/src/backend/utils/activity/pgstat_database.c

I've removed this.

Done

> > @@ -306,6 +306,40 @@ extern const char
> *pgstat_get_crashed_backend_activity(int pid, char *buffer,
> >
>                                 int buflen);

Fixed.

> The comments around this seem a bit over the top, but I also don't mind

Feel free to change them to something shorter. I couldn't think of
something I liked.

> latter, except that we already have a bunch of views with that prefix.
I have thoughts on this but thought it best deferred until after the _data
decision.

> might be helpful for understanding the view.

Yes, I like this idea.

Beyond just setting individual cells to NULL, if an entire row would be
NULL, I have now dropped it from the view.

So far, I have omitted from the view all rows for BackendTypes
B_ARCHIVER, B_LOGGER, and B_STARTUP.

Should I also omit rows for B_WAL_RECEIVER and B_WAL_WRITER for now?

I have also omitted rows for IOPATH_STRATEGY for all BackendTypes
*except* B_AUTOVAC_WORKER, B_BACKEND, B_STANDALONE_BACKEND, and
B_BG_WORKER.

Do these seem correct?

I think there are some BackendTypes which will never do IO Operations on
IOPATH_LOCAL but I am not sure which. Do you know which?

As for individual cells which should be NULL, so far what I have is:
- IOPATH_LOCAL + IOOP_FSYNC
I am sure there are others as well. Can you think of any?

> 'enum' prefix. I'd do that here, even if we don't need it.
So, I left it anonymous because I didn't want it being used as a type
or referenced anywhere else.

I am interested to hear more about your SQL enums idea from upthread.

- Melanie
From 5d3e3e702cd95e52cb015a23c0bbeccc5debd46d Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Tue, 28 Jun 2022 11:33:04 -0400
Subject: [PATCH v25 1/4] Add BackendType for standalone backends

 
 
 	return backendDesc;

From 965923536cfe72819b2877e9f1ad4a7e6373b0e8 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Tue, 12 Jul 2022 19:53:23 -0400
Subject: [PATCH v25 2/4] Remove unneeded call to pgstat_report_wal()

pgstat_report_stat() will be called before shutdown so an explicit call
to pgstat_report_wal() is wasted.
---
 src/backend/postmaster/walwriter.c | 11 -----------
 1 file changed, 11 deletions(-)

 

From 7ba696105c6a45d7b9c7c08fc178d8af4f60c910 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Wed, 29 Jun 2022 18:37:42 -0400
Subject: [PATCH v25 3/4] Track IO operation statistics

Introduce "IOOp", an IO operation done by a backend, and "IOPath", the
location or type of IO done by a backend. For example, the checkpointer
may write a shared buffer out. This would be counted as an IOOp "write"
on an IOPath IOPATH_SHARED by BackendType "checkpointer".

Each IOOp (alloc, extend, fsync, read, write) is counted per IOPath
(local, shared, or strategy) through a call to pgstat_count_io_op().

The IOPATH_STRATEGY IOPath concerns buffers
alloc'd/extended/fsync'd/read/written as part of a BufferAccessStrategy.

IOOP_ALLOC is counted for IOPATH_SHARED and IOPATH_LOCAL whenever a
buffer is acquired through [Local]BufferAlloc(). IOOP_ALLOC for
IOPATH_STRATEGY is counted whenever a buffer already in the strategy
ring is reused. And IOOP_WRITE for IOPATH_STRATEGY is counted whenever
the reused dirty buffer is written out.

Later they are flushed to shared memory and accumulated with those from
all other backends, exited and live. The accumulated stats in shared
memory could be extended in the future with per-backend stats -- useful
for per connection IO statistics and monitoring.

Some BackendTypes will not flush their pending statistics at regular
intervals and explicitly call pgstat_flush_io_ops() during the course of
normal operations to flush their backend-local IO Operation statistics
to shared memory in a timely manner.

Author: Melanie Plageman <[email-masked]>
Reviewed-by: Justin Pryzby <[email-masked]>, Kyotaro Horiguchi <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/20200124195226.lth52iydq2n2uilq%40alap3.anarazel.de
---
 src/backend/postmaster/checkpointer.c         |   1 +
 src/backend/storage/buffer/bufmgr.c           |  53 ++++-
 src/backend/storage/buffer/freelist.c         |  51 ++++-
 src/backend/storage/buffer/localbuf.c         |   6 +
 src/backend/storage/sync/sync.c               |   2 +
 src/backend/utils/activity/Makefile           |   1 +
 src/backend/utils/activity/pgstat.c           |  36 ++++
 src/backend/utils/activity/pgstat_bgwriter.c  |   7 +-
 .../utils/activity/pgstat_checkpointer.c      |   7 +-
 src/backend/utils/activity/pgstat_io_ops.c    | 192 ++++++++++++++++++
 src/backend/utils/activity/pgstat_relation.c  |  19 +-
 src/backend/utils/activity/pgstat_wal.c       |   4 +-
 src/backend/utils/adt/pgstatfuncs.c           |   4 +-
 src/include/miscadmin.h                       |   2 +
 src/include/pgstat.h                          |  58 ++++++
 src/include/storage/buf_internals.h           |   2 +-
 src/include/utils/backend_status.h            |  36 ++++
 src/include/utils/pgstat_internal.h           |  24 +++
 18 files changed, 485 insertions(+), 20 deletions(-)
 create mode 100644 src/backend/utils/activity/pgstat_io_ops.c

diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index c7d7abcd73..e872d7edc6 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -482,7 +482,7 @@ static BufferDesc *BufferAlloc(SMgrRelation smgr,
 							   BlockNumber blockNum,
 							   BufferAccessStrategy strategy,
 							   bool *foundPtr);
-static void FlushBuffer(BufferDesc *buf, SMgrRelation reln);
+static void FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOPath iopath);
 static void FindAndDropRelationBuffers(RelFileLocator rlocator,
 									   ForkNumber forkNum,
 									   BlockNumber nForkBlock,
@@ -813,6 +813,7 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	BufferDesc *bufHdr;
 	Block		bufBlock;
 	bool		found;
+	IOPath io_path;
 	bool		isExtend;
 	bool		isLocalBuf = SmgrIsTemp(smgr);
 
@@ -978,8 +979,17 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 
 	bufBlock = isLocalBuf ? LocalBufHdrGetBlock(bufHdr) : BufHdrGetBlock(bufHdr);
 
+	if (isLocalBuf)
@@ -1010,6 +1020,8 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 
 			smgrread(smgr, forkNum, blockNum, (char *) bufBlock);
 
+			pgstat_count_io_op(IOOP_READ, io_path);
+
 			if (track_io_timing)
 			{
 				INSTR_TIME_SET_CURRENT(io_time);
@@ -1180,6 +1192,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	/* Loop here in case we have to try another victim buffer */
 	for (;;)
 	{
+		bool write_from_ring = false;
 		/*
 		 * Ensure, while the spinlock's not yet held, that there's a free
 		 * refcount entry.
@@ -1227,6 +1240,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 			if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf),
 										 LW_SHARED))
 			{
+				IOPath iopath;
 				/*
 				 * If using a nondefault strategy, and writing the buffer
 				 * would require a WAL flush, let the strategy decide whether
@@ -1244,7 +1258,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 					UnlockBufHdr(buf, buf_state);
 
@@ -1253,13 +1267,27 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 					}
 				}
 
+				/*

 				ScheduleBufferTagForWriteback(&BackendWritebackContext,
@@ -2563,7 +2591,7 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
 	PinBuffer_Locked(bufHdr);
 	LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);

 
@@ -2810,9 +2838,12 @@ BufferGetTag(Buffer buffer, RelFileLocator *rlocator, ForkNumber *forknum,
  *
@@ -2892,6 +2923,8 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln)

@@ -3539,6 +3572,8 @@ FlushRelationBuffers(Relation rel)
 						  localpage,
 						  false);

@@ -3574,7 +3609,7 @@ FlushRelationBuffers(Relation rel)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, RelationGetSmgr(rel));
+			FlushBuffer(bufHdr, RelationGetSmgr(rel), IOPATH_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr, true);
 		}
@@ -3669,7 +3704,7 @@ FlushRelationsAllBuffers(SMgrRelation *smgrs, int nrels)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, srelent->srel);
+			FlushBuffer(bufHdr, srelent->srel, IOPATH_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr, true);
 		}
@@ -3877,7 +3912,7 @@ FlushDatabaseBuffers(Oid dbid)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, NULL);
+			FlushBuffer(bufHdr, NULL, IOPATH_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr, true);
 		}
@@ -3904,7 +3939,7 @@ FlushOneBuffer(Buffer buffer)

 
 /*
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index 990e081aae..29f5cbeab6 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -15,6 +15,7 @@
  */
 #include "postgres.h"
 
+#include "pgstat.h"
 #include "port/atomics.h"
 #include "storage/buf_internals.h"
 #include "storage/bufmgr.h"
@@ -212,8 +213,20 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
 	if (strategy != NULL)
 	{
 		buf = GetBufferFromRing(strategy, buf_state);
-		if (buf != NULL)
+		if (strategy->current_was_in_ring)
+		{
+			/*
+			 * When a strategy is in use, reused buffers from the strategy ring will
+			 * be counted as allocations for the purposes of IO Operation statistics
+			 * tracking.
+			 *
+			 * However, even when a strategy is in use, if a new buffer must be
+			 * allocated from shared buffers and added to the ring, this is counted
+			 * as a IOPATH_SHARED allocation.
+			 */
+			pgstat_count_io_op(IOOP_ALLOC, IOPATH_STRATEGY);
 			return buf;
+		}
 	}
 
 	/*
@@ -247,6 +260,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
 
 	/*
@@ -682,16 +696,38 @@ AddBufferToRing(BufferAccessStrategy strategy, BufferDesc *buf)
  * if this buffer should be written and re-used.
  */
 bool
-StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf)
+StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf, bool *write_from_ring)
 {
-	/* We only do this in bulkread mode */
+
+	/*
+	 * We only reject reusing and writing out the strategy buffer in bulkread
+	 * mode.
+	 */
 	if (strategy->btype != BAS_BULKREAD)
+	{
+		/*
+		 * If the buffer was from the ring and we are not rejecting it, consider it
+		 * a write of a strategy buffer. Note that this assumes that the buffer is
+		 * dirty.
+		 */
+		if (strategy->current_was_in_ring)
+			*write_from_ring = true;
 		return false;
+	}
 
-	/* Don't muck with behavior of normal buffer-replacement strategy */
+	/*
+	 * Don't muck with behavior of normal buffer-replacement strategy. Though we
+	 * are not rejecting this buffer, write_from_ring is false because shared
+	 * buffers that are added to the ring, either initially or when reuse is not
+	 * possible because all existing strategy buffers are pinned, are not
+	 * considered strategy writes for the purposes of IO Operation statistics.
+	 */
 	if (!strategy->current_was_in_ring ||
 		strategy->buffers[strategy->current] != BufferDescriptorGetBuffer(buf))
+	{
+		*write_from_ring = false;
 		return false;
+	}
 
 	/*
 	 * Remove the dirty buffer from the ring; necessary to prevent infinite
@@ -699,5 +735,12 @@ StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf)
 	 */
 	strategy->buffers[strategy->current] = InvalidBuffer;
 
+	/*
+	 * Caller should not use this flag since the buffer is being rejected (and it
+	 * should have been initialized to false anyway) and will not be written out.
+	 * Set the flag here anyway for clarity.
+	 */
+	*write_from_ring = false;
+
 	return true;
 }
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index 9c038851d7..edd3296dd7 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -15,6 +15,7 @@
  */
 #include "postgres.h"
 
+#include "pgstat.h"
@@ -123,6 +124,7 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
 
+
 	/* See if the desired buffer already exists */
 	hresult = (LocalBufferLookupEnt *)
 		hash_search(LocalBufHash, (void *) &newTag, HASH_FIND, NULL);
@@ -196,6 +198,8 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
 				LocalRefCount[b]++;
 				ResourceOwnerRememberBuffer(CurrentResourceOwner,
 											BufferDescriptorGetBuffer(bufHdr));
+
+				pgstat_count_io_op(IOOP_ALLOC, IOPATH_LOCAL);
 				break;
 			}
 		}
@@ -226,6 +230,8 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
 				  localpage,
 				  false);

index 88e5dd1b2b..3238d9ba85 100644

 

+	/*
+	 * Write IO Operations stats struct
+	 */
+	pgstat_build_snapshot_fixed(PGSTAT_KIND_IOOPS);
+	write_chunk_s(fpout, &pgStatLocal.snapshot.io_ops);
+
 	/*
 	 * Write SLRU stats struct
 	 */
@@ -1427,8 +1445,10 @@ pgstat_read_statsfile(void)
 	FILE	   *fpin;
 	int32		format_id;
 	bool		found;
+	PgStat_BackendIOPathOps io_stats;
 	const char *statfile = PGSTAT_STAT_PERMANENT_FILENAME;
 	PgStat_ShmemControl *shmem = pgStatLocal.shmem;
+	PgStatShared_BackendIOPathOps *io_stats_shmem = &shmem->io_ops;
 
 	/* shouldn't be called from postmaster */
 	Assert(IsUnderPostmaster || !IsPostmasterEnvironment);
@@ -1486,6 +1506,22 @@ pgstat_read_statsfile(void)
 	if (!read_chunk_s(fpin, &shmem->checkpointer.stats))
 		goto error;
 
+	/*
+	 * Read IO Operations stats struct
+	 */
+	if (!read_chunk_s(fpin, &io_stats))
+		goto error;
+
+	io_stats_shmem->stat_reset_timestamp = io_stats.stat_reset_timestamp;
+
+	for (int i = 0; i < BACKEND_NUM_TYPES; i++)
+	{
+		PgStat_IOPathOps *stats = &io_stats.stats[i];
+		PgStatShared_IOPathOps *stats_shmem = &io_stats_shmem->stats[i];
+
+		memcpy(stats_shmem->data, stats->data, sizeof(stats->data));
+	}
+
 	/*
 	 * Read SLRU stats struct
 	 */
diff --git a/src/backend/utils/activity/pgstat_bgwriter.c b/src/backend/utils/activity/pgstat_bgwriter.c
index fbb1edc527..3d7f90a1b7 100644
--- a/src/backend/utils/activity/pgstat_bgwriter.c
+++ b/src/backend/utils/activity/pgstat_bgwriter.c
@@ -24,7 +24,7 @@ PgStat_BgWriterStats PendingBgWriterStats = {0};

 /*
 
 /*

 /*
 
 /*
diff --git a/src/backend/utils/activity/pgstat_io_ops.c b/src/backend/utils/activity/pgstat_io_ops.c
new file mode 100644
index 0000000000..6e7351660f
--- /dev/null
+++ b/src/backend/utils/activity/pgstat_io_ops.c
@@ -0,0 +1,192 @@
+/* -------------------------------------------------------------------------
+ *
+ * pgstat_io_ops.c
+ *	  Implementation of IO operation statistics.
+ *
+ * This file contains the implementation of IO operation statistics. It is kept
+ * separate from pgstat.c to enforce the line between the statistics access /
+ * storage implementation and the details about individual types of
+ * statistics.
+ *
+ * Copyright (c) 2001-2022, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/activity/pgstat_io_ops.c
+ * -------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "utils/pgstat_internal.h"
+
+static PgStat_IOPathOps pending_IOOpStats;
+bool have_ioopstats = false;
+
+
+/*
+ * Flush out locally pending IO Operation statistics entries
+ *
+ * If nowait is true, this function returns false on lock failure. Otherwise
+ * this function always returns true.
+ *
+ * If nowait is true, this function returns true if the lock could not be
+ * acquired. Otherwise return false.
+ */
+bool
+pgstat_flush_io_ops(bool nowait)
+{
+	PgStatShared_IOPathOps *stats_shmem;
+
+	if (!have_ioopstats)
+		return false;
+
+	stats_shmem =
+		&pgStatLocal.shmem->io_ops.stats[backend_type_get_idx(MyBackendType)];
+
+	if (!nowait)
+		LWLockAcquire(&stats_shmem->lock, LW_EXCLUSIVE);
+	else if (!LWLockConditionalAcquire(&stats_shmem->lock, LW_EXCLUSIVE))
+		return true;
+
+
+	for (int i = 0; i < IOPATH_NUM_TYPES; i++)
+	{
+		PgStat_IOOpCounters *sharedent = &stats_shmem->data[i];
+		PgStat_IOOpCounters *pendingent = &pending_IOOpStats.data[i];
+
+#define IO_OP_ACC(fld) sharedent->fld += pendingent->fld
+		IO_OP_ACC(allocs);
+		IO_OP_ACC(extends);
+		IO_OP_ACC(fsyncs);
+		IO_OP_ACC(reads);
+		IO_OP_ACC(writes);
+#undef IO_OP_ACC
+	}
+
+	LWLockRelease(&stats_shmem->lock);
+
+	memset(&pending_IOOpStats, 0, sizeof(pending_IOOpStats));
+
+	have_ioopstats = false;
+
+	return false;
+}
+
+void
+pgstat_io_ops_snapshot_cb(void)
+{
+	PgStatShared_BackendIOPathOps *all_backend_stats_shmem = &pgStatLocal.shmem->io_ops;
+	PgStat_BackendIOPathOps *all_backend_stats_snap = &pgStatLocal.snapshot.io_ops;
+
+	for (int i = 0; i < BACKEND_NUM_TYPES; i++)
+	{
+		PgStatShared_IOPathOps *stats_shmem = &all_backend_stats_shmem->stats[i];
+		PgStat_IOPathOps *stats_snap = &all_backend_stats_snap->stats[i];
+
+		LWLockAcquire(&stats_shmem->lock, LW_EXCLUSIVE);
+		/*
+		 * Use the lock in the first BackendType's PgStat_IOPathOps to protect the
+		 * reset timestamp as well.
+		 */
+		if (i == 0)
+			all_backend_stats_snap->stat_reset_timestamp = all_backend_stats_shmem->stat_reset_timestamp;
+
+		memcpy(stats_snap->data, stats_shmem->data, sizeof(stats_shmem->data));
+		LWLockRelease(&stats_shmem->lock);
+	}
+
+}
+
+void
+pgstat_io_ops_reset_all_cb(TimestampTz ts)
+{
+	PgStatShared_BackendIOPathOps *all_backend_stats_shmem = &pgStatLocal.shmem->io_ops;
+
+	for (int i = 0; i < BACKEND_NUM_TYPES; i++)
+	{
+		PgStatShared_IOPathOps *stats_shmem = &all_backend_stats_shmem->stats[i];
+
+		LWLockAcquire(&stats_shmem->lock, LW_EXCLUSIVE);
+
+		/*
+		 * Use the lock in the first BackendType's PgStat_IOPathOps to protect the
+		 * reset timestamp as well.
+		 */
+		if (i == 0)
+			all_backend_stats_shmem->stat_reset_timestamp = ts;
+
+		memset(stats_shmem->data, 0, sizeof(stats_shmem->data));
+		LWLockRelease(&stats_shmem->lock);
+	}
+}
+
+void
+pgstat_count_io_op(IOOp io_op, IOPath io_path)
+{
+	PgStat_IOOpCounters *pending_counters = &pending_IOOpStats.data[io_path];
+
+	switch (io_op)
+	{
+		case IOOP_ALLOC:
+			pending_counters->allocs++;
+			break;
+		case IOOP_EXTEND:
+			pending_counters->extends++;
+			break;
+		case IOOP_FSYNC:
+			pending_counters->fsyncs++;
+			break;
+		case IOOP_READ:
+			pending_counters->reads++;
+			break;
+		case IOOP_WRITE:
+			pending_counters->writes++;
+			break;
+	}
+
+	have_ioopstats = true;
+}
+
+PgStat_BackendIOPathOps*
+pgstat_fetch_backend_io_path_ops(void)
+{
+	pgstat_snapshot_fixed(PGSTAT_KIND_IOOPS);
+
+	return &pgStatLocal.snapshot.io_ops;
+}
+
+const char *
+pgstat_io_path_desc(IOPath io_path)
+{
+	switch (io_path)
+	{
+		case IOPATH_LOCAL:
+			return "Local";
+		case IOPATH_SHARED:
+			return "Shared";
+		case IOPATH_STRATEGY:
+			return "Strategy";
+	}
+
+	elog(ERROR, "unrecognized IOPath value: %d", io_path);
+}
+
+const char *
+pgstat_io_op_desc(IOOp io_op)
+{
+	switch (io_op)
+	{
+		case IOOP_ALLOC:
+			return "Alloc";
+		case IOOP_EXTEND:
+			return "Extend";
+		case IOOP_FSYNC:
+			return "Fsync";
+		case IOOP_READ:
+			return "Read";
+		case IOOP_WRITE:
+			return "Write";
+	}
+
+	elog(ERROR, "unrecognized IOOp value: %d", io_op);
+}
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index a846d9ffb6..a17b3336db 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -205,7 +205,7 @@ pgstat_drop_relation(Relation rel)
 }
 
 /*
- * Report that the table was just vacuumed.
+ * Report that the table was just vacuumed and flush IO Operation statistics.
  */
 void
 pgstat_report_vacuum(Oid tableoid, bool shared,
@@ -257,10 +257,18 @@ pgstat_report_vacuum(Oid tableoid, bool shared,
 	}
 
 	pgstat_unlock_entry(entry_ref);
+
+	/*
+	 * Flush IO Operations statistics now. pgstat_report_stat() will flush IO
+	 * Operation stats, however this will not be called after an entire
+	 * autovacuum cycle is done -- which will likely vacuum many relations -- or
+	 * until the VACUUM command has processed all tables and committed.
+	 */
+	pgstat_flush_io_ops(false);
 }
 
 /*
@@ -340,6 +348,13 @@ pgstat_report_analyze(Relation rel,
 	}
 
 	pgstat_unlock_entry(entry_ref);
+
+	/*
+	 * Flush IO Operations statistics explicitly for the same reason as in
+	 * pgstat_report_vacuum(). We don't want to wait for an entire ANALYZE
+	 * command to complete before updating stats.
+	 */
+	pgstat_flush_io_ops(false);
 }
 
 /*
 
 /*
 
 /*
 
 	PG_RETURN_VOID();
 }

index ac28f813b4..d6ed6ec864 100644
@@ -276,6 +278,50 @@ typedef struct PgStat_CheckpointerStats
 
+/*
+	IOOP_READ,
+	PgStat_Counter reads;
@@ -453,6 +499,18 @@ extern void pgstat_report_checkpointer(void);

+/*
+extern PgStat_BackendIOPathOps *pgstat_fetch_backend_io_path_ops(void);
 
 extern int	StrategySyncStart(uint32 *complete_passes, uint32 *num_buf_alloc);
 extern void StrategyNotifyBgWriter(int bgwprocno);
diff --git a/src/include/utils/backend_status.h b/src/include/utils/backend_status.h
index 7403bca25e..d9b6d12acc 100644
--- a/src/include/utils/backend_status.h
+++ b/src/include/utils/backend_status.h
@@ -306,6 +306,42 @@ extern const char *pgstat_get_crashed_backend_activity(int pid, char *buffer,
 													   int buflen);
 extern uint64 pgstat_get_my_query_id(void);
 
+/* Utility functions */
+
+/*
+static inline int
+backend_type_get_idx(BackendType backend_type)
+static inline BackendType
+idx_get_backend_type(int idx)
 
 /* ----------
  * Support functions for the SQL-callable functions to
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 9303d05427..3151c43dfe 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -329,6 +329,19 @@ typedef struct PgStatShared_Checkpointer
 	PgStat_CheckpointerStats reset_offset;
 } PgStatShared_Checkpointer;
 
+typedef struct PgStatShared_IOPathOps
+{
+	LWLock		lock;
+	PgStat_IOOpCounters data[IOPATH_NUM_TYPES];
+} PgStatShared_IOPathOps;
+
+typedef struct PgStatShared_BackendIOPathOps
+{
+	TimestampTz stat_reset_timestamp;
+	PgStatShared_IOPathOps stats[BACKEND_NUM_TYPES];
+} PgStatShared_BackendIOPathOps;
+
+
 typedef struct PgStatShared_SLRU
 {
 	/* lock protects ->stats */
@@ -419,6 +432,7 @@ typedef struct PgStat_ShmemControl
 	PgStatShared_Archiver archiver;
 	PgStatShared_BgWriter bgwriter;
 	PgStatShared_Checkpointer checkpointer;
+	PgStatShared_BackendIOPathOps io_ops;
 	PgStatShared_SLRU slru;
 	PgStatShared_Wal wal;
 } PgStat_ShmemControl;
@@ -442,6 +456,8 @@ typedef struct PgStat_Snapshot

+	PgStat_BackendIOPathOps io_ops;
+
 	PgStat_SLRUStats slru[SLRU_NUM_ELEMENTS];
 
 	PgStat_WalStats wal;
@@ -549,6 +565,14 @@ extern void pgstat_database_reset_timestamp_cb(PgStatShared_Common *header, Time
 extern bool pgstat_function_flush_cb(PgStat_EntryRef *entry_ref, bool nowait);

+/*

From f1dd9c1ccddce6ee4cad4df70f3475ac2a83bca3 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Mon, 4 Jul 2022 15:44:17 -0400
Subject: [PATCH v25 4/4] Add system view tracking IO ops per backend type

Add pg_stat_io, a system view which tracks the number of IOOp (allocs,
extends, fsyncs, reads, and writes) done through each IOPath (shared
buffers, local buffers, strategy buffers) by each type of backend (e.g.
client backend, checkpointer).

Some IOPaths 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 row for the "strategy" IOPath for
checkpointer.

Some IOOps are invalid in combination with certain IOPaths. Those cells
will be NULL in the view. For example, local buffers are not fsync'd so
cells for all BackendTypes for IOPATH_STRATEGY and IOOP_FSYNC will be
NULL.

Author: Melanie Plageman <[email-masked]>
Reviewed-by: Justin Pryzby <[email-masked]>, Kyotaro Horiguchi <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/20200124195226.lth52iydq2n2uilq%40alap3.anarazel.de
---
 doc/src/sgml/monitoring.sgml         | 117 ++++++++++++++++++++++++++-
 src/backend/catalog/system_views.sql |  12 +++
 src/backend/utils/adt/pgstatfuncs.c  | 106 ++++++++++++++++++++++++
 src/include/catalog/pg_proc.dat      |   9 +++
 src/test/regress/expected/rules.out  |   9 +++
 src/test/regress/expected/stats.out  |  59 ++++++++++++++
 src/test/regress/sql/stats.sql       |  34 ++++++++
 7 files changed, 345 insertions(+), 1 deletion(-)

index 4549c2560e..2b0ee495ee 100644
 
+     <row>
@@ -3595,7 +3604,111 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
+       <structfield>read</structfield> <type>bigint</type>
+       Number of blocks read.
@@ -5355,6 +5468,8 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
index fedaed533b..1fe3b07daa 100644
@@ -1115,6 +1115,18 @@ CREATE VIEW pg_stat_bgwriter AS
 
+CREATE VIEW pg_stat_io AS
+SELECT
+       b.backend_type,
+       b.io_path,
+       b.alloc,
+       b.extend,
+       b.fsync,
+       b.read,
+       b.write,
+       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 6259cc4f4c..21d54ec9b1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1739,6 +1739,112 @@ 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.
+*/
+enum
+{
+	IO_COLUMN_BACKEND_TYPE,
+	IO_COLUMN_IO_PATH,
+	IO_COLUMN_ALLOCS,
+	IO_COLUMN_EXTENDS,
+	IO_COLUMN_FSYNCS,
+	IO_COLUMN_READS,
+	IO_COLUMN_WRITES,
+	IO_COLUMN_RESET_TIME,
+	IO_NUM_COLUMNS,
+};
+
+Datum
+pg_stat_get_io(PG_FUNCTION_ARGS)
+{
+	PgStat_BackendIOPathOps *io_stats;
+	PgStat_IOPathOps *io_path_ops;
+	ReturnSetInfo *rsinfo;
+	Datum reset_time;
+
+	SetSingleFuncCall(fcinfo, 0);
+	io_stats = pgstat_fetch_backend_io_path_ops();
+	rsinfo = (ReturnSetInfo *) fcinfo->resultinfo;
+
+	/*
+		* Currently it is not permitted to reset IO operation stats for individual
+		* IO Paths or individual BackendTypes. All IO Operation statistics are
+		* reset together. As such, it is easiest to reuse the first reset timestamp
+		* available.
+		*/
+	reset_time = TimestampTzGetDatum(io_stats->stat_reset_timestamp);
+
+	io_path_ops = io_stats->stats;
+	for (int i = 0; i < BACKEND_NUM_TYPES; i++)
+	{
+		bool can_use_strategy;
+		PgStat_IOOpCounters *counters = io_path_ops->data;
+		BackendType backend_type = idx_get_backend_type(i);
+
+	 /*
+		* IO Operation statistics are not collected for all BackendTypes.
+		* For those BackendTypes without IO Operation stats, skip representing them
+		* in the view altogether.
+		*
+		* The following BackendTypes do not participate in the cumulative stats
+		* subsystem or do not do IO operations worth reporting statistics on:
+		* - Startup process because it does not have relation OIDs
+		* - Syslogger because it is not connected to shared memory
+		* - Archiver because most relevant archiving IO is delegated to a
+		*   specialized command or module
+		*/
+		if (backend_type == B_ARCHIVER || backend_type == B_LOGGER || backend_type
+				== B_STARTUP)
+			continue;
+
+		/*
+		 * Not all BackendTypes will use a BufferAccessStrategy. Omit those rows
+		 * from the view.
+		 */
+		can_use_strategy = backend_type == B_AUTOVAC_WORKER || backend_type ==
+			B_BACKEND || backend_type == B_STANDALONE_BACKEND || backend_type ==
+			B_BG_WORKER;
+
+		for (int j = 0; j < IOPATH_NUM_TYPES; j++)
+		{
+			Datum values[IO_NUM_COLUMNS];
+			bool nulls[IO_NUM_COLUMNS];
+
+			if (j == IOPATH_STRATEGY && !can_use_strategy)
+				continue;
+
+			memset(values, 0, sizeof(values));
+			memset(nulls, 0, sizeof(nulls));
+
+			values[IO_COLUMN_BACKEND_TYPE] = CStringGetTextDatum(GetBackendTypeDesc(backend_type));
+			values[IO_COLUMN_IO_PATH] = CStringGetTextDatum(pgstat_io_path_desc(j));
+			values[IO_COLUMN_RESET_TIME] = TimestampTzGetDatum(reset_time);
+			values[IO_COLUMN_ALLOCS] = Int64GetDatum(counters->allocs);
+			values[IO_COLUMN_EXTENDS] = Int64GetDatum(counters->extends);
+			values[IO_COLUMN_FSYNCS] = Int64GetDatum(counters->fsyncs);
+			values[IO_COLUMN_READS] = Int64GetDatum(counters->reads);
+			values[IO_COLUMN_WRITES] = Int64GetDatum(counters->writes);
+
+		 /*
+			* Temporary tables using local buffers are not logged and thus do not
+			* require fsync'ing. Set this cell to NULL to differentiate between an
+			* invalid combination and 0 observed IO Operations.
+			*/
+			if (j == IOPATH_LOCAL)
+				nulls[IO_COLUMN_FSYNCS] = true;
+
+			tuplestore_putvalues(rsinfo->setResult, rsinfo->setDesc, values, nulls);
+			counters++;
+		}
+
+		io_path_ops++;
+	}
+
+	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 2e41f4d9e8..bec3c93991 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5646,6 +5646,15 @@
   proname => 'pg_stat_get_buf_alloc', provolatile => 's', proparallel => 'r',
   prorettype => 'int8', proargtypes => '', prosrc => 'pg_stat_get_buf_alloc' },
 
+{ oid => '8459', descr => 'statistics: counts of all IO operations done through all IO paths by each type of backend.',
+  proname => 'pg_stat_get_io', provolatile => 's', proisstrict => 'f',
+  prorows => '52', proretset => 't',
+  proparallel => 'r', prorettype => 'record', proargtypes => '',
+  proallargtypes => '{text,text,int8,int8,int8,int8,int8,timestamptz}',
+  proargmodes => '{o,o,o,o,o,o,o,o}',
+  proargnames => '{backend_type,io_path,alloc,extend,fsync,read,write,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 7ec3d2688f..2b269e005e 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1873,6 +1873,15 @@ 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_path,
+    b.alloc,
+    b.extend,
+    b.fsync,
+    b.read,
+    b.write,
+    b.stats_reset
+   FROM pg_stat_get_io() b(backend_type, io_path, alloc, extend, fsync, read, write, 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 5b0ebf090f..6dade03b65 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -554,4 +554,63 @@ SELECT pg_stat_get_live_tuples(:drop_stats_test_subxact_oid);

---

From: Melanie Plageman <[email-masked]>
Date: Thu, 14 Jul 2022 18:44:48 -0400

In addition to adding several new tests, the attached version 26 fixes a
major bug in constructing the view.

The only valid combination of IOPATH/IOOP that is not tested now is
IOPATH_STRATEGY + IOOP_WRITE. In most cases when I ran this in regress,
the checkpointer wrote out the dirty strategy buffer before VACUUM got
around to reusing and writing it out in my tests.

I've also changed the BACKEND_NUM_TYPES definition. Now arrays will have
that dead spot for B_INVALID, but I feel like it is much easier to
understand without trying to skip that spot and use those special helper
functions.

I also started skipping adding rows to the view for WAL_RECEIVER and
WAL_WRITER and for BackendTypes except B_BACKEND and WAL_SENDER for
IOPATH_LOCAL.

> I guess temp file writes would be reliably done by one backend... Don't
This was mainly an issue for IOPATH_STRATEGY writes as I mentioned. I
still have not solved this.

> COPY into a table should work. But might be unattractive due to the size

Did it with a CTAS as Horiguchi-san suggested.

> Yea, that's probably too hard to make reliable to be worth it.
Yes, I have skipped this.

- Melanie
From f7772e4d19821e0aeb19e906ba6f5e4bb046cfdb Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Wed, 29 Jun 2022 18:37:42 -0400
Subject: [PATCH v26 3/4] Track IO operation statistics

 src/backend/storage/buffer/freelist.c         |  25 ++-
 src/backend/utils/activity/pgstat_io_ops.c    | 193 ++++++++++++++++++
 src/include/utils/backend_status.h            |   1 -
 18 files changed, 424 insertions(+), 20 deletions(-)

index c7d7abcd73..536d422df2 100644

 
+	if (isLocalBuf)

@@ -1190,7 +1203,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,

 
+				/*

 

 

 

 
 /*
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index 990e081aae..62ec4518c8 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -15,6 +15,7 @@
  */
 #include "postgres.h"
 
+#include "pgstat.h"
 #include "port/atomics.h"
 #include "storage/buf_internals.h"
 #include "storage/bufmgr.h"
@@ -198,13 +199,15 @@ have_free_buffer(void)
  *	return the buffer with the buffer header spinlock still held.
  */
 BufferDesc *
-StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
+StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state, bool *from_ring)
 {
 	BufferDesc *buf;
 	int			bgwprocno;
 	int			trycounter;
 	uint32		local_buf_state;	/* to avoid repeated (de-)referencing */
 
+	*from_ring = false;
+
 	/*
 	 * If given a strategy object, see whether it can select a buffer. We
 	 * assume strategy objects don't need buffer_strategy_lock.
@@ -212,8 +215,21 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
 	if (strategy != NULL)
 	{
 		buf = GetBufferFromRing(strategy, buf_state);
-		if (buf != NULL)
+		*from_ring = buf != NULL;
+		if (*from_ring)
+		{
+			/*
+			 * When a strategy is in use, reused buffers from the strategy ring will
+			 * be counted as allocations for the purposes of IO Operation statistics
+			 * tracking.
+			 *
+			 * However, even when a strategy is in use, if a new buffer must be
+			 * allocated from shared buffers and added to the ring, this is counted
+			 * as a IOPATH_SHARED allocation.
+			 */
+			pgstat_count_io_op(IOOP_ALLOC, IOPATH_STRATEGY);
 			return buf;
+		}
 	}
 
 	/*
@@ -247,6 +263,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
 
 	/*
@@ -684,11 +701,13 @@ AddBufferToRing(BufferAccessStrategy strategy, BufferDesc *buf)
 bool
 StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf)
 {
+
 	/* We only do this in bulkread mode */
 	if (strategy->btype != BAS_BULKREAD)
 		return false;
 
-	/* Don't muck with behavior of normal buffer-replacement strategy */
+	/*
+	 * Don't muck with behavior of normal buffer-replacement strategy */
 	if (!strategy->current_was_in_ring ||
 		strategy->buffers[strategy->current] != BufferDescriptorGetBuffer(buf))
 		return false;
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index 9c038851d7..2d231daef0 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -15,6 +15,7 @@
  */
 #include "postgres.h"
 
+#include "pgstat.h"
@@ -196,6 +197,8 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
 
index 9d6a9e9109..65b69c4cbd 100644

 

 
+	/*

+	/*

 /*
 
 /*

 /*
 
 /*
index 0000000000..ec2919cca6
@@ -0,0 +1,193 @@
+		&pgStatLocal.shmem->io_ops.stats[MyBackendType];
 
 /*

 /*

 /*
 
 /*
 
 /*
 
 	PG_RETURN_VOID();
 }
index 5276bf25a1..e0b25c6815 100644
 
+#define BACKEND_NUM_TYPES B_WAL_WRITER + 1
+
 extern PGDLLIMPORT BackendType MyBackendType;

+/*

+/*
index 69e45900ba..da18999f59 100644
@@ -310,7 +310,7 @@ extern void ScheduleBufferTagForWriteback(WritebackContext *context, BufferTag *
 
 /* freelist.c */
index 7403bca25e..b401c7ade2 100644
@@ -306,7 +306,6 @@ extern const char *pgstat_get_crashed_backend_activity(int pid, char *buffer,
 
-
 /* ----------

 
 
 	PgStat_WalStats wal;

+/*

From 6d28dbdd174df4da115735c67f2dc3f5ff51555b Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Mon, 4 Jul 2022 15:44:17 -0400
Subject: [PATCH v26 4/4] Add system view tracking IO ops per backend type

Author: Melanie Plageman <[email-masked]>
Reviewed-by: Justin Pryzby <[email-masked]>, Kyotaro Horiguchi <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/20200124195226.lth52iydq2n2uilq%40alap3.anarazel.de
---
 doc/src/sgml/monitoring.sgml         | 117 ++++++++++++++++-
 src/backend/catalog/system_views.sql |  12 ++
 src/backend/utils/adt/pgstatfuncs.c  | 110 ++++++++++++++++
 src/include/catalog/pg_proc.dat      |   9 ++
 src/test/regress/expected/rules.out  |   9 ++
 src/test/regress/expected/stats.out  | 190 +++++++++++++++++++++++++++
 src/test/regress/sql/stats.sql       |  91 +++++++++++++
 7 files changed, 537 insertions(+), 1 deletion(-)

index f2066e5f0f..105e86d678 100644
 
+     <row>
@@ -3600,7 +3609,111 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
@@ -5360,6 +5473,8 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
 
+CREATE VIEW pg_stat_io AS
+SELECT
+       b.backend_type,
+       b.io_path,
+       b.alloc,
+       b.extend,
+       b.fsync,
+       b.read,
+       b.write,
+       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 6259cc4f4c..1c905d5413 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1739,6 +1739,116 @@ pg_stat_get_buf_alloc(PG_FUNCTION_ARGS)
 	PG_RETURN_INT64(pgstat_fetch_stat_bgwriter()->buf_alloc);
 }
 
+/*
+		bool uses_local;
+		bool uses_strategy;
+		Datum backend_type_desc = CStringGetTextDatum(GetBackendTypeDesc(i));
+		PgStat_IOPathOps *io_path_ops = &io_stats->stats[i];
+		* - WAL Receiver and WAL Writer IO is not tracked in pg_stat_io for now
+		if (i == B_INVALID || i == B_ARCHIVER || i == B_LOGGER || i == B_STARTUP ||
+				i == B_WAL_RECEIVER || i == B_WAL_WRITER)
+		uses_strategy = i == B_AUTOVAC_WORKER || i == B_BACKEND || i ==
+			B_STANDALONE_BACKEND || i == B_BG_WORKER;
+		uses_local = i == B_BACKEND || i == B_WAL_SENDER;
+			PgStat_IOOpCounters *counters = &io_path_ops->data[j];
+			if (j == IOPATH_STRATEGY && !uses_strategy)
+			if (j == IOPATH_LOCAL && !uses_local)
 
index 5b0ebf090f..7cdcadacd6 100644
@@ -554,4 +554,194 @@ SELECT pg_stat_get_live_tuples(:drop_stats_test_subxact_oid);
 
 DROP TABLE trunc_stats_test, trunc_stats_test1, trunc_stats_test2, trunc_stats_test3, trunc_stats_test4;
 DROP TABLE prevstats;
+-- Test that allocs, extends, reads, and writes to Shared Buffers and fsyncs
+-- done to ensure durability of Shared Buffers are tracked in pg_stat_io.
+SELECT sum(alloc) AS io_sum_shared_allocs_before FROM pg_stat_io WHERE io_path = 'Shared' \gset
+SELECT sum(extend) AS io_sum_shared_extends_before FROM pg_stat_io WHERE io_path = 'Shared' \gset
+SELECT sum(fsync) AS io_sum_shared_fsyncs_before FROM pg_stat_io WHERE io_path = 'Shared' \gset
+SELECT sum(read) AS io_sum_shared_reads_before FROM pg_stat_io WHERE io_path = 'Shared' \gset
+SELECT sum(write) AS io_sum_shared_writes_before FROM pg_stat_io WHERE io_path = 'Shared' \gset
+-- Create a regular table and insert some data to generate IOPATH_SHARED allocs 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 IOPATH_SHARED writes and fsyncs.
+CHECKPOINT;
+SELECT sum(alloc) AS io_sum_shared_allocs_after FROM pg_stat_io WHERE io_path = 'Shared' \gset
+SELECT sum(extend) AS io_sum_shared_extends_after FROM pg_stat_io WHERE io_path = 'Shared' \gset
+SELECT sum(write) AS io_sum_shared_writes_after FROM pg_stat_io WHERE io_path = 'Shared' \gset
+SELECT sum(fsync) AS io_sum_shared_fsyncs_after FROM pg_stat_io WHERE io_path = 'Shared' \gset
+SELECT :io_sum_shared_allocs_after > :io_sum_shared_allocs_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)
+
+SELECT :io_sum_shared_writes_after > :io_sum_shared_writes_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_path = '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 allocs, extends, reads, and writes of temporary tables are tracked
+-- in pg_stat_io.
+CREATE TEMPORARY TABLE test_io_local(a int, b TEXT);
+SELECT sum(alloc) AS io_sum_local_allocs_before FROM pg_stat_io WHERE io_path = 'Local' \gset
+SELECT sum(extend) AS io_sum_local_extends_before FROM pg_stat_io WHERE io_path = 'Local' \gset
+SELECT sum(read) AS io_sum_local_reads_before FROM pg_stat_io WHERE io_path = 'Local' \gset
+SELECT sum(write) AS io_sum_local_writes_before FROM pg_stat_io WHERE io_path = '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, 80000) as id,
+'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
+-- Read in evicted buffers.
+SELECT COUNT(*) FROM test_io_local;
+ count 
+-------
+ 80000
+(1 row)
+
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush 
+--------------------------
+ 
+(1 row)
+
+SELECT sum(alloc) AS io_sum_local_allocs_after FROM pg_stat_io WHERE io_path = 'Local' \gset
+SELECT sum(extend) AS io_sum_local_extends_after FROM pg_stat_io WHERE io_path = 'Local' \gset
+SELECT sum(read) AS io_sum_local_reads_after FROM pg_stat_io WHERE io_path = 'Local' \gset
+SELECT sum(write) AS io_sum_local_writes_after FROM pg_stat_io WHERE io_path = 'Local' \gset
+SELECT :io_sum_local_allocs_after > :io_sum_local_allocs_before;
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT :io_sum_local_extends_after > :io_sum_local_extends_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)
+
+-- Test that, when using a Strategy, reusing buffers from the Strategy ring
+-- count as "Strategy" allocs in pg_stat_io. Also test that Strategy reads are
+-- counted as such.
+SELECT sum(alloc) AS io_sum_strategy_allocs_before FROM pg_stat_io WHERE io_path = 'Strategy' \gset
+SELECT sum(read) AS io_sum_strategy_reads_before FROM pg_stat_io WHERE io_path = 'Strategy' \gset
+CREATE TABLE test_io_strategy(a INT, b INT);
+ALTER TABLE test_io_strategy SET (autovacuum_enabled = 'false');
+INSERT INTO test_io_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_strategy;
+VACUUM (PARALLEL 0) test_io_strategy;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush 
+--------------------------
+ 
+(1 row)
+
+SELECT sum(alloc) AS io_sum_strategy_allocs_after FROM pg_stat_io WHERE io_path = 'Strategy' \gset
+SELECT sum(read) AS io_sum_strategy_reads_after FROM pg_stat_io WHERE io_path = 'Strategy' \gset
+SELECT :io_sum_strategy_allocs_after > :io_sum_strategy_allocs_before;
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT :io_sum_strategy_reads_after > :io_sum_strategy_reads_before;
+ ?column? 
+----------
+ t
+(1 row)
+
+DROP TABLE test_io_strategy;
+-- Test that, when using a Strategy, if creating a relation, Strategy extends
+-- are counted in pg_stat_io.
+-- A CTAS uses a Bulkwrite strategy.
+SELECT sum(extend) AS io_sum_strategy_extends_before FROM pg_stat_io WHERE io_path = 'Strategy' \gset
+CREATE TABLE test_io_strategy_extend AS SELECT i FROM generate_series(1,100)i;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush 
+--------------------------
+ 
+(1 row)
+
+SELECT sum(extend) AS io_sum_strategy_extends_after FROM pg_stat_io WHERE io_path = 'Strategy' \gset
+SELECT :io_sum_strategy_extends_after > :io_sum_strategy_extends_before;
+ ?column? 
+----------
+ t
+(1 row)
+
+DROP TABLE test_io_strategy_extend;
+SELECT sum(alloc) + sum(extend) + sum(fsync) + sum(read) + sum(write) AS io_stats_pre_reset FROM pg_stat_io \gset
+SELECT pg_stat_reset_shared('io');
+ pg_stat_reset_shared 
+----------------------
+ 
+(1 row)
+
+SELECT sum(alloc) + sum(extend) + sum(fsync) + sum(read) + sum(write) 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 3f3cf8fb56..4d8932feba 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -285,4 +285,95 @@ SELECT pg_stat_get_live_tuples(:drop_stats_test_subxact_oid);
 
 DROP TABLE trunc_stats_test, trunc_stats_test1, trunc_stats_test2, trunc_stats_test3, trunc_stats_test4;
 DROP TABLE prevstats;
+
+-- Test that allocs, extends, reads, and writes to Shared Buffers and fsyncs
+-- done to ensure durability of Shared Buffers are tracked in pg_stat_io.
+SELECT sum(alloc) AS io_sum_shared_allocs_before FROM pg_stat_io WHERE io_path = 'Shared' \gset
+SELECT sum(extend) AS io_sum_shared_extends_before FROM pg_stat_io WHERE io_path = 'Shared' \gset
+SELECT sum(fsync) AS io_sum_shared_fsyncs_before FROM pg_stat_io WHERE io_path = 'Shared' \gset
+SELECT sum(read) AS io_sum_shared_reads_before FROM pg_stat_io WHERE io_path = 'Shared' \gset
+SELECT sum(write) AS io_sum_shared_writes_before FROM pg_stat_io WHERE io_path = 'Shared' \gset
+-- Create a regular table and insert some data to generate IOPATH_SHARED allocs 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 IOPATH_SHARED writes and fsyncs.
+CHECKPOINT;
+SELECT sum(alloc) AS io_sum_shared_allocs_after FROM pg_stat_io WHERE io_path = 'Shared' \gset
+SELECT sum(extend) AS io_sum_shared_extends_after FROM pg_stat_io WHERE io_path = 'Shared' \gset
+SELECT sum(write) AS io_sum_shared_writes_after FROM pg_stat_io WHERE io_path = 'Shared' \gset
+SELECT sum(fsync) AS io_sum_shared_fsyncs_after FROM pg_stat_io WHERE io_path = 'Shared' \gset
+SELECT :io_sum_shared_allocs_after > :io_sum_shared_allocs_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;
+SELECT :io_sum_shared_writes_after > :io_sum_shared_writes_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_path = '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 allocs, extends, reads, and writes of temporary tables are tracked
+-- in pg_stat_io.
+CREATE TEMPORARY TABLE test_io_local(a int, b TEXT);
+SELECT sum(alloc) AS io_sum_local_allocs_before FROM pg_stat_io WHERE io_path = 'Local' \gset
+SELECT sum(extend) AS io_sum_local_extends_before FROM pg_stat_io WHERE io_path = 'Local' \gset
+SELECT sum(read) AS io_sum_local_reads_before FROM pg_stat_io WHERE io_path = 'Local' \gset
+SELECT sum(write) AS io_sum_local_writes_before FROM pg_stat_io WHERE io_path = '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, 80000) as id,
+'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
+-- Read in evicted buffers.
+SELECT COUNT(*) FROM test_io_local;
+SELECT pg_stat_force_next_flush();
+SELECT sum(alloc) AS io_sum_local_allocs_after FROM pg_stat_io WHERE io_path = 'Local' \gset
+SELECT sum(extend) AS io_sum_local_extends_after FROM pg_stat_io WHERE io_path = 'Local' \gset
+SELECT sum(read) AS io_sum_local_reads_after FROM pg_stat_io WHERE io_path = 'Local' \gset
+SELECT sum(write) AS io_sum_local_writes_after FROM pg_stat_io WHERE io_path = 'Local' \gset
+SELECT :io_sum_local_allocs_after > :io_sum_local_allocs_before;
+SELECT :io_sum_local_extends_after > :io_sum_local_extends_before;
+SELECT :io_sum_local_reads_after > :io_sum_local_reads_before;
+SELECT :io_sum_local_writes_after > :io_sum_local_writes_before;
+
+-- Test that, when using a Strategy, reusing buffers from the Strategy ring
+-- count as "Strategy" allocs in pg_stat_io. Also test that Strategy reads are
+-- counted as such.
+SELECT sum(alloc) AS io_sum_strategy_allocs_before FROM pg_stat_io WHERE io_path = 'Strategy' \gset
+SELECT sum(read) AS io_sum_strategy_reads_before FROM pg_stat_io WHERE io_path = 'Strategy' \gset
+CREATE TABLE test_io_strategy(a INT, b INT);
+ALTER TABLE test_io_strategy SET (autovacuum_enabled = 'false');
+INSERT INTO test_io_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_strategy;
+VACUUM (PARALLEL 0) test_io_strategy;
+SELECT pg_stat_force_next_flush();
+SELECT sum(alloc) AS io_sum_strategy_allocs_after FROM pg_stat_io WHERE io_path = 'Strategy' \gset
+SELECT sum(read) AS io_sum_strategy_reads_after FROM pg_stat_io WHERE io_path = 'Strategy' \gset
+SELECT :io_sum_strategy_allocs_after > :io_sum_strategy_allocs_before;
+SELECT :io_sum_strategy_reads_after > :io_sum_strategy_reads_before;
+DROP TABLE test_io_strategy;
+
+-- Test that, when using a Strategy, if creating a relation, Strategy extends
+-- are counted in pg_stat_io.
+-- A CTAS uses a Bulkwrite strategy.
+SELECT sum(extend) AS io_sum_strategy_extends_before FROM pg_stat_io WHERE io_path = 'Strategy' \gset
+CREATE TABLE test_io_strategy_extend AS SELECT i FROM generate_series(1,100)i;
+SELECT pg_stat_force_next_flush();
+SELECT sum(extend) AS io_sum_strategy_extends_after FROM pg_stat_io WHERE io_path = 'Strategy' \gset
+SELECT :io_sum_strategy_extends_after > :io_sum_strategy_extends_before;
+DROP TABLE test_io_strategy_extend;
+
+SELECT sum(alloc) + sum(extend) + sum(fsync) + sum(read) + sum(write) AS io_stats_pre_reset FROM pg_stat_io \gset
+SELECT pg_stat_reset_shared('io');
+SELECT sum(alloc) + sum(extend) + sum(fsync) + sum(read) + sum(write) 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

From 2c869d7c48ddcedf52d61b2c18173c19e588c48b Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Tue, 12 Jul 2022 19:53:23 -0400
Subject: [PATCH v26 2/4] Remove unneeded call to pgstat_report_wal()

 

From 49251edea9c1d02420ae358db5d78cf0ef36504b Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Tue, 28 Jun 2022 11:33:04 -0400
Subject: [PATCH v26 1/4] Add BackendType for standalone backends

 
 
 	return backendDesc;

---

From: Melanie Plageman <[email-masked]>
Date: Fri, 15 Jul 2022 11:59:41 -0400

I am consolidating the various naming points from this thread into one
email:

---

From: Andres Freund <[email-masked]>
Date: Fri, 15 Jul 2022 11:52:45 -0700

Hi,

On 2022-07-15 11:59:41 -0400, Melanie Plageman wrote:
> I'm not sure about the idea of prefixing the IOOp and IOPath enums with
> Pg_Stat. I could see them being used outside of statistics (though they
> are defined in pgstat.h)

+1

> From Andres:
> Quoting me (Melanie):
> I can see the point about IOPATH.
> I'm not wild about origin mostly because of the number of O's given that
> IO Operation already has two O's. It gets kind of hard to read when
> using Pascal Case: IOOrigin and IOOp.
> Also, it doesn't totally make sense for alloc. I could be convinced,
> though.
> 
> IOSOURCE doesn't have the O problem but does still not make sense for
> alloc. I also thought of IOSITE and IOVENUE.

I like "source" - not too bothered by the alloc aspect. I can also see
"context" working.

> As far as pg_stat_io vs pg_statio, they are the only stats views which
> don't have an underscore between stat and the rest of the view name, so
> perhaps we should move away from statio to stat_io going forward anyway.
> I am imagining adding to them with other iostat type metrics once direct
> IO is introduced, so they may well be changing soon anyway.

I don't think I have strong opinions on this one. I can see arguments for
either naming.

Greetings,

Andres Freund

---

From: Andres Freund <[email-masked]>
Date: Wed, 20 Jul 2022 09:50:50 -0700

Hi,

> Subject: [PATCH v26 2/4] Remove unneeded call to pgstat_report_wal()

LGTM.

> +		io_path = IOPATH_SHARED;

Seems a bit ugly to have an if (isLocalBuf) just after an isLocalBuf ?.

> +			 * as a IOPATH_SHARED allocation.

There's a bit too much duplication between the paragraphs...

> +	partial_flush |= pgstat_flush_io_ops(nowait);

Could you either add a note to the commit message that the stats file
version needs to be increased, or just iclude that in the patch.

> +		memcpy(stats_shmem->data, stats->data, sizeof(stats->data));

Why can't the data be read directly into shared memory?

> +		LWLockAcquire(&stats_shmem->lock, LW_EXCLUSIVE);

Why acquire the same lock repeatedly for each type, rather than once for
the whole?

> +			all_backend_stats_snap->stat_reset_timestamp = all_backend_stats_shmem->stat_reset_timestamp;

Which also would make this look a bit less awkward.

Starting to look pretty good...

- Andres

---

From: Melanie Plageman <[email-masked]>
Date: Wed, 20 Jul 2022 13:40:40 -0400

> Why can't the data be read directly into shared memory?
It is not the same lock. Each PgStatShared_IOPathOps has a lock so that
they can be accessed individually (per BackendType in
PgStatShared_BackendIOPathOps). It is optimized for the more common
operation of flushing at the expense of the snapshot operation (which
should be less common) and reset operation.

> Why acquire the same lock repeatedly for each type, rather than once for
This is also because of having a LWLock in each PgStatShared_IOPathOps.
Because I don't want a lock in the backend local stats, I have two data
structures PgStatShared_IOPathOps and PgStat_IOPathOps. I thought it was
odd to write out the lock to the file, so when persisting the stats, I
write out the relevant data only and when reading it back in to shared
memory,  I read in the data member of PgStatShared_IOPathOps.

---

From: Melanie Plageman <[email-masked]>
Date: Thu, 11 Aug 2022 19:53:09 -0400

I've attached v27 of the patch.

I've renamed IOPATH to IOCONTEXT. I also have added assertions to
confirm that unexpected statistics are not being accumulated.

There are also assorted other cleanups and changes.

It would be good to confirm that the rows being skipped and cells that
are NULL in the view are the correct ones.
The startup process will never use a BufferAccessStrategy, right?

> Seems a bit ugly to have an if (isLocalBuf) just after an isLocalBuf ?.

Changed this.

> There's a bit too much duplication between the paragraphs...

I actually think the two paragraphs are making separate points. I've
edited this, so see if you like it better now.

> version needs to be increased, or just iclude that in the patch.
Bumped the stats file version in attached patchset.

- Melanie
From b382e216b4a3f1dae91b043c5c8d647ea17821b7 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 11 Aug 2022 18:28:46 -0400
Subject: [PATCH v27 3/4] Track IO operation statistics

Introduce "IOOp", an IO operation done by a backend, and "IOContext",
the IO location source or target or IO type done by a backend. For
example, the checkpointer may write a shared buffer out. This would be
counted as an IOOp "write" on an IOContext IOCONTEXT_SHARED by
BackendType "checkpointer".

Each IOOp (alloc, extend, fsync, read, write) is counted per IOContext
(local, shared, or strategy) through a call to pgstat_count_io_op().

IOCONTEXT_LOCAL and IOCONTEXT_SHARED IOContexts concern operations on
local and shared buffers.

The IOCONTEXT_STRATEGY IOContext concerns buffers
alloc'd/extended/fsync'd/read/written as part of a BufferAccessStrategy.

IOOP_ALLOC is counted for IOCONTEXT_SHARED and IOCONTEXT_LOCAL whenever
a buffer is acquired through [Local]BufferAlloc(). IOOP_ALLOC for
IOCONTEXT_STRATEGY is counted whenever a buffer already in the strategy
ring is reused. And IOOP_WRITE for IOCONTEXT_STRATEGY is counted
whenever the reused dirty buffer is written out.

Stats on IOOps for all IOContexts for a backend are initially
accumulated locally.

Author: Melanie Plageman <[email-masked]>
Reviewed-by: Justin Pryzby <[email-masked]>, Kyotaro Horiguchi <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/20200124195226.lth52iydq2n2uilq%40alap3.anarazel.de
---
 doc/src/sgml/monitoring.sgml                  |   2 +
 src/backend/postmaster/checkpointer.c         |  12 +
 src/backend/storage/buffer/bufmgr.c           |  64 +++-
 src/backend/storage/buffer/freelist.c         |  23 +-
 src/backend/storage/buffer/localbuf.c         |   5 +
 src/backend/storage/sync/sync.c               |   9 +
 src/backend/utils/activity/Makefile           |   1 +
 src/backend/utils/activity/pgstat.c           |  31 ++
 src/backend/utils/activity/pgstat_bgwriter.c  |   7 +-
 .../utils/activity/pgstat_checkpointer.c      |   7 +-
 src/backend/utils/activity/pgstat_io_ops.c    | 297 ++++++++++++++++++
 src/backend/utils/activity/pgstat_relation.c  |  15 +-
 src/backend/utils/activity/pgstat_shmem.c     |   4 +
 src/backend/utils/activity/pgstat_wal.c       |   4 +-
 src/backend/utils/adt/pgstatfuncs.c           |   4 +-
 src/include/miscadmin.h                       |   2 +
 src/include/pgstat.h                          | 103 +++++-
 src/include/storage/buf_internals.h           |   2 +-
 src/include/utils/pgstat_internal.h           |  29 ++
 19 files changed, 601 insertions(+), 20 deletions(-)
 create mode 100644 src/backend/utils/activity/pgstat_io_ops.c

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index a6e7e3b69d..14d97ec92c 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -5360,6 +5360,8 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
         the <structname>pg_stat_bgwriter</structname>
         view, <literal>archiver</literal> to reset all the counters shown in
         the <structname>pg_stat_archiver</structname> view,
+        <literal>io</literal> to reset all the counters shown in the
+        <structname>pg_stat_io</structname> view,
         <literal>wal</literal> to reset all the counters shown in the
         <structname>pg_stat_wal</structname> view or
         <literal>recovery_prefetch</literal> to reset all the counters shown
diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 5fc076fc14..bd2e1de7c2 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -1116,6 +1116,18 @@ ForwardSyncRequest(const FileTag *ftag, SyncRequestType type)
 		if (!AmBackgroundWriterProcess())
 			CheckpointerShmem->num_backend_fsync++;
 		LWLockRelease(CheckpointerCommLock);
+
+		/*
+		 * We have no way of knowing if the current IOContext is
+		 * IOCONTEXT_SHARED or IOCONTEXT_STRATEGY at this point, so count the
+		 * fsync as being in the IOCONTEXT_SHARED IOContext. This is probably
+		 * okay, because the number of backend fsyncs doesn't say anything
+		 * about the efficacy of the BufferAccessStrategy. And counting both
+		 * fsyncs done in IOCONTEXT_SHARED and IOCONTEXT_STRATEGY under
+		 * IOCONTEXT_SHARED is likely clearer when investigating the number of
+		 * backend fsyncs.
+		 */
+		pgstat_count_io_op(IOOP_FSYNC, IOCONTEXT_SHARED);
 		return false;
 	}
 
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 8ef0436c52..d4c9bf7c4f 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -482,7 +482,7 @@ static BufferDesc *BufferAlloc(SMgrRelation smgr,
 							   BlockNumber blockNum,
 							   BufferAccessStrategy strategy,
 							   bool *foundPtr);
-static void FlushBuffer(BufferDesc *buf, SMgrRelation reln);
+static void FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOContext io_context);
 static void FindAndDropRelationBuffers(RelFileLocator rlocator,
 									   ForkNumber forkNum,
 									   BlockNumber nForkBlock,
@@ -823,6 +823,7 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	BufferDesc *bufHdr;
 	Block		bufBlock;
 	bool		found;
+	IOContext	io_context;
 	bool		isExtend;
 	bool		isLocalBuf = SmgrIsTemp(smgr);
 
@@ -986,10 +987,25 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	 */
 	Assert(!(pg_atomic_read_u32(&bufHdr->state) & BM_VALID));	/* spinlock not needed */
 
-	bufBlock = isLocalBuf ? LocalBufHdrGetBlock(bufHdr) : BufHdrGetBlock(bufHdr);
+	if (isLocalBuf)
+	{
+		bufBlock = LocalBufHdrGetBlock(bufHdr);
+		io_context = IOCONTEXT_LOCAL;
+	}
+	else
+	{
+		bufBlock = BufHdrGetBlock(bufHdr);
+
+		if (strategy != NULL)
+			io_context = IOCONTEXT_STRATEGY;
+		else
+			io_context = IOCONTEXT_SHARED;
+	}
 
 	if (isExtend)
 	{
+
+		pgstat_count_io_op(IOOP_EXTEND, io_context);
 		/* new buffers are zero-filled */
 		MemSet((char *) bufBlock, 0, BLCKSZ);
 		/* don't set checksum for all-zero page */
@@ -1020,6 +1036,8 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,

+			pgstat_count_io_op(IOOP_READ, io_context);
+
 			if (track_io_timing)
 			{
 				INSTR_TIME_SET_CURRENT(io_time);
@@ -1190,6 +1208,8 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	/* Loop here in case we have to try another victim buffer */
 	for (;;)
 	{
+		bool		from_ring;
+
 		/*
 		 * Ensure, while the spinlock's not yet held, that there's a free
 		 * refcount entry.
@@ -1200,7 +1220,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 		 * Select a victim buffer.  The buffer is returned with its header
 		 * spinlock still held!
 		 */
-		buf = StrategyGetBuffer(strategy, &buf_state);
+		buf = StrategyGetBuffer(strategy, &buf_state, &from_ring);

@@ -1237,6 +1257,8 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 			if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf),
 										 LW_SHARED))
 			{
+				IOContext	io_context;
+
 				/*
 				 * If using a nondefault strategy, and writing the buffer
 				 * would require a WAL flush, let the strategy decide whether
@@ -1263,13 +1285,28 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 					}
 				}
 
+				/*
+				 * When a strategy is in use, if the target dirty buffer is an
+				 * existing strategy buffer being reused, count this as a
+				 * strategy write for the purposes of IO Operations statistics
+				 * tracking.
+				 *
+				 * All dirty shared buffers upon first being added to the ring
+				 * will be counted as shared buffer writes.
+				 *
+				 * When a strategy is not in use, the write can only be a
+				 * "regular" write of a dirty shared buffer.
+				 */
+
+				io_context = from_ring ? IOCONTEXT_STRATEGY : IOCONTEXT_SHARED;
+
 				/* OK, do the I/O */
 				TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum,
 														  smgr->smgr_rlocator.locator.spcOid,
 														  smgr->smgr_rlocator.locator.dbOid,
 														  smgr->smgr_rlocator.locator.relNumber);
 
-				FlushBuffer(buf, NULL);
+				FlushBuffer(buf, NULL, io_context);
 				LWLockRelease(BufferDescriptorGetContentLock(buf));
 
 				ScheduleBufferTagForWriteback(&BackendWritebackContext,
@@ -2573,7 +2610,7 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
 	PinBuffer_Locked(bufHdr);
 	LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
 
-	FlushBuffer(bufHdr, NULL);
+	FlushBuffer(bufHdr, NULL, IOCONTEXT_SHARED);

@@ -2820,9 +2857,12 @@ BufferGetTag(Buffer buffer, RelFileLocator *rlocator, ForkNumber *forknum,
  *
  * If the caller has an smgr reference for the buffer's relation, pass it
  * as the second parameter.  If not, pass NULL.
+ *
+ * IOContext will always be IOCONTEXT_SHARED except when a buffer access strategy is
+ * used and the buffer being flushed is a buffer from the strategy ring.
  */
 static void
-FlushBuffer(BufferDesc *buf, SMgrRelation reln)
+FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOContext io_context)
 {
 	XLogRecPtr	recptr;
 	ErrorContextCallback errcallback;
@@ -2902,6 +2942,8 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln)
 	 */
 	bufToWrite = PageSetChecksumCopy((Page) bufBlock, buf->tag.blockNum);
 
+	pgstat_count_io_op(IOOP_WRITE, io_context);
+
 	if (track_io_timing)
 		INSTR_TIME_SET_CURRENT(io_start);
 
@@ -3549,6 +3591,8 @@ FlushRelationBuffers(Relation rel)
 						  localpage,
 						  false);
 
+				pgstat_count_io_op(IOOP_WRITE, IOCONTEXT_LOCAL);
+
 				buf_state &= ~(BM_DIRTY | BM_JUST_DIRTIED);
 				pg_atomic_unlocked_write_u32(&bufHdr->state, buf_state);
 
@@ -3584,7 +3628,7 @@ FlushRelationBuffers(Relation rel)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, RelationGetSmgr(rel));
+			FlushBuffer(bufHdr, RelationGetSmgr(rel), IOCONTEXT_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr, true);
 		}
@@ -3679,7 +3723,7 @@ FlushRelationsAllBuffers(SMgrRelation *smgrs, int nrels)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, srelent->srel);
+			FlushBuffer(bufHdr, srelent->srel, IOCONTEXT_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr, true);
 		}
@@ -3883,7 +3927,7 @@ FlushDatabaseBuffers(Oid dbid)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, NULL);
+			FlushBuffer(bufHdr, NULL, IOCONTEXT_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr, true);
 		}
@@ -3910,7 +3954,7 @@ FlushOneBuffer(Buffer buffer)

-	FlushBuffer(bufHdr, NULL);
+	FlushBuffer(bufHdr, NULL, IOCONTEXT_SHARED);
 }
 
 /*
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index 990e081aae..237a48e8d8 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -15,6 +15,7 @@
  */
 #include "postgres.h"
 
+#include "pgstat.h"
 
+	*from_ring = false;
+
 	/*
 	 * If given a strategy object, see whether it can select a buffer. We
 	 * assume strategy objects don't need buffer_strategy_lock.
@@ -212,8 +215,23 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
 	if (strategy != NULL)
 	{
 		buf = GetBufferFromRing(strategy, buf_state);
-		if (buf != NULL)
+		*from_ring = buf != NULL;
+		if (*from_ring)
+		{
+			/*
+			 * When a strategy is in use, reused buffers from the strategy
+			 * ring will be counted as IOCONTEXT_STRATEGY allocations for the
+			 * purposes of IO Operation statistics tracking.
+			 *
+			 * However, even when a strategy is in use, if a new buffer must
+			 * be allocated from shared buffers and added to the ring, this is
+			 * counted instead as an IOCONTEXT_SHARED allocation. So, only
+			 * reused buffers are counted as being in the IOCONTEXT_STRATEGY
+			 * IOContext.
+			 */
+			pgstat_count_io_op(IOOP_ALLOC, IOCONTEXT_STRATEGY);
 			return buf;
+		}
 	}
 
 	/*
@@ -247,6 +265,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
 	 * the rate of buffer consumption.  Note that buffers recycled by a
 	 * strategy object are intentionally not counted here.
 	 */
+	pgstat_count_io_op(IOOP_ALLOC, IOCONTEXT_SHARED);
 	pg_atomic_fetch_add_u32(&StrategyControl->numBufferAllocs, 1);
 
 	/*
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index 014f644bf9..a3d76599bf 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -15,6 +15,7 @@
  */
 #include "postgres.h"
 
+#include "pgstat.h"
+				pgstat_count_io_op(IOOP_ALLOC, IOCONTEXT_LOCAL);
 
+		pgstat_count_io_op(IOOP_WRITE, IOCONTEXT_LOCAL);
+
 		/* Mark not-dirty now in case we error out below */
 		buf_state &= ~BM_DIRTY;
 		pg_atomic_unlocked_write_u32(&bufHdr->state, buf_state);
diff --git a/src/backend/storage/sync/sync.c b/src/backend/storage/sync/sync.c
index 9d6a9e9109..f310b7a435 100644
--- a/src/backend/storage/sync/sync.c
+++ b/src/backend/storage/sync/sync.c
@@ -432,6 +432,15 @@ ProcessSyncRequests(void)
 					total_elapsed += elapsed;
 					processed++;
 
+					/*
+					 * Note that if a backend using a BufferAccessStrategy is
+					 * forced to do its own fsync (as opposed to the
+					 * checkpointer doing it), it will not be counted as an
+					 * IOCONTEXT_STRATEGY IOOP_FSYNC and instead will be
+					 * counted as an IOCONTEXT_SHARED IOOP_FSYNC.
+					 */
+					pgstat_count_io_op(IOOP_FSYNC, IOCONTEXT_SHARED);
+
 					if (log_checkpoints)
 						elog(DEBUG1, "checkpoint sync: number=%d file=%s time=%.3f ms",
 							 processed,
diff --git a/src/backend/utils/activity/Makefile b/src/backend/utils/activity/Makefile
index a2e8507fd6..0098785089 100644
--- a/src/backend/utils/activity/Makefile
+++ b/src/backend/utils/activity/Makefile
@@ -22,6 +22,7 @@ OBJS = \
 	pgstat_checkpointer.o \
 	pgstat_database.o \
 	pgstat_function.o \
+	pgstat_io_ops.o \
 	pgstat_relation.o \
 	pgstat_replslot.o \
 	pgstat_shmem.o \
diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index 88e5dd1b2b..c30954d90a 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -359,6 +359,15 @@ static const PgStat_KindInfo pgstat_kind_infos[PGSTAT_NUM_KINDS] = {
 		.snapshot_cb = pgstat_checkpointer_snapshot_cb,
 	},

 
 
@@ -1312,6 +1324,14 @@ pgstat_write_statsfile(void)
 	pgstat_build_snapshot_fixed(PGSTAT_KIND_CHECKPOINTER);
 	write_chunk_s(fpout, &pgStatLocal.snapshot.checkpointer);
 
+	/*
+	 * Write IO Operations stats struct
+	 */
+	pgstat_build_snapshot_fixed(PGSTAT_KIND_IOOPS);
+	write_chunk_s(fpout, &pgStatLocal.snapshot.io_ops.stat_reset_timestamp);
+	for (int i = 0; i < BACKEND_NUM_TYPES; i++)
+		write_chunk_s(fpout, &pgStatLocal.snapshot.io_ops.stats[i]);
+
 	/*
 	 * Write SLRU stats struct
 	 */
@@ -1486,6 +1506,17 @@ pgstat_read_statsfile(void)
 	if (!read_chunk_s(fpin, &shmem->checkpointer.stats))
 		goto error;
 
+	/*
+	 * Read IO Operations stats struct
+	 */
+	if (!read_chunk_s(fpin, &shmem->io_ops.stat_reset_timestamp))
+		goto error;
+	for (int i = 0; i < BACKEND_NUM_TYPES; i++)
+	{
+		if (!read_chunk_s(fpin, &shmem->io_ops.stats[i].data))
+			goto error;
+	}
+
 	/*
 	 * Read SLRU stats struct
 	 */
diff --git a/src/backend/utils/activity/pgstat_bgwriter.c b/src/backend/utils/activity/pgstat_bgwriter.c
index fbb1edc527..3d7f90a1b7 100644
--- a/src/backend/utils/activity/pgstat_bgwriter.c
+++ b/src/backend/utils/activity/pgstat_bgwriter.c
@@ -24,7 +24,7 @@ PgStat_BgWriterStats PendingBgWriterStats = {0};

 /*
 
 /*

 /*
 
 /*
diff --git a/src/backend/utils/activity/pgstat_io_ops.c b/src/backend/utils/activity/pgstat_io_ops.c
new file mode 100644
index 0000000000..72d02f4dda
--- /dev/null
+++ b/src/backend/utils/activity/pgstat_io_ops.c
@@ -0,0 +1,297 @@
+/* -------------------------------------------------------------------------
+ *
+ * pgstat_io_ops.c
+ *	  Implementation of IO operation statistics.
+ *
+ * This file contains the implementation of IO operation statistics. It is kept
+ * separate from pgstat.c to enforce the line between the statistics access /
+ * storage implementation and the details about individual types of
+ * statistics.
+ *
+ * Copyright (c) 2001-2022, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/activity/pgstat_io_ops.c
+ * -------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "utils/pgstat_internal.h"
+
+static PgStat_IOContextOps pending_IOOpStats;
+bool		have_ioopstats = false;
+
+void
+pgstat_count_io_op(IOOp io_op, IOContext io_context)
+{
+	PgStat_IOOpCounters *pending_counters = &pending_IOOpStats.data[io_context];
+
+	Assert(pgstat_expect_io_op(MyBackendType, io_context, io_op));
+
+	switch (io_op)
+	{
+		case IOOP_ALLOC:
+			pending_counters->allocs++;
+			break;
+		case IOOP_EXTEND:
+			pending_counters->extends++;
+			break;
+		case IOOP_FSYNC:
+			pending_counters->fsyncs++;
+			break;
+		case IOOP_READ:
+			pending_counters->reads++;
+			break;
+		case IOOP_WRITE:
+			pending_counters->writes++;
+			break;
+	}
+
+	have_ioopstats = true;
+}
+
+PgStat_BackendIOContextOps *
+pgstat_fetch_backend_io_context_ops(void)
+{
+	pgstat_snapshot_fixed(PGSTAT_KIND_IOOPS);
+
+	return &pgStatLocal.snapshot.io_ops;
+}
+
+/*
+ * Flush out locally pending IO Operation statistics entries
+ *
+ * If no stats have been recorded, this function returns false.
+ *
+ * If nowait is true, this function returns true if the lock could not be
+ * acquired. Otherwise return false.
+ */
+bool
+pgstat_flush_io_ops(bool nowait)
+{
+	PgStatShared_IOContextOps *type_shstats;
+
+	if (!have_ioopstats)
+		return false;
+
+	type_shstats =
+		&pgStatLocal.shmem->io_ops.stats[MyBackendType];
+
+	if (!nowait)
+		LWLockAcquire(&type_shstats->lock, LW_EXCLUSIVE);
+	else if (!LWLockConditionalAcquire(&type_shstats->lock, LW_EXCLUSIVE))
+		return true;
+
+	for (int i = 0; i < IOCONTEXT_NUM_TYPES; i++)
+	{
+		PgStat_IOOpCounters *sharedent = &type_shstats->data[i];
+		PgStat_IOOpCounters *pendingent = &pending_IOOpStats.data[i];
+
+#define IO_OP_ACC(fld) sharedent->fld += pendingent->fld
+		IO_OP_ACC(allocs);
+		IO_OP_ACC(extends);
+		IO_OP_ACC(fsyncs);
+		IO_OP_ACC(reads);
+		IO_OP_ACC(writes);
+#undef IO_OP_ACC
+	}
+
+	LWLockRelease(&type_shstats->lock);
+
+	memset(&pending_IOOpStats, 0, sizeof(pending_IOOpStats));
+
+	have_ioopstats = false;
+
+	return false;
+}
+
+const char *
+pgstat_io_context_desc(IOContext io_context)
+{
+	switch (io_context)
+	{
+		case IOCONTEXT_LOCAL:
+			return "Local";
+		case IOCONTEXT_SHARED:
+			return "Shared";
+		case IOCONTEXT_STRATEGY:
+			return "Strategy";
+	}
+
+	elog(ERROR, "unrecognized IOContext value: %d", io_context);
+}
+
+const char *
+pgstat_io_op_desc(IOOp io_op)
+{
+	switch (io_op)
+	{
+		case IOOP_ALLOC:
+			return "Alloc";
+		case IOOP_EXTEND:
+			return "Extend";
+		case IOOP_FSYNC:
+			return "Fsync";
+		case IOOP_READ:
+			return "Read";
+		case IOOP_WRITE:
+			return "Write";
+	}
+
+	elog(ERROR, "unrecognized IOOp value: %d", io_op);
+}
+
+void
+pgstat_io_ops_reset_all_cb(TimestampTz ts)
+{
+	PgStatShared_BackendIOContextOps *backends_stats_shmem = &pgStatLocal.shmem->io_ops;
+
+	for (int i = 0; i < BACKEND_NUM_TYPES; i++)
+	{
+		PgStatShared_IOContextOps *stats_shmem = &backends_stats_shmem->stats[i];
+
+		LWLockAcquire(&stats_shmem->lock, LW_EXCLUSIVE);
+
+		/*
+		 * Use the lock in the first BackendType's PgStat_IOContextOps to
+		 * protect the reset timestamp as well.
+		 */
+		if (i == 0)
+			backends_stats_shmem->stat_reset_timestamp = ts;
+
+		memset(stats_shmem->data, 0, sizeof(stats_shmem->data));
+		LWLockRelease(&stats_shmem->lock);
+	}
+}
+
+void
+pgstat_io_ops_snapshot_cb(void)
+{
+	PgStatShared_BackendIOContextOps *backends_stats_shmem = &pgStatLocal.shmem->io_ops;
+	PgStat_BackendIOContextOps *backends_stats_snap = &pgStatLocal.snapshot.io_ops;
+
+	for (int i = 0; i < BACKEND_NUM_TYPES; i++)
+	{
+		PgStatShared_IOContextOps *stats_shmem = &backends_stats_shmem->stats[i];
+		PgStat_IOContextOps *stats_snap = &backends_stats_snap->stats[i];
+
+		LWLockAcquire(&stats_shmem->lock, LW_SHARED);
+
+		/*
+		 * Use the lock in the first BackendType's PgStat_IOContextOps to
+		 * protect the reset timestamp as well.
+		 */
+		if (i == 0)
+			backends_stats_snap->stat_reset_timestamp = backends_stats_shmem->stat_reset_timestamp;
+
+		memcpy(stats_snap->data, stats_shmem->data, sizeof(stats_shmem->data));
+		LWLockRelease(&stats_shmem->lock);
+	}
+
+}
+
+/*
+* IO Operation statistics are not collected for all BackendTypes.
+*
+* The following BackendTypes do not participate in the cumulative stats
+* subsystem or do not do IO operations worth reporting statistics on:
+* - Syslogger because it is not connected to shared memory
+* - Archiver because most relevant archiving IO is delegated to a
+*   specialized command or module
+* - WAL Receiver and WAL Writer IO is not tracked in pg_stat_io for now
+*
+* Function returns true if BackendType participates in the cumulative stats
+* subsystem for IO Operations and false if it does not.
+*/
+bool
+pgstat_io_op_stats_collected(BackendType bktype)
+{
+	return bktype != B_INVALID && bktype != B_ARCHIVER && bktype != B_LOGGER &&
+		bktype != B_WAL_RECEIVER && bktype != B_WAL_WRITER;
+}
+
+bool
+pgstat_bktype_io_context_valid(BackendType bktype, IOContext io_context)
+{
+	bool		no_strategy;
+	bool		no_local;
+
+	/*
+	 * Not all BackendTypes will use a BufferAccessStrategy.
+	 */
+	no_strategy = bktype == B_AUTOVAC_LAUNCHER || bktype ==
+		B_BG_WRITER || bktype == B_CHECKPOINTER;
+
+	/*
+	 * Only regular backends and WAL Sender processes executing queries should
+	 * use local buffers.
+	 */
+	no_local = bktype == B_AUTOVAC_LAUNCHER || bktype ==
+		B_BG_WRITER || bktype == B_CHECKPOINTER || bktype ==
+		B_AUTOVAC_WORKER || bktype == B_BG_WORKER || bktype ==
+		B_STANDALONE_BACKEND || bktype == B_STARTUP;
+
+	if (io_context == IOCONTEXT_STRATEGY && no_strategy)
+		return false;
+
+	if (io_context == IOCONTEXT_LOCAL && no_local)
+		return false;
+
+	return true;
+}
+
+bool
+pgstat_bktype_io_op_valid(BackendType bktype, IOOp io_op)
+{
+	if ((bktype == B_BG_WRITER || bktype == B_CHECKPOINTER) && io_op ==
+		IOOP_READ)
+		return false;
+
+	if ((bktype == B_AUTOVAC_LAUNCHER || bktype == B_BG_WRITER || bktype ==
+		 B_CHECKPOINTER) && io_op == IOOP_EXTEND)
+		return false;
+
+	return true;
+}
+
+bool
+pgstat_io_context_io_op_valid(IOContext io_context, IOOp io_op)
+{
+	/*
+	 * Temporary tables using local buffers are not logged and thus do not
+	 * require fsync'ing. Set this cell to NULL to differentiate between an
+	 * invalid combination and 0 observed IO Operations.
+	 *
+	 * IOOP_FSYNC IOOps done by a backend using a BufferAccessStrategy are
+	 * counted in the IOCONTEXT_SHARED IOContext. See comment in
+	 * ForwardSyncRequest() for more details.
+	 */
+	if ((io_context == IOCONTEXT_LOCAL || io_context == IOCONTEXT_STRATEGY) &&
+		io_op == IOOP_FSYNC)
+		return false;
+
+	return true;
+}
+
+bool
+pgstat_expect_io_op(BackendType bktype, IOContext io_context, IOOp io_op)
+{
+	if (!pgstat_io_op_stats_collected(bktype))
+		return false;
+
+	if (!pgstat_bktype_io_context_valid(bktype, io_context))
+		return false;
+
+	if (!pgstat_bktype_io_op_valid(bktype, io_op))
+		return false;
+
+	if (!pgstat_io_context_io_op_valid(io_context, io_op))
+		return false;
+
+	/*
+	 * There are currently no cases of a BackendType, IOContext, IOOp
+	 * combination that are specifically invalid.
+	 */
+	return true;
+}
diff --git a/src/backend/utils/activity/pgstat_relation.c b/src/backend/utils/activity/pgstat_relation.c
index a846d9ffb6..7a2fd1ccf9 100644
--- a/src/backend/utils/activity/pgstat_relation.c
+++ b/src/backend/utils/activity/pgstat_relation.c
@@ -205,7 +205,7 @@ pgstat_drop_relation(Relation rel)
 }
 
 /*
 
 	pgstat_unlock_entry(entry_ref);
+
+	/*
+	 * Flush IO Operations statistics now. pgstat_report_stat() will flush IO
+	 * Operation stats, however this will not be called after an entire
+	 * autovacuum cycle is done -- which will likely vacuum many relations --
+	 * or until the VACUUM command has processed all tables and committed.
+	 */
+	pgstat_flush_io_ops(false);
 }
 
 /*
@@ -340,6 +348,9 @@ pgstat_report_analyze(Relation rel,
 	}
 
 	pgstat_unlock_entry(entry_ref);
+
+	/* see pgstat_report_vacuum() */
+	pgstat_flush_io_ops(false);
 }
 
 /*
diff --git a/src/backend/utils/activity/pgstat_shmem.c b/src/backend/utils/activity/pgstat_shmem.c
index 89060ef29a..2acfeb3192 100644
--- a/src/backend/utils/activity/pgstat_shmem.c
+++ b/src/backend/utils/activity/pgstat_shmem.c
@@ -202,6 +202,10 @@ StatsShmemInit(void)
 		LWLockInitialize(&ctl->checkpointer.lock, LWTRANCHE_PGSTATS_DATA);
 		LWLockInitialize(&ctl->slru.lock, LWTRANCHE_PGSTATS_DATA);
 		LWLockInitialize(&ctl->wal.lock, LWTRANCHE_PGSTATS_DATA);
+
+		for (int i = 0; i < BACKEND_NUM_TYPES; i++)
+			LWLockInitialize(&ctl->io_ops.stats[i].lock,
+							 LWTRANCHE_PGSTATS_DATA);
 	}
 	else
 	{
diff --git a/src/backend/utils/activity/pgstat_wal.c b/src/backend/utils/activity/pgstat_wal.c
index 5a878bd115..9cac407b42 100644
--- a/src/backend/utils/activity/pgstat_wal.c
+++ b/src/backend/utils/activity/pgstat_wal.c
@@ -34,7 +34,7 @@ static WalUsage prevWalUsage;
 
 /*
 
 /*
index d9e2a79382..cda4447e53 100644
@@ -2092,6 +2092,8 @@ pg_stat_reset_shared(PG_FUNCTION_ARGS)
@@ -2100,7 +2102,7 @@ pg_stat_reset_shared(PG_FUNCTION_ARGS)
 
 	PG_RETURN_VOID();
 }
index 7c41b27994..f65e9635a3 100644

 extern const char *GetBackendTypeDesc(BackendType backendType);
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index ac28f813b4..83b416c59d 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -48,6 +48,7 @@ typedef enum PgStat_Kind
 	PGSTAT_KIND_ARCHIVER,
 	PGSTAT_KIND_BGWRITER,
 	PGSTAT_KIND_CHECKPOINTER,
+	PGSTAT_KIND_IOOPS,
 	PGSTAT_KIND_SLRU,
 	PGSTAT_KIND_WAL,
 } PgStat_Kind;
@@ -242,7 +243,7 @@ typedef struct PgStat_TableXactStatus
  * ------------------------------------------------------------
  */
 
-#define PGSTAT_FILE_FORMAT_ID	0x01A5BCA7
+#define PGSTAT_FILE_FORMAT_ID	0x01A5BCA8
 
 typedef struct PgStat_ArchiverStats
 {
@@ -276,6 +277,50 @@ typedef struct PgStat_CheckpointerStats
 	PgStat_Counter buf_fsync_backend;
 } PgStat_CheckpointerStats;
 
+/*
+ * Types related to counting IO Operations for various IO Contexts
+ */
+
+typedef enum IOOp
+{
+	IOOP_ALLOC,
+	IOOP_EXTEND,
+	IOOP_FSYNC,
+	IOOP_READ,
+	IOOP_WRITE,
+}			IOOp;
+
+#define IOOP_NUM_TYPES (IOOP_WRITE + 1)
+
+typedef enum IOContext
+{
+	IOCONTEXT_LOCAL,
+	IOCONTEXT_SHARED,
+	IOCONTEXT_STRATEGY,
+}			IOContext;
+
+#define IOCONTEXT_NUM_TYPES (IOCONTEXT_STRATEGY + 1)
+
+typedef struct PgStat_IOOpCounters
+{
+	PgStat_Counter allocs;
+	PgStat_Counter extends;
+	PgStat_Counter fsyncs;
+	PgStat_Counter reads;
+	PgStat_Counter writes;
+}			PgStat_IOOpCounters;
+
+typedef struct PgStat_IOContextOps
+{
+	PgStat_IOOpCounters data[IOCONTEXT_NUM_TYPES];
+}			PgStat_IOContextOps;
+
+typedef struct PgStat_BackendIOContextOps
+{
+	TimestampTz stat_reset_timestamp;
+	PgStat_IOContextOps stats[BACKEND_NUM_TYPES];
+}			PgStat_BackendIOContextOps;
+
 typedef struct PgStat_StatDBEntry
 {
 	PgStat_Counter n_xact_commit;
@@ -453,6 +498,62 @@ extern void pgstat_report_checkpointer(void);
 extern PgStat_CheckpointerStats *pgstat_fetch_stat_checkpointer(void);

+/*
+ * Functions in pgstat_io_ops.c
+ */
+
+extern void pgstat_count_io_op(IOOp io_op, IOContext io_context);
+extern PgStat_BackendIOContextOps * pgstat_fetch_backend_io_context_ops(void);
+extern bool pgstat_flush_io_ops(bool nowait);
+extern const char *pgstat_io_context_desc(IOContext io_context);
+extern const char *pgstat_io_op_desc(IOOp io_op);
+
+/* Validation functions in pgstat_io_ops.c */
+extern bool pgstat_io_op_stats_collected(BackendType bktype);
+extern bool pgstat_bktype_io_context_valid(BackendType bktype, IOContext io_context);
+extern bool pgstat_bktype_io_op_valid(BackendType bktype, IOOp io_op);
+extern bool pgstat_io_context_io_op_valid(IOContext io_context, IOOp io_op);
+extern bool pgstat_expect_io_op(BackendType bktype, IOContext io_context, IOOp io_op);
+
+/*
+ * Functions to assert that invalid IO Operation counters are zero. Used with
+ * the validation functions in pgstat_io_ops.c
+ */
+static inline void
+pgstat_io_context_ops_assert_zero(PgStat_IOOpCounters * counters)
+{
+	Assert(counters->allocs == 0 && counters->extends == 0 &&
+		   counters->fsyncs == 0 && counters->reads == 0 &&
+		   counters->writes == 0);
+}
+
+static inline void
+pgstat_io_op_assert_zero(PgStat_IOOpCounters * counters, IOOp io_op)
+{
+	switch (io_op)
+	{
+		case IOOP_ALLOC:
+			Assert(counters->allocs == 0);
+			return;
+		case IOOP_EXTEND:
+			Assert(counters->extends == 0);
+			return;
+		case IOOP_FSYNC:
+			Assert(counters->fsyncs == 0);
+			return;
+		case IOOP_READ:
+			Assert(counters->reads == 0);
+			return;
+		case IOOP_WRITE:
+			Assert(counters->writes == 0);
+			return;
+	}
+
+	elog(ERROR, "unrecognized IOOp value: %d", io_op);
+}
+
+
+
 /*
  * Functions in pgstat_database.c
  */
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index 72466551d7..aa064173ee 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -346,7 +346,7 @@ extern void ScheduleBufferTagForWriteback(WritebackContext *context, BufferTag *
 
 /* freelist.c */
index 9303d05427..26e8ec2331 100644
@@ -329,6 +329,24 @@ typedef struct PgStatShared_Checkpointer
 
+typedef struct PgStatShared_IOContextOps
+{
+	/*
+	 * lock protects ->data, PgStatShared_BackendIOContextOps->stats[0] also
+	 * protects PgStatShared_BackendIOContextOps->stat_reset_timestamp.
+	 */
+	LWLock		lock;
+	PgStat_IOOpCounters data[IOCONTEXT_NUM_TYPES];
+}			PgStatShared_IOContextOps;
+
+typedef struct PgStatShared_BackendIOContextOps
+{
+	/* ->stats_reset_timestamp is protected by ->stats[0].lock */
+	TimestampTz stat_reset_timestamp;
+	PgStatShared_IOContextOps stats[BACKEND_NUM_TYPES];
+}			PgStatShared_BackendIOContextOps;
+
+
 typedef struct PgStatShared_SLRU
 {
 	/* lock protects ->stats */
@@ -419,6 +437,7 @@ typedef struct PgStat_ShmemControl
 	PgStatShared_Archiver archiver;
 	PgStatShared_BgWriter bgwriter;
 	PgStatShared_Checkpointer checkpointer;
+	PgStatShared_BackendIOContextOps io_ops;
 	PgStatShared_SLRU slru;
 	PgStatShared_Wal wal;
 } PgStat_ShmemControl;
@@ -442,6 +461,8 @@ typedef struct PgStat_Snapshot

+	PgStat_BackendIOContextOps io_ops;
+
 	PgStat_SLRUStats slru[SLRU_NUM_ELEMENTS];
 
 	PgStat_WalStats wal;
@@ -549,6 +570,14 @@ extern void pgstat_database_reset_timestamp_cb(PgStatShared_Common *header, Time
 extern bool pgstat_function_flush_cb(PgStat_EntryRef *entry_ref, bool nowait);

+/*

From 17036e78a92a75da83ea7811fd738d490fc6c65e Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 11 Aug 2022 18:28:50 -0400
Subject: [PATCH v27 4/4] Add system view tracking IO ops per backend type

Add pg_stat_io, a system view which tracks the number of IOOp (allocs,
extends, fsyncs, reads, and writes) done through each IOContext (shared
buffers, local buffers, strategy buffers) 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 row for the "strategy" IOContext 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 fsync'd so cells for all BackendTypes for IOCONTEXT_STRATEGY 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.

particular IOContext (e.g. shared buffer accesses by checkpointer) and

Author: Melanie Plageman <[email-masked]>
Reviewed-by: Justin Pryzby <[email-masked]>, Kyotaro Horiguchi <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/20200124195226.lth52iydq2n2uilq%40alap3.anarazel.de
---
 doc/src/sgml/monitoring.sgml         | 115 ++++++++++++++-
 src/backend/catalog/system_views.sql |  12 ++
 src/backend/utils/adt/pgstatfuncs.c  | 102 ++++++++++++++
 src/include/catalog/pg_proc.dat      |   9 ++
 src/test/regress/expected/rules.out  |   9 ++
 src/test/regress/expected/stats.out  | 201 +++++++++++++++++++++++++++
 src/test/regress/sql/stats.sql       | 103 ++++++++++++++
 7 files changed, 550 insertions(+), 1 deletion(-)

index 14d97ec92c..98750121c5 100644
 
+     <row>
+      <entry>A row for each IO Context for each backend type showing
+   type for each possible IO Context containing global data for the cluster for
+   that backend and IO Context.
+       <structfield>io_context</structfield> <type>text</type>
+       IO Context used (e.g. shared buffers, direct).
index f369b1fc14..5fab964219 100644
 
+CREATE VIEW pg_stat_io AS
+SELECT
+       b.backend_type,
+       b.io_context,
+       b.alloc,
+       b.extend,
+       b.fsync,
+       b.read,
+       b.write,
+       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 cda4447e53..821216d01e 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1733,6 +1733,108 @@ pg_stat_get_buf_alloc(PG_FUNCTION_ARGS)
 	PG_RETURN_INT64(pgstat_fetch_stat_bgwriter()->buf_alloc);
 }
 
+Datum
+pg_stat_get_io(PG_FUNCTION_ARGS)
+{
+	PgStat_BackendIOContextOps *backends_io_stats;
+	ReturnSetInfo *rsinfo;
+	Datum		reset_time;
+
+	/*
+	 * When adding a new column to the pg_stat_io view, add a new enum value
+	 * here above IO_NUM_COLUMNS.
+	 */
+	enum
+	{
+		IO_COLUMN_BACKEND_TYPE,
+		IO_COLUMN_IO_CONTEXT,
+		IO_COLUMN_ALLOCS,
+		IO_COLUMN_EXTENDS,
+		IO_COLUMN_FSYNCS,
+		IO_COLUMN_READS,
+		IO_COLUMN_WRITES,
+		IO_COLUMN_RESET_TIME,
+		IO_NUM_COLUMNS,
+	};
+
+#define IO_COLUMN_IOOP_OFFSET (IO_COLUMN_IO_CONTEXT + 1)
+
+	SetSingleFuncCall(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(bktype));
+		PgStat_IOContextOps *io_context_ops = &backends_io_stats->stats[bktype];
+
+		/*
+		 * For those BackendTypes without IO Operation stats, skip
+		 * representing them in the view altogether.
+		 */
+		if (!pgstat_io_op_stats_collected(bktype))
+		{
+			for (int io_context = 0; io_context < IOCONTEXT_NUM_TYPES; io_context++)
+				pgstat_io_context_ops_assert_zero(&io_context_ops->data[io_context]);
+			continue;
+		}
+
+		for (int io_context = 0; io_context < IOCONTEXT_NUM_TYPES; io_context++)
+		{
+			PgStat_IOOpCounters *counters = &io_context_ops->data[io_context];
+			Datum		values[IO_NUM_COLUMNS];
+			bool		nulls[IO_NUM_COLUMNS];
+
+			/*
+			 * Some combinations of IOCONTEXT and BackendType are not valid
+			 * for any type of IO Operation. In such cases, omit the entire
+			 * row from the view.
+			 */
+			if (!pgstat_bktype_io_context_valid(bktype, io_context))
+			{
+				pgstat_io_context_ops_assert_zero(counters);
+				continue;
+			}
+
+			memset(values, 0, sizeof(values));
+			memset(nulls, 0, sizeof(nulls));
+
+			values[IO_COLUMN_BACKEND_TYPE] = bktype_desc;
+			values[IO_COLUMN_IO_CONTEXT] = CStringGetTextDatum(
+															   pgstat_io_context_desc(io_context));
+			values[IO_COLUMN_ALLOCS] = Int64GetDatum(counters->allocs);
+			values[IO_COLUMN_EXTENDS] = Int64GetDatum(counters->extends);
+			values[IO_COLUMN_FSYNCS] = Int64GetDatum(counters->fsyncs);
+			values[IO_COLUMN_READS] = Int64GetDatum(counters->reads);
+			values[IO_COLUMN_WRITES] = Int64GetDatum(counters->writes);
+			values[IO_COLUMN_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_bktype_io_op_valid(bktype, io_op) ||
+					!pgstat_io_context_io_op_valid(io_context, io_op))
+				{
+					pgstat_io_op_assert_zero(counters, io_op);
+					nulls[io_op + IO_COLUMN_IOOP_OFFSET] = 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 be47583122..4aefebc7f8 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5646,6 +5646,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 => '14', proretset => 't',
+  proparallel => 'r', prorettype => 'record', proargtypes => '',
+  proallargtypes => '{text,text,int8,int8,int8,int8,int8,timestamptz}',
+  proargmodes => '{o,o,o,o,o,o,o,o}',
+  proargnames => '{backend_type,io_context,alloc,extend,fsync,read,write,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 7ec3d2688f..d122c36556 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1873,6 +1873,15 @@ 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.alloc,
+    b.extend,
+    b.fsync,
+    b.read,
+    b.write,
+    b.stats_reset
+   FROM pg_stat_get_io() b(backend_type, io_context, alloc, extend, fsync, read, write, 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 6b233ff4c0..a75fc91c57 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -796,4 +796,205 @@ SELECT pg_stat_get_subscription_stats(NULL);
  
 (1 row)
 
+-- Test that allocs, extends, reads, and writes to Shared Buffers and fsyncs
+-- done to ensure durability of Shared Buffers are tracked in pg_stat_io.
+SELECT sum(alloc) AS io_sum_shared_allocs_before FROM pg_stat_io WHERE io_context = 'Shared' \gset
+SELECT sum(extend) AS io_sum_shared_extends_before FROM pg_stat_io WHERE io_context = 'Shared' \gset
+SELECT sum(fsync) AS io_sum_shared_fsyncs_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(write) AS io_sum_shared_writes_before FROM pg_stat_io WHERE io_context = 'Shared' \gset
+-- Create a regular table and insert some data to generate IOCONTEXT_SHARED allocs 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.
+CHECKPOINT;
+SELECT sum(alloc) AS io_sum_shared_allocs_after FROM pg_stat_io WHERE io_context = 'Shared' \gset
+SELECT sum(extend) AS io_sum_shared_extends_after FROM pg_stat_io WHERE io_context = 'Shared' \gset
+SELECT sum(write) AS io_sum_shared_writes_after FROM pg_stat_io WHERE io_context = 'Shared' \gset
+SELECT sum(fsync) AS io_sum_shared_fsyncs_after FROM pg_stat_io WHERE io_context = 'Shared' \gset
+SELECT :io_sum_shared_allocs_after > :io_sum_shared_allocs_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)
+
+SELECT :io_sum_shared_writes_after > :io_sum_shared_writes_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 allocs, extends, reads, and writes of temporary tables are tracked
+-- in pg_stat_io.
+CREATE TEMPORARY TABLE test_io_local(a int, b TEXT);
+SELECT sum(alloc) AS io_sum_local_allocs_before FROM pg_stat_io WHERE io_context = 'Local' \gset
+SELECT sum(extend) AS io_sum_local_extends_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(write) AS io_sum_local_writes_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, 80000) as id,
+'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
+-- Read in evicted buffers.
+SELECT COUNT(*) FROM test_io_local;
+ count 
+-------
+ 80000
+(1 row)
+
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush 
+--------------------------
+ 
+(1 row)
+
+SELECT sum(alloc) AS io_sum_local_allocs_after FROM pg_stat_io WHERE io_context = 'Local' \gset
+SELECT sum(extend) AS io_sum_local_extends_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(write) AS io_sum_local_writes_after FROM pg_stat_io WHERE io_context = 'Local' \gset
+SELECT :io_sum_local_allocs_after > :io_sum_local_allocs_before;
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT :io_sum_local_extends_after > :io_sum_local_extends_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)
+
+-- Test that, when using a Strategy, reusing buffers from the Strategy ring
+-- count as "Strategy" allocs in pg_stat_io. Also test that Strategy reads are
+-- counted as such.
+-- Set wal_skip_threshold smaller than the expected size of test_io_strategy so
+-- that, even if wal_level is minimal, VACUUM FULL will fsync the newly
+-- rewritten test_io_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 BufferAccessStrategy allocs and reads.
+SET wal_skip_threshold = '1 kB';
+SELECT sum(alloc) AS io_sum_strategy_allocs_before FROM pg_stat_io WHERE io_context = 'Strategy' \gset
+SELECT sum(read) AS io_sum_strategy_reads_before FROM pg_stat_io WHERE io_context = 'Strategy' \gset
+CREATE TABLE test_io_strategy(a INT, b INT);
+ALTER TABLE test_io_strategy SET (autovacuum_enabled = 'false');
+INSERT INTO test_io_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_strategy;
+VACUUM (PARALLEL 0) test_io_strategy;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush 
+--------------------------
+ 
+(1 row)
+
+SELECT sum(alloc) AS io_sum_strategy_allocs_after FROM pg_stat_io WHERE io_context = 'Strategy' \gset
+SELECT sum(read) AS io_sum_strategy_reads_after FROM pg_stat_io WHERE io_context = 'Strategy' \gset
+SELECT :io_sum_strategy_allocs_after > :io_sum_strategy_allocs_before;
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT :io_sum_strategy_reads_after > :io_sum_strategy_reads_before;
+ ?column? 
+----------
+ t
+(1 row)
+
+DROP TABLE test_io_strategy;
+-- Hope that the previous value of wal_skip_threshold was the default. We
+-- can't use BEGIN...SET LOCAL since VACUUM can't be run inside a transaction
+-- block.
+RESET wal_skip_threshold;
+-- Test that, when using a Strategy, if creating a relation, Strategy extends
+-- are counted in pg_stat_io.
+-- A CTAS uses a Bulkwrite strategy.
+SELECT sum(extend) AS io_sum_strategy_extends_before FROM pg_stat_io WHERE io_context = 'Strategy' \gset
+CREATE TABLE test_io_strategy_extend AS SELECT i FROM generate_series(1,100)i;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush 
+--------------------------
+ 
+(1 row)
+
+SELECT sum(extend) AS io_sum_strategy_extends_after FROM pg_stat_io WHERE io_context = 'Strategy' \gset
+SELECT :io_sum_strategy_extends_after > :io_sum_strategy_extends_before;
+ ?column? 
+----------
+ t
+(1 row)
+
+DROP TABLE test_io_strategy_extend;
+-- Test stats reset
+SELECT sum(alloc) + sum(extend) + sum(fsync) + sum(read) + sum(write) AS io_stats_pre_reset FROM pg_stat_io \gset
+SELECT pg_stat_reset_shared('io');
+ pg_stat_reset_shared 
+----------------------
+ 
+(1 row)
+
+SELECT sum(alloc) + sum(extend) + sum(fsync) + sum(read) + sum(write) 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 096f00ce8b..090cc67296 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -396,4 +396,107 @@ SELECT pg_stat_get_replication_slot(NULL);
 SELECT pg_stat_get_subscription_stats(NULL);

+
+-- Test that allocs, extends, reads, and writes to Shared Buffers and fsyncs
+-- done to ensure durability of Shared Buffers are tracked in pg_stat_io.
+SELECT sum(alloc) AS io_sum_shared_allocs_before FROM pg_stat_io WHERE io_context = 'Shared' \gset
+SELECT sum(extend) AS io_sum_shared_extends_before FROM pg_stat_io WHERE io_context = 'Shared' \gset
+SELECT sum(fsync) AS io_sum_shared_fsyncs_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(write) AS io_sum_shared_writes_before FROM pg_stat_io WHERE io_context = 'Shared' \gset
+-- Create a regular table and insert some data to generate IOCONTEXT_SHARED allocs 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.
+CHECKPOINT;
+SELECT sum(alloc) AS io_sum_shared_allocs_after FROM pg_stat_io WHERE io_context = 'Shared' \gset
+SELECT sum(extend) AS io_sum_shared_extends_after FROM pg_stat_io WHERE io_context = 'Shared' \gset
+SELECT sum(write) AS io_sum_shared_writes_after FROM pg_stat_io WHERE io_context = 'Shared' \gset
+SELECT sum(fsync) AS io_sum_shared_fsyncs_after FROM pg_stat_io WHERE io_context = 'Shared' \gset
+SELECT :io_sum_shared_allocs_after > :io_sum_shared_allocs_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;
+SELECT :io_sum_shared_writes_after > :io_sum_shared_writes_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 allocs, extends, reads, and writes of temporary tables are tracked
+-- in pg_stat_io.
+CREATE TEMPORARY TABLE test_io_local(a int, b TEXT);
+SELECT sum(alloc) AS io_sum_local_allocs_before FROM pg_stat_io WHERE io_context = 'Local' \gset
+SELECT sum(extend) AS io_sum_local_extends_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(write) AS io_sum_local_writes_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, 80000) as id,
+'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';
+-- Read in evicted buffers.
+SELECT COUNT(*) FROM test_io_local;
+SELECT pg_stat_force_next_flush();
+SELECT sum(alloc) AS io_sum_local_allocs_after FROM pg_stat_io WHERE io_context = 'Local' \gset
+SELECT sum(extend) AS io_sum_local_extends_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(write) AS io_sum_local_writes_after FROM pg_stat_io WHERE io_context = 'Local' \gset
+SELECT :io_sum_local_allocs_after > :io_sum_local_allocs_before;
+SELECT :io_sum_local_extends_after > :io_sum_local_extends_before;
+SELECT :io_sum_local_reads_after > :io_sum_local_reads_before;
+SELECT :io_sum_local_writes_after > :io_sum_local_writes_before;
+
+-- Test that, when using a Strategy, reusing buffers from the Strategy ring
+-- count as "Strategy" allocs in pg_stat_io. Also test that Strategy reads are
+-- counted as such.
+
+-- Set wal_skip_threshold smaller than the expected size of test_io_strategy so
+-- that, even if wal_level is minimal, VACUUM FULL will fsync the newly
+-- rewritten test_io_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 BufferAccessStrategy allocs and reads.
+SET wal_skip_threshold = '1 kB';
+SELECT sum(alloc) AS io_sum_strategy_allocs_before FROM pg_stat_io WHERE io_context = 'Strategy' \gset
+SELECT sum(read) AS io_sum_strategy_reads_before FROM pg_stat_io WHERE io_context = 'Strategy' \gset
+CREATE TABLE test_io_strategy(a INT, b INT);
+ALTER TABLE test_io_strategy SET (autovacuum_enabled = 'false');
+INSERT INTO test_io_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_strategy;
+VACUUM (PARALLEL 0) test_io_strategy;
+SELECT pg_stat_force_next_flush();
+SELECT sum(alloc) AS io_sum_strategy_allocs_after FROM pg_stat_io WHERE io_context = 'Strategy' \gset
+SELECT sum(read) AS io_sum_strategy_reads_after FROM pg_stat_io WHERE io_context = 'Strategy' \gset
+SELECT :io_sum_strategy_allocs_after > :io_sum_strategy_allocs_before;
+SELECT :io_sum_strategy_reads_after > :io_sum_strategy_reads_before;
+DROP TABLE test_io_strategy;
+-- Hope that the previous value of wal_skip_threshold was the default. We
+-- can't use BEGIN...SET LOCAL since VACUUM can't be run inside a transaction
+-- block.
+RESET wal_skip_threshold;
+
+-- Test that, when using a Strategy, if creating a relation, Strategy extends
+-- are counted in pg_stat_io.
+-- A CTAS uses a Bulkwrite strategy.
+SELECT sum(extend) AS io_sum_strategy_extends_before FROM pg_stat_io WHERE io_context = 'Strategy' \gset
+CREATE TABLE test_io_strategy_extend AS SELECT i FROM generate_series(1,100)i;
+SELECT pg_stat_force_next_flush();
+SELECT sum(extend) AS io_sum_strategy_extends_after FROM pg_stat_io WHERE io_context = 'Strategy' \gset
+SELECT :io_sum_strategy_extends_after > :io_sum_strategy_extends_before;
+DROP TABLE test_io_strategy_extend;
+
+-- Test stats reset
+SELECT sum(alloc) + sum(extend) + sum(fsync) + sum(read) + sum(write) AS io_stats_pre_reset FROM pg_stat_io \gset
+SELECT pg_stat_reset_shared('io');
+SELECT sum(alloc) + sum(extend) + sum(fsync) + sum(read) + sum(write) 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

From 2f67aadbf36ddca626d03474b962346ffeab89fb Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 11 Aug 2022 18:28:24 -0400
Subject: [PATCH v27 1/4] Add BackendType for standalone backends

index bd973ba613..bf3871a774 100644

 	return backendDesc;
index 067b729d5a..7c41b27994 100644

From 8035d5ca54b670e366af4a8688b942dad299eb66 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 11 Aug 2022 18:28:40 -0400
Subject: [PATCH v27 2/4] Remove unneeded call to pgstat_report_wal()

---

From: Melanie Plageman <[email-masked]>
Date: Mon, 22 Aug 2022 13:15:18 -0400

v28 attached.

I've added the new structs I added to typedefs.list.

I've split the commit which adds all of the logic to track
IO operation statistics into two commits -- one which includes all of
the code to count IOOps for IOContexts locally in a backend and a second
which includes all of the code to accumulate and manage these with the
cumulative stats system.

A few notes about the commit which adds local IO Operation stats:

- There is a comment above pgstat_io_op_stats_collected() which mentions
the cumulative stats system even though this commit doesn't engage the
cumulative stats system. I wasn't sure if it was more or less
confusing to have two different versions of this comment.

- should pgstat_count_io_op() take BackendType as a parameter instead of
using MyBackendType internally?

- pgstat_count_io_op() Assert()s that the passed-in IOOp and IOContext
are valid for this BackendType, but it doesn't check that all of the
pending stats which should be zero are zero. I thought this was okay
because if I did add that zero-check, it would be added to
pgstat_count_ioop() as well, and we already Assert() there that we can
count the op. Thus, it doesn't seem like checking that the stats are
zero would add any additional regression protection.

- I've kept pgstat_io_context_desc() and pgstat_io_op_desc() in the
commit which adds those types (the local stats commit), however they
are not used in that commit. I wasn't sure if I should keep them in
that commit or move them to the first commit using them (the commit
adding the new view).

Notes on the commit which accumulates IO Operation stats in shared
memory:

- I've extended the usage of the Assert()s that IO Operation stats that
should be zero are. Previously we only checked the stats validity when
querying the view. Now we check it when flushing pending stats and
when reading the stats file into shared memory.

Note that the three locations with these validity checks (when
flushing pending stats, when reading stats file into shared memory,
and when querying the view) have similar looking code to loop through
and validate the stats. However, the actual action they perform if the
stats are valid is different for each site (adding counters together,
doing a read, setting nulls in a tuple column to true). Also, some of
these instances have other code interspersed in the loops which would
require additional looping if separated from this logic. So it was
difficult to see a way of combining these into a single helper
function.

- I've left pgstat_fetch_backend_io_context_ops() in the shared stats
commit, however it is not used until the commit which adds the view in
pg_stat_get_io(). I wasn't sure which way seemed better.

- Melanie
From 19f89b5ba164272758f17a35973eae69475f7400 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Mon, 22 Aug 2022 11:08:23 -0400
Subject: [PATCH v28 3/5] Track IO operation statistics locally

Stats on IOOps for all IOContexts for a backend are counted in a
backend's local memory. This commit does not expose any functions for
aggregating or viewing these stats.

Author: Melanie Plageman <[email-masked]>
Reviewed-by: Justin Pryzby <[email-masked]>, Kyotaro Horiguchi <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/20200124195226.lth52iydq2n2uilq%40alap3.anarazel.de
---
 src/backend/postmaster/checkpointer.c      |  12 ++
 src/backend/storage/buffer/bufmgr.c        |  64 +++++--
 src/backend/storage/buffer/freelist.c      |  23 ++-
 src/backend/storage/buffer/localbuf.c      |   5 +
 src/backend/storage/sync/sync.c            |   9 +
 src/backend/utils/activity/Makefile        |   1 +
 src/backend/utils/activity/pgstat_io_ops.c | 191 +++++++++++++++++++++
 src/include/pgstat.h                       |  54 ++++++
 src/include/storage/buf_internals.h        |   2 +-
 src/tools/pgindent/typedefs.list           |   4 +
 10 files changed, 352 insertions(+), 13 deletions(-)
 create mode 100644 src/backend/utils/activity/pgstat_io_ops.c

index 7a1202c609..fbe8f69b7b 100644

 
 	if (isExtend)
 	{
+

 

+				/*

 

 

 
@@ -3889,7 +3933,7 @@ FlushDatabaseBuffers(Oid dbid)
@@ -3916,7 +3960,7 @@ FlushOneBuffer(Buffer buffer)

 
 /*
 
+#include "pgstat.h"
 
+	*from_ring = false;
+
 	/*
 
 	/*
 
 	/*
 
+#include "pgstat.h"

+					/*
index 0000000000..9c70a4a6dd
@@ -0,0 +1,191 @@
index ac28f813b4..32d1aec540 100644
@@ -276,6 +276,44 @@ typedef struct PgStat_CheckpointerStats
 
+/*
@@ -453,6 +491,22 @@ extern void pgstat_report_checkpointer(void);

+/*
 
 /* freelist.c */
 extern BufferDesc *StrategyGetBuffer(BufferAccessStrategy strategy,
-									 uint32 *buf_state);
+									 uint32 *buf_state, bool *from_ring);
 extern void StrategyFreeBuffer(BufferDesc *buf);
 extern bool StrategyRejectBuffer(BufferAccessStrategy strategy,
 								 BufferDesc *buf);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 35c9f1efce..72378b2148 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1105,7 +1105,9 @@ ID
 INFIX
 INT128
 INTERFACE_INFO
+IOContext
 IOFuncSelector
+IOOp
 IPCompareMethod
 ITEM
 IV
@@ -2037,6 +2039,8 @@ PgStat_FetchConsistency
 PgStat_FunctionCallUsage
 PgStat_FunctionCounts
 PgStat_HashKey
+PgStat_IOContextOps
+PgStat_IOOpCounters
 PgStat_Kind
 PgStat_KindInfo
 PgStat_LocalState
-- 
2.34.1

From e2dbdaf6cb71a1494df28d4fd7b5b13ed09c348e Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 11 Aug 2022 18:28:50 -0400
Subject: [PATCH v28 5/5] Add system view tracking IO ops per backend type

 src/backend/utils/adt/pgstatfuncs.c  | 100 +++++++++++++
 7 files changed, 548 insertions(+), 1 deletion(-)

index 9440b41770..9949011ba3 100644
 
+     <row>
index 5a844b63a1..3d52a664b0 100644
 
+CREATE VIEW pg_stat_io AS
+SELECT
+       b.backend_type,
+       b.io_context,
+       b.alloc,
+       b.extend,
+       b.fsync,
+       b.read,
+       b.write,
+       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 cda4447e53..262a5ac6c7 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1733,6 +1733,106 @@ pg_stat_get_buf_alloc(PG_FUNCTION_ARGS)
 	PG_RETURN_INT64(pgstat_fetch_stat_bgwriter()->buf_alloc);
 }
 
+Datum
+		bool		expect_backend_stats = true;
+			expect_backend_stats = false;
+			if (!expect_backend_stats ||
+				!pgstat_bktype_io_context_valid(bktype, io_context))

 (1 row)

+

From b203ed8e9900e861e2f99b2703461a065781ab17 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 11 Aug 2022 18:28:40 -0400
Subject: [PATCH v28 2/5] Remove unneeded call to pgstat_report_wal()

 

From 478d4747270df3b811723f7828353abe056c1631 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 11 Aug 2022 18:28:24 -0400
Subject: [PATCH v28 1/5] Add BackendType for standalone backends

 
 
 	return backendDesc;

From 0f141fa7f97a57b8628b1b6fd6029bd3782f16a1 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Mon, 22 Aug 2022 11:35:20 -0400
Subject: [PATCH v28 4/5] Aggregate IO operation stats per BackendType

Stats on IOOps for all IOContexts for a backend are tracked locally. Add
functionality for backends to flush these stats to shared memory and
accumulate them with those from all other backends, exited and live.
Also add reset and snapshot functions used by cumulative stats system
for management of these statistics.

The aggregated stats in shared memory could be extended in the future
with per-backend stats -- useful for per connection IO statistics and
monitoring.

Because not all BackendType, IOOp, IOContext combinations are valid, the
validity of the stats are checked before flushing pending stats and
before reading in the existing stats file to shared memory.

Author: Melanie Plageman <[email-masked]>
Reviewed-by: Justin Pryzby <[email-masked]>, Kyotaro Horiguchi <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/20200124195226.lth52iydq2n2uilq%40alap3.anarazel.de
---
 doc/src/sgml/monitoring.sgml                  |   2 +
 src/backend/utils/activity/pgstat.c           |  57 +++++++
 src/backend/utils/activity/pgstat_bgwriter.c  |   7 +-
 .../utils/activity/pgstat_checkpointer.c      |   7 +-
 src/backend/utils/activity/pgstat_io_ops.c    | 155 ++++++++++++++++++
 src/backend/utils/activity/pgstat_relation.c  |  15 +-
 src/backend/utils/activity/pgstat_shmem.c     |   4 +
 src/backend/utils/activity/pgstat_wal.c       |   4 +-
 src/backend/utils/adt/pgstatfuncs.c           |   4 +-
 src/include/miscadmin.h                       |   2 +
 src/include/pgstat.h                          |  48 +++++-
 src/include/utils/pgstat_internal.h           |  34 ++++
 src/tools/pgindent/typedefs.list              |   3 +
 13 files changed, 335 insertions(+), 7 deletions(-)

index 1d9509a2f6..9440b41770 100644
index 88e5dd1b2b..ae33d090c0 100644

@@ -582,6 +591,7 @@ pgstat_report_stat(bool force)
 
 	/* Don't expend a clock check if nothing to do */
 	if (dlist_is_empty(&pgStatPending) &&
+		!have_ioopstats &&
 		!have_slrustats &&
 		!pgstat_have_pending_wal())
 	{
@@ -628,6 +638,9 @@ pgstat_report_stat(bool force)
 	/* flush database / relation / function / ... stats */
 	partial_flush |= pgstat_flush_pending_entries(nowait);

@@ -1312,6 +1325,14 @@ pgstat_write_statsfile(void)
 	pgstat_build_snapshot_fixed(PGSTAT_KIND_CHECKPOINTER);
 	write_chunk_s(fpout, &pgStatLocal.snapshot.checkpointer);
 
+	/*
@@ -1486,6 +1507,42 @@ pgstat_read_statsfile(void)
 
+	/*
+	 * Read IO Operations stats struct
+	 */
+	if (!read_chunk_s(fpin, &shmem->io_ops.stat_reset_timestamp))
+		goto error;
+
+	for (int backend_type = 0; backend_type < BACKEND_NUM_TYPES; backend_type++)
+	{
+		PgStatShared_IOContextOps *backend_io_context_ops = &shmem->io_ops.stats[backend_type];
+		bool		expect_backend_stats = true;
+
+		if (!pgstat_io_op_stats_collected(backend_type))
+			expect_backend_stats = false;
+
+		for (int io_context = 0; io_context < IOCONTEXT_NUM_TYPES; io_context++)
+		{
+			if (!expect_backend_stats ||
+				!pgstat_bktype_io_context_valid(backend_type, io_context))
+			{
+				pgstat_io_context_ops_assert_zero(&backend_io_context_ops->data[io_context]);
+				continue;
+			}
+
+			for (int io_op = 0; io_op < IOOP_NUM_TYPES; io_op++)
+			{
+				if (!pgstat_bktype_io_op_valid(backend_type, io_op) ||
+					!pgstat_io_context_io_op_valid(io_context, io_op))
+					pgstat_io_op_assert_zero(&backend_io_context_ops->data[io_context],
+											 io_op);
+			}
+		}
+
+		if (!read_chunk_s(fpin, &backend_io_context_ops->data))
+			goto error;
+	}
+
 	/*
 	 * Read SLRU stats struct
 	 */
diff --git a/src/backend/utils/activity/pgstat_bgwriter.c b/src/backend/utils/activity/pgstat_bgwriter.c
index fbb1edc527..3d7f90a1b7 100644
--- a/src/backend/utils/activity/pgstat_bgwriter.c
+++ b/src/backend/utils/activity/pgstat_bgwriter.c
@@ -24,7 +24,7 @@ PgStat_BgWriterStats PendingBgWriterStats = {0};

 /*
 
 /*

 /*
 
 /*
diff --git a/src/backend/utils/activity/pgstat_io_ops.c b/src/backend/utils/activity/pgstat_io_ops.c
index 9c70a4a6dd..5d5728e8bc 100644
--- a/src/backend/utils/activity/pgstat_io_ops.c
+++ b/src/backend/utils/activity/pgstat_io_ops.c
@@ -20,6 +20,39 @@
 #include "utils/pgstat_internal.h"
 
 static PgStat_IOContextOps pending_IOOpStats;
+bool		have_ioopstats = false;
+
+/*
+ * Helper function to accumulate PgStat_IOOpCounters. If either of the
+ * passed-in PgStat_IOOpCounters are members of PgStatShared_IOContextOps, the
+ * caller is responsible for ensuring that the appropriate lock is held. This
+ * is not asserted because this function could plausibly be used to accumulate
+ * two local/pending PgStat_IOOpCounters.
+ */
+static void
+pgstat_accum_io_op(PgStat_IOOpCounters *shared, PgStat_IOOpCounters *local, IOOp io_op)
+{
+	switch (io_op)
+	{
+		case IOOP_ALLOC:
+			shared->allocs += local->allocs;
+			return;
+		case IOOP_EXTEND:
+			shared->extends += local->extends;
+			return;
+		case IOOP_FSYNC:
+			shared->fsyncs += local->fsyncs;
+			return;
+		case IOOP_READ:
+			shared->reads += local->reads;
+			return;
+		case IOOP_WRITE:
+			shared->writes += local->writes;
+			return;
+	}
+
+	elog(ERROR, "unrecognized IOOp value: %d", io_op);
+}
 
 void
 pgstat_count_io_op(IOOp io_op, IOContext io_context)
@@ -47,6 +80,79 @@ pgstat_count_io_op(IOOp io_op, IOContext io_context)
 			break;
 	}
 
+	have_ioopstats = true;
+}
+
+PgStat_BackendIOContextOps *
+pgstat_fetch_backend_io_context_ops(void)
+{
+	pgstat_snapshot_fixed(PGSTAT_KIND_IOOPS);
+
+	return &pgStatLocal.snapshot.io_ops;
+}
+
+/*
+ * Flush out locally pending IO Operation statistics entries
+ *
+ * If no stats have been recorded, this function returns false.
+ *
+ * If nowait is true, this function returns true if the lock could not be
+ * acquired. Otherwise return false.
+ */
+bool
+pgstat_flush_io_ops(bool nowait)
+{
+	PgStatShared_IOContextOps *type_shstats;
+	bool		expect_backend_stats = true;
+
+	if (!have_ioopstats)
+		return false;
+
+	type_shstats =
+		&pgStatLocal.shmem->io_ops.stats[MyBackendType];
+
+	if (!nowait)
+		LWLockAcquire(&type_shstats->lock, LW_EXCLUSIVE);
+	else if (!LWLockConditionalAcquire(&type_shstats->lock, LW_EXCLUSIVE))
+		return true;
+
+	if (!pgstat_io_op_stats_collected(MyBackendType))
+		expect_backend_stats = false;
+
+	for (int io_context = 0; io_context < IOCONTEXT_NUM_TYPES; io_context++)
+	{
+		PgStat_IOOpCounters *sharedent = &type_shstats->data[io_context];
+		PgStat_IOOpCounters *pendingent = &pending_IOOpStats.data[io_context];
+
+		if (!expect_backend_stats ||
+			!pgstat_bktype_io_context_valid(MyBackendType, io_context))
+		{
+			pgstat_io_context_ops_assert_zero(sharedent);
+			pgstat_io_context_ops_assert_zero(pendingent);
+			continue;
+		}
+
+		for (int io_op = 0; io_op < IOOP_NUM_TYPES; io_op++)
+		{
+			if (!pgstat_bktype_io_op_valid(MyBackendType, io_op) ||
+				!pgstat_io_context_io_op_valid(io_context, io_op))
+			{
+				pgstat_io_op_assert_zero(sharedent, io_op);
+				pgstat_io_op_assert_zero(pendingent, io_op);
+				continue;
+			}
+
+			pgstat_accum_io_op(sharedent, pendingent, io_op);
+		}
+	}
+
+	LWLockRelease(&type_shstats->lock);
+
+	memset(&pending_IOOpStats, 0, sizeof(pending_IOOpStats));
+
+	have_ioopstats = false;
+
+	return false;
 }
 
 const char *
@@ -85,6 +191,55 @@ pgstat_io_op_desc(IOOp io_op)
 	elog(ERROR, "unrecognized IOOp value: %d", io_op);
 }
 
+void
 * IO Operation statistics are not collected for all BackendTypes.
 
 /*

 /*

 /*
 
 /*
 
 /*
 
 	PG_RETURN_VOID();
 }

index 32d1aec540..24a056fae6 100644

 typedef struct PgStat_ArchiverStats
 {
@@ -314,6 +315,12 @@ typedef struct PgStat_IOContextOps
 	PgStat_IOOpCounters data[IOCONTEXT_NUM_TYPES];
 } PgStat_IOContextOps;
 
@@ -496,6 +503,8 @@ extern PgStat_CheckpointerStats *pgstat_fetch_stat_checkpointer(void);
  */
 
 extern void pgstat_count_io_op(IOOp io_op, IOContext io_context);
+extern PgStat_BackendIOContextOps *pgstat_fetch_backend_io_context_ops(void);
+extern bool pgstat_flush_io_ops(bool nowait);
 extern const char *pgstat_io_context_desc(IOContext io_context);
 extern const char *pgstat_io_op_desc(IOOp io_op);
 
@@ -506,6 +515,43 @@ extern bool pgstat_bktype_io_op_valid(BackendType bktype, IOOp io_op);
 extern bool pgstat_io_context_io_op_valid(IOContext io_context, IOOp io_op);
 extern bool pgstat_expect_io_op(BackendType bktype, IOContext io_context, IOOp io_op);
 
+/*
+pgstat_io_context_ops_assert_zero(PgStat_IOOpCounters *counters)
+pgstat_io_op_assert_zero(PgStat_IOOpCounters *counters, IOOp io_op)
 
 /*
index 9303d05427..14c28ee787 100644

 
 
 	PgStat_WalStats wal;

+/*
@@ -640,6 +669,11 @@ extern void pgstat_create_transactional(PgStat_Kind kind, Oid dboid, Oid objoid)
 
 extern PGDLLIMPORT PgStat_LocalState pgStatLocal;
 
+/*
+ * Variables in pgstat_io_ops.c
+ */
+
+extern PGDLLIMPORT bool have_ioopstats;
 
 /*
  * Variables in pgstat_slru.c
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 72378b2148..7f8029059c 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2016,12 +2016,14 @@ PgFdwRelationInfo
 PgFdwScanState
 PgIfAddrCallback
 PgStatShared_Archiver
+PgStatShared_BackendIOContextOps
 PgStatShared_BgWriter
 PgStatShared_Checkpointer
 PgStatShared_Common
 PgStatShared_Database
 PgStatShared_Function
 PgStatShared_HashEntry
+PgStatShared_IOContextOps
 PgStatShared_Relation
 PgStatShared_ReplSlot
 PgStatShared_SLRU
@@ -2029,6 +2031,7 @@ PgStatShared_Subscription
 PgStatShared_Wal
 PgStat_ArchiverStats
 PgStat_BackendFunctionEntry
+PgStat_BackendIOContextOps
 PgStat_BackendSubEntry
 PgStat_BgWriterStats
 PgStat_CheckpointerStats
-- 
2.34.1

---

From: Andres Freund <[email-masked]>
Date: Mon, 22 Aug 2022 20:31:22 -0700

Hi,

> v28 attached.

Pushed 0001, 0002. Thanks!

- Andres

---

From: Andres Freund <[email-masked]>
Date: Thu, 25 Aug 2022 12:15:27 -0700

Hi,

> which includes all of the code to accumulate and manage these with the

Thanks!

> confusing to have two different versions of this comment.

Not worth being worried about...

> using MyBackendType internally?

I don't forsee a case where a different value would be passed in.

> zero would add any additional regression protection.

It's probably ok.

> pg_stat_get_io(). I wasn't sure which way seemed better.

Think that's fine.

> difficult to see a way of combining these into a single helper

All of them seem to repeat something like

> +					!pgstat_io_context_io_op_valid(io_context, io_op))

perhaps those could be combined? Afaics nothing uses pgstat_bktype_io_op_valid
separately.

> example, the archiver or syslogger is not counted in these statistics.

s/is/are/?

> aggregating or viewing these stats.

s/This commit does not/A subsequent commit will expose/...

> +			io_context = IOCONTEXT_SHARED;

There's a isLocalBuf block earlier on, couldn't we just determine the context
there? I guess there's a branch here already, so it's probably fine as is.

> +		pgstat_count_io_op(IOOP_EXTEND, io_context);

Spurious newline.

> +FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOContext io_context)

Too long line?

But also, why document the possible values here? Seems likely to get out of
date at some point, and it doesn't seem important to know?

>  				pg_atomic_unlocked_write_u32(&bufHdr->state, buf_state);

Probably not worth doing, but these made me wonder whether there should be a
function for counting N operations at once.

> +		*from_ring = buf != NULL;

Don't really like the if (*from_ring) - why not keep it as buf != NULL? Seems
a bit confusing this way, making it less obvious what's being changed.

> +++ b/src/backend/storage/buffer/localbuf.c

Do most other places not put pgstat.h in the alphabetical order of headers?

> +					pgstat_count_io_op(IOOP_FSYNC, IOCONTEXT_SHARED);

Why is this noted here? Perhaps just point to the place where that happens
instead? I think it's also documented in ForwardSyncRequest()? Or just only
mention it there...

Arguably this would just be 2021-2022

> +			pending_counters->writes++;

How about replacing the breaks with a return and then erroring out if we reach
the end of the function? You did that below, and I think it makes sense.

> +pgstat_bktype_io_context_valid(BackendType bktype, IOContext io_context)

Maybe add a tiny comment about what 'valid' means here? Something like
'return whether the backend type counts io in io_context'.

> +		B_STANDALONE_BACKEND || bktype == B_STARTUP;

I think BG_WORKERS could end up using local buffers, extensions can do just
about everything in them.

> +	if ((bktype == B_BG_WRITER || bktype == B_CHECKPOINTER) && io_op ==

Perhaps we should add an assertion about the backend type making sense here?
I.e. that it's not archiver, walwriter etc?

> +	 * invalid combination and 0 observed IO Operations.

This comment feels a bit out of place?

> +	 * combination that are specifically invalid.

"specifically"?

> before reading in the existing stats file to shared memory.

s/are checked/is checked/?

> +		if (!read_chunk_s(fpin, &backend_io_context_ops->data))

Could we put the validation out of line? That's a lot of io stats specific
code to be in pgstat_read_statsfile().

> + * two local/pending PgStat_IOOpCounters.

What's "this" here?

> +pgstat_accum_io_op(PgStat_IOOpCounters *shared, PgStat_IOOpCounters *local, IOOp io_op)

Given that the comment above says both of them may be local, it's a bit odd to
call it 'shared' here...

> +	return &pgStatLocal.snapshot.io_ops;

Not for this patch series, but we really should replace this set of functions
with storing the relevant offset in the kind_info.

>  extern const char *pgstat_io_op_desc(IOOp io_op);

Is there any call to pgstat_flush_io_ops() from outside pgstat*.c? So possibly
it could be in pgstat_internal.h? Not that it's particularly important...

> +	elog(ERROR, "unrecognized IOOp value: %d", io_op);

Hm. This means it'll emit code even in non-assertion builds - this should
probably just be an Assert(false) or pg_unreachable().

> performs an IO Operation and maintained by the cumulative statistics

"fetched from statistics incremented"?

> each column in the view is the total number of IO Operations done (e.g.

s/is/shows/?

s/for a particular BackendType for a particular IOContext/for a particularl
BackendType and IOContext/? Somehow the repetition is weird.

> problematic due to the separate reset targets for 'bgwriter' and

I suspect we should still consider doing that in the future, perhaps by
documenting that the relevant fields in pg_stat_bgwriter aren't reset by the
'bgwriter' target anymore? And noting that reliance on those fields is
"deprecated" and that pg_stat_io should be used instead?

> +       <structname>pg_stat_io</structname></link> for details.

The "for each for each" thing again :)

> +       IO Context used (e.g. shared buffers, direct).

Wrong list of contexts.

> +       Time at which these statistics were last reset.

Part of me thinks it'd be nicer if it were "allocated, read, written, extended,
fsynced, stats_reset", instead of alphabetical order. The order already isn't
alphabetical.

> +		IO_COLUMN_BACKEND_TYPE,

Given it's local and some of the lines are long, maybe just use COL?

> +#define IO_COLUMN_IOOP_OFFSET (IO_COLUMN_IO_CONTEXT + 1)

Undef'ing it probably worth doing.

> +			expect_backend_stats = false;

Why not just expect_backend_stats = pgstat_io_op_stats_collected()?

> +			memset(nulls, 0, sizeof(nulls));

I'd replace the memset with values[...] = {0} etc.

> +															   pgstat_io_context_desc(io_context));

Pgindent, I hate you.

Perhaps put it the context desc in a local var, so it doesn't look quite this
ugly?

> +					nulls[io_op + IO_COLUMN_IOOP_OFFSET] = true;

A bit weird that we first assign a value and then set nulls separately. But
it's not obvious how to make it look nice otherwise.

> + pg_stat_force_next_flush 

Does that work reliably? A checkpoint could have started just before the
CREATE TABLE, I think? Then it'd not have flushed those writes yet. I think
doing two checkpoints would protect against that.

> +DROP TABLESPACE test_io_shared_stats_tblspc;

Tablespace creation is somewhat expensive, do we really need that? There
should be one set up in setup.sql or such.

> +'aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa';

Could be abbreviated with repeat('a', some-number) :P

Can the table be smaller than this? That might show up on a slow machine.

> +SELECT :io_sum_local_allocs_after > :io_sum_local_allocs_before;

Random q: Why are we uppercasing  the first letter of the context?

> +ALTER TABLE test_io_strategy SET (autovacuum_enabled = 'false');

I think you can specify that as part of the CREATE TABLE. Not sure if
otherwise there's not a race where autovac coul start before you do the ALTER.

> +-- first with VACUUM (FULL).

... because VACUUM FULL currently doesn't set all-visible etc on the pages,
which the subsequent vacuum will then do.

> +RESET wal_skip_threshold;

Nothing in this file set it before, so that's a pretty sure-to-be-fulfilled
hope.

> +-- Test that, when using a Strategy, if creating a relation, Strategy extends

s/if/when/?

Looks good!

Greetings,

Andres Freund

---

From: Melanie Plageman <[email-masked]>
Date: Fri, 26 Aug 2022 15:34:06 -0400

v29 attached

On Thu, Aug 25, 2022 at 3:15 PM Andres Freund <[email-masked]> wrote:

>  !pgstat_io_context_io_op_valid(io_context, io_op))
>

I've combined these into pgstat_io_op_valid().

> > example, the archiver or syslogger is not counted in these statistics.

changed

> s/This commit does not/A subsequent commit will expose/...

changed

> there? I guess there's a branch here already, so it's probably fine as is.

I've added this as close as possible to the code where we use the
io_context. If I were to move it, it would make sense to move it all the
way to the top of ReadBuffer_common() where we first define isLocalBuf.
I've left it as is.

> > +             pgstat_count_io_op(IOOP_EXTEND, io_context);

fixed

> date at some point, and it doesn't seem important to know?

Deleted.

> function for counting N operations at once.
Would it be worth it here? We would need a local variable to track how
many local buffers we end up writing. Do you think that
pgstat_count_io_op() will not be inlined and thus we will end up with
lots of extra function calls if we do a pgstat_count_io_op() on every
iteration? And that it will matter in FlushRelationBuffers()?
The other times that pgstat_count_io_op() is used in a loop, it is
part of the branch that will exit the loop and only be called once-ish.

Or are you thinking that just generally it might be nice to have?

Changed

Fixed

Removed

>
> > @@ -0,0 +1,191 @@
> > +/*
> -------------------------------------------------------------------------
> > + *
> > + * pgstat_io_ops.c

Changed

> the end of the function? You did that below, and I think it makes sense.
I used breaks because in the subsequent commit I introduce the variable
"have_ioopstats", and I set have_ioopstats to false in
pgstat_count_io_op() after counting.
It is probably safe to set have_ioopstats to true before incrementing it
since this backend is the only one that can see have_ioopstats and it
shouldn't fail while incrementing the counter but it seems less clear
than doing it after.

Instead of erroring out for an unknown IOOp, I decided to add Asserts
about the IOContext and IOOp being valid and that the combination of
MyBackendType, IOContext, and IOOp are valid. I think it will be good to
assert that the IOContext is valid before using it as an array index for
lookup in pending stats.

Changed

> about everything in them.

Fixed and added comment.

Done

> This comment feels a bit out of place?

Deleted

> > +      * combination that are specifically invalid.

I removed this and mentioned it (rephrased) above pgstat_io_op_valid()

Fixed

>  !pgstat_bktype_io_context_valid(backend_type, io_context))
> > +                     {
> > +
>  pgstat_io_context_ops_assert_zero(&backend_io_context_ops->data[io_context]);
> > +                             continue;
> > +                     }
> > +
> > +                             if
> (!pgstat_bktype_io_op_valid(backend_type, io_op) ||
> > +
>  !pgstat_io_context_io_op_valid(io_context, io_op))
> > +
>  pgstat_io_op_assert_zero(&backend_io_context_ops->data[io_context],
> > +
>               io_op);
> > +                     }
> > +             }
> > +

Done.

> > + * two local/pending PgStat_IOOpCounters.

I rephrased it.

> > +extern PgStat_BackendIOContextOps
> *pgstat_fetch_backend_io_context_ops(void);

Moved it.

Fixed.

> "fetched from statistics incremented"?

Rephrased it.

> BackendType and IOContext/? Somehow the repetition is weird.

Both of the above wordings are now changed.

> <entry><structname>pg_stat_io</structname><indexterm><primary>pg_stat_io</primary></indexterm></entry>

Changed it.

> > +       IO Context used (e.g. shared buffers, direct).

Fixed it.

> fsynced, stats_reset", instead of alphabetical order. The order already

I've updated the order in the view and docs.

> Given it's local and some of the lines are long, maybe just use COL?
I've shortened COLUMN to COL. However, I've also moved this enum outside
of the function and typedef'd it. I did this because, upon changing the
order of the columns in the view, I could no longer use
IO_COLUMN_IOOP_OFFSET and the IOOp value in the loop at the bottom of
pg_sta_get_io() to set the correct column to NULL. So, I created a
helper function which translates IOOp to io_stat_col.

> Undef'ing it probably worth doing.

It's gone now anyway.

> > +     reset_time =
> TimestampTzGetDatum(backends_io_stats->stat_reset_timestamp);
> > +
> > +             Datum           bktype_desc =
> CStringGetTextDatum(GetBackendTypeDesc(bktype));

Updated this everywhere it occurred.

>  pgstat_io_context_ops_assert_zero(counters);
> > +                             continue;
> > +                     }
> > +

Done.

> pgstat_io_context_desc(io_context));
>
> Pgindent, I hate you.
>

Did this.

> doing two checkpoints would protect against that.

If the first checkpoint starts just before creating the table and those
buffers are dirtied during that checkpoint and thus not written out by
checkpointer during that checkpoint, then the test's (single) explicit
checkpoint would end up picking up those dirty buffers and writing them
out, right?

> should be one set up in setup.sql or such.

The only ones I see in regress are for tablespace.sql which drops them
in the same test and is testing dropping tablespaces.

Done.

> Can the table be smaller than this? That might show up on a slow machine.
Setting temp_buffers to 1MB, 7500 tuples of this width seem like enough.
I inserted 8000 to be safe -- seems like an order of magnitude less
should be good.

> Random q: Why are we uppercasing  the first letter of the context?
hmm. dunno. I changed it to be lowercase now.

Done.

> which the subsequent vacuum will then do.

It is true that the second VACUUM will set all-visible while VACUUM FULL
will not. However, I didn't think that that writing was what allowed us
to test strategy reads and allocs. It would theoretically allow us to
test strategy writes, however, in practice, checkpointer or background
writer often wrote out these dirty pages with all-visible set before
this backend had a chance to reuse them and write them out itself.

Unless you are saying that the subsequent VACUUM would be a no-op were
VACUUM FULL to set all-visible on the rewritten pages?

> Nothing in this file set it before, so that's a pretty sure-to-be-fulfilled

I've removed the comment.

Changed this.

Thanks for the detailed review!

- Melanie
From b5deb62dc5c50434191cc2d4c12de46b6ea22ce2 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 11 Aug 2022 18:28:50 -0400
Subject: [PATCH v29 3/3] Add system view tracking IO ops per backend type

Add pg_stat_io, a system view which tracks the number of IOOps (allocs,
extends, fsyncs, reads, and writes) done through each IOContext (shared
buffers, local buffers, strategy buffers) by each type of backend (e.g.
client backend, checkpointer).

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.

problematic due to the separate reset targets for 'bgwriter' and 'io'.

Author: Melanie Plageman <[email-masked]>
Reviewed-by: Andres Freund <[email-masked]>
Reviewed-by: Justin Pryzby <[email-masked]>
Reviewed-by: Kyotaro Horiguchi <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/20200124195226.lth52iydq2n2uilq%40alap3.anarazel.de
---
 doc/src/sgml/monitoring.sgml         | 115 ++++++++++++++-
 src/backend/catalog/system_views.sql |  12 ++
 src/backend/utils/adt/pgstatfuncs.c  | 117 ++++++++++++++++
 src/include/catalog/pg_proc.dat      |   9 ++
 src/test/regress/expected/rules.out  |   9 ++
 src/test/regress/expected/stats.out  | 202 +++++++++++++++++++++++++++
 src/test/regress/sql/stats.sql       | 104 ++++++++++++++
 7 files changed, 567 insertions(+), 1 deletion(-)

index 9440b41770..c7ca078bc8 100644
 
+     <row>
+   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.
+       IO Context used (e.g. shared buffers, local buffers).
index 5a844b63a1..fa5cac7759 100644
 
+CREATE VIEW pg_stat_io AS
+SELECT
+       b.backend_type,
+       b.io_context,
+       b.alloc,
+       b.read,
+       b.write,
+       b.extend,
+       b.fsync,
+       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 dc3a1a26a4..b7f5818028 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1726,6 +1726,123 @@ 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_ALLOCS,
+	IO_COL_READS,
+	IO_COL_WRITES,
+	IO_COL_EXTENDS,
+	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_ALLOC:
+			return IO_COL_ALLOCS;
+		case IOOP_READ:
+			return IO_COL_READS;
+		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;
+
+	SetSingleFuncCall(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(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(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);
+
+			Datum		values[IO_NUM_COLUMNS] = {0};
+			bool		nulls[IO_NUM_COLUMNS] = {0};
+
+			/*
+			 * Some combinations of IOCONTEXT and BackendType are not valid
+			 * for any type of IO Operation. In such cases, omit the entire
+			 * row from the view.
+			 */
+			if (!expect_backend_stats ||
+				!pgstat_bktype_io_context_valid(bktype, 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_ALLOCS] = Int64GetDatum(counters->allocs);
+			values[IO_COL_READS] = Int64GetDatum(counters->reads);
+			values[IO_COL_WRITES] = Int64GetDatum(counters->writes);
+			values[IO_COL_EXTENDS] = Int64GetDatum(counters->extends);
+			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(bktype, io_context, io_op)))
+				{
+					pgstat_io_op_assert_zero(counters, io_op);
+					nulls[pgstat_io_op_get_index(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 be47583122..760055689f 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -5646,6 +5646,15 @@
   proname => 'pg_stat_get_buf_alloc', provolatile => 's', proparallel => 'r',
   prorettype => 'int8', proargtypes => '', prosrc => 'pg_stat_get_buf_alloc' },
 
+  proargnames => '{backend_type,io_context,alloc,read,write,extend,fsync,stats_reset}',
index 7ec3d2688f..d79a484df9 100644
+   FROM pg_stat_get_io() b(backend_type, io_context, alloc, read, write, extend, fsync, stats_reset);
index 6b233ff4c0..8aace2d81c 100644
@@ -796,4 +796,206 @@ SELECT pg_stat_get_subscription_stats(NULL);
  
 (1 row)
 
+-- Test that allocs, reads, writes, and extends to shared buffers and fsyncs
+-- done to ensure durability of shared buffers are tracked in pg_stat_io.
+SELECT sum(alloc) AS io_sum_shared_allocs_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(write) AS io_sum_shared_writes_before FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT sum(extend) AS io_sum_shared_extends_before FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT sum(fsync) 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
+-- allocs 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.
+CHECKPOINT;
+SELECT sum(alloc) AS io_sum_shared_allocs_after FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT sum(write) AS io_sum_shared_writes_after FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT sum(extend) AS io_sum_shared_extends_after FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT sum(fsync) AS io_sum_shared_fsyncs_after FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT :io_sum_shared_allocs_after > :io_sum_shared_allocs_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 allocs, extends, reads, and writes of temporary tables are tracked
+-- in pg_stat_io.
+-- Set temp_buffers to a low value so that we can trigger writes with fewer
+-- inserted tuples.
+SET temp_buffers TO '1MB';
+CREATE TEMPORARY TABLE test_io_local(a int, b TEXT);
+SELECT sum(alloc) AS io_sum_local_allocs_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(write) AS io_sum_local_writes_before FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT sum(extend) 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(alloc) AS io_sum_local_allocs_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(write) AS io_sum_local_writes_after FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT sum(extend) AS io_sum_local_extends_after FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT :io_sum_local_allocs_after > :io_sum_local_allocs_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, when using a BufferAccessStrategy, reusing buffers from the
+-- Strategy ring count as "Strategy" allocs in pg_stat_io. Also test that
+-- Strategy reads are counted as such.
+-- Set wal_skip_threshold smaller than the expected size of test_io_strategy so
+-- that, even if wal_level is minimal, VACUUM FULL will fsync the newly
+-- rewritten test_io_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 BufferAccessStrategy allocs and reads.
+SET wal_skip_threshold = '1 kB';
+SELECT sum(alloc) AS io_sum_strategy_allocs_before FROM pg_stat_io WHERE io_context = 'strategy' \gset
+SELECT sum(read) AS io_sum_strategy_reads_before FROM pg_stat_io WHERE io_context = 'strategy' \gset
+CREATE TABLE test_io_strategy(a int, b int) WITH (autovacuum_enabled = 'false');
+INSERT INTO test_io_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_strategy;
+VACUUM (PARALLEL 0) test_io_strategy;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush 
+--------------------------
+ 
+(1 row)
+
+SELECT sum(alloc) AS io_sum_strategy_allocs_after FROM pg_stat_io WHERE io_context = 'strategy' \gset
+SELECT sum(read) AS io_sum_strategy_reads_after FROM pg_stat_io WHERE io_context = 'strategy' \gset
+SELECT :io_sum_strategy_allocs_after > :io_sum_strategy_allocs_before;
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT :io_sum_strategy_reads_after > :io_sum_strategy_reads_before;
+ ?column? 
+----------
+ t
+(1 row)
+
+DROP TABLE test_io_strategy;
+RESET wal_skip_threshold;
+-- Test that, when using a BufferAccessStrategy and creating a relation,
+-- Strategy extends are counted in pg_stat_io.
+-- A CTAS uses a Bulk Write strategy.
+SELECT sum(extend) AS io_sum_strategy_extends_before FROM pg_stat_io WHERE io_context = 'strategy' \gset
+CREATE TABLE test_io_strategy_extend AS SELECT i FROM generate_series(1,100)i;
+SELECT pg_stat_force_next_flush();
+ pg_stat_force_next_flush 
+--------------------------
+ 
+(1 row)
+
+SELECT sum(extend) AS io_sum_strategy_extends_after FROM pg_stat_io WHERE io_context = 'strategy' \gset
+SELECT :io_sum_strategy_extends_after > :io_sum_strategy_extends_before;
+ ?column? 
+----------
+ t
+(1 row)
+
+DROP TABLE test_io_strategy_extend;
+-- Test stats reset
+SELECT sum(alloc) + sum(extend) + sum(fsync) + sum(read) + sum(write) AS io_stats_pre_reset FROM pg_stat_io \gset
+SELECT pg_stat_reset_shared('io');
+ pg_stat_reset_shared 
+----------------------
+ 
+(1 row)
+
+SELECT sum(alloc) + sum(extend) + sum(fsync) + sum(read) + sum(write) 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 096f00ce8b..3c61dd9c4f 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -396,4 +396,108 @@ SELECT pg_stat_get_replication_slot(NULL);
 SELECT pg_stat_get_subscription_stats(NULL);

+-- Test that allocs, reads, writes, and extends to shared buffers and fsyncs
+-- done to ensure durability of shared buffers are tracked in pg_stat_io.
+SELECT sum(alloc) AS io_sum_shared_allocs_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(write) AS io_sum_shared_writes_before FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT sum(extend) AS io_sum_shared_extends_before FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT sum(fsync) 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
+-- allocs 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.
+CHECKPOINT;
+SELECT sum(alloc) AS io_sum_shared_allocs_after FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT sum(write) AS io_sum_shared_writes_after FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT sum(extend) AS io_sum_shared_extends_after FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT sum(fsync) AS io_sum_shared_fsyncs_after FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT :io_sum_shared_allocs_after > :io_sum_shared_allocs_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 allocs, extends, reads, and writes of temporary tables are tracked
+-- in pg_stat_io.
+
+-- Set temp_buffers to a low value so that we can trigger writes with fewer
+-- inserted tuples.
+SET temp_buffers TO '1MB';
+CREATE TEMPORARY TABLE test_io_local(a int, b TEXT);
+SELECT sum(alloc) AS io_sum_local_allocs_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(write) AS io_sum_local_writes_before FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT sum(extend) 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(alloc) AS io_sum_local_allocs_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(write) AS io_sum_local_writes_after FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT sum(extend) AS io_sum_local_extends_after FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT :io_sum_local_allocs_after > :io_sum_local_allocs_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, when using a BufferAccessStrategy, reusing buffers from the
+-- Strategy ring count as "Strategy" allocs in pg_stat_io. Also test that
+-- Strategy reads are counted as such.
+
+-- Set wal_skip_threshold smaller than the expected size of test_io_strategy so
+-- that, even if wal_level is minimal, VACUUM FULL will fsync the newly
+-- rewritten test_io_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 BufferAccessStrategy allocs and reads.
+SET wal_skip_threshold = '1 kB';
+SELECT sum(alloc) AS io_sum_strategy_allocs_before FROM pg_stat_io WHERE io_context = 'strategy' \gset
+SELECT sum(read) AS io_sum_strategy_reads_before FROM pg_stat_io WHERE io_context = 'strategy' \gset
+CREATE TABLE test_io_strategy(a int, b int) WITH (autovacuum_enabled = 'false');
+INSERT INTO test_io_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_strategy;
+VACUUM (PARALLEL 0) test_io_strategy;
+SELECT pg_stat_force_next_flush();
+SELECT sum(alloc) AS io_sum_strategy_allocs_after FROM pg_stat_io WHERE io_context = 'strategy' \gset
+SELECT sum(read) AS io_sum_strategy_reads_after FROM pg_stat_io WHERE io_context = 'strategy' \gset
+SELECT :io_sum_strategy_allocs_after > :io_sum_strategy_allocs_before;
+SELECT :io_sum_strategy_reads_after > :io_sum_strategy_reads_before;
+DROP TABLE test_io_strategy;
+RESET wal_skip_threshold;
+
+-- Test that, when using a BufferAccessStrategy and creating a relation,
+-- Strategy extends are counted in pg_stat_io.
+-- A CTAS uses a Bulk Write strategy.
+SELECT sum(extend) AS io_sum_strategy_extends_before FROM pg_stat_io WHERE io_context = 'strategy' \gset
+CREATE TABLE test_io_strategy_extend AS SELECT i FROM generate_series(1,100)i;
+SELECT pg_stat_force_next_flush();
+SELECT sum(extend) AS io_sum_strategy_extends_after FROM pg_stat_io WHERE io_context = 'strategy' \gset
+SELECT :io_sum_strategy_extends_after > :io_sum_strategy_extends_before;
+DROP TABLE test_io_strategy_extend;
+
+-- Test stats reset
+SELECT sum(alloc) + sum(extend) + sum(fsync) + sum(read) + sum(write) AS io_stats_pre_reset FROM pg_stat_io \gset
+SELECT pg_stat_reset_shared('io');
+SELECT sum(alloc) + sum(extend) + sum(fsync) + sum(read) + sum(write) 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

From fbc1c53f9595cc4724539e199b2622844e746c03 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Mon, 22 Aug 2022 11:08:23 -0400
Subject: [PATCH v29 1/3] Track IO operation statistics locally

Introduce "IOOp", an IO operation done by a backend, and "IOContext",
the IO source, target, or type done by a backend. For example, the
checkpointer may write a shared buffer out. This would be counted as an
IOOp "write" on an IOContext IOCONTEXT_SHARED by BackendType
"checkpointer".

Each IOOp (alloc, read, write, extend, fsync) is counted per IOContext
(local, shared, or strategy) through a call to pgstat_count_io_op().

The primary concern of these statistics is IO operations on data blocks
during the course of normal database operations. IO operations done by,
for example, the archiver or syslogger are not counted in these
statistics.

The IOCONTEXT_STRATEGY IOContext concerns IO operations on buffers as
part of a BufferAccessStrategy.

IOOP_ALLOC IOOps are counted in IOCONTEXT_SHARED and IOCONTEXT_LOCAL
IOContexts whenever a buffer is acquired through [Local]BufferAlloc().

IOOP_ALLOC IOOps are counted in the IOCONTEXT_STRATEGY IOContext
whenever a buffer already in the strategy ring is reused. IOOP_WRITE
IOOps are counted in the IOCONTEXT_STRATEGY IOContext whenever the
reused dirty buffer is written out.

Stats on IOOps in all IOContexts for a given backend are counted in a
backend's local memory. A subsequent commit will expose functions for
aggregating and viewing these stats.

Author: Melanie Plageman <[email-masked]>
Reviewed-by: Andres Freund <[email-masked]>
Reviewed-by: Justin Pryzby <[email-masked]>
Reviewed-by: Kyotaro Horiguchi <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/20200124195226.lth52iydq2n2uilq%40alap3.anarazel.de
---
 src/backend/postmaster/checkpointer.c      |  12 ++
 src/backend/storage/buffer/bufmgr.c        |  60 +++++--
 src/backend/storage/buffer/freelist.c      |  21 ++-
 src/backend/storage/buffer/localbuf.c      |   5 +
 src/backend/storage/sync/sync.c            |   2 +
 src/backend/utils/activity/Makefile        |   1 +
 src/backend/utils/activity/pgstat_io_ops.c | 199 +++++++++++++++++++++
 src/include/pgstat.h                       |  53 ++++++
 src/include/storage/buf_internals.h        |   2 +-
 src/tools/pgindent/typedefs.list           |   4 +
 10 files changed, 347 insertions(+), 12 deletions(-)
 create mode 100644 src/backend/utils/activity/pgstat_io_ops.c

index e898ffad7b..a49223ee96 100644
 
@@ -986,10 +987,24 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	 */
 	Assert(!(pg_atomic_read_u32(&bufHdr->state) & BM_VALID));	/* spinlock not needed */

 	if (isExtend)
 	{
@@ -1020,6 +1035,8 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,

+			pgstat_count_io_op(IOOP_READ, io_context);
+
 			if (track_io_timing)
 			{
 				INSTR_TIME_SET_CURRENT(io_time);
@@ -1190,6 +1207,8 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	/* Loop here in case we have to try another victim buffer */
 	for (;;)
 	{
+		bool		from_ring;
+
 		/*
 		 * Ensure, while the spinlock's not yet held, that there's a free
 		 * refcount entry.
@@ -1200,7 +1219,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 		 * Select a victim buffer.  The buffer is returned with its header
 		 * spinlock still held!
 		 */
-		buf = StrategyGetBuffer(strategy, &buf_state);
+		buf = StrategyGetBuffer(strategy, &buf_state, &from_ring);

@@ -1237,6 +1256,8 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 			if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf),
 										 LW_SHARED))
 			{
+				IOContext	io_context;
+
 				/*
 				 * If using a nondefault strategy, and writing the buffer
 				 * would require a WAL flush, let the strategy decide whether
@@ -1263,13 +1284,28 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 					}
 				}
 
+				/*

 				ScheduleBufferTagForWriteback(&BackendWritebackContext,
@@ -2573,7 +2609,7 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
 	PinBuffer_Locked(bufHdr);
 	LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);

 
@@ -2823,7 +2859,7 @@ BufferGetTag(Buffer buffer, RelFileLocator *rlocator, ForkNumber *forknum,
  * as the second parameter.  If not, pass NULL.
  */
 static void
-FlushBuffer(BufferDesc *buf, SMgrRelation reln)
+FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOContext io_context)
 {
 	XLogRecPtr	recptr;
 	ErrorContextCallback errcallback;
@@ -2903,6 +2939,8 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln)
 	 */
 	bufToWrite = PageSetChecksumCopy((Page) bufBlock, buf->tag.blockNum);

@@ -3554,6 +3592,8 @@ FlushRelationBuffers(Relation rel)
 						  localpage,
 						  false);

@@ -3589,7 +3629,7 @@ FlushRelationBuffers(Relation rel)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, RelationGetSmgr(rel));
+			FlushBuffer(bufHdr, RelationGetSmgr(rel), IOCONTEXT_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr, true);
 		}
@@ -3687,7 +3727,7 @@ FlushRelationsAllBuffers(SMgrRelation *smgrs, int nrels)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, srelent->srel);
+			FlushBuffer(bufHdr, srelent->srel, IOCONTEXT_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr, true);
 		}
@@ -3897,7 +3937,7 @@ FlushDatabaseBuffers(Oid dbid)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, NULL);
+			FlushBuffer(bufHdr, NULL, IOCONTEXT_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr, true);
 		}
@@ -3924,7 +3964,7 @@ FlushOneBuffer(Buffer buffer)

 
 /*
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index 990e081aae..50d04b2b6d 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -15,6 +15,7 @@
  */
 #include "postgres.h"
 
+#include "pgstat.h"
 
+	*from_ring = false;
+
 	/*
 	 * If given a strategy object, see whether it can select a buffer. We
 	 * assume strategy objects don't need buffer_strategy_lock.
@@ -213,7 +216,22 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
 	{
 		buf = GetBufferFromRing(strategy, buf_state);
 		if (buf != NULL)
+		{
+			/*
+			 * When a strategy is in use, reused buffers from the strategy
+			 * ring will be counted as IOCONTEXT_STRATEGY allocations for the
+			 * purposes of IO Operation statistics tracking.
+			 *
+			 * However, even when a strategy is in use, if a new buffer must
+			 * be allocated from shared buffers and added to the ring, this is
+			 * counted instead as an IOCONTEXT_SHARED allocation. So, only
+			 * reused buffers are counted as having been allocated in the
+			 * IOCONTEXT_STRATEGY IOContext.
+			 */
+			*from_ring = true;
+			pgstat_count_io_op(IOOP_ALLOC, IOCONTEXT_STRATEGY);
 			return buf;
+		}
 	}
 
 	/*
 
 	/*
index 98530078a6..5d8f0c98eb 100644
@@ -18,6 +18,7 @@
 
index 9d6a9e9109..5718b52fb5 100644
 
+					pgstat_count_io_op(IOOP_FSYNC, IOCONTEXT_SHARED);
+
 					if (log_checkpoints)
 						elog(DEBUG1, "checkpoint sync: number=%d file=%s time=%.3f ms",
 							 processed,
diff --git a/src/backend/utils/activity/Makefile b/src/backend/utils/activity/Makefile
index a2e8507fd6..0098785089 100644
--- a/src/backend/utils/activity/Makefile
+++ b/src/backend/utils/activity/Makefile
@@ -22,6 +22,7 @@ OBJS = \
 	pgstat_checkpointer.o \
 	pgstat_database.o \
 	pgstat_function.o \
+	pgstat_io_ops.o \
 	pgstat_relation.o \
 	pgstat_replslot.o \
 	pgstat_shmem.o \
diff --git a/src/backend/utils/activity/pgstat_io_ops.c b/src/backend/utils/activity/pgstat_io_ops.c
new file mode 100644
index 0000000000..5cfd40068d
--- /dev/null
+++ b/src/backend/utils/activity/pgstat_io_ops.c
@@ -0,0 +1,199 @@
+/* -------------------------------------------------------------------------
+ *
+ * pgstat_io_ops.c
+ *	  Implementation of IO operation statistics.
+ *
+ * This file contains the implementation of IO operation statistics. It is kept
+ * separate from pgstat.c to enforce the line between the statistics access /
+ * storage implementation and the details about individual types of
+ * statistics.
+ *
+ * Copyright (c) 2021-2022, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/activity/pgstat_io_ops.c
+ * -------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "utils/pgstat_internal.h"
+
+static PgStat_IOContextOps pending_IOOpStats;
+
+void
+pgstat_count_io_op(IOOp io_op, IOContext io_context)
+{
+	PgStat_IOOpCounters *pending_counters;
+
+	Assert(io_context < IOCONTEXT_NUM_TYPES);
+	Assert(io_op < IOOP_NUM_TYPES);
+	Assert(pgstat_expect_io_op(MyBackendType, io_context, io_op));
+
+	pending_counters = &pending_IOOpStats.data[io_context];
+
+	switch (io_op)
+	{
+		case IOOP_ALLOC:
+			pending_counters->allocs++;
+			break;
+		case IOOP_EXTEND:
+			pending_counters->extends++;
+			break;
+		case IOOP_FSYNC:
+			pending_counters->fsyncs++;
+			break;
+		case IOOP_READ:
+			pending_counters->reads++;
+			break;
+		case IOOP_WRITE:
+			pending_counters->writes++;
+			break;
+	}
+
+}
+
+const char *
+pgstat_io_context_desc(IOContext io_context)
+{
+	switch (io_context)
+	{
+		case IOCONTEXT_LOCAL:
+			return "local";
+		case IOCONTEXT_SHARED:
+			return "shared";
+		case IOCONTEXT_STRATEGY:
+			return "strategy";
+	}
+
+	elog(ERROR, "unrecognized IOContext value: %d", io_context);
+}
+
+const char *
+pgstat_io_op_desc(IOOp io_op)
+{
+	switch (io_op)
+	{
+		case IOOP_ALLOC:
+			return "alloc";
+		case IOOP_EXTEND:
+			return "extend";
+		case IOOP_FSYNC:
+			return "fsync";
+		case IOOP_READ:
+			return "read";
+		case IOOP_WRITE:
+			return "write";
+	}
+
+	elog(ERROR, "unrecognized IOOp value: %d", io_op);
+}
+
+/*
+* IO Operation statistics are not collected for all BackendTypes.
+*
+* The following BackendTypes do not participate in the cumulative stats
+* subsystem or do not do IO operations worth reporting statistics on:
+* - Syslogger because it is not connected to shared memory
+* - Archiver because most relevant archiving IO is delegated to a
+*   specialized command or module
+* - WAL Receiver and WAL Writer IO is not tracked in pg_stat_io for now
+*
+* Function returns true if BackendType participates in the cumulative stats
+* subsystem for IO Operations and false if it does not.
+*/
+bool
+pgstat_io_op_stats_collected(BackendType bktype)
+{
+	return bktype != B_INVALID && bktype != B_ARCHIVER && bktype != B_LOGGER &&
+		bktype != B_WAL_RECEIVER && bktype != B_WAL_WRITER;
+}
+
+/*
+ * Some BackendTypes will never do IO in certain IOContexts. Check that the
+ * given BackendType is expected to do IO in the given IOContext.
+ */
+bool
+pgstat_bktype_io_context_valid(BackendType bktype, IOContext io_context)
+{
+	bool		no_strategy;
+	bool		no_local;
+
+	/*
+	 * Not all BackendTypes will use a BufferAccessStrategy.
+	 */
+	no_strategy = bktype == B_AUTOVAC_LAUNCHER || bktype ==
+		B_BG_WRITER || bktype == B_CHECKPOINTER;
+
+	/*
+	 * In core Postgres, only regular backends and WAL Sender processes
+	 * executing queries should use local buffers. Parallel workers will not
+	 * use local buffers (see InitLocalBuffers()); however, extensions
+	 * leveraging background workers have no such limitation, so track IO
+	 * Operations in IOCONTEXT_LOCAL for BackendType B_BG_WORKER.
+	 */
+	no_local = bktype == B_AUTOVAC_LAUNCHER || bktype == B_BG_WRITER || bktype
+		== B_CHECKPOINTER || bktype == B_AUTOVAC_WORKER || bktype ==
+		B_STANDALONE_BACKEND || bktype == B_STARTUP;
+
+	if (io_context == IOCONTEXT_STRATEGY && no_strategy)
+		return false;
+
+	if (io_context == IOCONTEXT_LOCAL && no_local)
+		return false;
+
+	return true;
+}
+
+/*
+ * Some BackendTypes will never do certain IOOps and some IOOps should not
+ * occur in certain IOContexts. Check that the given IO Operation is valid for
+ * the given BackendType in the given IOContext. Note that there are currently
+ * no cases of an IO Operation being invalid for a particular BackendType only
+ * within a certain IOContext.
+ */
+bool
+pgstat_io_op_valid(BackendType bktype, IOContext io_context, IOOp io_op)
+{
+	/*
+	 * Some BackendTypes should never track IO Operation statistics.
+	 */
+	Assert(pgstat_io_op_stats_collected(bktype));
+
+	if ((bktype == B_BG_WRITER || bktype == B_CHECKPOINTER) && io_op ==
+		IOOP_READ)
+		return false;
+
+	if ((bktype == B_AUTOVAC_LAUNCHER || bktype == B_BG_WRITER || bktype ==
+		 B_CHECKPOINTER) && io_op == IOOP_EXTEND)
+		return false;
+
+	/*
+	 * Temporary tables using local buffers are not logged and thus do not
+	 * require fsync'ing.
+	 *
+	 * IOOP_FSYNC IOOps done by a backend using a BufferAccessStrategy are
+	 * counted in the IOCONTEXT_SHARED IOContext. See comment in
+	 * ForwardSyncRequest() for more details.
+	 */
+	if ((io_context == IOCONTEXT_LOCAL || io_context == IOCONTEXT_STRATEGY) &&
+		io_op == IOOP_FSYNC)
+		return false;
+
+	return true;
+}
+
+bool
+pgstat_expect_io_op(BackendType bktype, IOContext io_context, IOOp io_op)
+{
+	if (!pgstat_io_op_stats_collected(bktype))
+		return false;
+
+	if (!pgstat_bktype_io_context_valid(bktype, io_context))
+		return false;
+
+	if (!(pgstat_io_op_valid(bktype, io_context, io_op)))
+		return false;
+
+	return true;
+}
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index ac28f813b4..d91bff3be7 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -276,6 +276,44 @@ typedef struct PgStat_CheckpointerStats
 	PgStat_Counter buf_fsync_backend;
 } PgStat_CheckpointerStats;
 
+/*
@@ -453,6 +491,21 @@ extern void pgstat_report_checkpointer(void);

+/*
+extern bool pgstat_io_op_valid(BackendType bktype, IOContext io_context, IOOp io_op);
index 406db6be78..50d7e586e9 100644
@@ -392,7 +392,7 @@ extern void ScheduleBufferTagForWriteback(WritebackContext *context, BufferTag *
 
 /* freelist.c */
 extern BufferDesc *StrategyGetBuffer(BufferAccessStrategy strategy,
-									 uint32 *buf_state);
+									 uint32 *buf_state, bool *from_ring);
 extern void StrategyFreeBuffer(BufferDesc *buf);
 extern bool StrategyRejectBuffer(BufferAccessStrategy strategy,
 								 BufferDesc *buf);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index a4a4e356e5..5db1caa112 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1106,7 +1106,9 @@ ID
 INFIX
 INT128
 INTERFACE_INFO
+IOContext
 IOFuncSelector
+IOOp
 IPCompareMethod
 ITEM
 IV
@@ -2038,6 +2040,8 @@ PgStat_FetchConsistency
 PgStat_FunctionCallUsage
 PgStat_FunctionCounts
 PgStat_HashKey
+PgStat_IOContextOps
+PgStat_IOOpCounters
 PgStat_Kind
 PgStat_KindInfo
 PgStat_LocalState
-- 
2.34.1

From 8d43b0f8975640c8945702f0abf90e653af98868 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Mon, 22 Aug 2022 11:35:20 -0400
Subject: [PATCH v29 2/3] Aggregate IO operation stats per BackendType

Because not all BackendType, IOOp, IOContext combinations are valid, the
validity of the stats is checked before flushing pending stats and
before reading in the existing stats file to shared memory.

Author: Melanie Plageman <[email-masked]>
Reviewed-by: Andres Freund <[email-masked]>
Reviewed-by: Justin Pryzby <[email-masked]>
Reviewed-by: Kyotaro Horiguchi <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/20200124195226.lth52iydq2n2uilq%40alap3.anarazel.de
---
 doc/src/sgml/monitoring.sgml                  |   2 +
 src/backend/utils/activity/pgstat.c           |  35 ++++
 src/backend/utils/activity/pgstat_bgwriter.c  |   7 +-
 .../utils/activity/pgstat_checkpointer.c      |   7 +-
 src/backend/utils/activity/pgstat_io_ops.c    | 153 ++++++++++++++++++
 src/backend/utils/activity/pgstat_relation.c  |  15 +-
 src/backend/utils/activity/pgstat_shmem.c     |   4 +
 src/backend/utils/activity/pgstat_wal.c       |   4 +-
 src/backend/utils/adt/pgstatfuncs.c           |   4 +-
 src/include/miscadmin.h                       |   2 +
 src/include/pgstat.h                          |  81 +++++++++-
 src/include/utils/pgstat_internal.h           |  37 +++++
 src/tools/pgindent/typedefs.list              |   3 +
 13 files changed, 347 insertions(+), 7 deletions(-)

index 6224c498c2..6c95a9d5de 100644

 

@@ -1321,6 +1334,14 @@ pgstat_write_statsfile(void)
 	pgstat_build_snapshot_fixed(PGSTAT_KIND_CHECKPOINTER);
 	write_chunk_s(fpout, &pgStatLocal.snapshot.checkpointer);
 
+	/*
@@ -1495,6 +1516,20 @@ pgstat_read_statsfile(void)
 
+	/*
+		pgstat_backend_io_stats_assert_well_formed(shmem->io_ops.stats[bktype].data,
+												   bktype);
+		if (!read_chunk_s(fpin, &shmem->io_ops.stats[bktype].data))

 /*
 
 /*

 /*
 
 /*
index 5cfd40068d..8cd1239622 100644
 
 static PgStat_IOContextOps pending_IOOpStats;
+bool		have_ioopstats = false;
+
+
+/*
+ * Helper function to accumulate source PgStat_IOOpCounters into target
+ * PgStat_IOOpCounters. If either of the passed-in PgStat_IOOpCounters are
+ * members of PgStatShared_IOContextOps, the caller is responsible for ensuring
+ * that the appropriate lock is held.
+ */
+static void
+pgstat_accum_io_op(PgStat_IOOpCounters *target, PgStat_IOOpCounters *source, IOOp io_op)
+{
+	switch (io_op)
+	{
+		case IOOP_ALLOC:
+			target->allocs += source->allocs;
+			return;
+		case IOOP_EXTEND:
+			target->extends += source->extends;
+			return;
+		case IOOP_FSYNC:
+			target->fsyncs += source->fsyncs;
+			return;
+		case IOOP_READ:
+			target->reads += source->reads;
+			return;
+		case IOOP_WRITE:
+			target->writes += source->writes;
+			return;
+	}
+
+	elog(ERROR, "unrecognized IOOp value: %d", io_op);
+}
 
 void
 pgstat_count_io_op(IOOp io_op, IOContext io_context)
@@ -51,6 +84,77 @@ pgstat_count_io_op(IOOp io_op, IOContext io_context)
 			break;
 	}
 
+	have_ioopstats = true;
+}
+
+	expect_backend_stats = pgstat_io_op_stats_collected(MyBackendType);
+			if (!(pgstat_io_op_valid(MyBackendType, io_context, io_op)))
 
 const char *
@@ -89,6 +193,55 @@ pgstat_io_op_desc(IOOp io_op)
 	elog(ERROR, "unrecognized IOOp value: %d", io_op);
 }
 
+void
 
 /*

 /*

 /*
index ac98918688..bfbe1c7942 100644
 
 /*
 
 /*
index 4cca30aae7..dc3a1a26a4 100644
@@ -2085,6 +2085,8 @@ pg_stat_reset_shared(PG_FUNCTION_ARGS)
@@ -2093,7 +2095,7 @@ pg_stat_reset_shared(PG_FUNCTION_ARGS)
 
 	PG_RETURN_VOID();
 }
index 65cf4ba50f..194bae52c8 100644

index d91bff3be7..b9604bb2ad 100644

 
@@ -496,6 +503,7 @@ extern PgStat_CheckpointerStats *pgstat_fetch_stat_checkpointer(void);
  */

@@ -505,6 +513,77 @@ extern bool pgstat_bktype_io_context_valid(BackendType bktype, IOContext io_cont
 extern bool pgstat_io_op_valid(BackendType bktype, IOContext io_context, IOOp io_op);
 extern bool pgstat_expect_io_op(BackendType bktype, IOContext io_context, IOOp io_op);
 
+/*
+ * Functions to assert that invalid IO Operation counters are zero.
+ */
+static inline void
+pgstat_io_context_ops_assert_zero(PgStat_IOOpCounters *counters)
+{
+	Assert(counters->allocs == 0 && counters->extends == 0 &&
+		   counters->fsyncs == 0 && counters->reads == 0 &&
+		   counters->writes == 0);
+}
+
+static inline void
+pgstat_io_op_assert_zero(PgStat_IOOpCounters *counters, IOOp io_op)
+{
+	switch (io_op)
+	{
+		case IOOP_ALLOC:
+			Assert(counters->allocs == 0);
+			return;
+		case IOOP_EXTEND:
+			Assert(counters->extends == 0);
+			return;
+		case IOOP_FSYNC:
+			Assert(counters->fsyncs == 0);
+			return;
+		case IOOP_READ:
+			Assert(counters->reads == 0);
+			return;
+		case IOOP_WRITE:
+			Assert(counters->writes == 0);
+			return;
+	}
+
+	/* Should not reach here */
+	Assert(false);
+}
+
+/*
+ * Assert that stats have not been counted for any combination of IOContext and
+ * IOOp which are not valid for the passed-in BackendType. The passed-in array
+ * of PgStat_IOOpCounters must contain stats from the BackendType specified by
+ * the second parameter. Caller is responsible for any locking if the passed-in
+ * array of PgStat_IOOpCounters is a member of PgStatShared_IOContextOps.
+ */
+static inline void
+pgstat_backend_io_stats_assert_well_formed(PgStat_IOOpCounters
+		backend_io_context_ops[IOCONTEXT_NUM_TYPES], BackendType bktype)
+{
+	bool		expect_backend_stats = true;
+
+	if (!pgstat_io_op_stats_collected(bktype))
+		expect_backend_stats = false;
+
+	for (int io_context = 0; io_context < IOCONTEXT_NUM_TYPES; io_context++)
+	{
+		if (!expect_backend_stats ||
+			!pgstat_bktype_io_context_valid(bktype, io_context))
+		{
+			pgstat_io_context_ops_assert_zero(&backend_io_context_ops[io_context]);
+			continue;
+		}
+
+		for (int io_op = 0; io_op < IOOP_NUM_TYPES; io_op++)
+		{
+			if (!pgstat_io_op_valid(bktype, io_context, io_op))
+				pgstat_io_op_assert_zero(&backend_io_context_ops[io_context],
+										 io_op);
+		}
+	}
+}
+
 
 /*
  * Functions in pgstat_database.c
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 901d2041d6..289610249a 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -329,6 +329,26 @@ typedef struct PgStatShared_Checkpointer
 	PgStat_CheckpointerStats reset_offset;
 } PgStatShared_Checkpointer;
 
+typedef struct PgStatShared_IOContextOps
+{
+	/*
+	 * lock protects ->data
+	 * If this PgStatShared_IOContextOps is
+	 * PgStatShared_BackendIOContextOps->stats[0], lock also protects
+	 * PgStatShared_BackendIOContextOps->stat_reset_timestamp.
+	 */
+	LWLock		lock;
+	PgStat_IOOpCounters data[IOCONTEXT_NUM_TYPES];
+} PgStatShared_IOContextOps;
+
+typedef struct PgStatShared_BackendIOContextOps
+{
+	/* ->stats_reset_timestamp is protected by ->stats[0].lock */
+	TimestampTz stat_reset_timestamp;
+	PgStatShared_IOContextOps stats[BACKEND_NUM_TYPES];
+} PgStatShared_BackendIOContextOps;
+
+
 typedef struct PgStatShared_SLRU
 {
 	/* lock protects ->stats */
@@ -419,6 +439,7 @@ typedef struct PgStat_ShmemControl
 	PgStatShared_Archiver archiver;
 	PgStatShared_BgWriter bgwriter;
 	PgStatShared_Checkpointer checkpointer;
+	PgStatShared_BackendIOContextOps io_ops;
 	PgStatShared_SLRU slru;
 	PgStatShared_Wal wal;
 } PgStat_ShmemControl;
@@ -442,6 +463,8 @@ typedef struct PgStat_Snapshot

 
 	PgStat_WalStats wal;
@@ -549,6 +572,15 @@ extern void pgstat_database_reset_timestamp_cb(PgStatShared_Common *header, Time
 extern bool pgstat_function_flush_cb(PgStat_EntryRef *entry_ref, bool nowait);

+/*
@@ -641,6 +673,11 @@ extern void pgstat_create_transactional(PgStat_Kind kind, Oid dboid, Oid objoid)

+/*
 
 /*
  * Variables in pgstat_slru.c
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 5db1caa112..71e5f62e40 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2017,12 +2017,14 @@ PgFdwRelationInfo
 PgFdwScanState
 PgIfAddrCallback
 PgStatShared_Archiver
+PgStatShared_BackendIOContextOps
 PgStatShared_BgWriter
 PgStatShared_Checkpointer
 PgStatShared_Common
 PgStatShared_Database
 PgStatShared_Function
 PgStatShared_HashEntry
+PgStatShared_IOContextOps
 PgStatShared_Relation
 PgStatShared_ReplSlot
 PgStatShared_SLRU
@@ -2030,6 +2032,7 @@ PgStatShared_Subscription
 PgStatShared_Wal
 PgStat_ArchiverStats
 PgStat_BackendFunctionEntry
+PgStat_BackendIOContextOps
 PgStat_BackendSubEntry
 PgStat_BgWriterStats
 PgStat_CheckpointerStats
-- 
2.34.1

---

From: Melanie Plageman <[email-masked]>
Date: Tue, 27 Sep 2022 14:20:44 -0400

v30 attached
rebased and pgstat_io_ops.c builds with meson now
also, I tested with pgstat_report_stat() only flushing when forced and
tests still pass
From 153b06200b36891c9e07df526a86dbd913e36e3e Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 11 Aug 2022 18:28:50 -0400
Subject: [PATCH v30 3/3] Add system view tracking IO ops per backend type

 
+     <row>
index 55f7ec79e0..f3c10ae711 100644
@@ -1114,6 +1114,18 @@ CREATE VIEW pg_stat_bgwriter AS
 
index 88c737f4f9..7815639350 100644
 
+/*
index a07e737a33..1c263abec8 100644
 
index 9dd137415e..d313420d67 100644
@@ -1868,6 +1868,15 @@ pg_stat_gssapi| SELECT s.pid,
index 6a10dc462b..d1a6502c54 100644
@@ -907,4 +907,206 @@ SELECT pg_stat_get_subscription_stats(NULL);
  
 (1 row)
 
index a6b0e9e042..bc8fa404a3 100644
@@ -443,4 +443,108 @@ SELECT pg_stat_get_replication_slot(NULL);

From e0df4be80d5b47a2837ad3c993798de6b4e9b618 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Mon, 22 Aug 2022 11:35:20 -0400
Subject: [PATCH v30 2/3] Aggregate IO operation stats per BackendType

index 609f0b1ad8..e66dde0ea5 100644

 

 
+	/*
 
+	/*

 /*
 
 /*

 /*
 
 /*

 void
 
+	have_ioopstats = true;
+}
+
 
 const char *
 
+void
 
 /*

 /*

 /*
index 9a4f037959..275a7be166 100644
 
 /*
 
 /*
index be15b4b2e5..88c737f4f9 100644
 
 	PG_RETURN_VOID();
 }
index ee48e392ed..2b73deda2f 100644

index ccc836a37f..d35072968b 100644

 

 
+/*
 
 /*
index 40a3602855..95f19cae99 100644

 
 
 	PgStat_WalStats wal;

+/*

+/*
 
 /*
  * Variables in pgstat_slru.c
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 67218ec6f2..33c9362257 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -2005,12 +2005,14 @@ PgFdwRelationInfo
 PgFdwScanState
 PgIfAddrCallback
 PgStatShared_Archiver
+PgStatShared_BackendIOContextOps
 PgStatShared_BgWriter
 PgStatShared_Checkpointer
 PgStatShared_Common
 PgStatShared_Database
 PgStatShared_Function
 PgStatShared_HashEntry
+PgStatShared_IOContextOps
 PgStatShared_Relation
 PgStatShared_ReplSlot
 PgStatShared_SLRU
@@ -2018,6 +2020,7 @@ PgStatShared_Subscription
 PgStatShared_Wal
 PgStat_ArchiverStats
 PgStat_BackendFunctionEntry
+PgStat_BackendIOContextOps
 PgStat_BackendSubEntry
 PgStat_BgWriterStats
 PgStat_CheckpointerStats
-- 
2.34.1

From 55564aca63fea479681456ff333fdbb5868589c4 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Mon, 22 Aug 2022 11:08:23 -0400
Subject: [PATCH v30 1/3] Track IO operation statistics locally

 src/backend/utils/activity/meson.build     |   1 +
 11 files changed, 348 insertions(+), 12 deletions(-)

index 5b0e531f97..3573047235 100644

 
 	if (isExtend)
 	{

 

+				/*

 

 

 

 
 /*
 
+#include "pgstat.h"
 
+	*from_ring = false;
+
 	/*
 
 	/*
 
 	/*
index 30d67d1c40..6b2f529fe7 100644
 #include "utils/guc_hooks.h"

diff --git a/src/backend/utils/activity/meson.build b/src/backend/utils/activity/meson.build
index 5b3b558a67..1038324c32 100644
--- a/src/backend/utils/activity/meson.build
+++ b/src/backend/utils/activity/meson.build
@@ -7,6 +7,7 @@ backend_sources += files(
   'pgstat_checkpointer.c',
   'pgstat_database.c',
   'pgstat_function.c',
+  'pgstat_io_ops.c',
   'pgstat_relation.c',
   'pgstat_replslot.c',
   'pgstat_shmem.c',
index ad7334a0d2..ccc836a37f 100644
 
+/*

+/*
 
 /* freelist.c */
 extern BufferDesc *StrategyGetBuffer(BufferAccessStrategy strategy,
-									 uint32 *buf_state);
+									 uint32 *buf_state, bool *from_ring);
 extern void StrategyFreeBuffer(BufferDesc *buf);
 extern bool StrategyRejectBuffer(BufferAccessStrategy strategy,
 								 BufferDesc *buf);
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 97c9bc1861..67218ec6f2 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1106,7 +1106,9 @@ ID
 INFIX
 INT128
 INTERFACE_INFO
+IOContext
 IOFuncSelector
+IOOp
 IPCompareMethod
 ITEM
 IV
@@ -2026,6 +2028,8 @@ PgStat_FetchConsistency
 PgStat_FunctionCallUsage
 PgStat_FunctionCounts
 PgStat_HashKey
+PgStat_IOContextOps
+PgStat_IOOpCounters
 PgStat_Kind
 PgStat_KindInfo
 PgStat_LocalState
-- 
2.34.1

---

From: Lukas Fittl <[email-masked]>
Date: Fri, 30 Sep 2022 16:17:25 -0700

> also, I tested with pgstat_report_stat() only flushing when forced and

First of all, I'm excited about this patch, and I think it will be a big
help to understand better which part of Postgres is producing I/O (and why).

I've paired up with Maciek (CCed) on a review of this patch and had a few
comments, focused on the user experience:

The term "strategy" as an "io_context" is hard to understand, as its not a
concept an end-user / DBA would be familiar with. Since this comes from
BufferAccessStrategyType (i.e. anything not NULL/BAS_NORMAL is treated as
"strategy"), maybe we could instead split this out into the individual
strategy types? i.e. making "strategy" three different I/O contexts
instead: "shared_bulkread", "shared_bulkwrite" and "shared_vacuum",
retaining "shared" to mean NULL / BAS_NORMAL.

Separately, could we also track buffer hits without incurring extra
overhead? (not just allocs and reads) -- Whilst we already have shared read
and hit counters in a few other places, this would help make the common
"What's my cache hit ratio" question more accurate to answer in the
presence of different shared buffer access strategies. Tracking hits could
also help for local buffers (e.g. to tune temp_buffers based on seeing a
low cache hit ratio).

Additionally, some minor notes:

- Since the stats are counting blocks, it would make sense to prefix the
view columns with "blks_", and word them in the past tense (to match
current style), i.e. "blks_written", "blks_read", "blks_extended",
"blks_fsynced" (realistically one would combine this new view with other
data e.g. from pg_stat_database or pg_stat_statements, which all use the
"blks_" prefix, and stop using pg_stat_bgwriter for this which does not use
such a prefix)

- "alloc" as a name doesn't seem intuitive (and it may be confused with
memory allocations) - whilst this is already named this way in
pg_stat_bgwriter, it feels like this is an opportunity to eventually
deprecate the column there and make this easier to understand -
specifically, maybe we can clarify that this means buffer *acquisitions*?
(either by renaming the field to "blks_acquired", or clarifying in the
documentation)

- Assuming we think this view could realistically cover all I/O produced by
Postgres in the future (thus warranting the name "pg_stat_io"), it may be
best to have an explicit list of things that are not currently tracked in
the documentation, to reduce user confusion (i.e. WAL writes are not
tracked, temporary files are not tracked, and some forms of direct writes
are not tracked, e.g. when a table moves to a different tablespace)

- In the view documentation, it would be good to explain the different
values for "io_strategy" (and what they mean)

- Overall it would be helpful if we had a dedicated documentation page on
I/O statistics that's linked from the pg_stat_io view description, and
explains how the I/O statistics tie into the various concepts of shared
buffers / buffer access strategies / etc (and what is not tracked today)

Thanks,
Lukas

-- 
Lukas Fittl

---

From: Andres Freund <[email-masked]>
Date: Sun, 2 Oct 2022 10:24:04 -0700

Hi,

> also, I tested with pgstat_report_stat() only flushing when forced and

Unfortunately tests fail in CI / cfbot. E.g.,
https://cirrus-ci.com/task/5816109319323648

https://api.cirrus-ci.com/v1/artifact/task/5816109319323648/testrun/build/testrun/main/regress/regression.diffs
diff -U3 /tmp/cirrus-ci-build/src/test/regress/expected/stats.out /tmp/cirrus-ci-build/build/testrun/main/regress/results/stats.out
--- /tmp/cirrus-ci-build/src/test/regress/expected/stats.out	2022-10-01 12:07:47.779183501 +0000
+++ /tmp/cirrus-ci-build/build/testrun/main/regress/results/stats.out	2022-10-01 12:11:38.686433303 +0000
@@ -997,6 +997,8 @@
 -- Set temp_buffers to a low value so that we can trigger writes with fewer
 -- inserted tuples.
 SET temp_buffers TO '1MB';
+ERROR:  invalid value for parameter "temp_buffers": 128
+DETAIL:  "temp_buffers" cannot be changed after any temporary tables have been accessed in the session.
 CREATE TEMPORARY TABLE test_io_local(a int, b TEXT);
 SELECT sum(alloc) AS io_sum_local_allocs_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
@@ -1037,7 +1039,7 @@
 SELECT :io_sum_local_writes_after > :io_sum_local_writes_before;
  ?column? 
 ----------
- t
+ f
 (1 row)
 
 SELECT :io_sum_local_extends_after > :io_sum_local_extends_before;

So the problem is just that something else accesses temp buffers earlier in
the same test.

That's likely because since you sent your email

commit d7e39d72ca1c6f188b400d7d58813ff5b5b79064
Author: Tom Lane <[email-masked]>
Date:   2022-09-29 12:14:39 -0400
 
    Use actual backend IDs in pg_stat_get_backend_idset() and friends.

was applied, which adds a temp table earlier in the same session.

I think the easiest way to make this robust would be to just add a reconnect
before the place you need to set temp_buffers, that way additional temp tables
won't cause a problem.

Setting the patch to waiting-for-author for now.

Greetings,

Andres Freund

---

From: Melanie Plageman <[email-masked]>
Date: Thu, 6 Oct 2022 13:42:09 -0400

v31 attached
I've also addressed failing test mentioned by Andres in [1]

On Fri, Sep 30, 2022 at 7:18 PM Lukas Fittl <[email-masked]> wrote:
>
> On Tue, Sep 27, 2022 at 11:20 AM Melanie Plageman <[email-masked]> wrote:
>
> First of all, I'm excited about this patch, and I think it will be a big help to understand better which part of Postgres is producing I/O (and why).
>

Thanks! I'm happy to hear that.

> I've paired up with Maciek (CCed) on a review of this patch and had a few comments, focused on the user experience:
>

Thanks for taking the time to review!

> The term "strategy" as an "io_context" is hard to understand, as its not a concept an end-user / DBA would be familiar with. Since this comes from BufferAccessStrategyType (i.e. anything not NULL/BAS_NORMAL is treated as "strategy"), maybe we could instead split this out into the individual strategy types? i.e. making "strategy" three different I/O contexts instead: "shared_bulkread", "shared_bulkwrite" and "shared_vacuum", retaining "shared" to mean NULL / BAS_NORMAL.

I have split strategy out into "vacuum", "bulkread", and "bulkwrite". I
thought it was less clear with shared as a prefix. If we were to have
BufferAccessStrategies in the future which acquire local buffers (for
example), we could start prefixing the columns to differentiate.

This opened up some new questions about which BufferAccessStrategies
will be employed by which BackendTypes and which IOOps will be valid in
a given BufferAccessStrategy.

I've excluded IOCONTEXT_BULKREAD and IOCONTEXT_BULKWRITE for autovacuum
worker -- though those may not be inherently invalid, they seem not to
be done now and added extra rows to the view.

I've also disallowed IOOP_EXTEND for IOCONTEXT_BULKREAD.

> Separately, could we also track buffer hits without incurring extra overhead? (not just allocs and reads) -- Whilst we already have shared read and hit counters in a few other places, this would help make the common "What's my cache hit ratio" question more accurate to answer in the presence of different shared buffer access strategies. Tracking hits could also help for local buffers (e.g. to tune temp_buffers based on seeing a low cache hit ratio).

I've started tracking hits and added "hit" to the view.
I added IOOP_HIT and IOOP_ACQUIRE to those IOOps disallowed for
checkpointer and bgwriter.

I have added tests for hit, but I'm not sure I can keep them. It seems
like they might fail if the blocks are evicted between the first and
second time I try to read them.

> - Since the stats are counting blocks, it would make sense to prefix the view columns with "blks_", and word them in the past tense (to match current style), i.e. "blks_written", "blks_read", "blks_extended", "blks_fsynced" (realistically one would combine this new view with other data e.g. from pg_stat_database or pg_stat_statements, which all use the "blks_" prefix, and stop using pg_stat_bgwriter for this which does not use such a prefix)

I have changed the column names to be in the past tense.

There are no columns equivalent to "dirty" or "misses" from the other
views containing information on buffer hits/block reads/writes/etc. I'm
not sure whether or not those make sense in this context.

Because we want to add non-block-oriented IO in the future (like
temporary file IO) to this view and want to use the same "read",
"written", "extended" columns, I would prefer not to prefix the columns
with "blks_". I have added a column "unit" which would contain the unit
in which read, written, and extended are in. Unfortunately, fsyncs are
not per block, so "unit" doesn't really work for this. I documented
this.

The most correct thing to do to accommodate block-oriented and
non-block-oriented IO would be to specify all the values in bytes.
However, I would like this view to be usable visually (as opposed to
just in scripts and by tools). The only current value of unit is
"block_size" which could potentially be combined with the value of the
GUC to get bytes.

I've hard-coded the string "block_size" into the view generation
function pg_stat_get_io(), so, if this idea makes sense, perhaps I
should do something better there.

> - "alloc" as a name doesn't seem intuitive (and it may be confused with memory allocations) - whilst this is already named this way in pg_stat_bgwriter, it feels like this is an opportunity to eventually deprecate the column there and make this easier to understand - specifically, maybe we can clarify that this means buffer *acquisitions*? (either by renaming the field to "blks_acquired", or clarifying in the documentation)

I have renamed it to acquired. It doesn't overlap completely with
buffers_alloc in pg_stat_bgwriter, so I didn't mention that in docs.

> - Assuming we think this view could realistically cover all I/O produced by Postgres in the future (thus warranting the name "pg_stat_io"), it may be best to have an explicit list of things that are not currently tracked in the documentation, to reduce user confusion (i.e. WAL writes are not tracked, temporary files are not tracked, and some forms of direct writes are not tracked, e.g. when a table moves to a different tablespace)

I have added this to the docs. The list is not exhaustive, so I would
love to get feedback on if there are other specific examples of IO which
is using smgr* directly that users will wonder about and I should call
out.

> - In the view documentation, it would be good to explain the different values for "io_strategy" (and what they mean)

I have added this and would love feedback on my docs additions.

> - Overall it would be helpful if we had a dedicated documentation page on I/O statistics that's linked from the pg_stat_io view description, and explains how the I/O statistics tie into the various concepts of shared buffers / buffer access strategies / etc (and what is not tracked today)

I haven't done this yet. How specific were you thinking -- like
interpretations of all the combinations and what to do with what you
see? Like you should run pg_prewarm if you see X? Specific checkpointer
or bgwriter GUCs to change? Or just links to other docs pages on
recommended tunings?

Were you imagining the other IO statistics views (like
pg_statio_all_tables and pg_stat_database) also being included in this
page? Like would it be a comprehensive guide to IO statistics and what
their significance/purposes are?

- Melanie

[1] https://www.postgresql.org/message-id/20221002172404.xyzhftbedh4zpio2%40awork3.anarazel.de
From 0ee5cda16066edbbe0b992cae7272c9e2671d1a7 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 6 Oct 2022 12:23:38 -0400
Subject: [PATCH v31 2/3] Aggregate IO operation stats per BackendType

Some BackendTypes will not flush their pending statistics at regular
intervals and explicitly call pgstat_flush_io_ops() during the course of
normal operations to flush their backend-local IO operation statistics
to shared memory in a timely manner.

Author: Melanie Plageman <[email-masked]>
Reviewed-by: Andres Freund <[email-masked]>
Reviewed-by: Justin Pryzby <[email-masked]>
Reviewed-by: Kyotaro Horiguchi <[email-masked]>
Reviewed-by: Maciek Sakrejda <[email-masked]>
Reviewed-by: Lukas Fittl <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/20200124195226.lth52iydq2n2uilq%40alap3.anarazel.de
---
 doc/src/sgml/monitoring.sgml                  |   2 +
 src/backend/utils/activity/pgstat.c           |  35 ++++
 src/backend/utils/activity/pgstat_bgwriter.c  |   7 +-
 .../utils/activity/pgstat_checkpointer.c      |   7 +-
 src/backend/utils/activity/pgstat_io_ops.c    | 157 ++++++++++++++++++
 src/backend/utils/activity/pgstat_relation.c  |  15 +-
 src/backend/utils/activity/pgstat_shmem.c     |   4 +
 src/backend/utils/activity/pgstat_wal.c       |   4 +-
 src/backend/utils/adt/pgstatfuncs.c           |   4 +-
 src/include/miscadmin.h                       |   2 +
 src/include/pgstat.h                          |  83 ++++++++-
 src/include/utils/pgstat_internal.h           |  37 +++++
 src/tools/pgindent/typedefs.list              |   3 +
 13 files changed, 353 insertions(+), 7 deletions(-)

index 342b20ebeb..14dfd650f8 100644

 

 
+	/*
 
+	/*

 /*
 
 /*

 /*
 
 /*
index cea46322a7..86d403fe55 100644
@@ -20,6 +20,42 @@
 
+		case IOOP_ACQUIRE:
+			target->acquires += source->acquires;
+		case IOOP_HIT:
+			target->hits += source->hits;
 
 void
 pgstat_count_io_op(IOOp io_op, IOContext io_context)
@@ -54,6 +90,77 @@ pgstat_count_io_op(IOOp io_op, IOContext io_context)
 			break;
 	}
 
+	have_ioopstats = true;
+}
+
+ * acquired. Otherwise, return false.
 
 const char *
@@ -98,6 +205,56 @@ pgstat_io_op_desc(IOOp io_op)
 	elog(ERROR, "unrecognized IOOp value: %d", io_op);
 }
 
+void
+			backends_stats_snap->stat_reset_timestamp =
+				backends_stats_shmem->stat_reset_timestamp;
 
 /*

 /*

 /*
 
 /*
 
 /*
index eadd8464ff..edd73e5c25 100644
@@ -2071,6 +2071,8 @@ pg_stat_reset_shared(PG_FUNCTION_ARGS)
@@ -2079,7 +2081,7 @@ pg_stat_reset_shared(PG_FUNCTION_ARGS)
 
 	PG_RETURN_VOID();
 }
index e7ebea4ff4..bf97162e83 100644

 extern const char *GetBackendTypeDesc(BackendType backendType);
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 472e0def97..0d3d5e28a1 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -49,6 +49,7 @@ typedef enum PgStat_Kind
 	PGSTAT_KIND_ARCHIVER,
 	PGSTAT_KIND_BGWRITER,
 	PGSTAT_KIND_CHECKPOINTER,
+	PGSTAT_KIND_IOOPS,
 	PGSTAT_KIND_SLRU,
 	PGSTAT_KIND_WAL,
 } PgStat_Kind;
@@ -243,7 +244,7 @@ typedef struct PgStat_TableXactStatus
  * ------------------------------------------------------------
  */

 typedef struct PgStat_ArchiverStats
 {
@@ -319,6 +320,12 @@ typedef struct PgStat_IOContextOps
 	PgStat_IOOpCounters data[IOCONTEXT_NUM_TYPES];
 } PgStat_IOContextOps;
 
@@ -501,6 +508,7 @@ extern PgStat_CheckpointerStats *pgstat_fetch_stat_checkpointer(void);
  */

@@ -512,6 +520,79 @@ extern bool pgstat_expect_io_op(BackendType bktype, IOContext io_context, IOOp i
 
 /* IO stats translation function in freelist.c */
 extern IOContext IOContextForStrategy(BufferAccessStrategy bas);
+/*
+	Assert(counters->acquires == 0 && counters->extends == 0 &&
+		case IOOP_ACQUIRE:
+			Assert(counters->acquires == 0);
+		case IOOP_HIT:
+			Assert(counters->hits == 0);

 /*

 
 
 	PgStat_WalStats wal;

+/*

+/*
 
 /*

From 275222925d4d207749c81982467dfc742a5c1d3e Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 6 Oct 2022 12:24:42 -0400
Subject: [PATCH v31 3/3] Add system view tracking IO ops per backend type

Add pg_stat_io, a system view which tracks the number of IOOps
(acquires, extends, fsyncs, hits, reads, and writes) 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 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 row for the relevant
BufferAccessStrategy IOContext 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 fsync'd so cells for all BackendTypes for IOCONTEXT_LOCAL and
IOOP_FSYNC will be NULL.

In anticipation of tracking WAL IO and non-block-oriented IO (such as
temporary file IO), the "unit" column specifies the unit of the
"acquired", "read", "written", and "extended" columns for a given row.

Author: Melanie Plageman <[email-masked]>
Reviewed-by: Andres Freund <[email-masked]>
Reviewed-by: Justin Pryzby <[email-masked]>
Reviewed-by: Kyotaro Horiguchi <[email-masked]>
Reviewed-by: Maciek Sakrejda <[email-masked]>
Reviewed-by: Lukas Fittl <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/20200124195226.lth52iydq2n2uilq%40alap3.anarazel.de
---
 doc/src/sgml/monitoring.sgml         | 200 ++++++++++++++++++++-
 src/backend/catalog/system_views.sql |  14 ++
 src/backend/utils/adt/pgstatfuncs.c  | 123 +++++++++++++
 src/include/catalog/pg_proc.dat      |   9 +
 src/test/regress/expected/rules.out  |  11 ++
 src/test/regress/expected/stats.out  | 256 +++++++++++++++++++++++++++
 src/test/regress/sql/stats.sql       | 134 ++++++++++++++
 7 files changed, 745 insertions(+), 2 deletions(-)

index 14dfd650f8..9e17d1e1ec 100644
 
+     <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,195 @@ 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).
+      </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.
+       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>acquired</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Acquisitions of memory by this <varname>backend_type</varname> for
+       performing IO operations in this <varname>io_context</varname>. For
+       block-oriented IO, <varname>acquired</varname> is the number of buffers
+       acquired or reused as part of a buffer access strategy.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>hit</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Relevant only for block-based IO of data accessed in the course of
+       satisfying queries, <varname>hit</varname> is the number of number of
+       accesses of blocks already located in a
+       <productname>PostgreSQL</productname> buffer in this specified
+       <varname>io_context</varname> by this <varname>backend_type</varname>.
+      </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
+       memory or 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 in which the acquired, read, written, and extended columns can
+      be interpreted. Currently <varname>block_size</varname> is the only
+      possible value. Reads, writes, and extends of relation data are done in
+      <varname>block_size</varname> units. Future values could include
+      <varname>wal_block_size</varname>, once WAL IO is tracked in this view,
+      and <quote>bytes</quote>, once non-block-oriented IO such as temp files
+      is tracked here.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>fsynced</structfield> <type>bigint</type>
+      </para>
+       <para>
+       Number of fsyncs performed by this <varname>backend_type</varname> for
+       the purpose of persisting data present in this
+       <varname>io_context</varname>. <literal>fsyncs</literal> are done at
+       segment boundaries so <varname>unit</varname> does not apply to
+       <varname>fsynced</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>fsynced</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 55f7ec79e0..6d6c5c6260 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1114,6 +1114,20 @@ 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.acquired,
+       b.hit,
+       b.read,
+       b.written,
+       b.extended,
+       b.unit,
+       b.fsynced,
+       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 edd73e5c25..6044cc35d1 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1712,6 +1712,129 @@ pg_stat_get_buf_alloc(PG_FUNCTION_ARGS)
 	PG_RETURN_INT64(pgstat_fetch_stat_bgwriter()->buf_alloc);
 }
 
+/*
+	IO_COL_HITS,
+	IO_COL_UNIT,
+		case IOOP_ACQUIRE:
+		case IOOP_HIT:
+			return IO_COL_HITS;
+			 * Some combinations of IOContext and BackendType are not valid
+			 * for any type of IOOp. In such cases, omit the entire row from
+			 * the view.
+			values[IO_COL_ALLOCS] = Int64GetDatum(counters->acquires);
+			values[IO_COL_HITS] = Int64GetDatum(counters->hits);
+			values[IO_COL_UNIT] = CStringGetTextDatum("block_size");
index 68bb032d3e..f7341c51ca 100644
@@ -5649,6 +5649,15 @@
 
+{ oid => '8459', descr => 'statistics: per backend type IO statistics',
+  proname => 'pg_stat_get_io', provolatile => 'v',
+  prorows => '14', proretset => 't',
+  proparallel => 'r', prorettype => 'record', proargtypes => '',
+  proallargtypes => '{text,text,int8,int8,int8,int8,int8,text,int8,timestamptz}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{backend_type,io_context,acquired,hit,read,written,extended,unit,fsynced,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 9dd137415e..8c00214958 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1868,6 +1868,17 @@ 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.acquired,
+    b.hit,
+    b.read,
+    b.written,
+    b.extended,
+    b.unit,
+    b.fsynced,
+    b.stats_reset
+   FROM pg_stat_get_io() b(backend_type, io_context, acquired, hit, read, written, extended, unit, fsynced, 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 f701da2069..779fb4d398 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -918,4 +918,260 @@ SELECT pg_stat_get_subscription_stats(NULL);
  
 (1 row)
 
+-- Test that the following operations are tracked in pg_stat_io:
+-- - acquisitions of shared buffers for IO operations
+-- - reads of target blocks into shared buffers
+-- - shared buffer cache hits when target blocks reside in shared buffers
+-- - writes of shared buffers
+-- - extends of relations using shared buffers
+-- - fsyncs done to ensure the durability of data dirtying shared buffers
+SELECT sum(acquired) AS io_sum_shared_acquisitions_before FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT sum(hit) AS io_sum_shared_hits_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(fsynced) 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
+-- 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(acquired) AS io_sum_shared_acquisitions_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(fsynced) AS io_sum_shared_fsyncs_after FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT :io_sum_shared_acquisitions_after > :io_sum_shared_acquisitions_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)
+
+-- Select from the table again once it is in shared buffers. There should be
+-- some hits recorded in pg_stat_io.
+SELECT sum(hit) AS io_sum_shared_hits_after FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT :io_sum_shared_hits_after > :io_sum_shared_hits_before;
+ ?column? 
+----------
+ t
+(1 row)
+
+DROP TABLE test_io_shared;
+DROP TABLESPACE test_io_shared_stats_tblspc;
+-- Test that acquisitions of local buffers, reads of temporary table blocks
+-- into local buffers, temporary table block cache hits in local buffers,
+-- writes of local buffers, and extends of temporary tables are tracked in
+-- pg_stat_io.
+-- 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(acquired) AS io_sum_local_acquisitions_before FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT sum(hit) AS io_sum_local_hits_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)
+
+-- Query tuples in local buffers to ensure new local buffer cache hits.
+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(acquired) AS io_sum_local_acquisitions_after FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT sum(hit) AS io_sum_local_hits_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_acquisitions_after > :io_sum_local_acquisitions_before;
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT :io_sum_local_reads_after > :io_sum_local_reads_before;
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT :io_sum_local_hits_after > :io_sum_local_hits_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
+-- acquisitions and reads.
+SET wal_skip_threshold = '1 kB';
+SELECT sum(acquired) AS io_sum_vac_strategy_acquisitions_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(acquired) AS io_sum_vac_strategy_acquisitions_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_acquisitions_after > :io_sum_vac_strategy_acquisitions_before;
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT :io_sum_vac_strategy_reads_after > :io_sum_vac_strategy_reads_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(acquired) + sum(extended) + sum(fsynced) + 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(acquired) + sum(extended) + sum(fsynced) + 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 eb081f65a4..b4f4359035 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -449,4 +449,138 @@ 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 for IO operations
+-- - reads of target blocks into shared buffers
+-- - shared buffer cache hits when target blocks reside in shared buffers
+-- - writes of shared buffers
+-- - extends of relations using shared buffers
+-- - fsyncs done to ensure the durability of data dirtying shared buffers
+SELECT sum(acquired) AS io_sum_shared_acquisitions_before FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT sum(hit) AS io_sum_shared_hits_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(fsynced) 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
+-- 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(acquired) AS io_sum_shared_acquisitions_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(fsynced) AS io_sum_shared_fsyncs_after FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT :io_sum_shared_acquisitions_after > :io_sum_shared_acquisitions_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;
+-- Select from the table again once it is in shared buffers. There should be
+-- some hits recorded in pg_stat_io.
+SELECT sum(hit) AS io_sum_shared_hits_after FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT :io_sum_shared_hits_after > :io_sum_shared_hits_before;
+DROP TABLE test_io_shared;
+DROP TABLESPACE test_io_shared_stats_tblspc;
+
+-- Test that acquisitions of local buffers, reads of temporary table blocks
+-- into local buffers, temporary table block cache hits in local buffers,
+-- writes of local buffers, and extends of temporary tables are tracked in
+-- pg_stat_io.
+
+-- 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(acquired) AS io_sum_local_acquisitions_before FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT sum(hit) AS io_sum_local_hits_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;
+-- Query tuples in local buffers to ensure new local buffer cache hits.
+SELECT COUNT(*) FROM test_io_local;
+SELECT pg_stat_force_next_flush();
+SELECT sum(acquired) AS io_sum_local_acquisitions_after FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT sum(hit) AS io_sum_local_hits_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_acquisitions_after > :io_sum_local_acquisitions_before;
+SELECT :io_sum_local_reads_after > :io_sum_local_reads_before;
+SELECT :io_sum_local_hits_after > :io_sum_local_hits_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
+-- acquisitions and reads.
+SET wal_skip_threshold = '1 kB';
+SELECT sum(acquired) AS io_sum_vac_strategy_acquisitions_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(acquired) AS io_sum_vac_strategy_acquisitions_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_acquisitions_after > :io_sum_vac_strategy_acquisitions_before;
+SELECT :io_sum_vac_strategy_reads_after > :io_sum_vac_strategy_reads_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(acquired) + sum(extended) + sum(fsynced) + sum(read) + sum(written) AS io_stats_pre_reset FROM pg_stat_io \gset
+SELECT pg_stat_reset_shared('io');
+SELECT sum(acquired) + sum(extended) + sum(fsynced) + 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

From 6891009af39bd9fa66824a23fc34e37e5505d862 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 6 Oct 2022 12:23:25 -0400
Subject: [PATCH v31 1/3] Track IO operation statistics locally

Introduce "IOOp", an IO operation done by a backend, and "IOContext",
the IO source, target, or type done by a backend. For example, the
checkpointer may write a shared buffer out. This would be counted as an
IOOp "written" on an IOContext IOCONTEXT_SHARED by BackendType
"checkpointer".

Each IOOp (acquire, hit, read, write, extend, fsync) is counted per
IOContext (bulkread, bulkwrite, local, shared, or vacuum) through a call
to pgstat_count_io_op().

The primary concern of these statistics is IO operations on data blocks
during the course of normal database operations. IO operations done by,
for example, the archiver or syslogger are not counted in these
statistics. WAL IO, temporary file IO, and IO done directly though smgr*
functions (such as when building an index) are not yet counted but would
be useful future additions.

The IOCONTEXT_BULKREAD, IOCONTEXT_BULKWRITE, and IOCONTEXT_VACUUM
IOContexts concern IO operations on buffers as part of a
BufferAccessStrategy.

IOOP_ACQUIRE IOOps are counted in IOCONTEXT_SHARED and IOCONTEXT_LOCAL
IOContexts whenever a buffer is acquired through [Local]BufferAlloc().

IOOP_ACQUIRE IOOps are counted in the BufferAccessStrategy IOContexts
whenever a buffer already in the strategy ring is reused. IOOP_WRITE
IOOps are counted in the BufferAccessStrategy IOContexts whenever the
reused dirty buffer is written out.

Author: Melanie Plageman <[email-masked]>
Reviewed-by: Andres Freund <[email-masked]>
Reviewed-by: Justin Pryzby <[email-masked]>
Reviewed-by: Kyotaro Horiguchi <[email-masked]>
Reviewed-by: Maciek Sakrejda <[email-masked]>
Reviewed-by: Lukas Fittl <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/20200124195226.lth52iydq2n2uilq%40alap3.anarazel.de
---
 src/backend/postmaster/checkpointer.c      |  13 ++
 src/backend/storage/buffer/bufmgr.c        |  59 +++++-
 src/backend/storage/buffer/freelist.c      |  52 ++++-
 src/backend/storage/buffer/localbuf.c      |   5 +
 src/backend/storage/sync/sync.c            |   2 +
 src/backend/utils/activity/Makefile        |   1 +
 src/backend/utils/activity/meson.build     |   1 +
 src/backend/utils/activity/pgstat_io_ops.c | 234 +++++++++++++++++++++
 src/include/pgstat.h                       |  61 ++++++
 src/include/storage/buf_internals.h        |   2 +-
 src/include/storage/bufmgr.h               |   7 +-
 src/tools/pgindent/typedefs.list           |   4 +
 12 files changed, 428 insertions(+), 13 deletions(-)
 create mode 100644 src/backend/utils/activity/pgstat_io_ops.c

diff --git a/src/backend/postmaster/checkpointer.c b/src/backend/postmaster/checkpointer.c
index 5fc076fc14..4ea4e6a298 100644
--- a/src/backend/postmaster/checkpointer.c
+++ b/src/backend/postmaster/checkpointer.c
@@ -1116,6 +1116,19 @@ ForwardSyncRequest(const FileTag *ftag, SyncRequestType type)
 		if (!AmBackgroundWriterProcess())
 			CheckpointerShmem->num_backend_fsync++;
 		LWLockRelease(CheckpointerCommLock);
+
+		/*
+		 * We have no way of knowing if the current IOContext is
+		 * IOCONTEXT_SHARED or IOCONTEXT_[BULKREAD, BULKWRITE, VACUUM] at this
+		 * point, so count the fsync as being in the IOCONTEXT_SHARED
+		 * IOContext. This is probably okay, because the number of backend
+		 * fsyncs doesn't say anything about the efficacy of the
+		 * BufferAccessStrategy. And counting both fsyncs done in
+		 * IOCONTEXT_SHARED and IOCONTEXT_[BULKREAD, BULKWRITE, VACUUM] under
+		 * IOCONTEXT_SHARED is likely clearer when investigating the number of
+		 * backend fsyncs.
+		 */
+		pgstat_count_io_op(IOOP_FSYNC, IOCONTEXT_SHARED);
 		return false;
 	}
 
index 6b95381481..1c14e305c1 100644
 
@@ -833,6 +834,13 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 
 	isExtend = (blockNum == P_NEW);
 
+	if (strategy)
+		io_context = IOContextForStrategy(strategy);
+	else if (isLocalBuf)
+		io_context = IOCONTEXT_LOCAL;
+	else
+		io_context = IOCONTEXT_SHARED;
+
 	TRACE_POSTGRESQL_BUFFER_READ_START(forkNum, blockNum,
 									   smgr->smgr_rlocator.locator.spcOid,
 									   smgr->smgr_rlocator.locator.dbOid,
@@ -886,6 +894,8 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	/* if it was already in the buffer pool, we're done */
 	if (found)
 	{
+		pgstat_count_io_op(IOOP_HIT, io_context);
+
 		if (!isExtend)
 		{
 			/* Just need to update stats before we exit */
@@ -986,10 +996,14 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	 */
 	Assert(!(pg_atomic_read_u32(&bufHdr->state) & BM_VALID));	/* spinlock not needed */

 	if (isExtend)
 	{
@@ -1020,6 +1034,8 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,

+			pgstat_count_io_op(IOOP_READ, io_context);
+
 			if (track_io_timing)
 			{
 				INSTR_TIME_SET_CURRENT(io_time);
@@ -1190,6 +1206,8 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	/* Loop here in case we have to try another victim buffer */
 	for (;;)
 	{
+		bool		from_ring;
+
 		/*
 		 * Ensure, while the spinlock's not yet held, that there's a free
 		 * refcount entry.
@@ -1200,7 +1218,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 		 * Select a victim buffer.  The buffer is returned with its header
 		 * spinlock still held!
 		 */
-		buf = StrategyGetBuffer(strategy, &buf_state);
+		buf = StrategyGetBuffer(strategy, &buf_state, &from_ring);

@@ -1237,6 +1255,8 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 			if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf),
 										 LW_SHARED))
 			{
+				IOContext	io_context;
+
 				/*
 				 * If using a nondefault strategy, and writing the buffer
 				 * would require a WAL flush, let the strategy decide whether
@@ -1263,13 +1283,28 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 					}
 				}
 
+				/*
+				io_context = from_ring ? IOContextForStrategy(strategy) : IOCONTEXT_SHARED;

 				ScheduleBufferTagForWriteback(&BackendWritebackContext,
@@ -2570,7 +2605,7 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
 	PinBuffer_Locked(bufHdr);
 	LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);

 
@@ -2820,7 +2855,7 @@ BufferGetTag(Buffer buffer, RelFileLocator *rlocator, ForkNumber *forknum,
  * as the second parameter.  If not, pass NULL.
  */
 static void
-FlushBuffer(BufferDesc *buf, SMgrRelation reln)
+FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOContext io_context)
 {
 	XLogRecPtr	recptr;
 	ErrorContextCallback errcallback;
@@ -2900,6 +2935,8 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln)
 	 */
 	bufToWrite = PageSetChecksumCopy((Page) bufBlock, buf->tag.blockNum);

@@ -3551,6 +3588,8 @@ FlushRelationBuffers(Relation rel)
 						  localpage,
 						  false);

@@ -3586,7 +3625,7 @@ FlushRelationBuffers(Relation rel)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, RelationGetSmgr(rel));
+			FlushBuffer(bufHdr, RelationGetSmgr(rel), IOCONTEXT_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr);
 		}
@@ -3684,7 +3723,7 @@ FlushRelationsAllBuffers(SMgrRelation *smgrs, int nrels)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, srelent->srel);
+			FlushBuffer(bufHdr, srelent->srel, IOCONTEXT_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr);
 		}
@@ -3894,7 +3933,7 @@ FlushDatabaseBuffers(Oid dbid)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, NULL);
+			FlushBuffer(bufHdr, NULL, IOCONTEXT_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr);
 		}
@@ -3921,7 +3960,7 @@ FlushOneBuffer(Buffer buffer)

 
 /*
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index 990e081aae..5fd65c17d1 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -15,6 +15,7 @@
  */
 #include "postgres.h"
 
+#include "pgstat.h"
 
+	*from_ring = false;
+
 	/*
 	 * If given a strategy object, see whether it can select a buffer. We
 	 * assume strategy objects don't need buffer_strategy_lock.
@@ -213,7 +216,23 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
 	{
 		buf = GetBufferFromRing(strategy, buf_state);
 		if (buf != NULL)
+		{
+			/*
+			 * When a BufferAccessStrategy is in use, reused buffers from the
+			 * strategy ring will be counted as IOCONTEXT_BULKREAD,
+			 * IOCONTEXT_BULKWRITE, or IOCONTEXT_VACUUM acquisitions for the
+			 * purposes of IO Operation statistics tracking.
+			 *
+			 * However, even when a strategy is in use, if a new buffer must
+			 * be acquired from shared buffers and added to the ring, this is
+			 * counted instead as an IOCONTEXT_SHARED acquisition. So, only
+			 * reused buffers are counted as having been acquired in a
+			 * BufferAccessStrategy IOContext.
+			 */
+			*from_ring = true;
+			pgstat_count_io_op(IOOP_ACQUIRE, IOContextForStrategy(strategy));
 			return buf;
+		}
 	}
 
 	/*
@@ -247,6 +266,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
 	 * the rate of buffer consumption.  Note that buffers recycled by a
 	 * strategy object are intentionally not counted here.
 	 */
+	pgstat_count_io_op(IOOP_ACQUIRE, IOCONTEXT_SHARED);
 	pg_atomic_fetch_add_u32(&StrategyControl->numBufferAllocs, 1);
 
 	/*
@@ -670,6 +690,36 @@ AddBufferToRing(BufferAccessStrategy strategy, BufferDesc *buf)
 	strategy->buffers[strategy->current] = BufferDescriptorGetBuffer(buf);
 }
 
+/*
+ * Utility function returning the IOContext of a given BufferAccessStrategy's
+ * strategy ring.
+ */
+IOContext
+IOContextForStrategy(BufferAccessStrategy strategy)
+{
+	Assert(strategy);
+
+	switch (strategy->btype)
+	{
+		case BAS_NORMAL:
+
+			/*
+			 * Currently, GetAccessStrategy() returns NULL for
+			 * BufferAccessStrategyType BAS_NORMAL, so this case is unlikely
+			 * to be hit.
+			 */
+			return IOCONTEXT_SHARED;
+		case BAS_BULKREAD:
+			return IOCONTEXT_BULKREAD;
+		case BAS_BULKWRITE:
+			return IOCONTEXT_BULKWRITE;
+		case BAS_VACUUM:
+			return IOCONTEXT_VACUUM;
+	}
+
+	elog(ERROR, "unrecognized BufferAccessStrategyType: %d", strategy->btype);
+}
+
 /*
  * StrategyRejectBuffer -- consider rejecting a dirty buffer
  *
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index 30d67d1c40..c2548f2b0b 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -18,6 +18,7 @@
 #include "access/parallel.h"
 #include "catalog/catalog.h"
 #include "executor/instrument.h"
+#include "pgstat.h"
 #include "storage/buf_internals.h"
 #include "storage/bufmgr.h"
 #include "utils/guc_hooks.h"
@@ -196,6 +197,8 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
 				LocalRefCount[b]++;
 				ResourceOwnerRememberBuffer(CurrentResourceOwner,
 											BufferDescriptorGetBuffer(bufHdr));
+
+				pgstat_count_io_op(IOOP_ACQUIRE, IOCONTEXT_LOCAL);
 				break;
 			}
 		}
@@ -226,6 +229,8 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
 				  localpage,
 				  false);

+					pgstat_count_io_op(IOOP_FSYNC, IOCONTEXT_SHARED);
+
 					if (log_checkpoints)
 						elog(DEBUG1, "checkpoint sync: number=%d file=%s time=%.3f ms",
 							 processed,
diff --git a/src/backend/utils/activity/Makefile b/src/backend/utils/activity/Makefile
index a2e8507fd6..0098785089 100644
--- a/src/backend/utils/activity/Makefile
+++ b/src/backend/utils/activity/Makefile
@@ -22,6 +22,7 @@ OBJS = \
 	pgstat_checkpointer.o \
 	pgstat_database.o \
 	pgstat_function.o \
+	pgstat_io_ops.o \
 	pgstat_relation.o \
 	pgstat_replslot.o \
 	pgstat_shmem.o \
diff --git a/src/backend/utils/activity/meson.build b/src/backend/utils/activity/meson.build
index 5b3b558a67..1038324c32 100644
--- a/src/backend/utils/activity/meson.build
+++ b/src/backend/utils/activity/meson.build
@@ -7,6 +7,7 @@ backend_sources += files(
   'pgstat_checkpointer.c',
   'pgstat_database.c',
   'pgstat_function.c',
+  'pgstat_io_ops.c',
   'pgstat_relation.c',
   'pgstat_replslot.c',
   'pgstat_shmem.c',
diff --git a/src/backend/utils/activity/pgstat_io_ops.c b/src/backend/utils/activity/pgstat_io_ops.c
new file mode 100644
index 0000000000..cea46322a7
--- /dev/null
+++ b/src/backend/utils/activity/pgstat_io_ops.c
@@ -0,0 +1,234 @@
+/* -------------------------------------------------------------------------
+ *
+ * pgstat_io_ops.c
+ *	  Implementation of IO operation statistics.
+ *
+ * This file contains the implementation of IO operation statistics. It is kept
+ * separate from pgstat.c to enforce the line between the statistics access /
+ * storage implementation and the details about individual types of
+ * statistics.
+ *
+ * Copyright (c) 2021-2022, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/activity/pgstat_io_ops.c
+ * -------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "utils/pgstat_internal.h"
+
+static PgStat_IOContextOps pending_IOOpStats;
+
+void
+pgstat_count_io_op(IOOp io_op, IOContext io_context)
+{
+	PgStat_IOOpCounters *pending_counters;
+
+	Assert(io_context < IOCONTEXT_NUM_TYPES);
+	Assert(io_op < IOOP_NUM_TYPES);
+	Assert(pgstat_expect_io_op(MyBackendType, io_context, io_op));
+
+	pending_counters = &pending_IOOpStats.data[io_context];
+
+	switch (io_op)
+	{
+		case IOOP_ACQUIRE:
+			pending_counters->acquires++;
+			break;
+		case IOOP_EXTEND:
+			pending_counters->extends++;
+			break;
+		case IOOP_FSYNC:
+			pending_counters->fsyncs++;
+			break;
+		case IOOP_HIT:
+			pending_counters->hits++;
+			break;
+		case IOOP_READ:
+			pending_counters->reads++;
+			break;
+		case IOOP_WRITE:
+			pending_counters->writes++;
+			break;
+	}
+
+}
+
+const char *
+pgstat_io_context_desc(IOContext io_context)
+{
+	switch (io_context)
+	{
+		case IOCONTEXT_BULKREAD:
+			return "bulkread";
+		case IOCONTEXT_BULKWRITE:
+			return "bulkwrite";
+		case IOCONTEXT_LOCAL:
+			return "local";
+		case IOCONTEXT_SHARED:
+			return "shared";
+		case IOCONTEXT_VACUUM:
+			return "vacuum";
+	}
+
+	elog(ERROR, "unrecognized IOContext value: %d", io_context);
+}
+
+const char *
+pgstat_io_op_desc(IOOp io_op)
+{
+	switch (io_op)
+	{
+		case IOOP_ACQUIRE:
+			return "acquire";
+		case IOOP_EXTEND:
+			return "extend";
+		case IOOP_FSYNC:
+			return "fsync";
+		case IOOP_HIT:
+			return "hit";
+		case IOOP_READ:
+			return "read";
+		case IOOP_WRITE:
+			return "write";
+	}
+
+	elog(ERROR, "unrecognized IOOp value: %d", io_op);
+}
+
+/*
+* IO Operation statistics are not collected for all BackendTypes.
+*
+* The following BackendTypes do not participate in the cumulative stats
+* subsystem or do not do IO operations worth reporting statistics on:
+* - Syslogger because it is not connected to shared memory
+* - Archiver because most relevant archiving IO is delegated to a
+*   specialized command or module
+* - WAL Receiver and WAL Writer IO is not tracked in pg_stat_io for now
+*
+* Function returns true if BackendType participates in the cumulative stats
+* subsystem for IO Operations and false if it does not.
+*/
+bool
+pgstat_io_op_stats_collected(BackendType bktype)
+{
+	return bktype != B_INVALID && bktype != B_ARCHIVER && bktype != B_LOGGER &&
+		bktype != B_WAL_RECEIVER && bktype != B_WAL_WRITER;
+}
+
+/*
+ * Some BackendTypes will never do IO in certain IOContexts. Check that the
+ * given BackendType is expected to do IO in the given IOContext.
+ */
+bool
+pgstat_bktype_io_context_valid(BackendType bktype, IOContext io_context)
+{
+	bool		no_strategy;
+	bool		no_local;
+
+	/*
+	 * In core Postgres, only regular backends and WAL Sender processes
+	 * executing queries should use local buffers. Parallel workers will not
+	 * use local buffers (see InitLocalBuffers()); however, extensions
+	 * leveraging background workers have no such limitation, so track IO
+	 * Operations in IOCONTEXT_LOCAL for BackendType B_BG_WORKER.
+	 */
+	no_local = bktype == B_AUTOVAC_LAUNCHER || bktype == B_BG_WRITER || bktype
+		== B_CHECKPOINTER || bktype == B_AUTOVAC_WORKER || bktype ==
+		B_STANDALONE_BACKEND || bktype == B_STARTUP;
+
+	if (io_context == IOCONTEXT_LOCAL && no_local)
+		return false;
+
+	/*
+	 * Some BackendTypes never use any BufferAccessStrategy.
+	 */
+	no_strategy = bktype == B_AUTOVAC_LAUNCHER || bktype ==
+		B_BG_WRITER || bktype == B_CHECKPOINTER;
+
+	if ((io_context == IOCONTEXT_BULKREAD || io_context == IOCONTEXT_BULKWRITE
+		 || io_context == IOCONTEXT_VACUUM) && no_strategy)
+		return false;
+
+	/*
+	 * There is not an explicit reason why an autovacuum worker could not use
+	 * a BAS_BULKREAD or BAS_BULKWRITE BufferAccessStrategy, however, they do
+	 * not currently do so and leaving these combinations out leans out the
+	 * view. If autovacuum workers use these strategies in the future, this
+	 * restriction can be removed.
+	 */
+	if ((io_context == IOCONTEXT_BULKWRITE || io_context == IOCONTEXT_BULKREAD)
+		&& bktype == B_AUTOVAC_WORKER)
+		return false;
+
+	return true;
+}
+
+/*
+ * Some BackendTypes will never do certain IOOps and some IOOps should not
+ * occur in certain IOContexts. Check that the given IOOp is valid for the
+ * given BackendType in the given IOContext. Note that there are currently no
+ * cases of an IOOp being invalid for a particular BackendType only within a
+ * certain IOContext.
+ */
+bool
+pgstat_io_op_valid(BackendType bktype, IOContext io_context, IOOp io_op)
+{
+	bool		strategy_io_context;
+
+	/*
+	 * Some BackendTypes should never track IO Operation statistics.
+	 */
+	Assert(pgstat_io_op_stats_collected(bktype));
+
+	/*
+	 * Some BackendTypes will not do certain IOOps.
+	 */
+	if ((bktype == B_BG_WRITER || bktype == B_CHECKPOINTER) &&
+		(io_op == IOOP_READ || io_op == IOOP_ACQUIRE || io_op == IOOP_HIT))
+		return false;
+
+	if ((bktype == B_AUTOVAC_LAUNCHER || bktype == B_BG_WRITER || bktype ==
+		 B_CHECKPOINTER) && io_op == IOOP_EXTEND)
+		return false;
+
+	/*
+	 * Some IOOps are not valid in certain IOContexts
+	 */
+	if (io_op == IOOP_EXTEND && io_context == IOCONTEXT_BULKREAD)
+		return false;
+
+	/*
+	 * Temporary tables using local buffers are not logged and thus do not
+	 * require fsync'ing.
+	 *
+	 * IOOP_FSYNC IOOps done by a backend using a BufferAccessStrategy are
+	 * counted in the IOCONTEXT_SHARED IOContext. See comment in
+	 * ForwardSyncRequest() for more details.
+	 */
+	strategy_io_context = io_context == IOCONTEXT_BULKREAD || io_context ==
+		IOCONTEXT_BULKWRITE || io_context == IOCONTEXT_VACUUM;
+
+	if ((io_context == IOCONTEXT_LOCAL || strategy_io_context) &&
+		io_op == IOOP_FSYNC)
+		return false;
+
+	return true;
+}
+
+bool
+pgstat_expect_io_op(BackendType bktype, IOContext io_context, IOOp io_op)
+{
+	if (!pgstat_io_op_stats_collected(bktype))
+		return false;
+
+	if (!pgstat_bktype_io_context_valid(bktype, io_context))
+		return false;
+
+	if (!(pgstat_io_op_valid(bktype, io_context, io_op)))
+		return false;
+
+	return true;
+}
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index ad7334a0d2..472e0def97 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -14,6 +14,7 @@
 #include "datatype/timestamp.h"
 #include "portability/instr_time.h"
 #include "postmaster/pgarch.h"	/* for MAX_XFN_CHARS */
+#include "storage/buf.h"
 #include "utils/backend_progress.h" /* for backward compatibility */
 #include "utils/backend_status.h"	/* for backward compatibility */
 #include "utils/relcache.h"
@@ -276,6 +277,48 @@ typedef struct PgStat_CheckpointerStats
 	PgStat_Counter buf_fsync_backend;
 } PgStat_CheckpointerStats;
 
+/*
+ * Types related to counting IO Operations for various IO Contexts
+ */
+
+typedef enum IOOp
+{
+	IOOP_ACQUIRE,
+	IOOP_EXTEND,
+	IOOP_FSYNC,
+	IOOP_HIT,
+	IOOP_READ,
+	IOOP_WRITE,
+} IOOp;
+
+#define IOOP_NUM_TYPES (IOOP_WRITE + 1)
+
+typedef enum IOContext
+{
+	IOCONTEXT_BULKREAD,
+	IOCONTEXT_BULKWRITE,
+	IOCONTEXT_LOCAL,
+	IOCONTEXT_SHARED,
+	IOCONTEXT_VACUUM,
+} IOContext;
+
+#define IOCONTEXT_NUM_TYPES (IOCONTEXT_VACUUM + 1)
+
+typedef struct PgStat_IOOpCounters
+{
+	PgStat_Counter acquires;
+	PgStat_Counter extends;
+	PgStat_Counter fsyncs;
+	PgStat_Counter hits;
+	PgStat_Counter reads;
+	PgStat_Counter writes;
+} PgStat_IOOpCounters;
+
+typedef struct PgStat_IOContextOps
+{
+	PgStat_IOOpCounters data[IOCONTEXT_NUM_TYPES];
+} PgStat_IOContextOps;
+
 typedef struct PgStat_StatDBEntry
 {
 	PgStat_Counter n_xact_commit;
@@ -453,6 +496,24 @@ extern void pgstat_report_checkpointer(void);
 extern PgStat_CheckpointerStats *pgstat_fetch_stat_checkpointer(void);

+/*
+/* IO stats translation function in freelist.c */
+extern IOContext IOContextForStrategy(BufferAccessStrategy bas);
 
 /* freelist.c */
 extern BufferDesc *StrategyGetBuffer(BufferAccessStrategy strategy,
-									 uint32 *buf_state);
+									 uint32 *buf_state, bool *from_ring);
 extern void StrategyFreeBuffer(BufferDesc *buf);
 extern bool StrategyRejectBuffer(BufferAccessStrategy strategy,
 								 BufferDesc *buf);
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 6f4dfa0960..d0eed71f63 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -23,7 +23,12 @@
 
 typedef void *Block;
 
-/* Possible arguments for GetAccessStrategy() */
+/*
+ * Possible arguments for GetAccessStrategy().
+ *
+ * If adding a new BufferAccessStrategyType, also add a new IOContext so
+ * statistics on IO operations using this strategy are tracked.
+ */
 typedef enum BufferAccessStrategyType
 {
 	BAS_NORMAL,					/* Normal random access */
diff --git a/src/tools/pgindent/typedefs.list b/src/tools/pgindent/typedefs.list
index 97c9bc1861..67218ec6f2 100644
--- a/src/tools/pgindent/typedefs.list
+++ b/src/tools/pgindent/typedefs.list
@@ -1106,7 +1106,9 @@ ID
 INFIX
 INT128
 INTERFACE_INFO
+IOContext
 IOFuncSelector
+IOOp
 IPCompareMethod
 ITEM
 IV
@@ -2026,6 +2028,8 @@ PgStat_FetchConsistency
 PgStat_FunctionCallUsage
 PgStat_FunctionCounts
 PgStat_HashKey
+PgStat_IOContextOps
+PgStat_IOOpCounters
 PgStat_Kind
 PgStat_KindInfo
 PgStat_LocalState
-- 
2.34.1

---

From: Melanie Plageman <[email-masked]>
Date: Thu, 6 Oct 2022 18:23:53 -0400

v31 failed in CI, so
I've attached v32 which has a few issues fixed:
- addressed some compiler warnings I hadn't noticed locally
- autovac launcher and worker do indeed use bulkread strategy if they
  end up starting before critical indexes have loaded and end up doing a
  sequential scan of some catalog tables, so I have changed the
  restrictions on BackendTypes allowed to track IO Operations in
  IOCONTEXT_BULKREAD
- changed the name of the column "fsynced" to "files_synced" to make it
  more clear what unit it is in (and that the unit differs from that of
  the "unit" column)

In an off-list discussion with Andres, he mentioned that he thought
buffers reused by a BufferAccessStrategy should be split from buffers
"acquired" and that "acquired" should be renamed "clocksweeps".

I have started doing this, but for BufferAccessStrategy IO there are a
few choices about how we want to count the clocksweeps:

Currently the following situations are counted under the following
IOContexts and IOOps:

IOCONTEXT_[VACUUM,BULKREAD,BULKWRITE], IOOP_ACQUIRE
- reuse a buffer from the ring

IOCONTEXT_SHARED, IOOP_ACQUIRE
- add a buffer to the strategy ring initially
- add a new shared buffer to the ring when all the existing buffers in
  the ring are pinned

And in the new paradigm, I think these are two good options:

1)
IOCONTEXT_[VACUUM,BULKREAD,BULKWRITE], IOOP_CLOCKSWEEP
- add a buffer to the strategy ring initially
- add a new shared buffer to the ring when all the existing buffers in
  the ring are pinned

IOCONTEXT_[VACUUM,BULKREAD,BULKWRITE], IOOP_REUSE
- reuse a buffer from the ring

2)
IOCONTEXT_[VACUUM,BULKREAD,BULKWRITE], IOOP_CLOCKSWEEP
- add a buffer to the strategy ring initially

IOCONTEXT_[VACUUM,BULKREAD,BULKWRITE], IOOP_REUSE
- reuse a buffer from the ring

IOCONTEXT SHARED, IOOP_CLOCKSWEEP
- add a new shared buffer to the ring when all the existing buffers in
  the ring are pinned

However, if we want to differentiate between buffers initially added to
the ring and buffers taken from shared buffers and added to the ring
because all strategy ring buffers are pinned or have a usage count above
one, then we would need to either do so inside of GetBufferFromRing() or
propagate this distinction out somehow (easy enough if we care to do
it).

There are other combinations that I could come up with a justification
for as well, but I wanted to know what other people thought made sense
(and would make sense to users).

- Melanie
From d5859f08316a185f8fb889da8552422d9b609dd1 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 6 Oct 2022 12:23:38 -0400
Subject: [PATCH v32 2/3] Aggregate IO operation stats per BackendType

 src/backend/utils/activity/pgstat_io_ops.c    | 158 ++++++++++++++++++
 src/include/utils/pgstat_internal.h           |  36 ++++

index 609f0b1ad8..07c02ac3d7 100644

 

 
+	/*
 
+	/*
+												   (BackendType) bktype);

 /*
 
 /*

 /*
 
 /*
index e1750b965f..237b7da6d2 100644

 void
 pgstat_count_io_op(IOOp io_op, IOContext io_context)
@@ -54,6 +90,78 @@ pgstat_count_io_op(IOOp io_op, IOContext io_context)
 			break;
 	}
 
+	have_ioopstats = true;
+}
+
+			!pgstat_bktype_io_context_valid(MyBackendType, (IOContext) io_context))
+			if (!(pgstat_io_op_valid(MyBackendType, (IOContext) io_context,
+									 (IOOp) io_op)))
+				pgstat_io_op_assert_zero(sharedent, (IOOp) io_op);
+				pgstat_io_op_assert_zero(pendingent, (IOOp) io_op);
+			pgstat_accum_io_op(sharedent, pendingent, (IOOp) io_op);
 
 const char *
@@ -98,6 +206,56 @@ pgstat_io_op_desc(IOOp io_op)
 	elog(ERROR, "unrecognized IOOp value: %d", io_op);
 }
 
+void
 
 /*

 /*

 /*
 
 /*
 
 /*
 
 	PG_RETURN_VOID();
 }

index 155b0b2d48..b4f5d75949 100644

 

 
+			!pgstat_bktype_io_context_valid(bktype, (IOContext) io_context))
+			if (!pgstat_io_op_valid(bktype, (IOContext) io_context, (IOOp) io_op))
+						(IOOp) io_op);
+		}
+	}
+}

 /*
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 40a3602855..3421c8a5c0 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -329,6 +329,25 @@ typedef struct PgStatShared_Checkpointer
 	PgStat_CheckpointerStats reset_offset;
 } PgStatShared_Checkpointer;
 
+	 * lock protects ->data If this PgStatShared_IOContextOps is
@@ -419,6 +438,7 @@ typedef struct PgStat_ShmemControl
@@ -442,6 +462,8 @@ typedef struct PgStat_Snapshot

 
 	PgStat_WalStats wal;
@@ -549,6 +571,15 @@ extern void pgstat_database_reset_timestamp_cb(PgStatShared_Common *header, Time
 extern bool pgstat_function_flush_cb(PgStat_EntryRef *entry_ref, bool nowait);

+/*
@@ -641,6 +672,11 @@ extern void pgstat_create_transactional(PgStat_Kind kind, Oid dboid, Oid objoid)

+/*
 
 /*

From caed7a11517799676a50570bad4b4d3bb412e42a Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 6 Oct 2022 12:23:25 -0400
Subject: [PATCH v32 1/3] Track IO operation statistics locally

 src/backend/utils/activity/pgstat_io_ops.c | 229 +++++++++++++++++++++
 12 files changed, 423 insertions(+), 13 deletions(-)

 

+	if (strategy)

 	if (isExtend)
 	{

 

+				/*

 

 

 

 
 /*
 
+#include "pgstat.h"
 
+	*from_ring = false;
+
 	/*
 
 	/*
 
 	/*
 
+/*

index 0000000000..e1750b965f
@@ -0,0 +1,229 @@
+ * Some BackendTypes do not perform IO operations in certain IOContexts. Check
+ * that the given BackendType is expected to do IO in the given IOContext.
+	 * Some BackendTypes do not currently perform any IO operations in certain
+	 * IOContexts, and, while it may not be inherently incorrect for them to
+	 * do so, excluding those rows from the view makes the view easier to use.
+		 || io_context == IOCONTEXT_VACUUM) && (bktype == B_CHECKPOINTER
+												|| bktype == B_BG_WRITER))
+	if (io_context == IOCONTEXT_VACUUM && bktype == B_AUTOVAC_LAUNCHER)
+	if (io_context == IOCONTEXT_BULKWRITE && (bktype == B_AUTOVAC_WORKER ||
+											  bktype == B_AUTOVAC_LAUNCHER))
index ad7334a0d2..155b0b2d48 100644
 
+/*
+	IOOP_ACQUIRE = 0,
+	IOCONTEXT_BULKREAD = 0,

+/*
 
 /* freelist.c */
 
 typedef void *Block;

From 8e9da4e2ecaae4bfb5a637b8e09eadeebaae4ee0 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 6 Oct 2022 12:24:42 -0400
Subject: [PATCH v32 3/3] Add system view tracking IO ops per backend type

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.

 src/backend/utils/adt/pgstatfuncs.c  | 126 +++++++++++++
 7 files changed, 748 insertions(+), 2 deletions(-)

index 14dfd650f8..104c4b0f1a 100644
 
+     <row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>unit</structfield> <type>text</type>
+      </para>
+      <para>
+      The unit in which the acquired, read, written, and extended columns can
+      be interpreted. Currently <varname>block_size</varname> is the only
+      possible value. Reads, writes, and extends of relation data are done in
+      <varname>block_size</varname> units. Future values could include
+      <varname>wal_block_size</varname>, once WAL IO is tracked in this view,
+      and <quote>bytes</quote>, once non-block-oriented IO such as temp files
+      is tracked here.
+      </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>
 
index 55f7ec79e0..4467a0df82 100644
 
+CREATE VIEW pg_stat_io AS
+SELECT
+       b.backend_type,
+       b.io_context,
+       b.acquired,
+       b.hit,
+       b.read,
+       b.written,
+       b.extended,
+       b.unit,
+       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 edd73e5c25..ee8712ace4 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1712,6 +1712,132 @@ pg_stat_get_buf_alloc(PG_FUNCTION_ARGS)
 	PG_RETURN_INT64(pgstat_fetch_stat_bgwriter()->buf_alloc);
 }
 
+/*
+		Datum		bktype_desc = CStringGetTextDatum(GetBackendTypeDesc((BackendType) bktype));
+		expect_backend_stats = pgstat_io_op_stats_collected((BackendType)
+				!pgstat_bktype_io_context_valid((BackendType) bktype,
+												(IOContext) io_context))
+				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;
index 68bb032d3e..250a1f17dc 100644
 
+  proargnames => '{backend_type,io_context,acquired,hit,read,written,extended,unit,files_synced,stats_reset}',
index 9dd137415e..24187acb61 100644
+    b.files_synced,
+   FROM pg_stat_get_io() b(backend_type, io_context, acquired, hit, read, written, extended, unit, files_synced, stats_reset);
index f701da2069..a9919a3e62 100644
  
 (1 row)
 
+SELECT sum(files_synced) AS io_sum_shared_fsyncs_before 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 sum(acquired) + sum(extended) + sum(files_synced) + sum(read) + sum(written) AS io_stats_pre_reset FROM pg_stat_io \gset
+SELECT sum(acquired) + sum(extended) + sum(files_synced) + sum(read) + sum(written) AS io_stats_post_reset FROM pg_stat_io \gset
index eb081f65a4..78798bc626 100644

+SELECT sum(files_synced) AS io_sum_shared_fsyncs_before 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 sum(acquired) + sum(extended) + sum(files_synced) + sum(read) + sum(written) AS io_stats_pre_reset FROM pg_stat_io \gset
+SELECT sum(acquired) + sum(extended) + sum(files_synced) + sum(read) + sum(written) AS io_stats_post_reset FROM pg_stat_io \gset

---

From: Melanie Plageman <[email-masked]>
Date: Mon, 10 Oct 2022 14:48:49 -0400

I've gone ahead and implemented option 1 (commented below).

> - reuse a buffer from the ring

I've implemented this option in attached v33.

> - add a new shared buffer to the ring when all the existing buffers in

- Melanie
From 6a83f0028a69a56243fa5b036299185766b80629 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 6 Oct 2022 12:23:38 -0400
Subject: [PATCH v33 2/4] Aggregate IO operation stats per BackendType

 src/backend/utils/activity/pgstat_io_ops.c    | 161 ++++++++++++++++++
 src/include/pgstat.h                          |  84 +++++++++
 13 files changed, 358 insertions(+), 6 deletions(-)

index 1b97597f17..4becee9a6c 100644

 

 
+	/*
 
+	/*

 /*
 
 /*

 /*
 
 /*
index a992882ac3..369aafa9f3 100644
@@ -20,6 +20,45 @@
 
+		case IOOP_CLOCKSWEEP:
+			target->clocksweeps += source->clocksweeps;
+		case IOOP_REUSE:
+			target->reuses += source->reuses;
 
 void
 pgstat_count_io_op(IOOp io_op, IOContext io_context)
@@ -57,6 +96,78 @@ pgstat_count_io_op(IOOp io_op, IOContext io_context)
 			break;
 	}
 
+	have_ioopstats = true;
+}
+
 
 const char *
@@ -103,6 +214,56 @@ pgstat_io_op_desc(IOOp io_op)
 	elog(ERROR, "unrecognized IOOp value: %d", io_op);
 }
 
+void
 
 /*

 /*

 /*
 
 /*
 
 /*
 
 	PG_RETURN_VOID();
 }

 extern const char *GetBackendTypeDesc(BackendType backendType);
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 015f17cd06..c2c127d846 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -49,6 +49,7 @@ typedef enum PgStat_Kind
 	PGSTAT_KIND_ARCHIVER,
 	PGSTAT_KIND_BGWRITER,
 	PGSTAT_KIND_CHECKPOINTER,
+	PGSTAT_KIND_IOOPS,
 	PGSTAT_KIND_SLRU,
 	PGSTAT_KIND_WAL,
 } PgStat_Kind;
@@ -321,6 +322,12 @@ typedef struct PgStat_IOContextOps
 	PgStat_IOOpCounters data[IOCONTEXT_NUM_TYPES];
 } PgStat_IOContextOps;
 
@@ -502,6 +509,7 @@ extern PgStat_CheckpointerStats *pgstat_fetch_stat_checkpointer(void);
  */

@@ -513,6 +521,82 @@ extern bool pgstat_expect_io_op(BackendType bktype, IOContext io_context, IOOp i
 
+	Assert(counters->clocksweeps == 0 && counters->extends == 0 &&
+		case IOOP_CLOCKSWEEP:
+			Assert(counters->clocksweeps == 0);
+		case IOOP_REUSE:
+			Assert(counters->reuses == 0);

 /*
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 627c1389e4..9066fed660 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -330,6 +330,25 @@ typedef struct PgStatShared_Checkpointer
 	PgStat_CheckpointerStats reset_offset;
 } PgStatShared_Checkpointer;
 
@@ -420,6 +439,7 @@ typedef struct PgStat_ShmemControl
@@ -443,6 +463,8 @@ typedef struct PgStat_Snapshot

 
 	PgStat_WalStats wal;
@@ -550,6 +572,15 @@ extern void pgstat_database_reset_timestamp_cb(PgStatShared_Common *header, Time
 extern bool pgstat_function_flush_cb(PgStat_EntryRef *entry_ref, bool nowait);

+/*
@@ -642,6 +673,11 @@ extern void pgstat_create_transactional(PgStat_Kind kind, Oid dboid, Oid objoid)

+/*
 
 /*

From 677a8cec3dadfcaf9476e27d1d9e9328a14753c9 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 6 Oct 2022 12:23:25 -0400
Subject: [PATCH v33 1/4] Track IO operation statistics locally

Each IOOp (clocksweep, hit, reuse, read, write, extend, fsync) is
counted per IOContext (bulkread, bulkwrite, local, shared, or vacuum)
through a call to pgstat_count_io_op().

IOOP_CLOCKSWEEP IOOps are counted in IOCONTEXT_SHARED and
IOCONTEXT_LOCAL IOContexts when a buffer is acquired through
[Local]BufferAlloc() and no BufferAccessStrategy is in use.

When a BufferAccessStrategy is in use, buffers added to the strategy
ring are counted as IOOP_CLOCKSWEEP IOOps in the
IOCONTEXT_[BULKREAD|BULKWRITE|VACUUM) IOContext. When one of these
buffers is reused, it is counted as an IOOP_REUSE IOOp in the
corresponding strategy IOContext.

IOOP_WRITE IOOps are counted in the BufferAccessStrategy IOContexts
whenever the reused dirty buffer is written out.

Author: Melanie Plageman <[email-masked]>
Reviewed-by: Andres Freund <[email-masked]>
Reviewed-by: Justin Pryzby <[email-masked]>
Reviewed-by: Kyotaro Horiguchi <[email-masked]>
Reviewed-by: Maciek Sakrejda <[email-masked]>
Reviewed-by: Lukas Fittl <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/20200124195226.lth52iydq2n2uilq%40alap3.anarazel.de
---
 src/backend/postmaster/checkpointer.c      |  13 ++
 src/backend/storage/buffer/bufmgr.c        |  54 ++++-
 src/backend/storage/buffer/freelist.c      |  60 +++++-
 src/backend/storage/buffer/localbuf.c      |   5 +
 src/backend/storage/sync/sync.c            |   2 +
 src/backend/utils/activity/Makefile        |   1 +
 src/backend/utils/activity/meson.build     |   1 +
 src/backend/utils/activity/pgstat_io_ops.c | 238 +++++++++++++++++++++
 src/include/pgstat.h                       |  63 ++++++
 src/include/storage/buf_internals.h        |   2 +-
 src/include/storage/bufmgr.h               |   7 +-
 src/tools/pgindent/typedefs.list           |   4 +
 12 files changed, 438 insertions(+), 12 deletions(-)
 create mode 100644 src/backend/utils/activity/pgstat_io_ops.c

index 6b95381481..fb539ed9e6 100644

 
+	if (strategy)
@@ -990,6 +1000,7 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 
 	if (isExtend)
 	{
@@ -1020,6 +1031,8 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,

+			pgstat_count_io_op(IOOP_READ, io_context);
+
 			if (track_io_timing)
 			{
 				INSTR_TIME_SET_CURRENT(io_time);
@@ -1190,6 +1203,8 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	/* Loop here in case we have to try another victim buffer */
 	for (;;)
 	{
+		bool		from_ring;
+
 		/*
 		 * Ensure, while the spinlock's not yet held, that there's a free
 		 * refcount entry.
@@ -1200,7 +1215,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 		 * Select a victim buffer.  The buffer is returned with its header
 		 * spinlock still held!
 		 */
-		buf = StrategyGetBuffer(strategy, &buf_state);
+		buf = StrategyGetBuffer(strategy, &buf_state, &from_ring);

@@ -1237,6 +1252,8 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 			if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf),
 										 LW_SHARED))
 			{
+				IOContext	io_context;
+
 				/*
 				 * If using a nondefault strategy, and writing the buffer
 				 * would require a WAL flush, let the strategy decide whether
@@ -1263,13 +1280,28 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 					}
 				}
 
+				/*

 				ScheduleBufferTagForWriteback(&BackendWritebackContext,
@@ -2570,7 +2602,7 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
 	PinBuffer_Locked(bufHdr);
 	LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);

 
@@ -2820,7 +2852,7 @@ BufferGetTag(Buffer buffer, RelFileLocator *rlocator, ForkNumber *forknum,
  * as the second parameter.  If not, pass NULL.
  */
 static void
-FlushBuffer(BufferDesc *buf, SMgrRelation reln)
+FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOContext io_context)
 {
 	XLogRecPtr	recptr;
 	ErrorContextCallback errcallback;
@@ -2900,6 +2932,8 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln)
 	 */
 	bufToWrite = PageSetChecksumCopy((Page) bufBlock, buf->tag.blockNum);

@@ -3551,6 +3585,8 @@ FlushRelationBuffers(Relation rel)
 						  localpage,
 						  false);

@@ -3586,7 +3622,7 @@ FlushRelationBuffers(Relation rel)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, RelationGetSmgr(rel));
+			FlushBuffer(bufHdr, RelationGetSmgr(rel), IOCONTEXT_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr);
 		}
@@ -3684,7 +3720,7 @@ FlushRelationsAllBuffers(SMgrRelation *smgrs, int nrels)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, srelent->srel);
+			FlushBuffer(bufHdr, srelent->srel, IOCONTEXT_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr);
 		}
@@ -3894,7 +3930,7 @@ FlushDatabaseBuffers(Oid dbid)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, NULL);
+			FlushBuffer(bufHdr, NULL, IOCONTEXT_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr);
 		}
@@ -3921,7 +3957,7 @@ FlushOneBuffer(Buffer buffer)

 
 /*
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index 990e081aae..15cd8bbf88 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -15,6 +15,7 @@
  */
 #include "postgres.h"
 
+#include "pgstat.h"
@@ -198,23 +199,40 @@ have_free_buffer(void)
 
+	*from_ring = false;
+
 	/*
+			 * IOCONTEXT_BULKWRITE, or IOCONTEXT_VACUUM reuses for the
+			pgstat_count_io_op(IOOP_REUSE, io_context);
 
 	/*
 	 * If asked, we need to waken the bgwriter. Since we don't want to rely on
@@ -249,6 +267,16 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
 	 */
 	pg_atomic_fetch_add_u32(&StrategyControl->numBufferAllocs, 1);
 
+	/*
+	 * When a BufferAccessStrategy is in use, clocksweeps adding a shared
+	 * buffer to the strategy ring are counted in the corresponding strategy's
+	 * context. This includes the clocksweeps done to add buffers to the ring
+	 * initially as well as those done to add a new shared buffer to the ring
+	 * when all existing buffers in the ring are pinned or have a usage count
+	 * above one.
+	 */
+	pgstat_count_io_op(IOOP_CLOCKSWEEP, io_context);
+
 	/*
 	 * First check, without acquiring the lock, whether there's buffers in the
 	 * freelist. Since we otherwise don't require the spinlock in every
@@ -670,6 +698,36 @@ AddBufferToRing(BufferAccessStrategy strategy, BufferDesc *buf)
 	strategy->buffers[strategy->current] = BufferDescriptorGetBuffer(buf);
 }
 
+/*
index 30d67d1c40..6fe7459401 100644
+				pgstat_count_io_op(IOOP_CLOCKSWEEP, IOCONTEXT_LOCAL);

index 0000000000..a992882ac3
@@ -0,0 +1,238 @@
+		case IOOP_CLOCKSWEEP:
+			pending_counters->clocksweeps++;
+		case IOOP_REUSE:
+			pending_counters->reuses++;
+		case IOOP_CLOCKSWEEP:
+			return "clocksweep";
+		case IOOP_REUSE:
+			return "reused";
+		(io_op == IOOP_READ || io_op == IOOP_CLOCKSWEEP || io_op == IOOP_HIT))
+	if (io_op == IOOP_REUSE &&
+		(io_context == IOCONTEXT_SHARED || io_context == IOCONTEXT_LOCAL))
index cc1d1dcb7d..015f17cd06 100644
 
+/*
+	IOOP_CLOCKSWEEP = 0,
+	IOOP_REUSE,
+	PgStat_Counter clocksweeps;
+	PgStat_Counter reuses;
@@ -452,6 +497,24 @@ extern void pgstat_report_checkpointer(void);

+/*
 
 /* freelist.c */
 
 typedef void *Block;

From 2f63439e189eff432bba4abfc12762e7c3acd8ea Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 6 Oct 2022 12:24:42 -0400
Subject: [PATCH v33 3/4] Add system view tracking IO ops per backend type

Add pg_stat_io, a system view which tracks the number of IOOps
(clocksweeps, reuses, hits, 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 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.

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.

Author: Melanie Plageman <[email-masked]>
Reviewed-by: Andres Freund <[email-masked]>
Reviewed-by: Justin Pryzby <[email-masked]>
Reviewed-by: Kyotaro Horiguchi <[email-masked]>
Reviewed-by: Maciek Sakrejda <[email-masked]>
Reviewed-by: Lukas Fittl <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/20200124195226.lth52iydq2n2uilq%40alap3.anarazel.de
---
 doc/src/sgml/monitoring.sgml         | 215 +++++++++++++++++++++-
 src/backend/catalog/system_views.sql |  15 ++
 src/backend/utils/adt/pgstatfuncs.c  | 130 +++++++++++++
 src/include/catalog/pg_proc.dat      |   9 +
 src/test/regress/expected/rules.out  |  12 ++
 src/test/regress/expected/stats.out  | 264 +++++++++++++++++++++++++++
 src/test/regress/sql/stats.sql       | 137 ++++++++++++++
 7 files changed, 780 insertions(+), 2 deletions(-)

index 14dfd650f8..926eb40f75 100644
 
+     <row>
@@ -3615,7 +3623,210 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
 
+     <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
+       memory or 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. Currently
+      <varname>block_size</varname> is the only possible value. Reads, writes,
+      and extends of relation data are done in <varname>block_size</varname>
+      units. Future values could include <varname>wal_block_size</varname>,
+      once WAL IO is tracked in this view, and <quote>bytes</quote>, once
+      non-block-oriented IO such as temporary file IO is tracked here.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>clocksweeps</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Number of clocksweeps done by this <varname>backend_type</varname> in
+       order to acquire a buffer in this <varname>io_context</varname>. A
+       <literal>clocksweep</literal> in the <literal>bulkread</literal>,
+       <literal>bulkwrite</literal>, or <literal>vacuum</literal>
+       <varname>io_context</varname>s is counted when a backend adds a shared
+       buffer to the fixed-size ring used to avoid consuming excessive shared
+       buffers. If the backend has pinned all of the buffers in the ring, it
+       may add a replacement shared buffer to the ring. This will also be
+       counted as a <literal>clocksweep</literal>.
+      </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 a buffer was reused as part of an operation in the
+       <literal>bulkread</literal>, <literal>bulkwrite</literal>, and
+       <literal>vacuum</literal> <varname>io_context</varname>s.
+      </para></entry>
+     </row>
+
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>hit</structfield> <type>bigint</type>
+      </para>
+      <para>
+       Relevant only for block-based IO of data accessed in the course of
+       satisfying queries, <varname>hit</varname> is the number of number of
+       accesses of blocks already located in a
+       <productname>PostgreSQL</productname> buffer in this specified
+       <varname>io_context</varname> by this <varname>backend_type</varname>.
+      </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 55f7ec79e0..25e0cef114 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1114,6 +1114,21 @@ 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.clocksweeps,
+       b.reused,
+       b.hit,
+       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 edd73e5c25..62fbf7e53a 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -1712,6 +1712,136 @@ pg_stat_get_buf_alloc(PG_FUNCTION_ARGS)
 	PG_RETURN_INT64(pgstat_fetch_stat_bgwriter()->buf_alloc);
 }
 
+/*
+	IO_COL_CLOCKSWEEPS,
+	IO_COL_REUSES,
+		case IOOP_CLOCKSWEEP:
+			return IO_COL_CLOCKSWEEPS;
+		case IOOP_REUSE:
+			return IO_COL_REUSES;
+			values[IO_COL_CLOCKSWEEPS] = Int64GetDatum(counters->clocksweeps);
+			values[IO_COL_REUSES] = Int64GetDatum(counters->reuses);
index 68bb032d3e..77edbe9517 100644
 
+{ oid => '8459', descr => 'statistics: per backend type IO statistics',
+  proname => 'pg_stat_get_io', provolatile => 'v',
+  prorows => '14', proretset => 't',
+  proparallel => 'r', prorettype => 'record', proargtypes => '',
+  proallargtypes => '{text,text,int8,int8,int8,text,int8,int8,int8,int8,timestamptz}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{backend_type,io_context,read,written,extended,unit,clocksweeps,reused,hit,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 9dd137415e..7cedd530f5 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1868,6 +1868,18 @@ 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.clocksweeps,
+    b.reused,
+    b.hit,
+    b.files_synced,
+    b.stats_reset
+   FROM pg_stat_get_io() b(backend_type, io_context, read, written, extended, unit, clocksweeps, reused, hit, 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 f701da2069..0172a6c95e 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -918,4 +918,268 @@ SELECT pg_stat_get_subscription_stats(NULL);
  
 (1 row)
 
+-- - clocksweeps of shared buffers
+SELECT sum(clocksweeps) AS io_sum_shared_clocksweeps_before FROM pg_stat_io WHERE io_context = 'shared' \gset
+-- clocksweeps and extends.
+SELECT sum(clocksweeps) AS io_sum_shared_clocksweeps_after FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT :io_sum_shared_clocksweeps_after > :io_sum_shared_clocksweeps_before;
+-- Test that clocksweeps of local buffers, reads of temporary table blocks
+SELECT sum(clocksweeps) AS io_sum_local_clocksweeps_before FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT sum(clocksweeps) AS io_sum_local_clocksweeps_after FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT :io_sum_local_clocksweeps_after > :io_sum_local_clocksweeps_before;
+-- clocksweeps and reads.
+SELECT sum(clocksweeps) AS io_sum_vac_strategy_clocksweeps_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(clocksweeps) AS io_sum_vac_strategy_clocksweeps_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 :io_sum_vac_strategy_clocksweeps_after > :io_sum_vac_strategy_clocksweeps_before;
+SELECT :io_sum_vac_strategy_reuses_after > :io_sum_vac_strategy_reuses_before;
+SELECT sum(clocksweeps) + sum(reused) + sum(extended) + sum(files_synced) + sum(read) + sum(written) AS io_stats_pre_reset FROM pg_stat_io \gset
+SELECT sum(clocksweeps) + sum(reused) + sum(extended) + sum(files_synced) + sum(read) + sum(written) AS io_stats_post_reset FROM pg_stat_io \gset
index eb081f65a4..d3860fd9df 100644
@@ -449,4 +449,141 @@ SELECT pg_stat_get_replication_slot(NULL);

+-- - clocksweeps of shared buffers
+SELECT sum(clocksweeps) AS io_sum_shared_clocksweeps_before FROM pg_stat_io WHERE io_context = 'shared' \gset
+-- clocksweeps and extends.
+SELECT sum(clocksweeps) AS io_sum_shared_clocksweeps_after FROM pg_stat_io WHERE io_context = 'shared' \gset
+SELECT :io_sum_shared_clocksweeps_after > :io_sum_shared_clocksweeps_before;
+-- Test that clocksweeps of local buffers, reads of temporary table blocks
+SELECT sum(clocksweeps) AS io_sum_local_clocksweeps_before FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT sum(clocksweeps) AS io_sum_local_clocksweeps_after FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT :io_sum_local_clocksweeps_after > :io_sum_local_clocksweeps_before;
+-- clocksweeps and reads.
+SELECT sum(clocksweeps) AS io_sum_vac_strategy_clocksweeps_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(clocksweeps) AS io_sum_vac_strategy_clocksweeps_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 :io_sum_vac_strategy_clocksweeps_after > :io_sum_vac_strategy_clocksweeps_before;
+SELECT :io_sum_vac_strategy_reuses_after > :io_sum_vac_strategy_reuses_before;
+SELECT sum(clocksweeps) + sum(reused) + sum(extended) + sum(files_synced) + sum(read) + sum(written) AS io_stats_pre_reset FROM pg_stat_io \gset
+SELECT sum(clocksweeps) + sum(reused) + sum(extended) + sum(files_synced) + sum(read) + sum(written) AS io_stats_post_reset FROM pg_stat_io \gset

---

From: Maciek Sakrejda <[email-masked]>
Date: Mon, 10 Oct 2022 16:42:55 -0700

Thanks for working on this! Like Lukas, I'm excited to see more
visibility into important parts of the system like this.

> I've gone ahead and implemented option 1 (commented below).

No strong opinion on 1 versus 2, but I guess at least partly because I
don't understand the implications (I do understand the difference,
just not when it might be important in terms of stats). Can we think
of a situation where combining stats about initial additions with
pinned additions hides some behavior that might be good to understand
and hard to pinpoint otherwise?

I took a look at the latest docs (as someone mostly familiar with
internals at only a pretty high level, so probably somewhat close to
the target audience) and have some feedback.

+     <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).
+      </para></entry>
+     </row>

Not critical, but is there a list of backend types we could
cross-reference elsewhere in the docs?

---

From: Melanie Plageman <[email-masked]>
Date: Thu, 13 Oct 2022 13:29:32 -0400

> visibility into important parts of the system like this.

Thanks for taking another look!

> and hard to pinpoint otherwise?

I think that it makes sense to count both the initial buffers added to
the ring and subsequent shared buffers added to the ring (either when
the current strategy buffer is pinned or in use or when a bulkread
rejects dirty strategy buffers in favor of new shared buffers) as
strategy clocksweeps because of how the statistic would be used.

Clocksweeps give you an idea of how much of your working set is cached
(setting aside initially reading data into shared buffers when you are
warming up the db). You may use clocksweeps to determine if you need to
make shared buffers larger.

Distinguishing strategy buffer clocksweeps from shared buffer
clocksweeps allows us to avoid enlarging shared buffers if most of the
clocksweeps are to bring in blocks for the strategy operation.

However, I could see an argument that discounting strategy clocksweeps
done because the current strategy buffer is pinned makes the number of
shared buffer clocksweeps artificially low since those other queries
using the buffer would have suffered a cache miss were it not for the
strategy. And, in this case, you would take strategy clocksweeps
together with shared clocksweeps to make your decision. And if we
include buffers initially added to the strategy ring in the strategy
clocksweep statistic, this number may be off because those blocks may
not be needed in the main shared working set. But you won't know that
until you try to reuse the buffer and it is pinned. So, I think we don't
have a better option than counting initial buffers added to the ring as
strategy clocksweeps (as opposed to as reuses).

So, in answer to your question, no, I cannot think of a scenario like
that.

Sitting down and thinking about that for a long time did, however, help
me realize that some of my code comments were misleading (and some
incorrect). I will update these in the next version once we agree on
updated docs.

It also made me remember that I am incorrectly counting rejected buffers
as reused. I'm not sure if it is a good idea to subtract from reuses
when a buffer is rejected. Waiting until after it is rejected to count
the reuse will take some other code changes. Perhaps we could also count
rejections in the stats?

> cross-reference elsewhere in the docs?

The most I could find was this longer explanation (with exhaustive list
of types) in pg_stat_activity docs [1]. I could duplicate what it says
or I could link to the view and say "see pg_stat_activity" for a
description of backend_type" or something like that (to keep them from
getting out of sync as new backend_types are added. I suppose I could
also add docs on backend_types, but I'm not sure where something like
that would go.

>
> From the io_context column description:
>
> I don't understand how this is relevant to the io_context column.
> Could you expand on that, or am I just missing something obvious?
>

I'm trying to explain why those other IO Contexts exist (bulkread,
bulkwrite, vacuum) and why they are separate from shared buffers.
Should I cut it altogether or preface it with something like: these are
counted separate from shared buffers because...?

> I understand what this is, but not why this is something I might want
> to know about.

Unlike writes, backends largely have to do their own extends, so
separating this from writes lets us determine whether or not we need to
change checkpointer/bgwriter to be more aggressive using the writes
without the distraction of the extends. Should I mention this in the
docs? The other stats views don't seems to editorialize at all, and I
wasn't sure if this was an objective enough point to include in docs.

>
> And from your earlier e-mail:
>
> On Thu, Oct 6, 2022 at 10:42 AM Melanie Plageman
> That seems broadly reasonable, but pg_settings also has a 'unit'
> field, and in that view, unit is '8kB' on my system--i.e., it
> (presumably) reflects the block size. Is that something we should try
> to be consistent with (not sure if that's a good idea, but thought it
> was worth asking)?
>

I think this idea is a good option. I am wondering if it would be clear
when mixed with non-block-oriented IO. Block-oriented IO would say 8kB
(or whatever the build-time value of a block was) and non-block-oriented
IO would say B or kB. The math would work out.

Looking at pg_settings now though, I am confused about
how the units for wal_buffers is 8kB but then the value of wal_buffers
when I show it in psql is "16MB"...

Though the units for the pg_stat_io view for block-oriented IO would be
the build-time values for block size, so it wouldn't line up exactly
with pg_settings. However, I do like the idea of having a unit column
that reflects the value and not the name of the GUC/setting which
determined the unit. I can update this in the next version.

- Melanie

[1] https://www.postgresql.org/docs/15/monitoring-stats.html#MONITORING-PG-STAT-ACTIVITY-VIEW

---

From: Maciek Sakrejda <[email-masked]>
Date: Sun, 16 Oct 2022 22:28:34 -0700

> So, in answer to your question, no, I cannot think of a scenario like

That analysis makes sense to me; thanks.

> the reuse will take some other code changes. Perhaps we could also count

I'm not sure what makes sense here.

> also add docs on backend_types, but I'm not sure where something like

I think linking pg_stat_activity is reasonable for now. A separate
section for this might be nice at some point, but that seems out of
scope.

> counted separate from shared buffers because...?

Oh I see. That makes sense; it just wasn't obvious to me this was
talking about the last three values of io_context. I think a brief
preface like that would be helpful (maybe explicitly with "these last
three values", and I think "counted separately").

> wasn't sure if this was an objective enough point to include in docs.

Thanks for the clarification. Just to make sure I understand, you mean
that if I see a high extended count, that may be interesting in terms
of write activity, but I can't fix that by tuning--it's just the
nature of my workload?

I think you're right that this is not objective enough. It's
unfortunate that there's not a good place in the docs for info like
that, since stats like this are hard to interpret without that
context, but I admit that it's not really this patch's job to solve
that larger issue.

> IO would say B or kB. The math would work out.

Right, yeah. Although maybe that's a little confusing? When you

>"block_size" which could potentially be combined with the value of the

Is this still usable visually if you have to compare values across
units? I don't really have any great ideas here (and maybe this is
still the best option), just pointing it out.

> when I show it in psql is "16MB"...

You mean the difference between

maciek=# select setting, unit from pg_settings where name = 'wal_buffers';
 setting | unit
---------+------
 512     | 8kB
(1 row)

and

maciek=# show wal_buffers;
 wal_buffers
-------------
 4MB
(1 row)

?

Poking around, I think it looks like that's due to
convert_int_from_base_unit (indirectly called from SHOW /
current_setting):

/*
 * Convert an integer value in some base unit to a human-friendly
unit.
 *
 * The output unit is chosen so that it's the greatest unit that can
represent
 * the value without loss.  For example, if the base unit is
GUC_UNIT_KB, 1024
 * is converted to 1 MB, but 1025 is represented as 1025 kB.
 */

> with pg_settings.

I don't follow--what would be the discrepancy?

---

From: Melanie Plageman <[email-masked]>
Date: Wed, 19 Oct 2022 15:26:51 -0400

v34 is attached.
I think the column names need discussion. Also, the docs need more work
(I added a lot of new content there). I could use feedback on the column
names and definitions and review/rephrasing ideas for the docs
additions.

> That analysis makes sense to me; thanks.

I have made some major changes in this area to make the columns more
useful. I have renamed and split "clocksweeps". It is now "evicted" and
"freelist acquired". This makes it clear when a block must be evicted
from a shared buffer must be and may help to identify misconfiguration
of shared buffers.

There is some nuance here that I tried to make clear in the docs.
"freelist acquired" in a shared context is straightforward.
"freelist acquired" in a strategy context is counted when a shared
buffer is added to the strategy ring (not when it is reused).

"freelist acquired" in the local buffer context is actually the initial
allocation of a local buffer (in contrast with reuse).

"evicted" in the shared IOContext is a block being evicted from a shared
buffer in order to reuse that buffer when not using a strategy.

"evicted" in a strategy IOContext is a block being evicted from
a shared buffer in order to add that shared buffer to the strategy ring.

This is in contrast with "reused" in a strategy IOContext which is when
an existing buffer in the strategy ring has a block evicted in order to
reuse that buffer in a strategy context.

"evicted" in a local IOContext is when an existing local buffer has a
block evicted in order to reuse that local buffer.

"freelist_acquired" is confusing for local buffers but I wanted to
distinguish between reuse/eviction of local buffers and initial
allocation. "freelist_acquired" seemed more fitting because there is a
clocksweep to find a local buffer and if it hasn't been allocated yet it
is allocated in a place similar to where shared buffers acquire a buffer
from the freelist. If I didn't count it here, I would need to make a new
column only for local buffers called "allocated" or something like that.

I chose not to call "evicted" "sb_evicted"
because then we would need a separate "local_evicted". I could instead
make "local_evicted", "sb_evicted", and rename "reused" to
"strat_evicted". If I did that we would end up with separate columns for
every IO Context describing behavior when a buffer is initially acquired
vs when it is reused.

It would look something like this:

shared buffers:
    initial: freelist_acquired
    reused: sb_evicted

local buffers:
    initial: allocated
    reused: local_evicted

strategy buffers:
    initial: sb_evicted | freelist_acquired
    reused: strat_evicted
    replaced: sb_evicted | freelist_acquired

This seems not too bad at first, but if you consider that later we will
add other kinds of IO -- eg WAL IO or temporary file IO, we won't be
able to use these existing columns and will need to add even more
columns describing the exact behavior in those cases.

I wanted to devise a paradigm which allowed for reuse of columns across
IOContexts even if with slightly different meanings.

I have also added the columns "repossessed" and "rejected". "rejected"
is when a bulkread rejects a strategy buffer because it is dirty and
requires flush. Seeing a lot of rejections could indicate you need to
vacuum. "repossessed" is the number of times a strategy buffer was
pinned or in use by another backend and had to be removed from the
strategy ring and replaced with a new shared buffer. This gives you some
indication that there is contention on blocks recently used by a
strategy.

I've also added some descriptions to the docs of how these columns might
be used or what a large value in one of them may mean.

I haven't added tests for repossessed or rejected yet. I can add tests
for repossessed if we decide to keep it. Rejected is hard to write a
test for because we can't guarantee checkpointer won't clean up the
buffer before we can reject it

> I'm not sure what makes sense here.

I have fixed the counting of rejected and have made a new column
dedicated to rejected.

> three values", and I think "counted separately").

I've done this. Thanks for the suggested wording.

> of write activity, but I can't fix that by tuning--it's just the

That is correct.

> maciek=# select setting, unit from pg_settings where name = 'wal_buffers';

I've implemented a change using the same function pg_settings uses to
turn the build-time parameter BLCKSZ into 8kB (get_config_unit_name())
using the flag GUC_UNIT_BLOCKS. I am unsure if this is better or worse
than "block_size". I am feeling very conflicted about this column.

> I don't follow--what would be the discrepancy?

I got confused.
You are right -- pg_settings does seem to use the build-time value of
BLCKSZ to derive this. I was confused because the description of
pg_settings says:

"The view pg_settings provides access to run-time parameters of the server."

- Melanie
From da4e97d2c7ec2997f07df53b7d7c734d2ee9f9ab Mon Sep 17 00:00:00 2001
From: Andres Freund <[email-masked]>
Date: Thu, 13 Oct 2022 11:03:05 -0700
Subject: [PATCH v34 1/5] Remove BufferAccessStrategyData->current_was_in_ring

It is a duplication of StrategyGetBuffer->from_ring.
---
 src/backend/storage/buffer/bufmgr.c   |  2 +-
 src/backend/storage/buffer/freelist.c | 15 ++-------------
 src/include/storage/buf_internals.h   |  2 +-
 3 files changed, 4 insertions(+), 15 deletions(-)

diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 6b95381481..4e7b0b31bb 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -1254,7 +1254,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 					UnlockBufHdr(buf, buf_state);
 
 					if (XLogNeedsFlush(lsn) &&
-						StrategyRejectBuffer(strategy, buf))
+						StrategyRejectBuffer(strategy, buf, from_ring))
 					{
 						/* Drop lock/pin and loop around for another buffer */
 						LWLockRelease(BufferDescriptorGetContentLock(buf));
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index 990e081aae..64728bd7ce 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -81,12 +81,6 @@ typedef struct BufferAccessStrategyData
 	 */
 	int			current;
 
-	/*
-	 * True if the buffer just returned by StrategyGetBuffer had been in the
-	 * ring already.
-	 */
-	bool		current_was_in_ring;
-
 	/*
 	 * Array of buffer numbers.  InvalidBuffer (that is, zero) indicates we
 	 * have not yet selected a buffer for this ring slot.  For allocation
@@ -625,10 +619,7 @@ GetBufferFromRing(BufferAccessStrategy strategy, uint32 *buf_state)
 	 */
 	bufnum = strategy->buffers[strategy->current];
 	if (bufnum == InvalidBuffer)
-	{
-		strategy->current_was_in_ring = false;
 		return NULL;
-	}
 
 	/*
 	 * If the buffer is pinned we cannot use it under any circumstances.
@@ -644,7 +635,6 @@ GetBufferFromRing(BufferAccessStrategy strategy, uint32 *buf_state)
 	if (BUF_STATE_GET_REFCOUNT(local_buf_state) == 0
 		&& BUF_STATE_GET_USAGECOUNT(local_buf_state) <= 1)
 	{
-		strategy->current_was_in_ring = true;
 		*buf_state = local_buf_state;
 		return buf;
 	}
@@ -654,7 +644,6 @@ GetBufferFromRing(BufferAccessStrategy strategy, uint32 *buf_state)
 	 * Tell caller to allocate a new buffer with the normal allocation
 	 * strategy.  He'll then replace this ring element via AddBufferToRing.
 	 */
-	strategy->current_was_in_ring = false;
 	return NULL;
 }
 
@@ -682,14 +671,14 @@ AddBufferToRing(BufferAccessStrategy strategy, BufferDesc *buf)
  * if this buffer should be written and re-used.
  */
 bool
-StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf)
+StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf, bool from_ring)
 {
 	/* We only do this in bulkread mode */
 	if (strategy->btype != BAS_BULKREAD)
 		return false;
 
 	/* Don't muck with behavior of normal buffer-replacement strategy */
-	if (!strategy->current_was_in_ring ||
+	if (!from_ring ||
 		strategy->buffers[strategy->current] != BufferDescriptorGetBuffer(buf))
 		return false;
 
diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h
index 406db6be78..b75481450d 100644
--- a/src/include/storage/buf_internals.h
+++ b/src/include/storage/buf_internals.h
@@ -395,7 +395,7 @@ extern BufferDesc *StrategyGetBuffer(BufferAccessStrategy strategy,
 									 uint32 *buf_state);
 extern void StrategyFreeBuffer(BufferDesc *buf);
 extern bool StrategyRejectBuffer(BufferAccessStrategy strategy,
-								 BufferDesc *buf);
+								 BufferDesc *buf, bool from_ring);

From 96e1968827218aaa50e252840cdc224e8a92e8ee Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 6 Oct 2022 12:23:38 -0400
Subject: [PATCH v34 3/5] Aggregate IO operation stats per BackendType

 src/backend/utils/activity/pgstat_io_ops.c    | 167 ++++++++++++++++++
 src/include/pgstat.h                          |  92 ++++++++++
 13 files changed, 372 insertions(+), 6 deletions(-)

index e5d622d514..698f274341 100644
@@ -5390,6 +5390,8 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i

 

 
+	/*
 
+	/*

 /*
 
 /*

 /*
 
 /*
index 00abca4e94..8280eee731 100644
@@ -20,6 +20,51 @@
 
 static PgStat_IOContextOps pending_IOOpStats;
+bool		have_ioopstats = false;
+
+
+/*
+ * Helper function to accumulate source PgStat_IOOpCounters into target
+ * PgStat_IOOpCounters. If either of the passed-in PgStat_IOOpCounters are
+ * members of PgStatShared_IOContextOps, the caller is responsible for ensuring
+ * that the appropriate lock is held.
+ */
+static void
+pgstat_accum_io_op(PgStat_IOOpCounters *target, PgStat_IOOpCounters *source, IOOp io_op)
+{
+	switch (io_op)
+	{
+		case IOOP_EVICT:
+			target->evictions += source->evictions;
+			return;
+		case IOOP_EXTEND:
+			target->extends += source->extends;
+			return;
+		case IOOP_FREELIST_ACQUIRE:
+			target->freelist_acquisitions += source->freelist_acquisitions;
+			return;
+		case IOOP_FSYNC:
+			target->fsyncs += source->fsyncs;
+			return;
+		case IOOP_READ:
+			target->reads += source->reads;
+			return;
+		case IOOP_REPOSSESS:
+			target->repossessions += source->repossessions;
+			return;
+		case IOOP_REJECT:
+			target->rejections += source->rejections;
+			return;
+		case IOOP_REUSE:
+			target->reuses += source->reuses;
+			return;
+		case IOOP_WRITE:
+			target->writes += source->writes;
+			return;
+	}
+
+	elog(ERROR, "unrecognized IOOp value: %d", io_op);
+}
 
 void
 pgstat_count_io_op(IOOp io_op, IOContext io_context)
@@ -63,6 +108,78 @@ pgstat_count_io_op(IOOp io_op, IOContext io_context)
 			break;
 	}
 
+	have_ioopstats = true;
+}
+
 
 const char *
@@ -113,6 +230,56 @@ pgstat_io_op_desc(IOOp io_op)
 	elog(ERROR, "unrecognized IOOp value: %d", io_op);
 }
 
+void
index 55a355f583..a23a90b133 100644
 
 /*

 /*

 /*
 
 /*
 
 /*
index 96bffc0f2a..b783af130c 100644
@@ -2084,6 +2084,8 @@ pg_stat_reset_shared(PG_FUNCTION_ARGS)
@@ -2092,7 +2094,7 @@ pg_stat_reset_shared(PG_FUNCTION_ARGS)
 
 	PG_RETURN_VOID();
 }

 extern const char *GetBackendTypeDesc(BackendType backendType);
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 2844d72f86..a1c345a270 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -49,6 +49,7 @@ typedef enum PgStat_Kind
 	PGSTAT_KIND_ARCHIVER,
 	PGSTAT_KIND_BGWRITER,
 	PGSTAT_KIND_CHECKPOINTER,
+	PGSTAT_KIND_IOOPS,
 	PGSTAT_KIND_SLRU,
 	PGSTAT_KIND_WAL,
 } PgStat_Kind;
@@ -328,6 +329,12 @@ typedef struct PgStat_IOContextOps
 	PgStat_IOOpCounters data[IOCONTEXT_NUM_TYPES];
 } PgStat_IOContextOps;
 
@@ -510,6 +517,7 @@ extern PgStat_CheckpointerStats *pgstat_fetch_stat_checkpointer(void);
  */

@@ -521,6 +529,90 @@ extern bool pgstat_expect_io_op(BackendType bktype, IOContext io_context, IOOp i
 
 /* IO stats translation function in freelist.c */
 extern IOContext IOContextForStrategy(BufferAccessStrategy bas);
+/*
+ * Functions to assert that invalid IO Operation counters are zero.
+ */
+static inline void
+pgstat_io_context_ops_assert_zero(PgStat_IOOpCounters *counters)
+{
+	Assert(counters->evictions == 0 && counters->extends == 0 &&
+			counters->freelist_acquisitions == 0 && counters->fsyncs == 0 &&
+			counters->reads == 0 && counters->rejections == 0 &&
+			counters->repossessions == 0 && counters->reuses == 0 &&
+			counters->writes == 0);
+}
+
+static inline void
+pgstat_io_op_assert_zero(PgStat_IOOpCounters *counters, IOOp io_op)
+{
+	switch (io_op)
+	{
+		case IOOP_EVICT:
+			Assert(counters->evictions == 0);
+			return;
+		case IOOP_EXTEND:
+			Assert(counters->extends == 0);
+			return;
+		case IOOP_FREELIST_ACQUIRE:
+			Assert(counters->freelist_acquisitions == 0);
+			return;
+		case IOOP_FSYNC:
+			Assert(counters->fsyncs == 0);
+			return;
+		case IOOP_READ:
+			Assert(counters->reads == 0);
+			return;
+		case IOOP_REJECT:
+			Assert(counters->rejections == 0);
+			return;
+		case IOOP_REPOSSESS:
+			Assert(counters->repossessions == 0);
+			return;
+		case IOOP_REUSE:
+			Assert(counters->reuses == 0);
+			return;
+		case IOOP_WRITE:
+			Assert(counters->writes == 0);
+			return;
+	}
+
+	/* Should not reach here */
+	Assert(false);
+}
+
+/*
+ * Assert that stats have not been counted for any combination of IOContext and
+ * IOOp which are not valid for the passed-in BackendType. The passed-in array
+ * of PgStat_IOOpCounters must contain stats from the BackendType specified by
+ * the second parameter. Caller is responsible for any locking if the passed-in
+ * array of PgStat_IOOpCounters is a member of PgStatShared_IOContextOps.
+ */
+static inline void
+pgstat_backend_io_stats_assert_well_formed(PgStat_IOOpCounters
+										   backend_io_context_ops[IOCONTEXT_NUM_TYPES], BackendType bktype)
+{
+	bool		expect_backend_stats = true;
+
+	if (!pgstat_io_op_stats_collected(bktype))
+		expect_backend_stats = false;
+
+	for (int io_context = 0; io_context < IOCONTEXT_NUM_TYPES; io_context++)
+	{
+		if (!expect_backend_stats ||
+			!pgstat_bktype_io_context_valid(bktype, (IOContext) io_context))
+		{
+			pgstat_io_context_ops_assert_zero(&backend_io_context_ops[io_context]);
+			continue;
+		}
+
+		for (int io_op = 0; io_op < IOOP_NUM_TYPES; io_op++)
+		{
+			if (!pgstat_io_op_valid(bktype, (IOContext) io_context, (IOOp) io_op))
+				pgstat_io_op_assert_zero(&backend_io_context_ops[io_context],
+						(IOOp) io_op);
+		}
+	}
+}

 /*

 
 
 	PgStat_WalStats wal;

+/*

+/*
 
 /*
index 600c0fe855..10781480cd 100644

From c540da75759239dd78b096e5b6711f79270f4672 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 6 Oct 2022 12:23:25 -0400
Subject: [PATCH v34 2/5] Track IO operation statistics locally

Each IOOp (evict, freelist acquisition, reject, repossess, reuse, read,
write, extend, and fsync) is counted per IOContext (bulkread, bulkwrite,
local, shared, or vacuum) through a call to pgstat_count_io_op().

IOOP_FREELIST_ACQUIRE and IOOP_EVICT IOOps are counted in
IOCONTEXT_SHARED and IOCONTEXT_LOCAL IOContexts when a buffer is
acquired or allocated through [Local]BufferAlloc() and no
BufferAccessStrategy is in use.

When a BufferAccessStrategy is in use, shared buffers added to the
strategy ring are counted as IOOP_FREELIST_ACQUIRE or IOOP_EVICT IOOps
in the IOCONTEXT_[BULKREAD|BULKWRITE|VACUUM) IOContext. When one of
these buffers is reused, it is counted as an IOOP_REUSE IOOp in the
corresponding strategy IOContext.

Author: Melanie Plageman <[email-masked]>
Reviewed-by: Andres Freund <[email-masked]>
Reviewed-by: Justin Pryzby <[email-masked]>
Reviewed-by: Kyotaro Horiguchi <[email-masked]>
Reviewed-by: Maciek Sakrejda <[email-masked]>
Reviewed-by: Lukas Fittl <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/20200124195226.lth52iydq2n2uilq%40alap3.anarazel.de
---
 src/backend/postmaster/checkpointer.c      |  13 ++
 src/backend/storage/buffer/bufmgr.c        |  73 +++++-
 src/backend/storage/buffer/freelist.c      |  71 +++++-
 src/backend/storage/buffer/localbuf.c      |  19 ++
 src/backend/storage/sync/sync.c            |   2 +
 src/backend/utils/activity/Makefile        |   1 +
 src/backend/utils/activity/meson.build     |   1 +
 src/backend/utils/activity/pgstat_io_ops.c | 260 +++++++++++++++++++++
 src/include/pgstat.h                       |  70 ++++++
 src/include/storage/buf_internals.h        |   2 +-
 src/include/storage/bufmgr.h               |   7 +-
 src/tools/pgindent/typedefs.list           |   4 +
 12 files changed, 510 insertions(+), 13 deletions(-)
 create mode 100644 src/backend/utils/activity/pgstat_io_ops.c

index 4e7b0b31bb..f064b7faf3 100644

 
+	if (strategy)
@@ -990,6 +998,7 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 
 	if (isExtend)
 	{
@@ -1020,6 +1029,8 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,

+			pgstat_count_io_op(IOOP_READ, io_context);
+
 			if (track_io_timing)
 			{
 				INSTR_TIME_SET_CURRENT(io_time);
@@ -1190,6 +1201,8 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	/* Loop here in case we have to try another victim buffer */
 	for (;;)
 	{
+		bool		from_ring;
+
 		/*
 		 * Ensure, while the spinlock's not yet held, that there's a free
 		 * refcount entry.
@@ -1200,7 +1213,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 		 * Select a victim buffer.  The buffer is returned with its header
 		 * spinlock still held!
 		 */
-		buf = StrategyGetBuffer(strategy, &buf_state);
+		buf = StrategyGetBuffer(strategy, &buf_state, &from_ring);

@@ -1237,6 +1250,8 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 			if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf),
 										 LW_SHARED))
 			{
+				IOContext	io_context;
+
 				/*
 				 * If using a nondefault strategy, and writing the buffer
 				 * would require a WAL flush, let the strategy decide whether
@@ -1263,13 +1278,36 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 					}
 				}
 
+				/*
+				 * When a strategy is in use, only flushes of dirty buffers
+				 * already in the strategy ring are counted as strategy writes
+				 * (IOCONTEXT [BULKREAD|BULKWRITE|VACUUM] IOOP_WRITE) for the
+				 * purpose of IO operation statistics tracking.
+				 *
+				 * If a shared buffer initially added to the ring must be
+				 * flushed before being used, this is counted as an
+				 * IOCONTEXT_SHARED IOOP_WRITE.
+				 *
+				 * If a shared buffer added to the ring later because the
+				 * current strategy buffer is pinned or in use or because all
+				 * strategy buffers were dirty and rejected (for BAS_BULKREAD
+				 * operations only) requires flushing, this is counted as an
+				 * IOCONTEXT_SHARED IOOP_WRITE (from_ring will be false).
+				 *
+				 * When a strategy is not in use, the write can only be a
+				 * "regular" write of a dirty shared buffer (IOCONTEXT_SHARED
+				 * IOOP_WRITE).
+				 */
+
+				io_context = IOContextForStrategy(strategy);
+
 				/* OK, do the I/O */
 				TRACE_POSTGRESQL_BUFFER_WRITE_DIRTY_START(forkNum, blockNum,
 														  smgr->smgr_rlocator.locator.spcOid,
 														  smgr->smgr_rlocator.locator.dbOid,
 														  smgr->smgr_rlocator.locator.relNumber);

 				ScheduleBufferTagForWriteback(&BackendWritebackContext,
@@ -1291,6 +1329,19 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 			}
 		}
 
+		/*
+		* When a BufferAccessStrategy is in use, reused buffers from the
+		* strategy ring will be counted as IOCONTEXT_BULKREAD,
+		* IOCONTEXT_BULKWRITE, or IOCONTEXT_VACUUM reuses for the
+		* purposes of IO Operation statistics tracking.
+		*
+		* We wait until this point to count reuses to avoid incorrectly
+		* counting a buffer as reused when it was rejected or concurrently
+		* pinned.
+		*/
+		if (from_ring)
+			pgstat_count_io_op(IOOP_REUSE, IOContextForStrategy(strategy));
+
 		/*
 		 * To change the association of a valid buffer, we'll need to have
 		 * exclusive lock on both the old and new mapping partitions.
@@ -2570,7 +2621,7 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
 	PinBuffer_Locked(bufHdr);
 	LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);

 
@@ -2820,7 +2871,7 @@ BufferGetTag(Buffer buffer, RelFileLocator *rlocator, ForkNumber *forknum,
  * as the second parameter.  If not, pass NULL.
  */
 static void
-FlushBuffer(BufferDesc *buf, SMgrRelation reln)
+FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOContext io_context)
 {
 	XLogRecPtr	recptr;
 	ErrorContextCallback errcallback;
@@ -2900,6 +2951,8 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln)
 	 */
 	bufToWrite = PageSetChecksumCopy((Page) bufBlock, buf->tag.blockNum);

@@ -3551,6 +3604,8 @@ FlushRelationBuffers(Relation rel)
 						  localpage,
 						  false);

@@ -3586,7 +3641,7 @@ FlushRelationBuffers(Relation rel)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, RelationGetSmgr(rel));
+			FlushBuffer(bufHdr, RelationGetSmgr(rel), IOCONTEXT_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr);
 		}
@@ -3684,7 +3739,7 @@ FlushRelationsAllBuffers(SMgrRelation *smgrs, int nrels)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, srelent->srel);
+			FlushBuffer(bufHdr, srelent->srel, IOCONTEXT_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr);
 		}
@@ -3894,7 +3949,7 @@ FlushDatabaseBuffers(Oid dbid)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, NULL);
+			FlushBuffer(bufHdr, NULL, IOCONTEXT_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr);
 		}
@@ -3921,7 +3976,7 @@ FlushOneBuffer(Buffer buffer)

 
 /*
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index 64728bd7ce..72028372be 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -15,6 +15,7 @@
  */
 #include "postgres.h"
 
+#include "pgstat.h"
@@ -192,13 +193,15 @@ have_free_buffer(void)
 
+	*from_ring = false;
+
 	/*
 	 * If given a strategy object, see whether it can select a buffer. We
 	 * assume strategy objects don't need buffer_strategy_lock.
@@ -207,7 +210,10 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
 	{
 		buf = GetBufferFromRing(strategy, buf_state);
 		if (buf != NULL)
+		{
+			*from_ring = true;
 			return buf;
+		}
 	}
 
 	/*
@@ -299,6 +305,15 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
 				if (strategy != NULL)
 					AddBufferToRing(strategy, buf);
 				*buf_state = local_buf_state;
+
+				/*
+				 * When a strategy is in use, IO Operation statistics count
+				 * buffers acquired from the freelist when a strategy is in use
+				 * in their corresponding strategy IOContext even though the
+				 * buffer acquired from the freelist is a shared buffer.
+				 */
+				pgstat_count_io_op(IOOP_FREELIST_ACQUIRE,
+						IOContextForStrategy(strategy));
 				return buf;
 			}
 			UnlockBufHdr(buf, local_buf_state);
@@ -331,6 +346,19 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
 				if (strategy != NULL)
 					AddBufferToRing(strategy, buf);
 				*buf_state = local_buf_state;
+				/*
+				* When a BufferAccessStrategy is in use, evictions adding a
+				* shared buffer to the strategy ring are counted in the
+				* corresponding strategy's context. This includes the evictions
+				* done to add buffers to the ring initially as well as those
+				* done to add a new shared buffer to the ring when current
+				* buffer is pinned or otherwise in use.
+				*
+				* We wait until this point to count evictions to avoid
+				* incorrectly counting cases in which we error out.
+				*/
+				pgstat_count_io_op(IOOP_EVICT, IOContextForStrategy(strategy));
+
 				return buf;
 			}
 		}
@@ -596,7 +624,7 @@ FreeAccessStrategy(BufferAccessStrategy strategy)
 
 /*
  * GetBufferFromRing -- returns a buffer from the ring, or NULL if the
- *		ring is empty.
+ *		ring is empty / not usable.
  *
  * The bufhdr spin lock is held on the returned buffer.
  */
@@ -643,7 +671,13 @@ GetBufferFromRing(BufferAccessStrategy strategy, uint32 *buf_state)
 	/*
 	 * Tell caller to allocate a new buffer with the normal allocation
 	 * strategy.  He'll then replace this ring element via AddBufferToRing.
+	 *
+	 * This counts as a "repossession" for the purposes of IO operation
+	 * statistic tracking, since the reason that we no longer consider the
+	 * current buffer to be part of the ring is that the block in it is in use
+	 * outside of the ring, preventing us from reusing the buffer.
 	 */
+	pgstat_count_io_op(IOOP_REPOSSESS, IOContextForStrategy(strategy));
 	return NULL;
 }
 
@@ -659,6 +693,37 @@ AddBufferToRing(BufferAccessStrategy strategy, BufferDesc *buf)
 	strategy->buffers[strategy->current] = BufferDescriptorGetBuffer(buf);
 }
 
+/*
+	if (!strategy)
+			 * BufferAccessStrategyType BAS_NORMAL, so this case is
+			 * unreachable.
+			pg_unreachable();
@@ -688,5 +753,7 @@ StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf, bool from_r
 
+	pgstat_count_io_op(IOOP_REJECT, IOContextForStrategy(strategy));
+
 	return true;
 }
diff --git a/src/backend/storage/buffer/localbuf.c b/src/backend/storage/buffer/localbuf.c
index 30d67d1c40..bfcd35e4e2 100644
--- a/src/backend/storage/buffer/localbuf.c
+++ b/src/backend/storage/buffer/localbuf.c
@@ -18,6 +18,7 @@
 #include "access/parallel.h"
 #include "catalog/catalog.h"
 #include "executor/instrument.h"
+#include "pgstat.h"
 #include "storage/buf_internals.h"
 #include "storage/bufmgr.h"
 #include "utils/guc_hooks.h"
@@ -117,6 +118,8 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
 	bool		found;
 	uint32		buf_state;
 
+	IOOp	io_op = IOOP_EVICT;
+
 	InitBufferTag(&newTag, &smgr->smgr_rlocator.locator, forkNum, blockNum);
 
 	/* Initialize local buffers if first request in this session */
@@ -196,6 +199,7 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
 				LocalRefCount[b]++;
 				ResourceOwnerRememberBuffer(CurrentResourceOwner,
 											BufferDescriptorGetBuffer(bufHdr));
+
 				break;
 			}
 		}
@@ -226,6 +230,8 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
 				  localpage,
 				  false);
 
+		pgstat_count_io_op(IOOP_WRITE, IOCONTEXT_LOCAL);
+
 		/* Mark not-dirty now in case we error out below */
 		buf_state &= ~BM_DIRTY;
 		pg_atomic_unlocked_write_u32(&bufHdr->state, buf_state);
@@ -238,6 +244,14 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
 	 */
 	if (LocalBufHdrGetBlock(bufHdr) == NULL)
 	{
+		/*
+		 * If this is the first use of the buffer count it as a "freelist
+		 * acquire". This isn't a perfect description of this allocation, since
+		 * we do not maintain a freelist with local buffers, however it allows
+		 * us to distinguish between initial use and evictions of local
+		 * buffers.
+		 */
+		io_op = IOOP_FREELIST_ACQUIRE;
 		/* Set pointer for use by BufferGetBlock() macro */
 		LocalBufHdrGetBlock(bufHdr) = GetLocalBufferStorage();
 	}
@@ -275,6 +289,11 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
 	pg_atomic_unlocked_write_u32(&bufHdr->state, buf_state);
 
 	*foundPtr = false;
+
+	/*
+	 * Count the IOOp here after we've ensured we were successful.
+	 */
+	pgstat_count_io_op(io_op, IOCONTEXT_LOCAL);
 	return bufHdr;
 }

index 0000000000..00abca4e94
@@ -0,0 +1,260 @@
+		case IOOP_EVICT:
+			pending_counters->evictions++;
+		case IOOP_FREELIST_ACQUIRE:
+			pending_counters->freelist_acquisitions++;
+		case IOOP_REPOSSESS:
+			pending_counters->repossessions++;
+		case IOOP_REJECT:
+			pending_counters->rejections++;
+		case IOOP_EVICT:
+			return "evicted";
+			return "extended";
+		case IOOP_FREELIST_ACQUIRE:
+			return "freelist acquired";
+			return "files synced";
+		case IOOP_REPOSSESS:
+			return "repossessed";
+		case IOOP_REJECT:
+			return "rejected";
+			return "written";
+		(io_op == IOOP_READ || io_op == IOOP_EVICT || io_op == IOOP_FREELIST_ACQUIRE))
+	 * Some IOOps are not valid in certain IOContexts and some IOOps are only
+	 * valid in certain contexts.
+	if (io_context == IOCONTEXT_BULKREAD && io_op == IOOP_EXTEND)
+	 * Only BAS_BULKREAD will reject strategy buffers
+	if (io_context != IOCONTEXT_BULKREAD && io_op == IOOP_REJECT)
+	 * IOOP_REPOSSESS and IOOP_REUSE are only relevant when a
+	 * BufferAccessStrategy is in use.
+	if (!strategy_io_context && (io_op == IOOP_REJECT || io_op ==
+				IOOP_REPOSSESS || io_op == IOOP_REUSE))
index 9e2ce6f011..2844d72f86 100644
@@ -276,6 +277,57 @@ typedef struct PgStat_CheckpointerStats
 
+/*
+ * Types related to counting IO Operations for various IO Contexts
+ * When adding a new value, ensure that the proper assertions are added to
+ * pgstat_io_context_ops_assert_zero() and pgstat_io_op_assert_zero() (though
+ * the compiler will remind you about the latter)
+ */
+
+typedef enum IOOp
+{
+	IOOP_EVICT = 0,
+	IOOP_EXTEND,
+	IOOP_FREELIST_ACQUIRE,
+	IOOP_FSYNC,
+	IOOP_READ,
+	IOOP_REJECT,
+	IOOP_REPOSSESS,
+	IOOP_REUSE,
+	IOOP_WRITE,
+} IOOp;
+
+#define IOOP_NUM_TYPES (IOOP_WRITE + 1)
+
+typedef enum IOContext
+{
+	IOCONTEXT_BULKREAD = 0,
+	IOCONTEXT_BULKWRITE,
+	IOCONTEXT_LOCAL,
+	IOCONTEXT_SHARED,
+	IOCONTEXT_VACUUM,
+} IOContext;
+
+#define IOCONTEXT_NUM_TYPES (IOCONTEXT_VACUUM + 1)
+
+typedef struct PgStat_IOOpCounters
+{
+	PgStat_Counter evictions;
+	PgStat_Counter extends;
+	PgStat_Counter freelist_acquisitions;
+	PgStat_Counter fsyncs;
+	PgStat_Counter reads;
+	PgStat_Counter rejections;
+	PgStat_Counter reuses;
+	PgStat_Counter repossessions;
+	PgStat_Counter writes;
+} PgStat_IOOpCounters;
+
+typedef struct PgStat_IOContextOps
+{
+	PgStat_IOOpCounters data[IOCONTEXT_NUM_TYPES];
+} PgStat_IOContextOps;
+
 typedef struct PgStat_StatDBEntry
 {
 	PgStat_Counter n_xact_commit;
@@ -453,6 +505,24 @@ extern void pgstat_report_checkpointer(void);
 extern PgStat_CheckpointerStats *pgstat_fetch_stat_checkpointer(void);

+/*
index b75481450d..7b67250747 100644
 
 /* freelist.c */
 								 BufferDesc *buf, bool from_ring);
 
 typedef void *Block;
 
index d9b839c979..600c0fe855 100644

From 1de196bad4aba06beb32ba6487145d765794a7c5 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
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).

Author: Melanie Plageman <[email-masked]>
Reviewed-by: Andres Freund <[email-masked]>
Reviewed-by: Justin Pryzby <[email-masked]>
Reviewed-by: Kyotaro Horiguchi <[email-masked]>
Reviewed-by: Maciek Sakrejda <[email-masked]>
Reviewed-by: Lukas Fittl <[email-masked]>
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(-)

index 698f274341..e144bb0c35 100644
 
+     <row>
@@ -3615,7 +3623,325 @@ SELECT pid, wait_event_type, wait_event FROM pg_stat_activity WHERE wait_event i
+   by that backend type in that IO context. Currently only a subset of IO
+       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;
 
+       b.freelist_acquired,
+       b.evicted,
+       b.rejected,
+       b.repossessed,
index b783af130c..74ee9f61d0 100644
@@ -29,6 +29,7 @@
 #include "storage/procarray.h"
 #include "utils/acl.h"
+#include "utils/guc.h"
 #include "utils/inet.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

---

From: Andres Freund <[email-masked]>
Date: Thu, 20 Oct 2022 10:31:01 -0700

Hi,

- we shouldn't do pgstat_count_io_op() while the buffer header lock is held,
  if possible.

  I wonder if we should add a "source" output argument to
  StrategyGetBuffer(). Then nearly all the counting can happen in
  BufferAlloc().

- "repossession" is a very unintuitive name for me. If we want something like
  it, can't we just name it reuse_failed or such?

- Wonder if the column names should be reads, writes, extends, etc instead of
  the current naming pattern

- Is it actually correct to count evictions in StrategyGetBuffer()? What if we
  then decide to not use that buffer in BufferAlloc()? Yes, that'll be counted
  via rejected, but that still leaves the eviction count to be "misleading"?

> from a shared buffer must be and may help to identify misconfiguration

I'm not sure freelist acquired is really that useful? If we don't add it, we
should however definitely not count buffers from the freelist as evictions.

> buffer is added to the strategy ring (not when it is reused).

Not sure what the second half here means - why would a buffer that's not from
the freelist ever be counted as being from the freelist?

> column only for local buffers called "allocated" or something like that.

I think you're making this too granular. We need to have more detail than
today. But we don't necessarily need to catch every nuance.

> columns describing the exact behavior in those cases.

I think it's clearly not the right direction.

> indication that there is contention on blocks recently used by a

I don't immediately see a real use case for repossessed. Why isn't it
sufficient to count it as part of rejected?

Greetings,

Andres Freund

---

From: Maciek Sakrejda <[email-masked]>
Date: Sun, 23 Oct 2022 15:35:38 -0700

> names and definitions and review/rephrasing ideas for the docs

Nice! I think the expanded docs are great, and make this information
much easier to interpret.

>+       dirty buffers in the ring requirng flush are

"requiring"

>+       strategy ring lazily. If the current buffer in the ring is pinned or in

This is the first mention of the term "strategy" in these docs. It's
not totally opaque, since there's some context, but maybe we should
either try to avoid that term or define it more explicitly?

>+       of the buffer are <quote>evicted</quote> but refers to the case when the

I don't quite follow this: does this mean that I should expect
'reused' and 'evicted' to be equal in the 'shared' context, because
they represent the same thing? Or will 'reused' just be null because
it's not distinct from 'evicted'? It looks like it's null right now,
but I find the wording here confusing.

>+      performing on-access pruning.

This is great. Just wanted to re-iterate that notes like this are
really helpful to understanding this view.

> than "block_size". I am feeling very conflicted about this column.

Yeah, I guess it feels less natural here than in pg_settings, but it
still kind of feels like one way of doing this is better than two...

---

From: Maciek Sakrejda <[email-masked]>
Date: Sun, 23 Oct 2022 15:48:09 -0700

>   it, can't we just name it reuse_failed or such?

+1, I think "repossessed" is awkward. I think "reuse_failed" works,
but no strong opinions on an alternate name.

>   the current naming pattern

Why? Lukas suggested alignment with existing views like
pg_stat_database and pg_stat_statements. It doesn't make sense to use
the blks_ prefix since it's not all blocks, but otherwise it seems
like we should be consistent, no?

> today. But we don't necessarily need to catch every nuance.

In general I agree that coarser granularity here may be easier to use.
I do think the current docs explain what's going on pretty well,
though, and I worry if merging too many concepts will make that harder
to follow. But if a less detailed breakdown still communicates
potential problems, +1.

> I think it's clearly not the right direction.

+1, I think the existing approach makes more sense.

---

From: Melanie Plageman <[email-masked]>
Date: Mon, 24 Oct 2022 14:38:52 -0400

> - we shouldn't do pgstat_count_io_op() while the buffer header lock is held,

I've changed this locally. It will be fixed in the next version I share.

>   StrategyGetBuffer(). Then nearly all the counting can happen in

I think we can just check for BM_VALID being set before invalidating it
in order to claim the buffer at the end of BufferAlloc(). Then we can
count it as an eviction or reuse.

>   it, can't we just name it reuse_failed or such?

Repossession could be called eviction_failed or reuse_failed.
Do we think we will ever want to use it to count buffers we released
in other IOContexts (thus making the name eviction_failed better than
reuse_failed)?

>   via rejected, but that still leaves the eviction count to be "misleading"?

I agree that counting evictions in StrategyGetBuffer() is incorrect.
Checking BM_VALID at bottom of BufferAlloc() should be better.

> today. But we don't necessarily need to catch every nuance.

I am fine with cutting freelist_acquired. The same actionable
information that it could provide could be provided by "read", right?
Also, removing it means I can remove the complicated explanation of how
freelist_acquired should be interpreted in IOCONTEXT_LOCAL.

Speaking of IOCONTEXT_LOCAL, I was wondering if it is confusing to call
it IOCONTEXT_LOCAL since it refers to IO done for temporary tables. What
if, in the future, we want to track other IO done using data in local
memory? Also, what if we want to track other IO done using data from
shared memory that is not in shared buffers? Would IOCONTEXT_SB and
IOCONTEXT_TEMP be better? Should IOContext literally describe the
context of the IO being done and there be a separate column which
indicates the source of the data for the IO?
Like wal_buffer, local_buffer, shared_buffer? Then if it is not
block-oriented, it could be shared_mem, local_mem, or bypass?

If we had another dimension to the matrix "data_src" which, with
block-oriented IO is equivalent to "buffer type", this could help with
some of the clarity problems.

We could remove the "reused" column and that becomes:

IOCONTEXT |     DATA_SRC    | IOOP
----------------------------------------
strategy  | strategy_buffer | EVICT

Having data_src and iocontext simplifies the meaning of all io
operations involving a strategy. Some operations are done on shared
buffers and some on existing strategy buffers and this would be more
clear without the addition of special columns for strategies.

> sufficient to count it as part of rejected?

I'm still on the fence about combining rejection and reuse_failed. A
buffer rejected by a bulkread for being dirty may indicate the need to
vacuum but doesn't say anything about contention.
Whereas, failed reuses indicate contention for the blocks operated on by
the strategy. You would react to them differently. And you could have a
bulkread racking up both failed reuses and rejections.

If this seems like an unlikely or niche case, I would be okay with
combining rejections with reuse_failed. But it would be nice if we could
help with interpreting the column. I wonder if there is a rule of thumb
for determining which scenario you have. For example, how likely is it
that if you see a high number of reuse_rejected in a bulkread IOContext
that you would see any reused if the rejections are due to the bulkread
dirtying its own buffers? I suppose it would depend on your workload and
how random your updates/deletes were? If there is some way to use
reuse_rejected in combination with another column to determine the cause
of the rejections, it would be easier to combine them.

- Melanie

---

From: Melanie Plageman <[email-masked]>
Date: Mon, 24 Oct 2022 15:39:01 -0400

Thanks!

> either try to avoid that term or define it more explicitly?

I am thinking it might be good to define the term strategy for use in
this view documentation.
In the IOContext column documentation, I've added this
...
    avoid occupying an undue portion of the main shared buffer pool. This
    pattern is called a Buffer Access Strategy and the fixed-size ring
    buffer can be referred to as a <quote>strategy ring buffer</quote>.
</para></entry>

I was thinking this would allow me to refer to the strategy ring buffer
more easily. I fear simply referring to "the" ring buffer throughout
this view documentation will be confusing.

> but I find the wording here confusing.

You should only see evictions when the strategy evicts shared buffers
and reuses when the strategy evicts existing strategy buffers.

How about this instead in this docs?

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>, or <literal>vacuum</literal>
<varname>io_context</varname>s. when a buffer access strategy
<quote>reuses</quote> a buffer in the strategy ring, it must evict its
contents, incrementing <varname>reused</varname>. when a buffer access
strategy adds a new shared buffer to the strategy ring and this shared
buffer is occupied, the buffer access strategy must evict the contents
of the shared buffer, incrementing <varname>evicted</varname>.

> still kind of feels like one way of doing this is better than two...

So, Andres pointed out that it would be nice to be able to multiply the
unit column by the operation column (e.g. select unit * reused from
pg_stat_io...) and get a number of bytes. Then you can use
pg_size_pretty to convert it to something more human readable.

It probably shouldn't be called unit, then, since that would be the same
name as pg_settings but a different meaning. I thought of
"bytes_conversion". Then, non-block-oriented IO also wouldn't have to be
in bytes. They could put 1000 or 10000 for bytes_conversion.

What do you think?

- Melanie

---

From: Melanie Plageman <[email-masked]>
Date: Tue, 25 Oct 2022 23:15:06 -0400

v35 is attached

> count it as an eviction or reuse.

Done this in attached version

> > today. But we don't necessarily need to catch every nuance.

I cut freelist_acquired in attached version.

> block-oriented, it could be shared_mem, local_mem, or bypass?

pg_stat_statements uses local_blks_read and temp_blks_read for local
buffers for temp tables and temp file IO respectively -- so perhaps we
should stick to that

Other updates in this version:

I've also updated the unit column to bytes_conversion.

I've made quite a few updates to the docs including more information
on overlaps between pg_stat_database, pg_statio_*, and
pg_stat_statements.

Let me know if there are other configuration tip resources from the
existing docs that I could link in the column "files_synced".

I still need to look at the docs with fresh eyes and do another round of
cleanup (probably).

- Melanie
From b19456745d4431c2271fa9fbd57148d4733a7f66 Mon Sep 17 00:00:00 2001
From: Andres Freund <[email-masked]>
Date: Thu, 13 Oct 2022 11:03:05 -0700
Subject: [PATCH v35 1/5] Remove BufferAccessStrategyData->current_was_in_ring

 
 
-	/*
 
 	/*

 

From 629683d8fa63064ec55da2d65794d5e5af251407 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 6 Oct 2022 12:23:25 -0400
Subject: [PATCH v35 2/5] Track IO operation statistics locally

 src/backend/storage/buffer/bufmgr.c        |  84 ++++++-
 src/backend/utils/activity/pgstat_io_ops.c | 255 +++++++++++++++++++++
 src/include/pgstat.h                       |  68 ++++++
 12 files changed, 481 insertions(+), 13 deletions(-)

index 4e7b0b31bb..9f25c5ce32 100644
 
@@ -833,6 +834,11 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,

+	if (isLocalBuf)
@@ -990,6 +996,7 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 
 	if (isExtend)
 	{
@@ -1015,6 +1022,9 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 			instr_time	io_start,
 						io_time;

@@ -1121,6 +1131,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 			BufferAccessStrategy strategy,
 			bool *foundPtr)
 {
+	bool		from_ring;
 	BufferTag	newTag;			/* identity of requested block */
 	uint32		newHash;		/* hash value for newTag */
 	LWLock	   *newPartitionLock;	/* buffer partition lock for it */
@@ -1190,6 +1201,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	/* Loop here in case we have to try another victim buffer */
 	for (;;)
 	{
+
 		/*
 		 * Ensure, while the spinlock's not yet held, that there's a free
 		 * refcount entry.
@@ -1200,7 +1212,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 		 * Select a victim buffer.  The buffer is returned with its header
 		 * spinlock still held!
 		 */
-		buf = StrategyGetBuffer(strategy, &buf_state);
+		buf = StrategyGetBuffer(strategy, &buf_state, &from_ring);

@@ -1237,6 +1249,8 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 			if (LWLockConditionalAcquire(BufferDescriptorGetContentLock(buf),
 										 LW_SHARED))
 			{
+				IOContext	io_context;
+
 				/*
 				 * If using a nondefault strategy, and writing the buffer
 				 * would require a WAL flush, let the strategy decide whether
@@ -1263,13 +1277,36 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 					}
 				}
 
+				/*

 				ScheduleBufferTagForWriteback(&BackendWritebackContext,
@@ -1441,6 +1478,31 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,

+	if (oldFlags & BM_VALID)
+	{
+		/*
+		* When a BufferAccessStrategy is in use, evictions adding a
+		* shared buffer to the strategy ring are counted in the
+		* corresponding strategy's context. This includes the evictions
+		* done to add buffers to the ring initially as well as those
+		* done to add a new shared buffer to the ring when current
+		* buffer is pinned or otherwise in use.
+		*
+		* Blocks evicted from buffers already in the strategy ring are counted
+		* as IOCONTEXT_BULKREAD, IOCONTEXT_BULKWRITE, or IOCONTEXT_VACUUM
+		* reuses.
+		*
+		* We wait until this point to count reuses and evictions in order to
+		* avoid incorrectly counting a buffer as reused or evicted when it was
+		* released because it was concurrently pinned or in use or counting it
+		* as reused when it was rejected or when we errored out.
+		*/
+		if (from_ring)
+			pgstat_count_io_op(IOOP_REUSE, IOContextForStrategy(strategy));
+		else
+			pgstat_count_io_op(IOOP_EVICT, IOCONTEXT_SHARED);
+	}
+
 	if (oldPartitionLock != NULL)
 	{
 		BufTableDelete(&oldTag, oldHash);
@@ -2570,7 +2632,7 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)
 	PinBuffer_Locked(bufHdr);
 	LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);

 
@@ -2820,7 +2882,7 @@ BufferGetTag(Buffer buffer, RelFileLocator *rlocator, ForkNumber *forknum,
  * as the second parameter.  If not, pass NULL.
  */
 static void
-FlushBuffer(BufferDesc *buf, SMgrRelation reln)
+FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOContext io_context)
 {
 	XLogRecPtr	recptr;
 	ErrorContextCallback errcallback;
@@ -2900,6 +2962,8 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln)
 	 */
 	bufToWrite = PageSetChecksumCopy((Page) bufBlock, buf->tag.blockNum);

@@ -3551,6 +3615,8 @@ FlushRelationBuffers(Relation rel)
 						  localpage,
 						  false);

@@ -3586,7 +3652,7 @@ FlushRelationBuffers(Relation rel)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, RelationGetSmgr(rel));
+			FlushBuffer(bufHdr, RelationGetSmgr(rel), IOCONTEXT_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr);
 		}
@@ -3684,7 +3750,7 @@ FlushRelationsAllBuffers(SMgrRelation *smgrs, int nrels)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, srelent->srel);
+			FlushBuffer(bufHdr, srelent->srel, IOCONTEXT_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr);
 		}
@@ -3894,7 +3960,7 @@ FlushDatabaseBuffers(Oid dbid)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, NULL);
+			FlushBuffer(bufHdr, NULL, IOCONTEXT_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr);
 		}
@@ -3921,7 +3987,7 @@ FlushOneBuffer(Buffer buffer)

 
 /*
diff --git a/src/backend/storage/buffer/freelist.c b/src/backend/storage/buffer/freelist.c
index 64728bd7ce..6eb2e00ae2 100644
--- a/src/backend/storage/buffer/freelist.c
+++ b/src/backend/storage/buffer/freelist.c
@@ -15,6 +15,7 @@
  */
 #include "postgres.h"
 
+#include "pgstat.h"
 
+	*from_ring = false;
+
 	/*
 
 	/*
@@ -299,6 +305,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
 				if (strategy != NULL)
 					AddBufferToRing(strategy, buf);
 				*buf_state = local_buf_state;
+
 				return buf;
 			}
 			UnlockBufHdr(buf, local_buf_state);
@@ -331,6 +338,7 @@ StrategyGetBuffer(BufferAccessStrategy strategy, uint32 *buf_state)
 				if (strategy != NULL)
 					AddBufferToRing(strategy, buf);
 				*buf_state = local_buf_state;
+
 				return buf;
 			}
 		}
@@ -596,7 +604,7 @@ FreeAccessStrategy(BufferAccessStrategy strategy)
 
 /*
@@ -643,7 +651,13 @@ GetBufferFromRing(BufferAccessStrategy strategy, uint32 *buf_state)
 
@@ -659,6 +673,37 @@ AddBufferToRing(BufferAccessStrategy strategy, BufferDesc *buf)
 	strategy->buffers[strategy->current] = BufferDescriptorGetBuffer(buf);
 }
 
+/*
@@ -688,5 +733,7 @@ StrategyRejectBuffer(BufferAccessStrategy strategy, BufferDesc *buf, bool from_r
 
index 30d67d1c40..cb9685564f 100644
@@ -196,6 +197,7 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
@@ -226,6 +228,8 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
 				  localpage,
 				  false);
 
+		pgstat_count_io_op(IOOP_WRITE, IOCONTEXT_LOCAL);
+
 		/* Mark not-dirty now in case we error out below */
 		buf_state &= ~BM_DIRTY;
 		pg_atomic_unlocked_write_u32(&bufHdr->state, buf_state);
@@ -256,6 +260,7 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
 		ClearBufferTag(&bufHdr->tag);
 		buf_state &= ~(BM_VALID | BM_TAG_VALID);
 		pg_atomic_unlocked_write_u32(&bufHdr->state, buf_state);
+		pgstat_count_io_op(IOOP_EVICT, IOCONTEXT_LOCAL);
 	}
 
 	hresult = (LocalBufferLookupEnt *)
@@ -275,6 +280,7 @@ LocalBufferAlloc(SMgrRelation smgr, ForkNumber forkNum, BlockNumber blockNum,
 	pg_atomic_unlocked_write_u32(&bufHdr->state, buf_state);
 
 	*foundPtr = false;
+
 	return bufHdr;
 }

index 0000000000..6f9c250907
@@ -0,0 +1,255 @@
+		(io_op == IOOP_READ || io_op == IOOP_EVICT))
index 9e2ce6f011..5883aafe9c 100644
@@ -276,6 +277,55 @@ typedef struct PgStat_CheckpointerStats
 
+/*
@@ -453,6 +503,24 @@ extern void pgstat_report_checkpointer(void);

+/*
 
 /* freelist.c */
 
 typedef void *Block;
 
index 2f02cc8f42..b080367073 100644

From 4562099222f41866099835ccfca40f22abfd9e8f Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 6 Oct 2022 12:24:42 -0400
Subject: [PATCH v35 4/5] Add system view tracking IO ops per backend type

Author: Melanie Plageman <[email-masked]>
Reviewed-by: Andres Freund <[email-masked]>
Reviewed-by: Justin Pryzby <[email-masked]>
Reviewed-by: Kyotaro Horiguchi <[email-masked]>
Reviewed-by: Maciek Sakrejda <[email-masked]>
Reviewed-by: Lukas Fittl <[email-masked]>
Discussion: https://www.postgresql.org/message-id/flat/20200124195226.lth52iydq2n2uilq%40alap3.anarazel.de
---
 doc/src/sgml/monitoring.sgml         | 384 +++++++++++++++++++++++++--
 src/backend/catalog/system_views.sql |  16 ++
 src/backend/utils/adt/pgstatfuncs.c  | 139 ++++++++++
 src/include/catalog/pg_proc.dat      |   9 +
 src/test/regress/expected/rules.out  |  13 +
 src/test/regress/expected/stats.out  | 224 ++++++++++++++++
 src/test/regress/sql/stats.sql       | 123 +++++++++
 7 files changed, 892 insertions(+), 16 deletions(-)

index 698f274341..de0850337b 100644
 
+     <row>
@@ -658,20 +667,20 @@ postgres   27093  0.0  0.0  30096  2752 ?        Ss   11:34   0:00 postgres: ser
   </para>
 
   <para>
-   The <structname>pg_statio_</structname> views are primarily useful to
-   determine the effectiveness of the buffer cache.  When the number
-   of actual disk reads is much smaller than the number of buffer
-   hits, then the cache is satisfying most read requests without
-   invoking a kernel call. However, these statistics do not give the
-   entire story: due to the way in which <productname>PostgreSQL</productname>
-   handles disk I/O, data that is not in the
-   <productname>PostgreSQL</productname> buffer cache might still reside in the
-   kernel's I/O cache, and might therefore still be fetched without
-   requiring a physical read. Users interested in obtaining more
-   detailed information on <productname>PostgreSQL</productname> I/O behavior are
-   advised to use the <productname>PostgreSQL</productname> statistics views
-   in combination with operating system utilities that allow insight
-   into the kernel's handling of I/O.
+   The <structname>pg_statio_</structname> and
+   <structname>pg_stat_io</structname> views are primarily useful to determine
+   the effectiveness of the buffer cache.  When the number of actual disk reads
+   is much smaller than the number of buffer hits, then the cache is satisfying
+   most read requests without invoking a kernel call. However, these statistics
+   do not give the entire story: due to the way in which
+   <productname>PostgreSQL</productname> handles disk I/O, data that is not in
+   the <productname>PostgreSQL</productname> buffer cache might still reside in
+   the kernel's I/O cache, and might therefore still be fetched without
+   requiring a physical read. Users interested in obtaining more detailed
+   information on <productname>PostgreSQL</productname> I/O behavior are
+   advised to use the <productname>PostgreSQL</productname> statistics views in
+   combination with operating system utilities that allow insight into the
+   kernel's handling of I/O.
   </para>
 
  </sect2>
@@ -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,351 @@ 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>
+       The context or location of an IO operation.
+       <varname>io_context</varname> <literal>shared</literal> refers to IO
+       operations of data in shared buffers, the primary buffer pool for
+       relation data. <varname>io_context</varname> <literal>local</literal>
+       refers to IO operations on process-local memory used for temporary
+       tables. <varname>io_context</varname> <literal>vacuum</literal> refers
+       to the IO operations incurred while vacuuming and analyzing.
+       <varname>io_context</varname> <literal>bulkread</literal> refers to IO
+       operations specially designated as <literal>bulk reads</literal>, such
+       as the sequential scan of a large table. <varname>io_context</varname>
+       <literal>bulkwrite</literal> refers to IO operations specially
+       designated as <literal>bulk writes</literal>, such as
+       <command>COPY</command>.
+       </para>
+
+       <para>
+       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. This pattern is called a
+       <quote>Buffer Access Strategy</quote> in the
+       <productname>PostgreSQL</productname> source code and the fixed-size
+       ring buffer is referred to as a <quote>strategy ring buffer</quote> for
+       the purposes of this view's documentation.
+      </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>
+       <para>
+       <varname>read</varname> and <varname>extended</varname> for
+       <varname>backend_type</varname>s <literal>autovacuum launcher</literal>,
+       <literal>autovacuum worker</literal>, <literal>client backend</literal>,
+       <literal>standalone backend</literal>, <literal>background
+       worker</literal>, and <literal>walsender</literal> for all
+       <varname>io_context</varname>s is similar to the sum of
+       <varname>heap_blks_read</varname>, <varname>idx_blks_read</varname>,
+       <varname>tidx_blks_read</varname>, and
+       <varname>toast_blks_read</varname> in <link
+       linkend="monitoring-pg-statio-all-tables-view">
+       <structname>pg_statio_all_tables</structname></link>. and
+       <varname>blks_read</varname> from <link
+       linkend="monitoring-pg-stat-database-view">
+       <structname>pg_stat_database</structname></link>. The difference is that
+       reads done as part of <command>CREATE DATABASE</command> are not counted
+       in <structname>pg_statio_all_tables</structname> and
+       <structname>pg_stat_database</structname>.
+       </para>
+       <para>If using the <productname>PostgreSQL</productname> extension,
+       <xref linkend="pgstatstatements"/>,
+       <varname>read</varname> for
+       <varname>backend_type</varname>s <literal>autovacuum launcher</literal>,
+       <literal>autovacuum worker</literal>, <literal>client backend</literal>,
+       <literal>standalone backend</literal>, <literal>background
+       worker</literal>, and <literal>walsender</literal> for all
+       <varname>io_context</varname>s is equivalent to
+       <varname>shared_blks_read</varname> together with
+       <varname>local_blks_read</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>.
+       </para>
+
+       <para>
+       Normal client backends should be able to rely on maintenance processes
+       like the checkpointer and background writer to write out dirty data as
+       much as possible. Large numbers of writes by
+       <varname>backend_type</varname> <literal>client backend</literal> in
+       <varname>io_context</varname> <literal>shared</literal> could indicate a
+       misconfiguration of shared buffers or of checkpointer . More information
+       on checkpointer configuration can be found in <xref
+       linkend="wal-configuration"/>.
+       </para>
+
+       <para>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>
+
+       <para>If using the <productname>PostgreSQL</productname> extension,
+       <xref linkend="pgstatstatements"/>, <varname>written</varname> and
+       <varname>extended</varname> for <varname>backend_type</varname>s
+       <literal>autovacuum launcher</literal>, <literal>autovacuum
+       worker</literal>, <literal>client backend</literal>, <literal>standalone
+       backend</literal>, <literal>background worker</literal>, and
+       <literal>walsender</literal> for all <varname>io_context</varname>s is
+       equivalent to <varname>shared_blks_written</varname> together with
+       <varname>local_blks_written</varname>.
+      </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>bytes_conversion</structfield> <type>bigint</type>
+      </para>
+      <para>
+      The number of bytes per 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, derived from the build-time
+      parameter <symbol>BLCKSZ</symbol>, which is <literal>8192</literal> by
+      default. Future values could include those derived from
+      <symbol>XLOG_BLCKSZ</symbol>, once WAL IO is tracked in this view, and
+      constant multipliers 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>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>. Blocks are only evicted when there are no
+       unoccupied buffers.
+       </para>
+
+      <para>
+       <varname>evicted</varname> in <varname>io_context</varname>
+       <literal>shared</literal> counts the number of times a block from a
+       shared buffer was evicted so that it can be replaced with another block,
+       also in shared buffers.
+
+       A high <varname>evicted</varname> count in <varname>io_context</varname>
+       <literal>shared</literal> could indicate that shared buffers is too
+       small and should be set to a larger value.
+       </para>
+
+       <para>
+       <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 requiring 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.
+       </para>
+
+       <para>
+       <varname>evicted</varname> in <varname>io_context</varname>
+       <literal>local</literal> counts the number of times a block of data from
+       an existing local buffer was evicted in order to replace it with another
+       block, also in local buffers.
+      </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>, or <literal>vacuum</literal>
+       <varname>io_context</varname>s. When a <quote>Buffer Access
+       Strategy</quote> reuses a buffer in the strategy ring, it must evict its
+       contents, incrementing <varname>reused</varname>. When a <quote>Buffer
+       Access Strategy</quote> adds a new shared buffer to the strategy ring
+       and this shared buffer is occupied, the <quote>Buffer Access
+       Strategy</quote> must evict the contents of the shared buffer,
+       incrementing <varname>evicted</varname>.
+      </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>bytes_conversion</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>.
+       </para>
 
+       <para>
+       Normal client backends should be able to rely on the checkpointer to
+       ensure data is persisted to permanent storage. Large numbers of
+       <varname>files_synced</varname> by <varname>backend_type</varname>
+       <literal>client backend</literal> could indicate a misconfiguration of
+       shared buffers or of checkpointer. More information on checkpointer
+       configuration can be found in <xref linkend="wal-configuration"/>.
+       </para>
+
+       <para>
+       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..571c422f73 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1117,6 +1117,22 @@ CREATE VIEW pg_stat_bgwriter AS
         pg_stat_get_buf_alloc() AS buffers_alloc,
         pg_stat_get_bgwriter_stat_reset_time() AS stats_reset;
 
+       b.bytes_conversion,
index b783af130c..5bd39733b6 100644
 
@@ -1725,6 +1726,144 @@ pg_stat_get_buf_alloc(PG_FUNCTION_ARGS)
 	PG_RETURN_INT64(pgstat_fetch_stat_bgwriter()->buf_alloc);
 }
 
+/*
+	IO_COL_CONVERSION,
+			values[IO_COL_CONVERSION] = Int64GetDatum(BLCKSZ);
index 62a5b8e655..fdb9c2f4a1 100644
 
+{ 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,int8,int8,int8,int8,int8,int8,timestamptz}',
+  proargmodes => '{o,o,o,o,o,o,o,o,o,o,o,o}',
+  proargnames => '{backend_type,io_context,read,written,extended,bytes_conversion,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..2ca46656d0 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -1871,6 +1871,19 @@ 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.bytes_conversion,
+    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, bytes_conversion, 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..28ef9171de 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -1120,4 +1120,228 @@ SELECT pg_stat_get_subscription_stats(NULL);
  
 (1 row)
 
+-- - writes of shared buffers to permanent storage
+-- There is no test for blocks evicted from shared buffers, because we cannot
+-- be sure of the state of shared buffers at the point the test is run.
+-- extends.
+-- - eviction of local buffers in order to reuse them
+-- - writes of local buffers to permanent storage
+SELECT sum(evicted) AS io_sum_local_evictions_before FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT sum(evicted) AS io_sum_local_evictions_after FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT :io_sum_local_evictions_after > :io_sum_local_evictions_before;
+-- reads.
+SELECT sum(evicted) + sum(reused) + sum(extended) + sum(files_synced) + sum(read) + sum(written) AS io_stats_pre_reset FROM pg_stat_io \gset
+SELECT sum(evicted) + sum(reused) + sum(extended) + sum(files_synced) + sum(read) + sum(written) AS io_stats_post_reset FROM pg_stat_io \gset
index f6270f7bad..75c2f6c4c0 100644
@@ -535,4 +535,127 @@ SELECT pg_stat_get_replication_slot(NULL);

+-- - writes of shared buffers to permanent storage
+-- There is no test for blocks evicted from shared buffers, because we cannot
+-- be sure of the state of shared buffers at the point the test is run.
+-- extends.
+-- - eviction of local buffers in order to reuse them
+-- - writes of local buffers to permanent storage
+SELECT sum(evicted) AS io_sum_local_evictions_before FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT sum(evicted) AS io_sum_local_evictions_after FROM pg_stat_io WHERE io_context = 'local' \gset
+SELECT :io_sum_local_evictions_after > :io_sum_local_evictions_before;
+-- reads.
+SELECT sum(evicted) + sum(reused) + sum(extended) + sum(files_synced) + sum(read) + sum(written) AS io_stats_pre_reset FROM pg_stat_io \gset
+SELECT sum(evicted) + sum(reused) + sum(extended) + sum(files_synced) + sum(read) + sum(written) AS io_stats_post_reset FROM pg_stat_io \gset

From d8479c25f234d05c864e9fd659c9b2fe2ce5ade0 Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 6 Oct 2022 12:23:38 -0400
Subject: [PATCH v35 3/5] Aggregate IO operation stats per BackendType

 src/backend/utils/activity/pgstat_io_ops.c    | 164 ++++++++++++++++++
 src/include/pgstat.h                          |  88 ++++++++++
 13 files changed, 365 insertions(+), 6 deletions(-)

 

 
 
+	/*
 
+	/*

 /*
 
 /*

 /*
 
 /*
index 6f9c250907..9f0f27da1f 100644
@@ -20,6 +20,48 @@

 void
 pgstat_count_io_op(IOOp io_op, IOContext io_context)
@@ -60,6 +102,78 @@ pgstat_count_io_op(IOOp io_op, IOContext io_context)
 			break;
 	}
 
+	have_ioopstats = true;
+}
+
 
 const char *
@@ -108,6 +222,56 @@ pgstat_io_op_desc(IOOp io_op)
 	elog(ERROR, "unrecognized IOOp value: %d", io_op);
 }
 
+void
 
 /*

 /*

 /*
 
 /*
 
 /*
 
 	PG_RETURN_VOID();
 }

 extern const char *GetBackendTypeDesc(BackendType backendType);
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 5883aafe9c..010dc7267b 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -49,6 +49,7 @@ typedef enum PgStat_Kind
 	PGSTAT_KIND_ARCHIVER,
 	PGSTAT_KIND_BGWRITER,
 	PGSTAT_KIND_CHECKPOINTER,
+	PGSTAT_KIND_IOOPS,
 	PGSTAT_KIND_SLRU,
 	PGSTAT_KIND_WAL,
 } PgStat_Kind;
@@ -326,6 +327,12 @@ typedef struct PgStat_IOContextOps
 	PgStat_IOOpCounters data[IOCONTEXT_NUM_TYPES];
 } PgStat_IOContextOps;
 
@@ -508,6 +515,7 @@ extern PgStat_CheckpointerStats *pgstat_fetch_stat_checkpointer(void);
  */

@@ -519,6 +527,86 @@ extern bool pgstat_expect_io_op(BackendType bktype, IOContext io_context, IOOp i
 
+			counters->rejections == 0 && counters->repossessions == 0 &&
+			counters->reuses == 0 && counters->writes == 0);

 /*

 
 
 	PgStat_WalStats wal;

+/*

+/*
 
 /*
index b080367073..6d33b2c9bb 100644

---

From: Melanie Plageman <[email-masked]>
Date: Wed, 26 Oct 2022 13:54:44 -0400

okay, so I realized v35 had an issue where I wasn't counting strategy
evictions correctly. fixed in attached v36. This made me wonder if there
is actually a way to add a test for evictions (in strategy and shared
contexts) that is not flakey.

> but no strong opinions on an alternate name.

Also, re: repossessed, I can change it to reuse_failed but I do think it
is important to give users a way to distinguish between bulkread
rejections of dirty buffers and strategies failing to reuse buffers due
to concurrent pinning (since the reaction to these two scenarios would
likely be different).

If we added another column called something like "claim_failed" which
counts buffers which we failed to reuse because of concurrent pinning or
usage, we could recommend use of this column together with
"reuse_failed" to determine the cause of the failed reuses for a
bulkread. We could also use "claim_failed" in IOContext shared to
provide information on shared buffer contention.

- Melanie
From 0d5fc7da60f6b02259b8dd1d2eab25967cb9a95a Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 6 Oct 2022 12:23:38 -0400
Subject: [PATCH v36 3/5] Aggregate IO operation stats per BackendType

 

 

+	/*
 
+	/*

 /*
 
 /*

 /*
 
 /*

 void
 
+	have_ioopstats = true;
+}
+
 
 const char *
 
+void
 
 /*

 /*

 /*
 
 /*
 
 /*
 
 	PG_RETURN_VOID();
 }

 

 

 /*

 
 
 	PgStat_WalStats wal;

+/*

+/*
 
 /*

From ac9fa4fe501fd948cfc4c4af983555813bfb20de Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 6 Oct 2022 12:23:25 -0400
Subject: [PATCH v36 2/5] Track IO operation statistics locally

Each IOOp (evict, reject, repossess, reuse, read, write, extend, and
fsync) is counted per IOContext (bulkread, bulkwrite, local, shared, or
vacuum) through a call to pgstat_count_io_op().

 src/backend/storage/buffer/bufmgr.c        |  82 ++++++-
 12 files changed, 479 insertions(+), 13 deletions(-)

index 4e7b0b31bb..1cc108004f 100644

 
+	if (isLocalBuf)
 
 	if (isExtend)
 	{

@@ -1121,6 +1131,8 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 			BufferAccessStrategy strategy,
 			bool *foundPtr)
 {
+	bool		from_ring;
+	IOContext	io_context;
 	BufferTag	newTag;			/* identity of requested block */
 	uint32		newHash;		/* hash value for newTag */
 	LWLock	   *newPartitionLock;	/* buffer partition lock for it */
@@ -1187,9 +1199,12 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 	 */
 	LWLockRelease(newPartitionLock);

 
@@ -1263,13 +1278,34 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
 					}
 				}
 
+				/*

 				ScheduleBufferTagForWriteback(&BackendWritebackContext,
@@ -1441,6 +1477,30 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,

+		IOOp io_op = from_ring ? IOOP_REUSE : IOOP_EVICT;
+		pgstat_count_io_op(io_op, io_context);
@@ -2570,7 +2630,7 @@ SyncOneBuffer(int buf_id, bool skip_recently_used, WritebackContext *wb_context)

 
@@ -2820,7 +2880,7 @@ BufferGetTag(Buffer buffer, RelFileLocator *rlocator, ForkNumber *forknum,
  * as the second parameter.  If not, pass NULL.
  */
 static void
-FlushBuffer(BufferDesc *buf, SMgrRelation reln)
+FlushBuffer(BufferDesc *buf, SMgrRelation reln, IOContext io_context)
 {
 	XLogRecPtr	recptr;
 	ErrorContextCallback errcallback;
@@ -2900,6 +2960,8 @@ FlushBuffer(BufferDesc *buf, SMgrRelation reln)
 	 */
 	bufToWrite = PageSetChecksumCopy((Page) bufBlock, buf->tag.blockNum);

@@ -3551,6 +3613,8 @@ FlushRelationBuffers(Relation rel)
 						  localpage,
 						  false);

@@ -3586,7 +3650,7 @@ FlushRelationBuffers(Relation rel)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, RelationGetSmgr(rel));
+			FlushBuffer(bufHdr, RelationGetSmgr(rel), IOCONTEXT_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr);
 		}
@@ -3684,7 +3748,7 @@ FlushRelationsAllBuffers(SMgrRelation *smgrs, int nrels)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, srelent->srel);
+			FlushBuffer(bufHdr, srelent->srel, IOCONTEXT_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr);
 		}
@@ -3894,7 +3958,7 @@ FlushDatabaseBuffers(Oid dbid)
 		{
 			PinBuffer_Locked(bufHdr);
 			LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
-			FlushBuffer(bufHdr, NULL);
+			FlushBuffer(bufHdr, NULL, IOCONTEXT_SHARED);
 			LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
 			UnpinBuffer(bufHdr);
 		}
@@ -3921,7 +3985,7 @@ FlushOneBuffer(Buffer buffer)

 
 /*
 
+#include "pgstat.h"
 
+	*from_ring = false;
+
 	/*
 
 	/*
 
 /*

+/*

 
 
 	*foundPtr = false;
+
 	return bufHdr;
 }

 
+/*

+/*
 
 /* freelist.c */
 
 typedef void *Block;

From 2bb6195640ec5f04dad43e276b4f2801bd5b76ab Mon Sep 17 00:00:00 2001
From: Melanie Plageman <[email-masked]>
Date: Thu, 6 Oct 2022 12:24:42 -0400
Subject: [PATCH v36 4/5] Add system view tracking IO ops per backend type

Add pg_stat_io, a system view which tracks the number of IOOps (
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).

 
+     <row>
 
   <para>
 
  </sect2>
 
+       <para>

 
 
+/*
index 20f5aa56ea..aae96db37a 100644
 
index 624d0e5aae..c46babade3 100644
@@ -1873,6 +1873,19 @@ pg_stat_gssapi| SELECT s.pid,
  
 (1 row)

From 4746ef5834de99836f81be8ffd322d139c940a25 Mon Sep 17 00:00:00 2001
From: Andres Freund <[email-masked]>
Date: Thu, 13 Oct 2022 11:03:05 -0700
Subject: [PATCH v36 1/5] Remove BufferAccessStrategyData->current_was_in_ring

 
 
-	/*
 
 	/*

---

From: Andres Freund <[email-masked]>
Date: Wed, 26 Oct 2022 11:58:08 -0700

Hi,

> in other IOContexts (thus making the name eviction_failed better than

I've a somewhat radical proposal: Let's just not count any of this in the
initial version. I think we want something, but clearly it's one of the harder
aspects of this patch. Let's get the rest in, and then work on this is in
isolation.

> memory?

Fair point. However, I think 'tmp' or 'temp' would be worse, because there's
other sources of temporary files that would be worth counting, consider
e.g. tuplestore temporary files. 'temptable' isn't good because it's not just
tables. 'temprel'? On balance I think local is better, but not sure.

> Also, what if we want to track other IO done using data from shared memory
> that is not in shared buffers? Would IOCONTEXT_SB and IOCONTEXT_TEMP be
> better? Should IOContext literally describe the context of the IO being done
> and there be a separate column which indicates the source of the data for
> the IO?  Like wal_buffer, local_buffer, shared_buffer? Then if it is not

Hm. I don't think we'd need _buffer for WAL or such, because there's nothing
else.

> clear without the addition of special columns for strategies.

-1, I think this just blows up the complexity further, without providing much
benefit. But:

Perhaps a somewhat similar idea could be used to address the concerns in the
preceding paragraphs. How about the following set of columns:

backend_type:
object: relation, temp_relation[, WAL, tempfiles, ...]
iocontext: buffer_pool, bulkread, bulkwrite, vacuum[, bypass]
read:
written:
extended:
bytes_conversion:
evicted:
reused:
files_synced:
stats_reset:

Greetings,

Andres Freund

---

From: Maciek Sakrejda <[email-masked]>
Date: Sun, 30 Oct 2022 18:08:55 -0700

On Wed, Oct 26, 2022 at 10:55 AM Melanie Plageman
<[email-masked]> wrote:

Totally nitpicking, but this reads a little funny to me. Previously
the trailing underscore suggested this is a group, and now with
pg_stat_io itself added (stupid question: should this be
"pg_statio"?), it sounds like we're talking about two views:
pg_stat_io and "pg_statio_". Maybe something like "The pg_stat_io view
and the pg_statio_ set of views are primarily..."?

Again nitpicking, but should this be "may be added"? I think "could"
suggests the possibility of implementation, whereas "may" feels more
like a hint as to how the feature could evolve.

Nice, I think this explanation is very helpful. You also use the term
"strategy context" and "strategy operation" below. I think it's fairly
obvious what those mean, but pointing it out in case we want to note
that here, too.

Maybe "plus" instead of "and" here for clarity (I'm assuming that's
what the "and" means)?

I'm reviewing the rendered docs now, and I noticed sentences like this
are a bit hard to scan: they force the reader to parse a big list of
backend types before even getting to the meat of what this is talking
about. Should we maybe reword this so that the backend list comes at
the end of the sentence? Or maybe even use a list (e.g., like in the
"state" column description in pg_stat_activity)?

I think that's a stray period before the "and."

Same comment as above re: the lengthy list.

Nice--it's great to see this mentioned. But I think these are
generally referred to as "auxiliary" not "maintenance" processes, no?

Again, should this be "plus" instead of "and"?

I think this general approach works (instead of unit). I'm not wild
about the name, but I don't really have a better suggestion. Maybe
"op_bytes" (since each cell is counting the number of I/O operations)?
But I think bytes_conversion is okay.

Also, is this (in the middle of the table) the right place for this
column? I would have expected to see it before or after all the actual
I/O op cells.

I think the parallel phrasing here makes this a little hard to follow.
Specifically, I think "must evict its contents" for the strategy case
sounds like a bad thing, but in fact this is a totally normal thing
that happens as part of strategy access, no? The idea is you probably
won't need that buffer again, so it's fine to evict it. I'm not sure
how to reword, but I think the current phrasing is misleading.

Maybe "...found ... to be dirty..."?

Are there docs to cross-reference here, especially for pruning? I
couldn't find much except a few un-explained mentions in the page
layout docs [2], and most of the search results refer to partition
pruning. Searching for hint bits at least gives some info in blog
posts and the wiki.

This (and in general the repossession description) makes sense, but
I'm not sure what to do with the information. Maybe Andres is right
that we could skip this in the first version?

> of the shared buffer, incrementing <varname>evicted</varname>.

It looks like you ended up with different wording in the patch, but
both this explanation and what's in the patch now make sense to me.
Thanks for clarifying.

Also, I noticed that the commit message explains missing rows for some
backend_type / io_context combinations and NULL (versus 0) in some
cells, but the docs don't really talk about that. Do you think that
should be in there as well?

Thanks,
Maciek

[1]: https://www.postgresql.org/docs/15/glossary.html#GLOSSARY-AUXILIARY-PROC
[2]: https://www.postgresql.org/docs/15/storage-page-layout.html

---

From: Melanie Plageman <[email-masked]>
Date: Thu, 3 Nov 2022 13:00:24 -0400

v37 attached

> and the pg_statio_ set of views are primarily..."?

I decided not to call it pg_statio because all of the other stats views
have an underscore after stat and I thought it was an opportunity to be
consistent with them.

> like a hint as to how the feature could evolve.

I've adopted the wording you suggested.

> obvious what those mean, but pointing it out in case we want to note

Thanks! I've added definitions of those as well.

> Maybe "plus" instead of "and" here for clarity (I'm assuming that's

Modified this -- in some cases by adding the lists mentioned below

> "state" column description in pg_stat_activity)?

Good idea with the bullet points.
For the lengthy lists, I've added bullet point lists to the docs for
several of the columns. It is quite long now but, hopefully, clearer?
Let me know if you think it improves the readability.

> I think that's a stray period before the "and."

Fixed!

Thanks! Fixed.

> But I think bytes_conversion is okay.

I really like op_bytes and have changed it to this. Thanks for the
suggestion!

> column? I would have expected to see it before or after all the actual

I put it after read, write, and extend columns because it applies to
them. It doesn't apply to files_synced. For reused and evicted, I didn't
think bytes reused and evicted made sense. Also, when we add non-block
oriented IO, reused and evicted won't be used but op_bytes will be. So I
thought it made more sense to place it after the operations it applies
to.

> how to reword, but I think the current phrasing is misleading.

I had trouble rephrasing this. I changed a few words. I see what you
mean. It is worth noting that reusing strategy buffers when there are
buffers on the freelist may not be the best behavior, so I wouldn't
necessarily consider "reused" a good thing. However, I'm not sure how
much the user could really do about this. I would at least like this
phrasing to be clear (evicted is for shared buffers, reused is for
strategy buffers), so, perhaps this section requires more work.

> Maybe "...found ... to be dirty..."?

Changed to this wording.

> pruning. Searching for hint bits at least gives some info in blog

yes, I don't see anything explaining this either -- below the page
layout it discusses tuple layout but that doesn't mention hint bits.

> that we could skip this in the first version?

I've removed repossessed and rejected in attached v37. I am a bit sad
about this because I don't see a good way forward and I think those
could be useful for users.

I have added the new column Andres recommended in [1] ("io_object") to
clarify temp and local buffers and pave the way for bypass IO (IO not
done through a buffer pool), which can be done on temp or permanent
files for temp or permanent relations, and spill file IO which is done
on temporary files but isn't related to temporary tables.

IOObject has increased the memory footprint and complexity of the code
around tracking and accumulating the statistics, though it has not
increased the number of rows in the view.

One question I still have about this additional dimension is how much
enumeration we need of the various combinations of IO operations, IO
objects, IO ops, and backend types which are allowed and not allowed.
Currently because it is only valid to operate on both IOOBJECT_RELATION
and IOOBJECT_TEMP_RELATION in IOCONTEXT_BUFFER_POOL, the changes to the
various functions asserting and validating what is "allowed" in terms of
combinations of ops, objects, contexts, and backend types aren't much
different than they were without IO Object. However, once we begin
adding other objects and contexts, we will need to make this logic more
comprehensive. I'm not sure whether or not I should do that
preemptively.

> both this explanation and what's in the patch now make sense to me.

Yes, I tried to rework it and your suggestion and feedback was very
helpful.

> should be in there as well?

Thanks for pointing this out. I have added notes about this to the
relevant columns in the docs.

- Melanie

[1] https://www.postgresql.org/message-id/20221026185808.4qnxowtn35x43u7u%40awork3.anarazel.de

---

From: Maciek Sakrejda <[email-masked]>
Date: Mon, 7 Nov 2022 10:26:06 -0800

> have an underscore after stat and I thought it was an opportunity to be

Oh, got it. Makes sense.

> Let me know if you think it improves the readability.

Hmm, I should have tried this before suggesting it. I think the lists
break up the flow of the column description too much. What do you
think about the attached (on top of your patches--attaching it as a
.diff to hopefully not confuse cfbot)? I kept the lists for backend
types but inlined the others as a middle ground. I also added a few
omitted periods and reworded "read plus extended" to avoid starting
the sentence with a (lowercase) varname (I think in general it's fine
to do that, but the more complicated sentence structure here makes it
easier to follow if the sentence starts with a capital).

Alternately, what do you think about pulling equivalencies to existing
views out of the main column descriptions, and adding them after the
main table as a sort of footnote? Most view docs don't have anything
like that, but pg_stat_replication does and it might be a good pattern
to follow.

Thoughts?

> thought it made more sense to place it after the operations it applies

Got it, makes sense.

> strategy buffers), so, perhaps this section requires more work.

Oh, I see. I think the updated wording works better. Although I think
we can drop the quotes around "Buffer Access Strategy" here. They're
useful when defining the term originally, but after that I think it's
clearer to use the term unquoted.

Just to understand this better myself, though: can you clarify when
"reused" is not a normal, expected part of the strategy execution? I
was under the impression that a ring buffer is used because each page
is needed only "once" (i.e., for one set of operations) for the
command using the strategy ring buffer. Naively, in that situation, it
seems better to reuse a no-longer-needed buffer than to claim another
buffer from the freelist (where other commands may eventually make
better use of it).

> could be useful for users.

I can see that, but I think as long as we're not doing anything to
preclude adding this in the future, it's better to get something out
there and expand it later. For what it's worth, I don't feel it needs
to be excluded, just that it's not worth getting hung up on.

> comprehensive. I'm not sure whether or not I should do that

It's definitely something to consider, but I have no useful input here.

Some more notes on the docs patch:

+ <row>
+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>io_context</structfield> <type>text</type>
+ </para>
+ <para>
+ The context or location of an IO operation.
+ </para>
+ <itemizedlist>
+ <listitem>
+ <para>
+ <varname>io_context</varname> <literal>buffer pool</literal> refers to
+ IO operations on data in both the shared buffer pool and process-local
+ buffer pools used for temporary relation data.
+ </para>
+ <para>
+ Operations on temporary relations are tracked in
+ <varname>io_context</varname> <literal>buffer pool</literal> and
+ <varname>io_object</varname> <literal>temp relation</literal>.
+ </para>
+ <para>
+ Operations on permanent relations are tracked in
+ <varname>io_context</varname> <literal>buffer pool</literal> and
+ <varname>io_object</varname> <literal>relation</literal>.
+ </para>
+ </listitem>

For this column, you repeat "io_context" in the list describing the
possible values of the column. Enum-style columns in other tables
don't do that (e.g., the pg_stat_activty "state" column). I think it
might read better to omit "io_context" from the list.

+ <entry role="catalog_table_entry"><para role="column_definition">
+ <structfield>io_object</structfield> <type>text</type>
+ </para>
+ <para>
+ Object operated on in a given <varname>io_context</varname> by a given
+ <varname>backend_type</varname>.
+ </para>

Is this a fixed set of objects we should list, like for io_context?

Thanks,
Maciek

---

From: Andres Freund <[email-masked]>
Date: Sun, 20 Nov 2022 16:38:15 -0800

Hi,

One good follow up patch will be to rip out the accounting for
pg_stat_bgwriter's buffers_backend, buffers_backend_fsync and perhaps
buffers_alloc and replace it with a subselect getting the equivalent data from
pg_stat_io.  It might not be quite worth doing for buffers_alloc because of
the way that's tied into bgwriter pacing.

On 2022-11-03 13:00:24 -0400, Melanie Plageman wrote:
> > + again. A high number of repossessions is a sign of contention for the +
> > blocks operated on by the strategy operation.
> >

Let's get the basic patch in and then check whether we can find a way to have
something providing at least some more information like repossessed and
rejected. I think it'll be easier to analyze in isolation.

> increased the number of rows in the view.

It doesn't look too bad from here. Is there a specific portion of the code
where it concerns you the most?

> comprehensive. I'm not sure whether or not I should do that

I'd not do it preemptively.

> @@ -833,6 +836,22 @@ ReadBuffer_common(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
>
> +		 * Though a strategy object may be passed in, no strategy is employed
> +		 * when using local buffers. This could happen when doing, for example,
> +		 * CREATE TEMPORRARY TABLE AS ...
> +		 */
> +		io_context = IOCONTEXT_BUFFER_POOL;
> +		io_object = IOOBJECT_TEMP_RELATION;
> +	}
> +	else
> +	{
> +		io_object = IOOBJECT_RELATION;
> +	}

I think given how frequently ReadBuffer_common() is called in some workloads,
it'd be good to make IOContextForStrategy inlinable. But I guess that's not
easily doable, because struct BufferAccessStrategyData is only defined in
freelist.c.

Could we defer this until later, given that we don't currently need this in
case of buffer hits afaict?

> @@ -1121,6 +1144,8 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
> @@ -1187,9 +1212,12 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
>  	 */

Hm - doesn't this mean we do IOContextForStrategy() twice? Once in
ReadBuffer_common() and then again here?

> @@ -1200,7 +1228,7 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,

I think patch 0001 relies on this change already having been made, If I am not misunderstanding?

> @@ -1263,13 +1291,34 @@ BufferAlloc(SMgrRelation smgr, char relpersistence, ForkNumber forkNum,
>  					}
>  				}
>
> +				/*
> +				 * IOCONTEXT_BUFFER_POOL IOOP_WRITE.
> +				 *

Missing word?

> +				 * IOCONTEXT_BUFFER_POOL IOOP_WRITE (from_ring will be false).

I think this makes sense for now, but it'd be good if somebody else could
chime in on this...

> +				 * "regular" write of a dirty shared buffer (IOCONTEXT_BUFFER_POOL
> +				 * IOOP_WRITE).
> +				 */
> +
>  				/* OK, do the I/O */
> +				FlushBuffer(buf, NULL, io_context, IOOBJECT_RELATION);

> +		* corresponding strategy's context.

Perhaps "adding a shared buffer to the ring are counted in the corresponding
context"? "strategy's context" sounds off to me.

> This includes the evictions

I think this sentence could use a few commas, but not sure.

s/current/the current/?

> +		* as reused when it was rejected or when we errored out.

I can't quite parse this sentence.

> +		pgstat_count_io_op(io_op, IOOBJECT_RELATION, io_context);
> +	}

I'd just inline the variable, but ...

>  											BufferDescriptorGetBuffer(bufHdr));

Spurious change.

>  	pg_atomic_unlocked_write_u32(&bufHdr->state, buf_state);

Dito.

> +* subsystem or do not do IO operations worth reporting statistics on:

s/worth reporting/we currently report/?

> +	 * executing queries will use local buffers and operate on temporary
> +	 * relations. Parallel workers will not use local buffers (see
> +	 * InitLocalBuffers()); however, extensions leveraging background workers
> +	 * have no such limitation, so track IO Operations on
> +	 * IOOBJECT_TEMP_RELATION for BackendType B_BG_WORKER.
> +	 */
> +	no_temp_rel = bktype == B_AUTOVAC_LAUNCHER || bktype == B_BG_WRITER || bktype
> +	if (no_temp_rel && io_context == IOCONTEXT_BUFFER_POOL && io_object ==
> +			IOOBJECT_TEMP_RELATION)
> +		return false;

Personally I don't like line breaks on the == and would rather break earlier
on the && or ||.

> +		PgStatShared_IOObjectOps *shared_objs = &type_shstats->data[io_context];
> +		PgStat_IOObjectOps *pending_objs = &pending_IOOpStats.data[io_context];
> +
> +		for (int io_object = 0; io_object < IOOBJECT_NUM_TYPES; io_object++)
> +		{

Is there any compiler that'd complain if you used IOContext/IOObject/IOOp as the
type in the for loop? I don't think so? Then you'd not need the casts in other
places, which I think would make the code easier to read.

> +			PgStat_IOOpCounters *sharedent = &shared_objs->data[io_object];
> +			PgStat_IOOpCounters *pendingent = &pending_objs->data[io_object];
> +
> +				!pgstat_bktype_io_context_io_object_valid(MyBackendType,
> +					(IOContext) io_context, (IOObject) io_object))
> +			{
> +								(IOObject) io_object, (IOOp) io_op)))

Superfluous parens after the !, I think?

> +	 * Operation stats, however this will not be called after an entire

Missing "until"?

> +pgstat_io_op_assert_zero(PgStat_IOOpCounters *counters, IOOp io_op)

Does this need to be in pgstat.h? Perhaps pgstat_internal.h would suffice,
afaict it's not used outside of pgstat code?

> +
> +/*
> + * Assert that stats have not been counted for any combination of IOContext,
> + * IOObject, and IOOp which is not valid for the passed-in BackendType. The
> + * passed-in array of PgStat_IOOpCounters must contain stats from the
> + * BackendType specified by the second parameter. Caller is responsible for
> + * locking of the passed-in PgStatShared_IOContextOps, if needed.
> + */
> +static inline void
> +pgstat_backend_io_stats_assert_well_formed(PgStatShared_IOContextOps *backend_io_context_ops,
> +		BackendType bktype)
> +{

This doesn't look like it should be an inline function - it's quite long.

I think it's also too complicated for the compiler to optimize out if
assertions are disabled. So you'd need to handle this with an explicit #ifdef
USE_ASSERT_CHECKING.

> +        buffer pools used for temporary relation data.

The indentation in the sgml part of the patch seems to be a bit wonky.

> +       writes use a fixed amount of memory, acquiring the equivalent number of

s/memory/buffers/? The amount of memory isn't really fixed.

> +       <varname>read</varname> plus <varname>extended</varname> for
> +       <varname>backend_type</varname>s
> +
> +       <itemizedlist>
> +
> +       <listitem>
> +        <para>
> +       <literal>autovacuum launcher</literal>
> +        </para>
> +       </listitem>

Hm. ISTM that we should not document the set of valid backend types as part of
this view. Couldn't we share it with pg_stat_activity.backend_type?

> +       The difference is that reads done as part of <command>CREATE
> +       DATABASE</command> are not counted in
> +       <structname>pg_statio_all_tables</structname> and
> +       <structname>pg_stat_database</structname>
> +       </para>

Hm, this seems a bit far into the weeds?

> +			PgStat_IOObjectOps *io_objs = &io_context_ops->data[io_context];
> +
> +			for (int io_object = 0; io_object < IOOBJECT_NUM_TYPES; io_object++)
> +			{
> +				PgStat_IOOpCounters *counters = &io_objs->data[io_object];
> +				const char *io_obj_str = pgstat_io_object_desc(io_object);
> +
> +				* Some combinations of IOContext, IOObject, and BackendType are
> +				* not valid for any type of IOOp. In such cases, omit the
> +				* entire row from the view.
> +				*/
> +					!pgstat_bktype_io_context_io_object_valid((BackendType) bktype,
> +						(IOContext) io_context, (IOObject) io_object))
> +				{

Perhaps mention in a comment two loops up that we don't skip the nested loops
despite !expect_backend_stats because we want to assert here?

Greetings,

Andres Freund

---

From: Justin Pryzby <[email-masked]>
Date: Tue, 22 Nov 2022 23:43:29 -0600

Note that 001 fails to compile without 002:

../src/backend/storage/buffer/bufmgr.c:1257:43: error: ‘from_ring’ undeclared (first use in this function)
 1257 |       StrategyRejectBuffer(strategy, buf, from_ring))

My "warnings" script informed me about these gripes from MSVC:

[03:42:30.607] c:\cirrus>call sh -c 'if grep ": warning " build.txt; then exit 1; fi; exit 0' 
[03:42:30.749] c:\cirrus\src\backend\storage\buffer\freelist.c(699) : warning C4715: 'IOContextForStrategy': not all control paths return a value
[03:42:30.749] c:\cirrus\src\backend\utils\activity\pgstat_io_ops.c(190) : warning C4715: 'pgstat_io_context_desc': not all control paths return a value
[03:42:30.749] c:\cirrus\src\backend\utils\activity\pgstat_io_ops.c(204) : warning C4715: 'pgstat_io_object_desc': not all control paths return a value
[03:42:30.749] c:\cirrus\src\backend\utils\activity\pgstat_io_ops.c(226) : warning C4715: 'pgstat_io_op_desc': not all control paths return a value
[03:42:30.749] c:\cirrus\src\backend\utils\adt\pgstatfuncs.c(1816) : warning C4715: 'pgstat_io_op_get_index': not all control paths return a value

In the docs table, you say things like:
| io_context vacuum refers to the IO operations incurred while vacuuming and analyzing.

..but it's a bit unclear (maybe due to the way the docs are rendered).
I think it may be more clear to say "when <io_context> is
<vacuum>, ..."

| acquiring the equivalent number of shared buffers

I don't think "equivelent" fits here, since it's actually acquiring a
different number of buffers.

There's a missing period before " The difference is"

The sentence beginning "read plus extended for backend_types" is difficult to
parse due to having a bulleted list in its middle.

There aren't many references to "IOOps", which is good, because I
started to read it as "I oops".

=> I think that's intended to say *until* after ?

=> There's a missing newline above this comment.

+       Assert(counters->evictions == 0 && counters->extends == 0 &&
+                       counters->fsyncs == 0 && counters->reads == 0 && counters->reuses
+                       == 0 && counters->writes == 0);

=> It'd be more readable and also maybe help debugging if these were separate
assertions.  I wondered in the past if that should be a general policy
for all assertions.

Similar: I'd prefer to see this as 5 "ifs" or a "switch" to return
false, else return true.  But YMMV.

=> typo: temporary

+       if (strategy_io_context  && io_op == IOOP_FSYNC)

=> Extra space.

pgstat_count_io_op() has a superflous newline before "}".

I think there may be a problem/deficiency with hint bits:

|postgres=# DROP TABLE u2; CREATE TABLE u2 AS SELECT generate_series(1,999999)a; SELECT pg_stat_reset_shared('io'); explain (analyze,buffers) SELECT * FROM u2;
|...
| Seq Scan on u2  (cost=0.00..15708.75 rows=1128375 width=4) (actual time=0.111..458.239 rows=999999 loops=1)
|   Buffers: shared hit=2048 read=2377 dirtied=2377 written=2345

|postgres=# SELECT COUNT(1), relname, COUNT(1) FILTER(WHERE isdirty) FROM pg_buffercache b LEFT JOIN pg_class c ON pg_relation_filenode(c.oid)=b.relfilenode GROUP BY 2 ORDER BY 1 DESC LIMIT 11;
| count |             relname             | count
|-------+---------------------------------+-------
| 13619 |                                 |     0
|  2080 | u2                              |  2080
|   104 | pg_attribute                    |     4
|    71 | pg_statistic                    |     1
|    51 | pg_class                        |     1

It says that SELECT caused 2377 buffers to be dirtied, of which 2080 are
associated with the new table in pg_buffercache.

|postgres=# SELECT * FROM pg_stat_io WHERE backend_type!~'autovac|archiver|logger|standalone|startup|^wal|background worker' or true ORDER BY 2;
|    backend_type     | io_context  |   io_object   | read | written | extended | op_bytes | evicted | reused | files_synced |          stats_reset
|...
| client backend      | bulkread    | relation      | 2377 |    2345 |          |     8192 |       0 |   2345 |              | 2022-11-22 22:32:33.044552-06

I think it's a known behavior that hint bits do not use the strategy
ring buffer.  For BAS_BULKREAD, ring_size = 256kB (32, 8kB pages), but
there's 2080 dirty pages in the buffercache (~16MB).

But the IO view says that 2345 of the pages were "reused", which seems
misleading to me.  Maybe that just follows from the behavior and the view is
fine.  If the view is fine, maybe this case should still be specifically
mentioned in the docs.

-- 
Justin

---

From: Andres Freund <[email-masked]>
Date: Fri, 25 Nov 2022 14:46:11 -0800

Hi,

> | count |             relname             | count

Note that there's 2048 dirty buffers for u2 in shared_buffers before the
SELECT, despite the relation being 4425 blocks long, due to the CTAS using
BAS_BULKWRITE.

> there's 2080 dirty pages in the buffercache (~16MB).

I don't think there's any "circumvention" of the ringbuffer here. There's 2048
buffers for u2 in s_b before, all dirty, there's 2080 after, also all
dirty. So the ringbuffer restricted the increase in shared buffers used for u2
to 2080-2048=32 additional buffers.

The reason hint bits don't prevent pages from being written out here is that a
BAS_BULKREAD strategy doesn't cause all buffer writes to be rejected, it just
causes buffer writes to be rejected when the page LSN would require a WAL
flush. And that's not typically the case when you just set a hint bit, unless
you use wal_log_hint_bits = true.

If I turn on wal_log_hints=true and add a CHECKPOINT after the CTAS I see 0
reuses (and 4425 dirty buffers), which is what I'd expect.

> fine.  If the view is fine, maybe this case should still be specifically

I think that's just confusing due to the reset. 2048 + 2345 = 4393, but we
only have 2080 buffers for u2 in s_b.

Greetings,

Andres Freund

---

From: Melanie Plageman <[email-masked]>
Date: Mon, 28 Nov 2022 21:05:33 -0500

v38 attached.

> the way that's tied into bgwriter pacing.

I don't see how it will make sense to have buffers_backend and
buffers_backend_fsync respond to a different reset target than the rest
of the fields in pg_stat_bgwriter.

> easily doable, because struct BufferAccessStrategyData is only defined in

Correct

> case of buffer hits afaict?

Yes, you are right. In ReadBuffer_common(), we can easily move the
IOContextForStrategy() call to directly before using io_context. I've
done that in the attached version.

> ReadBuffer_common() and then again here?

Yes. So, there are a few options for addressing this.

- if the goal is to call IOStrategyForContext() exactly once in a
  given codepath, BufferAlloc() can set IOContext
  (passed by reference as an output parameter). I don't like this much
  because it doesn't make sense to me that BufferAlloc() would set the
  "io_context" parameter -- especially given that strategy is already
  passed as a parameter and is obviously available to the caller.
  I also don't see a good way of waiting until BufferAlloc() returns to count
  the IO operations counted in FlushBuffer() and BufferAlloc() itself.

- if the goal is to avoid calling IOStrategyForContext() in more common
  codepaths or to call it as close to its use as possible, then we can
  push down its call in BufferAlloc() to the two locations where it is
  used -- when a dirty buffer must be flushed and when a block was
  evicted or reused. This will avoid calling it when we are not evicting
  a block from a valid buffer.

  However, if we do that, I don't know how to avoid calling it twice in
  that codepath. Even though we can assume io_context was set in the
  first location by the time we get to the second location, we would
  need to initialize the variable with something if we only plan to set
  it in some branches and there is no "invalid" or "default" value of
  the IOContext enum.

  Given the above, I've left the call in BufferAlloc() as is in the
  attached version.

Fixed.

Fixed.

Fixed.

> I think this sentence could use a few commas, but not sure.

Reworded.

> I can't quite parse this sentence.

I've reworded the whole comment.
I think it is clearer now.

Done.

Removed.

Removed.

> s/worth reporting/we currently report/?

Updated

> Personally I don't like line breaks on the == and would rather break earlier

I've gone through and fixed all of these that I could find.

> places, which I think would make the code easier to read.

I changed the type and currently get no compiler warnings, however, on
a previous CI run,
with the type changed to an enum I got the following warning:

/tmp/cirrus-ci-build/src/include/utils/pgstat_internal.h:605:48:
error: no ‘operator++(int)’ declared for postfix ‘++’ [-fpermissive]
 605 |    io_context < IOCONTEXT_NUM_TYPES; io_context++)

I'm not sure why I am no longer getting it.

> Superfluous parens after the !, I think?

Thanks! I've looked for other occurrences as well and fixed them.

Fixed.

> afaict it's not used outside of pgstat code?

It is used in pgstatfuncs.c during the view creation.

> assertions are disabled. So you'd need to handle this with an explicit #ifdef

I've made it a static helper function in pgstat.c.

> The indentation in the sgml part of the patch seems to be a bit wonky.

I'll address this and the other docs feedback in a separate patchset and email.

Done.

I've also removed the test for bulkread reads from regress because
CREATE DATABASE is expensive and added it to the verify_heapam test
since it is one of the only users of a BULKREAD strategy which
unconditionally uses a BULKREAD strategy.

Thanks,
Melanie

---

From: Melanie Plageman <[email-masked]>
Date: Mon, 28 Nov 2022 21:08:36 -0500

>  1257 |       StrategyRejectBuffer(strategy, buf, from_ring))

Thanks!
I fixed this in version 38 attached in response to Andres upthread [1].

> [03:42:30.749] c:\cirrus\src\backend\utils\adt\pgstatfuncs.c(1816) : warning C4715: 'pgstat_io_op_get_index': not all control paths return a value

Thanks, I forgot to look at those warnings in CI.
I added pg_unreachable() and think it silenced the warnings.

> I think it may be more clear to say "when <io_context> is

So, because I use this language [column name] [column value] so often in
the docs, I would prefer a pattern that is as concise as possible. I
agree it may be hard to see due to the rendering. Currently, I am using
<varname> tags for the column name and <literal> tags for the column
value. Is there another tag type I could use to perhaps make this more
clear without adding additional words?

This is what the code looks like for the above docs text:
<varname>io_context</varname> <literal>vacuum</literal> refers to the IO

> different number of buffers.

I'm planning to do docs changes in a separate patchset after addressing
code feedback. I plan to change "equivalent" to "corresponding" here.

> parse due to having a bulleted list in its middle.

Will address in future version.

> started to read it as "I oops".

Grep'ing for this in the code, I only use the word IOOp(s) in the code
when I very clearly want to use the type name -- and never in the docs.
But, yes, it does look like "I oops" :)

> => I think that's intended to say *until* after ?

Fixed in v38.

> => There's a missing newline above this comment.

Fixed in v38.

> assertions.

I have made this change.

> false, else return true.  But YMMV.

I don't know that separating it into multiple if statements or a switch
would make it more clear to me or help me with debugging here.

Separately, since this is used in non-assert builds, I would like to
ensure it is efficient. Do you know if a switch or if statements will
be compiled to the exact same thing as this at useful optimization
levels?

> +                * CREATE TEMPORRARY TABLE AS ...

Fixed in v38.

Fixed.

> pgstat_count_io_op() has a superflous newline before "}".

I couldn't find the one you are referencing.
Do you mind pasting in the code?

Thanks,
Melanie

[1] https://www.postgresql.org/message-id/CAAKRu_Zvaj_yFA_eiSRrLZsjhT0J8cJ044QhZfKuXq6WN5bu5g%40mail.gmail.com

---

From: Melanie Plageman <[email-masked]>
Date: Tue, 29 Nov 2022 20:12:47 -0500

Thanks for the review, Maciek!

I've attached a new version 39 of the patch which addresses your docs
feedback from this email as well as docs feedback from Andres in [1] and
Justin in [2].

I've made some additional code changes addressing a few of their other
points as well, and I've moved the verify_heapam test to a plain sql
test in contrib/amcheck instead of putting it in the perl test.

This patchset also includes various cleanup, pgindenting, and addressing
the sgml indentation issue brought up in the thread.

> like that, but pg_stat_replication does and it might be a good pattern

Thanks for including a patch!
In the attached v39, I've taken your suggestion of flattening some of
the lists and done some rewording as well. I have also moved the note
about equivalence with pg_stat_statements columns to the
pg_stat_statements documentation. The result is quite a bit different
than what I had before, so I would be interested to hear your thoughts.

My concern with the blue "note" section like you mentioned is that it
would be harder to read the lists of backend types than it was in the
tabular format.

> clearer to use the term unquoted.

Thanks! I've fixed this.

> buffer from the freelist (where other commands may eventually make

You are right: reused is a normal, expected part of strategy
execution. And you are correct: the idea behind reusing existing
strategy buffers instead of taking buffers off the freelist is to leave
those buffers for blocks that we might expect to be accessed more than
once.

In practice, however, if you happen to not be using many shared buffers,
and then do a large COPY, for example, you will end up doing a bunch of
writes (in order to reuse the strategy buffers) that you perhaps didn't
need to do at that time had you leveraged the freelist. I think the
decision about which tradeoff to make is quite contentious, though.

> might read better to omit "io_context" from the list.

I changed this.

> Is this a fixed set of objects we should list, like for io_context?

I've added this.

- Melanie

[1] https://www.postgresql.org/message-id/20221121003815.qnwlnz2lhkow2e5w%40awork3.anarazel.de
[2] https://www.postgresql.org/message-id/20221123054329.GG11463%40telsasoft.com

---

From: Justin Pryzby <[email-masked]>
Date: Tue, 29 Nov 2022 20:51:13 -0600

> be compiled to the exact same thing as this at useful optimization

This doesn't seem like a detail worth much bother, but I did a test.

With -O2 (but not -O1 nor -Og) the assembly (gcc 9.4) is the same when
written like:

+       if (bktype == B_INVALID)
+               return false;
+       if (bktype == B_ARCHIVER)
+               return false;
+       if (bktype == B_LOGGER)
+               return false;
+       if (bktype == B_WAL_RECEIVER)
+               return false;
+       if (bktype == B_WAL_WRITER)
+               return false;
+
+       return true;

objdump --disassemble=pgstat_io_op_stats_collected src/backend/postgres_lib.a.p/utils_activity_pgstat_io_ops.c.o

0000000000000110 <pgstat_io_op_stats_collected>:
 110:   f3 0f 1e fa             endbr64 
 114:   b8 01 00 00 00          mov    $0x1,%eax
 119:   83 ff 0d                cmp    $0xd,%edi
 11c:   77 10                   ja     12e <pgstat_io_op_stats_collected+0x1e>
 11e:   b8 03 29 00 00          mov    $0x2903,%eax
 123:   89 f9                   mov    %edi,%ecx
 125:   48 d3 e8                shr    %cl,%rax
 128:   48 f7 d0                not    %rax
 12b:   83 e0 01                and    $0x1,%eax
 12e:   c3                      retq   

I was surprised, but the assembly is *not* the same when I used a switch{}.

I think it's fine to write however you want.

+               case IOOP_WRITE:
+ --> here <--
+}

-- 
Justin

---

From: Maciek Sakrejda <[email-masked]>
Date: Sun, 4 Dec 2022 14:48:43 -0800

> feedback from this email as well as docs feedback from Andres in [1] and

This looks great! Just a couple of minor comments.

> decision about which tradeoff to make is quite contentious, though.

Thanks for the explanation--that makes sense.

> would be harder to read the lists of backend types than it was in the

Oh, I wasn't thinking of doing a separate "note": just additional
paragraphs of text after the table (like what pg_stat_replication has
before its "note", or the brief comment after the pg_stat_archiver
table). But I think the updated docs work also.

+      <para>

maybe "...of an IO operation:" (colon) instead?

I know Lukas had commented that we should communicate that the goal is
to eventually provide relatively comprehensive I/O stats in this view
(you do that in the view description and I think that works), and this
is sort of along those lines, but I think speculative documentation
like this is not all that helpful. I'd drop this last sentence. Just
my two cents.

+      <para>
+       <varname>evicted</varname> in <varname>io_context</varname>
+       <literal>buffer pool</literal> and <varname>io_object</varname>
+       <literal>temp relation</literal> counts the number of times a block of
+       data from an existing local buffer was evicted in order to replace it
+       with another block, also in local buffers.
+      </para>

Doesn't this follow from the first sentence of the column description?
I think we could drop this, no?

Otherwise, the docs look good to me.

Thanks,
Maciek

---

From: Andres Freund <[email-masked]>
Date: Mon, 5 Dec 2022 11:32:09 -0800

Hi,

- I think it might be worth to rename IOCONTEXT_BUFFER_POOL to
  IOCONTEXT_{NORMAL, PLAIN, DEFAULT}. I'd like at some point to track WAL IO ,
  temporary file IO etc, and it doesn't seem useful to define a version of
  BUFFER_POOL for each of them. And it'd make it less confusing, because all
  the other existing contexts are also in the buffer pool (for now, can't wait
  for "bypass" or whatever to be tracked as well).

- given that IOContextForStrategy() is defined in freelist.c, and that
  declaring it in pgstat.h requires including buf.h, I think it's probably
  better to move IOContextForStrategy()'s declaration to freelist.h (doesn't
  exist, but whatever the right one is)

- pgstat_backend_io_stats_assert_well_formed() doesn't seem to belong in
  pgstat.c. Why not pgstat_io_ops.c?

- Do pgstat_io_context_ops_assert_zero(), pgstat_io_op_assert_zero() have to
  be in pgstat.h?

I think the only non-trival thing is the first point, the rest is stuff than I
also evolve during commit.

Greetings,

Andres Freund

---

From: Melanie Plageman <[email-masked]>
Date: Mon, 5 Dec 2022 20:49:20 -0500

Attached is v40.

I have addressed the feedback from Justin [1] and Maciek [2] as well.
I took all of the suggestions regarding the docs that Maciek made,

>  like this is not all that helpful. I'd drop this last sentence. Just

I have removed this and added the relevant part of this as a comment to
the view generating function pg_stat_get_io().

>   for "bypass" or whatever to be tracked as well).

In attached v40, I've renamed IOCONTEXT_BUFFER_POOL to IOCONTEXT_NORMAL.

>   exist, but whatever the right one is)

I have moved it to buf_internals.h.

>   pgstat.c. Why not pgstat_io_ops.c?

I put it in pgstat.c because it is only used there -- so I made it
static. I've moved it to pg_stat_io_ops.c and declared it in
pgstat_internal.h

> - Do pgstat_io_context_ops_assert_zero(), pgstat_io_op_assert_zero() have to

They are used in pgstatfuncs.c, which I presume should not include
pgstat_internal.h. Or did you mean that I should not put them in a
header file at all?

- Melanie

[1] https://www.postgresql.org/message-id/20221130025113.GD24131%40telsasoft.com
[2] https://www.postgresql.org/message-id/CAOtHd0BfFdMqO7-zDOk%3DiJTatzSDgVcgYcaR1_wk0GS4NN%2BRUQ%40mail.gmail.com

---

From: Maciek Sakrejda <[email-masked]>
Date: Wed, 7 Dec 2022 21:09:18 -0800

In the pg_stat_statements docs, there are several column descriptions like

   Total number of ... by the statement

You added an additional sentence to some describing the equivalent
pg_stat_io values, but you only added a period to the previous
sentence for shared_blks_read (for other columns, the additional
description just follows directly). These should be consistent.

---

From: Andres Freund <[email-masked]>
Date: Wed, 28 Dec 2022 15:56:13 -0800

Hi,

> I have changed the column names to be in the past tense.

For a while I was convinced by the consistency argument (after Melanie
pointing it out to me). But the more I look, the less convinced I am. The
existing IO related stats in pg_stat_database, pg_stat_bgwriter aren't past
tense, just the ones in pg_stat_statements. pg_stat_database uses past tense
for tup_*, but not xact_*, deadlocks, checksum_failures etc.

And even pg_stat_statements isn't consistent about it - otherwise it'd be
'planned' instead of 'plans', 'called' instead of 'calls' etc.

I started to look at the naming "tense" issue again, after I got "confused"
about "extended", because that somehow makes me think about more detailed
stats or such, rather than files getting extended.

ISTM that 'evictions', 'extends', 'fsyncs', 'reads', 'reuses', 'writes' are
clearer than the past tense versions, and about as consistent with existing
columns.

FWIW, I've been hacking on this code a bunch, mostly around renaming things
and changing the 'stacking' of the patches. My current state is at
https://github.com/anarazel/postgres/tree/pg_stat_io
A bit more to do before posting the edited version...

Greetings,

Andres Freund