Skip to content

feat(devtools): asap_planner text output is terse by default; --verbose prints the details (#1261) - #1276

Open
milindsrivastava1997 wants to merge 3 commits into
refactor/1207-toolsfrom
feat/1261-readable-stage-output
Open

milindsrivastava1997 wants to merge 3 commits into
refactor/1207-toolsfrom
feat/1261-readable-stage-output

Conversation

@milindsrivastava1997

@milindsrivastava1997 milindsrivastava1997 commented Nov 10, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

asap_planner's text output is terse by default. It prints the memo structures with a legend for each stage, and the details move behind --verbose (renamed from --printing-full-candidates). On quantile-grouped.json --run-until stage3 the output drops from 1270 lines to 499. Closes #1261.

Review guide

Everything is in crates/devtools/src/planning_stages_text.rs. Suggested order:

  1. section and the *_LEGEND constants: the legends, one label per line.
  2. write_memo: the used by / reached by / grid column, and the properties line now printed only with --verbose.
  3. write_stored_logical_edges: Pass 2's default edge lines.
  4. write_physical_by_logical_candidate and write_physical_pruned: Stage 2's default layout and the pruned counts.

Behavior: before → after

Stage 2, candidate C4 (quantile-grouped.json):

before:
    C4 = G1[0] (QueryTime, None)  TimeRange 300s
        input 0 <- G0:
            C0 = G0[0] (QueryTime, None)                           ✓   selected
            C1 = G0[0] (QueryTime, OneEvaluation(Memory))          ✓
            C2 = G0[0] (IngestionTime, None)                       ✗ α^phys = 0: consumer (QueryTime, None) cannot read producer (IngestionTime, None)
            C3 = G0[0] (IngestionTime, AcrossEvaluations(Memory))  ✓
after:
G1[0]  TimeRange 300s <- G0  exact
    C4  (QueryTime, None)  ★
        input 0 <- G0: reads C0..C1, C3 · uses C0

Memo groups, before and after:

before:
G4:   SummaryAgg Kll   <- G1
    ts=data.ts:Unknown state=computed:NonNegative region=data.region:Unknown $promql_series_identity=... · reads only time series
after:
G4:   SummaryAgg Kll   <- G1   used by G5 · reached by q1 · every 10000 ms

Interfaces

- asap_planner ... [--printing-full-candidates]
+ asap_planner ... [--verbose]
- pub fn planning_stages_text(stages, evidence, plan_cost, printing_full_candidates: bool) -> String
+ pub fn planning_stages_text(stages, evidence, plan_cost, verbose: bool) -> String

Algorithm changes

None. Planning is unchanged; only the text rendering changes.

Decisions and open questions

Verification

  • New test the_default_text_is_the_terse_form_and_verbose_adds_the_details. It checks that:
    • the default output has no properties line, per-producer rows or α^ reasons, and --verbose has all of them;
    • every section starts with a legend;
    • every group shows reached by;
    • Stage 2 lists each physical candidate exactly once, in id order;
    • ★ marks exactly the plan's candidates.
  • Existing tests updated:
    • the_text_memo_after_pass_2_prints_every_group_with_its_properties and the_text_folds_candidates_that_differ_only_in_theta: flag renamed to --verbose.
    • stage3_selects_candidates_stage2_lists: matches the new Stage 2 line shape.
  • Ran cargo test -p asap-devtools --test devtools asap_planner::the_ asap_planner_acceptance, the devtools lib tests, and cargo clippy -p asap-devtools --tests.
  • Planner snapshots (crates/devtools/tests/corpus_snapshots/) are unchanged; they are JSON.

Follow-ups

🤖 Generated with Claude Code

https://claude.ai/code/session_014qhwq6eHKg6PF5nMxwGhWG

…se prints the details (#1261)

- Rename --printing-full-candidates to --verbose.
- Each stage starts with a legend, one label per line.
- Memo groups show `used by` / `reached by` (from `parents` / `queries`)
  and, from Pass 2, their evaluation grid; the column-properties line is
  verbose-only.
- Pass 2 prints the stored `LogicalASAPMemoCandidates.edges` as producer
  ids by α^log; the recomputed per-producer reasons are verbose-only.
- Stage 2 prints each logical candidate once with its physical candidates
  under it, `★` on the plan's, and per input the readable producers and
  the one the plan uses.
- Stage 3's pruned list is counts per cause, as Pass 2's and Stage 2's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@milindsrivastava1997 milindsrivastava1997 self-assigned this Nov 10, 2026

@milindsrivastava1997 milindsrivastava1997 left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code review: 10 findings. The first three (prune reason, ✗ list, verbose legends) change what users see.

}
}
if line.ends_with(':') {
line += " no surviving producer";

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Misleading prune reason. For a candidate Pass 2 pruned, LogicalASAPMemoCandidates::edges keeps no edges (edges exist only between surviving candidates), so this prints input i <- G<k>: no surviving producer even when G has many surviving producers that meet the requirement. The real cause is that the consumer itself was pruned. This label is also not in PASS2_LEGEND.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f0d4764. Pass 2 keeps no edges into a pruned candidate, so a candidate marked pruned now lists no input lines; its pruned flag already gives the reason. The legend says so ("a pruned candidate lists no inputs: Pass 2 keeps no edges into it"). The terse-text test checks that no input line follows a pruned candidate.

"new = added by Pass 2",
"pruned = eliminated by Pass 2",
"requires = the accuracy required per input",
"input i <- G<k>: ✓ / ✗ = the producers in G<k> that do / do not meet the input's requirement (--verbose: every producer, with why ✗)",

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Legend overstates the terse ✗ list. The default path omits producers Pass 2 pruned and producers of another θ, and the old "N pruned producers" count was removed. E.g. G7 with 280 candidates, 270 pruned for BelowRequiredAccuracy, shows ✓ G7[..] with no hint that 270 producers are missing. Either restore a count or narrow the legend.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f0d4764, with a count rather than only a narrower legend. Each line now ends with · N other(s): the candidates of the input group that have no stored edge, because they were pruned or have another θ. For example: input 0 <- G2: ✓ G2[0, 2] · 1 other. The legend now says the ✓/✗ entries are the edges Pass 2 kept, and that --verbose lists each other producer with the reason.

section(
&mut out,
"Stage 1 Pass 2 output = Stage 2 input: LogicalASAPMemoCandidates",
&[MEMO_LEGEND, CANDIDATE_LEGEND, PASS2_LEGEND].concat(),

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Legends don't depend on --verbose. Verbose output prints C3 = G1[0] ... ✓ selected rows and no ★, reads or uses, but gets the same STAGE2_LEGEND/PASS2_LEGEND, which describe ★/reads/uses and not selected, ✓, ✗ or C<n> = G<g>[<i>]. Same applies to the Stage 2 legend at L146.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f0d4764. With --verbose, Pass 2 gets PASS2_VERBOSE_EDGES_LEGEND: one row per candidate of the input group, ✓ / ✗ with the reason / no edge. Stage 2 gets STAGE2_VERBOSE_LEGEND: C<n> = G<g>[<i>] (timing, lifetime), ✓ / ✗ with the reason, and selected. The terse legends are unchanged.

for (index, candidate) in physical.physical_candidates.iter().enumerate() {
let id = PhysicalCandidateId(index);
let logical = memo.candidate(candidate.logical_candidate_id);
if previous != Some(candidate.logical_candidate_id) {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Assumes contiguous physical candidates per logical candidate. A header is printed whenever logical_candidate_id changes. Stage 2's catalog builds them contiguously today, but PhysicalASAPMemoCandidates::new neither enforces nor documents this (stage3 cost.rs/select.rs fixtures construct it directly). Interleaving would print the same G1[0] header twice, breaking the "each logical candidate once" promise in the user docs. Group explicitly, or document/assert the invariant.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f0d4764. write_physical_by_logical_candidate now builds a BTreeMap<CandidateForMemoGroupId, Vec<usize>> first, so each logical candidate prints once whatever order Stage 2 lists them in. The test now checks that the set of listed C<n> is exactly 0..count, instead of relying on id order.

for (input, group) in groups.into_iter().enumerate() {
let (mut satisfied, mut failed) = (Vec::new(), Vec::new());
// ponytail: scans every edge per input; index by (consumer, input) if large memos print slowly.
for edge in logical.edges.iter().filter(|edge| {

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

O(candidates × edges) on the default path. This scans all of logical.edges for every (consumer, input). With ~280 θ variants per group the edge list is already large. The edges are grouped by (c, i), so a partition_point/chunk_by lookup or one precomputed map would make it linear. The ponytail: note defers this, but terse is now the default.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f0d4764. write_memo buckets logical.edges once into a BTreeMap<(consumer, input), Vec<&LogicalEdge>>, which write_stored_logical_edges looks up. That makes it linear, and the ponytail: note is gone.

if !verbose {
write_physical_by_logical_candidate(out, physical, plan, &selected);
}
let edges_into = edges_per_input(physical);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unused work in terse mode. edges_per_input(physical) is built here (and accepted_metrics(&logical.memo) at L423) even when verbose is false, then discarded. Compute both only inside the verbose branch.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f0d4764. edges_per_input is built only inside write_physical_edges (the verbose path), and accepted_metrics only when verbose. Likewise, the stored-edge map is built only when not verbose.

.physical_candidates
.iter()
.enumerate()
.filter(|_| verbose)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: .enumerate().filter(|_| verbose) after the terse writer call is a hidden if/else. An explicit if verbose { ... } else { write_physical_by_logical_candidate(..) } reads more clearly.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f0d4764. write_physical is now an explicit if verbose { write_physical_edges(..) } else { write_physical_by_logical_candidate(..) }, followed by the shared pruned list.

context.push(match &logical.evaluation_grid_of_group[id] {
Some(grid) if grid.phase_ms == 0 => format!("every {} ms", grid.period_ms),
Some(grid) => format!("every {} ms at {}", grid.period_ms, grid.phase_ms),
None => "no known grid".to_owned(),

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: duplicates grid_text (L310) with different wording ("no known grid" vs "no known evaluation grid", "every N ms" vs "evaluated every N ms at 0"). The same group's grid is phrased differently depending on --verbose. Reuse grid_text, perhaps with a short variant.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f0d4764. There's one grid_text, used by both modes: every 10000 ms, every 10000 ms at 5000 when the phase is nonzero, or no known evaluation grid.

);
match (logical, &accepted, evidence) {
(Some(logical), Some(accepted), Some(evidence)) if verbose => {
write_logical_edges(out, &logical.memo, accepted, candidate_id, evidence, None)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: dead parameter. This is now the only call site of write_logical_edges and passes folded = None, so the pruned-producer/other-θ counting branches and the "and N pruned producers" output (L639–660) are unreachable, and the doc comment at L614 describes behavior that can't happen. Remove the parameter and its branches.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f0d4764. Removed the folded parameter, the pruned/other-θ counting branches, and the "and N pruned producers" output. The doc comment now describes only the recomputed rows.

Comment thread crates/devtools/tests/asap_planner.rs Outdated
.collect();
let plan_nodes = sections[5]
.lines()
.filter(|line| line.starts_with('n'))

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fragile count. Counting Stage 3 lines starting with n and asserting it equals the number of ★ candidates assumes every plan node has selected_physical_candidate set and no two nodes share a candidate. Either case fails the test although ★ marks exactly the plan's candidates. Compare against the set of C<n> named in the plan lines instead.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed in f0d4764. The test now collects the set of C<n> named in the plan lines (n<k>: C<c> = ...) and asserts it equals the set of ★ candidates.

- A pruned Pass 2 candidate lists no inputs (Pass 2 keeps no edges into
  it) instead of "no surviving producer".
- Each terse input line counts the input group's candidates with no edge
  (`· N others`); the legend says which producers are listed.
- Pass 2's and Stage 2's legends describe the rows `--verbose` prints.
- Stage 2 groups physical candidates by logical candidate explicitly.
- Stored edges are bucketed by (consumer, input) once; the recomputed
  edges' inputs and Stage 2's per-input edges are built only for
  `--verbose`.
- One grid wording in both modes; `write_logical_edges` loses its dead
  `folded` parameter.
- The terse-text test compares `★` with the set of candidates the plan's
  nodes run, and checks that pruned candidates list no inputs.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qhwq6eHKg6PF5nMxwGhWG
…and pruned filters), inputs/outputs layout, Plan Viewer rename (#1262) (#1277)

* feat(dag-viewer): drill into large memo groups by any candidate dimension (#1262)

A memo group with more than 12 candidates used to collapse to one count per
origin and expand to every candidate, so a 280-candidate CmsWithHeap group
(depth × width × heap_size) was one box or 280 boxes. It now splits by its
first varying dimension (origin, sketch family, each θ field, window width),
and clicking a count drills into it until at most 12 remain. The group label
shows the path, clicking the group goes back a level, and the group's details
offer the other varying dimensions to split by. The dimensions come from the
candidate's existing `parameters` and `operator`; the document is unchanged.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(dag-viewer): drilled-in candidates show the dimensions that tell them apart (#1262)

Drilled to depth=5 › heap_size=10, a CmsWithHeap group's 7 candidates all read
"θ CmsWithHeap(depth=5, heap_s…": the clipped θ hid width, the one dimension
that differs. Drilled in, a candidate now shows the dimensions still varying
among the shown ones ("θ width=28"). The split-by buttons use the page's .btn
style, so the dimension in use shows as pressed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(dag-viewer): deployment line names each summary's summary_type, not undefined (#1262)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(dag-viewer): one memo edge per group when its candidates share inputs, a how-to-read line, light theme by default with a toggle (#1262)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(dag-viewer): hide Pass 2's pruned memo candidates until asked; theme toggle top right (#1262)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(dag-viewer): README says pruned candidates are hidden until asked (#1262)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(dag-viewer): each Pass 2 candidate shows its Stage 2 physical variants as a timing × lifetime grid (#1262)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qhwq4QkRexgK7yPwbksve

* feat(dag-viewer): planner inputs and output as two sections, settings panel with the cost model, no Stage 3 sidebar, short step names, Stage 2 variants toggle (#1262)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qhwq4QkRexgK7yPwbksve

* feat(dag-viewer): zoom in, out and fit buttons on every graph; the memo's how-to-read as a short list (#1262)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qhwq4QkRexgK7yPwbksve

* docs: rename the Stage Viewer to the Plan Viewer (#1262)

The page, its server, code comments and every doc that names it; the user
guide moves to docs/user_guide_docs/plan-viewer.md. The document format
(asap-stage-pipeline/v2) keeps its name. asap_planner.rs's header comment
still says Stage Viewer: changing it runs the cargo hooks, where two
devtools tests already fail on this branch's base.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qhwq4QkRexgK7yPwbksve

* docs: the rest of the Plan Viewer rename: links, page, server and comments (#1262)

bce9c4017 added docs/user_guide_docs/plan-viewer.md; this removes
stage-viewer.md and renames the viewer everywhere else.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qhwq4QkRexgK7yPwbksve

* feat(dag-viewer): selected-plan-only memo filter; the queries' DAG shown with the inputs as Input; "error metric not set" (#1262)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qhwq4QkRexgK7yPwbksve

* docs(devtools): asap_planner's header comment names the Plan Viewer (#1262)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qhwq4QkRexgK7yPwbksve

* fix(dag-viewer): the Stage 2 grid image is wide enough for its third column (#1262)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_014qhwq4QkRexgK7yPwbksve

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

2 participants