perf(core): presize scene windows and separators, pin publish and moving-window draw allocations
- commit
- afb3cdc28db7091677b0e632a05090262f07d8fd
- author
- brice · · 2026-10-04 20:03 UTC
- committer
- brice · · 2026-10-04 20:13 UTC
- parent
- cca9f08
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
5 files · +134 -4
M Makefile +1 -1
| @@ -60,7 +60,7 @@ arch: | |||
| 60 | 60 | $(HOME)/go/bin/hexcheck -hexcheck.config .hexcheck.yaml -hexcheck.root . ./... | |
| 61 | 61 | # Fast, mandatory guards for tiled content publications and SHM copies. | |
| 62 | 62 | perf-check: | |
| 63 | - | CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/vulkan ./internal/adapters/wayland -run '^(TestAnimateAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget)$$' -count=1 | |
| 63 | + | CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/vulkan ./internal/adapters/wayland -run '^(TestAnimateAllocations|TestPublishAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget)$$' -count=1 | |
| 64 | 64 | check: vet test arch fakes-check log-check perf-check | |
| 65 | 65 | ||
| 66 | 66 | # On-demand SDR/HDR colour check with examples/testpattern on a headless | |
| @@ -60,7 +60,7 @@ arch: | |||||
| 60 | $(HOME)/go/bin/hexcheck -hexcheck.config .hexcheck.yaml -hexcheck.root . ./... | 60 | $(HOME)/go/bin/hexcheck -hexcheck.config .hexcheck.yaml -hexcheck.root . ./... | ||
| 61 | # Fast, mandatory guards for tiled content publications and SHM copies. | 61 | # Fast, mandatory guards for tiled content publications and SHM copies. | ||
| 62 | perf-check: | 62 | perf-check: | ||
| 63 | - | CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/vulkan ./internal/adapters/wayland -run '^(TestAnimateAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget)$$' -count=1 | 63 | + | CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/vulkan ./internal/adapters/wayland -run '^(TestAnimateAllocations|TestPublishAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget)$$' -count=1 |
| 64 | check: vet test arch fakes-check log-check perf-check | 64 | check: vet test arch fakes-check log-check perf-check | ||
| 65 | 65 | ||||
| 66 | # On-demand SDR/HDR colour check with examples/testpattern on a headless | 66 | # On-demand SDR/HDR colour check with examples/testpattern on a headless | ||
M internal/adapters/vulkan/scene_perf_test.go +37 -1
| @@ -9,7 +9,10 @@ import ( | |||
| 9 | 9 | ) | |
| 10 | 10 | ||
| 11 | 11 | // TestRenderSteadyStateAllocations measures the cost of a small stable scene, | |
| 12 | - | // on a normal and on a rotated output (orient works in place). | |
| 12 | + | // on a normal and on a rotated output (orient works in place), and that | |
| 13 | + | // building the draws of a scene with a moving window (a new Seq and window | |
| 14 | + | // slice every frame, as an animation publishes) allocates nothing. | |
| 15 | + | // TODO(A2): add the partial redraw seeded by sceneDelta once it is merged. | |
| 13 | 16 | func TestRenderSteadyStateAllocations(t *testing.T) { | |
| 14 | 17 | for _, tc := range []struct { | |
| 15 | 18 | name string | |
| @@ -41,6 +44,39 @@ func TestRenderSteadyStateAllocations(t *testing.T) { | |||
| 41 | 44 | } else { | |
| 42 | 45 | t.Logf("small steady-state Render: %.1f allocs/frame", allocs) | |
| 43 | 46 | } | |
| 47 | + | // A moving window: scenes are prebuilt (core allocates them, not | |
| 48 | + | // the renderer), each with a fresh Seq, so every frame draws | |
| 49 | + | // everything again. The draw building must not allocate. | |
| 50 | + | var moving [4]ports.Scene | |
| 51 | + | for i := range moving { | |
| 52 | + | moving[i] = ports.Scene{Seq: uint64(100 + i), Transform: tc.transform, Scale: 1, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: i, W: tc.sceneW - 4, H: tc.sceneH - 4}}}} | |
| 53 | + | } | |
| 54 | + | bounds := image.Rect(0, 0, r.width, r.height) | |
| 55 | + | n := 0 | |
| 56 | + | frameDraws := func() { | |
| 57 | + | n++ | |
| 58 | + | s := moving[n%len(moving)] | |
| 59 | + | _ = r.draws(s, contents, r.frameDamage(r.last, s, bounds)) | |
| 60 | + | } | |
| 61 | + | frameDraws() | |
| 62 | + | if allocs := testing.AllocsPerRun(50, frameDraws); allocs != 0 { | |
| 63 | + | t.Errorf("moving window draws: %.1f allocs/frame, want 0", allocs) | |
| 64 | + | } | |
| 65 | + | // A content change under an unchanged scene Seq: partial redraw | |
| 66 | + | // from the window damage, clipped to the changed region. | |
| 67 | + | changed := [2]ports.SurfaceContent{content, content} | |
| 68 | + | changed[1].Seq, changed[1].Version = 2, 2 | |
| 69 | + | same := scene | |
| 70 | + | same.Seq = r.last.sceneSeq | |
| 71 | + | partial := func() { | |
| 72 | + | n++ | |
| 73 | + | contents[1] = changed[n%2] | |
| 74 | + | _ = r.draws(same, contents, r.frameDamage(r.last, same, bounds)) | |
| 75 | + | } | |
| 76 | + | partial() | |
| 77 | + | if allocs := testing.AllocsPerRun(50, partial); allocs != 0 { | |
| 78 | + | t.Errorf("content-damage draws: %.1f allocs/frame, want 0", allocs) | |
| 79 | + | } | |
| 44 | 80 | }) | |
| 45 | 81 | } | |
| 46 | 82 | } | |
| @@ -9,7 +9,10 @@ import ( | |||||
| 9 | ) | 9 | ) | ||
| 10 | 10 | ||||
| 11 | // TestRenderSteadyStateAllocations measures the cost of a small stable scene, | 11 | // TestRenderSteadyStateAllocations measures the cost of a small stable scene, | ||
| 12 | - | // on a normal and on a rotated output (orient works in place). | 12 | + | // on a normal and on a rotated output (orient works in place), and that |
| 13 | + | // building the draws of a scene with a moving window (a new Seq and window | |||
| 14 | + | // slice every frame, as an animation publishes) allocates nothing. | |||
| 15 | + | // TODO(A2): add the partial redraw seeded by sceneDelta once it is merged. | |||
| 13 | func TestRenderSteadyStateAllocations(t *testing.T) { | 16 | func TestRenderSteadyStateAllocations(t *testing.T) { | ||
| 14 | for _, tc := range []struct { | 17 | for _, tc := range []struct { | ||
| 15 | name string | 18 | name string | ||
| @@ -41,6 +44,39 @@ func TestRenderSteadyStateAllocations(t *testing.T) { | |||||
| 41 | } else { | 44 | } else { | ||
| 42 | t.Logf("small steady-state Render: %.1f allocs/frame", allocs) | 45 | t.Logf("small steady-state Render: %.1f allocs/frame", allocs) | ||
| 43 | } | 46 | } | ||
| 47 | + | // A moving window: scenes are prebuilt (core allocates them, not | |||
| 48 | + | // the renderer), each with a fresh Seq, so every frame draws | |||
| 49 | + | // everything again. The draw building must not allocate. | |||
| 50 | + | var moving [4]ports.Scene | |||
| 51 | + | for i := range moving { | |||
| 52 | + | moving[i] = ports.Scene{Seq: uint64(100 + i), Transform: tc.transform, Scale: 1, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{X: i, W: tc.sceneW - 4, H: tc.sceneH - 4}}}} | |||
| 53 | + | } | |||
| 54 | + | bounds := image.Rect(0, 0, r.width, r.height) | |||
| 55 | + | n := 0 | |||
| 56 | + | frameDraws := func() { | |||
| 57 | + | n++ | |||
| 58 | + | s := moving[n%len(moving)] | |||
| 59 | + | _ = r.draws(s, contents, r.frameDamage(r.last, s, bounds)) | |||
| 60 | + | } | |||
| 61 | + | frameDraws() | |||
| 62 | + | if allocs := testing.AllocsPerRun(50, frameDraws); allocs != 0 { | |||
| 63 | + | t.Errorf("moving window draws: %.1f allocs/frame, want 0", allocs) | |||
| 64 | + | } | |||
| 65 | + | // A content change under an unchanged scene Seq: partial redraw | |||
| 66 | + | // from the window damage, clipped to the changed region. | |||
| 67 | + | changed := [2]ports.SurfaceContent{content, content} | |||
| 68 | + | changed[1].Seq, changed[1].Version = 2, 2 | |||
| 69 | + | same := scene | |||
| 70 | + | same.Seq = r.last.sceneSeq | |||
| 71 | + | partial := func() { | |||
| 72 | + | n++ | |||
| 73 | + | contents[1] = changed[n%2] | |||
| 74 | + | _ = r.draws(same, contents, r.frameDamage(r.last, same, bounds)) | |||
| 75 | + | } | |||
| 76 | + | partial() | |||
| 77 | + | if allocs := testing.AllocsPerRun(50, partial); allocs != 0 { | |||
| 78 | + | t.Errorf("content-damage draws: %.1f allocs/frame, want 0", allocs) | |||
| 79 | + | } | |||
| 44 | }) | 80 | }) | ||
| 45 | } | 81 | } | ||
| 46 | } | 82 | } | ||
M internal/core/core.go +9 -2
| @@ -67,6 +67,8 @@ type binding struct { | |||
| 67 | 67 | key string | |
| 68 | 68 | } | |
| 69 | 69 | type Core struct { | |
| 70 | + | // overviewReal is publish's scratch map of the real layouts under an open overview. | |
| 71 | + | overviewReal map[WindowID]Placement | |
| 70 | 72 | security ports.SecurityState | |
| 71 | 73 | lockSurfaces []ports.LockSurfacePlacement | |
| 72 | 74 | lockFocus WindowID | |
| @@ -559,13 +561,18 @@ func (c *Core) publish(ctx context.Context) error { | |||
| 559 | 561 | if frame != (Rect{W: o.W, H: o.H}) { | |
| 560 | 562 | clip = frame | |
| 561 | 563 | } | |
| 562 | - | scene := ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0), Layers: shownLayers(sc)} | |
| 563 | 564 | // layout is what is drawn; settled (same indexes) is where the | |
| 564 | 565 | // windows are going, and alone sizes the configures. | |
| 565 | 566 | layout, settled := sc.shown, sc.settledLayout | |
| 567 | + | scene := ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0, len(layout)+len(c.popupOrder)), Layers: shownLayers(sc)} | |
| 566 | 568 | var real map[WindowID]Placement | |
| 567 | 569 | if sc.mon.ov.open { | |
| 568 | - | real = make(map[WindowID]Placement) | |
| 570 | + | // Local to this screen's build: reused, cleared each time. | |
| 571 | + | if c.overviewReal == nil { | |
| 572 | + | c.overviewReal = make(map[WindowID]Placement) | |
| 573 | + | } | |
| 574 | + | clear(c.overviewReal) | |
| 575 | + | real = c.overviewReal | |
| 569 | 576 | for w := range sc.mon.all() { | |
| 570 | 577 | for _, p := range w.Layout() { | |
| 571 | 578 | real[p.ID] = p | |
| @@ -67,6 +67,8 @@ type binding struct { | |||||
| 67 | key string | 67 | key string | ||
| 68 | } | 68 | } | ||
| 69 | type Core struct { | 69 | type Core struct { | ||
| 70 | + | // overviewReal is publish's scratch map of the real layouts under an open overview. | |||
| 71 | + | overviewReal map[WindowID]Placement | |||
| 70 | security ports.SecurityState | 72 | security ports.SecurityState | ||
| 71 | lockSurfaces []ports.LockSurfacePlacement | 73 | lockSurfaces []ports.LockSurfacePlacement | ||
| 72 | lockFocus WindowID | 74 | lockFocus WindowID | ||
| @@ -559,13 +561,18 @@ func (c *Core) publish(ctx context.Context) error { | |||||
| 559 | if frame != (Rect{W: o.W, H: o.H}) { | 561 | if frame != (Rect{W: o.W, H: o.H}) { | ||
| 560 | clip = frame | 562 | clip = frame | ||
| 561 | } | 563 | } | ||
| 562 | - | scene := ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0), Layers: shownLayers(sc)} | |||
| 563 | // layout is what is drawn; settled (same indexes) is where the | 564 | // layout is what is drawn; settled (same indexes) is where the | ||
| 564 | // windows are going, and alone sizes the configures. | 565 | // windows are going, and alone sizes the configures. | ||
| 565 | layout, settled := sc.shown, sc.settledLayout | 566 | layout, settled := sc.shown, sc.settledLayout | ||
| 567 | + | scene := ports.Scene{Security: c.security, Output: sc.name(), OutputWidth: o.W, OutputHeight: o.H, WorkspaceClip: clip, Scale: sc.scale, Transform: sc.transform, Off: sc.off, Background: c.cfg.Background.Color, Border: ports.Border{Width: c.cfg.Border.Width, Active: c.cfg.Border.Active, Inactive: c.cfg.Border.Inactive}, Windows: make([]ports.SceneWindow, 0, len(layout)+len(c.popupOrder)), Layers: shownLayers(sc)} | |||
| 566 | var real map[WindowID]Placement | 568 | var real map[WindowID]Placement | ||
| 567 | if sc.mon.ov.open { | 569 | if sc.mon.ov.open { | ||
| 568 | - | real = make(map[WindowID]Placement) | 570 | + | // Local to this screen's build: reused, cleared each time. |
| 571 | + | if c.overviewReal == nil { | |||
| 572 | + | c.overviewReal = make(map[WindowID]Placement) | |||
| 573 | + | } | |||
| 574 | + | clear(c.overviewReal) | |||
| 575 | + | real = c.overviewReal | |||
| 569 | for w := range sc.mon.all() { | 576 | for w := range sc.mon.all() { | ||
| 570 | for _, p := range w.Layout() { | 577 | for _, p := range w.Layout() { | ||
| 571 | real[p.ID] = p | 578 | real[p.ID] = p | ||
A internal/core/publish_alloc_test.go +82 -0
| @@ -0,0 +1,82 @@ | |||
| 1 | + | package core | |
| 2 | + | ||
| 3 | + | import ( | |
| 4 | + | "context" | |
| 5 | + | "testing" | |
| 6 | + | "time" | |
| 7 | + | ||
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |
| 9 | + | ) | |
| 10 | + | ||
| 11 | + | // publishAllocBudget is the allocations of one animation frame (step) on two | |
| 12 | + | // screens of six windows each, with camera and rect motions running. Every | |
| 13 | + | // scene sent is freshly built (immutable once sent), and the layouts are | |
| 14 | + | // rebuilt for every publish, so the floor is not 0. Measured: 43 before | |
| 15 | + | // presizing the scene windows and separators, 36 after. | |
| 16 | + | const publishAllocBudget = 36 | |
| 17 | + | ||
| 18 | + | // TestPublishAllocations pins the cost of publishing an animation frame. | |
| 19 | + | func TestPublishAllocations(t *testing.T) { | |
| 20 | + | // No Clock port: a mock clock would count its own bookkeeping, and the | |
| 21 | + | // fallback timer then is the system one. | |
| 22 | + | var cfg ports.Config | |
| 23 | + | cfg.Keyboard.CmdKey = "super" | |
| 24 | + | cfg.Layout.MaxColumns = 2 | |
| 25 | + | cfg.Border.Width = 2 | |
| 26 | + | cfg.Animations.On = true | |
| 27 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Commands: make(chan ports.ClientCommand, 64)}) | |
| 28 | + | if err != nil { | |
| 29 | + | t.Fatal(err) | |
| 30 | + | } | |
| 31 | + | c.addScreen(ports.OutputInfo{Name: "A", Width: 300, Height: 200}) | |
| 32 | + | c.addScreen(ports.OutputInfo{Name: "B", Width: 300, Height: 200}) | |
| 33 | + | t0 := time.Now() | |
| 34 | + | next := WindowID(1) | |
| 35 | + | for _, sc := range c.screens { | |
| 36 | + | for range 6 { | |
| 37 | + | sc.mon.AddWindow(next) | |
| 38 | + | next++ | |
| 39 | + | } | |
| 40 | + | sc.arrange() | |
| 41 | + | } | |
| 42 | + | c.refreshShown() | |
| 43 | + | for _, sc := range c.screens { | |
| 44 | + | m := sc.mon | |
| 45 | + | ws := m.Current() | |
| 46 | + | ws.motion = c.spring(viewSpring(100, 0), t0) | |
| 47 | + | ws.shift = 100 | |
| 48 | + | sc.rects = make(map[WindowID]rectMotion, 6) | |
| 49 | + | sp := viewSpring(100, 0) | |
| 50 | + | for _, p := range sc.settledLayout { | |
| 51 | + | sc.rects[p.ID] = rectMotion{ | |
| 52 | + | x: c.spring(sp, t0), y: c.spring(sp, t0), | |
| 53 | + | w: c.spring(sp, t0), h: c.spring(sp, t0), | |
| 54 | + | dx: 100, dy: 100, dw: 100, dh: 100, | |
| 55 | + | } | |
| 56 | + | } | |
| 57 | + | sc.rectsWS = ws | |
| 58 | + | } | |
| 59 | + | ctx := context.Background() | |
| 60 | + | step := func() { | |
| 61 | + | if err := c.step(ctx, nil); err != nil { | |
| 62 | + | t.Fatal(err) | |
| 63 | + | } | |
| 64 | + | <-c.ch.Scenes | |
| 65 | + | } | |
| 66 | + | step() | |
| 67 | + | for _, sc := range c.screens { | |
| 68 | + | if len(sc.rects) != 6 || !sc.mon.Current().motion.on || len(sc.shown) < 6 { | |
| 69 | + | t.Fatalf("setup: rects %d camera %v shown %d", len(sc.rects), sc.mon.Current().motion.on, len(sc.shown)) | |
| 70 | + | } | |
| 71 | + | } | |
| 72 | + | n := testing.AllocsPerRun(50, step) | |
| 73 | + | t.Logf("allocs per step = %v", n) | |
| 74 | + | for _, sc := range c.screens { | |
| 75 | + | if len(sc.rects) == 0 || !sc.mon.Current().motion.on { | |
| 76 | + | t.Fatalf("motions settled during the measure: rects %d", len(sc.rects)) | |
| 77 | + | } | |
| 78 | + | } | |
| 79 | + | if n > publishAllocBudget { | |
| 80 | + | t.Errorf("publish allocs per frame = %v, budget %d", n, publishAllocBudget) | |
| 81 | + | } | |
| 82 | + | } | |
| @@ -0,0 +1,82 @@ | |||||
| 1 | + | package core | |||
| 2 | + | ||||
| 3 | + | import ( | |||
| 4 | + | "context" | |||
| 5 | + | "testing" | |||
| 6 | + | "time" | |||
| 7 | + | ||||
| 8 | + | "github.com/bnema/neferwl/internal/ports" | |||
| 9 | + | ) | |||
| 10 | + | ||||
| 11 | + | // publishAllocBudget is the allocations of one animation frame (step) on two | |||
| 12 | + | // screens of six windows each, with camera and rect motions running. Every | |||
| 13 | + | // scene sent is freshly built (immutable once sent), and the layouts are | |||
| 14 | + | // rebuilt for every publish, so the floor is not 0. Measured: 43 before | |||
| 15 | + | // presizing the scene windows and separators, 36 after. | |||
| 16 | + | const publishAllocBudget = 36 | |||
| 17 | + | ||||
| 18 | + | // TestPublishAllocations pins the cost of publishing an animation frame. | |||
| 19 | + | func TestPublishAllocations(t *testing.T) { | |||
| 20 | + | // No Clock port: a mock clock would count its own bookkeeping, and the | |||
| 21 | + | // fallback timer then is the system one. | |||
| 22 | + | var cfg ports.Config | |||
| 23 | + | cfg.Keyboard.CmdKey = "super" | |||
| 24 | + | cfg.Layout.MaxColumns = 2 | |||
| 25 | + | cfg.Border.Width = 2 | |||
| 26 | + | cfg.Animations.On = true | |||
| 27 | + | c, err := New(cfg, Channels{Scenes: make(chan []ports.Scene, 1), Commands: make(chan ports.ClientCommand, 64)}) | |||
| 28 | + | if err != nil { | |||
| 29 | + | t.Fatal(err) | |||
| 30 | + | } | |||
| 31 | + | c.addScreen(ports.OutputInfo{Name: "A", Width: 300, Height: 200}) | |||
| 32 | + | c.addScreen(ports.OutputInfo{Name: "B", Width: 300, Height: 200}) | |||
| 33 | + | t0 := time.Now() | |||
| 34 | + | next := WindowID(1) | |||
| 35 | + | for _, sc := range c.screens { | |||
| 36 | + | for range 6 { | |||
| 37 | + | sc.mon.AddWindow(next) | |||
| 38 | + | next++ | |||
| 39 | + | } | |||
| 40 | + | sc.arrange() | |||
| 41 | + | } | |||
| 42 | + | c.refreshShown() | |||
| 43 | + | for _, sc := range c.screens { | |||
| 44 | + | m := sc.mon | |||
| 45 | + | ws := m.Current() | |||
| 46 | + | ws.motion = c.spring(viewSpring(100, 0), t0) | |||
| 47 | + | ws.shift = 100 | |||
| 48 | + | sc.rects = make(map[WindowID]rectMotion, 6) | |||
| 49 | + | sp := viewSpring(100, 0) | |||
| 50 | + | for _, p := range sc.settledLayout { | |||
| 51 | + | sc.rects[p.ID] = rectMotion{ | |||
| 52 | + | x: c.spring(sp, t0), y: c.spring(sp, t0), | |||
| 53 | + | w: c.spring(sp, t0), h: c.spring(sp, t0), | |||
| 54 | + | dx: 100, dy: 100, dw: 100, dh: 100, | |||
| 55 | + | } | |||
| 56 | + | } | |||
| 57 | + | sc.rectsWS = ws | |||
| 58 | + | } | |||
| 59 | + | ctx := context.Background() | |||
| 60 | + | step := func() { | |||
| 61 | + | if err := c.step(ctx, nil); err != nil { | |||
| 62 | + | t.Fatal(err) | |||
| 63 | + | } | |||
| 64 | + | <-c.ch.Scenes | |||
| 65 | + | } | |||
| 66 | + | step() | |||
| 67 | + | for _, sc := range c.screens { | |||
| 68 | + | if len(sc.rects) != 6 || !sc.mon.Current().motion.on || len(sc.shown) < 6 { | |||
| 69 | + | t.Fatalf("setup: rects %d camera %v shown %d", len(sc.rects), sc.mon.Current().motion.on, len(sc.shown)) | |||
| 70 | + | } | |||
| 71 | + | } | |||
| 72 | + | n := testing.AllocsPerRun(50, step) | |||
| 73 | + | t.Logf("allocs per step = %v", n) | |||
| 74 | + | for _, sc := range c.screens { | |||
| 75 | + | if len(sc.rects) == 0 || !sc.mon.Current().motion.on { | |||
| 76 | + | t.Fatalf("motions settled during the measure: rects %d", len(sc.rects)) | |||
| 77 | + | } | |||
| 78 | + | } | |||
| 79 | + | if n > publishAllocBudget { | |||
| 80 | + | t.Errorf("publish allocs per frame = %v, budget %d", n, publishAllocBudget) | |||
| 81 | + | } | |||
| 82 | + | } | |||
M internal/core/separators.go +5 -0
| @@ -38,6 +38,11 @@ func separators(ps []Placement, width, gap int, o Rect, lit bool) []ports.Separa | |||
| 38 | 38 | if p.Floating { | |
| 39 | 39 | id = p.ID | |
| 40 | 40 | } | |
| 41 | + | if out == nil { | |
| 42 | + | // One line per side of every tile, and the focused one's four | |
| 43 | + | // lit lines: the common case grows no further. | |
| 44 | + | out = make([]ports.Separator, 0, 4*len(ps)+4) | |
| 45 | + | } | |
| 41 | 46 | out = append(out, ports.Separator{Rect: r, Active: active, Window: id}) | |
| 42 | 47 | } | |
| 43 | 48 | var focused *Placement | |
| @@ -38,6 +38,11 @@ func separators(ps []Placement, width, gap int, o Rect, lit bool) []ports.Separa | |||||
| 38 | if p.Floating { | 38 | if p.Floating { | ||
| 39 | id = p.ID | 39 | id = p.ID | ||
| 40 | } | 40 | } | ||
| 41 | + | if out == nil { | |||
| 42 | + | // One line per side of every tile, and the focused one's four | |||
| 43 | + | // lit lines: the common case grows no further. | |||
| 44 | + | out = make([]ports.Separator, 0, 4*len(ps)+4) | |||
| 45 | + | } | |||
| 41 | out = append(out, ports.Separator{Rect: r, Active: active, Window: id}) | 46 | out = append(out, ports.Separator{Rect: r, Active: active, Window: id}) | ||
| 42 | } | 47 | } | ||
| 43 | var focused *Placement | 48 | var focused *Placement | ||