fix(daemon): release benchmark fixtures per iteration
- commit
- 71b35042a5601ff8304fc8cc4ee1d9f0b7b03965
- author
- brice · · 2026-07-24 04:28 UTC
- parent
- cb1999a
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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 } | |||
| 64 | 64 | func (t *signalTimer) Stop() bool { return true } | |
| 65 | 65 | ||
| 66 | 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 | 72 | t.Helper() | |
| 68 | 73 | d := New(ptys, clk, slog.New(slog.NewTextHandler(io.Discard, nil))) | |
| 69 | 74 | d.serveCtx, d.serveCancel = context.WithCancel(context.Background()) | |
| 70 | 75 | 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 | + | } | |
| 73 | 80 | return d | |
| 74 | 81 | } | |
| 75 | 82 | ||
| @@ -368,7 +375,12 @@ func newCapturingTransport(t testing.TB) (*portsmocks.MockTransport, chan ports. | |||
| 368 | 375 | ||
| 369 | 376 | func newManualSessionWithPTYs(t testing.TB, ptys ...ports.PTY) (*Daemon, *session, *attachedClient, chan ports.Frame) { | |
| 370 | 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 | 384 | tr, sends := newCapturingTransport(t) | |
| 373 | 385 | ac := &attachedClient{tr: tr, output: newOutputStateStream(), size: domain.Size{Cols: 80, Rows: 24}} | |
| 374 | 386 | ac.initOverlays() | |
| @@ -387,7 +399,9 @@ func newManualSessionWithPTYs(t testing.TB, ptys ...ports.PTY) (*Daemon, *sessio | |||
| 387 | 399 | ac.setSession(sess) | |
| 388 | 400 | ac.keys = keys.NewRouter(d.clock, daemonKeyHandler{d: d, ac: ac}, nil) | |
| 389 | 401 | d.sessions[sess.id] = sess | |
| 390 | - | t.Cleanup(cancel) | |
| 402 | + | if registerCleanup { | |
| 403 | + | t.Cleanup(cancel) | |
| 404 | + | } | |
| 391 | 405 | return d, sess, ac, sends | |
| 392 | 406 | } | |
| 393 | 407 | ||
| @@ -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 d | 80 | 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] = sess | 401 | d.sessions[sess.id] = sess | ||
| 390 | - | t.Cleanup(cancel) | 402 | + | if registerCleanup { |
| 403 | + | t.Cleanup(cancel) | |||
| 404 | + | } | |||
| 391 | return d, sess, ac, sends | 405 | 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 | |||
| 2 | 2 | ||
| 3 | 3 | import ( | |
| 4 | 4 | "context" | |
| 5 | + | "errors" | |
| 5 | 6 | "fmt" | |
| 6 | 7 | "io" | |
| 7 | 8 | "sync" | |
| @@ -149,6 +150,33 @@ func TestPerformanceFixtureCellLimitedHistory(t *testing.T) { | |||
| 149 | 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 | 180 | func TestCountingOutputTransportCountsOpaquePayloadAndRejectsShortPayload(t *testing.T) { | |
| 153 | 181 | transport := &countingOutputTransport{} | |
| 154 | 182 | ||
| @@ -506,29 +534,39 @@ func benchmarkIncrementalSnapshotRepository(b *testing.B, scenario snapshotBench | |||
| 506 | 534 | b.ReportAllocs() | |
| 507 | 535 | b.ResetTimer() | |
| 508 | 536 | 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 | + | } | |
| 519 | 564 | 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() | |
| 529 | 566 | ||
| 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 | + | }() | |
| 532 | 570 | // B.Loop requires the timer to be running at the next iteration. | |
| 533 | 571 | b.StartTimer() | |
| 534 | 572 | } | |
| @@ -952,6 +990,7 @@ type performanceFixture struct { | |||
| 952 | 990 | resizedSize domain.Size | |
| 953 | 991 | pty *scriptedPerformancePTY | |
| 954 | 992 | clock *performanceClock | |
| 993 | + | killSession func(*session, uint8, bool) error | |
| 955 | 994 | resizeRequests uint64 | |
| 956 | 995 | resizeCommits uint64 | |
| 957 | 996 | resizeCommitNanos uint64 | |
| @@ -964,6 +1003,11 @@ type performanceFixture struct { | |||
| 964 | 1003 | } | |
| 965 | 1004 | ||
| 966 | 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 | 1011 | t.Helper() | |
| 968 | 1012 | if !config.size.Valid() { | |
| 969 | 1013 | config.size = domain.Size{Cols: 80, Rows: 24} | |
| @@ -980,7 +1024,7 @@ func newPerformanceFixture(t testing.TB, config performanceConfig) *performanceF | |||
| 980 | 1024 | pty := &scriptedPerformancePTY{} | |
| 981 | 1025 | ptys := make([]ports.PTY, config.tabs) | |
| 982 | 1026 | ptys[0] = pty | |
| 983 | - | d, sess, ac, _ := newManualSessionWithPTYs(t, ptys...) | |
| 1027 | + | d, sess, ac, _ := newManualSessionWithPTYsCleanup(t, registerCleanup, ptys...) | |
| 984 | 1028 | clock := &performanceClock{} | |
| 985 | 1029 | d.clock = clock | |
| 986 | 1030 | output := &countingOutputTransport{} | |
| @@ -999,12 +1043,15 @@ func newPerformanceFixture(t testing.TB, config performanceConfig) *performanceF | |||
| 999 | 1043 | snapshots: &countingSnapshotRepository{}, | |
| 1000 | 1044 | pty: pty, | |
| 1001 | 1045 | clock: clock, | |
| 1046 | + | killSession: d.killSession, | |
| 1002 | 1047 | liveWrites: [][]byte{[]byte("\x1b[1;1HA\x1b[2;2HA"), []byte("\x1b[1;1HB\x1b[2;2HB")}, | |
| 1003 | 1048 | resizeSizes: [2]domain.Size{{Cols: 100, Rows: 30}, config.size}, | |
| 1004 | 1049 | } | |
| 1005 | 1050 | WithSnapshotRepository(fixture.snapshots, nil)(d) | |
| 1006 | 1051 | d.startSnapshotEncodeWorker() | |
| 1007 | - | t.Cleanup(d.stopSnapshotEncodeWorker) | |
| 1052 | + | if registerCleanup { | |
| 1053 | + | t.Cleanup(d.stopSnapshotEncodeWorker) | |
| 1054 | + | } | |
| 1008 | 1055 | for tabIndex, tb := range sess.tabs { | |
| 1009 | 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 | 1076 | 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 | 1104 | func (f *performanceFixture) configureTab(tb *tab, tabIndex, paneCount, historyRows int, size domain.Size) { | |
| 1033 | 1105 | f.t.Helper() | |
| 1034 | 1106 | 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 every | 537 | + | 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 chunk | 540 | + | // 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.Size | 990 | resizedSize domain.Size | ||
| 953 | pty *scriptedPerformancePTY | 991 | pty *scriptedPerformancePTY | ||
| 954 | clock *performanceClock | 992 | clock *performanceClock | ||
| 993 | + | killSession func(*session, uint8, bool) error | |||
| 955 | resizeRequests uint64 | 994 | resizeRequests uint64 | ||
| 956 | resizeCommits uint64 | 995 | resizeCommits uint64 | ||
| 957 | resizeCommitNanos uint64 | 996 | 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] = pty | 1026 | 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 = clock | 1029 | 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 fixture | 1076 | 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() | ||