Skip to content

explorer: 60–90 s 'no dots' window on cold-cache deep-link to point mode (DuckDB-WASM 1.24.0 falls back to full HTTP read) #190

@rdhyee

Description

@rdhyee

Symptom

Deep-link a viewer URL whose altitude lands inside point mode (alt < 120 km) and the viewport stays empty for ~60–90 seconds on first visit before any dots appear. At wider altitudes the page looks fine because the global res4 cluster set (38K dots) is already painted from Phase 1.

Repros (live deploy, current HEAD):

Reported by Raymond Yee, debugged via Playwright against prod 2026-05-09 / 2026-05-10.

Timing (live console log, alt=62054 URL, cold cache)

12:52:12  Phase 1: 38406 clusters in 192ms              (res4 global, 580 KB)
12:52:13  WARN  falling back to full HTTP read for: …samples_map_lite.parquet  (60 MB)
12:52:13  WARN  falling back to full HTTP read for: …isamples_202601_h3_summary_res8.parquet
12:53:37  Res8: 175653 clusters in 458ms                (≈85 s after boot)
12:53:37  Entered point mode
12:53:37  Point mode: 47 samples in 22ms                (dots finally appear)

The state machine is doing the right thing — zoomWatcher honors deep-link altitude, fires enterPointMode(), calls loadViewportSamples(). The wait is all in the network.

Root cause — DuckDB-WASM 1.24.0 client behavior, not the CDN

Initial hypothesis was that data.isamples.org Range support was broken. Live captures (curl + Playwright) show the server is fine:

  • GET with Range: bytes=0-1023HTTP/2 206, Content-Range: bytes 0-1023/62631623
  • OPTIONS preflight returns 204 with Access-Control-Allow-Headers: Range and the right Access-Control-Expose-Headers
  • HEAD returns 200 with full Content-Length + Accept-Ranges: bytes. (RFC 7233 §3.1 lets a server ignore Range on HEAD; this is correct.)

The actual sequence emitted by the DuckDB-WASM worker (per Codex's Playwright capture):

HEAD samples_map_lite.parquet            Range: bytes=0-
RESP 200, content-length=62631623, accept-ranges=bytes
console: falling back to full HTTP read
GET  samples_map_lite.parquet            (no Range header)
RESP 200, content-length=62631623        ← entire 60 MB pulled

So DuckDB-WASM 1.24.0 treats the HEAD-with-Range 200 (no Content-Range) as failure, logs falling back to full HTTP read, and re-fetches the file with no Range header — pulling all 60 MB on cold cache. The file is cache-control: public, max-age=31536000, immutable, so reload visits hit disk cache and look instant; cold visits eat the full 60 MB.

The pinned version is in Quarto's bundled OJS runtime, not the project:

  • docs/site_libs/quarto-ojs/quarto-ojs-runtime.js:228const duckdb = dependency("@duckdb/duckdb-wasm", "1.24.0", "+esm");

Why the wider URL looks fine

At alt=136,823, zoomWatcher stays in cluster mode and only needs the 2.5 MB res8 file. The same wasteful "fallback then full read" still happens, but 2.5 MB completes in 1–2 s and the 175K res8 clusters paint immediately. The zoomed-in URL trips the much larger samples_map_lite.parquet fetch, which is why the symptom is altitude-dependent.

Recommended fixes (ordered)

  1. Move off Quarto's bundled DuckDB-WASM 1.24.0. Either:

    • bump the Quarto bundle if a newer Quarto ships a fixed runtime, or
    • bypass DuckDBClient.of() and instantiate a newer DuckDB-WASM directly from CDN inside explorer.qmd's OJS cells, so we control the version regardless of Quarto. Either path requires verifying the wasm worker no longer logs falling back to full HTTP read on the same files. Worth a quick spike against @duckdb/duckdb-wasm@1.28.x (or current latest) before committing.
  2. Surface the loading state during the boot→point-mode gap. Today updatePhaseMsg('Loading individual samples...', 'loading') only fires from inside loadViewportSamples (explorer.qmd:1451), but the camera handler already awaits loadRes(8, h3_res8_url) first (explorer.qmd:1838). The user gets no signal during the slow initial load. A "Fetching sample index…" message earlier in the chain converts "page is broken" into "page is busy" — and is worth shipping independently of fix (1), since 60 MB on a slow connection will always be a multi-second wait the user needs to see.

  3. Independent of (1)/(2): re-verify halo/terrain rendering once samples load. After the slow load completed in my repro, "5,000 samples" rendered but were hard to spot at the test viewport size — possibly intersects explorer: halo crescents / disappearing dots over hilly terrain (root cause not yet identified) #185.

Diagnostic command (corrected)

# Use GET, not HEAD — many servers (correctly per RFC 7233) ignore Range on HEAD,
# so HEAD is not a reliable pass/fail check.
curl -s -o /dev/null -D - -H 'Range: bytes=0-1023' \
     https://data.isamples.org/isamples_202601_samples_map_lite.parquet | head -10
# Expect: HTTP/2 206 + Accept-Ranges: bytes + Content-Range: bytes 0-1023/<size>
# Confirmed 206 as of 2026-05-10 — server is not the bug.

Refs

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingexplorerInteractive Explorer features

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions