Skip to content

Instantly share code, notes, and snippets.

@domoritz
Last active September 11, 2026 02:57
Show Gist options
  • Select an option

  • Save domoritz/0a5e6f7b8cccf8a9f545da5bec469e09 to your computer and use it in GitHub Desktop.

Select an option

Save domoritz/0a5e6f7b8cccf8a9f545da5bec469e09 to your computer and use it in GitHub Desktop.
Mosaic query scheduling timelines: before #475, main, and PR #1208, computed from a model driven by measured DuckDB costs
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Query scheduling: main vs this branch</title>
<style>
:root {
color-scheme: light;
--surface: #fcfcfb; --text: #0b0b0b; --text-2: #52514e; --muted: #9a9891;
--grid: #e6e5e1; --exec: #2a78d6; --select: #eb6834;
--old: #9a9891; --current: #4a3aa7; --new: #1baf7a; --idea: #eda100;
}
@media (prefers-color-scheme: dark) {
:root {
color-scheme: dark;
--surface: #1a1a19; --text: #ffffff; --text-2: #c3c2b7; --muted: #8a897f;
--grid: #33332f; --exec: #3987e5; --select: #d95926;
--old: #8a897f; --current: #9085e9; --new: #199e70; --idea: #c98500;
}
}
body {
margin: 0; padding: 32px 24px 64px; background: var(--surface); color: var(--text);
font: 15px/1.5 -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, sans-serif;
}
main { max-width: 1040px; margin: 0 auto; }
h1 { font-size: 24px; margin: 0 0 8px; }
h2 { font-size: 18px; margin: 44px 0 4px; }
p { color: var(--text-2); margin: 4px 0 12px; max-width: 78ch; }
.controls { position: sticky; top: 0; z-index: 1; display: flex; flex-wrap: wrap; gap: 8px 28px; margin: 14px 0 4px; padding: 10px 14px; border: 1px solid var(--grid); border-radius: 6px; background: var(--surface); box-shadow: 0 2px 6px rgba(0, 0, 0, .06); }
.controls label { display: inline-flex; align-items: center; gap: 8px; cursor: pointer; }
.controls select { font: inherit; }
.measured { border-collapse: collapse; font-size: 13px; color: var(--text-2); margin: 4px 0 8px; }
.measured th, .measured td { text-align: left; padding: 2px 18px 2px 0; }
.measured th { font-weight: 600; color: var(--text); }
.stages, .legend { display: flex; flex-wrap: wrap; gap: 10px 24px; margin: 12px 0 0; font-size: 13px; color: var(--text-2); }
.stages span, .legend span { display: inline-flex; align-items: center; gap: 8px; }
.stages i { display: inline-block; width: 6px; height: 18px; border-radius: 3px; }
.legend svg { width: 26px; height: 14px; }
.panel { position: relative; margin: 18px 0 0; padding-left: 18px; }
.panel::before { content: ''; position: absolute; left: 0; top: 0; bottom: 0; width: 6px; border-radius: 3px; background: var(--stage); }
.panel.stage-old { --stage: var(--old); }
.panel.stage-current { --stage: var(--current); }
.panel.stage-new { --stage: var(--new); }
.panel-title { font-size: 12px; font-weight: 600; letter-spacing: .04em; text-transform: uppercase; color: var(--text-2); display: flex; align-items: center; gap: 10px; }
.tag { display: inline-block; padding: 1px 8px; border-radius: 10px; font-size: 11px; color: var(--surface); background: var(--stage); }
svg.timeline { display: block; width: 100%; height: auto; font: 12px -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, sans-serif; }
.client { font-weight: 600; fill: var(--text); }
.label { fill: var(--text-2); }
.tick { fill: var(--text-2); font-size: 11px; }
.grid { stroke: var(--grid); }
.queued { fill: none; stroke: var(--muted); stroke-dasharray: 3 3; }
.waiting { fill: none; stroke: var(--muted); stroke-width: 1.5; stroke-dasharray: 1 3; }
.transfer { fill: var(--muted); opacity: .4; }
.exec { fill: var(--exec); }
.select { fill: var(--select); }
.held { fill: url(#hatch); stroke: var(--muted); stroke-width: 0.5; }
.marker { fill: var(--text); }
.sent { fill: var(--surface); stroke: var(--text); stroke-width: 1.5; }
.event { fill: var(--text-2); }
.cross { fill: none; stroke: var(--text-2); stroke-width: 1.5; }
.dep { fill: none; stroke: var(--text-2); stroke-width: 1.2; }
.dep-label { fill: var(--text-2); font-size: 11px; }
.finish { stroke: var(--text); stroke-width: 1; stroke-dasharray: 2 3; }
.finish-label { fill: var(--text); font-weight: 600; }
ul { color: var(--text-2); max-width: 78ch; }
li { margin: 4px 0; }
</style>
</head>
<body>
<main>
<h1>Query scheduling: main vs this branch</h1>
<p>
Each row is one query issued by one client. Time runs left to right. A query
waits in the coordinator's queue, leaves for the server, spends half a round
trip in transfer, may wait at a busy server, is processed,
spends half a round trip coming back, may sit finished but undelivered, and
is finally delivered to its client. Arrows show what a query waited for.
Every timeline is computed from a small model of the coordinator and the
database, driven by the settings below, so every control changes all of
them.
</p>
<div class="controls">
<label>Data <select id="dataset">
<option value="231k">flights, 231k rows</option>
<option value="10M">flights, 10M rows</option>
</select></label>
<label>Network <select id="network">
<option value="localhost">localhost, measured</option>
<option value="lan">office network, assumed 4 ms round trip</option>
<option value="remote">remote server, assumed 40 ms round trip</option>
<option value="mobile">mobile, Chrome's Fast 4G throttle, 170 ms round trip</option>
</select></label>
<label><input type="checkbox" id="parallelCreates"> Database runs creates in parallel</label>
<label><input type="checkbox" id="parallelSelects"> Database runs selects in parallel</label>
</div>
<p>
Query costs are measured, not made up. They come from the Go DuckDB server on
this laptop, in-memory database, medians of 5 to 9 runs sent with curl:
</p>
<table class="measured">
<tr><th></th><th>231k rows</th><th>10M rows</th></tr>
<tr><td>build a pre-aggregate table for a histogram</td><td>5.7 ms</td><td>87.7 ms</td></tr>
<tr><td>select over the pre-aggregate table</td><td>2.3 ms</td><td>2.3 ms</td></tr>
<tr><td>raw brush query over the base table</td><td>5.0 ms</td><td>52.3 ms</td></tr>
<tr><td>round trip for SELECT 1, HTTP / websocket</td><td colspan="2">1.0 ms / 0.6 ms</td></tr>
</table>
<p>
The other network options are assumptions, since only localhost can be
measured here. The two database boxes should match the setup you have in
mind. A single DuckDB connection runs one statement at a time; only
several connections run statements concurrently, and only some setups
open several:
</p>
<table class="measured">
<tr><th>Setup</th><th>Parallel creates</th><th>Parallel selects</th></tr>
<tr><td>one connection, any build</td><td>no</td><td>no</td></tr>
<tr><td>native DuckDB, several connections</td><td>yes, different tables</td><td>yes</td></tr>
<tr><td>DuckDB-WASM</td><td>no</td><td>no</td></tr>
<tr><td>Go or Rust server over REST</td><td>yes, up to the pool size</td><td>yes, up to the pool size</td></tr>
<tr><td>Python server</td><td>no</td><td>no</td></tr>
<tr><td>Node data server (mosaic-duckdb), one connection</td><td>no</td><td>no</td></tr>
<tr><td>Node connector, in-process, one connection</td><td>no</td><td>no</td></tr>
<tr><td>any server over the socket connector</td><td>no, per connection</td><td>no, per connection</td></tr>
</table>
<p>
Two creates of the same table, or of the same schema, conflict even on a
pool, so the coordinator serializes those. One query already uses all
cores, so parallel creates help latency-bound work, not CPU-bound work.
Both boxes off is the safe default.
</p>
<div class="stages">
<span><i style="background: var(--old)"></i><b>Old</b>: before #475, one request at a time</span>
<span><i style="background: var(--current)"></i><b>Current</b>: main, #475</span>
<span><i style="background: var(--new)"></i><b>New</b>: this branch</span>
</div>
<div class="legend" id="legend"></div>
<div id="scenarios"></div>
<h2>Takeaways</h2>
<ul>
<li><b>Before #475</b> the coordinator sent one request at a time, so nothing ever overlapped, not even the transfer legs.</li>
<li><b>Main</b> sends reads in parallel, serializes every exec behind every other request, and delivers all results in global request order.</li>
<li><b>This branch</b> reads the tables a request touches from its query and holds it only while an earlier request writes one of them. Raw SQL strings stay a full barrier. Each client receives its own results in request order, and clients never wait on each other.</li>
<li>Both keep a client's results in order, so a chart can never regress to an older state.</li>
<li>With a serial database, the gains come from keeping the server's queue full and from overlapping transfer with processing. With a parallel database, independent creates and selects also run at the same time.</li>
<li>On the socket connector the server runs one statement at a time, and since #1209 the connector sends requests without waiting for responses. The timelines here apply there too with both database boxes off: creates queue at the server instead of each paying a round trip.</li>
<li>While brushing, main dispatches one round per selection at a time. This branch lets each chart keep its own update in flight and always queries the newest brush state when a slot frees. A larger limit is an option, not the default.</li>
</ul>
</main>
<script>
const RES = 10; // model time unit: 0.1 ms
let T = 5; // one transfer leg, in model units, set from the network profile
const measured = {
datasets: {
'231k': { create: 5.7, preagg: 2.3, raw: 5.0 },
'10M': { create: 87.7, preagg: 2.3, raw: 52.3 }
},
roundTrip: { localhost: 1.0, lan: 4, remote: 40, mobile: 170 }
};
const ms = v => Math.round(v * RES);
const fmtLabel = v => +(v / RES).toFixed(1);
// ---- model ---------------------------------------------------------------
function tables(...names) { return new Set(names); }
function intersects(a, b) {
if (!a) return !b || b.size > 0;
if (!b) return a.size > 0;
for (const t of a) if (b.has(t)) return true;
return false;
}
function union(a, b) {
if (!a || !b) return null;
return new Set([...a, ...b]);
}
/**
* Simulate one panel. Returns rows with the times of each transition.
* policy: 'serial' (before #475) | 'main' | 'branch'
* dispatcher (brushing only): 'round' | 'window'
*/
function simulate(spec, db) {
const { policy, requests = [], events = [], clients = [], dispatcher, window: win = 1 } = spec;
const rows = [];
const replaced = [];
const byId = new Map();
let seq = 0;
function add(r, t) {
const o = {
...r, index: seq++, requestedAt: t, sentAt: null, wireAt: null, arriveAt: null,
startAt: null, doneAt: null, returnedAt: null, deliveredAt: null, waits: []
};
rows.push(o);
byId.set(o.id, o);
return o;
}
for (const r of requests) if (typeof r.at === 'number') add(r, r.at);
const deferred = requests.filter(r => typeof r.at === 'object');
let now = 0;
const inflight = r => r.sentAt != null && (r.returnedAt == null || r.returnedAt > now);
const lastReturned = (pred = () => true) =>
rows.filter(r => r.returnedAt != null && r.returnedAt <= now && pred(r)).sort((a, b) => b.returnedAt - a.returnedAt)[0];
// brushing state
const round = { active: new Set(), pending: null };
const perClient = new Map(clients.map(c => [c.name, { inflight: 0, dirty: null }]));
const brushRequest = (c, value, t) =>
add({ id: `${c.name}@${value}`, client: c.name, label: `brush at ${fmtLabel(value)} ms`, type: 'select', cost: c.cost, reads: c.reads, writes: tables() }, t);
function dispatchRound(value, t, on) {
for (const c of clients) {
const r = brushRequest(c, value, t);
if (on) r.waits.push({ kind: 'round', on, anchor: 'deliveredAt', target: 'sentAt' });
round.active.add(r.id);
}
}
function issueWindowed(c, value, t, on) {
const st = perClient.get(c.name);
const r = brushRequest(c, value, t);
if (on) r.waits.push({ kind: 'slot', on, anchor: 'deliveredAt', target: 'sentAt' });
st.inflight += 1;
}
function onEvent(t) {
if (dispatcher === 'round') {
if (round.active.size === 0) {
dispatchRound(t, t, null);
} else {
if (round.pending != null) for (const c of clients) replaced.push({ client: c.name, label: `brush at ${fmtLabel(round.pending)} ms`, request: round.pending, replaced: t, value: round.pending });
round.pending = t;
}
} else {
for (const c of clients) {
const st = perClient.get(c.name);
if (st.inflight < win) {
issueWindowed(c, t, t, null);
} else {
if (st.dirty != null) replaced.push({ client: c.name, label: `brush at ${fmtLabel(st.dirty)} ms`, request: st.dirty, replaced: t, value: st.dirty });
st.dirty = t;
}
}
}
}
function onDelivered(r, t) {
if (!dispatcher) return;
if (dispatcher === 'round') {
round.active.delete(r.id);
if (round.active.size === 0 && round.pending != null) {
const value = round.pending;
round.pending = null;
dispatchRound(value, t, r.id);
}
} else {
const st = perClient.get(r.client);
st.inflight -= 1;
if (st.dirty != null) {
const value = st.dirty;
st.dirty = null;
issueWindowed(clients.find(c => c.name === r.client), value, t, r.id);
}
}
}
function deliver(t) {
const ready = r => r.returnedAt != null && r.returnedAt <= t && r.deliveredAt == null;
const settle = (r, prev, kind) => {
r.deliveredAt = t;
if (prev && r.returnedAt < t) r.waits.push({ kind, on: prev.id, anchor: 'deliveredAt', target: 'deliveredAt' });
onDelivered(r, t);
};
if (policy === 'serial') {
for (const r of rows) if (ready(r)) settle(r, null);
} else if (policy === 'main') {
const order = rows.filter(r => r.sentAt != null).sort((a, b) => a.sentAt - b.sentAt || a.index - b.index);
let prev = null;
for (const r of order) {
if (r.deliveredAt != null) { prev = r; continue; }
if (ready(r)) { settle(r, prev, 'fifo'); prev = r; } else break;
}
} else {
for (const r of rows) if (r.type === 'exec' && ready(r)) settle(r, null);
const byClient = new Map();
for (const r of rows) if (r.type !== 'exec' && r.sentAt != null) {
if (!byClient.has(r.client)) byClient.set(r.client, []);
byClient.get(r.client).push(r);
}
for (const list of byClient.values()) {
list.sort((a, b) => a.requestedAt - b.requestedAt || a.index - b.index);
let prev = null;
for (const r of list) {
if (r.deliveredAt != null) { prev = r; continue; }
if (ready(r)) { settle(r, prev, 'client'); prev = r; } else break;
}
}
}
}
function send(t) {
const queued = rows.filter(r => r.requestedAt <= t && r.sentAt == null).sort((a, b) => a.requestedAt - b.requestedAt || a.index - b.index);
const mark = (r, kind, on) => {
r.sentAt = t;
if (on && r.requestedAt < t) r.waits.push({ kind, on: on.id, anchor: 'returnedAt', target: 'sentAt' });
};
if (policy === 'serial') {
if (queued.length && !rows.some(inflight)) mark(queued[0], 'serial', lastReturned());
} else if (policy === 'main') {
if (rows.some(r => r.type === 'exec' && inflight(r))) return;
for (const r of queued) {
mark(r, 'barrier', lastReturned(x => x.type === 'exec'));
if (r.type === 'exec') break;
}
} else {
let writes = tables();
for (const r of rows) if (inflight(r)) writes = union(writes, r.writes);
for (const r of queued) {
if (!intersects(r.reads, writes) && !intersects(r.writes, writes)) {
const conflict = lastReturned(x => intersects(r.reads, x.writes) || intersects(r.writes, x.writes));
mark(r, conflict?.writes === null ? 'unknown' : 'writes', conflict);
}
writes = union(writes, r.writes);
}
}
}
function wire() {
for (const r of rows) {
if (r.sentAt != null && r.wireAt == null) { r.wireAt = r.sentAt; r.arriveAt = r.sentAt + T; }
}
}
function start(t) {
const single = !db.parallelCreates && !db.parallelSelects;
const lane = r => r.type === 'exec' ? 'create' : 'select';
const parallel = l => l === 'create' ? db.parallelCreates : db.parallelSelects;
const running = l => rows.some(r => r.startAt != null && r.doneAt == null && (single || lane(r) === l));
const finished = l => rows.filter(r => r.doneAt === t && (single || lane(r) === l)).sort((a, b) => b.index - a.index)[0];
const waiting = rows.filter(r => r.arriveAt != null && r.arriveAt <= t && r.startAt == null).sort((a, b) => a.arriveAt - b.arriveAt || a.sentAt - b.sentAt || a.index - b.index);
const blockedLane = new Set();
for (const r of waiting) {
const l = lane(r);
if (single) {
if (running('any')) break;
} else if (!parallel(l)) {
if (blockedLane.has(l) || running(l)) { blockedLane.add(l); continue; }
}
r.startAt = t;
const on = finished(l);
if (r.arriveAt < t && on) r.waits.push({ kind: 'server', on: on.id, anchor: 'doneAt', target: 'startAt' });
}
}
for (let t = 0; t <= ms(3000); ++t) {
now = t;
if (events.includes(t)) onEvent(t);
for (const r of rows) if (r.startAt != null && r.doneAt == null && r.startAt + r.cost <= t) {
r.doneAt = r.startAt + r.cost;
r.returnedAt = r.doneAt + T;
}
deliver(t);
for (const d of deferred) if (!byId.has(d.id)) {
const dep = byId.get(d.at.after);
if (dep && dep.deliveredAt != null) {
const o = add(d, t);
o.waits.push({ kind: 'after', label: d.at.label, on: dep.id, anchor: 'deliveredAt', target: 'sentAt' });
}
}
send(t);
wire(t);
start(t);
}
if (dispatcher) {
const order = new Map(clients.map((c, i) => [c.name, i]));
const all = [...rows, ...replaced].map(r => ({ ...r, value: +r.label.match(/[\d.]+/)[0] }));
all.sort((a, b) => order.get(a.client) - order.get(b.client) || a.value - b.value);
return all;
}
const clientOrder = new Map();
requests.forEach(r => { if (!clientOrder.has(r.client)) clientOrder.set(r.client, clientOrder.size); });
const specIndex = new Map(requests.map((r, i) => [r.id, i]));
return rows.sort((a, b) => clientOrder.get(a.client) - clientOrder.get(b.client) || specIndex.get(a.id) - specIndex.get(b.id));
}
// ---- scenarios -----------------------------------------------------------
let CREATE, SELECT, RAW, SCHEMA;
function applyProfile() {
const ds = measured.datasets[document.getElementById('dataset').value];
CREATE = ms(ds.create); SELECT = ms(ds.preagg); RAW = ms(ds.raw); SCHEMA = ms(1);
T = Math.round(ms(measured.roundTrip[document.getElementById('network').value]) / 2);
}
function activation(policy) {
const charts = ['B', 'C', 'D'];
const chained = policy === 'branch';
const requests = [];
if (chained) requests.push({ id: 'schema', client: 'Pre-aggregator', label: 'CREATE SCHEMA', type: 'exec', cost: SCHEMA, reads: tables(), writes: tables(), at: 0 });
for (const c of charts) requests.push({
id: `c${c}`, client: `Chart ${c}`, label: chained ? 'CREATE TABLE' : 'CREATE SCHEMA; CREATE TABLE', type: 'exec', cost: chained ? CREATE : SCHEMA + CREATE,
reads: tables('base'), writes: tables(`t${c}`), at: chained ? { after: 'schema', label: 'schema created once' } : 0
});
for (const c of charts) requests.push({
id: `s${c}`, client: `Chart ${c}`, label: 'SELECT', type: 'select', cost: SELECT, reads: tables(`t${c}`), writes: tables(),
at: { after: `c${c}`, label: 'awaits its own create' }
});
return { policy, requests };
}
function fiveRequests(policy, rawFirst = false) {
return {
policy,
requests: [
{ id: 'c1', client: 'Table t1', label: rawFirst ? 'raw SQL CREATE t1' : 'CREATE t1', type: 'exec', cost: CREATE, reads: rawFirst ? null : tables('base'), writes: rawFirst ? null : tables('t1'), at: 0 },
{ id: 's1', client: 'Table t1', label: 'SELECT t1', type: 'select', cost: SELECT, reads: tables('t1'), writes: tables(), at: 0 },
{ id: 'c2', client: 'Table t2', label: 'CREATE t2', type: 'exec', cost: CREATE, reads: tables('base'), writes: tables('t2'), at: 0 },
{ id: 's2', client: 'Table t2', label: 'SELECT t2', type: 'select', cost: SELECT, reads: tables('t2'), writes: tables(), at: 0 },
{ id: 'sb', client: 'Table base', label: 'SELECT base', type: 'select', cost: SELECT, reads: tables('base'), writes: tables(), at: 0 }
]
};
}
function slowFast(policy) {
return {
policy,
requests: [
{ id: 'b', client: 'Chart B', label: 'SELECT (raw table)', type: 'select', cost: RAW, reads: tables('tb'), writes: tables(), at: 0 },
{ id: 'c', client: 'Chart C', label: 'SELECT (pre-aggregate)', type: 'select', cost: SELECT, reads: tables('tc'), writes: tables(), at: 0 }
]
};
}
function twoPages(policy) {
return {
policy,
requests: [
{ id: 'q1', client: 'Table input', label: 'SELECT page 1', type: 'select', cost: RAW, reads: tables('t'), writes: tables(), at: 0 },
{ id: 'q2', client: 'Table input', label: 'SELECT page 2', type: 'select', cost: SELECT, reads: tables('t'), writes: tables(), at: ms(1) }
]
};
}
function brushing(policy, dispatcher, win) {
return {
policy, dispatcher, window: win,
events: [0, 30, 60, 90, 120].map(ms),
clients: [
{ name: 'Chart B', cost: RAW, reads: tables('tb') },
{ name: 'Chart C', cost: SELECT, reads: tables('tc') }
]
};
}
const scenarios = () => [
{
title: 'A brush activates over three charts',
note: 'Three charts filtered by the brush each need a pre-aggregated table and then one query on it, and each chart issues its select once its own create has returned. Before #475 the coordinator sent one request at a time. On main, each exec blocks every other request until its response is back, so every select waits for the last create. On this branch creates are scheduled like reads: they leave together after a once-per-session schema request, nothing is held behind them, and each select leaves as soon as its own create is back. On a serial database the creates still queue at the server, but no round trip is spent between them.',
panels: [
{ name: 'before #475: one request at a time', spec: activation('serial') },
{ name: 'main (#475): parallel reads, exec barrier, global result order', spec: activation('main') },
{ name: 'this branch: each select waits only for its own create', spec: activation('branch') }
]
},
{
title: 'Creates that block only the select that needs them',
note: 'Five requests are issued together with no awaits: two table creates, a select on each new table, and a select on an unrelated table. Main holds every later request until the exec in flight is back. This branch reads the table names out of each query: only the select on a table being written waits, and only for that create. A raw SQL string has no readable tables and remains a full barrier.',
panels: [
{ name: 'before #475: one request at a time', spec: fiveRequests('serial') },
{ name: 'main: every select waits for the last exec', spec: fiveRequests('main') },
{ name: 'this branch: each select waits only for the create that writes its table', spec: fiveRequests('branch') },
{ name: 'this branch, when the first create is a raw SQL string', spec: fiveRequests('branch', true) }
]
},
{
title: 'Brushing: a slow chart and a fast chart',
note: 'One brush move issues a query per chart. Chart B cannot be pre-aggregated and queries the raw table; Chart C queries its pre-aggregate table. B was requested first. On main, C\'s finished result is held until B\'s is delivered, because results leave in global request order. On this branch C updates as soon as its own result is back. With a serial database C also waits for B on the server, so the difference only appears once selects run in parallel.',
panels: [
{ name: 'before #475: one request at a time', spec: slowFast('serial') },
{ name: 'main', spec: slowFast('main') },
{ name: 'this branch', spec: slowFast('branch') }
]
},
{
title: 'One client, two queries, responses out of order',
note: 'The Table input requests page 1, then page 2, and appends each result. Page 1 costs a raw-table query and page 2 happens to be cheap. When selects run in parallel the server answers page 2 first. Main and this branch both hold page 2 until page 1 is delivered: main through the global FIFO, this branch through the per-client chain. A client never sees an older result after a newer one.',
panels: [
{ name: 'before #475: one request at a time', spec: twoPages('serial') },
{ name: 'main', spec: twoPages('main') },
{ name: 'this branch', spec: twoPages('branch') }
]
},
{
title: 'Continuous brushing over chart A, with charts B and C updating',
note: 'The brush moves every 30 ms. Each move needs one query per other chart: B queries the raw table, C its pre-aggregate table. On main the selection dispatches one round at a time and keeps only the newest waiting brush state. On this branch each chart keeps its own update in flight and queries the newest brush state when its previous result is back, so a fast chart never waits for a slow one. A limit of two helps when the server is idle while a result travels back, because it runs queries concurrently or because the round trip is long compared to a query. When queries take longer than the round trip, a queued update only delays the newest one, which is why the default is one.',
panels: [
{ name: 'main: one round at a time', spec: brushing('main', 'round') },
{ name: 'this branch: one update in flight per chart', spec: brushing('branch', 'window', 1) },
{ name: 'this branch, with the option maxPendingUpdates: 2', spec: brushing('branch', 'window', 2) }
]
}
];
// ---- rendering -----------------------------------------------------------
const NS = 'http://www.w3.org/2000/svg';
const W = 1000, LEFT = 240, RIGHT = 40, ROW = 28, BAR = 14, TOP = 16, AXIS = 34;
const waitLabels = {
serial: 'previous response must be back',
barrier: 'exec barrier',
writes: on => `writes ${[...on.writes][0]}`,
unknown: 'unknown tables: full barrier',
server: 'server: one at a time',
fifo: 'global FIFO',
client: 'per-client order',
after: (on, w) => w.label,
round: 'next round waits for the slowest chart',
slot: 'a slot frees up'
};
function el(name, attrs = {}, text) {
const node = document.createElementNS(NS, name);
for (const [k, v] of Object.entries(attrs)) node.setAttribute(k, v);
if (text != null) node.textContent = text;
return node;
}
function defs() {
const d = el('defs');
const hatch = el('pattern', { id: 'hatch', patternUnits: 'userSpaceOnUse', width: 6, height: 6, patternTransform: 'rotate(45)' });
hatch.append(el('line', { x1: 0, y1: 0, x2: 0, y2: 6, stroke: 'var(--muted)', 'stroke-width': 1.5 }));
const arrow = el('marker', { id: 'arrow', viewBox: '0 0 8 8', refX: 7, refY: 4, markerWidth: 7, markerHeight: 7, orient: 'auto-start-reverse' });
arrow.append(el('path', { d: 'M0,0 L8,4 L0,8 Z', fill: 'var(--text-2)' }));
d.append(hatch, arrow);
return d;
}
function segment(svg, cls, x1, x2, top, title, xmax) {
if (x2 == null || x1 == null) return;
x2 = Math.min(x2, xmax);
if (x2 <= x1) return;
const rect = el('rect', { class: cls, x: x1, y: top, width: x2 - x1, height: BAR, rx: 2 });
rect.append(el('title', {}, title));
svg.append(rect);
}
function renderPanel(rows, time, events) {
const plotW = W - LEFT - RIGHT;
const x = t => LEFT + t / time * plotW;
const xmax = x(time);
const fmt = t => +(t / RES).toFixed(1);
const track = events.length ? ROW : 0;
const y = i => TOP + track + i * ROW + ROW / 2;
const H = TOP + track + rows.length * ROW + AXIS;
const svg = el('svg', { class: 'timeline', viewBox: `0 0 ${W} ${H}`, role: 'img' });
svg.append(defs());
const step = [1, 2, 5, 10, 20, 50, 100, 200, 500, 1000].map(v => v * RES).find(v => time / v <= 12) ?? time;
for (let t = 0; t <= time; t += step) {
svg.append(el('line', { class: 'grid', x1: x(t), x2: x(t), y1: TOP, y2: H - AXIS + 6 }));
svg.append(el('text', { class: 'tick', x: x(t), y: H - AXIS + 20, 'text-anchor': 'middle' }, t === 0 ? '0 ms' : fmt(t)));
}
if (track) {
const ty = TOP + ROW / 2;
svg.append(el('text', { class: 'client', x: 8, y: ty + 4 }, 'Brush moves'));
for (const t of events) svg.append(el('path', { class: 'event', d: `M${x(t) - 5},${ty - 7} L${x(t) + 5},${ty - 7} L${x(t)},${ty + 3} Z` }));
svg.append(el('line', { class: 'grid', x1: 8, x2: W - RIGHT, y1: TOP + ROW, y2: TOP + ROW }));
}
let lastClient = null;
rows.forEach((r, i) => {
const cy = y(i), top = cy - BAR / 2;
if (r.client !== lastClient) {
if (i) svg.append(el('line', { class: 'grid', x1: 8, x2: W - RIGHT, y1: cy - ROW / 2, y2: cy - ROW / 2 }));
svg.append(el('text', { class: 'client', x: 8, y: cy + 4 }, r.client));
lastClient = r.client;
}
svg.append(el('text', { class: 'label', x: 122, y: cy + 4 }, r.label));
const who = `${r.label} for ${r.client}`;
if (r.replaced != null) {
segment(svg, 'queued', x(r.request), x(r.replaced), top, `${who}: waited from ${fmt(r.request)} ms, replaced by a newer brush state at ${fmt(r.replaced)} ms`, xmax);
const rx = x(r.replaced);
if (r.replaced <= time) svg.append(el('path', { class: 'cross', d: `M${rx - 5},${cy - 5} L${rx + 5},${cy + 5} M${rx - 5},${cy + 5} L${rx + 5},${cy - 5}` }));
return;
}
const at = k => r[k] == null ? null : x(r[k]);
const open = k => r[k] == null ? xmax : x(r[k]);
segment(svg, 'queued', at('requestedAt'), open('sentAt'), top, `${who}: in the coordinator queue from ${fmt(r.requestedAt)} ms`, xmax);
segment(svg, 'transfer', at('wireAt'), at('arriveAt'), top, `${who}: request in transfer ${fmt(r.wireAt)} to ${fmt(r.arriveAt)} ms`, xmax);
segment(svg, 'waiting', at('arriveAt'), open('startAt'), top, `${who}: waiting at the server from ${fmt(r.arriveAt)} ms`, xmax);
segment(svg, r.type, at('startAt'), at('doneAt'), top, `${who}: processing ${fmt(r.startAt)} to ${fmt(r.doneAt)} ms`, xmax);
segment(svg, 'transfer', at('doneAt'), at('returnedAt'), top, `${who}: response in transfer ${fmt(r.doneAt)} to ${fmt(r.returnedAt)} ms`, xmax);
if (r.deliveredAt != null) segment(svg, 'held', at('returnedAt') + 1, at('deliveredAt'), top, `${who}: result back at ${fmt(r.returnedAt)} ms, delivered at ${fmt(r.deliveredAt)} ms`, xmax);
if (r.sentAt != null && r.sentAt <= time) svg.append(el('circle', { class: 'sent', cx: at('sentAt'), cy, r: 4 }));
if (r.deliveredAt != null && r.deliveredAt <= time) {
const mx = at('deliveredAt');
svg.append(el('path', { class: 'marker', d: `M${mx},${cy - 6} L${mx + 6},${cy} L${mx},${cy + 6} L${mx - 6},${cy} Z` }));
}
});
const index = new Map(rows.map((r, i) => [r.id, i]));
const labeled = new Set();
const placed = [];
rows.forEach((r, i) => {
for (const w of r.waits ?? []) {
if (!w.on || !index.has(w.on)) continue;
const on = rows[index.get(w.on)];
if (on[w.anchor] == null || r[w.target] == null || on[w.anchor] > r[w.target] || r[w.target] > time) continue;
const x1 = x(on[w.anchor]), y1 = y(index.get(w.on));
const x2 = x(r[w.target]), y2 = y(i);
const dir = y2 > y1 ? 1 : -1;
const path = x2 === x1
? `M${x1},${y1 + dir * (BAR / 2 + 1)} V${y2 - dir * (BAR / 2 + 2)}`
: `M${x1},${y1 + dir * (BAR / 2 + 1)} V${y2} H${x2 - 3}`;
svg.append(el('path', { class: 'dep', d: path, 'marker-end': 'url(#arrow)' }));
const key = w.kind === 'after' ? w.label : w.kind;
if (labeled.has(key)) continue;
labeled.add(key);
const text = typeof waitLabels[w.kind] === 'function' ? waitLabels[w.kind](on, w) : waitLabels[w.kind];
const vertical = x2 === x1;
const width = text.length * 6;
const fitsLeft = x1 - 6 - width > LEFT;
const left = vertical && fitsLeft;
const lx = left ? x1 - 6 : x1 + 6;
let ly = vertical ? y2 - dir * (BAR / 2 + 3) + (dir < 0 ? 12 : 0) : y2 - 6;
const box = () => ({ x1: left ? lx - width : lx, x2: left ? lx : lx + width, y1: ly - 11, y2: ly });
const overlaps = b => placed.some(p => b.x1 < p.x2 && b.x2 > p.x1 && b.y1 < p.y2 && b.y2 > p.y1);
for (let tries = 0; tries < 6 && overlaps(box()); ++tries) ly += (tries % 2 ? -1 : 1) * 13 * (tries + 1);
placed.push(box());
svg.append(el('text', { class: 'dep-label', x: lx, y: ly, 'text-anchor': left ? 'end' : 'start' }, text));
}
});
if (events.length) {
const byClient = new Map();
for (const r of rows) if (r.deliveredAt != null) {
if (!byClient.has(r.client)) byClient.set(r.client, []);
byClient.get(r.client).push(r.deliveredAt);
}
const gaps = [];
for (const times of byClient.values()) for (let i = 1; i < times.length; ++i) gaps.push(times[i] - times[i - 1]);
const text = gaps.length ? `updates every ~${fmt(gaps.reduce((a, b) => a + b) / gaps.length)} ms` : 'one update';
svg.append(el('text', { class: 'finish-label', x: W - RIGHT, y: TOP + 6, 'text-anchor': 'end' }, text));
} else {
const finish = Math.max(...rows.map(r => r.deliveredAt ?? 0));
svg.append(el('line', { class: 'finish', x1: x(finish), x2: x(finish), y1: TOP - 4, y2: H - AXIS + 6 }));
const flip = x(finish) + 150 > W;
svg.append(el('text', { class: 'finish-label', x: x(finish) + (flip ? -6 : 6), y: TOP + 6, 'text-anchor': flip ? 'end' : 'start' }, `all delivered at ${fmt(finish)} ms`));
}
return svg;
}
function stageOf(name) {
if (name.startsWith('before')) return { key: 'old', label: 'old' };
if (name.startsWith('this branch')) return { key: 'new', label: 'new' };
return { key: 'current', label: 'current' };
}
function render() {
const db = {
parallelCreates: document.getElementById('parallelCreates').checked,
parallelSelects: document.getElementById('parallelSelects').checked
};
const root = document.getElementById('scenarios');
root.replaceChildren();
applyProfile();
scenarios().forEach((s, i) => {
const results = s.panels.map(p => ({ ...p, rows: simulate(p.spec, db) }));
const brushing = results.some(p => p.spec.events);
const times = results.flatMap(p => p.rows.map(r => r.deliveredAt ?? r.replaced ?? 0));
const lastEvent = brushing ? Math.max(...results[0].spec.events) : 0;
const horizon = brushing ? Math.min(Math.max(...times), lastEvent + 3 * (RAW + 2 * T)) : Math.max(...times);
const unit = [1, 2, 5, 10, 20, 50, 100, 200, 500].map(v => v * RES).find(v => horizon / v <= 12) ?? horizon;
const time = Math.ceil((horizon + unit / 4) / unit) * unit;
const h = document.createElement('h2');
h.textContent = `${i + 1}. ${s.title}`;
const p = document.createElement('p');
p.textContent = s.note;
root.append(h, p);
for (const panel of results) {
const rows = brushing ? panel.rows.filter(r => (r.requestedAt ?? r.request) <= horizon) : panel.rows;
const stage = stageOf(panel.name);
const section = document.createElement('section');
section.className = `panel stage-${stage.key}`;
const t = document.createElement('div');
t.className = 'panel-title';
const tag = document.createElement('span');
tag.className = 'tag';
tag.textContent = stage.label;
t.append(tag, document.createTextNode(panel.name));
section.append(t, renderPanel(rows, time, panel.spec.events ?? []));
root.append(section);
}
});
}
const legendItems = [
['waiting in the coordinator queue', '<rect class="queued" x="1" y="1" width="24" height="12" rx="2"/>'],
['leaves the coordinator', '<circle class="sent" cx="13" cy="7" r="4"/>'],
['waiting at a busy server', '<rect class="waiting" x="1" y="1" width="24" height="12" rx="2"/>'],
['request or response in transfer', '<rect class="transfer" x="0" y="0" width="26" height="14" rx="2"/>'],
['exec processing on the server', '<rect class="exec" x="0" y="0" width="26" height="14" rx="2"/>'],
['select processing on the server', '<rect class="select" x="0" y="0" width="26" height="14" rx="2"/>'],
['result back, delivery held', '<rect class="held" x="0" y="0" width="26" height="14" rx="2"/>'],
['delivered to the client', '<path class="marker" d="M13,1 L19,7 L13,13 L7,7 Z"/>'],
['waited for', '<path class="dep" d="M2,7 H22" marker-end="url(#arrow)"/>'],
['brush move', '<path class="event" d="M8,2 L18,2 L13,12 Z"/>'],
['replaced by a newer brush state', '<path class="cross" d="M8,2 L18,12 M8,12 L18,2"/>']
];
const legend = document.getElementById('legend');
legend.innerHTML = legendItems.map(([text, svg]) => `<span><svg viewBox="0 0 26 14">${svg}</svg>${text}</span>`).join('');
legend.firstElementChild.querySelector('svg').prepend(defs());
const preset = new URLSearchParams(location.search).get('parallel') ?? '';
document.getElementById('parallelCreates').checked = preset.includes('creates');
document.getElementById('parallelSelects').checked = preset.includes('selects');
const q = new URLSearchParams(location.search);
if (q.get('data')) document.getElementById('dataset').value = q.get('data');
if (q.get('network')) document.getElementById('network').value = q.get('network');
for (const id of ['dataset', 'network', 'parallelCreates', 'parallelSelects']) document.getElementById(id).addEventListener('change', render);
render();
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment