Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/rust.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ jobs:
python3 -m unittest -v test_viewer test_server

- name: Run the DAG viewer's Node tests
run: node --test tools/dag-viewer/stages.test.mjs
run: node --test tools/dag-viewer/stages.test.mjs tools/dag-viewer/enumeration.test.mjs

# A PromQL-only runtime builds asap-planner without DataFusion or MetricsQL.
- name: Check and test asap-planner with the PromQL frontend alone
Expand Down
34 changes: 31 additions & 3 deletions tools/dag-viewer/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,28 @@ To run it, see [RUNNING.md](RUNNING.md).
for the newest), a `TimeRange` of one pane and the pane's build with its
window (`window t−40 s … t−20 s`), and clicking a pane folds them back.
This is display only: the document keeps the chain.
- **Enumerated DAGs**, between the Stage 0 and Stage 1 lanes, shown only
when the document has `memo.enumeration`: the complete candidate DAGs the
memo stands for. The header counts them after each memo step (`Stage 0: 1 ·
Pass 1: 49 · Pass 2: ≥ 1000 · concrete states: ≥ 1000`; `≥` and the limit when counting stopped), says how
many of the enumerated step's DAGs the document lists, and, when they were
priced, how many are cheaper than the selected plan. Each row names the
groups whose candidate differs from the selected plan's choice
(`G7 → #2 CountSketch+heap (pass1)`: group, candidate index, its label and
origin when the step's memo lists the candidate; a group a choice does not
name takes candidate 0). The selected choice is over the last memo step;
when an earlier step was enumerated, the header says so and only each
row's own groups are compared. A row also shows `budget share < 1` when
a chosen candidate shares its budget, and, when priced, its cost and
validity (`✗ invalid` with the cause). A row cheaper than the selected
plan is red; the selected choice says `✓ selected`. Clicking a row draws
its DAG in the Stage 1 lane; the document holds every listed choice's
composed DAG, so the page composes nothing. Such a DAG has no Stage 2
plan, so the Stage 2 lane is cleared. Its roots are its root groups, in
increasing group id; the lane draws each as the root of the queries in its
group's `roots_of`, or labels it `root group #n` when that cannot be
matched (the enumerated step's memo is not in the document). When the enumerated step is earlier than the last
one, no row is marked selected.
- **Highlighting**: the **highlight query** menu dims, in every lane, what
that query's root does not reach. Clicking a node dims everything but
what it reads and what reads it, directly or through other nodes; clicking
Expand Down Expand Up @@ -340,15 +362,21 @@ are ignored, `stage_pipeline`'s default.
The viewer checks the document before showing it: one root per query, node,
edge and root references, `scalar_roots` positions, `from_logical`, the Stage 3 ids, costs and
`per_node` keys, the data workload's shape, and each materialized view's fields
and `stage2_node_id`. Documents written before these sections are still
shown. If a check fails, it shows the problems instead.
and `stage2_node_id`, and, when present, `memo.enumeration`: per-step
counts, the listed choices' ids, group → candidate maps, DAGs (one root per
root group) and the optional pricing fields. Documents written before these
sections are still shown. If a check fails, it shows the problems instead.

## Files

- `index.html`, `app.js`: the page.
- `stages.js`: document checks, the candidate list, lane elements and node
labels, pane-chain expansion, the queries reaching each node and the
highlighted neighborhoods; no DOM. `stages.test.mjs`: its Node tests.
- `enumeration.js`: the Enumerated DAGs panel and the checks of
`memo.enumeration`. `enumeration.test.mjs`: its Node tests, over
`test-fixtures/memo-enumeration-small.json` (a hand-written priced memo
for Example 1).
- `node-style.js`: the operator-kind table (`KIND_CATEGORY_JSON`, checked
against the IR by `crates/devtools/tests/viewer_contract.rs`) and the
node groups.
Expand Down Expand Up @@ -399,7 +427,7 @@ python3 -m unittest test_viewer test_server
cytoscape (headless), from the repository root:

```bash
node --test tools/dag-viewer/stages.test.mjs
node --test tools/dag-viewer/stages.test.mjs tools/dag-viewer/enumeration.test.mjs
```

The JavaScript runs in V8 through `py_mini_racer` (`pip install
Expand Down
29 changes: 24 additions & 5 deletions tools/dag-viewer/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,11 @@
// The query whose path the lanes highlight ('' for none).
let highlightedQuery = '';

function render(i, dag, physical, costPerNode) {
// `ids` names the queries of `dag.roots` in order (default: the workload's).
function render(i, dag, physical, costPerNode, ids = queryIds) {
if (cys[i]) cys[i].destroy();
drawn[i] = { dag, physical, costPerNode };
const elements = stageLaneElements(dag, { physical, costPerNode, queryIds, expandedChains: [...expandedOf(dag)] })
drawn[i] = { dag, physical, costPerNode, ids };
const elements = stageLaneElements(dag, { physical, costPerNode, queryIds: ids, expandedChains: [...expandedOf(dag)] })
.map((el) => ({ data: el.data, classes: [el.classes, el.data.rootFor && el.data.rootFor.length ? 'root' : ''].join(' ').trim() }));
laneElements[i] = elements;
const cy = cytoscape({
Expand All @@ -72,7 +73,7 @@
dim(i, queryKeep(i));
}

function redraw(i) { const d = drawn[i]; render(i, d.dag, d.physical, d.costPerNode); }
function redraw(i) { const d = drawn[i]; render(i, d.dag, d.physical, d.costPerNode, d.ids); }

// A PaneChain expands into its panes; a pane collapses its chain; any
// other node is shown and its upstream and downstream highlighted.
Expand Down Expand Up @@ -179,6 +180,7 @@
// Choose Stage 1 candidate `id`: show its physical plan when it has one;
// a pruned or uncomposed candidate has none, so Stage 2 is cleared.
function selectLogical(id) {
pressEnumeratedRow(null);
const physical = doc.stage2_physical_asap_optimization.candidates.find((p) => p.from_logical === id);
if (physical) { selectPhysical(physical.id); return; }
showLogical(id);
Expand All @@ -205,7 +207,23 @@
}
}

// Draw enumerated DAG `id` of `memo.enumeration` (enumeration.js) in
// lane 1; the document has no physical plan for it.
function showEnumerated(id, dag, description, perQueryDag) {
current = { logical: null, physical: null };
$('pick1').value = '';
$('pick2').value = '';
// Its roots are its root groups; drawn per query when they map to queries.
if (perQueryDag) render(1, perQueryDag, false, null);
else render(1, dag, false, null, dag.roots.map((_, k) => `root group #${k + 1}`));
$('lane1-meta').innerHTML = `enumerated DAG <b>${esc(id)}</b> · ${esc(description)} · ${dag.nodes.length} nodes` +
(perQueryDag ? '' : ' · roots not matched to queries');
clearLane(2, 'no physical plan for an enumerated DAG');
renderRanking();
}

function selectPhysical(id) {
pressEnumeratedRow(null);
current.physical = id;
$('pick2').value = id;
const c = physicalById.get(id);
Expand All @@ -230,7 +248,7 @@

// Show one stage document; `source` names it, `story` explains it.
function showDocument(data, source, story) {
const errors = validateStagePipeline(data);
const errors = validateStagePipeline(data).concat(validateMemoEnumeration(data));
if (errors.length) throw new Error(errors.slice(0, 3).join('; '));
doc = data;
current = { logical: null, physical: null };
Expand Down Expand Up @@ -284,6 +302,7 @@
}
renderMaterializedViews();
renderMemoSearch();
renderEnumeration(doc, showEnumerated);
}

const table = (head, body) => `<table><thead><tr>${head.map((h) => `<th>${esc(h)}</th>`).join('')}</tr></thead><tbody>${body}</tbody></table>`;
Expand Down
Loading
Loading