From b01c3c45e0b003f00288aeadd12940c23c7503b1 Mon Sep 17 00:00:00 2001 From: Raymond Yee Date: Fri, 17 Apr 2026 08:54:07 -0700 Subject: [PATCH] Port ?perf=1 timing panel to Search Explorer (baseline step) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit First step toward the Explorer rethink: establish measured baseline for the current slow implementation before touching architecture. Adds performance.mark() hooks at four natural boundaries: - explorer_db (DuckDB init + CREATE VIEW for samples/sample_facets) - explorer_facets (2 KB facet summaries load) - explorer_count (COUNT(*) on wide parquet through WHERE clause) - explorer_samples (ORDER BY RANDOM() LIMIT N on wide parquet — the slow one) New perfPanel cell gated on ?perf=1. Also shows ?v=2 in the header so v1 vs v2 numbers can be compared side-by-side in follow-up PR. Co-Authored-By: Claude Opus 4.7 --- tutorials/isamples_explorer.qmd | 93 +++++++++++++++++++++++++++++++++ 1 file changed, 93 insertions(+) diff --git a/tutorials/isamples_explorer.qmd b/tutorials/isamples_explorer.qmd index 1c5ec09..d41ed8d 100644 --- a/tutorials/isamples_explorer.qmd +++ b/tutorials/isamples_explorer.qmd @@ -296,6 +296,7 @@ html`
//| code-fold: true // Initialize DuckDB-WASM db = { + performance.mark('explorer-db-start'); const bundle = await duckdbModule.selectBundle(duckdbModule.getJsDelivrBundles()); const worker_url = URL.createObjectURL( @@ -315,6 +316,8 @@ db = { await conn.query(`CREATE VIEW sample_facets AS SELECT * FROM read_parquet('${sample_facets_url}')`); await conn.close(); + performance.mark('explorer-db-end'); + performance.measure('explorer_db', 'explorer-db-start', 'explorer-db-end'); return instance; } @@ -347,8 +350,11 @@ mutable facetSummariesError = null // Tier 1: Load pre-computed facet summaries (2KB, instant) facetSummaries = { mutable facetSummariesError = null; + performance.mark('explorer-facets-start'); try { const rows = await runQuery(`SELECT * FROM read_parquet('${facet_summaries_url}')`); + performance.mark('explorer-facets-end'); + performance.measure('explorer_facets', 'explorer-facets-start', 'explorer-facets-end'); return rows; } catch (e) { console.error("Facet summaries load error:", e); @@ -642,9 +648,12 @@ sourceCounts = facetsByType.source //| code-fold: true // Get total count matching current filters totalCount = { + performance.mark('explorer-count-start'); const query = `SELECT COUNT(*) as count FROM samples WHERE ${whereClause}`; try { const rows = await runQuery(query); + performance.mark('explorer-count-end'); + performance.measure('explorer_count', 'explorer-count-start', 'explorer-count-end'); return rows[0]?.count || 0; } catch (e) { return 0; @@ -662,6 +671,7 @@ sampleData = { statusDiv.textContent = 'Loading samples...'; } + performance.mark('explorer-samples-start'); try { const query = ` SELECT @@ -681,6 +691,9 @@ sampleData = { const data = await runQuery(query); + performance.mark('explorer-samples-end'); + performance.measure('explorer_samples', 'explorer-samples-start', 'explorer-samples-end'); + if (statusDiv) { statusDiv.textContent = `Loaded ${data.length.toLocaleString()} samples`; setTimeout(() => { statusDiv.style.display = 'none'; }, 2000); @@ -906,6 +919,86 @@ Loaded: ${sampleData.length.toLocaleString()} --- +```{ojs} +//| echo: false +//| output: false + +// === Performance timing panel (opt-in: append ?perf=1 to URL) === +// Ported from progressive_globe.qmd. Reads performance.mark/measure entries +// and renders a small fixed panel. Ship with perf=1 to measure baseline, +// then v2=1 to compare. +perfPanel = { + // Depend on sampleData so the panel appears after initial data loads + if (sampleData == null) return; + + const params = new URLSearchParams(location.search); + if (params.get('perf') !== '1') return; + + await new Promise(r => setTimeout(r, 100)); + + const mark = (name) => { + const e = performance.getEntriesByName(name, 'mark').pop(); + return e ? e.startTime : null; + }; + const measure = (name) => { + const e = performance.getEntriesByName(name, 'measure').pop(); + return e ? e.duration : null; + }; + + const paintEntries = performance.getEntriesByType('paint'); + const fcp = paintEntries.find(e => e.name === 'first-contentful-paint')?.startTime; + const fp = paintEntries.find(e => e.name === 'first-paint')?.startTime; + + const rows = [ + ['first-paint (browser)', fp], + ['first-contentful-paint', fcp], + ['DuckDB init + views', measure('explorer_db')], + ['facet summaries query', measure('explorer_facets')], + ['count query', measure('explorer_count')], + ['sample data query', measure('explorer_samples')], + ['nav → DuckDB ready', mark('explorer-db-end')], + ['nav → facets ready', mark('explorer-facets-end')], + ['nav → count ready', mark('explorer-count-end')], + ['nav → samples ready', mark('explorer-samples-end')], + ].filter(([, v]) => v != null); + + console.table(Object.fromEntries(rows.map(([k, v]) => [k, `${v.toFixed(0)} ms`]))); + + const fmt = (ms) => ms == null ? '—' : ms >= 1000 ? `${(ms/1000).toFixed(2)} s` : `${ms.toFixed(0)} ms`; + // Remove any prior panel (page re-renders on filter change) + const prior = document.getElementById('perfPanel'); + if (prior) prior.remove(); + + const version = params.get('v') === '2' ? 'v2' : 'v1'; + const panel = document.createElement('div'); + panel.id = 'perfPanel'; + panel.style.cssText = ` + position: fixed; bottom: 12px; right: 12px; z-index: 9999; + background: rgba(0,0,0,0.82); color: #e8f5e9; padding: 10px 12px; + border-radius: 6px; font: 11px/1.4 ui-monospace, SFMono-Regular, monospace; + max-width: 340px; box-shadow: 0 2px 12px rgba(0,0,0,0.3); + `; + panel.innerHTML = ` +
+ ⏱ Explorer perf (${version}) + +
+ + ${rows.map(([label, v]) => ` + + + `).join('')} +
${label}${fmt(v)}
+ `; + document.body.appendChild(panel); + panel.querySelector('#perfClose').onclick = () => panel.remove(); + + return "shown"; +} +``` + +--- +