Hi hackers, In the work of trying to optimize async.c, I came across a surprisingly seemingly low hanging fruit in procsignal.c, to $subject.
This optimization improves not only LISTEN/NOTIFY,
but procsignal.c in general, for all ProcSignalReasons,
by avoiding to send redundant signals in the case
when the backend hasn't received and handled them yet.
--- PATCH ---
Previously, ProcSignal used an array of volatile sig_atomic_t flags, one
per signal reason. A sender would set a flag and then unconditionally
send a SIGUSR1 to the target process. This could result in a storm of
redundant signals if multiple processes signaled the same target before
it had a chance to run its signal handler.
Change this to use a single pg_atomic_uint32 as a bitmask of pending
signals. When sending, use pg_atomic_fetch_or_u32 to set the appropriate
signal bit and inspect the prior state of the flags word. Then only
issue a SIGUSR1 if the previous flags state was zero. This works safely
because the receiving backend's signal handler atomically resets the
entire bitmask upon receipt, thus processing all pending signals at
once. Consequently, subsequent senders seeing a nonzero prior state know
a signal is already in flight, significantly reducing redundant
kill(pid, SIGUSR1) system calls under heavy contention.
On the receiving end, the SIGUSR1 handler now atomically fetches and
clears the entire bitmask with a single pg_atomic_exchange_u32, then
calls the appropriate sub-handlers.
The further optimization to only check if the old flags word was zero is
due to Andreas Karlsson.
--- BENCHMARK ---
The attached benchmark script does LISTEN on one connection,
and then uses pgbench to send NOTIFY on a varying number of
connections and jobs, to cause a high procsignal load.
I've run the benchmark on my MacBook Pro M3 Max,
10 seconds per run, 3 runs.
Connections=Jobs | TPS (master) | TPS (patch) | Relative Diff (%) | StdDev
(master) | StdDev (patch)
------------------+--------------+-------------+-------------------+-----------------+----------------
1 | 118833 | 118902 | 0.06% |
484 | 520
2 | 156005 | 194873 | 24.91% |
3145 | 631
4 | 177351 | 190672 | 7.51% |
4305 | 1439
8 | 116597 | 124793 | 7.03% |
1549 | 1011
16 | 40835 | 113312 | 177.49% |
2695 | 1155
32 | 37940 | 108469 | 185.90% |
2533 | 487
64 | 35495 | 104994 | 195.80% |
1837 | 318
128 | 40193 | 100246 | 149.41% |
2254 | 393
(8 rows)
Raw Data Summary:
Version | Connections | Runs | Min TPS | Max TPS | Avg TPS
----------+-------------+------+---------+---------+---------
master | 1 | 3 | 118274 | 119119 | 118833
master | 2 | 3 | 152803 | 159090 | 156005
master | 4 | 3 | 174381 | 182288 | 177351
master | 8 | 3 | 115021 | 118117 | 116597
master | 16 | 3 | 39048 | 43935 | 40835
master | 32 | 3 | 35754 | 40716 | 37940
master | 64 | 3 | 33417 | 36906 | 35495
master | 128 | 3 | 37925 | 42433 | 40193
patch-v1 | 1 | 3 | 118589 | 119503 | 118902
patch-v1 | 2 | 3 | 194204 | 195457 | 194873
patch-v1 | 4 | 3 | 189771 | 192332 | 190672
patch-v1 | 8 | 3 | 123929 | 125904 | 124793
patch-v1 | 16 | 3 | 112328 | 114584 | 113312
patch-v1 | 32 | 3 | 107975 | 108949 | 108469
patch-v1 | 64 | 3 | 104649 | 105275 | 104994
patch-v1 | 128 | 3 | 99792 | 100479 | 100246
(16 rows)
/Joel#!/bin/bash
# Configuration for the PostgreSQL instances using absolute paths.
# This script does NOT modify the shell's PATH variable.
# --- Master Config ---
MASTER_NAME="master"
MASTER_PORT=5432
MASTER_BIN_PATH="$HOME/pg-master/bin"
MASTER_DATA="$HOME/pg-master-data"
MASTER_LOG="/tmp/pg-master.log"
# --- Patch v1 Config ---
PATCH_NAME="patch-v1"
PATCH_PORT=5432
PATCH_BIN_PATH="$HOME/pg-patch-v1/bin"
PATCH_DATA="$HOME/pg-patch-v1-data"
PATCH_LOG="/tmp/pg-patch-v1.log"
# Benchmark settings
CHANNEL_NAME="mychannel"
CONNECTIONS=(1 2 4 8 16 32 64 128)
DURATION=10 # Benchmark duration in seconds for each run
MEASUREMENTS=3 # Number of measurements per configuration
# CSV output file
CSV_OUTPUT="benchmark_results.csv"
# Temporary files
PGBENCH_SCRIPT=$(mktemp)
# --- Cleanup Function ---
# Ensures that servers are stopped and temp files are removed on script exit.
cleanup() {
echo ""
echo "Cleaning up..."
# Ensure both servers are stopped, silencing errors if they are not running.
# Use absolute paths and explicit data directories.
"$MASTER_BIN_PATH/pg_ctl" -D "$MASTER_DATA" -m fast stop &> /dev/null
"$PATCH_BIN_PATH/pg_ctl" -D "$PATCH_DATA" -m fast stop &> /dev/null
rm -f "$PGBENCH_SCRIPT"
echo "Cleanup complete."
}
# Trap the script's exit (normal or interrupted) to run the cleanup function
trap cleanup EXIT
# Initialize CSV file with headers
echo "version,connections,jobs,tps,run" > "$CSV_OUTPUT"
# --- Benchmark Function ---
# A generic function to run the benchmark for a given configuration.
# It starts, benchmarks, and then stops the specified server instance.
run_benchmark() {
local name=$1
local port=$2
local bin_path=$3
local data_path=$4
local log_file=$5
echo "--- Starting benchmark for: $name ---"
# Set PGPORT for client tools (pgbench, psql) for this run
export PGPORT=$port
# 1. Start the server using absolute path and explicit data directory
echo "Starting $name server on port $port..."
"$bin_path/pg_ctl" -D "$data_path" -l "$log_file" -o "-p $port" start
sleep 2 # Give server a moment to become available
# Create the pgbench script content
cat > "$PGBENCH_SCRIPT" << EOF
NOTIFY ${CHANNEL_NAME};
EOF
# 2. Start the listener in the background for this server
(echo "LISTEN ${CHANNEL_NAME};"; sleep 300) | "$bin_path/psql" -d postgres &> /dev/null &
local listener_pid=$!
# 3. Run the benchmark loop
echo "Running pgbench for connection counts: ${CONNECTIONS[*]}"
for c in "${CONNECTIONS[@]}"; do
echo " Testing with $c connections ($MEASUREMENTS measurements per run)..."
# Run multiple measurements for each connection count
for m in $(seq 1 $MEASUREMENTS); do
# Run pgbench and extract TPS value
tps=$("$bin_path/pgbench" -d postgres -f "$PGBENCH_SCRIPT" -c "$c" -j "$c" -T "$DURATION" -n \
| grep -E '^tps' \
| awk '{printf "%.0f", $3}')
# Write to CSV: version,connections,jobs,tps,run
echo "$name,$c,$c,$tps,$m" >> "$CSV_OUTPUT"
done
done
# 4. Stop the listener and the server
kill "$listener_pid" &> /dev/null
echo "Stopping $name server..."
"$bin_path/pg_ctl" -D "$data_path" -m fast stop &> /dev/null
echo "--- Benchmark for $name complete ---"
echo ""
}
# --- Main Execution ---
# 1. Run benchmark for master
run_benchmark "$MASTER_NAME" "$MASTER_PORT" "$MASTER_BIN_PATH" "$MASTER_DATA" "$MASTER_LOG"
# 2. Run benchmark for patch-v1
run_benchmark "$PATCH_NAME" "$PATCH_PORT" "$PATCH_BIN_PATH" "$PATCH_DATA" "$PATCH_LOG"
# 3. Generate report using PostgreSQL
echo "--- Generating Benchmark Report using PostgreSQL ---"
# Start the master server to run the analysis
export PGPORT=$MASTER_PORT
"$MASTER_BIN_PATH/pg_ctl" -D "$MASTER_DATA" -l "$MASTER_LOG" -o "-p $MASTER_PORT" start
sleep 2
# Create analysis database and load data
"$MASTER_BIN_PATH/psql" -d postgres << EOF
-- Create a temporary database for analysis
DROP DATABASE IF EXISTS bench_analysis;
CREATE DATABASE bench_analysis;
\c bench_analysis
-- Create table for benchmark results
CREATE TABLE benchmark_results (
version TEXT,
connections INT,
jobs INT,
tps NUMERIC,
run INT
);
-- Load CSV data
\COPY benchmark_results FROM '$CSV_OUTPUT' CSV HEADER
-- Generate comparison report
WITH avg_results AS (
SELECT
version,
connections,
AVG(tps) AS avg_tps,
STDDEV(tps) AS stddev_tps,
COUNT(*) AS runs
FROM benchmark_results
GROUP BY version, connections
),
comparison AS (
SELECT
m.connections,
m.avg_tps AS master_tps,
p.avg_tps AS patch_tps,
CASE
WHEN m.avg_tps > 0 THEN ((p.avg_tps - m.avg_tps) / m.avg_tps * 100)
ELSE 0
END AS relative_diff_pct,
m.stddev_tps AS master_stddev,
p.stddev_tps AS patch_stddev
FROM avg_results m
JOIN avg_results p ON m.connections = p.connections
WHERE m.version = 'master' AND p.version = 'patch-v1'
ORDER BY m.connections
)
SELECT
connections AS "Connections=Jobs",
ROUND(master_tps) AS "TPS (master)",
ROUND(patch_tps) AS "TPS (patch)",
ROUND(relative_diff_pct, 2) || '%' AS "Relative Diff (%)",
ROUND(master_stddev) AS "StdDev (master)",
ROUND(patch_stddev) AS "StdDev (patch)"
FROM comparison;
-- Also show raw data summary
\echo ''
\echo 'Raw Data Summary:'
SELECT
version AS "Version",
connections AS "Connections",
COUNT(*) AS "Runs",
ROUND(MIN(tps)) AS "Min TPS",
ROUND(MAX(tps)) AS "Max TPS",
ROUND(AVG(tps)) AS "Avg TPS"
FROM benchmark_results
GROUP BY version, connections
ORDER BY version, connections;
EOF
# Stop the server
"$MASTER_BIN_PATH/pg_ctl" -D "$MASTER_DATA" -m fast stop &> /dev/null
echo ""
echo "CSV results saved to: $CSV_OUTPUT"
version,connections,jobs,tps,run master,1,1,118274,1 master,1,1,119119,2 master,1,1,119105,3 master,2,2,152803,1 master,2,2,156122,2 master,2,2,159090,3 master,4,4,174381,1 master,4,4,175384,2 master,4,4,182288,3 master,8,8,116653,1 master,8,8,118117,2 master,8,8,115021,3 master,16,16,43935,1 master,16,16,39522,2 master,16,16,39048,3 master,32,32,40716,1 master,32,32,35754,2 master,32,32,37349,3 master,64,64,36161,1 master,64,64,33417,2 master,64,64,36906,3 master,128,128,37925,1 master,128,128,40221,2 master,128,128,42433,3 patch-v1,1,1,118589,1 patch-v1,1,1,119503,2 patch-v1,1,1,118615,3 patch-v1,2,2,194957,1 patch-v1,2,2,194204,2 patch-v1,2,2,195457,3 patch-v1,4,4,189771,1 patch-v1,4,4,189914,2 patch-v1,4,4,192332,3 patch-v1,8,8,123929,1 patch-v1,8,8,125904,2 patch-v1,8,8,124545,3 patch-v1,16,16,114584,1 patch-v1,16,16,113024,2 patch-v1,16,16,112328,3 patch-v1,32,32,108949,1 patch-v1,32,32,108483,2 patch-v1,32,32,107975,3 patch-v1,64,64,105059,1 patch-v1,64,64,105275,2 patch-v1,64,64,104649,3 patch-v1,128,128,100467,1 patch-v1,128,128,99792,2 patch-v1,128,128,100479,3
0001-Optimize-ProcSignal-to-avoid-redundant-SIGUSR1-signa.patch
Description: Binary data
