1package wayland
2
3import (
4 "context"
5 "testing"
6 "time"
7
8 "github.com/bnema/neferwl/internal/ports"
9 "github.com/stretchr/testify/mock"
10)
11
12// The steady-state hot paths that run for every frame or commit allocate
13// nothing (or one pooled object per damage-history generation).
14
15func TestReleaseHeldAllocations(t *testing.T) {
16 out := &output{place: ports.OutputPlacement{Info: ports.OutputInfo{Name: "DP-2", RefreshMilli: 60000}}}
17 s := &Server{outputs: []*output{out}, reports: map[string]ports.OutputPresented{}}
18 reports := []ports.OutputPresented{{Output: "DP-2", Seen: map[ports.WindowID]uint64{1: 1}}}
19 now := time.Unix(100, 0)
20 s.releaseHeld(now, reports)
21 if allocs := testing.AllocsPerRun(100, func() { s.releaseHeld(now, reports) }); allocs != 0 {
22 t.Errorf("releaseHeld: %.1f allocs, want 0", allocs)
23 }
24}
25
26func TestCommitDamageAllocations(t *testing.T) {
27 srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1)}
28 surf := &surface{server: srv}
29 commit := func() {
30 surf.Damage(nil, 0, 0, 4, 4)
31 surf.DamageBuffer(nil, 1, 1, 2, 2)
32 surf.queueUpdate()
33 surf.queue[0].applyGraph()
34 }
35 commit()
36 if allocs := testing.AllocsPerRun(100, commit); allocs != 0 {
37 t.Errorf("damage requests + queueUpdate + apply: %.1f allocs, want 0", allocs)
38 }
39 // The queued update keeps its rects while the next commit collects its own.
40 surf.Damage(nil, 0, 0, 1, 1)
41 surf.queueUpdate()
42 first := surf.queue[0]
43 surf.Damage(nil, 5, 5, 2, 2)
44 if len(first.damage) != 1 || first.damage[0].W != 1 || len(surf.next.damage) != 1 || surf.next.damage[0].W != 2 {
45 t.Fatalf("damage shared between commits: queued %v pending %v", first.damage, surf.next.damage)
46 }
47 surf.queueUpdate()
48 if second := surf.queue[1]; len(second.damage) != 1 || second.damage[0].W != 2 || first.damage[0].W != 1 {
49 t.Fatalf("damage mixed up: %v %v", first.damage, second.damage)
50 }
51}
52
53// An explicit-sync commit moves its points into the update: the commit
54// itself allocates no commitSync, and a pending one is not overwritten.
55func TestCommitSyncStorageIsPerUpdate(t *testing.T) {
56 dev := newMocksyncobjDevice(t)
57 dev.EXPECT().signalled(mock.Anything, mock.Anything).Return(true, nil)
58 srv := &Server{fifoSurfaces: map[*surface]struct{}{}, frameReady: make(chan struct{}, 1)}
59 sw, err := newSyncWaiter(func() {})
60 if err != nil {
61 t.Fatal(err)
62 }
63 defer sw.pipeR.Close()
64 defer sw.close()
65 srv.syncWait = sw
66 tl := &timeline{dev: dev, handle: 7, alive: true}
67 surf := &surface{server: srv, sync: &syncState{}}
68 take := func(point uint64) {
69 surf.sync.acquire, surf.sync.release = syncPoint{tl: tl, point: point}, syncPoint{tl: tl, point: point + 1}
70 surf.takeSyncPoints()
71 surf.queueUpdate()
72 }
73 take(10)
74 take(20)
75 a, b := surf.queue[0], surf.queue[1]
76 if a.sync == nil || b.sync == nil || a.sync == b.sync || a.sync.acquire.point != 10 || b.sync.acquire.point != 20 || surf.next.sync != nil {
77 t.Fatalf("sync points mixed up: %+v %+v pending %+v", a.sync, b.sync, surf.next.sync)
78 }
79 // The commit's points live in the surface until queueUpdate: no heap
80 // commitSync per commit (the mocked device's own allocations would drown
81 // an AllocsPerRun count here).
82 surf.sync.acquire, surf.sync.release = syncPoint{tl: tl, point: 30}, syncPoint{tl: tl, point: 31}
83 surf.takeSyncPoints()
84 if surf.next.sync != &surf.syncScratch {
85 t.Fatal("commit points not held in the surface scratch")
86 }
87}
88
89func TestEmitContentAllocations(t *testing.T) {
90 s := &Server{
91 channels: Channels{Contents: make(chan ports.SurfaceContent)},
92 contents: map[ports.WindowID]ports.SurfaceContent{},
93 contentSeq: map[ports.WindowID]uint64{},
94 damage: map[ports.WindowID][]ports.SeqDamage{},
95 contentReady: make(chan struct{}, 1),
96 }
97 c := ports.SurfaceContent{ID: 1, Solid: &ports.SolidColor{}}
98 var held []ports.SurfaceContent // readers retaining published histories
99 var snapshots [][]ports.SeqDamage
100 for range 3 * damageHistoryCap {
101 s.emitContent(c, damage{full: true})
102 got := s.contents[1]
103 held = append(held, got)
104 snapshots = append(snapshots, append([]ports.SeqDamage(nil), got.DamageHistory...))
105 }
106 for i, h := range held {
107 // A published history never changes, nor can its owner append to it.
108 if len(h.DamageHistory) != len(snapshots[i]) || cap(h.DamageHistory) != len(h.DamageHistory) {
109 t.Fatalf("history %d: len %d cap %d", i, len(h.DamageHistory), cap(h.DamageHistory))
110 }
111 for j, d := range h.DamageHistory {
112 if d.Seq != snapshots[i][j].Seq || d.Full != snapshots[i][j].Full {
113 t.Fatalf("history %d changed at %d: %+v want %+v", i, j, d, snapshots[i][j])
114 }
115 }
116 if n := len(h.DamageHistory); h.DamageHistory[n-1].Seq != h.Seq || n > damageHistory {
117 t.Fatalf("history %d: %+v", i, h.DamageHistory)
118 }
119 }
120 allocs := testing.AllocsPerRun(100, func() { s.emitContent(c, damage{full: true}) })
121 if allocs != 0 {
122 t.Errorf("emitContent: %.2f allocs, want 0 (amortized)", allocs)
123 }
124 t.Logf("emitContent: %.2f allocs", allocs)
125}
126
127// A forwarder blocked on a stale frame is woken by the next publication
128// and sends the newer frame instead.
129func TestForwardContentsSendsNewerFrame(t *testing.T) {
130 contents := make(chan ports.SurfaceContent)
131 s := &Server{
132 channels: Channels{Contents: contents},
133 contents: map[ports.WindowID]ports.SurfaceContent{},
134 contentSeq: map[ports.WindowID]uint64{},
135 damage: map[ports.WindowID][]ports.SeqDamage{},
136 contentReady: make(chan struct{}, 1),
137 }
138 ctx, cancel := context.WithCancel(context.Background())
139 done := make(chan struct{})
140 go func() { defer close(done); s.forwardContents(ctx) }()
141 defer func() { cancel(); <-done }()
142 s.emitContent(ports.SurfaceContent{ID: 1, Seq: 1, Solid: &ports.SolidColor{}}, damage{full: true})
143 time.Sleep(10 * time.Millisecond) // let the forwarder block on seq 1
144 s.emitContent(ports.SurfaceContent{ID: 1, Seq: 2, Solid: &ports.SolidColor{}}, damage{full: true})
145 select {
146 case c := <-contents:
147 if c.Seq != 2 {
148 t.Fatalf("stale frame %d forwarded, want 2", c.Seq)
149 }
150 case <-time.After(2 * time.Second):
151 t.Fatal("no frame forwarded")
152 }
153}