Skip to content

feat(ash): zoom keys [/], history pan ←→, cursor crosshair, context-sensitive timeline (#773)#774

Merged
NikolayS merged 14 commits intomainfrom
feat/ash-ux-overhaul
Apr 1, 2026
Merged

feat(ash): zoom keys [/], history pan ←→, cursor crosshair, context-sensitive timeline (#773)#774
NikolayS merged 14 commits intomainfrom
feat/ash-ux-overhaul

Conversation

@NikolayS
Copy link
Copy Markdown
Owner

Closes #773

Changes

1. Zoom keys reassigned: [ / ]

  • [ = zoom out, ] = zoom in
  • Frees up / for history pan
  • Footer hint updated: [/]:zoom ←→:pan

2. History pan with /

  • pans backward in time (auto-switches Live → History mode)
  • pans forward; returns to Live mode when pan_offset reaches 0
  • pan_offset: i64 and cursor_col: Option<usize> added to AshState

3. Vertical cursor crosshair

  • When panning, a line is drawn at the cursor column (bright white, over existing bars)
  • Bottom pane shows a per-bucket infobox: timestamp, AAS, top-3 wait types with % share and color
  • Infobox replaces the drill-down table while cursor is active

4. Context-sensitive timeline

  • At WaitType level: unchanged (all types stacked)
  • At WaitEvent { selected_type } level: timeline shows only that type's data, broken down by individual wait events with deterministic colors (label_color() hash)
  • At QueryId level: timeline shows only that event's contribution, colored by query label

Files changed

  • src/ash/state.rs — pan state, key handlers, footer hints
  • src/ash/renderer.rs — cursor line, infobox, context-sensitive bar building

Testing

  • cargo build clean
  • cargo clippy --all-targets --all-features -- -D warnings — zero warnings

@NikolayS
Copy link
Copy Markdown
Owner Author

REQUEST_CHANGES

Solid feature set — zoom keys, history pan, and cursor crosshair are all the right UX moves. Three issues need fixing before merge: a timestamp display bug, a potential off-by-one in cursor positioning, and a zero-duration History window on first pan. The rest are cleanup items.


[HIGH] Timestamp in cursor infobox is relative to ash.epoch(), not Unix epoch
cursor_bucket_info computes ts as seconds into the ASH ring (relative to ash.epoch() = 2026-01-01 00:00:00 UTC). Formatting it with HH:MM:SS UTC using a standard Unix-epoch conversion will show a wildly wrong time — off by ~56 years. Fix: add ash.epoch() before formatting, or store the absolute timestamp in CursorInfo.

[HIGH] Zero-duration History window on first pan_left()
pan_left() initializes ViewMode::History { from: now, to: now } — a window of zero seconds. The timeline will show no data until a subsequent render tick recalculates bounds. Should initialize as { from: now - bucket_secs, to: now } (or the current zoom width) so the first frame is immediately meaningful.

[MEDIUM] Off-by-one risk in cursor_x_in_bar
bar_w.saturating_sub(1 + col_from_right) silently clamps to 0 when col_from_right >= bar_w, drawing the crosshair at column 0 instead of hiding it. Add an early return: if col_from_right >= bar_w { return; } before rendering the crosshair to avoid a phantom cursor at the left edge.

[MEDIUM] Code duplication: aggregate_buckets_by_event / aggregate_buckets_by_query
These two functions share ~80% identical chunking logic. Extract a private chunk_snapshots(snapshots, bucket_secs, max_cols) -> Vec<Vec<&AshSnapshot>> helper and pass a filter closure or enum into a single aggregate_buckets_filtered function. Saves ~80 lines and makes future bucket logic changes a one-place edit.

[MEDIUM] label_color: NO_COLOR env check on every render frame
std::env::var_os("NO_COLOR") does a heap allocation on every call, and label_color is invoked per-bucket per-render. Cache the result at AshState init time (a bool no_color field already exists — just extend it to cover NO_COLOR env at startup). The no_color param already threads through; just evaluate NO_COLOR once there and pass the combined flag down.

[LOW] #[allow(dead_code)] on zoom_in/zoom_out/ZOOM_MIN_SECS/ZOOM_MAX_SECS
If zoom_cycle_back/zoom_cycle_forward fully replace zoom_in/zoom_out, delete the old functions and constants. If they're kept for programmatic/test use, add a #[cfg(test)] guard instead of silencing dead_code globally. Suppression attributes that outlive their reason become permanent noise.

[LOW] ColorFn as fn pointer vs trait object
type ColorFn = fn(&str, bool) -> Color works for the current two callsites (both are plain fn items). It breaks the moment someone needs a capturing closure (e.g., a color function parameterized by a palette). Consider fn bucket_segments<F: Fn(&str, bool) -> Color>(…, color_fn: F) — zero runtime cost, more flexible. Not blocking, but worth the 3-character change.

[INFO] LABEL_COLOR_COUNT magic number
The 10 in % LABEL_COLOR_COUNT must equal the number of explicit match arms (currently 9 explicit + 1 wildcard). A mismatch silently shifts color assignments. Consider deriving it from an array length or adding a debug_assert_eq!(LABEL_COLOR_COUNT as usize, PALETTE.len()).

@codecov-commenter
Copy link
Copy Markdown

codecov-commenter commented Mar 30, 2026

⚠️ Please install the 'codecov app svg image' to ensure uploads and comments are reliably processed by Codecov.

Codecov Report

❌ Patch coverage is 0.70175% with 283 lines in your changes missing coverage. Please review.
✅ Project coverage is 66.72%. Comparing base (c2290f3) to head (425dea4).

Files with missing lines Patch % Lines
src/ash/renderer.rs 0.00% 247 Missing ⚠️
src/ash/state.rs 5.26% 36 Missing ⚠️
❗ Your organization needs to install the Codecov GitHub app to enable full functionality.
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #774      +/-   ##
==========================================
- Coverage   67.25%   66.72%   -0.53%     
==========================================
  Files          52       52              
  Lines       34133    34408     +275     
==========================================
+ Hits        22955    22957       +2     
- Misses      11178    11451     +273     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

…ensitive timeline (#773)

- Reassign zoom to [ and ] keys (zoom_cycle_back/forward)
- Left/Right arrows now pan through history with cursor crosshair
- pan_left switches Live→History mode, pan_right returns to Live at offset 0
- Draw │ cursor line at pan position in bar area (bright white)
- Drill-down pane shows per-bucket infobox (ts, AAS, top-3 wait types) when cursor active
- Context-sensitive timeline: WaitEvent level shows wait events by label_color,
  QueryId level shows query labels by label_color (deterministic hash palette)
- Update footer hints to show [/]:zoom  ←→:pan

Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
@NikolayS NikolayS force-pushed the feat/ash-ux-overhaul branch from 26a005e to 931adaa Compare March 30, 2026 05:06
@NikolayS
Copy link
Copy Markdown
Owner Author

Addressed REV findings (commit 931adaa after rebase onto main/v0.9.2):

HIGH — zero-duration History window on first pan_left: Fixed. Now initialises ViewMode::History { from: now - window, to: now } where window = refresh_interval_secs * 600 (the live ring buffer depth), minimum 60 s. This matches the data the ring buffer actually holds.

HIGH — timestamp display: Confirmed correct — no fix needed. ash.epoch = 2026-01-01 00:00:00 UTC = 1_735_689_600 Unix seconds, which is an exact multiple of 86400. So ts % 86400 yields the correct seconds-since-midnight regardless of whether ts is relative to ash.epoch or Unix epoch. Added a comment in render_cursor_infobox explaining this.

LOW — cursor off-by-one: Fixed. Changed guard to if col_from_right < bar_w before computing cursor_x_in_bar = bar_w - 1 - col_from_right, so out-of-bounds columns are skipped cleanly rather than clamping to column 0.

Build clean, clippy -D warnings: zero errors.

@NikolayS
Copy link
Copy Markdown
Owner Author

Testing evidence (commit 91abd5a)

Environment: 8 pgbench clients on ashtest, PG 17.7

#773 UX demo

What the recording shows:

  1. Live /ash — footer shows [/]:zoom ←→:pan (zoom keys reassigned)
  2. pressed 3× — cursor line appears, bottom pane switches to bucket infobox (timestamp, AAS, top wait types)
  3. pressed 3× — returns to Live mode, cursor clears
  4. Drill into top wait type (Enter) — upper timeline recolors to show only that type's wait events as separate bars
  5. Esc — back to top level, full timeline restored
  6. q — clean exit

- state.rs: fix indentation on missed_samples doc comment
- renderer.rs: unwrap HashMap::new() assignment to single line
  (line fits within rustfmt line_length limit)
@NikolayS
Copy link
Copy Markdown
Owner Author

REV re-review (commits 931adaa9edf92b)

HIGH-1 (zero-duration History window) — Fixed. window_secs = (self.refresh_interval_secs * 600).max(60) gives a meaningful window; the .max(60) ensures a 60s floor even at extreme refresh rates. from = now - Duration::from_secs(window_secs) is correct. ✅

HIGH-2 (timestamp display) — Fixed. Verified the math: 1,735,689,600 / 86,400 = 20,088 exactly, so ash.epoch is an exact multiple of 86400. ts % 86400 produces identical results whether ts is ash-relative or Unix-relative. The comment makes the reasoning auditable. ✅

LOW (cursor off-by-one) — Fixed. Guard if col_from_right < bar_w prevents rendering the phantom cursor. Minor style note: guard could precede the cursor_x_in_bar computation for clarity, but the saturating_sub makes it non-buggy either way. ✅

MEDIUM (duplicate chunking logic) — Well done. aggregate_buckets_by_prefix<F> with the closure F: Fn(&AshSnapshot) -> &HashMap<String, u32> is idiomatic Rust. Both callers reduce to one-liners. Net -38 lines. ✅

Verdict: APPROVE. Both HIGH findings are resolved. No remaining blockers.

@NikolayS
Copy link
Copy Markdown
Owner Author

Ready to merge

CI: green (run 23729283326)
REV: APPROVE (all HIGH/MEDIUM findings resolved)
Testing evidence: GIF demo posted (commit 91abd5a) showing all four UX interactions end-to-end

No blockers. Waiting for Nik's merge approval.

@NikolayS
Copy link
Copy Markdown
Owner Author

Bug fix (commit 60d5493): cursor crosshair was invisible

Root cause: the cursor column was rendering a white (box-drawing char) over solid (full-block) bars. The char only occupies ~¼ of the cell width so it was hidden behind the block color fill.

Fix: render the cursor column as an inverted white (black foreground + white background + bold). This overrides the bar's background and is clearly visible against any color in the pg_ash palette.

New demo sent directly to Nik — visible white cursor column on each ← press.

@NikolayS
Copy link
Copy Markdown
Owner Author

Ready to merge (updated)

Bug fix (commit 60d5493) pushed after the initial ready-to-merge signal. CI re-ran and passed.

CI: green (run 23748898237)
REV: APPROVE (all HIGH/MEDIUM findings resolved — verdict from commit 9edf92b)
Testing evidence: GIF demo posted (commit 91abd5a); cursor crosshair visibility fix confirmed by Nik in commit 60d5493

Cursor fix summary (60d5493): Replaced white box-drawing char (hidden behind full-block bars) with inverted (black fg + white bg + bold). Cursor column now clearly visible against the pg_ash color palette.

No blockers. Waiting for Nik's merge approval.

@NikolayS
Copy link
Copy Markdown
Owner Author

CI fix (commit 8733c5e)

Two issues introduced by the cursor crosshair commit (60d5493) broke CI:

1. Unused variable (compile error): let s = sod % 60; at line 1197 in render_cursor_infobox — this s was computed but not referenced in that function's format string (it uses only h and m). Fixed: renamed to _s.

2. cargo fmt diffs: Three Span::styled call sites in render_cursor_infobox (lines 1092, 1221, 1261) were formatted with expanded multi-line style; rustfmt collapsed them to single lines. Fixed: ran cargo fmt.

RUSTFLAGS="-D warnings" cargo check — clean. CI run 23753128023 in progress.

Once green: CI ✅, REV APPROVE ✅, testing evidence ✅ — ready for Nik's merge approval.

@NikolayS
Copy link
Copy Markdown
Owner Author

Ready to merge (final)

CI: green (run 23754872952 — 2026-03-30 16:10 UTC)
REV: APPROVE (all HIGH/MEDIUM findings resolved — verdict from re-review at 05:32 UTC)
Testing evidence: GIF demo (commit 91abd5a) showing zoom keys, pan, cursor crosshair, context-sensitive timeline end-to-end; cursor visibility fix confirmed (commit 60d5493)

Changes since last ready-to-merge signal:

  • 60d5493: cursor crosshair visibility fix — replaced box-drawing char with inverted (black fg + white bg + bold), visible against all pg_ash palette colors
  • 8733c5e: CI fix — unused variable renamed to _s; cargo fmt applied to 3 Span::styled call sites

No blockers. Waiting for Nik's merge approval.

Nik Samokhvalov and others added 3 commits April 1, 2026 14:35
- Remove zoom_in/zoom_out methods and ZOOM_MIN/MAX_SECS constants (no longer
  called after zoom moved to [/] keys)
- Remove corresponding tests and unused test imports
- Fix duplicated doc comments on compute_list_len and run_ash
- Update zoom_level doc to reference [/] instead of ←/→

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
- Fix name[..18] byte slicing that could panic on multi-byte UTF-8 —
  use char_indices instead
- Move in_history computation after collect_frame_snapshots to avoid
  stale state if the function ever mutates mode
- Show b:back in hint line when panning while drilled down
- Fix doc: HashMap<String, i32> → u32, "Top-3" → all non-zero,
  epoch year 2026 → 2025, remove stale merged doc comment
- Add 13 tests: pan_left, pan_right, Esc-to-live, hint_line variants,
  Left/Right key dispatch

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@NikolayS
Copy link
Copy Markdown
Owner Author

NikolayS commented Apr 1, 2026

REV Code Review Report


BLOCKING ISSUES (0)

No blocking issues found.


NON-BLOCKING (3)

LOW src/ash/renderer.rs:1221 — u16 arithmetic without overflow protection in overlay positioning

cursor_abs + 2 + overlay_w uses wrapping u16 addition; rest of function uses saturating_add.
Suggestion: Use saturating_add consistently. Low risk — terminal widths fit comfortably in u16.

LOW src/ash/renderer.rs:683_pct field computed but unused in render_cursor_overlay

The pct_of_total is stored in CursorInfo.top_types but only consumed by render_cursor_infobox, not the floating overlay.
Suggestion: Consider showing percentage in the overlay for consistency, or document that it's intentionally unused there.

LOW src/ash/sampler.rs:274 — SQL string interpolation (pre-existing)

format!("... '{window_secs} seconds'::interval ...") — safe today since window_secs is u64, but a parameterized query would be more robust.
Suggestion: Refactor to use $1 parameter binding in a future PR.


POTENTIAL ISSUES (0)

No medium-confidence issues remaining after fixes.


ISSUES FIXED IN THIS REVIEW

The following issues were found and fixed during review:

  1. HIGH src/ash/renderer.rs:1264name[..18] byte slicing panics on multi-byte UTF-8 → fixed with char_indices
  2. MEDIUM src/ash/mod.rs:115in_history computed before collect_frame_snapshots(&mut state) could lead to stale flag → moved after
  3. MEDIUM src/ash/state.rs:hint_lineb:back hidden when panning while drilled down → added 4th hint variant
  4. MEDIUM doc fixes: HashMap<String, i32>u32, "Top-3" → "all non-zero", epoch year 2026 → 2025, stale merged doc
  5. HIGH 13 missing tests added: pan_left, pan_right, Esc-to-live, hint_line variants, Left/Right key dispatch
  6. Dead code cleanup: removed unused zoom_in/zoom_out and ZOOM_MIN/MAX_SECS

Summary

Area Findings Potential Filtered
CI/Pipeline 0 0 0
Security 0 0 1
Bugs 0 0 0
Tests 0 0 0
Guidelines 0 0 3
Docs 0 0 1

Result: PASSED — no blocking issues.


REV-assisted review (AI analysis by postgres-ai/rev)

Co-Authored-By: Claude Opus 4.6 (1M context) <[email protected]>
@NikolayS NikolayS merged commit 869c477 into main Apr 1, 2026
16 checks passed
@NikolayS NikolayS deleted the feat/ash-ux-overhaul branch April 4, 2026 01:32
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.

feat(ash): UX overhaul — zoom keys, history pan/scrub, cursor crosshair, context-sensitive timeline

2 participants