detached 7898c05

perf(core): filter empty named rows once and cover the named overview budget

overviewWorkspaces drops empty, non-current named rows before grouping instead of on every numbered row, clears its named scratch after use, and documents that the result keeps the last rows reachable. overviewSlots.at moves to the tests. TestPublishOverviewAllocations gains a named case with the boundary divider (5 allocs).
commit
7898c05b558b33395f2980fc8ef72679dd898f55
author
brice · · 2026-10-16 14:08 UTC
parent
08c5e16
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
3 files · +50 -20
M internal/core/overview.go +7 -14
@@ -328,7 +328,8 @@ func restoreRow(w *Workspace, s rowSnapshot) {
328328 // other empty rows are skipped unless current.
329329 //
330330 // The result is the monitor's scratch, rebuilt by the next call: callers
331-// read it and drop it before calling again (Clone it to keep it).
331+// read it and drop it before calling again (Clone it to keep it). It keeps
332+// the last result's workspaces reachable until then.
332333 func (m *Monitor) overviewWorkspaces() []*Workspace {
333334 fallback := m.Workspaces[0]
334335 for _, w := range m.Workspaces {
@@ -344,10 +345,11 @@ func (m *Monitor) overviewWorkspaces() []*Workspace {
344345 }
345346 return len(m.named)
346347 }
348+ cur := m.Current()
347349 named := append(m.ovNamed[:0], m.hidden...)
350+ named = slices.DeleteFunc(named, func(n *Workspace) bool { return n.empty() && n != cur })
348351 m.ovNamed = named
349352 slices.SortStableFunc(named, func(a, b *Workspace) int { return rank(a) - rank(b) })
350- cur := m.Current()
351353 rows := m.ovList[:0]
352354 for _, w := range m.Workspaces {
353355 // The row is kept when occupied, current or anchoring a named
@@ -355,9 +357,6 @@ func (m *Monitor) overviewWorkspaces() []*Workspace {
355357 at := len(rows)
356358 rows = append(rows, w)
357359 for _, n := range named {
358- if n.empty() && n != cur {
359- continue
360- }
361360 anchor := n.overviewAfter
362361 if indexOf(m.Workspaces, anchor) < 0 {
363362 anchor = fallback
@@ -370,9 +369,9 @@ func (m *Monitor) overviewWorkspaces() []*Workspace {
370369 rows = rows[:at]
371370 }
372371 }
373- // Clear the unused tails so a workspace gone from this monitor is not
374- // kept reachable by the scratch.
375- clear(named[len(named):cap(named)])
372+ // named is only needed here; the rows' tail is cleared so the scratch
373+ // holds no more than the last result.
374+ clear(named[:cap(named)])
376375 clear(rows[len(rows):cap(rows)])
377376 m.ovList = rows
378377 return rows
@@ -701,12 +700,6 @@ func (s *overviewSlots) add(w *Workspace, r Rect) {
701700 s.n++
702701 }
703702
704-// at is the slot of w, the zero Rect if it has none.
705-func (s *overviewSlots) at(w *Workspace) Rect {
706- r, _ := s.lookup(w)
707- return r
708-}
709-
710703 // lookup is the slot of w, if it has one.
711704 func (s *overviewSlots) lookup(w *Workspace) (Rect, bool) {
712705 for i := range s.n {
@@ -328,7 +328,8 @@ func restoreRow(w *Workspace, s rowSnapshot) {
328 // other empty rows are skipped unless current.328 // other empty rows are skipped unless current.
329 //329 //
330 // The result is the monitor's scratch, rebuilt by the next call: callers330 // The result is the monitor's scratch, rebuilt by the next call: callers
331-// read it and drop it before calling again (Clone it to keep it).331+// read it and drop it before calling again (Clone it to keep it). It keeps
332+// the last result's workspaces reachable until then.
332 func (m *Monitor) overviewWorkspaces() []*Workspace {333 func (m *Monitor) overviewWorkspaces() []*Workspace {
333 fallback := m.Workspaces[0]334 fallback := m.Workspaces[0]
334 for _, w := range m.Workspaces {335 for _, w := range m.Workspaces {
@@ -344,10 +345,11 @@ func (m *Monitor) overviewWorkspaces() []*Workspace {
344 }345 }
345 return len(m.named)346 return len(m.named)
346 }347 }
348+ cur := m.Current()
347 named := append(m.ovNamed[:0], m.hidden...)349 named := append(m.ovNamed[:0], m.hidden...)
350+ named = slices.DeleteFunc(named, func(n *Workspace) bool { return n.empty() && n != cur })
348 m.ovNamed = named351 m.ovNamed = named
349 slices.SortStableFunc(named, func(a, b *Workspace) int { return rank(a) - rank(b) })352 slices.SortStableFunc(named, func(a, b *Workspace) int { return rank(a) - rank(b) })
350- cur := m.Current()
351 rows := m.ovList[:0]353 rows := m.ovList[:0]
352 for _, w := range m.Workspaces {354 for _, w := range m.Workspaces {
353 // The row is kept when occupied, current or anchoring a named355 // The row is kept when occupied, current or anchoring a named
@@ -355,9 +357,6 @@ func (m *Monitor) overviewWorkspaces() []*Workspace {
355 at := len(rows)357 at := len(rows)
356 rows = append(rows, w)358 rows = append(rows, w)
357 for _, n := range named {359 for _, n := range named {
358- if n.empty() && n != cur {
359- continue
360- }
361 anchor := n.overviewAfter360 anchor := n.overviewAfter
362 if indexOf(m.Workspaces, anchor) < 0 {361 if indexOf(m.Workspaces, anchor) < 0 {
363 anchor = fallback362 anchor = fallback
@@ -370,9 +369,9 @@ func (m *Monitor) overviewWorkspaces() []*Workspace {
370 rows = rows[:at]369 rows = rows[:at]
371 }370 }
372 }371 }
373- // Clear the unused tails so a workspace gone from this monitor is not372+ // named is only needed here; the rows' tail is cleared so the scratch
374- // kept reachable by the scratch.373+ // holds no more than the last result.
375- clear(named[len(named):cap(named)])374+ clear(named[:cap(named)])
376 clear(rows[len(rows):cap(rows)])375 clear(rows[len(rows):cap(rows)])
377 m.ovList = rows376 m.ovList = rows
378 return rows377 return rows
@@ -701,12 +700,6 @@ func (s *overviewSlots) add(w *Workspace, r Rect) {
701 s.n++700 s.n++
702 }701 }
703 702
704-// at is the slot of w, the zero Rect if it has none.
705-func (s *overviewSlots) at(w *Workspace) Rect {
706- r, _ := s.lookup(w)
707- return r
708-}
709-
710 // lookup is the slot of w, if it has one.703 // lookup is the slot of w, if it has one.
711 func (s *overviewSlots) lookup(w *Workspace) (Rect, bool) {704 func (s *overviewSlots) lookup(w *Workspace) (Rect, bool) {
712 for i := range s.n {705 for i := range s.n {
M internal/core/overview_test.go +6 -0
@@ -763,3 +763,9 @@ func TestOverviewFingerScroll(t *testing.T) {
763763 t.Fatal("continuous scroll held by the finger scroll")
764764 }
765765 }
766+
767+// at is the slot of w, the zero Rect if it has none.
768+func (s *overviewSlots) at(w *Workspace) Rect {
769+ r, _ := s.lookup(w)
770+ return r
771+}
@@ -763,3 +763,9 @@ func TestOverviewFingerScroll(t *testing.T) {
763 t.Fatal("continuous scroll held by the finger scroll")763 t.Fatal("continuous scroll held by the finger scroll")
764 }764 }
765 }765 }
766+
767+// at is the slot of w, the zero Rect if it has none.
768+func (s *overviewSlots) at(w *Workspace) Rect {
769+ r, _ := s.lookup(w)
770+ return r
771+}
M internal/core/publish_alloc_test.go +37 -6
@@ -24,12 +24,13 @@ import (
2424 // (copied only when it changed). What is left is the scenes themselves
2525 // (windows, separators, the scene set). Leaving entries (a closed window
2626 // still fading) add none: 3, less than a frame with six moving windows
27-// (fewer configure targets). The opening overview costs 6 (7 under -race),
28-// 4 alone: it was 11 and 13 when it rebuilt the row map, the neighbor list,
29-// the workspace order (groups map, hidden clone) and the outline slice
30-// (appended onto the dividers) every frame; the rows are now a fixed array,
31-// the order and dividers monitor scratch, and the scene's separators one
32-// slice. What is left is the scene, the stack items and the preview rows.
27+// (fewer configure targets). The opening overview costs 4 alone and 6 with
28+// neighbors (5 and 7 under -race); it was 11 and 13 when it rebuilt the row
29+// map, the neighbor list, the workspace order (groups map, hidden clone) and
30+// the outline slice (appended onto the dividers) every frame. The rows are
31+// now a fixed array, the order and dividers monitor scratch, and the scene's
32+// separators one slice. What is left is the scene, the stack items and the
33+// preview rows.
3334 const (
3435 publishAllocBudget = 5
3536 publishLeavingAllocBudget = 3
@@ -254,6 +255,36 @@ func TestPublishOverviewAllocations(t *testing.T) {
254255 })
255256 }
256257 }
258+ // A named row below its anchor adds the boundary divider: the scene
259+ // copies it into its own separators, one slice like the outline alone.
260+ t.Run("named", func(t *testing.T) {
261+ c := publishRig(t, 1)
262+ sc := c.cur()
263+ m := sc.mon
264+ m.SetNamed([]NamedWorkspace{{Name: "dev"}})
265+ m.ToggleNamed("dev")
266+ m.AddWindow(30)
267+ m.ToggleNamed("dev")
268+ sc.arrange()
269+ c.refreshShown()
270+ now := time.Now()
271+ shots := c.snapshot(now)
272+ m.ToggleOverview()
273+ c.transition(shots, now)
274+ step := publishStep(t, c, nil)
275+ step()
276+ if _, dividers := m.overviewRows(); len(dividers) != 1 || !m.ov.open || len(sc.rects) == 0 {
277+ t.Fatalf("setup: %d dividers, overview %v, rects %d", len(dividers), m.ov.open, len(sc.rects))
278+ }
279+ n := testing.AllocsPerRun(50, step)
280+ t.Logf("named overview allocs per step = %v", n)
281+ if len(sc.rects) == 0 {
282+ t.Fatal("card motions settled during the measure")
283+ }
284+ if n > publishOverviewAllocBudget {
285+ t.Errorf("named overview publish allocs per frame = %v, budget %d", n, publishOverviewAllocBudget)
286+ }
287+ })
257288 // The column switcher's cards are previews too: a held switch shows
258289 // them, and a publish while their motions run costs a plain frame.
259290 t.Run("switcher", func(t *testing.T) {
@@ -24,12 +24,13 @@ import (
24 // (copied only when it changed). What is left is the scenes themselves24 // (copied only when it changed). What is left is the scenes themselves
25 // (windows, separators, the scene set). Leaving entries (a closed window25 // (windows, separators, the scene set). Leaving entries (a closed window
26 // still fading) add none: 3, less than a frame with six moving windows26 // still fading) add none: 3, less than a frame with six moving windows
27-// (fewer configure targets). The opening overview costs 6 (7 under -race),27+// (fewer configure targets). The opening overview costs 4 alone and 6 with
28-// 4 alone: it was 11 and 13 when it rebuilt the row map, the neighbor list,28+// neighbors (5 and 7 under -race); it was 11 and 13 when it rebuilt the row
29-// the workspace order (groups map, hidden clone) and the outline slice29+// map, the neighbor list, the workspace order (groups map, hidden clone) and
30-// (appended onto the dividers) every frame; the rows are now a fixed array,30+// the outline slice (appended onto the dividers) every frame. The rows are
31-// the order and dividers monitor scratch, and the scene's separators one31+// now a fixed array, the order and dividers monitor scratch, and the scene's
32-// slice. What is left is the scene, the stack items and the preview rows.32+// separators one slice. What is left is the scene, the stack items and the
33+// preview rows.
33 const (34 const (
34 publishAllocBudget = 535 publishAllocBudget = 5
35 publishLeavingAllocBudget = 336 publishLeavingAllocBudget = 3
@@ -254,6 +255,36 @@ func TestPublishOverviewAllocations(t *testing.T) {
254 })255 })
255 }256 }
256 }257 }
258+ // A named row below its anchor adds the boundary divider: the scene
259+ // copies it into its own separators, one slice like the outline alone.
260+ t.Run("named", func(t *testing.T) {
261+ c := publishRig(t, 1)
262+ sc := c.cur()
263+ m := sc.mon
264+ m.SetNamed([]NamedWorkspace{{Name: "dev"}})
265+ m.ToggleNamed("dev")
266+ m.AddWindow(30)
267+ m.ToggleNamed("dev")
268+ sc.arrange()
269+ c.refreshShown()
270+ now := time.Now()
271+ shots := c.snapshot(now)
272+ m.ToggleOverview()
273+ c.transition(shots, now)
274+ step := publishStep(t, c, nil)
275+ step()
276+ if _, dividers := m.overviewRows(); len(dividers) != 1 || !m.ov.open || len(sc.rects) == 0 {
277+ t.Fatalf("setup: %d dividers, overview %v, rects %d", len(dividers), m.ov.open, len(sc.rects))
278+ }
279+ n := testing.AllocsPerRun(50, step)
280+ t.Logf("named overview allocs per step = %v", n)
281+ if len(sc.rects) == 0 {
282+ t.Fatal("card motions settled during the measure")
283+ }
284+ if n > publishOverviewAllocBudget {
285+ t.Errorf("named overview publish allocs per frame = %v, budget %d", n, publishOverviewAllocBudget)
286+ }
287+ })
257 // The column switcher's cards are previews too: a held switch shows288 // The column switcher's cards are previews too: a held switch shows
258 // them, and a publish while their motions run costs a plain frame.289 // them, and a publish while their motions run costs a plain frame.
259 t.Run("switcher", func(t *testing.T) {290 t.Run("switcher", func(t *testing.T) {

Keyboard shortcuts

Single-key shortcuts are on. ctrl k and esc work whatever this says.

ctrl k :
go to a section, branch, tag, file or repository
?
this list
j k
move the row cursor down / up
enter
open the row under the cursor; on Files, a directory opens or closes in place
↑ ↓ ← → home end
in the file tree (focus on a row): previous / next row, close / open a directory or go to its parent / first entry, first / last row. They never open a file
l h
on Files: open a directory or a file / close a directory or go up to its parent, then to the parent page. Elsewhere: into the side panel (on a merge request, its files) / back to the content; there j k move between its controls
g g G
first / last row
/
filter the list; on Files, find a file anywhere in the repository (esc clears, enter opens the best match)
b
switch branch or tag
g s g f g c g b g m g i g r g a
tabs: summary, files, commits, branches & tags, merge requests, issues, releases, actions (when available)
g h
home
w
wide mode: use the whole width of the screen, or not (remembered)
y
copy a permalink pinned to the commit
d u x
diff: split / unified / collapse or expand all
e E
diff: the whole file in view / every file, with the lines the hunks hide
n
merge request: next note in the diff
esc
close a dialog, leave a field

Branches 23

Tags 10

↑↓ moveenter openesc close