Hi Hannu,

I measured v2 on current master (it applies cleanly).  Behaviourally
it is v1 plus the postgresql.conf.sample entry, so the observations
below hold on both versions; the attached scripts reproduce every
point.  I looked at the behaviour rather than the design; some of this
may well be intended.

The good news first: in plain mode the cost is negligible -- +2
instructions per ReadyForQuery (+0.01%), tps within noise over two
runs, and the 'Z' message is byte-identical to master.

Two of the stated claims did not hold in my tests.

> libpq (fe-protocol3.c) is updated so that clients ignore any trailing
> extension bytes in ReadyForQuery messages if they receive them

An unpatched client does not tolerate a rich 'Z'.  With master's own
libpq and with PG18's libpq, a rich session fails to connect:
  message contents do not agree with length in message type "Z"
And since ready_for_query_message is PGC_USERSET, a client that
connects in plain and later runs SET ready_for_query_message = rich
hits the same error mid-session and hangs (I had to kill psql after
8s, twice).

> Unless you set ready_for_query_message=rich the protocol does not
> change at all

The 'Z' itself is unchanged, but because the GUC is GUC_REPORT the
patched server sends one extra ParameterStatus
(ready_for_query_message=plain) per connection that master does not.
Harmless, but not strictly no change.

On the session indicators, comparing the rich values with SQL in the
same session:
- T stays 1 after the temp objects are gone -- DROP, DISCARD TEMP and
  DISCARD ALL all leave T=1 with an empty pg_class.  A pooler would
  read the connection as dirty permanently.
- P reflects the whole cluster, not the session: a session that never
  prepared a transaction reports P=1 while another session has one
  prepared.
- L matches pg_current_wal_insert_lsn() numerically, but it is printed
  with %X where pg_lsn uses %08X, so the text differs only while the
  low word is under 8 hex digits, e.g. on a fresh cluster (0/17F5830
  vs 0/017F5830).
- H was correct in every case.

Cost of rich grows with open cursors.  The H check walks the full
portal list on every 'Z', so it is O(cursors), not O(1): +5.9%
instructions per 'Z' on a clean session, +21% with 100 open cursors,
+160% with 1000.  As 'Z' is on the per-query path, that may matter for
the pooling case the patch targets.

On CI: v2 turns it green by adding the GUC to postgresql.conf.sample
(that was the 003_check_guc failure).  The rich path itself is
unchanged -- fe-trace.c is still not updated, so PQtrace() prints
"mismatched message length" on each rich 'Z' and the 7 libpq_pipeline
trace tests fail under rich; they pass in plain, which is what CI
runs.  make check is 239/239 in both modes.  The new test module
passes but does not exercise any of the cases above, and there is
still an unrelated change to Cluster.pm.

The attached README lists the exact steps for each point above.

Regards,
Manu
How to reproduce the observations on the ReadyForQuery 'rich' patch
(v1-0001 / v2-0001; the behaviour is identical)
===================================================================

All four scripts are attached as .txt so the commitfest bot ignores them;
drop the "nocfbot-" prefix and the ".txt" suffix to use them.

  rfq-wire-client.py      raw protocol v3 client, no libpq
  rfq-old-libpq-client.c  libpq client to test an unpatched libpq
  rfq-pgbench.sh          pgbench -S, three variants interleaved
  rfq-instructions.sh     perf instructions:u per ReadyForQuery


1. Builds and servers
---------------------

Two installs from the same master commit (5c74e122ef2), one with v1-0001
applied (it applies cleanly).  Both built with

  ./configure --prefix=<install> --enable-tap-tests --enable-depend \
              CFLAGS="-O2 -fno-omit-frame-pointer"

(no --enable-cassert).  Then two servers on one Unix socket directory,
trust auth, no TCP:

  SOCK=/tmp/rfq-sock; mkdir -p $SOCK
  BASE=<base install>; PATCHED=<patched install>
  $BASE/bin/initdb    -D /tmp/rfq-data/base    -U postgres -A trust --no-sync
  $PATCHED/bin/initdb -D /tmp/rfq-data/patched -U postgres -A trust --no-sync
  OPTS="-k $SOCK -c listen_addresses='' -c max_prepared_transactions=5 -c 
shared_buffers=256MB"
  $BASE/bin/pg_ctl    -D /tmp/rfq-data/base    -l /tmp/rfq-data/base.log    -o 
"-p 54501 $OPTS" start
  $PATCHED/bin/pg_ctl -D /tmp/rfq-data/patched -l /tmp/rfq-data/patched.log -o 
"-p 54502 $OPTS" start

The scripts default to SOCK=/tmp/rfq-sock, BASE_PORT=54501 and
PATCHED_PORT=54502.


2. Wire bytes in plain mode (rfq-wire-client.py capture)
--------------------------------------------------------

  python3 rfq-wire-client.py capture $SOCK 54501 > base.json
  python3 rfq-wire-client.py capture $SOCK 54502 > patched-plain.json

Compare the hex of each message (BackendKeyData differs on every
connection).  What we saw: the 'Z' after startup, after a simple Query and
after Sync is 5a 00000005 49 on both.  The startup sequence of the patched
server has one more ParameterStatus, ready_for_query_message=plain, because
the GUC is GUC_REPORT.


3. What T, H, P and L report (rfq-wire-client.py script)
---------------------------------------------------------

Each statement is sent as a simple Query; the output has the 'Z' that
closes it, decoded.

  python3 rfq-wire-client.py script $SOCK 54502 '-c 
ready_for_query_message=rich' -- \
      'CREATE TEMP TABLE t(a int)' 'DROP TABLE t' 'DISCARD TEMP' 'DISCARD ALL' \
      'SELECT count(*) FROM pg_class WHERE relpersistence = $$t$$'

T: it goes to 1 on CREATE TEMP TABLE and stays 1 after DROP TABLE,
DISCARD TEMP and DISCARD ALL, with no temporary relation left.

P: from a second session, prepare a transaction, then query from the
first one:

  $PATCHED/bin/psql -h $SOCK -p 54502 -U postgres \
      -c "BEGIN" -c "CREATE TABLE p(a int)" -c "PREPARE TRANSACTION 'x'"
  python3 rfq-wire-client.py script $SOCK 54502 '-c 
ready_for_query_message=rich' -- 'SELECT 1'
  $PATCHED/bin/psql -h $SOCK -p 54502 -U postgres -c "COMMIT PREPARED 'x'"

The session that never prepared anything gets P=1 until the COMMIT
PREPARED.

L: compare it with the server function in the same session:

  python3 rfq-wire-client.py script $SOCK 54502 '-c 
ready_for_query_message=rich' -- \
      'CREATE TABLE l(a int)' 'SELECT pg_current_wal_insert_lsn()'

The values are equal as LSNs, but the text differs: L is printed with
%X/%X (0/17F5830) and pg_lsn with %X/%08X (0/017F5830).  This only shows
while the low half has fewer than 8 hex digits, i.e. on a fresh cluster.



4. An unpatched libpq against rich mode (rfq-old-libpq-client.c)
----------------------------------------------------------------

  gcc -o old_client rfq-old-libpq-client.c -I$BASE/include -L$BASE/lib -lpq
  for lib in $BASE/lib $PATCHED/lib; do
    LD_LIBRARY_PATH=$lib ./old_client \
      "host=$SOCK port=54502 user=postgres dbname=postgres options='-c 
ready_for_query_message=rich'" \
      trace-$(basename $(dirname $lib)).txt
  done

With the unpatched libpq the connection fails with: message contents do not
agree with length in message type "Z".  We saw the same with a stock
PostgreSQL 18 libpq.  With options set to plain, all steps pass with any
libpq.

Switching after connecting, since the GUC is PGC_USERSET:

  LD_LIBRARY_PATH=$BASE/lib timeout 8 $BASE/bin/psql -X -h $SOCK -p 54502 -U 
postgres \
      -c 'select 1' -c 'set ready_for_query_message = rich' -c 'select 2'

The SET is answered, then the same error appears and psql does not return
(exit 124 from timeout).

With the patched libpq the trace file shows "mismatched message length:
consumed 5, expected 23" on every rich 'Z' (fe-trace.c is not changed by
the patch); this is also why the "trace match" checks of libpq_pipeline
fail when it runs with PGOPTIONS='-c ready_for_query_message=rich'.


5. Throughput (rfq-pgbench.sh)
------------------------------

  PATCHED_BIN=$PATCHED/bin CPUS=<a fixed core set> bash rfq-pgbench.sh 10 10 8

pgbench -S -M prepared, 8 clients, 10 s per run, 10 rounds, the three
variants rotated in every round, always with the patched pgbench.  Prints
the median tps per variant and the relative differences.


6. Instructions per ReadyForQuery (rfq-instructions.sh)
-------------------------------------------------------

  PATCHED_BIN=$PATCHED/bin bash rfq-instructions.sh 5

Counts instructions:u of a single backend for 10000 and for 30000
'SELECT 1', and divides the difference by 20000.  Cases: a clean session,
and 100 or 1000 cursors without HOLD open in the transaction.  Our numbers
(identical over two separate runs):

  clean         plain 29114   rich 30828   (+1714, +5.9%)
  100 cursors   plain 26115   rich 31703   (+5588, +21.4%)
  1000 cursors  plain 26112   rich 67815   (+41703, +159.7%)

Plain minus master is +2 instructions per ReadyForQuery.  The rich cost
grows with the number of open portals, about 40 instructions per portal,
which matches HasActiveWithHoldCursors() walking the whole portal hash
table on every ReadyForQuery.
#!/usr/bin/env python3
"""Raw protocol v3 client (no libpq).  It records the exact bytes of every
message the server sends, i.e. what is on the socket, with no dissector in
between.

  rfq_wire_client.py capture <sockdir> <port> [options]
      -> JSON with every server message for three phases: startup, a simple
         Query 'SELECT 1', and an extended Parse/Bind/Execute/Sync 'SELECT 1'.
  rfq_wire_client.py script <sockdir> <port> [options] -- 'sql1' 'sql2' ...
      -> for each statement (sent as a simple Query), the 'Z' message that
         closes it, raw and decoded, plus the first row and any error.

  [options] is the value of the startup 'options' parameter,
  e.g. '-c ready_for_query_message=rich'.

Connects over a Unix socket as user postgres and expects trust authentication.
"""
import json
import socket
import struct
import sys


class Conn:
    def __init__(self, sockdir, port, options=None, user="postgres", 
db="postgres"):
        self.s = socket.socket(socket.AF_UNIX)
        self.s.connect(f"{sockdir}/.s.PGSQL.{port}")
        self.buf = b""
        params = [b"user", user.encode(), b"database", db.encode()]
        if options:
            params += [b"options", options.encode()]
        body = struct.pack("!I", 196608) + b"".join(p + b"\0" for p in params) 
+ b"\0"
        self.s.sendall(struct.pack("!I", len(body) + 4) + body)

    def _read(self, n):
        while len(self.buf) < n:
            chunk = self.s.recv(65536)
            if not chunk:
                raise EOFError("server closed the connection")
            self.buf += chunk
        out, self.buf = self.buf[:n], self.buf[n:]
        return out

    def msg(self):
        head = self._read(5)
        typ, length = head[:1], struct.unpack("!I", head[1:])[0]
        return typ, head + self._read(length - 4)

    def until_z(self):
        msgs = []
        while True:
            t, raw = self.msg()
            msgs.append((t, raw))
            if t == b"R" and struct.unpack("!I", raw[5:9])[0] != 0:
                raise RuntimeError("server asks for authentication; use trust")
            if t == b"Z":
                return msgs

    def send(self, typ, body):
        self.s.sendall(typ + struct.pack("!I", len(body) + 4) + body)

    def query(self, sql):
        self.send(b"Q", sql.encode() + b"\0")
        return self.until_z()

    def extended(self, sql):
        self.send(b"P", b"\0" + sql.encode() + b"\0" + struct.pack("!H", 0))
        self.send(b"B", b"\0\0" + struct.pack("!HHH", 0, 0, 0))
        self.send(b"E", b"\0" + struct.pack("!I", 0))
        self.send(b"S", b"")
        return self.until_z()


def decode_z(raw):
    """Raw 'Z' -> {len, status, kv: [(key, value)], unparsed}."""
    length = struct.unpack("!I", raw[1:5])[0]
    body = raw[5:]
    out = {"len": length, "status": body[:1].decode(), "kv": []}
    off = 1
    while off < len(body):
        klen = body[off]; off += 1
        k = body[off:off + klen]; off += klen
        if off >= len(body):
            out["unparsed"] = body[off - klen - 1:].hex(); break
        vlen = body[off]; off += 1
        v = body[off:off + vlen]; off += vlen
        out["kv"].append([k.decode(), v.decode()])
    return out


def describe(t, raw):
    d = {"type": t.decode(), "len": len(raw), "hex": raw.hex()}
    if t == b"S":
        k, v, _ = raw[5:].split(b"\0", 2)
        d["param"] = [k.decode(), v.decode()]
    if t == b"K":
        d["note"] = "BackendKeyData: differs on every connection, ignore when 
comparing"
    if t == b"Z":
        d["z"] = decode_z(raw)
    return d


def main():
    mode, sockdir, port = sys.argv[1], sys.argv[2], sys.argv[3]
    rest = sys.argv[4:]
    options = None
    if rest and rest[0] != "--":
        options = rest.pop(0)
    c = Conn(sockdir, port, options)
    if mode == "capture":
        phases = {"startup": c.until_z(), "simple_select_1": c.query("SELECT 
1"),
                  "extended_select_1": c.extended("SELECT 1")}
        print(json.dumps({p: [describe(t, r) for t, r in m] for p, m in 
phases.items()}, indent=1))
    elif mode == "script":
        sqls = rest[1:] if rest and rest[0] == "--" else rest
        res = [{"sql": "(startup)", "z": decode_z(c.until_z()[-1][1])}]
        for sql in sqls:
            msgs = c.query(sql)
            err = [r[5:].split(b"\0M")[1].split(b"\0")[0].decode() for t, r in 
msgs if t == b"E"]
            rows = [r for t, r in msgs if t == b"D"]
            row = None
            if rows:
                r = rows[0]; n = struct.unpack("!H", r[5:7])[0]; off = 7; vals 
= []
                for _ in range(n):
                    vlen = struct.unpack("!i", r[off:off + 4])[0]; off += 4
                    vals.append(None if vlen < 0 else r[off:off + 
vlen].decode()); off += max(vlen, 0)
                row = vals
            res.append({"sql": sql, "row": row, "error": err or None,
                        "z_hex": msgs[-1][1].hex(), "z": decode_z(msgs[-1][1])})
        print(json.dumps(res, indent=1))
    else:
        sys.exit(__doc__)


if __name__ == "__main__":
    main()
/*
 * Does a libpq built WITHOUT the patch accept the rich 'Z' message?
 *
 * Build once against any libpq, then swap the library at run time with
 * LD_LIBRARY_PATH.  It exercises the three libpq paths that read
 * ReadyForQuery: simple query, extended query (PQexecParams) and fast-path
 * (PQfn, through lo_creat).  Every message is traced with PQtrace().
 *
 *   old_libpq_client "<conninfo>" <trace_file>
 *
 * Exit status: 0 = all steps OK, 1 = some step failed, 2 = could not connect.
 */
#include <stdio.h>
#include <stdlib.h>
#include "libpq-fe.h"
#include "libpq/libpq-fs.h"

static int
step(PGconn *conn, const char *label, PGresult *res)
{
        ExecStatusType st = res ? PQresultStatus(res) : PGRES_FATAL_ERROR;
        int                     ok = (st == PGRES_TUPLES_OK || st == 
PGRES_COMMAND_OK);

        printf("  %-28s %s  conn=%s  txstatus=%d  value=%s  error=%s\n", label,
                   ok ? "OK  " : "FAIL", PQstatus(conn) == CONNECTION_OK ? "OK" 
: "BAD",
                   PQtransactionStatus(conn),
                   (ok && st == PGRES_TUPLES_OK && PQntuples(res) > 0) ? 
PQgetvalue(res, 0, 0) : "-",
                   ok ? "-" : PQerrorMessage(conn));
        PQclear(res);
        return ok;
}

int
main(int argc, char **argv)
{
        PGconn     *conn;
        FILE       *trace;
        const char *params[1] = {"7"};
        Oid                     lo;
        int                     failures = 0;

        if (argc != 3)
        {
                fprintf(stderr, "usage: %s \"<conninfo>\" <trace_file>\n", 
argv[0]);
                return 2;
        }
        printf("  libpq version=%d\n", PQlibVersion());
        conn = PQconnectdb(argv[1]);
        if (PQstatus(conn) != CONNECTION_OK)
        {
                printf("  CONNECT FAILED: %s\n", PQerrorMessage(conn));
                return 2;
        }
        printf("  connect OK  ready_for_query_message=%s\n",
                   PQparameterStatus(conn, "ready_for_query_message") ?
                   PQparameterStatus(conn, "ready_for_query_message") : "(not 
reported)");

        trace = fopen(argv[2], "w");
        PQtrace(conn, trace);
        failures += !step(conn, "simple: SELECT 1", PQexec(conn, "SELECT 1"));
        failures += !step(conn, "simple: SELECT 2", PQexec(conn, "SELECT 2"));
        failures += !step(conn, "extended: SELECT $1",
                                          PQexecParams(conn, "SELECT $1::int", 
1, NULL, params, NULL, NULL, 0));
        failures += !step(conn, "BEGIN", PQexec(conn, "BEGIN"));
        lo = lo_creat(conn, INV_READ | INV_WRITE);
        printf("  %-28s %s  oid=%u  error=%s\n", "fast-path: lo_creat (PQfn)",
                   lo ? "OK  " : "FAIL", lo, lo ? "-" : PQerrorMessage(conn));
        failures += !lo;
        failures += !step(conn, "COMMIT", PQexec(conn, "COMMIT"));
        failures += !step(conn, "simple: SELECT 3", PQexec(conn, "SELECT 3"));
        printf("  RESULT: %d failures, final conn=%s\n", failures,
                   PQstatus(conn) == CONNECTION_OK ? "OK" : "BAD");
        PQfinish(conn);
        fclose(trace);
        return failures ? 1 : 0;
}
#!/usr/bin/env bash
# Throughput of three variants with pgbench -S -M prepared, interleaved and
# rotated per round so slow drift is spread evenly across them:
#   base   = master without the patch                          (BASE_PORT)
#   pplain = patched server, GUC at its default (plain)        (PATCHED_PORT)
#   prich  = patched server, ready_for_query_message=rich      (PATCHED_PORT, 
via PGOPTIONS)
# The client is always the PATCHED pgbench: an unpatched libpq cannot connect
# in rich mode (see nocfbot-rfq-old-libpq-client.c.txt).
#
#   PATCHED_BIN=/path/to/patched/bin bash rfq_pgbench.sh [rounds] [seconds] 
[clients]
#
# Both servers must already be running (see the README).  Optional: CPUS=4-11
# pins every pgbench run to those cores; OUT= sets the output directory.
set -euo pipefail
: "${PATCHED_BIN:?set PATCHED_BIN to the bin/ directory of the patched install}"
SOCK=${SOCK:-/tmp/rfq-sock}
BASE_PORT=${BASE_PORT:-54501}
PATCHED_PORT=${PATCHED_PORT:-54502}
ROUNDS=${1:-10}; SECS=${2:-10}; CLIENTS=${3:-8}
JOBS=$(( CLIENTS < 4 ? CLIENTS : 4 ))
OUT=${OUT:-./rfq-pgbench-out}
PGB=$PATCHED_BIN/pgbench
PIN=(); [ -n "${CPUS:-}" ] && PIN=(taskset -c "$CPUS")
mkdir -p "$OUT"; rm -f "$OUT"/{base,pplain,prich}.txt

for port in "$BASE_PORT" "$PATCHED_PORT"; do
  "$PGB" -h "$SOCK" -p "$port" -U postgres -i -s 10 -q postgres >/dev/null 2>&1
done

variant_port() { [ "$1" = base ] && echo "$BASE_PORT" || echo "$PATCHED_PORT"; }
variant_opts() { [ "$1" = prich ] && echo '-c ready_for_query_message=rich' || 
true; }

run_one() {  # <variant> <seconds> -> prints tps
  PGOPTIONS="$(variant_opts "$1")" "${PIN[@]}" "$PGB" -h "$SOCK" -p 
"$(variant_port "$1")" \
    -U postgres -n -S -M prepared -c "$CLIENTS" -j "$JOBS" -T "$2" postgres 
2>&1 |
    awk '/^tps/{print $3}'
}

# Sanity check: the effective setting seen by each variant.
for v in base pplain prich; do
  echo "$v: ready_for_query_message=$(PGOPTIONS="$(variant_opts "$v")" 
"$PATCHED_BIN/psql" -X -At \
    -h "$SOCK" -p "$(variant_port "$v")" -U postgres \
    -c "select coalesce(current_setting('ready_for_query_message', true), 
'(does not exist)')")"
done

for v in base pplain prich; do run_one "$v" 5 >/dev/null; done   # warm-up

order=(base pplain prich)
for ((r = 0; r < ROUNDS; r++)); do
  for ((k = 0; k < 3; k++)); do
    v=${order[$(( (r + k) % 3 ))]}
    run_one "$v" "$SECS" >> "$OUT/$v.txt"
  done
  echo "round $((r + 1))/$ROUNDS done"
done

python3 - "$OUT" "$CLIENTS" <<'PY'
import statistics as st
import sys

out, clients = sys.argv[1], int(sys.argv[2])
tps = {v: [float(x) for x in open(f"{out}/{v}.txt") if x.strip()]
       for v in ("base", "pplain", "prich")}
for v, xs in tps.items():
    med = st.median(xs)
    # closed loop: mean latency per transaction = clients / tps
    print(f"{v:7s} n={len(xs):2d}  median tps={med:10.1f}  latency={clients * 
1e6 / med:7.3f} us"
          f"  all={[round(x) for x in xs]}")
b, p, r = (st.median(tps[v]) for v in ("base", "pplain", "prich"))
print(f"pplain vs base:  {(p / b - 1) * 100:+.2f}% (median tps)")
print(f"prich vs pplain: {(r / p - 1) * 100:+.2f}% (median tps)")
PY
#!/usr/bin/env bash
# User-space CPU instructions that ONE backend executes per ReadyForQuery.
#
# For each variant and case, the backend is measured with `perf stat -e
# instructions:u -p <pid>` while it runs N1 and then N2 'SELECT 1' statements
# (each one ends with a 'Z').  (I(N2) - I(N1)) / (N2 - N1) removes the fixed
# cost (setup, markers, perf attach).  Variants are interleaved per repetition.
#
#   cases:  clean        = session with no state
#           cursors100   = 100 cursors WITHOUT HOLD open in the transaction
#           cursors1000  = 1000 cursors WITHOUT HOLD open in the transaction
#   variants: base (master, BASE_PORT), pplain and prich (patched, PATCHED_PORT)
#
#   PATCHED_BIN=/path/to/patched/bin bash rfq_instructions.sh [reps]
#
# Needs perf with kernel.perf_event_paranoid <= 2 (no root needed for :u).
# Both servers must already be running (see the README).
set -euo pipefail
: "${PATCHED_BIN:?set PATCHED_BIN to the bin/ directory of the patched install}"
SOCK=${SOCK:-/tmp/rfq-sock}
BASE_PORT=${BASE_PORT:-54501}
PATCHED_PORT=${PATCHED_PORT:-54502}
REPS=${1:-5}; N1=10000; N2=30000
OUT=${OUT:-./rfq-instructions-out}; mkdir -p "$OUT"; : > "$OUT/raw.tsv"

measure() {  # <port> <pgoptions> <setup sql> <n> -> instructions:u of the 
backend
  local port=$1 opts=$2 setup=$3 n=$4 dir pid psql_pid perf_pid
  dir=$(mktemp -d); mkfifo "$dir/in"
  PGOPTIONS="$opts" "$PATCHED_BIN/psql" -X -q -At -h "$SOCK" -p "$port" -U 
postgres -d postgres \
    < "$dir/in" > "$dir/out" 2>&1 &
  psql_pid=$!; exec 7>"$dir/in"
  printf '%s\nSELECT %s, pg_backend_pid();\n' "$setup" "'PID'" >&7
  for _ in $(seq 1 400); do
    pid=$(grep -m1 '^PID|' "$dir/out" | cut -d'|' -f2 || true); [ -n "$pid" ] 
&& break; sleep 0.05
  done
  if [ -z "$pid" ]; then
    echo "no backend pid: $(head -3 "$dir/out")" >&2
    exec 7>&-; kill "$psql_pid"; rm -rf "$dir"; echo; return 0
  fi
  perf stat -x, -e instructions:u -p "$pid" -o "$dir/perf" 2>/dev/null & 
perf_pid=$!
  sleep 0.3
  { for _ in $(seq 1 "$n"); do echo 'SELECT 1;'; done; echo "SELECT 'DONE';"; } 
>&7
  for _ in $(seq 1 6000); do grep -q '^DONE' "$dir/out" && break; sleep 0.02; 
done
  kill -INT "$perf_pid"; wait "$perf_pid" 2>/dev/null || true
  exec 7>&-; wait "$psql_pid" 2>/dev/null || true
  grep -m1 instructions "$dir/perf" | cut -d, -f1
  rm -rf "$dir"
}

cursors() {  # <count> -> BEGIN plus <count> DECLARE ... CURSOR (without HOLD)
  printf 'BEGIN; '
  for i in $(seq 1 "$1"); do printf 'DECLARE c%d CURSOR FOR SELECT 1; ' "$i"; 
done
}

declare -A PORT=([base]=$BASE_PORT [pplain]=$PATCHED_PORT [prich]=$PATCHED_PORT)
declare -A OPTS=([base]="" [pplain]="" [prich]="-c 
ready_for_query_message=rich")
declare -A SETUP=([clean]="" [cursors100]="$(cursors 100)" 
[cursors1000]="$(cursors 1000)")
for rep in $(seq 1 "$REPS"); do
  for case_ in clean cursors100 cursors1000; do
    for v in base pplain prich; do
      for n in $N1 $N2; do
        printf '%s\t%s\t%s\t%s\n' "$v" "$case_" "$n" \
          "$(measure "${PORT[$v]}" "${OPTS[$v]}" "${SETUP[$case_]}" "$n")" >> 
"$OUT/raw.tsv"
      done
    done
  done
  echo "repetition $rep/$REPS done" >&2
done

python3 - "$OUT/raw.tsv" "$N1" "$N2" <<'PY'
import collections
import statistics as st
import sys

raw, n1, n2 = sys.argv[1], int(sys.argv[2]), int(sys.argv[3])
d = collections.defaultdict(list)
for line in open(raw):
    v, case, n, x = line.rstrip("\n").split("\t")
    d[(v, case, int(n))].append(int(x) if x.strip().isdigit() else None)
for case in ("clean", "cursors100", "cursors1000"):
    print(f"== {case}: instructions:u per ReadyForQuery = (I({n2}) - I({n1})) / 
{n2 - n1}")
    per = {}
    for v in ("base", "pplain", "prich"):
        xs = [(y - x) / (n2 - n1) for x, y in zip(d[(v, case, n1)], d[(v, case, 
n2)]) if x and y]
        per[v] = xs
        print(f"  {v:7s} n={len(xs)} median={st.median(xs):9.1f}  
min={min(xs):9.1f}  max={max(xs):9.1f}")
    mb, mp, mr = (st.median(per[k]) for k in ("base", "pplain", "prich"))
    print(f"  pplain - base  = {mp - mb:+8.1f} instr/RFQ ({(mp / mb - 1) * 
100:+.2f}%)")
    print(f"  prich - pplain = {mr - mp:+8.1f} instr/RFQ ({(mr / mp - 1) * 
100:+.2f}%)")
PY

Reply via email to