Skip to content

feat(dag-viewer): explain analytical plan cost and benefit - #339

Merged
zzylol merged 18 commits into
mainfrom
feat/dag-viewer-cost-annotations-top
Sep 4, 2026
Merged

zzylol merged 18 commits into
mainfrom
feat/dag-viewer-cost-annotations-top

Conversation

@zzylol

@zzylol zzylol commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Why

The DAG viewer must make an analytical planner decision auditable. A workload-total box alone does not show the selected replacement region, resource inputs, provenance, or whether the post-ASAP plan is actually cheaper.

What

  • Exports structured baseline, selected, and benefit annotations only for fully evidenced analytical winners.
  • Shows CPU operations, peak-memory bytes, source-read bytes, calibrated totals, absolute savings, and percentage savings on the selected replacement region and workload summary.
  • Removes the structural-node-count producer and RelativeStructuralUnits; the viewer never estimates cost from graph shape.
  • Keeps edge annotations reserved for evidence attributable to a real physical edge rather than inventing path costs from logical DAG sharing.
  • Adds current screenshots of the rendered result.

Evidence and provenance

Each target carries its exact QueryExpr and ComparisonScope. Each rewrite or summary alternative carries candidate-local query-node evidence; summary alternatives also carry their complete physical DAG. The exporter invokes PhysicalPlanCostModel and emits only a complete cheaper winner.

model_version identifies the analytical formulas plus calibration. evidence_version separately identifies the immutable catalog/runtime generation. Both are exported and displayed. Blank evidence versions, non-finite values/inputs, incomplete or unused evidence, duplicate bindings, stale scopes, invalid candidate DAGs, mixed workload evidence generations, and baseline/selected evidence mismatches fail closed as unavailable.

Candidate matching includes the complete exported plan and accuracy guarantee. Integer/graph identity is exact; only derived floating-point guarantee constants receive a one-ULP JSON round-trip tolerance.

Reproducible fixture

The checked-in example compares 100 evaluations of an exact raw aggregation over 100 million 64-byte rows and 100,000 groups with one incrementally built CMS. Its visible inputs reproduce:

  • pre-ASAP: 104.0032 cost units
  • post-ASAP: 1.14001088 cost units
  • benefit: 102.86318912 cost units, or 98.9%

Screenshots

Analytical inputs and provenance on the selected summary region

Analytical cost inputs

Positive post-ASAP savings and query shape

Positive savings example

Verification

  • cargo test -p asap-types --lib — 140 tests
  • cargo test -p asap-aware-mapping --lib
  • cargo test -p asap-devtools --bin dag_export — 18 tests
  • python3 -m unittest discover -s tools/dag-viewer -p test_render.py — 21 tests
  • cargo clippy -p asap-types -p asap-aware-mapping -p asap-devtools --all-targets -- -D warnings
  • cargo fmt --all -- --check

Stack: based on #338. This is the viewer/export layer above the analytical and streaming cost stack.

@zzylol
zzylol force-pushed the feat/cost-window-framework-selection branch from 2845b70 to 64f0e9a Compare September 3, 2026 16:45
@zzylol
zzylol force-pushed the feat/dag-viewer-cost-annotations-top branch from 03f6b6c to 952590d Compare September 3, 2026 16:45
@zzylol
zzylol force-pushed the feat/cost-window-framework-selection branch from 64f0e9a to 156ee2e Compare September 3, 2026 16:50
@zzylol
zzylol force-pushed the feat/dag-viewer-cost-annotations-top branch from 952590d to 16baf2a Compare September 3, 2026 16:50
@zzylol
zzylol force-pushed the feat/cost-window-framework-selection branch from 156ee2e to cae4c8c Compare September 3, 2026 16:57
@zzylol
zzylol force-pushed the feat/dag-viewer-cost-annotations-top branch from 16baf2a to dcb1089 Compare September 3, 2026 16:57
@zzylol
zzylol force-pushed the feat/cost-window-framework-selection branch from cae4c8c to 739753e Compare September 3, 2026 17:05
@zzylol
zzylol force-pushed the feat/dag-viewer-cost-annotations-top branch from dcb1089 to 09e8948 Compare September 3, 2026 17:05
@zzylol
zzylol force-pushed the feat/cost-window-framework-selection branch from 739753e to 000fe98 Compare September 4, 2026 01:04
@zzylol
zzylol force-pushed the feat/dag-viewer-cost-annotations-top branch 2 times, most recently from 1b6c018 to 870bc2a Compare September 4, 2026 01:26
@zzylol
zzylol force-pushed the feat/dag-viewer-cost-annotations-top branch 3 times, most recently from 8eef70b to 451376f Compare September 4, 2026 01:35
@zzylol
zzylol force-pushed the feat/cost-window-framework-selection branch from 000fe98 to cb1e2e4 Compare September 4, 2026 02:26
@zzylol
zzylol force-pushed the feat/dag-viewer-cost-annotations-top branch from 451376f to 6c8191c Compare September 4, 2026 02:29
@zzylol
zzylol force-pushed the feat/cost-window-framework-selection branch from cb1e2e4 to 334ac1b Compare September 4, 2026 03:13
@zzylol
zzylol force-pushed the feat/dag-viewer-cost-annotations-top branch from 6c8191c to 7a2cc2f Compare September 4, 2026 03:17
@zzylol
zzylol force-pushed the feat/cost-window-framework-selection branch from 45ea342 to 1d19224 Compare September 4, 2026 03:33
@zzylol
zzylol force-pushed the feat/dag-viewer-cost-annotations-top branch 2 times, most recently from e34c1ca to 87a5ec5 Compare September 4, 2026 03:37
@zzylol
zzylol force-pushed the feat/cost-window-framework-selection branch from 1d19224 to 48c01f7 Compare September 4, 2026 04:02
zzylol and others added 7 commits September 3, 2026 22:02
Adds a structured, optional CostAnnotation schema (crates/types/src/cost.rs)
and wires it through dag_export's JSON output and the tools/dag-viewer
sidebar/on-graph UI, per issue #286.

Rust:
- `asap_types::cost`: `CostAnnotation` (value/unit/source/baseline/delta/
  benefit_ratio/model_version/benchmark_id/inputs), `CostUnit`
  (CostUnitsPerSecond / CostUnits / RelativeStructuralUnits), `CostSource`
  (Modeled/Measured/Unavailable), `BaselineRef`, `CostInput`,
  `total_cost(rate, horizon, one_shot)`, `sum_workload_costs` (dedups by an
  explicit key, rejects unit-mismatched aggregation), and
  `WorkloadCostSummary`/`workload_cost_summary`.
- `DagGraph` gains `edge_annotations: Vec<EdgeCostAnnotation>`, populated by
  `deduplicate_pointer_shared_nodes` for every edge running into a genuine
  DAG merge point (never a guessed multi-hop path cost).
- `DagDecision` and `TargetReplacement` gain `baseline_cost`/`selected_cost`/
  `benefit` alongside their existing bare `cost: f64` (unchanged, for
  backward compat). `NamedGraph`/`WorkloadGraph` gain `workload_cost:
  Option<WorkloadCostSummary>`.
- crates/devtools/src/bin/dag_export.rs populates all of the above from
  today's `asap_aware_mapping::cost_model` output (`estimate_cost`,
  `default_cse_recompute_cost`), deduplicating workload totals by
  `decision.id`.

Every value dag_export produces today is honestly unit-tagged
`RelativeStructuralUnits` (the same structural-size proxy the cost model
already uses for ranking), not `CostUnitsPerSecond`: the cost model has no
update_rate/evaluation_rate/query_interval recurrence inputs yet (#287's
job). The annotation plumbing accepts a real rate unchanged once #287 lands
those inputs. Nothing is ever fabricated: a value the cost model can't
estimate is `CostSource::Unavailable` (`value: None`), never `0`.

JS/viewer:
- viewer.js renders concise on-graph `▼NN%`/`▲NN%` badges on a costed
  post-ASAP node's label, full baseline/selected/benefit/provenance blocks
  in the sidebar (node click, edge click, and a workload-scope cost summary
  for the current single- or multi-query selection), all sourced only from
  explicit JSON fields (decision.id dedup, `EdgeCostAnnotation`,
  `workload_cost`) — no client-side cost estimation.
- index.html: cost UI CSS (light/dark aware, via existing --var tokens).
- tools/dag-viewer/dag.example.json regenerated via generate-sample.sh
  (real lowering -> ASAP-aware mapping -> post-ASAP -> dag_export
  pipeline), not hand-patched.
- README.md documents the new JSON contract fields.

Tests: `cargo test --workspace` (all green) and
`python3 -m unittest discover -s tools/dag-viewer -p test_render.py`
(18/18) both pass.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fixes 5 confirmed bugs from PR review plus 3 lower-priority follow-ups.

Confirmed bugs:

1. viewer.js: `loadFiles()`/`loadWorkload()` dropped `workload_cost` from
   the object pushed onto `queries` — the single-query "Workload cost"
   panel (renderScopeSummary's `selected[0].workload_cost` read) was
   silently `undefined` on every interactive load path (drag-and-drop,
   file picker, and the planner/embedded path), even though the exported
   JSON carried the data. Both loaders now forward `workload_cost`.

2. viewer.js: `computeSelectionWorkloadCost` deduped decisions across a
   multi-query selection by bare `decision.id`, which is only unique
   within one `dag_export` process invocation, not across independently
   loaded files — a real collision (two files reusing the same small
   integer id) would silently drop one file's cost from the aggregate.
   Added a `sourceBatch` id assigned once per loaded document/file and
   changed the dedup key to `${sourceBatch}:${decision.id}`.

3. dag_export.rs: `shared_node_edge_annotations` counted edge occurrences
   (`Vec`) rather than distinct consuming nodes as `consumer_count` — a
   `Join` whose left and right operands are the same `Rc` (post
   pointer-dedup) inflated `consumer_count` to 2 for one real downstream
   consumer, halving the reported per-edge cost and producing two
   colliding `(from, to)` `EdgeCostAnnotation` entries (which
   `edgeCostByPair` in viewer.js then silently collapsed via Map
   overwrite). Switched to a `HashSet` per child so a single parent
   referencing the same shared child twice counts as one consumer. Added
   a regression test plus updated the existing edge-annotation test to use
   two genuinely distinct parents.

4. cost.rs: `total_cost()` validated `horizon` but not
   `recurring_cost_rate`/`one_shot_cost` themselves — a non-finite rate
   (e.g. a stray NaN from a future #287 caller) silently produced
   `Some(NaN)` instead of `None`, violating the module's own "never
   fabricate, never a poisoned total" rule. Both inputs are now validated
   finite before use; added regression tests.

5. dag_export.rs: `NamedGraph.workload_cost`'s doc claimed cross-query
   dedup via `workload_node_id`, but the actual producer
   (`decision_cost_entries`) only dedups within one query by
   `decision.id` — a reader trusting the doc and summing
   `NamedGraph.workload_cost` across queries would double-count a target
   shared between them. Corrected the doc to state the per-query-only
   scope and point cross-query readers at `WorkloadGraph.workload_cost`
   instead (the implementation was already correct; only the doc was
   wrong).

Lower-priority follow-ups also addressed:

- dag_export.rs: the legacy scalar `cost: f64` on `DagDecision`/
  `TargetReplacement` is now derived from `selected_cost.value` at both
  call sites instead of being set independently from `winner.cost` a
  second time, closing the "kept in sync by convention only" gap the
  review flagged.
- dag_export.rs: `default_cse_recompute_cost` is now memoized once per
  winner (`per_consumer_recompute_costs`, built right after `winners`)
  instead of being recomputed on every `winner_cost_annotations` call —
  a winner's target can be reached from more than one node position
  (internal sharing within a query, or the same CSE-shared target across
  several queries), so this avoided redundant subtree walks.
- dag_export.rs (types crate): `shared_node_edge_annotations`'s
  `parents_of` map is now built inline inside
  `deduplicate_pointer_shared_nodes`'s existing per-node loop (which
  already visits every remapped child edge once while assigning final
  ids) instead of a second full pass over the deduplicated node list.

Not fixed (noted only): `computeSelectionWorkloadCost` in viewer.js still
hand-reimplements `cost.rs`'s `sum_workload_costs`/`workload_cost_summary`
dedup-and-sum algorithm in JS, with no shared source of truth — there's no
JS/Rust code-sharing mechanism in this tool today, so keeping the two
algorithms in sync remains a manual/review responsibility. Flagged as a
follow-up in the PR description.

Testing: `cargo build --workspace`, `cargo test --workspace` (all green,
no regressions), `cargo clippy --workspace --all-targets -- -D warnings`
(clean), `cargo fmt --all -- --check` (clean, after running `cargo fmt
--all` once for pre-existing drift), and
`python3 -m unittest discover -s tools/dag-viewer` (18/18). Regenerated
`dag.example.json` via generate-sample.sh — byte-identical, since the
sample workload doesn't happen to exercise the same-parent-twice edge
case fixed in item 3. `node --check` remains unavailable in this sandbox
(no Node.js installed); verified the viewer.js changes by careful manual
review plus the Python test suite, which inlines and structurally checks
viewer.js.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@zzylol
zzylol force-pushed the feat/dag-viewer-cost-annotations-top branch from 87a5ec5 to c6a5a71 Compare September 4, 2026 04:02
@zzylol zzylol changed the title feat(dag-viewer): explain physical plan cost and benefit feat(dag-viewer): explain analytical plan cost and benefit Sep 4, 2026
@zzylol
zzylol changed the base branch from feat/cost-window-framework-selection to main September 4, 2026 11:29
@zzylol
zzylol merged commit 93eab71 into main Sep 4, 2026
3 checks passed
@zzylol
zzylol deleted the feat/dag-viewer-cost-annotations-top branch September 7, 2026 15:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant