fix(perf): validate proxy measurements
- commit
- a57483f66d5e9cbd5323650782d1880912ad20aa
- author
- brice · · 2026-08-01 05:30 UTC
- parent
- 9a66c88
- signature
- GPG Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· arch-gpg Verified for the committer email
9 files · +262 -26
M cmd/vev-perf-harness/evidence_test.go +10 -1
| @@ -254,7 +254,13 @@ func TestHarnessWritesRequiredEvidenceWithSufficientSpans(t *testing.T) { | |||
| 254 | 254 | h := defaultHarness() | |
| 255 | 255 | h.clock = &fakeClock{} | |
| 256 | 256 | h.launcher = l | |
| 257 | - | 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 | + | } | |
| 258 | 264 | if err := run([]string{"--vev-bin", "ignored", "--manifest", manifestPath, "--out", filepath.Join(dir, "out"), "--warmup", "1s", "--duration", "30s", "--repetitions", "10"}, h); err != nil { | |
| 259 | 265 | t.Fatal(err) | |
| 260 | 266 | } | |
| @@ -271,6 +277,9 @@ func TestHarnessWritesRequiredEvidenceWithSufficientSpans(t *testing.T) { | |||
| 271 | 277 | if err := json.Unmarshal(b, &got); err != nil { | |
| 272 | 278 | t.Fatal(err) | |
| 273 | 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 | + | } | |
| 274 | 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 { | |
| 275 | 284 | t.Fatalf("summary=%+v", got) | |
| 276 | 285 | } | |
| @@ -254,7 +254,13 @@ func TestHarnessWritesRequiredEvidenceWithSufficientSpans(t *testing.T) { | |||||
| 254 | h := defaultHarness() | 254 | h := defaultHarness() | ||
| 255 | h.clock = &fakeClock{} | 255 | h.clock = &fakeClock{} | ||
| 256 | h.launcher = l | 256 | h.launcher = l | ||
| 257 | - | 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 | + | } | |||
| 258 | 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 { | ||
| 259 | t.Fatal(err) | 265 | t.Fatal(err) | ||
| 260 | } | 266 | } | ||
| @@ -271,6 +277,9 @@ func TestHarnessWritesRequiredEvidenceWithSufficientSpans(t *testing.T) { | |||||
| 271 | if err := json.Unmarshal(b, &got); err != nil { | 277 | if err := json.Unmarshal(b, &got); err != nil { | ||
| 272 | t.Fatal(err) | 278 | t.Fatal(err) | ||
| 273 | } | 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 | + | } | |||
| 274 | 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 { | ||
| 275 | t.Fatalf("summary=%+v", got) | 284 | t.Fatalf("summary=%+v", got) | ||
| 276 | } | 285 | } | ||
M cmd/vev-perf-harness/launcher.go +5 -0
| @@ -198,6 +198,11 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces | |||
| 198 | 198 | if err != nil { | |
| 199 | 199 | return nil, err | |
| 200 | 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 | + | } | |
| 201 | 206 | output, err := os.OpenFile(filepath.Join(filepath.Dir(m.TracePath), "terminal-output"), os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0o600) | |
| 202 | 207 | if err != nil { | |
| 203 | 208 | _ = master.Close() | |
| @@ -198,6 +198,11 @@ func (l *cliLauncher) Launch(m processMapping, role roleCommand) (launchedProces | |||||
| 198 | if err != nil { | 198 | if err != nil { | ||
| 199 | return nil, err | 199 | return nil, err | ||
| 200 | } | 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 | + | } | |||
| 201 | 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) | ||
| 202 | if err != nil { | 207 | if err != nil { | ||
| 203 | _ = master.Close() | 208 | _ = master.Close() | ||
M cmd/vev-perf-harness/launcher_test.go +84 -0
| @@ -5,6 +5,7 @@ package main | |||
| 5 | 5 | import ( | |
| 6 | 6 | "bytes" | |
| 7 | 7 | "context" | |
| 8 | + | "errors" | |
| 8 | 9 | "fmt" | |
| 9 | 10 | "net" | |
| 10 | 11 | "os" | |
| @@ -12,6 +13,7 @@ import ( | |||
| 12 | 13 | "path/filepath" | |
| 13 | 14 | "strings" | |
| 14 | 15 | "sync" | |
| 16 | + | "syscall" | |
| 15 | 17 | "testing" | |
| 16 | 18 | "time" | |
| 17 | 19 | ||
| @@ -31,6 +33,88 @@ type fakeFDPTY struct { | |||
| 31 | 33 | ||
| 32 | 34 | func (p *fakeFDPTY) Fd() uintptr { return p.fd } | |
| 33 | 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: %v", 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: %v", 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 | + | ||
| 34 | 118 | func TestCLIProcessResize(t *testing.T) { | |
| 35 | 119 | original := setPTYWinsize | |
| 36 | 120 | t.Cleanup(func() { setPTYWinsize = original }) | |
| @@ -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 | ||||
| @@ -31,6 +33,88 @@ type fakeFDPTY struct { | |||||
| 31 | 33 | ||||
| 32 | func (p *fakeFDPTY) Fd() uintptr { return p.fd } | 34 | func (p *fakeFDPTY) Fd() uintptr { return p.fd } | ||
| 33 | 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: %v", 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: %v", 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 | + | ||||
| 34 | func TestCLIProcessResize(t *testing.T) { | 118 | func TestCLIProcessResize(t *testing.T) { | ||
| 35 | original := setPTYWinsize | 119 | original := setPTYWinsize | ||
| 36 | t.Cleanup(func() { setPTYWinsize = original }) | 120 | t.Cleanup(func() { setPTYWinsize = original }) | ||
M cmd/vev-perf-harness/main.go +1 -1
| @@ -18,5 +18,5 @@ func main() { | |||
| 18 | 18 | } | |
| 19 | 19 | ||
| 20 | 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 | } | |
| @@ -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 { | |||
| 99 | 99 | type summary struct { | |
| 100 | 100 | Schema uint16 `json:"schema"` | |
| 101 | 101 | GitSHA string `json:"git_sha"` | |
| 102 | + | VevGitSHA string `json:"vev_git_sha"` | |
| 102 | 103 | Warmup string `json:"warmup"` | |
| 103 | 104 | Duration string `json:"duration"` | |
| 104 | 105 | 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 +49 -9
| @@ -4,6 +4,7 @@ package main | |||
| 4 | 4 | ||
| 5 | 5 | import ( | |
| 6 | 6 | "context" | |
| 7 | + | "debug/buildinfo" | |
| 7 | 8 | "encoding/json" | |
| 8 | 9 | "errors" | |
| 9 | 10 | "fmt" | |
| @@ -13,6 +14,7 @@ import ( | |||
| 13 | 14 | "path/filepath" | |
| 14 | 15 | "runtime/debug" | |
| 15 | 16 | "sort" | |
| 17 | + | "strconv" | |
| 16 | 18 | "strings" | |
| 17 | 19 | "time" | |
| 18 | 20 | ||
| @@ -51,6 +53,7 @@ type harness struct { | |||
| 51 | 53 | create func(string) (*os.File, error) | |
| 52 | 54 | removeAll func(string) error | |
| 53 | 55 | gitSHA func() string | |
| 56 | + | vevGitSHA func(string) string | |
| 54 | 57 | selectScenarios func(manifest) []scenario // test-only bounded fixture seam | |
| 55 | 58 | } | |
| 56 | 59 | ||
| @@ -71,6 +74,19 @@ func createExclusiveRunDir(path string) error { | |||
| 71 | 74 | // go run evidence attributable as well. | |
| 72 | 75 | 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 | 90 | func recordedGitSHA() string { | |
| 75 | 91 | if info, ok := debug.ReadBuildInfo(); ok { | |
| 76 | 92 | for _, setting := range info.Settings { | |
| @@ -148,7 +164,7 @@ func run(args []string, h *harness) (err error) { | |||
| 148 | 164 | if err := writeJSON(filepath.Join(opt.out, "runs.json"), results); err != nil { | |
| 149 | 165 | 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 | 170 | // aggregateEventResults deliberately preserves raw event latencies for | |
| @@ -307,16 +323,22 @@ func (h *harness) runOne(o options, mat manifest, s scenario, run int, raw io.Wr | |||
| 307 | 323 | flushedRecorded = true | |
| 308 | 324 | return nil | |
| 309 | 325 | } | |
| 326 | + | measureInjected := injected | |
| 310 | 327 | if cols, rows, resize := workloadResize(s, sequence); resize { | |
| 311 | 328 | resizer, ok := p.process.(processResizer) | |
| 312 | 329 | if !ok { | |
| 313 | 330 | return res, errors.New("client process does not support resize workload") | |
| 314 | 331 | } | |
| 332 | + | if err := injected(); err != nil { | |
| 333 | + | return res, err | |
| 334 | + | } | |
| 335 | + | measureInjected = func() error { return nil } | |
| 315 | 336 | if resizeErr := resizer.Resize(cols, rows); resizeErr != nil { | |
| 316 | - | return res, fmt.Errorf("resize client PTY: %w", resizeErr) | |
| 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) | |
| 317 | 339 | } | |
| 318 | 340 | } | |
| 319 | - | if measureErr := p.process.Measure(workloadInput(s, run, fmt.Sprintf("measured-%d", sequence)), injected, flushed); measureErr != nil { | |
| 341 | + | if measureErr := p.process.Measure(workloadInput(s, run, fmt.Sprintf("measured-%d", sequence)), measureInjected, flushed); measureErr != nil { | |
| 320 | 342 | if injectedRecorded && !flushedRecorded { | |
| 321 | 343 | diagnosticErr := recordMark(harnessMark{Scenario: s.ID, Run: run, Sequence: sequence, Kind: "terminal_flushed", Tick: h.clock.Now(), Valid: false}) | |
| 322 | 344 | return res, errors.Join(measureErr, diagnosticErr) | |
| @@ -448,9 +470,14 @@ func workloadResize(s scenario, sequence uint64) (uint16, uint16, bool) { | |||
| 448 | 470 | func workloadInput(s scenario, run int, phase string) []byte { | |
| 449 | 471 | marker := fmt.Sprintf("__VEV_HARNESS_%s_r%d_%s__", safeName(s.ID), run, safeName(phase)) | |
| 450 | 472 | body := "printf 'vev perf %s\\n'" | |
| 473 | + | markerSeparator := "; " | |
| 451 | 474 | switch s.Workload { | |
| 452 | 475 | case "active_output": | |
| 453 | - | body = `printf '\033[10;20HX'; : '%s'` | |
| 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) | |
| 454 | 481 | case "all_output": | |
| 455 | 482 | body = `printf '%%-120s' 'vev perf line %s'` | |
| 456 | 483 | case "inactive_output": | |
| @@ -458,16 +485,29 @@ func workloadInput(s scenario, run int, phase string) []byte { | |||
| 458 | 485 | case "interactive_flood": | |
| 459 | 486 | body = "i=0; while [ $i -lt 128 ]; do printf 'vev perf output %s\\n'; i=$((i+1)); done" | |
| 460 | 487 | case "resize_sweep": | |
| 461 | - | 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 = " && " | |
| 462 | 491 | case "copy_search": | |
| 463 | 492 | body = "printf 'vev perf copy-search %s\\n'" | |
| 464 | 493 | case "snapshot_output_resize": | |
| 465 | - | body = `printf '\033[2J\033[Hvev 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 = " && " | |
| 466 | 497 | case "attach_restore_tab_switch": | |
| 467 | - | body = `"$VEV_PERF_BIN" cmd next-tab; "$VEV_PERF_BIN" cmd previous-tab; 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'` | |
| 468 | 499 | 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'` | |
| 500 | + | body = `"$VEV_PERF_BIN" cmd new-tab && "$VEV_PERF_BIN" cmd previous-tab && printf 'vev perf attach-restore-tab-switch %s\n'` | |
| 470 | 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 | |
| 471 | 511 | } | |
| 472 | - | return []byte(fmt.Sprintf(body+"; printf '%s\\n'\n", s.ID, marker)) | |
| 512 | + | return 120, 40 | |
| 473 | 513 | } | |
| @@ -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) error | 54 | removeAll func(string) error | ||
| 53 | gitSHA func() string | 55 | gitSHA func() string | ||
| 56 | + | vevGitSHA func(string) string | |||
| 54 | selectScenarios func(manifest) []scenario // test-only bounded fixture seam | 57 | 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.Second | 75 | 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 err | 165 | 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 for | 170 | // aggregateEventResults deliberately preserves raw event latencies for | ||
| @@ -307,16 +323,22 @@ func (h *harness) runOne(o options, mat manifest, s scenario, run int, raw io.Wr | |||||
| 307 | flushedRecorded = true | 323 | flushedRecorded = true | ||
| 308 | return nil | 324 | return nil | ||
| 309 | } | 325 | } | ||
| 326 | + | measureInjected := injected | |||
| 310 | if cols, rows, resize := workloadResize(s, sequence); resize { | 327 | if cols, rows, resize := workloadResize(s, sequence); resize { | ||
| 311 | resizer, ok := p.process.(processResizer) | 328 | resizer, ok := p.process.(processResizer) | ||
| 312 | if !ok { | 329 | if !ok { | ||
| 313 | return res, errors.New("client process does not support resize workload") | 330 | return res, errors.New("client process does not support resize workload") | ||
| 314 | } | 331 | } | ||
| 332 | + | if err := injected(); err != nil { | |||
| 333 | + | return res, err | |||
| 334 | + | } | |||
| 335 | + | measureInjected = func() error { return nil } | |||
| 315 | if resizeErr := resizer.Resize(cols, rows); resizeErr != nil { | 336 | if resizeErr := resizer.Resize(cols, rows); resizeErr != nil { | ||
| 316 | - | return res, fmt.Errorf("resize client PTY: %w", resizeErr) | 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) | |||
| 317 | } | 339 | } | ||
| 318 | } | 340 | } | ||
| 319 | - | if measureErr := p.process.Measure(workloadInput(s, run, fmt.Sprintf("measured-%d", sequence)), injected, flushed); measureErr != nil { | 341 | + | if measureErr := p.process.Measure(workloadInput(s, run, fmt.Sprintf("measured-%d", sequence)), measureInjected, flushed); measureErr != nil { |
| 320 | if injectedRecorded && !flushedRecorded { | 342 | if injectedRecorded && !flushedRecorded { | ||
| 321 | 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}) | ||
| 322 | return res, errors.Join(measureErr, diagnosticErr) | 344 | return res, errors.Join(measureErr, diagnosticErr) | ||
| @@ -448,9 +470,14 @@ func workloadResize(s scenario, sequence uint64) (uint16, uint16, bool) { | |||||
| 448 | func workloadInput(s scenario, run int, phase string) []byte { | 470 | func workloadInput(s scenario, run int, phase string) []byte { | ||
| 449 | 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)) | ||
| 450 | body := "printf 'vev perf %s\\n'" | 472 | body := "printf 'vev perf %s\\n'" | ||
| 473 | + | markerSeparator := "; " | |||
| 451 | switch s.Workload { | 474 | switch s.Workload { | ||
| 452 | case "active_output": | 475 | case "active_output": | ||
| 453 | - | body = `printf '\033[10;20HX'; : '%s'` | 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) | |||
| 454 | case "all_output": | 481 | case "all_output": | ||
| 455 | body = `printf '%%-120s' 'vev perf line %s'` | 482 | body = `printf '%%-120s' 'vev perf line %s'` | ||
| 456 | case "inactive_output": | 483 | case "inactive_output": | ||
| @@ -458,16 +485,29 @@ func workloadInput(s scenario, run int, phase string) []byte { | |||||
| 458 | case "interactive_flood": | 485 | case "interactive_flood": | ||
| 459 | 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" | ||
| 460 | case "resize_sweep": | 487 | case "resize_sweep": | ||
| 461 | - | 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 = " && " | |||
| 462 | case "copy_search": | 491 | case "copy_search": | ||
| 463 | body = "printf 'vev perf copy-search %s\\n'" | 492 | body = "printf 'vev perf copy-search %s\\n'" | ||
| 464 | case "snapshot_output_resize": | 493 | case "snapshot_output_resize": | ||
| 465 | - | body = `printf '\033[2J\033[Hvev 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 = " && " | |||
| 466 | case "attach_restore_tab_switch": | 497 | case "attach_restore_tab_switch": | ||
| 467 | - | body = `"$VEV_PERF_BIN" cmd next-tab; "$VEV_PERF_BIN" cmd previous-tab; 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'` |
| 468 | if phase == "warmup" { | 499 | 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'` | 500 | + | body = `"$VEV_PERF_BIN" cmd new-tab && "$VEV_PERF_BIN" cmd previous-tab && printf 'vev perf attach-restore-tab-switch %s\n'` |
| 470 | } | 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 | |||
| 471 | } | 511 | } | ||
| 472 | - | return []byte(fmt.Sprintf(body+"; printf '%s\\n'\n", s.ID, marker)) | 512 | + | return 120, 40 |
| 473 | } | 513 | } | ||
M cmd/vev-perf-harness/runner_test.go +109 -8
| @@ -314,7 +314,7 @@ func TestWorkloadInputShapes(t *testing.T) { | |||
| 314 | 314 | workload string | |
| 315 | 315 | contains []string | |
| 316 | 316 | }{ | |
| 317 | - | {"active_output", []string{`printf '\033[10;20HX'`}}, | |
| 317 | + | {"active_output", []string{`printf '\033[10;20H1'`}}, | |
| 318 | 318 | {"all_output", []string{"120", "vev perf line"}}, | |
| 319 | 319 | {"inactive_output", []string{ | |
| 320 | 320 | `printf '\033[38;2;20;120;220mred'`, | |
| @@ -339,9 +339,19 @@ func TestWorkloadInputShapes(t *testing.T) { | |||
| 339 | 339 | }) | |
| 340 | 340 | } | |
| 341 | 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) | |
| 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) | |
| 345 | 355 | } | |
| 346 | 356 | inactive := string(workloadInput(scenario{ID: "s", Workload: "inactive_output"}, 1, "measured-1")) | |
| 347 | 357 | if count := strings.Count(inactive, `\033[38;2;`); count != 3 { | |
| @@ -352,12 +362,14 @@ func TestWorkloadInputShapes(t *testing.T) { | |||
| 352 | 362 | if count := strings.Count(warmup, `"$VEV_PERF_BIN" cmd new-tab`); count != 1 { | |
| 353 | 363 | t.Fatalf("attach warmup new-tab commands = %d, want 1: %q", count, warmup) | |
| 354 | 364 | } | |
| 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) | |
| 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) | |
| 357 | 368 | } | |
| 358 | 369 | 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) | |
| 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) | |
| 361 | 373 | } | |
| 362 | 374 | } | |
| 363 | 375 | ||
| @@ -385,6 +397,95 @@ func TestWorkloadResize(t *testing.T) { | |||
| 385 | 397 | } | |
| 386 | 398 | } | |
| 387 | 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 | + | ||
| 388 | 489 | func TestHarnessAggregatesRawEventsSeparatelyFromRunDispersion(t *testing.T) { | |
| 389 | 490 | first := append([]int64(nil), make([]int64, minimumInIntervalEventSamples+5)...) | |
| 390 | 491 | for i := range first { | |
| @@ -314,7 +314,7 @@ func TestWorkloadInputShapes(t *testing.T) { | |||||
| 314 | workload string | 314 | workload string | ||
| 315 | contains []string | 315 | contains []string | ||
| 316 | }{ | 316 | }{ | ||
| 317 | - | {"active_output", []string{`printf '\033[10;20HX'`}}, | 317 | + | {"active_output", []string{`printf '\033[10;20H1'`}}, |
| 318 | {"all_output", []string{"120", "vev perf line"}}, | 318 | {"all_output", []string{"120", "vev perf line"}}, | ||
| 319 | {"inactive_output", []string{ | 319 | {"inactive_output", []string{ | ||
| 320 | `printf '\033[38;2;20;120;220mred'`, | 320 | `printf '\033[38;2;20;120;220mred'`, | ||
| @@ -339,9 +339,19 @@ func TestWorkloadInputShapes(t *testing.T) { | |||||
| 339 | }) | 339 | }) | ||
| 340 | } | 340 | } | ||
| 341 | 341 | ||||
| 342 | - | active := string(workloadInput(scenario{ID: "s", Workload: "active_output"}, 1, "measured-1")) | 342 | + | activeWarmup := string(workloadInput(scenario{ID: "s", Workload: "active_output"}, 1, "warmup")) |
| 343 | - | if strings.Contains(active, `\r`) || strings.Contains(active, "vev perf") { | 343 | + | activeOne := string(workloadInput(scenario{ID: "s", Workload: "active_output"}, 1, "measured-1")) |
| 344 | - | t.Fatalf("active output is not an absolute-cursor one-cell write: %q", active) | 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) | |||
| 345 | } | 355 | } | ||
| 346 | inactive := string(workloadInput(scenario{ID: "s", Workload: "inactive_output"}, 1, "measured-1")) | 356 | inactive := string(workloadInput(scenario{ID: "s", Workload: "inactive_output"}, 1, "measured-1")) | ||
| 347 | if count := strings.Count(inactive, `\033[38;2;`); count != 3 { | 357 | if count := strings.Count(inactive, `\033[38;2;`); count != 3 { | ||
| @@ -352,12 +362,14 @@ func TestWorkloadInputShapes(t *testing.T) { | |||||
| 352 | if count := strings.Count(warmup, `"$VEV_PERF_BIN" cmd new-tab`); count != 1 { | 362 | 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) | 363 | t.Fatalf("attach warmup new-tab commands = %d, want 1: %q", count, warmup) | ||
| 354 | } | 364 | } | ||
| 355 | - | if strings.Contains(warmup, "cmd next-tab") || !strings.Contains(warmup, `cmd previous-tab; printf 'vev perf attach`) { | 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" |
| 356 | - | t.Fatalf("attach warmup does not return to the marker-producing tab: %q", warmup) | 366 | + | if warmup != wantWarmup { |
| 367 | + | t.Fatalf("attach warmup success marker is not gated by every command:\n got %q\nwant %q", warmup, wantWarmup) | |||
| 357 | } | 368 | } | ||
| 358 | measured := string(workloadInput(scenario{ID: "s", Workload: "attach_restore_tab_switch"}, 1, "measured-1")) | 369 | measured := string(workloadInput(scenario{ID: "s", Workload: "attach_restore_tab_switch"}, 1, "measured-1")) | ||
| 359 | - | if strings.Contains(measured, "cmd new-tab") { | 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" |
| 360 | - | t.Fatalf("measured attach workload creates another tab: %q", measured) | 371 | + | if measured != wantMeasured { |
| 372 | + | t.Fatalf("attach measured success marker is not gated by every command:\n got %q\nwant %q", measured, wantMeasured) | |||
| 361 | } | 373 | } | ||
| 362 | } | 374 | } | ||
| 363 | 375 | ||||
| @@ -385,6 +397,95 @@ func TestWorkloadResize(t *testing.T) { | |||||
| 385 | } | 397 | } | ||
| 386 | } | 398 | } | ||
| 387 | 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 | + | ||||
| 388 | func TestHarnessAggregatesRawEventsSeparatelyFromRunDispersion(t *testing.T) { | 489 | func TestHarnessAggregatesRawEventsSeparatelyFromRunDispersion(t *testing.T) { | ||
| 389 | first := append([]int64(nil), make([]int64, minimumInIntervalEventSamples+5)...) | 490 | first := append([]int64(nil), make([]int64, minimumInIntervalEventSamples+5)...) | ||
| 390 | for i := range first { | 491 | for i := range first { | ||
M docs/performance.md +2 -1
| @@ -123,7 +123,8 @@ Straddling or failed events remain raw diagnostics, not samples. Each run must | |||
| 123 | 123 | contain `manifest.json`, `raw-harness.jsonl`, per-process JSONL, `runs.json`, and | |
| 124 | 124 | `summary.json`. | |
| 125 | 125 | ||
| 126 | - | `summary.json` records the source `git_sha` and measurement parameters; | |
| 126 | + | `summary.json` records the harness source as `git_sha`, the measured vev binary | |
| 127 | + | source as `vev_git_sha`, and the measurement parameters; | |
| 127 | 128 | `runs.json`, run manifests, and JSONL identify scenario, run, process, component, | |
| 128 | 129 | and correlation IDs. Preserve those files with the benchmark output and record | |
| 129 | 130 | `go version -m /tmp/vev-proxy-baseline`; do not commit result directories. CPU | |
| @@ -123,7 +123,8 @@ 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 | 123 | contain `manifest.json`, `raw-harness.jsonl`, per-process JSONL, `runs.json`, and | ||
| 124 | `summary.json`. | 124 | `summary.json`. | ||
| 125 | 125 | ||||
| 126 | - | `summary.json` records the source `git_sha` and measurement parameters; | 126 | + | `summary.json` records the harness source as `git_sha`, the measured vev binary |
| 127 | + | source as `vev_git_sha`, and the measurement parameters; | |||
| 127 | `runs.json`, run manifests, and JSONL identify scenario, run, process, component, | 128 | `runs.json`, run manifests, and JSONL identify scenario, run, process, component, | ||
| 128 | and correlation IDs. Preserve those files with the benchmark output and record | 129 | and correlation IDs. Preserve those files with the benchmark output and record | ||
| 129 | `go version -m /tmp/vev-proxy-baseline`; do not commit result directories. CPU | 130 | `go version -m /tmp/vev-proxy-baseline`; do not commit result directories. CPU | ||
M internal/usecase/daemon/performance_benchmark_test.go +1 -6
| @@ -214,8 +214,7 @@ func TestProxyANSIBenchmarkFixtures(t *testing.T) { | |||
| 214 | 214 | ||
| 215 | 215 | for _, fixture := range fixtures { | |
| 216 | 216 | t.Run(fixture.name, func(t *testing.T) { | |
| 217 | - | proxy, err := newProxySession(domain.RemoteSessionKey{Host: "benchmark", Name: "ansi"}, domain.Size{Cols: 120, Rows: 40 + tabChromeRows}) | |
| 218 | - | require.NoError(t, err) | |
| 217 | + | proxy := newProxyANSIBenchmarkSession(t) | |
| 219 | 218 | ||
| 220 | 219 | ack, reset, changed := proxy.applyOutputForGeneration(proxy.linkGeneration, ports.Output{ | |
| 221 | 220 | BaseStateNum: 0, | |
| @@ -233,10 +232,6 @@ func TestProxyANSIBenchmarkFixtures(t *testing.T) { | |||
| 233 | 232 | } | |
| 234 | 233 | } | |
| 235 | 234 | ||
| 236 | - | func TestProxyANSIBenchmarkGeometry(t *testing.T) { | |
| 237 | - | newProxyANSIBenchmarkSession(t) | |
| 238 | - | } | |
| 239 | - | ||
| 240 | 235 | func newProxyANSIBenchmarkSession(t testing.TB) *proxySession { | |
| 241 | 236 | t.Helper() | |
| 242 | 237 | proxy, err := newProxySession(domain.RemoteSessionKey{Host: "benchmark", Name: "ansi"}, domain.Size{Cols: 120, Rows: 40 + tabChromeRows}) | |
| @@ -214,8 +214,7 @@ func TestProxyANSIBenchmarkFixtures(t *testing.T) { | |||||
| 214 | 214 | ||||
| 215 | for _, fixture := range fixtures { | 215 | for _, fixture := range fixtures { | ||
| 216 | t.Run(fixture.name, func(t *testing.T) { | 216 | t.Run(fixture.name, func(t *testing.T) { | ||
| 217 | - | proxy, err := newProxySession(domain.RemoteSessionKey{Host: "benchmark", Name: "ansi"}, domain.Size{Cols: 120, Rows: 40 + tabChromeRows}) | 217 | + | proxy := newProxyANSIBenchmarkSession(t) |
| 218 | - | require.NoError(t, err) | |||
| 219 | 218 | ||||
| 220 | ack, reset, changed := proxy.applyOutputForGeneration(proxy.linkGeneration, ports.Output{ | 219 | ack, reset, changed := proxy.applyOutputForGeneration(proxy.linkGeneration, ports.Output{ | ||
| 221 | BaseStateNum: 0, | 220 | BaseStateNum: 0, | ||
| @@ -233,10 +232,6 @@ func TestProxyANSIBenchmarkFixtures(t *testing.T) { | |||||
| 233 | } | 232 | } | ||
| 234 | } | 233 | } | ||
| 235 | 234 | ||||
| 236 | - | func TestProxyANSIBenchmarkGeometry(t *testing.T) { | |||
| 237 | - | newProxyANSIBenchmarkSession(t) | |||
| 238 | - | } | |||
| 239 | - | ||||
| 240 | func newProxyANSIBenchmarkSession(t testing.TB) *proxySession { | 235 | func newProxyANSIBenchmarkSession(t testing.TB) *proxySession { | ||
| 241 | t.Helper() | 236 | t.Helper() | ||
| 242 | proxy, err := newProxySession(domain.RemoteSessionKey{Host: "benchmark", Name: "ansi"}, domain.Size{Cols: 120, Rows: 40 + tabChromeRows}) | 237 | proxy, err := newProxySession(domain.RemoteSessionKey{Host: "benchmark", Name: "ansi"}, domain.Size{Cols: 120, Rows: 40 + tabChromeRows}) | ||