Repository navigation
feat(devtools): asap_planner text output is terse by default; --verbose prints the details (#1261) - #1276
feat(devtools): asap_planner text output is terse by default; --verbose prints the details (#1261)#1276milindsrivastava1997 wants to merge 3 commits into
Conversation
…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
left a comment
There was a problem hiding this comment.
Code review: 10 findings. The first three (prune reason, ✗ list, verbose legends) change what users see.
| } | ||
| } | ||
| if line.ends_with(':') { | ||
| line += " no surviving producer"; |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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 ✗)", |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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(), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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| { |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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); |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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(), |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
| .collect(); | ||
| let plan_nodes = sections[5] | ||
| .lines() | ||
| .filter(|line| line.starts_with('n')) |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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>
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). Onquantile-grouped.json --run-until stage3the output drops from 1270 lines to 499. Closes #1261.Review guide
Everything is in
crates/devtools/src/planning_stages_text.rs. Suggested order:sectionand the*_LEGENDconstants: the legends, one label per line.write_memo: theused by/reached by/ grid column, and the properties line now printed only with--verbose.write_stored_logical_edges: Pass 2's default edge lines.write_physical_by_logical_candidateandwrite_physical_pruned: Stage 2's default layout and the pruned counts.Behavior: before → after
Stage 2, candidate C4 (
quantile-grouped.json):Memo groups, before and after:
Interfaces
Algorithm changes
None. Planning is unchanged; only the text rendering changes.
Decisions and open questions
--verbosereplaces--printing-full-candidates, with no alias.LogicalASAPMemoCandidates.edges, as producer ids grouped by whether they meet the accuracy requirement.--verbose.used by/reached bycome fromMemoGroup.parents/queries.α^log/α^phys. Both are tracked in asap_planner and viewer: Pass 2 edges are recomputed instead of read from the stored list, and are labeled with α symbols #1275.Verification
the_default_text_is_the_terse_form_and_verbose_adds_the_details. It checks that:α^reasons, and--verbosehas all of them;reached by;★marks exactly the plan's candidates.the_text_memo_after_pass_2_prints_every_group_with_its_propertiesandthe_text_folds_candidates_that_differ_only_in_theta: flag renamed to--verbose.stage3_selects_candidates_stage2_lists: matches the new Stage 2 line shape.cargo test -p asap-devtools --test devtools asap_planner::the_ asap_planner_acceptance, the devtools lib tests, andcargo clippy -p asap-devtools --tests.crates/devtools/tests/corpus_snapshots/) are unchanged; they are JSON.Follow-ups
α^log/α^phys.ponytail:). Index them by (consumer, input) if printing gets slow on large memos.🤖 Generated with Claude Code
https://claude.ai/code/session_014qhwq6eHKg6PF5nMxwGhWG