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:
Ting-Yu Wang
2020-09-22 17:56:40 -07:00
committed by gVisor bot
parent cf3cef1171
commit c0f21bb19a
8 changed files with 237 additions and 35 deletions
+6 -1
View File
@@ -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
View File
@@ -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)
},
}
+83
View File
@@ -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
}
}
}
+51
View File
@@ -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)
}
}
+2 -2
View File
@@ -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())
+2
View File
@@ -23,6 +23,8 @@ import (
)
func TestSafemem(t *testing.T) {
const bufferSize = defaultBufferSize
testCases := []struct {
name string
input string
+7 -6
View File
@@ -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)
}
+77
View File
@@ -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)
}
}