π€ from Claude
Problem
Shard-map invoke payloads are hitting the Lambda async event cap (256 KB). The orchestrator holds no S3 credentials (deliberate divide: lambdas write, the orchestrator doesn't), so today the only channel for a shard map is the invoke payload itself β and measured against the real benchmark fixtures, the as-shipped JSON is over the cap across the board, not just on monster shards:
fixture (tests/data/benchmark/shardmaps/) |
granules |
JSON as b64 event |
vs 256 KB |
sm_healpix_o10_88s.json (1 shard) |
4,605 |
2.15 MB |
8.4Γ over |
sm_healpix_o9_88s.json (1 shard) |
5,620 |
2.62 MB |
10.2Γ over |
sm_healpix_o11.json (22 shards) |
737 |
347 KB |
1.35Γ over |
(b64: an event payload is JSON, so binary/compressed bytes pay a 4/3 base64 tax; all "event" numbers below include it.)
Why the payload is fat: derivable-URL denormalization
Each granule record is {id, s3, https} β and both URLs are derivable from the id (same filename; the date path parses out of ATL03_YYYYMMDDHHMMSSβ¦). ~89% of every record is redundancy. That shape also decides the format question below.
Measured options (real fixtures, zstd level 3 via pyarrow)
| encoding |
o10_88s (4,605) |
o9_88s (5,620) |
o11 (737) |
| json as shipped (event) |
2.15 MB β |
2.62 MB β |
347 KB β |
| json + zstd (event) |
93 KB β
23Γ |
111 KB β
24Γ |
4.9 KB β
70Γ |
| parquet + zstd (event) |
172 KB β
12.5Γ |
205 KB β
13Γ |
8.6 KB β
40Γ |
| json ids-only (event) |
259 KB β 8.3Γ |
316 KB β 8.3Γ |
43 KB β
8Γ |
| json ids-only + zstd (event) |
45 KB β
48Γ |
54 KB β
48Γ |
2.4 KB β
144Γ |
Notable:
- Parquet-over-the-wire loses to plain zstd-JSON β by 2Γ. On string-heavy, cross-record-redundant records, whole-blob zstd exploits the redundancy globally; parquet pays per-column page overhead and per-value string lengths at these sizes. (Parquet's wins β columnar scans, selective reads β don't apply to "decode the whole map once in a worker".) An "orchestrator sends parquet, worker re-emits JSON" pipeline was considered and is dominated on this data; it also would have squeaked under the cap with only ~3% headroom at the 6k-granule scale on synthetic records.
- Normalization alone is not enough (o9_88s ids-only is still over) β it needs the zstd too.
- The records carry no geometry β the one shape where parquet/WKB would win. If footprints ever move into these payloads, re-measure.
Recommendation (sequenced)
- Now: ids-only + zstd envelope. Ship granule ids plus URL templates in metadata; workers reconstruct
s3/https (two-line template expansion β the collection is already known). Wrap every invoke in a versioned, unconditional envelope, e.g. {v: 1, codec: "zstd+json", data: <b64>} β no size threshold: a threshold is two codepaths plus a cliff rediscovered in production. Add an orchestrator-side size guard that fails loudly before the Invoke API rejects. Result: worst real fixture becomes a 54 KB event (48Γ); headroom extrapolates to ~25β30k granules/shard.
- Durable: presigned-URL staging (claim-check that honors the creds divide). A lambda mints a scoped, expiring presigned PUT URL (tiny sync invoke); the orchestrator HTTP-PUTs the map directly β never holding S3 credentials β and async-invokes workers with the object key + size/hash. Removes the ceiling category entirely; events stay small, loggable, replayable; the at-rest format becomes a pure reader-side choice. (A simpler "stager" sync-invoke variant β worker PUTs the 6 MB-capped sync payload and returns a key β works but caps at 6 MB and moves the bytes through Lambda twice.)
Rejected: parquet/arrow-IPC on the wire (dominated, above β arrow IPC additionally doesn't fit at 6k granules); threshold-switched dual formats (permanent two-path reader complexity for every consumer, cliff merely postponed).
Why the durable fix matters even at 48Γ
The _88s fixtures are the polar-convergence shard: ICESat-2's 92Β° inclination drives essentially every track through the 88Β°S ring, so that shard's granule count grows ~linearly with mission duration. ~25β30k granules of headroom is comfortable today and is still a date, not a fix β which is what item 2 is for.
Side note: this closes the "why is the root MOC JSON?" question too
The same measurement run covered mortie spec Β§7.3 (coverage.moc root ranges): on real Antarctic-basin covers, the spec's runs transform does the heavy lifting (12β98Γ cardinality reduction, growing with order), leaving the worst matrix case (191k cells) at 70 KB of spec JSON for a read-once-per-open object β with parquet saving only ~64 KB more at the cost of a browser-side arrow-wasm dependency (gridlook phase 6 reads it with JSON.parse) and a spec change. The spec's tiering is already right: declarations in JSON, the size-sensitive tier (leaf bitmaps, Β§7.2) already binary+zstd, bulk cell data in arrow/parquet via the interchange docs. No change recommended there.
Repro
Fixtures: tests/data/benchmark/shardmaps/. Encoders: zstd via pyarrow.compress (level default), parquet compression="zstd" with a shard-index column preserving bucketing. Numbers above are from the current fixtures on main; the MOC-side numbers are from mortie main (morton_coverage flat covers at orders 8/10/12, Β§7.3 run computation, Β§1 word layout).
π€ from Claude
Problem
Shard-map invoke payloads are hitting the Lambda async event cap (256 KB). The orchestrator holds no S3 credentials (deliberate divide: lambdas write, the orchestrator doesn't), so today the only channel for a shard map is the invoke payload itself β and measured against the real benchmark fixtures, the as-shipped JSON is over the cap across the board, not just on monster shards:
tests/data/benchmark/shardmaps/)sm_healpix_o10_88s.json(1 shard)sm_healpix_o9_88s.json(1 shard)sm_healpix_o11.json(22 shards)(b64: an event payload is JSON, so binary/compressed bytes pay a 4/3 base64 tax; all "event" numbers below include it.)
Why the payload is fat: derivable-URL denormalization
Each granule record is
{id, s3, https}β and both URLs are derivable from the id (same filename; the date path parses out ofATL03_YYYYMMDDHHMMSSβ¦). ~89% of every record is redundancy. That shape also decides the format question below.Measured options (real fixtures, zstd level 3 via pyarrow)
Notable:
Recommendation (sequenced)
s3/https(two-line template expansion β the collection is already known). Wrap every invoke in a versioned, unconditional envelope, e.g.{v: 1, codec: "zstd+json", data: <b64>}β no size threshold: a threshold is two codepaths plus a cliff rediscovered in production. Add an orchestrator-side size guard that fails loudly before the Invoke API rejects. Result: worst real fixture becomes a 54 KB event (48Γ); headroom extrapolates to ~25β30k granules/shard.Rejected: parquet/arrow-IPC on the wire (dominated, above β arrow IPC additionally doesn't fit at 6k granules); threshold-switched dual formats (permanent two-path reader complexity for every consumer, cliff merely postponed).
Why the durable fix matters even at 48Γ
The
_88sfixtures are the polar-convergence shard: ICESat-2's 92Β° inclination drives essentially every track through the 88Β°S ring, so that shard's granule count grows ~linearly with mission duration. ~25β30k granules of headroom is comfortable today and is still a date, not a fix β which is what item 2 is for.Side note: this closes the "why is the root MOC JSON?" question too
The same measurement run covered mortie spec Β§7.3 (
coverage.mocroot ranges): on real Antarctic-basin covers, the spec's runs transform does the heavy lifting (12β98Γ cardinality reduction, growing with order), leaving the worst matrix case (191k cells) at 70 KB of spec JSON for a read-once-per-open object β with parquet saving only ~64 KB more at the cost of a browser-side arrow-wasm dependency (gridlook phase 6 reads it withJSON.parse) and a spec change. The spec's tiering is already right: declarations in JSON, the size-sensitive tier (leaf bitmaps, Β§7.2) already binary+zstd, bulk cell data in arrow/parquet via the interchange docs. No change recommended there.Repro
Fixtures:
tests/data/benchmark/shardmaps/. Encoders: zstd viapyarrow.compress(level default), parquetcompression="zstd"with a shard-index column preserving bucketing. Numbers above are from the current fixtures on main; the MOC-side numbers are from mortie main (morton_coverageflat covers at orders 8/10/12, Β§7.3 run computation, Β§1 word layout).