summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorTing-Yu Wang <anivia@google.com>2020-09-22 17:54:37 -0700
committergVisor bot <gvisor-bot@google.com>2020-09-22 17:56:40 -0700
commitc0f21bb19a0ff0fd4bc3bd1f0bed9171e43faf68 (patch)
treea0c75508fce62d8b2b52eca971d523f77acd10af
parentcf3cef1171bdfb41a27d563eb368d4488e0b99f1 (diff)
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
-rw-r--r--pkg/buffer/BUILD7
-rw-r--r--pkg/buffer/buffer.go35
-rw-r--r--pkg/buffer/pool.go83
-rw-r--r--pkg/buffer/pool_test.go51
-rw-r--r--pkg/buffer/safemem.go4
-rw-r--r--pkg/buffer/safemem_test.go2
-rw-r--r--pkg/buffer/view.go13
-rw-r--r--pkg/buffer/view_test.go77
8 files changed, 237 insertions, 35 deletions
diff --git a/pkg/buffer/BUILD b/pkg/buffer/BUILD
index b03d46d18..1186f788e 100644
--- a/pkg/buffer/BUILD
+++ b/pkg/buffer/BUILD
@@ -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",
+ ],
)
diff --git a/pkg/buffer/buffer.go b/pkg/buffer/buffer.go
index c6d089fd9..311808ae9 100644
--- a/pkg/buffer/buffer.go
+++ b/pkg/buffer/buffer.go
@@ -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)
- },
-}
diff --git a/pkg/buffer/pool.go b/pkg/buffer/pool.go
new file mode 100644
index 000000000..7ad6132ab
--- /dev/null
+++ b/pkg/buffer/pool.go
@@ -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
+ }
+ }
+}
diff --git a/pkg/buffer/pool_test.go b/pkg/buffer/pool_test.go
new file mode 100644
index 000000000..8584bac89
--- /dev/null
+++ b/pkg/buffer/pool_test.go
@@ -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)
+ }
+}
diff --git a/pkg/buffer/safemem.go b/pkg/buffer/safemem.go
index b789e56e9..8b42575b4 100644
--- a/pkg/buffer/safemem.go
+++ b/pkg/buffer/safemem.go
@@ -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())
diff --git a/pkg/buffer/safemem_test.go b/pkg/buffer/safemem_test.go
index 47f357e0c..721cc5934 100644
--- a/pkg/buffer/safemem_test.go
+++ b/pkg/buffer/safemem_test.go
@@ -23,6 +23,8 @@ import (
)
func TestSafemem(t *testing.T) {
+ const bufferSize = defaultBufferSize
+
testCases := []struct {
name string
input string
diff --git a/pkg/buffer/view.go b/pkg/buffer/view.go
index e6901eadb..00652d675 100644
--- a/pkg/buffer/view.go
+++ b/pkg/buffer/view.go
@@ -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)
}
diff --git a/pkg/buffer/view_test.go b/pkg/buffer/view_test.go
index 3db1bc6ee..839af0223 100644
--- a/pkg/buffer/view_test.go
+++ b/pkg/buffer/view_test.go
@@ -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)
+ }
+}