perf(harness): drive proxy workload shapes
- commit
- 9a4b27fd904918a3e81d827339f941ec13ffc109
- author
- brice · · 2026-08-01 04:53 UTC
- parent
- b54b862
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
4 files · +161 -9
M cmd/vev-perf-harness/launcher.go +20 -6
| @@ -116,6 +116,8 @@ type terminalOutput interface { | |||
| 116 | 116 | Close() error | |
| 117 | 117 | } | |
| 118 | 118 | ||
| 119 | + | var setPTYWinsize = rawterm.SetWinsize | |
| 120 | + | ||
| 119 | 121 | type cliProcess struct { | |
| 120 | 122 | cmd *exec.Cmd | |
| 121 | 123 | pty io.ReadWriteCloser | |
| @@ -135,19 +137,31 @@ type cliProcess struct { | |||
| 135 | 137 | 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 | 148 | func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProcess, error) { | |
| 139 | 149 | if len(role.Args) == 0 { | |
| 140 | 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 | 157 | return nil, err | |
| 144 | 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 | 161 | if m.Role == "ssh_stdio_peer" || m.Role == "udp_peer" { | |
| 148 | 162 | return l.preparePeer(m, role) | |
| 149 | 163 | } | |
| 150 | - | cmd := exec.Command(l.bin, role.Args...) | |
| 164 | + | cmd := exec.Command(bin, role.Args...) | |
| 151 | 165 | runDir := filepath.Dir(m.TracePath) | |
| 152 | 166 | runtimeDir, err := l.runtimeDir(runDir) | |
| 153 | 167 | if err != nil { | |
| @@ -157,9 +171,9 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces | |||
| 157 | 171 | // a role working directory: daemon-owned subprocess cleanup may otherwise | |
| 158 | 172 | // treat the evidence directory as its working tree and remove a preallocated | |
| 159 | 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 | 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 | 177 | if m.Role == "client" { | |
| 164 | 178 | l.mu.Lock() | |
| 165 | 179 | peer, routed := l.peers[runDir] | |
| @@ -236,7 +250,7 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces | |||
| 236 | 250 | p.shutdown = func() error { | |
| 237 | 251 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) | |
| 238 | 252 | defer cancel() | |
| 239 | - | shutdown := exec.CommandContext(ctx, l.bin, "kill", "--daemon") | |
| 253 | + | shutdown := exec.CommandContext(ctx, bin, "kill", "--daemon") | |
| 240 | 254 | shutdown.Env = withoutEnv(cmd.Env, "VEV_PERF_TRACE", "VEV_PERF_PROCESS_ID", "VEV_PERF_SCENARIO", "VEV_PERF_RUN") | |
| 241 | 255 | return shutdown.Run() | |
| 242 | 256 | } | |
| @@ -116,6 +116,8 @@ type terminalOutput interface { | |||||
| 116 | Close() error | 116 | Close() error | ||
| 117 | } | 117 | } | ||
| 118 | 118 | ||||
| 119 | + | var setPTYWinsize = rawterm.SetWinsize | |||
| 120 | + | ||||
| 119 | type cliProcess struct { | 121 | type cliProcess struct { | ||
| 120 | cmd *exec.Cmd | 122 | cmd *exec.Cmd | ||
| 121 | pty io.ReadWriteCloser | 123 | pty io.ReadWriteCloser | ||
| @@ -135,19 +137,31 @@ type cliProcess struct { | |||||
| 135 | closeResult error | 137 | 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, err | 157 | 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 otherwise | 171 | // a role working directory: daemon-owned subprocess cleanup may otherwise | ||
| 158 | // treat the evidence directory as its working tree and remove a preallocated | 172 | // 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] | ||
| @@ -236,7 +250,7 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces | |||||
| 236 | p.shutdown = func() error { | 250 | p.shutdown = func() error { | ||
| 237 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) | 251 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) | ||
| 238 | defer cancel() | 252 | defer cancel() | ||
| 239 | - | shutdown := exec.CommandContext(ctx, l.bin, "kill", "--daemon") | 253 | + | 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") | 254 | shutdown.Env = withoutEnv(cmd.Env, "VEV_PERF_TRACE", "VEV_PERF_PROCESS_ID", "VEV_PERF_SCENARIO", "VEV_PERF_RUN") | ||
| 241 | return shutdown.Run() | 255 | return shutdown.Run() | ||
| 242 | } | 256 | } | ||
M cmd/vev-perf-harness/launcher_test.go +30 -0
| @@ -24,6 +24,36 @@ func (discardTerminalOutput) Write(p []byte) (int, error) { return len(p), nil } | |||
| 24 | 24 | func (discardTerminalOutput) Sync() error { return nil } | |
| 25 | 25 | func (discardTerminalOutput) Close() error { return nil } | |
| 26 | 26 | ||
| 27 | + | type fakeFDPTY struct { | |
| 28 | + | *fakePTY | |
| 29 | + | fd uintptr | |
| 30 | + | } | |
| 31 | + | ||
| 32 | + | func (p *fakeFDPTY) Fd() uintptr { return p.fd } | |
| 33 | + | ||
| 34 | + | func TestCLIProcessResize(t *testing.T) { | |
| 35 | + | original := setPTYWinsize | |
| 36 | + | t.Cleanup(func() { setPTYWinsize = original }) | |
| 37 | + | ||
| 38 | + | var gotFD int | |
| 39 | + | var gotCols, gotRows uint16 | |
| 40 | + | setPTYWinsize = func(fd int, cols, rows uint16) error { | |
| 41 | + | gotFD, gotCols, gotRows = fd, cols, rows | |
| 42 | + | return nil | |
| 43 | + | } | |
| 44 | + | p := &cliProcess{pty: &fakeFDPTY{fakePTY: &fakePTY{}, fd: 42}} | |
| 45 | + | if err := p.Resize(119, 40); err != nil { | |
| 46 | + | t.Fatal(err) | |
| 47 | + | } | |
| 48 | + | if gotFD != 42 || gotCols != 119 || gotRows != 40 { | |
| 49 | + | t.Fatalf("winsize setter got fd=%d cols=%d rows=%d", gotFD, gotCols, gotRows) | |
| 50 | + | } | |
| 51 | + | ||
| 52 | + | if err := (&cliProcess{pty: &fakePTY{}}).Resize(119, 40); err == nil || !strings.Contains(err.Error(), "file descriptor") { | |
| 53 | + | t.Fatalf("Resize without FD error = %v", err) | |
| 54 | + | } | |
| 55 | + | } | |
| 56 | + | ||
| 27 | 57 | func TestCLIProcessCloseDrainsQueuedTerminalOutputBeforeClientExit(t *testing.T) { | |
| 28 | 58 | harnessPTY, clientPTY := net.Pipe() | |
| 29 | 59 | t.Cleanup(func() { _ = clientPTY.Close() }) | |
| @@ -24,6 +24,36 @@ func (discardTerminalOutput) Write(p []byte) (int, error) { return len(p), nil } | |||||
| 24 | func (discardTerminalOutput) Sync() error { return nil } | 24 | func (discardTerminalOutput) Sync() error { return nil } | ||
| 25 | func (discardTerminalOutput) Close() error { return nil } | 25 | func (discardTerminalOutput) Close() error { return nil } | ||
| 26 | 26 | ||||
| 27 | + | type fakeFDPTY struct { | |||
| 28 | + | *fakePTY | |||
| 29 | + | fd uintptr | |||
| 30 | + | } | |||
| 31 | + | ||||
| 32 | + | func (p *fakeFDPTY) Fd() uintptr { return p.fd } | |||
| 33 | + | ||||
| 34 | + | func TestCLIProcessResize(t *testing.T) { | |||
| 35 | + | original := setPTYWinsize | |||
| 36 | + | t.Cleanup(func() { setPTYWinsize = original }) | |||
| 37 | + | ||||
| 38 | + | var gotFD int | |||
| 39 | + | var gotCols, gotRows uint16 | |||
| 40 | + | setPTYWinsize = func(fd int, cols, rows uint16) error { | |||
| 41 | + | gotFD, gotCols, gotRows = fd, cols, rows | |||
| 42 | + | return nil | |||
| 43 | + | } | |||
| 44 | + | p := &cliProcess{pty: &fakeFDPTY{fakePTY: &fakePTY{}, fd: 42}} | |||
| 45 | + | if err := p.Resize(119, 40); err != nil { | |||
| 46 | + | t.Fatal(err) | |||
| 47 | + | } | |||
| 48 | + | if gotFD != 42 || gotCols != 119 || gotRows != 40 { | |||
| 49 | + | t.Fatalf("winsize setter got fd=%d cols=%d rows=%d", gotFD, gotCols, gotRows) | |||
| 50 | + | } | |||
| 51 | + | ||||
| 52 | + | if err := (&cliProcess{pty: &fakePTY{}}).Resize(119, 40); err == nil || !strings.Contains(err.Error(), "file descriptor") { | |||
| 53 | + | t.Fatalf("Resize without FD error = %v", err) | |||
| 54 | + | } | |||
| 55 | + | } | |||
| 56 | + | ||||
| 27 | func TestCLIProcessCloseDrainsQueuedTerminalOutputBeforeClientExit(t *testing.T) { | 57 | func TestCLIProcessCloseDrainsQueuedTerminalOutputBeforeClientExit(t *testing.T) { | ||
| 28 | harnessPTY, clientPTY := net.Pipe() | 58 | harnessPTY, clientPTY := net.Pipe() | ||
| 29 | t.Cleanup(func() { _ = clientPTY.Close() }) | 59 | t.Cleanup(func() { _ = clientPTY.Close() }) | ||
M cmd/vev-perf-harness/runner.go +35 -3
| @@ -307,6 +307,15 @@ func (h *harness) runOne(o options, mat manifest, s scenario, run int, raw io.Wr | |||
| 307 | 307 | flushedRecorded = true | |
| 308 | 308 | return nil | |
| 309 | 309 | } | |
| 310 | + | if cols, rows, resize := workloadResize(s, sequence); resize { | |
| 311 | + | resizer, ok := p.process.(processResizer) | |
| 312 | + | if !ok { | |
| 313 | + | return res, errors.New("client process does not support resize workload") | |
| 314 | + | } | |
| 315 | + | if resizeErr := resizer.Resize(cols, rows); resizeErr != nil { | |
| 316 | + | return res, fmt.Errorf("resize client PTY: %w", resizeErr) | |
| 317 | + | } | |
| 318 | + | } | |
| 310 | 319 | if measureErr := p.process.Measure(workloadInput(s, run, fmt.Sprintf("measured-%d", sequence)), injected, flushed); measureErr != nil { | |
| 311 | 320 | if injectedRecorded && !flushedRecorded { | |
| 312 | 321 | diagnosticErr := recordMark(harnessMark{Scenario: s.ID, Run: run, Sequence: sequence, Kind: "terminal_flushed", Tick: h.clock.Now(), Valid: false}) | |
| @@ -418,6 +427,20 @@ func launchOrder(mappings []processMapping) []processMapping { | |||
| 418 | 427 | return out | |
| 419 | 428 | } | |
| 420 | 429 | ||
| 430 | + | type processResizer interface { | |
| 431 | + | Resize(cols, rows uint16) error | |
| 432 | + | } | |
| 433 | + | ||
| 434 | + | func workloadResize(s scenario, sequence uint64) (uint16, uint16, bool) { | |
| 435 | + | if s.Workload != "resize_sweep" && s.Workload != "snapshot_output_resize" { | |
| 436 | + | return 0, 0, false | |
| 437 | + | } | |
| 438 | + | if sequence%2 == 1 { | |
| 439 | + | return 119, 40, true | |
| 440 | + | } | |
| 441 | + | return 120, 40, true | |
| 442 | + | } | |
| 443 | + | ||
| 421 | 444 | // workloadInput is shell input delivered through the client PTY. It avoids any | |
| 422 | 445 | // private daemon API and makes every topology/workload/transport manifest row | |
| 423 | 446 | // execute a real terminal workload. The marker is the output boundary awaited | |
| @@ -426,16 +449,25 @@ func workloadInput(s scenario, run int, phase string) []byte { | |||
| 426 | 449 | marker := fmt.Sprintf("__VEV_HARNESS_%s_r%d_%s__", safeName(s.ID), run, safeName(phase)) | |
| 427 | 450 | body := "printf 'vev perf %s\\n'" | |
| 428 | 451 | switch s.Workload { | |
| 429 | - | case "active_output", "all_output", "inactive_output", "interactive_flood": | |
| 452 | + | case "active_output": | |
| 453 | + | body = `printf '\033[10;20HX'; : '%s'` | |
| 454 | + | case "all_output": | |
| 455 | + | body = `printf '%%-120s' 'vev perf line %s'` | |
| 456 | + | case "inactive_output": | |
| 457 | + | 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'` | |
| 458 | + | case "interactive_flood": | |
| 430 | 459 | body = "i=0; while [ $i -lt 128 ]; do printf 'vev perf output %s\\n'; i=$((i+1)); done" | |
| 431 | 460 | case "resize_sweep": | |
| 432 | 461 | body = "printf 'vev perf resize-sweep %s\\n'" | |
| 433 | 462 | case "copy_search": | |
| 434 | 463 | body = "printf 'vev perf copy-search %s\\n'" | |
| 435 | 464 | case "snapshot_output_resize": | |
| 436 | - | body = "printf 'vev perf snapshot-output-resize %s\\n'" | |
| 465 | + | body = `printf '\033[2J\033[Hvev perf snapshot-output-resize %s\n'` | |
| 437 | 466 | case "attach_restore_tab_switch": | |
| 438 | - | body = "printf 'vev perf attach-restore-tab-switch %s\\n'" | |
| 467 | + | body = `"$VEV_PERF_BIN" cmd next-tab; "$VEV_PERF_BIN" cmd previous-tab; printf 'vev perf attach-restore-tab-switch %s\n'` | |
| 468 | + | if phase == "warmup" { | |
| 469 | + | body = `"$VEV_PERF_BIN" cmd new-tab; "$VEV_PERF_BIN" cmd previous-tab; printf 'vev perf attach-restore-tab-switch %s\n'` | |
| 470 | + | } | |
| 439 | 471 | } | |
| 440 | 472 | return []byte(fmt.Sprintf(body+"; printf '%s\\n'\n", s.ID, marker)) | |
| 441 | 473 | } | |
| @@ -307,6 +307,15 @@ func (h *harness) runOne(o options, mat manifest, s scenario, run int, raw io.Wr | |||||
| 307 | flushedRecorded = true | 307 | flushedRecorded = true | ||
| 308 | return nil | 308 | return nil | ||
| 309 | } | 309 | } | ||
| 310 | + | if cols, rows, resize := workloadResize(s, sequence); resize { | |||
| 311 | + | resizer, ok := p.process.(processResizer) | |||
| 312 | + | if !ok { | |||
| 313 | + | return res, errors.New("client process does not support resize workload") | |||
| 314 | + | } | |||
| 315 | + | if resizeErr := resizer.Resize(cols, rows); resizeErr != nil { | |||
| 316 | + | return res, fmt.Errorf("resize client PTY: %w", resizeErr) | |||
| 317 | + | } | |||
| 318 | + | } | |||
| 310 | if measureErr := p.process.Measure(workloadInput(s, run, fmt.Sprintf("measured-%d", sequence)), injected, flushed); measureErr != nil { | 319 | if measureErr := p.process.Measure(workloadInput(s, run, fmt.Sprintf("measured-%d", sequence)), injected, flushed); measureErr != nil { | ||
| 311 | if injectedRecorded && !flushedRecorded { | 320 | if injectedRecorded && !flushedRecorded { | ||
| 312 | diagnosticErr := recordMark(harnessMark{Scenario: s.ID, Run: run, Sequence: sequence, Kind: "terminal_flushed", Tick: h.clock.Now(), Valid: false}) | 321 | diagnosticErr := recordMark(harnessMark{Scenario: s.ID, Run: run, Sequence: sequence, Kind: "terminal_flushed", Tick: h.clock.Now(), Valid: false}) | ||
| @@ -418,6 +427,20 @@ func launchOrder(mappings []processMapping) []processMapping { | |||||
| 418 | return out | 427 | return out | ||
| 419 | } | 428 | } | ||
| 420 | 429 | ||||
| 430 | + | type processResizer interface { | |||
| 431 | + | Resize(cols, rows uint16) error | |||
| 432 | + | } | |||
| 433 | + | ||||
| 434 | + | func workloadResize(s scenario, sequence uint64) (uint16, uint16, bool) { | |||
| 435 | + | if s.Workload != "resize_sweep" && s.Workload != "snapshot_output_resize" { | |||
| 436 | + | return 0, 0, false | |||
| 437 | + | } | |||
| 438 | + | if sequence%2 == 1 { | |||
| 439 | + | return 119, 40, true | |||
| 440 | + | } | |||
| 441 | + | return 120, 40, true | |||
| 442 | + | } | |||
| 443 | + | ||||
| 421 | // workloadInput is shell input delivered through the client PTY. It avoids any | 444 | // workloadInput is shell input delivered through the client PTY. It avoids any | ||
| 422 | // private daemon API and makes every topology/workload/transport manifest row | 445 | // private daemon API and makes every topology/workload/transport manifest row | ||
| 423 | // execute a real terminal workload. The marker is the output boundary awaited | 446 | // execute a real terminal workload. The marker is the output boundary awaited | ||
| @@ -426,16 +449,25 @@ 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)) | 449 | marker := fmt.Sprintf("__VEV_HARNESS_%s_r%d_%s__", safeName(s.ID), run, safeName(phase)) | ||
| 427 | body := "printf 'vev perf %s\\n'" | 450 | body := "printf 'vev perf %s\\n'" | ||
| 428 | switch s.Workload { | 451 | switch s.Workload { | ||
| 429 | - | case "active_output", "all_output", "inactive_output", "interactive_flood": | 452 | + | case "active_output": |
| 453 | + | body = `printf '\033[10;20HX'; : '%s'` | |||
| 454 | + | case "all_output": | |||
| 455 | + | body = `printf '%%-120s' 'vev perf line %s'` | |||
| 456 | + | case "inactive_output": | |||
| 457 | + | 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'` | |||
| 458 | + | case "interactive_flood": | |||
| 430 | body = "i=0; while [ $i -lt 128 ]; do printf 'vev perf output %s\\n'; i=$((i+1)); done" | 459 | body = "i=0; while [ $i -lt 128 ]; do printf 'vev perf output %s\\n'; i=$((i+1)); done" | ||
| 431 | case "resize_sweep": | 460 | case "resize_sweep": | ||
| 432 | body = "printf 'vev perf resize-sweep %s\\n'" | 461 | body = "printf 'vev perf resize-sweep %s\\n'" | ||
| 433 | case "copy_search": | 462 | case "copy_search": | ||
| 434 | body = "printf 'vev perf copy-search %s\\n'" | 463 | body = "printf 'vev perf copy-search %s\\n'" | ||
| 435 | case "snapshot_output_resize": | 464 | case "snapshot_output_resize": | ||
| 436 | - | body = "printf 'vev perf snapshot-output-resize %s\\n'" | 465 | + | body = `printf '\033[2J\033[Hvev perf snapshot-output-resize %s\n'` |
| 437 | case "attach_restore_tab_switch": | 466 | case "attach_restore_tab_switch": | ||
| 438 | - | body = "printf 'vev perf attach-restore-tab-switch %s\\n'" | 467 | + | body = `"$VEV_PERF_BIN" cmd next-tab; "$VEV_PERF_BIN" cmd previous-tab; printf 'vev perf attach-restore-tab-switch %s\n'` |
| 468 | + | if phase == "warmup" { | |||
| 469 | + | body = `"$VEV_PERF_BIN" cmd new-tab; "$VEV_PERF_BIN" cmd previous-tab; printf 'vev perf attach-restore-tab-switch %s\n'` | |||
| 470 | + | } | |||
| 439 | } | 471 | } | ||
| 440 | return []byte(fmt.Sprintf(body+"; printf '%s\\n'\n", s.ID, marker)) | 472 | return []byte(fmt.Sprintf(body+"; printf '%s\\n'\n", s.ID, marker)) | ||
| 441 | } | 473 | } | ||
M cmd/vev-perf-harness/runner_test.go +76 -0
| @@ -309,6 +309,82 @@ 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;20HX'`}}, | |
| 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 | + | active := string(workloadInput(scenario{ID: "s", Workload: "active_output"}, 1, "measured-1")) | |
| 343 | + | if strings.Contains(active, `\r`) || strings.Contains(active, "vev perf") { | |
| 344 | + | t.Fatalf("active output is not an absolute-cursor one-cell write: %q", active) | |
| 345 | + | } | |
| 346 | + | inactive := string(workloadInput(scenario{ID: "s", Workload: "inactive_output"}, 1, "measured-1")) | |
| 347 | + | if count := strings.Count(inactive, `\033[38;2;`); count != 3 { | |
| 348 | + | t.Fatalf("inactive output truecolor runs = %d, want 3: %q", count, inactive) | |
| 349 | + | } | |
| 350 | + | ||
| 351 | + | warmup := string(workloadInput(scenario{ID: "s", Workload: "attach_restore_tab_switch"}, 1, "warmup")) | |
| 352 | + | if count := strings.Count(warmup, `"$VEV_PERF_BIN" cmd new-tab`); count != 1 { | |
| 353 | + | t.Fatalf("attach warmup new-tab commands = %d, want 1: %q", count, warmup) | |
| 354 | + | } | |
| 355 | + | if strings.Contains(warmup, "cmd next-tab") || !strings.Contains(warmup, `cmd previous-tab; printf 'vev perf attach`) { | |
| 356 | + | t.Fatalf("attach warmup does not return to the marker-producing tab: %q", warmup) | |
| 357 | + | } | |
| 358 | + | measured := string(workloadInput(scenario{ID: "s", Workload: "attach_restore_tab_switch"}, 1, "measured-1")) | |
| 359 | + | if strings.Contains(measured, "cmd new-tab") { | |
| 360 | + | t.Fatalf("measured attach workload creates another tab: %q", measured) | |
| 361 | + | } | |
| 362 | + | } | |
| 363 | + | ||
| 364 | + | func TestWorkloadResize(t *testing.T) { | |
| 365 | + | for _, tt := range []struct { | |
| 366 | + | workload string | |
| 367 | + | sequence uint64 | |
| 368 | + | wantCols uint16 | |
| 369 | + | wantOK bool | |
| 370 | + | }{ | |
| 371 | + | {"active_output", 1, 0, false}, | |
| 372 | + | {"resize_sweep", 1, 119, true}, | |
| 373 | + | {"resize_sweep", 2, 120, true}, | |
| 374 | + | {"snapshot_output_resize", 1, 119, true}, | |
| 375 | + | } { | |
| 376 | + | t.Run(fmt.Sprintf("%s-%d", tt.workload, tt.sequence), func(t *testing.T) { | |
| 377 | + | cols, rows, ok := workloadResize(scenario{Workload: tt.workload}, tt.sequence) | |
| 378 | + | if ok != tt.wantOK || cols != tt.wantCols { | |
| 379 | + | t.Fatalf("workloadResize() = (%d, %d, %t), want cols=%d ok=%t", cols, rows, ok, tt.wantCols, tt.wantOK) | |
| 380 | + | } | |
| 381 | + | if ok && rows != 40 { | |
| 382 | + | t.Fatalf("workloadResize() rows = %d, want 40", rows) | |
| 383 | + | } | |
| 384 | + | }) | |
| 385 | + | } | |
| 386 | + | } | |
| 387 | + | ||
| 312 | 388 | func TestHarnessAggregatesRawEventsSeparatelyFromRunDispersion(t *testing.T) { | |
| 313 | 389 | first := append([]int64(nil), make([]int64, minimumInIntervalEventSamples+5)...) | |
| 314 | 390 | for i := range first { | |
| @@ -309,6 +309,82 @@ 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;20HX'`}}, | |||
| 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 | + | active := string(workloadInput(scenario{ID: "s", Workload: "active_output"}, 1, "measured-1")) | |||
| 343 | + | if strings.Contains(active, `\r`) || strings.Contains(active, "vev perf") { | |||
| 344 | + | t.Fatalf("active output is not an absolute-cursor one-cell write: %q", active) | |||
| 345 | + | } | |||
| 346 | + | inactive := string(workloadInput(scenario{ID: "s", Workload: "inactive_output"}, 1, "measured-1")) | |||
| 347 | + | if count := strings.Count(inactive, `\033[38;2;`); count != 3 { | |||
| 348 | + | t.Fatalf("inactive output truecolor runs = %d, want 3: %q", count, inactive) | |||
| 349 | + | } | |||
| 350 | + | ||||
| 351 | + | warmup := string(workloadInput(scenario{ID: "s", Workload: "attach_restore_tab_switch"}, 1, "warmup")) | |||
| 352 | + | if count := strings.Count(warmup, `"$VEV_PERF_BIN" cmd new-tab`); count != 1 { | |||
| 353 | + | t.Fatalf("attach warmup new-tab commands = %d, want 1: %q", count, warmup) | |||
| 354 | + | } | |||
| 355 | + | if strings.Contains(warmup, "cmd next-tab") || !strings.Contains(warmup, `cmd previous-tab; printf 'vev perf attach`) { | |||
| 356 | + | t.Fatalf("attach warmup does not return to the marker-producing tab: %q", warmup) | |||
| 357 | + | } | |||
| 358 | + | measured := string(workloadInput(scenario{ID: "s", Workload: "attach_restore_tab_switch"}, 1, "measured-1")) | |||
| 359 | + | if strings.Contains(measured, "cmd new-tab") { | |||
| 360 | + | t.Fatalf("measured attach workload creates another tab: %q", measured) | |||
| 361 | + | } | |||
| 362 | + | } | |||
| 363 | + | ||||
| 364 | + | func TestWorkloadResize(t *testing.T) { | |||
| 365 | + | for _, tt := range []struct { | |||
| 366 | + | workload string | |||
| 367 | + | sequence uint64 | |||
| 368 | + | wantCols uint16 | |||
| 369 | + | wantOK bool | |||
| 370 | + | }{ | |||
| 371 | + | {"active_output", 1, 0, false}, | |||
| 372 | + | {"resize_sweep", 1, 119, true}, | |||
| 373 | + | {"resize_sweep", 2, 120, true}, | |||
| 374 | + | {"snapshot_output_resize", 1, 119, true}, | |||
| 375 | + | } { | |||
| 376 | + | t.Run(fmt.Sprintf("%s-%d", tt.workload, tt.sequence), func(t *testing.T) { | |||
| 377 | + | cols, rows, ok := workloadResize(scenario{Workload: tt.workload}, tt.sequence) | |||
| 378 | + | if ok != tt.wantOK || cols != tt.wantCols { | |||
| 379 | + | t.Fatalf("workloadResize() = (%d, %d, %t), want cols=%d ok=%t", cols, rows, ok, tt.wantCols, tt.wantOK) | |||
| 380 | + | } | |||
| 381 | + | if ok && rows != 40 { | |||
| 382 | + | t.Fatalf("workloadResize() rows = %d, want 40", rows) | |||
| 383 | + | } | |||
| 384 | + | }) | |||
| 385 | + | } | |||
| 386 | + | } | |||
| 387 | + | ||||
| 312 | func TestHarnessAggregatesRawEventsSeparatelyFromRunDispersion(t *testing.T) { | 388 | func TestHarnessAggregatesRawEventsSeparatelyFromRunDispersion(t *testing.T) { | ||
| 313 | first := append([]int64(nil), make([]int64, minimumInIntervalEventSamples+5)...) | 389 | first := append([]int64(nil), make([]int64, minimumInIntervalEventSamples+5)...) | ||
| 314 | for i := range first { | 390 | for i := range first { | ||