105 lines
3.2 KiB
Go
105 lines
3.2 KiB
Go
package client
|
|
|
|
import (
|
|
"testing"
|
|
)
|
|
|
|
// The retained region is the one real cost stream resumption adds to the send
|
|
// path: a chunk has to survive past the frame write, so it is copied. These
|
|
// pin both halves of that claim — that the copy is the only cost, and that
|
|
// disabling the feature removes it entirely rather than merely shrinking it.
|
|
|
|
func benchStream(resumable bool) *Stream {
|
|
s := &Stream{resumable: resumable}
|
|
return s
|
|
}
|
|
|
|
// BenchmarkRetainChunk measures what emit adds over a bare frame write: the
|
|
// trim-and-append into the retained region. Compare the two variants; the delta
|
|
// is the per-byte copy the feature costs.
|
|
func BenchmarkRetainChunk(b *testing.B) {
|
|
chunk := make([]byte, DataChunkSize)
|
|
window := int64(DefaultStreamWindow)
|
|
|
|
b.Run("resume-on", func(b *testing.B) {
|
|
s := benchStream(true)
|
|
b.SetBytes(int64(len(chunk)))
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
// Model the steady state: credit trails one window behind, so the
|
|
// buffer trims about as fast as it grows and stays bounded.
|
|
acked := s.un.end() - window
|
|
if acked < 0 {
|
|
acked = 0
|
|
}
|
|
s.un.advance(acked)
|
|
s.un.append(chunk)
|
|
}
|
|
if got := int64(s.un.length()); got > window+int64(len(chunk)) {
|
|
b.Fatalf("retained region grew past one window: %d", got)
|
|
}
|
|
})
|
|
|
|
b.Run("resume-off", func(b *testing.B) {
|
|
s := benchStream(false)
|
|
b.SetBytes(int64(len(chunk)))
|
|
b.ReportAllocs()
|
|
b.ResetTimer()
|
|
for i := 0; i < b.N; i++ {
|
|
if s.resumable {
|
|
s.un.advance(0)
|
|
s.un.append(chunk)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
// TestResumeDisabledAllocatesNothing pins the off switch at the level that
|
|
// matters. It is easy for a feature flag to stop the wire behaviour while
|
|
// leaving the bookkeeping running, which would keep the memory cost and the
|
|
// per-byte copy for a user who explicitly turned it off — a partial revert that
|
|
// nobody would notice.
|
|
func TestResumeDisabledAllocatesNothing(t *testing.T) {
|
|
chunk := make([]byte, DataChunkSize)
|
|
s := benchStream(false)
|
|
|
|
allocs := testing.AllocsPerRun(1000, func() {
|
|
if s.resumable {
|
|
s.un.advance(0)
|
|
s.un.append(chunk)
|
|
}
|
|
})
|
|
if allocs != 0 {
|
|
t.Fatalf("resume disabled still allocated %.1f times per send", allocs)
|
|
}
|
|
if s.un.buf != nil {
|
|
t.Fatalf("resume disabled still allocated a retained region of %d bytes", cap(s.un.buf))
|
|
}
|
|
}
|
|
|
|
// TestRetainedRegionStaysWithinWindow is the memory bound the design rests on:
|
|
// flow control already caps outstanding bytes at one window, so the retained
|
|
// region needs no cap of its own. If that ever stopped holding, a busy stream
|
|
// would grow without limit and the hub would be the first to notice.
|
|
func TestRetainedRegionStaysWithinWindow(t *testing.T) {
|
|
const window = DefaultStreamWindow
|
|
chunk := make([]byte, DataChunkSize)
|
|
var u unackedBuf
|
|
|
|
for i := 0; i < 5000; i++ {
|
|
// A sender may never have more than one window outstanding, which is
|
|
// exactly what acquireSendWnd enforces before emit is ever reached.
|
|
if u.length()+len(chunk) > window {
|
|
u.advance(u.base() + int64(len(chunk)))
|
|
}
|
|
u.append(chunk)
|
|
if u.length() > window {
|
|
t.Fatalf("round %d: retained %d bytes for a %d-byte window", i, u.length(), window)
|
|
}
|
|
}
|
|
if cap(u.buf) > 4*window {
|
|
t.Fatalf("backing array grew to %d for a %d-byte window", cap(u.buf), window)
|
|
}
|
|
}
|