From cf6718867bc1af11bfdfaa78c9ffbfe44e0aea66 Mon Sep 17 00:00:00 2001 From: tamnd <1218621+tamnd@users.noreply.github.com> Date: Wed, 7 Oct 2026 13:53:58 +0700 Subject: [PATCH 1/6] Add the pgbench driver and its consistency check --- src/main.rs | 111 ++++++++++++++ src/pg.rs | 404 +++++++++++++++++++++++++++++++++++++++++++++++++ src/pgbench.rs | 178 ++++++++++++++++++++++ 3 files changed, 693 insertions(+) create mode 100644 src/pg.rs create mode 100644 src/pgbench.rs diff --git a/src/main.rs b/src/main.rs index fa35f69..1317a39 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,8 @@ mod cgroup; mod gates; mod json; mod load; +mod pg; +mod pgbench; mod pins; mod report; mod toml; @@ -44,6 +46,11 @@ commands: compare two answer sets as multisets of rows (spec/20 section 20.5). PATH is a file or a directory of .tsv files. An expected file .out is a TPC-H answer file and is compared with the rules of TPC-H clause 2.1.3.5. + pgbench (--unit UNIT | --attach PATH) [--conn WORDS] [--bin DIR] [--scale N] [--clients N] [--jobs N] [--time SECONDS] [--smoke] [--json] + run pgbench -i and the TPC-B like script against the server, measure the server cgroup + with the idle base, and run the consistency check of spec/21 section 21.4.6. + --conn takes libpq words: host, port, user, dbname, password. + --smoke marks the result as a smoke run, which is not a baseline. --version print the version"; fn main() -> ExitCode { @@ -87,6 +94,7 @@ fn run(argv: &[String]) -> Result<(), String> { "load" => load_command(a)?, "report" => report_command(a)?, "answers" => answers_command(a)?, + "pgbench" => pgbench_command(a)?, "--version" | "-V" => println!("rupg-bench {}", env!("CARGO_PKG_VERSION")), "--help" | "-h" | "help" => println!("{USAGE}"), other => return Err(format!("usage: unknown command {other:?}\n{USAGE}")), @@ -331,3 +339,106 @@ fn answers_command(mut a: args::Args) -> Result<(), String> { } if wrong > 0 { Err(format!("{wrong} of {} answers are wrong", pairs.len())) } else { Ok(()) } } + +/// A number option with a default. +fn number(a: &mut args::Args, name: &str, default: T) -> Result { + match a.value(name) { + None => Ok(default), + Some(v) => v.parse().map_err(|_| format!("usage: --{name} {v:?} is not a number")), + } +} + +/// The `pgbench` command: the pgbench run and its consistency check of spec/21 section 21.4.6. +fn pgbench_command(mut a: args::Args) -> Result<(), String> { + let conn = pg::Config::parse(&a.value("conn").unwrap_or_default())?; + let bin = a.value("bin").unwrap_or_else(|| "/usr/lib/postgresql/19/bin".to_owned()); + let scale: u32 = number(&mut a, "scale", 1)?; + let clients: u32 = number(&mut a, "clients", 4)?; + let jobs: u32 = number(&mut a, "jobs", clients.min(4))?; + let time: u32 = number(&mut a, "time", 60)?; + let smoke = a.flag("smoke"); + let json = a.flag("json"); + if a.value_is_set("name") { + return Err( + "usage: pgbench measures a running server, so give --unit or --attach".to_owned() + ); + } + let cg = target(&mut a)?; + a.finish()?; + let bin = Path::new(&bin); + with_cgroup(cg, json, |cg, out| { + out.add("suite", "pgbench", "suite pgbench\n"); + let note = if smoke { + "smoke run: it shows that the driver works, it is not a baseline\n" + } else { + "" + }; + out.add("smoke", smoke, note); + let version = pgbench::output({ + let mut c = std::process::Command::new(bin.join("pgbench")); + c.arg("--version"); + c + })?; + out.add( + "pgbench_version", + version.trim(), + &format!("pgbench {}\n", version.trim()), + ); + let settings = Json::obj() + .with("scale", scale) + .with("clients", clients) + .with("jobs", jobs) + .with("time_s", time); + out.add( + "settings", + settings, + &format!( + "settings scale {scale}, {clients} clients, {jobs} threads, {time} s\n" + ), + ); + + let mut init = pgbench::command(bin, &conn); + init.args(["-i", "-q", "-s", &scale.to_string()]); + pgbench::output(init)?; + let mut db = pg::Conn::connect(&conn)?; + let server = db.server_version.clone(); + out.add("server_version", server.as_str(), &format!("server {server}\n")); + + let base = cgroup::idle_base(cg, cgroup::IDLE_BASE)?; + out.add("idle_base", base.to_json(), &format!("idle base over 10 s:\n{}", base.text())); + + let mut run = pgbench::command(bin, &conn); + run.args(["-c", &clients.to_string(), "-j", &jobs.to_string(), "-T", &time.to_string()]); + let interval = cgroup::Interval::start(cg)?; + let printed = pgbench::output(run); + let usage = interval.finish()?; + let printed = printed?; + let summary = pgbench::parse_summary(&printed)?; + let sj = Json::obj() + .with("transactions", summary.transactions) + .with("failed", summary.failed) + .with("latency_average_ms", summary.latency_ms) + .with("tps", summary.tps) + .with("output", printed.as_str()); + out.add("pgbench", sj, &format!("pgbench output:\n{printed}")); + out.add("run", usage.to_json(), &format!("server over the run:\n{}", usage.text())); + + let check = pgbench::Check::read(&mut db, summary.transactions)?; + let failures = check.failures(); + let text = format!( + "check sum(abalance) {}, sum(tbalance) {}, sum(bbalance) {}, history rows {}, transactions {}: {}\n", + check.abalance, + check.tbalance, + check.bbalance, + check.history, + check.transactions, + if failures.is_empty() { "passed" } else { "FAILED" } + ); + out.add("check", check.to_json(), &text); + if failures.is_empty() { + Ok(()) + } else { + Err(format!("wrong answer, the run is not a number: {}", failures.join("; "))) + } + }) +} diff --git a/src/pg.rs b/src/pg.rs new file mode 100644 index 0000000..b84a80a --- /dev/null +++ b/src/pg.rs @@ -0,0 +1,404 @@ +//! A small client for the PostgreSQL wire protocol, version 3.0. +//! +//! The drivers read their checks through this client, and the drivers for TPC-C with statements and YCSB will send their statements through it. spec/20 section 20.9 names `libpq`. The harness has no dependencies and no unsafe code, so this module speaks the protocol itself. This first part has the start of a connection and the simple query protocol. +//! +//! It supports the `trust` and `password` methods of `pg_hba.conf`. The machine scripts set `trust` for the benchmark user on the local host. Values are sent and received as text. + +use std::fmt; +use std::io::{BufReader, BufWriter, Read, Write}; +use std::net::TcpStream; +use std::os::unix::net::UnixStream; + +/// Where the server is. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct Config { + /// A host name, or a directory that holds the Unix socket, such as `/var/run/postgresql`. + pub(crate) host: String, + pub(crate) port: u16, + pub(crate) user: String, + pub(crate) dbname: String, + pub(crate) password: Option, +} + +impl Config { + /// Parses `key=value` words, as `libpq` does: `host`, `port`, `user`, `dbname` and `password`. + pub(crate) fn parse(s: &str) -> Result { + let mut c = Config { + host: "/var/run/postgresql".to_owned(), + port: 5432, + user: "postgres".to_owned(), + dbname: String::new(), + password: None, + }; + for word in s.split_whitespace() { + let (k, v) = word + .split_once('=') + .ok_or(format!("the connection word {word:?} is not key=value"))?; + match k { + "host" => c.host = v.to_owned(), + "port" => { + c.port = v.parse().map_err(|_| format!("the port {v:?} is not a number"))? + } + "user" => c.user = v.to_owned(), + "dbname" => c.dbname = v.to_owned(), + "password" => c.password = Some(v.to_owned()), + _ => return Err(format!("the connection key {k:?} is not supported")), + } + } + if c.dbname.is_empty() { + c.dbname = c.user.clone(); + } + Ok(c) + } +} + +/// An error from the server, or a failure of the connection. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct PgError { + /// The SQLSTATE, empty when the error is not from the server. + pub(crate) code: String, + pub(crate) message: String, +} + +impl fmt::Display for PgError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + if self.code.is_empty() { + f.write_str(&self.message) + } else { + write!(f, "{}: {}", self.code, self.message) + } + } +} + +impl From for PgError { + fn from(e: std::io::Error) -> PgError { + PgError { code: String::new(), message: format!("connection: {e}") } + } +} + +impl From for String { + fn from(e: PgError) -> String { + e.to_string() + } +} + +fn client_error(message: impl Into) -> PgError { + PgError { code: String::new(), message: message.into() } +} + +/// The result of one statement. +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub(crate) struct Rows { + pub(crate) columns: Vec, + pub(crate) rows: Vec>>, + /// The command tag, for example `INSERT 0 1` or `SELECT 3`. + pub(crate) tag: String, +} + +/// The two kinds of socket. +#[derive(Debug)] +enum Socket { + Tcp(TcpStream), + Unix(UnixStream), +} + +impl Read for Socket { + fn read(&mut self, buf: &mut [u8]) -> std::io::Result { + match self { + Socket::Tcp(s) => s.read(buf), + Socket::Unix(s) => s.read(buf), + } + } +} + +impl Write for Socket { + fn write(&mut self, buf: &[u8]) -> std::io::Result { + match self { + Socket::Tcp(s) => s.write(buf), + Socket::Unix(s) => s.write(buf), + } + } + + fn flush(&mut self) -> std::io::Result<()> { + match self { + Socket::Tcp(s) => s.flush(), + Socket::Unix(s) => s.flush(), + } + } +} + +/// One connection. +#[derive(Debug)] +pub(crate) struct Conn { + reader: BufReader, + writer: BufWriter, + /// The `server_version` that the server reported at the start. + pub(crate) server_version: String, +} + +impl Conn { + pub(crate) fn connect(c: &Config) -> Result { + let (r, w) = if c.host.starts_with('/') { + let s = UnixStream::connect(format!("{}/.s.PGSQL.{}", c.host, c.port))?; + (Socket::Unix(s.try_clone()?), Socket::Unix(s)) + } else { + let s = TcpStream::connect((c.host.as_str(), c.port))?; + s.set_nodelay(true)?; + (Socket::Tcp(s.try_clone()?), Socket::Tcp(s)) + }; + let mut conn = Conn { + reader: BufReader::with_capacity(1 << 16, r), + writer: BufWriter::with_capacity(1 << 16, w), + server_version: String::new(), + }; + let mut body = Vec::new(); + body.extend_from_slice(&196608u32.to_be_bytes()); + for (k, v) in [ + ("user", c.user.as_str()), + ("database", c.dbname.as_str()), + ("application_name", "rupg-bench"), + ] { + cstr(&mut body, k); + cstr(&mut body, v); + } + body.push(0); + let len = u32::try_from(body.len() + 4) + .map_err(|_| client_error("the startup message is too long"))?; + conn.writer.write_all(&len.to_be_bytes())?; + conn.writer.write_all(&body)?; + conn.writer.flush()?; + loop { + let (tag, msg) = conn.read_message()?; + match tag { + b'R' => { + let kind = be32(&msg, 0)?; + match kind { + 0 => {} + 3 => { + let pw = c.password.as_deref().ok_or(client_error( + "the server asks for a password and none was given", + ))?; + let mut m = Vec::new(); + cstr(&mut m, pw); + conn.send(b'p', &m)?; + conn.writer.flush()?; + } + _ => { + return Err(client_error(format!( + "the server asks for authentication method {kind}. This client supports trust and password. Set trust for the benchmark user in pg_hba.conf" + ))); + } + } + } + b'S' => { + let mut parts = msg.split(|&b| b == 0); + if parts.next() == Some(b"server_version") { + conn.server_version = + String::from_utf8_lossy(parts.next().unwrap_or_default()).into_owned(); + } + } + b'K' | b'N' => {} + b'E' => return Err(parse_error(&msg)), + b'Z' => return Ok(conn), + other => { + return Err(client_error(format!( + "unexpected message {:?} at the start", + other as char + ))); + } + } + } + } + + /// Runs one or more statements with the simple query protocol and returns the result of each. An error stops the rest, as in `psql -c`. + pub(crate) fn simple(&mut self, sql: &str) -> Result, PgError> { + let mut m = Vec::new(); + cstr(&mut m, sql); + self.send(b'Q', &m)?; + self.writer.flush()?; + let mut out = Vec::new(); + let mut cur = Rows::default(); + let mut err = None; + loop { + let (tag, msg) = self.read_message()?; + match tag { + b'T' => cur.columns = parse_row_description(&msg)?, + b'D' => cur.rows.push(parse_data_row(&msg)?), + b'C' => { + cur.tag = cstring_at(&msg, 0)?.0; + out.push(std::mem::take(&mut cur)); + } + b'I' => out.push(std::mem::take(&mut cur)), + b'E' => err = Some(parse_error(&msg)), + b'Z' => return err.map_or(Ok(out), Err), + b'N' | b'S' | b'A' => {} + b'G' | b'H' => return Err(client_error("COPY is not supported by this client")), + other => { + return Err(client_error(format!("unexpected message {:?}", other as char))); + } + } + } + } + + /// Runs one statement with the simple protocol and returns its result. + pub(crate) fn query(&mut self, sql: &str) -> Result { + self.simple(sql)?.pop().ok_or(client_error("the statement returned nothing")) + } + + fn send(&mut self, tag: u8, body: &[u8]) -> Result<(), PgError> { + let len = + u32::try_from(body.len() + 4).map_err(|_| client_error("a message is too long"))?; + self.writer.write_all(&[tag])?; + self.writer.write_all(&len.to_be_bytes())?; + self.writer.write_all(body)?; + Ok(()) + } + + fn read_message(&mut self) -> Result<(u8, Vec), PgError> { + let mut head = [0u8; 5]; + self.reader.read_exact(&mut head)?; + let len = u32::from_be_bytes([head[1], head[2], head[3], head[4]]) as usize; + if len < 4 { + return Err(client_error("a message with a length below 4")); + } + let mut body = vec![0u8; len - 4]; + self.reader.read_exact(&mut body)?; + Ok((head[0], body)) + } +} + +impl Drop for Conn { + fn drop(&mut self) { + // Terminate. A failure here does not matter, the socket closes anyway. + let _ = self.send(b'X', &[]); + let _ = self.writer.flush(); + } +} + +fn cstr(out: &mut Vec, s: &str) { + out.extend_from_slice(s.as_bytes()); + out.push(0); +} + +fn be32(b: &[u8], at: usize) -> Result { + b.get(at..at + 4) + .map(|s| u32::from_be_bytes([s[0], s[1], s[2], s[3]])) + .ok_or(client_error("a short message")) +} + +fn be16(b: &[u8], at: usize) -> Result { + b.get(at..at + 2) + .map(|s| u16::from_be_bytes([s[0], s[1]])) + .ok_or(client_error("a short message")) +} + +/// The string at `at` and the offset after its terminating zero. +fn cstring_at(b: &[u8], at: usize) -> Result<(String, usize), PgError> { + let rest = b.get(at..).ok_or(client_error("a short message"))?; + let end = rest.iter().position(|&c| c == 0).ok_or(client_error("a string with no end"))?; + Ok((String::from_utf8_lossy(&rest[..end]).into_owned(), at + end + 1)) +} + +fn parse_row_description(b: &[u8]) -> Result, PgError> { + let n = be16(b, 0)?; + let mut at = 2; + let mut out = Vec::with_capacity(n.into()); + for _ in 0..n { + let (name, next) = cstring_at(b, at)?; + out.push(name); + // Table oid, column number, type oid, type size, type modifier, format code. + at = next + 18; + } + Ok(out) +} + +fn parse_data_row(b: &[u8]) -> Result>, PgError> { + let n = be16(b, 0)?; + let mut at = 2; + let mut out = Vec::with_capacity(n.into()); + for _ in 0..n { + let len = be32(b, at)? as i32; + at += 4; + if len < 0 { + out.push(None); + } else { + let end = at + len as usize; + let v = b.get(at..end).ok_or(client_error("a short data row"))?; + out.push(Some(String::from_utf8_lossy(v).into_owned())); + at = end; + } + } + Ok(out) +} + +fn parse_error(b: &[u8]) -> PgError { + let mut code = String::new(); + let mut message = String::new(); + let mut at = 0; + while at < b.len() && b[at] != 0 { + let field = b[at]; + let Ok((value, next)) = cstring_at(b, at + 1) else { break }; + match field { + b'C' => code = value, + b'M' => message = value, + _ => {} + } + at = next; + } + PgError { code, message } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn config_words() { + let c = Config::parse("host=127.0.0.1 port=5433 user=bench").unwrap(); + assert_eq!( + (c.host.as_str(), c.port, c.user.as_str(), c.dbname.as_str()), + ("127.0.0.1", 5433, "bench", "bench") + ); + let d = Config::parse("").unwrap(); + assert_eq!( + (d.host.as_str(), d.port, d.dbname.as_str()), + ("/var/run/postgresql", 5432, "postgres") + ); + assert!(Config::parse("sslmode=require").is_err()); + assert!(Config::parse("port=x").is_err()); + } + + #[test] + fn data_row_with_a_null() { + let mut b = Vec::new(); + b.extend_from_slice(&2u16.to_be_bytes()); + b.extend_from_slice(&3i32.to_be_bytes()); + b.extend_from_slice(b"abc"); + b.extend_from_slice(&(-1i32).to_be_bytes()); + assert_eq!(parse_data_row(&b).unwrap(), vec![Some("abc".to_owned()), None]); + assert!(parse_data_row(&b[..6]).is_err()); + } + + #[test] + fn row_description_names() { + let mut b = Vec::new(); + b.extend_from_slice(&2u16.to_be_bytes()); + for name in ["aid", "abalance"] { + cstr(&mut b, name); + b.extend_from_slice(&[0; 18]); + } + assert_eq!(parse_row_description(&b).unwrap(), vec!["aid", "abalance"]); + } + + #[test] + fn error_fields() { + let mut b = Vec::new(); + for (f, v) in [(b'S', "ERROR"), (b'C', "40001"), (b'M', "could not serialize access")] { + b.push(f); + cstr(&mut b, v); + } + b.push(0); + let e = parse_error(&b); + assert_eq!(e.to_string(), "40001: could not serialize access"); + } +} diff --git a/src/pgbench.rs b/src/pgbench.rs new file mode 100644 index 0000000..5d0ba97 --- /dev/null +++ b/src/pgbench.rs @@ -0,0 +1,178 @@ +//! The pgbench driver and its consistency check (spec/21 section 21.4.6). +//! +//! The driver runs the `pgbench` program of the PostgreSQL build: `pgbench -i` at the scale, then the built-in TPC-B like script for a fixed time. The server is measured in its cgroup over the run. After the run the driver checks that the sum of `abalance` equals the sum of `tbalance` and the sum of `bbalance`, and that the row count of `pgbench_history` equals the transaction count that pgbench reported. A run that fails the check is a wrong answer and not a number. + +use std::path::Path; +use std::process::Command; + +use crate::json::Json; +use crate::pg::{Config, Conn}; + +/// What pgbench printed at the end of a run. +#[derive(Clone, Debug, Default, PartialEq)] +pub(crate) struct Summary { + pub(crate) transactions: u64, + pub(crate) failed: u64, + pub(crate) latency_ms: Option, + pub(crate) tps: Option, +} + +/// Parses the summary lines of pgbench 15 and later. +pub(crate) fn parse_summary(text: &str) -> Result { + let mut s = Summary::default(); + let mut found = false; + for line in text.lines() { + let line = line.trim(); + if let Some(rest) = line.strip_prefix("number of transactions actually processed:") { + // With -t the line is "N/M". With -T it is "N". + let n = rest.trim().split('/').next().unwrap_or(""); + s.transactions = n.trim().parse().map_err(|_| format!("pgbench printed {line:?}"))?; + found = true; + } else if let Some(rest) = line.strip_prefix("number of failed transactions:") { + s.failed = rest.split_whitespace().next().and_then(|n| n.parse().ok()).unwrap_or(0); + } else if let Some(rest) = line.strip_prefix("latency average =") { + s.latency_ms = rest.trim().trim_end_matches("ms").trim().parse().ok(); + } else if let Some(rest) = line.strip_prefix("tps =") { + s.tps = rest.split_whitespace().next().and_then(|n| n.parse().ok()); + } + } + if found { Ok(s) } else { Err("pgbench printed no transaction count".to_owned()) } +} + +/// The numbers of the consistency check. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct Check { + pub(crate) abalance: i64, + pub(crate) tbalance: i64, + pub(crate) bbalance: i64, + pub(crate) history: u64, + pub(crate) transactions: u64, +} + +impl Check { + pub(crate) fn read(conn: &mut Conn, transactions: u64) -> Result { + let r = conn.query( + "SELECT (SELECT coalesce(sum(abalance), 0) FROM pgbench_accounts), \ + (SELECT coalesce(sum(tbalance), 0) FROM pgbench_tellers), \ + (SELECT coalesce(sum(bbalance), 0) FROM pgbench_branches), \ + (SELECT count(*) FROM pgbench_history)", + )?; + let row = r.rows.first().ok_or("the check returned no row")?; + let num = |i: usize| -> Result { + row.get(i) + .and_then(|v| v.as_deref()) + .and_then(|v| v.parse().ok()) + .ok_or(format!("the check returned {row:?}")) + }; + Ok(Check { + abalance: num(0)?, + tbalance: num(1)?, + bbalance: num(2)?, + history: u64::try_from(num(3)?).map_err(|_| "a negative count")?, + transactions, + }) + } + + /// The failures of the check, empty when it passes. + pub(crate) fn failures(&self) -> Vec { + let mut out = Vec::new(); + if self.abalance != self.tbalance || self.abalance != self.bbalance { + out.push(format!( + "sum(abalance) = {}, sum(tbalance) = {}, sum(bbalance) = {}: the sums differ", + self.abalance, self.tbalance, self.bbalance + )); + } + if self.history != self.transactions { + out.push(format!( + "pgbench_history has {} rows, pgbench reported {} transactions", + self.history, self.transactions + )); + } + out + } + + pub(crate) fn to_json(&self) -> Json { + let failures = self.failures(); + Json::obj() + .with("sum_abalance", self.abalance) + .with("sum_tbalance", self.tbalance) + .with("sum_bbalance", self.bbalance) + .with("history_rows", self.history) + .with("transactions", self.transactions) + .with("passed", failures.is_empty()) + .with("failures", failures) + } +} + +/// A `pgbench` command with the connection in the environment, as libpq reads it. +pub(crate) fn command(bin: &Path, c: &Config) -> Command { + let mut cmd = Command::new(bin.join("pgbench")); + cmd.env("PGHOST", &c.host) + .env("PGPORT", c.port.to_string()) + .env("PGUSER", &c.user) + .env("PGDATABASE", &c.dbname); + if let Some(p) = &c.password { + cmd.env("PGPASSWORD", p); + } + cmd +} + +/// Runs a command and returns its standard output, or the error with its standard error. +pub(crate) fn output(mut cmd: Command) -> Result { + let out = cmd.output().map_err(|e| format!("{:?}: {e}", cmd.get_program()))?; + if !out.status.success() { + return Err(format!( + "{:?} failed: {}\n{}", + cmd.get_program(), + out.status, + String::from_utf8_lossy(&out.stderr).trim() + )); + } + Ok(String::from_utf8_lossy(&out.stdout).into_owned()) +} + +#[cfg(test)] +mod tests { + use super::*; + + const OUTPUT: &str = "pgbench (19beta4) +transaction type: +scaling factor: 1 +query mode: simple +number of clients: 4 +number of threads: 2 +maximum number of tries: 1 +duration: 30 s +number of transactions actually processed: 12345 +number of failed transactions: 0 (0.000%) +latency average = 9.721 ms +initial connection time = 6.130 ms +tps = 411.497617 (without initial connection time) +"; + + #[test] + fn summary() { + let s = parse_summary(OUTPUT).unwrap(); + assert_eq!(s.transactions, 12345); + assert_eq!(s.failed, 0); + assert_eq!(s.latency_ms, Some(9.721)); + assert_eq!(s.tps, Some(411.497617)); + let t = parse_summary("number of transactions actually processed: 100/100\n").unwrap(); + assert_eq!(t.transactions, 100); + assert!(parse_summary("tps = 1\n").is_err()); + } + + #[test] + fn the_check() { + let mut c = + Check { abalance: -5, tbalance: -5, bbalance: -5, history: 10, transactions: 10 }; + assert!(c.failures().is_empty()); + c.bbalance = 7; + c.history = 9; + let f = c.failures(); + assert_eq!(f.len(), 2); + assert!(f[0].contains("the sums differ")); + assert!(f[1].contains("9 rows")); + assert!(c.to_json().pretty().contains("\"passed\": false")); + } +} From 4a628bc26239fd0997ca130c19d2d56767ed00b5 Mon Sep 17 00:00:00 2001 From: tamnd <1218621+tamnd@users.noreply.github.com> Date: Wed, 7 Oct 2026 15:56:24 +0700 Subject: [PATCH 2/6] Write the report of a pgbench run --- machines/install/postgresql.sh | 14 ++- src/main.rs | 163 ++++++++++++++++++++------------- 2 files changed, 110 insertions(+), 67 deletions(-) diff --git a/machines/install/postgresql.sh b/machines/install/postgresql.sh index b8994f9..99ea7f6 100755 --- a/machines/install/postgresql.sh +++ b/machines/install/postgresql.sh @@ -34,6 +34,18 @@ fi if ! pg_lsclusters -h | awk '{print $1"/"$2}' | grep -qx "$major/main"; then pg_createcluster "$major" main >/dev/null fi +# The harness connects as the role bench to the database bench over the Unix socket, with no password. +# The rule is for the local socket only, and it is the first rule, so peer does not reject the role. +hba=/etc/postgresql/$major/main/pg_hba.conf +if ! grep -qE '^local[[:space:]]+all[[:space:]]+bench[[:space:]]+trust' "$hba"; then + sed -i '1i local all bench trust' "$hba" +fi systemctl daemon-reload systemctl restart "postgresql@$major-main" -sudo -u postgres psql -Atc 'SELECT version()' +if [ "$(sudo -u postgres psql -Atc "SELECT count(*) FROM pg_roles WHERE rolname = 'bench'")" = 0 ]; then + sudo -u postgres psql -qc 'CREATE ROLE bench LOGIN; GRANT pg_read_server_files TO bench' +fi +if [ "$(sudo -u postgres psql -Atc "SELECT count(*) FROM pg_database WHERE datname = 'bench'")" = 0 ]; then + sudo -u postgres createdb -O bench bench +fi +psql -h /var/run/postgresql -U bench -d bench -Atc 'SELECT version()' diff --git a/src/main.rs b/src/main.rs index 1317a39..7f8eae7 100644 --- a/src/main.rs +++ b/src/main.rs @@ -46,10 +46,12 @@ commands: compare two answer sets as multisets of rows (spec/20 section 20.5). PATH is a file or a directory of .tsv files. An expected file .out is a TPC-H answer file and is compared with the rules of TPC-H clause 2.1.3.5. - pgbench (--unit UNIT | --attach PATH) [--conn WORDS] [--bin DIR] [--scale N] [--clients N] [--jobs N] [--time SECONDS] [--smoke] [--json] + pgbench (--unit UNIT | --attach PATH) [--conn WORDS] [--bin DIR] [--scale N] [--clients N] [--jobs N] [--time SECONDS] + [--smoke] [--json] [--report DIR [--machine M]] run pgbench -i and the TPC-B like script against the server, measure the server cgroup with the idle base, and run the consistency check of spec/21 section 21.4.6. - --conn takes libpq words: host, port, user, dbname, password. + --conn takes libpq words: host, port, user, dbname, password (user=bench dbname=bench by default). + --report writes the report files under DIR (see report). --smoke marks the result as a smoke run, which is not a baseline. --version print the version"; @@ -350,7 +352,10 @@ fn number(a: &mut args::Args, name: &str, default: T) -> R /// The `pgbench` command: the pgbench run and its consistency check of spec/21 section 21.4.6. fn pgbench_command(mut a: args::Args) -> Result<(), String> { - let conn = pg::Config::parse(&a.value("conn").unwrap_or_default())?; + // The role and the database that machines/install/postgresql.sh makes. + let conn = pg::Config::parse( + &a.value("conn").unwrap_or_else(|| "user=bench dbname=bench".to_owned()), + )?; let bin = a.value("bin").unwrap_or_else(|| "/usr/lib/postgresql/19/bin".to_owned()); let scale: u32 = number(&mut a, "scale", 1)?; let clients: u32 = number(&mut a, "clients", 4)?; @@ -358,6 +363,8 @@ fn pgbench_command(mut a: args::Args) -> Result<(), String> { let time: u32 = number(&mut a, "time", 60)?; let smoke = a.flag("smoke"); let json = a.flag("json"); + let report_dir = a.value("report"); + let machine = a.value("machine"); if a.value_is_set("name") { return Err( "usage: pgbench measures a running server, so give --unit or --attach".to_owned() @@ -367,78 +374,102 @@ fn pgbench_command(mut a: args::Args) -> Result<(), String> { a.finish()?; let bin = Path::new(&bin); with_cgroup(cg, json, |cg, out| { + let meta = report::Meta::now("pgbench", machine, None, smoke); out.add("suite", "pgbench", "suite pgbench\n"); + out.add("machine", meta.machine.as_str(), &format!("machine {}\n", meta.machine)); + out.add("date", meta.date.as_str(), &format!("date {}\n", meta.date)); + out.add("commit", meta.commit.as_str(), &format!("harness commit {}\n", meta.commit)); let note = if smoke { "smoke run: it shows that the driver works, it is not a baseline\n" } else { "" }; out.add("smoke", smoke, note); - let version = pgbench::output({ - let mut c = std::process::Command::new(bin.join("pgbench")); - c.arg("--version"); - c - })?; - out.add( - "pgbench_version", - version.trim(), - &format!("pgbench {}\n", version.trim()), - ); - let settings = Json::obj() - .with("scale", scale) - .with("clients", clients) - .with("jobs", jobs) - .with("time_s", time); - out.add( - "settings", - settings, - &format!( - "settings scale {scale}, {clients} clients, {jobs} threads, {time} s\n" - ), - ); + // machines/install/postgresql.sh writes the commit of the build next to bin. + if let Ok(c) = std::fs::read_to_string(bin.join("../COMMIT")) { + out.add("postgresql_commit", c.trim(), &format!("postgresql {}\n", c.trim())); + } + let result = pgbench_run(cg, out, bin, &conn, [scale, clients, jobs, time]); + if let Err(e) = &result { + out.add("error", e.as_str(), ""); + } + if let Some(dir) = &report_dir { + // A failed run gets a report too, with its error. + let (md, json) = report::write(Path::new(dir), &out.json)?; + let text = + format!("report {}\n {}\n", md.display(), json.display()); + out.add("report", md.display().to_string(), &text); + } + result + }) +} - let mut init = pgbench::command(bin, &conn); - init.args(["-i", "-q", "-s", &scale.to_string()]); - pgbench::output(init)?; - let mut db = pg::Conn::connect(&conn)?; - let server = db.server_version.clone(); - out.add("server_version", server.as_str(), &format!("server {server}\n")); +/// The steps of the `pgbench` command after the header of the result. +fn pgbench_run( + cg: &Cgroup, + out: &mut Out, + bin: &Path, + conn: &pg::Config, + [scale, clients, jobs, time]: [u32; 4], +) -> Result<(), String> { + let version = pgbench::output({ + let mut c = std::process::Command::new(bin.join("pgbench")); + c.arg("--version"); + c + })?; + out.add("pgbench_version", version.trim(), &format!("pgbench {}\n", version.trim())); + let settings = Json::obj() + .with("scale", scale) + .with("clients", clients) + .with("jobs", jobs) + .with("time_s", time); + out.add( + "settings", + settings, + &format!("settings scale {scale}, {clients} clients, {jobs} threads, {time} s\n"), + ); - let base = cgroup::idle_base(cg, cgroup::IDLE_BASE)?; - out.add("idle_base", base.to_json(), &format!("idle base over 10 s:\n{}", base.text())); + let mut init = pgbench::command(bin, conn); + init.args(["-i", "-q", "-s", &scale.to_string()]); + pgbench::output(init)?; + let mut db = pg::Conn::connect(conn)?; + let server = db.server_version.clone(); + out.add("server_version", server.as_str(), &format!("server {server}\n")); - let mut run = pgbench::command(bin, &conn); - run.args(["-c", &clients.to_string(), "-j", &jobs.to_string(), "-T", &time.to_string()]); - let interval = cgroup::Interval::start(cg)?; - let printed = pgbench::output(run); - let usage = interval.finish()?; - let printed = printed?; - let summary = pgbench::parse_summary(&printed)?; - let sj = Json::obj() - .with("transactions", summary.transactions) - .with("failed", summary.failed) - .with("latency_average_ms", summary.latency_ms) - .with("tps", summary.tps) - .with("output", printed.as_str()); - out.add("pgbench", sj, &format!("pgbench output:\n{printed}")); - out.add("run", usage.to_json(), &format!("server over the run:\n{}", usage.text())); + let base = cgroup::idle_base(cg, cgroup::IDLE_BASE)?; + out.add("idle_base", base.to_json(), &format!("idle base over 10 s:\n{}", base.text())); - let check = pgbench::Check::read(&mut db, summary.transactions)?; - let failures = check.failures(); - let text = format!( - "check sum(abalance) {}, sum(tbalance) {}, sum(bbalance) {}, history rows {}, transactions {}: {}\n", - check.abalance, - check.tbalance, - check.bbalance, - check.history, - check.transactions, - if failures.is_empty() { "passed" } else { "FAILED" } - ); - out.add("check", check.to_json(), &text); - if failures.is_empty() { - Ok(()) - } else { - Err(format!("wrong answer, the run is not a number: {}", failures.join("; "))) - } - }) + let mut run = pgbench::command(bin, conn); + run.args(["-c", &clients.to_string(), "-j", &jobs.to_string(), "-T", &time.to_string()]); + let interval = cgroup::Interval::start(cg)?; + let printed = pgbench::output(run); + let usage = interval.finish()?; + let printed = printed?; + let summary = pgbench::parse_summary(&printed)?; + let sj = Json::obj() + .with("transactions", summary.transactions) + .with("failed", summary.failed) + .with("latency_average_ms", summary.latency_ms) + .with("tps", summary.tps) + .with("output", printed.as_str()); + out.add("pgbench", sj, &format!("pgbench output:\n{printed}")); + out.add("run", usage.to_json(), &format!("server over the run:\n{}", usage.text())); + + let check = pgbench::Check::read(&mut db, summary.transactions)?; + let failures = check.failures(); + let text = format!( + "check sum(abalance) {}, sum(tbalance) {}, sum(bbalance) {}, history rows {}, transactions {}: {}\n", + check.abalance, + check.tbalance, + check.bbalance, + check.history, + check.transactions, + if failures.is_empty() { "passed" } else { "FAILED" } + ); + out.add("check", check.to_json(), &text); + if failures.is_empty() { + Ok(()) + } else { + Err(format!("wrong answer, the run is not a number: {}", failures.join("; "))) + } } From 4e6bede98b062db2b749e8853d0582ca4781e109 Mon Sep 17 00:00:00 2001 From: tamnd <1218621+tamnd@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:00:01 +0700 Subject: [PATCH 3/6] Report io bytes as not measured when io.stat is missing --- machines/install/postgresql.sh | 4 ++++ src/cgroup.rs | 36 ++++++++++++++++++++++------------ 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/machines/install/postgresql.sh b/machines/install/postgresql.sh index 99ea7f6..86a9e80 100755 --- a/machines/install/postgresql.sh +++ b/machines/install/postgresql.sh @@ -40,6 +40,10 @@ hba=/etc/postgresql/$major/main/pg_hba.conf if ! grep -qE '^local[[:space:]]+all[[:space:]]+bench[[:space:]]+trust' "$hba"; then sed -i '1i local all bench trust' "$hba" fi +# spec/20 section 20.4 reads io.stat in the cgroup of the server. systemd turns on the io controller for a unit only with IOAccounting=yes. +dropin=/etc/systemd/system/postgresql@$major-main.service.d +mkdir -p "$dropin" +printf '[Service]\nCPUAccounting=yes\nMemoryAccounting=yes\nIOAccounting=yes\n' >"$dropin/rupg-bench.conf" systemctl daemon-reload systemctl restart "postgresql@$major-main" if [ "$(sudo -u postgres psql -Atc "SELECT count(*) FROM pg_roles WHERE rolname = 'bench'")" = 0 ]; then diff --git a/src/cgroup.rs b/src/cgroup.rs index 29dc341..fad1ef7 100644 --- a/src/cgroup.rs +++ b/src/cgroup.rs @@ -133,10 +133,13 @@ impl Cgroup { pub(crate) fn counters(&self) -> Result { let (usage_usec, user_usec, system_usec) = parse_cpu_stat(&read(&self.path.join("cpu.stat"))?)?; - // io.stat is empty until the cgroup does I/O, and it is missing if the io controller is off. + // io.stat is empty until the cgroup does I/O. It is missing if the io controller is off for the cgroup, for example in a systemd unit without IOAccounting=yes. Then the bytes are not known, and they are not 0. let (rbytes, wbytes) = match fs::read_to_string(self.path.join("io.stat")) { - Ok(text) => parse_io_stat(&text), - Err(_) => (0, 0), + Ok(text) => { + let (r, w) = parse_io_stat(&text); + (Some(r), Some(w)) + } + Err(_) => (None, None), }; let memory_current = read_u64(&self.path.join("memory.current"))?; Ok(Counters { usage_usec, user_usec, system_usec, rbytes, wbytes, memory_current }) @@ -195,8 +198,8 @@ pub(crate) struct Counters { pub(crate) usage_usec: u64, pub(crate) user_usec: u64, pub(crate) system_usec: u64, - pub(crate) rbytes: u64, - pub(crate) wbytes: u64, + pub(crate) rbytes: Option, + pub(crate) wbytes: Option, pub(crate) memory_current: u64, } @@ -338,8 +341,8 @@ impl Interval { cpu_usec: end.usage_usec.saturating_sub(self.start.usage_usec), user_usec: end.user_usec.saturating_sub(self.start.user_usec), system_usec: end.system_usec.saturating_sub(self.start.system_usec), - rbytes: end.rbytes.saturating_sub(self.start.rbytes), - wbytes: end.wbytes.saturating_sub(self.start.wbytes), + rbytes: end.rbytes.zip(self.start.rbytes).map(|(e, s)| e.saturating_sub(s)), + wbytes: end.wbytes.zip(self.start.wbytes).map(|(e, s)| e.saturating_sub(s)), memory_peak, peak_scope: self.peak.scope, file_max: max.file, @@ -357,8 +360,9 @@ pub(crate) struct Usage { pub(crate) cpu_usec: u64, pub(crate) user_usec: u64, pub(crate) system_usec: u64, - pub(crate) rbytes: u64, - pub(crate) wbytes: u64, + /// None when the io controller is off for the cgroup. + pub(crate) rbytes: Option, + pub(crate) wbytes: Option, pub(crate) memory_peak: u64, pub(crate) peak_scope: PeakScope, pub(crate) file_max: u64, @@ -397,13 +401,17 @@ impl Usage { mib(self.file_max), mib(self.pss_max), mib(self.current_max), - mib(self.rbytes), - mib(self.wbytes), + io_bytes(self.rbytes), + io_bytes(self.wbytes), self.samples ) } } +fn io_bytes(bytes: Option) -> String { + bytes.map_or_else(|| "not measured: the io controller is off for this cgroup".to_owned(), mib) +} + /// Bytes as MiB with two decimals, and the exact byte count. pub(crate) fn mib(bytes: u64) -> String { format!("{:.2} MiB ({bytes} bytes)", bytes as f64 / 1048576.0) @@ -547,8 +555,8 @@ mod tests { cpu_usec: 1, user_usec: 1, system_usec: 0, - rbytes: 2, - wbytes: 3, + rbytes: Some(2), + wbytes: None, memory_peak: 4, peak_scope: PeakScope::Cgroup, file_max: 5, @@ -563,6 +571,8 @@ mod tests { assert!(text.contains(&format!("\"{key}\"")), "{key}"); } assert!(text.contains("since the cgroup was created")); + assert!(text.contains("\"wbytes\": null")); + assert!(u.text().contains("io write not measured")); } #[test] From de0a7ce2b870e6919a2289982d0281737be4c5ae Mon Sep 17 00:00:00 2001 From: tamnd <1218621+tamnd@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:01:23 +0700 Subject: [PATCH 4/6] Take the harness commit from RUPG_BENCH_COMMIT --- src/report.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/report.rs b/src/report.rs index 7c0d7cf..7e26cb3 100644 --- a/src/report.rs +++ b/src/report.rs @@ -72,8 +72,13 @@ impl Meta { } } -/// The short commit of the harness, or `unknown` outside a git checkout. +/// The short commit of the harness: `RUPG_BENCH_COMMIT` when it is set, for a copy of the tree with no `.git`, else `git rev-parse` in the tree, else `unknown`. fn harness_commit() -> String { + if let Ok(c) = std::env::var("RUPG_BENCH_COMMIT") { + if !c.is_empty() { + return c; + } + } let dir = Path::new(env!("CARGO_MANIFEST_DIR")); Command::new("git") .arg("-C") From 7ffbd6f52db668c0fdb69927677f3b4ecbfe1615 Mon Sep 17 00:00:00 2001 From: tamnd <1218621+tamnd@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:04:41 +0700 Subject: [PATCH 5/6] Collapse the commit check --- src/report.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/report.rs b/src/report.rs index 7e26cb3..bc326fb 100644 --- a/src/report.rs +++ b/src/report.rs @@ -74,10 +74,10 @@ impl Meta { /// The short commit of the harness: `RUPG_BENCH_COMMIT` when it is set, for a copy of the tree with no `.git`, else `git rev-parse` in the tree, else `unknown`. fn harness_commit() -> String { - if let Ok(c) = std::env::var("RUPG_BENCH_COMMIT") { - if !c.is_empty() { - return c; - } + if let Ok(c) = std::env::var("RUPG_BENCH_COMMIT") + && !c.is_empty() + { + return c; } let dir = Path::new(env!("CARGO_MANIFEST_DIR")); Command::new("git") From 14595f5e95fa2a5c83f197b0884af71e026c2cf4 Mon Sep 17 00:00:00 2001 From: tamnd <1218621+tamnd@users.noreply.github.com> Date: Wed, 7 Oct 2026 16:05:02 +0700 Subject: [PATCH 6/6] Add the pgbench smoke report and the README text --- README.md | 8 +- .../de0a7ce2-server3-pgbench-smoke.json | 63 +++++++++++ .../de0a7ce2-server3-pgbench-smoke.md | 100 ++++++++++++++++++ 3 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 reports/2026-10-07/de0a7ce2-server3-pgbench-smoke.json create mode 100644 reports/2026-10-07/de0a7ce2-server3-pgbench-smoke.md diff --git a/README.md b/README.md index b878b35..e1753c6 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ The procedure is [`spec/20-benchmarks.md`](https://github.com/tamnd/rupg/blob/ma ## Status -Early. The crate builds and CI is green, but no suite runs yet. `rupg-bench gates` prints the twelve gates of spec/02 section 2.10, `rupg-bench pins` prints the pinned versions, `rupg-bench measure` runs the cgroup v2 runner, `rupg-bench load` runs the load cases, `rupg-bench answers` checks answer sets, and `rupg-bench report` writes the report files. The first milestone, M0, builds the harness and measures every baseline that a later gate uses. Every command below is the planned interface from the spec. +Early. The crate builds and CI is green, but no suite runs yet. `rupg-bench gates` prints the twelve gates of spec/02 section 2.10, `rupg-bench pins` prints the pinned versions, `rupg-bench measure` runs the cgroup v2 runner, `rupg-bench load` runs the load cases, `rupg-bench answers` checks answer sets, `rupg-bench report` writes the report files, and `rupg-bench pgbench` runs the pgbench smoke run with its check. The first milestone, M0, builds the harness and measures every baseline that a later gate uses. Every command below is the planned interface from the spec. ## The gates @@ -117,6 +117,12 @@ rupg-bench measure --unit cron.service --idle --json > idle.json rupg-bench report --result idle.json --suite idle-base --machine server3 --smoke ``` +`rupg-bench pgbench` runs the smoke test of spec/21 section 21.4.6 against a running PostgreSQL server. It runs `pgbench -i` at the scale, measures the idle base of the server cgroup over 10 s, and runs the TPC-B like script of pgbench for a fixed time while it measures the server cgroup. Then it checks that the sums of `abalance`, `tbalance` and `bbalance` are equal, and that `pgbench_history` has one row for each transaction that pgbench reports. A run that fails the check is a wrong answer and the command fails. The harness talks to the server with its own small client for protocol 3.0, so it needs no `libpq`. `machines/install/postgresql.sh` makes the role and the database `bench`, allows that role on the Unix socket with no password, and turns on the io accounting of the unit. + +```sh +rupg-bench pgbench --unit postgresql@19-main --scale 1 --clients 4 --time 30 --smoke --report reports --machine server3 +``` + ## The reporting rules 1. A claim has five parts: one named machine, one metric, a baseline that we measured, the same transport on both sides, and the ratio. A claim without all five is not made. diff --git a/reports/2026-10-07/de0a7ce2-server3-pgbench-smoke.json b/reports/2026-10-07/de0a7ce2-server3-pgbench-smoke.json new file mode 100644 index 0000000..b5a8f3a --- /dev/null +++ b/reports/2026-10-07/de0a7ce2-server3-pgbench-smoke.json @@ -0,0 +1,63 @@ +{ + "cgroup": "/sys/fs/cgroup/system.slice/system-postgresql.slice/postgresql@19-main.service", + "unix_time": 1791363827, + "kernel": "6.8.0-106-generic", + "suite": "pgbench", + "machine": "server3", + "date": "2026-10-07", + "commit": "de0a7ce2", + "smoke": true, + "postgresql_commit": "7d3d2db7d5e533fa42a4202c7d308cbb0cbf9333", + "pgbench_version": "pgbench (PostgreSQL) 19beta4", + "settings": { + "scale": 1, + "clients": 4, + "jobs": 4, + "time_s": 30 + }, + "server_version": "19beta4", + "idle_base": { + "wall_s": 10.03181032, + "cpu_usec": 26703, + "user_usec": 0, + "system_usec": 26704, + "rbytes": 0, + "wbytes": 2408448, + "memory_peak_bytes": 80031744, + "memory_peak_scope": "since the cgroup was created", + "file_max_bytes": 61829120, + "pss_max_bytes": 34781184, + "memory_current_max_bytes": 71446528, + "samples": 20 + }, + "pgbench": { + "transactions": 588, + "failed": 0, + "latency_average_ms": 202.763, + "tps": 19.727433, + "output": "pgbench (19beta4)\ntransaction type: \nscaling factor: 1\nquery mode: simple\nnumber of clients: 4\nnumber of threads: 4\nmaximum number of tries: 1\nduration: 30 s\nnumber of transactions actually processed: 588\nnumber of failed transactions: 0 (0.000%)\nlatency average = 202.763 ms\ninitial connection time = 432.160 ms\ntps = 19.727433 (without initial connection time)\n" + }, + "run": { + "wall_s": 31.112581314, + "cpu_usec": 2373405, + "user_usec": 1382255, + "system_usec": 991149, + "rbytes": 602112, + "wbytes": 5218304, + "memory_peak_bytes": 83308544, + "memory_peak_scope": "since the cgroup was created", + "file_max_bytes": 65441792, + "pss_max_bytes": 58931200, + "memory_current_max_bytes": 82362368, + "samples": 44 + }, + "check": { + "sum_abalance": 141710, + "sum_tbalance": 141710, + "sum_bbalance": 141710, + "history_rows": 588, + "transactions": 588, + "passed": true, + "failures": [] + } +} diff --git a/reports/2026-10-07/de0a7ce2-server3-pgbench-smoke.md b/reports/2026-10-07/de0a7ce2-server3-pgbench-smoke.md new file mode 100644 index 0000000..bd324af --- /dev/null +++ b/reports/2026-10-07/de0a7ce2-server3-pgbench-smoke.md @@ -0,0 +1,100 @@ +# pgbench on server3, 2026-10-07 + +**This is a smoke run.** It shows that the driver works. It ran on a small scale or on a shared machine, so its numbers are not baselines and no claim may use them. + +| Field | Value | +|---|---| +| cgroup | /sys/fs/cgroup/system.slice/system-postgresql.slice/postgresql@19-main.service | +| unix_time | 1791363827 | +| kernel | 6.8.0-106-generic | +| suite | pgbench | +| machine | server3 | +| date | 2026-10-07 | +| commit | de0a7ce2 | +| smoke | true | +| postgresql_commit | 7d3d2db7d5e533fa42a4202c7d308cbb0cbf9333 | +| pgbench_version | pgbench (PostgreSQL) 19beta4 | +| server_version | 19beta4 | + +## settings + +| Field | Value | +|---|---| +| scale | 1 | +| clients | 4 | +| jobs | 4 | +| time_s | 30 | + +## idle_base + +| Field | Value | +|---|---| +| wall_s | 10.03181 | +| cpu_usec | 26703 | +| user_usec | 0 | +| system_usec | 26704 | +| rbytes | 0 | +| wbytes | 2408448 | +| memory_peak_bytes | 80031744 | +| memory_peak_scope | since the cgroup was created | +| file_max_bytes | 61829120 | +| pss_max_bytes | 34781184 | +| memory_current_max_bytes | 71446528 | +| samples | 20 | + +## pgbench + +| Field | Value | +|---|---| +| transactions | 588 | +| failed | 0 | +| latency_average_ms | 202.763 | +| tps | 19.727433 | + +output: + +``` +pgbench (19beta4) +transaction type: +scaling factor: 1 +query mode: simple +number of clients: 4 +number of threads: 4 +maximum number of tries: 1 +duration: 30 s +number of transactions actually processed: 588 +number of failed transactions: 0 (0.000%) +latency average = 202.763 ms +initial connection time = 432.160 ms +tps = 19.727433 (without initial connection time) +``` + +## run + +| Field | Value | +|---|---| +| wall_s | 31.112581 | +| cpu_usec | 2373405 | +| user_usec | 1382255 | +| system_usec | 991149 | +| rbytes | 602112 | +| wbytes | 5218304 | +| memory_peak_bytes | 83308544 | +| memory_peak_scope | since the cgroup was created | +| file_max_bytes | 65441792 | +| pss_max_bytes | 58931200 | +| memory_current_max_bytes | 82362368 | +| samples | 44 | + +## check + +| Field | Value | +|---|---| +| sum_abalance | 141710 | +| sum_tbalance | 141710 | +| sum_bbalance | 141710 | +| history_rows | 588 | +| transactions | 588 | +| passed | true | +| failures | | +