From 9442374fe890435510c24a2fd158d72670b5ab8c Mon Sep 17 00:00:00 2001 From: Nazir Bilal Yavuz Date: Thu, 16 Jul 2026 15:29:52 +0300 Subject: [PATCH v1 3/3] Benchmark script You can run it by using DATADIR=... ./bench_read_stream.sh --- bench_read_stream.sh | 130 ++++++++++++++++++++ src/test/modules/test_aio/test_aio--1.0.sql | 6 + src/test/modules/test_aio/test_aio.c | 68 ++++++++++ 3 files changed, 204 insertions(+) create mode 100755 bench_read_stream.sh diff --git a/bench_read_stream.sh b/bench_read_stream.sh new file mode 100755 index 00000000000..036bcc1c7a1 --- /dev/null +++ b/bench_read_stream.sh @@ -0,0 +1,130 @@ +#!/usr/bin/env bash +# +# bench_read_stream.sh - micro-benchmark for read-stream backward I/O combining. +# +# Uses the test_aio helper function: +# +# read_stream_bench(rel regclass, blocks int4[], nruns int4) -> int8 +# +# which streams a run of blocks forward (ascending) vs backward (descending) +# without materializing a result set, so the timing difference reflects the +# read-stream machinery itself. For each io_combine_limit it prints the +# forward time, backward time and their ratio. +# +# The script initdb's a fresh cluster in DATADIR, starts a Postgres server on +# it, runs the benchmark and stops it again, using plain initdb/pg_ctl/psql +# binaries (from BINDIR if set, else PATH). +# +# Required: +# DATADIR data directory to create and run on (initdb'd if empty) +# +# Optional: +# BINDIR directory with pg_ctl/psql (default: from PATH) +# MODE cached | cold | both (default: both) +# NBLOCKS heap blocks in the test relation (default: 60000) +# RUNS timed repetitions per data point (default: 5) +# NRUNS_CACHED internal repetitions when cached (default: 20) +# ICLS io_combine_limit values to sweep (default: "1 8 16 32") +# IO_METHOD io_method GUC (default: worker) +# SHARED_BUFFERS shared_buffers GUC (default: 1GB) +# PGPORT port to start the server on (default: 5440) +# +# Example: +# DATADIR=/path/to/pgdata BINDIR=/path/to/bin ./bench_read_stream.sh +# +set -euo pipefail + +: "${DATADIR:?set DATADIR to a data directory to create and run on}" +[ -n "${BINDIR:-}" ] && export PATH="$BINDIR:$PATH" + +MODE="${MODE:-both}" +NBLOCKS="${NBLOCKS:-60000}" +RUNS="${RUNS:-5}" +NRUNS_CACHED="${NRUNS_CACHED:-20}" +ICLS="${ICLS:-1 8 16 32}" +IO_METHOD="${IO_METHOD:-worker}" +SHARED_BUFFERS="${SHARED_BUFFERS:-1GB}" +export PGPORT="${PGPORT:-5440}" +export PGHOST=localhost +export PGDATABASE=postgres + +REL="bench_rs" +FILLER_BYTES=6000 +LOGFILE="$(mktemp)" +PSQL=(psql -X -q -v ON_ERROR_STOP=1) + +psql_q() { "${PSQL[@]}" -c "$1" >/dev/null; } +scalar() { "${PSQL[@]}" -tAc "$1"; } +# psql prints one "Time:" line per statement; keep the last (the bench call). +time_ms() { awk '/^Time:/ { v=$2 } END { print v }'; } + +stop_server() { pg_ctl -D "$DATADIR" -w -m fast stop >/dev/null 2>&1 || true; } +trap stop_server EXIT + +[ -s "$DATADIR/PG_VERSION" ] || initdb -D "$DATADIR" -A trust >/dev/null + +pg_ctl -D "$DATADIR" -l "$LOGFILE" -w \ + -o "-c port=$PGPORT -c listen_addresses=localhost \ + -c io_method=$IO_METHOD -c shared_buffers=$SHARED_BUFFERS \ + -c io_max_combine_limit=32 -c max_parallel_workers_per_gather=0" \ + start + +psql_q "DROP EXTENSION IF EXISTS test_aio; CREATE EXTENSION test_aio;" +if [ "$(scalar "SELECT count(*) FROM pg_proc WHERE proname='read_stream_bench'")" != 1 ]; then + echo "ERROR: read_stream_bench() not found; install a test_aio built with it." >&2 + exit 1 +fi + +# int4[] of 0..NBLOCKS-1, ascending (forward) or descending (backward). +arr_expr() { + local order=ASC; [ "$1" = backward ] && order=DESC + printf "(SELECT array_agg(g ORDER BY g %s)::int4[] FROM generate_series(0,%d) g)" \ + "$order" "$((NBLOCKS - 1))" +} + +# Rebuild the test relation with one row per block, then update NBLOCKS to the +# actual page count. +psql_q "DROP TABLE IF EXISTS $REL; + CREATE TABLE $REL (k int, filler text); + ALTER TABLE $REL ALTER COLUMN filler SET STORAGE PLAIN; + INSERT INTO $REL SELECT g, repeat('x', $FILLER_BYTES) + FROM generate_series(1, $NBLOCKS) g;" +psql_q "VACUUM (ANALYZE, FREEZE) $REL;" +NBLOCKS="$(scalar "SELECT relpages FROM pg_class WHERE relname='$REL'")" + +# $1 mode $2 direction $3 io_combine_limit -> best time in ms (echoed). +measure() { + local mode="$1" dir="$2" icl="$3" arr nruns=1 best="" t + arr="$(arr_expr "$dir")" + [ "$mode" = cached ] && nruns="$NRUNS_CACHED" + + for _ in $(seq 1 "$RUNS"); do + [ "$mode" = cold ] && psql_q "SELECT evict_rel('$REL');" + t="$("${PSQL[@]}" -tA -c "\timing on" \ + -c "SET io_combine_limit=$icl;" \ + -c "SELECT read_stream_bench('$REL', $arr, $nruns);" | time_ms)" + [ "$mode" = cached ] && t="$(awk "BEGIN{printf \"%.3f\", $t/$nruns}")" + if [ -z "$best" ] || awk "BEGIN{exit !($t < $best)}"; then best="$t"; fi + done + echo "$best" +} + +run_mode() { + local mode="$1" icl fwd bwd + printf '\n=== %s ===\n' "$mode" + printf '%-16s | %12s | %12s | %8s\n' io_combine_limit forward'(ms)' backward'(ms)' bwd/fwd + printf -- '-----------------+--------------+--------------+---------\n' + for icl in $ICLS; do + fwd="$(measure "$mode" forward "$icl")" + bwd="$(measure "$mode" backward "$icl")" + printf '%-16s | %12s | %12s | %7sx\n' "$icl" "$fwd" "$bwd" \ + "$(awk "BEGIN{printf \"%.2f\", $bwd/$fwd}")" + done +} + +case "$MODE" in + both) MODES="cached cold" ;; + cached|cold) MODES="$MODE" ;; + *) echo "ERROR: MODE must be cached|cold|both" >&2; exit 1 ;; +esac +for m in $MODES; do run_mode "$m"; done 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 0faddf93d02..200069bbc29 100644 --- a/src/test/modules/test_aio/test_aio--1.0.sql +++ b/src/test/modules/test_aio/test_aio--1.0.sql @@ -64,6 +64,12 @@ CREATE FUNCTION read_stream_for_blocks(rel regclass, blocks int4[], check_per_bu RETURNS SETOF record STRICT AS 'MODULE_PATHNAME' LANGUAGE C; +-- Benchmarking helper (not for committing): streams the given blocks nruns +-- times without materializing an SRF, returning the total buffers streamed. +CREATE FUNCTION read_stream_bench(rel regclass, blocks int4[], nruns int4 DEFAULT 1) +RETURNS int8 STRICT +AS 'MODULE_PATHNAME' LANGUAGE C; + /* * Handle related functions diff --git a/src/test/modules/test_aio/test_aio.c b/src/test/modules/test_aio/test_aio.c index 39637f68661..9420560d526 100644 --- a/src/test/modules/test_aio/test_aio.c +++ b/src/test/modules/test_aio/test_aio.c @@ -964,6 +964,74 @@ read_stream_for_blocks(PG_FUNCTION_ARGS) } +/* + * Benchmarking helper (NOT for committing). + * + * Like read_stream_for_blocks(), but streams the given blocks nruns times + * without materializing an SRF/tuplestore, pinning and immediately releasing + * each buffer. This isolates the CPU cost of the read stream machinery + * itself (e.g. the backward I/O combining bookkeeping) from the per-row SRF + * overhead, so timing differences between forward and backward block arrays + * reflect read-stream overhead rather than result materialization. + * + * Returns the total number of buffers streamed across all runs. + */ +PG_FUNCTION_INFO_V1(read_stream_bench); +Datum +read_stream_bench(PG_FUNCTION_ARGS) +{ + Oid relid = PG_GETARG_OID(0); + ArrayType *blocksarray = PG_GETARG_ARRAYTYPE_P(1); + int32 nruns = PG_GETARG_INT32(2); + Relation rel; + BlocksReadStreamData stream_data; + int64 total = 0; + + if (nruns < 1) + elog(ERROR, "nruns must be >= 1"); + + if (ARR_NDIM(blocksarray) != 1 || + ARR_HASNULL(blocksarray) || + ARR_ELEMTYPE(blocksarray) != INT4OID) + elog(ERROR, "expected 1 dimensional int4 array"); + + stream_data.nblocks = ARR_DIMS(blocksarray)[0]; + stream_data.blocks = (uint32 *) ARR_DATA_PTR(blocksarray); + + rel = relation_open(relid, AccessShareLock); + + for (int run = 0; run < nruns; run++) + { + ReadStream *stream; + Buffer buf; + + stream_data.curblock = 0; + + stream = read_stream_begin_relation(READ_STREAM_FULL, + NULL, + rel, + MAIN_FORKNUM, + read_stream_for_blocks_cb, + &stream_data, + 0); + + while ((buf = read_stream_next_buffer(stream, NULL)) != InvalidBuffer) + { + ReleaseBuffer(buf); + total++; + } + + read_stream_end(stream); + + CHECK_FOR_INTERRUPTS(); + } + + relation_close(rel, NoLock); + + PG_RETURN_INT64(total); +} + + PG_FUNCTION_INFO_V1(handle_get); Datum handle_get(PG_FUNCTION_ARGS) -- 2.47.3