🎬 Projects-box slide, round two: frames from the steady rendering pipeline (#416)#436
Merged
Conversation
BenchmarkProjectsAnimFrame (Apple M1 Max, darwin/arm64) — one full per-frame slide render (rasterize + drawSkyline + label row + band slice), all under the 60fps/16.7ms budget: /100-10 289304 ns/op 58207 B/op 322 allocs/op /1000-10 1185059 ns/op 410865 B/op 502 allocs/op /5000-10 5201372 ns/op 1881785 B/op 1147 allocs/op Widen seedModelAt / seedBarModelWithMessages / armProjectsShowForTest to testing.TB so the benchmark reuses them.
renderProjectsFrame replaces the snapshot rasterizer: bar modes go through renderWindow, line mode re-issues the steady full-canvas buildLineChart + setX re-apply, both at the lever-derived animated chartHeight. The arm no longer repaints frame 0 (the current steady frame IS frame 0), which is half of the endpoint-identity guarantee. Bench and mid-slide View test follow the rename (they called the deleted renderProjectsAnimFrame).
…416) View composes the box through the steady renderProjectsBox at projectsHeight() every frame — real borders, title, and cells from the first frames. projectsBandRows/projectsTopBorder and their tests die. renderProjectsBox gains exact-height degenerate frames for the heights only the slide passes through (1: top border, 2: closed shell, 3: shell around the title row) — its old innerH/bodyRows floors emitted 3-4 rows for any height under 4, which would have broken View's per-frame height conservation; the spec's borders-only intent, pinned by TestRenderProjectsBox_DegenerateHeights.
The springKindProjects overlay case now reads live unit/peak/zoom — the exact inputs the steady cases read — instead of the arm-time snapshot, removing the last reader of projectsSnap's chart fields. The label- stability test pins the steady x-label row appearing verbatim (bytes, incl. ANSI color) in every sampled mid-slide frame.
…eight (#416) projectsAnimSnapshot dies — frames render from live state, so the only slide state left is the spring + projectsSlideFrom/To endpoints. A second 'p' mid-slide reverses from the current animated height (no snap to an extreme); u/z aborts keep the refreshChart hard-cut. lerpInt now clamps r to [0,1] (round-one finding ccpulse-416.5: the spring does not guarantee it). The 'p' handler snaps on an empty/cleared chart (lastCanvasW==0), mirroring renderWindow's own no-op guard. armProjectsShowForTest now delegates to the production arm path.
The implementation landed with renderProjectsFrame (Task 1); this pins it: frame 0 and the settle frame in remaining mode are byte-identical to the steady line views. seedRemainingModelWithSamples widens to testing.TB so the reworked benchmark can reuse it.
Both modes land well inside the 16.7ms 60fps budget — the line mode is
nowhere near the ~12ms gate, so the 30fps escape hatch stays unused.
goos: darwin / goarch: arm64 / cpu: Apple M1 Max
│ sec/op │
ProjectsAnimFrame/bar-10 3.753m ± 1%
ProjectsAnimFrame/line-10 2.500m ± 3%
│ B/op │
ProjectsAnimFrame/bar-10 3.941Mi ± 0%
ProjectsAnimFrame/line-10 3.822Mi ± 0%
│ allocs/op │
ProjectsAnimFrame/bar-10 14.35k ± 0%
ProjectsAnimFrame/line-10 2.333k ± 0%
The empty-chart guard pushed handleKey over the gocyclo gate; the projects branch moves into its own handler, mirroring handleZoomKey / handleUnitKey.
The per-tick line frame rebuilt the full-history canvas (EarliestMessageTime -> now): ~41ms / 68.9MB / 68k allocs per frame at a 30-day 15m-zoom canvas (2880 cols) - 2.5x the 16.7ms 60fps budget. Mirror renderSpringLineFrame's #180 windowing (visibleWindow + slicePointsInRange at viewport width, SetXOffset(0) keeping the viewportXOffset shadow for the settle restore); pixel-identical inside the visible region, endpoint frames untouched (frame 0 never repaints, settle repaints via refreshChart's full canvas). The line bench fixture (60 samples, zero messages) collapsed the canvas to viewport width, so the >=12ms/op 30fps gate could never fire. Add a line_30d sub-bench seeded with 2880 buckets of message history and a lastCanvasW guard so the gate measures the realistic worst case. BenchmarkProjectsAnimFrame (Apple M1 Max, count=2): bar 3.80ms 3.76ms 4.13MB/op 14.3k allocs/op line 2.65ms 2.53ms 4.00MB/op 2318 allocs/op line_30d 2.48ms 2.52ms 4.00MB/op 2317 allocs/op
The windowed per-frame line build synthesized x-labels for the visible window only, dropping any label straddling the window's left edge (the steady viewport shows its clipped tail) — labels popped out mid-slide and back at settle. Build the row for the full canvas exactly as the steady path does and cut the visible columns with the same ansi.Cut the viewport applies to content. Pinned by the new remaining-mode sibling of TestProjectsSlide_XLabelRowStable, which found the defect.
…ide (#416) RefreshMsg, nowTick, and the u/z arm paths abort an in-flight projects slide via refreshChart's spring-abort block, which snaps chartHeight() back to the steady value — but viewport.Height kept the mid-slide value renderProjectsFrame last wrote, so every View() painted the wrong number of rows (51 on a 40-row terminal) until the next resize or 'p'. Re-sync inside the abort block (no-op for unit/zoom aborts) and pin the RefreshMsg and nowTick paths with siblings of the resize regression test.
Brings in the content-aware projects box height (#420/#429) and the usage-line projects window fix (#430/#431), reconciled with the #416 slide: - projectsTargetHeight adopts the content-aware computation; the projectsHeight lever keeps returning the animated height mid-slide. - beginProjectsAnimation queries aggs BEFORE reading the target: on a show the aggs are still nil from the hidden state, so reading first would arm the slide against the 4-row empty floor and jump to the real height at settle. - handleProjectsTick's mid-spring reflow guard now also defers across the projects slide; its settle refreshChart performs the reflow. - Slide tests updated for the content-aware target (single-project fixture target is 4, not 12): re-arm test drives to an observed mid-flight height instead of a fixed tick count; content-presence thresholds tightened to 3 (title) / 4 (top spender) rows so they stay non-vacuous at the smaller target. Verified against the dev fixture: usage-line view + p now renders the per-project rollup for the visible window (the #430 symptom).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #416. Supersedes #417 (round one, closed — "I'll try another approach").
🧭 Why round two
Round one animated the slide through a parallel rendering pipeline (snapshot skyline rasterizer + pre-rendered box sliced bottom-first under a phantom border). One root cause, four visible defects: the box rose empty, the chart's endpoint frames didn't match the steady chart, and the x-labels shifted sideways and changed color mid-slide.
✨ What changed
Same motion (single critically-damped harmonica spring on the box's outer height,
pto toggle), but every frame is now produced by the same code that paints steady state, at the animated height:renderWindow(the steady windowed painter: visible slice, flush-right slack, on-screen peak, in-bar labels) at the lever-derivedchartHeight().buildLineChartat viewport width (visibleWindow+slicePointsInRange): the full-canvas per-frame rebuild measured ~41ms at a 30-day canvas, 2.5× the 60fps budget. The x-label row is built for the full canvas exactly as the steady path does, then cut to the visible columns, so it stays byte-stable mid-slide; endpoint frames are untouched (frame 0 never repaints, settle restores the full canvas viarefreshChart).View()path viarenderProjectsBoxatprojectsHeight()every frame: real borders and title from the first frames, cells filling top-down, the "…N more" overflow recounting as rows fit.renderProjectsBoxgains exact-height degenerate frames (1: top border, 2: closed shell, 3: shell + title) for the heights only the slide passes through.projectsAnimSnapshot,projectsBandRows,projectsTopBorderand the band View branch are deleted; the only slide state left is the spring +projectsSlideFrom/To. A secondpmid-slide reverses from the current height.lerpIntnow clamps r to [0,1]. An empty/cleared chart snaps instead of animating.Endpoint identity by construction: frame 0 is byte-identical to the pre-slide steady view, the settle frame byte-identical to the post-slide steady view — pinned by tests in both chart modes under forced color.
🔀 Merged main mid-flight
The branch merged
mainto pick up the content-aware box height (#420/#429) and the usage-line projects window fix (#430/#431), reconciled with the slide:projectsTargetHeightis now content-aware while theprojectsHeight()lever still returns the animated height mid-slide, andbeginProjectsAnimationqueries the aggregates before reading the target so a show slides to the real content height rather than the empty-box floor.✅ Testing
prenders the per-project rollup for the visible window (the 🪟 Projects box shows "no activity in this window" on the usage-line view after zoom/scroll #430 symptom is gone).BenchmarkProjectsAnimFrame(Apple M1 Max): bar 3.75ms ± 1%, line 2.50ms ± 3%, andline_30d(realistic 2880-col canvas) 2.48–2.52ms / 4.00MB per frame — all well inside the 16.7ms 60fps budget; the 30fps line-mode escape hatch stays unused.make lint0 issues, fullgo test ./...green (goleak guards included),make buildclean.🤖 Generated with Claude Code