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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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) { | |||
| 328 | 328 | // other empty rows are skipped unless current. | |
| 329 | 329 | // | |
| 330 | 330 | // 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 | 333 | func (m *Monitor) overviewWorkspaces() []*Workspace { | |
| 333 | 334 | fallback := m.Workspaces[0] | |
| 334 | 335 | for _, w := range m.Workspaces { | |
| @@ -344,10 +345,11 @@ func (m *Monitor) overviewWorkspaces() []*Workspace { | |||
| 344 | 345 | } | |
| 345 | 346 | return len(m.named) | |
| 346 | 347 | } | |
| 348 | + | cur := m.Current() | |
| 347 | 349 | named := append(m.ovNamed[:0], m.hidden...) | |
| 350 | + | named = slices.DeleteFunc(named, func(n *Workspace) bool { return n.empty() && n != cur }) | |
| 348 | 351 | m.ovNamed = named | |
| 349 | 352 | slices.SortStableFunc(named, func(a, b *Workspace) int { return rank(a) - rank(b) }) | |
| 350 | - | cur := m.Current() | |
| 351 | 353 | rows := m.ovList[:0] | |
| 352 | 354 | for _, w := range m.Workspaces { | |
| 353 | 355 | // The row is kept when occupied, current or anchoring a named | |
| @@ -355,9 +357,6 @@ func (m *Monitor) overviewWorkspaces() []*Workspace { | |||
| 355 | 357 | at := len(rows) | |
| 356 | 358 | rows = append(rows, w) | |
| 357 | 359 | for _, n := range named { | |
| 358 | - | if n.empty() && n != cur { | |
| 359 | - | continue | |
| 360 | - | } | |
| 361 | 360 | anchor := n.overviewAfter | |
| 362 | 361 | if indexOf(m.Workspaces, anchor) < 0 { | |
| 363 | 362 | anchor = fallback | |
| @@ -370,9 +369,9 @@ func (m *Monitor) overviewWorkspaces() []*Workspace { | |||
| 370 | 369 | rows = rows[:at] | |
| 371 | 370 | } | |
| 372 | 371 | } | |
| 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)]) | |
| 376 | 375 | clear(rows[len(rows):cap(rows)]) | |
| 377 | 376 | m.ovList = rows | |
| 378 | 377 | return rows | |
| @@ -701,12 +700,6 @@ func (s *overviewSlots) add(w *Workspace, r Rect) { | |||
| 701 | 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 | 703 | // lookup is the slot of w, if it has one. | |
| 711 | 704 | func (s *overviewSlots) lookup(w *Workspace) (Rect, bool) { | |
| 712 | 705 | 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: callers | 330 | // 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 = named | 351 | 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 named | 355 | // 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.overviewAfter | 360 | anchor := n.overviewAfter | ||
| 362 | if indexOf(m.Workspaces, anchor) < 0 { | 361 | if indexOf(m.Workspaces, anchor) < 0 { | ||
| 363 | anchor = fallback | 362 | 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 not | 372 | + | // 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 = rows | 376 | m.ovList = rows | ||
| 378 | return rows | 377 | 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) { | |||
| 763 | 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 | + | } | |
| @@ -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 ( | |||
| 24 | 24 | // (copied only when it changed). What is left is the scenes themselves | |
| 25 | 25 | // (windows, separators, the scene set). Leaving entries (a closed window | |
| 26 | 26 | // 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. | |
| 33 | 34 | const ( | |
| 34 | 35 | publishAllocBudget = 5 | |
| 35 | 36 | 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 | 288 | // The column switcher's cards are previews too: a held switch shows | |
| 258 | 289 | // them, and a publish while their motions run costs a plain frame. | |
| 259 | 290 | t.Run("switcher", func(t *testing.T) { | |
| @@ -24,12 +24,13 @@ import ( | |||||
| 24 | // (copied only when it changed). What is left is the scenes themselves | 24 | // (copied only when it changed). What is left is the scenes themselves | ||
| 25 | // (windows, separators, the scene set). Leaving entries (a closed window | 25 | // (windows, separators, the scene set). Leaving entries (a closed window | ||
| 26 | // still fading) add none: 3, less than a frame with six moving windows | 26 | // 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 slice | 29 | + | // 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 one | 31 | + | // 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 = 5 | 35 | publishAllocBudget = 5 | ||
| 35 | publishLeavingAllocBudget = 3 | 36 | 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 shows | 288 | // 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) { | ||