Skip to content

perf(photos): orient full frames by row and by tile instead of per pixel - #889

Merged
oddharsh merged 2 commits into
mainfrom
claude/zenc-orient-in-resample
Sep 22, 2026
Merged

oddharsh merged 2 commits into
mainfrom
claude/zenc-orient-in-resample

Conversation

@oddharsh

Copy link
Copy Markdown
Owner

Stacked on #888, which carries the zenc:bench instrument this was measured with. Merge that first; this PR's diff is the orientation work alone.

The first pass of the agentic-iteration loop on zenc. orient() re-ran the orientation match and a bounds-checked slice copy for every pixel of a 26 MP frame, and for the four rotations that swap axes it read the source down a column, 75 KB between reads. 135 of 185 source photos are rotated, so it ran on nearly every ingest.

The change

  • Flips (2, 3, 4) copy or reverse whole rows.
  • Transposes (5-8) walk 64px tiles, so each tile's source block stays in cache while its destination rows are written front to back. In all four, source x depends on destination y alone and source y on destination x alone, which is what makes the tiling a pure reindex.
  • The channel count is a const generic, so each pixel copy is a fixed-size move.

Same values to the same places, so output is byte-identical by construction, and the gate checks it anyway.

Measured

Decode-to-resample floor, one 6240x4160 JPEG, interleaved:

orientation before after
1 (control) 508 ms 506 ms
3 731 ms 572 ms
6 774 ms 577 ms
8 859 ms 601 ms

bun run zenc:bench, 7 trials, exact Mann-Whitney per source:

source wall CPU p
XT507333.JPG, rotated 1.173x 1.095x 0.0006
XT500010.HIF, rotated 1.125x 1.056x 0.0006
XT507876.JPG, orientation 6 1.091x 1.050x 0.0006
three upright sources 0.999-1.007x 0.999-1.006x not called

Gate: all 24 square outputs and 258 histograms byte-identical.

Throughput is the honest caveat. At --parallel 8 the batch read 1.021x wall and 1.016x CPU, p = 0.9 and 0.3. The batch is half rotated, so the expected saving is about 2.5%, and an 8-worker pool on a machine at load 7.7 spreads CPU time ±13%. Seven batches can't resolve that. The per-photo CPU saving above is the number to trust.

How the loop got here

  1. A row-band transpose first. It saved 144 ms at orientation 8 but only 64 ms at 6, and still left about 200 ms over upright.
  2. 2D tiles at 32, 64 and 128, timed round-robin against the band and the old binary. Tile size barely mattered, and 64 sits in the middle.
  3. Stopped there. The transposes now sit about 90 ms over upright, close to a flip's 64 ms, and that remainder is the copy into a fresh frame buffer. Removing it means reading through the orientation inside halflight's resample, a halflight API change with its own A/B. What's left is under 5% of a photo, which is the convergence line.

Checks

  • orient_reference, the old loop verbatim, is the oracle for a new test over 11 frame sizes that cross tile edges, every orientation, gray and RGB. Mutation controls: it fails on a wrong axis (1x5, orientation 7) and on a one-pixel tile-edge error (33x65, orientation 5).
  • images/histograms re-locked after regenerating with the new zenc: histograms.json and metadata.json rewritten, byte-identical.
  • cargo test 39/39, clippy -D warnings, lint, test 847/0.

@github-actions github-actions Bot added area: docs Runbooks and repository prose area: infra Declared Cloudflare state, wrangler config, pins area: photos The photo pipeline and everything it emits type: perf Changes what something costs labels Sep 22, 2026
@github-actions

github-actions Bot commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Wire-size diff vs merge base

b9f81505c7f9a5368f2f39d1699811e129a71823 → cbbe30254d853403edba052b23089bc81ec42ea7

Brotli q11, the quality the edge serves. Every number here is deterministic:
an unchanged file produces no row. Advisory — this check fails on nothing.

Client assets

No change. 26 files, 87.55 KiB Brotli total.

Pages

No change. 57 files, 585.04 KiB Brotli total.

Worker bundle

No change. 250.16 KiB gzip.

Dictionary transport

Family dictionary acquisition: no change, 14.41 KiB q11.

pd/: no change. 113 deltas, 488.91 KiB total.

@oddharsh
oddharsh added this pull request to stack #890 September 22, 2026 19:01
Base automatically changed from claude/agentic-iteration-optimization-17c1b2 to main September 22, 2026 19:01
zenc's orient() re-ran the orientation match and a bounds-checked slice copy
for every pixel of a 26 MP frame, and read the source down a column for the
four rotations that swap axes. Flips now copy or reverse whole rows; those four
rotations walk the frame in 64px tiles so each tile's source block stays in
cache. Same values to the same places, so output is byte-identical.

Measured 2026-09-22 on a 6240x4160 JPEG, decode-to-resample only:
orientation 3 731 -> 572 ms, 6 774 -> 577 ms, 8 859 -> 601 ms.
zenc:bench: identical on all 24 outputs and 258 histograms; latency 1.12-1.17x
on rotated sources, upright unchanged. CPU per rotated photo -5.4 to -7.0%.

The old loop stays as orient_reference, the oracle for a test over 11 frame
sizes that cross tile edges; that test fails on a wrong axis and on a
one-pixel tile-edge error.
pixels.rs is a declared input to images/histograms, so the orient change marks
it stale. Regenerated with the new zenc (extract-photo-metadata.sh --merge over
the SOOC folder, then build-histogram-index.ts): histograms.json and
metadata.json were rewritten and came back byte-identical, 258 of 258 packed.
Re-locked with derive:check --lock --only images/histograms.

The zenc README records the pass, what orientation still costs (the copy into a
fresh frame, which needs a halflight API change to remove), and a unit-test
count that had read 35 against 38.
@oddharsh
oddharsh force-pushed the claude/zenc-orient-in-resample branch from 889d2f0 to 354bea0 Compare September 22, 2026 19:01
@oddharsh
oddharsh merged commit 6917a59 into main Sep 22, 2026
14 checks passed
@oddharsh
oddharsh deleted the claude/zenc-orient-in-resample branch September 22, 2026 19:06
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area: docs Runbooks and repository prose area: infra Declared Cloudflare state, wrangler config, pins area: photos The photo pipeline and everything it emits type: perf Changes what something costs

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant