detached 4063d10

perf(headless): reuse the shows map between flips

commit
4063d10ea00e15bdb5ba658233b3d14a88c0c8f2
author
brice · · 2026-10-13 18:10 UTC
parent
bc00385
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
4 files · +65 -13
M Makefile +1 -1
@@ -69,7 +69,7 @@ adapter-imports:
6969 [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; }
7070 # Fast, mandatory guards for tiled content publications and SHM copies.
7171 perf-check:
72- CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/vulkan ./internal/adapters/wayland -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations)$$' -count=1
72+ CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations)$$' -count=1
7373 check: vet test arch fakes-check log-check perf-check
7474
7575 # On-demand SDR/HDR colour check with examples/testpattern on a headless
@@ -69,7 +69,7 @@ adapter-imports:
69 [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; }69 [ -z "$$bad" ] || { echo "$$bad"; echo 'adapter imports another adapter: use ports or a shared library (Makefile ADAPTER_LIBS)' >&2; exit 1; }
70 # Fast, mandatory guards for tiled content publications and SHM copies.70 # Fast, mandatory guards for tiled content publications and SHM copies.
71 perf-check:71 perf-check:
72- CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/vulkan ./internal/adapters/wayland -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations)$$' -count=172+ CGO_ENABLED=0 go test ./internal/core ./internal/app ./internal/adapters/drm ./internal/adapters/headless ./internal/adapters/vulkan ./internal/adapters/wayland -run '^(TestAnimateAllocations|TestPublishAllocations|TestPublishSubscribersAllocations|TestPublishStashSlideAllocations|TestPublishOverviewAllocations|TestPublishLeavingAllocations|TestOutputRoutingAllocations|TestReportSeenAllocations|TestFrameLifecycleTransitionsAllocations|TestFullscreenShownAllocations|TestFrameDecisionAllocations|TestCommitFrameAllocations|TestColorPipelineAllocations|TestShownBySnapshotAllocations|TestFlipShowsAllocations|TestMRUNoteFocusDoesNotAllocateWhenSettled|TestDueFramesAllocations|TestRenderSteadyStateAllocations|TestSceneOpaqueRegionAllocations|TestSceneDeltaAllocations|TestAccountFlipAllocations|TestFlipDoneAllocations|TestSceneWalkUnchangedTiledSHM|TestTiledCommitPublishAllocations|TestCapturedCommitApplyAllocations|TestCapturedViewportApplyAllocations|TestEffectiveInputEmptyTreeAllocations|TestHeadlessTiledSHMCallbackAndCopyBudget|TestReleaseHeldAllocations|TestCommitDamageAllocations|TestEmitContentAllocations|TestPresentationPushAllocations|TestAnimatingFrameSendAllocations|TestContentBatchCycleAllocations)$$' -count=1
73 check: vet test arch fakes-check log-check perf-check73 check: vet test arch fakes-check log-check perf-check
74 74
75 # On-demand SDR/HDR colour check with examples/testpattern on a headless75 # On-demand SDR/HDR colour check with examples/testpattern on a headless
A internal/adapters/headless/flip_alloc_test.go +28 -0
@@ -0,0 +1,28 @@
1+package headless
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/neferwl/internal/ports"
7+ "github.com/stretchr/testify/require"
8+)
9+
10+// Steady-state flip reporting: the Shows snapshot is reused while it does not
11+// change, and a changed one never mutates what an earlier flip handed out.
12+func TestFlipShowsAllocations(t *testing.T) {
13+ var f flipShows
14+ scene := ports.Scene{OutputWidth: 10, OutputHeight: 10, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 10, H: 10}}}}
15+ contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Seq: 1}, 2: {ID: 2, Seq: 2}}
16+ first := f.flipInfo(scene, contents, nil)
17+ require.Equal(t, map[ports.WindowID]uint64{1: 1}, first.Shows)
18+
19+ require.Zero(t, testing.AllocsPerRun(100, func() { f.of(scene, contents, nil) }), "unchanged shows")
20+ // The FlipInfo is the one allocation left: the sink's merge writes into it.
21+ require.LessOrEqual(t, testing.AllocsPerRun(100, func() { f.flipInfo(scene, contents, nil) }), 1.0, "flip report")
22+
23+ contents[1] = ports.SurfaceContent{ID: 1, Seq: 3}
24+ second := f.flipInfo(scene, contents, nil)
25+ require.Equal(t, map[ports.WindowID]uint64{1: 1}, first.Shows, "an earlier snapshot is immutable")
26+ require.Equal(t, map[ports.WindowID]uint64{1: 3}, second.Shows)
27+ require.NotSame(t, first, second)
28+}
@@ -0,0 +1,28 @@
1+package headless
2+
3+import (
4+ "testing"
5+
6+ "github.com/bnema/neferwl/internal/ports"
7+ "github.com/stretchr/testify/require"
8+)
9+
10+// Steady-state flip reporting: the Shows snapshot is reused while it does not
11+// change, and a changed one never mutates what an earlier flip handed out.
12+func TestFlipShowsAllocations(t *testing.T) {
13+ var f flipShows
14+ scene := ports.Scene{OutputWidth: 10, OutputHeight: 10, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 10, H: 10}}}}
15+ contents := map[ports.WindowID]ports.SurfaceContent{1: {ID: 1, Seq: 1}, 2: {ID: 2, Seq: 2}}
16+ first := f.flipInfo(scene, contents, nil)
17+ require.Equal(t, map[ports.WindowID]uint64{1: 1}, first.Shows)
18+
19+ require.Zero(t, testing.AllocsPerRun(100, func() { f.of(scene, contents, nil) }), "unchanged shows")
20+ // The FlipInfo is the one allocation left: the sink's merge writes into it.
21+ require.LessOrEqual(t, testing.AllocsPerRun(100, func() { f.flipInfo(scene, contents, nil) }), 1.0, "flip report")
22+
23+ contents[1] = ports.SurfaceContent{ID: 1, Seq: 3}
24+ second := f.flipInfo(scene, contents, nil)
25+ require.Equal(t, map[ports.WindowID]uint64{1: 1}, first.Shows, "an earlier snapshot is immutable")
26+ require.Equal(t, map[ports.WindowID]uint64{1: 3}, second.Shows)
27+ require.NotSame(t, first, second)
28+}
M internal/adapters/headless/leaving_test.go +3 -2
@@ -10,15 +10,16 @@ import (
1010 // A window drawn from a content its client withdrew (closed, fading out) is
1111 // not reported shown: the empty content has no frame to present.
1212 func TestFlipInfoSkipsKeptContent(t *testing.T) {
13+ var shows flipShows
1314 table := surfaces.New()
1415 s := ports.Scene{OutputWidth: 2, OutputHeight: 2, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 2, H: 2}}, {ID: 2, Rect: ports.Rect{W: 1, H: 1}}}}
1516 table.Update(ports.SurfaceContent{ID: 1, Seq: 3, SHM: &ports.SHMBuffer{Pool: 1}}, s)
1617 table.Update(ports.SurfaceContent{ID: 2, Seq: 1, SHM: &ports.SHMBuffer{Pool: 2}}, s)
17- if f := flipInfo(s, table.Map(), table.Kept); f.Shows[1] != 3 || f.Shows[2] != 1 {
18+ if f := shows.flipInfo(s, table.Map(), table.Kept); f.Shows[1] != 3 || f.Shows[2] != 1 {
1819 t.Fatalf("control: %v", f.Shows)
1920 }
2021 table.Update(ports.SurfaceContent{ID: 1, Seq: 4}, s)
21- f := flipInfo(s, table.Map(), table.Kept)
22+ f := shows.flipInfo(s, table.Map(), table.Kept)
2223 if _, ok := f.Shows[1]; ok || f.Shows[2] != 1 {
2324 t.Fatalf("a leaving window is reported shown: %v", f.Shows)
2425 }
@@ -10,15 +10,16 @@ import (
10 // A window drawn from a content its client withdrew (closed, fading out) is10 // A window drawn from a content its client withdrew (closed, fading out) is
11 // not reported shown: the empty content has no frame to present.11 // not reported shown: the empty content has no frame to present.
12 func TestFlipInfoSkipsKeptContent(t *testing.T) {12 func TestFlipInfoSkipsKeptContent(t *testing.T) {
13+ var shows flipShows
13 table := surfaces.New()14 table := surfaces.New()
14 s := ports.Scene{OutputWidth: 2, OutputHeight: 2, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 2, H: 2}}, {ID: 2, Rect: ports.Rect{W: 1, H: 1}}}}15 s := ports.Scene{OutputWidth: 2, OutputHeight: 2, Windows: []ports.SceneWindow{{ID: 1, Rect: ports.Rect{W: 2, H: 2}}, {ID: 2, Rect: ports.Rect{W: 1, H: 1}}}}
15 table.Update(ports.SurfaceContent{ID: 1, Seq: 3, SHM: &ports.SHMBuffer{Pool: 1}}, s)16 table.Update(ports.SurfaceContent{ID: 1, Seq: 3, SHM: &ports.SHMBuffer{Pool: 1}}, s)
16 table.Update(ports.SurfaceContent{ID: 2, Seq: 1, SHM: &ports.SHMBuffer{Pool: 2}}, s)17 table.Update(ports.SurfaceContent{ID: 2, Seq: 1, SHM: &ports.SHMBuffer{Pool: 2}}, s)
17- if f := flipInfo(s, table.Map(), table.Kept); f.Shows[1] != 3 || f.Shows[2] != 1 {18+ if f := shows.flipInfo(s, table.Map(), table.Kept); f.Shows[1] != 3 || f.Shows[2] != 1 {
18 t.Fatalf("control: %v", f.Shows)19 t.Fatalf("control: %v", f.Shows)
19 }20 }
20 table.Update(ports.SurfaceContent{ID: 1, Seq: 4}, s)21 table.Update(ports.SurfaceContent{ID: 1, Seq: 4}, s)
21- f := flipInfo(s, table.Map(), table.Kept)22+ f := shows.flipInfo(s, table.Map(), table.Kept)
22 if _, ok := f.Shows[1]; ok || f.Shows[2] != 1 {23 if _, ok := f.Shows[1]; ok || f.Shows[2] != 1 {
23 t.Fatalf("a leaving window is reported shown: %v", f.Shows)24 t.Fatalf("a leaving window is reported shown: %v", f.Shows)
24 }25 }
M internal/adapters/headless/output.go +33 -10
@@ -5,6 +5,7 @@ import (
55 "fmt"
66 "image"
77 "image/png"
8+ "maps"
89 "os"
910 "path/filepath"
1011 "time"
@@ -117,6 +118,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
117118 // reports queues what the output read; every report is pushed to the
118119 // sink at once (Push never blocks), so it holds one at most.
119120 var reports presented.Queue
121+ var shows flipShows
120122 var security ports.SecurityState
121123 securityKnown := false
122124 failRequests := func(reason error) {
@@ -541,7 +543,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
541543 pipeline.EndGate(nil)
542544 requests.List = capture.Waiting(requests.List, scene)
543545 frame++
544- opts.report(&reports, flipInfo(scene, drawn, table.Kept), seen)
546+ opts.report(&reports, shows.flipInfo(scene, drawn, table.Kept), seen)
545547 if scene.Animating && opts.Frames != nil {
546548 select {
547549 case opts.Frames <- ports.OutputFrame{Output: opts.Name}:
@@ -627,19 +629,40 @@ func (opts Options) report(q *presented.Queue, flip *ports.FlipInfo, seen map[po
627629 q.Flush(opts.Presented)
628630 }
629631
630-// flipInfo describes a drawn frame as a flip at the current CLOCK_MONOTONIC
631-// time (software clock, refresh unknown), so presentation feedback and
632-// frame callbacks follow headless frames.
633-func flipInfo(scene ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, kept func(ports.WindowID) bool) *ports.FlipInfo {
634- var ts unix.Timespec
635- _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts)
636- shows := map[ports.WindowID]uint64{}
632+// flipShows builds the Shows of successive flips. Its owner goroutine is the
633+// only user. A flip's Shows map is an immutable snapshot: wayland may still
634+// read the one of an earlier flip on another goroutine, so the scratch map is
635+// never sent and a snapshot is replaced by a clone only when it changed.
636+type flipShows struct {
637+ scratch, snapshot map[ports.WindowID]uint64
638+}
639+
640+// of returns the content Seq of the windows scene showed to their client. The
641+// result is shared with earlier flips of the same content: never modify it.
642+func (f *flipShows) of(scene ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, kept func(ports.WindowID) bool) map[ports.WindowID]uint64 {
643+ if f.scratch == nil {
644+ f.scratch = make(map[ports.WindowID]uint64)
645+ }
646+ clear(f.scratch)
637647 for id, c := range contents {
638648 // A closed window drawn from its kept content is not shown to
639649 // its client.
640650 if surfaces.Shown(scene, id, kept) {
641- shows[id] = c.Seq
651+ f.scratch[id] = c.Seq
642652 }
643653 }
644- return &ports.FlipInfo{When: time.Duration(ts.Nano()), Shows: shows}
654+ if f.snapshot == nil || !maps.Equal(f.snapshot, f.scratch) {
655+ f.snapshot = maps.Clone(f.scratch)
656+ }
657+ return f.snapshot
658+}
659+
660+// flipInfo describes a drawn frame as a flip at the current CLOCK_MONOTONIC
661+// time (software clock, refresh unknown), so presentation feedback and
662+// frame callbacks follow headless frames. The FlipInfo itself is new per
663+// flip (the sink counts merges on it); only its Shows is shared.
664+func (f *flipShows) flipInfo(scene ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, kept func(ports.WindowID) bool) *ports.FlipInfo {
665+ var ts unix.Timespec
666+ _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts)
667+ return &ports.FlipInfo{When: time.Duration(ts.Nano()), Shows: f.of(scene, contents, kept)}
645668 }
@@ -5,6 +5,7 @@ import (
5 "fmt"5 "fmt"
6 "image"6 "image"
7 "image/png"7 "image/png"
8+ "maps"
8 "os"9 "os"
9 "path/filepath"10 "path/filepath"
10 "time"11 "time"
@@ -117,6 +118,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
117 // reports queues what the output read; every report is pushed to the118 // reports queues what the output read; every report is pushed to the
118 // sink at once (Push never blocks), so it holds one at most.119 // sink at once (Push never blocks), so it holds one at most.
119 var reports presented.Queue120 var reports presented.Queue
121+ var shows flipShows
120 var security ports.SecurityState122 var security ports.SecurityState
121 securityKnown := false123 securityKnown := false
122 failRequests := func(reason error) {124 failRequests := func(reason error) {
@@ -541,7 +543,7 @@ func Run(ctx context.Context, opts Options, scenes <-chan ports.Scene, contents
541 pipeline.EndGate(nil)543 pipeline.EndGate(nil)
542 requests.List = capture.Waiting(requests.List, scene)544 requests.List = capture.Waiting(requests.List, scene)
543 frame++545 frame++
544- opts.report(&reports, flipInfo(scene, drawn, table.Kept), seen)546+ opts.report(&reports, shows.flipInfo(scene, drawn, table.Kept), seen)
545 if scene.Animating && opts.Frames != nil {547 if scene.Animating && opts.Frames != nil {
546 select {548 select {
547 case opts.Frames <- ports.OutputFrame{Output: opts.Name}:549 case opts.Frames <- ports.OutputFrame{Output: opts.Name}:
@@ -627,19 +629,40 @@ func (opts Options) report(q *presented.Queue, flip *ports.FlipInfo, seen map[po
627 q.Flush(opts.Presented)629 q.Flush(opts.Presented)
628 }630 }
629 631
630-// flipInfo describes a drawn frame as a flip at the current CLOCK_MONOTONIC632+// flipShows builds the Shows of successive flips. Its owner goroutine is the
631-// time (software clock, refresh unknown), so presentation feedback and633+// only user. A flip's Shows map is an immutable snapshot: wayland may still
632-// frame callbacks follow headless frames.634+// read the one of an earlier flip on another goroutine, so the scratch map is
633-func flipInfo(scene ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, kept func(ports.WindowID) bool) *ports.FlipInfo {635+// never sent and a snapshot is replaced by a clone only when it changed.
634- var ts unix.Timespec636+type flipShows struct {
635- _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts)637+ scratch, snapshot map[ports.WindowID]uint64
636- shows := map[ports.WindowID]uint64{}638+}
639+
640+// of returns the content Seq of the windows scene showed to their client. The
641+// result is shared with earlier flips of the same content: never modify it.
642+func (f *flipShows) of(scene ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, kept func(ports.WindowID) bool) map[ports.WindowID]uint64 {
643+ if f.scratch == nil {
644+ f.scratch = make(map[ports.WindowID]uint64)
645+ }
646+ clear(f.scratch)
637 for id, c := range contents {647 for id, c := range contents {
638 // A closed window drawn from its kept content is not shown to648 // A closed window drawn from its kept content is not shown to
639 // its client.649 // its client.
640 if surfaces.Shown(scene, id, kept) {650 if surfaces.Shown(scene, id, kept) {
641- shows[id] = c.Seq651+ f.scratch[id] = c.Seq
642 }652 }
643 }653 }
644- return &ports.FlipInfo{When: time.Duration(ts.Nano()), Shows: shows}654+ if f.snapshot == nil || !maps.Equal(f.snapshot, f.scratch) {
655+ f.snapshot = maps.Clone(f.scratch)
656+ }
657+ return f.snapshot
658+}
659+
660+// flipInfo describes a drawn frame as a flip at the current CLOCK_MONOTONIC
661+// time (software clock, refresh unknown), so presentation feedback and
662+// frame callbacks follow headless frames. The FlipInfo itself is new per
663+// flip (the sink counts merges on it); only its Shows is shared.
664+func (f *flipShows) flipInfo(scene ports.Scene, contents map[ports.WindowID]ports.SurfaceContent, kept func(ports.WindowID) bool) *ports.FlipInfo {
665+ var ts unix.Timespec
666+ _ = unix.ClockGettime(unix.CLOCK_MONOTONIC, &ts)
667+ return &ports.FlipInfo{When: time.Duration(ts.Nano()), Shows: f.of(scene, contents, kept)}
645 }668 }

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 25

Tags 10

↑↓ moveenter openesc close