Skip to Content
Gnomon Docs
Operations

Performance

What Gnomon costs, measured. Every figure comes from a script in bench/, so you can run the same numbers on your own hardware.

Conditions. PostgreSQL 18.3 on an Apple Silicon laptop, not dedicated server hardware, with fsync = off and synchronous_commit = off. The machine was shared with other work, so absolute numbers are pessimistic; the ratios taken within one run (tracked against untracked, branch against main) are the ones to read. Point reads and writes are pgbench with prepared statements on a 200,000-row table, with the heartbeat running once a second as in production.

PGHOST=$PWD/build PGPORT=54330 bench/targets.sh 200000 4        # the whole set, with the C module
PGHOST=$PWD/build PGPORT=54330 bench/point_reads.sh 200000 8 3  # main, branch depth 1 and 4, and the past, at once
PGHOST=$PWD/build bench/write_best.sh 54330 3 5                 # writes, bulk updates, WAL and storage
PGHOST=$PWD/build bench/write_cpu.sh 54330 5 30000              # server CPU per tracked write
PGHOST=$PWD/build bench/merge_best.sh 54330 4                   # merges
PGHOST=$PWD/build PGPORT=54329 bench/soak_branches.sh 100000 5  # 100,000 open branches

Reading

Reads on main are untouched: tracking adds one permissive policy and changes no plan.

Point read on main, untracked vs tracked54.5k vs 54.6k tps — the same, within noise
jsonb @> with a GIN index, untracked vs tracked11.2k vs 11.6k tps
Point read on a branch (C module)0.035 ms against 0.031 ms on main: +13% at depth 1, and the same at depth 4 — depth costs nothing
Point read in the past, table unchanged+6% against a current read
Point read in the past, 10% of rows changed since0.027 ms, so one read in ten is rebuilt from history
Past scan of 200,000 rows25 ms as of now; 143 ms with 10% changed since, about 7.7 µs per changed row

With a history checkpoint near the moment you read, a past scan of a table where everything changed drops from 595 ms to 27 ms.

Writing

Each write on a tracked table also writes its version: one probe for the row's latest version, one heap insert and one index entry. With the C trigger that is about 5 µs of server CPU per write: a single-row UPDATE in its own transaction takes 10.0 µs untracked and 15.0 µs tracked.

Single-row UPDATE, 1 clientuntracked 52.6k tps; C trigger 41.0k (77%); PL/pgSQL trigger 21.6k (41%)
Single-row UPDATE, 8 clientsuntracked 182k tps; C trigger 139k (76%); PL/pgSQL trigger 93.0k (51%)
Bulk UPDATE of 200,000 rowsuntracked 215 ms; C trigger 609 ms; PL/pgSQL trigger 2.3 s
WAL per write224 bytes untracked; 434 with the C trigger: the version's heap insert (about 105 bytes) and its index entry (about 75)
Storage per version141-147 bytes, lazy baselines and carried-forward keyframes included
Branch writes, 4 clients9.2k tps on a logged branch, 6.1k on a scratch branch

The C module's trigger is the one to use under write load: it inserts versions into the partition and probes indexes directly, keeps the partition and its indexes open for the rest of the statement, and opens SPI only for the cases that need it.

Branching and merging

Create a branch0.13 ms each, or 0.05 ms each when creating 100,000
100,000 open branchesno effect on main's reads or writes; 53 MB of catalog
Merge into main105k rows/s with the C trigger (50,000 updated rows), no table lock
Diff of 1,000 changed rows in a 200,000-row table33 ms; it scales with the changes, not the table
Track an existing table52 ms for 200,000 rows — the lock is held while objects are generated, and nothing is done per row

The change feed

Change set, no row images2.0-2.3M rows/s over 266,666 versions
With row images, new and oldabout 220k rows/s
Draining a 152,000-change backlog in pages of 1,0005.0 s, since each page continues from the consumer's position
Capture a coordinate0.017 ms per round trip, about 5 µs inside the server

Correctness under load

The history checker runs eight concurrent sessions against main and three branches, at every isolation level, and verifies after each run that every logged read replays exactly, that the feed has no gaps or duplicates, that a branch read matches main at its cutoff for untouched keys, and that version keys never go backwards along a row. It does this across kill -9, a crash-restart and a failover to a promoted standby. The merge fuzzer runs ten million random three-way cases against the strategy invariants, about 20k cases/s.

Operations →

On this page