#351: fetch samples_map_lite once, serve every lite query from an in-memory copy - #352
Conversation
…from an in-memory copy At boot the samples table fires COUNT(*) and the first-page query over samples_map_lite_v3.parquet (62.9 MB) concurrently; with isamplesorg#345's range requests working, each concurrent scan re-fetches the whole file through DuckDB-WASM's per-read ranged GETs (measured 132.8 MB in 65 GETs, 54 % overlap). Later consumers (isamplesorg#300 aggregation, search JOINs) scan it again. - liteFile cell: after facetIndexReady settles (never competes with the boot-critical facet chain), stream-fetch the file once with a 30 s no-bytes watchdog, check Content-Length, registerFileBuffer() it, and validate the parquet footer before declaring it ready. Any failure drops the virtual file and leaves the URL path (today's ranged reads). - db.query wrapper: resolve read_parquet('<lite_url>') to the in-memory name; a lite query waits at most 120 s (one shared deadline) for the buffer, then reads the URL. Metadata probes on the URL pass through. - isamplesorg#300 preflight becomes a footer-only parquet_schema() probe, so readiness flips at ~10 s as before; the filtered-cluster load waits for the buffer before its idle wait so the heavy aggregation runs after the boot scans. - tests/playwright/measure_parquet_ranges.py: the measurement method behind isamplesorg#345/isamplesorg#351 (every .parquet GET incl. Web-Worker fetches, range coverage and overlap per file, time-to-table, warm reload). Measured (local render -> production data, cold, 200 s): lite 132.8 MB -> 62.9 MB in one GET; all parquet 156 MB -> 86 MB; samples table at 63 s instead of 122 s. filtered-clusters-300 spec vs production data: passes on this branch, fails on main. Codex review x6 -> LGTM-with-nits (applied). Fixes isamplesorg#351 Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LtTxB4jfTZgaTR7CK4zKqy
|
🤖 rbotyee (Claude, operated by @rdhyee) — Raymond's intent: have Codex refute the change before a human looks. What I did: six static review rounds; posting the final verdict verbatim. Codex round 6 (final) — LGTM-with-nitsVerdict: LGTM-with-nitsYour P1 reasoning is correct. I cannot construct a realistic timeline where this branch creates worse heavy-query concurrency than upstream.
The new pre-query gate has no unbounded wait: one deadline is armed by the first lite demand and shared by every caller (explorer.qmd). Repeated calls after expiry resolve immediately to the URL; they do not pay another 120 seconds. The #300 path therefore has at most the remaining shared deadline plus the 20-second idle cap, and its supersession check remains immediately before query dispatch (explorer.qmd). Query execution itself remains uncapped, as before. There is also no semantic cross-query inconsistency. A query already released to the URL may coexist with a later query redirected to the buffer, but both name the same versioned, immutable object. Count/page queries can therefore use different transports without seeing different data. This depends on the established immutability contract; the patch does not independently enforce a hash.
|
…lesorg#300 readiness, lite buffer) Polls window.__facetIndexStatus / __filteredClustersReady / __liteFile each second and reports the first time each was observed, so readiness timing can be compared across builds (used to show isamplesorg#351 leaves isamplesorg#300 readiness unchanged: 5.1 s vs 5.2 s on the spec's URL). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LtTxB4jfTZgaTR7CK4zKqy
…staging works A bare '/explorer.html' replaces TEST_URL's sub-path, so against https://rdhyee.github.io/isamplesorg.github.io the spec was loading a 404 page and 'filteredClustersReady' could never flip. helpers/url.js exists for exactly this (PR isamplesorg#238); use it. Verified 2/2 against staging. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LtTxB4jfTZgaTR7CK4zKqy
|
🤖 rbotyee (Claude, operated by @rdhyee) — Raymond's intent: "deploy 351 to rdhyee to check everything is ok first." What I did: dispatched the fork's Staging: https://rdhyee.github.io/isamplesorg.github.io/explorer.htmlBandwidth (
Boot milestones with the #300 spec's URL ( Playwright against staging (
One false alarm, recorded so nobody chases it: a manual Chrome check showed DuckDB not initialising for ~78 s. That tab was Also added: Nothing merged or deployed to production. |
Same latent bug fixed for filtered-clusters-300 in #352: a bare '/explorer.html' replaces TEST_URL's sub-path, so against fork staging the spec loads a 404 page. Spec stays gated on FACET_TREE_LOCAL as before. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LtTxB4jfTZgaTR7CK4zKqy
🤖 rbotyee (Claude, operated by @rdhyee) — Raymond's intent: "continue with #351" — make the cold boot stop reading
samples_map_lite_v3twice, without giving back the #345 time-to-usable win. What I did: measured the cause, fixed it at the one choke point every query passes through, took it through three Codex review rounds, re-measured, and ran the affected Playwright specs against a local render with production data. Nothing is merged or deployed; this PR is the proposal.Fixes #351.
Cause (measured, not inferred)
At boot the samples table's
refreshAll()firesloadCountandloadPage(0)concurrently (Promise.all, explorer.qmdtableView). Each is a full scan ofsamples_map_lite_v3.parquet(62.9 MB). With #345's range requests working, each concurrent scan pulls the whole file through DuckDB-WASM's per-read ranged GETs and nothing shares the bytes between them: 132.8 MB in 65 ranged GETs, 54 % overlap, for a 63 MB file. Later consumers (the #300 filtered-cluster aggregation, search-result JOINs) scan it again.Fix
liteFilecell: afterfacetIndexReadysettles (so the 63 MB never competes with Explorer downloads ~74 MB cold: DuckDB-WASM 1.24.0's HEAD+Range probe expects 206; our (correct) 200 defeats range requests #345's ~3.5 MB boot-critical facet chain),fetch()the file once, stream it with a 30 s no-bytes watchdog, check the byte count againstContent-Length,registerFileBuffer()it as an in-memory DuckDB file, and validate the parquet footer (parquet_metadata) before declaring it ready. Any failure → the virtual file is dropped and queries keep reading the URL with ranged GETs (today's behaviour).dbcell's existingquerywrapper (which already tracks in-flight queries for the explorer: filtered cluster/heatmap view at world zoom (don't promote to raw point mode for broad filters) #300 idle gate) resolves everyread_parquet('<lite URL>')to the in-memory name. The 24 query sites are untouched;lite_urlstays a URL (tests and the Substrate↔UI coherence: release manifest so sibling data files can't silently diverge #334 manifest check read it). A lite query waits at most 120 s for the fetch to settle, then reads the URL for that query — the shared gate is bounded per query (Codex rounds 1–2).tests/playwright/measure_parquet_ranges.py: the measurement method behind Explorer downloads ~74 MB cold: DuckDB-WASM 1.24.0's HEAD+Range probe expects 206; our (correct) 200 defeats range requests #345/Explorer reads samples_map_lite_v3 twice via range requests (~120 MB for a 63 MB file) — #345 fixes time-to-usable, not total bytes #351 (every.parquetGET incl. Web-Worker fetches, range coverage/overlap per file, time-to-table, warm reload), previously living in/tmp.Numbers (local render → production data host, cold cache, headless Chromium, 200 s window, same machine/link)
upstream/main)samples_map_lite_v3sample_facets_v4/ masks (range reads kept)The table gets faster, not slower: two scans from memory beat two scans over ranged reads. Caveats, stated plainly: this link ran at ~1–2 MB/s, so the fetch took 46 s; on a much slower link (< ~0.6 MB/s) a lite query issued early can hit the 120 s cap and fall back to ranged reads for that query — no worse than today for it, plus the background fetch. Not claimed: browser caching of the whole file on revisits — the immutable headers allow it, but a warm reload in headless Chromium re-fetched all 63 MB, so the comment says so rather than promising a win.
Memory: steady state one copy in the DuckDB WASM heap (the footprint the pre-#345 "full HTTP read" fallback had); transiently ~2 copies while the bytes are assembled and transferred to the worker.
Review
Codex (gpt-5.4, static, read-only sandbox) × 6 rounds, each asked to refute: R1 changes-required (P1 unbounded wait if the facet chain or fetch never settles; P2 memory comment; P2 measure latency) → R2 changes-required (P1 the cap must be absolute, not phase-scoped; P2 validate a truncated body) → R3 LGTM-with-nits → R4 changes-required (P1 the #300 readiness probe waited for the whole fetch — caught by the Playwright run; fixed with the
parquet_schemaprobe + narrowed literal) → R5 changes-required (P2 a cap expiry could cost a second 120 s wait — fixed with one shared deadline; P1 idle-cap overlap) → R6 LGTM-with-nits: agreed the residual idle-cap overlap is #300's pre-existing best-effort gate, strictly narrowed here (P3, follow-up), nit applied. Its refutation attempts that held: all 23 table-read sites match the exact literal;?data_base=sanitisation blocks quote injection; no double registration; URL/buffer mixing is consistent under the immutable-file contract.Playwright (local
:5860render, production data host,--workers=1):explorer-smoke4/4 on the final render.filtered-clusters-300withDATA_BASE=https://data.isamples.org: 2/2 on this branch, 1/2 onmain(baseline's readiness probe never flipped inside 120 s on this link). Its count-conservation step queries the lite file through the new in-memory path and matches the rendered clusters. Spec header updated: with the_v3republish, production carries res4/res6, so it can run against live data.explorer-map-overlay: 6/10 on both branch andmain— the same four table tests and the base-layer-picker occlusion test fail identically on baseline on this ~1–2 MB/s link (60 s budgets vs a 63 s table); not a regression, no signal.Not in this PR
bandwidth_matrix.pyhost-filter widening (onperf/313-bandwidth-findings).measure_parquet_ranges.py+ Playwright's CDP throttling can produce it.🤖 Generated with Claude Code
https://claude.ai/code/session_01LtTxB4jfTZgaTR7CK4zKqy