osm basemap: wire the slab straight into the renderer (OSM1 binary tiles) - #119
Conversation
…les)
The live deploy draws (69,125 shapes from 132,382 rows, zero rented
pixels) and is "terribly slow" (operator). Neither half of that was a
compute problem, so it was never a SIMD problem: two REPRESENTATIONS
were wrong, which is this workspace's own measured precedent
(tesseract-rs: representation is the first-order lever, SIMD second).
1. 69k retained SVG DOM elements. The browser re-rasterizes every
element on every pan frame; zoom rebuilt the layer from scratch and
its cache was never evicted, so memory grew per zoom visited.
2. serde-JSON on the hot path — ~45 B per point of [lon,lat] f64 text,
re-fetched on every zoom with no cache validator. This is exactly
what the house doctrine already forbids (T3/ADR-022: no
serialization in the hot path; to_le_bytes IS the wire).
Operator's framing, which is the right one: the tile endpoint is
`render_field_view` for the geo domain — an askama-style PROJECTION over
data already resident in the slab, same binary, same bytes. The tile
range IS the mask (`tile_range` = a Morton-prefix row range over the same
mmap), and ZOOM IS the projection depth: `pixel_shift(z) = 32-(z+8)-1` is
the ClassView choosing a reading granularity of the same z32 register — a
shift, never a branch, nothing re-encoded or stored twice.
So the data is now wired straight through:
slab z32 cells --(one multiply)--> LE f32 pairs on the wire
--> ArrayBuffer --(Float32Array lens; no JSON tree, no per-point
objects)--> Path2D per (tile,class) <-- the ONE materialization,
directly into the renderer's own retained native form
--> ~150 native canvas draw calls per frame
Server:
- OSM1 binary wire (`/api/osm/geometry/tile-bin/:z/:x/:y`): header counts
+ per shape (idx u32, class u8, closed u8, npoints u16) + npoints x
(f32,f32) TILE-RELATIVE world-pixels at z, projected straight from the
chain's z32 cells with no lon/lat round-trip and none of its trig.
Tile-relative so the f32 mantissa buys sub-pixel precision instead of
world position (absolute world px at z19 needs 27 bits; f32 has 24).
- ETag from the slab digest — already the cross-bake pin, so a new bake
busts caches by construction and nothing else does. Zoom churn becomes
304s instead of re-downloads.
- query_tile_shapes() returns RAW survivor cells; JSON and binary are
both projections of it, so the two wires cannot drift in sampling or
classification. JSON endpoint unchanged (tests, curl, the click path).
- ShapeClass::wire_code() is pinned by value, not `as u8`: the client's
CLASS_ORDER indexes by these bytes, and an enum reorder would silently
recolour the map with no error anywhere.
Client:
- Canvas replaces the retained SVG basemap entirely (the selection SVG
stays — one shape, DOM is right for it).
- Per (tile,class) merged Path2D; fills back-to-front then strokes with
roads on top.
- rAF-throttled draw: pointermove fires faster than the display
refreshes.
- Zoom now EVICTS the geometry cache (paths are one zoom's pixel space) —
fixes the unbounded-memory bug on top of the perf.
Measured, headless Chromium, real client code over a genuine OSM1 buffer:
desktop canvas 960x800, 146,199 px painted, SVG nodes 0, 49 bin tiles,
pan repaints, 0 external requests
phone canvas 390x490, 33,217 px painted, SVG nodes 0, map 100% of vp
wire 2,524 B binary vs 12,873 B JSON for the identical tile = 5.10x
Tests: 100 passed, 0 failed. The three new ones pin the wire codes, the
empty-tile header, and a round-trip whose coordinates are cross-checked
against an INDEPENDENT lon/lat projection (the binary encoder never
touches lon/lat, so agreement is two implementations meeting rather than
one echoed).
Not verified: the real bake. No slab in this container, so the numbers
above are synthetic-fixture through the real client code. The live
deploy is the test that matters.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NMeiLmtDKhomJNSo2ecbJw
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (4)
📝 WalkthroughWalkthroughThe PR adds an OSM1 binary geometry endpoint with ETags and conditional caching. It shares raw geometry projection between JSON and binary outputs. The cockpit replaces retained SVG rendering with DPR-aware canvas rendering and class-based ChangesOSM vector tile pipeline
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related PRs
Suggested reviewers: Poem
✨ Finishing Touches📝 Generate docstrings
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
Bugbot couldn't run - usage limit reachedBugbot is counted against Cursor usage for this user or team, and this run hit a usage or spend limit. A user or team admin can review and increase usage limits in the Cursor dashboard. (requestId: serverGenReqId_953db3ff-f546-42ca-9545-33ddea7dd342) |
The live deploy draws (69,125 shapes from 132,382 rows, zero rented pixels) and is "terribly slow". Asked whether ndarray was the answer: no — and that's the useful finding. Neither half of the slowness is compute, so SIMD has nothing to bite on. Two representations were wrong, which is this workspace's own measured precedent (tesseract-rs: representation is the first-order lever, SIMD second).
[lon,lat]f64 text, re-fetched on every zoom with no cache validator. Already forbidden by house doctrine (T3/ADR-022: no serialization in the hot path;to_le_bytesIS the wire).The framing this PR is built on
The tile endpoint is
render_field_viewfor the geo domain — an askama-style projection over data already resident in the slab. Same binary, same bytes:slab.tile_range(z,x,y)is a Morton-prefix row range over the same mmap —surface ∩ mask, nothing copied.pixel_shift(z) = 32−(z+8)−1is the ClassView choosing a reading granularity of the same z32 register — a shift, never a branch. Zooming in narrows the mask (deeper Morton prefix) and deepens the reading (smaller shift). Nothing is re-encoded or stored twice.So the data now goes straight through, with exactly one materialization — into the renderer's own native retained form:
Measured (headless Chromium, real client code, genuine OSM1 buffers)
Wire size, identical tile: 2,524 B binary vs 12,873 B JSON = 5.10× smaller — before the ETag turns zoom revisits into 304s instead of re-downloads.
Server
OSM1wire (/api/osm/geometry/tile-bin/:z/:x/:y): header counts + per shape(idx u32, class u8, closed u8, npoints u16)+npoints × (f32,f32)tile-relative world-pixels at z, projected straight from z32 cells — no lon/lat round-trip, none of its trig. Tile-relative so the f32 mantissa buys sub-pixel precision instead of world position (absolute world px at z19 needs 27 bits; f32 has 24).query_tile_shapes()returns raw survivor cells; JSON and binary are both projections of it, so the two wires cannot drift in sampling or classification. The JSON endpoint is unchanged (tests, curl, click path).ShapeClass::wire_code()pinned by value, notas u8— the client'sCLASS_ORDERindexes by these bytes, and an enum reorder would silently recolour the whole map with no error anywhere.Client
Path2Dper (tile, class); fills back-to-front, strokes with roads on top.pointermovefires faster than the display refreshes.Tests
100 passed, 0 failed. Three new: wire codes pinned, empty-tile header, and a round-trip whose coordinates are cross-checked against an independent lon/lat projection — the binary encoder never touches lon/lat, so agreement is two implementations meeting rather than one echoed.
Where ndarray actually fits (recorded in the plan, not built)
Not per-request SIMD. The honest homes are bake-time pre-tiling (a
.tilessidecar so serving becomes a range memcpy, with the projection SIMD-batched once at bake) and a zero-copy chains lens (Chains::getheap-allocates aVec<TileXy>per way per request — a lens question, not a SIMD one). Both are worth doing only if serving still profiles hot after this lands.Not verified
No slab in this container, so every number above is synthetic-fixture through the real client code. The live deploy is the test that matters.
Plan:
claude-notes/plans/2026-08-12-osm-vector-perf.md🤖 Generated with Claude Code
https://claude.ai/code/session_01NMeiLmtDKhomJNSo2ecbJw
Generated by Claude Code
Summary by CodeRabbit
Performance
Bug Fixes