Repository navigation
The heat projection's cost follows the file rather than the window #55
Description
Found by take-next's fifth pre-flight comparison, the one #34 added: an open SPEC.md §10 bullet that no issue names.
The bullet
The heat projection is the one drawn thing whose cost follows the file rather than the window. Placing a change needs its working-tree line number, and a line number is only known by counting the lines before it, so
heat_ofwalks every line of a drawn file's hunks even though the viewport shows a screenful.
It has been open since #39 landed the heat strip. Nothing tracks it, so it is unassignable and would never be returned by the milestone-filtered query take-next runs first.
Why it is not urgent, said out loud
The bullet carries its own measurement and the measurement is comfortable. Release, over the 100-file 100k-line fixture, the shell frame went from 7.66ms to 8.14ms p99 against the 16ms I9 budget, while the core frame path, which executes none of this code, moved 6.96ms to 7.29ms on the same runs. So most of the difference is run-to-run variation and what is left is a few tenths of a millisecond.
It is also not a read. reads.rs does not move and I2a is untouched: this is arithmetic over lines already in memory, and it runs only for headings actually drawn, which is one or two per frame.
Why it is still worth a row
The shape is the one I4 exists to forbid: a drawn thing whose cost follows the file rather than the window. Everything else in the shell follows the screen. Rows above the window are counted rather than built, highlighting is asked for only on a row that is pushed, and a file the viewport never reaches is never diffed. This is the single exception, and #45 is the precedent for why a known instance of a forbidden shape should not sit both unfixed and ungated: that one was affordable in wall clock too, right up until a reader scrolled a Japanese README.
The fix, when it is taken
Carry the line number on Line rather than recomputing it. That is the same information the gutter already derives twice, so this is the by-product check §5.2 asks for, and it has paid four times in this project already.
Acceptance
-
heat_of's cost follows the viewport rather than the file, or the bullet is closed with a measured argument that it should not - A structural gate over a counter, not a wall-clock one, so it holds on a hosted runner. The pair
paint.rsuses is the precedent: a count of what was examined, against a bound derived from the run rather than from a constant - The gate is driven red by mutation before it is trusted
-
SPEC.md§10's bullet is closed with whichever answer the measurement gives
Activity
The status bar says what a frame cost and what the process holds, and…
spec: six open questions had issues and named none of them
breferrari commentedon Sep 30, 2026
Plan (pre-approved by the reader for this run)
Taken from the Shelf by the unattended loop on 2026-09-30.
next.shis dry. The deferral reason on this row is "nothing had ever taken it", which is not a reason that can still hold, so the row is eligible.What it rests on
SPEC.mdI4: "A frame builds only what it draws. Diffs, highlighting and reads of file content follow the window, never the worktree." The §10 bullet this issue tracks names the heat projection as the one drawn thing whose cost follows the file. This pass closes that bullet.SPEC.md§5.2: "the expensive-looking property was a by-product of work already being done. Look for the by-product before you design." The by-product here is the diff itself: the projection depends on nothing but the hunks, so it is true for exactly as long as the cached diff is, and the frame path already knows that interval.Line. See the deviation below.Premises
heat_ofruns once per drawn entry per frameentry_ofcallsheat_of(diff)at every call, and it is called from the diff walk and the list walk on every frame (view.rs). No cache sits in front of it.heat_of(&FileDiff)readsdiff.hunksanddiff.linesonly.Frame::diffreuses aCached { taken, diff }entry only whenreusableproves it, and replaces the whole entry when it recomputes. A value stored beside the diff is dropped exactly when the diff is.Frame::diffcall for the same file in one frame is freestatreusablecallsfresh()each time). So the derived value has to come back from the same call that returns the diff, not from a second one.SPEC.md§10: a 200x40 pane drawing thirty-six pinned rows takes 0.71ms p50 and 0.79ms p99 against I9's 16ms. This pass re-measures it on both sides of the change witha_frame_beside_a_rail_holds_the_frame_budget.Checks on the record
Deviation from the recorded fix, with the reason
The bullet and the vault note name the fix as carrying the working-tree line number on
Line. That removes the arithmetic inHunk::positionsbut not the walk: counting added and removed lines into slices still visits every hunk line of every drawn file on every frame, so the cost class does not change. What changes the class is computing the projection once per computed diff and serving it from the cache while the diff is reused. That is the by-product §5.2 asks for, and it makes the per-frame cost a copy of 48 buckets per drawn heading.Promises
crates/vigia-core/src/frame.rsCachedgainsderived: Option<Box<dyn Any + Send + Sync>>, dropped with the entry.FrameStatsgainspub derived: u64: values derived from a cached diff, each computed once per diff. This is a public field on a public struct, so it is a breaking addition for an exhaustive literal outside this workspace. Flagged for the bump level in the report.pub fn diff_with<T: Any + Clone + Send + Sync>(&mut self, index: usize, derive: impl FnOnce(&FileDiff) -> T) -> Result<(&FileChange, &FileDiff, T)>. Callsdiff, then serves the derived value from the slot or computes and stores it. A failed read is not cached, so its hunkless diff derives in constant time on each call.Frame::diffis unchanged.crates/vigia/src/view.rsChangedgainsheat: [HeatBucket; HEAT_BUCKETS].frame.diff_with(index, heat_of)where they calledframe.diff(index).entry_oftakes the heat as a parameter instead of callingheat_of.heat_of,bucket_of,sampledand their unit tests do not move.crates/vigia-core/tests/support/mod.rs,crates/vigia-core/examples/timings.rs:deltacarries the new counter.Tests
crates/vigia/tests/reads.rs::a_heat_strip_is_projected_once_per_diff: settle a frame, draw it twice. The first view derives at least one value and draws at least one heading with heat in it. The second view derives zero. Then edit one drawn file so its change moves to another part of it and let it settle. The next view derives no more values than it computed diffs, and that file's drawn heat differs from before. The first assertion is what the mutation reddens; the last is what a slot that outlived its diff would fail.crates/vigia-core/tests/frame.rs::a_derived_value_is_computed_once_per_diff:diff_withon a settled frame derives once, then serves; after a write and settle, it derives again and the value follows the new diff.Mutation: skip the slot read in
diff_withand watch the reads gate go red. Keep the old slot across a recompute and watch the edit assertion go red.Documents
SPEC.md§10: the bullet closes, struck through, with the mechanism and the before-and-after numbers from the rail frame.ROADMAP.md: the row flips, and the Pull-forward log gets a line.Out of scope:
slice_ofwalks a file's positions once per old-side note per frame. That is bounded by the notes a file carries, not by the file, and this pass does not touch it. Named in the report.Label:
internal,chore:title. The pane shows nothing different. Said in the report.Close the heat projection bullet
fix: project a file's heat strip once instead of on every frame (#628)
breferrari commentedon Sep 30, 2026
Closed by #628.
The heat strip is projected once per computed diff:
Frame::diff_withkeeps a value derived from a cached diff beside that diff and serves it while the diff is reused, andFrameStats::derivedcounts the derivations. Gates:crates/vigia/tests/reads.rs::heat_once_per_diff(a second screen over settled diffs derives 0; a rewrite of one file derives exactly as many strips as diffs it recomputed, and the strip follows the new diff) andcrates/vigia-core/tests/frame.rs::derived_once_per_diff. Four mutations killed by those gates; a fifth, the shell projecting per frame while ignoring the frame's value, is refused by-D warnings.Rail frame, release, 200x40 pane drawing 36 pinned rows, two rounds each interleaved: p50 5.69ms and 5.65ms before, 5.67ms and 5.64ms after; p99 7.05ms and 7.11ms before, 6.74ms and 6.96ms after, against I9's 16ms. The frame did not move measurably; the shape did.
SPEC.md§10's bullet is closed with those numbers.Left as it was:
slice_ofwalks a file's positions once per old-side note per frame, bounded by the notes a file carries.