From 2d423d2b88e0f14a2fb51657ddfd93f7648598ca Mon Sep 17 00:00:00 2001
From: Jingtang Zhang <mrdrivingduck@gmail.com>
Date: Sun, 1 Feb 2026 16:12:48 +0800
Subject: [PATCH] Optimize CPU usage of dropping buffers during recovery

Initialize cached nblocks to 0 when redo CREATE record.
---
 src/backend/access/transam/xlogutils.c    |  7 +++
 src/backend/catalog/storage.c             |  7 +++
 src/test/recovery/t/060_truncate_empty.pl | 69 +++++++++++++++++++++++
 3 files changed, 83 insertions(+)
 create mode 100644 src/test/recovery/t/060_truncate_empty.pl

diff --git a/src/backend/access/transam/xlogutils.c b/src/backend/access/transam/xlogutils.c
index 5fbe39133b8..387c7621218 100644
--- a/src/backend/access/transam/xlogutils.c
+++ b/src/backend/access/transam/xlogutils.c
@@ -489,6 +489,13 @@ XLogReadBufferExtended(RelFileLocator rlocator, ForkNumber forknum,
 	 */
 	smgrcreate(smgr, forknum, true);
 
+	/*
+	 * Invalidate the cache if the cached value is 0, and let smgrnblocks ask
+	 * the kernel. The relation might be longer than the cached value due to
+	 * relation extension before crash.
+	 */
+	if (smgr->smgr_cached_nblocks[forknum] == 0)
+		smgr->smgr_cached_nblocks[forknum] = InvalidBlockNumber;
 	lastblock = smgrnblocks(smgr, forknum);
 
 	if (blkno < lastblock)
diff --git a/src/backend/catalog/storage.c b/src/backend/catalog/storage.c
index e443a4993c5..859f515411a 100644
--- a/src/backend/catalog/storage.c
+++ b/src/backend/catalog/storage.c
@@ -993,6 +993,13 @@ smgr_redo(XLogReaderState *record)
 
 		reln = smgropen(xlrec->rlocator, INVALID_PROC_NUMBER);
 		smgrcreate(reln, xlrec->forkNum, true);
+
+		if (xlrec->forkNum == MAIN_FORKNUM)
+		{
+			reln->smgr_cached_nblocks[MAIN_FORKNUM] = 0;
+			reln->smgr_cached_nblocks[FSM_FORKNUM] = 0;
+			reln->smgr_cached_nblocks[VISIBILITYMAP_FORKNUM] = 0;
+		}
 	}
 	else if (info == XLOG_SMGR_TRUNCATE)
 	{
diff --git a/src/test/recovery/t/060_truncate_empty.pl b/src/test/recovery/t/060_truncate_empty.pl
new file mode 100644
index 00000000000..1956733f05f
--- /dev/null
+++ b/src/test/recovery/t/060_truncate_empty.pl
@@ -0,0 +1,69 @@
+use strict;
+use warnings FATAL => 'all';
+use PostgreSQL::Test::Cluster;
+use PostgreSQL::Test::Utils;
+use Test::More;
+use Time::HiRes qw(gettimeofday tv_interval);
+
+my $node = PostgreSQL::Test::Cluster->new('primary');
+$node->init();
+
+$node->append_conf('postgresql.conf', 'shared_buffers = 4GB');
+$node->append_conf('postgresql.conf', 'restart_after_crash = on');
+
+$node->start();
+
+$node->safe_psql('postgres',
+    q[CREATE TABLE test (id int);]);
+
+# SIGSTOP checkpointer and run some transactions
+my $checkpointer_pid = $node->safe_psql('postgres',
+    q[SELECT pid FROM pg_stat_activity WHERE backend_type = 'checkpointer';]);
+chomp($checkpointer_pid);
+kill 'STOP', $checkpointer_pid;
+note("Checkpointer stopped");
+
+$node->pgbench(
+    '--no-vacuum --client=10 --transactions=1000',
+    0,
+    [qr{actually processed}],
+    [qr{^$}],
+    'concurrent CREATE and DROP TABLE transactions',
+    {
+        'truncate_empty_script' => q(
+            BEGIN;
+            INSERT INTO test VALUES (:client_id);
+            DELETE FROM test WHERE id = :client_id;
+            CREATE TABLE test_empty_:client_id (id int);
+            DROP TABLE test_empty_:client_id;
+            COMMIT;
+        )
+    });
+
+# stop the node in immediate mode for crash recovery
+$node->stop('immediate');
+
+my $recovery_start = [gettimeofday];
+$node->start();
+my $recovery_end = [gettimeofday];
+my $recovery_time = tv_interval($recovery_start, $recovery_end);
+
+note("Crash recovery time: ${recovery_time} seconds");
+
+my $log_content = $node->log_content();
+if ($log_content =~ /redo done at .+? system usage: CPU: user: ([\d.]+) s, system: ([\d.]+) s, elapsed: ([\d.]+) s/m)
+{
+    my $cpu_user = $1;
+    my $cpu_system = $2;
+    my $redo_elapsed = $3;
+    
+    note("Redo elapsed time: $redo_elapsed s");
+    note("  CPU user: $cpu_user s, system: $cpu_system s");
+}
+
+# consistency check
+my $result = $node->safe_psql('postgres', q[SELECT COUNT(*) FROM test;]);
+is($result, '0', 'test table is empty after recovery');
+
+$node->stop();
+done_testing();
-- 
2.50.1 (Apple Git-155)

