# Measurement records One schema for every number the benchmark harnesses report. A harness writes records (`schema.py`), `check.py` refuses a set that cannot be compared, `report.py` generates the tables from the records that pass. No figure of a document, a CHANGELOG line or a hand-off is typed: it comes from a record set and cites its `run_id`. python3 -I bench/steps/records/check.py FILE... one line for each problem, exit 1 python3 -I bench/steps/records/report.py [--tsv] FILE... the tables of a set that passes ## Who writes records bench/bench cases --records FILE every query case on the arms it can answer with: the default plan, a forced heap scan, the sift access method alone, the b-tree twin of a `btree` variant and each other variant as a sift arm named by it (an arm that no setting makes the plan of its name is listed as not forceable and left out); a `-- fresh: yes` case runs every repetition in a new backend. A load case has no plan to pick: its lanes (the sift table with its index, the heap with the b-tree or GIN twin, each variant) are forced arms, each repetition prepares its table again, the statements are timed and added, a second session counts the parallel workers. The file is written after every case (a run that stops keeps the cases before it) and checked on the way out; a case without records fails the run. An arm whose statement reaches `--timeout`, or that uses `--arm-budget` seconds (300) over its repetitions, is recorded as unavailable and named with its reason (RECORDS ARM TIMED OUT); the other arms and the cases after it are measured. The cluster of the cases is a test cluster (no autovacuum, no automatic merge), so its records say `maintenance` = `disabled`. steps/records/collect.py the library any other harness calls (`measure_statement`) with its subject, arms and environment. ## The figures of the CHANGELOG `bench/claims.tsv` lists the figures a CHANGELOG line states (a phrase of the line, the statement, size, arm, metric and value, a tolerance in percent). `bench records claims RECORDS...` (and `bench cases --check --records FILE`) fails a figure that the median of the records misses by more than its tolerance and prints both: the line and the run. A phrase that matches no line is stale and fails as well. The value is the one the line states, typed once there; the records are never typed. The phrase `-` is a figure that no CHANGELOG line states (a design note, a page; its `note` column says where): it is checked against the records alone, and its metric may be a build's `instr_leader_m`, `instr_workers_m`, `index_bytes`, `wal_bytes`, `segments`, `immutable_segments` or `buffer_segments`. ## What one row is One statement, on one arm, warm or cold, one repetition. The **arm** is the structure that answered: `heap` (a sequential scan), `btree`, `gin`, `sift` or `native` (the normal plan), with its option (`default`, `key_order=ts`, ...). Arms are chosen by forcing (`enable_*`, the index in the search path or the setting of the structure) on the same data, so they are comparable; the plan the planner chooses with nothing forced is measured as well (`mode` = `default`) and each forced arm says whether it is that plan (`picked`). The arm `native` is the normal plan (`sift.enable_agg = off`): not a structure to force, so it has the structure its plan names, and it is never picked. The structure is read from the EXPLAIN of the run and the arm must be it. Time is three columns: planning, execution (EXPLAIN ANALYZE) and the whole statement from the client. Instructions are those of the postmaster tree, workers included, and say their scope. Blocks are split by the kind of relation: heap, b-tree, gin, sift. Cold means the shared buffers of the relations were evicted; `os_cache_dropped` says whether the OS cache was emptied as well. | column | type | required | meaning | |---|---|---|---| | `run_id` | text | yes | the run that wrote the row: tip, UTC time and four hex digits; every number in a document cites one | | `tip` | text | yes | the commit of the build measured (12 hex digits) | | `harness` | text | yes | the program that measured, with the version of its records library: rows of two harnesses are not compared | | `dataset` | text | yes | the name of the data (generator and parameters) | | `rows` | integer | yes | rows of the table | | `keys` | integer | | distinct values of the key column | | `rows_per_key` | number | | rows / keys | | `heap_order` | text | yes | how the heap holds the key: arrival, sorted, hash or other | | `heap_corr` | number | | the correlation of the key with the heap, measured (pg_stats), not the order the generator meant | | `table_state` | text | yes | fresh_vacuumed, fresh_unvacuumed, churned_vacuumed or churned_unvacuumed | | `allvisible_pct` | number | | pages marked all-visible, in percent | | `churn_history` | text | | what was done to the table before the arms: upd=, ins=, del= (percent of the rows a round) and rounds= when the harness churned it with apply_churn, else the tuples inserted, updated and deleted since its statistics began (ins=, upd=, del=); empty for a fresh table | | `vacuums` | integer | | VACUUMs run on the table (pg_stat_user_tables vacuum_count) when the arms started | | `autovacuums` | integer | | autovacuums run on the table (autovacuum_count) when the arms started | | `settled` | 0 or 1 | | 1 when the harness waited for the maintenance (VACUUM, the worker pass) to settle before the arms; needs a VACUUM or a pass on record | | `settled_s` | number | | seconds from the last VACUUM, autovacuum or sift maintenance pass of the table to the start of the arms | | `dead_pct` | number | | dead tuples in percent of live and dead (pg_stat_user_tables) at the start | | `tombstone_pct` | number | | dead rows in percent of the rows of the segments of the sift index (sift.col_segments) at the start, when the table has one | | `structures` | text | yes | the structures on the table, comma separated and sorted (btree,heap,sift): what was built, not what the arm used; `constraint` stands for the indexes of a primary key or unique constraint, the table's definition, which a write pays for in every arm | | `statement` | text | yes | the name of the statement | | `sql_hash` | text | yes | hash of the statement text without its white space: one name is one text | | `variant` | text | yes | the table the arm ran on when a comparison has several (a heap in arrival order and one sorted by the key, an index built with other options): `main` when there is one; the planner picks within a variant | | `kind` | text | yes | read, maintenance (VACUUM, merge), load or build (CREATE INDEX of a sift index: the instructions of the leader and of the workers, the segments it wrote, the options it was built with) | | `protocol` | text | yes | what one repetition is: single, or the rounds it averages (rounds=12): a figure of one round is not the average of twelve | | `mode` | text | yes | forced (the arm was selected with settings), default (the planner chose) or unavailable (no setting makes the plan of this arm: one row without times, `structure` names what the plans were) | | `arm` | text | yes | heap, btree, gin, sift or native (the normal plan, sift.enable_agg off: its structure is whatever the plan names, and it is never picked); `default` rows carry the structure the plan named | | `arm_option` | text | yes | the option of the structure: default, key_order=ts, key_cut, ...; a default row names the option of the forced arm that ran the plan the planner chose (default when none did) | | `picked` | 0 or 1 | yes | 1 when this arm ran the plan the planner chooses by default: the same plan_hash (schema 7 on), before it the same structure and option | | `structure` | text | yes | the structure the EXPLAIN of this run names: heap, btree, gin, sift or mixed | | `plan_hash` | text | | twelve hex digits of the shape of the plan that ran (node types, join types, relations and indexes; no cost, row count or time): rows with one hash ran one plan, so the pick of the planner among arms of one structure that differ in the join method is matched by it (a read, schema 7 on) | | `plan_shape` | text | | the shape the hash is of, as text (`Hash Join[Inner](Seq Scan orders, Custom Scan SiftAgg orders_col)`), cut at 1,000 characters | | `temp` | text | yes | warm or cold | | `rep` | integer | yes | the repetition, from 1 | | `est_cost` | number | | the planner total cost of the plan that ran | | `plan_ms` | number | | planning time (EXPLAIN SUMMARY), apart from the execution | | `exec_ms` | number | | execution time (EXPLAIN ANALYZE, per-node timing on), planning not included; compares only plans of similar row flow (the clock reads of a node that moves many rows inflate it), `wall_ms` is the figure across join methods | | `wall_ms` | number | yes | the statement from the client: planning, execution and the transfer of the rows | | `instr_m` | number | | millions of user instructions of the statement | | `cycles_m` | number | | millions of user cycles | | `instr_scope` | text | | cluster (the postmaster tree: backend and parallel workers) or backend (the backend alone: only valid with no worker launched) | | `instr_n` | integer | | statements run in the window the instructions were counted over (a statement under 5 ms is run again and again in the one backend between two readings and the count divided: the number is of one run); 1 for a long one | | `instr_leader_m` | number | | build: millions of user instructions of the leader backend alone, counted in a run of its own after the cluster count (the same statement on the same prepared table) | | `instr_workers_m` | number | | build: instr_m - instr_leader_m, the instructions of the parallel workers (0 when none launched) | | `workers_requested` | integer | yes | parallel workers the plan asked for (a maintenance statement or a load has no plan: the workers it started) | | `workers_launched` | integer | yes | parallel workers that ran | | `heap_hit` | integer | | blocks of heap relations found in shared buffers | | `heap_read` | integer | | blocks of heap relations read | | `btree_hit` | integer | | b-tree index blocks found in shared buffers | | `btree_read` | integer | | b-tree index blocks read | | `gin_hit` | integer | | gin index blocks found in shared buffers | | `gin_read` | integer | | gin index blocks read | | `sift_hit` | integer | | sift index blocks found in shared buffers | | `sift_read` | integer | | sift index blocks read | | `other_hit` | integer | | blocks of other relations (toast, other index kinds) found in shared buffers | | `other_read` | integer | | blocks of other relations read | | `os_cache_dropped` | 0 or 1 | | cold rows: 1 when the OS page cache was emptied too, 0 when only the shared buffers were | | `table_bytes` | integer | | pg_table_size of the table after the run (heap, TOAST and free space map; a load names the schema, the sum of its tables) | | `index_bytes` | integer | | pg_relation_size of the indexes of the arm's structure after the run (sift, b-tree or GIN; none for the heap arm): what a CREATE INDEX or a rewrite built | | `segments` | integer | | segments in the directory of the sift index of the arm after the run (all kinds: sift.col_segments, sift.segments); none for an arm without a sift index | | `immutable_segments` | integer | | of them the immutable ones (a build or merge wrote them) | | `buffer_segments` | integer | | of them the write buffers, open or sealed (rows the maintenance has not yet written as a segment) | | `segments_declared` | integer | | the segment count the harness says the index has (a case that names it, `-- segments: N`): a row whose count is another is refused | | `index_options` | text | | the options the sift index of the arm was built with (its reloptions, name=value, sorted, `;` between), `default` when it has none | | `wal_bytes` | integer | | WAL written by the run | | `fpw` | 0 or 1 | yes | full_page_writes of the server | | `cpus` | number | yes | the CPU quota the server ran with (cpus) | | `noise` | 0 or 1 | | 1 when the host CPU or IO pressure was above 10 points during the run: a flag, not a refusal (the load of the host is a fact about the run) | | `psi_cpu_start` | number | | host CPU pressure (some, avg10) at the start of the run | | `psi_cpu_end` | number | | host CPU pressure at its end | | `psi_io_start` | number | | host IO pressure at the start | | `psi_io_end` | number | | host IO pressure at its end | | `shared_buffers_mb` | number | yes | shared_buffers | | `work_mem_mb` | number | yes | work_mem | | `pg_version` | text | yes | the server version | | `settings` | text | | every setting that is not the default (name=value, sorted, `;` between), the cluster and the session before the arm forced anything | | `settings_hash` | text | yes | ten hex digits of `settings`: two rows compared have the same one | | `maintenance` | text | yes | default, or disabled when a switch that turns the maintenance off is set (autovacuum, sift.col_auto_merge, sift.async_maintenance, a sift.test_* setting) | A harness that declares what the table of a write or a maintenance holds (`Subject.structures`, `Arm.structures`) is held to it: after the statement the library reads the table's indexes, and a run whose table holds something else (a CREATE INDEX that failed, an index nobody listed) raises. An index that backs a primary key or a unique constraint is listed as `constraint`, whatever its access method: it is the table's definition, every write pays for it, and the checker accepts it beside the arm's structure when every write figure of the comparison has it (COMPANION DIFFERS). Any other index, a b-tree too, is a structure of the figure and refuses it (OTHER STRUCTURES). The `tip` of a record is a commit: a tree that is no work tree of its revision names it by hand. The subject table of a partitioned table has no statistics, storage or access method of its own, so its facts are those of its leaf partitions together: tuples and dead tuples summed, `vacuums` and `autovacuums` the fewest of any leaf, `settled_s` the time since the latest pass of any leaf (empty while a leaf has had none), `tombstone_pct` over the leaves with a sift structure, `table_bytes` and `index_bytes` summed over the leaves. A view, or a partitioned table with no leaf, has no facts. A build (kind `build`: a case with `-- kind: build`, `col_build_default` and `col_build_date_key`) is the `CREATE INDEX` of a sift index alone, the arms its options and worker settings. Each warm repetition counts the cluster (leader and workers, `instr_m`), then the leader alone in a run of its own on a table prepared again (`instr_leader_m`); `instr_workers_m` is the difference. Every record of a sift arm that has `index_bytes` carries the directory of its index after the run (`segments`, `immutable_segments`, `buffer_segments`, from `sift.col_segments` or `sift.segments`) and the options it was built with (`index_options`); a case that says `-- segments: N` is recorded with `segments_declared` and refused when the directory holds another count. ## Versions of the schema A file starts with `#records-schema N` (files before the mark are known by their header). Every version's columns are frozen in `history.py`; columns are only added. A reader upgrades an older file in memory (`schema.MIGRATIONS`: a step from each version to the next, a new column gets its value from what the row says or "not recorded") and the tools say so: `note: FILE is schema 3, read as schema 5`; `bench records migrate FILE...` writes the file again in the current schema and keeps the old one as `FILE.schema3`. A change of the columns is a new `VERSION`, its columns in `history.py`, a step in `MIGRATIONS` and a frozen sample in `bench/test/records_samples`; `test_records.py` (part of lint) fails until the three exist and when a frozen sample is edited. ## What the checker refuses A comparison is the rows of one statement on one dataset and size. Within it: | refusal | rule | |---|---| | MIXED RUNS | one `run_id`: the arms were measured together, on one build, under one load | | MIXED STATE | one table state, churn history (updates, inserts, deletes, rounds, VACUUMs and autovacuums on record, settled), protocol and key distribution within a comparison, dead tuples and tombstones within 5 points; one heap order within a `variant` (a variant is one table, so a heap in arrival order and one sorted by the key are two variants, each with its own default plan and picked flag) | | MIXED SETTINGS | one `settings_hash` (every setting that is not the default, the cluster and the session before an arm forces anything), `shared_buffers`, `work_mem`, version, cpu quota, `full_page_writes` and harness; the report header shows the hash and the list | | TEST SETTINGS NOT LABELLED / LABELLED DISABLED WITHOUT A SWITCH | a setting that turns the maintenance off (`autovacuum`, `sift.col_auto_merge`, `sift.async_maintenance`, any `sift.test_*`, as a test cluster's configuration sets them) is refused unless the set says `maintenance` = `disabled`, which the report prints in capitals; the label without such a setting is refused too | | TWO STATEMENTS | one `sql_hash` for a statement name | | HEAP ORDER NOT AS DECLARED | `sorted` needs a measured correlation above 0.9, `hash` under 0.1 | | WRONG ARM | a forced arm is the structure its EXPLAIN names (a mixed plan is no arm) | | NO PICKED / PICKED FLAG | the default plan is in the set and each arm says whether it is that plan; a default plan that mixes structures is a row of its own (arm `mixed`) that no forced arm equals; a maintenance or a load has no plan to pick: forced, picked 0 The arm `native` (the normal plan) is never picked, whatever its plan. | | CHURN HISTORY MISSING / SETTLED WITHOUT A VACUUM | a churned read says what was applied (`collect.apply_churn` returns it: percent of rows updated, inserted and deleted a round, rounds; else the tuples the statistics counted); `settled` needs a VACUUM, an autovacuum or a sift pass on record | | INSTRUCTIONS MISSING | a warm row has a count (perf failed or was not run: refused, not left empty) | | INSTRUCTIONS WITHOUT SCOPE / WITHOUT WORKERS | a count of the backend alone is refused when workers ran | | WORKERS SHORT / QUOTA UNDER THE PROCESSES | the workers the plan asked for ran, and the cpu quota holds them and the backend | | PLANNING NOT SEPARATE | a read has planning and execution apart | | COLD WITHOUT os_cache_dropped | a cold row says what was emptied | | TOO FEW RUNS / DUPLICATE REPETITIONS | at least 3 repetitions of each arm and temperature | | (not a refusal) noisy host | a run above 10 points of host pressure carries `noise` = 1 and a cell above it or with a CV over 10% is marked noisy; the report warns when the arms of a comparison were measured under pressure more than 10 points apart. The load of the host is a fact about the run, so it never refuses a file; different data, table state or settings do | | OTHER STRUCTURES | a maintenance or load figure is of a table that holds the arm's structure alone (and `constraint`: the indexes a primary key or unique constraint needs, part of the table's definition) | | COMPANION DIFFERS | the write figures of a comparison are not all of a table with a constraint index, or all of one without: the arms would differ in more than their structure | | SEGMENTS MISSING | a sift arm with `index_bytes` has no `segments`, `immutable_segments`, `buffer_segments` or `index_options` (rows of schema 6 and later; an older row is not refused) | | SEGMENT KINDS | immutable and write buffer segments are more than the segments of the directory | | SEGMENTS CONTRADICT | the count a case declares (`segments_declared`) is not the count of the directory | | BUILD WITHOUT SPLIT / BUILD SPLIT / BUILD WORKERS | a warm build row lacks `instr_leader_m` or `instr_workers_m`, their sum is not the cluster count (1%), or the cluster ran beyond the leader with no worker launched | | BUILD WITHOUT index_bytes / wal_bytes | a build row lacks the bytes of its index or its WAL | | BUILD ON TEST SETTINGS | a build measured on a cluster with a `sift.test_*` setting: the build is not the product's | | TIP UNKNOWN | the `tip` of a record is not a commit (12 hexadecimal digits) | | NO RECORDS | an empty set proves nothing | ## The imprecisions this prevents Each row is a way a reported number was wrong or had to be revised, and the column or rule that now makes it impossible to state. | what went wrong | prevented by | |---|---| | a figure taken on sequential strings quoted beside a case that used random strings | the dataset columns (`keys`, `rows_per_key`, `heap_order`, `heap_corr`) and one dataset in a comparison | | the time of a CREATE INDEX quoted as the time of the merge statement | the statement name and `sql_hash`, `kind`; a claim names its statement | | gains of parallel workers hidden by a one-cpu quota | `cpus`, `workers_requested`, `workers_launched`: QUOTA UNDER THE PROCESSES, WORKERS SHORT | | instructions of the backend beside a time with workers | `instr_scope`: INSTRUCTIONS WITHOUT WORKERS | | execution time quoted as the whole statement, or the reverse | `plan_ms`, `exec_ms`, `wall_ms` apart | | a cold read that was cold in the shared buffers only | `temp`, `os_cache_dropped` | | WAL of a run with full page writes beside one without | `fpw` in the settings that must agree | | a fresh table beside a churned one that was not vacuumed | `table_state`: MIXED STATE | | the plan the planner picks quoted for an arm it did not run, or two arms that were the same plan | `mode`, `picked`, `structure`: WRONG ARM, PICKED FLAG | | a heap meant to be in no order that was clustered by the key | `heap_corr` measured: HEAP ORDER NOT AS DECLARED | | a maintenance figure of an arm whose table also kept another index | `structures`: OTHER STRUCTURES | | a write cost with the primary key of the table in it, beside one without | `structures`: `constraint`, COMPANION DIFFERS | | a record of code nobody can find again | `tip`: TIP UNKNOWN | | a "7 segments" index whose record holds no count | `segments`, `segments_declared`: SEGMENTS MISSING, SEGMENTS CONTRADICT | | a build cost that counts the leader only, or the workers by guess | `instr_leader_m`, `instr_workers_m`: BUILD SPLIT | | a baseline file of an older harness compared with a new run | `harness` with the library version and one `run_id` | | a number that held for one setting of a structure stated for all | `arm_option` carries the options that changed the structure | | one round quoted as the average of twelve | `protocol` | | a state measured on a cluster whose configuration turned the maintenance off (dead rows no VACUUM took, segments no merge joined), reported as the default's | `settings`, `settings_hash`, `maintenance`: TEST SETTINGS NOT LABELLED, MIXED SETTINGS | | a time taken while the host was busy beside one taken while it was not | `psi_*`, `noise`: the noisy mark and the warning of the report | | a pass read from an empty output | NO RECORDS, and the count of rows the tools print | | a CHANGELOG figure that no longer matches the measurement | the claims check against the records |