feat/data-dnd e05cb15

presented.go RawHistoryFolder

1package wayland
2
3import (
4	"slices"
5	"sync"
6	"time"
7
8	"github.com/bnema/neferwl/internal/adapters/outputkit/presented"
9	"github.com/bnema/neferwl/internal/ports"
10)
11
12// maxPresentRing bounds the reports waiting for the display goroutine. Output
13// queues are flushed every loop turn and hold no backlog, so the ring only
14// fills when the display loop is stalled; this sink alone then merges flips.
15const maxPresentRing = 64
16
17// mergeWarnEvery spaces the warnings about merged flips.
18const mergeWarnEvery = 10 * time.Second
19
20// presentSink is the ports.PresentationSink wayland offers to the outputs:
21// output owner goroutines Push reports, the display goroutine drains them and
22// paces frames. The ring is a transport queue between those goroutines, not
23// window state: the mutex guards only the slices, the pending flag and the
24// merge counters.
25type presentSink struct {
26	s *Server
27
28	mu          sync.Mutex
29	ring, spare []ports.OutputPresented
30	pending     bool // a drain is posted and has not taken the ring yet
31	// merged counts flips merged since the last warning;
32	// lastWarn is when that warning was logged.
33	merged       int
34	lastWarn     time.Time
35	drainFn      func()
36	scratchFlips map[string]bool
37}
38
39func newPresentSink(s *Server) *presentSink {
40	p := &presentSink{
41		s:            s,
42		ring:         make([]ports.OutputPresented, 0, maxPresentRing),
43		spare:        make([]ports.OutputPresented, 0, maxPresentRing),
44		scratchFlips: make(map[string]bool),
45	}
46	p.drainFn = p.drain
47	return p
48}
49
50// Presentation returns the sink outputs report to. Push never blocks.
51func (s *Server) Presentation() ports.PresentationSink { return s.sink }
52
53// Push queues a report for the display goroutine and wakes it; one wake
54// serves every report pushed before the drain runs. When the ring is full the
55// flip of the oldest report is carried or merged forward; merges are counted
56// and warned about at most every mergeWarnEvery.
57func (p *presentSink) Push(r ports.OutputPresented) {
58	p.mu.Lock()
59	warn, merged := p.enqueueLocked(r)
60	post := !p.pending
61	p.pending = true
62	p.mu.Unlock()
63	if warn {
64		p.s.log.Warn().Int("merged", merged).Msg("outputs report faster than the display loop drains; flips merged")
65	}
66	if post {
67		p.s.display.Post(p.drainFn)
68	}
69}
70
71// enqueueLocked appends r, making room first when the ring is full. It
72// reports whether to warn now and how many flips were merged since the last
73// warning.
74func (p *presentSink) enqueueLocked(r ports.OutputPresented) (warn bool, merged int) {
75	if len(p.ring) >= maxPresentRing {
76		if p.makeRoomLocked(&r) {
77			p.merged++
78		}
79		if now := p.s.clock.Now(); p.merged > 0 && now.Sub(p.lastWarn) >= mergeWarnEvery {
80			warn, merged = true, p.merged
81			p.merged, p.lastWarn = 0, now
82		}
83	}
84	p.ring = append(p.ring, r)
85	return warn, merged
86}
87
88// makeRoomLocked frees a ring slot for incoming without losing a flip: the
89// oldest report that a later report of the same output follows (in the ring,
90// or incoming itself) is removed and its flip carried into that report. A
91// flip-less successor takes the flip over; a successor with a flip merges it
92// (presented.Merge: the older feedback is discarded), which is what the
93// result counts.
94//
95// When no report has a successor, every ring entry and incoming belong to
96// distinct outputs: only possible with more than maxPresentRing outputs.
97// Nothing is dropped then; the ring grows by one, bounded by the number of
98// outputs, because the next report of any of them finds a successor.
99func (p *presentSink) makeRoomLocked(incoming *ports.OutputPresented) (merged bool) {
100	for i := range p.ring {
101		next := p.successorLocked(i, incoming)
102		if next == nil {
103			continue
104		}
105		switch old := p.ring[i].Flip; {
106		case old == nil:
107		case next.Flip == nil:
108			next.Flip = old
109		default:
110			presented.Merge(old, next.Flip)
111			merged = true
112		}
113		p.ring = slices.Delete(p.ring, i, i+1)
114		return merged
115	}
116	return false
117}
118
119// successorLocked returns the next report of ring[i]'s output: a later ring
120// entry, else incoming; nil when none follows.
121func (p *presentSink) successorLocked(i int, incoming *ports.OutputPresented) *ports.OutputPresented {
122	for j := i + 1; j < len(p.ring); j++ {
123		if p.ring[j].Output == p.ring[i].Output {
124			return &p.ring[j]
125		}
126	}
127	if incoming.Output == p.ring[i].Output {
128		return incoming
129	}
130	return nil
131}
132
133// drain runs on the display goroutine: it paces frames for every report that
134// arrived since the last drain as one tick, so a burst of flips fires one
135// burst of frame callbacks per output.
136func (p *presentSink) drain() {
137	p.mu.Lock()
138	batch := p.ring
139	p.ring, p.spare = p.spare[:0], nil
140	p.pending = false
141	p.mu.Unlock()
142	flipped := p.scratchFlips
143	clear(flipped)
144	for _, r := range batch {
145		flipped[r.Output] = flipped[r.Output] || paces(r)
146	}
147	if len(batch) > 0 {
148		p.s.paceTick(flipped, batch)
149	}
150	// The reports carry maps: do not retain them in the spare batch.
151	clear(batch)
152	p.mu.Lock()
153	p.spare = batch[:0]
154	p.mu.Unlock()
155}

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