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 Verified
CD88F9E564E0102646FB6077A1A07C7C7C3E349D· 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. | |||
| 9 | 9 | ||
| 10 | 10 | ## 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 | 15 | ```sh | |
| 15 | 16 | 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-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`: | |
| 20 | 22 | ||
| 21 | 23 | ```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 | |
| 23 | 26 | ``` | |
| 24 | 27 | ||
| 25 | 28 | The matrix exercises live paint, snapshot capture, copy-mode entry, copy search, | |
| @@ -49,8 +52,10 @@ context: | |||
| 49 | 52 | go version | |
| 50 | 53 | uname -a | |
| 51 | 54 | git rev-parse HEAD | |
| 55 | + | # Smoke only; do not use this result for comparisons. | |
| 52 | 56 | 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 | |
| 54 | 59 | } > /tmp/vev-history-before.txt 2>&1 | |
| 55 | 60 | ``` | |
| 56 | 61 | ||
| @@ -9,17 +9,20 @@ Real Unix-socket, SSH, and UDP impairment/latency benchmarks are deferred. | |||||
| 9 | 9 | ||||
| 10 | ## Large-history daemon baseline | 10 | ## 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 | ```sh | 15 | ```sh | ||
| 15 | go test ./internal/usecase/daemon -run '^$' -bench '^BenchmarkDaemonHistory' -benchtime=1x -benchmem | 16 | 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-second | 19 | + | 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 | ```sh | 23 | ```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 | |||
| 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 version | 52 | go version | ||
| 50 | uname -a | 53 | uname -a | ||
| 51 | git rev-parse HEAD | 54 | 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 -benchmem | 56 | 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 | |||
| 54 | } > /tmp/vev-history-before.txt 2>&1 | 59 | } > /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 ( | |||
| 14 | 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 | 45 | func TestPerformanceFixtureCounters(t *testing.T) { | |
| 18 | 46 | fixture := newPerformanceFixture(t, performanceConfig{}) | |
| 19 | 47 | ||
| @@ -102,6 +130,20 @@ func benchmarkDaemonLargeHistory(b *testing.B, workload string, run func(*perfor | |||
| 102 | 130 | } | |
| 103 | 131 | b.StopTimer() | |
| 104 | 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 | 147 | if workload == "live-paint" && metrics.outputFrames != uint64(b.N) { | |
| 106 | 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 | 377 | type countingOutputMetrics struct{ frames, bytes, payloadBytes uint64 } | |
| 334 | 378 | ||
| 335 | 379 | type countingOutputTransport struct { | |
| 336 | 380 | mu sync.Mutex | |
| 337 | 381 | countingOutputMetrics | |
| 382 | + | last []byte | |
| 338 | 383 | } | |
| 339 | 384 | ||
| 340 | 385 | func (t *countingOutputTransport) Send(frame ports.Frame) error { | |
| 341 | 386 | if frame.Type != ports.MsgOutput { | |
| 342 | 387 | return nil | |
| 343 | 388 | } | |
| 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) | |
| 347 | 391 | } | |
| 348 | 392 | t.mu.Lock() | |
| 349 | 393 | t.frames++ | |
| 350 | - | t.bytes += uint64(len(output.Data)) | |
| 394 | + | t.bytes += uint64(len(frame.Payload) - outputPayloadHeaderBytes) | |
| 351 | 395 | t.payloadBytes += uint64(len(frame.Payload)) | |
| 396 | + | t.last = frame.Payload | |
| 352 | 397 | t.mu.Unlock() | |
| 353 | 398 | return nil | |
| 354 | 399 | } | |
| @@ -357,6 +402,7 @@ func (*countingOutputTransport) Close() error { return nil } | |||
| 357 | 402 | func (t *countingOutputTransport) reset() { | |
| 358 | 403 | t.mu.Lock() | |
| 359 | 404 | t.countingOutputMetrics = countingOutputMetrics{} | |
| 405 | + | t.last = nil | |
| 360 | 406 | t.mu.Unlock() | |
| 361 | 407 | } | |
| 362 | 408 | func (t *countingOutputTransport) metrics() countingOutputMetrics { | |
| @@ -364,20 +410,24 @@ func (t *countingOutputTransport) metrics() countingOutputMetrics { | |||
| 364 | 410 | defer t.mu.Unlock() | |
| 365 | 411 | 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 | 419 | type countingSnapshotMetrics struct{ writes, bytes uint64 } | |
| 369 | 420 | type countingSnapshotStore struct { | |
| 370 | 421 | mu sync.Mutex | |
| 371 | 422 | countingSnapshotMetrics | |
| 423 | + | last []byte | |
| 372 | 424 | } | |
| 373 | 425 | ||
| 374 | 426 | func (s *countingSnapshotStore) Write(_ string, data []byte) error { | |
| 375 | - | if _, err := snapcodec.Unmarshal(data); err != nil { | |
| 376 | - | return err | |
| 377 | - | } | |
| 378 | 427 | s.mu.Lock() | |
| 379 | 428 | s.writes++ | |
| 380 | 429 | s.bytes += uint64(len(data)) | |
| 430 | + | s.last = data | |
| 381 | 431 | s.mu.Unlock() | |
| 382 | 432 | return nil | |
| 383 | 433 | } | |
| @@ -386,6 +436,7 @@ func (*countingSnapshotStore) Delete(string) error { return nil | |||
| 386 | 436 | func (s *countingSnapshotStore) reset() { | |
| 387 | 437 | s.mu.Lock() | |
| 388 | 438 | s.countingSnapshotMetrics = countingSnapshotMetrics{} | |
| 439 | + | s.last = nil | |
| 389 | 440 | s.mu.Unlock() | |
| 390 | 441 | } | |
| 391 | 442 | func (s *countingSnapshotStore) metrics() countingSnapshotMetrics { | |
| @@ -393,3 +444,8 @@ func (s *countingSnapshotStore) metrics() countingSnapshotMetrics { | |||
| 393 | 444 | defer s.mu.Unlock() | |
| 394 | 445 | 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 | + | } | |
| @@ -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.Mutex | 380 | mu sync.Mutex | ||
| 337 | countingOutputMetrics | 381 | 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 nil | 387 | 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 nil | 398 | 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.countingOutputMetrics | 411 | 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.Mutex | 421 | mu sync.Mutex | ||
| 371 | countingSnapshotMetrics | 422 | 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 nil | 432 | 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.countingSnapshotMetrics | 445 | 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 | + | } | |||