mirror of
https://github.com/netbirdio/gvisor.git
synced 2026-05-22 17:12:49 -07:00
pkg/buffer: Reorganize internal structure to allow dynamic sizes.
This change changes `buffer.data` into a `[]byte`, from `[bufferSize]byte`. In exchange, each `buffer` is now grouped together to reduce the number of allocation. Plus, `View` now holds an embeded list of `buffer` (via `pool`) to support the happy path which the number of buffer is small. Expect no extra allocation for the happy path. It is to enable the use case for PacketBuffer, which * each `View` is small (way less than `defaultBufferSize`), and * needs to dynamically transfer ownership of `[]byte` to `View`. (to allow gradual migration) PiperOrigin-RevId: 333197252
This commit is contained in:
+6
-1
@@ -20,6 +20,7 @@ go_library(
|
||||
srcs = [
|
||||
"buffer.go",
|
||||
"buffer_list.go",
|
||||
"pool.go",
|
||||
"safemem.go",
|
||||
"view.go",
|
||||
"view_unsafe.go",
|
||||
@@ -37,9 +38,13 @@ go_test(
|
||||
name = "buffer_test",
|
||||
size = "small",
|
||||
srcs = [
|
||||
"pool_test.go",
|
||||
"safemem_test.go",
|
||||
"view_test.go",
|
||||
],
|
||||
library = ":buffer",
|
||||
deps = ["//pkg/safemem"],
|
||||
deps = [
|
||||
"//pkg/safemem",
|
||||
"//pkg/state",
|
||||
],
|
||||
)
|
||||
|
||||
+9
-26
@@ -14,36 +14,26 @@
|
||||
|
||||
// Package buffer provides the implementation of a buffer view.
|
||||
//
|
||||
// A view is an flexible buffer, backed by a pool, supporting the safecopy
|
||||
// operations natively as well as the ability to grow via either prepend or
|
||||
// append, as well as shrink.
|
||||
// A view is an flexible buffer, supporting the safecopy operations natively as
|
||||
// well as the ability to grow via either prepend or append, as well as shrink.
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"sync"
|
||||
)
|
||||
|
||||
const bufferSize = 8144 // See below.
|
||||
|
||||
// buffer encapsulates a queueable byte buffer.
|
||||
//
|
||||
// Note that the total size is slightly less than two pages. This is done
|
||||
// intentionally to ensure that the buffer object aligns with runtime
|
||||
// internals. We have no hard size or alignment requirements. This two page
|
||||
// size will effectively minimize internal fragmentation, but still have a
|
||||
// large enough chunk to limit excessive segmentation.
|
||||
//
|
||||
// +stateify savable
|
||||
type buffer struct {
|
||||
data [bufferSize]byte
|
||||
data []byte
|
||||
read int
|
||||
write int
|
||||
bufferEntry
|
||||
}
|
||||
|
||||
// reset resets internal data.
|
||||
//
|
||||
// This must be called before returning the buffer to the pool.
|
||||
// init performs in-place initialization for zero value.
|
||||
func (b *buffer) init(size int) {
|
||||
b.data = make([]byte, size)
|
||||
}
|
||||
|
||||
// Reset resets read and write locations, effectively emptying the buffer.
|
||||
func (b *buffer) Reset() {
|
||||
b.read = 0
|
||||
b.write = 0
|
||||
@@ -85,10 +75,3 @@ func (b *buffer) WriteMove(n int) {
|
||||
func (b *buffer) WriteSlice() []byte {
|
||||
return b.data[b.write:]
|
||||
}
|
||||
|
||||
// bufferPool is a pool for buffers.
|
||||
var bufferPool = sync.Pool{
|
||||
New: func() interface{} {
|
||||
return new(buffer)
|
||||
},
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
// Copyright 2020 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package buffer
|
||||
|
||||
const (
|
||||
// embeddedCount is the number of buffer structures embedded in the pool. It
|
||||
// is also the number for overflow allocations.
|
||||
embeddedCount = 8
|
||||
|
||||
// defaultBufferSize is the default size for each underlying storage buffer.
|
||||
//
|
||||
// It is slightly less than two pages. This is done intentionally to ensure
|
||||
// that the buffer object aligns with runtime internals. This two page size
|
||||
// will effectively minimize internal fragmentation, but still have a large
|
||||
// enough chunk to limit excessive segmentation.
|
||||
defaultBufferSize = 8144
|
||||
)
|
||||
|
||||
// pool allocates buffer.
|
||||
//
|
||||
// It contains an embedded buffer storage for fast path when the number of
|
||||
// buffers needed is small.
|
||||
//
|
||||
// +stateify savable
|
||||
type pool struct {
|
||||
bufferSize int
|
||||
avail []buffer `state:"nosave"`
|
||||
embeddedStorage [embeddedCount]buffer `state:"wait"`
|
||||
}
|
||||
|
||||
// get gets a new buffer from p.
|
||||
func (p *pool) get() *buffer {
|
||||
if p.avail == nil {
|
||||
p.avail = p.embeddedStorage[:]
|
||||
}
|
||||
if len(p.avail) == 0 {
|
||||
p.avail = make([]buffer, embeddedCount)
|
||||
}
|
||||
if p.bufferSize <= 0 {
|
||||
p.bufferSize = defaultBufferSize
|
||||
}
|
||||
buf := &p.avail[0]
|
||||
buf.init(p.bufferSize)
|
||||
p.avail = p.avail[1:]
|
||||
return buf
|
||||
}
|
||||
|
||||
// put releases buf.
|
||||
func (p *pool) put(buf *buffer) {
|
||||
// Remove reference to the underlying storage, allowing it to be garbage
|
||||
// collected.
|
||||
buf.data = nil
|
||||
}
|
||||
|
||||
// setBufferSize sets the size of underlying storage buffer for future
|
||||
// allocations. It can be called at any time.
|
||||
func (p *pool) setBufferSize(size int) {
|
||||
p.bufferSize = size
|
||||
}
|
||||
|
||||
// afterLoad is invoked by stateify.
|
||||
func (p *pool) afterLoad() {
|
||||
// S/R does not save subslice into embeddedStorage correctly. Restore
|
||||
// available portion of embeddedStorage manually. Restore as nil if none used.
|
||||
for i := len(p.embeddedStorage); i > 0; i-- {
|
||||
if p.embeddedStorage[i-1].data != nil {
|
||||
p.avail = p.embeddedStorage[i:]
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
// Copyright 2020 The gVisor Authors.
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package buffer
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestGetDefaultBufferSize(t *testing.T) {
|
||||
var p pool
|
||||
for i := 0; i < embeddedCount*2; i++ {
|
||||
buf := p.get()
|
||||
if got, want := len(buf.data), defaultBufferSize; got != want {
|
||||
t.Errorf("#%d len(buf.data) = %d, want %d", i, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetCustomBufferSize(t *testing.T) {
|
||||
const size = 100
|
||||
|
||||
var p pool
|
||||
p.setBufferSize(size)
|
||||
for i := 0; i < embeddedCount*2; i++ {
|
||||
buf := p.get()
|
||||
if got, want := len(buf.data), size; got != want {
|
||||
t.Errorf("#%d len(buf.data) = %d, want %d", i, got, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPut(t *testing.T) {
|
||||
var p pool
|
||||
buf := p.get()
|
||||
p.put(buf)
|
||||
if buf.data != nil {
|
||||
t.Errorf("buf.data = %x, want nil", buf.data)
|
||||
}
|
||||
}
|
||||
@@ -44,7 +44,7 @@ func (v *View) WriteFromSafememReader(r safemem.Reader, count uint64) (uint64, e
|
||||
// Need at least one buffer.
|
||||
firstBuf := v.data.Back()
|
||||
if firstBuf == nil {
|
||||
firstBuf = bufferPool.Get().(*buffer)
|
||||
firstBuf = v.pool.get()
|
||||
v.data.PushBack(firstBuf)
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ func (v *View) WriteFromSafememReader(r safemem.Reader, count uint64) (uint64, e
|
||||
count -= l
|
||||
blocks = append(blocks, firstBuf.WriteBlock())
|
||||
for count > 0 {
|
||||
emptyBuf := bufferPool.Get().(*buffer)
|
||||
emptyBuf := v.pool.get()
|
||||
v.data.PushBack(emptyBuf)
|
||||
block := emptyBuf.WriteBlock().TakeFirst64(count)
|
||||
count -= uint64(block.Len())
|
||||
|
||||
@@ -23,6 +23,8 @@ import (
|
||||
)
|
||||
|
||||
func TestSafemem(t *testing.T) {
|
||||
const bufferSize = defaultBufferSize
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
input string
|
||||
|
||||
+7
-6
@@ -27,6 +27,7 @@ import (
|
||||
type View struct {
|
||||
data bufferList
|
||||
size int64
|
||||
pool pool
|
||||
}
|
||||
|
||||
// TrimFront removes the first count bytes from the buffer.
|
||||
@@ -81,7 +82,7 @@ func (v *View) advanceRead(count int64) {
|
||||
buf = buf.Next() // Iterate.
|
||||
v.data.Remove(oldBuf)
|
||||
oldBuf.Reset()
|
||||
bufferPool.Put(oldBuf)
|
||||
v.pool.put(oldBuf)
|
||||
|
||||
// Update counts.
|
||||
count -= sz
|
||||
@@ -118,7 +119,7 @@ func (v *View) Truncate(length int64) {
|
||||
// Drop the buffer completely; see above.
|
||||
v.data.Remove(buf)
|
||||
buf.Reset()
|
||||
bufferPool.Put(buf)
|
||||
v.pool.put(buf)
|
||||
v.size -= sz
|
||||
}
|
||||
}
|
||||
@@ -137,7 +138,7 @@ func (v *View) Grow(length int64, zero bool) {
|
||||
|
||||
// Is there some space in the last buffer?
|
||||
if buf == nil || buf.Full() {
|
||||
buf = bufferPool.Get().(*buffer)
|
||||
buf = v.pool.get()
|
||||
v.data.PushBack(buf)
|
||||
}
|
||||
|
||||
@@ -181,7 +182,7 @@ func (v *View) Prepend(data []byte) {
|
||||
|
||||
for len(data) > 0 {
|
||||
// Do we need an empty buffer?
|
||||
buf := bufferPool.Get().(*buffer)
|
||||
buf := v.pool.get()
|
||||
v.data.PushFront(buf)
|
||||
|
||||
// The buffer is empty; copy last chunk.
|
||||
@@ -211,7 +212,7 @@ func (v *View) Append(data []byte) {
|
||||
|
||||
// Ensure there's a buffer with space.
|
||||
if buf == nil || buf.Full() {
|
||||
buf = bufferPool.Get().(*buffer)
|
||||
buf = v.pool.get()
|
||||
v.data.PushBack(buf)
|
||||
}
|
||||
|
||||
@@ -297,7 +298,7 @@ func (v *View) WriteFromReader(r io.Reader, count int64) (int64, error) {
|
||||
|
||||
// Ensure we have an empty buffer.
|
||||
if buf == nil || buf.Full() {
|
||||
buf = bufferPool.Get().(*buffer)
|
||||
buf = v.pool.get()
|
||||
v.data.PushBack(buf)
|
||||
}
|
||||
|
||||
|
||||
@@ -16,11 +16,16 @@ package buffer
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"io"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gvisor.dev/gvisor/pkg/state"
|
||||
)
|
||||
|
||||
const bufferSize = defaultBufferSize
|
||||
|
||||
func fillAppend(v *View, data []byte) {
|
||||
v.Append(data)
|
||||
}
|
||||
@@ -50,6 +55,30 @@ var fillFuncs = map[string]func(*View, []byte){
|
||||
"writeFromReaderEnd": fillWriteFromReaderEnd,
|
||||
}
|
||||
|
||||
func BenchmarkReadAt(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
var v View
|
||||
v.Append(make([]byte, 100))
|
||||
|
||||
buf := make([]byte, 10)
|
||||
for i := 0; i < b.N; i++ {
|
||||
v.ReadAt(buf, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkWriteRead(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
var v View
|
||||
sz := 1000
|
||||
wbuf := make([]byte, sz)
|
||||
rbuf := bytes.NewBuffer(make([]byte, sz))
|
||||
for i := 0; i < b.N; i++ {
|
||||
v.Append(wbuf)
|
||||
rbuf.Reset()
|
||||
v.ReadToWriter(rbuf, int64(sz))
|
||||
}
|
||||
}
|
||||
|
||||
func testReadAt(t *testing.T, v *View, offset int64, n int, wantStr string, wantErr error) {
|
||||
t.Helper()
|
||||
d := make([]byte, n)
|
||||
@@ -465,3 +494,51 @@ func TestView(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func doSaveAndLoad(t *testing.T, toSave, toLoad *View) {
|
||||
t.Helper()
|
||||
var buf bytes.Buffer
|
||||
ctx := context.Background()
|
||||
if _, err := state.Save(ctx, &buf, toSave); err != nil {
|
||||
t.Fatal("state.Save:", err)
|
||||
}
|
||||
if _, err := state.Load(ctx, bytes.NewReader(buf.Bytes()), toLoad); err != nil {
|
||||
t.Fatal("state.Load:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveRestoreViewEmpty(t *testing.T) {
|
||||
var toSave View
|
||||
var v View
|
||||
doSaveAndLoad(t, &toSave, &v)
|
||||
|
||||
if got := v.pool.avail; got != nil {
|
||||
t.Errorf("pool is not in zero state: v.pool.avail = %v, want nil", got)
|
||||
}
|
||||
if got := v.Flatten(); len(got) != 0 {
|
||||
t.Errorf("v.Flatten() = %x, want []", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSaveRestoreView(t *testing.T) {
|
||||
// Create data that fits 2.5 slots.
|
||||
data := bytes.Join([][]byte{
|
||||
bytes.Repeat([]byte{1, 2}, defaultBufferSize),
|
||||
bytes.Repeat([]byte{3}, defaultBufferSize/2),
|
||||
}, nil)
|
||||
|
||||
var toSave View
|
||||
toSave.Append(data)
|
||||
|
||||
var v View
|
||||
doSaveAndLoad(t, &toSave, &v)
|
||||
|
||||
// Next available slot at index 3; 0-2 slot are used.
|
||||
i := 3
|
||||
if got, want := &v.pool.avail[0], &v.pool.embeddedStorage[i]; got != want {
|
||||
t.Errorf("next available buffer points to %p, want %p (&v.pool.embeddedStorage[%d])", got, want, i)
|
||||
}
|
||||
if got := v.Flatten(); !bytes.Equal(got, data) {
|
||||
t.Errorf("v.Flatten() = %x, want %x", got, data)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user