Skip to content

perf: time the zarr export's four phases — 5h07m with no breakdown could not be attributed (#501) - #502

Merged
Polichinel merged 1 commit into
developmentfrom
perf/measure-export-phases
Oct 18, 2026
Merged

Polichinel merged 1 commit into
developmentfrom
perf/measure-export-phases

Conversation

@Polichinel

Copy link
Copy Markdown
Contributor

Closes the first ask of #501. Measurement only — nothing is optimised.

The problem

The 2026-10-16 run measured the zarr export at 18,427s (5h07m): 88% of the pipeline, and ~5× ADR-041's June figure of ~57 min. The regression could be stated but not located, because the export printed one Time: line and nothing else.

What this adds

Four phases, printed in the export's summary and written to export_ledger.jsonl as phase_seconds:

Phase Covers
scan the source_sums loop — one traversal of grid.npy per feature
write ds.to_zarr + consolidate_metadata — the second traversal, plus compression
verify the round-trip integrity check — full decompression of the written store
swap the C-144 .tmp → rename dance
other the remainder, printed explicitly so the columns account for the whole of Time:

I added swap beyond the three #501 asked for because measuring it is free and it lets us rule it out instead of assuming it's cheap.

On the synthetic grid (7 features, 0.1 GB, fits in RAM):

Time: 1.9s
Phases:
  scan          0.2s   10.1%
  write         0.7s   35.2%
  verify        0.5s   24.7%
  swap          0.0s    0.0%
  other         0.6s   30.0%

The ledger is the point, not the print. logs/refresh.log rotates. phase_seconds in the ledger makes the breakdown queryable across runs, which is what turns one measurement into a trend — and the absence of exactly that is why a 5× regression sat unnoticed for four months.

Two claims verified rather than inferred

The comments in the diff assert things about the data layout. I measured them:

Claim Evidence
grid.npy is C-order with features as the contiguous last axis C_CONTIGUOUS=True, strides=(..., 28, 4) at F=7 → grid[:,:,:,i] strides F × 4 bytes between adjacent (t,h,w), i.e. 316 bytes at F=79. A 4 KB page carries ~13 useful float32s out of 1024.
source_sums exists only to feed the verify comparison exactly one consumer, export_zarr.py:408

The second matters: the scan phase reads the entire grid purely to produce checksums for a later comparison.

Explicitly not done

No change to chunking, compression, the slicing pattern, or the integrity check. Each is a plausible remedy for a cause that is not established, and the integrity check is an ADR-011/C-144 guarantee that must not be weakened to buy time.

#501 carries a pre-registered falsification criterion written before any production measurement exists:

if scan comes in under ~25% of the export, the strided-memmap explanation is refuted and I stop advancing it

The hypothesis is mine, so pre-committing the criterion is the only thing stopping me from reading the result as confirmation. The synthetic baseline above — where scan is the smallest named phase — is the contrast.

Guards, drilled

Three tests in TestZarrExport. The phase labels are asserted as an exact set: a subset assertion would let a dropped phase silently restore the original blindness. Both substantive guards were sabotaged to confirm they bite:

Sabotage Result
delete phase_seconds["swap"] = ... test_phase_timings_recorded FAILED
inflate the scan timer past the total test_phases_do_not_exceed_the_total FAILED — phases sum to 9999.8s but total is 1.3s

Register: no new entry, and why

Nothing in the register covers runtime regression, and check_health.py has freshness SLOs (how old the data is) but no duration SLO. That gap is real.

It is not a new entry, for two reasons. The instrument that would have caught this is what this PR builds — registering a concern this PR largely closes would be noise. And the root cause is already recorded: the 5× went unnoticed because ADR-041's figures were the only record and nobody re-measured, which is C-343's pattern exactly, registered as its 2026-10-18 addendum in #500.

What remains genuinely open: durations are now recorded but nothing compares them across runs. One run of data does not yet justify a trend-watching mechanism, so I am flagging it rather than building it.

Verification

  • full suite — exit 0, 0 FAILED, 0 ERROR (not 137; that would be OOM, not green)
  • uv run ruff check . — clean
  • uv run mypy src/ — clean, 89 source files
  • bash docs/validate_docs.sh — PASSED
  • export-touching test files re-run green against the final tree, since the full suite above predates a variable rename

Pre-existing and out of scope: export_zarr.py:100 has a mypy union-attr error on sys.stdout.reconfigure, identical on a clean tree. CI typechecks src/ only, so it does not gate — but it is real and unowned.

🤖 Generated with Claude Code

https://claude.ai/code/session_01EfeuULbPTqrx4qtNki5dy9

…uld not be attributed (#501)

The 2026-10-16 run measured the export at 18,427s, 88% of the pipeline
and ~5x ADR-041's June figure of ~57 min. Nothing recorded WHERE that
time went, so the regression could be stated but not located.

Four phases are now timed, printed in the export's summary and written
to export_ledger.jsonl as `phase_seconds`:

  scan    the source_sums loop — one traversal of grid.npy per feature
  write   ds.to_zarr + consolidate_metadata — the second traversal
  verify  the round-trip integrity check — full decompression
  swap    the C-144 .tmp -> rename dance

Plus `other`, printed explicitly so the columns account for the whole of
the existing `Time:` line rather than silently omitting a remainder.

The ledger is the point: a print lives in logs/refresh.log, which
rotates. `phase_seconds` in the ledger makes the breakdown queryable
across runs, which is what turns a single measurement into a trend.

Verified empirically, not inferred:

- grid.npy is C_CONTIGUOUS with features as the contiguous last axis, so
  grid[:, :, :, i] strides F x 4 bytes between adjacent (t, h, w) — 316
  bytes at F=79. A 4 KB page carries ~13 useful float32s out of 1024.
- `source_sums` has exactly one consumer: the verify comparison. The
  scan phase's full-grid read exists solely to feed that check.

NOT optimised. No change to chunking, compression, the slicing pattern,
or the integrity check. Each is a plausible remedy for a cause that is
not yet established, and the integrity check is an ADR-011/C-144
guarantee that must not be weakened to save time. #501 carries a
pre-registered falsification criterion so the first production
measurement is a test rather than a confirmation.

Three guards in TestZarrExport, both substantive ones drilled by
sabotage rather than assumed:

  delete phase_seconds["swap"]      -> test_phase_timings_recorded FAILED
  inflate the scan timer past total -> test_phases_do_not_exceed_the_total FAILED

The phase labels are asserted as an exact set; a subset assertion would
let a dropped phase restore the original blindness silently.

Verification: full suite exit 0 (0 FAILED, 0 ERROR), ruff clean,
mypy src/ clean (89 files), validate_docs.sh PASSED, export-touching
test files re-run green against the final tree.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EfeuULbPTqrx4qtNki5dy9
@Polichinel
Polichinel enabled auto-merge (squash) October 18, 2026 17:37
@Polichinel
Polichinel merged commit 7ce9b6b into development Oct 18, 2026
6 checks passed
@Polichinel
Polichinel deleted the perf/measure-export-phases branch October 18, 2026 17:50
Polichinel added a commit that referenced this pull request Oct 20, 2026
…he whole point (#501) (#503)

Not one file under src/ changed since v1.14.0, and the wheel packages only
the nine src/datafactory_* trees, so a consumer installing 1.15.0 from PyPI
gets exactly what 1.14.0 gave them with different version metadata.

That is the point rather than a defect. The server deploys by git tag
(ADR-022): refresh_pipeline.sh reads ~/.views-deploy-tag and checks out that
exact tag. The two things this release carries — scripts/export_zarr.py and a
set of corrected guides — reach production no other way, because the wheel
does not contain them. v1.11.0 and v1.12.0 were both minor bumps with zero
src/ change, so the shape is house precedent, not a judgement made here.

Why now: #502 merged the only instrument that can attribute the zarr export's
5h07m to a phase, and it sits on development where the server cannot see it.
The monthly cron fires 00:00 on the 21st. A tag before then gets the
measurement on a run that was happening anyway; missing it means a deliberate
out-of-band six-hour run or waiting until 21 November.

The plan edit is not housekeeping tacked on. TestF7ProductPlanCurrency
asserts the product plan's title carries the current minor version, so the
version bump alone turns it red — the two edits are one atomic change.
Confirmed by breaking it rather than by reading it: with the title reverted to
"through v1.14" and the version at 1.15.0 the test FAILED, and went green when
the title was restored.

uv.lock moves too: it pins the root project version (line 1701), and uv
re-synced it on the first uv run after the bump. refresh_pipeline.sh:178 does
git checkout -- uv.lock before the tag checkout precisely because the server
mutates it locally.

Noticed, deliberately not fixed: "## Version History" in the same plan file
stops at v1.6 and jumps to v2.0 — eight releases happened without it. The
per-release addenda at the top have been the live record since v1.7.
Repairing a section stale for eight releases inside a version bump would hide
a documentation change inside a release.

No tag, no GitHub Release, no deploy. Each is separately gated.

Verification: full suite EXIT=0, 0 FAILED, 0 ERROR (XFAILs are documented
deferrals); ruff check . clean; mypy src/ clean (89 files); validate_docs.sh
PASSED; deploy_v160 + _v160_r2 + merge_readiness 10 passed 1 skipped (HEAD not
on a tag, expected pre-release); main is an ancestor of development.

Refs #501.


Claude-Session: https://claude.ai/code/session_01EfeuULbPTqrx4qtNki5dy9

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Polichinel added a commit that referenced this pull request Oct 20, 2026
…ve times in eight days (C-343) (#509)

Covers the export phase instrumentation (#501/#502), four rounds of C-343
correction (#497-#500), the release/publish/deploy of v1.15.0, a fifth C-343
round found while writing this up (#507), and a licensing contradiction found
while onboarding an external consumer (#508).

The v1.11.0 post-mortem named the spine running through everything it
reported: "a written record that was true when written and stopped being true
without anything failing." Both it and its companion observation — that the
post-mortem convention keeps failing to be applied — recurred.

Three of the four releases since are unwritten and now never will be. The
spine produced five fresh instances in eight days, all in C-343, the fifth
found inside the register entry created to track the first, including in its
Trigger: field. A sixth variant was committed by the author of the fourth
correction three days later: merging the instrumentation is not deploying the
instrumentation.

The conclusion recorded is not "try harder". Every instance in this window was
found by a human asking an unprompted question or by an author re-reading
their own work. Five rounds have produced a procedure that is right and
nothing that notices when a copy of it goes wrong.

Also records what went well rather than only the failures: block 4 of the
deploy procedure executed successfully for the first time since it was
written, the #501 falsification criterion was committed before any production
figure existed, three guards were sabotaged and confirmed red, and the clean-
venv publish check caught a defect in itself rather than a false alarm on the
release.

Verification: validate_docs.sh PASSED; all 14 register-reading test files
41 passed. Every figure quoted is cited to a file rather than recalled; the
release count was corrected against `git tag` before committing.

Refs #501, #508.


Claude-Session: https://claude.ai/code/session_01EfeuULbPTqrx4qtNki5dy9

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant