@@ -296,6 +296,7 @@ html`<div style="margin-bottom: 16px;">
296296//| code-fold: true
297297// Initialize DuckDB-WASM
298298db = {
299+ performance.mark('explorer-db-start');
299300 const bundle = await duckdbModule.selectBundle(duckdbModule.getJsDelivrBundles());
300301
301302 const worker_url = URL.createObjectURL(
@@ -315,6 +316,8 @@ db = {
315316 await conn.query(`CREATE VIEW sample_facets AS SELECT * FROM read_parquet('${sample_facets_url}')`);
316317 await conn.close();
317318
319+ performance.mark('explorer-db-end');
320+ performance.measure('explorer_db', 'explorer-db-start', 'explorer-db-end');
318321 return instance;
319322}
320323
@@ -347,8 +350,11 @@ mutable facetSummariesError = null
347350// Tier 1: Load pre-computed facet summaries (2KB, instant)
348351facetSummaries = {
349352 mutable facetSummariesError = null;
353+ performance.mark('explorer-facets-start');
350354 try {
351355 const rows = await runQuery(`SELECT * FROM read_parquet('${facet_summaries_url}')`);
356+ performance.mark('explorer-facets-end');
357+ performance.measure('explorer_facets', 'explorer-facets-start', 'explorer-facets-end');
352358 return rows;
353359 } catch (e) {
354360 console.error("Facet summaries load error:", e);
@@ -642,9 +648,12 @@ sourceCounts = facetsByType.source
642648//| code-fold: true
643649// Get total count matching current filters
644650totalCount = {
651+ performance.mark('explorer-count-start');
645652 const query = `SELECT COUNT(*) as count FROM samples WHERE ${whereClause}`;
646653 try {
647654 const rows = await runQuery(query);
655+ performance.mark('explorer-count-end');
656+ performance.measure('explorer_count', 'explorer-count-start', 'explorer-count-end');
648657 return rows[0]?.count || 0;
649658 } catch (e) {
650659 return 0;
@@ -662,6 +671,7 @@ sampleData = {
662671 statusDiv.textContent = 'Loading samples...';
663672 }
664673
674+ performance.mark('explorer-samples-start');
665675 try {
666676 const query = `
667677 SELECT
@@ -681,6 +691,9 @@ sampleData = {
681691
682692 const data = await runQuery(query);
683693
694+ performance.mark('explorer-samples-end');
695+ performance.measure('explorer_samples', 'explorer-samples-start', 'explorer-samples-end');
696+
684697 if (statusDiv) {
685698 statusDiv.textContent = `Loaded ${data.length.toLocaleString()} samples`;
686699 setTimeout(() => { statusDiv.style.display = 'none'; }, 2000);
@@ -906,6 +919,86 @@ Loaded: ${sampleData.length.toLocaleString()}
906919
907920---
908921
922+ ``` {ojs}
923+ //| echo: false
924+ //| output: false
925+
926+ // === Performance timing panel (opt-in: append ?perf=1 to URL) ===
927+ // Ported from progressive_globe.qmd. Reads performance.mark/measure entries
928+ // and renders a small fixed panel. Ship with perf=1 to measure baseline,
929+ // then v2=1 to compare.
930+ perfPanel = {
931+ // Depend on sampleData so the panel appears after initial data loads
932+ if (sampleData == null) return;
933+
934+ const params = new URLSearchParams(location.search);
935+ if (params.get('perf') !== '1') return;
936+
937+ await new Promise(r => setTimeout(r, 100));
938+
939+ const mark = (name) => {
940+ const e = performance.getEntriesByName(name, 'mark').pop();
941+ return e ? e.startTime : null;
942+ };
943+ const measure = (name) => {
944+ const e = performance.getEntriesByName(name, 'measure').pop();
945+ return e ? e.duration : null;
946+ };
947+
948+ const paintEntries = performance.getEntriesByType('paint');
949+ const fcp = paintEntries.find(e => e.name === 'first-contentful-paint')?.startTime;
950+ const fp = paintEntries.find(e => e.name === 'first-paint')?.startTime;
951+
952+ const rows = [
953+ ['first-paint (browser)', fp],
954+ ['first-contentful-paint', fcp],
955+ ['DuckDB init + views', measure('explorer_db')],
956+ ['facet summaries query', measure('explorer_facets')],
957+ ['count query', measure('explorer_count')],
958+ ['sample data query', measure('explorer_samples')],
959+ ['nav → DuckDB ready', mark('explorer-db-end')],
960+ ['nav → facets ready', mark('explorer-facets-end')],
961+ ['nav → count ready', mark('explorer-count-end')],
962+ ['nav → samples ready', mark('explorer-samples-end')],
963+ ].filter(([, v]) => v != null);
964+
965+ console.table(Object.fromEntries(rows.map(([k, v]) => [k, `${v.toFixed(0)} ms`])));
966+
967+ const fmt = (ms) => ms == null ? '—' : ms >= 1000 ? `${(ms/1000).toFixed(2)} s` : `${ms.toFixed(0)} ms`;
968+ // Remove any prior panel (page re-renders on filter change)
969+ const prior = document.getElementById('perfPanel');
970+ if (prior) prior.remove();
971+
972+ const version = params.get('v') === '2' ? 'v2' : 'v1';
973+ const panel = document.createElement('div');
974+ panel.id = 'perfPanel';
975+ panel.style.cssText = `
976+ position: fixed; bottom: 12px; right: 12px; z-index: 9999;
977+ background: rgba(0,0,0,0.82); color: #e8f5e9; padding: 10px 12px;
978+ border-radius: 6px; font: 11px/1.4 ui-monospace, SFMono-Regular, monospace;
979+ max-width: 340px; box-shadow: 0 2px 12px rgba(0,0,0,0.3);
980+ `;
981+ panel.innerHTML = `
982+ <div style="font-weight:600;color:#fff;margin-bottom:6px;display:flex;justify-content:space-between;align-items:center;">
983+ <span>⏱ Explorer perf (${version})</span>
984+ <button id="perfClose" style="background:none;border:none;color:#aaa;cursor:pointer;font-size:14px;padding:0 4px;">×</button>
985+ </div>
986+ <table style="border-collapse:collapse;width:100%;">
987+ ${rows.map(([label, v]) => `
988+ <tr><td style="padding:1px 8px 1px 0;color:#bbb;">${label}</td>
989+ <td style="padding:1px 0;text-align:right;color:#a5d6a7;font-variant-numeric:tabular-nums;">${fmt(v)}</td></tr>
990+ `).join('')}
991+ </table>
992+ `;
993+ document.body.appendChild(panel);
994+ panel.querySelector('#perfClose').onclick = () => panel.remove();
995+
996+ return "shown";
997+ }
998+ ```
999+
1000+ ---
1001+
9091002<style >
9101003.card {
9111004 background : #fafafa ;
0 commit comments