mirror of
https://github.com/tailscale/tailscale.git
synced 2026-10-29 06:28:57 -04:00
A naive sync.Pool that stores dynamically sized []byte is incorrect since the Pool assumes that each stored item carries approximately the same memory cost. Different cost items should be stored in different pools. The bufpool tracks buffer pools by powers of two and provides a Buffer type that mimics the API of bytes.Buffer. This is a performance optimization. Avoid using this unless there is a clear performance gain for doing so. Incorrect use can lead to widespread data corruption (as is already the case with any incorrect sync.Pool use). Updates tailscale/corp#21363 Signed-off-by: Joe Tsai <joetsai@digital-static.net>
168 lines
4.8 KiB
Go
168 lines
4.8 KiB
Go
// Copyright (c) Tailscale Inc & contributors
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package bufpool
|
|
|
|
import (
|
|
"bytes"
|
|
"crypto/rand"
|
|
"io"
|
|
"slices"
|
|
"testing"
|
|
"testing/iotest"
|
|
|
|
qt "github.com/frankban/quicktest"
|
|
"github.com/google/go-cmp/cmp/cmpopts"
|
|
"golang.org/x/sync/errgroup"
|
|
"tailscale.com/util/must"
|
|
)
|
|
|
|
// Reduce the range of pool levels for testing purposes.
|
|
func init() {
|
|
minBits = 8
|
|
maxBits = 16
|
|
}
|
|
|
|
func Test(t *testing.T) {
|
|
c := qt.New(t)
|
|
|
|
const maxSize = 1 << 20
|
|
p := NewPool()
|
|
p.SetMaxSize(maxSize)
|
|
|
|
c.Run("NoCapacity", func(c *qt.C) {
|
|
_, err := p.Get(1 + maxSize)
|
|
c.Assert(err, qt.CmpEquals(cmpopts.EquateErrors()), cmpopts.AnyError)
|
|
c.Assert(p.counterUsedBytes.Value(), qt.Equals, int64(0))
|
|
c.Assert(p.counterUsedBuffers.Value(), qt.Equals, int64(0))
|
|
})
|
|
|
|
c.Run("BackToBack", func(c *qt.C) {
|
|
var usedBytes int64
|
|
sizes := []int{0, 3, 184, 254, 255, 256, 257, 1381, 1023, 1024, 1025, 30105, 30105, 65534, 65535, 65536, 65537, 184823, 184823}
|
|
func() {
|
|
for _, n := range sizes {
|
|
b := must.Get(p.Get(n))
|
|
defer p.Put(b)
|
|
|
|
if n < 1<<minBits {
|
|
c.Assert(b.Cap(), qt.Equals, 1<<minBits)
|
|
} else {
|
|
c.Assert(n <= b.Cap() && b.Cap() <= 2*n, qt.IsTrue)
|
|
}
|
|
usedBytes += int64(b.Cap())
|
|
}
|
|
_, err := p.Get(int(maxSize - usedBytes + 1))
|
|
c.Assert(err, qt.CmpEquals(cmpopts.EquateErrors()), cmpopts.AnyError)
|
|
c.Assert(p.gaugeUsedBytes.Value(), qt.Equals, usedBytes)
|
|
c.Assert(p.gaugeUsedBuffers.Value(), qt.Equals, int64(len(sizes)))
|
|
c.Assert(p.counterUsedBytes.Value(), qt.Equals, usedBytes)
|
|
c.Assert(p.counterUsedBuffers.Value(), qt.Equals, int64(len(sizes)))
|
|
}()
|
|
c.Assert(p.gaugeUsedBytes.Value(), qt.Equals, int64(0))
|
|
c.Assert(p.gaugeUsedBuffers.Value(), qt.Equals, int64(0))
|
|
})
|
|
|
|
c.Run("Buffers", func(c *qt.C) {
|
|
var group errgroup.Group
|
|
for range 10 {
|
|
group.Go(func() error {
|
|
b := must.Get(p.Get(0))
|
|
defer p.Put(b)
|
|
|
|
c.Assert(b.Len(), qt.Equals, 0)
|
|
c.Assert(b.Cap(), qt.Equals, 1<<minBits)
|
|
c.Assert(b.Bytes(), qt.DeepEquals, []byte{})
|
|
|
|
n, err := b.Read([]byte{0})
|
|
c.Assert(n, qt.Equals, 0)
|
|
c.Assert(err, qt.Equals, io.EOF)
|
|
|
|
want := []byte("hello, world!")
|
|
n = must.Get(b.Write(want))
|
|
c.Assert(n, qt.Equals, len(want))
|
|
c.Assert(b.Len(), qt.Equals, len(want))
|
|
c.Assert(b.Cap(), qt.Equals, 1<<minBits)
|
|
c.Assert(b.Bytes(), qt.DeepEquals, want)
|
|
|
|
got := make([]byte, len("hello"))
|
|
n = must.Get(b.Read(got))
|
|
c.Assert(n, qt.Equals, len(got))
|
|
c.Assert(got, qt.DeepEquals, want[:len(got)])
|
|
c.Assert(b.Len(), qt.Equals, len(want)-len(got))
|
|
c.Assert(b.Cap(), qt.Equals, 1<<minBits)
|
|
c.Assert(b.Bytes(), qt.DeepEquals, want[len(got):])
|
|
|
|
must.Do(b.Grow(1<<minBits - len(want)))
|
|
c.Assert(b.Cap(), qt.Equals, 1<<minBits)
|
|
must.Do(b.Grow(789))
|
|
c.Assert(b.Cap(), qt.Equals, 1<<10)
|
|
|
|
want2 := make([]byte, cap(b.Bytes())-len(b.Bytes()))
|
|
must.Get(rand.Read(want2))
|
|
must.Get(b.Write(want2))
|
|
c.Assert(b.Len(), qt.Equals, len(want)-len(got)+len(want2))
|
|
c.Assert(b.Cap(), qt.Equals, 1<<10)
|
|
c.Assert(b.Bytes(), qt.DeepEquals, append(slices.Clone(want[len(got):]), want2...))
|
|
|
|
want3 := make([]byte, 4000)
|
|
must.Get(rand.Read(want3))
|
|
must.Get(b.Write(want3))
|
|
c.Assert(b.Len(), qt.Equals, len(want)-len(got)+len(want2)+len(want3))
|
|
c.Assert(b.Cap(), qt.Equals, 1<<13)
|
|
c.Assert(b.Bytes(), qt.DeepEquals, append(append(slices.Clone(want[len(got):]), want2...), want3...))
|
|
b.Reset()
|
|
|
|
must.Get(b.Write(append(b.AvailableBuffer(), "hello"...)))
|
|
want4 := must.Get(io.ReadAll(io.LimitReader(rand.Reader, 1<<12)))
|
|
must.Get(b.ReadFrom(iotest.HalfReader(bytes.NewReader(want4))))
|
|
must.Get(io.ReadFull(b, got))
|
|
c.Assert(string(got), qt.Equals, "hello")
|
|
got2 := new(bytes.Buffer)
|
|
must.Get(b.WriteTo(got2))
|
|
c.Assert(got2.Bytes(), qt.DeepEquals, want4)
|
|
|
|
b.Reset()
|
|
|
|
b2 := b.AvailableBuffer()
|
|
b2 = append(b2, "fizzbuzz"...)
|
|
n = must.Get(b.Write(b2))
|
|
c.Assert(n, qt.Equals, len(b2))
|
|
b2 = b.Next(1000)
|
|
c.Assert(b2, qt.DeepEquals, []byte("fizzbuzz"))
|
|
b2 = b.Next(1000)
|
|
c.Assert(b2, qt.DeepEquals, []byte(""))
|
|
return nil
|
|
})
|
|
}
|
|
group.Wait()
|
|
c.Assert(p.gaugeUsedBytes.Value(), qt.Equals, int64(0))
|
|
c.Assert(p.gaugeUsedBuffers.Value(), qt.Equals, int64(0))
|
|
})
|
|
}
|
|
|
|
func BenchmarkAppend(b *testing.B) {
|
|
b.ReportAllocs()
|
|
pool := NewPool()
|
|
pool.SetMaxSize(1 << maxBits)
|
|
for range b.N {
|
|
buf := must.Get(pool.Get(1 << maxBits))
|
|
b := buf.AvailableBuffer() // should have zero length, but large capacity
|
|
must.Get(buf.Write(b[:cap(b)])) // write entire capacity of same buffer is noop
|
|
pool.Put(buf)
|
|
}
|
|
}
|
|
|
|
func BenchmarkGrow(b *testing.B) {
|
|
b.ReportAllocs()
|
|
pool := NewPool()
|
|
pool.SetMaxSize(1 << maxBits)
|
|
for range b.N {
|
|
buf := must.Get(pool.Get(0))
|
|
for range 6 {
|
|
buf.Grow(buf.Cap() + 1)
|
|
}
|
|
pool.Put(buf)
|
|
}
|
|
}
|