detached aff7050

Merge pull request #164 from bnema/perf/proxy-screen-baseline

perf(proxy): establish screen-stream baseline
commit
aff705062e29382938f3e1366e09349176cfbabf
author
bnema · · 2026-08-01 11:34 UTC
committer
GitHub · · 2026-08-01 11:34 UTC
merge of
919bcd1 + cce35a8 · diff against the first parent
signature
GPG UnverifiedB5690EEEBB952194 The key is unknown
13 files · +663 -45
M cmd/vev-perf-harness/evidence.go +15 -10
@@ -134,16 +134,21 @@ func pairedSamples(marks []harnessMark) ([]int64, error) {
134134 // traceRecord is intentionally local to the command. It mirrors JSONL fields
135135 // needed for correlation but never exposes a clock to any production component.
136136 type traceRecord struct {
137- ProcessID string `json:"process_id"`
138- Component string `json:"component"`
139- Scenario string `json:"scenario"`
140- Run uint64 `json:"run"`
141- Sequence uint64 `json:"sequence"`
142- RequestID uint64 `json:"request_id"`
143- Epoch uint64 `json:"epoch"`
144- Kind string `json:"kind"`
145- Tick int64 `json:"tick"`
146- Valid bool `json:"valid"`
137+ ProcessID string `json:"process_id"`
138+ Component string `json:"component"`
139+ Scenario string `json:"scenario"`
140+ Run uint64 `json:"run"`
141+ Sequence uint64 `json:"sequence"`
142+ RequestID uint64 `json:"request_id"`
143+ Epoch uint64 `json:"epoch"`
144+ Kind string `json:"kind"`
145+ Tick int64 `json:"tick"`
146+ Bytes uint64 `json:"bytes"`
147+ Fragments uint64 `json:"fragments"`
148+ Retransmits uint64 `json:"retransmits"`
149+ Pending uint64 `json:"pending"`
150+ AckRTTNanos int64 `json:"ack_rtt_nanos"`
151+ Valid bool `json:"valid"`
147152 }
148153
149154 type spanPair struct{ start, end, name string }
@@ -134,16 +134,21 @@ func pairedSamples(marks []harnessMark) ([]int64, error) {
134 // traceRecord is intentionally local to the command. It mirrors JSONL fields134 // traceRecord is intentionally local to the command. It mirrors JSONL fields
135 // needed for correlation but never exposes a clock to any production component.135 // needed for correlation but never exposes a clock to any production component.
136 type traceRecord struct {136 type traceRecord struct {
137- ProcessID string `json:"process_id"`137+ ProcessID string `json:"process_id"`
138- Component string `json:"component"`138+ Component string `json:"component"`
139- Scenario string `json:"scenario"`139+ Scenario string `json:"scenario"`
140- Run uint64 `json:"run"`140+ Run uint64 `json:"run"`
141- Sequence uint64 `json:"sequence"`141+ Sequence uint64 `json:"sequence"`
142- RequestID uint64 `json:"request_id"`142+ RequestID uint64 `json:"request_id"`
143- Epoch uint64 `json:"epoch"`143+ Epoch uint64 `json:"epoch"`
144- Kind string `json:"kind"`144+ Kind string `json:"kind"`
145- Tick int64 `json:"tick"`145+ Tick int64 `json:"tick"`
146- Valid bool `json:"valid"`146+ Bytes uint64 `json:"bytes"`
147+ Fragments uint64 `json:"fragments"`
148+ Retransmits uint64 `json:"retransmits"`
149+ Pending uint64 `json:"pending"`
150+ AckRTTNanos int64 `json:"ack_rtt_nanos"`
151+ Valid bool `json:"valid"`
147 }152 }
148 153
149 type spanPair struct{ start, end, name string }154 type spanPair struct{ start, end, name string }
M cmd/vev-perf-harness/evidence_test.go +23 -1
@@ -11,6 +11,8 @@ import (
1111 "strings"
1212 "testing"
1313 "time"
14+
15+ "github.com/stretchr/testify/require"
1416 )
1517
1618 func TestCLITransportSeamOwnsExclusivePeerTraceAndCleanup(t *testing.T) {
@@ -252,7 +254,13 @@ func TestHarnessWritesRequiredEvidenceWithSufficientSpans(t *testing.T) {
252254 h := defaultHarness()
253255 h.clock = &fakeClock{}
254256 h.launcher = l
255- h.gitSHA = func() string { return "test-sha" }
257+ h.gitSHA = func() string { return "harness-sha" }
258+ h.vevGitSHA = func(path string) string {
259+ if !filepath.IsAbs(path) {
260+ t.Fatalf("measured binary path is not resolved: %q", path)
261+ }
262+ return "measured-vev-sha"
263+ }
256264 if err := run([]string{"--vev-bin", "ignored", "--manifest", manifestPath, "--out", filepath.Join(dir, "out"), "--warmup", "1s", "--duration", "30s", "--repetitions", "10"}, h); err != nil {
257265 t.Fatal(err)
258266 }
@@ -269,6 +277,9 @@ func TestHarnessWritesRequiredEvidenceWithSufficientSpans(t *testing.T) {
269277 if err := json.Unmarshal(b, &got); err != nil {
270278 t.Fatal(err)
271279 }
280+ if got.GitSHA != "harness-sha" || got.VevGitSHA != "measured-vev-sha" {
281+ t.Fatalf("summary provenance = harness %q measured vev %q", got.GitSHA, got.VevGitSHA)
282+ }
272283 if got.EndToEnd.Count < 10*minimumInIntervalEventSamples || got.Repetitions != 10 || got.RunP50Dispersion.Count != 10 || got.Cadence.Count == 0 || got.MaxGap == 0 || len(got.Spans) == 0 {
273284 t.Fatalf("summary=%+v", got)
274285 }
@@ -279,6 +290,17 @@ func TestHarnessWritesRequiredEvidenceWithSufficientSpans(t *testing.T) {
279290 }
280291 }
281292
293+func TestTraceRecordRetainsTransportDiagnostics(t *testing.T) {
294+ line := []byte(`{"process_id":"p","component":"udp","scenario":"s","run":1,"sequence":1,"request_id":1,"epoch":1,"kind":"transport_diagnostic","tick":2,"bytes":3,"fragments":4,"retransmits":5,"pending":6,"ack_rtt_nanos":7,"valid":true}`)
295+ var got traceRecord
296+ require.NoError(t, json.Unmarshal(line, &got))
297+ require.Equal(t, uint64(3), got.Bytes)
298+ require.Equal(t, uint64(4), got.Fragments)
299+ require.Equal(t, uint64(5), got.Retransmits)
300+ require.Equal(t, uint64(6), got.Pending)
301+ require.Equal(t, int64(7), got.AckRTTNanos)
302+}
303+
282304 func TestHarnessAcceptsConcurrentProductionTraceInterleaving(t *testing.T) {
283305 path := filepath.Join(t.TempDir(), "local-client.jsonl")
284306 if err := os.WriteFile(path, []byte(boundedLocalConcurrentTrace), 0o600); err != nil {
@@ -11,6 +11,8 @@ import (
11 "strings"11 "strings"
12 "testing"12 "testing"
13 "time"13 "time"
14+
15+ "github.com/stretchr/testify/require"
14 )16 )
15 17
16 func TestCLITransportSeamOwnsExclusivePeerTraceAndCleanup(t *testing.T) {18 func TestCLITransportSeamOwnsExclusivePeerTraceAndCleanup(t *testing.T) {
@@ -252,7 +254,13 @@ func TestHarnessWritesRequiredEvidenceWithSufficientSpans(t *testing.T) {
252 h := defaultHarness()254 h := defaultHarness()
253 h.clock = &fakeClock{}255 h.clock = &fakeClock{}
254 h.launcher = l256 h.launcher = l
255- h.gitSHA = func() string { return "test-sha" }257+ h.gitSHA = func() string { return "harness-sha" }
258+ h.vevGitSHA = func(path string) string {
259+ if !filepath.IsAbs(path) {
260+ t.Fatalf("measured binary path is not resolved: %q", path)
261+ }
262+ return "measured-vev-sha"
263+ }
256 if err := run([]string{"--vev-bin", "ignored", "--manifest", manifestPath, "--out", filepath.Join(dir, "out"), "--warmup", "1s", "--duration", "30s", "--repetitions", "10"}, h); err != nil {264 if err := run([]string{"--vev-bin", "ignored", "--manifest", manifestPath, "--out", filepath.Join(dir, "out"), "--warmup", "1s", "--duration", "30s", "--repetitions", "10"}, h); err != nil {
257 t.Fatal(err)265 t.Fatal(err)
258 }266 }
@@ -269,6 +277,9 @@ func TestHarnessWritesRequiredEvidenceWithSufficientSpans(t *testing.T) {
269 if err := json.Unmarshal(b, &got); err != nil {277 if err := json.Unmarshal(b, &got); err != nil {
270 t.Fatal(err)278 t.Fatal(err)
271 }279 }
280+ if got.GitSHA != "harness-sha" || got.VevGitSHA != "measured-vev-sha" {
281+ t.Fatalf("summary provenance = harness %q measured vev %q", got.GitSHA, got.VevGitSHA)
282+ }
272 if got.EndToEnd.Count < 10*minimumInIntervalEventSamples || got.Repetitions != 10 || got.RunP50Dispersion.Count != 10 || got.Cadence.Count == 0 || got.MaxGap == 0 || len(got.Spans) == 0 {283 if got.EndToEnd.Count < 10*minimumInIntervalEventSamples || got.Repetitions != 10 || got.RunP50Dispersion.Count != 10 || got.Cadence.Count == 0 || got.MaxGap == 0 || len(got.Spans) == 0 {
273 t.Fatalf("summary=%+v", got)284 t.Fatalf("summary=%+v", got)
274 }285 }
@@ -279,6 +290,17 @@ func TestHarnessWritesRequiredEvidenceWithSufficientSpans(t *testing.T) {
279 }290 }
280 }291 }
281 292
293+func TestTraceRecordRetainsTransportDiagnostics(t *testing.T) {
294+ line := []byte(`{"process_id":"p","component":"udp","scenario":"s","run":1,"sequence":1,"request_id":1,"epoch":1,"kind":"transport_diagnostic","tick":2,"bytes":3,"fragments":4,"retransmits":5,"pending":6,"ack_rtt_nanos":7,"valid":true}`)
295+ var got traceRecord
296+ require.NoError(t, json.Unmarshal(line, &got))
297+ require.Equal(t, uint64(3), got.Bytes)
298+ require.Equal(t, uint64(4), got.Fragments)
299+ require.Equal(t, uint64(5), got.Retransmits)
300+ require.Equal(t, uint64(6), got.Pending)
301+ require.Equal(t, int64(7), got.AckRTTNanos)
302+}
303+
282 func TestHarnessAcceptsConcurrentProductionTraceInterleaving(t *testing.T) {304 func TestHarnessAcceptsConcurrentProductionTraceInterleaving(t *testing.T) {
283 path := filepath.Join(t.TempDir(), "local-client.jsonl")305 path := filepath.Join(t.TempDir(), "local-client.jsonl")
284 if err := os.WriteFile(path, []byte(boundedLocalConcurrentTrace), 0o600); err != nil {306 if err := os.WriteFile(path, []byte(boundedLocalConcurrentTrace), 0o600); err != nil {
M cmd/vev-perf-harness/launcher.go +25 -6
@@ -116,6 +116,8 @@ type terminalOutput interface {
116116 Close() error
117117 }
118118
119+var setPTYWinsize = rawterm.SetWinsize
120+
119121 type cliProcess struct {
120122 cmd *exec.Cmd
121123 pty io.ReadWriteCloser
@@ -135,19 +137,31 @@ type cliProcess struct {
135137 closeResult error
136138 }
137139
140+func (p *cliProcess) Resize(cols, rows uint16) error {
141+ fd, ok := p.pty.(interface{ Fd() uintptr })
142+ if !ok {
143+ return errors.New("client PTY does not expose a file descriptor")
144+ }
145+ return setPTYWinsize(int(fd.Fd()), cols, rows)
146+}
147+
138148 func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProcess, error) {
139149 if len(role.Args) == 0 {
140150 return nil, errors.New("public CLI role has no arguments")
141151 }
142- if info, err := os.Stat(l.bin); err != nil {
152+ bin, err := filepath.Abs(l.bin)
153+ if err != nil {
154+ return nil, err
155+ }
156+ if info, err := os.Stat(bin); err != nil {
143157 return nil, err
144158 } else if info.IsDir() {
145- return nil, fmt.Errorf("public CLI binary %q is a directory", l.bin)
159+ return nil, fmt.Errorf("public CLI binary %q is a directory", bin)
146160 }
147161 if m.Role == "ssh_stdio_peer" || m.Role == "udp_peer" {
148162 return l.preparePeer(m, role)
149163 }
150- cmd := exec.Command(l.bin, role.Args...)
164+ cmd := exec.Command(bin, role.Args...)
151165 runDir := filepath.Dir(m.TracePath)
152166 runtimeDir, err := l.runtimeDir(runDir)
153167 if err != nil {
@@ -157,9 +171,9 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces
157171 // a role working directory: daemon-owned subprocess cleanup may otherwise
158172 // treat the evidence directory as its working tree and remove a preallocated
159173 // trace while a later repetition is being merged.
160- cmd.Env = append(withoutEnv(os.Environ(), "VEV", "VEV_PERF_TRACE", "VEV_PERF_PROCESS_ID", "VEV_PERF_SCENARIO", "VEV_PERF_RUN", "VEV_REMOTE_TRANSPORT", "XDG_RUNTIME_DIR", "XDG_STATE_HOME"), traceEnvironment(m)...)
174+ cmd.Env = append(withoutEnv(os.Environ(), "VEV", "VEV_PERF_TRACE", "VEV_PERF_PROCESS_ID", "VEV_PERF_SCENARIO", "VEV_PERF_RUN", "VEV_PERF_BIN", "VEV_REMOTE_TRANSPORT", "XDG_RUNTIME_DIR", "XDG_STATE_HOME"), traceEnvironment(m)...)
161175 cmd.Env = append(cmd.Env, "XDG_RUNTIME_DIR="+runtimeDir,
162- "XDG_STATE_HOME="+filepath.Join(runDir, "state"), "TERM=xterm-256color")
176+ "XDG_STATE_HOME="+filepath.Join(runDir, "state"), "TERM=xterm-256color", "VEV_PERF_BIN="+bin)
163177 if m.Role == "client" {
164178 l.mu.Lock()
165179 peer, routed := l.peers[runDir]
@@ -184,6 +198,11 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces
184198 if err != nil {
185199 return nil, err
186200 }
201+ if err := setPTYWinsize(int(master.Fd()), 120, 40); err != nil {
202+ _ = master.Close()
203+ _ = slave.Close()
204+ return nil, fmt.Errorf("set canonical client PTY geometry: %w", err)
205+ }
187206 output, err := os.OpenFile(filepath.Join(filepath.Dir(m.TracePath), "terminal-output"), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o600)
188207 if err != nil {
189208 _ = master.Close()
@@ -236,7 +255,7 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces
236255 p.shutdown = func() error {
237256 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
238257 defer cancel()
239- shutdown := exec.CommandContext(ctx, l.bin, "kill", "--daemon")
258+ shutdown := exec.CommandContext(ctx, bin, "kill", "--daemon")
240259 shutdown.Env = withoutEnv(cmd.Env, "VEV_PERF_TRACE", "VEV_PERF_PROCESS_ID", "VEV_PERF_SCENARIO", "VEV_PERF_RUN")
241260 return shutdown.Run()
242261 }
@@ -116,6 +116,8 @@ type terminalOutput interface {
116 Close() error116 Close() error
117 }117 }
118 118
119+var setPTYWinsize = rawterm.SetWinsize
120+
119 type cliProcess struct {121 type cliProcess struct {
120 cmd *exec.Cmd122 cmd *exec.Cmd
121 pty io.ReadWriteCloser123 pty io.ReadWriteCloser
@@ -135,19 +137,31 @@ type cliProcess struct {
135 closeResult error137 closeResult error
136 }138 }
137 139
140+func (p *cliProcess) Resize(cols, rows uint16) error {
141+ fd, ok := p.pty.(interface{ Fd() uintptr })
142+ if !ok {
143+ return errors.New("client PTY does not expose a file descriptor")
144+ }
145+ return setPTYWinsize(int(fd.Fd()), cols, rows)
146+}
147+
138 func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProcess, error) {148 func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProcess, error) {
139 if len(role.Args) == 0 {149 if len(role.Args) == 0 {
140 return nil, errors.New("public CLI role has no arguments")150 return nil, errors.New("public CLI role has no arguments")
141 }151 }
142- if info, err := os.Stat(l.bin); err != nil {152+ bin, err := filepath.Abs(l.bin)
153+ if err != nil {
154+ return nil, err
155+ }
156+ if info, err := os.Stat(bin); err != nil {
143 return nil, err157 return nil, err
144 } else if info.IsDir() {158 } else if info.IsDir() {
145- return nil, fmt.Errorf("public CLI binary %q is a directory", l.bin)159+ return nil, fmt.Errorf("public CLI binary %q is a directory", bin)
146 }160 }
147 if m.Role == "ssh_stdio_peer" || m.Role == "udp_peer" {161 if m.Role == "ssh_stdio_peer" || m.Role == "udp_peer" {
148 return l.preparePeer(m, role)162 return l.preparePeer(m, role)
149 }163 }
150- cmd := exec.Command(l.bin, role.Args...)164+ cmd := exec.Command(bin, role.Args...)
151 runDir := filepath.Dir(m.TracePath)165 runDir := filepath.Dir(m.TracePath)
152 runtimeDir, err := l.runtimeDir(runDir)166 runtimeDir, err := l.runtimeDir(runDir)
153 if err != nil {167 if err != nil {
@@ -157,9 +171,9 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces
157 // a role working directory: daemon-owned subprocess cleanup may otherwise171 // a role working directory: daemon-owned subprocess cleanup may otherwise
158 // treat the evidence directory as its working tree and remove a preallocated172 // treat the evidence directory as its working tree and remove a preallocated
159 // trace while a later repetition is being merged.173 // trace while a later repetition is being merged.
160- cmd.Env = append(withoutEnv(os.Environ(), "VEV", "VEV_PERF_TRACE", "VEV_PERF_PROCESS_ID", "VEV_PERF_SCENARIO", "VEV_PERF_RUN", "VEV_REMOTE_TRANSPORT", "XDG_RUNTIME_DIR", "XDG_STATE_HOME"), traceEnvironment(m)...)174+ cmd.Env = append(withoutEnv(os.Environ(), "VEV", "VEV_PERF_TRACE", "VEV_PERF_PROCESS_ID", "VEV_PERF_SCENARIO", "VEV_PERF_RUN", "VEV_PERF_BIN", "VEV_REMOTE_TRANSPORT", "XDG_RUNTIME_DIR", "XDG_STATE_HOME"), traceEnvironment(m)...)
161 cmd.Env = append(cmd.Env, "XDG_RUNTIME_DIR="+runtimeDir,175 cmd.Env = append(cmd.Env, "XDG_RUNTIME_DIR="+runtimeDir,
162- "XDG_STATE_HOME="+filepath.Join(runDir, "state"), "TERM=xterm-256color")176+ "XDG_STATE_HOME="+filepath.Join(runDir, "state"), "TERM=xterm-256color", "VEV_PERF_BIN="+bin)
163 if m.Role == "client" {177 if m.Role == "client" {
164 l.mu.Lock()178 l.mu.Lock()
165 peer, routed := l.peers[runDir]179 peer, routed := l.peers[runDir]
@@ -184,6 +198,11 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces
184 if err != nil {198 if err != nil {
185 return nil, err199 return nil, err
186 }200 }
201+ if err := setPTYWinsize(int(master.Fd()), 120, 40); err != nil {
202+ _ = master.Close()
203+ _ = slave.Close()
204+ return nil, fmt.Errorf("set canonical client PTY geometry: %w", err)
205+ }
187 output, err := os.OpenFile(filepath.Join(filepath.Dir(m.TracePath), "terminal-output"), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o600)206 output, err := os.OpenFile(filepath.Join(filepath.Dir(m.TracePath), "terminal-output"), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o600)
188 if err != nil {207 if err != nil {
189 _ = master.Close()208 _ = master.Close()
@@ -236,7 +255,7 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces
236 p.shutdown = func() error {255 p.shutdown = func() error {
237 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)256 ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second)
238 defer cancel()257 defer cancel()
239- shutdown := exec.CommandContext(ctx, l.bin, "kill", "--daemon")258+ shutdown := exec.CommandContext(ctx, bin, "kill", "--daemon")
240 shutdown.Env = withoutEnv(cmd.Env, "VEV_PERF_TRACE", "VEV_PERF_PROCESS_ID", "VEV_PERF_SCENARIO", "VEV_PERF_RUN")259 shutdown.Env = withoutEnv(cmd.Env, "VEV_PERF_TRACE", "VEV_PERF_PROCESS_ID", "VEV_PERF_SCENARIO", "VEV_PERF_RUN")
241 return shutdown.Run()260 return shutdown.Run()
242 }261 }
M cmd/vev-perf-harness/launcher_test.go +114 -0
@@ -5,6 +5,7 @@ package main
55 import (
66 "bytes"
77 "context"
8+ "errors"
89 "fmt"
910 "net"
1011 "os"
@@ -12,6 +13,7 @@ import (
1213 "path/filepath"
1314 "strings"
1415 "sync"
16+ "syscall"
1517 "testing"
1618 "time"
1719
@@ -24,6 +26,118 @@ func (discardTerminalOutput) Write(p []byte) (int, error) { return len(p), nil }
2426 func (discardTerminalOutput) Sync() error { return nil }
2527 func (discardTerminalOutput) Close() error { return nil }
2628
29+type fakeFDPTY struct {
30+ *fakePTY
31+ fd uintptr
32+}
33+
34+func (p *fakeFDPTY) Fd() uintptr { return p.fd }
35+
36+func TestCLILauncherSetsCanonicalClientGeometryBeforeStart(t *testing.T) {
37+ original := setPTYWinsize
38+ t.Cleanup(func() { setPTYWinsize = original })
39+
40+ for _, tt := range []struct {
41+ name string
42+ setSize func(started string, fd int, cols, rows uint16) error
43+ wantErr bool
44+ }{
45+ {
46+ name: "starts after canonical geometry",
47+ setSize: func(started string, _ int, cols, rows uint16) error {
48+ if _, err := os.Stat(started); !errors.Is(err, os.ErrNotExist) {
49+ return fmt.Errorf("client started before geometry was set: %w", err)
50+ }
51+ if cols != 120 || rows != 40 {
52+ return fmt.Errorf("geometry = %dx%d, want 120x40", cols, rows)
53+ }
54+ return nil
55+ },
56+ },
57+ {
58+ name: "winsize failure closes PTY before returning",
59+ setSize: func(started string, fd int, _, _ uint16) error {
60+ if _, err := os.Stat(started); !errors.Is(err, os.ErrNotExist) {
61+ return fmt.Errorf("client started after winsize failure: %w", err)
62+ }
63+ return errTestWinsize
64+ },
65+ wantErr: true,
66+ },
67+ } {
68+ t.Run(tt.name, func(t *testing.T) {
69+ started := filepath.Join(t.TempDir(), "started")
70+ t.Setenv("STARTED", started)
71+ bin := filepath.Join(t.TempDir(), "client")
72+ if err := os.WriteFile(bin, []byte("#!/bin/sh\ntouch \"$STARTED\"\n"), 0o700); err != nil {
73+ t.Fatal(err)
74+ }
75+ runDir := filepath.Join(t.TempDir(), "run")
76+ if err := safedir.EnsurePrivate(runDir); err != nil {
77+ t.Fatal(err)
78+ }
79+ setFD := -1
80+ setPTYWinsize = func(fd int, cols, rows uint16) error {
81+ setFD = fd
82+ return tt.setSize(started, fd, cols, rows)
83+ }
84+ launcher := &cliLauncher{bin: bin}
85+ t.Cleanup(func() {
86+ if err := launcher.releaseRuntime(runDir); err != nil {
87+ t.Error(err)
88+ }
89+ })
90+ p, err := launcher.Launch(processMapping{Role: "client", TracePath: filepath.Join(runDir, "client.jsonl")}, roleCommand{Args: []string{"test"}})
91+ if tt.wantErr {
92+ if !errors.Is(err, errTestWinsize) || p != nil {
93+ t.Fatalf("Launch() = (%T, %v), want winsize error", p, err)
94+ }
95+ if setFD < 0 {
96+ t.Fatal("winsize setter was not called")
97+ }
98+ if closeErr := syscall.Close(setFD); !errors.Is(closeErr, syscall.EBADF) {
99+ t.Fatalf("winsize failure left PTY fd %d open: close error %v", setFD, closeErr)
100+ }
101+ return
102+ }
103+ if err != nil {
104+ t.Fatal(err)
105+ }
106+ if setFD < 0 {
107+ t.Fatal("winsize setter was not called before successful launch")
108+ }
109+ if err := p.Close(); err != nil {
110+ t.Fatal(err)
111+ }
112+ })
113+ }
114+}
115+
116+var errTestWinsize = errors.New("test winsize failure")
117+
118+func TestCLIProcessResize(t *testing.T) {
119+ original := setPTYWinsize
120+ t.Cleanup(func() { setPTYWinsize = original })
121+
122+ var gotFD int
123+ var gotCols, gotRows uint16
124+ setPTYWinsize = func(fd int, cols, rows uint16) error {
125+ gotFD, gotCols, gotRows = fd, cols, rows
126+ return nil
127+ }
128+ p := &cliProcess{pty: &fakeFDPTY{fakePTY: &fakePTY{}, fd: 42}}
129+ if err := p.Resize(119, 40); err != nil {
130+ t.Fatal(err)
131+ }
132+ if gotFD != 42 || gotCols != 119 || gotRows != 40 {
133+ t.Fatalf("winsize setter got fd=%d cols=%d rows=%d", gotFD, gotCols, gotRows)
134+ }
135+
136+ if err := (&cliProcess{pty: &fakePTY{}}).Resize(119, 40); err == nil || !strings.Contains(err.Error(), "file descriptor") {
137+ t.Fatalf("Resize without FD error = %v", err)
138+ }
139+}
140+
27141 func TestCLIProcessCloseDrainsQueuedTerminalOutputBeforeClientExit(t *testing.T) {
28142 harnessPTY, clientPTY := net.Pipe()
29143 t.Cleanup(func() { _ = clientPTY.Close() })
@@ -5,6 +5,7 @@ package main
5 import (5 import (
6 "bytes"6 "bytes"
7 "context"7 "context"
8+ "errors"
8 "fmt"9 "fmt"
9 "net"10 "net"
10 "os"11 "os"
@@ -12,6 +13,7 @@ import (
12 "path/filepath"13 "path/filepath"
13 "strings"14 "strings"
14 "sync"15 "sync"
16+ "syscall"
15 "testing"17 "testing"
16 "time"18 "time"
17 19
@@ -24,6 +26,118 @@ func (discardTerminalOutput) Write(p []byte) (int, error) { return len(p), nil }
24 func (discardTerminalOutput) Sync() error { return nil }26 func (discardTerminalOutput) Sync() error { return nil }
25 func (discardTerminalOutput) Close() error { return nil }27 func (discardTerminalOutput) Close() error { return nil }
26 28
29+type fakeFDPTY struct {
30+ *fakePTY
31+ fd uintptr
32+}
33+
34+func (p *fakeFDPTY) Fd() uintptr { return p.fd }
35+
36+func TestCLILauncherSetsCanonicalClientGeometryBeforeStart(t *testing.T) {
37+ original := setPTYWinsize
38+ t.Cleanup(func() { setPTYWinsize = original })
39+
40+ for _, tt := range []struct {
41+ name string
42+ setSize func(started string, fd int, cols, rows uint16) error
43+ wantErr bool
44+ }{
45+ {
46+ name: "starts after canonical geometry",
47+ setSize: func(started string, _ int, cols, rows uint16) error {
48+ if _, err := os.Stat(started); !errors.Is(err, os.ErrNotExist) {
49+ return fmt.Errorf("client started before geometry was set: %w", err)
50+ }
51+ if cols != 120 || rows != 40 {
52+ return fmt.Errorf("geometry = %dx%d, want 120x40", cols, rows)
53+ }
54+ return nil
55+ },
56+ },
57+ {
58+ name: "winsize failure closes PTY before returning",
59+ setSize: func(started string, fd int, _, _ uint16) error {
60+ if _, err := os.Stat(started); !errors.Is(err, os.ErrNotExist) {
61+ return fmt.Errorf("client started after winsize failure: %w", err)
62+ }
63+ return errTestWinsize
64+ },
65+ wantErr: true,
66+ },
67+ } {
68+ t.Run(tt.name, func(t *testing.T) {
69+ started := filepath.Join(t.TempDir(), "started")
70+ t.Setenv("STARTED", started)
71+ bin := filepath.Join(t.TempDir(), "client")
72+ if err := os.WriteFile(bin, []byte("#!/bin/sh\ntouch \"$STARTED\"\n"), 0o700); err != nil {
73+ t.Fatal(err)
74+ }
75+ runDir := filepath.Join(t.TempDir(), "run")
76+ if err := safedir.EnsurePrivate(runDir); err != nil {
77+ t.Fatal(err)
78+ }
79+ setFD := -1
80+ setPTYWinsize = func(fd int, cols, rows uint16) error {
81+ setFD = fd
82+ return tt.setSize(started, fd, cols, rows)
83+ }
84+ launcher := &cliLauncher{bin: bin}
85+ t.Cleanup(func() {
86+ if err := launcher.releaseRuntime(runDir); err != nil {
87+ t.Error(err)
88+ }
89+ })
90+ p, err := launcher.Launch(processMapping{Role: "client", TracePath: filepath.Join(runDir, "client.jsonl")}, roleCommand{Args: []string{"test"}})
91+ if tt.wantErr {
92+ if !errors.Is(err, errTestWinsize) || p != nil {
93+ t.Fatalf("Launch() = (%T, %v), want winsize error", p, err)
94+ }
95+ if setFD < 0 {
96+ t.Fatal("winsize setter was not called")
97+ }
98+ if closeErr := syscall.Close(setFD); !errors.Is(closeErr, syscall.EBADF) {
99+ t.Fatalf("winsize failure left PTY fd %d open: close error %v", setFD, closeErr)
100+ }
101+ return
102+ }
103+ if err != nil {
104+ t.Fatal(err)
105+ }
106+ if setFD < 0 {
107+ t.Fatal("winsize setter was not called before successful launch")
108+ }
109+ if err := p.Close(); err != nil {
110+ t.Fatal(err)
111+ }
112+ })
113+ }
114+}
115+
116+var errTestWinsize = errors.New("test winsize failure")
117+
118+func TestCLIProcessResize(t *testing.T) {
119+ original := setPTYWinsize
120+ t.Cleanup(func() { setPTYWinsize = original })
121+
122+ var gotFD int
123+ var gotCols, gotRows uint16
124+ setPTYWinsize = func(fd int, cols, rows uint16) error {
125+ gotFD, gotCols, gotRows = fd, cols, rows
126+ return nil
127+ }
128+ p := &cliProcess{pty: &fakeFDPTY{fakePTY: &fakePTY{}, fd: 42}}
129+ if err := p.Resize(119, 40); err != nil {
130+ t.Fatal(err)
131+ }
132+ if gotFD != 42 || gotCols != 119 || gotRows != 40 {
133+ t.Fatalf("winsize setter got fd=%d cols=%d rows=%d", gotFD, gotCols, gotRows)
134+ }
135+
136+ if err := (&cliProcess{pty: &fakePTY{}}).Resize(119, 40); err == nil || !strings.Contains(err.Error(), "file descriptor") {
137+ t.Fatalf("Resize without FD error = %v", err)
138+ }
139+}
140+
27 func TestCLIProcessCloseDrainsQueuedTerminalOutputBeforeClientExit(t *testing.T) {141 func TestCLIProcessCloseDrainsQueuedTerminalOutputBeforeClientExit(t *testing.T) {
28 harnessPTY, clientPTY := net.Pipe()142 harnessPTY, clientPTY := net.Pipe()
29 t.Cleanup(func() { _ = clientPTY.Close() })143 t.Cleanup(func() { _ = clientPTY.Close() })
M cmd/vev-perf-harness/main.go +1 -1
@@ -18,5 +18,5 @@ func main() {
1818 }
1919
2020 func defaultHarness() *harness {
21- return &harness{clock: systemClock{time.Now()}, mkdir: safedir.EnsurePrivate, createRunDir: createExclusiveRunDir, create: func(p string) (*os.File, error) { return os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) }, removeAll: os.RemoveAll, gitSHA: recordedGitSHA}
21+ return &harness{clock: systemClock{time.Now()}, mkdir: safedir.EnsurePrivate, createRunDir: createExclusiveRunDir, create: func(p string) (*os.File, error) { return os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) }, removeAll: os.RemoveAll, gitSHA: recordedGitSHA, vevGitSHA: recordedBinaryGitSHA}
2222 }
@@ -18,5 +18,5 @@ func main() {
18 }18 }
19 19
20 func defaultHarness() *harness {20 func defaultHarness() *harness {
21- return &harness{clock: systemClock{time.Now()}, mkdir: safedir.EnsurePrivate, createRunDir: createExclusiveRunDir, create: func(p string) (*os.File, error) { return os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) }, removeAll: os.RemoveAll, gitSHA: recordedGitSHA}21+ return &harness{clock: systemClock{time.Now()}, mkdir: safedir.EnsurePrivate, createRunDir: createExclusiveRunDir, create: func(p string) (*os.File, error) { return os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600) }, removeAll: os.RemoveAll, gitSHA: recordedGitSHA, vevGitSHA: recordedBinaryGitSHA}
22 }22 }
M cmd/vev-perf-harness/model.go +1 -0
@@ -99,6 +99,7 @@ type runResult struct {
9999 type summary struct {
100100 Schema uint16 `json:"schema"`
101101 GitSHA string `json:"git_sha"`
102+ VevGitSHA string `json:"vev_git_sha"`
102103 Warmup string `json:"warmup"`
103104 Duration string `json:"duration"`
104105 Repetitions int `json:"repetitions"`
@@ -99,6 +99,7 @@ type runResult struct {
99 type summary struct {99 type summary struct {
100 Schema uint16 `json:"schema"`100 Schema uint16 `json:"schema"`
101 GitSHA string `json:"git_sha"`101 GitSHA string `json:"git_sha"`
102+ VevGitSHA string `json:"vev_git_sha"`
102 Warmup string `json:"warmup"`103 Warmup string `json:"warmup"`
103 Duration string `json:"duration"`104 Duration string `json:"duration"`
104 Repetitions int `json:"repetitions"`105 Repetitions int `json:"repetitions"`
M cmd/vev-perf-harness/runner.go +79 -7
@@ -4,6 +4,7 @@ package main
44
55 import (
66 "context"
7+ "debug/buildinfo"
78 "encoding/json"
89 "errors"
910 "fmt"
@@ -13,6 +14,7 @@ import (
1314 "path/filepath"
1415 "runtime/debug"
1516 "sort"
17+ "strconv"
1618 "strings"
1719 "time"
1820
@@ -51,6 +53,7 @@ type harness struct {
5153 create func(string) (*os.File, error)
5254 removeAll func(string) error
5355 gitSHA func() string
56+ vevGitSHA func(string) string
5457 selectScenarios func(manifest) []scenario // test-only bounded fixture seam
5558 }
5659
@@ -71,6 +74,19 @@ func createExclusiveRunDir(path string) error {
7174 // go run evidence attributable as well.
7275 const gitSHACommandTimeout = time.Second
7376
77+func recordedBinaryGitSHA(path string) string {
78+ info, err := buildinfo.ReadFile(path)
79+ if err != nil {
80+ return "unknown"
81+ }
82+ for _, setting := range info.Settings {
83+ if setting.Key == "vcs.revision" && setting.Value != "" {
84+ return setting.Value
85+ }
86+ }
87+ return "unknown"
88+}
89+
7490 func recordedGitSHA() string {
7591 if info, ok := debug.ReadBuildInfo(); ok {
7692 for _, setting := range info.Settings {
@@ -148,7 +164,7 @@ func run(args []string, h *harness) (err error) {
148164 if err := writeJSON(filepath.Join(opt.out, "runs.json"), results); err != nil {
149165 return err
150166 }
151- return writeJSON(filepath.Join(opt.out, "summary.json"), summary{Schema: 1, GitSHA: h.gitSHA(), Warmup: opt.warmup.String(), Duration: opt.duration.String(), Repetitions: opt.repetitions, EndToEnd: percentiles(all), Cadence: percentiles(cadence), MaxGap: maxGap, RunP50Dispersion: percentiles(runP50s), Spans: summarizeSpans(localSpans), Runs: len(results)})
167+ return writeJSON(filepath.Join(opt.out, "summary.json"), summary{Schema: 1, GitSHA: h.gitSHA(), VevGitSHA: h.vevGitSHA(opt.vevBin), Warmup: opt.warmup.String(), Duration: opt.duration.String(), Repetitions: opt.repetitions, EndToEnd: percentiles(all), Cadence: percentiles(cadence), MaxGap: maxGap, RunP50Dispersion: percentiles(runP50s), Spans: summarizeSpans(localSpans), Runs: len(results)})
152168 }
153169
154170 // aggregateEventResults deliberately preserves raw event latencies for
@@ -307,7 +323,22 @@ func (h *harness) runOne(o options, mat manifest, s scenario, run int, raw io.Wr
307323 flushedRecorded = true
308324 return nil
309325 }
310- if measureErr := p.process.Measure(workloadInput(s, run, fmt.Sprintf("measured-%d", sequence)), injected, flushed); measureErr != nil {
326+ measureInjected := injected
327+ if cols, rows, resize := workloadResize(s, sequence); resize {
328+ resizer, ok := p.process.(processResizer)
329+ if !ok {
330+ return res, errors.New("client process does not support resize workload")
331+ }
332+ if err := injected(); err != nil {
333+ return res, err
334+ }
335+ measureInjected = func() error { return nil }
336+ if resizeErr := resizer.Resize(cols, rows); resizeErr != nil {
337+ diagnosticErr := recordMark(harnessMark{Scenario: s.ID, Run: run, Sequence: sequence, Kind: "terminal_flushed", Tick: h.clock.Now(), Valid: false})
338+ return res, errors.Join(fmt.Errorf("resize client PTY: %w", resizeErr), diagnosticErr)
339+ }
340+ }
341+ if measureErr := p.process.Measure(workloadInput(s, run, fmt.Sprintf("measured-%d", sequence)), measureInjected, flushed); measureErr != nil {
311342 if injectedRecorded && !flushedRecorded {
312343 diagnosticErr := recordMark(harnessMark{Scenario: s.ID, Run: run, Sequence: sequence, Kind: "terminal_flushed", Tick: h.clock.Now(), Valid: false})
313344 return res, errors.Join(measureErr, diagnosticErr)
@@ -418,6 +449,20 @@ func launchOrder(mappings []processMapping) []processMapping {
418449 return out
419450 }
420451
452+type processResizer interface {
453+ Resize(cols, rows uint16) error
454+}
455+
456+func workloadResize(s scenario, sequence uint64) (uint16, uint16, bool) {
457+ if s.Workload != "resize_sweep" && s.Workload != "snapshot_output_resize" {
458+ return 0, 0, false
459+ }
460+ if sequence%2 == 1 {
461+ return 119, 40, true
462+ }
463+ return 120, 40, true
464+}
465+
421466 // workloadInput is shell input delivered through the client PTY. It avoids any
422467 // private daemon API and makes every topology/workload/transport manifest row
423468 // execute a real terminal workload. The marker is the output boundary awaited
@@ -425,17 +470,44 @@ func launchOrder(mappings []processMapping) []processMapping {
425470 func workloadInput(s scenario, run int, phase string) []byte {
426471 marker := fmt.Sprintf("__VEV_HARNESS_%s_r%d_%s__", safeName(s.ID), run, safeName(phase))
427472 body := "printf 'vev perf %s\\n'"
473+ markerSeparator := "; "
428474 switch s.Workload {
429- case "active_output", "all_output", "inactive_output", "interactive_flood":
475+ case "active_output":
476+ cell := byte('X')
477+ if sequence, err := strconv.ParseUint(strings.TrimPrefix(phase, "measured-"), 10, 64); err == nil && strings.HasPrefix(phase, "measured-") {
478+ cell = '0' + byte(sequence%2)
479+ }
480+ body = fmt.Sprintf(`printf '\033[10;20H%c'; : '%%s'`, cell)
481+ case "all_output":
482+ body = `printf '%%-120s' 'vev perf line %s'`
483+ case "inactive_output":
484+ body = `printf '\033[38;2;20;120;220mred'; printf '\033[38;2;220;80;40mgreen'; printf '\033[38;2;80;220;120m%s'; printf '\033[0m'`
485+ case "interactive_flood":
430486 body = "i=0; while [ $i -lt 128 ]; do printf 'vev perf output %s\\n'; i=$((i+1)); done"
431487 case "resize_sweep":
432- body = "printf 'vev perf resize-sweep %s\\n'"
488+ cols, _ := resizeWorkloadGeometry(s, phase)
489+ body = fmt.Sprintf(`i=0; while [ "$i" -lt 100 ]; do set -- $(stty size); [ "$2" = "%d" ] && break; i=$((i+1)); sleep 0.01; done; set -- $(stty size); [ "$2" = "%d" ] && printf 'vev perf resize-sweep %%s\n'`, cols, cols)
490+ markerSeparator = " && "
433491 case "copy_search":
434492 body = "printf 'vev perf copy-search %s\\n'"
435493 case "snapshot_output_resize":
436- body = "printf 'vev perf snapshot-output-resize %s\\n'"
494+ cols, _ := resizeWorkloadGeometry(s, phase)
495+ body = fmt.Sprintf(`i=0; while [ "$i" -lt 100 ]; do set -- $(stty size); [ "$2" = "%d" ] && break; i=$((i+1)); sleep 0.01; done; set -- $(stty size); [ "$2" = "%d" ] && printf '\033[2J\033[Hvev perf snapshot-output-resize %%s\n'`, cols, cols)
496+ markerSeparator = " && "
437497 case "attach_restore_tab_switch":
438- body = "printf 'vev perf attach-restore-tab-switch %s\\n'"
498+ body = `"$VEV_PERF_BIN" cmd next-tab && "$VEV_PERF_BIN" cmd previous-tab && printf 'vev perf attach-restore-tab-switch %s\n'`
499+ if phase == "warmup" {
500+ body = `"$VEV_PERF_BIN" cmd new-tab && "$VEV_PERF_BIN" cmd previous-tab && printf 'vev perf attach-restore-tab-switch %s\n'`
501+ }
502+ markerSeparator = " && "
503+ }
504+ return []byte(fmt.Sprintf(body+markerSeparator+"printf '%s\\n'\n", s.ID, marker))
505+}
506+
507+func resizeWorkloadGeometry(s scenario, phase string) (uint16, uint16) {
508+ if sequence, err := strconv.ParseUint(strings.TrimPrefix(phase, "measured-"), 10, 64); err == nil && strings.HasPrefix(phase, "measured-") {
509+ cols, rows, _ := workloadResize(s, sequence)
510+ return cols, rows
439511 }
440- return []byte(fmt.Sprintf(body+"; printf '%s\\n'\n", s.ID, marker))
512+ return 120, 40
441513 }
@@ -4,6 +4,7 @@ package main
4 4
5 import (5 import (
6 "context"6 "context"
7+ "debug/buildinfo"
7 "encoding/json"8 "encoding/json"
8 "errors"9 "errors"
9 "fmt"10 "fmt"
@@ -13,6 +14,7 @@ import (
13 "path/filepath"14 "path/filepath"
14 "runtime/debug"15 "runtime/debug"
15 "sort"16 "sort"
17+ "strconv"
16 "strings"18 "strings"
17 "time"19 "time"
18 20
@@ -51,6 +53,7 @@ type harness struct {
51 create func(string) (*os.File, error)53 create func(string) (*os.File, error)
52 removeAll func(string) error54 removeAll func(string) error
53 gitSHA func() string55 gitSHA func() string
56+ vevGitSHA func(string) string
54 selectScenarios func(manifest) []scenario // test-only bounded fixture seam57 selectScenarios func(manifest) []scenario // test-only bounded fixture seam
55 }58 }
56 59
@@ -71,6 +74,19 @@ func createExclusiveRunDir(path string) error {
71 // go run evidence attributable as well.74 // go run evidence attributable as well.
72 const gitSHACommandTimeout = time.Second75 const gitSHACommandTimeout = time.Second
73 76
77+func recordedBinaryGitSHA(path string) string {
78+ info, err := buildinfo.ReadFile(path)
79+ if err != nil {
80+ return "unknown"
81+ }
82+ for _, setting := range info.Settings {
83+ if setting.Key == "vcs.revision" && setting.Value != "" {
84+ return setting.Value
85+ }
86+ }
87+ return "unknown"
88+}
89+
74 func recordedGitSHA() string {90 func recordedGitSHA() string {
75 if info, ok := debug.ReadBuildInfo(); ok {91 if info, ok := debug.ReadBuildInfo(); ok {
76 for _, setting := range info.Settings {92 for _, setting := range info.Settings {
@@ -148,7 +164,7 @@ func run(args []string, h *harness) (err error) {
148 if err := writeJSON(filepath.Join(opt.out, "runs.json"), results); err != nil {164 if err := writeJSON(filepath.Join(opt.out, "runs.json"), results); err != nil {
149 return err165 return err
150 }166 }
151- return writeJSON(filepath.Join(opt.out, "summary.json"), summary{Schema: 1, GitSHA: h.gitSHA(), Warmup: opt.warmup.String(), Duration: opt.duration.String(), Repetitions: opt.repetitions, EndToEnd: percentiles(all), Cadence: percentiles(cadence), MaxGap: maxGap, RunP50Dispersion: percentiles(runP50s), Spans: summarizeSpans(localSpans), Runs: len(results)})167+ return writeJSON(filepath.Join(opt.out, "summary.json"), summary{Schema: 1, GitSHA: h.gitSHA(), VevGitSHA: h.vevGitSHA(opt.vevBin), Warmup: opt.warmup.String(), Duration: opt.duration.String(), Repetitions: opt.repetitions, EndToEnd: percentiles(all), Cadence: percentiles(cadence), MaxGap: maxGap, RunP50Dispersion: percentiles(runP50s), Spans: summarizeSpans(localSpans), Runs: len(results)})
152 }168 }
153 169
154 // aggregateEventResults deliberately preserves raw event latencies for170 // aggregateEventResults deliberately preserves raw event latencies for
@@ -307,7 +323,22 @@ func (h *harness) runOne(o options, mat manifest, s scenario, run int, raw io.Wr
307 flushedRecorded = true323 flushedRecorded = true
308 return nil324 return nil
309 }325 }
310- if measureErr := p.process.Measure(workloadInput(s, run, fmt.Sprintf("measured-%d", sequence)), injected, flushed); measureErr != nil {326+ measureInjected := injected
327+ if cols, rows, resize := workloadResize(s, sequence); resize {
328+ resizer, ok := p.process.(processResizer)
329+ if !ok {
330+ return res, errors.New("client process does not support resize workload")
331+ }
332+ if err := injected(); err != nil {
333+ return res, err
334+ }
335+ measureInjected = func() error { return nil }
336+ if resizeErr := resizer.Resize(cols, rows); resizeErr != nil {
337+ diagnosticErr := recordMark(harnessMark{Scenario: s.ID, Run: run, Sequence: sequence, Kind: "terminal_flushed", Tick: h.clock.Now(), Valid: false})
338+ return res, errors.Join(fmt.Errorf("resize client PTY: %w", resizeErr), diagnosticErr)
339+ }
340+ }
341+ if measureErr := p.process.Measure(workloadInput(s, run, fmt.Sprintf("measured-%d", sequence)), measureInjected, flushed); measureErr != nil {
311 if injectedRecorded && !flushedRecorded {342 if injectedRecorded && !flushedRecorded {
312 diagnosticErr := recordMark(harnessMark{Scenario: s.ID, Run: run, Sequence: sequence, Kind: "terminal_flushed", Tick: h.clock.Now(), Valid: false})343 diagnosticErr := recordMark(harnessMark{Scenario: s.ID, Run: run, Sequence: sequence, Kind: "terminal_flushed", Tick: h.clock.Now(), Valid: false})
313 return res, errors.Join(measureErr, diagnosticErr)344 return res, errors.Join(measureErr, diagnosticErr)
@@ -418,6 +449,20 @@ func launchOrder(mappings []processMapping) []processMapping {
418 return out449 return out
419 }450 }
420 451
452+type processResizer interface {
453+ Resize(cols, rows uint16) error
454+}
455+
456+func workloadResize(s scenario, sequence uint64) (uint16, uint16, bool) {
457+ if s.Workload != "resize_sweep" && s.Workload != "snapshot_output_resize" {
458+ return 0, 0, false
459+ }
460+ if sequence%2 == 1 {
461+ return 119, 40, true
462+ }
463+ return 120, 40, true
464+}
465+
421 // workloadInput is shell input delivered through the client PTY. It avoids any466 // workloadInput is shell input delivered through the client PTY. It avoids any
422 // private daemon API and makes every topology/workload/transport manifest row467 // private daemon API and makes every topology/workload/transport manifest row
423 // execute a real terminal workload. The marker is the output boundary awaited468 // execute a real terminal workload. The marker is the output boundary awaited
@@ -425,17 +470,44 @@ func launchOrder(mappings []processMapping) []processMapping {
425 func workloadInput(s scenario, run int, phase string) []byte {470 func workloadInput(s scenario, run int, phase string) []byte {
426 marker := fmt.Sprintf("__VEV_HARNESS_%s_r%d_%s__", safeName(s.ID), run, safeName(phase))471 marker := fmt.Sprintf("__VEV_HARNESS_%s_r%d_%s__", safeName(s.ID), run, safeName(phase))
427 body := "printf 'vev perf %s\\n'"472 body := "printf 'vev perf %s\\n'"
473+ markerSeparator := "; "
428 switch s.Workload {474 switch s.Workload {
429- case "active_output", "all_output", "inactive_output", "interactive_flood":475+ case "active_output":
476+ cell := byte('X')
477+ if sequence, err := strconv.ParseUint(strings.TrimPrefix(phase, "measured-"), 10, 64); err == nil && strings.HasPrefix(phase, "measured-") {
478+ cell = '0' + byte(sequence%2)
479+ }
480+ body = fmt.Sprintf(`printf '\033[10;20H%c'; : '%%s'`, cell)
481+ case "all_output":
482+ body = `printf '%%-120s' 'vev perf line %s'`
483+ case "inactive_output":
484+ body = `printf '\033[38;2;20;120;220mred'; printf '\033[38;2;220;80;40mgreen'; printf '\033[38;2;80;220;120m%s'; printf '\033[0m'`
485+ case "interactive_flood":
430 body = "i=0; while [ $i -lt 128 ]; do printf 'vev perf output %s\\n'; i=$((i+1)); done"486 body = "i=0; while [ $i -lt 128 ]; do printf 'vev perf output %s\\n'; i=$((i+1)); done"
431 case "resize_sweep":487 case "resize_sweep":
432- body = "printf 'vev perf resize-sweep %s\\n'"488+ cols, _ := resizeWorkloadGeometry(s, phase)
489+ body = fmt.Sprintf(`i=0; while [ "$i" -lt 100 ]; do set -- $(stty size); [ "$2" = "%d" ] && break; i=$((i+1)); sleep 0.01; done; set -- $(stty size); [ "$2" = "%d" ] && printf 'vev perf resize-sweep %%s\n'`, cols, cols)
490+ markerSeparator = " && "
433 case "copy_search":491 case "copy_search":
434 body = "printf 'vev perf copy-search %s\\n'"492 body = "printf 'vev perf copy-search %s\\n'"
435 case "snapshot_output_resize":493 case "snapshot_output_resize":
436- body = "printf 'vev perf snapshot-output-resize %s\\n'"494+ cols, _ := resizeWorkloadGeometry(s, phase)
495+ body = fmt.Sprintf(`i=0; while [ "$i" -lt 100 ]; do set -- $(stty size); [ "$2" = "%d" ] && break; i=$((i+1)); sleep 0.01; done; set -- $(stty size); [ "$2" = "%d" ] && printf '\033[2J\033[Hvev perf snapshot-output-resize %%s\n'`, cols, cols)
496+ markerSeparator = " && "
437 case "attach_restore_tab_switch":497 case "attach_restore_tab_switch":
438- body = "printf 'vev perf attach-restore-tab-switch %s\\n'"498+ body = `"$VEV_PERF_BIN" cmd next-tab && "$VEV_PERF_BIN" cmd previous-tab && printf 'vev perf attach-restore-tab-switch %s\n'`
499+ if phase == "warmup" {
500+ body = `"$VEV_PERF_BIN" cmd new-tab && "$VEV_PERF_BIN" cmd previous-tab && printf 'vev perf attach-restore-tab-switch %s\n'`
501+ }
502+ markerSeparator = " && "
503+ }
504+ return []byte(fmt.Sprintf(body+markerSeparator+"printf '%s\\n'\n", s.ID, marker))
505+}
506+
507+func resizeWorkloadGeometry(s scenario, phase string) (uint16, uint16) {
508+ if sequence, err := strconv.ParseUint(strings.TrimPrefix(phase, "measured-"), 10, 64); err == nil && strings.HasPrefix(phase, "measured-") {
509+ cols, rows, _ := workloadResize(s, sequence)
510+ return cols, rows
439 }511 }
440- return []byte(fmt.Sprintf(body+"; printf '%s\\n'\n", s.ID, marker))512+ return 120, 40
441 }513 }
M cmd/vev-perf-harness/runner_test.go +177 -0
@@ -309,6 +309,183 @@ func TestHarnessFakeRunnerRoutesClientToPeerAndCleansEveryRole(t *testing.T) {
309309 }
310310 }
311311
312+func TestWorkloadInputShapes(t *testing.T) {
313+ tests := []struct {
314+ workload string
315+ contains []string
316+ }{
317+ {"active_output", []string{`printf '\033[10;20H1'`}},
318+ {"all_output", []string{"120", "vev perf line"}},
319+ {"inactive_output", []string{
320+ `printf '\033[38;2;20;120;220mred'`,
321+ `printf '\033[38;2;220;80;40mgreen'`,
322+ `printf '\033[38;2;80;220;120ms'`,
323+ }},
324+ {"interactive_flood", []string{"while [ $i -lt 128 ]"}},
325+ {"snapshot_output_resize", []string{`\033[2J`, "vev perf snapshot-output-resize"}},
326+ {"attach_restore_tab_switch", []string{`"$VEV_PERF_BIN" cmd next-tab`, `"$VEV_PERF_BIN" cmd previous-tab`}},
327+ }
328+ for _, tt := range tests {
329+ t.Run(tt.workload, func(t *testing.T) {
330+ got := string(workloadInput(scenario{ID: "s", Workload: tt.workload}, 1, "measured-1"))
331+ for _, want := range tt.contains {
332+ if !strings.Contains(got, want) {
333+ t.Errorf("workload input %q does not contain %q", got, want)
334+ }
335+ }
336+ if !strings.Contains(got, "__VEV_HARNESS_s_r1_measured-1__") {
337+ t.Errorf("workload input has no terminal marker: %q", got)
338+ }
339+ })
340+ }
341+
342+ activeWarmup := string(workloadInput(scenario{ID: "s", Workload: "active_output"}, 1, "warmup"))
343+ activeOne := string(workloadInput(scenario{ID: "s", Workload: "active_output"}, 1, "measured-1"))
344+ activeTwo := string(workloadInput(scenario{ID: "s", Workload: "active_output"}, 1, "measured-2"))
345+ for phase, got := range map[string]string{"warmup": activeWarmup, "measured-1": activeOne, "measured-2": activeTwo} {
346+ marker := fmt.Sprintf("__VEV_HARNESS_s_r1_%s__", phase)
347+ wantByte := map[string]byte{"warmup": 'X', "measured-1": '1', "measured-2": '0'}[phase]
348+ want := fmt.Sprintf("printf '\\033[10;20H%c'; : 's'; printf '%s\\n'\n", wantByte, marker)
349+ if got != want {
350+ t.Errorf("active %s input = %q, want exactly one changing cell write %q", phase, got, want)
351+ }
352+ }
353+ if activeWarmup == activeOne || activeOne == activeTwo {
354+ t.Fatalf("active one-cell payload did not change by sequence: %q %q %q", activeWarmup, activeOne, activeTwo)
355+ }
356+ inactive := string(workloadInput(scenario{ID: "s", Workload: "inactive_output"}, 1, "measured-1"))
357+ if count := strings.Count(inactive, `\033[38;2;`); count != 3 {
358+ t.Fatalf("inactive output truecolor runs = %d, want 3: %q", count, inactive)
359+ }
360+
361+ warmup := string(workloadInput(scenario{ID: "s", Workload: "attach_restore_tab_switch"}, 1, "warmup"))
362+ if count := strings.Count(warmup, `"$VEV_PERF_BIN" cmd new-tab`); count != 1 {
363+ t.Fatalf("attach warmup new-tab commands = %d, want 1: %q", count, warmup)
364+ }
365+ wantWarmup := `"$VEV_PERF_BIN" cmd new-tab && "$VEV_PERF_BIN" cmd previous-tab && printf 'vev perf attach-restore-tab-switch s\n' && printf '__VEV_HARNESS_s_r1_warmup__\n'` + "\n"
366+ if warmup != wantWarmup {
367+ t.Fatalf("attach warmup success marker is not gated by every command:\n got %q\nwant %q", warmup, wantWarmup)
368+ }
369+ measured := string(workloadInput(scenario{ID: "s", Workload: "attach_restore_tab_switch"}, 1, "measured-1"))
370+ wantMeasured := `"$VEV_PERF_BIN" cmd next-tab && "$VEV_PERF_BIN" cmd previous-tab && printf 'vev perf attach-restore-tab-switch s\n' && printf '__VEV_HARNESS_s_r1_measured-1__\n'` + "\n"
371+ if measured != wantMeasured {
372+ t.Fatalf("attach measured success marker is not gated by every command:\n got %q\nwant %q", measured, wantMeasured)
373+ }
374+}
375+
376+func TestWorkloadResize(t *testing.T) {
377+ for _, tt := range []struct {
378+ workload string
379+ sequence uint64
380+ wantCols uint16
381+ wantOK bool
382+ }{
383+ {"active_output", 1, 0, false},
384+ {"resize_sweep", 1, 119, true},
385+ {"resize_sweep", 2, 120, true},
386+ {"snapshot_output_resize", 1, 119, true},
387+ } {
388+ t.Run(fmt.Sprintf("%s-%d", tt.workload, tt.sequence), func(t *testing.T) {
389+ cols, rows, ok := workloadResize(scenario{Workload: tt.workload}, tt.sequence)
390+ if ok != tt.wantOK || cols != tt.wantCols {
391+ t.Fatalf("workloadResize() = (%d, %d, %t), want cols=%d ok=%t", cols, rows, ok, tt.wantCols, tt.wantOK)
392+ }
393+ if ok && rows != 40 {
394+ t.Fatalf("workloadResize() rows = %d, want 40", rows)
395+ }
396+ })
397+ }
398+}
399+
400+type resizeOrderingWriter struct {
401+ bytes.Buffer
402+ order *[]string
403+}
404+
405+func (w *resizeOrderingWriter) Write(p []byte) (int, error) {
406+ if bytes.Contains(p, []byte(`"sequence":2`)) {
407+ switch {
408+ case bytes.Contains(p, []byte(`"kind":"input_injected"`)):
409+ *w.order = append(*w.order, "injected")
410+ case bytes.Contains(p, []byte(`"kind":"terminal_flushed"`)):
411+ *w.order = append(*w.order, "terminal_flushed")
412+ }
413+ }
414+ return w.Buffer.Write(p)
415+}
416+
417+type resizeOrderingProcess struct {
418+ order *[]string
419+ inputs [][]byte
420+ resizes int
421+ measures int
422+}
423+
424+func (*resizeOrderingProcess) Warmup([]byte) error { return nil }
425+
426+func (p *resizeOrderingProcess) Resize(cols, rows uint16) error {
427+ p.resizes++
428+ if p.resizes == 1 {
429+ *p.order = append(*p.order, fmt.Sprintf("resize:%dx%d", cols, rows))
430+ }
431+ return nil
432+}
433+
434+func (p *resizeOrderingProcess) Measure(input []byte, injected, flushed func() error) error {
435+ p.measures++
436+ p.inputs = append(p.inputs, append([]byte(nil), input...))
437+ if p.measures == 1 {
438+ *p.order = append(*p.order, "workload_write")
439+ }
440+ if err := injected(); err != nil {
441+ return err
442+ }
443+ if p.measures == 1 {
444+ *p.order = append(*p.order, "workload_flush")
445+ }
446+ return flushed()
447+}
448+
449+func (*resizeOrderingProcess) Close() error { return nil }
450+
451+type resizeOrderingLauncher struct{ client *resizeOrderingProcess }
452+
453+func (l resizeOrderingLauncher) Launch(m processMapping, _ roleCommand) (launchedProcess, error) {
454+ if m.Role == "client" {
455+ return l.client, nil
456+ }
457+ return &fakeProcess{}, nil
458+}
459+
460+func TestResizeBoundaryPrecedesResizeAndMarkerWaitsForShellGeometry(t *testing.T) {
461+ dir := t.TempDir()
462+ order := []string{}
463+ client := &resizeOrderingProcess{order: &order}
464+ h := defaultHarness()
465+ h.clock, h.launcher = &fakeClock{}, resizeOrderingLauncher{client: client}
466+ raw := &resizeOrderingWriter{order: &order}
467+ _, err := h.runOne(options{out: dir, warmup: time.Second, duration: minimumDuration}, manifest{}, scenario{ID: "resize", Workload: "resize_sweep", Roles: []string{"daemon", "client"}}, 1, raw)
468+ if err != nil {
469+ t.Fatal(err)
470+ }
471+ wantOrder := []string{"injected", "resize:120x40", "workload_write", "workload_flush", "terminal_flushed"}
472+ if len(order) < len(wantOrder) {
473+ t.Fatalf("resize event order = %q, want prefix %q", order, wantOrder)
474+ }
475+ if !equalStrings(order[:len(wantOrder)], wantOrder) {
476+ t.Fatalf("first resize event order = %q, want %q", order[:len(wantOrder)], wantOrder)
477+ }
478+ first := string(client.inputs[0])
479+ for _, want := range []string{`set -- $(stty size)`, `[ "$2" = "120" ]`, `sleep 0.01`, `&& printf '__VEV_HARNESS_resize_r1_measured-2__\n'`} {
480+ if !strings.Contains(first, want) {
481+ t.Errorf("resize input has no bounded expected-column gate %q: %q", want, first)
482+ }
483+ }
484+ if strings.Contains(first, `"40 120"`) {
485+ t.Fatalf("resize input incorrectly gates on client rows that shell chrome does not expose: %q", first)
486+ }
487+}
488+
312489 func TestHarnessAggregatesRawEventsSeparatelyFromRunDispersion(t *testing.T) {
313490 first := append([]int64(nil), make([]int64, minimumInIntervalEventSamples+5)...)
314491 for i := range first {
@@ -309,6 +309,183 @@ func TestHarnessFakeRunnerRoutesClientToPeerAndCleansEveryRole(t *testing.T) {
309 }309 }
310 }310 }
311 311
312+func TestWorkloadInputShapes(t *testing.T) {
313+ tests := []struct {
314+ workload string
315+ contains []string
316+ }{
317+ {"active_output", []string{`printf '\033[10;20H1'`}},
318+ {"all_output", []string{"120", "vev perf line"}},
319+ {"inactive_output", []string{
320+ `printf '\033[38;2;20;120;220mred'`,
321+ `printf '\033[38;2;220;80;40mgreen'`,
322+ `printf '\033[38;2;80;220;120ms'`,
323+ }},
324+ {"interactive_flood", []string{"while [ $i -lt 128 ]"}},
325+ {"snapshot_output_resize", []string{`\033[2J`, "vev perf snapshot-output-resize"}},
326+ {"attach_restore_tab_switch", []string{`"$VEV_PERF_BIN" cmd next-tab`, `"$VEV_PERF_BIN" cmd previous-tab`}},
327+ }
328+ for _, tt := range tests {
329+ t.Run(tt.workload, func(t *testing.T) {
330+ got := string(workloadInput(scenario{ID: "s", Workload: tt.workload}, 1, "measured-1"))
331+ for _, want := range tt.contains {
332+ if !strings.Contains(got, want) {
333+ t.Errorf("workload input %q does not contain %q", got, want)
334+ }
335+ }
336+ if !strings.Contains(got, "__VEV_HARNESS_s_r1_measured-1__") {
337+ t.Errorf("workload input has no terminal marker: %q", got)
338+ }
339+ })
340+ }
341+
342+ activeWarmup := string(workloadInput(scenario{ID: "s", Workload: "active_output"}, 1, "warmup"))
343+ activeOne := string(workloadInput(scenario{ID: "s", Workload: "active_output"}, 1, "measured-1"))
344+ activeTwo := string(workloadInput(scenario{ID: "s", Workload: "active_output"}, 1, "measured-2"))
345+ for phase, got := range map[string]string{"warmup": activeWarmup, "measured-1": activeOne, "measured-2": activeTwo} {
346+ marker := fmt.Sprintf("__VEV_HARNESS_s_r1_%s__", phase)
347+ wantByte := map[string]byte{"warmup": 'X', "measured-1": '1', "measured-2": '0'}[phase]
348+ want := fmt.Sprintf("printf '\\033[10;20H%c'; : 's'; printf '%s\\n'\n", wantByte, marker)
349+ if got != want {
350+ t.Errorf("active %s input = %q, want exactly one changing cell write %q", phase, got, want)
351+ }
352+ }
353+ if activeWarmup == activeOne || activeOne == activeTwo {
354+ t.Fatalf("active one-cell payload did not change by sequence: %q %q %q", activeWarmup, activeOne, activeTwo)
355+ }
356+ inactive := string(workloadInput(scenario{ID: "s", Workload: "inactive_output"}, 1, "measured-1"))
357+ if count := strings.Count(inactive, `\033[38;2;`); count != 3 {
358+ t.Fatalf("inactive output truecolor runs = %d, want 3: %q", count, inactive)
359+ }
360+
361+ warmup := string(workloadInput(scenario{ID: "s", Workload: "attach_restore_tab_switch"}, 1, "warmup"))
362+ if count := strings.Count(warmup, `"$VEV_PERF_BIN" cmd new-tab`); count != 1 {
363+ t.Fatalf("attach warmup new-tab commands = %d, want 1: %q", count, warmup)
364+ }
365+ wantWarmup := `"$VEV_PERF_BIN" cmd new-tab && "$VEV_PERF_BIN" cmd previous-tab && printf 'vev perf attach-restore-tab-switch s\n' && printf '__VEV_HARNESS_s_r1_warmup__\n'` + "\n"
366+ if warmup != wantWarmup {
367+ t.Fatalf("attach warmup success marker is not gated by every command:\n got %q\nwant %q", warmup, wantWarmup)
368+ }
369+ measured := string(workloadInput(scenario{ID: "s", Workload: "attach_restore_tab_switch"}, 1, "measured-1"))
370+ wantMeasured := `"$VEV_PERF_BIN" cmd next-tab && "$VEV_PERF_BIN" cmd previous-tab && printf 'vev perf attach-restore-tab-switch s\n' && printf '__VEV_HARNESS_s_r1_measured-1__\n'` + "\n"
371+ if measured != wantMeasured {
372+ t.Fatalf("attach measured success marker is not gated by every command:\n got %q\nwant %q", measured, wantMeasured)
373+ }
374+}
375+
376+func TestWorkloadResize(t *testing.T) {
377+ for _, tt := range []struct {
378+ workload string
379+ sequence uint64
380+ wantCols uint16
381+ wantOK bool
382+ }{
383+ {"active_output", 1, 0, false},
384+ {"resize_sweep", 1, 119, true},
385+ {"resize_sweep", 2, 120, true},
386+ {"snapshot_output_resize", 1, 119, true},
387+ } {
388+ t.Run(fmt.Sprintf("%s-%d", tt.workload, tt.sequence), func(t *testing.T) {
389+ cols, rows, ok := workloadResize(scenario{Workload: tt.workload}, tt.sequence)
390+ if ok != tt.wantOK || cols != tt.wantCols {
391+ t.Fatalf("workloadResize() = (%d, %d, %t), want cols=%d ok=%t", cols, rows, ok, tt.wantCols, tt.wantOK)
392+ }
393+ if ok && rows != 40 {
394+ t.Fatalf("workloadResize() rows = %d, want 40", rows)
395+ }
396+ })
397+ }
398+}
399+
400+type resizeOrderingWriter struct {
401+ bytes.Buffer
402+ order *[]string
403+}
404+
405+func (w *resizeOrderingWriter) Write(p []byte) (int, error) {
406+ if bytes.Contains(p, []byte(`"sequence":2`)) {
407+ switch {
408+ case bytes.Contains(p, []byte(`"kind":"input_injected"`)):
409+ *w.order = append(*w.order, "injected")
410+ case bytes.Contains(p, []byte(`"kind":"terminal_flushed"`)):
411+ *w.order = append(*w.order, "terminal_flushed")
412+ }
413+ }
414+ return w.Buffer.Write(p)
415+}
416+
417+type resizeOrderingProcess struct {
418+ order *[]string
419+ inputs [][]byte
420+ resizes int
421+ measures int
422+}
423+
424+func (*resizeOrderingProcess) Warmup([]byte) error { return nil }
425+
426+func (p *resizeOrderingProcess) Resize(cols, rows uint16) error {
427+ p.resizes++
428+ if p.resizes == 1 {
429+ *p.order = append(*p.order, fmt.Sprintf("resize:%dx%d", cols, rows))
430+ }
431+ return nil
432+}
433+
434+func (p *resizeOrderingProcess) Measure(input []byte, injected, flushed func() error) error {
435+ p.measures++
436+ p.inputs = append(p.inputs, append([]byte(nil), input...))
437+ if p.measures == 1 {
438+ *p.order = append(*p.order, "workload_write")
439+ }
440+ if err := injected(); err != nil {
441+ return err
442+ }
443+ if p.measures == 1 {
444+ *p.order = append(*p.order, "workload_flush")
445+ }
446+ return flushed()
447+}
448+
449+func (*resizeOrderingProcess) Close() error { return nil }
450+
451+type resizeOrderingLauncher struct{ client *resizeOrderingProcess }
452+
453+func (l resizeOrderingLauncher) Launch(m processMapping, _ roleCommand) (launchedProcess, error) {
454+ if m.Role == "client" {
455+ return l.client, nil
456+ }
457+ return &fakeProcess{}, nil
458+}
459+
460+func TestResizeBoundaryPrecedesResizeAndMarkerWaitsForShellGeometry(t *testing.T) {
461+ dir := t.TempDir()
462+ order := []string{}
463+ client := &resizeOrderingProcess{order: &order}
464+ h := defaultHarness()
465+ h.clock, h.launcher = &fakeClock{}, resizeOrderingLauncher{client: client}
466+ raw := &resizeOrderingWriter{order: &order}
467+ _, err := h.runOne(options{out: dir, warmup: time.Second, duration: minimumDuration}, manifest{}, scenario{ID: "resize", Workload: "resize_sweep", Roles: []string{"daemon", "client"}}, 1, raw)
468+ if err != nil {
469+ t.Fatal(err)
470+ }
471+ wantOrder := []string{"injected", "resize:120x40", "workload_write", "workload_flush", "terminal_flushed"}
472+ if len(order) < len(wantOrder) {
473+ t.Fatalf("resize event order = %q, want prefix %q", order, wantOrder)
474+ }
475+ if !equalStrings(order[:len(wantOrder)], wantOrder) {
476+ t.Fatalf("first resize event order = %q, want %q", order[:len(wantOrder)], wantOrder)
477+ }
478+ first := string(client.inputs[0])
479+ for _, want := range []string{`set -- $(stty size)`, `[ "$2" = "120" ]`, `sleep 0.01`, `&& printf '__VEV_HARNESS_resize_r1_measured-2__\n'`} {
480+ if !strings.Contains(first, want) {
481+ t.Errorf("resize input has no bounded expected-column gate %q: %q", want, first)
482+ }
483+ }
484+ if strings.Contains(first, `"40 120"`) {
485+ t.Fatalf("resize input incorrectly gates on client rows that shell chrome does not expose: %q", first)
486+ }
487+}
488+
312 func TestHarnessAggregatesRawEventsSeparatelyFromRunDispersion(t *testing.T) {489 func TestHarnessAggregatesRawEventsSeparatelyFromRunDispersion(t *testing.T) {
313 first := append([]int64(nil), make([]int64, minimumInIntervalEventSamples+5)...)490 first := append([]int64(nil), make([]int64, minimumInIntervalEventSamples+5)...)
314 for i := range first {491 for i := range first {
M docs/performance.md +91 -19
@@ -42,30 +42,102 @@ marks are paired within one process and summarized by component/adapter with
4242 raw records, counts, p50/p95/p99, and max. Diagnostic fields (bytes, fragments,
4343 retransmits, pending, ACK RTT) remain separate from duration calculations.
4444
45-### Baseline procedure and validity
45+### Proxy ANSI baseline
4646
47-Use the public CLI harness (not internal packages):
47+The current proxy path transports ANSI and applies it through the local proxy
48+VT parser. Build the fixed baseline binary and harness, then verify the harness
49+and parser fixtures:
4850
4951 ```sh
50-go build -o /tmp/vev-perf ./
51-rm -rf testdata/perf/results
52-go run ./cmd/vev-perf-harness --vev-bin /tmp/vev-perf \
53- --manifest testdata/perf/manifest.json --out testdata/perf/results \
52+go build -o /tmp/vev-proxy-baseline .
53+go build -o /tmp/vev-perf-harness ./cmd/vev-perf-harness
54+go test ./cmd/vev-perf-harness -count=1
55+go test ./internal/usecase/daemon -run '^TestProxyANSIBenchmarkFixtures$' -count=1
56+go test ./internal/usecase/daemon -run '^$' \
57+ -bench '^BenchmarkProxyANSIApply$' -benchmem -count=10
58+```
59+
60+The pre-change parser results below are medians of ten repetitions on
61+Linux/amd64 with an AMD Ryzen 9 7900X3D. The fixed
62+`/tmp/vev-proxy-baseline` binary was built from exact Phase 1 commit
63+`1f2c802d0707785cab979134db6a95645250a82c`. Runtime production code remains
64+based on `919bcd18c428ef90abfda6c2381ba1cfbb06b0dd` because Phase 1 changes
65+only benchmarks, harness code, tests, and documentation. The benchmark command
66+above ran with Go 1.26.5.
67+
68+| ANSI workload | ns/op | B/op | allocs/op | ANSI bytes/op |
69+| --- | ---: | ---: | ---: | ---: |
70+| absolute-position one-cell | 91.06 | 16 | 1 | 9 |
71+| 120-column full line | 4,659.5 | 129 | 1 | 126 |
72+| fragmented truecolor styled line | 5,861 | 450 | 1 | 419 |
73+| full-width 40-row scroll | 233,271.5 | 5,472 | 1 | 4,880 |
74+
75+Keep the ordinary local ANSI renderer as a separate guardrail:
76+
77+```sh
78+go test ./pkg/renderer -run '^$' -bench '^BenchmarkRenderer' -benchmem -count=10
79+```
80+
81+### Public-CLI transport runs
82+
83+Use a 10-second warmup, a 30-second measured interval, and ten independent
84+repetitions. This representative matrix covers one-cell active output,
85+120-column full-line output, fragmented truecolor-styled output, sustained
86+interactive flood, resize sweep, snapshot/output/resize, and attach/tab-switch
87+across local, SSH stdio, UDP baseline, UDP 25 ms, and UDP 1% loss (35
88+scenarios):
89+
90+```sh
91+rm -rf /tmp/vev-proxy-selected
92+mkdir -m 700 /tmp/vev-proxy-selected
93+for workload in \
94+ active_output all_output inactive_output interactive_flood resize_sweep \
95+ snapshot_output_resize attach_restore_tab_switch
96+do
97+ for transport in local ssh_stdio udp_baseline udp_25ms udp_loss_1pct
98+ do
99+ scenario="1x4-${workload}-${transport}"
100+ /tmp/vev-perf-harness --vev-bin /tmp/vev-proxy-baseline \
101+ --manifest testdata/perf/manifest.json --scenario "$scenario" \
102+ --out "/tmp/vev-proxy-selected/$scenario" \
103+ --warmup 10s --duration 30s --repetitions 10
104+ done
105+done
106+```
107+
108+`all_output` and `inactive_output` provide full-line and styled payload shapes;
109+these runs do not claim to activate every pane in their manifest topologies.
110+Run the complete canonical matrix only when full-matrix evidence is required:
111+
112+```sh
113+rm -rf /tmp/vev-proxy-matrix
114+/tmp/vev-perf-harness --vev-bin /tmp/vev-proxy-baseline \
115+ --manifest testdata/perf/manifest.json --out /tmp/vev-proxy-matrix \
54116 --warmup 10s --duration 30s --repetitions 10
55117 ```
56118
57-The minimums are 30 seconds measured and 10 independent repetitions; warmup is
58-excluded. During the measured interval the harness injects at most one event per
59-second. At least 10 complete event pairs are required: both injection and
60-successful terminal flush must fall inside the measured interval; straddling or
61-failed events are retained only as raw diagnostics and are not samples. Each run must contain `manifest.json`, `raw-harness.jsonl`, per-process
62-JSONL, `runs.json`, and `summary.json`. Reject results with missing input/flush
63-boundaries, unmatched IDs, bad correlation, nonmonotonic same-process sequence,
64-negative same-domain spans, fewer than 10 complete in-interval event pairs,
65-invalid denominators,
66-any cross-process tick arithmetic, or unmet duration/repetition minima. Raw
67-JSONL and run manifests are the evidence files; no result authorizes a policy
68-or optimization change.
119+Warmup is excluded. During the measured interval the harness injects at most
120+one event per second. At least 10 complete event pairs are required: both input
121+injection and successful terminal flush must fall inside the interval.
122+Straddling or failed events remain raw diagnostics, not samples. Each run must
123+contain `manifest.json`, `raw-harness.jsonl`, per-process JSONL, `runs.json`, and
124+`summary.json`.
125+
126+`summary.json` records the harness source as `git_sha`, the measured vev binary
127+source as `vev_git_sha`, and the measurement parameters;
128+`runs.json`, run manifests, and JSONL identify scenario, run, process, component,
129+and correlation IDs. Preserve those files with the benchmark output and record
130+`go version -m /tmp/vev-proxy-baseline`; do not commit result directories. CPU
131+and allocation evidence comes from Go microbenchmarks and process-local span
132+durations. End-to-end latency, terminal flush, and byte evidence comes from the
133+public-CLI harness. Reset, snapshot, and span counters for structured output are
134+candidate-only until that output exists.
135+
136+Reject results with missing input/flush boundaries, unmatched IDs, bad
137+correlation, nonmonotonic same-process sequence, negative same-domain spans,
138+fewer than 10 complete in-interval event pairs, invalid denominators, any
139+cross-process tick arithmetic, or unmet duration/repetition minima. Raw JSONL
140+and run manifests are evidence; no result alone authorizes an optimization.
69141
70142 ## Existing in-process smoke checks
71143
@@ -75,5 +147,5 @@ results:
75147 ```sh
76148 go test ./internal/usecase/daemon -run '^$' -bench '^BenchmarkDaemonHistory' -benchtime=1x -benchmem
77149 go test ./pkg/renderer ./pkg/vt ./internal/adapters/ipc -run '^$' -bench=. -benchmem
78-go test ./internal/usecase/daemon -run '^$' -bench '^BenchmarkComposeFloatingFrameCached$' -benchmem
150+go test ./internal/usecase/daemon -run '^$' -bench '^BenchmarkComposeCapturedFloatingFrameCached$' -benchmem
79151 ```
@@ -42,30 +42,102 @@ marks are paired within one process and summarized by component/adapter with
42 raw records, counts, p50/p95/p99, and max. Diagnostic fields (bytes, fragments,42 raw records, counts, p50/p95/p99, and max. Diagnostic fields (bytes, fragments,
43 retransmits, pending, ACK RTT) remain separate from duration calculations.43 retransmits, pending, ACK RTT) remain separate from duration calculations.
44 44
45-### Baseline procedure and validity45+### Proxy ANSI baseline
46 46
47-Use the public CLI harness (not internal packages):47+The current proxy path transports ANSI and applies it through the local proxy
48+VT parser. Build the fixed baseline binary and harness, then verify the harness
49+and parser fixtures:
48 50
49 ```sh51 ```sh
50-go build -o /tmp/vev-perf ./52+go build -o /tmp/vev-proxy-baseline .
51-rm -rf testdata/perf/results53+go build -o /tmp/vev-perf-harness ./cmd/vev-perf-harness
52-go run ./cmd/vev-perf-harness --vev-bin /tmp/vev-perf \54+go test ./cmd/vev-perf-harness -count=1
53- --manifest testdata/perf/manifest.json --out testdata/perf/results \55+go test ./internal/usecase/daemon -run '^TestProxyANSIBenchmarkFixtures$' -count=1
56+go test ./internal/usecase/daemon -run '^$' \
57+ -bench '^BenchmarkProxyANSIApply$' -benchmem -count=10
58+```
59+
60+The pre-change parser results below are medians of ten repetitions on
61+Linux/amd64 with an AMD Ryzen 9 7900X3D. The fixed
62+`/tmp/vev-proxy-baseline` binary was built from exact Phase 1 commit
63+`1f2c802d0707785cab979134db6a95645250a82c`. Runtime production code remains
64+based on `919bcd18c428ef90abfda6c2381ba1cfbb06b0dd` because Phase 1 changes
65+only benchmarks, harness code, tests, and documentation. The benchmark command
66+above ran with Go 1.26.5.
67+
68+| ANSI workload | ns/op | B/op | allocs/op | ANSI bytes/op |
69+| --- | ---: | ---: | ---: | ---: |
70+| absolute-position one-cell | 91.06 | 16 | 1 | 9 |
71+| 120-column full line | 4,659.5 | 129 | 1 | 126 |
72+| fragmented truecolor styled line | 5,861 | 450 | 1 | 419 |
73+| full-width 40-row scroll | 233,271.5 | 5,472 | 1 | 4,880 |
74+
75+Keep the ordinary local ANSI renderer as a separate guardrail:
76+
77+```sh
78+go test ./pkg/renderer -run '^$' -bench '^BenchmarkRenderer' -benchmem -count=10
79+```
80+
81+### Public-CLI transport runs
82+
83+Use a 10-second warmup, a 30-second measured interval, and ten independent
84+repetitions. This representative matrix covers one-cell active output,
85+120-column full-line output, fragmented truecolor-styled output, sustained
86+interactive flood, resize sweep, snapshot/output/resize, and attach/tab-switch
87+across local, SSH stdio, UDP baseline, UDP 25 ms, and UDP 1% loss (35
88+scenarios):
89+
90+```sh
91+rm -rf /tmp/vev-proxy-selected
92+mkdir -m 700 /tmp/vev-proxy-selected
93+for workload in \
94+ active_output all_output inactive_output interactive_flood resize_sweep \
95+ snapshot_output_resize attach_restore_tab_switch
96+do
97+ for transport in local ssh_stdio udp_baseline udp_25ms udp_loss_1pct
98+ do
99+ scenario="1x4-${workload}-${transport}"
100+ /tmp/vev-perf-harness --vev-bin /tmp/vev-proxy-baseline \
101+ --manifest testdata/perf/manifest.json --scenario "$scenario" \
102+ --out "/tmp/vev-proxy-selected/$scenario" \
103+ --warmup 10s --duration 30s --repetitions 10
104+ done
105+done
106+```
107+
108+`all_output` and `inactive_output` provide full-line and styled payload shapes;
109+these runs do not claim to activate every pane in their manifest topologies.
110+Run the complete canonical matrix only when full-matrix evidence is required:
111+
112+```sh
113+rm -rf /tmp/vev-proxy-matrix
114+/tmp/vev-perf-harness --vev-bin /tmp/vev-proxy-baseline \
115+ --manifest testdata/perf/manifest.json --out /tmp/vev-proxy-matrix \
54 --warmup 10s --duration 30s --repetitions 10116 --warmup 10s --duration 30s --repetitions 10
55 ```117 ```
56 118
57-The minimums are 30 seconds measured and 10 independent repetitions; warmup is119+Warmup is excluded. During the measured interval the harness injects at most
58-excluded. During the measured interval the harness injects at most one event per120+one event per second. At least 10 complete event pairs are required: both input
59-second. At least 10 complete event pairs are required: both injection and121+injection and successful terminal flush must fall inside the interval.
60-successful terminal flush must fall inside the measured interval; straddling or122+Straddling or failed events remain raw diagnostics, not samples. Each run must
61-failed events are retained only as raw diagnostics and are not samples. Each run must contain `manifest.json`, `raw-harness.jsonl`, per-process123+contain `manifest.json`, `raw-harness.jsonl`, per-process JSONL, `runs.json`, and
62-JSONL, `runs.json`, and `summary.json`. Reject results with missing input/flush124+`summary.json`.
63-boundaries, unmatched IDs, bad correlation, nonmonotonic same-process sequence,125+
64-negative same-domain spans, fewer than 10 complete in-interval event pairs,126+`summary.json` records the harness source as `git_sha`, the measured vev binary
65-invalid denominators,127+source as `vev_git_sha`, and the measurement parameters;
66-any cross-process tick arithmetic, or unmet duration/repetition minima. Raw128+`runs.json`, run manifests, and JSONL identify scenario, run, process, component,
67-JSONL and run manifests are the evidence files; no result authorizes a policy129+and correlation IDs. Preserve those files with the benchmark output and record
68-or optimization change.130+`go version -m /tmp/vev-proxy-baseline`; do not commit result directories. CPU
131+and allocation evidence comes from Go microbenchmarks and process-local span
132+durations. End-to-end latency, terminal flush, and byte evidence comes from the
133+public-CLI harness. Reset, snapshot, and span counters for structured output are
134+candidate-only until that output exists.
135+
136+Reject results with missing input/flush boundaries, unmatched IDs, bad
137+correlation, nonmonotonic same-process sequence, negative same-domain spans,
138+fewer than 10 complete in-interval event pairs, invalid denominators, any
139+cross-process tick arithmetic, or unmet duration/repetition minima. Raw JSONL
140+and run manifests are evidence; no result alone authorizes an optimization.
69 141
70 ## Existing in-process smoke checks142 ## Existing in-process smoke checks
71 143
@@ -75,5 +147,5 @@ results:
75 ```sh147 ```sh
76 go test ./internal/usecase/daemon -run '^$' -bench '^BenchmarkDaemonHistory' -benchtime=1x -benchmem148 go test ./internal/usecase/daemon -run '^$' -bench '^BenchmarkDaemonHistory' -benchtime=1x -benchmem
77 go test ./pkg/renderer ./pkg/vt ./internal/adapters/ipc -run '^$' -bench=. -benchmem149 go test ./pkg/renderer ./pkg/vt ./internal/adapters/ipc -run '^$' -bench=. -benchmem
78-go test ./internal/usecase/daemon -run '^$' -bench '^BenchmarkComposeFloatingFrameCached$' -benchmem150+go test ./internal/usecase/daemon -run '^$' -bench '^BenchmarkComposeCapturedFloatingFrameCached$' -benchmem
79 ```151 ```
M internal/usecase/daemon/daemon.go +3 -0
@@ -835,6 +835,9 @@ func (d *Daemon) shutdownAllWithSnapshotDeadline(reason uint8, deadline *snapsho
835835 }
836836 }
837837 }
838+ // Signal Serve even when a concurrent role transition made this initial
839+ // pass abort; Serve owns the bounded defensive teardown passes.
840+ d.doneOnce.Do(func() { close(d.done) })
838841 return checkpointIncomplete
839842 }
840843
@@ -835,6 +835,9 @@ func (d *Daemon) shutdownAllWithSnapshotDeadline(reason uint8, deadline *snapsho
835 }835 }
836 }836 }
837 }837 }
838+ // Signal Serve even when a concurrent role transition made this initial
839+ // pass abort; Serve owns the bounded defensive teardown passes.
840+ d.doneOnce.Do(func() { close(d.done) })
838 return checkpointIncomplete841 return checkpointIncomplete
839 }842 }
840 843
M internal/usecase/daemon/performance_benchmark_test.go +101 -0
@@ -1,6 +1,7 @@
11 package daemon
22
33 import (
4+ "bytes"
45 "context"
56 "errors"
67 "fmt"
@@ -196,6 +197,81 @@ func TestCountingOutputTransportCountsOpaquePayloadAndRejectsShortPayload(t *tes
196197 require.Equal(t, payload, transport.lastPayload(), "the counting transport must retain the payload slice header, not copy it")
197198 }
198199
200+func TestProxyANSIBenchmarkFixtures(t *testing.T) {
201+ fixtures := proxyANSIBenchmarkFixtures()
202+ require.Len(t, fixtures, 4)
203+ require.Equal(t, []string{
204+ "absolute-position-one-cell",
205+ "120-column-full-line",
206+ "fragmented-truecolor-styled-line",
207+ "full-width-40-row-scroll",
208+ }, []string{fixtures[0].name, fixtures[1].name, fixtures[2].name, fixtures[3].name})
209+ require.Equal(t, fixtures, proxyANSIBenchmarkFixtures(), "fixtures must be deterministic")
210+ require.Equal(t, []byte("\x1b[20;60HX"), fixtures[0].data)
211+ require.Equal(t, append([]byte("\x1b[1;1H"), bytes.Repeat([]byte("l"), 120)...), fixtures[1].data)
212+ require.Equal(t, 12, bytes.Count(fixtures[2].data, []byte("\x1b[38;2;")))
213+ require.Equal(t, 40, bytes.Count(fixtures[3].data, []byte("\r\n")))
214+
215+ for _, fixture := range fixtures {
216+ t.Run(fixture.name, func(t *testing.T) {
217+ proxy := newProxyANSIBenchmarkSession(t)
218+
219+ ack, reset, changed := proxy.applyOutputForGeneration(proxy.linkGeneration, ports.Output{
220+ BaseStateNum: 0,
221+ NewStateNum: 1,
222+ Data: fixture.data,
223+ })
224+
225+ require.Equal(t, uint64(1), ack)
226+ require.False(t, reset)
227+ require.True(t, changed)
228+ proxy.mu.Lock()
229+ require.Equal(t, domain.Size{Cols: 120, Rows: 40}, proxy.contentSize)
230+ proxy.mu.Unlock()
231+ })
232+ }
233+}
234+
235+func newProxyANSIBenchmarkSession(t testing.TB) *proxySession {
236+ t.Helper()
237+ proxy, err := newProxySession(domain.RemoteSessionKey{Host: "benchmark", Name: "ansi"}, domain.Size{Cols: 120, Rows: 40 + tabChromeRows})
238+ require.NoError(t, err)
239+ proxy.mu.Lock()
240+ contentSize := proxy.contentSize
241+ proxy.mu.Unlock()
242+ require.Equal(t, domain.Size{Cols: 120, Rows: 40}, contentSize)
243+ return proxy
244+}
245+
246+type proxyANSIBenchmarkFixture struct {
247+ name string
248+ data []byte
249+}
250+
251+func proxyANSIBenchmarkFixtures() []proxyANSIBenchmarkFixture {
252+ fullLine := append([]byte("\x1b[1;1H"), bytes.Repeat([]byte("l"), 120)...)
253+
254+ var styledLine []byte
255+ for fragment := range 12 {
256+ styledLine = fmt.Appendf(styledLine, "\x1b[12;%dH\x1b[38;2;%d;%d;%dm", fragment*10+1, fragment*17, fragment*13, fragment*7)
257+ styledLine = append(styledLine, bytes.Repeat([]byte{byte('a' + fragment)}, 10)...)
258+ }
259+ styledLine = append(styledLine, "\x1b[0m"...)
260+
261+ scroll := make([]byte, 0, 40*122)
262+ for row := range 40 {
263+ scroll = append(scroll, performanceFullWidthRow(120, 0, row)...)
264+ scroll = append(scroll, '\r', '\n')
265+ }
266+
267+ return []proxyANSIBenchmarkFixture{
268+ {name: "absolute-position-one-cell", data: []byte("\x1b[20;60HX")},
269+ {name: "120-column-full-line", data: fullLine},
270+ {name: "fragmented-truecolor-styled-line", data: styledLine},
271+ {name: "full-width-40-row-scroll", data: scroll},
272+ }
273+}
274+
199275 func TestPerformanceFixtureCounters(t *testing.T) {
200276 fixture := newPerformanceFixture(t, performanceConfig{})
201277
@@ -359,6 +435,31 @@ var daemonSnapshotTopologies = []daemonHistoryTopology{
359435 {name: "1tab-4panes", tabs: 1, panes: 4},
360436 }
361437
438+func BenchmarkProxyANSIApply(b *testing.B) {
439+ for _, fixture := range proxyANSIBenchmarkFixtures() {
440+ b.Run(fixture.name, func(b *testing.B) {
441+ proxy := newProxyANSIBenchmarkSession(b)
442+ generation := proxy.linkGeneration
443+ state := uint64(0)
444+ b.ReportAllocs()
445+ b.SetBytes(int64(len(fixture.data)))
446+ for b.Loop() {
447+ next := state + 1
448+ ack, reset, changed := proxy.applyOutputForGeneration(generation, ports.Output{
449+ BaseStateNum: state,
450+ NewStateNum: next,
451+ Data: fixture.data,
452+ })
453+ if ack != next || reset || !changed {
454+ b.Fatalf("apply output = ack %d, reset %t, changed %t; want ack %d, reset false, changed true", ack, reset, changed, next)
455+ }
456+ state = next
457+ }
458+ b.ReportMetric(float64(len(fixture.data)), "ansibytes/op")
459+ })
460+ }
461+}
462+
362463 func BenchmarkDaemonHistoryLivePaint(b *testing.B) {
363464 benchmarkDaemonLargeHistory(b, "live-paint", func(f *performanceFixture) { f.paintLive() })
364465 }
@@ -1,6 +1,7 @@
1 package daemon1 package daemon
2 2
3 import (3 import (
4+ "bytes"
4 "context"5 "context"
5 "errors"6 "errors"
6 "fmt"7 "fmt"
@@ -196,6 +197,81 @@ func TestCountingOutputTransportCountsOpaquePayloadAndRejectsShortPayload(t *tes
196 require.Equal(t, payload, transport.lastPayload(), "the counting transport must retain the payload slice header, not copy it")197 require.Equal(t, payload, transport.lastPayload(), "the counting transport must retain the payload slice header, not copy it")
197 }198 }
198 199
200+func TestProxyANSIBenchmarkFixtures(t *testing.T) {
201+ fixtures := proxyANSIBenchmarkFixtures()
202+ require.Len(t, fixtures, 4)
203+ require.Equal(t, []string{
204+ "absolute-position-one-cell",
205+ "120-column-full-line",
206+ "fragmented-truecolor-styled-line",
207+ "full-width-40-row-scroll",
208+ }, []string{fixtures[0].name, fixtures[1].name, fixtures[2].name, fixtures[3].name})
209+ require.Equal(t, fixtures, proxyANSIBenchmarkFixtures(), "fixtures must be deterministic")
210+ require.Equal(t, []byte("\x1b[20;60HX"), fixtures[0].data)
211+ require.Equal(t, append([]byte("\x1b[1;1H"), bytes.Repeat([]byte("l"), 120)...), fixtures[1].data)
212+ require.Equal(t, 12, bytes.Count(fixtures[2].data, []byte("\x1b[38;2;")))
213+ require.Equal(t, 40, bytes.Count(fixtures[3].data, []byte("\r\n")))
214+
215+ for _, fixture := range fixtures {
216+ t.Run(fixture.name, func(t *testing.T) {
217+ proxy := newProxyANSIBenchmarkSession(t)
218+
219+ ack, reset, changed := proxy.applyOutputForGeneration(proxy.linkGeneration, ports.Output{
220+ BaseStateNum: 0,
221+ NewStateNum: 1,
222+ Data: fixture.data,
223+ })
224+
225+ require.Equal(t, uint64(1), ack)
226+ require.False(t, reset)
227+ require.True(t, changed)
228+ proxy.mu.Lock()
229+ require.Equal(t, domain.Size{Cols: 120, Rows: 40}, proxy.contentSize)
230+ proxy.mu.Unlock()
231+ })
232+ }
233+}
234+
235+func newProxyANSIBenchmarkSession(t testing.TB) *proxySession {
236+ t.Helper()
237+ proxy, err := newProxySession(domain.RemoteSessionKey{Host: "benchmark", Name: "ansi"}, domain.Size{Cols: 120, Rows: 40 + tabChromeRows})
238+ require.NoError(t, err)
239+ proxy.mu.Lock()
240+ contentSize := proxy.contentSize
241+ proxy.mu.Unlock()
242+ require.Equal(t, domain.Size{Cols: 120, Rows: 40}, contentSize)
243+ return proxy
244+}
245+
246+type proxyANSIBenchmarkFixture struct {
247+ name string
248+ data []byte
249+}
250+
251+func proxyANSIBenchmarkFixtures() []proxyANSIBenchmarkFixture {
252+ fullLine := append([]byte("\x1b[1;1H"), bytes.Repeat([]byte("l"), 120)...)
253+
254+ var styledLine []byte
255+ for fragment := range 12 {
256+ styledLine = fmt.Appendf(styledLine, "\x1b[12;%dH\x1b[38;2;%d;%d;%dm", fragment*10+1, fragment*17, fragment*13, fragment*7)
257+ styledLine = append(styledLine, bytes.Repeat([]byte{byte('a' + fragment)}, 10)...)
258+ }
259+ styledLine = append(styledLine, "\x1b[0m"...)
260+
261+ scroll := make([]byte, 0, 40*122)
262+ for row := range 40 {
263+ scroll = append(scroll, performanceFullWidthRow(120, 0, row)...)
264+ scroll = append(scroll, '\r', '\n')
265+ }
266+
267+ return []proxyANSIBenchmarkFixture{
268+ {name: "absolute-position-one-cell", data: []byte("\x1b[20;60HX")},
269+ {name: "120-column-full-line", data: fullLine},
270+ {name: "fragmented-truecolor-styled-line", data: styledLine},
271+ {name: "full-width-40-row-scroll", data: scroll},
272+ }
273+}
274+
199 func TestPerformanceFixtureCounters(t *testing.T) {275 func TestPerformanceFixtureCounters(t *testing.T) {
200 fixture := newPerformanceFixture(t, performanceConfig{})276 fixture := newPerformanceFixture(t, performanceConfig{})
201 277
@@ -359,6 +435,31 @@ var daemonSnapshotTopologies = []daemonHistoryTopology{
359 {name: "1tab-4panes", tabs: 1, panes: 4},435 {name: "1tab-4panes", tabs: 1, panes: 4},
360 }436 }
361 437
438+func BenchmarkProxyANSIApply(b *testing.B) {
439+ for _, fixture := range proxyANSIBenchmarkFixtures() {
440+ b.Run(fixture.name, func(b *testing.B) {
441+ proxy := newProxyANSIBenchmarkSession(b)
442+ generation := proxy.linkGeneration
443+ state := uint64(0)
444+ b.ReportAllocs()
445+ b.SetBytes(int64(len(fixture.data)))
446+ for b.Loop() {
447+ next := state + 1
448+ ack, reset, changed := proxy.applyOutputForGeneration(generation, ports.Output{
449+ BaseStateNum: state,
450+ NewStateNum: next,
451+ Data: fixture.data,
452+ })
453+ if ack != next || reset || !changed {
454+ b.Fatalf("apply output = ack %d, reset %t, changed %t; want ack %d, reset false, changed true", ack, reset, changed, next)
455+ }
456+ state = next
457+ }
458+ b.ReportMetric(float64(len(fixture.data)), "ansibytes/op")
459+ })
460+ }
461+}
462+
362 func BenchmarkDaemonHistoryLivePaint(b *testing.B) {463 func BenchmarkDaemonHistoryLivePaint(b *testing.B) {
363 benchmarkDaemonLargeHistory(b, "live-paint", func(f *performanceFixture) { f.paintLive() })464 benchmarkDaemonLargeHistory(b, "live-paint", func(f *performanceFixture) { f.paintLive() })
364 }465 }
M internal/usecase/daemon/role_effect_gate_test.go +31 -0
@@ -353,6 +353,37 @@ func TestKillSessionInterruptsCapturedSendWithoutClosingNewerIncarnation(t *test
353353 requireRoleGateRetired(t, ac)
354354 }
355355
356+func TestShutdownSignalsServeWhenParticipantGateAcquisitionTimesOut(t *testing.T) {
357+ d, sess, active, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{})
358+ transport := &closeTrackingTransport{}
359+ active.replaceTransport(transport)
360+ d.attachCoordinator(sess, nil, active, true)
361+ active.publishRoleCapability(sess.attachmentToken(active, transport))
362+
363+ owner := freezeRoleEffectGates(active)
364+ require.True(t, owner.acquired)
365+ defer owner.unfreeze()
366+
367+ clock := &signalClock{timers: make(chan *signalTimer, 1)}
368+ d.clock = clock
369+ shutdownDone := make(chan bool, 1)
370+ go func() { shutdownDone <- d.shutdownAll(ports.ReasonServerShutdown) }()
371+
372+ deadline := awaitTestValue(t, clock.timers, "shutdown did not arm the gate acquisition deadline")
373+ deadline.ch <- clock.Now()
374+ require.False(t, awaitTestValue(t, shutdownDone, "shutdown did not return after its gate acquisition deadline"))
375+
376+ d.mu.Lock()
377+ registered := d.sessions[sess.id]
378+ d.mu.Unlock()
379+ require.Same(t, sess, registered, "timed-out initial teardown should leave the session for Serve's shutdown pass")
380+ select {
381+ case <-d.done:
382+ case <-time.After(time.Second):
383+ t.Fatal("shutdown request did not signal Serve after the initial teardown pass timed out")
384+ }
385+}
386+
356387 func TestShutdownInterruptsBlockedParticipantSend(t *testing.T) {
357388 d, sess, active, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{})
358389 blocked := newTeardownBlockingTransport()
@@ -353,6 +353,37 @@ func TestKillSessionInterruptsCapturedSendWithoutClosingNewerIncarnation(t *test
353 requireRoleGateRetired(t, ac)353 requireRoleGateRetired(t, ac)
354 }354 }
355 355
356+func TestShutdownSignalsServeWhenParticipantGateAcquisitionTimesOut(t *testing.T) {
357+ d, sess, active, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{})
358+ transport := &closeTrackingTransport{}
359+ active.replaceTransport(transport)
360+ d.attachCoordinator(sess, nil, active, true)
361+ active.publishRoleCapability(sess.attachmentToken(active, transport))
362+
363+ owner := freezeRoleEffectGates(active)
364+ require.True(t, owner.acquired)
365+ defer owner.unfreeze()
366+
367+ clock := &signalClock{timers: make(chan *signalTimer, 1)}
368+ d.clock = clock
369+ shutdownDone := make(chan bool, 1)
370+ go func() { shutdownDone <- d.shutdownAll(ports.ReasonServerShutdown) }()
371+
372+ deadline := awaitTestValue(t, clock.timers, "shutdown did not arm the gate acquisition deadline")
373+ deadline.ch <- clock.Now()
374+ require.False(t, awaitTestValue(t, shutdownDone, "shutdown did not return after its gate acquisition deadline"))
375+
376+ d.mu.Lock()
377+ registered := d.sessions[sess.id]
378+ d.mu.Unlock()
379+ require.Same(t, sess, registered, "timed-out initial teardown should leave the session for Serve's shutdown pass")
380+ select {
381+ case <-d.done:
382+ case <-time.After(time.Second):
383+ t.Fatal("shutdown request did not signal Serve after the initial teardown pass timed out")
384+ }
385+}
386+
356 func TestShutdownInterruptsBlockedParticipantSend(t *testing.T) {387 func TestShutdownInterruptsBlockedParticipantSend(t *testing.T) {
357 d, sess, active, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{})388 d, sess, active, _ := newManualSessionWithPTYs(t, &transactionalResizePTY{})
358 blocked := newTeardownBlockingTransport()389 blocked := newTeardownBlockingTransport()
M internal/usecase/daemon/session_test.go +2 -1
@@ -310,8 +310,9 @@ func TestAttachReplaceKeepsOldClientSnatched(t *testing.T) {
310310 sess.mu.Lock()
311311 require.NotNil(t, sess.client)
312312 require.NotSame(t, acA, sess.client)
313- require.Contains(t, sess.snatched, acA)
313+ _, oldSnatched := sess.snatched[acA]
314314 sess.mu.Unlock()
315+ require.True(t, oldSnatched)
315316 require.Same(t, sess, acA.currentSession())
316317
317318 releaseA()
@@ -310,8 +310,9 @@ func TestAttachReplaceKeepsOldClientSnatched(t *testing.T) {
310 sess.mu.Lock()310 sess.mu.Lock()
311 require.NotNil(t, sess.client)311 require.NotNil(t, sess.client)
312 require.NotSame(t, acA, sess.client)312 require.NotSame(t, acA, sess.client)
313- require.Contains(t, sess.snatched, acA)313+ _, oldSnatched := sess.snatched[acA]
314 sess.mu.Unlock()314 sess.mu.Unlock()
315+ require.True(t, oldSnatched)
315 require.Same(t, sess, acA.currentSession())316 require.Same(t, sess, acA.currentSession())
316 317
317 releaseA()318 releaseA()

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