From 9a78b8267c7a3d1544d36d16e8ca6cbfeeb607fd Mon Sep 17 00:00:00 2001 From: Lucas Manning Date: Tue, 14 Jun 2022 18:19:27 -0700 Subject: [PATCH] Add new buffer implementation. PiperOrigin-RevId: 455003647 --- pkg/bufferv2/BUILD | 65 +++ pkg/bufferv2/buffer.go | 608 +++++++++++++++++++++++++++ pkg/bufferv2/buffer_state.go | 26 ++ pkg/bufferv2/buffer_test.go | 792 +++++++++++++++++++++++++++++++++++ pkg/bufferv2/chunk.go | 113 +++++ pkg/bufferv2/view.go | 232 ++++++++++ pkg/bufferv2/view_test.go | 174 ++++++++ pkg/bufferv2/view_unsafe.go | 32 ++ 8 files changed, 2042 insertions(+) create mode 100644 pkg/bufferv2/BUILD create mode 100644 pkg/bufferv2/buffer.go create mode 100644 pkg/bufferv2/buffer_state.go create mode 100644 pkg/bufferv2/buffer_test.go create mode 100644 pkg/bufferv2/chunk.go create mode 100644 pkg/bufferv2/view.go create mode 100644 pkg/bufferv2/view_test.go create mode 100644 pkg/bufferv2/view_unsafe.go diff --git a/pkg/bufferv2/BUILD b/pkg/bufferv2/BUILD new file mode 100644 index 000000000..632e89584 --- /dev/null +++ b/pkg/bufferv2/BUILD @@ -0,0 +1,65 @@ +load("//tools:defs.bzl", "go_library", "go_test") +load("//tools/go_generics:defs.bzl", "go_template_instance") + +package(licenses = ["notice"]) + +go_template_instance( + name = "chunk_refs", + out = "chunk_refs.go", + package = "buffer", + prefix = "chunk", + template = "//pkg/refsvfs2:refs_template", + types = { + "T": "chunk", + }, +) + +go_template_instance( + name = "view_list", + out = "view_list.go", + package = "buffer", + prefix = "view", + template = "//pkg/ilist:generic_list", + types = { + "Element": "*View", + "Linker": "*View", + }, +) + +go_library( + name = "buffer", + srcs = [ + "buffer.go", + "buffer_state.go", + "chunk.go", + "chunk_refs.go", + "view.go", + "view_list.go", + "view_unsafe.go", + ], + visibility = ["//visibility:public"], + deps = [ + "//pkg/atomicbitops", + "//pkg/bits", + "//pkg/context", + "//pkg/ilist", + "//pkg/log", + "//pkg/pool", + "//pkg/refsvfs2", + "//pkg/sync", + ], +) + +go_test( + name = "buffer_test", + size = "small", + srcs = [ + "buffer_test.go", + "view_test.go", + ], + library = ":buffer", + deps = [ + "//pkg/state", + "@com_github_google_go_cmp//cmp:go_default_library", + ], +) diff --git a/pkg/bufferv2/buffer.go b/pkg/bufferv2/buffer.go new file mode 100644 index 000000000..ea961ab18 --- /dev/null +++ b/pkg/bufferv2/buffer.go @@ -0,0 +1,608 @@ +// Copyright 2022 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 provides the implementation of a non-contiguous buffer. +// +// A buffer 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 ( + "fmt" + "io" +) + +// Buffer is a non-linear buffer. +// +// +stateify savable +type Buffer struct { + data viewList `state:".([]byte)"` + size int64 +} + +func (b *Buffer) removeView(v *View) { + b.data.Remove(v) + v.Release() +} + +// MakeWithData creates a new Buffer initialized with given data. This function +// should be used with caution to avoid unnecessary []byte allocations. When in +// doubt use NewWithView to maximize chunk reuse. +func MakeWithData(b []byte) Buffer { + buf := Buffer{} + if len(b) == 0 { + return buf + } + v := NewViewWithData(b) + buf.Append(v) + return buf +} + +// MakeWithView creates a new Buffer initialized with given view. This function +// takes ownership of v. +func MakeWithView(v *View) Buffer { + if v == nil { + return Buffer{} + } + b := Buffer{ + size: int64(v.Size()), + } + if b.size == 0 { + v.Release() + return b + } + b.data.PushBack(v) + return b +} + +// Release frees all resources held by b. +func (b *Buffer) Release() { + for v := b.data.Front(); v != nil; v = b.data.Front() { + b.removeView(v) + } + b.size = 0 +} + +// TrimFront removes the first count bytes from the buffer. +func (b *Buffer) TrimFront(count int64) { + if count >= b.size { + b.advanceRead(b.size) + } else { + b.advanceRead(count) + } +} + +// ReadAt implements io.ReaderAt.ReadAt. +func (b *Buffer) ReadAt(p []byte, offset int64) (int, error) { + var ( + skipped int64 + done int64 + ) + for v := b.data.Front(); v != nil && done < int64(len(p)); v = v.Next() { + needToSkip := int(offset - skipped) + if sz := v.Size(); sz <= needToSkip { + skipped += int64(sz) + continue + } + + // Actually read data. + n := copy(p[done:], v.AsSlice()[needToSkip:]) + skipped += int64(needToSkip) + done += int64(n) + } + if int(done) < len(p) || offset+done == b.size { + return int(done), io.EOF + } + return int(done), nil +} + +// advanceRead advances the Buffer's read index. +// +// Precondition: there must be sufficient bytes in the buffer. +func (b *Buffer) advanceRead(count int64) { + for v := b.data.Front(); v != nil && count > 0; { + sz := int64(v.Size()) + if sz > count { + // There is still data for reading. + v.TrimFront(int(count)) + b.size -= count + count = 0 + return + } + + // Consume the whole view. + oldView := v + v = v.Next() // Iterate. + b.removeView(oldView) + + // Update counts. + count -= sz + b.size -= sz + } + if count > 0 { + panic(fmt.Sprintf("advanceRead still has %d bytes remaining", count)) + } +} + +// Truncate truncates the Buffer to the given length. +// +// This will not grow the Buffer, only shrink it. If a length is passed that is +// greater than the current size of the Buffer, then nothing will happen. +// +// Precondition: length must be >= 0. +func (b *Buffer) Truncate(length int64) { + if length < 0 { + panic("negative length provided") + } + if length >= b.size { + return // Nothing to do. + } + for v := b.data.Back(); v != nil && b.size > length; v = b.data.Back() { + sz := int64(v.Size()) + if after := b.size - sz; after < length { + // Truncate the buffer locally. + left := (length - after) + v.write = v.read + int(left) + b.size = length + break + } + + // Drop the buffer completely; see above. + b.removeView(v) + b.size -= sz + } +} + +// GrowTo grows the given Buffer to the number of bytes, which will be appended. +// If zero is true, all these bytes will be zero. If zero is false, then this is +// the caller's responsibility. +// +// Precondition: length must be >= 0. +func (b *Buffer) GrowTo(length int64, zero bool) { + if length < 0 { + panic("negative length provided") + } + for b.size < length { + v := b.data.Back() + + // Is there some space in the last buffer? + if v.Full() { + v = NewView(int(length - b.size)) + b.data.PushBack(v) + } + + // Write up to length bytes. + sz := v.AvailableSize() + if int64(sz) > length-b.size { + sz = int(length - b.size) + } + + // Zero the written section; note that this pattern is + // specifically recognized and optimized by the compiler. + if zero { + for i := v.write; i < v.write+sz; i++ { + v.chunk.data[i] = 0 + } + } + + // Advance the index. + v.Grow(sz) + b.size += int64(sz) + } +} + +// Prepend prepends the given data. Prepend takes ownership of src. +func (b *Buffer) Prepend(src *View) error { + if src == nil { + return nil + } + // If the first buffer does not have room just prepend the view. + v := b.data.Front() + if v == nil || v.read == 0 { + b.prependOwned(src) + return nil + } + + // If there's room at the front and we won't incur a copy by writing to this + // view, fill in the extra room first. + if !v.sharesChunk() { + avail := v.read + vStart := 0 + srcStart := src.Size() - avail + if avail > src.Size() { + vStart = avail - src.Size() + srcStart = 0 + } + // Save the write index and restore it after. + old := v.write + v.read = vStart + n, err := v.WriteAt(src.AsSlice()[srcStart:], 0) + if err != nil { + return fmt.Errorf("could not write to view during append: %w", err) + } + b.size += int64(n) + v.write = old + src.write = srcStart + + // If there's no more to be written, then we're done. + if src.Size() == 0 { + src.Release() + return nil + } + } + + // Otherwise, just prepend the view. + b.prependOwned(src) + return nil +} + +// Append appends the given data. Append takes ownership of src. +func (b *Buffer) Append(src *View) error { + if src == nil { + return nil + } + // If the last buffer is full, just append the view. + v := b.data.Back() + if v.Full() { + b.appendOwned(src) + return nil + } + + // If a write won't incur a copy, then fill the back of the existing last + // chunk. + if !v.sharesChunk() { + writeSz := src.Size() + if src.Size() > v.AvailableSize() { + writeSz = v.AvailableSize() + } + done, err := v.Write(src.AsSlice()[:writeSz]) + if err != nil { + return fmt.Errorf("could not write to view during append: %w", err) + } + src.TrimFront(done) + b.size += int64(done) + if src.Size() == 0 { + src.Release() + return nil + } + } + + // If there is still data left just append the src. + b.appendOwned(src) + return nil +} + +func (b *Buffer) appendOwned(v *View) { + b.data.PushBack(v) + b.size += int64(v.Size()) +} + +func (b *Buffer) prependOwned(v *View) { + b.data.PushFront(v) + b.size += int64(v.Size()) +} + +// PullUp makes the specified range contiguous and returns the backing memory. +func (b *Buffer) PullUp(offset, length int) (*View, bool) { + if length == 0 { + return nil, true + } + tgt := Range{begin: offset, end: offset + length} + if tgt.Intersect(Range{end: int(b.size)}).Len() != length { + return nil, false + } + + curr := Range{} + v := b.data.Front() + for ; v != nil; v = v.Next() { + origLen := v.Size() + curr.end = curr.begin + origLen + + if x := curr.Intersect(tgt); x.Len() == tgt.Len() { + // buf covers the whole requested target range. + sub := x.Offset(-curr.begin) + new := viewPool.Get().(*View) + new.read = sub.begin + new.write = sub.end + // Don't increment the reference count of the underlying chunk. Views + // returned by PullUp are explicitly unowned and read only + new.chunk = v.chunk + return new, true + } else if x.Len() > 0 { + // buf is pointing at the starting buffer we want to merge. + break + } + + curr.begin += origLen + } + + // Calculate the total merged length. + totLen := 0 + for n := v; n != nil; n = n.Next() { + totLen += n.Size() + if curr.begin+totLen >= tgt.end { + break + } + } + + // Merge the buffers. + merged := NewViewSize(totLen) + off := 0 + for n := v; n != nil && off < totLen; { + merged.WriteAt(n.AsSlice(), off) + off += n.Size() + + // Remove buffers except for the first one, which will be reused. + if n == v { + n = n.Next() + } else { + old := n + n = n.Next() + b.removeView(old) + } + } + // Make data the first buffer. + b.data.InsertBefore(v, merged) + b.removeView(v) + + r := tgt.Offset(-curr.begin) + pulled := viewPool.Get().(*View) + pulled.read = r.begin + pulled.write = r.end + pulled.chunk = merged.chunk + return pulled, true +} + +// Flatten returns a flattened copy of this data. +// +// This method should not be used in any performance-sensitive paths. It may +// allocate a fresh byte slice sufficiently large to contain all the data in +// the buffer. This is principally for debugging. +// +// N.B. Tee data still belongs to this Buffer, as if there is a single buffer +// present, then it will be returned directly. This should be used for +// temporary use only, and a reference to the given slice should not be held. +func (b *Buffer) Flatten() []byte { + if v := b.data.Front(); v == nil { + return nil // No data at all. + } else if v.Next() == nil { + return v.AsSlice() // Only one buffer. + } + data := make([]byte, 0, b.size) // Need to flatten. + for v := b.data.Front(); v != nil; v = v.Next() { + // Copy to the allocated slice. + data = append(data, v.AsSlice()...) + } + return data +} + +// Size indicates the total amount of data available in this Buffer. +func (b *Buffer) Size() int64 { + return b.size +} + +// Clone creates a copy-on-write clone of b. The underlying chunks are shared +// until they are written to. +func (b *Buffer) Clone() Buffer { + other := Buffer{ + size: b.size, + } + for v := b.data.Front(); v != nil; v = v.Next() { + newView := v.Clone() + other.data.PushBack(newView) + } + return other +} + +// Apply applies the given function across all valid data. +func (b *Buffer) Apply(fn func(*View)) { + for v := b.data.Front(); v != nil; v = v.Next() { + d := v.Clone() + fn(d) + d.Release() + } +} + +// SubApply applies fn to a given range of data in b. Any part of the range +// outside of b is ignored. +func (b *Buffer) SubApply(offset, length int, fn func(*View)) { + for v := b.data.Front(); length > 0 && v != nil; v = v.Next() { + d := v.Clone() + if offset >= d.Size() { + offset -= d.Size() + continue + } + if offset > 0 { + d.TrimFront(offset) + offset = 0 + } + if length < d.Size() { + d.write = d.read + length + } + fn(d) + length -= d.Size() + d.Release() + } +} + +// Merge merges the provided Buffer with this one. +// +// The other Buffer will be appended to v, and other will be empty after this +// operation completes. +func (b *Buffer) Merge(other *Buffer) { + // Copy over all buffers. + for v := other.data.Front(); v != nil; v = other.data.Front() { + b.Append(v.Clone()) + other.removeView(v) + } + + // Adjust sizes. + other.size = 0 +} + +// WriteFromReader writes to the buffer from an io.Reader. +// +// A minimum read size equal to unsafe.Sizeof(unintptr) is enforced, +// provided that count is greater than or equal to unsafe.Sizeof(uintptr). +func (b *Buffer) WriteFromReader(r io.Reader, count int64) (int64, error) { + var ( + done int64 + n int + err error + ) + for done < count { + view := b.data.Back() + + // Ensure we have an empty buffer. + if view.Full() { + view = NewView(int(count - done)) + b.data.PushBack(view) + } + + // Is this less than the minimum batch? + if view.AvailableSize() < minBatch && (count-done) >= int64(minBatch) { + tmp := NewView(minBatch) + n, err = r.Read(tmp.availableSlice()) + tmp.Grow(n) + b.Append(tmp) + done += int64(n) + if err != nil { + break + } + continue + } + + // Limit the read, if necessary. + sz := view.AvailableSize() + if left := count - done; int64(sz) > left { + sz = int(left) + } + + // Pass the relevant portion of the buffer. + n, err = r.Read(view.availableSlice()[:sz]) + view.Grow(n) + done += int64(n) + b.size += int64(n) + if err == io.EOF { + err = nil // Short write allowed. + break + } else if err != nil { + break + } + } + return done, err +} + +// ReadToWriter reads from the buffer into an io.Writer. +// +// N.B. This does not consume the bytes read. TrimFront should +// be called appropriately after this call in order to do so. +// +// A minimum write size equal to unsafe.Sizeof(unintptr) is enforced, +// provided that count is greater than or equal to unsafe.Sizeof(uintptr). +func (b *Buffer) ReadToWriter(w io.Writer, count int64) (int64, error) { + var ( + done int64 + n int + err error + ) + offset := 0 // Spill-over for batching. + for view := b.data.Front(); view != nil && done < count; view = view.Next() { + // Has this been consumed? Skip it. + sz := view.Size() + if sz <= offset { + offset -= sz + continue + } + sz -= offset + + // Is this less than the minimum batch? + left := count - done + if sz < minBatch && left >= int64(minBatch) && (b.size-done) >= int64(minBatch) { + tmp := NewView(minBatch) + n, err = b.ReadAt(tmp.availableSlice()[:minBatch], done) + tmp.Grow(n) + w.Write(tmp.AsSlice()) + tmp.Release() + done += int64(n) + offset = n - sz // Reset below. + if err != nil { + break + } + continue + } + + // Limit the write if necessary. + if int64(sz) >= left { + sz = int(left) + } + + // Perform the actual write. + n, err = w.Write(view.AsSlice()[offset : offset+sz]) + done += int64(n) + if err != nil { + break + } + + // Reset spill-over. + offset = 0 + } + return done, err +} + +// AsSlices returns a list of each of Buffer's underlying Views as Slices. +// The slices returned should not be modifed. +func (b *Buffer) AsSlices() [][]byte { + slices := make([][]byte, 0, b.data.Len()) + for v := b.data.Front(); v != nil; v = v.Next() { + slices = append(slices, v.AsSlice()) + } + return slices +} + +// Range specifies a range of buffer. +type Range struct { + begin int + end int +} + +// Intersect returns the intersection of x and y. +func (x Range) Intersect(y Range) Range { + if x.begin < y.begin { + x.begin = y.begin + } + if x.end > y.end { + x.end = y.end + } + if x.begin >= x.end { + return Range{} + } + return x +} + +// Offset returns x offset by off. +func (x Range) Offset(off int) Range { + x.begin += off + x.end += off + return x +} + +// Len returns the length of x. +func (x Range) Len() int { + l := x.end - x.begin + if l < 0 { + l = 0 + } + return l +} diff --git a/pkg/bufferv2/buffer_state.go b/pkg/bufferv2/buffer_state.go new file mode 100644 index 000000000..447dfe389 --- /dev/null +++ b/pkg/bufferv2/buffer_state.go @@ -0,0 +1,26 @@ +// Copyright 2022 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 + +// saveBuf is invoked by stateify. +func (b *Buffer) saveData() []byte { + return b.Flatten() +} + +// loadBuf is invoked by stateify. +func (b *Buffer) loadData(data []byte) { + v := NewViewWithData(data) + b.Append(v) +} diff --git a/pkg/bufferv2/buffer_test.go b/pkg/bufferv2/buffer_test.go new file mode 100644 index 000000000..a5a0a2ea1 --- /dev/null +++ b/pkg/bufferv2/buffer_test.go @@ -0,0 +1,792 @@ +// Copyright 2021 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 ( + "bytes" + "context" + "fmt" + "io" + "reflect" + "strings" + "testing" + + "gvisor.dev/gvisor/pkg/state" +) + +func BenchmarkReadAt(b *testing.B) { + b.ReportAllocs() + var buf Buffer + buf.Append(NewView(100)) + defer buf.Release() + + bytes := make([]byte, 10) + for i := 0; i < b.N; i++ { + buf.ReadAt(bytes, 0) + } +} + +func BenchmarkWriteRead(b *testing.B) { + b.ReportAllocs() + var buf Buffer + defer buf.Release() + sz := 1000 + rbuf := bytes.NewBuffer(make([]byte, sz)) + for i := 0; i < b.N; i++ { + buf.Append(NewView(sz)) + rbuf.Reset() + buf.ReadToWriter(rbuf, int64(sz)) + } +} + +func fillAppend(b *Buffer, data []byte) { + b.Append(NewViewWithData(data)) +} + +func fillAppendEnd(b *Buffer, data []byte) { + b.GrowTo(baseChunkSize-1, false) + b.Append(NewViewWithData(data)) + b.TrimFront(baseChunkSize - 1) +} + +func fillWriteFromReader(b *Buffer, data []byte) { + buf := bytes.NewBuffer(data) + b.WriteFromReader(buf, int64(len(data))) +} + +func fillWriteFromReaderEnd(b *Buffer, data []byte) { + b.GrowTo(baseChunkSize-1, false) + buf := bytes.NewBuffer(data) + b.WriteFromReader(buf, int64(len(data))) + b.TrimFront(baseChunkSize - 1) +} + +var fillFuncs = map[string]func(*Buffer, []byte){ + "append": fillAppend, + "appendEnd": fillAppendEnd, + "writeFromReader": fillWriteFromReader, + "writeFromReaderEnd": fillWriteFromReaderEnd, +} + +func testReadAt(t *testing.T, b *Buffer, offset int64, n int, wantStr string, wantErr error) { + t.Helper() + d := make([]byte, n) + n, err := b.ReadAt(d, offset) + if n != len(wantStr) { + t.Errorf("got %d, want %d", n, len(wantStr)) + } + if err != wantErr { + t.Errorf("got err %v, want %v", err, wantErr) + } + if !bytes.Equal(d[:n], []byte(wantStr)) { + t.Errorf("got %q, want %q", string(d[:n]), wantStr) + } +} + +func TestBuffer(t *testing.T) { + testCases := []struct { + name string + input string + output string + op func(*testing.T, *Buffer) + }{ + // Preconditions. + { + name: "truncate-check", + input: "hello", + output: "hello", // Not touched. + op: func(t *testing.T, b *Buffer) { + defer func() { + if r := recover(); r == nil { + t.Errorf("Truncate(-1) did not panic") + } + }() + b.Truncate(-1) + }, + }, + { + name: "growto-check", + input: "hello", + output: "hello", // Not touched. + op: func(t *testing.T, b *Buffer) { + defer func() { + if r := recover(); r == nil { + t.Errorf("GrowTo(-1) did not panic") + } + }() + b.GrowTo(-1, false) + }, + }, + { + name: "advance-check", + input: "hello", + output: "", // Consumed. + op: func(t *testing.T, b *Buffer) { + defer func() { + if r := recover(); r == nil { + t.Errorf("advanceRead(Size()+1) did not panic") + } + }() + b.advanceRead(b.Size() + 1) + }, + }, + + // Prepend. + { + name: "prepend", + input: "world", + output: "hello world", + op: func(t *testing.T, b *Buffer) { + b.Prepend(NewViewWithData([]byte("hello "))) + }, + }, + { + name: "prepend-backfill-full", + input: "hello world", + output: "jello world", + op: func(t *testing.T, b *Buffer) { + b.TrimFront(1) + b.Prepend(NewViewWithData([]byte("j"))) + }, + }, + { + name: "prepend-backfill-under", + input: "hello world", + output: "hola world", + op: func(t *testing.T, b *Buffer) { + b.TrimFront(5) + b.Prepend(NewViewWithData([]byte("hola"))) + }, + }, + { + name: "prepend-backfill-over", + input: "hello world", + output: "smello world", + op: func(t *testing.T, b *Buffer) { + b.TrimFront(1) + b.Prepend(NewViewWithData([]byte("sm"))) + }, + }, + { + name: "prepend-fill", + input: strings.Repeat("1", baseChunkSize-1), + output: "0" + strings.Repeat("1", baseChunkSize-1), + op: func(t *testing.T, b *Buffer) { + b.Prepend(NewViewWithData([]byte("0"))) + }, + }, + { + name: "prepend-overflow", + input: strings.Repeat("1", baseChunkSize), + output: "0" + strings.Repeat("1", baseChunkSize), + op: func(t *testing.T, b *Buffer) { + b.Prepend(NewViewWithData([]byte("0"))) + }, + }, + { + name: "prepend-multiple-buffers", + input: strings.Repeat("1", baseChunkSize-1), + output: strings.Repeat("0", baseChunkSize*3) + strings.Repeat("1", baseChunkSize-1), + op: func(t *testing.T, b *Buffer) { + b.Prepend(NewViewWithData([]byte(strings.Repeat("0", baseChunkSize)))) + b.Prepend(NewViewWithData([]byte(strings.Repeat("0", baseChunkSize)))) + b.Prepend(NewViewWithData([]byte(strings.Repeat("0", baseChunkSize)))) + }, + }, + + // Append and write. + { + name: "append", + input: "hello", + output: "hello world", + op: func(t *testing.T, b *Buffer) { + b.Append(NewViewWithData([]byte(" world"))) + }, + }, + { + name: "append-fill", + input: strings.Repeat("1", baseChunkSize-1), + output: strings.Repeat("1", baseChunkSize-1) + "0", + op: func(t *testing.T, b *Buffer) { + b.Append(NewViewWithData([]byte("0"))) + }, + }, + { + name: "append-overflow", + input: strings.Repeat("1", baseChunkSize), + output: strings.Repeat("1", baseChunkSize) + "0", + op: func(t *testing.T, b *Buffer) { + b.Append(NewViewWithData([]byte("0"))) + }, + }, + { + name: "append-multiple-views", + input: strings.Repeat("1", baseChunkSize-1), + output: strings.Repeat("1", baseChunkSize-1) + strings.Repeat("0", baseChunkSize*3), + op: func(t *testing.T, b *Buffer) { + b.Append(NewViewWithData([]byte(strings.Repeat("0", baseChunkSize)))) + b.Append(NewViewWithData([]byte(strings.Repeat("0", baseChunkSize)))) + b.Append(NewViewWithData([]byte(strings.Repeat("0", baseChunkSize)))) + }, + }, + + // AppendOwned. + { + name: "append-owned", + input: "hello", + output: "hello world", + op: func(t *testing.T, b *Buffer) { + v := NewViewWithData([]byte("Xworld")) + // Appending to a buffer that has extra references means this will + // degrade into an "appendOwned" for the chunk being added. + c := b.Clone() + defer c.Release() + b.Append(v) + v.chunk.data[0] = ' ' + }, + }, + + // Truncate. + { + name: "truncate", + input: "hello world", + output: "hello", + op: func(t *testing.T, b *Buffer) { + b.Truncate(5) + }, + }, + { + name: "truncate-noop", + input: "hello world", + output: "hello world", + op: func(t *testing.T, b *Buffer) { + b.Truncate(b.Size() + 1) + }, + }, + { + name: "truncate-multiple-buffers", + input: strings.Repeat("1", baseChunkSize), + output: strings.Repeat("1", baseChunkSize*2-1), + op: func(t *testing.T, b *Buffer) { + b.Append(NewViewWithData([]byte(strings.Repeat("1", baseChunkSize)))) + b.Truncate(baseChunkSize*2 - 1) + }, + }, + { + name: "truncate-multiple-buffers-to-one", + input: strings.Repeat("1", baseChunkSize), + output: "11111", + op: func(t *testing.T, b *Buffer) { + b.Append(NewViewWithData([]byte(strings.Repeat("1", baseChunkSize)))) + b.Truncate(5) + }, + }, + + // TrimFront. + { + name: "trim", + input: "hello world", + output: "world", + op: func(t *testing.T, b *Buffer) { + b.TrimFront(6) + }, + }, + { + name: "trim-too-large", + input: "hello world", + output: "", + op: func(t *testing.T, b *Buffer) { + b.TrimFront(b.Size() + 1) + }, + }, + { + name: "trim-multiple-buffers", + input: strings.Repeat("1", baseChunkSize), + output: strings.Repeat("1", baseChunkSize*2-1), + op: func(t *testing.T, b *Buffer) { + b.Append(NewViewWithData([]byte(strings.Repeat("1", baseChunkSize)))) + b.TrimFront(1) + }, + }, + { + name: "trim-multiple-buffers-to-one-buffer", + input: strings.Repeat("1", baseChunkSize), + output: "1", + op: func(t *testing.T, b *Buffer) { + b.Append(NewViewWithData([]byte(strings.Repeat("1", baseChunkSize)))) + b.TrimFront(baseChunkSize*2 - 1) + }, + }, + + // GrowTo. + { + name: "growto", + input: "hello world", + output: "hello world", + op: func(t *testing.T, b *Buffer) { + b.GrowTo(1, true) + }, + }, + { + name: "growto-from-zero", + output: strings.Repeat("\x00", 1024), + op: func(t *testing.T, b *Buffer) { + b.GrowTo(1024, true) + }, + }, + { + name: "growto-from-non-zero", + input: strings.Repeat("1", baseChunkSize), + output: strings.Repeat("1", baseChunkSize) + strings.Repeat("\x00", baseChunkSize), + op: func(t *testing.T, b *Buffer) { + b.GrowTo(baseChunkSize*2, true) + }, + }, + + // Clone. + { + name: "clone", + input: "hello", + output: "hello", + op: func(t *testing.T, b *Buffer) { + other := b.Clone() + bs := other.Flatten() + want := []byte("hello") + if !bytes.Equal(bs, want) { + t.Errorf("expected %v, got %v", want, bs) + } + }, + }, + { + name: "copy-large", + input: strings.Repeat("1", baseChunkSize+1), + output: strings.Repeat("1", baseChunkSize+1), + op: func(t *testing.T, b *Buffer) { + other := b.Clone() + bs := other.Flatten() + want := []byte(strings.Repeat("1", baseChunkSize+1)) + if !bytes.Equal(bs, want) { + t.Errorf("expected %v, got %v", want, bs) + } + }, + }, + + // Merge. + { + name: "merge", + input: "hello", + output: "hello world", + op: func(t *testing.T, b *Buffer) { + var other Buffer + other.Append(NewViewWithData([]byte(" world"))) + b.Merge(&other) + if sz := other.Size(); sz != 0 { + t.Errorf("expected 0, got %d", sz) + } + }, + }, + { + name: "merge-large", + input: strings.Repeat("1", baseChunkSize+1), + output: strings.Repeat("1", baseChunkSize+1) + strings.Repeat("0", baseChunkSize+1), + op: func(t *testing.T, b *Buffer) { + var other Buffer + other.Append(NewViewWithData(([]byte(strings.Repeat("0", baseChunkSize+1))))) + b.Merge(&other) + if sz := other.Size(); sz != 0 { + t.Errorf("expected 0, got %d", sz) + } + }, + }, + + // ReadAt. + { + name: "readat", + input: "hello", + output: "hello", + op: func(t *testing.T, b *Buffer) { testReadAt(t, b, 0, 6, "hello", io.EOF) }, + }, + { + name: "readat-long", + input: "hello", + output: "hello", + op: func(t *testing.T, b *Buffer) { testReadAt(t, b, 0, 8, "hello", io.EOF) }, + }, + { + name: "readat-short", + input: "hello", + output: "hello", + op: func(t *testing.T, b *Buffer) { testReadAt(t, b, 0, 3, "hel", nil) }, + }, + { + name: "readat-offset", + input: "hello", + output: "hello", + op: func(t *testing.T, b *Buffer) { testReadAt(t, b, 2, 3, "llo", io.EOF) }, + }, + { + name: "readat-long-offset", + input: "hello", + output: "hello", + op: func(t *testing.T, b *Buffer) { testReadAt(t, b, 2, 8, "llo", io.EOF) }, + }, + { + name: "readat-short-offset", + input: "hello", + output: "hello", + op: func(t *testing.T, b *Buffer) { testReadAt(t, b, 2, 2, "ll", nil) }, + }, + { + name: "readat-skip-all", + input: "hello", + output: "hello", + op: func(t *testing.T, b *Buffer) { testReadAt(t, b, baseChunkSize+1, 1, "", io.EOF) }, + }, + { + name: "readat-second-view", + input: strings.Repeat("0", baseChunkSize+1) + "12", + output: strings.Repeat("0", baseChunkSize+1) + "12", + op: func(t *testing.T, b *Buffer) { testReadAt(t, b, baseChunkSize+1, 1, "1", nil) }, + }, + { + name: "readat-second-buffer-end", + input: strings.Repeat("0", baseChunkSize+1) + "12", + output: strings.Repeat("0", baseChunkSize+1) + "12", + op: func(t *testing.T, b *Buffer) { testReadAt(t, b, baseChunkSize+1, 2, "12", io.EOF) }, + }, + } + + for _, tc := range testCases { + for fillName, fn := range fillFuncs { + t.Run(fillName+"/"+tc.name, func(t *testing.T) { + // Construct & fill the view. + var buf Buffer + fn(&buf, []byte(tc.input)) + + // Run the operation. + if tc.op != nil { + tc.op(t, &buf) + } + + // Flatten and validate. + out := buf.Flatten() + if !bytes.Equal([]byte(tc.output), out) { + t.Errorf("expected %q, got %q", tc.output, string(out)) + } + + // Ensure the size is correct. + if len(out) != int(buf.Size()) { + t.Errorf("size is wrong: expected %d, got %d", len(out), buf.Size()) + } + + // Calculate contents via apply. + var appliedOut []byte + buf.Apply(func(v *View) { + appliedOut = append(appliedOut, v.AsSlice()...) + }) + if len(appliedOut) != len(out) { + t.Errorf("expected %d, got %d", len(out), len(appliedOut)) + } + if !bytes.Equal(appliedOut, out) { + t.Errorf("expected %v, got %v", out, appliedOut) + } + + // Calculate contents via ReadToWriter. + var b bytes.Buffer + n, err := buf.ReadToWriter(&b, int64(len(out))) + if n != int64(len(out)) { + t.Errorf("expected %d, got %d", len(out), n) + } + if err != nil { + t.Errorf("expected nil, got %v", err) + } + if !bytes.Equal(b.Bytes(), out) { + t.Errorf("expected %v, got %v", out, b.Bytes()) + } + }) + } + } +} + +func TestBufferPullUp(t *testing.T) { + for _, tc := range []struct { + desc string + inputs []string + offset int + length int + output string + failed bool + // lengths is the lengths of each buffer node after the pull up. + lengths []int + }{ + { + desc: "whole empty view", + }, + { + desc: "zero pull", + inputs: []string{"hello", " world"}, + lengths: []int{5, 6}, + }, + { + desc: "whole view", + inputs: []string{"hello", " world"}, + offset: 0, + length: 11, + output: "hello world", + lengths: []int{11}, + }, + { + desc: "middle to end aligned", + inputs: []string{"0123", "45678", "9abcd"}, + offset: 4, + length: 10, + output: "456789abcd", + lengths: []int{4, 10}, + }, + { + desc: "middle to end unaligned", + inputs: []string{"0123", "45678", "9abcd"}, + offset: 6, + length: 8, + output: "6789abcd", + lengths: []int{4, 10}, + }, + { + desc: "middle aligned", + inputs: []string{"0123", "45678", "9abcd", "efgh"}, + offset: 6, + length: 5, + output: "6789a", + lengths: []int{4, 10, 4}, + }, + + // Failed cases. + { + desc: "empty view - length too long", + offset: 0, + length: 1, + failed: true, + }, + { + desc: "empty view - offset too large", + offset: 1, + length: 1, + failed: true, + }, + { + desc: "length too long", + inputs: []string{"0123", "45678", "9abcd"}, + offset: 4, + length: 100, + failed: true, + lengths: []int{4, 5, 5}, + }, + { + desc: "offset too large", + inputs: []string{"0123", "45678", "9abcd"}, + offset: 100, + length: 1, + failed: true, + lengths: []int{4, 5, 5}, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + var b Buffer + defer b.Release() + for _, s := range tc.inputs { + v := NewViewWithData([]byte(s)) + b.appendOwned(v) + } + + got, gotOk := b.PullUp(tc.offset, tc.length) + want, wantOk := []byte(tc.output), !tc.failed + if gotOk == wantOk && got == nil && len(want) == 0 { + return + } + if gotOk != wantOk || !bytes.Equal(got.AsSlice(), want) { + t.Errorf("v.PullUp(%d, %d) = %q, %t; %q, %t", tc.offset, tc.length, got.AsSlice(), gotOk, want, wantOk) + } + + var gotLengths []int + for v := b.data.Front(); v != nil; v = v.Next() { + gotLengths = append(gotLengths, v.Size()) + } + if !reflect.DeepEqual(gotLengths, tc.lengths) { + t.Errorf("lengths = %v; want %v", gotLengths, tc.lengths) + } + }) + } +} + +func TestBufferClone(t *testing.T) { + const ( + originalSize = 90 + bytesToDelete = 30 + ) + b := MakeWithData(bytes.Repeat([]byte{originalSize}, originalSize)) + clonedB := b.Clone() + b.TrimFront(bytesToDelete) + + if got, want := int(b.Size()), originalSize-bytesToDelete; got != want { + t.Errorf("original buffer was not changed: size expected = %d, got = %d", want, got) + } + if got := clonedB.Size(); got != originalSize { + t.Errorf("cloned buffer should not be modified: expected size = %d, got = %d", originalSize, got) + } +} + +func TestBufferSubApply(t *testing.T) { + var b Buffer + defer b.Release() + b.appendOwned(NewViewWithData([]byte("0123"))) + b.appendOwned(NewViewWithData([]byte("45678"))) + b.appendOwned(NewViewWithData([]byte("9abcd"))) + data := []byte("0123456789abcd") + + for i := 0; i <= len(data); i++ { + for j := i; j <= len(data); j++ { + t.Run(fmt.Sprintf("SubApply(%d,%d)", i, j), func(t *testing.T) { + var got []byte + b.SubApply(i, j-i, func(v *View) { + got = append(got, v.AsSlice()...) + }) + if want := data[i:j]; !bytes.Equal(got, want) { + t.Errorf("got = %q; want %q", got, want) + } + }) + } + } +} + +func doSaveAndLoad(t *testing.T, toSave, toLoad *Buffer) { + 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 TestSaveRestoreBufferEmpty(t *testing.T) { + var toSave Buffer + var b Buffer + doSaveAndLoad(t, &toSave, &b) + + if got := b.Flatten(); len(got) != 0 { + t.Errorf("v.Flatten() = %x, want []", got) + } +} + +func TestSaveRestoreBuffer(t *testing.T) { + // Create data that fits slots. + data := bytes.Join([][]byte{ + bytes.Repeat([]byte{1, 2}, baseChunkSize), + }, nil) + + var toSave Buffer + toSave.appendOwned(NewViewWithData(data)) + + var b Buffer + doSaveAndLoad(t, &toSave, &b) + + // Next available slot at index 3; 0-2 slot are used. + if got := b.Flatten(); !bytes.Equal(got, data) { + t.Errorf("v.Flatten() = %x, want %x", got, data) + } +} + +func TestRangeIntersect(t *testing.T) { + for _, tc := range []struct { + desc string + x, y, want Range + }{ + { + desc: "empty intersects empty", + }, + { + desc: "empty intersection", + x: Range{end: 10}, + y: Range{begin: 10, end: 20}, + }, + { + desc: "some intersection", + x: Range{begin: 5, end: 20}, + y: Range{end: 10}, + want: Range{begin: 5, end: 10}, + }, + } { + t.Run(tc.desc, func(t *testing.T) { + if got := tc.x.Intersect(tc.y); got != tc.want { + t.Errorf("(%#v).Intersect(%#v) = %#v; want %#v", tc.x, tc.y, got, tc.want) + } + if got := tc.y.Intersect(tc.x); got != tc.want { + t.Errorf("(%#v).Intersect(%#v) = %#v; want %#v", tc.y, tc.x, got, tc.want) + } + }) + } +} + +func TestRangeOffset(t *testing.T) { + for _, tc := range []struct { + input Range + offset int + output Range + }{ + { + input: Range{}, + offset: 0, + output: Range{}, + }, + { + input: Range{}, + offset: -1, + output: Range{begin: -1, end: -1}, + }, + { + input: Range{begin: 10, end: 20}, + offset: -1, + output: Range{begin: 9, end: 19}, + }, + { + input: Range{begin: 10, end: 20}, + offset: 2, + output: Range{begin: 12, end: 22}, + }, + } { + if got := tc.input.Offset(tc.offset); got != tc.output { + t.Errorf("(%#v).Offset(%d) = %#v, want %#v", tc.input, tc.offset, got, tc.output) + } + } +} + +func TestRangeLen(t *testing.T) { + for _, tc := range []struct { + r Range + want int + }{ + {r: Range{}, want: 0}, + {r: Range{begin: 1, end: 1}, want: 0}, + {r: Range{begin: -1, end: -1}, want: 0}, + {r: Range{end: 10}, want: 10}, + {r: Range{begin: 5, end: 10}, want: 5}, + } { + if got := tc.r.Len(); got != tc.want { + t.Errorf("(%#v).Len() = %d, want %d", tc.r, got, tc.want) + } + } +} diff --git a/pkg/bufferv2/chunk.go b/pkg/bufferv2/chunk.go new file mode 100644 index 000000000..8cab3cba0 --- /dev/null +++ b/pkg/bufferv2/chunk.go @@ -0,0 +1,113 @@ +// Copyright 2022 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 ( + "fmt" + + "gvisor.dev/gvisor/pkg/bits" + "gvisor.dev/gvisor/pkg/sync" +) + +const ( + // This is log2(baseChunkSize). This number is used to calculate which pool + // to use for a payload size by right shifting the payload size by this + // number and passing the result to MostSignificantOne64. + baseChunkSizeLog2 = 6 + + // This is the size of the buffers in the first pool. Each subsquent pool + // creates payloads 2^(pool index) times larger than the first pool's + // payloads. + baseChunkSize = 1 << baseChunkSizeLog2 // 64 + + // The largest payload size that we pool. Payloads larger than this will + // allocated from the heap and garbage collected as normal. + maxChunkSize = baseChunkSize << (numPools - 1) // 65536 + + // The number of chunk pools we have for use. + numPools = 11 +) + +// chunkPools is a collection of pools for payloads of different sizes. The +// size of the payloads doubles in each successive pool. +var chunkPools [numPools]sync.Pool + +func init() { + for i := 0; i < numPools; i++ { + chunkSize := baseChunkSize * (1 << i) + chunkPools[i].New = func() interface{} { + return &chunk{ + data: make([]byte, chunkSize), + } + } + } +} + +// Precondition: 0 <= size <= maxChunkSize +func getChunkPool(size int) *sync.Pool { + idx := 0 + if size > baseChunkSize { + idx = bits.MostSignificantOne64(uint64(size) >> baseChunkSizeLog2) + if size > 1<<(idx+baseChunkSizeLog2) { + idx++ + } + } + if idx >= numPools { + panic(fmt.Sprintf("pool for chunk size %d does not exist", size)) + } + return &chunkPools[idx] +} + +// Chunk represents a slice of pooled memory. +type chunk struct { + chunkRefs + data []byte +} + +func newChunk(size int) *chunk { + var c *chunk + if size > maxChunkSize { + c = &chunk{ + data: make([]byte, size), + } + } else { + pool := getChunkPool(size) + c = pool.Get().(*chunk) + for i := range c.data { + c.data[i] = 0 + } + } + c.InitRefs() + return c +} + +func (c *chunk) destroy() { + if len(c.data) > maxChunkSize { + c.data = nil + return + } + pool := getChunkPool(len(c.data)) + pool.Put(c) +} + +func (c *chunk) DecRef() { + c.chunkRefs.DecRef(c.destroy) +} + +func (c *chunk) Clone() *chunk { + cpy := newChunk(len(c.data)) + copy(cpy.data, c.data) + return cpy +} diff --git a/pkg/bufferv2/view.go b/pkg/bufferv2/view.go new file mode 100644 index 000000000..659493bcc --- /dev/null +++ b/pkg/bufferv2/view.go @@ -0,0 +1,232 @@ +// Copyright 2022 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 ( + "fmt" + "io" + + "gvisor.dev/gvisor/pkg/sync" +) + +var viewPool = sync.Pool{ + New: func() interface{} { + return &View{} + }, +} + +// View is a window into a shared chunk. Views are held by Buffers in +// viewLists to represent contiguous memory. +// +// A View must be created with NewView, NewViewWithData, or Clone. Owners are +// responsible for maintaining ownership over their views. When Views need to be +// shared or copied, the owner should create a new View with Clone. Clone must +// only ever be called on a owned View, not a borrowed one. +// +// Users are responsible for calling Release when finished with their View so +// that its resources can be returned to the pool. +// +// Users must not write directly to slices returned by AsSlice. Instead, they +// must use Write/WriteAt/CopyIn to modify the underlying View. This preserves +// the safety guarantees of copy-on-write. +type View struct { + sync.NoCopy + + viewEntry + read int + write int + chunk *chunk +} + +// NewView creates a new view with capacity at least as big as cap. It is +// analogous to make([]byte, 0, cap). +func NewView(cap int) *View { + c := newChunk(cap) + v := viewPool.Get().(*View) + *v = View{chunk: c} + return v +} + +// NewViewSize creates a new view with capacity at least as big as size and +// length that is exactly size. It is analogous to make([]byte, size). +func NewViewSize(size int) *View { + v := NewView(size) + v.Grow(size) + return v +} + +// NewViewWithData creates a new view and initializes it with data. This +// function should be used with caution to avoid unnecessary []byte allocations. +// When in doubt use NewWithView to maximize chunk reuse in production +// environments. +func NewViewWithData(data []byte) *View { + c := newChunk(len(data)) + v := viewPool.Get().(*View) + *v = View{chunk: c} + v.Write(data) + return v +} + +// Clone creates a shallow clone of v where the underlying chunk is shared. +// +// The caller must own the View to call Clone. It is not safe to call Clone +// on a borrowed or shared View because it can race with other View methods. +func (v *View) Clone() *View { + v.chunk.IncRef() + newV := viewPool.Get().(*View) + newV.chunk = v.chunk + newV.read = v.read + newV.write = v.write + return newV +} + +// Release releases the chunk held by v and returns v to the pool. +func (v *View) Release() { + v.chunk.DecRef() + *v = View{} + viewPool.Put(v) +} + +func (v *View) sharesChunk() bool { + return v.chunk.refCount.Load() > 1 +} + +// Full indicates the chunk is full. +// +// This indicates there is no capacity left to write. +func (v *View) Full() bool { + return v == nil || v.write == len(v.chunk.data) +} + +// Capacity returns the total size of this view's chunk. +func (v *View) Capacity() int { + return len(v.chunk.data) +} + +// Size returns the size of data written to the view. +func (v *View) Size() int { + return v.write - v.read +} + +// TrimFront advances the read index by the given amount. +func (v *View) TrimFront(n int) { + v.read += n +} + +// AsSlice returns a slice of the data written to this view. +func (v *View) AsSlice() []byte { + if v == nil { + return nil + } + return v.chunk.data[v.read:v.write] +} + +// AvailableSize returns the number of bytes available for writing. +func (v *View) AvailableSize() int { + return len(v.chunk.data) - v.write +} + +// Read reads v's data into p. +// +// Implements the io.Reader interface. +func (v *View) Read(p []byte) (int, error) { + if len(p) == 0 { + return 0, nil + } + if v.Size() == 0 { + return 0, io.EOF + } + n := copy(p, v.AsSlice()) + v.TrimFront(n) + return n, nil +} + +// ReadAt reads data to the p starting at offset. +// +// Implements the io.ReaderAt interface. +func (v *View) ReadAt(p []byte, off int) (int, error) { + if off < 0 || off > v.Size() { + return 0, fmt.Errorf("ReadAt(): offset out of bounds: want 0 < off < %d, got off=%d", v.Size(), off) + } + n := copy(p, v.AsSlice()[off:]) + return n, nil +} + +// Write writes data to the view's chunk starting at the v.write index. If the +// view's chunk has a reference count greater than 1, the chunk is copied first +// and then written to. +// +// Implements the io.Writer interface. +func (v *View) Write(p []byte) (int, error) { + if v.sharesChunk() { + defer v.chunk.DecRef() + v.chunk = v.chunk.Clone() + } + n := copy(v.chunk.data[v.write:], p) + v.write += n + if n < len(p) { + return n, fmt.Errorf("could not finish write: want len(p) <= v.AvailableSize(), got len(p)=%d, v.AvailableSize()=%d", len(p), v.AvailableSize()) + } + return n, nil +} + +// WriteAt writes data to the views's chunk starting at start. If the +// view's chunk has a reference count greater than 1, the chunk is copied first +// and then written to. +// +// Implements the io.WriterAt interface. +func (v *View) WriteAt(p []byte, off int) (int, error) { + if off < 0 || off > v.Size() { + return 0, fmt.Errorf("write offset out of bounds: want 0 < off < %d, got off=%d", v.Size(), off) + } + if v.sharesChunk() { + defer v.chunk.DecRef() + v.chunk = v.chunk.Clone() + } + n := copy(v.AsSlice()[off:], p) + if n < len(p) { + return n, fmt.Errorf("could not finish write: want off + len(p) < v.Capacity(), got off=%d, len(p)=%d ,v.Size() = %d", off, len(p), v.Size()) + } + return n, nil +} + +// Grow advances the write index by the given amount. +func (v *View) Grow(n int) { + if n+v.write > v.Capacity() { + panic("cannot grow view past capacity") + } + v.write += n +} + +// CapLength caps the length of the view's read slice to n. If n > v.Size(), +// the function is a no-op. +func (v *View) CapLength(n int) { + if n < 0 { + panic("n must be >= 0") + } + if n > v.Size() { + n = v.Size() + } + v.write = v.read + n +} + +func (v *View) availableSlice() []byte { + if v.sharesChunk() { + defer v.chunk.DecRef() + c := v.chunk.Clone() + v.chunk = c + } + return v.chunk.data[v.write:] +} diff --git a/pkg/bufferv2/view_test.go b/pkg/bufferv2/view_test.go new file mode 100644 index 000000000..25eda9e22 --- /dev/null +++ b/pkg/bufferv2/view_test.go @@ -0,0 +1,174 @@ +// Copyright 2022 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 ( + "math/rand" + "testing" + + "github.com/google/go-cmp/cmp" +) + +func TestNewView(t *testing.T) { + for sz := baseChunkSize; sz < maxChunkSize; sz <<= 1 { + v := NewView(sz - 1) + defer v.Release() + + if v.Capacity() != sz { + t.Errorf("v.Capacity() = %d, want %d", v.Capacity(), sz) + } + if v.AvailableSize() != sz { + t.Errorf("v.WriteSize() = %d, want %d", v.AvailableSize(), sz) + } + if v.Size() != 0 { + t.Errorf("v.ReadSize() = %d, want %d", v.Size(), 0) + } + + v1 := NewView(sz) + defer v1.Release() + + if v1.Capacity() != sz { + t.Errorf("v.Capacity() = %d, want %d", v.Capacity(), sz) + } + if v1.AvailableSize() != sz { + t.Errorf("v.WriteSize() = %d, want %d", v.AvailableSize(), sz) + } + if v1.Size() != 0 { + t.Errorf("v.ReadSize() = %d, want %d", v.Size(), 0) + } + } + + // Allocating from heap should produce a chunk with the exact size requested + // instead of a chunk where the size is contingent on the pool it came from. + viewSize := maxChunkSize + 1 + v := NewView(viewSize) + defer v.Release() + if v.Capacity() != viewSize { + t.Errorf("v.Capacity() = %d, want %d", v.Capacity(), viewSize) + } +} + +func TestClone(t *testing.T) { + orig := NewView(100) + clone := orig.Clone() + if orig.chunk != clone.chunk { + t.Errorf("orig.Clone().chunk = %p, want %p", clone.chunk, orig.chunk) + } + if orig.chunk.refCount.Load() != 2 { + t.Errorf("got orig.chunk.chunkRefs.Load() = %d, want 2", orig.chunk.refCount.Load()) + } + orig.Release() + if clone.chunk.refCount.Load() != 1 { + t.Errorf("got clone.chunk.chunkRefs.Load() = %d, want 1", clone.chunk.refCount.Load()) + } + clone.Release() +} + +func TestWrite(t *testing.T) { + for _, tc := range []struct { + name string + view *View + initData []byte + writeSize int + }{ + { + name: "empty view", + view: NewView(100), + writeSize: 50, + }, + { + name: "full view", + view: NewView(100), + initData: make([]byte, 100), + writeSize: 50, + }, + { + name: "full write to partially full view", + view: NewView(100), + initData: make([]byte, 20), + writeSize: 50, + }, + { + name: "partial write to partially full view", + view: NewView(100), + initData: make([]byte, 80), + writeSize: 50, + }, + } { + t.Run(tc.name, func(t *testing.T) { + tc.view.Write(tc.initData) + defer tc.view.Release() + origWriteSize := tc.view.AvailableSize() + + var orig []byte + orig = append(orig, tc.view.AsSlice()...) + toWrite := make([]byte, tc.writeSize) + rand.Read(toWrite) + + n, _ := tc.view.Write(toWrite) + + if n > origWriteSize { + t.Errorf("got tc.view.Write() = %d, want <=%d", n, origWriteSize) + } + if tc.writeSize > origWriteSize { + toWrite = toWrite[:origWriteSize] + } + if tc.view.AvailableSize() != tc.view.Capacity()-(len(toWrite)+len(orig)) { + t.Errorf("got tc.view.WriteSize() = %d, want %d", tc.view.AvailableSize(), tc.view.Capacity()-(len(toWrite)+len(orig))) + } + if !cmp.Equal(tc.view.AsSlice(), append(orig, toWrite...)) { + t.Errorf("got tc.view.ReadSlice() = %d, want %d", tc.view.AsSlice(), toWrite) + } + }) + } +} + +func TestWriteToCloned(t *testing.T) { + orig := NewView(100) + toWrite := make([]byte, 20) + rand.Read(toWrite) + orig.Write(toWrite) + + clone := orig.Clone() + clone.Write(toWrite) + + if !cmp.Equal(orig.AsSlice(), toWrite) { + t.Errorf("got orig.ReadSlice() = %v, want %v", orig.AsSlice(), toWrite) + } + + toWrite = append(toWrite, toWrite...) + if !cmp.Equal(clone.AsSlice(), toWrite) { + t.Errorf("got clone.ReadSlice() = %v, want %v", clone.AsSlice(), toWrite) + } +} + +func TestWriteAt(t *testing.T) { + size := 10 + off := 5 + v := NewViewSize(size) + p := make([]byte, 20) + rand.Read(p) + orig := v.Clone() + + if n, _ := v.WriteAt(p, off); n != size-off { + t.Errorf("got v.CopyIn()= %v, want %v", n, size-off) + } + if !cmp.Equal(v.AsSlice()[off:], p[:size-off]) { + t.Errorf("got v.AsSlice()[off:] = %v, want %v", v.AsSlice()[off:], p[off:size]) + } + if !cmp.Equal(v.AsSlice()[:off], orig.AsSlice()[:off]) { + t.Errorf("got v.AsSlice()[:off] = %v, want %v", v.AsSlice()[:off], orig.AsSlice()[:off]) + } +} diff --git a/pkg/bufferv2/view_unsafe.go b/pkg/bufferv2/view_unsafe.go new file mode 100644 index 000000000..19c99bcbf --- /dev/null +++ b/pkg/bufferv2/view_unsafe.go @@ -0,0 +1,32 @@ +// 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 ( + "reflect" + "unsafe" +) + +// minBatch is the smallest Read or Write operation that the +// WriteFromReader and ReadToWriter functions will use. +// +// This is defined as the size of a native pointer. +const minBatch = int(unsafe.Sizeof(uintptr(0))) + +// BasePtr returns a pointer to the view's chunk. +func (v *View) BasePtr() *byte { + hdr := (*reflect.SliceHeader)(unsafe.Pointer(&v.chunk.data)) + return (*byte)(unsafe.Pointer(hdr.Data)) +}