detached bf7a30f

fix(daemon): isolate benchmark instrumentation

commit
bf7a30f6e81bd6c7e1ce3d0b7e841b0b89d7a6aa
author
brice · · 2026-07-11 05:46 UTC
committer
brice · · 2026-07-11 06:15 UTC
parent
8aba936
signature
GPG VerifiedCD88F9E564E0102646FB6077A1A07C7C7C3E349D · arch-gpg Verified for the committer email
2 files · +73 -12
M docs/performance.md +10 -5
@@ -9,17 +9,20 @@ Real Unix-socket, SSH, and UDP impairment/latency benchmarks are deferred.
99
1010 ## Large-history daemon baseline
1111
12-Run the full daemon matrix once when establishing a baseline:
12+Use a single iteration only as a smoke check; it is not suitable for drawing
13+performance conclusions:
1314
1415 ```sh
1516 go test ./internal/usecase/daemon -run '^$' -bench '^BenchmarkDaemonHistory' -benchtime=1x -benchmem
1617 ```
1718
18-For repeatable comparison of the focused control workload, run three one-second
19-samples (pin CPU/governor and record the host when comparing runs):
19+For before/after comparison, collect repeated samples for the **full matrix**
20+(all workloads and topologies), pinning CPU/governor and recording the host.
21+The fixed duration and count make the output suitable for `benchstat`:
2022
2123 ```sh
22-go test ./internal/usecase/daemon -run '^$' -bench '^BenchmarkDaemonHistoryLivePaint/1tab-1pane-control$' -benchtime=1s -count=3 -benchmem
24+go test ./internal/usecase/daemon -run '^$' -bench '^BenchmarkDaemonHistory' -benchtime=1s -count=5 -benchmem > /tmp/vev-history-after.txt
25+benchstat /tmp/vev-history-before.txt /tmp/vev-history-after.txt
2326 ```
2427
2528 The matrix exercises live paint, snapshot capture, copy-mode entry, copy search,
@@ -49,8 +52,10 @@ context:
4952 go version
5053 uname -a
5154 git rev-parse HEAD
55+ # Smoke only; do not use this result for comparisons.
5256 go test ./internal/usecase/daemon -run '^$' -bench '^BenchmarkDaemonHistory' -benchtime=1x -benchmem
53- go test ./internal/usecase/daemon -run '^$' -bench '^BenchmarkDaemonHistoryLivePaint/1tab-1pane-control$' -benchtime=1s -count=3 -benchmem
57+ # Repeated full matrix for every workload/topology used in conclusions.
58+ go test ./internal/usecase/daemon -run '^$' -bench '^BenchmarkDaemonHistory' -benchtime=1s -count=5 -benchmem
5459 } > /tmp/vev-history-before.txt 2>&1
5560 ```
5661
@@ -9,17 +9,20 @@ Real Unix-socket, SSH, and UDP impairment/latency benchmarks are deferred.
9 9
10 ## Large-history daemon baseline10 ## Large-history daemon baseline
11 11
12-Run the full daemon matrix once when establishing a baseline:12+Use a single iteration only as a smoke check; it is not suitable for drawing
13+performance conclusions:
13 14
14 ```sh15 ```sh
15 go test ./internal/usecase/daemon -run '^$' -bench '^BenchmarkDaemonHistory' -benchtime=1x -benchmem16 go test ./internal/usecase/daemon -run '^$' -bench '^BenchmarkDaemonHistory' -benchtime=1x -benchmem
16 ```17 ```
17 18
18-For repeatable comparison of the focused control workload, run three one-second19+For before/after comparison, collect repeated samples for the **full matrix**
19-samples (pin CPU/governor and record the host when comparing runs):20+(all workloads and topologies), pinning CPU/governor and recording the host.
21+The fixed duration and count make the output suitable for `benchstat`:
20 22
21 ```sh23 ```sh
22-go test ./internal/usecase/daemon -run '^$' -bench '^BenchmarkDaemonHistoryLivePaint/1tab-1pane-control$' -benchtime=1s -count=3 -benchmem24+go test ./internal/usecase/daemon -run '^$' -bench '^BenchmarkDaemonHistory' -benchtime=1s -count=5 -benchmem > /tmp/vev-history-after.txt
25+benchstat /tmp/vev-history-before.txt /tmp/vev-history-after.txt
23 ```26 ```
24 27
25 The matrix exercises live paint, snapshot capture, copy-mode entry, copy search,28 The matrix exercises live paint, snapshot capture, copy-mode entry, copy search,
@@ -49,8 +52,10 @@ context:
49 go version52 go version
50 uname -a53 uname -a
51 git rev-parse HEAD54 git rev-parse HEAD
55+ # Smoke only; do not use this result for comparisons.
52 go test ./internal/usecase/daemon -run '^$' -bench '^BenchmarkDaemonHistory' -benchtime=1x -benchmem56 go test ./internal/usecase/daemon -run '^$' -bench '^BenchmarkDaemonHistory' -benchtime=1x -benchmem
53- go test ./internal/usecase/daemon -run '^$' -bench '^BenchmarkDaemonHistoryLivePaint/1tab-1pane-control$' -benchtime=1s -count=3 -benchmem57+ # Repeated full matrix for every workload/topology used in conclusions.
58+ go test ./internal/usecase/daemon -run '^$' -bench '^BenchmarkDaemonHistory' -benchtime=1s -count=5 -benchmem
54 } > /tmp/vev-history-before.txt 2>&159 } > /tmp/vev-history-before.txt 2>&1
55 ```60 ```
56 61
M internal/usecase/daemon/performance_benchmark_test.go +63 -7
@@ -14,6 +14,34 @@ import (
1414 snapcodec "github.com/bnema/vev/internal/usecase/snapshot"
1515 )
1616
17+func TestCountingSnapshotStoreWriteCountsAndRetainsOpaquePayload(t *testing.T) {
18+ store := &countingSnapshotStore{}
19+ payload := []byte{0xff, 0x00, 0xfe}
20+
21+ require.NoError(t, store.Write("session", payload))
22+ require.Equal(t, countingSnapshotMetrics{writes: 1, bytes: uint64(len(payload))}, store.metrics())
23+ require.Equal(t, payload, store.lastPayload())
24+
25+ payload[0] = 0x01
26+ require.Equal(t, payload, store.lastPayload(), "the counting sink must retain the payload slice header, not copy it")
27+}
28+
29+func TestCountingOutputTransportCountsOpaquePayloadAndRejectsShortPayload(t *testing.T) {
30+ transport := &countingOutputTransport{}
31+
32+ err := transport.Send(ports.Frame{Type: ports.MsgOutput, Payload: make([]byte, 23)})
33+ require.Error(t, err)
34+ require.Equal(t, countingOutputMetrics{}, transport.metrics())
35+
36+ payload := append(make([]byte, 24), 0xff, 0x00, 0xfe)
37+ require.NoError(t, transport.Send(ports.Frame{Type: ports.MsgOutput, Payload: payload}))
38+ require.Equal(t, countingOutputMetrics{frames: 1, bytes: 3, payloadBytes: uint64(len(payload))}, transport.metrics())
39+ require.Equal(t, payload, transport.lastPayload())
40+
41+ payload[24] = 0x01
42+ require.Equal(t, payload, transport.lastPayload(), "the counting transport must retain the payload slice header, not copy it")
43+}
44+
1745 func TestPerformanceFixtureCounters(t *testing.T) {
1846 fixture := newPerformanceFixture(t, performanceConfig{})
1947
@@ -102,6 +130,20 @@ func benchmarkDaemonLargeHistory(b *testing.B, workload string, run func(*perfor
102130 }
103131 b.StopTimer()
104132 metrics := fixture.metrics()
133+ if payload := fixture.output.lastPayload(); payload != nil {
134+ if _, err := ports.UnmarshalOutput(payload); err != nil {
135+ b.Fatalf("decode last output: %v", err)
136+ }
137+ }
138+ if workload == "snapshot-capture" {
139+ payload := fixture.snaps.lastPayload()
140+ if payload == nil {
141+ b.Fatal("snapshot capture retained no snapshot payload")
142+ }
143+ if _, err := snapcodec.Unmarshal(payload); err != nil {
144+ b.Fatalf("decode last snapshot: %v", err)
145+ }
146+ }
105147 if workload == "live-paint" && metrics.outputFrames != uint64(b.N) {
106148 b.Fatalf("live paint emitted %d frames for %d operations", metrics.outputFrames, b.N)
107149 }
@@ -330,25 +372,28 @@ func (f *performanceFixture) metrics() performanceMetrics {
330372 }
331373 }
332374
375+const outputPayloadHeaderBytes = 24
376+
333377 type countingOutputMetrics struct{ frames, bytes, payloadBytes uint64 }
334378
335379 type countingOutputTransport struct {
336380 mu sync.Mutex
337381 countingOutputMetrics
382+ last []byte
338383 }
339384
340385 func (t *countingOutputTransport) Send(frame ports.Frame) error {
341386 if frame.Type != ports.MsgOutput {
342387 return nil
343388 }
344- output, err := ports.UnmarshalOutput(frame.Payload)
345- if err != nil {
346- return err
389+ if len(frame.Payload) < outputPayloadHeaderBytes {
390+ return fmt.Errorf("output payload is %d bytes, want at least %d", len(frame.Payload), outputPayloadHeaderBytes)
347391 }
348392 t.mu.Lock()
349393 t.frames++
350- t.bytes += uint64(len(output.Data))
394+ t.bytes += uint64(len(frame.Payload) - outputPayloadHeaderBytes)
351395 t.payloadBytes += uint64(len(frame.Payload))
396+ t.last = frame.Payload
352397 t.mu.Unlock()
353398 return nil
354399 }
@@ -357,6 +402,7 @@ func (*countingOutputTransport) Close() error { return nil }
357402 func (t *countingOutputTransport) reset() {
358403 t.mu.Lock()
359404 t.countingOutputMetrics = countingOutputMetrics{}
405+ t.last = nil
360406 t.mu.Unlock()
361407 }
362408 func (t *countingOutputTransport) metrics() countingOutputMetrics {
@@ -364,20 +410,24 @@ func (t *countingOutputTransport) metrics() countingOutputMetrics {
364410 defer t.mu.Unlock()
365411 return t.countingOutputMetrics
366412 }
413+func (t *countingOutputTransport) lastPayload() []byte {
414+ t.mu.Lock()
415+ defer t.mu.Unlock()
416+ return t.last
417+}
367418
368419 type countingSnapshotMetrics struct{ writes, bytes uint64 }
369420 type countingSnapshotStore struct {
370421 mu sync.Mutex
371422 countingSnapshotMetrics
423+ last []byte
372424 }
373425
374426 func (s *countingSnapshotStore) Write(_ string, data []byte) error {
375- if _, err := snapcodec.Unmarshal(data); err != nil {
376- return err
377- }
378427 s.mu.Lock()
379428 s.writes++
380429 s.bytes += uint64(len(data))
430+ s.last = data
381431 s.mu.Unlock()
382432 return nil
383433 }
@@ -386,6 +436,7 @@ func (*countingSnapshotStore) Delete(string) error { return nil
386436 func (s *countingSnapshotStore) reset() {
387437 s.mu.Lock()
388438 s.countingSnapshotMetrics = countingSnapshotMetrics{}
439+ s.last = nil
389440 s.mu.Unlock()
390441 }
391442 func (s *countingSnapshotStore) metrics() countingSnapshotMetrics {
@@ -393,3 +444,8 @@ func (s *countingSnapshotStore) metrics() countingSnapshotMetrics {
393444 defer s.mu.Unlock()
394445 return s.countingSnapshotMetrics
395446 }
447+func (s *countingSnapshotStore) lastPayload() []byte {
448+ s.mu.Lock()
449+ defer s.mu.Unlock()
450+ return s.last
451+}
@@ -14,6 +14,34 @@ import (
14 snapcodec "github.com/bnema/vev/internal/usecase/snapshot"14 snapcodec "github.com/bnema/vev/internal/usecase/snapshot"
15 )15 )
16 16
17+func TestCountingSnapshotStoreWriteCountsAndRetainsOpaquePayload(t *testing.T) {
18+ store := &countingSnapshotStore{}
19+ payload := []byte{0xff, 0x00, 0xfe}
20+
21+ require.NoError(t, store.Write("session", payload))
22+ require.Equal(t, countingSnapshotMetrics{writes: 1, bytes: uint64(len(payload))}, store.metrics())
23+ require.Equal(t, payload, store.lastPayload())
24+
25+ payload[0] = 0x01
26+ require.Equal(t, payload, store.lastPayload(), "the counting sink must retain the payload slice header, not copy it")
27+}
28+
29+func TestCountingOutputTransportCountsOpaquePayloadAndRejectsShortPayload(t *testing.T) {
30+ transport := &countingOutputTransport{}
31+
32+ err := transport.Send(ports.Frame{Type: ports.MsgOutput, Payload: make([]byte, 23)})
33+ require.Error(t, err)
34+ require.Equal(t, countingOutputMetrics{}, transport.metrics())
35+
36+ payload := append(make([]byte, 24), 0xff, 0x00, 0xfe)
37+ require.NoError(t, transport.Send(ports.Frame{Type: ports.MsgOutput, Payload: payload}))
38+ require.Equal(t, countingOutputMetrics{frames: 1, bytes: 3, payloadBytes: uint64(len(payload))}, transport.metrics())
39+ require.Equal(t, payload, transport.lastPayload())
40+
41+ payload[24] = 0x01
42+ require.Equal(t, payload, transport.lastPayload(), "the counting transport must retain the payload slice header, not copy it")
43+}
44+
17 func TestPerformanceFixtureCounters(t *testing.T) {45 func TestPerformanceFixtureCounters(t *testing.T) {
18 fixture := newPerformanceFixture(t, performanceConfig{})46 fixture := newPerformanceFixture(t, performanceConfig{})
19 47
@@ -102,6 +130,20 @@ func benchmarkDaemonLargeHistory(b *testing.B, workload string, run func(*perfor
102 }130 }
103 b.StopTimer()131 b.StopTimer()
104 metrics := fixture.metrics()132 metrics := fixture.metrics()
133+ if payload := fixture.output.lastPayload(); payload != nil {
134+ if _, err := ports.UnmarshalOutput(payload); err != nil {
135+ b.Fatalf("decode last output: %v", err)
136+ }
137+ }
138+ if workload == "snapshot-capture" {
139+ payload := fixture.snaps.lastPayload()
140+ if payload == nil {
141+ b.Fatal("snapshot capture retained no snapshot payload")
142+ }
143+ if _, err := snapcodec.Unmarshal(payload); err != nil {
144+ b.Fatalf("decode last snapshot: %v", err)
145+ }
146+ }
105 if workload == "live-paint" && metrics.outputFrames != uint64(b.N) {147 if workload == "live-paint" && metrics.outputFrames != uint64(b.N) {
106 b.Fatalf("live paint emitted %d frames for %d operations", metrics.outputFrames, b.N)148 b.Fatalf("live paint emitted %d frames for %d operations", metrics.outputFrames, b.N)
107 }149 }
@@ -330,25 +372,28 @@ func (f *performanceFixture) metrics() performanceMetrics {
330 }372 }
331 }373 }
332 374
375+const outputPayloadHeaderBytes = 24
376+
333 type countingOutputMetrics struct{ frames, bytes, payloadBytes uint64 }377 type countingOutputMetrics struct{ frames, bytes, payloadBytes uint64 }
334 378
335 type countingOutputTransport struct {379 type countingOutputTransport struct {
336 mu sync.Mutex380 mu sync.Mutex
337 countingOutputMetrics381 countingOutputMetrics
382+ last []byte
338 }383 }
339 384
340 func (t *countingOutputTransport) Send(frame ports.Frame) error {385 func (t *countingOutputTransport) Send(frame ports.Frame) error {
341 if frame.Type != ports.MsgOutput {386 if frame.Type != ports.MsgOutput {
342 return nil387 return nil
343 }388 }
344- output, err := ports.UnmarshalOutput(frame.Payload)389+ if len(frame.Payload) < outputPayloadHeaderBytes {
345- if err != nil {390+ return fmt.Errorf("output payload is %d bytes, want at least %d", len(frame.Payload), outputPayloadHeaderBytes)
346- return err
347 }391 }
348 t.mu.Lock()392 t.mu.Lock()
349 t.frames++393 t.frames++
350- t.bytes += uint64(len(output.Data))394+ t.bytes += uint64(len(frame.Payload) - outputPayloadHeaderBytes)
351 t.payloadBytes += uint64(len(frame.Payload))395 t.payloadBytes += uint64(len(frame.Payload))
396+ t.last = frame.Payload
352 t.mu.Unlock()397 t.mu.Unlock()
353 return nil398 return nil
354 }399 }
@@ -357,6 +402,7 @@ func (*countingOutputTransport) Close() error { return nil }
357 func (t *countingOutputTransport) reset() {402 func (t *countingOutputTransport) reset() {
358 t.mu.Lock()403 t.mu.Lock()
359 t.countingOutputMetrics = countingOutputMetrics{}404 t.countingOutputMetrics = countingOutputMetrics{}
405+ t.last = nil
360 t.mu.Unlock()406 t.mu.Unlock()
361 }407 }
362 func (t *countingOutputTransport) metrics() countingOutputMetrics {408 func (t *countingOutputTransport) metrics() countingOutputMetrics {
@@ -364,20 +410,24 @@ func (t *countingOutputTransport) metrics() countingOutputMetrics {
364 defer t.mu.Unlock()410 defer t.mu.Unlock()
365 return t.countingOutputMetrics411 return t.countingOutputMetrics
366 }412 }
413+func (t *countingOutputTransport) lastPayload() []byte {
414+ t.mu.Lock()
415+ defer t.mu.Unlock()
416+ return t.last
417+}
367 418
368 type countingSnapshotMetrics struct{ writes, bytes uint64 }419 type countingSnapshotMetrics struct{ writes, bytes uint64 }
369 type countingSnapshotStore struct {420 type countingSnapshotStore struct {
370 mu sync.Mutex421 mu sync.Mutex
371 countingSnapshotMetrics422 countingSnapshotMetrics
423+ last []byte
372 }424 }
373 425
374 func (s *countingSnapshotStore) Write(_ string, data []byte) error {426 func (s *countingSnapshotStore) Write(_ string, data []byte) error {
375- if _, err := snapcodec.Unmarshal(data); err != nil {
376- return err
377- }
378 s.mu.Lock()427 s.mu.Lock()
379 s.writes++428 s.writes++
380 s.bytes += uint64(len(data))429 s.bytes += uint64(len(data))
430+ s.last = data
381 s.mu.Unlock()431 s.mu.Unlock()
382 return nil432 return nil
383 }433 }
@@ -386,6 +436,7 @@ func (*countingSnapshotStore) Delete(string) error { return nil
386 func (s *countingSnapshotStore) reset() {436 func (s *countingSnapshotStore) reset() {
387 s.mu.Lock()437 s.mu.Lock()
388 s.countingSnapshotMetrics = countingSnapshotMetrics{}438 s.countingSnapshotMetrics = countingSnapshotMetrics{}
439+ s.last = nil
389 s.mu.Unlock()440 s.mu.Unlock()
390 }441 }
391 func (s *countingSnapshotStore) metrics() countingSnapshotMetrics {442 func (s *countingSnapshotStore) metrics() countingSnapshotMetrics {
@@ -393,3 +444,8 @@ func (s *countingSnapshotStore) metrics() countingSnapshotMetrics {
393 defer s.mu.Unlock()444 defer s.mu.Unlock()
394 return s.countingSnapshotMetrics445 return s.countingSnapshotMetrics
395 }446 }
447+func (s *countingSnapshotStore) lastPayload() []byte {
448+ s.mu.Lock()
449+ defer s.mu.Unlock()
450+ return s.last
451+}

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