# Measurement notes

Experiment date: 2026-10-28. PostgreSQL 16.13, Python 3.11.2, asyncpg 0.31.0, Linux/aarch64, Raspberry Pi 5.
Client and database communicate through a Unix socket on the same machine. Database limits: 0.5 CPU / 768MiB; separate client limits: 0.5 CPU / 256MiB. CPU quota period: 10ms.
This is a read-only experiment on 20,000 warmed-up rows. Settings: shared_buffers=64MiB, work_mem=1MiB, JIT off, parallel queries off, max_connections fixed at 110.
All 5/20/100 pool connections were established and exercised before measurement.

## Files and fields

- summary.csv: pooled measurements per condition; 7,200 requests for each light condition and 1,440 for each heavy/heavy_burst condition.
- runs.csv: summaries of 27 runs, linked to requests by run_id.
- requests.csv: 30,240 raw request measurements, without internal hostnames, accounts, absolute timestamps, or credentials.

light: fetch one primary-key row every 5ms (200/sec).
heavy: aggregate MD5 computations across 2,000 rows every 25ms (40/sec).
heavy_burst: the same aggregation, 10 requests every 250ms (40/sec).
Each run uses 2 seconds of warm-up and 12 seconds of scheduled arrivals. Three runs per condition, rotating pool order. All scheduled requests are observed until completion. Requests/sec uses observation duration as its denominator and is not maximum capacity.

scheduled_s, start_s, end_s: seconds relative to the start of each run.
latency_ms: scheduled send until pool release completes. It equals schedule_lag_ms + pool_wait_ms + query_roundtrip_ms + release_ms.
query_roundtrip_ms is client-observed elapsed time, not pure SQL execution time inside the database.
release_ms includes asyncpg's standard connection reset. worker is a compatibility field fixed at 0.

The response time at the slower end is the `ceil(count × 0.99)`-th sorted measurement. It is stored in existing CSV fields such as `latency_p99_ms`; field names and numbers are unchanged.
The values in summary.csv pool all requests from the three runs of each condition, rather than averaging the per-run values.
`run_p99_min_ms` and `run_p99_max_ms` are the minimum and maximum of the same measure across the three runs, not confidence intervals.
Interval means are arithmetic means over all requests. Adding the slower-end values for separate intervals does not produce the slower-end value for the whole request.

db_cpu_cores/client_cpu_cores use 1 for one full core: the increase in usage_usec, converted to seconds, divided by observation seconds. Condition CPU values are weighted by observation duration. db_cpu_quota_percent treats the 0.5-core limit as 100%.
db_throttled_periods_percent is the increase in nr_throttled divided by the increase in nr_periods from cpu.stat; it is not a fraction of time spent delayed.
db_memory_mib averages the start/end memory.current readings for each run, then averages the three runs. It includes shared memory and page cache and is not memory uniquely attributable to a connection.
max_concurrent_query_roundtrips_derived is maximum concurrent client-observed round trips, not the number simultaneously using CPU inside the database.

## Measurement blocks

A safety monitor stopped the experiment after r01–r11. For r12–r27 we recreated the temporary database with the same settings and data.
On resuming, only post-measurement connection shutdown was changed to sequential shutdown; measured SQL, arrival schedules, resource caps, and warm-up were unchanged.
This was not one continuous experiment, and variations from the shared host were not isolated. Earlier closed-loop preliminary trials are excluded.

## SQL

```sql
CREATE UNLOGGED TABLE pool_lab (
  id integer PRIMARY KEY, balance integer NOT NULL, payload text NOT NULL
);
INSERT INTO pool_lab
SELECT i, i % 1000, repeat(md5(i::text), 4)
FROM generate_series(1, 20000) AS i;
VACUUM ANALYZE pool_lab;
```

Light query: `SELECT balance FROM pool_lab WHERE id = $1`.
Heavy query: `SELECT sum(length(md5(payload || id::text))) FROM pool_lab WHERE id BETWEEN $1 AND $1+1999`.
Using each run's request sequence, $1 is `(sequence*193)%20000+1` for light and `(sequence*193)%18001+1` otherwise. The two SQL statements are measured in separate conditions, not mixed.

Specifications: [asyncpg](https://magicstack.github.io/asyncpg/current/api/index.html#connection-pools), [PostgreSQL 16](https://www.postgresql.org/docs/16/runtime-config-connection.html), [Linux cgroup v2](https://docs.kernel.org/admin-guide/cgroup-v2.html#cpu-interface-files).
