VisiMark — examples
Table of Contents
# Test-suite sharding — a build tool reads this, not a person

Test-suite sharding — a build tool reads this, not a person

CI needs to split the test suite across parallel runners so the slowest shard
finishes as close to the others as possible. Most teams keep that split in a
YAML matrix nobody remembers to update, so it drifts from the suite's actual
shape until one runner takes eleven minutes and the rest take two. This
document is the sharding policy, and it is also the thing CI reads to build
the matrix — there is no second file to forget.

CI needs to split the test suite across parallel runners so the slowest shard finishes as close to the others as possible. Most teams keep that split in a YAML matrix nobody remembers to update, so it drifts from the suite's actual shape until one runner takes eleven minutes and the rest take two. This document is the sharding policy, and it is also the thing CI reads to build the matrix — there is no second file to forget.

## Measured suite time

Measured suite time

Timings below are the median of the last 20 runs on `main`, updated by a
scheduled job that edits this table and nothing else.

Timings below are the median of the last 20 runs on main, updated by a scheduled job that edits this table and nothing else.

| Suite        | Seconds |
|--------------|--------:|
| unit         |      42 |
| integration  |     311 |
| e2e-checkout |     498 |
| e2e-search   |     266 |
| e2e-admin    |     189 |
| contract     |      97 |
Suite Seconds
unit 42
integration 311
e2e-checkout 498
e2e-search 266
e2e-admin 189
contract 97
```vmark #suite
runners = 4

total_seconds  = SUM(Seconds)
avg_per_runner precision 2 = total_seconds / runners
worst_case     = MAX(Seconds)

assert worst_case <= avg_per_runner * 2
```
runners = 4

total_seconds  = SUM(Seconds)
avg_per_runner precision 2 = total_seconds / runners
worst_case     = MAX(Seconds)

assert worst_case <= avg_per_runner * 2
Six suites total **1403**<!--vmark=suite.total_seconds--> seconds of
sequential test time. Split across **4**<!--vmark=suite.runners--> runners
that would be **350.75**<!--vmark=suite.avg_per_runner--> seconds each if the
split were perfect; no single suite may cost more than double that, which
`e2e-checkout` at 498 seconds still satisfies.

Six suites total 1403 seconds of sequential test time. Split across 4 runners that would be 350.75 seconds each if the split were perfect; no single suite may cost more than double that, which e2e-checkout at 498 seconds still satisfies.

## The matrix CI actually consumes

The matrix CI actually consumes

CI does not read the table above — it cannot do bin-packing over Markdown
prose. It runs `visimark eval` and gets numbers a script can act on:

CI does not read the table above — it cannot do bin-packing over Markdown prose. It runs visimark eval and gets numbers a script can act on:

```console
$ visimark eval docs/example-ci-sharding.md --get suite.total_seconds
1403

$ visimark eval docs/example-ci-sharding.md --get suite.worst_case
498
```
$ visimark eval docs/example-ci-sharding.md --get suite.total_seconds
1403

$ visimark eval docs/example-ci-sharding.md --get suite.worst_case
498
A packaging script (outside VisiMark's scope — it consumes these numbers, it
does not produce them) greedily assigns each suite to the currently-lightest
runner and emits the matrix. The runner totals still live as a table column,
not four scalar additions, because `MAX` and `MIN` are reduces: they take one
column, never a list of expressions, so the audit trail stays a column a
reader can see rather than an inline sum hidden inside a function call.

A packaging script (outside VisiMark's scope — it consumes these numbers, it does not produce them) greedily assigns each suite to the currently-lightest runner and emits the matrix. The runner totals still live as a table column, not four scalar additions, because MAX and MIN are reduces: they take one column, never a list of expressions, so the audit trail stays a column a reader can see rather than an inline sum hidden inside a function call.

| Runner | Assigned suites            | Seconds |
|--------|-----------------------------|--------:|
| 1      | e2e-admin, contract         |     286 |
| 2      | e2e-checkout                |     498 |
| 3      | integration, unit           |     353 |
| 4      | e2e-search                  |     266 |
Runner Assigned suites Seconds
1 e2e-admin, contract 286
2 e2e-checkout 498
3 integration, unit 353
4 e2e-search 266
```vmark #matrix
max_runner = MAX(Seconds)
min_runner = MIN(Seconds)
spread     = max_runner - min_runner

assert spread <= suite.avg_per_runner
```
max_runner = MAX(Seconds)
min_runner = MIN(Seconds)
spread     = max_runner - min_runner

assert spread <= suite.avg_per_runner
The slowest runner finishes in **498**<!--vmark=matrix.max_runner--> seconds
against a fastest of **266**<!--vmark=matrix.min_runner--> — a spread of
**232**<!--vmark=matrix.spread--> seconds, inside the one-average tolerance
this policy sets for itself.

The slowest runner finishes in 498 seconds against a fastest of 266 — a spread of 232 seconds, inside the one-average tolerance this policy sets for itself.

## Why a build tool reads a Markdown file

Why a build tool reads a Markdown file

The alternative is the usual one: a `ci-matrix.yml` maintained by hand, next
to a `TIMINGS.md` nobody wires back into it. They agree on day one and
diverge on day forty, silently, because nothing checks them against each
other.

The alternative is the usual one: a ci-matrix.yml maintained by hand, next to a TIMINGS.md nobody wires back into it. They agree on day one and diverge on day forty, silently, because nothing checks them against each other.

Here there is one artifact. `visimark check` fails the moment `e2e-checkout`
grows past the point where `worst_case <= avg_per_runner * 2` still holds —
before the matrix is regenerated, not after a runner starts timing out. The
matrix table is still hand-maintained (VisiMark has no bin-packing primitive
and should not grow one — see the vocabulary catalogue's stance on scope), but
it is now a **checked** hand-maintained table: `spread` is asserted, not
eyeballed, and `visimark eval --json` is the one interface both a human
reading the PR and the CI job configuring `strategy.matrix` read from.

Here there is one artifact. visimark check fails the moment e2e-checkout grows past the point where worst_case <= avg_per_runner * 2 still holds — before the matrix is regenerated, not after a runner starts timing out. The matrix table is still hand-maintained (VisiMark has no bin-packing primitive and should not grow one — see the vocabulary catalogue's stance on scope), but it is now a checked hand-maintained table: spread is asserted, not eyeballed, and visimark eval --json is the one interface both a human reading the PR and the CI job configuring strategy.matrix read from.