detached 71b3504

fix(daemon): release benchmark fixtures per iteration

commit
71b35042a5601ff8304fc8cc4ee1d9f0b7b03965
author
brice · · 2026-07-24 04:28 UTC
parent
cb1999a
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +113 -27
M internal/usecase/daemon/daemon_test.go +18 -4
@@ -64,12 +64,19 @@ func (t *signalTimer) Reset(time.Duration) bool { return false }
6464 func (t *signalTimer) Stop() bool { return true }
6565
6666 func newTestDaemon(t testing.TB, ptys ports.PTYFactory, clk ports.Clock) *Daemon {
67+ t.Helper()
68+ return newTestDaemonWithCleanup(t, ptys, clk, true)
69+}
70+
71+func newTestDaemonWithCleanup(t testing.TB, ptys ports.PTYFactory, clk ports.Clock, registerCleanup bool) *Daemon {
6772 t.Helper()
6873 d := New(ptys, clk, slog.New(slog.NewTextHandler(io.Discard, nil)))
6974 d.serveCtx, d.serveCancel = context.WithCancel(context.Background())
7075 d.hardCtx, d.hardCancel = context.WithCancel(context.Background())
71- t.Cleanup(d.serveCancel)
72- t.Cleanup(d.hardCancel)
76+ if registerCleanup {
77+ t.Cleanup(d.serveCancel)
78+ t.Cleanup(d.hardCancel)
79+ }
7380 return d
7481 }
7582
@@ -368,7 +375,12 @@ func newCapturingTransport(t testing.TB) (*portsmocks.MockTransport, chan ports.
368375
369376 func newManualSessionWithPTYs(t testing.TB, ptys ...ports.PTY) (*Daemon, *session, *attachedClient, chan ports.Frame) {
370377 t.Helper()
371- d := newTestDaemon(t, nil, stubClock{})
378+ return newManualSessionWithPTYsCleanup(t, true, ptys...)
379+}
380+
381+func newManualSessionWithPTYsCleanup(t testing.TB, registerCleanup bool, ptys ...ports.PTY) (*Daemon, *session, *attachedClient, chan ports.Frame) {
382+ t.Helper()
383+ d := newTestDaemonWithCleanup(t, nil, stubClock{}, registerCleanup)
372384 tr, sends := newCapturingTransport(t)
373385 ac := &attachedClient{tr: tr, output: newOutputStateStream(), size: domain.Size{Cols: 80, Rows: 24}}
374386 ac.initOverlays()
@@ -387,7 +399,9 @@ func newManualSessionWithPTYs(t testing.TB, ptys ...ports.PTY) (*Daemon, *sessio
387399 ac.setSession(sess)
388400 ac.keys = keys.NewRouter(d.clock, daemonKeyHandler{d: d, ac: ac}, nil)
389401 d.sessions[sess.id] = sess
390- t.Cleanup(cancel)
402+ if registerCleanup {
403+ t.Cleanup(cancel)
404+ }
391405 return d, sess, ac, sends
392406 }
393407
@@ -64,12 +64,19 @@ func (t *signalTimer) Reset(time.Duration) bool { return false }
64 func (t *signalTimer) Stop() bool { return true }64 func (t *signalTimer) Stop() bool { return true }
65 65
66 func newTestDaemon(t testing.TB, ptys ports.PTYFactory, clk ports.Clock) *Daemon {66 func newTestDaemon(t testing.TB, ptys ports.PTYFactory, clk ports.Clock) *Daemon {
67+ t.Helper()
68+ return newTestDaemonWithCleanup(t, ptys, clk, true)
69+}
70+
71+func newTestDaemonWithCleanup(t testing.TB, ptys ports.PTYFactory, clk ports.Clock, registerCleanup bool) *Daemon {
67 t.Helper()72 t.Helper()
68 d := New(ptys, clk, slog.New(slog.NewTextHandler(io.Discard, nil)))73 d := New(ptys, clk, slog.New(slog.NewTextHandler(io.Discard, nil)))
69 d.serveCtx, d.serveCancel = context.WithCancel(context.Background())74 d.serveCtx, d.serveCancel = context.WithCancel(context.Background())
70 d.hardCtx, d.hardCancel = context.WithCancel(context.Background())75 d.hardCtx, d.hardCancel = context.WithCancel(context.Background())
71- t.Cleanup(d.serveCancel)76+ if registerCleanup {
72- t.Cleanup(d.hardCancel)77+ t.Cleanup(d.serveCancel)
78+ t.Cleanup(d.hardCancel)
79+ }
73 return d80 return d
74 }81 }
75 82
@@ -368,7 +375,12 @@ func newCapturingTransport(t testing.TB) (*portsmocks.MockTransport, chan ports.
368 375
369 func newManualSessionWithPTYs(t testing.TB, ptys ...ports.PTY) (*Daemon, *session, *attachedClient, chan ports.Frame) {376 func newManualSessionWithPTYs(t testing.TB, ptys ...ports.PTY) (*Daemon, *session, *attachedClient, chan ports.Frame) {
370 t.Helper()377 t.Helper()
371- d := newTestDaemon(t, nil, stubClock{})378+ return newManualSessionWithPTYsCleanup(t, true, ptys...)
379+}
380+
381+func newManualSessionWithPTYsCleanup(t testing.TB, registerCleanup bool, ptys ...ports.PTY) (*Daemon, *session, *attachedClient, chan ports.Frame) {
382+ t.Helper()
383+ d := newTestDaemonWithCleanup(t, nil, stubClock{}, registerCleanup)
372 tr, sends := newCapturingTransport(t)384 tr, sends := newCapturingTransport(t)
373 ac := &attachedClient{tr: tr, output: newOutputStateStream(), size: domain.Size{Cols: 80, Rows: 24}}385 ac := &attachedClient{tr: tr, output: newOutputStateStream(), size: domain.Size{Cols: 80, Rows: 24}}
374 ac.initOverlays()386 ac.initOverlays()
@@ -387,7 +399,9 @@ func newManualSessionWithPTYs(t testing.TB, ptys ...ports.PTY) (*Daemon, *sessio
387 ac.setSession(sess)399 ac.setSession(sess)
388 ac.keys = keys.NewRouter(d.clock, daemonKeyHandler{d: d, ac: ac}, nil)400 ac.keys = keys.NewRouter(d.clock, daemonKeyHandler{d: d, ac: ac}, nil)
389 d.sessions[sess.id] = sess401 d.sessions[sess.id] = sess
390- t.Cleanup(cancel)402+ if registerCleanup {
403+ t.Cleanup(cancel)
404+ }
391 return d, sess, ac, sends405 return d, sess, ac, sends
392 }406 }
393 407
M internal/usecase/daemon/performance_benchmark_test.go +95 -23
@@ -2,6 +2,7 @@ package daemon
22
33 import (
44 "context"
5+ "errors"
56 "fmt"
67 "io"
78 "sync"
@@ -149,6 +150,33 @@ func TestPerformanceFixtureCellLimitedHistory(t *testing.T) {
149150 require.LessOrEqual(t, cells, capCells)
150151 }
151152
153+func TestPerformanceFixtureExplicitCloseReleasesIterationState(t *testing.T) {
154+ fixture := newPerformanceFixtureWithCleanup(t, performanceConfig{}, false)
155+ d, sess := fixture.d, fixture.sess
156+ serveCtx, hardCtx := d.serveCtx, d.hardCtx
157+ killErr := errors.New("scripted kill failure")
158+ fixture.killSession = func(sess *session, reason uint8, purge bool) error {
159+ require.NoError(t, d.killSession(sess, reason, purge))
160+ return killErr
161+ }
162+
163+ err := fixture.close()
164+
165+ require.ErrorIs(t, err, killErr)
166+ require.Nil(t, fixture.d)
167+ require.Nil(t, fixture.sess)
168+ require.Nil(t, fixture.activePane)
169+ require.Error(t, sess.ctx.Err())
170+ require.Error(t, serveCtx.Err())
171+ require.Error(t, hardCtx.Err())
172+ d.mu.Lock()
173+ require.NotContains(t, d.sessions, sess.id)
174+ d.mu.Unlock()
175+ d.snapshotWorkerMu.Lock()
176+ require.Nil(t, d.snapshotWorkerCancel)
177+ d.snapshotWorkerMu.Unlock()
178+}
179+
152180 func TestCountingOutputTransportCountsOpaquePayloadAndRejectsShortPayload(t *testing.T) {
153181 transport := &countingOutputTransport{}
154182
@@ -506,29 +534,39 @@ func benchmarkIncrementalSnapshotRepository(b *testing.B, scenario snapshotBench
506534 b.ReportAllocs()
507535 b.ResetTimer()
508536 for operation := 0; b.Loop(); operation++ {
509- // A new fixture is deliberately outside the timed region. It gives every
510- // iteration the same filesystem state: initial has no generation,
511- // unchanged/visible/tail/sealed begin after one persisted generation.
512- // Reusing one fixture would eventually turn a tail into a sealed chunk
513- // and dilute initial-write bytes over later idempotent publications.
514- b.StopTimer()
515- fixture := newPerformanceFixture(b, performanceConfig{size: scenario.size, panes: 1, historyRows: scenario.historyRows})
516- validateSnapshotBenchmarkFixture(b, fixture, scenario)
517- generation := uint64(1)
518- if scenario.baseline {
537+ func() {
538+ // A new fixture is deliberately outside the timed region. It gives every
539+ // iteration the same filesystem state: initial has no generation,
540+ // unchanged/visible/tail/sealed begin after one persisted generation.
541+ // Reusing one fixture would eventually turn a tail into a sealed chunk
542+ // and dilute initial-write bytes over later idempotent publications.
543+ b.StopTimer()
544+ // b.Cleanup retains callbacks until the sub-benchmark ends; deferring an
545+ // explicit close avoids retaining every 10k-row fixture and OOM.
546+ fixture := newPerformanceFixtureWithCleanup(b, performanceConfig{size: scenario.size, panes: 1, historyRows: scenario.historyRows}, false)
547+ defer func() {
548+ b.StopTimer()
549+ if err := fixture.close(); err != nil {
550+ b.Fatalf("close performance fixture: %v", err)
551+ }
552+ }()
553+ validateSnapshotBenchmarkFixture(b, fixture, scenario)
554+ generation := uint64(1)
555+ if scenario.baseline {
556+ publishBenchmarkSnapshot(b, fixture, generation)
557+ generation++
558+ fixture.snapshots.reset()
559+ }
560+ b.StartTimer()
561+ if scenario.mutate != nil {
562+ scenario.mutate(fixture, operation)
563+ }
519564 publishBenchmarkSnapshot(b, fixture, generation)
520- generation++
521- fixture.snapshots.reset()
522- }
523- b.StartTimer()
524- if scenario.mutate != nil {
525- scenario.mutate(fixture, operation)
526- }
527- publishBenchmarkSnapshot(b, fixture, generation)
528- b.StopTimer()
565+ b.StopTimer()
529566
530- metrics = addCountingSnapshotMetrics(metrics, fixture.snapshots.metrics())
531- peakCacheBytes = max(peakCacheBytes, benchmarkSnapshotCacheBytes(fixture.sess))
567+ metrics = addCountingSnapshotMetrics(metrics, fixture.snapshots.metrics())
568+ peakCacheBytes = max(peakCacheBytes, benchmarkSnapshotCacheBytes(fixture.sess))
569+ }()
532570 // B.Loop requires the timer to be running at the next iteration.
533571 b.StartTimer()
534572 }
@@ -952,6 +990,7 @@ type performanceFixture struct {
952990 resizedSize domain.Size
953991 pty *scriptedPerformancePTY
954992 clock *performanceClock
993+ killSession func(*session, uint8, bool) error
955994 resizeRequests uint64
956995 resizeCommits uint64
957996 resizeCommitNanos uint64
@@ -964,6 +1003,11 @@ type performanceFixture struct {
9641003 }
9651004
9661005 func newPerformanceFixture(t testing.TB, config performanceConfig) *performanceFixture {
1006+ t.Helper()
1007+ return newPerformanceFixtureWithCleanup(t, config, true)
1008+}
1009+
1010+func newPerformanceFixtureWithCleanup(t testing.TB, config performanceConfig, registerCleanup bool) *performanceFixture {
9671011 t.Helper()
9681012 if !config.size.Valid() {
9691013 config.size = domain.Size{Cols: 80, Rows: 24}
@@ -980,7 +1024,7 @@ func newPerformanceFixture(t testing.TB, config performanceConfig) *performanceF
9801024 pty := &scriptedPerformancePTY{}
9811025 ptys := make([]ports.PTY, config.tabs)
9821026 ptys[0] = pty
983- d, sess, ac, _ := newManualSessionWithPTYs(t, ptys...)
1027+ d, sess, ac, _ := newManualSessionWithPTYsCleanup(t, registerCleanup, ptys...)
9841028 clock := &performanceClock{}
9851029 d.clock = clock
9861030 output := &countingOutputTransport{}
@@ -999,12 +1043,15 @@ func newPerformanceFixture(t testing.TB, config performanceConfig) *performanceF
9991043 snapshots: &countingSnapshotRepository{},
10001044 pty: pty,
10011045 clock: clock,
1046+ killSession: d.killSession,
10021047 liveWrites: [][]byte{[]byte("\x1b[1;1HA\x1b[2;2HA"), []byte("\x1b[1;1HB\x1b[2;2HB")},
10031048 resizeSizes: [2]domain.Size{{Cols: 100, Rows: 30}, config.size},
10041049 }
10051050 WithSnapshotRepository(fixture.snapshots, nil)(d)
10061051 d.startSnapshotEncodeWorker()
1007- t.Cleanup(d.stopSnapshotEncodeWorker)
1052+ if registerCleanup {
1053+ t.Cleanup(d.stopSnapshotEncodeWorker)
1054+ }
10081055 for tabIndex, tb := range sess.tabs {
10091056 fixture.configureTab(tb, tabIndex, config.panes, config.historyRows, tabSize(config.size))
10101057 }
@@ -1029,6 +1076,31 @@ func newPerformanceFixture(t testing.TB, config performanceConfig) *performanceF
10291076 return fixture
10301077 }
10311078
1079+func (f *performanceFixture) close() error {
1080+ if f.d == nil {
1081+ return nil
1082+ }
1083+ d, sess := f.d, f.sess
1084+ sess.mu.Lock()
1085+ sess.ephemeral = true
1086+ sess.mu.Unlock()
1087+ killErr := f.killSession(sess, ports.ReasonServerShutdown, false)
1088+ d.stopSnapshotEncodeWorker()
1089+ d.serveCancel()
1090+ d.hardCancel()
1091+
1092+ f.d = nil
1093+ f.sess = nil
1094+ f.ac = nil
1095+ f.output = nil
1096+ f.snapshots = nil
1097+ f.activePane = nil
1098+ f.pty = nil
1099+ f.clock = nil
1100+ f.killSession = nil
1101+ return killErr
1102+}
1103+
10321104 func (f *performanceFixture) configureTab(tb *tab, tabIndex, paneCount, historyRows int, size domain.Size) {
10331105 f.t.Helper()
10341106 tb.mu.Lock()
@@ -2,6 +2,7 @@ package daemon
2 2
3 import (3 import (
4 "context"4 "context"
5+ "errors"
5 "fmt"6 "fmt"
6 "io"7 "io"
7 "sync"8 "sync"
@@ -149,6 +150,33 @@ func TestPerformanceFixtureCellLimitedHistory(t *testing.T) {
149 require.LessOrEqual(t, cells, capCells)150 require.LessOrEqual(t, cells, capCells)
150 }151 }
151 152
153+func TestPerformanceFixtureExplicitCloseReleasesIterationState(t *testing.T) {
154+ fixture := newPerformanceFixtureWithCleanup(t, performanceConfig{}, false)
155+ d, sess := fixture.d, fixture.sess
156+ serveCtx, hardCtx := d.serveCtx, d.hardCtx
157+ killErr := errors.New("scripted kill failure")
158+ fixture.killSession = func(sess *session, reason uint8, purge bool) error {
159+ require.NoError(t, d.killSession(sess, reason, purge))
160+ return killErr
161+ }
162+
163+ err := fixture.close()
164+
165+ require.ErrorIs(t, err, killErr)
166+ require.Nil(t, fixture.d)
167+ require.Nil(t, fixture.sess)
168+ require.Nil(t, fixture.activePane)
169+ require.Error(t, sess.ctx.Err())
170+ require.Error(t, serveCtx.Err())
171+ require.Error(t, hardCtx.Err())
172+ d.mu.Lock()
173+ require.NotContains(t, d.sessions, sess.id)
174+ d.mu.Unlock()
175+ d.snapshotWorkerMu.Lock()
176+ require.Nil(t, d.snapshotWorkerCancel)
177+ d.snapshotWorkerMu.Unlock()
178+}
179+
152 func TestCountingOutputTransportCountsOpaquePayloadAndRejectsShortPayload(t *testing.T) {180 func TestCountingOutputTransportCountsOpaquePayloadAndRejectsShortPayload(t *testing.T) {
153 transport := &countingOutputTransport{}181 transport := &countingOutputTransport{}
154 182
@@ -506,29 +534,39 @@ func benchmarkIncrementalSnapshotRepository(b *testing.B, scenario snapshotBench
506 b.ReportAllocs()534 b.ReportAllocs()
507 b.ResetTimer()535 b.ResetTimer()
508 for operation := 0; b.Loop(); operation++ {536 for operation := 0; b.Loop(); operation++ {
509- // A new fixture is deliberately outside the timed region. It gives every537+ func() {
510- // iteration the same filesystem state: initial has no generation,538+ // A new fixture is deliberately outside the timed region. It gives every
511- // unchanged/visible/tail/sealed begin after one persisted generation.539+ // iteration the same filesystem state: initial has no generation,
512- // Reusing one fixture would eventually turn a tail into a sealed chunk540+ // unchanged/visible/tail/sealed begin after one persisted generation.
513- // and dilute initial-write bytes over later idempotent publications.541+ // Reusing one fixture would eventually turn a tail into a sealed chunk
514- b.StopTimer()542+ // and dilute initial-write bytes over later idempotent publications.
515- fixture := newPerformanceFixture(b, performanceConfig{size: scenario.size, panes: 1, historyRows: scenario.historyRows})543+ b.StopTimer()
516- validateSnapshotBenchmarkFixture(b, fixture, scenario)544+ // b.Cleanup retains callbacks until the sub-benchmark ends; deferring an
517- generation := uint64(1)545+ // explicit close avoids retaining every 10k-row fixture and OOM.
518- if scenario.baseline {546+ fixture := newPerformanceFixtureWithCleanup(b, performanceConfig{size: scenario.size, panes: 1, historyRows: scenario.historyRows}, false)
547+ defer func() {
548+ b.StopTimer()
549+ if err := fixture.close(); err != nil {
550+ b.Fatalf("close performance fixture: %v", err)
551+ }
552+ }()
553+ validateSnapshotBenchmarkFixture(b, fixture, scenario)
554+ generation := uint64(1)
555+ if scenario.baseline {
556+ publishBenchmarkSnapshot(b, fixture, generation)
557+ generation++
558+ fixture.snapshots.reset()
559+ }
560+ b.StartTimer()
561+ if scenario.mutate != nil {
562+ scenario.mutate(fixture, operation)
563+ }
519 publishBenchmarkSnapshot(b, fixture, generation)564 publishBenchmarkSnapshot(b, fixture, generation)
520- generation++565+ b.StopTimer()
521- fixture.snapshots.reset()
522- }
523- b.StartTimer()
524- if scenario.mutate != nil {
525- scenario.mutate(fixture, operation)
526- }
527- publishBenchmarkSnapshot(b, fixture, generation)
528- b.StopTimer()
529 566
530- metrics = addCountingSnapshotMetrics(metrics, fixture.snapshots.metrics())567+ metrics = addCountingSnapshotMetrics(metrics, fixture.snapshots.metrics())
531- peakCacheBytes = max(peakCacheBytes, benchmarkSnapshotCacheBytes(fixture.sess))568+ peakCacheBytes = max(peakCacheBytes, benchmarkSnapshotCacheBytes(fixture.sess))
569+ }()
532 // B.Loop requires the timer to be running at the next iteration.570 // B.Loop requires the timer to be running at the next iteration.
533 b.StartTimer()571 b.StartTimer()
534 }572 }
@@ -952,6 +990,7 @@ type performanceFixture struct {
952 resizedSize domain.Size990 resizedSize domain.Size
953 pty *scriptedPerformancePTY991 pty *scriptedPerformancePTY
954 clock *performanceClock992 clock *performanceClock
993+ killSession func(*session, uint8, bool) error
955 resizeRequests uint64994 resizeRequests uint64
956 resizeCommits uint64995 resizeCommits uint64
957 resizeCommitNanos uint64996 resizeCommitNanos uint64
@@ -964,6 +1003,11 @@ type performanceFixture struct {
964 }1003 }
965 1004
966 func newPerformanceFixture(t testing.TB, config performanceConfig) *performanceFixture {1005 func newPerformanceFixture(t testing.TB, config performanceConfig) *performanceFixture {
1006+ t.Helper()
1007+ return newPerformanceFixtureWithCleanup(t, config, true)
1008+}
1009+
1010+func newPerformanceFixtureWithCleanup(t testing.TB, config performanceConfig, registerCleanup bool) *performanceFixture {
967 t.Helper()1011 t.Helper()
968 if !config.size.Valid() {1012 if !config.size.Valid() {
969 config.size = domain.Size{Cols: 80, Rows: 24}1013 config.size = domain.Size{Cols: 80, Rows: 24}
@@ -980,7 +1024,7 @@ func newPerformanceFixture(t testing.TB, config performanceConfig) *performanceF
980 pty := &scriptedPerformancePTY{}1024 pty := &scriptedPerformancePTY{}
981 ptys := make([]ports.PTY, config.tabs)1025 ptys := make([]ports.PTY, config.tabs)
982 ptys[0] = pty1026 ptys[0] = pty
983- d, sess, ac, _ := newManualSessionWithPTYs(t, ptys...)1027+ d, sess, ac, _ := newManualSessionWithPTYsCleanup(t, registerCleanup, ptys...)
984 clock := &performanceClock{}1028 clock := &performanceClock{}
985 d.clock = clock1029 d.clock = clock
986 output := &countingOutputTransport{}1030 output := &countingOutputTransport{}
@@ -999,12 +1043,15 @@ func newPerformanceFixture(t testing.TB, config performanceConfig) *performanceF
999 snapshots: &countingSnapshotRepository{},1043 snapshots: &countingSnapshotRepository{},
1000 pty: pty,1044 pty: pty,
1001 clock: clock,1045 clock: clock,
1046+ killSession: d.killSession,
1002 liveWrites: [][]byte{[]byte("\x1b[1;1HA\x1b[2;2HA"), []byte("\x1b[1;1HB\x1b[2;2HB")},1047 liveWrites: [][]byte{[]byte("\x1b[1;1HA\x1b[2;2HA"), []byte("\x1b[1;1HB\x1b[2;2HB")},
1003 resizeSizes: [2]domain.Size{{Cols: 100, Rows: 30}, config.size},1048 resizeSizes: [2]domain.Size{{Cols: 100, Rows: 30}, config.size},
1004 }1049 }
1005 WithSnapshotRepository(fixture.snapshots, nil)(d)1050 WithSnapshotRepository(fixture.snapshots, nil)(d)
1006 d.startSnapshotEncodeWorker()1051 d.startSnapshotEncodeWorker()
1007- t.Cleanup(d.stopSnapshotEncodeWorker)1052+ if registerCleanup {
1053+ t.Cleanup(d.stopSnapshotEncodeWorker)
1054+ }
1008 for tabIndex, tb := range sess.tabs {1055 for tabIndex, tb := range sess.tabs {
1009 fixture.configureTab(tb, tabIndex, config.panes, config.historyRows, tabSize(config.size))1056 fixture.configureTab(tb, tabIndex, config.panes, config.historyRows, tabSize(config.size))
1010 }1057 }
@@ -1029,6 +1076,31 @@ func newPerformanceFixture(t testing.TB, config performanceConfig) *performanceF
1029 return fixture1076 return fixture
1030 }1077 }
1031 1078
1079+func (f *performanceFixture) close() error {
1080+ if f.d == nil {
1081+ return nil
1082+ }
1083+ d, sess := f.d, f.sess
1084+ sess.mu.Lock()
1085+ sess.ephemeral = true
1086+ sess.mu.Unlock()
1087+ killErr := f.killSession(sess, ports.ReasonServerShutdown, false)
1088+ d.stopSnapshotEncodeWorker()
1089+ d.serveCancel()
1090+ d.hardCancel()
1091+
1092+ f.d = nil
1093+ f.sess = nil
1094+ f.ac = nil
1095+ f.output = nil
1096+ f.snapshots = nil
1097+ f.activePane = nil
1098+ f.pty = nil
1099+ f.clock = nil
1100+ f.killSession = nil
1101+ return killErr
1102+}
1103+
1032 func (f *performanceFixture) configureTab(tb *tab, tabIndex, paneCount, historyRows int, size domain.Size) {1104 func (f *performanceFixture) configureTab(tb *tab, tabIndex, paneCount, historyRows int, size domain.Size) {
1033 f.t.Helper()1105 f.t.Helper()
1034 tb.mu.Lock()1106 tb.mu.Lock()

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 12

Tags 11

↑↓ moveenter openesc close