From 17a6c723757b22ef5314045184fb78a2af324793 Mon Sep 17 00:00:00 2001 From: Melanie Plageman Date: Wed, 3 Jun 2026 18:19:35 -0400 Subject: [PATCH v15 16/19] Add client backend write combining test --- src/backend/storage/buffer/bufmgr.c | 2 +- src/include/storage/buf_internals.h | 3 + .../modules/test_aio/t/005_write_combining.pl | 166 ++++++++++++++++++ src/test/modules/test_aio/test_aio--1.0.sql | 4 + src/test/modules/test_aio/test_aio.c | 25 +++ 5 files changed, 199 insertions(+), 1 deletion(-) diff --git a/src/backend/storage/buffer/bufmgr.c b/src/backend/storage/buffer/bufmgr.c index abe78c39306..09205bda26f 100644 --- a/src/backend/storage/buffer/bufmgr.c +++ b/src/backend/storage/buffer/bufmgr.c @@ -2708,7 +2708,7 @@ EagerCleanStrategyBuffer(BufferAccessStrategy strategy, Buffer bufnum, * with it in a single write. The victim buffer must be already pinned and * locked and will remain pinned upon return. */ -static void +void EagerCleanBuffer(Buffer bufnum, BufferDesc *buf_hdr, IOContext io_context, WritebackContext *wb_context) { diff --git a/src/include/storage/buf_internals.h b/src/include/storage/buf_internals.h index 70b22419ad4..d107c1c0215 100644 --- a/src/include/storage/buf_internals.h +++ b/src/include/storage/buf_internals.h @@ -587,6 +587,9 @@ extern StartBufferIOResult StartSharedBufferIO(BufferDesc *buf, bool forInput, b extern void TerminateBufferIO(BufferDesc *buf, bool clear_dirty, uint64 set_flag_bits, bool forget_owner, bool release_aio); +extern void EagerCleanBuffer(Buffer bufnum, BufferDesc *buf_hdr, IOContext io_context, + WritebackContext *wb_context); + /* freelist.c */ extern IOContext IOContextForStrategy(BufferAccessStrategy strategy); diff --git a/src/test/modules/test_aio/t/005_write_combining.pl b/src/test/modules/test_aio/t/005_write_combining.pl index 089b7094eda..c51d14691e7 100644 --- a/src/test/modules/test_aio/t/005_write_combining.pl +++ b/src/test/modules/test_aio/t/005_write_combining.pl @@ -32,6 +32,8 @@ my $block_size = $node->safe_psql('postgres', "SELECT current_setting('block_size')::int"); test_checkpointer_combines_writes($node, $block_size); +test_regular_backend_combines_writes($node, $block_size); +test_eager_clean_combines_writes($node, $block_size); test_copy_from_combines_writes($node, $block_size); $node->stop(); @@ -56,6 +58,20 @@ WHERE backend_type = '$backend_type' return split /\|/, $result; } +sub io_stat_evictions +{ + my ($node, $backend_type, $context) = @_; + + return $node->safe_psql( + 'postgres', qq( +SELECT COALESCE(sum(evictions), 0)::bigint +FROM pg_stat_io +WHERE backend_type = '$backend_type' + AND object = 'relation' + AND context = '$context'; +)); +} + sub assert_combined_writes { local $Test::Builder::Level = $Test::Builder::Level + 1; @@ -98,6 +114,18 @@ sub assert_writes_at_least "$label wrote at least $expected_bytes bytes"); } +sub assert_evictions_at_least +{ + local $Test::Builder::Level = $Test::Builder::Level + 1; + + my ($node, $label, $expected_evictions) = @_; + my $evictions = io_stat_evictions($node, 'client backend', 'normal'); + + note "$label: evictions=$evictions"; + ok($evictions >= $expected_evictions, + "$label evicted at least $expected_evictions buffers"); +} + sub assert_blocks_dirty { local $Test::Builder::Level = $Test::Builder::Level + 1; @@ -136,6 +164,33 @@ sub dirty_blocks "SELECT make_blocks_unused_dirty_flushed('$table', ARRAY[$blocks])"); } +sub allocate_until_blocks_clean +{ + my ($node, $psql, $table, $blocks, $filler, $label) = @_; + my $shared_buffers_blocks = $node->safe_psql( + 'postgres', + "SELECT pg_size_bytes(current_setting('shared_buffers')) / current_setting('block_size')::int" + ); + my $extend_by = 1024; + my $allocated = 0; + my $max_allocations = 3 * $shared_buffers_blocks; + + while ($node->safe_psql('postgres', + "SELECT true = ANY (rel_blocks_are_dirty('$table', ARRAY[$blocks]))") eq 't') + { + if ($allocated >= $max_allocations) + { + die "$label: blocks $blocks still dirty after $allocated buffer allocations"; + } + + $psql->query_safe("SELECT grow_rel('$filler'::regclass, $extend_by)"); + $allocated += $extend_by; + } + + note "$label: allocated $allocated buffers to reach regular backend victims"; + $psql->query_safe('SELECT pg_stat_force_next_flush()'); +} + sub test_copy_from_combines_writes { my ($node, $block_size) = @_; @@ -212,3 +267,114 @@ sub test_checkpointer_combines_writes $psql->quit(); } + +# A higher level test that might be too expensive to commit +sub test_regular_backend_combines_writes +{ + my ($node, $block_size) = @_; + my $psql = $node->background_psql('postgres', on_error_stop => 0); + + # Keep unrelated dirty buffers out of the client-backend write statistics. + $node->safe_psql('postgres', 'CHECKPOINT'); + + $node->safe_psql( + 'postgres', qq( + CREATE TABLE wc_backend (id int, payload text); + INSERT INTO wc_backend SELECT g, repeat('y', 200) FROM generate_series(1, 1000) AS g; + SELECT flush_rel_buffers('wc_backend'::regclass); + CREATE UNLOGGED TABLE wc_backend_filler (id int); + CHECKPOINT; + )); + + #### + # Test one big combined write from regular backend buffer allocation. + #### + + dirty_blocks($psql, 'wc_backend', '0,1,2,3,4,5'); + assert_blocks_dirty($node, 'wc_backend', '0,1,2,3,4,5', 't', + 'contiguous buffers are dirty before regular backend allocation'); + + flush_and_reset_io_stats($node, $psql); + allocate_until_blocks_clean($node, $psql, 'wc_backend', '0,1,2,3,4,5', + 'wc_backend_filler', 'contiguous regular backend'); + + assert_writes($node, 'contiguous regular backend', 'client backend', + 'normal', 1, 6 * $block_size); + assert_evictions_at_least($node, 'contiguous regular backend', 6); + assert_any_blocks_dirty($node, 'wc_backend', '0,1,2,3,4,5', 'f', + 'regular backend wrote contiguous dirty buffers'); + + #### + # Test multiple single block writes when interspersed blocks are not in + # shared buffers. + #### + + $psql->query_safe( + "SELECT invalidate_rel_blocks('wc_backend', ARRAY[1,3,5])"); + dirty_blocks($psql, 'wc_backend', '0,2,4'); + flush_and_reset_io_stats($node, $psql); + + allocate_until_blocks_clean($node, $psql, 'wc_backend', '0,2,4', + 'wc_backend_filler', 'nonresident gaps regular backend'); + assert_writes($node, 'nonresident gaps regular backend', 'client backend', + 'normal', 3, + 3 * $block_size); + assert_evictions_at_least($node, 'nonresident gaps regular backend', 3); + assert_any_blocks_dirty($node, 'wc_backend', '0,2,4', 'f', + 'regular backend wrote dirty buffers separated by nonresident gaps'); + + $psql->quit(); +} + +sub test_eager_clean_combines_writes +{ + my ($node, $block_size) = @_; + my $psql = $node->background_psql('postgres', on_error_stop => 0); + + $node->safe_psql( + 'postgres', qq( + CREATE TABLE wc_victim (id int, payload text); + INSERT INTO wc_victim SELECT g, repeat('y', 200) FROM generate_series(1, 1000) AS g; + SELECT flush_rel_buffers('wc_victim'::regclass); + CHECKPOINT; + )); + + #### + # Test one big combined write when EagerCleanBuffer() is called directly. + #### + + dirty_blocks($psql, 'wc_victim', '0,1,2,3,4,5'); + assert_blocks_dirty($node, 'wc_victim', '0,1,2,3,4,5', 't', + 'contiguous buffers are dirty before direct eager clean'); + + flush_and_reset_io_stats($node, $psql); + $psql->query_safe("SELECT eager_clean_rel_block('wc_victim', 0)"); + $psql->query_safe('SELECT pg_stat_force_next_flush()'); + + assert_writes($node, 'contiguous direct eager clean', 'client backend', + 'normal', 1, 6 * $block_size); + assert_any_blocks_dirty($node, 'wc_victim', '0,1,2,3,4,5', 'f', + 'direct eager clean wrote contiguous dirty buffers'); + + #### + # Test multiple single block writes when interspersed blocks are not in + # shared buffers. + #### + + $psql->query_safe( + "SELECT invalidate_rel_blocks('wc_victim', ARRAY[1,3,5])"); + dirty_blocks($psql, 'wc_victim', '0,2,4'); + flush_and_reset_io_stats($node, $psql); + + $psql->query_safe("SELECT eager_clean_rel_block('wc_victim', 0)"); + $psql->query_safe("SELECT eager_clean_rel_block('wc_victim', 2)"); + $psql->query_safe("SELECT eager_clean_rel_block('wc_victim', 4)"); + $psql->query_safe('SELECT pg_stat_force_next_flush()'); + assert_writes($node, 'nonresident gaps direct eager clean', 'client backend', + 'normal', 3, + 3 * $block_size); + assert_any_blocks_dirty($node, 'wc_victim', '0,2,4', 'f', + 'direct eager clean wrote dirty buffers separated by nonresident gaps'); + + $psql->quit(); +} diff --git a/src/test/modules/test_aio/test_aio--1.0.sql b/src/test/modules/test_aio/test_aio--1.0.sql index e1dc6a5ef10..d7d5006b3d5 100644 --- a/src/test/modules/test_aio/test_aio--1.0.sql +++ b/src/test/modules/test_aio/test_aio--1.0.sql @@ -45,6 +45,10 @@ CREATE FUNCTION make_blocks_unused_dirty_flushed(rel regclass, blocks int4[]) RETURNS pg_catalog.void STRICT AS 'MODULE_PATHNAME' LANGUAGE C; +CREATE FUNCTION eager_clean_rel_block(rel regclass, blockno int4) +RETURNS pg_catalog.void STRICT +AS 'MODULE_PATHNAME' LANGUAGE C; + CREATE FUNCTION rel_blocks_are_dirty(rel regclass, blocks int4[]) RETURNS pg_catalog.bool[] STRICT AS 'MODULE_PATHNAME' LANGUAGE C; diff --git a/src/test/modules/test_aio/test_aio.c b/src/test/modules/test_aio/test_aio.c index c062b9177a4..b54764dcb46 100644 --- a/src/test/modules/test_aio/test_aio.c +++ b/src/test/modules/test_aio/test_aio.c @@ -709,6 +709,31 @@ make_blocks_unused_dirty_flushed(PG_FUNCTION_ARGS) PG_RETURN_VOID(); } +PG_FUNCTION_INFO_V1(eager_clean_rel_block); +Datum +eager_clean_rel_block(PG_FUNCTION_ARGS) +{ + Oid relid = PG_GETARG_OID(0); + BlockNumber blkno = PG_GETARG_UINT32(1); + Relation rel; + Buffer buf; + + rel = relation_open(relid, AccessShareLock); + if (RelationUsesLocalBuffers(rel)) + ereport(ERROR, + (errmsg("cannot eager clean local buffers"))); + + buf = ReadBufferExtended(rel, MAIN_FORKNUM, blkno, RBM_NORMAL, NULL); + LockBuffer(buf, BUFFER_LOCK_SHARE_EXCLUSIVE); + EagerCleanBuffer(buf, GetBufferDescriptor(buf - 1), IOCONTEXT_NORMAL, + &BackendWritebackContext); + ReleaseBuffer(buf); + + relation_close(rel, NoLock); + + PG_RETURN_VOID(); +} + PG_FUNCTION_INFO_V1(rel_blocks_are_dirty); Datum rel_blocks_are_dirty(PG_FUNCTION_ARGS) -- 2.43.0