Add pg_buffercache_evict_all() and pg_buffercache_mark_dirty[_all]() functions

Started by Nazir Bilal Yavuzabout 1 year ago28 messages
#1Nazir Bilal Yavuz
byavuz81@gmail.com
2 attachment(s)

Hi,

Andres off-list mentioned that:

1- It is time consuming to evict all shared buffers one by one using
the pg_buffercache_evict() function.
2- It would be good to have a function to mark buffers as dirty to
test different scenarios.

So, this patchset extends pg_buffercache with 3 functions:

1- pg_buffercache_evict_all(): This is very similar to the already
existing pg_buffercache_evict() function. The difference is
pg_buffercache_evict_all() does not take an argument. Instead it just
loops over the shared buffers and tries to evict all of them. It
returns the number of buffers evicted and flushed.

2- pg_buffercache_mark_dirty(): This function takes a buffer id as an
argument and tries to mark this buffer as dirty. Returns true on
success. This returns false if the buffer is already dirty. Do you
think this makes sense or do you prefer it to return true if the
buffer is already dirty?

3- pg_buffercache_mark_dirty_all(): This is very similar to the
pg_buffercache_mark_dirty() function. The difference is
pg_buffercache_mark_dirty_all() does not take an argument. Instead it
just loops over the shared buffers and tries to mark all of them as
dirty. It returns the number of buffers marked as dirty.

I tested these functions with 16GB shared buffers.

pg_buffercache_evict() -> 790ms
pg_buffercache_evict_all() -> 270ms

pg_buffercache_mark_dirty() -> 550ms
pg_buffercache_mark_dirty_all() -> 70ms

Any feedback would be appreciated.

--
Regards,
Nazir Bilal Yavuz
Microsoft

Attachments:

v1-0001-Add-pg_buffercache_evict_all-function-for-testing.patchtext/x-patch; charset=US-ASCII; name=v1-0001-Add-pg_buffercache_evict_all-function-for-testing.patchDownload
From 813e5ec0da4c65970b4b1ce2ec2918e4652da9ab Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@gmail.com>
Date: Fri, 20 Dec 2024 14:06:47 +0300
Subject: [PATCH v1 1/2] Add pg_buffercache_evict_all() function for testing

This new function provides a mechanism to evict all shared buffers at
once. It is designed to address the inefficiency of repeatedly calling
pg_buffercache_evict() for each individual buffer, which can be
time-consuming when dealing with large shared buffer pools
(e.g., ~790ms vs. ~270ms for 16GB of shared buffers).

It is intended for developer testing and debugging purposes and is
available to superusers only.
---
 src/include/storage/bufmgr.h                  |  2 +-
 src/backend/storage/buffer/bufmgr.c           |  5 +-
 doc/src/sgml/pgbuffercache.sgml               | 22 ++++++++-
 contrib/pg_buffercache/Makefile               |  3 +-
 contrib/pg_buffercache/meson.build            |  1 +
 .../pg_buffercache--1.5--1.6.sql              |  7 +++
 contrib/pg_buffercache/pg_buffercache.control |  2 +-
 contrib/pg_buffercache/pg_buffercache_pages.c | 47 ++++++++++++++++++-
 8 files changed, 82 insertions(+), 7 deletions(-)
 create mode 100644 contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql

diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index eb0fba4230b..7f4eeca4afd 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -297,7 +297,7 @@ extern bool BgBufferSync(struct WritebackContext *wb_context);
 extern void LimitAdditionalPins(uint32 *additional_pins);
 extern void LimitAdditionalLocalPins(uint32 *additional_pins);
 
-extern bool EvictUnpinnedBuffer(Buffer buf);
+extern bool EvictUnpinnedBuffer(Buffer buf, bool *flushed);
 
 /* in buf_init.c */
 extern void BufferManagerShmemInit(void);
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 2622221809c..d2a93cf7cc0 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -6098,12 +6098,14 @@ ResOwnerPrintBufferPin(Datum res)
  * or if the buffer becomes dirty again while we're trying to write it out.
  */
 bool
-EvictUnpinnedBuffer(Buffer buf)
+EvictUnpinnedBuffer(Buffer buf, bool *flushed)
 {
 	BufferDesc *desc;
 	uint32		buf_state;
 	bool		result;
 
+	*flushed = false;
+
 	/* Make sure we can pin the buffer. */
 	ResourceOwnerEnlarge(CurrentResourceOwner);
 	ReservePrivateRefCountEntry();
@@ -6134,6 +6136,7 @@ EvictUnpinnedBuffer(Buffer buf)
 		LWLockAcquire(BufferDescriptorGetContentLock(desc), LW_SHARED);
 		FlushBuffer(desc, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL);
 		LWLockRelease(BufferDescriptorGetContentLock(desc));
+		*flushed = true;
 	}
 
 	/* This will return false if it becomes dirty or someone else pins it. */
diff --git a/doc/src/sgml/pgbuffercache.sgml b/doc/src/sgml/pgbuffercache.sgml
index 4b90eefc0b0..df4d90a650a 100644
--- a/doc/src/sgml/pgbuffercache.sgml
+++ b/doc/src/sgml/pgbuffercache.sgml
@@ -31,8 +31,9 @@
   This module provides the <function>pg_buffercache_pages()</function>
   function (wrapped in the <structname>pg_buffercache</structname> view),
   the <function>pg_buffercache_summary()</function> function, the
-  <function>pg_buffercache_usage_counts()</function> function and
-  the <function>pg_buffercache_evict()</function> function.
+  <function>pg_buffercache_usage_counts()</function> function, the
+  <function>pg_buffercache_evict()</function> and the
+  <function>pg_buffercache_evict_all()</function> function.
  </para>
 
  <para>
@@ -65,6 +66,12 @@
   function is restricted to superusers only.
  </para>
 
+ <para>
+  The <function>pg_buffercache_evict_all()</function> function tries to evict
+  all buffers in the buffer pool. It returns how many buffers are
+  evicted and flushed. Use of this function is restricted to superusers only.
+ </para>
+
  <sect2 id="pgbuffercache-pg-buffercache">
   <title>The <structname>pg_buffercache</structname> View</title>
 
@@ -378,6 +385,17 @@
   </para>
  </sect2>
 
+ <sect2 id="pgbuffercache-pg-buffercache-evict-all">
+  <title>The <structname>pg_buffercache_evict_all</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_evict_all()</function> function is very similar
+   to <function>pg_buffercache_evict()</function> function.  The difference is,
+   the <function>pg_buffercache_evict_all()</function> does not take argument;
+   instead it tries to evict all buffers in the buffer pool.  The function is
+   intended for developer testing only.
+  </para>
+ </sect2>
+
 <sect2 id="pgbuffercache-sample-output">
   <title>Sample Output</title>
 
diff --git a/contrib/pg_buffercache/Makefile b/contrib/pg_buffercache/Makefile
index eae65ead9e5..2a33602537e 100644
--- a/contrib/pg_buffercache/Makefile
+++ b/contrib/pg_buffercache/Makefile
@@ -8,7 +8,8 @@ OBJS = \
 EXTENSION = pg_buffercache
 DATA = pg_buffercache--1.2.sql pg_buffercache--1.2--1.3.sql \
 	pg_buffercache--1.1--1.2.sql pg_buffercache--1.0--1.1.sql \
-	pg_buffercache--1.3--1.4.sql pg_buffercache--1.4--1.5.sql
+	pg_buffercache--1.3--1.4.sql pg_buffercache--1.4--1.5.sql \
+	pg_buffercache--1.5--1.6.sql
 PGFILEDESC = "pg_buffercache - monitoring of shared buffer cache in real-time"
 
 REGRESS = pg_buffercache
diff --git a/contrib/pg_buffercache/meson.build b/contrib/pg_buffercache/meson.build
index 1ca3452918b..1ff57aa22e9 100644
--- a/contrib/pg_buffercache/meson.build
+++ b/contrib/pg_buffercache/meson.build
@@ -23,6 +23,7 @@ install_data(
   'pg_buffercache--1.2.sql',
   'pg_buffercache--1.3--1.4.sql',
   'pg_buffercache--1.4--1.5.sql',
+  'pg_buffercache--1.5--1.6.sql',
   'pg_buffercache.control',
   kwargs: contrib_data_args,
 )
diff --git a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
new file mode 100644
index 00000000000..c08b799be94
--- /dev/null
+++ b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
@@ -0,0 +1,7 @@
+\echo Use "ALTER EXTENSION pg_buffercache UPDATE TO '1.6'" to load this file. \quit
+
+CREATE FUNCTION pg_buffercache_evict_all(
+    OUT buffers_evicted int4,
+    OUT buffers_flushed int4)
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict_all'
+LANGUAGE C PARALLEL SAFE VOLATILE;
diff --git a/contrib/pg_buffercache/pg_buffercache.control b/contrib/pg_buffercache/pg_buffercache.control
index 5ee875f77dd..b030ba3a6fa 100644
--- a/contrib/pg_buffercache/pg_buffercache.control
+++ b/contrib/pg_buffercache/pg_buffercache.control
@@ -1,5 +1,5 @@
 # pg_buffercache extension
 comment = 'examine the shared buffer cache'
-default_version = '1.5'
+default_version = '1.6'
 module_pathname = '$libdir/pg_buffercache'
 relocatable = true
diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index 3ae0a018e10..ea7c0e6934c 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -19,6 +19,7 @@
 #define NUM_BUFFERCACHE_PAGES_ELEM	9
 #define NUM_BUFFERCACHE_SUMMARY_ELEM 5
 #define NUM_BUFFERCACHE_USAGE_COUNTS_ELEM 4
+#define NUM_BUFFERCACHE_EVICT_ALL_ELEM 2
 
 PG_MODULE_MAGIC;
 
@@ -64,6 +65,7 @@ PG_FUNCTION_INFO_V1(pg_buffercache_pages);
 PG_FUNCTION_INFO_V1(pg_buffercache_summary);
 PG_FUNCTION_INFO_V1(pg_buffercache_usage_counts);
 PG_FUNCTION_INFO_V1(pg_buffercache_evict);
+PG_FUNCTION_INFO_V1(pg_buffercache_evict_all);
 
 Datum
 pg_buffercache_pages(PG_FUNCTION_ARGS)
@@ -356,6 +358,7 @@ Datum
 pg_buffercache_evict(PG_FUNCTION_ARGS)
 {
 	Buffer		buf = PG_GETARG_INT32(0);
+	bool		flushed;
 
 	if (!superuser())
 		ereport(ERROR,
@@ -365,5 +368,47 @@ pg_buffercache_evict(PG_FUNCTION_ARGS)
 	if (buf < 1 || buf > NBuffers)
 		elog(ERROR, "bad buffer ID: %d", buf);
 
-	PG_RETURN_BOOL(EvictUnpinnedBuffer(buf));
+	PG_RETURN_BOOL(EvictUnpinnedBuffer(buf, &flushed));
+}
+
+/*
+ * Try to evict all shared buffers.
+ */
+Datum
+pg_buffercache_evict_all(PG_FUNCTION_ARGS)
+{
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_EVICT_ALL_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_EVICT_ALL_ELEM] = {0};
+
+	int32		buffers_evicted = 0;
+	int32		buffers_flushed = 0;
+	bool		flushed;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use pg_buffercache_evict_all function")));
+
+	for (int buf = 1; buf < NBuffers; buf++)
+	{
+		if (EvictUnpinnedBuffer(buf, &flushed))
+			buffers_evicted++;
+		if (flushed)
+			buffers_flushed++;
+	}
+
+	values[0] = Int32GetDatum(buffers_evicted);
+	values[1] = Int32GetDatum(buffers_flushed);
+
+	/* Build and return the tuple. */
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
 }
-- 
2.45.2

v1-0002-Add-pg_buffercache_mark_dirty-_all-functions-for-.patchtext/x-patch; charset=US-ASCII; name=v1-0002-Add-pg_buffercache_mark_dirty-_all-functions-for-.patchDownload
From 19b0a56e72525ee1c9754fb06ab5f31d41e43ca7 Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@gmail.com>
Date: Wed, 25 Dec 2024 15:46:10 +0300
Subject: [PATCH v1 2/2] Add pg_buffercache_mark_dirty[_all]() functions for
 testing

This commit introduces two new functions for marking shared buffers as
dirty:

pg_buffercache_mark_dirty(): Marks a specific shared buffer as dirty.
pg_buffercache_mark_dirty_all(): Marks all shared buffers as dirty in a
single operation.

The pg_buffercache_mark_dirty_all() function provides an efficient
way to dirty the entire buffer pool (e.g., ~550ms vs. ~70ms for 16GB of
shared buffers), complementing pg_buffercache_mark_dirty() for more
granular control.

These functions are intended for developer testing and debugging
scenarios, enabling users to simulate various buffer pool states and
test write-back behavior. Both functions are superuser-only.
---
 src/include/storage/bufmgr.h                  |  1 +
 src/backend/storage/buffer/bufmgr.c           | 62 +++++++++++++++++++
 doc/src/sgml/pgbuffercache.sgml               | 40 +++++++++++-
 .../pg_buffercache--1.5--1.6.sql              | 10 +++
 contrib/pg_buffercache/pg_buffercache_pages.c | 43 +++++++++++++
 5 files changed, 154 insertions(+), 2 deletions(-)

diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 7f4eeca4afd..ac077402dd9 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -298,6 +298,7 @@ extern void LimitAdditionalPins(uint32 *additional_pins);
 extern void LimitAdditionalLocalPins(uint32 *additional_pins);
 
 extern bool EvictUnpinnedBuffer(Buffer buf, bool *flushed);
+extern bool MarkUnpinnedBufferDirty(Buffer buf);
 
 /* in buf_init.c */
 extern void BufferManagerShmemInit(void);
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index d2a93cf7cc0..7ab8bcaa228 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -6146,3 +6146,65 @@ EvictUnpinnedBuffer(Buffer buf, bool *flushed)
 
 	return result;
 }
+
+/*
+ * MarkUnpinnedBufferDirty
+ *
+ * This function is intended for testing/development use only!
+ *
+ * To succeed, the buffer must not be pinned on entry, so if the caller had a
+ * particular block in mind, it might already have been replaced by some other
+ * block by the time this function runs.  It's also unpinned on return, so the
+ * buffer might be occupied and flushed by the time control is returned.  This
+ * inherent raciness without other interlocking makes the function unsuitable
+ * for non-testing usage.
+ *
+ * Returns true if the buffer was not dirty and it has now been marked as
+ * dirty.  Returns false if it wasn't valid, if it couldn't be marked as dirty
+ * due to a pin, or if the buffer was already dirty.
+ */
+bool
+MarkUnpinnedBufferDirty(Buffer buf)
+{
+	BufferDesc *desc;
+	uint32		buf_state;
+	bool		result = false;
+
+	Assert(!BufferIsLocal(buf));
+
+	/* Make sure we can pin the buffer. */
+	ResourceOwnerEnlarge(CurrentResourceOwner);
+	ReservePrivateRefCountEntry();
+
+	desc = GetBufferDescriptor(buf - 1);
+
+	/* Lock the header and check if it's valid. */
+	buf_state = LockBufHdr(desc);
+	if ((buf_state & BM_VALID) == 0)
+	{
+		UnlockBufHdr(desc, buf_state);
+		return false;
+	}
+
+	/* Check that it's not pinned already. */
+	if (BUF_STATE_GET_REFCOUNT(buf_state) > 0)
+	{
+		UnlockBufHdr(desc, buf_state);
+		return false;
+	}
+
+	PinBuffer_Locked(desc);		/* releases spinlock */
+
+	/* If it was not already dirty, mark it as dirty. */
+	if (!(buf_state & BM_DIRTY))
+	{
+		LWLockAcquire(BufferDescriptorGetContentLock(desc), LW_EXCLUSIVE);
+		MarkBufferDirty(buf);
+		LWLockRelease(BufferDescriptorGetContentLock(desc));
+		result = true;
+	}
+
+	UnpinBuffer(desc);
+
+	return result;
+}
diff --git a/doc/src/sgml/pgbuffercache.sgml b/doc/src/sgml/pgbuffercache.sgml
index df4d90a650a..a1c5036a383 100644
--- a/doc/src/sgml/pgbuffercache.sgml
+++ b/doc/src/sgml/pgbuffercache.sgml
@@ -32,8 +32,10 @@
   function (wrapped in the <structname>pg_buffercache</structname> view),
   the <function>pg_buffercache_summary()</function> function, the
   <function>pg_buffercache_usage_counts()</function> function, the
-  <function>pg_buffercache_evict()</function> and the
-  <function>pg_buffercache_evict_all()</function> function.
+  <function>pg_buffercache_evict()</function> the
+  <function>pg_buffercache_evict_all()</function>, the
+  <function>pg_buffercache_mark_dirty()</function> and the
+  <function>pg_buffercache_mark_dirty_all()</function> function.
  </para>
 
  <para>
@@ -72,6 +74,18 @@
   evicted and flushed. Use of this function is restricted to superusers only.
  </para>
 
+ <para>
+  The <function>pg_buffercache_mark_dirty()</function> function allows a block
+  to be marked as dirty from the buffer pool given a buffer identifier.  Use of
+  this function is restricted to superusers only.
+ </para>
+
+ <para>
+  The <function>pg_buffercache_mark_dirty_all()</function> function tries to
+  mark all buffers dirty in the buffer pool. It returns how many buffers are
+  dirtied. Use of this function is restricted to superusers only.
+ </para>
+
  <sect2 id="pgbuffercache-pg-buffercache">
   <title>The <structname>pg_buffercache</structname> View</title>
 
@@ -396,6 +410,28 @@
   </para>
  </sect2>
 
+ <sect2 id="pgbuffercache-pg-buffercache-mark-dirty">
+  <title>The <structname>pg_buffercache_mark_dirty</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_mark_dirty()</function> function takes a buffer
+   identifier, as shown in the <structfield>bufferid</structfield> column of
+   the <structname>pg_buffercache</structname> view.  It returns true on success,
+   and false if the buffer wasn't valid or if it couldn't be marked as dirty
+   because it was pinned.  The function is intended for developer testing only.
+  </para>
+ </sect2>
+
+ <sect2 id="pgbuffercache-pg-buffercache-mark-dirty-all">
+  <title>The <structname>pg_buffercache_mark_dirty_all</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_mark_dirty_all()</function> function is very similar
+   to <function>pg_buffercache_mark_dirty()</function> function.  The difference is,
+   the <function>pg_buffercache_mark_dirty_all()</function> does not take argument;
+   instead it tries to mark all buffers dirty in the buffer pool.  The function is
+   intended for developer testing only.
+  </para>
+ </sect2>
+
 <sect2 id="pgbuffercache-sample-output">
   <title>Sample Output</title>
 
diff --git a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
index c08b799be94..e725fef1247 100644
--- a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
+++ b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
@@ -5,3 +5,13 @@ CREATE FUNCTION pg_buffercache_evict_all(
     OUT buffers_flushed int4)
 AS 'MODULE_PATHNAME', 'pg_buffercache_evict_all'
 LANGUAGE C PARALLEL SAFE VOLATILE;
+
+CREATE FUNCTION pg_buffercache_mark_dirty(IN int)
+RETURNS bool
+AS 'MODULE_PATHNAME', 'pg_buffercache_mark_dirty'
+LANGUAGE C PARALLEL SAFE VOLATILE STRICT;
+
+CREATE FUNCTION pg_buffercache_mark_dirty_all()
+RETURNS INT4
+AS 'MODULE_PATHNAME', 'pg_buffercache_mark_dirty_all'
+LANGUAGE C PARALLEL SAFE VOLATILE;
diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index ea7c0e6934c..1a0992467b2 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -66,6 +66,8 @@ PG_FUNCTION_INFO_V1(pg_buffercache_summary);
 PG_FUNCTION_INFO_V1(pg_buffercache_usage_counts);
 PG_FUNCTION_INFO_V1(pg_buffercache_evict);
 PG_FUNCTION_INFO_V1(pg_buffercache_evict_all);
+PG_FUNCTION_INFO_V1(pg_buffercache_mark_dirty);
+PG_FUNCTION_INFO_V1(pg_buffercache_mark_dirty_all);
 
 Datum
 pg_buffercache_pages(PG_FUNCTION_ARGS)
@@ -412,3 +414,44 @@ pg_buffercache_evict_all(PG_FUNCTION_ARGS)
 
 	PG_RETURN_DATUM(result);
 }
+
+/*
+ * Try to mark dirty a shared buffer.
+ */
+Datum
+pg_buffercache_mark_dirty(PG_FUNCTION_ARGS)
+{
+	Buffer		buf = PG_GETARG_INT32(0);
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use pg_buffercache_mark_dirty function")));
+
+	if (buf < 1 || buf > NBuffers)
+		elog(ERROR, "bad buffer ID: %d", buf);
+
+	PG_RETURN_BOOL(MarkUnpinnedBufferDirty(buf));
+}
+
+/*
+ * Try to mark dirty all shared buffers.
+ */
+Datum
+pg_buffercache_mark_dirty_all(PG_FUNCTION_ARGS)
+{
+	int32		buffers_dirtied = 0;
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use pg_buffercache_mark_dirty_all function")));
+
+	for (int buf = 1; buf < NBuffers; buf++)
+	{
+		if (MarkUnpinnedBufferDirty(buf))
+			buffers_dirtied++;
+	}
+
+	PG_RETURN_INT32(buffers_dirtied);
+}
-- 
2.45.2

#2Andres Freund
andres@anarazel.de
In reply to: Nazir Bilal Yavuz (#1)
Re: Add pg_buffercache_evict_all() and pg_buffercache_mark_dirty[_all]() functions

Hi,

On 2024-12-25 15:57:34 +0300, Nazir Bilal Yavuz wrote:

1- It is time consuming to evict all shared buffers one by one using
the pg_buffercache_evict() function.

This is really useful for benchmarking. When testing IO heavy workloads it
turns out to be a lot lower noise to measure with shared buffers already
"touched" before taking actual measurements. The first time a buffer is used
the kernel needs to actually initialize the memory for the buffer, which can
take a substantial amount of time. Which obviously can hide many performance
differences. And it adds a lot of noise.

2- It would be good to have a function to mark buffers as dirty to
test different scenarios.

This is important to be able to measure checkpointer performance. Otherwise
one has to load data from scratch. That's bad for three reasons:

1) After re-loading the data from scratch the data can be laid-out differently
on-disk, which can have substantial performance impacts. By re-dirtying
data one instead can measure the performance effects of a change with the
same on-disk layaout.
2) It takes a lot of time to reload data from scratch.
3) The first write to data has substantially different performance
characteristics than later writes, but often isn't the most relevant factor
for real-world performance test.

So, this patchset extends pg_buffercache with 3 functions:

1- pg_buffercache_evict_all(): This is very similar to the already
existing pg_buffercache_evict() function. The difference is
pg_buffercache_evict_all() does not take an argument. Instead it just
loops over the shared buffers and tries to evict all of them. It
returns the number of buffers evicted and flushed.

I do think that'd be rather useful. Perhaps it's also worth having a version
that just evicts a specific relation?

2- pg_buffercache_mark_dirty(): This function takes a buffer id as an
argument and tries to mark this buffer as dirty. Returns true on
success. This returns false if the buffer is already dirty. Do you
think this makes sense or do you prefer it to return true if the
buffer is already dirty?

I don't really have feelings about that ;)

From 813e5ec0da4c65970b4b1ce2ec2918e4652da9ab Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@gmail.com>
Date: Fri, 20 Dec 2024 14:06:47 +0300
Subject: [PATCH v1 1/2] Add pg_buffercache_evict_all() function for testing

This new function provides a mechanism to evict all shared buffers at
once. It is designed to address the inefficiency of repeatedly calling
pg_buffercache_evict() for each individual buffer, which can be
time-consuming when dealing with large shared buffer pools
(e.g., ~790ms vs. ~270ms for 16GB of shared buffers).

*/
bool
-EvictUnpinnedBuffer(Buffer buf)
+EvictUnpinnedBuffer(Buffer buf, bool *flushed)
{
BufferDesc *desc;
uint32		buf_state;
bool		result;

+ *flushed = false;
+
/* Make sure we can pin the buffer. */
ResourceOwnerEnlarge(CurrentResourceOwner);
ReservePrivateRefCountEntry();
@@ -6134,6 +6136,7 @@ EvictUnpinnedBuffer(Buffer buf)
LWLockAcquire(BufferDescriptorGetContentLock(desc), LW_SHARED);
FlushBuffer(desc, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL);
LWLockRelease(BufferDescriptorGetContentLock(desc));
+ *flushed = true;
}

/* This will return false if it becomes dirty or someone else pins
it. */

I don't think *flushed is necessarily accurate this way, as it might detect
that it doesn't need to flush the data (via StartBufferIO() returning false).

+ */
+Datum
+pg_buffercache_evict_all(PG_FUNCTION_ARGS)
+{
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_EVICT_ALL_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_EVICT_ALL_ELEM] = {0};
+
+	int32		buffers_evicted = 0;
+	int32		buffers_flushed = 0;
+	bool		flushed;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use pg_buffercache_evict_all function")));
+
+	for (int buf = 1; buf < NBuffers; buf++)
+	{
+		if (EvictUnpinnedBuffer(buf, &flushed))
+			buffers_evicted++;
+		if (flushed)
+			buffers_flushed++;

I'd probably add a pre-check for the buffer not being in use. We don't need an
external function call, with an unconditional LockBufHdr() inside, for that
case.

+/*
+ * MarkUnpinnedBufferDirty
+ *
+ * This function is intended for testing/development use only!
+ *
+ * To succeed, the buffer must not be pinned on entry, so if the caller had a
+ * particular block in mind, it might already have been replaced by some other
+ * block by the time this function runs.  It's also unpinned on return, so the
+ * buffer might be occupied and flushed by the time control is returned.  This
+ * inherent raciness without other interlocking makes the function unsuitable
+ * for non-testing usage.
+ *
+ * Returns true if the buffer was not dirty and it has now been marked as
+ * dirty.  Returns false if it wasn't valid, if it couldn't be marked as dirty
+ * due to a pin, or if the buffer was already dirty.
+ */

Hm. One potentially problematic thing with this is that it can pin and dirty a
buffer even while its relation is locked exclusively. Which i think some code
doesn't expect. Not sure if there's a way around that :(

Greetings,

Andres Freund

#3Nazir Bilal Yavuz
byavuz81@gmail.com
In reply to: Andres Freund (#2)
Re: Add pg_buffercache_evict_all() and pg_buffercache_mark_dirty[_all]() functions

Hi,

On Mon, 17 Feb 2025 at 19:59, Andres Freund <andres@anarazel.de> wrote:

Hi,

Thanks for the review! And sorry for the late reply.

On 2024-12-25 15:57:34 +0300, Nazir Bilal Yavuz wrote:

So, this patchset extends pg_buffercache with 3 functions:

1- pg_buffercache_evict_all(): This is very similar to the already
existing pg_buffercache_evict() function. The difference is
pg_buffercache_evict_all() does not take an argument. Instead it just
loops over the shared buffers and tries to evict all of them. It
returns the number of buffers evicted and flushed.

I do think that'd be rather useful. Perhaps it's also worth having a version
that just evicts a specific relation?

That makes sense. I will work on this.

2- pg_buffercache_mark_dirty(): This function takes a buffer id as an
argument and tries to mark this buffer as dirty. Returns true on
success. This returns false if the buffer is already dirty. Do you
think this makes sense or do you prefer it to return true if the
buffer is already dirty?

I don't really have feelings about that ;)

I feel the same. I just wanted to have a symmetry between dirty and
evict functions. If you think this would be useless, I can remove it.

From 813e5ec0da4c65970b4b1ce2ec2918e4652da9ab Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@gmail.com>
Date: Fri, 20 Dec 2024 14:06:47 +0300
Subject: [PATCH v1 1/2] Add pg_buffercache_evict_all() function for testing

This new function provides a mechanism to evict all shared buffers at
once. It is designed to address the inefficiency of repeatedly calling
pg_buffercache_evict() for each individual buffer, which can be
time-consuming when dealing with large shared buffer pools
(e.g., ~790ms vs. ~270ms for 16GB of shared buffers).

*/
bool
-EvictUnpinnedBuffer(Buffer buf)
+EvictUnpinnedBuffer(Buffer buf, bool *flushed)
{
BufferDesc *desc;
uint32          buf_state;
bool            result;

+ *flushed = false;
+
/* Make sure we can pin the buffer. */
ResourceOwnerEnlarge(CurrentResourceOwner);
ReservePrivateRefCountEntry();
@@ -6134,6 +6136,7 @@ EvictUnpinnedBuffer(Buffer buf)
LWLockAcquire(BufferDescriptorGetContentLock(desc), LW_SHARED);
FlushBuffer(desc, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL);
LWLockRelease(BufferDescriptorGetContentLock(desc));
+ *flushed = true;
}

/* This will return false if it becomes dirty or someone else pins
it. */

I don't think *flushed is necessarily accurate this way, as it might detect
that it doesn't need to flush the data (via StartBufferIO() returning false).

You are right. It seems there is no way to get that information
without editing the FlushBuffer(), right? And I think editing
FlushBuffer() is not worth it. So, I can either remove it or explain
in the docs that #buffers_flushed may not be completely accurate.

+ */
+Datum
+pg_buffercache_evict_all(PG_FUNCTION_ARGS)
+{
+     Datum           result;
+     TupleDesc       tupledesc;
+     HeapTuple       tuple;
+     Datum           values[NUM_BUFFERCACHE_EVICT_ALL_ELEM];
+     bool            nulls[NUM_BUFFERCACHE_EVICT_ALL_ELEM] = {0};
+
+     int32           buffers_evicted = 0;
+     int32           buffers_flushed = 0;
+     bool            flushed;
+
+     if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+             elog(ERROR, "return type must be a row type");
+
+     if (!superuser())
+             ereport(ERROR,
+                             (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+                              errmsg("must be superuser to use pg_buffercache_evict_all function")));
+
+     for (int buf = 1; buf < NBuffers; buf++)
+     {
+             if (EvictUnpinnedBuffer(buf, &flushed))
+                     buffers_evicted++;
+             if (flushed)
+                     buffers_flushed++;

I'd probably add a pre-check for the buffer not being in use. We don't need an
external function call, with an unconditional LockBufHdr() inside, for that
case.

I did not understand why we would need this. Does not
EvictUnpinnedBuffer() already check that the buffer is not in use?

+/*
+ * MarkUnpinnedBufferDirty
+ *
+ * This function is intended for testing/development use only!
+ *
+ * To succeed, the buffer must not be pinned on entry, so if the caller had a
+ * particular block in mind, it might already have been replaced by some other
+ * block by the time this function runs.  It's also unpinned on return, so the
+ * buffer might be occupied and flushed by the time control is returned.  This
+ * inherent raciness without other interlocking makes the function unsuitable
+ * for non-testing usage.
+ *
+ * Returns true if the buffer was not dirty and it has now been marked as
+ * dirty.  Returns false if it wasn't valid, if it couldn't be marked as dirty
+ * due to a pin, or if the buffer was already dirty.
+ */

Hm. One potentially problematic thing with this is that it can pin and dirty a
buffer even while its relation is locked exclusively. Which i think some code
doesn't expect. Not sure if there's a way around that :(

I see, I did not think about that. Since this function can be used
only by superusers, that problem might not be a big issue. What do you
think?

--
Regards,
Nazir Bilal Yavuz
Microsoft

#4Nazir Bilal Yavuz
byavuz81@gmail.com
In reply to: Nazir Bilal Yavuz (#3)
2 attachment(s)
Re: Add pg_buffercache_evict_all() and pg_buffercache_mark_dirty[_all]() functions

Hi,

Here is the v2. Changes prior to v1 are:

- pg_buffercache_evict_relation() function is introduced. Which takes
a relation as an argument and evicts all shared buffers in the
relation.
- It is mentioned in the docs that the #buffers_flushed in the
pg_buffercache_evict_relation() and pg_buffercache_evict_all()
functions do not necessarily mean these buffers are flushed by these
functions.

Remaining questions from the v1:

On Wed, 12 Mar 2025 at 19:15, Nazir Bilal Yavuz <byavuz81@gmail.com> wrote:

On Mon, 17 Feb 2025 at 19:59, Andres Freund <andres@anarazel.de> wrote:

On 2024-12-25 15:57:34 +0300, Nazir Bilal Yavuz wrote:

+ */
+Datum
+pg_buffercache_evict_all(PG_FUNCTION_ARGS)
+{
+     Datum           result;
+     TupleDesc       tupledesc;
+     HeapTuple       tuple;
+     Datum           values[NUM_BUFFERCACHE_EVICT_ALL_ELEM];
+     bool            nulls[NUM_BUFFERCACHE_EVICT_ALL_ELEM] = {0};
+
+     int32           buffers_evicted = 0;
+     int32           buffers_flushed = 0;
+     bool            flushed;
+
+     if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+             elog(ERROR, "return type must be a row type");
+
+     if (!superuser())
+             ereport(ERROR,
+                             (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+                              errmsg("must be superuser to use pg_buffercache_evict_all function")));
+
+     for (int buf = 1; buf < NBuffers; buf++)
+     {
+             if (EvictUnpinnedBuffer(buf, &flushed))
+                     buffers_evicted++;
+             if (flushed)
+                     buffers_flushed++;

I'd probably add a pre-check for the buffer not being in use. We don't need an
external function call, with an unconditional LockBufHdr() inside, for that
case.

I did not understand why we would need this. Does not
EvictUnpinnedBuffer() already check that the buffer is not in use?

+/*
+ * MarkUnpinnedBufferDirty
+ *
+ * This function is intended for testing/development use only!
+ *
+ * To succeed, the buffer must not be pinned on entry, so if the caller had a
+ * particular block in mind, it might already have been replaced by some other
+ * block by the time this function runs.  It's also unpinned on return, so the
+ * buffer might be occupied and flushed by the time control is returned.  This
+ * inherent raciness without other interlocking makes the function unsuitable
+ * for non-testing usage.
+ *
+ * Returns true if the buffer was not dirty and it has now been marked as
+ * dirty.  Returns false if it wasn't valid, if it couldn't be marked as dirty
+ * due to a pin, or if the buffer was already dirty.
+ */

Hm. One potentially problematic thing with this is that it can pin and dirty a
buffer even while its relation is locked exclusively. Which i think some code
doesn't expect. Not sure if there's a way around that :(

I see, I did not think about that. Since this function can be used
only by superusers, that problem might not be a big issue. What do you
think?

--
Regards,
Nazir Bilal Yavuz
Microsoft

Attachments:

v2-0001-Add-pg_buffercache_evict_-relation-all-functions-.patchtext/x-patch; charset=US-ASCII; name=v2-0001-Add-pg_buffercache_evict_-relation-all-functions-.patchDownload
From 2090816c1a2d97b257f136ea7ab36c1c86dc1e2a Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@gmail.com>
Date: Fri, 20 Dec 2024 14:06:47 +0300
Subject: [PATCH v2 1/2] Add pg_buffercache_evict_[relation | all]() functions
 for testing

pg_buffercache_evict_relation(): Evicts all shared buffers in a
relation at once.
pg_buffercache_evict_all(): Evicts all shared buffers at once.

Both functions provide mechanism to evict multiple shared buffers at
once. They are designed to address the inefficiency of repeatedly calling
pg_buffercache_evict() for each individual buffer, which can be
time-consuming when dealing with large shared buffer pools.
(e.g., ~790ms vs. ~270ms for 16GB of shared buffers).

These functions are intended for developer testing and debugging
purposes and are available to superusers only.
---
 src/include/storage/bufmgr.h                  |   2 +-
 src/backend/storage/buffer/bufmgr.c           |  13 +-
 doc/src/sgml/pgbuffercache.sgml               |  44 +++++-
 contrib/pg_buffercache/Makefile               |   3 +-
 contrib/pg_buffercache/meson.build            |   1 +
 .../pg_buffercache--1.5--1.6.sql              |  15 ++
 contrib/pg_buffercache/pg_buffercache.control |   2 +-
 contrib/pg_buffercache/pg_buffercache_pages.c | 131 +++++++++++++++++-
 8 files changed, 201 insertions(+), 10 deletions(-)
 create mode 100644 contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql

diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 79a89f87fcc..16c92383969 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -297,7 +297,7 @@ extern uint32 GetAdditionalLocalPinLimit(void);
 extern void LimitAdditionalPins(uint32 *additional_pins);
 extern void LimitAdditionalLocalPins(uint32 *additional_pins);
 
-extern bool EvictUnpinnedBuffer(Buffer buf);
+extern bool EvictUnpinnedBuffer(Buffer buf, uint32 buf_state, bool *flushed);
 
 /* in buf_init.c */
 extern void BufferManagerShmemInit(void);
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 8243f4b2445..53bbdd92e53 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -6056,12 +6056,13 @@ ResOwnerPrintBufferPin(Datum res)
  * or if the buffer becomes dirty again while we're trying to write it out.
  */
 bool
-EvictUnpinnedBuffer(Buffer buf)
+EvictUnpinnedBuffer(Buffer buf, uint32 buf_state, bool *flushed)
 {
 	BufferDesc *desc;
-	uint32		buf_state;
 	bool		result;
 
+	*flushed = false;
+
 	/* Make sure we can pin the buffer. */
 	ResourceOwnerEnlarge(CurrentResourceOwner);
 	ReservePrivateRefCountEntry();
@@ -6069,8 +6070,11 @@ EvictUnpinnedBuffer(Buffer buf)
 	Assert(!BufferIsLocal(buf));
 	desc = GetBufferDescriptor(buf - 1);
 
-	/* Lock the header and check if it's valid. */
-	buf_state = LockBufHdr(desc);
+	/* Lock the header if it is not already locked. */
+	if (!buf_state)
+		buf_state = LockBufHdr(desc);
+
+	/* Check if it's valid. */
 	if ((buf_state & BM_VALID) == 0)
 	{
 		UnlockBufHdr(desc, buf_state);
@@ -6092,6 +6096,7 @@ EvictUnpinnedBuffer(Buffer buf)
 		LWLockAcquire(BufferDescriptorGetContentLock(desc), LW_SHARED);
 		FlushBuffer(desc, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL);
 		LWLockRelease(BufferDescriptorGetContentLock(desc));
+		*flushed = true;
 	}
 
 	/* This will return false if it becomes dirty or someone else pins it. */
diff --git a/doc/src/sgml/pgbuffercache.sgml b/doc/src/sgml/pgbuffercache.sgml
index 802a5112d77..83950ca5cce 100644
--- a/doc/src/sgml/pgbuffercache.sgml
+++ b/doc/src/sgml/pgbuffercache.sgml
@@ -31,8 +31,10 @@
   This module provides the <function>pg_buffercache_pages()</function>
   function (wrapped in the <structname>pg_buffercache</structname> view),
   the <function>pg_buffercache_summary()</function> function, the
-  <function>pg_buffercache_usage_counts()</function> function and
-  the <function>pg_buffercache_evict()</function> function.
+  <function>pg_buffercache_usage_counts()</function> function, the
+  <function>pg_buffercache_evict()</function>, the
+  <function>pg_buffercache_evict_relation()</function>, and the
+  <function>pg_buffercache_evict_all()</function> function.
  </para>
 
  <para>
@@ -65,6 +67,22 @@
   function is restricted to superusers only.
  </para>
 
+ <para>
+  The <function>pg_buffercache_evict_relation()</function> function allows all
+  buffers in the relation to be evicted from the buffer pool given a relation
+  identifier.  It returns how many buffers are evicted and flushed. Flushed buffers
+  do not have to be flushed by this function call, they might be flushed by
+  something else.  Use of this function is restricted to superusers only.
+ </para>
+
+ <para>
+  The <function>pg_buffercache_evict_all()</function> function allows all
+  buffers to be evicted in the buffer pool. It returns how many buffers are
+  evicted and flushed.  Flushed buffers do not have to be flushed by this
+  function call, they might be flushed by something else.  Use of this function
+  is restricted to superusers only.
+ </para>
+
  <sect2 id="pgbuffercache-pg-buffercache">
   <title>The <structname>pg_buffercache</structname> View</title>
 
@@ -378,6 +396,28 @@
   </para>
  </sect2>
 
+ <sect2 id="pgbuffercache-pg-buffercache-evict-relation">
+  <title>The <structname>pg_buffercache_evict_relation</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_evict_relation()</function> function is very similar
+   to <function>pg_buffercache_evict()</function> function.  The difference is that
+   <function>pg_buffercache_evict_relation()</function> takes a relation
+   identifier instead of buffer identifier.  Then, it tries to evict all
+   buffers in that relation.  The function is intended for developer testing only.
+  </para>
+ </sect2>
+
+ <sect2 id="pgbuffercache-pg-buffercache-evict-all">
+  <title>The <structname>pg_buffercache_evict_all</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_evict_all()</function> function is very similar
+   to <function>pg_buffercache_evict()</function> function.  The difference is,
+   the <function>pg_buffercache_evict_all()</function> does not take argument;
+   instead it tries to evict all buffers in the buffer pool.  The function is
+   intended for developer testing only.
+  </para>
+ </sect2>
+
 <sect2 id="pgbuffercache-sample-output">
   <title>Sample Output</title>
 
diff --git a/contrib/pg_buffercache/Makefile b/contrib/pg_buffercache/Makefile
index eae65ead9e5..2a33602537e 100644
--- a/contrib/pg_buffercache/Makefile
+++ b/contrib/pg_buffercache/Makefile
@@ -8,7 +8,8 @@ OBJS = \
 EXTENSION = pg_buffercache
 DATA = pg_buffercache--1.2.sql pg_buffercache--1.2--1.3.sql \
 	pg_buffercache--1.1--1.2.sql pg_buffercache--1.0--1.1.sql \
-	pg_buffercache--1.3--1.4.sql pg_buffercache--1.4--1.5.sql
+	pg_buffercache--1.3--1.4.sql pg_buffercache--1.4--1.5.sql \
+	pg_buffercache--1.5--1.6.sql
 PGFILEDESC = "pg_buffercache - monitoring of shared buffer cache in real-time"
 
 REGRESS = pg_buffercache
diff --git a/contrib/pg_buffercache/meson.build b/contrib/pg_buffercache/meson.build
index 12d1fe48717..9b2e9393410 100644
--- a/contrib/pg_buffercache/meson.build
+++ b/contrib/pg_buffercache/meson.build
@@ -23,6 +23,7 @@ install_data(
   'pg_buffercache--1.2.sql',
   'pg_buffercache--1.3--1.4.sql',
   'pg_buffercache--1.4--1.5.sql',
+  'pg_buffercache--1.5--1.6.sql',
   'pg_buffercache.control',
   kwargs: contrib_data_args,
 )
diff --git a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
new file mode 100644
index 00000000000..3a02d56a89f
--- /dev/null
+++ b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
@@ -0,0 +1,15 @@
+\echo Use "ALTER EXTENSION pg_buffercache UPDATE TO '1.6'" to load this file. \quit
+
+CREATE FUNCTION pg_buffercache_evict_relation(
+    IN regclass,
+    IN fork text default 'main',
+    OUT buffers_evicted int4,
+    OUT buffers_flushed int4)
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict_relation'
+LANGUAGE C PARALLEL SAFE VOLATILE;
+
+CREATE FUNCTION pg_buffercache_evict_all(
+    OUT buffers_evicted int4,
+    OUT buffers_flushed int4)
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict_all'
+LANGUAGE C PARALLEL SAFE VOLATILE;
diff --git a/contrib/pg_buffercache/pg_buffercache.control b/contrib/pg_buffercache/pg_buffercache.control
index 5ee875f77dd..b030ba3a6fa 100644
--- a/contrib/pg_buffercache/pg_buffercache.control
+++ b/contrib/pg_buffercache/pg_buffercache.control
@@ -1,5 +1,5 @@
 # pg_buffercache extension
 comment = 'examine the shared buffer cache'
-default_version = '1.5'
+default_version = '1.6'
 module_pathname = '$libdir/pg_buffercache'
 relocatable = true
diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index 3ae0a018e10..9932a19735a 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -9,16 +9,21 @@
 #include "postgres.h"
 
 #include "access/htup_details.h"
+#include "access/relation.h"
 #include "catalog/pg_type.h"
 #include "funcapi.h"
 #include "storage/buf_internals.h"
 #include "storage/bufmgr.h"
+#include "utils/builtins.h"
+#include "utils/rel.h"
 
 
 #define NUM_BUFFERCACHE_PAGES_MIN_ELEM	8
 #define NUM_BUFFERCACHE_PAGES_ELEM	9
 #define NUM_BUFFERCACHE_SUMMARY_ELEM 5
 #define NUM_BUFFERCACHE_USAGE_COUNTS_ELEM 4
+#define NUM_BUFFERCACHE_EVICT_RELATION_ELEM 2
+#define NUM_BUFFERCACHE_EVICT_ALL_ELEM 2
 
 PG_MODULE_MAGIC;
 
@@ -64,6 +69,8 @@ PG_FUNCTION_INFO_V1(pg_buffercache_pages);
 PG_FUNCTION_INFO_V1(pg_buffercache_summary);
 PG_FUNCTION_INFO_V1(pg_buffercache_usage_counts);
 PG_FUNCTION_INFO_V1(pg_buffercache_evict);
+PG_FUNCTION_INFO_V1(pg_buffercache_evict_relation);
+PG_FUNCTION_INFO_V1(pg_buffercache_evict_all);
 
 Datum
 pg_buffercache_pages(PG_FUNCTION_ARGS)
@@ -356,6 +363,7 @@ Datum
 pg_buffercache_evict(PG_FUNCTION_ARGS)
 {
 	Buffer		buf = PG_GETARG_INT32(0);
+	bool		flushed;
 
 	if (!superuser())
 		ereport(ERROR,
@@ -365,5 +373,126 @@ pg_buffercache_evict(PG_FUNCTION_ARGS)
 	if (buf < 1 || buf > NBuffers)
 		elog(ERROR, "bad buffer ID: %d", buf);
 
-	PG_RETURN_BOOL(EvictUnpinnedBuffer(buf));
+	PG_RETURN_BOOL(EvictUnpinnedBuffer(buf, 0, &flushed));
+}
+
+/*
+ * Try to evict specified relation.
+ */
+Datum
+pg_buffercache_evict_relation(PG_FUNCTION_ARGS)
+{
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_EVICT_RELATION_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_EVICT_RELATION_ELEM] = {0};
+
+	Oid			relOid;
+	Relation	rel;
+	int32		buffers_evicted = 0;
+	int32		buffers_flushed = 0;
+	bool		flushed;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use pg_buffercache_evict function")));
+
+	if (PG_ARGISNULL(0))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relation cannot be null")));
+
+
+	relOid = PG_GETARG_OID(0);
+
+	/* Open relation. */
+	rel = relation_open(relOid, AccessShareLock);
+
+	if (RelationUsesLocalBuffers(rel))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relation uses local buffers,"
+						"pg_buffercache_evict_relation function is intended to"
+						"used for shared buffers only")));
+
+	for (int buf = 1; buf < NBuffers; buf++)
+	{
+		uint32		buf_state;
+		BufferDesc *bufHdr = GetBufferDescriptor(buf - 1);
+
+		/*
+		 * No need to call UnlockBufHdr() if BufTagMatchesRelFileLocator()
+		 * returns true, EvictUnpinnedBuffer() will take care of it.
+		 */
+		buf_state = LockBufHdr(bufHdr);
+		if (BufTagMatchesRelFileLocator(&bufHdr->tag, &rel->rd_locator))
+		{
+			if (EvictUnpinnedBuffer(buf, buf_state, &flushed))
+				buffers_evicted++;
+			if (flushed)
+				buffers_flushed++;
+		}
+		else
+			UnlockBufHdr(bufHdr, buf_state);
+	}
+
+	/* Close relation, release lock. */
+	relation_close(rel, AccessShareLock);
+
+	values[0] = Int32GetDatum(buffers_evicted);
+	values[1] = Int32GetDatum(buffers_flushed);
+
+	/* Build and return the tuple. */
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
+
+
+/*
+ * Try to evict all shared buffers.
+ */
+Datum
+pg_buffercache_evict_all(PG_FUNCTION_ARGS)
+{
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_EVICT_ALL_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_EVICT_ALL_ELEM] = {0};
+
+	int32		buffers_evicted = 0;
+	int32		buffers_flushed = 0;
+	bool		flushed;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use pg_buffercache_evict_all function")));
+
+	for (int buf = 1; buf < NBuffers; buf++)
+	{
+		if (EvictUnpinnedBuffer(buf, 0, &flushed))
+			buffers_evicted++;
+		if (flushed)
+			buffers_flushed++;
+	}
+
+	values[0] = Int32GetDatum(buffers_evicted);
+	values[1] = Int32GetDatum(buffers_flushed);
+
+	/* Build and return the tuple. */
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
 }
-- 
2.47.2

v2-0002-Add-pg_buffercache_mark_dirty-_all-functions-for-.patchtext/x-patch; charset=US-ASCII; name=v2-0002-Add-pg_buffercache_mark_dirty-_all-functions-for-.patchDownload
From 56ef07473155d3e5f568dab710501bd92f8718ff Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@gmail.com>
Date: Wed, 25 Dec 2024 15:46:10 +0300
Subject: [PATCH v2 2/2] Add pg_buffercache_mark_dirty[_all]() functions for
 testing

This commit introduces two new functions for marking shared buffers as
dirty:

pg_buffercache_mark_dirty(): Marks a specific shared buffer as dirty.
pg_buffercache_mark_dirty_all(): Marks all shared buffers as dirty in a
single operation.

The pg_buffercache_mark_dirty_all() function provides an efficient
way to dirty the entire buffer pool (e.g., ~550ms vs. ~70ms for 16GB of
shared buffers), complementing pg_buffercache_mark_dirty() for more
granular control.

These functions are intended for developer testing and debugging
scenarios, enabling users to simulate various buffer pool states and
test write-back behavior. Both functions are superuser-only.
---
 src/include/storage/bufmgr.h                  |  1 +
 src/backend/storage/buffer/bufmgr.c           | 62 +++++++++++++++++++
 doc/src/sgml/pgbuffercache.sgml               | 40 +++++++++++-
 .../pg_buffercache--1.5--1.6.sql              | 10 +++
 contrib/pg_buffercache/pg_buffercache_pages.c | 43 +++++++++++++
 5 files changed, 154 insertions(+), 2 deletions(-)

diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 16c92383969..fbd16440e55 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -298,6 +298,7 @@ extern void LimitAdditionalPins(uint32 *additional_pins);
 extern void LimitAdditionalLocalPins(uint32 *additional_pins);
 
 extern bool EvictUnpinnedBuffer(Buffer buf, uint32 buf_state, bool *flushed);
+extern bool MarkUnpinnedBufferDirty(Buffer buf);
 
 /* in buf_init.c */
 extern void BufferManagerShmemInit(void);
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 53bbdd92e53..e3c84a5342c 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -6106,3 +6106,65 @@ EvictUnpinnedBuffer(Buffer buf, uint32 buf_state, bool *flushed)
 
 	return result;
 }
+
+/*
+ * MarkUnpinnedBufferDirty
+ *
+ * This function is intended for testing/development use only!
+ *
+ * To succeed, the buffer must not be pinned on entry, so if the caller had a
+ * particular block in mind, it might already have been replaced by some other
+ * block by the time this function runs.  It's also unpinned on return, so the
+ * buffer might be occupied and flushed by the time control is returned.  This
+ * inherent raciness without other interlocking makes the function unsuitable
+ * for non-testing usage.
+ *
+ * Returns true if the buffer was not dirty and it has now been marked as
+ * dirty.  Returns false if it wasn't valid, if it couldn't be marked as dirty
+ * due to a pin, or if the buffer was already dirty.
+ */
+bool
+MarkUnpinnedBufferDirty(Buffer buf)
+{
+	BufferDesc *desc;
+	uint32		buf_state;
+	bool		result = false;
+
+	Assert(!BufferIsLocal(buf));
+
+	/* Make sure we can pin the buffer. */
+	ResourceOwnerEnlarge(CurrentResourceOwner);
+	ReservePrivateRefCountEntry();
+
+	desc = GetBufferDescriptor(buf - 1);
+
+	/* Lock the header and check if it's valid. */
+	buf_state = LockBufHdr(desc);
+	if ((buf_state & BM_VALID) == 0)
+	{
+		UnlockBufHdr(desc, buf_state);
+		return false;
+	}
+
+	/* Check that it's not pinned already. */
+	if (BUF_STATE_GET_REFCOUNT(buf_state) > 0)
+	{
+		UnlockBufHdr(desc, buf_state);
+		return false;
+	}
+
+	PinBuffer_Locked(desc);		/* releases spinlock */
+
+	/* If it was not already dirty, mark it as dirty. */
+	if (!(buf_state & BM_DIRTY))
+	{
+		LWLockAcquire(BufferDescriptorGetContentLock(desc), LW_EXCLUSIVE);
+		MarkBufferDirty(buf);
+		LWLockRelease(BufferDescriptorGetContentLock(desc));
+		result = true;
+	}
+
+	UnpinBuffer(desc);
+
+	return result;
+}
diff --git a/doc/src/sgml/pgbuffercache.sgml b/doc/src/sgml/pgbuffercache.sgml
index 83950ca5cce..b0c19d56283 100644
--- a/doc/src/sgml/pgbuffercache.sgml
+++ b/doc/src/sgml/pgbuffercache.sgml
@@ -33,8 +33,10 @@
   the <function>pg_buffercache_summary()</function> function, the
   <function>pg_buffercache_usage_counts()</function> function, the
   <function>pg_buffercache_evict()</function>, the
-  <function>pg_buffercache_evict_relation()</function>, and the
-  <function>pg_buffercache_evict_all()</function> function.
+  <function>pg_buffercache_evict_relation()</function>, the
+  <function>pg_buffercache_evict_all()</function>, the
+  <function>pg_buffercache_mark_dirty()</function> and the
+  <function>pg_buffercache_mark_dirty_all()</function> function.
  </para>
 
  <para>
@@ -83,6 +85,18 @@
   is restricted to superusers only.
  </para>
 
+ <para>
+  The <function>pg_buffercache_mark_dirty()</function> function allows a block
+  to be marked as dirty from the buffer pool given a buffer identifier.  Use of
+  this function is restricted to superusers only.
+ </para>
+
+ <para>
+  The <function>pg_buffercache_mark_dirty_all()</function> function tries to
+  mark all buffers dirty in the buffer pool. It returns how many buffers are
+  dirtied. Use of this function is restricted to superusers only.
+ </para>
+
  <sect2 id="pgbuffercache-pg-buffercache">
   <title>The <structname>pg_buffercache</structname> View</title>
 
@@ -418,6 +432,28 @@
   </para>
  </sect2>
 
+ <sect2 id="pgbuffercache-pg-buffercache-mark-dirty">
+  <title>The <structname>pg_buffercache_mark_dirty</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_mark_dirty()</function> function takes a buffer
+   identifier, as shown in the <structfield>bufferid</structfield> column of
+   the <structname>pg_buffercache</structname> view.  It returns true on success,
+   and false if the buffer wasn't valid or if it couldn't be marked as dirty
+   because it was pinned.  The function is intended for developer testing only.
+  </para>
+ </sect2>
+
+ <sect2 id="pgbuffercache-pg-buffercache-mark-dirty-all">
+  <title>The <structname>pg_buffercache_mark_dirty_all</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_mark_dirty_all()</function> function is very similar
+   to <function>pg_buffercache_mark_dirty()</function> function.  The difference is,
+   the <function>pg_buffercache_mark_dirty_all()</function> does not take argument;
+   instead it tries to mark all buffers dirty in the buffer pool.  The function is
+   intended for developer testing only.
+  </para>
+ </sect2>
+
 <sect2 id="pgbuffercache-sample-output">
   <title>Sample Output</title>
 
diff --git a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
index 3a02d56a89f..9aff57a8729 100644
--- a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
+++ b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
@@ -13,3 +13,13 @@ CREATE FUNCTION pg_buffercache_evict_all(
     OUT buffers_flushed int4)
 AS 'MODULE_PATHNAME', 'pg_buffercache_evict_all'
 LANGUAGE C PARALLEL SAFE VOLATILE;
+
+CREATE FUNCTION pg_buffercache_mark_dirty(IN int)
+RETURNS bool
+AS 'MODULE_PATHNAME', 'pg_buffercache_mark_dirty'
+LANGUAGE C PARALLEL SAFE VOLATILE STRICT;
+
+CREATE FUNCTION pg_buffercache_mark_dirty_all()
+RETURNS INT4
+AS 'MODULE_PATHNAME', 'pg_buffercache_mark_dirty_all'
+LANGUAGE C PARALLEL SAFE VOLATILE;
diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index 9932a19735a..d20dced2077 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -71,6 +71,8 @@ PG_FUNCTION_INFO_V1(pg_buffercache_usage_counts);
 PG_FUNCTION_INFO_V1(pg_buffercache_evict);
 PG_FUNCTION_INFO_V1(pg_buffercache_evict_relation);
 PG_FUNCTION_INFO_V1(pg_buffercache_evict_all);
+PG_FUNCTION_INFO_V1(pg_buffercache_mark_dirty);
+PG_FUNCTION_INFO_V1(pg_buffercache_mark_dirty_all);
 
 Datum
 pg_buffercache_pages(PG_FUNCTION_ARGS)
@@ -496,3 +498,44 @@ pg_buffercache_evict_all(PG_FUNCTION_ARGS)
 
 	PG_RETURN_DATUM(result);
 }
+
+/*
+ * Try to mark dirty a shared buffer.
+ */
+Datum
+pg_buffercache_mark_dirty(PG_FUNCTION_ARGS)
+{
+	Buffer		buf = PG_GETARG_INT32(0);
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use pg_buffercache_mark_dirty function")));
+
+	if (buf < 1 || buf > NBuffers)
+		elog(ERROR, "bad buffer ID: %d", buf);
+
+	PG_RETURN_BOOL(MarkUnpinnedBufferDirty(buf));
+}
+
+/*
+ * Try to mark dirty all shared buffers.
+ */
+Datum
+pg_buffercache_mark_dirty_all(PG_FUNCTION_ARGS)
+{
+	int32		buffers_dirtied = 0;
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use pg_buffercache_mark_dirty_all function")));
+
+	for (int buf = 1; buf < NBuffers; buf++)
+	{
+		if (MarkUnpinnedBufferDirty(buf))
+			buffers_dirtied++;
+	}
+
+	PG_RETURN_INT32(buffers_dirtied);
+}
-- 
2.47.2

#5Aidar Imamov
a.imamov@postgrespro.ru
In reply to: Nazir Bilal Yavuz (#4)
Re: Add pg_buffercache_evict_all() and pg_buffercache_mark_dirty[_all]() functions

Hi!

This is my first time trying out as a patch reviewer, and I don't claim
to be
an expert. I am very interested in the topic of buffer cache and would
like to
learn more about it.

I have reviewed the patches after they were
edited and I would like to share my thoughts on some points.

pg_buffercache_evict_all():

for (int buf = 1; buf < NBuffers; buf++)

Mb it would be more correct to use <= NBuffers?

I also did not fully understand what A. Freund was referring to.
However, we
cannot avoid blocking the buffer header, as we need to ensure that the
buffer
is not being pinned by anyone else. Perhaps it would be beneficial to
call
LockBufHdr() outside and check if BUT_STATE_GET_REFCOUNT == 0 before
calling
EvictUnpinnedBuffer(). This would help to prevent unnecessary calls to
EvictUnpinnedBuffer() itself, ResourceOwnerEnlarge() and
ReservePrivateRefCountEntry().

pg_buffercache_mark_dirty_all():

for (int buf = 1; buf < NBuffers; buf++)

Mb it would be more correct to use <= NBuffers?

pg_buffercache_evict_relation():

errmsg("must be superuser to use pg_buffercache_evict function")

'_relation' postfix got lost here

/* Open relation. */
rel = relation_open(relOid, AccessShareLock);

If I understand correctly, this function is meant to replicate the
behavior of
VACUUM FULL, but only for dirty relation pages. In this case, wouldn't
it be
necessary to obtain an exclusive access lock?

for (int buf = 1; buf < NBuffers; buf++)

Mb it would be more correct to use <= NBuffers?

/*
* No need to call UnlockBufHdr() if BufTagMatchesRelFileLocator()
* returns true, EvictUnpinnedBuffer() will take care of it.
*/
buf_state = LockBufHdr(bufHdr);
if (BufTagMatchesRelFileLocator(&bufHdr->tag, &rel->rd_locator))
{
if (EvictUnpinnedBuffer(buf, buf_state, &flushed))
buffers_evicted++;
if (flushed)
buffers_flushed++;
}

If you have previously acquired the exclusive access lock to the
relation,
you may want to consider replacing the order of the
BufTagMatchesRelFileLocator()
and LockBufHdr() calls for improved efficiency (as mentioned in the
comment on
the BufTagMatchesRelFileLocator() call in the DropRelationBuffers()
function in
bufmgr.c).
And it maybe useful also to add BUT_STATE_GET_REFCOUNT == 0 precheck
before calling
EvictUnpinnedBuffer()?

regards,
Aidar Imamov

#6Joseph Koshakow
koshy44@gmail.com
In reply to: Aidar Imamov (#5)
Re: Add pg_buffercache_evict_all() and pg_buffercache_mark_dirty[_all]() functions

Hi I am working with Aidar to give a review and I am also a beginner
reviewer.

From 813e5ec0da4c65970b4b1ce2ec2918e4652da9ab Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81(at)gmail(dot)com>
Date: Fri, 20 Dec 2024 14:06:47 +0300
Subject: [PATCH v1 1/2] Add pg_buffercache_evict_all() function for

testing

*/
bool
-EvictUnpinnedBuffer(Buffer buf)
+EvictUnpinnedBuffer(Buffer buf, bool *flushed)

I think you should update the comment above this function to include
details on the new `flushed` argument.

diff --git a/doc/src/sgml/pgbuffercache.sgml

b/doc/src/sgml/pgbuffercache.sgml

index 802a5112d77..83950ca5cce 100644
--- a/doc/src/sgml/pgbuffercache.sgml
+++ b/doc/src/sgml/pgbuffercache.sgml
@@ -31,8 +31,10 @@
This module provides the <function>pg_buffercache_pages()</function>
function (wrapped in the <structname>pg_buffercache</structname> view),
the <function>pg_buffercache_summary()</function> function, the
-  <function>pg_buffercache_usage_counts()</function> function and
-  the <function>pg_buffercache_evict()</function> function.
+  <function>pg_buffercache_usage_counts()</function> function, the
+  <function>pg_buffercache_evict()</function>, the
+  <function>pg_buffercache_evict_relation()</function>, and the
+  <function>pg_buffercache_evict_all()</function> function.

All the other functions have indexterm tags above this, should we add
indexterms for the new functions?

+ <sect2 id="pgbuffercache-pg-buffercache-evict-relation">
+  <title>The <structname>pg_buffercache_evict_relation</structname>

Function</title>

+  <para>
+   The <function>pg_buffercache_evict_relation()</function> function is

very similar

+ to <function>pg_buffercache_evict()</function> function. The

difference is that

+   <function>pg_buffercache_evict_relation()</function> takes a relation
+   identifier instead of buffer identifier.  Then, it tries to evict all
+   buffers in that relation.  The function is intended for developer

testing only.

+  </para>
+ </sect2>
+
+ <sect2 id="pgbuffercache-pg-buffercache-evict-all">
+  <title>The <structname>pg_buffercache_evict_all</structname>

Function</title>

+  <para>
+   The <function>pg_buffercache_evict_all()</function> function is very

similar

+ to <function>pg_buffercache_evict()</function> function. The

difference is,

+ the <function>pg_buffercache_evict_all()</function> does not take

argument;

+ instead it tries to evict all buffers in the buffer pool. The

function is

+   intended for developer testing only.
+  </para>
+ </sect2>

The other difference is that these new functions have an additional
output argument to indicate if the buffer was flushed which we probably
want to mention here.

Also I think it's more gramatically correct to say "the <fn-name>
function" or just "<fn-name>". A couple of times you say "<fn-name>
function" or "the <fn-name>".

Also I think the third to last sentence should end with "... does not
take **an** argument" or "... does not take argument**s**".

+CREATE FUNCTION pg_buffercache_evict_relation(
+    IN regclass,
+    IN fork text default 'main',
+    OUT buffers_evicted int4,
+    OUT buffers_flushed int4)
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict_relation'
+LANGUAGE C PARALLEL SAFE VOLATILE;
+
+CREATE FUNCTION pg_buffercache_evict_all(
+    OUT buffers_evicted int4,
+    OUT buffers_flushed int4)
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict_all'
+LANGUAGE C PARALLEL SAFE VOLATILE;

Does it make sense to also update pg_buffercache_evict to also return
a bool if the buffer was flushed? Or is that too much of a breaking
change?

+ /* Lock the header and check if it's valid. */
+ buf_state = LockBufHdr(desc);
+ if ((buf_state & BM_VALID) == 0)
+ {
+ UnlockBufHdr(desc, buf_state);
+ return false;
+ }

EvictUnpinnedBuffer first checks if the buffer is locked before
attempting to lock it. Do we need a similar check here?

/* Lock the header if it is not already locked. */
if (!buf_state)
buf_state = LockBufHdr(desc);

I don't think *flushed is necessarily accurate this way, as it might

detect

that it doesn't need to flush the data (via StartBufferIO() returning

false).

You are right. It seems there is no way to get that information
without editing the FlushBuffer(), right? And I think editing
FlushBuffer() is not worth it. So, I can either remove it or explain
in the docs that #buffers_flushed may not be completely accurate.

There's already a lot of caveats on EvictUnpinnedBuffer that it might
have unexpected results when run concurrently, so I think it's fine to
add one more.

Thanks,
Joe Koshakow

#7Aidar Imamov
a.imamov@postgrespro.ru
In reply to: Joseph Koshakow (#6)
Re: Add pg_buffercache_evict_all() and pg_buffercache_mark_dirty[_all]() functions

Joseph Koshakow 2025-03-21 01:25:

Hi I am working with Aidar to give a review and I am also a beginner
reviewer.

From 813e5ec0da4c65970b4b1ce2ec2918e4652da9ab Mon Sep 17 00:00:00

2001

From: Nazir Bilal Yavuz <byavuz81(at)gmail(dot)com>
Date: Fri, 20 Dec 2024 14:06:47 +0300
Subject: [PATCH v1 1/2] Add pg_buffercache_evict_all() function for

testing

*/
bool
-EvictUnpinnedBuffer(Buffer buf)
+EvictUnpinnedBuffer(Buffer buf, bool *flushed)

I think you should update the comment above this function to include
details on the new `flushed` argument.

diff --git a/doc/src/sgml/pgbuffercache.sgml

b/doc/src/sgml/pgbuffercache.sgml

index 802a5112d77..83950ca5cce 100644
--- a/doc/src/sgml/pgbuffercache.sgml
+++ b/doc/src/sgml/pgbuffercache.sgml
@@ -31,8 +31,10 @@
This module provides the

<function>pg_buffercache_pages()</function>

function (wrapped in the <structname>pg_buffercache</structname>

view),

the <function>pg_buffercache_summary()</function> function, the
-  <function>pg_buffercache_usage_counts()</function> function and
-  the <function>pg_buffercache_evict()</function> function.
+  <function>pg_buffercache_usage_counts()</function> function, the
+  <function>pg_buffercache_evict()</function>, the
+  <function>pg_buffercache_evict_relation()</function>, and the
+  <function>pg_buffercache_evict_all()</function> function.

All the other functions have indexterm tags above this, should we add
indexterms for the new functions?

+ <sect2 id="pgbuffercache-pg-buffercache-evict-relation">
+  <title>The <structname>pg_buffercache_evict_relation</structname>

Function</title>

+  <para>
+   The <function>pg_buffercache_evict_relation()</function>

function is very similar

+ to <function>pg_buffercache_evict()</function> function. The

difference is that

+ <function>pg_buffercache_evict_relation()</function> takes a

relation

+ identifier instead of buffer identifier. Then, it tries to

evict all

+ buffers in that relation. The function is intended for

developer testing only.

+  </para>
+ </sect2>
+
+ <sect2 id="pgbuffercache-pg-buffercache-evict-all">
+  <title>The <structname>pg_buffercache_evict_all</structname>

Function</title>

+  <para>
+   The <function>pg_buffercache_evict_all()</function> function is

very similar

+ to <function>pg_buffercache_evict()</function> function. The

difference is,

+ the <function>pg_buffercache_evict_all()</function> does not

take argument;

+ instead it tries to evict all buffers in the buffer pool. The

function is

+   intended for developer testing only.
+  </para>
+ </sect2>

The other difference is that these new functions have an additional
output argument to indicate if the buffer was flushed which we
probably
want to mention here.

Also I think it's more gramatically correct to say "the <fn-name>
function" or just "<fn-name>". A couple of times you say "<fn-name>
function" or "the <fn-name>".

Also I think the third to last sentence should end with "... does not
take **an** argument" or "... does not take argument**s**".

+CREATE FUNCTION pg_buffercache_evict_relation(
+    IN regclass,
+    IN fork text default 'main',
+    OUT buffers_evicted int4,
+    OUT buffers_flushed int4)
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict_relation'
+LANGUAGE C PARALLEL SAFE VOLATILE;
+
+CREATE FUNCTION pg_buffercache_evict_all(
+    OUT buffers_evicted int4,
+    OUT buffers_flushed int4)
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict_all'
+LANGUAGE C PARALLEL SAFE VOLATILE;

Does it make sense to also update pg_buffercache_evict to also return
a bool if the buffer was flushed? Or is that too much of a breaking
change?

+ /* Lock the header and check if it's valid. */
+ buf_state = LockBufHdr(desc);
+ if ((buf_state & BM_VALID) == 0)
+ {
+ UnlockBufHdr(desc, buf_state);
+ return false;
+ }

EvictUnpinnedBuffer first checks if the buffer is locked before
attempting to lock it. Do we need a similar check here?

/* Lock the header if it is not already locked. */
if (!buf_state)
buf_state = LockBufHdr(desc);

I don't think *flushed is necessarily accurate this way, as it

might detect

that it doesn't need to flush the data (via StartBufferIO()

returning false).

You are right. It seems there is no way to get that information
without editing the FlushBuffer(), right? And I think editing
FlushBuffer() is not worth it. So, I can either remove it or explain
in the docs that #buffers_flushed may not be completely accurate.

There's already a lot of caveats on EvictUnpinnedBuffer that it might
have unexpected results when run concurrently, so I think it's fine to
add one more.

Thanks,
Joe Koshakow

I agree with most of what Joseph said. However, I would like to add some
comments.

At the moment, the "flushed" flag essentially indicates whether the
buffer
was dirty at the time of eviction and it does not guarantee that it has
been
written to disk. Therefore, it would be better to rename the
buffers_flushed
column in the output of pg_buffer_cache_evict_all() and
pg_buffercache_evict_relation() functions to dirty_buffers mb? This
would
allow us to avoid the confusion that arises from the fact that not all
dirty
buffers could have actually been written to disk. In addition, this
would
remove the "flushed" parameter from the EvictUnpinnedBuffer() function.
Because if we explicitly call LockBufHdr() outside of
EvictUnpinnedBuffer(),
we can already know in advance whether the buffer is dirty or not.

The same applies to the suggestion to retrieve "flushed" count from the
pg_buffercache_evict() call. We cannot say this for certain, but we can
determine whether the buffer was dirty.

regards,
Aidar Imamov

#8Nazir Bilal Yavuz
byavuz81@gmail.com
In reply to: Aidar Imamov (#5)
Re: Add pg_buffercache_evict_all() and pg_buffercache_mark_dirty[_all]() functions

Hi,

On Wed, 19 Mar 2025 at 01:02, Aidar Imamov <a.imamov@postgrespro.ru> wrote:

Hi!

This is my first time trying out as a patch reviewer, and I don't claim
to be
an expert. I am very interested in the topic of buffer cache and would
like to
learn more about it.

Thank you so much for the review!

pg_buffercache_evict_all():

for (int buf = 1; buf < NBuffers; buf++)

Mb it would be more correct to use <= NBuffers?

Yes, done.

I also did not fully understand what A. Freund was referring to.
However, we
cannot avoid blocking the buffer header, as we need to ensure that the
buffer
is not being pinned by anyone else. Perhaps it would be beneficial to
call
LockBufHdr() outside and check if BUT_STATE_GET_REFCOUNT == 0 before
calling
EvictUnpinnedBuffer(). This would help to prevent unnecessary calls to
EvictUnpinnedBuffer() itself, ResourceOwnerEnlarge() and
ReservePrivateRefCountEntry().

I had an off-list talk with Andres and learned that
ResourceOwnerEnlarge() and ReservePrivateRefCountEntry() need to be
called before acquiring the buffer header lock. I introduced the
EvictUnpinnedBuffersFromSharedRelation() function for that [1].

pg_buffercache_mark_dirty_all():

for (int buf = 1; buf < NBuffers; buf++)

Mb it would be more correct to use <= NBuffers?

Done.

pg_buffercache_evict_relation():

errmsg("must be superuser to use pg_buffercache_evict function")

'_relation' postfix got lost here

Done.

/* Open relation. */
rel = relation_open(relOid, AccessShareLock);

If I understand correctly, this function is meant to replicate the
behavior of
VACUUM FULL, but only for dirty relation pages. In this case, wouldn't
it be
necessary to obtain an exclusive access lock?

I think VACUUM FULL does more things but I agree with you, obtaining
an exclusive access lock is the correct way.

for (int buf = 1; buf < NBuffers; buf++)

Mb it would be more correct to use <= NBuffers?

Done.

/*
* No need to call UnlockBufHdr() if BufTagMatchesRelFileLocator()
* returns true, EvictUnpinnedBuffer() will take care of it.
*/
buf_state = LockBufHdr(bufHdr);
if (BufTagMatchesRelFileLocator(&bufHdr->tag, &rel->rd_locator))
{
if (EvictUnpinnedBuffer(buf, buf_state, &flushed))
buffers_evicted++;
if (flushed)
buffers_flushed++;
}

If you have previously acquired the exclusive access lock to the
relation,
you may want to consider replacing the order of the
BufTagMatchesRelFileLocator()
and LockBufHdr() calls for improved efficiency (as mentioned in the
comment on
the BufTagMatchesRelFileLocator() call in the DropRelationBuffers()
function in
bufmgr.c).

I think you are right, we are basically copying FlushRelationBuffers()
to the contrib/pg_buffer_cache. Done.

And it maybe useful also to add BUT_STATE_GET_REFCOUNT == 0 precheck
before calling
EvictUnpinnedBuffer()?

I think we do not need to do this. I see EvictUnpinnedBuffer() as a
central place that checks all conditions. If we add this pre-check
then we should not do the same check in the EvictUnpinnedBuffer(),
creating this logic would add unnecessary complexity to
EvictUnpinnedBuffer().

I addressed these reviews in v3. I will share the patches in my next reply.

--
Regards,
Nazir Bilal Yavuz
Microsoft

#9Nazir Bilal Yavuz
byavuz81@gmail.com
In reply to: Joseph Koshakow (#6)
3 attachment(s)
Re: Add pg_buffercache_evict_all() and pg_buffercache_mark_dirty[_all]() functions

Hi,

On Fri, 21 Mar 2025 at 01:25, Joseph Koshakow <koshy44@gmail.com> wrote:

Hi I am working with Aidar to give a review and I am also a beginner
reviewer.

Thank you so much for the review!

From 813e5ec0da4c65970b4b1ce2ec2918e4652da9ab Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81(at)gmail(dot)com>
Date: Fri, 20 Dec 2024 14:06:47 +0300
Subject: [PATCH v1 1/2] Add pg_buffercache_evict_all() function for testing
*/
bool
-EvictUnpinnedBuffer(Buffer buf)
+EvictUnpinnedBuffer(Buffer buf, bool *flushed)

I think you should update the comment above this function to include
details on the new `flushed` argument.

Yes, done.

diff --git a/doc/src/sgml/pgbuffercache.sgml b/doc/src/sgml/pgbuffercache.sgml
index 802a5112d77..83950ca5cce 100644
--- a/doc/src/sgml/pgbuffercache.sgml
+++ b/doc/src/sgml/pgbuffercache.sgml
@@ -31,8 +31,10 @@
This module provides the <function>pg_buffercache_pages()</function>
function (wrapped in the <structname>pg_buffercache</structname> view),
the <function>pg_buffercache_summary()</function> function, the
-  <function>pg_buffercache_usage_counts()</function> function and
-  the <function>pg_buffercache_evict()</function> function.
+  <function>pg_buffercache_usage_counts()</function> function, the
+  <function>pg_buffercache_evict()</function>, the
+  <function>pg_buffercache_evict_relation()</function>, and the
+  <function>pg_buffercache_evict_all()</function> function.

All the other functions have indexterm tags above this, should we add
indexterms for the new functions?

I think so, done.

+ <sect2 id="pgbuffercache-pg-buffercache-evict-relation">
+  <title>The <structname>pg_buffercache_evict_relation</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_evict_relation()</function> function is very similar
+   to <function>pg_buffercache_evict()</function> function.  The difference is that
+   <function>pg_buffercache_evict_relation()</function> takes a relation
+   identifier instead of buffer identifier.  Then, it tries to evict all
+   buffers in that relation.  The function is intended for developer testing only.
+  </para>
+ </sect2>
+
+ <sect2 id="pgbuffercache-pg-buffercache-evict-all">
+  <title>The <structname>pg_buffercache_evict_all</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_evict_all()</function> function is very similar
+   to <function>pg_buffercache_evict()</function> function.  The difference is,
+   the <function>pg_buffercache_evict_all()</function> does not take argument;
+   instead it tries to evict all buffers in the buffer pool.  The function is
+   intended for developer testing only.
+  </para>
+ </sect2>

The other difference is that these new functions have an additional
output argument to indicate if the buffer was flushed which we probably
want to mention here.

You are right, done.

Also I think it's more gramatically correct to say "the <fn-name>
function" or just "<fn-name>". A couple of times you say "<fn-name>
function" or "the <fn-name>".

I choose "the <fn-name> function", done.

Also I think the third to last sentence should end with "... does not
take **an** argument" or "... does not take argument**s**".

I agree, done.

+CREATE FUNCTION pg_buffercache_evict_relation(
+    IN regclass,
+    IN fork text default 'main',
+    OUT buffers_evicted int4,
+    OUT buffers_flushed int4)
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict_relation'
+LANGUAGE C PARALLEL SAFE VOLATILE;
+
+CREATE FUNCTION pg_buffercache_evict_all(
+    OUT buffers_evicted int4,
+    OUT buffers_flushed int4)
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict_all'
+LANGUAGE C PARALLEL SAFE VOLATILE;

Does it make sense to also update pg_buffercache_evict to also return
a bool if the buffer was flushed? Or is that too much of a breaking
change?

I think it makes sense but I did that change in another patch (0002)
as this may need more discussion.

+ /* Lock the header and check if it's valid. */
+ buf_state = LockBufHdr(desc);
+ if ((buf_state & BM_VALID) == 0)
+ {
+ UnlockBufHdr(desc, buf_state);
+ return false;
+ }

EvictUnpinnedBuffer first checks if the buffer is locked before
attempting to lock it. Do we need a similar check here?

I do not think so, for now we do not call MarkUnpinnedBufferDirty()
while the buffer header is locked.

/* Lock the header if it is not already locked. */
if (!buf_state)
buf_state = LockBufHdr(desc);

I don't think *flushed is necessarily accurate this way, as it might detect
that it doesn't need to flush the data (via StartBufferIO() returning false).

You are right. It seems there is no way to get that information
without editing the FlushBuffer(), right? And I think editing
FlushBuffer() is not worth it. So, I can either remove it or explain
in the docs that #buffers_flushed may not be completely accurate.

There's already a lot of caveats on EvictUnpinnedBuffer that it might
have unexpected results when run concurrently, so I think it's fine to
add one more.

I agree.

v3 is attached, I addressed both you and Aidar's reviews in the v3.

--
Regards,
Nazir Bilal Yavuz
Microsoft

Attachments:

v3-0001-Add-pg_buffercache_evict_-relation-all-functions-.patchtext/x-patch; charset=US-ASCII; name=v3-0001-Add-pg_buffercache_evict_-relation-all-functions-.patchDownload
From 522d285af9e2294efe02f67c964e264f35e855c4 Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@gmail.com>
Date: Mon, 24 Mar 2025 13:52:04 +0300
Subject: [PATCH v3 1/3] Add pg_buffercache_evict_[relation | all]() functions
 for testing

pg_buffercache_evict_relation(): Evicts all shared buffers in a
relation at once.
pg_buffercache_evict_all(): Evicts all shared buffers at once.

Both functions provide mechanism to evict multiple shared buffers at
once. They are designed to address the inefficiency of repeatedly calling
pg_buffercache_evict() for each individual buffer, which can be
time-consuming when dealing with large shared buffer pools.
(e.g., ~790ms vs. ~270ms for 16GB of shared buffers).

These functions are intended for developer testing and debugging
purposes and are available to superusers only.

Author: Nazir Bilal Yavuz <byavuz81@gmail.com>
Reviewed-by: Andres Freund <andres@anarazel.de>
Reviewed-by: Aidar Imamov <a.imamov@postgrespro.ru>
Reviewed-by: Joseph Koshakow <koshy44@gmail.com>
Discussion: https://postgr.es/m/CAN55FZ0h_YoSqqutxV6DES1RW8ig6wcA8CR9rJk358YRMxZFmw%40mail.gmail.com
---
 src/include/storage/bufmgr.h                  |   3 +-
 src/backend/storage/buffer/bufmgr.c           |  83 ++++++++++++--
 doc/src/sgml/pgbuffercache.sgml               |  56 ++++++++-
 contrib/pg_buffercache/Makefile               |   3 +-
 contrib/pg_buffercache/meson.build            |   1 +
 .../pg_buffercache--1.5--1.6.sql              |  16 +++
 contrib/pg_buffercache/pg_buffercache.control |   2 +-
 contrib/pg_buffercache/pg_buffercache_pages.c | 108 ++++++++++++++++++
 8 files changed, 260 insertions(+), 12 deletions(-)
 create mode 100644 contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql

diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 538b890a51d..e068319b736 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -298,7 +298,8 @@ extern uint32 GetAdditionalLocalPinLimit(void);
 extern void LimitAdditionalPins(uint32 *additional_pins);
 extern void LimitAdditionalLocalPins(uint32 *additional_pins);
 
-extern bool EvictUnpinnedBuffer(Buffer buf);
+extern bool EvictUnpinnedBuffer(Buffer buf, uint32 buf_state, bool *flushed);
+extern void EvictUnpinnedBuffersFromSharedRelation(Relation rel, int32 *buffers_evicted, int32 *buffers_flushed);
 
 /* in buf_init.c */
 extern void BufferManagerShmemInit(void);
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 323382dcfa8..74cb1ef1b9f 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -6078,26 +6078,40 @@ ResOwnerPrintBufferPin(Datum res)
  * even by the same block.  This inherent raciness without other interlocking
  * makes the function unsuitable for non-testing usage.
  *
+ * flushed is set to true if the buffer is flushed but this does not
+ * necessarily mean that the buffer is flushed by us, it might be flushed by
+ * someone else.
+ *
  * Returns true if the buffer was valid and it has now been made invalid.
  * Returns false if it wasn't valid, if it couldn't be evicted due to a pin,
  * or if the buffer becomes dirty again while we're trying to write it out.
  */
 bool
-EvictUnpinnedBuffer(Buffer buf)
+EvictUnpinnedBuffer(Buffer buf, uint32 buf_state, bool *flushed)
 {
 	BufferDesc *desc;
-	uint32		buf_state;
 	bool		result;
 
-	/* Make sure we can pin the buffer. */
-	ResourceOwnerEnlarge(CurrentResourceOwner);
-	ReservePrivateRefCountEntry();
+	*flushed = false;
 
 	Assert(!BufferIsLocal(buf));
 	desc = GetBufferDescriptor(buf - 1);
 
-	/* Lock the header and check if it's valid. */
-	buf_state = LockBufHdr(desc);
+	/*
+	 * If the buffer is already locked, we assume that preparations to pinning
+	 * buffer are already done.
+	 */
+	if (!buf_state)
+	{
+		/* Make sure we can pin the buffer. */
+		ResourceOwnerEnlarge(CurrentResourceOwner);
+		ReservePrivateRefCountEntry();
+
+		/* Lock the header if it is not already locked. */
+		buf_state = LockBufHdr(desc);
+	}
+
+	/* Check if it's valid. */
 	if ((buf_state & BM_VALID) == 0)
 	{
 		UnlockBufHdr(desc, buf_state);
@@ -6119,6 +6133,7 @@ EvictUnpinnedBuffer(Buffer buf)
 		LWLockAcquire(BufferDescriptorGetContentLock(desc), LW_SHARED);
 		FlushBuffer(desc, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL);
 		LWLockRelease(BufferDescriptorGetContentLock(desc));
+		*flushed = true;
 	}
 
 	/* This will return false if it becomes dirty or someone else pins it. */
@@ -6128,3 +6143,57 @@ EvictUnpinnedBuffer(Buffer buf)
 
 	return result;
 }
+
+/*
+ * Try to evict all shared buffers in the relation by calling
+ * EvictUnpinnedBuffer() for all the shared buffers in the relation.
+ *
+ * We need this helper function because of the following reason.
+ * ReservePrivateRefCountEntry() needs to be called before acquiring the
+ * buffer header lock but ReservePrivateRefCountEntry() is static and it would
+ * be better to have it as static. Hence, it can't be called from outside of
+ * this file. This helper function is created to bypass that problem.
+ *
+ * buffers_evicted and buffers_flushed is set the total number of buffers
+ * evicted and flushed respectively.
+ *
+ * If the relation uses local buffers retun false, otherwise return true.
+ */
+void
+EvictUnpinnedBuffersFromSharedRelation(Relation rel, int32 *buffers_evicted, int32 *buffers_flushed)
+{
+	bool		flushed;
+
+	*buffers_evicted = *buffers_flushed = 0;
+
+	Assert(!RelationUsesLocalBuffers(rel));
+
+	for (int buf = 1; buf <= NBuffers; buf++)
+	{
+		uint32		buf_state = 0;
+		BufferDesc *bufHdr = GetBufferDescriptor(buf - 1);
+
+		/* An unlocked precheck should be safe and saves some cycles. */
+		if (!BufTagMatchesRelFileLocator(&bufHdr->tag, &rel->rd_locator))
+			continue;
+
+		/* Make sure we can pin the buffer. */
+		ResourceOwnerEnlarge(CurrentResourceOwner);
+		ReservePrivateRefCountEntry();
+
+		/*
+		 * No need to call UnlockBufHdr() if BufTagMatchesRelFileLocator()
+		 * returns true, EvictUnpinnedBuffer() will take care of it.
+		 */
+		buf_state = LockBufHdr(bufHdr);
+		if (BufTagMatchesRelFileLocator(&bufHdr->tag, &rel->rd_locator))
+		{
+			if (EvictUnpinnedBuffer(buf, buf_state, &flushed))
+				(*buffers_evicted)++;
+			if (flushed)
+				(*buffers_flushed)++;
+		}
+		else
+			UnlockBufHdr(bufHdr, buf_state);
+	}
+}
diff --git a/doc/src/sgml/pgbuffercache.sgml b/doc/src/sgml/pgbuffercache.sgml
index 802a5112d77..d99aa979410 100644
--- a/doc/src/sgml/pgbuffercache.sgml
+++ b/doc/src/sgml/pgbuffercache.sgml
@@ -27,12 +27,22 @@
   <primary>pg_buffercache_evict</primary>
  </indexterm>
 
+ <indexterm>
+  <primary>pg_buffercache_evict_relation</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>pg_buffercache_evict_all</primary>
+ </indexterm>
+
  <para>
   This module provides the <function>pg_buffercache_pages()</function>
   function (wrapped in the <structname>pg_buffercache</structname> view),
   the <function>pg_buffercache_summary()</function> function, the
-  <function>pg_buffercache_usage_counts()</function> function and
-  the <function>pg_buffercache_evict()</function> function.
+  <function>pg_buffercache_usage_counts()</function> function, the
+  <function>pg_buffercache_evict()</function>, the
+  <function>pg_buffercache_evict_relation()</function> function and the
+  <function>pg_buffercache_evict_all()</function> function.
  </para>
 
  <para>
@@ -65,6 +75,18 @@
   function is restricted to superusers only.
  </para>
 
+ <para>
+  The <function>pg_buffercache_evict_relation()</function> function allows all
+  shared buffers in the relation to be evicted from the buffer pool given a
+  relation identifier.  Use of this function is restricted to superusers only.
+ </para>
+
+ <para>
+  The <function>pg_buffercache_evict_all()</function> function allows all
+  shared buffers to be evicted in the buffer pool.  Use of this function is
+  restricted to superusers only.
+ </para>
+
  <sect2 id="pgbuffercache-pg-buffercache">
   <title>The <structname>pg_buffercache</structname> View</title>
 
@@ -378,6 +400,36 @@
   </para>
  </sect2>
 
+ <sect2 id="pgbuffercache-pg-buffercache-evict-relation">
+  <title>The <structname>pg_buffercache_evict_relation</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_evict_relation()</function> function is very
+   similar to the <function>pg_buffercache_evict()</function> function.  The
+   difference is that the <function>pg_buffercache_evict_relation()</function>
+   takes a relation identifier instead of buffer identifier.  Then, it tries
+   to evict all buffers in that relation.  It returns the number of evicted and
+   flushed buffers.  Flushed buffers aren't necessarily flushed by us, they
+   might be flushed by someone else.  The result is immediately out of date
+   upon return, as the buffer might become valid again at any time due to
+   concurrent activity.  The function is intended for developer testing only.
+  </para>
+ </sect2>
+
+ <sect2 id="pgbuffercache-pg-buffercache-evict-all">
+  <title>The <structname>pg_buffercache_evict_all</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_evict_all()</function> function is very
+   similar to the <function>pg_buffercache_evict()</function> function.  The
+   difference is, the <function>pg_buffercache_evict_all()</function> function
+   does not take an argument; instead it tries to evict all buffers in the
+   buffer pool.  It returns the number of evicted and flushed buffers.
+   Flushed buffers aren't necessarily flushed by us, they might be flushed by
+   someone else.  The result is immediately out of date upon return, as the
+   buffer might become valid again at any time due to concurrent activity.
+   The function is intended for developer testing only.
+  </para>
+ </sect2>
+
 <sect2 id="pgbuffercache-sample-output">
   <title>Sample Output</title>
 
diff --git a/contrib/pg_buffercache/Makefile b/contrib/pg_buffercache/Makefile
index eae65ead9e5..2a33602537e 100644
--- a/contrib/pg_buffercache/Makefile
+++ b/contrib/pg_buffercache/Makefile
@@ -8,7 +8,8 @@ OBJS = \
 EXTENSION = pg_buffercache
 DATA = pg_buffercache--1.2.sql pg_buffercache--1.2--1.3.sql \
 	pg_buffercache--1.1--1.2.sql pg_buffercache--1.0--1.1.sql \
-	pg_buffercache--1.3--1.4.sql pg_buffercache--1.4--1.5.sql
+	pg_buffercache--1.3--1.4.sql pg_buffercache--1.4--1.5.sql \
+	pg_buffercache--1.5--1.6.sql
 PGFILEDESC = "pg_buffercache - monitoring of shared buffer cache in real-time"
 
 REGRESS = pg_buffercache
diff --git a/contrib/pg_buffercache/meson.build b/contrib/pg_buffercache/meson.build
index 12d1fe48717..9b2e9393410 100644
--- a/contrib/pg_buffercache/meson.build
+++ b/contrib/pg_buffercache/meson.build
@@ -23,6 +23,7 @@ install_data(
   'pg_buffercache--1.2.sql',
   'pg_buffercache--1.3--1.4.sql',
   'pg_buffercache--1.4--1.5.sql',
+  'pg_buffercache--1.5--1.6.sql',
   'pg_buffercache.control',
   kwargs: contrib_data_args,
 )
diff --git a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
new file mode 100644
index 00000000000..2494a0a19b1
--- /dev/null
+++ b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
@@ -0,0 +1,16 @@
+\echo Use "ALTER EXTENSION pg_buffercache UPDATE TO '1.6'" to load this file. \quit
+
+CREATE FUNCTION pg_buffercache_evict_relation(
+    IN regclass,
+    OUT buffers_evicted int4,
+    OUT buffers_flushed int4)
+RETURNS record
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict_relation'
+LANGUAGE C PARALLEL SAFE VOLATILE STRICT;
+
+CREATE FUNCTION pg_buffercache_evict_all(
+    OUT buffers_evicted int4,
+    OUT buffers_flushed int4)
+RETURNS record
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict_all'
+LANGUAGE C PARALLEL SAFE VOLATILE;
diff --git a/contrib/pg_buffercache/pg_buffercache.control b/contrib/pg_buffercache/pg_buffercache.control
index 5ee875f77dd..b030ba3a6fa 100644
--- a/contrib/pg_buffercache/pg_buffercache.control
+++ b/contrib/pg_buffercache/pg_buffercache.control
@@ -1,5 +1,5 @@
 # pg_buffercache extension
 comment = 'examine the shared buffer cache'
-default_version = '1.5'
+default_version = '1.6'
 module_pathname = '$libdir/pg_buffercache'
 relocatable = true
diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index 3ae0a018e10..7d3bb50b942 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -9,16 +9,21 @@
 #include "postgres.h"
 
 #include "access/htup_details.h"
+#include "access/relation.h"
 #include "catalog/pg_type.h"
 #include "funcapi.h"
 #include "storage/buf_internals.h"
 #include "storage/bufmgr.h"
+#include "utils/builtins.h"
+#include "utils/rel.h"
 
 
 #define NUM_BUFFERCACHE_PAGES_MIN_ELEM	8
 #define NUM_BUFFERCACHE_PAGES_ELEM	9
 #define NUM_BUFFERCACHE_SUMMARY_ELEM 5
 #define NUM_BUFFERCACHE_USAGE_COUNTS_ELEM 4
+#define NUM_BUFFERCACHE_EVICT_RELATION_ELEM 2
+#define NUM_BUFFERCACHE_EVICT_ALL_ELEM 2
 
 PG_MODULE_MAGIC;
 
@@ -64,6 +69,8 @@ PG_FUNCTION_INFO_V1(pg_buffercache_pages);
 PG_FUNCTION_INFO_V1(pg_buffercache_summary);
 PG_FUNCTION_INFO_V1(pg_buffercache_usage_counts);
 PG_FUNCTION_INFO_V1(pg_buffercache_evict);
+PG_FUNCTION_INFO_V1(pg_buffercache_evict_relation);
+PG_FUNCTION_INFO_V1(pg_buffercache_evict_all);
 
 Datum
 pg_buffercache_pages(PG_FUNCTION_ARGS)
@@ -367,3 +374,104 @@ pg_buffercache_evict(PG_FUNCTION_ARGS)
 
 	PG_RETURN_BOOL(EvictUnpinnedBuffer(buf));
 }
+
+/*
+ * Try to evict specified relation.
+ */
+Datum
+pg_buffercache_evict_relation(PG_FUNCTION_ARGS)
+{
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_EVICT_RELATION_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_EVICT_RELATION_ELEM] = {0};
+
+	Oid			relOid;
+	Relation	rel;
+	int32		buffers_evicted = 0;
+	int32		buffers_flushed = 0;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use pg_buffercache_evict_relation function")));
+
+	if (PG_ARGISNULL(0))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relation cannot be null")));
+
+
+	relOid = PG_GETARG_OID(0);
+
+	/* Open relation. */
+	rel = relation_open(relOid, AccessExclusiveLock);
+
+	if (RelationUsesLocalBuffers(rel))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relation uses local buffers,"
+						"pg_buffercache_evict_relation function is intended to"
+						"used for shared buffers only")));
+
+	EvictUnpinnedBuffersFromSharedRelation(rel, &buffers_evicted, &buffers_flushed);
+
+	/* Close relation, release lock. */
+	relation_close(rel, AccessExclusiveLock);
+
+	values[0] = Int32GetDatum(buffers_evicted);
+	values[1] = Int32GetDatum(buffers_flushed);
+
+	/* Build and return the tuple. */
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
+
+
+/*
+ * Try to evict all shared buffers.
+ */
+Datum
+pg_buffercache_evict_all(PG_FUNCTION_ARGS)
+{
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_EVICT_ALL_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_EVICT_ALL_ELEM] = {0};
+
+	int32		buffers_evicted = 0;
+	int32		buffers_flushed = 0;
+	bool		flushed;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use pg_buffercache_evict_all function")));
+
+	for (int buf = 1; buf <= NBuffers; buf++)
+	{
+		if (EvictUnpinnedBuffer(buf, 0, &flushed))
+			buffers_evicted++;
+		if (flushed)
+			buffers_flushed++;
+	}
+
+	values[0] = Int32GetDatum(buffers_evicted);
+	values[1] = Int32GetDatum(buffers_flushed);
+
+	/* Build and return the tuple. */
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
-- 
2.47.2

v3-0002-Return-buffer-flushed-information-in-pg_buffercac.patchtext/x-patch; charset=US-ASCII; name=v3-0002-Return-buffer-flushed-information-in-pg_buffercac.patchDownload
From 683dc0d8e27f99ee0897fdbd33f58a1caf56bb80 Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@gmail.com>
Date: Mon, 24 Mar 2025 13:53:26 +0300
Subject: [PATCH v3 2/3] Return buffer flushed information in
 pg_buffercache_evict function

Prior commit added ability to get buffer flushed information from
EvictUnpinnedBuffer() function. Show this information in
pg_buffercache_evict() function too.

Author: Nazir Bilal Yavuz <byavuz81@gmail.com>
Suggested-by: Joseph Koshakow <koshy44@gmail.com>
Discussion: https://postgr.es/m/CAN55FZ0h_YoSqqutxV6DES1RW8ig6wcA8CR9rJk358YRMxZFmw%40mail.gmail.com
---
 doc/src/sgml/pgbuffercache.sgml               | 15 ++++++++------
 .../pg_buffercache--1.5--1.6.sql              |  9 +++++++++
 contrib/pg_buffercache/pg_buffercache_pages.c | 20 ++++++++++++++++++-
 3 files changed, 37 insertions(+), 7 deletions(-)

diff --git a/doc/src/sgml/pgbuffercache.sgml b/doc/src/sgml/pgbuffercache.sgml
index d99aa979410..681c74251d4 100644
--- a/doc/src/sgml/pgbuffercache.sgml
+++ b/doc/src/sgml/pgbuffercache.sgml
@@ -391,12 +391,15 @@
   <para>
    The <function>pg_buffercache_evict()</function> function takes a buffer
    identifier, as shown in the <structfield>bufferid</structfield> column of
-   the <structname>pg_buffercache</structname> view.  It returns true on success,
-   and false if the buffer wasn't valid, if it couldn't be evicted because it
-   was pinned, or if it became dirty again after an attempt to write it out.
-   The result is immediately out of date upon return, as the buffer might
-   become valid again at any time due to concurrent activity.  The function is
-   intended for developer testing only.
+   the <structname>pg_buffercache</structname> view.  It returns the
+   information about whether the buffer is evicted and flushed.  evicted
+   column is true on success, and false if the buffer wasn't valid, if it
+   couldn't be evicted because it was pinned, or if it became dirty again
+   after an attempt to write it out.  flushed column is true if the buffer is
+   flushed.  This does not necessarily mean that buffer is flushed by us, it
+   might be flushed by someone else.  The result is immediately out of date
+   upon return, as the buffer might become valid again at any time due to
+   concurrent activity. The function is intended for developer testing only.
   </para>
  </sect2>
 
diff --git a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
index 2494a0a19b1..15881f5b8fe 100644
--- a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
+++ b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
@@ -1,5 +1,14 @@
 \echo Use "ALTER EXTENSION pg_buffercache UPDATE TO '1.6'" to load this file. \quit
 
+DROP FUNCTION pg_buffercache_evict(integer);
+CREATE OR REPLACE FUNCTION pg_buffercache_evict(
+    IN int,
+    OUT evicted boolean,
+    OUT flushed boolean)
+RETURNS record
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict'
+LANGUAGE C PARALLEL SAFE VOLATILE STRICT;
+
 CREATE FUNCTION pg_buffercache_evict_relation(
     IN regclass,
     OUT buffers_evicted int4,
diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index 7d3bb50b942..77a80401525 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -22,6 +22,7 @@
 #define NUM_BUFFERCACHE_PAGES_ELEM	9
 #define NUM_BUFFERCACHE_SUMMARY_ELEM 5
 #define NUM_BUFFERCACHE_USAGE_COUNTS_ELEM 4
+#define NUM_BUFFERCACHE_EVICT_ELEM 2
 #define NUM_BUFFERCACHE_EVICT_RELATION_ELEM 2
 #define NUM_BUFFERCACHE_EVICT_ALL_ELEM 2
 
@@ -362,7 +363,17 @@ pg_buffercache_usage_counts(PG_FUNCTION_ARGS)
 Datum
 pg_buffercache_evict(PG_FUNCTION_ARGS)
 {
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_EVICT_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_EVICT_ELEM] = {0};
+
 	Buffer		buf = PG_GETARG_INT32(0);
+	bool		flushed;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
 
 	if (!superuser())
 		ereport(ERROR,
@@ -372,7 +383,14 @@ pg_buffercache_evict(PG_FUNCTION_ARGS)
 	if (buf < 1 || buf > NBuffers)
 		elog(ERROR, "bad buffer ID: %d", buf);
 
-	PG_RETURN_BOOL(EvictUnpinnedBuffer(buf));
+	values[0] = BoolGetDatum(EvictUnpinnedBuffer(buf, 0, &flushed));
+	values[1] = BoolGetDatum(flushed);
+
+	/* Build and return the tuple. */
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
 }
 
 /*
-- 
2.47.2

v3-0003-Add-pg_buffercache_mark_dirty-_all-functions-for-.patchtext/x-patch; charset=US-ASCII; name=v3-0003-Add-pg_buffercache_mark_dirty-_all-functions-for-.patchDownload
From 4c436cd2d474c8013cfcd85f999b98ece096dbd7 Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@gmail.com>
Date: Wed, 25 Dec 2024 15:46:10 +0300
Subject: [PATCH v3 3/3] Add pg_buffercache_mark_dirty[_all]() functions for
 testing

This commit introduces two new functions for marking shared buffers as
dirty:

pg_buffercache_mark_dirty(): Marks a specific shared buffer as dirty.
pg_buffercache_mark_dirty_all(): Marks all shared buffers as dirty in a
single operation.

The pg_buffercache_mark_dirty_all() function provides an efficient
way to dirty the entire buffer pool (e.g., ~550ms vs. ~70ms for 16GB of
shared buffers), complementing pg_buffercache_mark_dirty() for more
granular control.

These functions are intended for developer testing and debugging
scenarios, enabling users to simulate various buffer pool states and
test write-back behavior. Both functions are superuser-only.

Author: Nazir Bilal Yavuz <byavuz81@gmail.com>
Reviewed-by: Andres Freund <andres@anarazel.de>
Reviewed-by: Aidar Imamov <a.imamov@postgrespro.ru>
Reviewed-by: Joseph Koshakow <koshy44@gmail.com>
Discussion: https://postgr.es/m/CAN55FZ0h_YoSqqutxV6DES1RW8ig6wcA8CR9rJk358YRMxZFmw%40mail.gmail.com
---
 src/include/storage/bufmgr.h                  |  1 +
 src/backend/storage/buffer/bufmgr.c           | 62 +++++++++++++++++++
 doc/src/sgml/pgbuffercache.sgml               | 54 +++++++++++++++-
 .../pg_buffercache--1.5--1.6.sql              | 10 +++
 contrib/pg_buffercache/pg_buffercache_pages.c | 43 +++++++++++++
 5 files changed, 167 insertions(+), 3 deletions(-)

diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index e068319b736..ce046712405 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -300,6 +300,7 @@ extern void LimitAdditionalLocalPins(uint32 *additional_pins);
 
 extern bool EvictUnpinnedBuffer(Buffer buf, uint32 buf_state, bool *flushed);
 extern void EvictUnpinnedBuffersFromSharedRelation(Relation rel, int32 *buffers_evicted, int32 *buffers_flushed);
+extern bool MarkUnpinnedBufferDirty(Buffer buf);
 
 /* in buf_init.c */
 extern void BufferManagerShmemInit(void);
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 74cb1ef1b9f..52525953669 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -6197,3 +6197,65 @@ EvictUnpinnedBuffersFromSharedRelation(Relation rel, int32 *buffers_evicted, int
 			UnlockBufHdr(bufHdr, buf_state);
 	}
 }
+
+/*
+ * MarkUnpinnedBufferDirty
+ *
+ * This function is intended for testing/development use only!
+ *
+ * To succeed, the buffer must not be pinned on entry, so if the caller had a
+ * particular block in mind, it might already have been replaced by some other
+ * block by the time this function runs.  It's also unpinned on return, so the
+ * buffer might be occupied and flushed by the time control is returned.  This
+ * inherent raciness without other interlocking makes the function unsuitable
+ * for non-testing usage.
+ *
+ * Returns true if the buffer was not dirty and it has now been marked as
+ * dirty.  Returns false if it wasn't valid, if it couldn't be marked as dirty
+ * due to a pin, or if the buffer was already dirty.
+ */
+bool
+MarkUnpinnedBufferDirty(Buffer buf)
+{
+	BufferDesc *desc;
+	uint32		buf_state;
+	bool		result = false;
+
+	Assert(!BufferIsLocal(buf));
+
+	/* Make sure we can pin the buffer. */
+	ResourceOwnerEnlarge(CurrentResourceOwner);
+	ReservePrivateRefCountEntry();
+
+	desc = GetBufferDescriptor(buf - 1);
+
+	/* Lock the header and check if it's valid. */
+	buf_state = LockBufHdr(desc);
+	if ((buf_state & BM_VALID) == 0)
+	{
+		UnlockBufHdr(desc, buf_state);
+		return false;
+	}
+
+	/* Check that it's not pinned already. */
+	if (BUF_STATE_GET_REFCOUNT(buf_state) > 0)
+	{
+		UnlockBufHdr(desc, buf_state);
+		return false;
+	}
+
+	PinBuffer_Locked(desc);		/* releases spinlock */
+
+	/* If it was not already dirty, mark it as dirty. */
+	if (!(buf_state & BM_DIRTY))
+	{
+		LWLockAcquire(BufferDescriptorGetContentLock(desc), LW_EXCLUSIVE);
+		MarkBufferDirty(buf);
+		LWLockRelease(BufferDescriptorGetContentLock(desc));
+		result = true;
+	}
+
+	UnpinBuffer(desc);
+
+	return result;
+}
diff --git a/doc/src/sgml/pgbuffercache.sgml b/doc/src/sgml/pgbuffercache.sgml
index 681c74251d4..baf64e07b4e 100644
--- a/doc/src/sgml/pgbuffercache.sgml
+++ b/doc/src/sgml/pgbuffercache.sgml
@@ -35,14 +35,24 @@
   <primary>pg_buffercache_evict_all</primary>
  </indexterm>
 
+ <indexterm>
+  <primary>pg_buffercache_mark_dirty</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>pg_buffercache_mark_dirty_all</primary>
+ </indexterm>
+
  <para>
   This module provides the <function>pg_buffercache_pages()</function>
   function (wrapped in the <structname>pg_buffercache</structname> view),
   the <function>pg_buffercache_summary()</function> function, the
   <function>pg_buffercache_usage_counts()</function> function, the
-  <function>pg_buffercache_evict()</function>, the
-  <function>pg_buffercache_evict_relation()</function> function and the
-  <function>pg_buffercache_evict_all()</function> function.
+  <function>pg_buffercache_evict()</function> function, the
+  <function>pg_buffercache_evict_relation()</function> function, the
+  <function>pg_buffercache_evict_all()</function> function, the
+  <function>pg_buffercache_mark_dirty()</function> function and the
+  <function>pg_buffercache_mark_dirty_all()</function> function.
  </para>
 
  <para>
@@ -87,6 +97,18 @@
   restricted to superusers only.
  </para>
 
+ <para>
+  The <function>pg_buffercache_mark_dirty()</function> function allows a block
+  to be marked as dirty from the buffer pool given a buffer identifier.  Use of
+  this function is restricted to superusers only.
+ </para>
+
+ <para>
+  The <function>pg_buffercache_mark_dirty_all()</function> function tries to
+  mark all buffers dirty in the buffer pool.  Use of this function is
+  restricted to superusers only.
+ </para>
+
  <sect2 id="pgbuffercache-pg-buffercache">
   <title>The <structname>pg_buffercache</structname> View</title>
 
@@ -433,6 +455,32 @@
   </para>
  </sect2>
 
+ <sect2 id="pgbuffercache-pg-buffercache-mark-dirty">
+  <title>The <structname>pg_buffercache_mark_dirty</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_mark_dirty()</function> function takes a
+   buffer identifier, as shown in the <structfield>bufferid</structfield>
+   column of the <structname>pg_buffercache</structname> view.  It returns
+   true on success, and false if the buffer wasn't valid or if it couldn't be
+   marked as dirty because it was pinned.  The result is immediately out of
+   date upon return, as the buffer might become valid again at any time due to
+   concurrent activity.  The function is intended for developer testing only.
+  </para>
+ </sect2>
+
+ <sect2 id="pgbuffercache-pg-buffercache-mark-dirty-all">
+  <title>The <structname>pg_buffercache_mark_dirty_all</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_mark_dirty_all()</function> function is very
+   similar to the <function>pg_buffercache_mark_dirty()</function> function.
+   The difference is, the <function>pg_buffercache_mark_dirty_all()</function>
+   function does not take an argument; instead it tries to mark all buffers
+   dirty in the buffer pool.  The result is immediately out of date upon
+   return, as the buffer might become valid again at any time due to
+   concurrent activity.  The function is intended for developer testing only.
+  </para>
+ </sect2>
+
 <sect2 id="pgbuffercache-sample-output">
   <title>Sample Output</title>
 
diff --git a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
index 15881f5b8fe..9e7c3fb6a5f 100644
--- a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
+++ b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
@@ -23,3 +23,13 @@ CREATE FUNCTION pg_buffercache_evict_all(
 RETURNS record
 AS 'MODULE_PATHNAME', 'pg_buffercache_evict_all'
 LANGUAGE C PARALLEL SAFE VOLATILE;
+
+CREATE FUNCTION pg_buffercache_mark_dirty(IN int)
+RETURNS bool
+AS 'MODULE_PATHNAME', 'pg_buffercache_mark_dirty'
+LANGUAGE C PARALLEL SAFE VOLATILE STRICT;
+
+CREATE FUNCTION pg_buffercache_mark_dirty_all()
+RETURNS INT4
+AS 'MODULE_PATHNAME', 'pg_buffercache_mark_dirty_all'
+LANGUAGE C PARALLEL SAFE VOLATILE;
diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index 77a80401525..a4d82c14ff6 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -72,6 +72,8 @@ PG_FUNCTION_INFO_V1(pg_buffercache_usage_counts);
 PG_FUNCTION_INFO_V1(pg_buffercache_evict);
 PG_FUNCTION_INFO_V1(pg_buffercache_evict_relation);
 PG_FUNCTION_INFO_V1(pg_buffercache_evict_all);
+PG_FUNCTION_INFO_V1(pg_buffercache_mark_dirty);
+PG_FUNCTION_INFO_V1(pg_buffercache_mark_dirty_all);
 
 Datum
 pg_buffercache_pages(PG_FUNCTION_ARGS)
@@ -493,3 +495,44 @@ pg_buffercache_evict_all(PG_FUNCTION_ARGS)
 
 	PG_RETURN_DATUM(result);
 }
+
+/*
+ * Try to mark dirty a shared buffer.
+ */
+Datum
+pg_buffercache_mark_dirty(PG_FUNCTION_ARGS)
+{
+	Buffer		buf = PG_GETARG_INT32(0);
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use pg_buffercache_mark_dirty function")));
+
+	if (buf < 1 || buf > NBuffers)
+		elog(ERROR, "bad buffer ID: %d", buf);
+
+	PG_RETURN_BOOL(MarkUnpinnedBufferDirty(buf));
+}
+
+/*
+ * Try to mark dirty all shared buffers.
+ */
+Datum
+pg_buffercache_mark_dirty_all(PG_FUNCTION_ARGS)
+{
+	int32		buffers_dirtied = 0;
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use pg_buffercache_mark_dirty_all function")));
+
+	for (int buf = 1; buf <= NBuffers; buf++)
+	{
+		if (MarkUnpinnedBufferDirty(buf))
+			buffers_dirtied++;
+	}
+
+	PG_RETURN_INT32(buffers_dirtied);
+}
-- 
2.47.2

#10Nazir Bilal Yavuz
byavuz81@gmail.com
In reply to: Aidar Imamov (#7)
Re: Add pg_buffercache_evict_all() and pg_buffercache_mark_dirty[_all]() functions

Hi,

On Sun, 23 Mar 2025 at 22:16, Aidar Imamov <a.imamov@postgrespro.ru> wrote:

I agree with most of what Joseph said. However, I would like to add some
comments.

At the moment, the "flushed" flag essentially indicates whether the
buffer
was dirty at the time of eviction and it does not guarantee that it has
been
written to disk.

I think flushed means 'passing the buffer contents to the kernel' in
the Postgres context (as it is explained in the FlushBuffer()). We
know that flush has happened, we just do not know if the buffer is
flushed by us or someone else.

--
Regards,
Nazir Bilal Yavuz
Microsoft

#11Nazir Bilal Yavuz
byavuz81@gmail.com
In reply to: Nazir Bilal Yavuz (#9)
Re: Add pg_buffercache_evict_all() and pg_buffercache_mark_dirty[_all]() functions

Hi,

It seems that Aidar's email ended up as another thread [1]/messages/by-id/flat/76a550315baef9d7424b70144f1c6a2d@postgrespro.ru. I am
copy-pasting mail and answer here to keep the discussion in this
thread.

On Sun, 23 Mar 2025 at 22:16, Aidar Imamov <a.imamov@postgrespro.ru> wrote:

I agree with most of what Joseph said. However, I would like to add some
comments.

At the moment, the "flushed" flag essentially indicates whether the
buffer
was dirty at the time of eviction and it does not guarantee that it has
been
written to disk. Therefore, it would be better to rename the
buffers_flushed
column in the output of pg_buffer_cache_evict_all() and
pg_buffercache_evict_relation() functions to dirty_buffers mb? This
would
allow us to avoid the confusion that arises from the fact that not all
dirty
buffers could have actually been written to disk. In addition, this
would
remove the "flushed" parameter from the EvictUnpinnedBuffer() function.
Because if we explicitly call LockBufHdr() outside of
EvictUnpinnedBuffer(),
we can already know in advance whether the buffer is dirty or not.

The same applies to the suggestion to retrieve "flushed" count from the
pg_buffercache_evict() call. We cannot say this for certain, but we can
determine whether the buffer was dirty.

I think flushed means 'passing the buffer contents to the kernel' in
the Postgres context (as it is explained in the FlushBuffer()). We
know that flush has happened, we just do not know if the buffer is
flushed by us or someone else.

[1]: /messages/by-id/flat/76a550315baef9d7424b70144f1c6a2d@postgrespro.ru

--
Regards,
Nazir Bilal Yavuz
Microsoft

#12Aidar Imamov
a.imamov@postgrespro.ru
In reply to: Nazir Bilal Yavuz (#11)
Re: Add pg_buffercache_evict_all() and pg_buffercache_mark_dirty[_all]() functions

Hi!

I've been looking at the patches v3 and there are a few things I want to
talk
about.

EvictUnpinnedBuffer():
I think we should change the paragraph with "flushed" of the comment to
something more like this: "If the buffer was dirty at the time of the
call it
will be flushed by calling FlushBuffer() and if 'flushed' is not NULL
'*flushed'
will be set to true."

I also think it'd be a good idea to add null-checks for "flushed" before
dereferencing it:

*flushed = false;
*flushed = true;

If the (!buf_state) clause is entered then we assume that the header is
not locked.
Maybe edit the comment: "Lock the header if it is not already locked."
-> "Lock the header"?

if (!buf_state)
{
/* Make sure we can pin the buffer. */
ResourceOwnerEnlarge(CurrentResourceOwner);
ReservePrivateRefCountEntry();

/* Lock the header if it is not already locked. */
buf_state = LockBufHdr(desc);
}

EvictUnpinnedBuffersFromSharedRelation():
Maybe it will be more accurate to name the function as
EvictRelUnpinnedBuffers()?

I think the comment will seem more correct in the following manner:
"Try to evict all the shared buffers containing provided relation's
pages.

This function is intended for testing/development use only!

Before calling this function, it is important to acquire
AccessExclusiveLock on
the specified relation to avoid replacing the current block of this
relation with
another one during execution.

If not null, buffers_evicted and buffers_flushed are set to the total
number of
buffers evicted and flushed respectively."

I also think it'd be a good idea to add null-checks for
"buffers_evicted" and
"buffers_flushed" before dereferencing them:

*buffers_evicted = *buffers_flushed = 0;

I think we don't need to check this clause again if AccessExclusiveLock
was acquired
before function call. Don't we?

if (BufTagMatchesRelFileLocator(&bufHdr->tag, &rel->rd_locator))
{
if (EvictUnpinnedBuffer(buf, buf_state, &flushed))
(*buffers_evicted)++;
if (flushed)
(*buffers_flushed)++;
}

MarkUnpinnedBufferDirty():
I think the comment will seem more correct in the following manner:
"Try to mark provided shared buffer as dirty.

This function is intended for testing/development use only!

Same as EvictUnpinnedBuffer() but with MarkBufferDirty() call inside.

Returns true if the buffer was already dirty or it has successfully been
marked as
dirty."

And also I think the function should return true at the case when the
buffer was
already dirty. What do you think?

pg_buffercache_evict_relation():
"pg_buffercache_evict_relation function is intended to" - 'be' is missed
here.

pg_buffercache_mark_dirty():
Maybe edit the comment to: "Try to mark a shared buffer as dirty."?

Maybe edit the elog text to: "bad shared buffer ID" - just to clarify
the case when
provided buffer number is negative (local buffer number).

pg_buffercache_mark_dirty_all():
Maybe also edit the comment to: "Try to mark all the shared buffers as
dirty."?

bufmgr.h:
I think it might be a good idea to follow the Postgres formatting style
and move the
function's arguments to the next line if they exceed 80 characters.

regards,
Aidar Imamov

#13Nazir Bilal Yavuz
byavuz81@gmail.com
In reply to: Aidar Imamov (#12)
3 attachment(s)
Re: Add pg_buffercache_evict_all() and pg_buffercache_mark_dirty[_all]() functions

Hi,

On Fri, 28 Mar 2025 at 01:53, Aidar Imamov <a.imamov@postgrespro.ru> wrote:

Hi!

I've been looking at the patches v3 and there are a few things I want to
talk
about.

Thank you for looking into this!

EvictUnpinnedBuffer():
I think we should change the paragraph with "flushed" of the comment to
something more like this: "If the buffer was dirty at the time of the
call it
will be flushed by calling FlushBuffer() and if 'flushed' is not NULL
'*flushed'
will be set to true."

This is correct if the buffer header lock is acquired before calling
the EvictUnpinnedBuffer(). Otherwise, the buffer might be flushed
after calling the EvictUnpinnedBuffer() and before acquiring the
buffer header lock. I slightly edited this comment and also added a
comment for the buf_state variable:

* buf_state is used to understand if the buffer header lock is acquired
* before calling this function. If it has non-zero value, it is assumed that
* buffer header lock is acquired before calling this function. This is
* helpful for evicting buffers in the relation as the buffer header lock
* needs to be taken before calling this function in this case.
*
* *flushed is set to true if the buffer was dirty and has been flushed.
* However, this does not necessarily mean that we flushed the buffer, it
* could have been flushed by someone else.

I also think it'd be a good idea to add null-checks for "flushed" before
dereferencing it:

*flushed = false;
*flushed = true;

Assert check is added.

If the (!buf_state) clause is entered then we assume that the header is
not locked.
Maybe edit the comment: "Lock the header if it is not already locked."
-> "Lock the header"?

if (!buf_state)
{
/* Make sure we can pin the buffer. */
ResourceOwnerEnlarge(CurrentResourceOwner);
ReservePrivateRefCountEntry();

/* Lock the header if it is not already locked. */
buf_state = LockBufHdr(desc);
}

I think this is better, done.

EvictUnpinnedBuffersFromSharedRelation():
Maybe it will be more accurate to name the function as
EvictRelUnpinnedBuffers()?

I liked that, done.

I think the comment will seem more correct in the following manner:
"Try to evict all the shared buffers containing provided relation's
pages.

This function is intended for testing/development use only!

Before calling this function, it is important to acquire
AccessExclusiveLock on
the specified relation to avoid replacing the current block of this
relation with
another one during execution.

If not null, buffers_evicted and buffers_flushed are set to the total
number of
buffers evicted and flushed respectively."

I added all comments except the not null part, I also preserved the
explanation of why we need this function.

I also think it'd be a good idea to add null-checks for
"buffers_evicted" and
"buffers_flushed" before dereferencing them:

*buffers_evicted = *buffers_flushed = 0;

Assert check is added.

I think we don't need to check this clause again if AccessExclusiveLock
was acquired
before function call. Don't we?

if (BufTagMatchesRelFileLocator(&bufHdr->tag, &rel->rd_locator))
{
if (EvictUnpinnedBuffer(buf, buf_state, &flushed))
(*buffers_evicted)++;
if (flushed)
(*buffers_flushed)++;
}

I think we need it. Copy-pasting a comment from the DropRelationBuffers():

* We can make this a tad faster by prechecking the buffer tag before
* we attempt to lock the buffer; this saves a lot of lock
* acquisitions in typical cases. It should be safe because the
* caller must have AccessExclusiveLock on the relation, or some other
* reason to be certain that no one is loading new pages of the rel
* into the buffer pool. (Otherwise we might well miss such pages
* entirely.) Therefore, while the tag might be changing while we
* look at it, it can't be changing *to* a value we care about, only
* *away* from such a value. So false negatives are impossible, and
* false positives are safe because we'll recheck after getting the
* buffer lock.

MarkUnpinnedBufferDirty():
I think the comment will seem more correct in the following manner:
"Try to mark provided shared buffer as dirty.

This function is intended for testing/development use only!

Same as EvictUnpinnedBuffer() but with MarkBufferDirty() call inside.

Returns true if the buffer was already dirty or it has successfully been
marked as
dirty."

Done.

And also I think the function should return true at the case when the
buffer was
already dirty. What do you think?

I asked the same question in the first email and I still could not
decide but I will be applying this as it was not decided before.

pg_buffercache_evict_relation():
"pg_buffercache_evict_relation function is intended to" - 'be' is missed
here.

Done.

pg_buffercache_mark_dirty():
Maybe edit the comment to: "Try to mark a shared buffer as dirty."?

Done.

Maybe edit the elog text to: "bad shared buffer ID" - just to clarify
the case when
provided buffer number is negative (local buffer number).

Although I think your suggestion makes sense, I did not change this
since it is already implemented like that in the
pg_buffercache_evict().

pg_buffercache_mark_dirty_all():
Maybe also edit the comment to: "Try to mark all the shared buffers as
dirty."?

Done.

bufmgr.h:
I think it might be a good idea to follow the Postgres formatting style
and move the
function's arguments to the next line if they exceed 80 characters.

I thought that pgindent already does this, done.

v4 is attached.

--
Regards,
Nazir Bilal Yavuz
Microsoft

Attachments:

v4-0001-Add-pg_buffercache_evict_-relation-all-functions-.patchapplication/octet-stream; name=v4-0001-Add-pg_buffercache_evict_-relation-all-functions-.patchDownload
From 656443b51ec4c2d38db4c7264568363b90db773e Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@gmail.com>
Date: Mon, 24 Mar 2025 13:52:04 +0300
Subject: [PATCH v4 1/3] Add pg_buffercache_evict_[relation | all]() functions
 for testing

pg_buffercache_evict_relation(): Evicts all shared buffers in a
relation at once.
pg_buffercache_evict_all(): Evicts all shared buffers at once.

Both functions provide mechanism to evict multiple shared buffers at
once. They are designed to address the inefficiency of repeatedly calling
pg_buffercache_evict() for each individual buffer, which can be
time-consuming when dealing with large shared buffer pools.
(e.g., ~790ms vs. ~270ms for 16GB of shared buffers).

These functions are intended for developer testing and debugging
purposes and are available to superusers only.

Author: Nazir Bilal Yavuz <byavuz81@gmail.com>
Reviewed-by: Andres Freund <andres@anarazel.de>
Reviewed-by: Aidar Imamov <a.imamov@postgrespro.ru>
Reviewed-by: Joseph Koshakow <koshy44@gmail.com>
Discussion: https://postgr.es/m/CAN55FZ0h_YoSqqutxV6DES1RW8ig6wcA8CR9rJk358YRMxZFmw%40mail.gmail.com
---
 contrib/pg_buffercache/Makefile               |   3 +-
 contrib/pg_buffercache/meson.build            |   1 +
 .../pg_buffercache--1.5--1.6.sql              |  16 +++
 contrib/pg_buffercache/pg_buffercache.control |   2 +-
 contrib/pg_buffercache/pg_buffercache_pages.c | 108 ++++++++++++++++++
 doc/src/sgml/pgbuffercache.sgml               |  56 ++++++++-
 src/backend/storage/buffer/bufmgr.c           |  94 +++++++++++++--
 src/include/storage/bufmgr.h                  |   4 +-
 8 files changed, 272 insertions(+), 12 deletions(-)
 create mode 100644 contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql

diff --git a/contrib/pg_buffercache/Makefile b/contrib/pg_buffercache/Makefile
index eae65ead9e5..2a33602537e 100644
--- a/contrib/pg_buffercache/Makefile
+++ b/contrib/pg_buffercache/Makefile
@@ -8,7 +8,8 @@ OBJS = \
 EXTENSION = pg_buffercache
 DATA = pg_buffercache--1.2.sql pg_buffercache--1.2--1.3.sql \
 	pg_buffercache--1.1--1.2.sql pg_buffercache--1.0--1.1.sql \
-	pg_buffercache--1.3--1.4.sql pg_buffercache--1.4--1.5.sql
+	pg_buffercache--1.3--1.4.sql pg_buffercache--1.4--1.5.sql \
+	pg_buffercache--1.5--1.6.sql
 PGFILEDESC = "pg_buffercache - monitoring of shared buffer cache in real-time"
 
 REGRESS = pg_buffercache
diff --git a/contrib/pg_buffercache/meson.build b/contrib/pg_buffercache/meson.build
index 12d1fe48717..9b2e9393410 100644
--- a/contrib/pg_buffercache/meson.build
+++ b/contrib/pg_buffercache/meson.build
@@ -23,6 +23,7 @@ install_data(
   'pg_buffercache--1.2.sql',
   'pg_buffercache--1.3--1.4.sql',
   'pg_buffercache--1.4--1.5.sql',
+  'pg_buffercache--1.5--1.6.sql',
   'pg_buffercache.control',
   kwargs: contrib_data_args,
 )
diff --git a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
new file mode 100644
index 00000000000..2494a0a19b1
--- /dev/null
+++ b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
@@ -0,0 +1,16 @@
+\echo Use "ALTER EXTENSION pg_buffercache UPDATE TO '1.6'" to load this file. \quit
+
+CREATE FUNCTION pg_buffercache_evict_relation(
+    IN regclass,
+    OUT buffers_evicted int4,
+    OUT buffers_flushed int4)
+RETURNS record
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict_relation'
+LANGUAGE C PARALLEL SAFE VOLATILE STRICT;
+
+CREATE FUNCTION pg_buffercache_evict_all(
+    OUT buffers_evicted int4,
+    OUT buffers_flushed int4)
+RETURNS record
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict_all'
+LANGUAGE C PARALLEL SAFE VOLATILE;
diff --git a/contrib/pg_buffercache/pg_buffercache.control b/contrib/pg_buffercache/pg_buffercache.control
index 5ee875f77dd..b030ba3a6fa 100644
--- a/contrib/pg_buffercache/pg_buffercache.control
+++ b/contrib/pg_buffercache/pg_buffercache.control
@@ -1,5 +1,5 @@
 # pg_buffercache extension
 comment = 'examine the shared buffer cache'
-default_version = '1.5'
+default_version = '1.6'
 module_pathname = '$libdir/pg_buffercache'
 relocatable = true
diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index 62602af1775..6314ba653bc 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -9,16 +9,21 @@
 #include "postgres.h"
 
 #include "access/htup_details.h"
+#include "access/relation.h"
 #include "catalog/pg_type.h"
 #include "funcapi.h"
 #include "storage/buf_internals.h"
 #include "storage/bufmgr.h"
+#include "utils/builtins.h"
+#include "utils/rel.h"
 
 
 #define NUM_BUFFERCACHE_PAGES_MIN_ELEM	8
 #define NUM_BUFFERCACHE_PAGES_ELEM	9
 #define NUM_BUFFERCACHE_SUMMARY_ELEM 5
 #define NUM_BUFFERCACHE_USAGE_COUNTS_ELEM 4
+#define NUM_BUFFERCACHE_EVICT_RELATION_ELEM 2
+#define NUM_BUFFERCACHE_EVICT_ALL_ELEM 2
 
 PG_MODULE_MAGIC_EXT(
 					.name = "pg_buffercache",
@@ -67,6 +72,8 @@ PG_FUNCTION_INFO_V1(pg_buffercache_pages);
 PG_FUNCTION_INFO_V1(pg_buffercache_summary);
 PG_FUNCTION_INFO_V1(pg_buffercache_usage_counts);
 PG_FUNCTION_INFO_V1(pg_buffercache_evict);
+PG_FUNCTION_INFO_V1(pg_buffercache_evict_relation);
+PG_FUNCTION_INFO_V1(pg_buffercache_evict_all);
 
 Datum
 pg_buffercache_pages(PG_FUNCTION_ARGS)
@@ -370,3 +377,104 @@ pg_buffercache_evict(PG_FUNCTION_ARGS)
 
 	PG_RETURN_BOOL(EvictUnpinnedBuffer(buf));
 }
+
+/*
+ * Try to evict specified relation.
+ */
+Datum
+pg_buffercache_evict_relation(PG_FUNCTION_ARGS)
+{
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_EVICT_RELATION_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_EVICT_RELATION_ELEM] = {0};
+
+	Oid			relOid;
+	Relation	rel;
+	int32		buffers_evicted = 0;
+	int32		buffers_flushed = 0;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use pg_buffercache_evict_relation function")));
+
+	if (PG_ARGISNULL(0))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relation cannot be null")));
+
+
+	relOid = PG_GETARG_OID(0);
+
+	/* Open relation. */
+	rel = relation_open(relOid, AccessExclusiveLock);
+
+	if (RelationUsesLocalBuffers(rel))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relation uses local buffers,"
+						"pg_buffercache_evict_relation function is intended to"
+						"be used for shared buffers only")));
+
+	EvictRelUnpinnedBuffers(rel, &buffers_evicted, &buffers_flushed);
+
+	/* Close relation, release lock. */
+	relation_close(rel, AccessExclusiveLock);
+
+	values[0] = Int32GetDatum(buffers_evicted);
+	values[1] = Int32GetDatum(buffers_flushed);
+
+	/* Build and return the tuple. */
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
+
+
+/*
+ * Try to evict all shared buffers.
+ */
+Datum
+pg_buffercache_evict_all(PG_FUNCTION_ARGS)
+{
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_EVICT_ALL_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_EVICT_ALL_ELEM] = {0};
+
+	int32		buffers_evicted = 0;
+	int32		buffers_flushed = 0;
+	bool		flushed;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use pg_buffercache_evict_all function")));
+
+	for (int buf = 1; buf <= NBuffers; buf++)
+	{
+		if (EvictUnpinnedBuffer(buf, 0, &flushed))
+			buffers_evicted++;
+		if (flushed)
+			buffers_flushed++;
+	}
+
+	values[0] = Int32GetDatum(buffers_evicted);
+	values[1] = Int32GetDatum(buffers_flushed);
+
+	/* Build and return the tuple. */
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
diff --git a/doc/src/sgml/pgbuffercache.sgml b/doc/src/sgml/pgbuffercache.sgml
index 802a5112d77..d99aa979410 100644
--- a/doc/src/sgml/pgbuffercache.sgml
+++ b/doc/src/sgml/pgbuffercache.sgml
@@ -27,12 +27,22 @@
   <primary>pg_buffercache_evict</primary>
  </indexterm>
 
+ <indexterm>
+  <primary>pg_buffercache_evict_relation</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>pg_buffercache_evict_all</primary>
+ </indexterm>
+
  <para>
   This module provides the <function>pg_buffercache_pages()</function>
   function (wrapped in the <structname>pg_buffercache</structname> view),
   the <function>pg_buffercache_summary()</function> function, the
-  <function>pg_buffercache_usage_counts()</function> function and
-  the <function>pg_buffercache_evict()</function> function.
+  <function>pg_buffercache_usage_counts()</function> function, the
+  <function>pg_buffercache_evict()</function>, the
+  <function>pg_buffercache_evict_relation()</function> function and the
+  <function>pg_buffercache_evict_all()</function> function.
  </para>
 
  <para>
@@ -65,6 +75,18 @@
   function is restricted to superusers only.
  </para>
 
+ <para>
+  The <function>pg_buffercache_evict_relation()</function> function allows all
+  shared buffers in the relation to be evicted from the buffer pool given a
+  relation identifier.  Use of this function is restricted to superusers only.
+ </para>
+
+ <para>
+  The <function>pg_buffercache_evict_all()</function> function allows all
+  shared buffers to be evicted in the buffer pool.  Use of this function is
+  restricted to superusers only.
+ </para>
+
  <sect2 id="pgbuffercache-pg-buffercache">
   <title>The <structname>pg_buffercache</structname> View</title>
 
@@ -378,6 +400,36 @@
   </para>
  </sect2>
 
+ <sect2 id="pgbuffercache-pg-buffercache-evict-relation">
+  <title>The <structname>pg_buffercache_evict_relation</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_evict_relation()</function> function is very
+   similar to the <function>pg_buffercache_evict()</function> function.  The
+   difference is that the <function>pg_buffercache_evict_relation()</function>
+   takes a relation identifier instead of buffer identifier.  Then, it tries
+   to evict all buffers in that relation.  It returns the number of evicted and
+   flushed buffers.  Flushed buffers aren't necessarily flushed by us, they
+   might be flushed by someone else.  The result is immediately out of date
+   upon return, as the buffer might become valid again at any time due to
+   concurrent activity.  The function is intended for developer testing only.
+  </para>
+ </sect2>
+
+ <sect2 id="pgbuffercache-pg-buffercache-evict-all">
+  <title>The <structname>pg_buffercache_evict_all</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_evict_all()</function> function is very
+   similar to the <function>pg_buffercache_evict()</function> function.  The
+   difference is, the <function>pg_buffercache_evict_all()</function> function
+   does not take an argument; instead it tries to evict all buffers in the
+   buffer pool.  It returns the number of evicted and flushed buffers.
+   Flushed buffers aren't necessarily flushed by us, they might be flushed by
+   someone else.  The result is immediately out of date upon return, as the
+   buffer might become valid again at any time due to concurrent activity.
+   The function is intended for developer testing only.
+  </para>
+ </sect2>
+
 <sect2 id="pgbuffercache-sample-output">
   <title>Sample Output</title>
 
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 323382dcfa8..6105c6d2d73 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -6078,26 +6078,47 @@ ResOwnerPrintBufferPin(Datum res)
  * even by the same block.  This inherent raciness without other interlocking
  * makes the function unsuitable for non-testing usage.
  *
+ * buf_state is used to understand if the buffer header lock is acquired
+ * before calling this function. If it has non-zero value, it is assumed that
+ * buffer header lock is acquired before calling this function. This is
+ * helpful for evicting buffers in the relation as the buffer header lock
+ * needs to be taken before calling this function in this case.
+ *
+ * *flushed is set to true if the buffer was dirty and has been flushed.
+ * However, this does not necessarily mean that we flushed the buffer, it
+ * could have been flushed by someone else.
+ *
  * Returns true if the buffer was valid and it has now been made invalid.
  * Returns false if it wasn't valid, if it couldn't be evicted due to a pin,
  * or if the buffer becomes dirty again while we're trying to write it out.
  */
 bool
-EvictUnpinnedBuffer(Buffer buf)
+EvictUnpinnedBuffer(Buffer buf, uint32 buf_state, bool *flushed)
 {
 	BufferDesc *desc;
-	uint32		buf_state;
 	bool		result;
 
-	/* Make sure we can pin the buffer. */
-	ResourceOwnerEnlarge(CurrentResourceOwner);
-	ReservePrivateRefCountEntry();
+	Assert(flushed);
+	*flushed = false;
 
 	Assert(!BufferIsLocal(buf));
 	desc = GetBufferDescriptor(buf - 1);
 
-	/* Lock the header and check if it's valid. */
-	buf_state = LockBufHdr(desc);
+	/*
+	 * If the buffer is already locked, we assume that preparations to pinning
+	 * buffer are already done.
+	 */
+	if (!buf_state)
+	{
+		/* Make sure we can pin the buffer. */
+		ResourceOwnerEnlarge(CurrentResourceOwner);
+		ReservePrivateRefCountEntry();
+
+		/* Lock the header */
+		buf_state = LockBufHdr(desc);
+	}
+
+	/* Check if it's valid. */
 	if ((buf_state & BM_VALID) == 0)
 	{
 		UnlockBufHdr(desc, buf_state);
@@ -6119,6 +6140,7 @@ EvictUnpinnedBuffer(Buffer buf)
 		LWLockAcquire(BufferDescriptorGetContentLock(desc), LW_SHARED);
 		FlushBuffer(desc, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL);
 		LWLockRelease(BufferDescriptorGetContentLock(desc));
+		*flushed = true;
 	}
 
 	/* This will return false if it becomes dirty or someone else pins it. */
@@ -6128,3 +6150,61 @@ EvictUnpinnedBuffer(Buffer buf)
 
 	return result;
 }
+
+/*
+ * Try to evict all the shared buffers containing provided relation's pages.
+ *
+ * This function is intended for testing/development use only!
+ *
+ * We need this helper function because of the following reason.
+ * ReservePrivateRefCountEntry() needs to be called before acquiring the
+ * buffer header lock but ReservePrivateRefCountEntry() is static and it would
+ * be better to have it as static. Hence, it can't be called from outside of
+ * this file. This helper function is created to bypass that problem.
+ *
+ * Before calling this function, it is important to acquire
+ * AccessExclusiveLock on the specified relation to avoid replacing the
+ * current block of this relation with another one during execution.
+
+ * buffers_evicted and buffers_flushed are set the total number of buffers
+ * evicted and flushed respectively.
+ */
+void
+EvictRelUnpinnedBuffers(Relation rel, int32 *buffers_evicted, int32 *buffers_flushed)
+{
+	bool		flushed;
+
+	Assert(buffers_evicted && buffers_flushed);
+	*buffers_evicted = *buffers_flushed = 0;
+
+	Assert(!RelationUsesLocalBuffers(rel));
+
+	for (int buf = 1; buf <= NBuffers; buf++)
+	{
+		uint32		buf_state = 0;
+		BufferDesc *bufHdr = GetBufferDescriptor(buf - 1);
+
+		/* An unlocked precheck should be safe and saves some cycles. */
+		if (!BufTagMatchesRelFileLocator(&bufHdr->tag, &rel->rd_locator))
+			continue;
+
+		/* Make sure we can pin the buffer. */
+		ResourceOwnerEnlarge(CurrentResourceOwner);
+		ReservePrivateRefCountEntry();
+
+		/*
+		 * No need to call UnlockBufHdr() if BufTagMatchesRelFileLocator()
+		 * returns true, EvictUnpinnedBuffer() will take care of it.
+		 */
+		buf_state = LockBufHdr(bufHdr);
+		if (BufTagMatchesRelFileLocator(&bufHdr->tag, &rel->rd_locator))
+		{
+			if (EvictUnpinnedBuffer(buf, buf_state, &flushed))
+				(*buffers_evicted)++;
+			if (flushed)
+				(*buffers_flushed)++;
+		}
+		else
+			UnlockBufHdr(bufHdr, buf_state);
+	}
+}
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 538b890a51d..952c26c01c1 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -298,7 +298,9 @@ extern uint32 GetAdditionalLocalPinLimit(void);
 extern void LimitAdditionalPins(uint32 *additional_pins);
 extern void LimitAdditionalLocalPins(uint32 *additional_pins);
 
-extern bool EvictUnpinnedBuffer(Buffer buf);
+extern bool EvictUnpinnedBuffer(Buffer buf, uint32 buf_state, bool *flushed);
+extern void EvictRelUnpinnedBuffers(Relation rel, int32 *buffers_evicted,
+									int32 *buffers_flushed);
 
 /* in buf_init.c */
 extern void BufferManagerShmemInit(void);
-- 
2.43.0

v4-0002-Return-buffer-flushed-information-in-pg_buffercac.patchapplication/octet-stream; name=v4-0002-Return-buffer-flushed-information-in-pg_buffercac.patchDownload
From 53cb1188ac035a3504a2a9ac2f037087c07cca43 Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@gmail.com>
Date: Mon, 24 Mar 2025 13:53:26 +0300
Subject: [PATCH v4 2/3] Return buffer flushed information in
 pg_buffercache_evict function

Prior commit added ability to get buffer flushed information from
EvictUnpinnedBuffer() function. Show this information in
pg_buffercache_evict() function too.

Author: Nazir Bilal Yavuz <byavuz81@gmail.com>
Suggested-by: Joseph Koshakow <koshy44@gmail.com>
Discussion: https://postgr.es/m/CAN55FZ0h_YoSqqutxV6DES1RW8ig6wcA8CR9rJk358YRMxZFmw%40mail.gmail.com
---
 .../pg_buffercache--1.5--1.6.sql              |  9 +++++++++
 contrib/pg_buffercache/pg_buffercache_pages.c | 20 ++++++++++++++++++-
 doc/src/sgml/pgbuffercache.sgml               | 15 ++++++++------
 3 files changed, 37 insertions(+), 7 deletions(-)

diff --git a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
index 2494a0a19b1..15881f5b8fe 100644
--- a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
+++ b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
@@ -1,5 +1,14 @@
 \echo Use "ALTER EXTENSION pg_buffercache UPDATE TO '1.6'" to load this file. \quit
 
+DROP FUNCTION pg_buffercache_evict(integer);
+CREATE OR REPLACE FUNCTION pg_buffercache_evict(
+    IN int,
+    OUT evicted boolean,
+    OUT flushed boolean)
+RETURNS record
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict'
+LANGUAGE C PARALLEL SAFE VOLATILE STRICT;
+
 CREATE FUNCTION pg_buffercache_evict_relation(
     IN regclass,
     OUT buffers_evicted int4,
diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index 6314ba653bc..a76625307a7 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -22,6 +22,7 @@
 #define NUM_BUFFERCACHE_PAGES_ELEM	9
 #define NUM_BUFFERCACHE_SUMMARY_ELEM 5
 #define NUM_BUFFERCACHE_USAGE_COUNTS_ELEM 4
+#define NUM_BUFFERCACHE_EVICT_ELEM 2
 #define NUM_BUFFERCACHE_EVICT_RELATION_ELEM 2
 #define NUM_BUFFERCACHE_EVICT_ALL_ELEM 2
 
@@ -365,7 +366,17 @@ pg_buffercache_usage_counts(PG_FUNCTION_ARGS)
 Datum
 pg_buffercache_evict(PG_FUNCTION_ARGS)
 {
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_EVICT_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_EVICT_ELEM] = {0};
+
 	Buffer		buf = PG_GETARG_INT32(0);
+	bool		flushed;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
 
 	if (!superuser())
 		ereport(ERROR,
@@ -375,7 +386,14 @@ pg_buffercache_evict(PG_FUNCTION_ARGS)
 	if (buf < 1 || buf > NBuffers)
 		elog(ERROR, "bad buffer ID: %d", buf);
 
-	PG_RETURN_BOOL(EvictUnpinnedBuffer(buf));
+	values[0] = BoolGetDatum(EvictUnpinnedBuffer(buf, 0, &flushed));
+	values[1] = BoolGetDatum(flushed);
+
+	/* Build and return the tuple. */
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
 }
 
 /*
diff --git a/doc/src/sgml/pgbuffercache.sgml b/doc/src/sgml/pgbuffercache.sgml
index d99aa979410..681c74251d4 100644
--- a/doc/src/sgml/pgbuffercache.sgml
+++ b/doc/src/sgml/pgbuffercache.sgml
@@ -391,12 +391,15 @@
   <para>
    The <function>pg_buffercache_evict()</function> function takes a buffer
    identifier, as shown in the <structfield>bufferid</structfield> column of
-   the <structname>pg_buffercache</structname> view.  It returns true on success,
-   and false if the buffer wasn't valid, if it couldn't be evicted because it
-   was pinned, or if it became dirty again after an attempt to write it out.
-   The result is immediately out of date upon return, as the buffer might
-   become valid again at any time due to concurrent activity.  The function is
-   intended for developer testing only.
+   the <structname>pg_buffercache</structname> view.  It returns the
+   information about whether the buffer is evicted and flushed.  evicted
+   column is true on success, and false if the buffer wasn't valid, if it
+   couldn't be evicted because it was pinned, or if it became dirty again
+   after an attempt to write it out.  flushed column is true if the buffer is
+   flushed.  This does not necessarily mean that buffer is flushed by us, it
+   might be flushed by someone else.  The result is immediately out of date
+   upon return, as the buffer might become valid again at any time due to
+   concurrent activity. The function is intended for developer testing only.
   </para>
  </sect2>
 
-- 
2.43.0

v4-0003-Add-pg_buffercache_mark_dirty-_all-functions-for-.patchapplication/octet-stream; name=v4-0003-Add-pg_buffercache_mark_dirty-_all-functions-for-.patchDownload
From 22f90970ddd79ec9dd48b1ab3083e17e3d18f0cb Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@gmail.com>
Date: Wed, 25 Dec 2024 15:46:10 +0300
Subject: [PATCH v4 3/3] Add pg_buffercache_mark_dirty[_all]() functions for
 testing

This commit introduces two new functions for marking shared buffers as
dirty:

pg_buffercache_mark_dirty(): Marks a specific shared buffer as dirty.
pg_buffercache_mark_dirty_all(): Marks all shared buffers as dirty in a
single operation.

The pg_buffercache_mark_dirty_all() function provides an efficient
way to dirty the entire buffer pool (e.g., ~550ms vs. ~70ms for 16GB of
shared buffers), complementing pg_buffercache_mark_dirty() for more
granular control.

These functions are intended for developer testing and debugging
scenarios, enabling users to simulate various buffer pool states and
test write-back behavior. Both functions are superuser-only.

Author: Nazir Bilal Yavuz <byavuz81@gmail.com>
Reviewed-by: Andres Freund <andres@anarazel.de>
Reviewed-by: Aidar Imamov <a.imamov@postgrespro.ru>
Reviewed-by: Joseph Koshakow <koshy44@gmail.com>
Discussion: https://postgr.es/m/CAN55FZ0h_YoSqqutxV6DES1RW8ig6wcA8CR9rJk358YRMxZFmw%40mail.gmail.com
---
 .../pg_buffercache--1.5--1.6.sql              | 10 ++++
 contrib/pg_buffercache/pg_buffercache_pages.c | 43 +++++++++++++++
 doc/src/sgml/pgbuffercache.sgml               | 54 +++++++++++++++++--
 src/backend/storage/buffer/bufmgr.c           | 54 +++++++++++++++++++
 src/include/storage/bufmgr.h                  |  1 +
 5 files changed, 159 insertions(+), 3 deletions(-)

diff --git a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
index 15881f5b8fe..9e7c3fb6a5f 100644
--- a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
+++ b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
@@ -23,3 +23,13 @@ CREATE FUNCTION pg_buffercache_evict_all(
 RETURNS record
 AS 'MODULE_PATHNAME', 'pg_buffercache_evict_all'
 LANGUAGE C PARALLEL SAFE VOLATILE;
+
+CREATE FUNCTION pg_buffercache_mark_dirty(IN int)
+RETURNS bool
+AS 'MODULE_PATHNAME', 'pg_buffercache_mark_dirty'
+LANGUAGE C PARALLEL SAFE VOLATILE STRICT;
+
+CREATE FUNCTION pg_buffercache_mark_dirty_all()
+RETURNS INT4
+AS 'MODULE_PATHNAME', 'pg_buffercache_mark_dirty_all'
+LANGUAGE C PARALLEL SAFE VOLATILE;
diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index a76625307a7..a02067ec162 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -75,6 +75,8 @@ PG_FUNCTION_INFO_V1(pg_buffercache_usage_counts);
 PG_FUNCTION_INFO_V1(pg_buffercache_evict);
 PG_FUNCTION_INFO_V1(pg_buffercache_evict_relation);
 PG_FUNCTION_INFO_V1(pg_buffercache_evict_all);
+PG_FUNCTION_INFO_V1(pg_buffercache_mark_dirty);
+PG_FUNCTION_INFO_V1(pg_buffercache_mark_dirty_all);
 
 Datum
 pg_buffercache_pages(PG_FUNCTION_ARGS)
@@ -496,3 +498,44 @@ pg_buffercache_evict_all(PG_FUNCTION_ARGS)
 
 	PG_RETURN_DATUM(result);
 }
+
+/*
+ * Try to mark a shared buffer as dirty.
+ */
+Datum
+pg_buffercache_mark_dirty(PG_FUNCTION_ARGS)
+{
+	Buffer		buf = PG_GETARG_INT32(0);
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use pg_buffercache_mark_dirty function")));
+
+	if (buf < 1 || buf > NBuffers)
+		elog(ERROR, "bad buffer ID: %d", buf);
+
+	PG_RETURN_BOOL(MarkUnpinnedBufferDirty(buf));
+}
+
+/*
+ * Try to mark all the shared buffers as dirty.
+ */
+Datum
+pg_buffercache_mark_dirty_all(PG_FUNCTION_ARGS)
+{
+	int32		buffers_dirtied = 0;
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use pg_buffercache_mark_dirty_all function")));
+
+	for (int buf = 1; buf <= NBuffers; buf++)
+	{
+		if (MarkUnpinnedBufferDirty(buf))
+			buffers_dirtied++;
+	}
+
+	PG_RETURN_INT32(buffers_dirtied);
+}
diff --git a/doc/src/sgml/pgbuffercache.sgml b/doc/src/sgml/pgbuffercache.sgml
index 681c74251d4..baf64e07b4e 100644
--- a/doc/src/sgml/pgbuffercache.sgml
+++ b/doc/src/sgml/pgbuffercache.sgml
@@ -35,14 +35,24 @@
   <primary>pg_buffercache_evict_all</primary>
  </indexterm>
 
+ <indexterm>
+  <primary>pg_buffercache_mark_dirty</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>pg_buffercache_mark_dirty_all</primary>
+ </indexterm>
+
  <para>
   This module provides the <function>pg_buffercache_pages()</function>
   function (wrapped in the <structname>pg_buffercache</structname> view),
   the <function>pg_buffercache_summary()</function> function, the
   <function>pg_buffercache_usage_counts()</function> function, the
-  <function>pg_buffercache_evict()</function>, the
-  <function>pg_buffercache_evict_relation()</function> function and the
-  <function>pg_buffercache_evict_all()</function> function.
+  <function>pg_buffercache_evict()</function> function, the
+  <function>pg_buffercache_evict_relation()</function> function, the
+  <function>pg_buffercache_evict_all()</function> function, the
+  <function>pg_buffercache_mark_dirty()</function> function and the
+  <function>pg_buffercache_mark_dirty_all()</function> function.
  </para>
 
  <para>
@@ -87,6 +97,18 @@
   restricted to superusers only.
  </para>
 
+ <para>
+  The <function>pg_buffercache_mark_dirty()</function> function allows a block
+  to be marked as dirty from the buffer pool given a buffer identifier.  Use of
+  this function is restricted to superusers only.
+ </para>
+
+ <para>
+  The <function>pg_buffercache_mark_dirty_all()</function> function tries to
+  mark all buffers dirty in the buffer pool.  Use of this function is
+  restricted to superusers only.
+ </para>
+
  <sect2 id="pgbuffercache-pg-buffercache">
   <title>The <structname>pg_buffercache</structname> View</title>
 
@@ -433,6 +455,32 @@
   </para>
  </sect2>
 
+ <sect2 id="pgbuffercache-pg-buffercache-mark-dirty">
+  <title>The <structname>pg_buffercache_mark_dirty</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_mark_dirty()</function> function takes a
+   buffer identifier, as shown in the <structfield>bufferid</structfield>
+   column of the <structname>pg_buffercache</structname> view.  It returns
+   true on success, and false if the buffer wasn't valid or if it couldn't be
+   marked as dirty because it was pinned.  The result is immediately out of
+   date upon return, as the buffer might become valid again at any time due to
+   concurrent activity.  The function is intended for developer testing only.
+  </para>
+ </sect2>
+
+ <sect2 id="pgbuffercache-pg-buffercache-mark-dirty-all">
+  <title>The <structname>pg_buffercache_mark_dirty_all</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_mark_dirty_all()</function> function is very
+   similar to the <function>pg_buffercache_mark_dirty()</function> function.
+   The difference is, the <function>pg_buffercache_mark_dirty_all()</function>
+   function does not take an argument; instead it tries to mark all buffers
+   dirty in the buffer pool.  The result is immediately out of date upon
+   return, as the buffer might become valid again at any time due to
+   concurrent activity.  The function is intended for developer testing only.
+  </para>
+ </sect2>
+
 <sect2 id="pgbuffercache-sample-output">
   <title>Sample Output</title>
 
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 6105c6d2d73..2decd8858b5 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -6208,3 +6208,57 @@ EvictRelUnpinnedBuffers(Relation rel, int32 *buffers_evicted, int32 *buffers_flu
 			UnlockBufHdr(bufHdr, buf_state);
 	}
 }
+
+/*
+ * Try to mark the provided shared buffer as dirty.
+ *
+ * This function is intended for testing/development use only!
+ *
+ * Same as EvictUnpinnedBuffer() but with MarkBufferDirty() call inside.
+ *
+ * Returns true if the buffer was already dirty or it has successfully been
+ * marked as dirty.
+ */
+bool
+MarkUnpinnedBufferDirty(Buffer buf)
+{
+	BufferDesc *desc;
+	uint32		buf_state;
+
+	Assert(!BufferIsLocal(buf));
+
+	/* Make sure we can pin the buffer. */
+	ResourceOwnerEnlarge(CurrentResourceOwner);
+	ReservePrivateRefCountEntry();
+
+	desc = GetBufferDescriptor(buf - 1);
+
+	/* Lock the header and check if it's valid. */
+	buf_state = LockBufHdr(desc);
+	if ((buf_state & BM_VALID) == 0)
+	{
+		UnlockBufHdr(desc, buf_state);
+		return false;
+	}
+
+	/* Check that it's not pinned already. */
+	if (BUF_STATE_GET_REFCOUNT(buf_state) > 0)
+	{
+		UnlockBufHdr(desc, buf_state);
+		return false;
+	}
+
+	PinBuffer_Locked(desc);		/* releases spinlock */
+
+	/* If it was not already dirty, mark it as dirty. */
+	if (!(buf_state & BM_DIRTY))
+	{
+		LWLockAcquire(BufferDescriptorGetContentLock(desc), LW_EXCLUSIVE);
+		MarkBufferDirty(buf);
+		LWLockRelease(BufferDescriptorGetContentLock(desc));
+	}
+
+	UnpinBuffer(desc);
+
+	return true;
+}
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 952c26c01c1..f78cda19ec6 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -301,6 +301,7 @@ extern void LimitAdditionalLocalPins(uint32 *additional_pins);
 extern bool EvictUnpinnedBuffer(Buffer buf, uint32 buf_state, bool *flushed);
 extern void EvictRelUnpinnedBuffers(Relation rel, int32 *buffers_evicted,
 									int32 *buffers_flushed);
+extern bool MarkUnpinnedBufferDirty(Buffer buf);
 
 /* in buf_init.c */
 extern void BufferManagerShmemInit(void);
-- 
2.43.0

#14Aidar Imamov
a.imamov@postgrespro.ru
In reply to: Nazir Bilal Yavuz (#13)
Re: Add pg_buffercache_evict_all() and pg_buffercache_mark_dirty[_all]() functions

Hi!

I've reviewed the latest version of the patches and found a few things
worth
discussing. This is probably my final feedback on the patches at this
point.
Maybe Joseph has something to add.

After this discussion, I think it would be helpful if one of the more
experienced
hackers could take a look at the overall picture (perhaps we should set
the
status "Ready for Committer"? To be honest, I'm not sure what step that
status
should be set at).

pg_buffercache--1.5--1.6.sql:
It seems that there is no need for the OR REPLACE clause after the
pg_buffercache_evict() function has been dropped.

Maybe we should remove the RETURNS clause from function declarations
that have
OUT parameters?
Doc: "When there are OUT or INOUT parameters, the RETURNS clause can be
omitted"

pg_buffercache_evict_relation():
The function is marked as STRICT so I think we do not need for redundant
check:

if (PG_ARGISNULL(0))
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("relation cannot be null")));

Doc: "returns null whenever any of its arguments are null", "the
function is not
executed when there are null arguments;".

EvictUnpinnedBuffer():
Maybe edit this comment a little (just not to repeat the sentences):

buf_state is used to check if the buffer header lock has been acquired
before
calling this function. If buf_state has a non-zero value, it means that
the buffer
header has already been locked. This information is useful for evicting
specific
relation's buffers, as the buffers headers need to be locked before
this function
can be called with such a intention.

EvictUnpinnedBuffer() & EvictRelUnpinnedBuffers():
Why did you decide to use the assert to check for NULLs?
I understand that currently, the use of these functions is limited to a
specific set
of calls through the pg_buffercache interface. However, this may not
always be the
case. Wouldn't it be better to allow users to choose whether or not they
want to
receive additional information? For example, you could simply ignore any
arguments
that are passed as NULL.

Additionally, I believe it would be beneficial to include some
regression tests to
check at least the following cases: return type, being a superuser, bad
buffer id,
local buffers case.

Also, there's a little thing about declaring functions as PARALLEL SAFE.
To be honest,
I don't really have any solid arguments against it. I just have some
doubts. For
example, how will it work if the plan is split up and we try to work on
an object in
one part, while the other part of the plan evicts the pages of that
object or marks
them as dirty... I can't really say for sure about that. And in that
context, I'd
suggest referring to that awesome statement in the documentation: "If in
doubt,
functions should be labeled as UNSAFE, which is the default."

regards,
Aidar Imamov

#15Nazir Bilal Yavuz
byavuz81@gmail.com
In reply to: Aidar Imamov (#14)
3 attachment(s)
Re: Add pg_buffercache_evict_all() and pg_buffercache_mark_dirty[_all]() functions

Hi,

On Mon, 31 Mar 2025 at 16:37, Aidar Imamov <a.imamov@postgrespro.ru> wrote:

Hi!

I've reviewed the latest version of the patches and found a few things
worth
discussing. This is probably my final feedback on the patches at this
point.
Maybe Joseph has something to add.

Thank you so much for the reviews, these were very helpful!

After this discussion, I think it would be helpful if one of the more
experienced
hackers could take a look at the overall picture (perhaps we should set
the
status "Ready for Committer"? To be honest, I'm not sure what step that
status
should be set at).

I agree. I will mark this as 'ready for committer' after writing the tests.

pg_buffercache--1.5--1.6.sql:
It seems that there is no need for the OR REPLACE clause after the
pg_buffercache_evict() function has been dropped.

Maybe we should remove the RETURNS clause from function declarations
that have
OUT parameters?
Doc: "When there are OUT or INOUT parameters, the RETURNS clause can be
omitted"

You are right, both are done.

pg_buffercache_evict_relation():
The function is marked as STRICT so I think we do not need for redundant
check:

if (PG_ARGISNULL(0))
ereport(ERROR,
(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
errmsg("relation cannot be null")));

Doc: "returns null whenever any of its arguments are null", "the
function is not
executed when there are null arguments;".

Done.

EvictUnpinnedBuffer():
Maybe edit this comment a little (just not to repeat the sentences):

buf_state is used to check if the buffer header lock has been acquired
before
calling this function. If buf_state has a non-zero value, it means that
the buffer
header has already been locked. This information is useful for evicting
specific
relation's buffers, as the buffers headers need to be locked before
this function
can be called with such a intention.

This is better, done.

EvictUnpinnedBuffer() & EvictRelUnpinnedBuffers():
Why did you decide to use the assert to check for NULLs?
I understand that currently, the use of these functions is limited to a
specific set
of calls through the pg_buffercache interface. However, this may not
always be the
case. Wouldn't it be better to allow users to choose whether or not they
want to
receive additional information? For example, you could simply ignore any
arguments
that are passed as NULL.

I do not have a strong opinion on this. I agree that these functions
can be used somewhere else later but it is easier to ignore these on
the return instead of handling NULLs in the functions. If we want to
consider the NULL possibility in the EvictUnpinnedBuffer() &
EvictRelUnpinnedBuffers(), then we need to write additional if
clauses. I think this increases the complexity unnecessarily.

Additionally, I believe it would be beneficial to include some
regression tests to
check at least the following cases: return type, being a superuser, bad
buffer id,
local buffers case.

You are right, I will try to write the tests but I am sharing the new
version without tests to speed things up.

Also, there's a little thing about declaring functions as PARALLEL SAFE.
To be honest,
I don't really have any solid arguments against it. I just have some
doubts. For
example, how will it work if the plan is split up and we try to work on
an object in
one part, while the other part of the plan evicts the pages of that
object or marks
them as dirty... I can't really say for sure about that. And in that
context, I'd
suggest referring to that awesome statement in the documentation: "If in
doubt,
functions should be labeled as UNSAFE, which is the default."

You may be right. I thought they are parallel safe as we acquire the
buffer header lock for each buffer but now, I have the same doubts as
you. I want to hear other people's opinions on that before labeling
them as UNSAFE.

v5 is attached.

--
Regards,
Nazir Bilal Yavuz
Microsoft

Attachments:

v5-0001-Add-pg_buffercache_evict_-relation-all-functions-.patchapplication/octet-stream; name=v5-0001-Add-pg_buffercache_evict_-relation-all-functions-.patchDownload
From a59be4b50d7691ba952d0abd32198e972d4a6ea0 Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@gmail.com>
Date: Mon, 24 Mar 2025 13:52:04 +0300
Subject: [PATCH v5 1/3] Add pg_buffercache_evict_[relation | all]() functions
 for testing

pg_buffercache_evict_relation(): Evicts all shared buffers in a
relation at once.
pg_buffercache_evict_all(): Evicts all shared buffers at once.

Both functions provide mechanism to evict multiple shared buffers at
once. They are designed to address the inefficiency of repeatedly calling
pg_buffercache_evict() for each individual buffer, which can be
time-consuming when dealing with large shared buffer pools.
(e.g., ~790ms vs. ~270ms for 16GB of shared buffers).

These functions are intended for developer testing and debugging
purposes and are available to superusers only.

Author: Nazir Bilal Yavuz <byavuz81@gmail.com>
Reviewed-by: Andres Freund <andres@anarazel.de>
Reviewed-by: Aidar Imamov <a.imamov@postgrespro.ru>
Reviewed-by: Joseph Koshakow <koshy44@gmail.com>
Discussion: https://postgr.es/m/CAN55FZ0h_YoSqqutxV6DES1RW8ig6wcA8CR9rJk358YRMxZFmw%40mail.gmail.com
---
 contrib/pg_buffercache/Makefile               |   3 +-
 contrib/pg_buffercache/meson.build            |   1 +
 .../pg_buffercache--1.5--1.6.sql              |  14 +++
 contrib/pg_buffercache/pg_buffercache.control |   2 +-
 contrib/pg_buffercache/pg_buffercache_pages.c | 102 ++++++++++++++++++
 doc/src/sgml/pgbuffercache.sgml               |  56 +++++++++-
 src/backend/storage/buffer/bufmgr.c           |  94 ++++++++++++++--
 src/include/storage/bufmgr.h                  |   4 +-
 8 files changed, 264 insertions(+), 12 deletions(-)
 create mode 100644 contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql

diff --git a/contrib/pg_buffercache/Makefile b/contrib/pg_buffercache/Makefile
index eae65ead9e5..2a33602537e 100644
--- a/contrib/pg_buffercache/Makefile
+++ b/contrib/pg_buffercache/Makefile
@@ -8,7 +8,8 @@ OBJS = \
 EXTENSION = pg_buffercache
 DATA = pg_buffercache--1.2.sql pg_buffercache--1.2--1.3.sql \
 	pg_buffercache--1.1--1.2.sql pg_buffercache--1.0--1.1.sql \
-	pg_buffercache--1.3--1.4.sql pg_buffercache--1.4--1.5.sql
+	pg_buffercache--1.3--1.4.sql pg_buffercache--1.4--1.5.sql \
+	pg_buffercache--1.5--1.6.sql
 PGFILEDESC = "pg_buffercache - monitoring of shared buffer cache in real-time"
 
 REGRESS = pg_buffercache
diff --git a/contrib/pg_buffercache/meson.build b/contrib/pg_buffercache/meson.build
index 12d1fe48717..9b2e9393410 100644
--- a/contrib/pg_buffercache/meson.build
+++ b/contrib/pg_buffercache/meson.build
@@ -23,6 +23,7 @@ install_data(
   'pg_buffercache--1.2.sql',
   'pg_buffercache--1.3--1.4.sql',
   'pg_buffercache--1.4--1.5.sql',
+  'pg_buffercache--1.5--1.6.sql',
   'pg_buffercache.control',
   kwargs: contrib_data_args,
 )
diff --git a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
new file mode 100644
index 00000000000..2e255f3fc10
--- /dev/null
+++ b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
@@ -0,0 +1,14 @@
+\echo Use "ALTER EXTENSION pg_buffercache UPDATE TO '1.6'" to load this file. \quit
+
+CREATE FUNCTION pg_buffercache_evict_relation(
+    IN regclass,
+    OUT buffers_evicted int4,
+    OUT buffers_flushed int4)
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict_relation'
+LANGUAGE C PARALLEL SAFE VOLATILE STRICT;
+
+CREATE FUNCTION pg_buffercache_evict_all(
+    OUT buffers_evicted int4,
+    OUT buffers_flushed int4)
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict_all'
+LANGUAGE C PARALLEL SAFE VOLATILE;
diff --git a/contrib/pg_buffercache/pg_buffercache.control b/contrib/pg_buffercache/pg_buffercache.control
index 5ee875f77dd..b030ba3a6fa 100644
--- a/contrib/pg_buffercache/pg_buffercache.control
+++ b/contrib/pg_buffercache/pg_buffercache.control
@@ -1,5 +1,5 @@
 # pg_buffercache extension
 comment = 'examine the shared buffer cache'
-default_version = '1.5'
+default_version = '1.6'
 module_pathname = '$libdir/pg_buffercache'
 relocatable = true
diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index 62602af1775..fa8aae43afd 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -9,16 +9,21 @@
 #include "postgres.h"
 
 #include "access/htup_details.h"
+#include "access/relation.h"
 #include "catalog/pg_type.h"
 #include "funcapi.h"
 #include "storage/buf_internals.h"
 #include "storage/bufmgr.h"
+#include "utils/builtins.h"
+#include "utils/rel.h"
 
 
 #define NUM_BUFFERCACHE_PAGES_MIN_ELEM	8
 #define NUM_BUFFERCACHE_PAGES_ELEM	9
 #define NUM_BUFFERCACHE_SUMMARY_ELEM 5
 #define NUM_BUFFERCACHE_USAGE_COUNTS_ELEM 4
+#define NUM_BUFFERCACHE_EVICT_RELATION_ELEM 2
+#define NUM_BUFFERCACHE_EVICT_ALL_ELEM 2
 
 PG_MODULE_MAGIC_EXT(
 					.name = "pg_buffercache",
@@ -67,6 +72,8 @@ PG_FUNCTION_INFO_V1(pg_buffercache_pages);
 PG_FUNCTION_INFO_V1(pg_buffercache_summary);
 PG_FUNCTION_INFO_V1(pg_buffercache_usage_counts);
 PG_FUNCTION_INFO_V1(pg_buffercache_evict);
+PG_FUNCTION_INFO_V1(pg_buffercache_evict_relation);
+PG_FUNCTION_INFO_V1(pg_buffercache_evict_all);
 
 Datum
 pg_buffercache_pages(PG_FUNCTION_ARGS)
@@ -370,3 +377,98 @@ pg_buffercache_evict(PG_FUNCTION_ARGS)
 
 	PG_RETURN_BOOL(EvictUnpinnedBuffer(buf));
 }
+
+/*
+ * Try to evict specified relation.
+ */
+Datum
+pg_buffercache_evict_relation(PG_FUNCTION_ARGS)
+{
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_EVICT_RELATION_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_EVICT_RELATION_ELEM] = {0};
+
+	Oid			relOid;
+	Relation	rel;
+	int32		buffers_evicted = 0;
+	int32		buffers_flushed = 0;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use pg_buffercache_evict_relation function")));
+
+	relOid = PG_GETARG_OID(0);
+
+	/* Open relation. */
+	rel = relation_open(relOid, AccessExclusiveLock);
+
+	if (RelationUsesLocalBuffers(rel))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relation uses local buffers,"
+						"pg_buffercache_evict_relation function is intended to"
+						"be used for shared buffers only")));
+
+	EvictRelUnpinnedBuffers(rel, &buffers_evicted, &buffers_flushed);
+
+	/* Close relation, release lock. */
+	relation_close(rel, AccessExclusiveLock);
+
+	values[0] = Int32GetDatum(buffers_evicted);
+	values[1] = Int32GetDatum(buffers_flushed);
+
+	/* Build and return the tuple. */
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
+
+
+/*
+ * Try to evict all shared buffers.
+ */
+Datum
+pg_buffercache_evict_all(PG_FUNCTION_ARGS)
+{
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_EVICT_ALL_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_EVICT_ALL_ELEM] = {0};
+
+	int32		buffers_evicted = 0;
+	int32		buffers_flushed = 0;
+	bool		flushed;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use pg_buffercache_evict_all function")));
+
+	for (int buf = 1; buf <= NBuffers; buf++)
+	{
+		if (EvictUnpinnedBuffer(buf, 0, &flushed))
+			buffers_evicted++;
+		if (flushed)
+			buffers_flushed++;
+	}
+
+	values[0] = Int32GetDatum(buffers_evicted);
+	values[1] = Int32GetDatum(buffers_flushed);
+
+	/* Build and return the tuple. */
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
diff --git a/doc/src/sgml/pgbuffercache.sgml b/doc/src/sgml/pgbuffercache.sgml
index 802a5112d77..d99aa979410 100644
--- a/doc/src/sgml/pgbuffercache.sgml
+++ b/doc/src/sgml/pgbuffercache.sgml
@@ -27,12 +27,22 @@
   <primary>pg_buffercache_evict</primary>
  </indexterm>
 
+ <indexterm>
+  <primary>pg_buffercache_evict_relation</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>pg_buffercache_evict_all</primary>
+ </indexterm>
+
  <para>
   This module provides the <function>pg_buffercache_pages()</function>
   function (wrapped in the <structname>pg_buffercache</structname> view),
   the <function>pg_buffercache_summary()</function> function, the
-  <function>pg_buffercache_usage_counts()</function> function and
-  the <function>pg_buffercache_evict()</function> function.
+  <function>pg_buffercache_usage_counts()</function> function, the
+  <function>pg_buffercache_evict()</function>, the
+  <function>pg_buffercache_evict_relation()</function> function and the
+  <function>pg_buffercache_evict_all()</function> function.
  </para>
 
  <para>
@@ -65,6 +75,18 @@
   function is restricted to superusers only.
  </para>
 
+ <para>
+  The <function>pg_buffercache_evict_relation()</function> function allows all
+  shared buffers in the relation to be evicted from the buffer pool given a
+  relation identifier.  Use of this function is restricted to superusers only.
+ </para>
+
+ <para>
+  The <function>pg_buffercache_evict_all()</function> function allows all
+  shared buffers to be evicted in the buffer pool.  Use of this function is
+  restricted to superusers only.
+ </para>
+
  <sect2 id="pgbuffercache-pg-buffercache">
   <title>The <structname>pg_buffercache</structname> View</title>
 
@@ -378,6 +400,36 @@
   </para>
  </sect2>
 
+ <sect2 id="pgbuffercache-pg-buffercache-evict-relation">
+  <title>The <structname>pg_buffercache_evict_relation</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_evict_relation()</function> function is very
+   similar to the <function>pg_buffercache_evict()</function> function.  The
+   difference is that the <function>pg_buffercache_evict_relation()</function>
+   takes a relation identifier instead of buffer identifier.  Then, it tries
+   to evict all buffers in that relation.  It returns the number of evicted and
+   flushed buffers.  Flushed buffers aren't necessarily flushed by us, they
+   might be flushed by someone else.  The result is immediately out of date
+   upon return, as the buffer might become valid again at any time due to
+   concurrent activity.  The function is intended for developer testing only.
+  </para>
+ </sect2>
+
+ <sect2 id="pgbuffercache-pg-buffercache-evict-all">
+  <title>The <structname>pg_buffercache_evict_all</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_evict_all()</function> function is very
+   similar to the <function>pg_buffercache_evict()</function> function.  The
+   difference is, the <function>pg_buffercache_evict_all()</function> function
+   does not take an argument; instead it tries to evict all buffers in the
+   buffer pool.  It returns the number of evicted and flushed buffers.
+   Flushed buffers aren't necessarily flushed by us, they might be flushed by
+   someone else.  The result is immediately out of date upon return, as the
+   buffer might become valid again at any time due to concurrent activity.
+   The function is intended for developer testing only.
+  </para>
+ </sect2>
+
 <sect2 id="pgbuffercache-sample-output">
   <title>Sample Output</title>
 
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index f9681d09e1e..5d82e3fa297 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -6512,26 +6512,47 @@ ResOwnerPrintBufferPin(Datum res)
  * even by the same block.  This inherent raciness without other interlocking
  * makes the function unsuitable for non-testing usage.
  *
+ * buf_state is used to check if the buffer header lock has been acquired
+ * before calling this function.  If buf_state has a non-zero value, it means
+ * that the buffer header has already been locked.  This information is useful
+ * for evicting a specific relation's buffers, as the buffers' headers need to
+ * be locked before this function can be called with such an intention.
+ *
+ * *flushed is set to true if the buffer was dirty and has been flushed.
+ * However, this does not necessarily mean that we flushed the buffer, it
+ * could have been flushed by someone else.
+ *
  * Returns true if the buffer was valid and it has now been made invalid.
  * Returns false if it wasn't valid, if it couldn't be evicted due to a pin,
  * or if the buffer becomes dirty again while we're trying to write it out.
  */
 bool
-EvictUnpinnedBuffer(Buffer buf)
+EvictUnpinnedBuffer(Buffer buf, uint32 buf_state, bool *flushed)
 {
 	BufferDesc *desc;
-	uint32		buf_state;
 	bool		result;
 
-	/* Make sure we can pin the buffer. */
-	ResourceOwnerEnlarge(CurrentResourceOwner);
-	ReservePrivateRefCountEntry();
+	Assert(flushed);
+	*flushed = false;
 
 	Assert(!BufferIsLocal(buf));
 	desc = GetBufferDescriptor(buf - 1);
 
-	/* Lock the header and check if it's valid. */
-	buf_state = LockBufHdr(desc);
+	/*
+	 * If the buffer is already locked, we assume that preparations to pinning
+	 * buffer are already done.
+	 */
+	if (!buf_state)
+	{
+		/* Make sure we can pin the buffer. */
+		ResourceOwnerEnlarge(CurrentResourceOwner);
+		ReservePrivateRefCountEntry();
+
+		/* Lock the header */
+		buf_state = LockBufHdr(desc);
+	}
+
+	/* Check if it's valid. */
 	if ((buf_state & BM_VALID) == 0)
 	{
 		UnlockBufHdr(desc, buf_state);
@@ -6553,6 +6574,7 @@ EvictUnpinnedBuffer(Buffer buf)
 		LWLockAcquire(BufferDescriptorGetContentLock(desc), LW_SHARED);
 		FlushBuffer(desc, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL);
 		LWLockRelease(BufferDescriptorGetContentLock(desc));
+		*flushed = true;
 	}
 
 	/* This will return false if it becomes dirty or someone else pins it. */
@@ -6563,6 +6585,64 @@ EvictUnpinnedBuffer(Buffer buf)
 	return result;
 }
 
+/*
+ * Try to evict all the shared buffers containing provided relation's pages.
+ *
+ * This function is intended for testing/development use only!
+ *
+ * We need this helper function because of the following reason.
+ * ReservePrivateRefCountEntry() needs to be called before acquiring the
+ * buffer header lock but ReservePrivateRefCountEntry() is static and it would
+ * be better to have it as static. Hence, it can't be called from outside of
+ * this file. This helper function is created to bypass that problem.
+ *
+ * Before calling this function, it is important to acquire
+ * AccessExclusiveLock on the specified relation to avoid replacing the
+ * current block of this relation with another one during execution.
+
+ * buffers_evicted and buffers_flushed are set the total number of buffers
+ * evicted and flushed respectively.
+ */
+void
+EvictRelUnpinnedBuffers(Relation rel, int32 *buffers_evicted, int32 *buffers_flushed)
+{
+	bool		flushed;
+
+	Assert(buffers_evicted && buffers_flushed);
+	*buffers_evicted = *buffers_flushed = 0;
+
+	Assert(!RelationUsesLocalBuffers(rel));
+
+	for (int buf = 1; buf <= NBuffers; buf++)
+	{
+		uint32		buf_state = 0;
+		BufferDesc *bufHdr = GetBufferDescriptor(buf - 1);
+
+		/* An unlocked precheck should be safe and saves some cycles. */
+		if (!BufTagMatchesRelFileLocator(&bufHdr->tag, &rel->rd_locator))
+			continue;
+
+		/* Make sure we can pin the buffer. */
+		ResourceOwnerEnlarge(CurrentResourceOwner);
+		ReservePrivateRefCountEntry();
+
+		/*
+		 * No need to call UnlockBufHdr() if BufTagMatchesRelFileLocator()
+		 * returns true, EvictUnpinnedBuffer() will take care of it.
+		 */
+		buf_state = LockBufHdr(bufHdr);
+		if (BufTagMatchesRelFileLocator(&bufHdr->tag, &rel->rd_locator))
+		{
+			if (EvictUnpinnedBuffer(buf, buf_state, &flushed))
+				(*buffers_evicted)++;
+			if (flushed)
+				(*buffers_flushed)++;
+		}
+		else
+			UnlockBufHdr(bufHdr, buf_state);
+	}
+}
+
 /*
  * Generic implementation of the AIO handle staging callback for readv/writev
  * on local/shared buffers.
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index f2192ceb271..ac6a2d521f3 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -304,7 +304,9 @@ extern uint32 GetAdditionalLocalPinLimit(void);
 extern void LimitAdditionalPins(uint32 *additional_pins);
 extern void LimitAdditionalLocalPins(uint32 *additional_pins);
 
-extern bool EvictUnpinnedBuffer(Buffer buf);
+extern bool EvictUnpinnedBuffer(Buffer buf, uint32 buf_state, bool *flushed);
+extern void EvictRelUnpinnedBuffers(Relation rel, int32 *buffers_evicted,
+									int32 *buffers_flushed);
 
 /* in buf_init.c */
 extern void BufferManagerShmemInit(void);
-- 
2.43.0

v5-0002-Return-buffer-flushed-information-in-pg_buffercac.patchapplication/octet-stream; name=v5-0002-Return-buffer-flushed-information-in-pg_buffercac.patchDownload
From 4d6b08f5680ecf7f34578f52945d448007bbc0df Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@gmail.com>
Date: Mon, 24 Mar 2025 13:53:26 +0300
Subject: [PATCH v5 2/3] Return buffer flushed information in
 pg_buffercache_evict function

Prior commit added ability to get buffer flushed information from
EvictUnpinnedBuffer() function. Show this information in
pg_buffercache_evict() function too.

Author: Nazir Bilal Yavuz <byavuz81@gmail.com>
Suggested-by: Joseph Koshakow <koshy44@gmail.com>
Reviewed-by: Aidar Imamov <a.imamov@postgrespro.ru>
Discussion: https://postgr.es/m/CAN55FZ0h_YoSqqutxV6DES1RW8ig6wcA8CR9rJk358YRMxZFmw%40mail.gmail.com
---
 .../pg_buffercache--1.5--1.6.sql              |  8 ++++++++
 contrib/pg_buffercache/pg_buffercache_pages.c | 20 ++++++++++++++++++-
 doc/src/sgml/pgbuffercache.sgml               | 15 ++++++++------
 3 files changed, 36 insertions(+), 7 deletions(-)

diff --git a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
index 2e255f3fc10..d54bb1fd6f8 100644
--- a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
+++ b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
@@ -1,5 +1,13 @@
 \echo Use "ALTER EXTENSION pg_buffercache UPDATE TO '1.6'" to load this file. \quit
 
+DROP FUNCTION pg_buffercache_evict(integer);
+CREATE FUNCTION pg_buffercache_evict(
+    IN int,
+    OUT evicted boolean,
+    OUT flushed boolean)
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict'
+LANGUAGE C PARALLEL SAFE VOLATILE STRICT;
+
 CREATE FUNCTION pg_buffercache_evict_relation(
     IN regclass,
     OUT buffers_evicted int4,
diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index fa8aae43afd..2f275354c56 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -22,6 +22,7 @@
 #define NUM_BUFFERCACHE_PAGES_ELEM	9
 #define NUM_BUFFERCACHE_SUMMARY_ELEM 5
 #define NUM_BUFFERCACHE_USAGE_COUNTS_ELEM 4
+#define NUM_BUFFERCACHE_EVICT_ELEM 2
 #define NUM_BUFFERCACHE_EVICT_RELATION_ELEM 2
 #define NUM_BUFFERCACHE_EVICT_ALL_ELEM 2
 
@@ -365,7 +366,17 @@ pg_buffercache_usage_counts(PG_FUNCTION_ARGS)
 Datum
 pg_buffercache_evict(PG_FUNCTION_ARGS)
 {
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_EVICT_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_EVICT_ELEM] = {0};
+
 	Buffer		buf = PG_GETARG_INT32(0);
+	bool		flushed;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
 
 	if (!superuser())
 		ereport(ERROR,
@@ -375,7 +386,14 @@ pg_buffercache_evict(PG_FUNCTION_ARGS)
 	if (buf < 1 || buf > NBuffers)
 		elog(ERROR, "bad buffer ID: %d", buf);
 
-	PG_RETURN_BOOL(EvictUnpinnedBuffer(buf));
+	values[0] = BoolGetDatum(EvictUnpinnedBuffer(buf, 0, &flushed));
+	values[1] = BoolGetDatum(flushed);
+
+	/* Build and return the tuple. */
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
 }
 
 /*
diff --git a/doc/src/sgml/pgbuffercache.sgml b/doc/src/sgml/pgbuffercache.sgml
index d99aa979410..681c74251d4 100644
--- a/doc/src/sgml/pgbuffercache.sgml
+++ b/doc/src/sgml/pgbuffercache.sgml
@@ -391,12 +391,15 @@
   <para>
    The <function>pg_buffercache_evict()</function> function takes a buffer
    identifier, as shown in the <structfield>bufferid</structfield> column of
-   the <structname>pg_buffercache</structname> view.  It returns true on success,
-   and false if the buffer wasn't valid, if it couldn't be evicted because it
-   was pinned, or if it became dirty again after an attempt to write it out.
-   The result is immediately out of date upon return, as the buffer might
-   become valid again at any time due to concurrent activity.  The function is
-   intended for developer testing only.
+   the <structname>pg_buffercache</structname> view.  It returns the
+   information about whether the buffer is evicted and flushed.  evicted
+   column is true on success, and false if the buffer wasn't valid, if it
+   couldn't be evicted because it was pinned, or if it became dirty again
+   after an attempt to write it out.  flushed column is true if the buffer is
+   flushed.  This does not necessarily mean that buffer is flushed by us, it
+   might be flushed by someone else.  The result is immediately out of date
+   upon return, as the buffer might become valid again at any time due to
+   concurrent activity. The function is intended for developer testing only.
   </para>
  </sect2>
 
-- 
2.43.0

v5-0003-Add-pg_buffercache_mark_dirty-_all-functions-for-.patchapplication/octet-stream; name=v5-0003-Add-pg_buffercache_mark_dirty-_all-functions-for-.patchDownload
From 570bdcc5387dc829eb891a58ef5fa4533a5982e1 Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@gmail.com>
Date: Wed, 25 Dec 2024 15:46:10 +0300
Subject: [PATCH v5 3/3] Add pg_buffercache_mark_dirty[_all]() functions for
 testing

This commit introduces two new functions for marking shared buffers as
dirty:

pg_buffercache_mark_dirty(): Marks a specific shared buffer as dirty.
pg_buffercache_mark_dirty_all(): Marks all shared buffers as dirty in a
single operation.

The pg_buffercache_mark_dirty_all() function provides an efficient
way to dirty the entire buffer pool (e.g., ~550ms vs. ~70ms for 16GB of
shared buffers), complementing pg_buffercache_mark_dirty() for more
granular control.

These functions are intended for developer testing and debugging
scenarios, enabling users to simulate various buffer pool states and
test write-back behavior. Both functions are superuser-only.

Author: Nazir Bilal Yavuz <byavuz81@gmail.com>
Reviewed-by: Andres Freund <andres@anarazel.de>
Reviewed-by: Aidar Imamov <a.imamov@postgrespro.ru>
Reviewed-by: Joseph Koshakow <koshy44@gmail.com>
Discussion: https://postgr.es/m/CAN55FZ0h_YoSqqutxV6DES1RW8ig6wcA8CR9rJk358YRMxZFmw%40mail.gmail.com
---
 .../pg_buffercache--1.5--1.6.sql              | 10 ++++
 contrib/pg_buffercache/pg_buffercache_pages.c | 43 +++++++++++++++
 doc/src/sgml/pgbuffercache.sgml               | 54 +++++++++++++++++--
 src/backend/storage/buffer/bufmgr.c           | 54 +++++++++++++++++++
 src/include/storage/bufmgr.h                  |  1 +
 5 files changed, 159 insertions(+), 3 deletions(-)

diff --git a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
index d54bb1fd6f8..b40ee2599a4 100644
--- a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
+++ b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
@@ -20,3 +20,13 @@ CREATE FUNCTION pg_buffercache_evict_all(
     OUT buffers_flushed int4)
 AS 'MODULE_PATHNAME', 'pg_buffercache_evict_all'
 LANGUAGE C PARALLEL SAFE VOLATILE;
+
+CREATE FUNCTION pg_buffercache_mark_dirty(IN int)
+RETURNS bool
+AS 'MODULE_PATHNAME', 'pg_buffercache_mark_dirty'
+LANGUAGE C PARALLEL SAFE VOLATILE STRICT;
+
+CREATE FUNCTION pg_buffercache_mark_dirty_all()
+RETURNS INT4
+AS 'MODULE_PATHNAME', 'pg_buffercache_mark_dirty_all'
+LANGUAGE C PARALLEL SAFE VOLATILE;
diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index 2f275354c56..23415016a8f 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -75,6 +75,8 @@ PG_FUNCTION_INFO_V1(pg_buffercache_usage_counts);
 PG_FUNCTION_INFO_V1(pg_buffercache_evict);
 PG_FUNCTION_INFO_V1(pg_buffercache_evict_relation);
 PG_FUNCTION_INFO_V1(pg_buffercache_evict_all);
+PG_FUNCTION_INFO_V1(pg_buffercache_mark_dirty);
+PG_FUNCTION_INFO_V1(pg_buffercache_mark_dirty_all);
 
 Datum
 pg_buffercache_pages(PG_FUNCTION_ARGS)
@@ -490,3 +492,44 @@ pg_buffercache_evict_all(PG_FUNCTION_ARGS)
 
 	PG_RETURN_DATUM(result);
 }
+
+/*
+ * Try to mark a shared buffer as dirty.
+ */
+Datum
+pg_buffercache_mark_dirty(PG_FUNCTION_ARGS)
+{
+	Buffer		buf = PG_GETARG_INT32(0);
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use pg_buffercache_mark_dirty function")));
+
+	if (buf < 1 || buf > NBuffers)
+		elog(ERROR, "bad buffer ID: %d", buf);
+
+	PG_RETURN_BOOL(MarkUnpinnedBufferDirty(buf));
+}
+
+/*
+ * Try to mark all the shared buffers as dirty.
+ */
+Datum
+pg_buffercache_mark_dirty_all(PG_FUNCTION_ARGS)
+{
+	int32		buffers_dirtied = 0;
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use pg_buffercache_mark_dirty_all function")));
+
+	for (int buf = 1; buf <= NBuffers; buf++)
+	{
+		if (MarkUnpinnedBufferDirty(buf))
+			buffers_dirtied++;
+	}
+
+	PG_RETURN_INT32(buffers_dirtied);
+}
diff --git a/doc/src/sgml/pgbuffercache.sgml b/doc/src/sgml/pgbuffercache.sgml
index 681c74251d4..baf64e07b4e 100644
--- a/doc/src/sgml/pgbuffercache.sgml
+++ b/doc/src/sgml/pgbuffercache.sgml
@@ -35,14 +35,24 @@
   <primary>pg_buffercache_evict_all</primary>
  </indexterm>
 
+ <indexterm>
+  <primary>pg_buffercache_mark_dirty</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>pg_buffercache_mark_dirty_all</primary>
+ </indexterm>
+
  <para>
   This module provides the <function>pg_buffercache_pages()</function>
   function (wrapped in the <structname>pg_buffercache</structname> view),
   the <function>pg_buffercache_summary()</function> function, the
   <function>pg_buffercache_usage_counts()</function> function, the
-  <function>pg_buffercache_evict()</function>, the
-  <function>pg_buffercache_evict_relation()</function> function and the
-  <function>pg_buffercache_evict_all()</function> function.
+  <function>pg_buffercache_evict()</function> function, the
+  <function>pg_buffercache_evict_relation()</function> function, the
+  <function>pg_buffercache_evict_all()</function> function, the
+  <function>pg_buffercache_mark_dirty()</function> function and the
+  <function>pg_buffercache_mark_dirty_all()</function> function.
  </para>
 
  <para>
@@ -87,6 +97,18 @@
   restricted to superusers only.
  </para>
 
+ <para>
+  The <function>pg_buffercache_mark_dirty()</function> function allows a block
+  to be marked as dirty from the buffer pool given a buffer identifier.  Use of
+  this function is restricted to superusers only.
+ </para>
+
+ <para>
+  The <function>pg_buffercache_mark_dirty_all()</function> function tries to
+  mark all buffers dirty in the buffer pool.  Use of this function is
+  restricted to superusers only.
+ </para>
+
  <sect2 id="pgbuffercache-pg-buffercache">
   <title>The <structname>pg_buffercache</structname> View</title>
 
@@ -433,6 +455,32 @@
   </para>
  </sect2>
 
+ <sect2 id="pgbuffercache-pg-buffercache-mark-dirty">
+  <title>The <structname>pg_buffercache_mark_dirty</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_mark_dirty()</function> function takes a
+   buffer identifier, as shown in the <structfield>bufferid</structfield>
+   column of the <structname>pg_buffercache</structname> view.  It returns
+   true on success, and false if the buffer wasn't valid or if it couldn't be
+   marked as dirty because it was pinned.  The result is immediately out of
+   date upon return, as the buffer might become valid again at any time due to
+   concurrent activity.  The function is intended for developer testing only.
+  </para>
+ </sect2>
+
+ <sect2 id="pgbuffercache-pg-buffercache-mark-dirty-all">
+  <title>The <structname>pg_buffercache_mark_dirty_all</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_mark_dirty_all()</function> function is very
+   similar to the <function>pg_buffercache_mark_dirty()</function> function.
+   The difference is, the <function>pg_buffercache_mark_dirty_all()</function>
+   function does not take an argument; instead it tries to mark all buffers
+   dirty in the buffer pool.  The result is immediately out of date upon
+   return, as the buffer might become valid again at any time due to
+   concurrent activity.  The function is intended for developer testing only.
+  </para>
+ </sect2>
+
 <sect2 id="pgbuffercache-sample-output">
   <title>Sample Output</title>
 
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 5d82e3fa297..1f902c4b54f 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -6643,6 +6643,60 @@ EvictRelUnpinnedBuffers(Relation rel, int32 *buffers_evicted, int32 *buffers_flu
 	}
 }
 
+/*
+ * Try to mark the provided shared buffer as dirty.
+ *
+ * This function is intended for testing/development use only!
+ *
+ * Same as EvictUnpinnedBuffer() but with MarkBufferDirty() call inside.
+ *
+ * Returns true if the buffer was already dirty or it has successfully been
+ * marked as dirty.
+ */
+bool
+MarkUnpinnedBufferDirty(Buffer buf)
+{
+	BufferDesc *desc;
+	uint32		buf_state;
+
+	Assert(!BufferIsLocal(buf));
+
+	/* Make sure we can pin the buffer. */
+	ResourceOwnerEnlarge(CurrentResourceOwner);
+	ReservePrivateRefCountEntry();
+
+	desc = GetBufferDescriptor(buf - 1);
+
+	/* Lock the header and check if it's valid. */
+	buf_state = LockBufHdr(desc);
+	if ((buf_state & BM_VALID) == 0)
+	{
+		UnlockBufHdr(desc, buf_state);
+		return false;
+	}
+
+	/* Check that it's not pinned already. */
+	if (BUF_STATE_GET_REFCOUNT(buf_state) > 0)
+	{
+		UnlockBufHdr(desc, buf_state);
+		return false;
+	}
+
+	PinBuffer_Locked(desc);		/* releases spinlock */
+
+	/* If it was not already dirty, mark it as dirty. */
+	if (!(buf_state & BM_DIRTY))
+	{
+		LWLockAcquire(BufferDescriptorGetContentLock(desc), LW_EXCLUSIVE);
+		MarkBufferDirty(buf);
+		LWLockRelease(BufferDescriptorGetContentLock(desc));
+	}
+
+	UnpinBuffer(desc);
+
+	return true;
+}
+
 /*
  * Generic implementation of the AIO handle staging callback for readv/writev
  * on local/shared buffers.
diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index ac6a2d521f3..a2e65a1d918 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -307,6 +307,7 @@ extern void LimitAdditionalLocalPins(uint32 *additional_pins);
 extern bool EvictUnpinnedBuffer(Buffer buf, uint32 buf_state, bool *flushed);
 extern void EvictRelUnpinnedBuffers(Relation rel, int32 *buffers_evicted,
 									int32 *buffers_flushed);
+extern bool MarkUnpinnedBufferDirty(Buffer buf);
 
 /* in buf_init.c */
 extern void BufferManagerShmemInit(void);
-- 
2.43.0

#16Andres Freund
andres@anarazel.de
In reply to: Nazir Bilal Yavuz (#15)
Re: Add pg_buffercache_evict_all() and pg_buffercache_mark_dirty[_all]() functions

Hi,

On 2025-03-31 19:49:25 +0300, Nazir Bilal Yavuz wrote:

After this discussion, I think it would be helpful if one of the more
experienced
hackers could take a look at the overall picture (perhaps we should set
the
status "Ready for Committer"? To be honest, I'm not sure what step that
status
should be set at).

I agree. I will mark this as 'ready for committer' after writing the tests.

Are you still working on tests?

Also, there's a little thing about declaring functions as PARALLEL SAFE.
To be honest,
I don't really have any solid arguments against it. I just have some
doubts. For
example, how will it work if the plan is split up and we try to work on
an object in
one part, while the other part of the plan evicts the pages of that
object or marks
them as dirty... I can't really say for sure about that. And in that
context, I'd
suggest referring to that awesome statement in the documentation: "If in
doubt,
functions should be labeled as UNSAFE, which is the default."

You may be right. I thought they are parallel safe as we acquire the
buffer header lock for each buffer but now, I have the same doubts as
you. I want to hear other people's opinions on that before labeling
them as UNSAFE.

I don't see a problem with them being parallel safe.

From a59be4b50d7691ba952d0abd32198e972d4a6ea0 Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@gmail.com>
Date: Mon, 24 Mar 2025 13:52:04 +0300
Subject: [PATCH v5 1/3] Add pg_buffercache_evict_[relation | all]() functions
for testing

pg_buffercache_evict_relation(): Evicts all shared buffers in a
relation at once.
pg_buffercache_evict_all(): Evicts all shared buffers at once.

Both functions provide mechanism to evict multiple shared buffers at
once. They are designed to address the inefficiency of repeatedly calling
pg_buffercache_evict() for each individual buffer, which can be
time-consuming when dealing with large shared buffer pools.
(e.g., ~790ms vs. ~270ms for 16GB of shared buffers).

These functions are intended for developer testing and debugging
purposes and are available to superusers only.

+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use pg_buffercache_evict_relation function")));

I think it'd be nicer if we didn't ask translators to translate 3 different
versions of this message, for different functions. Why not pass in the functio
name as a parameter?

+	if (RelationUsesLocalBuffers(rel))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relation uses local buffers,"
+						"pg_buffercache_evict_relation function is intended to"
+						"be used for shared buffers only")));

We try not to break messages across multiple lines, as that makes searching
for them harder.

+	EvictRelUnpinnedBuffers(rel, &buffers_evicted, &buffers_flushed);
+
+	/* Close relation, release lock. */

This comment imo isn't useful, it just restates the code verbatim.

+ relation_close(rel, AccessExclusiveLock);

Hm. Why are we dropping the lock here early? It's probably ok, but it's not
clear to me why we should do so.

+ /* Build and return the tuple. */

Similar to above, the comment is just a restatement of a short piece of code.

+ <para>
+  The <function>pg_buffercache_evict_relation()</function> function allows all
+  shared buffers in the relation to be evicted from the buffer pool given a
+  relation identifier.  Use of this function is restricted to superusers only.
+ </para>

I'd say "all unpinned shared buffers".

I wonder if these functions ought to also return the number of buffers that
could not be evicted?

Should this, or the longer comment for the function, mention that buffers for
all relation forks are evicted?

+ <para>
+  The <function>pg_buffercache_evict_all()</function> function allows all
+  shared buffers to be evicted in the buffer pool.  Use of this function is
+  restricted to superusers only.
+ </para>

Basically the same comments as above, except for the fork portion.

diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index f9681d09e1e..5d82e3fa297 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -6512,26 +6512,47 @@ ResOwnerPrintBufferPin(Datum res)
* even by the same block.  This inherent raciness without other interlocking
* makes the function unsuitable for non-testing usage.
*
+ * buf_state is used to check if the buffer header lock has been acquired
+ * before calling this function.  If buf_state has a non-zero value, it means
+ * that the buffer header has already been locked.  This information is useful
+ * for evicting a specific relation's buffers, as the buffers' headers need to
+ * be locked before this function can be called with such an intention.

I don't like this aspect of the API changes one bit. IMO something callable
from outside storage/buffer should have no business knowing about buf_state.

And detecting whether already hold a lock by checking whether the buf_state is
0 feels really wrong to me.

+/*
+ * Try to evict all the shared buffers containing provided relation's pages.
+ *
+ * This function is intended for testing/development use only!
+ *
+ * We need this helper function because of the following reason.
+ * ReservePrivateRefCountEntry() needs to be called before acquiring the
+ * buffer header lock but ReservePrivateRefCountEntry() is static and it would
+ * be better to have it as static. Hence, it can't be called from outside of
+ * this file. This helper function is created to bypass that problem.

I think there are other reasons - we should minimize the amount of code
outside of storage/buffer that needs to know about BuferDescs etc, it's a
layering violation to access that outside.

+ * Before calling this function, it is important to acquire
+ * AccessExclusiveLock on the specified relation to avoid replacing the
+ * current block of this relation with another one during execution.

What do you mean with "current block of this relation"?

+ * buffers_evicted and buffers_flushed are set the total number of buffers
+ * evicted and flushed respectively.
+ */
+void
+EvictRelUnpinnedBuffers(Relation rel, int32 *buffers_evicted, int32 *buffers_flushed)
+{
+	bool		flushed;
+
+	Assert(buffers_evicted && buffers_flushed);
+	*buffers_evicted = *buffers_flushed = 0;

I'd personally not bother with the Assert, the next line would just crash
anyway...

diff --git a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
index 2e255f3fc10..d54bb1fd6f8 100644
--- a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
+++ b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
@@ -1,5 +1,13 @@
\echo Use "ALTER EXTENSION pg_buffercache UPDATE TO '1.6'" to load this file. \quit
+DROP FUNCTION pg_buffercache_evict(integer);
+CREATE FUNCTION pg_buffercache_evict(
+    IN int,
+    OUT evicted boolean,
+    OUT flushed boolean)
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict'
+LANGUAGE C PARALLEL SAFE VOLATILE STRICT;

I assume the function has to be dropped because the return type changes?

Greetings,

Andres Freund

#17Nazir Bilal Yavuz
byavuz81@gmail.com
In reply to: Andres Freund (#16)
4 attachment(s)
Re: Add pg_buffercache_evict_all() and pg_buffercache_mark_dirty[_all]() functions

Hi,

Thanks for the review!

On Wed, 2 Apr 2025 at 20:06, Andres Freund <andres@anarazel.de> wrote:

Hi,

On 2025-03-31 19:49:25 +0300, Nazir Bilal Yavuz wrote:

After this discussion, I think it would be helpful if one of the more
experienced
hackers could take a look at the overall picture (perhaps we should set
the
status "Ready for Committer"? To be honest, I'm not sure what step that
status
should be set at).

I agree. I will mark this as 'ready for committer' after writing the tests.

Are you still working on tests?

Sorry, I forgot. They are attached as 0004.

Also, there's a little thing about declaring functions as PARALLEL SAFE.
To be honest,
I don't really have any solid arguments against it. I just have some
doubts. For
example, how will it work if the plan is split up and we try to work on
an object in
one part, while the other part of the plan evicts the pages of that
object or marks
them as dirty... I can't really say for sure about that. And in that
context, I'd
suggest referring to that awesome statement in the documentation: "If in
doubt,
functions should be labeled as UNSAFE, which is the default."

You may be right. I thought they are parallel safe as we acquire the
buffer header lock for each buffer but now, I have the same doubts as
you. I want to hear other people's opinions on that before labeling
them as UNSAFE.

I don't see a problem with them being parallel safe.

From a59be4b50d7691ba952d0abd32198e972d4a6ea0 Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@gmail.com>
Date: Mon, 24 Mar 2025 13:52:04 +0300
Subject: [PATCH v5 1/3] Add pg_buffercache_evict_[relation | all]() functions
for testing

pg_buffercache_evict_relation(): Evicts all shared buffers in a
relation at once.
pg_buffercache_evict_all(): Evicts all shared buffers at once.

Both functions provide mechanism to evict multiple shared buffers at
once. They are designed to address the inefficiency of repeatedly calling
pg_buffercache_evict() for each individual buffer, which can be
time-consuming when dealing with large shared buffer pools.
(e.g., ~790ms vs. ~270ms for 16GB of shared buffers).

These functions are intended for developer testing and debugging
purposes and are available to superusers only.

+     if (!superuser())
+             ereport(ERROR,
+                             (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+                              errmsg("must be superuser to use pg_buffercache_evict_relation function")));

I think it'd be nicer if we didn't ask translators to translate 3 different
versions of this message, for different functions. Why not pass in the functio
name as a parameter?

Ah, I didn't think of this aspect, done.

+     if (RelationUsesLocalBuffers(rel))
+             ereport(ERROR,
+                             (errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+                              errmsg("relation uses local buffers,"
+                                             "pg_buffercache_evict_relation function is intended to"
+                                             "be used for shared buffers only")));

We try not to break messages across multiple lines, as that makes searching
for them harder.

Done.

+     EvictRelUnpinnedBuffers(rel, &buffers_evicted, &buffers_flushed);
+
+     /* Close relation, release lock. */

This comment imo isn't useful, it just restates the code verbatim.

Removed.

+ relation_close(rel, AccessExclusiveLock);

Hm. Why are we dropping the lock here early? It's probably ok, but it's not
clear to me why we should do so.

We are dropping the lock after we processed the relation. I didn't
understand what could be the problem here. Why do you think it is
early?

+ /* Build and return the tuple. */

Similar to above, the comment is just a restatement of a short piece of code.

Done. It exists in other places in the pg_buffercache_pages.c file, I
only removed the ones that I added.

+ <para>
+  The <function>pg_buffercache_evict_relation()</function> function allows all
+  shared buffers in the relation to be evicted from the buffer pool given a
+  relation identifier.  Use of this function is restricted to superusers only.
+ </para>

I'd say "all unpinned shared buffers".

Done.

I wonder if these functions ought to also return the number of buffers that
could not be evicted?

I didn't add this as I thought this information could be gathered if wanted.

Should this, or the longer comment for the function, mention that buffers for
all relation forks are evicted?

I added this to a longer comment for the function.

+ <para>
+  The <function>pg_buffercache_evict_all()</function> function allows all
+  shared buffers to be evicted in the buffer pool.  Use of this function is
+  restricted to superusers only.
+ </para>

Basically the same comments as above, except for the fork portion.

Done.

diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index f9681d09e1e..5d82e3fa297 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -6512,26 +6512,47 @@ ResOwnerPrintBufferPin(Datum res)
* even by the same block.  This inherent raciness without other interlocking
* makes the function unsuitable for non-testing usage.
*
+ * buf_state is used to check if the buffer header lock has been acquired
+ * before calling this function.  If buf_state has a non-zero value, it means
+ * that the buffer header has already been locked.  This information is useful
+ * for evicting a specific relation's buffers, as the buffers' headers need to
+ * be locked before this function can be called with such an intention.

I don't like this aspect of the API changes one bit. IMO something callable
from outside storage/buffer should have no business knowing about buf_state.

And detecting whether already hold a lock by checking whether the buf_state is
0 feels really wrong to me.

I changed this. I basically copied EvictUnpinnedBuffer() to the inside
of EvictRelUnpinnedBuffers(), we don't need any hacky methods now.

+/*
+ * Try to evict all the shared buffers containing provided relation's pages.
+ *
+ * This function is intended for testing/development use only!
+ *
+ * We need this helper function because of the following reason.
+ * ReservePrivateRefCountEntry() needs to be called before acquiring the
+ * buffer header lock but ReservePrivateRefCountEntry() is static and it would
+ * be better to have it as static. Hence, it can't be called from outside of
+ * this file. This helper function is created to bypass that problem.

I think there are other reasons - we should minimize the amount of code
outside of storage/buffer that needs to know about BuferDescs etc, it's a
layering violation to access that outside.

I added that as a comment.

+ * Before calling this function, it is important to acquire
+ * AccessExclusiveLock on the specified relation to avoid replacing the
+ * current block of this relation with another one during execution.

What do you mean with "current block of this relation"?

It is used timewise, like a snapshot of the blocks when the function
is called. I updated this with:

'The caller must hold at least AccessShareLock on the relation to
prevent the relation from being dropped.' [1]

+ * buffers_evicted and buffers_flushed are set the total number of buffers
+ * evicted and flushed respectively.
+ */
+void
+EvictRelUnpinnedBuffers(Relation rel, int32 *buffers_evicted, int32 *buffers_flushed)
+{
+     bool            flushed;
+
+     Assert(buffers_evicted && buffers_flushed);
+     *buffers_evicted = *buffers_flushed = 0;

I'd personally not bother with the Assert, the next line would just crash
anyway...

AIO tests already use EvictUnpinnedBuffer() without flushed
information. I made '*buffers_evicted, *buffers_flushed in the
EvictRelUnpinnedBuffers() and *flushed in the EvictUnpinnedBuffer()'
optional as Aidar suggested at upthread.

diff --git a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
index 2e255f3fc10..d54bb1fd6f8 100644
--- a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
+++ b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
@@ -1,5 +1,13 @@
\echo Use "ALTER EXTENSION pg_buffercache UPDATE TO '1.6'" to load this file. \quit
+DROP FUNCTION pg_buffercache_evict(integer);
+CREATE FUNCTION pg_buffercache_evict(
+    IN int,
+    OUT evicted boolean,
+    OUT flushed boolean)
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict'
+LANGUAGE C PARALLEL SAFE VOLATILE STRICT;

I assume the function has to be dropped because the return type changes?

Correct.

v6 is attached. Additional changes prior to v5:

* Tests are added.
* Andres said off-list that AccessExclusiveLock is too much, all the
lock needs to do is to prevent the relation from being dropped. So,
pg_buffercache_evict_relation() opens a relation with AccessShareLock
now. Function comment in EvictRelUnpinnedBuffers() is updated
regarding that. [1]
* EvictRelUnpinnedBuffers() does not call EvictUnpinnedBuffer() now. I
basically copied EvictUnpinnedBuffer() to the inside of
EvictRelUnpinnedBuffers() with the needed updates.

--
Regards,
Nazir Bilal Yavuz
Microsoft

Attachments:

v6-0002-Add-pg_buffercache_evict_-relation-all-functions-.patchtext/x-patch; charset=US-ASCII; name=v6-0002-Add-pg_buffercache_evict_-relation-all-functions-.patchDownload
From fd6f751c14cc5e637a60f8f2bcae8871861321c9 Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@gmail.com>
Date: Fri, 4 Apr 2025 13:35:17 +0300
Subject: [PATCH v6 2/4] Add pg_buffercache_evict_[relation | all]() functions
 for testing

pg_buffercache_evict_relation(): Evicts all shared buffers in a
relation at once.
pg_buffercache_evict_all(): Evicts all shared buffers at once.

Both functions provide mechanism to evict multiple shared buffers at
once. They are designed to address the inefficiency of repeatedly calling
pg_buffercache_evict() for each individual buffer, which can be
time-consuming when dealing with large shared buffer pools.
(e.g., ~790ms vs. ~270ms for 16GB of shared buffers).

These functions are intended for developer testing and debugging
purposes and are available to superusers only.

Author: Nazir Bilal Yavuz <byavuz81@gmail.com>
Reviewed-by: Andres Freund <andres@anarazel.de>
Reviewed-by: Aidar Imamov <a.imamov@postgrespro.ru>
Reviewed-by: Joseph Koshakow <koshy44@gmail.com>
Discussion: https://postgr.es/m/CAN55FZ0h_YoSqqutxV6DES1RW8ig6wcA8CR9rJk358YRMxZFmw%40mail.gmail.com
---
 src/include/storage/bufmgr.h                  |  2 +
 src/backend/storage/buffer/bufmgr.c           | 92 +++++++++++++++++
 doc/src/sgml/pgbuffercache.sgml               | 58 ++++++++++-
 .../pg_buffercache--1.5--1.6.sql              | 13 +++
 contrib/pg_buffercache/pg_buffercache_pages.c | 98 +++++++++++++++++++
 5 files changed, 261 insertions(+), 2 deletions(-)

diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index fab65824b18..48f5182635c 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -305,6 +305,8 @@ extern void LimitAdditionalPins(uint32 *additional_pins);
 extern void LimitAdditionalLocalPins(uint32 *additional_pins);
 
 extern bool EvictUnpinnedBuffer(Buffer buf, bool *flushed);
+extern void EvictRelUnpinnedBuffers(Relation rel, int32 *buffers_evicted,
+									int32 *buffers_flushed);
 
 /* in buf_init.c */
 extern void BufferManagerShmemInit(void);
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index e585b72e5fa..e2bc155b700 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -6568,6 +6568,98 @@ EvictUnpinnedBuffer(Buffer buf, bool *flushed)
 	return result;
 }
 
+/*
+ * Try to evict all the shared buffers containing provided relation's pages.
+ *
+ * This function is intended for testing/development use only!
+ *
+ * We need this helper function because of the following reason.
+ * ReservePrivateRefCountEntry() needs to be called before acquiring the
+ * buffer header lock but ReservePrivateRefCountEntry() is static and it would
+ * be better to have it as static. Hence, it can't be called from outside of
+ * this file. This helper function is created to bypass that problem.
+ *
+ * Also, we should minimize the amount of code outside of storage/buffer that
+ * needs to know about BuferDescs etc., it is a layering violation to access
+ * that outside.
+ *
+ * The caller must hold at least AccessShareLock on the relation to prevent
+ * the relation from being dropped.
+ *
+ * buffers_evicted and buffers_flushed are optional. If they are provided,
+ * they are set the total number of buffers evicted and flushed respectively.
+ */
+void
+EvictRelUnpinnedBuffers(Relation rel, int32 *buffers_evicted, int32 *buffers_flushed)
+{
+	if (buffers_evicted)
+		*buffers_evicted = 0;
+	if (buffers_flushed)
+		*buffers_flushed = 0;
+
+	Assert(!RelationUsesLocalBuffers(rel));
+
+	for (int buf = 1; buf <= NBuffers; buf++)
+	{
+		uint32		buf_state = 0;
+		BufferDesc *bufHdr = GetBufferDescriptor(buf - 1);
+
+		/* An unlocked precheck should be safe and saves some cycles. */
+		if (!BufTagMatchesRelFileLocator(&bufHdr->tag, &rel->rd_locator))
+			continue;
+
+		/* Make sure we can pin the buffer. */
+		ResourceOwnerEnlarge(CurrentResourceOwner);
+		ReservePrivateRefCountEntry();
+
+		buf_state = LockBufHdr(bufHdr);
+		if (BufTagMatchesRelFileLocator(&bufHdr->tag, &rel->rd_locator))
+		{
+			/*
+			 * This is basically copy of the EvictUnpinnedBuffer(). This is
+			 * required because buffer header lock needs to be acquired before
+			 * calling the EvictUnpinnedBuffer().
+			 */
+
+			if ((buf_state & BM_VALID) == 0)
+			{
+				UnlockBufHdr(bufHdr, buf_state);
+				continue;
+			}
+
+			/* Check that it's not pinned already. */
+			if (BUF_STATE_GET_REFCOUNT(buf_state) > 0)
+			{
+				UnlockBufHdr(bufHdr, buf_state);
+				continue;
+			}
+
+			PinBuffer_Locked(bufHdr);	/* releases spinlock */
+
+			/* If it was dirty, try to clean it once. */
+			if (buf_state & BM_DIRTY)
+			{
+				LWLockAcquire(BufferDescriptorGetContentLock(bufHdr), LW_SHARED);
+				FlushBuffer(bufHdr, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL);
+				if (buffers_flushed)
+					(*buffers_flushed)++;
+				LWLockRelease(BufferDescriptorGetContentLock(bufHdr));
+			}
+
+			/*
+			 * This will return false if it becomes dirty or someone else pins
+			 * it.
+			 */
+			if (InvalidateVictimBuffer(bufHdr) && buffers_evicted)
+				(*buffers_evicted)++;
+
+			UnpinBuffer(bufHdr);
+		}
+		else
+			UnlockBufHdr(bufHdr, buf_state);
+	}
+}
+
 /*
  * Generic implementation of the AIO handle staging callback for readv/writev
  * on local/shared buffers.
diff --git a/doc/src/sgml/pgbuffercache.sgml b/doc/src/sgml/pgbuffercache.sgml
index 187e13e8cda..c7e10f97ab6 100644
--- a/doc/src/sgml/pgbuffercache.sgml
+++ b/doc/src/sgml/pgbuffercache.sgml
@@ -27,12 +27,22 @@
   <primary>pg_buffercache_evict</primary>
  </indexterm>
 
+ <indexterm>
+  <primary>pg_buffercache_evict_relation</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>pg_buffercache_evict_all</primary>
+ </indexterm>
+
  <para>
   This module provides the <function>pg_buffercache_pages()</function>
   function (wrapped in the <structname>pg_buffercache</structname> view),
   the <function>pg_buffercache_summary()</function> function, the
-  <function>pg_buffercache_usage_counts()</function> function and
-  the <function>pg_buffercache_evict()</function> function.
+  <function>pg_buffercache_usage_counts()</function> function, the
+  <function>pg_buffercache_evict()</function>, the
+  <function>pg_buffercache_evict_relation()</function> function and the
+  <function>pg_buffercache_evict_all()</function> function.
  </para>
 
  <para>
@@ -65,6 +75,19 @@
   function is restricted to superusers only.
  </para>
 
+ <para>
+  The <function>pg_buffercache_evict_relation()</function> function allows all
+  unpinned shared buffers in the relation to be evicted from the buffer pool
+  given a relation identifier.  Use of this function is restricted to
+  superusers only.
+ </para>
+
+ <para>
+  The <function>pg_buffercache_evict_all()</function> function allows all
+  unpinned shared buffers to be evicted in the buffer pool.  Use of this
+  function is restricted to superusers only.
+ </para>
+
  <sect2 id="pgbuffercache-pg-buffercache">
   <title>The <structname>pg_buffercache</structname> View</title>
 
@@ -381,6 +404,37 @@
   </para>
  </sect2>
 
+ <sect2 id="pgbuffercache-pg-buffercache-evict-relation">
+  <title>The <structname>pg_buffercache_evict_relation</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_evict_relation()</function> function is very
+   similar to the <function>pg_buffercache_evict()</function> function.  The
+   difference is that the <function>pg_buffercache_evict_relation()</function>
+   takes a relation identifier instead of buffer identifier.  Then, it tries
+   to evict all buffers for all forks in that relation.  It returns the number
+   of evicted and flushed buffers.  Flushed buffers aren't necessarily flushed
+   by us, they might be flushed by someone else.  The result is immediately
+   out of date upon return, as the buffer might become valid again at any time
+   due to concurrent activity.  The function is intended for developer testing
+   only.
+  </para>
+ </sect2>
+
+ <sect2 id="pgbuffercache-pg-buffercache-evict-all">
+  <title>The <structname>pg_buffercache_evict_all</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_evict_all()</function> function is very
+   similar to the <function>pg_buffercache_evict()</function> function.  The
+   difference is, the <function>pg_buffercache_evict_all()</function> function
+   does not take an argument; instead it tries to evict all buffers in the
+   buffer pool.  It returns the number of evicted and flushed buffers.
+   Flushed buffers aren't necessarily flushed by us, they might be flushed by
+   someone else.  The result is immediately out of date upon return, as the
+   buffer might become valid again at any time due to concurrent activity.
+   The function is intended for developer testing only.
+  </para>
+ </sect2>
+
 <sect2 id="pgbuffercache-sample-output">
   <title>Sample Output</title>
 
diff --git a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
index 18597923af8..d54bb1fd6f8 100644
--- a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
+++ b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
@@ -7,3 +7,16 @@ CREATE FUNCTION pg_buffercache_evict(
     OUT flushed boolean)
 AS 'MODULE_PATHNAME', 'pg_buffercache_evict'
 LANGUAGE C PARALLEL SAFE VOLATILE STRICT;
+
+CREATE FUNCTION pg_buffercache_evict_relation(
+    IN regclass,
+    OUT buffers_evicted int4,
+    OUT buffers_flushed int4)
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict_relation'
+LANGUAGE C PARALLEL SAFE VOLATILE STRICT;
+
+CREATE FUNCTION pg_buffercache_evict_all(
+    OUT buffers_evicted int4,
+    OUT buffers_flushed int4)
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict_all'
+LANGUAGE C PARALLEL SAFE VOLATILE;
diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index 9493a40e804..b6eba76fd27 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -9,10 +9,12 @@
 #include "postgres.h"
 
 #include "access/htup_details.h"
+#include "access/relation.h"
 #include "catalog/pg_type.h"
 #include "funcapi.h"
 #include "storage/buf_internals.h"
 #include "storage/bufmgr.h"
+#include "utils/rel.h"
 
 
 #define NUM_BUFFERCACHE_PAGES_MIN_ELEM	8
@@ -20,6 +22,8 @@
 #define NUM_BUFFERCACHE_SUMMARY_ELEM 5
 #define NUM_BUFFERCACHE_USAGE_COUNTS_ELEM 4
 #define NUM_BUFFERCACHE_EVICT_ELEM 2
+#define NUM_BUFFERCACHE_EVICT_RELATION_ELEM 2
+#define NUM_BUFFERCACHE_EVICT_ALL_ELEM 2
 
 PG_MODULE_MAGIC_EXT(
 					.name = "pg_buffercache",
@@ -68,6 +72,8 @@ PG_FUNCTION_INFO_V1(pg_buffercache_pages);
 PG_FUNCTION_INFO_V1(pg_buffercache_summary);
 PG_FUNCTION_INFO_V1(pg_buffercache_usage_counts);
 PG_FUNCTION_INFO_V1(pg_buffercache_evict);
+PG_FUNCTION_INFO_V1(pg_buffercache_evict_relation);
+PG_FUNCTION_INFO_V1(pg_buffercache_evict_all);
 
 Datum
 pg_buffercache_pages(PG_FUNCTION_ARGS)
@@ -388,3 +394,95 @@ pg_buffercache_evict(PG_FUNCTION_ARGS)
 
 	PG_RETURN_DATUM(result);
 }
+
+/*
+ * Try to evict specified relation.
+ */
+Datum
+pg_buffercache_evict_relation(PG_FUNCTION_ARGS)
+{
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_EVICT_RELATION_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_EVICT_RELATION_ELEM] = {0};
+
+	Oid			relOid;
+	Relation	rel;
+	int32		buffers_evicted = 0;
+	int32		buffers_flushed = 0;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use %s()",
+						"pg_buffercache_evict_relation")));
+
+	relOid = PG_GETARG_OID(0);
+
+	rel = relation_open(relOid, AccessShareLock);
+
+	if (RelationUsesLocalBuffers(rel))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relation uses local buffers, %s() is intended to be used for shared buffers only",
+						"pg_buffercache_evict_relation")));
+
+	EvictRelUnpinnedBuffers(rel, &buffers_evicted, &buffers_flushed);
+
+	relation_close(rel, AccessShareLock);
+
+	values[0] = Int32GetDatum(buffers_evicted);
+	values[1] = Int32GetDatum(buffers_flushed);
+
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
+
+
+/*
+ * Try to evict all shared buffers.
+ */
+Datum
+pg_buffercache_evict_all(PG_FUNCTION_ARGS)
+{
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_EVICT_ALL_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_EVICT_ALL_ELEM] = {0};
+
+	int32		buffers_evicted = 0;
+	int32		buffers_flushed = 0;
+	bool		flushed;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use %s()",
+						"pg_buffercache_evict_all")));
+
+	for (int buf = 1; buf <= NBuffers; buf++)
+	{
+		if (EvictUnpinnedBuffer(buf, &flushed))
+			buffers_evicted++;
+		if (flushed)
+			buffers_flushed++;
+	}
+
+	values[0] = Int32GetDatum(buffers_evicted);
+	values[1] = Int32GetDatum(buffers_flushed);
+
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
-- 
2.47.2

v6-0003-Add-pg_buffercache_mark_dirty-_all-functions-for-.patchtext/x-patch; charset=US-ASCII; name=v6-0003-Add-pg_buffercache_mark_dirty-_all-functions-for-.patchDownload
From 9d8948f4f78fe1c9ed1e757bf1e5a190403c82ba Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@gmail.com>
Date: Fri, 4 Apr 2025 13:39:49 +0300
Subject: [PATCH v6 3/4] Add pg_buffercache_mark_dirty[_all]() functions for
 testing

This commit introduces two new functions for marking shared buffers as
dirty:

pg_buffercache_mark_dirty(): Marks a specific shared buffer as dirty.
pg_buffercache_mark_dirty_all(): Marks all shared buffers as dirty in a
single operation.

The pg_buffercache_mark_dirty_all() function provides an efficient
way to dirty the entire buffer pool (e.g., ~550ms vs. ~70ms for 16GB of
shared buffers), complementing pg_buffercache_mark_dirty() for more
granular control.

These functions are intended for developer testing and debugging
scenarios, enabling users to simulate various buffer pool states and
test write-back behavior. Both functions are superuser-only.

Author: Nazir Bilal Yavuz <byavuz81@gmail.com>
Reviewed-by: Andres Freund <andres@anarazel.de>
Reviewed-by: Aidar Imamov <a.imamov@postgrespro.ru>
Reviewed-by: Joseph Koshakow <koshy44@gmail.com>
Discussion: https://postgr.es/m/CAN55FZ0h_YoSqqutxV6DES1RW8ig6wcA8CR9rJk358YRMxZFmw%40mail.gmail.com
---
 src/include/storage/bufmgr.h                  |  1 +
 src/backend/storage/buffer/bufmgr.c           | 54 +++++++++++++++++++
 doc/src/sgml/pgbuffercache.sgml               | 54 +++++++++++++++++--
 .../pg_buffercache--1.5--1.6.sql              | 10 ++++
 contrib/pg_buffercache/pg_buffercache_pages.c | 45 ++++++++++++++++
 5 files changed, 161 insertions(+), 3 deletions(-)

diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index 48f5182635c..ddb442b600d 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -307,6 +307,7 @@ extern void LimitAdditionalLocalPins(uint32 *additional_pins);
 extern bool EvictUnpinnedBuffer(Buffer buf, bool *flushed);
 extern void EvictRelUnpinnedBuffers(Relation rel, int32 *buffers_evicted,
 									int32 *buffers_flushed);
+extern bool MarkUnpinnedBufferDirty(Buffer buf);
 
 /* in buf_init.c */
 extern void BufferManagerShmemInit(void);
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index e2bc155b700..4b7b0534c67 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -6660,6 +6660,60 @@ EvictRelUnpinnedBuffers(Relation rel, int32 *buffers_evicted, int32 *buffers_flu
 	}
 }
 
+/*
+ * Try to mark the provided shared buffer as dirty.
+ *
+ * This function is intended for testing/development use only!
+ *
+ * Same as EvictUnpinnedBuffer() but with MarkBufferDirty() call inside.
+ *
+ * Returns true if the buffer was already dirty or it has successfully been
+ * marked as dirty.
+ */
+bool
+MarkUnpinnedBufferDirty(Buffer buf)
+{
+	BufferDesc *desc;
+	uint32		buf_state;
+
+	Assert(!BufferIsLocal(buf));
+
+	/* Make sure we can pin the buffer. */
+	ResourceOwnerEnlarge(CurrentResourceOwner);
+	ReservePrivateRefCountEntry();
+
+	desc = GetBufferDescriptor(buf - 1);
+
+	/* Lock the header and check if it's valid. */
+	buf_state = LockBufHdr(desc);
+	if ((buf_state & BM_VALID) == 0)
+	{
+		UnlockBufHdr(desc, buf_state);
+		return false;
+	}
+
+	/* Check that it's not pinned already. */
+	if (BUF_STATE_GET_REFCOUNT(buf_state) > 0)
+	{
+		UnlockBufHdr(desc, buf_state);
+		return false;
+	}
+
+	PinBuffer_Locked(desc);		/* releases spinlock */
+
+	/* If it was not already dirty, mark it as dirty. */
+	if (!(buf_state & BM_DIRTY))
+	{
+		LWLockAcquire(BufferDescriptorGetContentLock(desc), LW_EXCLUSIVE);
+		MarkBufferDirty(buf);
+		LWLockRelease(BufferDescriptorGetContentLock(desc));
+	}
+
+	UnpinBuffer(desc);
+
+	return true;
+}
+
 /*
  * Generic implementation of the AIO handle staging callback for readv/writev
  * on local/shared buffers.
diff --git a/doc/src/sgml/pgbuffercache.sgml b/doc/src/sgml/pgbuffercache.sgml
index c7e10f97ab6..83818fa6328 100644
--- a/doc/src/sgml/pgbuffercache.sgml
+++ b/doc/src/sgml/pgbuffercache.sgml
@@ -35,14 +35,24 @@
   <primary>pg_buffercache_evict_all</primary>
  </indexterm>
 
+ <indexterm>
+  <primary>pg_buffercache_mark_dirty</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>pg_buffercache_mark_dirty_all</primary>
+ </indexterm>
+
  <para>
   This module provides the <function>pg_buffercache_pages()</function>
   function (wrapped in the <structname>pg_buffercache</structname> view),
   the <function>pg_buffercache_summary()</function> function, the
   <function>pg_buffercache_usage_counts()</function> function, the
-  <function>pg_buffercache_evict()</function>, the
-  <function>pg_buffercache_evict_relation()</function> function and the
-  <function>pg_buffercache_evict_all()</function> function.
+  <function>pg_buffercache_evict()</function> function, the
+  <function>pg_buffercache_evict_relation()</function> function, the
+  <function>pg_buffercache_evict_all()</function> function, the
+  <function>pg_buffercache_mark_dirty()</function> function and the
+  <function>pg_buffercache_mark_dirty_all()</function> function.
  </para>
 
  <para>
@@ -88,6 +98,18 @@
   function is restricted to superusers only.
  </para>
 
+ <para>
+  The <function>pg_buffercache_mark_dirty()</function> function allows a block
+  to be marked as dirty from the buffer pool given a buffer identifier.  Use of
+  this function is restricted to superusers only.
+ </para>
+
+ <para>
+  The <function>pg_buffercache_mark_dirty_all()</function> function tries to
+  mark all buffers dirty in the buffer pool.  Use of this function is
+  restricted to superusers only.
+ </para>
+
  <sect2 id="pgbuffercache-pg-buffercache">
   <title>The <structname>pg_buffercache</structname> View</title>
 
@@ -435,6 +457,32 @@
   </para>
  </sect2>
 
+ <sect2 id="pgbuffercache-pg-buffercache-mark-dirty">
+  <title>The <structname>pg_buffercache_mark_dirty</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_mark_dirty()</function> function takes a
+   buffer identifier, as shown in the <structfield>bufferid</structfield>
+   column of the <structname>pg_buffercache</structname> view.  It returns
+   true on success, and false if the buffer wasn't valid or if it couldn't be
+   marked as dirty because it was pinned.  The result is immediately out of
+   date upon return, as the buffer might become valid again at any time due to
+   concurrent activity.  The function is intended for developer testing only.
+  </para>
+ </sect2>
+
+ <sect2 id="pgbuffercache-pg-buffercache-mark-dirty-all">
+  <title>The <structname>pg_buffercache_mark_dirty_all</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_mark_dirty_all()</function> function is very
+   similar to the <function>pg_buffercache_mark_dirty()</function> function.
+   The difference is, the <function>pg_buffercache_mark_dirty_all()</function>
+   function does not take an argument; instead it tries to mark all buffers
+   dirty in the buffer pool.  The result is immediately out of date upon
+   return, as the buffer might become valid again at any time due to
+   concurrent activity.  The function is intended for developer testing only.
+  </para>
+ </sect2>
+
 <sect2 id="pgbuffercache-sample-output">
   <title>Sample Output</title>
 
diff --git a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
index d54bb1fd6f8..b40ee2599a4 100644
--- a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
+++ b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
@@ -20,3 +20,13 @@ CREATE FUNCTION pg_buffercache_evict_all(
     OUT buffers_flushed int4)
 AS 'MODULE_PATHNAME', 'pg_buffercache_evict_all'
 LANGUAGE C PARALLEL SAFE VOLATILE;
+
+CREATE FUNCTION pg_buffercache_mark_dirty(IN int)
+RETURNS bool
+AS 'MODULE_PATHNAME', 'pg_buffercache_mark_dirty'
+LANGUAGE C PARALLEL SAFE VOLATILE STRICT;
+
+CREATE FUNCTION pg_buffercache_mark_dirty_all()
+RETURNS INT4
+AS 'MODULE_PATHNAME', 'pg_buffercache_mark_dirty_all'
+LANGUAGE C PARALLEL SAFE VOLATILE;
diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index b6eba76fd27..1afe70b3eb2 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -74,6 +74,8 @@ PG_FUNCTION_INFO_V1(pg_buffercache_usage_counts);
 PG_FUNCTION_INFO_V1(pg_buffercache_evict);
 PG_FUNCTION_INFO_V1(pg_buffercache_evict_relation);
 PG_FUNCTION_INFO_V1(pg_buffercache_evict_all);
+PG_FUNCTION_INFO_V1(pg_buffercache_mark_dirty);
+PG_FUNCTION_INFO_V1(pg_buffercache_mark_dirty_all);
 
 Datum
 pg_buffercache_pages(PG_FUNCTION_ARGS)
@@ -486,3 +488,46 @@ pg_buffercache_evict_all(PG_FUNCTION_ARGS)
 
 	PG_RETURN_DATUM(result);
 }
+
+/*
+ * Try to mark a shared buffer as dirty.
+ */
+Datum
+pg_buffercache_mark_dirty(PG_FUNCTION_ARGS)
+{
+	Buffer		buf = PG_GETARG_INT32(0);
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use %s()",
+						"pg_buffercache_mark_dirty")));
+
+	if (buf < 1 || buf > NBuffers)
+		elog(ERROR, "bad buffer ID: %d", buf);
+
+	PG_RETURN_BOOL(MarkUnpinnedBufferDirty(buf));
+}
+
+/*
+ * Try to mark all the shared buffers as dirty.
+ */
+Datum
+pg_buffercache_mark_dirty_all(PG_FUNCTION_ARGS)
+{
+	int32		buffers_dirtied = 0;
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use %s()",
+						"pg_buffercache_mark_dirty_all")));
+
+	for (int buf = 1; buf <= NBuffers; buf++)
+	{
+		if (MarkUnpinnedBufferDirty(buf))
+			buffers_dirtied++;
+	}
+
+	PG_RETURN_INT32(buffers_dirtied);
+}
-- 
2.47.2

v6-0004-Extend-pg_buffercache-extension-s-tests.patchtext/x-patch; charset=US-ASCII; name=v6-0004-Extend-pg_buffercache-extension-s-tests.patchDownload
From 99f8af2cfca5319e2c9d9b9f9518e5c9d2d04e84 Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@gmail.com>
Date: Fri, 4 Apr 2025 13:40:29 +0300
Subject: [PATCH v6 4/4] Extend pg_buffercache extension's tests

Prior commits extended pg_buffercache extension with
pg_buffercache_evict_relation(), pg_buffercache_evict_all(),
pg_buffercache_mark_dirty() and pg_buffercache_mark_dirty_all()
functions. Add test for these.

Also, there was no test for pg_buffercache_evict(), add test for this
too.

Author: Nazir Bilal Yavuz <byavuz81@gmail.com>
Suggested-by: Aidar Imamov <a.imamov@postgrespro.ru>
Discussion: https://postgr.es/m/CAN55FZ0h_YoSqqutxV6DES1RW8ig6wcA8CR9rJk358YRMxZFmw%40mail.gmail.com
---
 .../expected/pg_buffercache.out               | 89 +++++++++++++++++++
 contrib/pg_buffercache/sql/pg_buffercache.sql | 51 +++++++++++
 2 files changed, 140 insertions(+)

diff --git a/contrib/pg_buffercache/expected/pg_buffercache.out b/contrib/pg_buffercache/expected/pg_buffercache.out
index b745dc69eae..5d1a6f8aefa 100644
--- a/contrib/pg_buffercache/expected/pg_buffercache.out
+++ b/contrib/pg_buffercache/expected/pg_buffercache.out
@@ -55,3 +55,92 @@ SELECT count(*) > 0 FROM pg_buffercache_usage_counts();
  t
 (1 row)
 
+RESET role;
+------
+---- Test pg_buffercache_evict* and pg_buffercache_mark_dirty* functions
+------
+CREATE ROLE regress_buffercache_normal;
+SET ROLE regress_buffercache_normal;
+-- These should fail because these are STRICT functions
+SELECT * FROM pg_buffercache_evict();
+ERROR:  function pg_buffercache_evict() does not exist
+LINE 1: SELECT * FROM pg_buffercache_evict();
+                      ^
+HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+SELECT * FROM pg_buffercache_evict_relation();
+ERROR:  function pg_buffercache_evict_relation() does not exist
+LINE 1: SELECT * FROM pg_buffercache_evict_relation();
+                      ^
+HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+SELECT * FROM pg_buffercache_mark_dirty();
+ERROR:  function pg_buffercache_mark_dirty() does not exist
+LINE 1: SELECT * FROM pg_buffercache_mark_dirty();
+                      ^
+HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+-- These should fail because they need to be called as SUPERUSER
+SELECT * FROM pg_buffercache_evict(1);
+ERROR:  must be superuser to use pg_buffercache_evict()
+SELECT * FROM pg_buffercache_evict_relation(1);
+ERROR:  must be superuser to use pg_buffercache_evict_relation()
+SELECT * FROM pg_buffercache_evict_all();
+ERROR:  must be superuser to use pg_buffercache_evict_all()
+SELECT * FROM pg_buffercache_mark_dirty(1);
+ERROR:  must be superuser to use pg_buffercache_mark_dirty()
+SELECT * FROM pg_buffercache_mark_dirty_all();
+ERROR:  must be superuser to use pg_buffercache_mark_dirty_all()
+RESET ROLE;
+CREATE ROLE regress_buffercache_superuser SUPERUSER;
+SET ROLE regress_buffercache_superuser;
+-- These should fail because they are not called by valid range of buffers
+-- Number of the shared buffers are limited by max integer
+SELECT 2147483647 max_buffers \gset
+SELECT * FROM pg_buffercache_evict(0);
+ERROR:  bad buffer ID: 0
+SELECT * FROM pg_buffercache_evict(:max_buffers);
+ERROR:  bad buffer ID: 2147483647
+SELECT * FROM pg_buffercache_mark_dirty(0);
+ERROR:  bad buffer ID: 0
+SELECT * FROM pg_buffercache_mark_dirty(:max_buffers);
+ERROR:  bad buffer ID: 2147483647
+-- This should fail because pg_buffercache_evict_relation() doesn't accept
+-- local relations
+CREATE TEMP TABLE temp_pg_buffercache();
+SELECT * FROM pg_buffercache_evict_relation('temp_pg_buffercache');
+ERROR:  relation uses local buffers, pg_buffercache_evict_relation() is intended to be used for shared buffers only
+DROP TABLE temp_pg_buffercache;
+-- These shouldn't fail
+SELECT COUNT(*) > 0 FROM pg_buffercache_evict(1);
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT COUNT(*) > 0 FROM pg_buffercache_evict_all();
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT COUNT(*) > 0 FROM pg_buffercache_mark_dirty(1);
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT COUNT(*) > 0 FROM pg_buffercache_mark_dirty_all();
+ ?column? 
+----------
+ t
+(1 row)
+
+CREATE TABLE shared_pg_buffercache();
+SELECT COUNT(*) > 0 FROM pg_buffercache_evict_relation('shared_pg_buffercache');
+ ?column? 
+----------
+ t
+(1 row)
+
+DROP TABLE shared_pg_buffercache;
+RESET ROLE;
+DROP ROLE regress_buffercache_normal;
+DROP ROLE regress_buffercache_superuser;
diff --git a/contrib/pg_buffercache/sql/pg_buffercache.sql b/contrib/pg_buffercache/sql/pg_buffercache.sql
index 944fbb1beae..f859af15220 100644
--- a/contrib/pg_buffercache/sql/pg_buffercache.sql
+++ b/contrib/pg_buffercache/sql/pg_buffercache.sql
@@ -26,3 +26,54 @@ SET ROLE pg_monitor;
 SELECT count(*) > 0 FROM pg_buffercache;
 SELECT buffers_used + buffers_unused > 0 FROM pg_buffercache_summary();
 SELECT count(*) > 0 FROM pg_buffercache_usage_counts();
+RESET role;
+
+------
+---- Test pg_buffercache_evict* and pg_buffercache_mark_dirty* functions
+------
+
+CREATE ROLE regress_buffercache_normal;
+SET ROLE regress_buffercache_normal;
+
+-- These should fail because these are STRICT functions
+SELECT * FROM pg_buffercache_evict();
+SELECT * FROM pg_buffercache_evict_relation();
+SELECT * FROM pg_buffercache_mark_dirty();
+
+-- These should fail because they need to be called as SUPERUSER
+SELECT * FROM pg_buffercache_evict(1);
+SELECT * FROM pg_buffercache_evict_relation(1);
+SELECT * FROM pg_buffercache_evict_all();
+SELECT * FROM pg_buffercache_mark_dirty(1);
+SELECT * FROM pg_buffercache_mark_dirty_all();
+
+RESET ROLE;
+CREATE ROLE regress_buffercache_superuser SUPERUSER;
+SET ROLE regress_buffercache_superuser;
+
+-- These should fail because they are not called by valid range of buffers
+-- Number of the shared buffers are limited by max integer
+SELECT 2147483647 max_buffers \gset
+SELECT * FROM pg_buffercache_evict(0);
+SELECT * FROM pg_buffercache_evict(:max_buffers);
+SELECT * FROM pg_buffercache_mark_dirty(0);
+SELECT * FROM pg_buffercache_mark_dirty(:max_buffers);
+
+-- This should fail because pg_buffercache_evict_relation() doesn't accept
+-- local relations
+CREATE TEMP TABLE temp_pg_buffercache();
+SELECT * FROM pg_buffercache_evict_relation('temp_pg_buffercache');
+DROP TABLE temp_pg_buffercache;
+
+-- These shouldn't fail
+SELECT COUNT(*) > 0 FROM pg_buffercache_evict(1);
+SELECT COUNT(*) > 0 FROM pg_buffercache_evict_all();
+SELECT COUNT(*) > 0 FROM pg_buffercache_mark_dirty(1);
+SELECT COUNT(*) > 0 FROM pg_buffercache_mark_dirty_all();
+CREATE TABLE shared_pg_buffercache();
+SELECT COUNT(*) > 0 FROM pg_buffercache_evict_relation('shared_pg_buffercache');
+DROP TABLE shared_pg_buffercache;
+
+RESET ROLE;
+DROP ROLE regress_buffercache_normal;
+DROP ROLE regress_buffercache_superuser;
-- 
2.47.2

v6-0001-Return-buffer-flushed-information-in-pg_buffercac.patchtext/x-patch; charset=US-ASCII; name=v6-0001-Return-buffer-flushed-information-in-pg_buffercac.patchDownload
From 88624ea0f768ebf1a015cc0c06d2c72c822577a8 Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@gmail.com>
Date: Fri, 4 Apr 2025 13:22:00 +0300
Subject: [PATCH v6 1/4] Return buffer flushed information in
 pg_buffercache_evict function

pg_buffercache_evict() function shows buffer flushed information now.
This feature will be used in the following commits.

Author: Nazir Bilal Yavuz <byavuz81@gmail.com>
Suggested-by: Joseph Koshakow <koshy44@gmail.com>
Reviewed-by: Aidar Imamov <a.imamov@postgrespro.ru>
Reviewed-by: Andres Freund <andres@anarazel.de>
Discussion: https://postgr.es/m/CAN55FZ0h_YoSqqutxV6DES1RW8ig6wcA8CR9rJk358YRMxZFmw%40mail.gmail.com
---
 src/include/storage/bufmgr.h                  |  2 +-
 src/backend/storage/buffer/bufmgr.c           | 11 +++++++++-
 src/test/modules/test_aio/test_aio.c          |  4 ++--
 doc/src/sgml/pgbuffercache.sgml               | 15 ++++++++-----
 contrib/pg_buffercache/Makefile               |  3 ++-
 contrib/pg_buffercache/meson.build            |  1 +
 .../pg_buffercache--1.5--1.6.sql              |  9 ++++++++
 contrib/pg_buffercache/pg_buffercache.control |  2 +-
 contrib/pg_buffercache/pg_buffercache_pages.c | 22 +++++++++++++++++--
 9 files changed, 55 insertions(+), 14 deletions(-)
 create mode 100644 contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql

diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index f2192ceb271..fab65824b18 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -304,7 +304,7 @@ extern uint32 GetAdditionalLocalPinLimit(void);
 extern void LimitAdditionalPins(uint32 *additional_pins);
 extern void LimitAdditionalLocalPins(uint32 *additional_pins);
 
-extern bool EvictUnpinnedBuffer(Buffer buf);
+extern bool EvictUnpinnedBuffer(Buffer buf, bool *flushed);
 
 /* in buf_init.c */
 extern void BufferManagerShmemInit(void);
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 1c37d7dfe2f..e585b72e5fa 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -6508,17 +6508,24 @@ ResOwnerPrintBufferPin(Datum res)
  * even by the same block.  This inherent raciness without other interlocking
  * makes the function unsuitable for non-testing usage.
  *
+ * *flushed is optional. If it is provided, it is set to true if the buffer
+ * was dirty and has been flushed.  However, this does not necessarily mean
+ * that we flushed the buffer, it could have been flushed by someone else.
+ *
  * Returns true if the buffer was valid and it has now been made invalid.
  * Returns false if it wasn't valid, if it couldn't be evicted due to a pin,
  * or if the buffer becomes dirty again while we're trying to write it out.
  */
 bool
-EvictUnpinnedBuffer(Buffer buf)
+EvictUnpinnedBuffer(Buffer buf, bool *flushed)
 {
 	BufferDesc *desc;
 	uint32		buf_state;
 	bool		result;
 
+	if (flushed)
+		*flushed = false;
+
 	/* Make sure we can pin the buffer. */
 	ResourceOwnerEnlarge(CurrentResourceOwner);
 	ReservePrivateRefCountEntry();
@@ -6548,6 +6555,8 @@ EvictUnpinnedBuffer(Buffer buf)
 	{
 		LWLockAcquire(BufferDescriptorGetContentLock(desc), LW_SHARED);
 		FlushBuffer(desc, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL);
+		if (flushed)
+			*flushed = true;
 		LWLockRelease(BufferDescriptorGetContentLock(desc));
 	}
 
diff --git a/src/test/modules/test_aio/test_aio.c b/src/test/modules/test_aio/test_aio.c
index bef0ecd9007..f9cf6ce8a26 100644
--- a/src/test/modules/test_aio/test_aio.c
+++ b/src/test/modules/test_aio/test_aio.c
@@ -237,7 +237,7 @@ modify_rel_block(PG_FUNCTION_ARGS)
 	if (BufferIsLocal(buf))
 		InvalidateLocalBuffer(GetLocalBufferDescriptor(-buf - 1), true);
 	else
-		EvictUnpinnedBuffer(buf);
+		EvictUnpinnedBuffer(buf, NULL);
 
 	/*
 	 * Now modify the page as asked for by the caller.
@@ -493,7 +493,7 @@ invalidate_rel_block(PG_FUNCTION_ARGS)
 
 			if (BufferIsLocal(buf))
 				InvalidateLocalBuffer(GetLocalBufferDescriptor(-buf - 1), true);
-			else if (!EvictUnpinnedBuffer(buf))
+			else if (!EvictUnpinnedBuffer(buf, NULL))
 				elog(ERROR, "couldn't evict");
 		}
 	}
diff --git a/doc/src/sgml/pgbuffercache.sgml b/doc/src/sgml/pgbuffercache.sgml
index 802a5112d77..187e13e8cda 100644
--- a/doc/src/sgml/pgbuffercache.sgml
+++ b/doc/src/sgml/pgbuffercache.sgml
@@ -369,12 +369,15 @@
   <para>
    The <function>pg_buffercache_evict()</function> function takes a buffer
    identifier, as shown in the <structfield>bufferid</structfield> column of
-   the <structname>pg_buffercache</structname> view.  It returns true on success,
-   and false if the buffer wasn't valid, if it couldn't be evicted because it
-   was pinned, or if it became dirty again after an attempt to write it out.
-   The result is immediately out of date upon return, as the buffer might
-   become valid again at any time due to concurrent activity.  The function is
-   intended for developer testing only.
+   the <structname>pg_buffercache</structname> view.  It returns the
+   information about whether the buffer is evicted and flushed.  evicted
+   column is true on success, and false if the buffer wasn't valid, if it
+   couldn't be evicted because it was pinned, or if it became dirty again
+   after an attempt to write it out.  flushed column is true if the buffer is
+   flushed.  This does not necessarily mean that buffer is flushed by us, it
+   might be flushed by someone else.  The result is immediately out of date
+   upon return, as the buffer might become valid again at any time due to
+   concurrent activity. The function is intended for developer testing only.
   </para>
  </sect2>
 
diff --git a/contrib/pg_buffercache/Makefile b/contrib/pg_buffercache/Makefile
index eae65ead9e5..2a33602537e 100644
--- a/contrib/pg_buffercache/Makefile
+++ b/contrib/pg_buffercache/Makefile
@@ -8,7 +8,8 @@ OBJS = \
 EXTENSION = pg_buffercache
 DATA = pg_buffercache--1.2.sql pg_buffercache--1.2--1.3.sql \
 	pg_buffercache--1.1--1.2.sql pg_buffercache--1.0--1.1.sql \
-	pg_buffercache--1.3--1.4.sql pg_buffercache--1.4--1.5.sql
+	pg_buffercache--1.3--1.4.sql pg_buffercache--1.4--1.5.sql \
+	pg_buffercache--1.5--1.6.sql
 PGFILEDESC = "pg_buffercache - monitoring of shared buffer cache in real-time"
 
 REGRESS = pg_buffercache
diff --git a/contrib/pg_buffercache/meson.build b/contrib/pg_buffercache/meson.build
index 12d1fe48717..9b2e9393410 100644
--- a/contrib/pg_buffercache/meson.build
+++ b/contrib/pg_buffercache/meson.build
@@ -23,6 +23,7 @@ install_data(
   'pg_buffercache--1.2.sql',
   'pg_buffercache--1.3--1.4.sql',
   'pg_buffercache--1.4--1.5.sql',
+  'pg_buffercache--1.5--1.6.sql',
   'pg_buffercache.control',
   kwargs: contrib_data_args,
 )
diff --git a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
new file mode 100644
index 00000000000..18597923af8
--- /dev/null
+++ b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
@@ -0,0 +1,9 @@
+\echo Use "ALTER EXTENSION pg_buffercache UPDATE TO '1.6'" to load this file. \quit
+
+DROP FUNCTION pg_buffercache_evict(integer);
+CREATE FUNCTION pg_buffercache_evict(
+    IN int,
+    OUT evicted boolean,
+    OUT flushed boolean)
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict'
+LANGUAGE C PARALLEL SAFE VOLATILE STRICT;
diff --git a/contrib/pg_buffercache/pg_buffercache.control b/contrib/pg_buffercache/pg_buffercache.control
index 5ee875f77dd..b030ba3a6fa 100644
--- a/contrib/pg_buffercache/pg_buffercache.control
+++ b/contrib/pg_buffercache/pg_buffercache.control
@@ -1,5 +1,5 @@
 # pg_buffercache extension
 comment = 'examine the shared buffer cache'
-default_version = '1.5'
+default_version = '1.6'
 module_pathname = '$libdir/pg_buffercache'
 relocatable = true
diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index 62602af1775..9493a40e804 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -19,6 +19,7 @@
 #define NUM_BUFFERCACHE_PAGES_ELEM	9
 #define NUM_BUFFERCACHE_SUMMARY_ELEM 5
 #define NUM_BUFFERCACHE_USAGE_COUNTS_ELEM 4
+#define NUM_BUFFERCACHE_EVICT_ELEM 2
 
 PG_MODULE_MAGIC_EXT(
 					.name = "pg_buffercache",
@@ -358,15 +359,32 @@ pg_buffercache_usage_counts(PG_FUNCTION_ARGS)
 Datum
 pg_buffercache_evict(PG_FUNCTION_ARGS)
 {
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_EVICT_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_EVICT_ELEM] = {0};
+
 	Buffer		buf = PG_GETARG_INT32(0);
+	bool		flushed;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
 
 	if (!superuser())
 		ereport(ERROR,
 				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-				 errmsg("must be superuser to use pg_buffercache_evict function")));
+				 errmsg("must be superuser to use %s()",
+						"pg_buffercache_evict")));
 
 	if (buf < 1 || buf > NBuffers)
 		elog(ERROR, "bad buffer ID: %d", buf);
 
-	PG_RETURN_BOOL(EvictUnpinnedBuffer(buf));
+	values[0] = BoolGetDatum(EvictUnpinnedBuffer(buf, &flushed));
+	values[1] = BoolGetDatum(flushed);
+
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
 }
-- 
2.47.2

#18Andres Freund
andres@anarazel.de
In reply to: Nazir Bilal Yavuz (#17)
Re: Add pg_buffercache_evict_all() and pg_buffercache_mark_dirty[_all]() functions

Hi,

On 2025-04-04 18:36:57 +0300, Nazir Bilal Yavuz wrote:

v6 is attached. Additional changes prior to v5:

* Tests are added.

I don't think the mark-dirty stuff is quite ready to commit. Given that, could
you change the split of the patches so that the tests are for the evict
patches alone? I don't really see a need to add the tests separately from the
code introducing the relevant functionality.

* EvictRelUnpinnedBuffers() does not call EvictUnpinnedBuffer() now. I
basically copied EvictUnpinnedBuffer() to the inside of
EvictRelUnpinnedBuffers() with the needed updates.

I think I'd instead introduce a EvictBufferInternal() that is used both by
EvictUnpinnedBuffer() and EvictRelUnpinnedBuffers(), that is expected to be
called with the buffer header lock held.

+ relation_close(rel, AccessExclusiveLock);

Hm. Why are we dropping the lock here early? It's probably ok, but it's not
clear to me why we should do so.

We are dropping the lock after we processed the relation. I didn't
understand what could be the problem here. Why do you think it is
early?

Most commonly we close relations without releasing the lock, instead relying
on the lock being released at the end of the transaction.

I wonder if these functions ought to also return the number of buffers that
could not be evicted?

I didn't add this as I thought this information could be gathered if wanted.

How? A separate query will also return buffers that were since newly read in.

I think it'be better to just return all that information, given that we are
already dropping the function. Dropping objects in extension upgrades can be
painful for users, due to other objects potentially depending on the dropped
objects. So we shouldn't do that unnecessarily often.

From 88624ea0f768ebf1a015cc0c06d2c72c822577a8 Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@gmail.com>
Date: Fri, 4 Apr 2025 13:22:00 +0300
Subject: [PATCH v6 1/4] Return buffer flushed information in
pg_buffercache_evict function

pg_buffercache_evict() function shows buffer flushed information now.
This feature will be used in the following commits.

Author: Nazir Bilal Yavuz <byavuz81@gmail.com>
Suggested-by: Joseph Koshakow <koshy44@gmail.com>
Reviewed-by: Aidar Imamov <a.imamov@postgrespro.ru>
Reviewed-by: Andres Freund <andres@anarazel.de>
Discussion: /messages/by-id/CAN55FZ0h_YoSqqutxV6DES1RW8ig6wcA8CR9rJk358YRMxZFmw@mail.gmail.com
---
src/include/storage/bufmgr.h | 2 +-
src/backend/storage/buffer/bufmgr.c | 11 +++++++++-
src/test/modules/test_aio/test_aio.c | 4 ++--
doc/src/sgml/pgbuffercache.sgml | 15 ++++++++-----
contrib/pg_buffercache/Makefile | 3 ++-
contrib/pg_buffercache/meson.build | 1 +
.../pg_buffercache--1.5--1.6.sql | 9 ++++++++

I think I'd squash this with the following changes, as it seems unnecessary to
increase the version by multiple steps in immediately successive commits.

+/*
+ * Try to evict all the shared buffers containing provided relation's pages.
+ *
+ * This function is intended for testing/development use only!
+ *
+ * We need this helper function because of the following reason.
+ * ReservePrivateRefCountEntry() needs to be called before acquiring the
+ * buffer header lock but ReservePrivateRefCountEntry() is static and it would
+ * be better to have it as static. Hence, it can't be called from outside of
+ * this file. This helper function is created to bypass that problem.
+ * Also, we should minimize the amount of code outside of storage/buffer that
+ * needs to know about BuferDescs etc., it is a layering violation to access
+ * that outside.
+ *

FWIW, I think this explanation is kind of true, but the real reason is that
from a layering perspective this simply is bufmgr.c's responsibility, not
pg_buffercache's. I'd probably just drop these two paragraphs.

+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use %s()",
+						"pg_buffercache_evict_relation")));

FWIW, I'd upate the existing superuser check in the same file to use the same
string. And perhaps I'd just put the check into a small helper function that
is shared by pg_buffercache_evict() / pg_buffercache_evict() /
pg_buffercache_evict_all().

+/*
+ * Try to evict all shared buffers.
+ */
+Datum
+pg_buffercache_evict_all(PG_FUNCTION_ARGS)
+{
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_EVICT_ALL_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_EVICT_ALL_ELEM] = {0};
+
+	int32		buffers_evicted = 0;
+	int32		buffers_flushed = 0;
+	bool		flushed;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use %s()",
+						"pg_buffercache_evict_all")));
+
+	for (int buf = 1; buf <= NBuffers; buf++)
+	{
+		if (EvictUnpinnedBuffer(buf, &flushed))
+			buffers_evicted++;
+		if (flushed)
+			buffers_flushed++;
+	}

FWIW, I'd put this loop in bufmgr.c, in a similar helper function as for the
other cases.

--- a/contrib/pg_buffercache/sql/pg_buffercache.sql
+++ b/contrib/pg_buffercache/sql/pg_buffercache.sql
@@ -26,3 +26,54 @@ SET ROLE pg_monitor;
SELECT count(*) > 0 FROM pg_buffercache;
SELECT buffers_used + buffers_unused > 0 FROM pg_buffercache_summary();
SELECT count(*) > 0 FROM pg_buffercache_usage_counts();
+RESET role;
+
+------
+---- Test pg_buffercache_evict* and pg_buffercache_mark_dirty* functions
+------
+
+CREATE ROLE regress_buffercache_normal;
+SET ROLE regress_buffercache_normal;
+
+-- These should fail because these are STRICT functions
+SELECT * FROM pg_buffercache_evict();
+SELECT * FROM pg_buffercache_evict_relation();
+SELECT * FROM pg_buffercache_mark_dirty();
+-- These should fail because they need to be called as SUPERUSER
+SELECT * FROM pg_buffercache_evict(1);
+SELECT * FROM pg_buffercache_evict_relation(1);
+SELECT * FROM pg_buffercache_evict_all();
+SELECT * FROM pg_buffercache_mark_dirty(1);
+SELECT * FROM pg_buffercache_mark_dirty_all();
+
+RESET ROLE;
+CREATE ROLE regress_buffercache_superuser SUPERUSER;
+SET ROLE regress_buffercache_superuser;

Not sure what we gain by creating this role?

Greetings,

Andres Freund

#19Nazir Bilal Yavuz
byavuz81@gmail.com
In reply to: Andres Freund (#18)
1 attachment(s)
Re: Add pg_buffercache_evict_all() and pg_buffercache_mark_dirty[_all]() functions

Hi,

On Mon, 7 Apr 2025 at 16:38, Andres Freund <andres@anarazel.de> wrote:

Hi,

On 2025-04-04 18:36:57 +0300, Nazir Bilal Yavuz wrote:

v6 is attached. Additional changes prior to v5:

* Tests are added.

I don't think the mark-dirty stuff is quite ready to commit. Given that, could
you change the split of the patches so that the tests are for the evict
patches alone? I don't really see a need to add the tests separately from the
code introducing the relevant functionality.

Done.

* EvictRelUnpinnedBuffers() does not call EvictUnpinnedBuffer() now. I
basically copied EvictUnpinnedBuffer() to the inside of
EvictRelUnpinnedBuffers() with the needed updates.

I think I'd instead introduce a EvictBufferInternal() that is used both by
EvictUnpinnedBuffer() and EvictRelUnpinnedBuffers(), that is expected to be
called with the buffer header lock held.

Makes sense, done that way. I named it as 'EvictUnpinnedBufferInternal'.

+ relation_close(rel, AccessExclusiveLock);

Hm. Why are we dropping the lock here early? It's probably ok, but it's not
clear to me why we should do so.

We are dropping the lock after we processed the relation. I didn't
understand what could be the problem here. Why do you think it is
early?

Most commonly we close relations without releasing the lock, instead relying
on the lock being released at the end of the transaction.

I see. I was looking at pg_prewarm as an example and copied it from there.

I wonder if these functions ought to also return the number of buffers that
could not be evicted?

I didn't add this as I thought this information could be gathered if wanted.

How? A separate query will also return buffers that were since newly read in.

Yes, correct. I missed that.

I think it'be better to just return all that information, given that we are
already dropping the function. Dropping objects in extension upgrades can be
painful for users, due to other objects potentially depending on the dropped
objects. So we shouldn't do that unnecessarily often.

Done that way. pg_buffercache_evict_relation() and
pg_buffercache_evict_all() return the total number of buffers
processed.

From 88624ea0f768ebf1a015cc0c06d2c72c822577a8 Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@gmail.com>
Date: Fri, 4 Apr 2025 13:22:00 +0300
Subject: [PATCH v6 1/4] Return buffer flushed information in
pg_buffercache_evict function

pg_buffercache_evict() function shows buffer flushed information now.
This feature will be used in the following commits.

Author: Nazir Bilal Yavuz <byavuz81@gmail.com>
Suggested-by: Joseph Koshakow <koshy44@gmail.com>
Reviewed-by: Aidar Imamov <a.imamov@postgrespro.ru>
Reviewed-by: Andres Freund <andres@anarazel.de>
Discussion: /messages/by-id/CAN55FZ0h_YoSqqutxV6DES1RW8ig6wcA8CR9rJk358YRMxZFmw@mail.gmail.com
---
src/include/storage/bufmgr.h | 2 +-
src/backend/storage/buffer/bufmgr.c | 11 +++++++++-
src/test/modules/test_aio/test_aio.c | 4 ++--
doc/src/sgml/pgbuffercache.sgml | 15 ++++++++-----
contrib/pg_buffercache/Makefile | 3 ++-
contrib/pg_buffercache/meson.build | 1 +
.../pg_buffercache--1.5--1.6.sql | 9 ++++++++

I think I'd squash this with the following changes, as it seems unnecessary to
increase the version by multiple steps in immediately successive commits.

Done.

+/*
+ * Try to evict all the shared buffers containing provided relation's pages.
+ *
+ * This function is intended for testing/development use only!
+ *
+ * We need this helper function because of the following reason.
+ * ReservePrivateRefCountEntry() needs to be called before acquiring the
+ * buffer header lock but ReservePrivateRefCountEntry() is static and it would
+ * be better to have it as static. Hence, it can't be called from outside of
+ * this file. This helper function is created to bypass that problem.
+ * Also, we should minimize the amount of code outside of storage/buffer that
+ * needs to know about BuferDescs etc., it is a layering violation to access
+ * that outside.
+ *

FWIW, I think this explanation is kind of true, but the real reason is that
from a layering perspective this simply is bufmgr.c's responsibility, not
pg_buffercache's. I'd probably just drop these two paragraphs.

Done.

+     if (!superuser())
+             ereport(ERROR,
+                             (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+                              errmsg("must be superuser to use %s()",
+                                             "pg_buffercache_evict_relation")));

FWIW, I'd upate the existing superuser check in the same file to use the same
string. And perhaps I'd just put the check into a small helper function that
is shared by pg_buffercache_evict() / pg_buffercache_evict() /
pg_buffercache_evict_all().

Done. I added the pg_buffercache_superuser_check() function.

+/*
+ * Try to evict all shared buffers.
+ */
+Datum
+pg_buffercache_evict_all(PG_FUNCTION_ARGS)
+{
+     Datum           result;
+     TupleDesc       tupledesc;
+     HeapTuple       tuple;
+     Datum           values[NUM_BUFFERCACHE_EVICT_ALL_ELEM];
+     bool            nulls[NUM_BUFFERCACHE_EVICT_ALL_ELEM] = {0};
+
+     int32           buffers_evicted = 0;
+     int32           buffers_flushed = 0;
+     bool            flushed;
+
+     if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+             elog(ERROR, "return type must be a row type");
+
+     if (!superuser())
+             ereport(ERROR,
+                             (errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+                              errmsg("must be superuser to use %s()",
+                                             "pg_buffercache_evict_all")));
+
+     for (int buf = 1; buf <= NBuffers; buf++)
+     {
+             if (EvictUnpinnedBuffer(buf, &flushed))
+                     buffers_evicted++;
+             if (flushed)
+                     buffers_flushed++;
+     }

FWIW, I'd put this loop in bufmgr.c, in a similar helper function as for the
other cases.

Done. I moved this loop to the EvictAllUnpinnedBuffers() function in bufmgr.c.

--- a/contrib/pg_buffercache/sql/pg_buffercache.sql
+++ b/contrib/pg_buffercache/sql/pg_buffercache.sql
@@ -26,3 +26,54 @@ SET ROLE pg_monitor;
SELECT count(*) > 0 FROM pg_buffercache;
SELECT buffers_used + buffers_unused > 0 FROM pg_buffercache_summary();
SELECT count(*) > 0 FROM pg_buffercache_usage_counts();
+RESET role;
+
+------
+---- Test pg_buffercache_evict* and pg_buffercache_mark_dirty* functions
+------
+
+CREATE ROLE regress_buffercache_normal;
+SET ROLE regress_buffercache_normal;
+
+-- These should fail because these are STRICT functions
+SELECT * FROM pg_buffercache_evict();
+SELECT * FROM pg_buffercache_evict_relation();
+SELECT * FROM pg_buffercache_mark_dirty();
+-- These should fail because they need to be called as SUPERUSER
+SELECT * FROM pg_buffercache_evict(1);
+SELECT * FROM pg_buffercache_evict_relation(1);
+SELECT * FROM pg_buffercache_evict_all();
+SELECT * FROM pg_buffercache_mark_dirty(1);
+SELECT * FROM pg_buffercache_mark_dirty_all();
+
+RESET ROLE;
+CREATE ROLE regress_buffercache_superuser SUPERUSER;
+SET ROLE regress_buffercache_superuser;

Not sure what we gain by creating this role?

I missed that the role used for running the tests is already a superuser. Done.

v7 is attached. I only attached pg_buffercache_evict* patch to run it on cfbot.

--
Regards,
Nazir Bilal Yavuz
Microsoft

Attachments:

v7-0001-Add-pg_buffercache_evict_-relation-all-functions-.patchtext/x-patch; charset=US-ASCII; name=v7-0001-Add-pg_buffercache_evict_-relation-all-functions-.patchDownload
From 9e15f54dab47e93dd619a61b5b3eef389f819b34 Mon Sep 17 00:00:00 2001
From: Nazir Bilal Yavuz <byavuz81@gmail.com>
Date: Fri, 4 Apr 2025 13:22:00 +0300
Subject: [PATCH v7] Add pg_buffercache_evict_[relation | all]() functions for
 testing

pg_buffercache_evict() function shows buffer flushed information now.
This feature will be used in the following functions.

pg_buffercache_evict_relation(): Evicts all shared buffers in a
relation at once.
pg_buffercache_evict_all(): Evicts all shared buffers at once.

Both functions provide mechanism to evict multiple shared buffers at
once. They are designed to address the inefficiency of repeatedly calling
pg_buffercache_evict() for each individual buffer, which can be
time-consuming when dealing with large shared buffer pools.
(e.g., ~790ms vs. ~270ms for 16GB of shared buffers).

These functions are intended for developer testing and debugging
purposes and are available to superusers only.

Tests for this functions are added. Also, there was no test for
pg_buffercache_evict(), test for this added too.

Author: Nazir Bilal Yavuz <byavuz81@gmail.com>
Reviewed-by: Andres Freund <andres@anarazel.de>
Reviewed-by: Aidar Imamov <a.imamov@postgrespro.ru>
Reviewed-by: Joseph Koshakow <koshy44@gmail.com>
Discussion: https://postgr.es/m/CAN55FZ0h_YoSqqutxV6DES1RW8ig6wcA8CR9rJk358YRMxZFmw%40mail.gmail.com
---
 src/include/storage/bufmgr.h                  |   7 +-
 src/backend/storage/buffer/bufmgr.c           | 145 +++++++++++++++---
 src/test/modules/test_aio/test_aio.c          |   4 +-
 doc/src/sgml/pgbuffercache.sgml               |  73 ++++++++-
 contrib/pg_buffercache/Makefile               |   3 +-
 .../expected/pg_buffercache.out               |  61 ++++++++
 contrib/pg_buffercache/meson.build            |   1 +
 .../pg_buffercache--1.5--1.6.sql              |  24 +++
 contrib/pg_buffercache/pg_buffercache.control |   2 +-
 contrib/pg_buffercache/pg_buffercache_pages.c | 126 ++++++++++++++-
 contrib/pg_buffercache/sql/pg_buffercache.sql |  41 +++++
 11 files changed, 449 insertions(+), 38 deletions(-)
 create mode 100644 contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql

diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index f2192ceb271..29140264c24 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -304,7 +304,12 @@ extern uint32 GetAdditionalLocalPinLimit(void);
 extern void LimitAdditionalPins(uint32 *additional_pins);
 extern void LimitAdditionalLocalPins(uint32 *additional_pins);
 
-extern bool EvictUnpinnedBuffer(Buffer buf);
+extern bool EvictUnpinnedBuffer(Buffer buf, bool *flushed);
+extern void EvictAllUnpinnedBuffers(int32 *buffers_evicted,
+									int32 *buffers_flushed);
+extern void EvictRelUnpinnedBuffers(Relation rel, int32 *buffers_processed,
+									int32 *buffers_evicted,
+									int32 *buffers_flushed);
 
 /* in buf_init.c */
 extern void BufferManagerShmemInit(void);
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index ffaca5ee54d..a77f5c27940 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -6497,37 +6497,27 @@ ResOwnerPrintBufferPin(Datum res)
 }
 
 /*
- * Try to evict the current block in a shared buffer.
+ * Helper function to evict unpinned buffer whose buffer header lock is
+ * already acquired.
  *
  * This function is intended for testing/development use only!
- *
- * To succeed, the buffer must not be pinned on entry, so if the caller had a
- * particular block in mind, it might already have been replaced by some other
- * block by the time this function runs.  It's also unpinned on return, so the
- * buffer might be occupied again by the time control is returned, potentially
- * even by the same block.  This inherent raciness without other interlocking
- * makes the function unsuitable for non-testing usage.
- *
- * Returns true if the buffer was valid and it has now been made invalid.
- * Returns false if it wasn't valid, if it couldn't be evicted due to a pin,
- * or if the buffer becomes dirty again while we're trying to write it out.
  */
-bool
-EvictUnpinnedBuffer(Buffer buf)
+static bool
+EvictUnpinnedBufferInternal(Buffer buf, bool *flushed)
 {
 	BufferDesc *desc;
 	uint32		buf_state;
 	bool		result;
 
-	/* Make sure we can pin the buffer. */
-	ResourceOwnerEnlarge(CurrentResourceOwner);
-	ReservePrivateRefCountEntry();
+	if (flushed)
+		*flushed = false;
 
 	Assert(!BufferIsLocal(buf));
 	desc = GetBufferDescriptor(buf - 1);
 
-	/* Lock the header and check if it's valid. */
-	buf_state = LockBufHdr(desc);
+	buf_state = pg_atomic_read_u32(&(desc->state));
+	Assert(buf_state & BM_LOCKED);
+
 	if ((buf_state & BM_VALID) == 0)
 	{
 		UnlockBufHdr(desc, buf_state);
@@ -6548,6 +6538,8 @@ EvictUnpinnedBuffer(Buffer buf)
 	{
 		LWLockAcquire(BufferDescriptorGetContentLock(desc), LW_SHARED);
 		FlushBuffer(desc, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL);
+		if (flushed)
+			*flushed = true;
 		LWLockRelease(BufferDescriptorGetContentLock(desc));
 	}
 
@@ -6559,6 +6551,121 @@ EvictUnpinnedBuffer(Buffer buf)
 	return result;
 }
 
+/*
+ * Try to evict the current block in a shared buffer.
+ *
+ * This function is intended for testing/development use only!
+ *
+ * To succeed, the buffer must not be pinned on entry, so if the caller had a
+ * particular block in mind, it might already have been replaced by some other
+ * block by the time this function runs.  It's also unpinned on return, so the
+ * buffer might be occupied again by the time control is returned, potentially
+ * even by the same block.  This inherent raciness without other interlocking
+ * makes the function unsuitable for non-testing usage.
+ *
+ * *flushed is optional. If it is provided, it is set to true if the buffer
+ * was dirty and has been flushed.  However, this does not necessarily mean
+ * that we flushed the buffer, it could have been flushed by someone else.
+ *
+ * Returns true if the buffer was valid and it has now been made invalid.
+ * Returns false if it wasn't valid, if it couldn't be evicted due to a pin,
+ * or if the buffer becomes dirty again while we're trying to write it out.
+ */
+bool
+EvictUnpinnedBuffer(Buffer buf, bool *flushed)
+{
+	/* Make sure we can pin the buffer. */
+	ResourceOwnerEnlarge(CurrentResourceOwner);
+	ReservePrivateRefCountEntry();
+
+	Assert(!BufferIsLocal(buf));
+	LockBufHdr(GetBufferDescriptor(buf - 1));
+
+	return EvictUnpinnedBufferInternal(buf, flushed);
+}
+
+/*
+ * Try to evict all the shared buffers.
+ *
+ * This function is intended for testing/development use only!
+ *
+ * buffers_evicted and buffers_flushed are optional. If they are provided,
+ * they are set the total number of buffers evicted and flushed respectively.
+ */
+void
+EvictAllUnpinnedBuffers(int32 *buffers_evicted, int32 *buffers_flushed)
+{
+	if (buffers_evicted)
+		*buffers_evicted = 0;
+	if (buffers_flushed)
+		*buffers_flushed = 0;
+
+	for (int buf = 1; buf <= NBuffers; buf++)
+	{
+		bool		flushed;
+
+		if (EvictUnpinnedBuffer(buf, &flushed) && buffers_evicted)
+			(*buffers_evicted)++;
+		if (buffers_flushed && flushed)
+			(*buffers_flushed)++;
+	}
+}
+
+/*
+ * Try to evict all the shared buffers containing provided relation's pages.
+ *
+ * This function is intended for testing/development use only!
+ *
+ * The caller must hold at least AccessShareLock on the relation to prevent
+ * the relation from being dropped.
+ *
+ * buffers_processed, buffers_evicted and buffers_flushed are optional. If
+ * they are provided, they are set the total number of buffers processed,
+ * evicted and flushed respectively.
+ */
+void
+EvictRelUnpinnedBuffers(Relation rel, int32 *buffers_processed,
+						int32 *buffers_evicted, int32 *buffers_flushed)
+{
+	if (buffers_processed)
+		*buffers_processed = 0;
+	if (buffers_evicted)
+		*buffers_evicted = 0;
+	if (buffers_flushed)
+		*buffers_flushed = 0;
+
+	Assert(!RelationUsesLocalBuffers(rel));
+
+	for (int buf = 1; buf <= NBuffers; buf++)
+	{
+		uint32		buf_state = 0;
+		BufferDesc *bufHdr = GetBufferDescriptor(buf - 1);
+
+		/* An unlocked precheck should be safe and saves some cycles. */
+		if (!BufTagMatchesRelFileLocator(&bufHdr->tag, &rel->rd_locator))
+			continue;
+
+		/* Make sure we can pin the buffer. */
+		ResourceOwnerEnlarge(CurrentResourceOwner);
+		ReservePrivateRefCountEntry();
+
+		buf_state = LockBufHdr(bufHdr);
+		if (BufTagMatchesRelFileLocator(&bufHdr->tag, &rel->rd_locator))
+		{
+			bool		flushed;
+
+			if (EvictUnpinnedBufferInternal(buf, &flushed) && buffers_evicted)
+				(*buffers_evicted)++;
+			if (buffers_flushed && flushed)
+				(*buffers_flushed)++;
+			if (buffers_processed)
+				(*buffers_processed)++;
+		}
+		else
+			UnlockBufHdr(bufHdr, buf_state);
+	}
+}
+
 /*
  * Generic implementation of the AIO handle staging callback for readv/writev
  * on local/shared buffers.
diff --git a/src/test/modules/test_aio/test_aio.c b/src/test/modules/test_aio/test_aio.c
index bef0ecd9007..f9cf6ce8a26 100644
--- a/src/test/modules/test_aio/test_aio.c
+++ b/src/test/modules/test_aio/test_aio.c
@@ -237,7 +237,7 @@ modify_rel_block(PG_FUNCTION_ARGS)
 	if (BufferIsLocal(buf))
 		InvalidateLocalBuffer(GetLocalBufferDescriptor(-buf - 1), true);
 	else
-		EvictUnpinnedBuffer(buf);
+		EvictUnpinnedBuffer(buf, NULL);
 
 	/*
 	 * Now modify the page as asked for by the caller.
@@ -493,7 +493,7 @@ invalidate_rel_block(PG_FUNCTION_ARGS)
 
 			if (BufferIsLocal(buf))
 				InvalidateLocalBuffer(GetLocalBufferDescriptor(-buf - 1), true);
-			else if (!EvictUnpinnedBuffer(buf))
+			else if (!EvictUnpinnedBuffer(buf, NULL))
 				elog(ERROR, "couldn't evict");
 		}
 	}
diff --git a/doc/src/sgml/pgbuffercache.sgml b/doc/src/sgml/pgbuffercache.sgml
index 802a5112d77..c7e10f97ab6 100644
--- a/doc/src/sgml/pgbuffercache.sgml
+++ b/doc/src/sgml/pgbuffercache.sgml
@@ -27,12 +27,22 @@
   <primary>pg_buffercache_evict</primary>
  </indexterm>
 
+ <indexterm>
+  <primary>pg_buffercache_evict_relation</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>pg_buffercache_evict_all</primary>
+ </indexterm>
+
  <para>
   This module provides the <function>pg_buffercache_pages()</function>
   function (wrapped in the <structname>pg_buffercache</structname> view),
   the <function>pg_buffercache_summary()</function> function, the
-  <function>pg_buffercache_usage_counts()</function> function and
-  the <function>pg_buffercache_evict()</function> function.
+  <function>pg_buffercache_usage_counts()</function> function, the
+  <function>pg_buffercache_evict()</function>, the
+  <function>pg_buffercache_evict_relation()</function> function and the
+  <function>pg_buffercache_evict_all()</function> function.
  </para>
 
  <para>
@@ -65,6 +75,19 @@
   function is restricted to superusers only.
  </para>
 
+ <para>
+  The <function>pg_buffercache_evict_relation()</function> function allows all
+  unpinned shared buffers in the relation to be evicted from the buffer pool
+  given a relation identifier.  Use of this function is restricted to
+  superusers only.
+ </para>
+
+ <para>
+  The <function>pg_buffercache_evict_all()</function> function allows all
+  unpinned shared buffers to be evicted in the buffer pool.  Use of this
+  function is restricted to superusers only.
+ </para>
+
  <sect2 id="pgbuffercache-pg-buffercache">
   <title>The <structname>pg_buffercache</structname> View</title>
 
@@ -369,12 +392,46 @@
   <para>
    The <function>pg_buffercache_evict()</function> function takes a buffer
    identifier, as shown in the <structfield>bufferid</structfield> column of
-   the <structname>pg_buffercache</structname> view.  It returns true on success,
-   and false if the buffer wasn't valid, if it couldn't be evicted because it
-   was pinned, or if it became dirty again after an attempt to write it out.
-   The result is immediately out of date upon return, as the buffer might
-   become valid again at any time due to concurrent activity.  The function is
-   intended for developer testing only.
+   the <structname>pg_buffercache</structname> view.  It returns the
+   information about whether the buffer is evicted and flushed.  evicted
+   column is true on success, and false if the buffer wasn't valid, if it
+   couldn't be evicted because it was pinned, or if it became dirty again
+   after an attempt to write it out.  flushed column is true if the buffer is
+   flushed.  This does not necessarily mean that buffer is flushed by us, it
+   might be flushed by someone else.  The result is immediately out of date
+   upon return, as the buffer might become valid again at any time due to
+   concurrent activity. The function is intended for developer testing only.
+  </para>
+ </sect2>
+
+ <sect2 id="pgbuffercache-pg-buffercache-evict-relation">
+  <title>The <structname>pg_buffercache_evict_relation</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_evict_relation()</function> function is very
+   similar to the <function>pg_buffercache_evict()</function> function.  The
+   difference is that the <function>pg_buffercache_evict_relation()</function>
+   takes a relation identifier instead of buffer identifier.  Then, it tries
+   to evict all buffers for all forks in that relation.  It returns the number
+   of evicted and flushed buffers.  Flushed buffers aren't necessarily flushed
+   by us, they might be flushed by someone else.  The result is immediately
+   out of date upon return, as the buffer might become valid again at any time
+   due to concurrent activity.  The function is intended for developer testing
+   only.
+  </para>
+ </sect2>
+
+ <sect2 id="pgbuffercache-pg-buffercache-evict-all">
+  <title>The <structname>pg_buffercache_evict_all</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_evict_all()</function> function is very
+   similar to the <function>pg_buffercache_evict()</function> function.  The
+   difference is, the <function>pg_buffercache_evict_all()</function> function
+   does not take an argument; instead it tries to evict all buffers in the
+   buffer pool.  It returns the number of evicted and flushed buffers.
+   Flushed buffers aren't necessarily flushed by us, they might be flushed by
+   someone else.  The result is immediately out of date upon return, as the
+   buffer might become valid again at any time due to concurrent activity.
+   The function is intended for developer testing only.
   </para>
  </sect2>
 
diff --git a/contrib/pg_buffercache/Makefile b/contrib/pg_buffercache/Makefile
index eae65ead9e5..2a33602537e 100644
--- a/contrib/pg_buffercache/Makefile
+++ b/contrib/pg_buffercache/Makefile
@@ -8,7 +8,8 @@ OBJS = \
 EXTENSION = pg_buffercache
 DATA = pg_buffercache--1.2.sql pg_buffercache--1.2--1.3.sql \
 	pg_buffercache--1.1--1.2.sql pg_buffercache--1.0--1.1.sql \
-	pg_buffercache--1.3--1.4.sql pg_buffercache--1.4--1.5.sql
+	pg_buffercache--1.3--1.4.sql pg_buffercache--1.4--1.5.sql \
+	pg_buffercache--1.5--1.6.sql
 PGFILEDESC = "pg_buffercache - monitoring of shared buffer cache in real-time"
 
 REGRESS = pg_buffercache
diff --git a/contrib/pg_buffercache/expected/pg_buffercache.out b/contrib/pg_buffercache/expected/pg_buffercache.out
index b745dc69eae..b44af0fe21e 100644
--- a/contrib/pg_buffercache/expected/pg_buffercache.out
+++ b/contrib/pg_buffercache/expected/pg_buffercache.out
@@ -55,3 +55,64 @@ SELECT count(*) > 0 FROM pg_buffercache_usage_counts();
  t
 (1 row)
 
+RESET role;
+------
+---- Test pg_buffercache_evict* functions
+------
+CREATE ROLE regress_buffercache_normal;
+SET ROLE regress_buffercache_normal;
+-- These should fail because these are STRICT functions
+SELECT * FROM pg_buffercache_evict();
+ERROR:  function pg_buffercache_evict() does not exist
+LINE 1: SELECT * FROM pg_buffercache_evict();
+                      ^
+HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+SELECT * FROM pg_buffercache_evict_relation();
+ERROR:  function pg_buffercache_evict_relation() does not exist
+LINE 1: SELECT * FROM pg_buffercache_evict_relation();
+                      ^
+HINT:  No function matches the given name and argument types. You might need to add explicit type casts.
+-- These should fail because they need to be called as SUPERUSER
+SELECT * FROM pg_buffercache_evict(1);
+ERROR:  must be superuser to use pg_buffercache_evict()
+SELECT * FROM pg_buffercache_evict_relation(1);
+ERROR:  must be superuser to use pg_buffercache_evict_relation()
+SELECT * FROM pg_buffercache_evict_all();
+ERROR:  must be superuser to use pg_buffercache_evict_all()
+RESET ROLE;
+-- These should fail because they are not called by valid range of buffers
+-- Number of the shared buffers are limited by max integer
+SELECT 2147483647 max_buffers \gset
+SELECT * FROM pg_buffercache_evict(0);
+ERROR:  bad buffer ID: 0
+SELECT * FROM pg_buffercache_evict(:max_buffers);
+ERROR:  bad buffer ID: 2147483647
+-- This should fail because pg_buffercache_evict_relation() doesn't accept
+-- local relations
+CREATE TEMP TABLE temp_pg_buffercache();
+SELECT * FROM pg_buffercache_evict_relation('temp_pg_buffercache');
+ERROR:  relation uses local buffers, pg_buffercache_evict_relation() is intended to be used for shared buffers only
+DROP TABLE temp_pg_buffercache;
+-- These shouldn't fail
+SELECT COUNT(*) > 0 FROM pg_buffercache_evict(1);
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT COUNT(*) > 0 FROM pg_buffercache_evict_all();
+ ?column? 
+----------
+ t
+(1 row)
+
+CREATE TABLE shared_pg_buffercache();
+SELECT COUNT(*) > 0 FROM pg_buffercache_evict_relation('shared_pg_buffercache');
+ ?column? 
+----------
+ t
+(1 row)
+
+DROP TABLE shared_pg_buffercache;
+RESET ROLE;
+DROP ROLE regress_buffercache_normal;
diff --git a/contrib/pg_buffercache/meson.build b/contrib/pg_buffercache/meson.build
index 12d1fe48717..9b2e9393410 100644
--- a/contrib/pg_buffercache/meson.build
+++ b/contrib/pg_buffercache/meson.build
@@ -23,6 +23,7 @@ install_data(
   'pg_buffercache--1.2.sql',
   'pg_buffercache--1.3--1.4.sql',
   'pg_buffercache--1.4--1.5.sql',
+  'pg_buffercache--1.5--1.6.sql',
   'pg_buffercache.control',
   kwargs: contrib_data_args,
 )
diff --git a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
new file mode 100644
index 00000000000..e37fa66930c
--- /dev/null
+++ b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
@@ -0,0 +1,24 @@
+\echo Use "ALTER EXTENSION pg_buffercache UPDATE TO '1.6'" to load this file. \quit
+
+DROP FUNCTION pg_buffercache_evict(integer);
+CREATE FUNCTION pg_buffercache_evict(
+    IN int,
+    OUT evicted boolean,
+    OUT flushed boolean)
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict'
+LANGUAGE C PARALLEL SAFE VOLATILE STRICT;
+
+CREATE FUNCTION pg_buffercache_evict_relation(
+    IN regclass,
+    OUT buffers_processed int4,
+    OUT buffers_evicted int4,
+    OUT buffers_flushed int4)
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict_relation'
+LANGUAGE C PARALLEL SAFE VOLATILE STRICT;
+
+CREATE FUNCTION pg_buffercache_evict_all(
+    OUT buffers_processed int4,
+    OUT buffers_evicted int4,
+    OUT buffers_flushed int4)
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict_all'
+LANGUAGE C PARALLEL SAFE VOLATILE;
diff --git a/contrib/pg_buffercache/pg_buffercache.control b/contrib/pg_buffercache/pg_buffercache.control
index 5ee875f77dd..b030ba3a6fa 100644
--- a/contrib/pg_buffercache/pg_buffercache.control
+++ b/contrib/pg_buffercache/pg_buffercache.control
@@ -1,5 +1,5 @@
 # pg_buffercache extension
 comment = 'examine the shared buffer cache'
-default_version = '1.5'
+default_version = '1.6'
 module_pathname = '$libdir/pg_buffercache'
 relocatable = true
diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index 62602af1775..3fe2dbca4bb 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -9,16 +9,21 @@
 #include "postgres.h"
 
 #include "access/htup_details.h"
+#include "access/relation.h"
 #include "catalog/pg_type.h"
 #include "funcapi.h"
 #include "storage/buf_internals.h"
 #include "storage/bufmgr.h"
+#include "utils/rel.h"
 
 
 #define NUM_BUFFERCACHE_PAGES_MIN_ELEM	8
 #define NUM_BUFFERCACHE_PAGES_ELEM	9
 #define NUM_BUFFERCACHE_SUMMARY_ELEM 5
 #define NUM_BUFFERCACHE_USAGE_COUNTS_ELEM 4
+#define NUM_BUFFERCACHE_EVICT_ELEM 2
+#define NUM_BUFFERCACHE_EVICT_RELATION_ELEM 3
+#define NUM_BUFFERCACHE_EVICT_ALL_ELEM 3
 
 PG_MODULE_MAGIC_EXT(
 					.name = "pg_buffercache",
@@ -67,6 +72,8 @@ PG_FUNCTION_INFO_V1(pg_buffercache_pages);
 PG_FUNCTION_INFO_V1(pg_buffercache_summary);
 PG_FUNCTION_INFO_V1(pg_buffercache_usage_counts);
 PG_FUNCTION_INFO_V1(pg_buffercache_evict);
+PG_FUNCTION_INFO_V1(pg_buffercache_evict_relation);
+PG_FUNCTION_INFO_V1(pg_buffercache_evict_all);
 
 Datum
 pg_buffercache_pages(PG_FUNCTION_ARGS)
@@ -352,21 +359,128 @@ pg_buffercache_usage_counts(PG_FUNCTION_ARGS)
 	return (Datum) 0;
 }
 
+/*
+ * Helper function to check if the user has superuser priveleges.
+ */
+static void
+pg_buffercache_superuser_check(char *func_name)
+{
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use %s()",
+						func_name)));
+}
+
 /*
  * Try to evict a shared buffer.
  */
 Datum
 pg_buffercache_evict(PG_FUNCTION_ARGS)
 {
-	Buffer		buf = PG_GETARG_INT32(0);
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_EVICT_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_EVICT_ELEM] = {0};
 
-	if (!superuser())
-		ereport(ERROR,
-				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-				 errmsg("must be superuser to use pg_buffercache_evict function")));
+	Buffer		buf = PG_GETARG_INT32(0);
+	bool		flushed;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	pg_buffercache_superuser_check("pg_buffercache_evict");
 
 	if (buf < 1 || buf > NBuffers)
 		elog(ERROR, "bad buffer ID: %d", buf);
 
-	PG_RETURN_BOOL(EvictUnpinnedBuffer(buf));
+	values[0] = BoolGetDatum(EvictUnpinnedBuffer(buf, &flushed));
+	values[1] = BoolGetDatum(flushed);
+
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
+
+/*
+ * Try to evict specified relation.
+ */
+Datum
+pg_buffercache_evict_relation(PG_FUNCTION_ARGS)
+{
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_EVICT_RELATION_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_EVICT_RELATION_ELEM] = {0};
+
+	Oid			relOid;
+	Relation	rel;
+	int32		buffers_processed = 0;
+	int32		buffers_evicted = 0;
+	int32		buffers_flushed = 0;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	pg_buffercache_superuser_check("pg_buffercache_evict_relation");
+
+	relOid = PG_GETARG_OID(0);
+
+	rel = relation_open(relOid, AccessShareLock);
+
+	if (RelationUsesLocalBuffers(rel))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relation uses local buffers, %s() is intended to be used for shared buffers only",
+						"pg_buffercache_evict_relation")));
+
+	EvictRelUnpinnedBuffers(rel, &buffers_processed, &buffers_evicted,
+							&buffers_flushed);
+
+	relation_close(rel, AccessShareLock);
+
+	values[0] = Int32GetDatum(buffers_processed);
+	values[1] = Int32GetDatum(buffers_evicted);
+	values[2] = Int32GetDatum(buffers_flushed);
+
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
+
+
+/*
+ * Try to evict all shared buffers.
+ */
+Datum
+pg_buffercache_evict_all(PG_FUNCTION_ARGS)
+{
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_EVICT_ALL_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_EVICT_ALL_ELEM] = {0};
+
+	int32		buffers_evicted = 0;
+	int32		buffers_flushed = 0;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	pg_buffercache_superuser_check("pg_buffercache_evict_all");
+
+	EvictAllUnpinnedBuffers(&buffers_evicted, &buffers_flushed);
+
+	values[0] = Int32GetDatum(NBuffers);
+	values[1] = Int32GetDatum(buffers_evicted);
+	values[2] = Int32GetDatum(buffers_flushed);
+
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
 }
diff --git a/contrib/pg_buffercache/sql/pg_buffercache.sql b/contrib/pg_buffercache/sql/pg_buffercache.sql
index 944fbb1beae..2c4bd8c1e9e 100644
--- a/contrib/pg_buffercache/sql/pg_buffercache.sql
+++ b/contrib/pg_buffercache/sql/pg_buffercache.sql
@@ -26,3 +26,44 @@ SET ROLE pg_monitor;
 SELECT count(*) > 0 FROM pg_buffercache;
 SELECT buffers_used + buffers_unused > 0 FROM pg_buffercache_summary();
 SELECT count(*) > 0 FROM pg_buffercache_usage_counts();
+RESET role;
+
+------
+---- Test pg_buffercache_evict* functions
+------
+
+CREATE ROLE regress_buffercache_normal;
+SET ROLE regress_buffercache_normal;
+
+-- These should fail because these are STRICT functions
+SELECT * FROM pg_buffercache_evict();
+SELECT * FROM pg_buffercache_evict_relation();
+
+-- These should fail because they need to be called as SUPERUSER
+SELECT * FROM pg_buffercache_evict(1);
+SELECT * FROM pg_buffercache_evict_relation(1);
+SELECT * FROM pg_buffercache_evict_all();
+
+RESET ROLE;
+
+-- These should fail because they are not called by valid range of buffers
+-- Number of the shared buffers are limited by max integer
+SELECT 2147483647 max_buffers \gset
+SELECT * FROM pg_buffercache_evict(0);
+SELECT * FROM pg_buffercache_evict(:max_buffers);
+
+-- This should fail because pg_buffercache_evict_relation() doesn't accept
+-- local relations
+CREATE TEMP TABLE temp_pg_buffercache();
+SELECT * FROM pg_buffercache_evict_relation('temp_pg_buffercache');
+DROP TABLE temp_pg_buffercache;
+
+-- These shouldn't fail
+SELECT COUNT(*) > 0 FROM pg_buffercache_evict(1);
+SELECT COUNT(*) > 0 FROM pg_buffercache_evict_all();
+CREATE TABLE shared_pg_buffercache();
+SELECT COUNT(*) > 0 FROM pg_buffercache_evict_relation('shared_pg_buffercache');
+DROP TABLE shared_pg_buffercache;
+
+RESET ROLE;
+DROP ROLE regress_buffercache_normal;
-- 
2.49.0

#20Andres Freund
andres@anarazel.de
In reply to: Nazir Bilal Yavuz (#19)
1 attachment(s)
Re: Add pg_buffercache_evict_all() and pg_buffercache_mark_dirty[_all]() functions

Hi,

On 2025-04-07 19:37:50 +0300, Nazir Bilal Yavuz wrote:

+ relation_close(rel, AccessExclusiveLock);

Hm. Why are we dropping the lock here early? It's probably ok, but it's not
clear to me why we should do so.

We are dropping the lock after we processed the relation. I didn't
understand what could be the problem here. Why do you think it is
early?

Most commonly we close relations without releasing the lock, instead relying
on the lock being released at the end of the transaction.

I see. I was looking at pg_prewarm as an example and copied it from there.

I don't think we're particularly consistent about it. And I think there's some
differing views about what precisely the right behaviour is...

I've tried to polish the patch. Changes I made:

- The number of processed buffers for EvictAllUnpinnedBuffers() was alwasy
NBuffers, that didn't seem right. But that's obsoleted by the next point:

- I think it'd be more useful to return the number of skipped buffers,
i.e. buffers that could not be evicted, than the number of processed
buffers.

I'm not_evicted or such would also work.

- EvictAllUnpinnedBuffers() did not check whether the buffer was valid before
locking the buffer, which made it a fair bit more expensive than
EvictRelUnpinnedBuffers, which kinda has such a check via the buffer tag
check.

That sped up EvictAllUnpinnedBuffers() 3x when using a cluster with mostly
unused shared buffers.

- The optional pointer arguments made the code look a bit ugly. I made them
mandatory now.

- I made EvictUnpinnedBufferInternal()'s argument the BufferDesc, rather than
the Buffer.

- The tests for STRICTness worked, they just errored out because there isn't a
function of the relevant names without arguments. I called them with NULL.

- The count(*) tests would have succeeded even if the call had "failed" due to
STRICTness. I used <colname> IS NOT NULL instead.

- rebased over the other pg_buffercache changes

Other points:

- I don't love the buffers_ prefix for the column names / C function
arguments. Seems long. It seems particularly weird because
pg_buffercache_evict() doesn't have a buffer_ prefix.

I left it as-is, but I think something perhaps ought to change before
commit.

Otoh, pg_buffercache_summary() and pg_buffercache_usage_counts() already
inconsistent in a similar way with each other.

- Arguably these functions ought to check BM_TAG_VALID, not BM_VALID. But that
only rather rarely happens when there are no pins. Since this is a
pre-existing pattern, I left it alone.

- The documentation format of the functions isn't quite what we usually do (a
table documenting the columns returned by a function with multiple columns),
but otoh, these are really developer oriented functions, so spending 30
lines of a <table> on each of these functions feels a bit silly.

I'd be ok with it as-is.

- The docs for pg_buffercache_evict() don't quite sound right to me, there's
some oddity in the phrasing. Nothing too bad, but perhaps worht a small bit
of additional polish.

Greetings,

Andres Freund

Attachments:

v8-0001-Add-pg_buffercache_evict_-relation-all-functions.patchtext/x-diff; charset=us-asciiDownload
From dd4eae37f3610e0afe82a19efae015abc8e9c6a6 Mon Sep 17 00:00:00 2001
From: Andres Freund <andres@anarazel.de>
Date: Mon, 7 Apr 2025 19:21:37 -0400
Subject: [PATCH v8] Add pg_buffercache_evict_{relation,all} functions

In addition to the added functions, the pg_buffercache_evict() function now
shows whether the buffer was flushed.

pg_buffercache_evict_relation(): Evicts all shared buffers in a
relation at once.
pg_buffercache_evict_all(): Evicts all shared buffers at once.

Both functions provide mechanism to evict multiple shared buffers at
once. They are designed to address the inefficiency of repeatedly calling
pg_buffercache_evict() for each individual buffer, which can be time-consuming
when dealing with large shared buffer pools. (e.g., ~477ms vs. ~2576ms for
16GB of fully populated shared buffers).

These functions are intended for developer testing and debugging
purposes and are available to superusers only.

Minimal tests for the new functions are included. Also, there was no test for
pg_buffercache_evict(), test for this added too.

No new extension version is needed, as it was already increased this release
by ba2a3c2302f.

Author: Nazir Bilal Yavuz <byavuz81@gmail.com>
Reviewed-by: Andres Freund <andres@anarazel.de>
Reviewed-by: Aidar Imamov <a.imamov@postgrespro.ru>
Reviewed-by: Joseph Koshakow <koshy44@gmail.com>
Discussion: https://postgr.es/m/CAN55FZ0h_YoSqqutxV6DES1RW8ig6wcA8CR9rJk358YRMxZFmw%40mail.gmail.com
---
 src/include/storage/bufmgr.h                  |   9 +-
 src/backend/storage/buffer/bufmgr.c           | 178 +++++++++++++++---
 .../expected/pg_buffercache.out               |  64 +++++++
 .../pg_buffercache--1.5--1.6.sql              |  24 +++
 contrib/pg_buffercache/pg_buffercache_pages.c | 127 ++++++++++++-
 contrib/pg_buffercache/sql/pg_buffercache.sql |  42 +++++
 doc/src/sgml/pgbuffercache.sgml               |  72 ++++++-
 src/test/modules/test_aio/test_aio.c          |   6 +-
 8 files changed, 481 insertions(+), 41 deletions(-)

diff --git a/src/include/storage/bufmgr.h b/src/include/storage/bufmgr.h
index f2192ceb271..235b6f18962 100644
--- a/src/include/storage/bufmgr.h
+++ b/src/include/storage/bufmgr.h
@@ -304,7 +304,14 @@ extern uint32 GetAdditionalLocalPinLimit(void);
 extern void LimitAdditionalPins(uint32 *additional_pins);
 extern void LimitAdditionalLocalPins(uint32 *additional_pins);
 
-extern bool EvictUnpinnedBuffer(Buffer buf);
+extern bool EvictUnpinnedBuffer(Buffer buf, bool *flushed);
+extern void EvictAllUnpinnedBuffers(int32 *buffers_evicted,
+									int32 *buffers_flushed,
+									int32 *buffers_skipped);
+extern void EvictRelUnpinnedBuffers(Relation rel,
+									int32 *buffers_evicted,
+									int32 *buffers_flushed,
+									int32 *buffers_skipped);
 
 /* in buf_init.c */
 extern void BufferManagerShmemInit(void);
diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c
index 941d7fa3d94..2c29af36159 100644
--- a/src/backend/storage/buffer/bufmgr.c
+++ b/src/backend/storage/buffer/bufmgr.c
@@ -6509,6 +6509,53 @@ ResOwnerPrintBufferPin(Datum res)
 	return DebugPrintBufferRefcount(DatumGetInt32(res));
 }
 
+/*
+ * Helper function to evict unpinned buffer whose buffer header lock is
+ * already acquired.
+ */
+static bool
+EvictUnpinnedBufferInternal(BufferDesc *desc, bool *flushed)
+{
+	uint32		buf_state;
+	bool		result;
+
+	*flushed = false;
+
+	buf_state = pg_atomic_read_u32(&(desc->state));
+	Assert(buf_state & BM_LOCKED);
+
+	if ((buf_state & BM_VALID) == 0)
+	{
+		UnlockBufHdr(desc, buf_state);
+		return false;
+	}
+
+	/* Check that it's not pinned already. */
+	if (BUF_STATE_GET_REFCOUNT(buf_state) > 0)
+	{
+		UnlockBufHdr(desc, buf_state);
+		return false;
+	}
+
+	PinBuffer_Locked(desc);		/* releases spinlock */
+
+	/* If it was dirty, try to clean it once. */
+	if (buf_state & BM_DIRTY)
+	{
+		LWLockAcquire(BufferDescriptorGetContentLock(desc), LW_SHARED);
+		FlushBuffer(desc, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL);
+		*flushed = true;
+		LWLockRelease(BufferDescriptorGetContentLock(desc));
+	}
+
+	/* This will return false if it becomes dirty or someone else pins it. */
+	result = InvalidateVictimBuffer(desc);
+
+	UnpinBuffer(desc);
+
+	return result;
+}
+
 /*
  * Try to evict the current block in a shared buffer.
  *
@@ -6521,55 +6568,134 @@ ResOwnerPrintBufferPin(Datum res)
  * even by the same block.  This inherent raciness without other interlocking
  * makes the function unsuitable for non-testing usage.
  *
+ * *flushed is set to true if the buffer was dirty and has been flushed, false
+ * otherwise.  However, *flushed=true does not necessarily mean that we
+ * flushed the buffer, it could have been flushed by someone else.
+ *
  * Returns true if the buffer was valid and it has now been made invalid.
  * Returns false if it wasn't valid, if it couldn't be evicted due to a pin,
  * or if the buffer becomes dirty again while we're trying to write it out.
  */
 bool
-EvictUnpinnedBuffer(Buffer buf)
+EvictUnpinnedBuffer(Buffer buf, bool *flushed)
 {
 	BufferDesc *desc;
-	uint32		buf_state;
-	bool		result;
+
+	Assert(BufferIsValid(buf) && !BufferIsLocal(buf));
 
 	/* Make sure we can pin the buffer. */
 	ResourceOwnerEnlarge(CurrentResourceOwner);
 	ReservePrivateRefCountEntry();
 
-	Assert(!BufferIsLocal(buf));
 	desc = GetBufferDescriptor(buf - 1);
+	LockBufHdr(desc);
 
-	/* Lock the header and check if it's valid. */
-	buf_state = LockBufHdr(desc);
-	if ((buf_state & BM_VALID) == 0)
-	{
-		UnlockBufHdr(desc, buf_state);
-		return false;
-	}
+	return EvictUnpinnedBufferInternal(desc, flushed);
+}
 
-	/* Check that it's not pinned already. */
-	if (BUF_STATE_GET_REFCOUNT(buf_state) > 0)
+/*
+ * Try to evict all the shared buffers.
+ *
+ * This function is intended for testing/development use only! See
+ * EvictUnpinnedBuffer().
+ *
+ * The buffers_* parameters are mandatory and indicate the total count of
+ * buffers that:
+ * - buffers_evicted - were evicted
+ * - buffers_flushed - were flushed
+ * - buffers_skipped - could not be evicted
+ */
+void
+EvictAllUnpinnedBuffers(int32 *buffers_evicted, int32 *buffers_flushed,
+						int32 *buffers_skipped)
+{
+	*buffers_evicted = 0;
+	*buffers_skipped = 0;
+	*buffers_flushed = 0;
+
+	for (int buf = 1; buf <= NBuffers; buf++)
 	{
-		UnlockBufHdr(desc, buf_state);
-		return false;
+		BufferDesc *desc = GetBufferDescriptor(buf - 1);
+		uint32		buf_state;
+		bool		flushed;
+
+		buf_state = pg_atomic_read_u32(&desc->state);
+		if (!(buf_state & BM_VALID))
+			continue;
+
+		ResourceOwnerEnlarge(CurrentResourceOwner);
+		ReservePrivateRefCountEntry();
+
+		LockBufHdr(desc);
+
+		if (EvictUnpinnedBufferInternal(desc, &flushed))
+			(*buffers_evicted)++;
+		else
+			(*buffers_skipped)++;
+
+		if (flushed)
+			(*buffers_flushed)++;
 	}
+}
+
+/*
+ * Try to evict all the shared buffers containing provided relation's pages.
+ *
+ * This function is intended for testing/development use only! See
+ * EvictUnpinnedBuffer().
+ *
+ * The caller must hold at least AccessShareLock on the relation to prevent
+ * the relation from being dropped.
+ *
+ * The buffers_* parameters are mandatory and indicate the total count of
+ * buffers that:
+ * - buffers_evicted - were evicted
+ * - buffers_flushed - were flushed
+ * - buffers_skipped - could not be evicted
+ */
+void
+EvictRelUnpinnedBuffers(Relation rel, int32 *buffers_evicted,
+						int32 *buffers_flushed, int32 *buffers_skipped)
+{
+	Assert(!RelationUsesLocalBuffers(rel));
 
-	PinBuffer_Locked(desc);		/* releases spinlock */
+	*buffers_skipped = 0;
+	*buffers_evicted = 0;
+	*buffers_flushed = 0;
 
-	/* If it was dirty, try to clean it once. */
-	if (buf_state & BM_DIRTY)
+	for (int buf = 1; buf <= NBuffers; buf++)
 	{
-		LWLockAcquire(BufferDescriptorGetContentLock(desc), LW_SHARED);
-		FlushBuffer(desc, NULL, IOOBJECT_RELATION, IOCONTEXT_NORMAL);
-		LWLockRelease(BufferDescriptorGetContentLock(desc));
-	}
+		BufferDesc *desc = GetBufferDescriptor(buf - 1);
+		uint32		buf_state = pg_atomic_read_u32(&(desc->state));
+		bool		flushed;
+
+		/* An unlocked precheck should be safe and saves some cycles. */
+		if ((buf_state & BM_VALID) == 0 ||
+			!BufTagMatchesRelFileLocator(&desc->tag, &rel->rd_locator))
+			continue;
 
-	/* This will return false if it becomes dirty or someone else pins it. */
-	result = InvalidateVictimBuffer(desc);
+		/* Make sure we can pin the buffer. */
+		ResourceOwnerEnlarge(CurrentResourceOwner);
+		ReservePrivateRefCountEntry();
 
-	UnpinBuffer(desc);
+		buf_state = LockBufHdr(desc);
 
-	return result;
+		/* recheck, could have changed without the lock */
+		if ((buf_state & BM_VALID) == 0 ||
+			!BufTagMatchesRelFileLocator(&desc->tag, &rel->rd_locator))
+		{
+			UnlockBufHdr(desc, buf_state);
+			continue;
+		}
+
+		if (EvictUnpinnedBufferInternal(desc, &flushed))
+			(*buffers_evicted)++;
+		else
+			(*buffers_skipped)++;
+
+		if (flushed)
+			(*buffers_flushed)++;
+	}
 }
 
 /*
diff --git a/contrib/pg_buffercache/expected/pg_buffercache.out b/contrib/pg_buffercache/expected/pg_buffercache.out
index b745dc69eae..ba6fc14044d 100644
--- a/contrib/pg_buffercache/expected/pg_buffercache.out
+++ b/contrib/pg_buffercache/expected/pg_buffercache.out
@@ -55,3 +55,67 @@ SELECT count(*) > 0 FROM pg_buffercache_usage_counts();
  t
 (1 row)
 
+RESET role;
+------
+---- Test pg_buffercache_evict* functions
+------
+CREATE ROLE regress_buffercache_normal;
+SET ROLE regress_buffercache_normal;
+-- These should fail because they need to be called as SUPERUSER
+SELECT * FROM pg_buffercache_evict(1);
+ERROR:  must be superuser to use pg_buffercache_evict()
+SELECT * FROM pg_buffercache_evict_relation(1);
+ERROR:  must be superuser to use pg_buffercache_evict_relation()
+SELECT * FROM pg_buffercache_evict_all();
+ERROR:  must be superuser to use pg_buffercache_evict_all()
+RESET ROLE;
+-- These should return nothing, because these are STRICT functions
+SELECT * FROM pg_buffercache_evict(NULL);
+ evicted | flushed 
+---------+---------
+         | 
+(1 row)
+
+SELECT * FROM pg_buffercache_evict_relation(NULL);
+ buffers_evicted | buffers_flushed | buffers_skipped 
+-----------------+-----------------+-----------------
+                 |                 |                
+(1 row)
+
+-- These should fail because they are not called by valid range of buffers
+-- Number of the shared buffers are limited by max integer
+SELECT 2147483647 max_buffers \gset
+SELECT * FROM pg_buffercache_evict(-1);
+ERROR:  bad buffer ID: -1
+SELECT * FROM pg_buffercache_evict(0);
+ERROR:  bad buffer ID: 0
+SELECT * FROM pg_buffercache_evict(:max_buffers);
+ERROR:  bad buffer ID: 2147483647
+-- This should fail because pg_buffercache_evict_relation() doesn't accept
+-- local relations
+CREATE TEMP TABLE temp_pg_buffercache();
+SELECT * FROM pg_buffercache_evict_relation('temp_pg_buffercache');
+ERROR:  relation uses local buffers, pg_buffercache_evict_relation() is intended to be used for shared buffers only
+DROP TABLE temp_pg_buffercache;
+-- These shouldn't fail
+SELECT evicted IS NOT NULL FROM pg_buffercache_evict(1);
+ ?column? 
+----------
+ t
+(1 row)
+
+SELECT buffers_evicted IS NOT NULL FROM pg_buffercache_evict_all();
+ ?column? 
+----------
+ t
+(1 row)
+
+CREATE TABLE shared_pg_buffercache();
+SELECT buffers_evicted IS NOT NULL FROM pg_buffercache_evict_relation('shared_pg_buffercache');
+ ?column? 
+----------
+ t
+(1 row)
+
+DROP TABLE shared_pg_buffercache;
+DROP ROLE regress_buffercache_normal;
diff --git a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
index f6668e41b37..b7ac7daecae 100644
--- a/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
+++ b/contrib/pg_buffercache/pg_buffercache--1.5--1.6.sql
@@ -20,3 +20,27 @@ REVOKE ALL ON pg_buffercache_numa FROM PUBLIC;
 
 GRANT EXECUTE ON FUNCTION pg_buffercache_numa_pages() TO pg_monitor;
 GRANT SELECT ON pg_buffercache_numa TO pg_monitor;
+
+
+DROP FUNCTION pg_buffercache_evict(integer);
+CREATE FUNCTION pg_buffercache_evict(
+    IN int,
+    OUT evicted boolean,
+    OUT flushed boolean)
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict'
+LANGUAGE C PARALLEL SAFE VOLATILE STRICT;
+
+CREATE FUNCTION pg_buffercache_evict_relation(
+    IN regclass,
+    OUT buffers_evicted int4,
+    OUT buffers_flushed int4,
+    OUT buffers_skipped int4)
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict_relation'
+LANGUAGE C PARALLEL SAFE VOLATILE STRICT;
+
+CREATE FUNCTION pg_buffercache_evict_all(
+    OUT buffers_evicted int4,
+    OUT buffers_flushed int4,
+    OUT buffers_skipped int4)
+AS 'MODULE_PATHNAME', 'pg_buffercache_evict_all'
+LANGUAGE C PARALLEL SAFE VOLATILE;
diff --git a/contrib/pg_buffercache/pg_buffercache_pages.c b/contrib/pg_buffercache/pg_buffercache_pages.c
index a702a47efe9..6cff2b5f8c1 100644
--- a/contrib/pg_buffercache/pg_buffercache_pages.c
+++ b/contrib/pg_buffercache/pg_buffercache_pages.c
@@ -9,17 +9,22 @@
 #include "postgres.h"
 
 #include "access/htup_details.h"
+#include "access/relation.h"
 #include "catalog/pg_type.h"
 #include "funcapi.h"
 #include "port/pg_numa.h"
 #include "storage/buf_internals.h"
 #include "storage/bufmgr.h"
+#include "utils/rel.h"
 
 
 #define NUM_BUFFERCACHE_PAGES_MIN_ELEM	8
 #define NUM_BUFFERCACHE_PAGES_ELEM	9
 #define NUM_BUFFERCACHE_SUMMARY_ELEM 5
 #define NUM_BUFFERCACHE_USAGE_COUNTS_ELEM 4
+#define NUM_BUFFERCACHE_EVICT_ELEM 2
+#define NUM_BUFFERCACHE_EVICT_RELATION_ELEM 3
+#define NUM_BUFFERCACHE_EVICT_ALL_ELEM 3
 
 #define NUM_BUFFERCACHE_NUMA_ELEM	3
 
@@ -93,6 +98,8 @@ PG_FUNCTION_INFO_V1(pg_buffercache_numa_pages);
 PG_FUNCTION_INFO_V1(pg_buffercache_summary);
 PG_FUNCTION_INFO_V1(pg_buffercache_usage_counts);
 PG_FUNCTION_INFO_V1(pg_buffercache_evict);
+PG_FUNCTION_INFO_V1(pg_buffercache_evict_relation);
+PG_FUNCTION_INFO_V1(pg_buffercache_evict_all);
 
 
 /* Only need to touch memory once per backend process lifetime */
@@ -637,21 +644,131 @@ pg_buffercache_usage_counts(PG_FUNCTION_ARGS)
 	return (Datum) 0;
 }
 
+/*
+ * Helper function to check if the user has superuser privileges.
+ */
+static void
+pg_buffercache_superuser_check(char *func_name)
+{
+	if (!superuser())
+		ereport(ERROR,
+				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
+				 errmsg("must be superuser to use %s()",
+						func_name)));
+}
+
 /*
  * Try to evict a shared buffer.
  */
 Datum
 pg_buffercache_evict(PG_FUNCTION_ARGS)
 {
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_EVICT_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_EVICT_ELEM] = {0};
+
 	Buffer		buf = PG_GETARG_INT32(0);
+	bool		flushed;
 
-	if (!superuser())
-		ereport(ERROR,
-				(errcode(ERRCODE_INSUFFICIENT_PRIVILEGE),
-				 errmsg("must be superuser to use pg_buffercache_evict function")));
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	pg_buffercache_superuser_check("pg_buffercache_evict");
 
 	if (buf < 1 || buf > NBuffers)
 		elog(ERROR, "bad buffer ID: %d", buf);
 
-	PG_RETURN_BOOL(EvictUnpinnedBuffer(buf));
+	values[0] = BoolGetDatum(EvictUnpinnedBuffer(buf, &flushed));
+	values[1] = BoolGetDatum(flushed);
+
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
+
+/*
+ * Try to evict specified relation.
+ */
+Datum
+pg_buffercache_evict_relation(PG_FUNCTION_ARGS)
+{
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_EVICT_RELATION_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_EVICT_RELATION_ELEM] = {0};
+
+	Oid			relOid;
+	Relation	rel;
+
+	int32		buffers_evicted = 0;
+	int32		buffers_flushed = 0;
+	int32		buffers_skipped = 0;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	pg_buffercache_superuser_check("pg_buffercache_evict_relation");
+
+	relOid = PG_GETARG_OID(0);
+
+	rel = relation_open(relOid, AccessShareLock);
+
+	if (RelationUsesLocalBuffers(rel))
+		ereport(ERROR,
+				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
+				 errmsg("relation uses local buffers, %s() is intended to be used for shared buffers only",
+						"pg_buffercache_evict_relation")));
+
+	EvictRelUnpinnedBuffers(rel, &buffers_evicted, &buffers_flushed,
+							&buffers_skipped);
+
+	relation_close(rel, AccessShareLock);
+
+	values[0] = Int32GetDatum(buffers_evicted);
+	values[1] = Int32GetDatum(buffers_flushed);
+	values[2] = Int32GetDatum(buffers_skipped);
+
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
+}
+
+
+/*
+ * Try to evict all shared buffers.
+ */
+Datum
+pg_buffercache_evict_all(PG_FUNCTION_ARGS)
+{
+	Datum		result;
+	TupleDesc	tupledesc;
+	HeapTuple	tuple;
+	Datum		values[NUM_BUFFERCACHE_EVICT_ALL_ELEM];
+	bool		nulls[NUM_BUFFERCACHE_EVICT_ALL_ELEM] = {0};
+
+	int32		buffers_evicted = 0;
+	int32		buffers_flushed = 0;
+	int32		buffers_skipped = 0;
+
+	if (get_call_result_type(fcinfo, NULL, &tupledesc) != TYPEFUNC_COMPOSITE)
+		elog(ERROR, "return type must be a row type");
+
+	pg_buffercache_superuser_check("pg_buffercache_evict_all");
+
+	EvictAllUnpinnedBuffers(&buffers_evicted, &buffers_flushed,
+							&buffers_skipped);
+
+	values[0] = Int32GetDatum(buffers_evicted);
+	values[1] = Int32GetDatum(buffers_flushed);
+	values[2] = Int32GetDatum(buffers_skipped);
+
+	tuple = heap_form_tuple(tupledesc, values, nulls);
+	result = HeapTupleGetDatum(tuple);
+
+	PG_RETURN_DATUM(result);
 }
diff --git a/contrib/pg_buffercache/sql/pg_buffercache.sql b/contrib/pg_buffercache/sql/pg_buffercache.sql
index 944fbb1beae..21dd666199c 100644
--- a/contrib/pg_buffercache/sql/pg_buffercache.sql
+++ b/contrib/pg_buffercache/sql/pg_buffercache.sql
@@ -26,3 +26,45 @@ SET ROLE pg_monitor;
 SELECT count(*) > 0 FROM pg_buffercache;
 SELECT buffers_used + buffers_unused > 0 FROM pg_buffercache_summary();
 SELECT count(*) > 0 FROM pg_buffercache_usage_counts();
+RESET role;
+
+
+------
+---- Test pg_buffercache_evict* functions
+------
+
+CREATE ROLE regress_buffercache_normal;
+SET ROLE regress_buffercache_normal;
+
+-- These should fail because they need to be called as SUPERUSER
+SELECT * FROM pg_buffercache_evict(1);
+SELECT * FROM pg_buffercache_evict_relation(1);
+SELECT * FROM pg_buffercache_evict_all();
+
+RESET ROLE;
+
+-- These should return nothing, because these are STRICT functions
+SELECT * FROM pg_buffercache_evict(NULL);
+SELECT * FROM pg_buffercache_evict_relation(NULL);
+
+-- These should fail because they are not called by valid range of buffers
+-- Number of the shared buffers are limited by max integer
+SELECT 2147483647 max_buffers \gset
+SELECT * FROM pg_buffercache_evict(-1);
+SELECT * FROM pg_buffercache_evict(0);
+SELECT * FROM pg_buffercache_evict(:max_buffers);
+
+-- This should fail because pg_buffercache_evict_relation() doesn't accept
+-- local relations
+CREATE TEMP TABLE temp_pg_buffercache();
+SELECT * FROM pg_buffercache_evict_relation('temp_pg_buffercache');
+DROP TABLE temp_pg_buffercache;
+
+-- These shouldn't fail
+SELECT evicted IS NOT NULL FROM pg_buffercache_evict(1);
+SELECT buffers_evicted IS NOT NULL FROM pg_buffercache_evict_all();
+CREATE TABLE shared_pg_buffercache();
+SELECT buffers_evicted IS NOT NULL FROM pg_buffercache_evict_relation('shared_pg_buffercache');
+DROP TABLE shared_pg_buffercache;
+
+DROP ROLE regress_buffercache_normal;
diff --git a/doc/src/sgml/pgbuffercache.sgml b/doc/src/sgml/pgbuffercache.sgml
index b5050cd7343..1585c7cdf75 100644
--- a/doc/src/sgml/pgbuffercache.sgml
+++ b/doc/src/sgml/pgbuffercache.sgml
@@ -27,14 +27,24 @@
   <primary>pg_buffercache_evict</primary>
  </indexterm>
 
+ <indexterm>
+  <primary>pg_buffercache_evict_relation</primary>
+ </indexterm>
+
+ <indexterm>
+  <primary>pg_buffercache_evict_all</primary>
+ </indexterm>
+
  <para>
   This module provides the <function>pg_buffercache_pages()</function>
   function (wrapped in the <structname>pg_buffercache</structname> view),
   <function>pg_buffercache_numa_pages()</function> function (wrapped in the
   <structname>pg_buffercache_numa</structname> view), the
   <function>pg_buffercache_summary()</function> function, the
-  <function>pg_buffercache_usage_counts()</function> function and
-  the <function>pg_buffercache_evict()</function> function.
+  <function>pg_buffercache_usage_counts()</function> function, the
+  <function>pg_buffercache_evict()</function>, the
+  <function>pg_buffercache_evict_relation()</function> function and the
+  <function>pg_buffercache_evict_all()</function> function.
  </para>
 
  <para>
@@ -76,6 +86,19 @@
   function is restricted to superusers only.
  </para>
 
+ <para>
+  The <function>pg_buffercache_evict_relation()</function> function allows all
+  unpinned shared buffers in the relation to be evicted from the buffer pool
+  given a relation identifier.  Use of this function is restricted to
+  superusers only.
+ </para>
+
+ <para>
+  The <function>pg_buffercache_evict_all()</function> function allows all
+  unpinned shared buffers to be evicted in the buffer pool.  Use of this
+  function is restricted to superusers only.
+ </para>
+
  <sect2 id="pgbuffercache-pg-buffercache">
   <title>The <structname>pg_buffercache</structname> View</title>
 
@@ -452,11 +475,46 @@
   <para>
    The <function>pg_buffercache_evict()</function> function takes a buffer
    identifier, as shown in the <structfield>bufferid</structfield> column of
-   the <structname>pg_buffercache</structname> view.  It returns true on success,
-   and false if the buffer wasn't valid, if it couldn't be evicted because it
-   was pinned, or if it became dirty again after an attempt to write it out.
-   The result is immediately out of date upon return, as the buffer might
-   become valid again at any time due to concurrent activity.  The function is
+   the <structname>pg_buffercache</structname> view.  It returns information
+   about whether the buffer was evicted and flushed.  The evicted column is
+   true on success, and false if the buffer wasn't valid, if it couldn't be
+   evicted because it was pinned, or if it became dirty again after an attempt
+   to write it out.  The flushed column is true if the buffer was flushed.
+   This does not necessarily mean that buffer was flushed by us, it might be
+   flushed by someone else.  The result is immediately out of date upon
+   return, as the buffer might become valid again at any time due to
+   concurrent activity. The function is intended for developer testing only.
+  </para>
+ </sect2>
+
+ <sect2 id="pgbuffercache-pg-buffercache-evict-relation">
+  <title>The <structname>pg_buffercache_evict_relation</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_evict_relation()</function> function is very
+   similar to the <function>pg_buffercache_evict()</function> function.  The
+   difference is that the <function>pg_buffercache_evict_relation()</function>
+   takes a relation identifier instead of buffer identifier.  It tries to
+   evict all buffers for all forks in that relation.  It returns the number of
+   evicted buffers, flushed buffers and the number of buffers that could not
+   be evicted.  Flushed buffers haven't necessarily been flushed by us, they
+   might have been flushed by someone else.  The result is immediately out of
+   date upon return, as buffers might immediately be read back in due to
+   concurrent activity.  The function is intended for developer testing only.
+  </para>
+ </sect2>
+
+ <sect2 id="pgbuffercache-pg-buffercache-evict-all">
+  <title>The <structname>pg_buffercache_evict_all</structname> Function</title>
+  <para>
+   The <function>pg_buffercache_evict_all()</function> function is very
+   similar to the <function>pg_buffercache_evict()</function> function.  The
+   difference is, the <function>pg_buffercache_evict_all()</function> function
+   does not take an argument; instead it tries to evict all buffers in the
+   buffer pool.  It returns the number of evicted buffers, flushed buffers and
+   the number of buffers that could not be evicted.  Flushed buffers haven't
+   necessarily been flushed by us, they might have been flushed by someone
+   else.  The result is immediately out of date upon return, as buffers might
+   immediately be read back in due to concurrent activity.  The function is
    intended for developer testing only.
   </para>
  </sect2>
diff --git a/src/test/modules/test_aio/test_aio.c b/src/test/modules/test_aio/test_aio.c
index bef0ecd9007..1d776010ef4 100644
--- a/src/test/modules/test_aio/test_aio.c
+++ b/src/test/modules/test_aio/test_aio.c
@@ -203,6 +203,7 @@ modify_rel_block(PG_FUNCTION_ARGS)
 	bool		corrupt_header = PG_GETARG_BOOL(3);
 	bool		corrupt_checksum = PG_GETARG_BOOL(4);
 	Page		page = palloc_aligned(BLCKSZ, PG_IO_ALIGN_SIZE, 0);
+	bool		flushed;
 	Relation	rel;
 	Buffer		buf;
 	PageHeader	ph;
@@ -237,7 +238,7 @@ modify_rel_block(PG_FUNCTION_ARGS)
 	if (BufferIsLocal(buf))
 		InvalidateLocalBuffer(GetLocalBufferDescriptor(-buf - 1), true);
 	else
-		EvictUnpinnedBuffer(buf);
+		EvictUnpinnedBuffer(buf, &flushed);
 
 	/*
 	 * Now modify the page as asked for by the caller.
@@ -478,6 +479,7 @@ invalidate_rel_block(PG_FUNCTION_ARGS)
 			BufferDesc *buf_hdr = BufferIsLocal(buf) ?
 				GetLocalBufferDescriptor(-buf - 1)
 				: GetBufferDescriptor(buf - 1);
+			bool		flushed;
 
 			LockBuffer(buf, BUFFER_LOCK_EXCLUSIVE);
 
@@ -493,7 +495,7 @@ invalidate_rel_block(PG_FUNCTION_ARGS)
 
 			if (BufferIsLocal(buf))
 				InvalidateLocalBuffer(GetLocalBufferDescriptor(-buf - 1), true);
-			else if (!EvictUnpinnedBuffer(buf))
+			else if (!EvictUnpinnedBuffer(buf, &flushed))
 				elog(ERROR, "couldn't evict");
 		}
 	}
-- 
2.48.1.76.g4e746b1a31.dirty

#21Nazir Bilal Yavuz
byavuz81@gmail.com
In reply to: Andres Freund (#20)
Re: Add pg_buffercache_evict_all() and pg_buffercache_mark_dirty[_all]() functions

Hi,

On Tue, 8 Apr 2025 at 02:29, Andres Freund <andres@anarazel.de> wrote:

On 2025-04-07 19:37:50 +0300, Nazir Bilal Yavuz wrote:

+ relation_close(rel, AccessExclusiveLock);

Hm. Why are we dropping the lock here early? It's probably ok, but it's not
clear to me why we should do so.

We are dropping the lock after we processed the relation. I didn't
understand what could be the problem here. Why do you think it is
early?

Most commonly we close relations without releasing the lock, instead relying
on the lock being released at the end of the transaction.

I see. I was looking at pg_prewarm as an example and copied it from there.

I don't think we're particularly consistent about it. And I think there's some
differing views about what precisely the right behaviour is...

I've tried to polish the patch. Changes I made:

- The number of processed buffers for EvictAllUnpinnedBuffers() was alwasy
NBuffers, that didn't seem right. But that's obsoleted by the next point:

- I think it'd be more useful to return the number of skipped buffers,
i.e. buffers that could not be evicted, than the number of processed
buffers.

I agree. The number of buffers that we tried to evict but couldn't
evict will give more information rather than NBuffers.

I'm not_evicted or such would also work.

Did you mean that 'not_evicted' can be used instead of
'skipped_buffers' as a column name? If that is the case, both are okay
to me.

- EvictAllUnpinnedBuffers() did not check whether the buffer was valid before
locking the buffer, which made it a fair bit more expensive than
EvictRelUnpinnedBuffers, which kinda has such a check via the buffer tag
check.

That sped up EvictAllUnpinnedBuffers() 3x when using a cluster with mostly
unused shared buffers.

Very nice speedup, thank you.

- The optional pointer arguments made the code look a bit ugly. I made them
mandatory now.

I agree, I think they look better now.

- I made EvictUnpinnedBufferInternal()'s argument the BufferDesc, rather than
the Buffer.

- The tests for STRICTness worked, they just errored out because there isn't a
function of the relevant names without arguments. I called them with NULL.

- The count(*) tests would have succeeded even if the call had "failed" due to
STRICTness. I used <colname> IS NOT NULL instead.

These look more correct now.

- rebased over the other pg_buffercache changes

Other points:

- I don't love the buffers_ prefix for the column names / C function
arguments. Seems long. It seems particularly weird because
pg_buffercache_evict() doesn't have a buffer_ prefix.

I left it as-is, but I think something perhaps ought to change before
commit.

Both are okay to me, I still couldn't decide which I like more.

Otoh, pg_buffercache_summary() and pg_buffercache_usage_counts() already
inconsistent in a similar way with each other.

- Arguably these functions ought to check BM_TAG_VALID, not BM_VALID. But that
only rather rarely happens when there are no pins. Since this is a
pre-existing pattern, I left it alone.

- The documentation format of the functions isn't quite what we usually do (a
table documenting the columns returned by a function with multiple columns),
but otoh, these are really developer oriented functions, so spending 30
lines of a <table> on each of these functions feels a bit silly.

I'd be ok with it as-is.

- The docs for pg_buffercache_evict() don't quite sound right to me, there's
some oddity in the phrasing. Nothing too bad, but perhaps worht a small bit
of additional polish.

I tried to update it without changing the old doc, perhaps that was
the wrong move.

Thank you so much for the review and the updates!

v8 applies cleanly and passes the CI.

--
Regards,
Nazir Bilal Yavuz
Microsoft

#22Andres Freund
andres@anarazel.de
In reply to: Nazir Bilal Yavuz (#21)
Re: Add pg_buffercache_evict_all() and pg_buffercache_mark_dirty[_all]() functions

Hi,

On 2025-04-08 03:15:57 +0300, Nazir Bilal Yavuz wrote:

On Tue, 8 Apr 2025 at 02:29, Andres Freund <andres@anarazel.de> wrote:

On 2025-04-07 19:37:50 +0300, Nazir Bilal Yavuz wrote:

+ relation_close(rel, AccessExclusiveLock);

Hm. Why are we dropping the lock here early? It's probably ok, but it's not
clear to me why we should do so.

We are dropping the lock after we processed the relation. I didn't
understand what could be the problem here. Why do you think it is
early?

Most commonly we close relations without releasing the lock, instead relying
on the lock being released at the end of the transaction.

I see. I was looking at pg_prewarm as an example and copied it from there.

I don't think we're particularly consistent about it. And I think there's some
differing views about what precisely the right behaviour is...

I've tried to polish the patch. Changes I made:

- The number of processed buffers for EvictAllUnpinnedBuffers() was alwasy
NBuffers, that didn't seem right. But that's obsoleted by the next point:

- I think it'd be more useful to return the number of skipped buffers,
i.e. buffers that could not be evicted, than the number of processed
buffers.

I agree. The number of buffers that we tried to evict but couldn't
evict will give more information rather than NBuffers.

Cool.

I'm not_evicted or such would also work.

Did you mean that 'not_evicted' can be used instead of
'skipped_buffers' as a column name? If that is the case, both are okay
to me.

Correct, sorry for that garbled sentence.

- I don't love the buffers_ prefix for the column names / C function
arguments. Seems long. It seems particularly weird because
pg_buffercache_evict() doesn't have a buffer_ prefix.

I left it as-is, but I think something perhaps ought to change before
commit.

Both are okay to me, I still couldn't decide which I like more.

I went back and forth at least three times. In the end I added the buffer_
(singular) prefix for pg_buffercache_evict().

- The documentation format of the functions isn't quite what we usually do (a
table documenting the columns returned by a function with multiple columns),
but otoh, these are really developer oriented functions, so spending 30
lines of a <table> on each of these functions feels a bit silly.

I'd be ok with it as-is.

- The docs for pg_buffercache_evict() don't quite sound right to me, there's
some oddity in the phrasing. Nothing too bad, but perhaps worht a small bit
of additional polish.

I tried to update it without changing the old doc, perhaps that was
the wrong move.

I think it's ok for now. It might be worth doing a larger redesign of the
pgbuffercache docs at some point...

Pushed.

Thanks for your patches and thanks for all the reviewers getting this ready!

Greetings,

Andres Freund

#23Bertrand Drouvot
bertranddrouvot.pg@gmail.com
In reply to: Andres Freund (#22)
Re: Add pg_buffercache_evict_all() and pg_buffercache_mark_dirty[_all]() functions

Hi,

On Tue, Apr 08, 2025 at 02:40:52AM -0400, Andres Freund wrote:

I think it's ok for now. It might be worth doing a larger redesign of the
pgbuffercache docs at some point...

Pushed.

Thanks for your patches and thanks for all the reviewers getting this ready!

Thanks for the patch! That sounds like a great addition. I was doing some
tests and did not see any issues. Also while doing the tests I thouhgt that it
could be useful to evict only from a subset of NUMA nodes (now that NUMA
awareness is in). We'd need to figure out what to do for buffers that are spread
across NUMA nodes though.

Does that sound like an idea worth to spend time on? (If so, I'd be happy to work
on it).

Regards,

--
Bertrand Drouvot
PostgreSQL Contributors Team
RDS Open Source Databases
Amazon Web Services: https://aws.amazon.com

#24Robert Haas
robertmhaas@gmail.com
In reply to: Aidar Imamov (#5)
Re: Add pg_buffercache_evict_all() and pg_buffercache_mark_dirty[_all]() functions

On Tue, Mar 18, 2025 at 6:03 PM Aidar Imamov <a.imamov@postgrespro.ru> wrote:

for (int buf = 1; buf < NBuffers; buf++)

Mb it would be more correct to use <= NBuffers?

I agree that (int buf = 1; buf < NBuffers; buf++) isn't right because
that iterates one fewer times than the number of buffers. What was
ultimately committed was:

+   for (int buf = 1; buf <= NBuffers; buf++)
+   {
+       BufferDesc *desc = GetBufferDescriptor(buf - 1);

Curiously, there is no other instance of <= NBuffers in the code.
Elsewhere we instead do:

for (i = 0; i < NBuffers; i++)
{
BufferDesc *bufHdr = GetBufferDescriptor(i);

Or in BufferSync:

for (buf_id = 0; buf_id < NBuffers; buf_id++)
{
BufferDesc *bufHdr = GetBufferDescriptor(buf_id);

Nonetheless what was committed seems pretty defensible, because we
have lots of other places that do GetBufferDescriptor(buffer - 1) and
similar. Alternating between 0-based indexing and 1-based indexing
like this seems rather error-prone somehow. :-(

--
Robert Haas
EDB: http://www.enterprisedb.com

#25Nazir Bilal Yavuz
byavuz81@gmail.com
In reply to: Bertrand Drouvot (#23)
Re: Add pg_buffercache_evict_all() and pg_buffercache_mark_dirty[_all]() functions

Hi,

On Thu, 10 Apr 2025 at 16:50, Bertrand Drouvot
<bertranddrouvot.pg@gmail.com> wrote:

On Tue, Apr 08, 2025 at 02:40:52AM -0400, Andres Freund wrote:

I think it's ok for now. It might be worth doing a larger redesign of the
pgbuffercache docs at some point...

Pushed.

Thanks for your patches and thanks for all the reviewers getting this ready!

Thanks for the patch! That sounds like a great addition. I was doing some
tests and did not see any issues.

Thank you for looking into this!

Also while doing the tests I thouhgt that it
could be useful to evict only from a subset of NUMA nodes (now that NUMA
awareness is in). We'd need to figure out what to do for buffers that are spread
across NUMA nodes though.

Does that sound like an idea worth to spend time on? (If so, I'd be happy to work
on it).

I think it looks useful, but I’m not too familiar with NUMA, so I’m
not sure I can say much with confidence. Maybe someone else can chime
in with a more solid take?

--
Regards,
Nazir Bilal Yavuz
Microsoft

#26Nazir Bilal Yavuz
byavuz81@gmail.com
In reply to: Robert Haas (#24)
Re: Add pg_buffercache_evict_all() and pg_buffercache_mark_dirty[_all]() functions

Hi,

Thank you for looking into this!

On Thu, 10 Apr 2025 at 23:06, Robert Haas <robertmhaas@gmail.com> wrote:

On Tue, Mar 18, 2025 at 6:03 PM Aidar Imamov <a.imamov@postgrespro.ru> wrote:

for (int buf = 1; buf < NBuffers; buf++)

Mb it would be more correct to use <= NBuffers?

I agree that (int buf = 1; buf < NBuffers; buf++) isn't right because
that iterates one fewer times than the number of buffers. What was
ultimately committed was:

+   for (int buf = 1; buf <= NBuffers; buf++)
+   {
+       BufferDesc *desc = GetBufferDescriptor(buf - 1);

Curiously, there is no other instance of <= NBuffers in the code.
Elsewhere we instead do:

for (i = 0; i < NBuffers; i++)
{
BufferDesc *bufHdr = GetBufferDescriptor(i);

Or in BufferSync:

for (buf_id = 0; buf_id < NBuffers; buf_id++)
{
BufferDesc *bufHdr = GetBufferDescriptor(buf_id);

Nonetheless what was committed seems pretty defensible, because we
have lots of other places that do GetBufferDescriptor(buffer - 1) and
similar. Alternating between 0-based indexing and 1-based indexing
like this seems rather error-prone somehow. :-(

I understand your point. I did it like that because bufferids start
from 1 and go to NBuffers inclusive in the pg_buffercache view, so it
seems more natural to me to implement it like that. I am okay to
replace these loops with [1]for (int buf = 0; buf < NBuffers; buf++) { BufferDesc *desc = GetBufferDescriptor(buf); to make it standart everywhere:

[1]: for (int buf = 0; buf < NBuffers; buf++) { BufferDesc *desc = GetBufferDescriptor(buf);
for (int buf = 0; buf < NBuffers; buf++)
{
BufferDesc *desc = GetBufferDescriptor(buf);

--
Regards,
Nazir Bilal Yavuz
Microsoft

#27Robert Haas
robertmhaas@gmail.com
In reply to: Nazir Bilal Yavuz (#26)
Re: Add pg_buffercache_evict_all() and pg_buffercache_mark_dirty[_all]() functions

On Fri, Apr 11, 2025 at 4:02 AM Nazir Bilal Yavuz <byavuz81@gmail.com> wrote:

I understand your point. I did it like that because bufferids start
from 1 and go to NBuffers inclusive in the pg_buffercache view, so it
seems more natural to me to implement it like that. I am okay to
replace these loops with [1] to make it standart everywhere:

[1]
for (int buf = 0; buf < NBuffers; buf++)
{
BufferDesc *desc = GetBufferDescriptor(buf);

I'm more making an observation than asking for a change. If you and
others think it should be changed, that is fine, but I'm uncertain
myself what we ought to be doing here. I just wanted to raise the
issue.

--
Robert Haas
EDB: http://www.enterprisedb.com

#28Andres Freund
andres@anarazel.de
In reply to: Bertrand Drouvot (#23)
Re: Add pg_buffercache_evict_all() and pg_buffercache_mark_dirty[_all]() functions

Hi,

On 2025-04-10 13:50:36 +0000, Bertrand Drouvot wrote:

Thanks for the patch! That sounds like a great addition. I was doing some
tests and did not see any issues. Also while doing the tests I thouhgt that it
could be useful to evict only from a subset of NUMA nodes (now that NUMA
awareness is in). We'd need to figure out what to do for buffers that are spread
across NUMA nodes though.

Does that sound like an idea worth to spend time on? (If so, I'd be happy to work
on it).

I'm not sure that's common enough to warrant its own function. You can do that
with pg_buffercache_evict(), it'll be slower than pg_buffercache_evict_all(),
but given that determining the numa node already is somewhat expensive, I'm
not sure it's going to make that big a difference.

Greetings,

Andres Freund