From 32261b18df3a49a159a7702f59a4c6bb6d8808f9 Mon Sep 17 00:00:00 2001
From: AyoubKAZ <kazarayoub2004@gmail.com>
Date: Sun, 29 Mar 2026 21:40:23 +0200
Subject: [PATCH] Add pg_stat_vfdcache view for VFD cache statistics

PostgreSQL's virtual file descriptor (VFD) layer maintains a
per-backend cache of open file descriptors bounded by
max_files_per_process (default 1000).  When the cache is full, the
least-recently-used entry is evicted (its OS fd closed) so a new file
can be opened. A subsequent access to an evicted file must call
open() again.

A trivial example is with partitioned tables: a table with 1500
partitions requires up to many file descriptors per full scan (main
fork, vm ...), which is more than the default limit, causing
potential evictions and reopens.

This commit adds:

  pg_stat_vfdcache -- a single-row view exposing cluster-wide VFD cache
  statistics:
      hits                   number of VFD cache hits
      misses                 number of VFD cache misses
      evictions              number of LRU evictions
      cache_entries          total allocated VFD entries across backends
      cache_bytes            total VFD entry memory footprint (bytes)
      max_files_per_process  current value of max_files_per_process
      hit_ratio              hits / (hits + misses)
      stats_reset            timestamp of last counter reset

  pg_stat_reset_vfdcache() -- resets shared VFD counters

The implementation follows the same cumulative shared statistics infrastructure like pgstat_bgwriter and others do.

Event counting remains cheap in backend-local pending storage and is flushed
into shared fixed stats. In addition, each backend publishes VFD gauge values
(vfd_entries, vfd_cache_bytes) into backend stats, and SQL accessors sum these
per-backend gauges at read time to produce cluster totals.

Hit and miss counters are placed in FileAccess(), which is the
single gate through which all VFD-mediated file reads, writes,
truncations, and size checks pass. The eviction counter is placed
in ReleaseLruFile(), before LruDelete() is called.
---
 doc/src/sgml/monitoring.sgml                 | 148 ++++++++++++++++++
 src/backend/catalog/system_views.sql         |  18 +++
 src/backend/storage/file/fd.c                |  49 ++++++
 src/backend/utils/activity/Makefile          |   1 +
 src/backend/utils/activity/meson.build       |   1 +
 src/backend/utils/activity/pgstat.c          |  17 ++
 src/backend/utils/activity/pgstat_vfdcache.c | 154 +++++++++++++++++++
 src/backend/utils/adt/pgstatfuncs.c          | 123 ++++++++++++++-
 src/include/catalog/pg_proc.dat              |  42 +++++
 src/include/pgstat.h                         |  45 ++++++
 src/include/storage/fd.h                     |   3 +
 src/include/utils/pgstat_internal.h          |  22 +++
 src/include/utils/pgstat_kind.h              |   3 +-
 src/test/regress/expected/rules.out          |  11 ++
 src/test/regress/expected/stats.out          |  16 +-
 src/test/regress/sql/stats.sql               |   5 +
 16 files changed, 655 insertions(+), 3 deletions(-)
 create mode 100644 src/backend/utils/activity/pgstat_vfdcache.c

diff --git a/doc/src/sgml/monitoring.sgml b/doc/src/sgml/monitoring.sgml
index bb75ed1069b..0478d7ea50e 100644
--- a/doc/src/sgml/monitoring.sgml
+++ b/doc/src/sgml/monitoring.sgml
@@ -472,6 +472,19 @@ postgres   27093  0.0  0.0  30096  2752 ?        Ss   11:34   0:00 postgres: ser
      </entry>
      </row>
 
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structname>pg_stat_vfdcache</structname>
+       <indexterm><primary>pg_stat_vfdcache</primary></indexterm>
+      </para>
+      <para>
+        One row only, showing cluster-wide statistics about virtual file
+        descriptor (VFD) cache activity.  See
+       <link linkend="monitoring-pg-stat-vfdcache-view">
+       <structname>pg_stat_vfdcache</structname></link> for details.
+      </para></entry>
+     </row>
+
      <row>
       <entry><structname>pg_stat_checkpointer</structname><indexterm><primary>pg_stat_checkpointer</primary></indexterm></entry>
       <entry>One row only, showing statistics about the
@@ -3456,6 +3469,128 @@ description | Waiting for a newly initialized WAL file to reach durable storage
 
  </sect2>
 
+ <sect2 id="monitoring-pg-stat-vfdcache-view">
+  <title><structname>pg_stat_vfdcache</structname></title>
+
+  <indexterm zone="monitoring-pg-stat-vfdcache-view">
+   <primary>pg_stat_vfdcache</primary>
+  </indexterm>
+
+  <para>
+    The <structname>pg_stat_vfdcache</structname> view will always have a
+    single row, containing data about cluster-wide VFD (Virtual File
+    Descriptor) cache activity.
+  </para>
+
+  <table id="pg-stat-vfdcache-view" xreflabel="pg_stat_vfdcache">
+   <title><structname>pg_stat_vfdcache</structname> View</title>
+   <tgroup cols="3">
+    <thead>
+     <row>
+      <entry role="catalog_table_entry">Column</entry>
+      <entry>Type</entry>
+      <entry>Description</entry>
+     </row>
+    </thead>
+    <tbody>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>hits</structfield> <type>bigint</type>
+      </para></entry>
+      <entry><type>bigint</type></entry>
+      <entry>
+       Number of file accesses where the physical file descriptor was
+       already open in the cache, requiring no system call.
+      </entry>
+     </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>misses</structfield> <type>bigint</type>
+      </para></entry>
+      <entry><type>bigint</type></entry>
+      <entry>
+       Number of file accesses where the physical file descriptor had
+       been evicted from the cache, requiring <function>open()</function>
+       to be called again before the access could proceed.
+      </entry>
+     </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>evictions</structfield> <type>bigint</type>
+      </para></entry>
+      <entry><type>bigint</type></entry>
+      <entry>
+       Number of times a physical file descriptor was closed to make
+       room in the cache for a new one (LRU eviction).  Each eviction
+       will eventually produce a miss when the evicted file is accessed
+       again.  When the cache is thrashing, this value will be close to
+       or equal to <structfield>misses</structfield>.
+      </entry>
+     </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+         <structfield>cache_entries</structfield> <type>integer</type>
+      </para></entry>
+      <entry><type>integer</type></entry>
+      <entry>
+         Sum of currently allocated VFD cache entries across all active
+         backends.
+      </entry>
+     </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+         <structfield>cache_bytes</structfield> <type>bigint</type>
+        </para></entry>
+        <entry><type>bigint</type></entry>
+        <entry>
+         Sum of memory used by allocated VFD cache entries across all active
+         backends, in bytes.
+        </entry>
+       </row>
+       <row>
+        <entry role="catalog_table_entry"><para role="column_definition">
+         <structfield>max_files_per_process</structfield> <type>integer</type>
+      </para></entry>
+      <entry><type>integer</type></entry>
+      <entry>
+       Current value of <varname>max_files_per_process</varname>.  The
+       effective limit on simultaneously open file descriptors may be
+       lower than this value if the operating system's per-process file
+       descriptor limit is smaller.
+      </entry>
+     </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>hit_ratio</structfield> <type>float8</type>
+      </para></entry>
+      <entry><type>float8</type></entry>
+      <entry>
+       Fraction of file accesses that were cache hits:
+       <literal>hits / (hits + misses)</literal>.  A value of
+       <literal>1.0</literal> indicates no cache pressure.  Values
+       significantly below <literal>1.0</literal> indicate that
+       <varname>max_files_per_process</varname> should be increased.
+       <literal>NULL</literal> if no accesses have been recorded yet.
+      </entry>
+     </row>
+     <row>
+      <entry role="catalog_table_entry"><para role="column_definition">
+       <structfield>stats_reset</structfield>
+       <type>timestamp with time zone</type>
+      </para></entry>
+      <entry><type>timestamp with time zone</type></entry>
+      <entry>
+       Time at which the counters were last reset by
+       <function>pg_stat_reset_vfdcache()</function>.
+       <literal>NULL</literal> if the counters have never been reset.
+      </entry>
+     </row>
+    </tbody>
+   </tgroup>
+  </table>
+
+ </sect2>
+
  <sect2 id="monitoring-pg-stat-checkpointer-view">
   <title><structname>pg_stat_checkpointer</structname></title>
 
@@ -5640,6 +5775,19 @@ description | Waiting for a newly initialized WAL file to reach durable storage
         can be granted EXECUTE to run the function.
        </para></entry>
       </row>
+
+      <row>
+       <entry role="func_table_entry"><para role="func_signature">
+        <indexterm><primary>pg_stat_reset_vfdcache</primary></indexterm>
+        <function>pg_stat_reset_vfdcache</function> ()
+        <returnvalue>void</returnvalue>
+       </para>
+       <para>
+          Reset shared VFD cache statistics counters to zero.  The reset
+          timestamp is recorded in
+        <structname>pg_stat_vfdcache</structname>.<structfield>stats_reset</structfield>.
+       </para></entry>
+      </row>
      </tbody>
     </tgroup>
    </table>
diff --git a/src/backend/catalog/system_views.sql b/src/backend/catalog/system_views.sql
index e54018004db..31d25db6f42 100644
--- a/src/backend/catalog/system_views.sql
+++ b/src/backend/catalog/system_views.sql
@@ -1516,3 +1516,21 @@ CREATE VIEW pg_aios AS
     SELECT * FROM pg_get_aios();
 REVOKE ALL ON pg_aios FROM PUBLIC;
 GRANT SELECT ON pg_aios TO pg_read_all_stats;
+
+CREATE VIEW pg_stat_vfdcache AS
+    SELECT
+        pg_stat_get_vfd_hits()                          AS hits,
+        pg_stat_get_vfd_misses()                        AS misses,
+        pg_stat_get_vfd_evictions()                     AS evictions,
+        pg_stat_get_vfd_cache_size()                    AS cache_entries,
+        pg_stat_get_vfd_cache_bytes()                   AS cache_bytes,
+        current_setting('max_files_per_process')::int   AS max_files_per_process,
+        CASE
+            WHEN pg_stat_get_vfd_hits() + pg_stat_get_vfd_misses() = 0
+            THEN NULL::float8
+            ELSE pg_stat_get_vfd_hits()::float8
+                 / (pg_stat_get_vfd_hits() + pg_stat_get_vfd_misses())
+        END                                             AS hit_ratio,
+        pg_stat_get_vfd_stat_reset_time()               AS stats_reset;
+ 
+GRANT SELECT ON pg_stat_vfdcache TO PUBLIC;
\ No newline at end of file
diff --git a/src/backend/storage/file/fd.c b/src/backend/storage/file/fd.c
index 01f1bd6e687..b273c173b11 100644
--- a/src/backend/storage/file/fd.c
+++ b/src/backend/storage/file/fd.c
@@ -1378,6 +1378,7 @@ ReleaseLruFile(void)
 		 * in the ring.
 		 */
 		Assert(VfdCache[0].lruMoreRecently != 0);
+		pgstat_count_vfd_eviction();
 		LruDelete(VfdCache[0].lruMoreRecently);
 		return true;			/* freed a file */
 	}
@@ -1491,6 +1492,7 @@ FileAccess(File file)
 
 	if (FileIsNotOpen(file))
 	{
+		pgstat_count_vfd_miss();
 		returnValue = LruInsert(file);
 		if (returnValue != 0)
 			return returnValue;
@@ -1501,14 +1503,61 @@ FileAccess(File file)
 		 * We now know that the file is open and that it is not the last one
 		 * accessed, so we need to move it to the head of the Lru ring.
 		 */
+		pgstat_count_vfd_hit();
 
 		Delete(file);
 		Insert(file);
 	}
+	else
+	{
+		/* fd is open and already at MRU end */
+		pgstat_count_vfd_hit();
+	}
 
 	return 0;
 }
 
+/*
+ * GetVfdCacheOccupancy
+ *
+ * Return the number of physical file descriptors currently open in the
+ * VFD cache (nfile).  This is the live cache size exposed by
+ * pg_stat_vfdcache.cache_size.
+ *
+ */
+int
+GetVfdCacheOccupancy(void)
+{
+	return nfile;
+}
+
+/*
+ * GetVfdCacheEntries
+ *
+ * Return the number of VFD cache entries currently allocated for this
+ * backend, excluding slot 0 which is the freelist/LRU header.
+ */
+uint64
+GetVfdCacheEntries(void)
+{
+	if (SizeVfdCache == 0)
+		return 0;
+
+	return (uint64) (SizeVfdCache - 1);
+}
+
+/*
+ * GetVfdCacheBytes
+ *
+ * Return the memory footprint in bytes of currently allocated per-backend
+ * VFD entries, excluding slot 0.
+ */
+uint64
+GetVfdCacheBytes(void)
+{
+	return GetVfdCacheEntries() * sizeof(Vfd);
+}
+
 /*
  * Called whenever a temporary file is deleted to report its size.
  */
diff --git a/src/backend/utils/activity/Makefile b/src/backend/utils/activity/Makefile
index ca3ef89bf59..fe8fc00d966 100644
--- a/src/backend/utils/activity/Makefile
+++ b/src/backend/utils/activity/Makefile
@@ -32,6 +32,7 @@ OBJS = \
 	pgstat_shmem.o \
 	pgstat_slru.o \
 	pgstat_subscription.o \
+	pgstat_vfdcache.o \
 	pgstat_wal.o \
 	pgstat_xact.o \
 	wait_event.o \
diff --git a/src/backend/utils/activity/meson.build b/src/backend/utils/activity/meson.build
index 1aa7ece5290..5c1bcec7f8b 100644
--- a/src/backend/utils/activity/meson.build
+++ b/src/backend/utils/activity/meson.build
@@ -17,6 +17,7 @@ backend_sources += files(
   'pgstat_shmem.c',
   'pgstat_slru.c',
   'pgstat_subscription.c',
+  'pgstat_vfdcache.c',
   'pgstat_wal.c',
   'pgstat_xact.c',
 )
diff --git a/src/backend/utils/activity/pgstat.c b/src/backend/utils/activity/pgstat.c
index eb8ccbaa628..95d366b6ce9 100644
--- a/src/backend/utils/activity/pgstat.c
+++ b/src/backend/utils/activity/pgstat.c
@@ -500,6 +500,23 @@ static const PgStat_KindInfo pgstat_kind_builtin_infos[PGSTAT_KIND_BUILTIN_SIZE]
 		.reset_all_cb = pgstat_wal_reset_all_cb,
 		.snapshot_cb = pgstat_wal_snapshot_cb,
 	},
+
+	[PGSTAT_KIND_VFDCACHE] = {
+		.name = "vfdcache",
+
+		.fixed_amount = true,
+		.write_to_file = true,
+
+		.snapshot_ctl_off = offsetof(PgStat_Snapshot, vfdcache),
+		.shared_ctl_off = offsetof(PgStat_ShmemControl, vfdcache),
+		.shared_data_off = offsetof(PgStatShared_VfdCache, stats),
+		.shared_data_len = sizeof(((PgStatShared_VfdCache *) 0)->stats),
+
+		.flush_static_cb = pgstat_vfdcache_flush_cb,
+		.init_shmem_cb = pgstat_vfdcache_init_shmem_cb,
+		.reset_all_cb = pgstat_vfdcache_reset_all_cb,
+		.snapshot_cb = pgstat_vfdcache_snapshot_cb,
+	},
 };
 
 /*
diff --git a/src/backend/utils/activity/pgstat_vfdcache.c b/src/backend/utils/activity/pgstat_vfdcache.c
new file mode 100644
index 00000000000..069b833e66e
--- /dev/null
+++ b/src/backend/utils/activity/pgstat_vfdcache.c
@@ -0,0 +1,154 @@
+/* -------------------------------------------------------------------------
+ *
+ * pgstat_vfdcache.c
+ *	  Implementation of VFD cache statistics.
+ *
+ * VFD events are first counted in backend-local pending storage and then
+ * flushed into shared-memory cumulative stats, following the same model as
+ * other fixed stats kinds.
+ *
+ * Portions Copyright (c) 1996-2026, PostgreSQL Global Development Group
+ *
+ * IDENTIFICATION
+ *	  src/backend/utils/activity/pgstat_vfdcache.c
+ * -------------------------------------------------------------------------
+ */
+
+#include "postgres.h"
+
+#include "pgstat.h"
+#include "storage/fd.h"
+#include "utils/memutils.h"
+#include "utils/pgstat_internal.h"
+
+/*
+ * Backend-local VFD counters waiting to be flushed.
+ */
+PgStat_VfdCacheStats PendingVfdCacheStats = {0};
+
+/*
+ * Count a VFD cache hit.
+ */
+void
+pgstat_count_vfd_hit(void)
+{
+	PendingVfdCacheStats.vfd_hits++;
+	pgstat_report_fixed = true;
+}
+
+/*
+ * Count a VFD cache miss.
+ */
+void
+pgstat_count_vfd_miss(void)
+{
+	PendingVfdCacheStats.vfd_misses++;
+	pgstat_report_fixed = true;
+}
+
+/*
+ * Count a VFD cache eviction.
+ */
+void
+pgstat_count_vfd_eviction(void)
+{
+	PendingVfdCacheStats.vfd_evictions++;
+	pgstat_report_fixed = true;
+}
+
+/*
+ * Flush out backend-local pending VFD cache stats.
+ */
+bool
+pgstat_vfdcache_flush_cb(bool nowait)
+{
+	PgStat_EntryRef *entry_ref;
+	PgStatShared_Backend *shbackendent;
+	PgStatShared_VfdCache *stats_shmem = &pgStatLocal.shmem->vfdcache;
+
+	if (pg_memory_is_all_zeros(&PendingVfdCacheStats,
+							   sizeof(struct PgStat_VfdCacheStats)))
+		return false;
+
+	entry_ref = pgstat_get_entry_ref_locked(PGSTAT_KIND_BACKEND, InvalidOid,
+											MyProcNumber, nowait);
+	if (!entry_ref)
+		return true;
+
+	shbackendent = (PgStatShared_Backend *) entry_ref->shared_stats;
+
+	shbackendent->stats.vfd_stats.vfd_entries = (PgStat_Counter) GetVfdCacheEntries();
+	shbackendent->stats.vfd_stats.vfd_cache_bytes = (PgStat_Counter) GetVfdCacheBytes();
+
+	pgstat_unlock_entry(entry_ref);
+
+	pgstat_begin_changecount_write(&stats_shmem->changecount);
+	stats_shmem->stats.vfd_hits += PendingVfdCacheStats.vfd_hits;
+	stats_shmem->stats.vfd_misses += PendingVfdCacheStats.vfd_misses;
+	stats_shmem->stats.vfd_evictions += PendingVfdCacheStats.vfd_evictions;
+	pgstat_end_changecount_write(&stats_shmem->changecount);
+
+	MemSet(&PendingVfdCacheStats, 0, sizeof(PendingVfdCacheStats));
+
+	return false;
+}
+
+/*
+ * Support function for SQL-callable pg_stat_get_vfd_* functions.
+ */
+PgStat_VfdCacheStats *
+pgstat_fetch_stat_vfdcache(void)
+{
+	pgstat_snapshot_fixed(PGSTAT_KIND_VFDCACHE);
+
+	return &pgStatLocal.snapshot.vfdcache;
+}
+
+void
+pgstat_vfdcache_init_shmem_cb(void *stats)
+{
+	PgStatShared_VfdCache *stats_shmem = (PgStatShared_VfdCache *) stats;
+
+	LWLockInitialize(&stats_shmem->lock, LWTRANCHE_PGSTATS_DATA);
+}
+
+void
+pgstat_vfdcache_reset_all_cb(TimestampTz ts)
+{
+	PgStatShared_VfdCache *stats_shmem = &pgStatLocal.shmem->vfdcache;
+
+	LWLockAcquire(&stats_shmem->lock, LW_EXCLUSIVE);
+	pgstat_copy_changecounted_stats(&stats_shmem->reset_offset,
+									&stats_shmem->stats,
+									sizeof(stats_shmem->stats),
+									&stats_shmem->changecount);
+	stats_shmem->stats.stat_reset_timestamp = ts;
+	LWLockRelease(&stats_shmem->lock);
+}
+
+void
+pgstat_vfdcache_snapshot_cb(void)
+{
+	PgStatShared_VfdCache *stats_shmem = &pgStatLocal.shmem->vfdcache;
+	PgStat_VfdCacheStats *reset_offset = &stats_shmem->reset_offset;
+	PgStat_VfdCacheStats reset;
+
+	pgstat_copy_changecounted_stats(&pgStatLocal.snapshot.vfdcache,
+									&stats_shmem->stats,
+									sizeof(stats_shmem->stats),
+									&stats_shmem->changecount);
+
+	LWLockAcquire(&stats_shmem->lock, LW_SHARED);
+	memcpy(&reset, reset_offset, sizeof(stats_shmem->stats));
+	LWLockRelease(&stats_shmem->lock);
+
+	pgStatLocal.snapshot.vfdcache.vfd_hits -= reset.vfd_hits;
+	pgStatLocal.snapshot.vfdcache.vfd_misses -= reset.vfd_misses;
+	pgStatLocal.snapshot.vfdcache.vfd_evictions -= reset.vfd_evictions;
+}
+
+void
+pgstat_reset_vfdcache(void)
+{
+	pgstat_reset_of_kind(PGSTAT_KIND_VFDCACHE);
+}
diff --git a/src/backend/utils/adt/pgstatfuncs.c b/src/backend/utils/adt/pgstatfuncs.c
index 9185a8e6b83..0f36acac56c 100644
--- a/src/backend/utils/adt/pgstatfuncs.c
+++ b/src/backend/utils/adt/pgstatfuncs.c
@@ -14,6 +14,7 @@
  */
 #include "postgres.h"
 
+#include "access/xact.h"
 #include "access/htup_details.h"
 #include "access/xlog.h"
 #include "access/xlogprefetcher.h"
@@ -28,6 +29,7 @@
 #include "replication/logicallauncher.h"
 #include "storage/proc.h"
 #include "storage/procarray.h"
+#include "storage/fd.h"
 #include "utils/acl.h"
 #include "utils/builtins.h"
 #include "utils/timestamp.h"
@@ -1340,6 +1342,122 @@ pg_stat_get_buf_alloc(PG_FUNCTION_ARGS)
 	PG_RETURN_INT64(pgstat_fetch_stat_bgwriter()->buf_alloc);
 }
 
+/*
+ * Sum per-backend VFD gauges across currently active backends.
+ */
+static void
+pgstat_get_vfd_backend_sums(PgStat_Counter *entries_sum,
+							PgStat_Counter *bytes_sum)
+{
+	static TimestampTz cached_stmt_start_ts = 0;
+	static PgStat_Counter cached_entries_sum = 0;
+	static PgStat_Counter cached_bytes_sum = 0;
+	TimestampTz stmt_start_ts = GetCurrentStatementStartTimestamp();
+	int			num_backends = pgstat_fetch_stat_numbackends();
+
+	if (cached_stmt_start_ts == stmt_start_ts)
+	{
+		*entries_sum = cached_entries_sum;
+		*bytes_sum = cached_bytes_sum;
+		return;
+	}
+
+	*entries_sum = 0;
+	*bytes_sum = 0;
+
+	for (int curr_backend = 1; curr_backend <= num_backends; curr_backend++)
+	{
+		LocalPgBackendStatus *local_beentry;
+		PgBackendStatus *beentry;
+		PgStat_Backend *backend_stats;
+
+		local_beentry = pgstat_get_local_beentry_by_index(curr_backend);
+		beentry = &local_beentry->backendStatus;
+
+		if (!pgstat_tracks_backend_bktype(beentry->st_backendType))
+			continue;
+
+		backend_stats = pgstat_fetch_stat_backend(local_beentry->proc_number);
+		if (!backend_stats)
+			continue;
+
+		*entries_sum += backend_stats->vfd_stats.vfd_entries;
+		*bytes_sum += backend_stats->vfd_stats.vfd_cache_bytes;
+	}
+
+	cached_entries_sum = *entries_sum;
+	cached_bytes_sum = *bytes_sum;
+	cached_stmt_start_ts = stmt_start_ts;
+}
+
+Datum
+pg_stat_get_vfd_hits(PG_FUNCTION_ARGS)
+{
+	PG_RETURN_INT64(pgstat_fetch_stat_vfdcache()->vfd_hits);
+}
+
+Datum
+pg_stat_get_vfd_misses(PG_FUNCTION_ARGS)
+{
+	PG_RETURN_INT64(pgstat_fetch_stat_vfdcache()->vfd_misses);
+}
+
+Datum
+pg_stat_get_vfd_evictions(PG_FUNCTION_ARGS)
+{
+	PG_RETURN_INT64(pgstat_fetch_stat_vfdcache()->vfd_evictions);
+}
+
+Datum
+pg_stat_get_vfd_cache_size(PG_FUNCTION_ARGS)
+{
+	PgStat_Counter entries_sum;
+	PgStat_Counter bytes_dummy;
+
+	pgstat_get_vfd_backend_sums(&entries_sum, &bytes_dummy);
+
+	PG_RETURN_INT32((int32) entries_sum);
+}
+
+Datum
+pg_stat_get_vfd_cache_bytes(PG_FUNCTION_ARGS)
+{
+	PgStat_Counter entries_sum;
+	PgStat_Counter bytes_sum;
+
+	pgstat_get_vfd_backend_sums(&entries_sum, &bytes_sum);
+
+	PG_RETURN_INT64(bytes_sum);
+}
+
+/*
+ * pg_stat_get_vfd_stat_reset_time
+ *		Timestamp of the last pg_stat_reset_vfdcache() call, or NULL if
+ *		the counters have never been reset.
+ */
+Datum
+pg_stat_get_vfd_stat_reset_time(PG_FUNCTION_ARGS)
+{
+	TimestampTz ts = pgstat_fetch_stat_vfdcache()->stat_reset_timestamp;
+
+	if (ts == 0)
+		PG_RETURN_NULL();
+
+	PG_RETURN_TIMESTAMPTZ(ts);
+}
+
+/*
+ * pg_stat_reset_vfdcache
+ *		Reset shared VFD cache counters.
+ */
+Datum
+pg_stat_reset_vfdcache(PG_FUNCTION_ARGS)
+{
+	pgstat_reset_vfdcache();
+	PG_RETURN_VOID();
+}
+
+
 /*
 * When adding a new column to the pg_stat_io view and the
 * pg_stat_get_backend_io() function, add a new enum value here above
@@ -1965,6 +2083,7 @@ pg_stat_reset_shared(PG_FUNCTION_ARGS)
 		XLogPrefetchResetStats();
 		pgstat_reset_of_kind(PGSTAT_KIND_SLRU);
 		pgstat_reset_of_kind(PGSTAT_KIND_WAL);
+		pgstat_reset_of_kind(PGSTAT_KIND_VFDCACHE);
 
 		PG_RETURN_VOID();
 	}
@@ -1987,11 +2106,13 @@ pg_stat_reset_shared(PG_FUNCTION_ARGS)
 		pgstat_reset_of_kind(PGSTAT_KIND_SLRU);
 	else if (strcmp(target, "wal") == 0)
 		pgstat_reset_of_kind(PGSTAT_KIND_WAL);
+	else if (strcmp(target, "vfdcache") == 0)
+		pgstat_reset_of_kind(PGSTAT_KIND_VFDCACHE);
 	else
 		ereport(ERROR,
 				(errcode(ERRCODE_INVALID_PARAMETER_VALUE),
 				 errmsg("unrecognized reset target: \"%s\"", target),
-				 errhint("Target must be \"archiver\", \"bgwriter\", \"checkpointer\", \"io\", \"recovery_prefetch\", \"slru\", or \"wal\".")));
+				 errhint("Target must be \"archiver\", \"bgwriter\", \"checkpointer\", \"io\", \"recovery_prefetch\", \"slru\", \"vfdcache\", or \"wal\".")));
 
 	PG_RETURN_VOID();
 }
diff --git a/src/include/catalog/pg_proc.dat b/src/include/catalog/pg_proc.dat
index 0118e970dda..b6930174afb 100644
--- a/src/include/catalog/pg_proc.dat
+++ b/src/include/catalog/pg_proc.dat
@@ -12850,5 +12850,47 @@
 { oid => '8281', descr => 'hash',
   proname => 'hashoid8extended', prorettype => 'int8',
   proargtypes => 'oid8 int8', prosrc => 'hashoid8extended' },
+{ oid => '9560',
+    descr => 'statistics: number of VFD cache hits',
+  proname => 'pg_stat_get_vfd_hits',
+  provolatile => 'v', proparallel => 'r',
+  prorettype => 'int8', proargtypes => '',
+  prosrc => 'pg_stat_get_vfd_hits' },
+{ oid => '9561',
+    descr => 'statistics: number of VFD cache misses',
+  proname => 'pg_stat_get_vfd_misses',
+  provolatile => 'v', proparallel => 'r',
+  prorettype => 'int8', proargtypes => '',
+  prosrc => 'pg_stat_get_vfd_misses' },
+{ oid => '9562',
+    descr => 'statistics: number of VFD cache evictions',
+  proname => 'pg_stat_get_vfd_evictions',
+  provolatile => 'v', proparallel => 'r',
+  prorettype => 'int8', proargtypes => '',
+  prosrc => 'pg_stat_get_vfd_evictions' },
+{ oid => '9563',
+    descr => 'statistics: total number of allocated VFD cache entries',
+  proname => 'pg_stat_get_vfd_cache_size',
+  provolatile => 'v', proparallel => 'r',
+  prorettype => 'int4', proargtypes => '',
+  prosrc => 'pg_stat_get_vfd_cache_size' },
+{ oid => '9566',
+    descr => 'statistics: total memory footprint of VFD cache entries in bytes',
+    proname => 'pg_stat_get_vfd_cache_bytes',
+    provolatile => 'v', proparallel => 'r',
+    prorettype => 'int8', proargtypes => '',
+    prosrc => 'pg_stat_get_vfd_cache_bytes' },
+{ oid => '9564',
+  descr => 'statistics: timestamp of last VFD cache stats reset',
+  proname => 'pg_stat_get_vfd_stat_reset_time',
+  provolatile => 'v', proparallel => 'r',
+  prorettype => 'timestamptz', proargtypes => '',
+  prosrc => 'pg_stat_get_vfd_stat_reset_time' },
+{ oid => '9565',
+    descr => 'statistics: reset shared VFD cache counters',
+  proname => 'pg_stat_reset_vfdcache',
+  provolatile => 'v', proparallel => 'r',
+  prorettype => 'void', proargtypes => '',
+  prosrc => 'pg_stat_reset_vfdcache' },
 
 ]
diff --git a/src/include/pgstat.h b/src/include/pgstat.h
index 8e3549c3752..7e7e5b9383e 100644
--- a/src/include/pgstat.h
+++ b/src/include/pgstat.h
@@ -272,6 +272,35 @@ typedef struct PgStat_CheckpointerStats
 	TimestampTz stat_reset_timestamp;
 } PgStat_CheckpointerStats;
 
+/* ---------
+ * PgStat_VfdCacheStats		Virtual File Descriptor cache statistics
+ *
+ * Tracks hit/miss/eviction events in the VFD cache (fd.c).  These counters
+ * are accumulated in shared fixed stats and exposed by pg_stat_vfdcache.
+ * ---------
+ */
+typedef struct PgStat_VfdCacheStats
+{
+	PgStat_Counter vfd_hits;	/* fd was open, no open() syscall needed */
+	PgStat_Counter vfd_misses;	/* fd was VFD_CLOSED, open() was required */
+	PgStat_Counter vfd_evictions;	/* close() called to free a slot for a new
+									 * fd */
+	TimestampTz stat_reset_timestamp;
+}			PgStat_VfdCacheStats;
+
+/* ---------
+ * PgStat_BackendVfdCacheStats	VFD cache stats stored per backend
+ *
+ * Keeps per-backend VFD gauges in PGSTAT_KIND_BACKEND entries.
+ * vfd_entries and vfd_cache_bytes represent the current backend-local VFD
+ * cache footprint and are used to derive cluster-wide totals.
+ * ---------
+ */
+typedef struct PgStat_BackendVfdCacheStats
+{
+	PgStat_Counter vfd_entries;
+	PgStat_Counter vfd_cache_bytes;
+}			PgStat_BackendVfdCacheStats;
 
 /*
  * Types related to counting IO operations
@@ -521,6 +550,7 @@ typedef struct PgStat_WalStats
 typedef struct PgStat_Backend
 {
 	TimestampTz stat_reset_timestamp;
+	PgStat_BackendVfdCacheStats vfd_stats;
 	PgStat_BktypeIO io_stats;
 	PgStat_WalCounters wal_counters;
 } PgStat_Backend;
@@ -611,6 +641,15 @@ extern PgStat_BgWriterStats *pgstat_fetch_stat_bgwriter(void);
 extern void pgstat_report_checkpointer(void);
 extern PgStat_CheckpointerStats *pgstat_fetch_stat_checkpointer(void);
 
+/*
+ * Functions in pgstat_vfdcache.c
+ */
+
+extern PgStat_VfdCacheStats * pgstat_fetch_stat_vfdcache(void);
+extern void pgstat_reset_vfdcache(void);
+extern void pgstat_count_vfd_hit(void);
+extern void pgstat_count_vfd_miss(void);
+extern void pgstat_count_vfd_eviction(void);
 
 /*
  * Functions in pgstat_io.c
@@ -851,6 +890,12 @@ extern PGDLLIMPORT int pgstat_fetch_consistency;
 /* updated directly by bgwriter and bufmgr */
 extern PGDLLIMPORT PgStat_BgWriterStats PendingBgWriterStats;
 
+/*
+ * Variables in pgstat_vfdcache.c
+ */
+
+/* updated by VFD counting functions called from fd.c */
+extern PGDLLIMPORT PgStat_VfdCacheStats PendingVfdCacheStats;
 
 /*
  * Variables in pgstat_checkpointer.c
diff --git a/src/include/storage/fd.h b/src/include/storage/fd.h
index 8ac466fd346..e666a59f18d 100644
--- a/src/include/storage/fd.h
+++ b/src/include/storage/fd.h
@@ -149,6 +149,9 @@ extern char *FilePathName(File file);
 extern int	FileGetRawDesc(File file);
 extern int	FileGetRawFlags(File file);
 extern mode_t FileGetRawMode(File file);
+extern int	GetVfdCacheOccupancy(void);
+extern uint64 GetVfdCacheEntries(void);
+extern uint64 GetVfdCacheBytes(void);
 
 /* Operations used for sharing named temporary files */
 extern File PathNameCreateTemporaryFile(const char *path, bool error_on_failure);
diff --git a/src/include/utils/pgstat_internal.h b/src/include/utils/pgstat_internal.h
index 97704421a92..98d3f8ffbf9 100644
--- a/src/include/utils/pgstat_internal.h
+++ b/src/include/utils/pgstat_internal.h
@@ -488,6 +488,15 @@ typedef struct PgStatShared_Wal
 	PgStat_WalStats stats;
 } PgStatShared_Wal;
 
+typedef struct PgStatShared_VfdCache
+{
+	/* lock protects ->reset_offset as well as stats->stat_reset_timestamp */
+	LWLock		lock;
+	uint32		changecount;
+	PgStat_VfdCacheStats stats;
+	PgStat_VfdCacheStats reset_offset;
+}			PgStatShared_VfdCache;
+
 
 
 /* ----------
@@ -583,6 +592,7 @@ typedef struct PgStat_ShmemControl
 	PgStatShared_Lock lock;
 	PgStatShared_SLRU slru;
 	PgStatShared_Wal wal;
+	PgStatShared_VfdCache vfdcache;
 
 	/*
 	 * Custom stats data with fixed-numbered objects, indexed by (PgStat_Kind
@@ -619,6 +629,8 @@ typedef struct PgStat_Snapshot
 
 	PgStat_WalStats wal;
 
+	PgStat_VfdCacheStats vfdcache;
+
 	/*
 	 * Data in snapshot for custom fixed-numbered statistics, indexed by
 	 * (PgStat_Kind - PGSTAT_KIND_CUSTOM_MIN).  Each entry is allocated in
@@ -732,6 +744,16 @@ extern void pgstat_checkpointer_reset_all_cb(TimestampTz ts);
 extern void pgstat_checkpointer_snapshot_cb(void);
 
 
+/*
+ * Functions in pgstat_vfdcache.c
+ */
+
+extern bool pgstat_vfdcache_flush_cb(bool nowait);
+extern void pgstat_vfdcache_init_shmem_cb(void *stats);
+extern void pgstat_vfdcache_reset_all_cb(TimestampTz ts);
+extern void pgstat_vfdcache_snapshot_cb(void);
+
+
 /*
  * Functions in pgstat_database.c
  */
diff --git a/src/include/utils/pgstat_kind.h b/src/include/utils/pgstat_kind.h
index 2d78a029683..319ec0bd63b 100644
--- a/src/include/utils/pgstat_kind.h
+++ b/src/include/utils/pgstat_kind.h
@@ -39,9 +39,10 @@
 #define PGSTAT_KIND_LOCK	11
 #define PGSTAT_KIND_SLRU	12
 #define PGSTAT_KIND_WAL	13
+#define PGSTAT_KIND_VFDCACHE	14
 
 #define PGSTAT_KIND_BUILTIN_MIN PGSTAT_KIND_DATABASE
-#define PGSTAT_KIND_BUILTIN_MAX PGSTAT_KIND_WAL
+#define PGSTAT_KIND_BUILTIN_MAX PGSTAT_KIND_VFDCACHE
 #define PGSTAT_KIND_BUILTIN_SIZE (PGSTAT_KIND_BUILTIN_MAX + 1)
 
 /* Custom stats kinds */
diff --git a/src/test/regress/expected/rules.out b/src/test/regress/expected/rules.out
index 2b3cf6d8569..40320652624 100644
--- a/src/test/regress/expected/rules.out
+++ b/src/test/regress/expected/rules.out
@@ -2362,6 +2362,17 @@ pg_stat_user_tables| SELECT relid,
     stats_reset
    FROM pg_stat_all_tables
   WHERE ((schemaname <> ALL (ARRAY['pg_catalog'::name, 'information_schema'::name])) AND (schemaname !~ '^pg_toast'::text));
+pg_stat_vfdcache| SELECT pg_stat_get_vfd_hits() AS hits,
+    pg_stat_get_vfd_misses() AS misses,
+    pg_stat_get_vfd_evictions() AS evictions,
+    pg_stat_get_vfd_cache_size() AS cache_entries,
+    pg_stat_get_vfd_cache_bytes() AS cache_bytes,
+    (current_setting('max_files_per_process'::text))::integer AS max_files_per_process,
+        CASE
+            WHEN ((pg_stat_get_vfd_hits() + pg_stat_get_vfd_misses()) = 0) THEN NULL::double precision
+            ELSE ((pg_stat_get_vfd_hits())::double precision / ((pg_stat_get_vfd_hits() + pg_stat_get_vfd_misses()))::double precision)
+        END AS hit_ratio,
+    pg_stat_get_vfd_stat_reset_time() AS stats_reset;
 pg_stat_wal| SELECT wal_records,
     wal_fpi,
     wal_bytes,
diff --git a/src/test/regress/expected/stats.out b/src/test/regress/expected/stats.out
index ea7f7846895..2c168a40ed9 100644
--- a/src/test/regress/expected/stats.out
+++ b/src/test/regress/expected/stats.out
@@ -1127,10 +1127,24 @@ SELECT stats_reset > :'wal_reset_ts'::timestamptz FROM pg_stat_wal;
  t
 (1 row)
 
+-- Test that reset_shared with vfdcache specified as the stats type works
+SELECT stats_reset AS vfdcache_reset_ts FROM pg_stat_vfdcache \gset
+SELECT pg_stat_reset_shared('vfdcache');
+ pg_stat_reset_shared 
+----------------------
+ 
+(1 row)
+
+SELECT stats_reset > :'vfdcache_reset_ts'::timestamptz FROM pg_stat_vfdcache;
+ ?column? 
+----------
+ t
+(1 row)
+
 -- Test error case for reset_shared with unknown stats type
 SELECT pg_stat_reset_shared('unknown');
 ERROR:  unrecognized reset target: "unknown"
-HINT:  Target must be "archiver", "bgwriter", "checkpointer", "io", "recovery_prefetch", "slru", or "wal".
+HINT:  Target must be "archiver", "bgwriter", "checkpointer", "io", "recovery_prefetch", "slru", "vfdcache", or "wal".
 -- Test that reset works for pg_stat_database and pg_stat_database_conflicts
 -- Since pg_stat_database stats_reset starts out as NULL, reset it once first so that we
 -- have a baseline for comparison. The same for pg_stat_database_conflicts as it shares
diff --git a/src/test/regress/sql/stats.sql b/src/test/regress/sql/stats.sql
index 65d8968c83e..0b486e0f884 100644
--- a/src/test/regress/sql/stats.sql
+++ b/src/test/regress/sql/stats.sql
@@ -520,6 +520,11 @@ SELECT stats_reset AS wal_reset_ts FROM pg_stat_wal \gset
 SELECT pg_stat_reset_shared('wal');
 SELECT stats_reset > :'wal_reset_ts'::timestamptz FROM pg_stat_wal;
 
+-- Test that reset_shared with vfdcache specified as the stats type works
+SELECT stats_reset AS vfdcache_reset_ts FROM pg_stat_vfdcache \gset
+SELECT pg_stat_reset_shared('vfdcache');
+SELECT stats_reset > :'vfdcache_reset_ts'::timestamptz FROM pg_stat_vfdcache;
+
 -- Test error case for reset_shared with unknown stats type
 SELECT pg_stat_reset_shared('unknown');
 
-- 
2.34.1

