Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 7 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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.
Expand Down
18 changes: 17 additions & 1 deletion machines/install/postgresql.sh
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,22 @@ 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
# 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"
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()'
63 changes: 63 additions & 0 deletions reports/2026-10-07/de0a7ce2-server3-pgbench-smoke.json
Original file line number Diff line number Diff line change
@@ -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: <builtin: TPC-B (sort of)>\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": []
}
}
100 changes: 100 additions & 0 deletions reports/2026-10-07/de0a7ce2-server3-pgbench-smoke.md
Original file line number Diff line number Diff line change
@@ -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: <builtin: TPC-B (sort of)>
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 | |

36 changes: 23 additions & 13 deletions src/cgroup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,13 @@ impl Cgroup {
pub(crate) fn counters(&self) -> Result<Counters, String> {
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 })
Expand Down Expand Up @@ -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<u64>,
pub(crate) wbytes: Option<u64>,
pub(crate) memory_current: u64,
}

Expand Down Expand Up @@ -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,
Expand All @@ -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<u64>,
pub(crate) wbytes: Option<u64>,
pub(crate) memory_peak: u64,
pub(crate) peak_scope: PeakScope,
pub(crate) file_max: u64,
Expand Down Expand Up @@ -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<u64>) -> 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)
Expand Down Expand Up @@ -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,
Expand All @@ -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]
Expand Down
Loading
Loading