feat: add interactive visualization command (fallow viz)#33
feat: add interactive visualization command (fallow viz)#33BartWaardenburg wants to merge 8 commits into
Conversation
🌿 FallowDead code (2 issues)
Tip Run |
There was a problem hiding this comment.
Fallow Allocations
Details
| Benchmark suite | Current: 2d7c7dc | Previous: 93867ff | Ratio |
|---|---|---|---|
Total Bytes Allocated |
3036870 bytes |
3031062 bytes |
1.00 |
Total Allocations |
11957 allocations |
11945 allocations |
1.00 |
Peak Memory |
422082 bytes |
420734 bytes |
1.00 |
Peak Allocations |
3878 allocations |
5168 allocations |
0.75 |
This comment was automatically generated by workflow using github-action-benchmark.
There was a problem hiding this comment.
Fallow Binary Size
Details
| Benchmark suite | Current: 2d7c7dc | Previous: 93867ff | Ratio |
|---|---|---|---|
Binary Size (fallow) |
122352176 bytes |
120895224 bytes |
1.01 |
This comment was automatically generated by workflow using github-action-benchmark.
There was a problem hiding this comment.
Fallow Benchmarks
Details
| Benchmark suite | Current: 2d7c7dc | Previous: 93867ff | Ratio |
|---|---|---|---|
parse_single_file |
39342 ns/iter (± 197) |
45718 ns/iter (± 1433) |
0.86 |
full_pipeline_10_files |
959723 ns/iter (± 13443) |
1134386 ns/iter (± 13178) |
0.85 |
full_pipeline_100_files |
1956124 ns/iter (± 20472) |
2508647 ns/iter (± 60862) |
0.78 |
full_pipeline_1000_files |
11430520 ns/iter (± 155832) |
15023901 ns/iter (± 247282) |
0.76 |
resolve_re_export_chains |
95989 ns/iter (± 2867) |
100273 ns/iter (± 375) |
0.96 |
cache_round_trip |
2786 ns/iter (± 14) |
1973 ns/iter (± 18) |
1.41 |
dupe_detect_2x500_identical |
161257 ns/iter (± 9645) |
205327 ns/iter (± 5285) |
0.79 |
dupe_detect_2x2000_identical |
732251 ns/iter (± 6462) |
914681 ns/iter (± 8636) |
0.80 |
dupe_detect_10x500_identical |
1513312 ns/iter (± 14922) |
1709302 ns/iter (± 35970) |
0.89 |
dupe_detect_50x200_diverse |
462024 ns/iter (± 16295) |
530777 ns/iter (± 12876) |
0.87 |
dupe_detect_100x200_mixed |
3443195 ns/iter (± 44195) |
4235683 ns/iter (± 38499) |
0.81 |
dupe_detect_2x5000_identical |
2011548 ns/iter (± 5157) |
2536583 ns/iter (± 6925) |
0.79 |
This comment was automatically generated by workflow using github-action-benchmark.
There was a problem hiding this comment.
⚠️ Performance Alert ⚠️
Possible performance regression was detected for benchmark 'Fallow Benchmarks'.
Benchmark result of this commit is worse than the previous benchmark result exceeding threshold 1.10.
| Benchmark suite | Current: 2d7c7dc | Previous: 93867ff | Ratio |
|---|---|---|---|
cache_round_trip |
2786 ns/iter (± 14) |
1973 ns/iter (± 18) |
1.41 |
This comment was automatically generated by workflow using github-action-benchmark.
2d7c7dc to
69497ae
Compare
Wire Command::Viz + dispatch + telemetry workflow + --help group; re-add AnalysisOutput.project field; switch viz.rs off deprecated analyze_with_trace to analyze_retaining_modules; fix *Finding-wrapper + is_entry_point() API drift.
… help-groups test Rebuild viz-frontend bundle with the corrected user-facing strings.
…li-crate, project tree Document the viz command + viz-frontend build, add the [Unreleased] changelog entry, and unit-test the dot/mermaid emitters and html_escape.
… whitespace Address /fallow-review SHIP-WITH-NOTES: viz now honors FALLOW_PRODUCTION and config production (not just the flag) via dispatch.production_for, matching list/watch; add README section; trim trailing blank line in summary.ts.
The node-drag handlers (graphDragStart/Drag/End) were fully implemented but never connected. Wire them through interactions.ts (mousedown/mousemove/mouseup) with a d3-zoom .filter() so a press on a node drags it while a press on empty space still pans. graphDrag now also sets node.x/y for immediate cursor tracking. Verified via agent-browser: a node follows the cursor, the rest of the view holds. Delete the genuinely-dead getGraphFilter/getClusterMode getters and the zoomToCluster no-op stub, so fallow's dead-code check on viz-frontend is clean. Rebuild the bundle.
Make the generated visualization usable with assistive tech and the keyboard. The canvas gets role="img" with a descriptive aria-label and tabindex so screen-reader users land on it and hear what it is; the view tabs, graph filters, cluster toggle, and dark-mode button expose aria-pressed state synced to their active class; the search input and legend get labels and list semantics. Motion respects the OS preference: the drag reheat of the d3-force simulation is skipped under prefers-reduced-motion (the node still tracks the cursor, its neighbors just do not spring), and a reduced-motion media block neutralizes CSS transitions. Buttons and the canvas get a focus-visible outline so keyboard focus is visible. Frontend-only; regenerates the embedded viz.js/viz.css assets.
a20c72b to
3e6c412
Compare
riker-wamf
left a comment
There was a problem hiding this comment.
Second-reviewer review — feat: add interactive visualization command (fallow viz) (@ 3e6c412e, 33 files, +4296/-14)
Reviewed statically (no Rust toolchain in my environment, so I relied on CI + reading; the viz:: unit tests are present and pass logically). The Rust integration is clean and idiomatic; the treemap path is solid. My concerns are concentrated in the graph view's interaction layer, where a chunk of advertised behaviour is wired to write-only state that nothing renders — which is exactly the class of dead code this tool exists to catch.
🔴 Requesting changes — graph interactions are advertised but not implemented
AppState carries graphFocusNode, graphFocusDepth, graphPan, graphZoom (state.ts:17‑20). They are written in several places but never read by renderGraph (it draws from gvs.transform, the d3-zoom-owned GraphViewState, not from these fields). Concretely:
- Keys
1‑3("graph focus depth") do nothing.interactions.ts:219‑228setstate.graphFocusDepthand callrenderGraph, butrenderGraph(graph.ts) never referencesgraphFocusDepth. Yet this is advertised in the canvasaria-label("use the number keys 1-3 to set graph focus depth", main.ts:163‑166) and in the README/CHANGELOG ("keyboard navigation", "drill-down"). The accessibility text describes behaviour that does not exist. - Escape "reset zoom/pan" is broken.
interactions.ts:203‑207setsstate.graphZoom = 1; state.graphPan = {0,0}and re-renders — but the real transform lives ingvs.transformand is untouched, so Escape does not actually reset the graph. To reset you need to drive the d3-zoom behaviour's transform (e.g.select(canvas).call(zoomBehavior.transform, ...)). - Clicking a graph node does nothing.
handleGraphClick(graph.ts:695) is an empty stub (_state,_hitIdxare_-prefixed to silencenoUnusedParameters), andgraphGoBack(graph.ts:697) always returnsfalse. So the graph "drill-down" the README promises isn't there, and the dblclick "clear focus" (interactions.ts:184‑189) mutatesgraphFocusNode, which again nothing reads.
Note this slips through both guards: tsc --noEmit with noUnusedLocals won't flag write-only interface fields, and fallow itself detects unused exports/deps, not unused struct fields — so dogfooding stays green while the dead state remains.
Ask: either implement focus-depth / click-to-focus / working Escape-reset, or remove the dead AppState fields + no-op handlers and correct the aria-label and the README/CHANGELOG so they only claim what ships (treemap drill-down + search + filters + dark mode + the working Escape/Backspace on the treemap). Right now a screen-reader user is told to press keys that do nothing.
🟡 Non-blocking
- Committed build artifacts can silently drift.
crates/cli/viz-assets/viz.js(179 KB minified) andviz.cssare the rolldown/cpoutputs ofviz-frontend/, embedded viainclude_str!. Nothing in CI rebuilds or verifies them, so a contributor who editsviz-frontend/src/*and forgetsnpm run buildships a stale bundle with no signal. Thecli-crate.mdnote on how to rebuild is good; consider also a CI step (npm ci && npm run build && git diff --exit-code crates/cli/viz-assets/) to enforce sync. - Theme duplicated as a literal.
main.ts:34‑55inlines the entire lightColorThemeinstead ofgetTheme(false)from colors.ts, becausestateisn't created until line 169. Side effect: the summary bar's status-value colours are frozen to the light palette and don't switch in dark mode. Createstatefirst, then passstate.themetocreateSummaryBar. - HTML output is non-deterministic. Graph initial positions use
Math.random()(graph.ts:299‑300), so eachfallow vizrun yields a different layout while DOT/Mermaid/JSON are deterministic. A seeded PRNG would make the HTML reproducible (and snapshot-testable). - Binary size +1.46 MB (120.9 → 122.4 MB per the size benchmark) from the embedded assets +
opencrate + serde monomorphization. Fine for a self-contained design, just flagging that the size-alert is real, not a warm-cache false positive — avizcargo feature could gate it if size-sensitive. tooltip.ts:79—names.map((n) => n)is a no-op; justnames.join(", ").- A11y: breadcrumb segments are clickable
<span>s (interactions.ts), not keyboard-focusable like the view/filter<button>s. Minor inconsistency. viz-frontend/package.jsondeclaresd3-transition/@types/d3-transition, which are not imported anywhere insrc/(only a transitive dep of d3-zoom). Self-suppressed by.fallowrc.jsonturning offunused-dev-dependencies, but it's a genuine unused direct dep — worth dropping.- Tests/verification: Rust unit tests cover DOT/Mermaid/escape only; there's no assertion that
write_htmlembeds the JSON safely (the</→<\/guard is untested) and no frontend tests at all. The test-plan checkboxes are all unchecked. A smoke test that actually opens the generated HTML would have caught the graph-interaction issues above.
Verdict: structurally sound and a nice feature; the Rust side and treemap are good. Blocking only on #1‑#3 — the graph view's keyboard/click model is non-functional yet advertised, including in accessibility text. Please address the feedback and reply to each point directly, then ask coworkers to re-review.
riker-wamf
left a comment
There was a problem hiding this comment.
Second WAAF reviewer — feat: add interactive visualization command (fallow viz) (@ 3e6c412e, reviewed against the thermonuclear maintainability bar)
I reviewed independently from a fresh checkout at this exact head: rebuilt the frontend from source (npm ci && npm run build) and confirmed the committed viz-assets/viz.js + viz.css reproduce byte-for-byte (git diff --stat empty), and tsc --noEmit is clean. To avoid duplication I'll keep this focused on what @riker-wamf's review does not already cover.
Concur with the blocker (independently verified, not re-litigating)
I verified @riker-wamf's #1‑#3: graphFocusNode / graphFocusDepth / graphPan / graphZoom (state.ts:17‑20) are written-only — grep across src/ shows the only references outside state.ts are the writes in interactions.ts:186,204‑205,220‑226; nothing in graph.ts ever reads them, and renderGraph draws solely from the d3-zoom-owned gvs.transform. The canvas aria-label (main.ts:162‑165) does tell screen-reader users to "use the number keys 1‑3 to set graph focus depth" — behaviour that does not exist. Under the thermonuclear bar this is a correctly-identified blocker; I support CHANGES_REQUESTED on those points and won't restate the fix.
Additional high-confidence findings (additive)
🟡 Synchronous force-layout has no scale ceiling. Initial layout runs for (let i=0;i<300;i++) sim.tick() (graph.ts:326) plus for (let i=0;i<200;i++) groupSim.tick() (graph.ts:268), fully synchronous on the main thread, with no node cap anywhere. The prefersReducedMotion guard (graph.ts:57,710) only skips drag-reheat, not this initial 300-tick block. Each is O(n+e), so a large monorepo's graph blocks the UI for a noticeable beat on open. Worth a node cap (or "graph too large — showing treemap") and/or offloading ticks. This is the one performance issue the size/alloc CI benches won't surface, since they don't exercise the emitted HTML.
🟡 renderGraph is a full O(n+e) redraw on every hover move. Hover sets gvs.hoveredNode and re-runs the whole node+link draw (graph.ts:420,466‑536). Fine for small graphs; compounds the scalability concern above on large ones.
🟡 Empty-graph produces a NaN transform. When there are zero file nodes the bbox loop never executes, so minX/minY stay Infinity and maxX/maxY stay -Infinity (graph.ts:351‑358); bboxW/bboxH and therefore fitScale/fitX/fitY (graph.ts:361‑365) become NaN, feeding a NaN initial zoom transform. A fileNodes.length === 0 early-return (or identity fallback) avoids it.
🟢 window.__FALLOW_DATA__ is consumed unvalidated (main.ts:18). Self-injected by the Rust writer so not an attack surface, but a missing/shape-mismatched payload throws an opaque error rather than a friendly message — a small guard would help when the HTML is opened standalone.
Verification notes for other reviewers (to pre-empt false positives)
Two things that look like bugs but are correct, so nobody blocks on them:
- Treemap retina scaling is not cumulative.
renderTreemapunconditionally reassignscanvas.widthevery render (treemap.ts:246), and assigningcanvas.widthresets the 2D context transform to identity (HTML spec) — soctx.scale(dpr,dpr)applies once from identity, not compounding. (The graph path reassigns width only conditionally, which is exactly why it correctly uses absolutectx.setTransform(...)instead.) - Cluster-toggle does not leak d3-zoom listeners. Re-
select(canvas).call(zoom)replaces listeners under the fixed.zoomnamespace rather than stacking them. - XSS posture is clean: no
innerHTMLanywhere insrc/; all DOM is built via theel()helper /textContent/replaceChildren, and the Rust writer escapes</→<\/plus HTML-escapes the title before embedding JSON.
Verdict: structurally sound, additive, read-only feature with a clean Rust integration and a reproducible build. Aligned with @riker-wamf — blocking only on the advertised-but-dead graph interaction state (#1‑#3); the scalability/empty-graph items above are the highest-value additions. Please reply per-point and ask coworkers to re-review once addressed.
(Posted on behalf of Data data@wearemobilefirst.com, WAAF — second of two required reviewers.)
Summary
fallow vizsubcommand that generates a self-contained HTML file with treemap and graph viewsRebased from #18.
Test plan
cargo run -- vizgenerates HTML and opens in browser🤖 Generated with Claude Code