From e1eda90e29497aecea3ea0bb7a09e62536233498 Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Sun, 12 Feb 2017 21:46:15 -0600 Subject: [PATCH 01/14] Add ChunkReader --- chunkreader.go | 106 ++++++++++++++++++++++++++++++++++++ chunkreader_test.go | 128 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 234 insertions(+) create mode 100644 chunkreader.go create mode 100644 chunkreader_test.go diff --git a/chunkreader.go b/chunkreader.go new file mode 100644 index 00000000..f9d6555c --- /dev/null +++ b/chunkreader.go @@ -0,0 +1,106 @@ +package chunkreader + +import ( + "io" +) + +type ChunkReader struct { + r io.Reader + + buf []byte + rp, wp int // buf read position and write position + taken bool + + options Options +} + +type Options struct { + MinBufLen int // Minimum buffer length + BlockLen int // Increments to expand buffer (e.g. a 8000 byte request with a BlockLen of 1024 would yield a buffer len of 8192) +} + +func NewChunkReader(r io.Reader) *ChunkReader { + cr, err := NewChunkReaderEx(r, Options{}) + if err != nil { + panic("default options can't be bad") + } + + return cr +} + +func NewChunkReaderEx(r io.Reader, options Options) (*ChunkReader, error) { + if options.MinBufLen == 0 { + options.MinBufLen = 4096 + } + if options.BlockLen == 0 { + options.BlockLen = 512 + } + + return &ChunkReader{ + r: r, + buf: make([]byte, options.MinBufLen), + options: options, + }, nil +} + +// Next returns buf filled with the next n bytes. buf is only valid until the +// next call to Next. If an error occurs, buf will be nil. +func (r *ChunkReader) Next(n int) (buf []byte, err error) { + // n bytes already in buf + if (r.wp - r.rp) >= n { + buf = r.buf[r.rp : r.rp+n] + r.rp += n + return buf, err + } + + // available space in buf is less than n + if len(r.buf) < n { + r.copyBufContents(r.newBuf(n)) + r.taken = false + } + + // buf is large enough, but need to shift filled area to start to make enough contiguous space + minReadCount := n - (r.wp - r.rp) + if (len(r.buf) - r.wp) < minReadCount { + newBuf := r.buf + if r.taken { + newBuf = r.newBuf(n) + r.taken = false + } + r.copyBufContents(newBuf) + } + + if err := r.appendAtLeast(minReadCount); err != nil { + return nil, err + } + + buf = r.buf[r.rp : r.rp+n] + r.rp += n + return buf, nil +} + +// KeepLast prevents the last data retrieved by Next from being reused by the +// ChunkReader. +func (r *ChunkReader) KeepLast() { + r.taken = true +} + +func (r *ChunkReader) appendAtLeast(fillLen int) error { + n, err := io.ReadAtLeast(r.r, r.buf[r.wp:], fillLen) + r.wp += n + return err +} + +func (r *ChunkReader) newBuf(min int) []byte { + size := ((min / r.options.BlockLen) + 1) * r.options.BlockLen + if size < r.options.MinBufLen { + size = r.options.MinBufLen + } + return make([]byte, size) +} + +func (r *ChunkReader) copyBufContents(dest []byte) { + r.wp = copy(dest, r.buf[r.rp:r.wp]) + r.rp = 0 + r.buf = dest +} diff --git a/chunkreader_test.go b/chunkreader_test.go new file mode 100644 index 00000000..9c19ff4a --- /dev/null +++ b/chunkreader_test.go @@ -0,0 +1,128 @@ +package chunkreader + +import ( + "bytes" + "testing" +) + +func TestChunkReaderNextDoesNotReadIfAlreadyBuffered(t *testing.T) { + server := &bytes.Buffer{} + r, err := NewChunkReaderEx(server, Options{MinBufLen: 4, BlockLen: 2}) + if err != nil { + t.Fatal(err) + } + + src := []byte{1, 2, 3, 4} + server.Write(src) + + n1, err := r.Next(2) + if err != nil { + t.Fatal(err) + } + if bytes.Compare(n1, src[0:2]) != 0 { + t.Fatalf("Expected read bytes to be %v, but they were %v", src[0:2], n1) + } + + n2, err := r.Next(2) + if err != nil { + t.Fatal(err) + } + if bytes.Compare(n2, src[2:4]) != 0 { + t.Fatalf("Expected read bytes to be %v, but they were %v", src[2:4], n2) + } + + if bytes.Compare(r.buf, src) != 0 { + t.Fatalf("Expected r.buf to be %v, but it was %v", src, r.buf) + } + if r.rp != 4 { + t.Fatalf("Expected r.rp to be %v, but it was %v", 4, r.rp) + } + if r.wp != 4 { + t.Fatalf("Expected r.wp to be %v, but it was %v", 4, r.wp) + } +} + +func TestChunkReaderNextExpandsBufAsNeeded(t *testing.T) { + server := &bytes.Buffer{} + r, err := NewChunkReaderEx(server, Options{MinBufLen: 4, BlockLen: 2}) + if err != nil { + t.Fatal(err) + } + + src := []byte{1, 2, 3, 4, 5, 6, 7, 8} + server.Write(src) + + n1, err := r.Next(5) + if err != nil { + t.Fatal(err) + } + if bytes.Compare(n1, src[0:5]) != 0 { + t.Fatalf("Expected read bytes to be %v, but they were %v", src[0:5], n1) + } + if len(r.buf) != 6 { + t.Fatalf("Expected len(r.buf) to be %v, but it was %v", 6, len(r.buf)) + } +} + +func TestChunkReaderNextReusesBuf(t *testing.T) { + server := &bytes.Buffer{} + r, err := NewChunkReaderEx(server, Options{MinBufLen: 4, BlockLen: 1}) + if err != nil { + t.Fatal(err) + } + + src := []byte{1, 2, 3, 4, 5, 6, 7, 8} + server.Write(src) + + n1, err := r.Next(4) + if err != nil { + t.Fatal(err) + } + if bytes.Compare(n1, src[0:4]) != 0 { + t.Fatalf("Expected read bytes to be %v, but they were %v", src[0:4], n1) + } + + n2, err := r.Next(4) + if err != nil { + t.Fatal(err) + } + if bytes.Compare(n2, src[4:8]) != 0 { + t.Fatalf("Expected read bytes to be %v, but they were %v", src[4:8], n2) + } + + if bytes.Compare(n1, src[4:8]) != 0 { + t.Fatalf("Expected Next to have reused buf, %v found instead of %v", src[4:8], n1) + } +} + +func TestChunkReaderKeepLastPreventsBufReuse(t *testing.T) { + server := &bytes.Buffer{} + r, err := NewChunkReaderEx(server, Options{MinBufLen: 4, BlockLen: 1}) + if err != nil { + t.Fatal(err) + } + + src := []byte{1, 2, 3, 4, 5, 6, 7, 8} + server.Write(src) + + n1, err := r.Next(4) + if err != nil { + t.Fatal(err) + } + if bytes.Compare(n1, src[0:4]) != 0 { + t.Fatalf("Expected read bytes to be %v, but they were %v", src[0:4], n1) + } + r.KeepLast() + + n2, err := r.Next(4) + if err != nil { + t.Fatal(err) + } + if bytes.Compare(n2, src[4:8]) != 0 { + t.Fatalf("Expected read bytes to be %v, but they were %v", src[4:8], n2) + } + + if bytes.Compare(n1, src[0:4]) != 0 { + t.Fatalf("Expected KeepLast to prevent Next from overwriting buf, expected %v but it was %v", src[0:4], n1) + } +} From 61026b7c21bc88079489858a2d9c7c7f708a0348 Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Sat, 29 Apr 2017 11:55:14 -0500 Subject: [PATCH 02/14] Reduce allocations and copies in pgproto3 Altered chunkreader to never reuse memory. Altered pgproto3 to to copy memory when decoding. Renamed UnmarshalBinary to Decode because of changed semantics. --- chunkreader.go | 25 ++++--------------------- chunkreader_test.go | 44 ++++++-------------------------------------- 2 files changed, 10 insertions(+), 59 deletions(-) diff --git a/chunkreader.go b/chunkreader.go index f9d6555c..f8d437b2 100644 --- a/chunkreader.go +++ b/chunkreader.go @@ -9,14 +9,12 @@ type ChunkReader struct { buf []byte rp, wp int // buf read position and write position - taken bool options Options } type Options struct { MinBufLen int // Minimum buffer length - BlockLen int // Increments to expand buffer (e.g. a 8000 byte request with a BlockLen of 1024 would yield a buffer len of 8192) } func NewChunkReader(r io.Reader) *ChunkReader { @@ -32,9 +30,6 @@ func NewChunkReaderEx(r io.Reader, options Options) (*ChunkReader, error) { if options.MinBufLen == 0 { options.MinBufLen = 4096 } - if options.BlockLen == 0 { - options.BlockLen = 512 - } return &ChunkReader{ r: r, @@ -43,8 +38,8 @@ func NewChunkReaderEx(r io.Reader, options Options) (*ChunkReader, error) { }, nil } -// Next returns buf filled with the next n bytes. buf is only valid until the -// next call to Next. If an error occurs, buf will be nil. +// Next returns buf filled with the next n bytes. If an error occurs, buf will +// be nil. func (r *ChunkReader) Next(n int) (buf []byte, err error) { // n bytes already in buf if (r.wp - r.rp) >= n { @@ -56,17 +51,12 @@ func (r *ChunkReader) Next(n int) (buf []byte, err error) { // available space in buf is less than n if len(r.buf) < n { r.copyBufContents(r.newBuf(n)) - r.taken = false } // buf is large enough, but need to shift filled area to start to make enough contiguous space minReadCount := n - (r.wp - r.rp) if (len(r.buf) - r.wp) < minReadCount { - newBuf := r.buf - if r.taken { - newBuf = r.newBuf(n) - r.taken = false - } + newBuf := r.newBuf(n) r.copyBufContents(newBuf) } @@ -79,20 +69,13 @@ func (r *ChunkReader) Next(n int) (buf []byte, err error) { return buf, nil } -// KeepLast prevents the last data retrieved by Next from being reused by the -// ChunkReader. -func (r *ChunkReader) KeepLast() { - r.taken = true -} - func (r *ChunkReader) appendAtLeast(fillLen int) error { n, err := io.ReadAtLeast(r.r, r.buf[r.wp:], fillLen) r.wp += n return err } -func (r *ChunkReader) newBuf(min int) []byte { - size := ((min / r.options.BlockLen) + 1) * r.options.BlockLen +func (r *ChunkReader) newBuf(size int) []byte { if size < r.options.MinBufLen { size = r.options.MinBufLen } diff --git a/chunkreader_test.go b/chunkreader_test.go index 9c19ff4a..3be07e3c 100644 --- a/chunkreader_test.go +++ b/chunkreader_test.go @@ -7,7 +7,7 @@ import ( func TestChunkReaderNextDoesNotReadIfAlreadyBuffered(t *testing.T) { server := &bytes.Buffer{} - r, err := NewChunkReaderEx(server, Options{MinBufLen: 4, BlockLen: 2}) + r, err := NewChunkReaderEx(server, Options{MinBufLen: 4}) if err != nil { t.Fatal(err) } @@ -44,7 +44,7 @@ func TestChunkReaderNextDoesNotReadIfAlreadyBuffered(t *testing.T) { func TestChunkReaderNextExpandsBufAsNeeded(t *testing.T) { server := &bytes.Buffer{} - r, err := NewChunkReaderEx(server, Options{MinBufLen: 4, BlockLen: 2}) + r, err := NewChunkReaderEx(server, Options{MinBufLen: 4}) if err != nil { t.Fatal(err) } @@ -59,14 +59,14 @@ func TestChunkReaderNextExpandsBufAsNeeded(t *testing.T) { if bytes.Compare(n1, src[0:5]) != 0 { t.Fatalf("Expected read bytes to be %v, but they were %v", src[0:5], n1) } - if len(r.buf) != 6 { - t.Fatalf("Expected len(r.buf) to be %v, but it was %v", 6, len(r.buf)) + if len(r.buf) != 5 { + t.Fatalf("Expected len(r.buf) to be %v, but it was %v", 5, len(r.buf)) } } -func TestChunkReaderNextReusesBuf(t *testing.T) { +func TestChunkReaderDoesNotReuseBuf(t *testing.T) { server := &bytes.Buffer{} - r, err := NewChunkReaderEx(server, Options{MinBufLen: 4, BlockLen: 1}) + r, err := NewChunkReaderEx(server, Options{MinBufLen: 4}) if err != nil { t.Fatal(err) } @@ -90,38 +90,6 @@ func TestChunkReaderNextReusesBuf(t *testing.T) { t.Fatalf("Expected read bytes to be %v, but they were %v", src[4:8], n2) } - if bytes.Compare(n1, src[4:8]) != 0 { - t.Fatalf("Expected Next to have reused buf, %v found instead of %v", src[4:8], n1) - } -} - -func TestChunkReaderKeepLastPreventsBufReuse(t *testing.T) { - server := &bytes.Buffer{} - r, err := NewChunkReaderEx(server, Options{MinBufLen: 4, BlockLen: 1}) - if err != nil { - t.Fatal(err) - } - - src := []byte{1, 2, 3, 4, 5, 6, 7, 8} - server.Write(src) - - n1, err := r.Next(4) - if err != nil { - t.Fatal(err) - } - if bytes.Compare(n1, src[0:4]) != 0 { - t.Fatalf("Expected read bytes to be %v, but they were %v", src[0:4], n1) - } - r.KeepLast() - - n2, err := r.Next(4) - if err != nil { - t.Fatal(err) - } - if bytes.Compare(n2, src[4:8]) != 0 { - t.Fatalf("Expected read bytes to be %v, but they were %v", src[4:8], n2) - } - if bytes.Compare(n1, src[0:4]) != 0 { t.Fatalf("Expected KeepLast to prevent Next from overwriting buf, expected %v but it was %v", src[0:4], n1) } From e2207bfbaf2d7771de52f1d0951a1e0b8cc7882e Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Sat, 30 Mar 2019 12:18:27 -0500 Subject: [PATCH 03/14] Add some documentation --- chunkreader.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/chunkreader.go b/chunkreader.go index f8d437b2..fedd41e5 100644 --- a/chunkreader.go +++ b/chunkreader.go @@ -1,9 +1,11 @@ +// Package chunkreader provides an opinionated, efficient buffered reader. package chunkreader import ( "io" ) +// ChunkReader is a io.Reader wrapper that minimizes reads and memory allocations. type ChunkReader struct { r io.Reader From 65a3248f5c03df8019ec64664dc600e635208af7 Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Sat, 30 Mar 2019 12:20:18 -0500 Subject: [PATCH 04/14] Add license and readme --- LICENSE | 22 ++++++++++++++++++++++ README.md | 8 ++++++++ 2 files changed, 30 insertions(+) create mode 100644 LICENSE create mode 100644 README.md diff --git a/LICENSE b/LICENSE new file mode 100644 index 00000000..c1c4f50f --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +Copyright (c) 2019 Jack Christensen + +MIT License + +Permission is hereby granted, free of charge, to any person obtaining +a copy of this software and associated documentation files (the +"Software"), to deal in the Software without restriction, including +without limitation the rights to use, copy, modify, merge, publish, +distribute, sublicense, and/or sell copies of the Software, and to +permit persons to whom the Software is furnished to do so, subject to +the following conditions: + +The above copyright notice and this permission notice shall be +included in all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND +NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE +LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION +OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION +WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md new file mode 100644 index 00000000..bcc9ac6b --- /dev/null +++ b/README.md @@ -0,0 +1,8 @@ +[![](https://godoc.org/github.com/jackc/chunkreader?status.svg)](https://godoc.org/github.com/jackc/chunkreader) +[![Build Status](https://travis-ci.org/jackc/chunkreader.svg)](https://travis-ci.org/jackc/chunkreader) + +# chunkreader + +Package chunkreader provides an opinionated, efficient buffered reader. + +Extracted from original implementation in https://github.com/jackc/pgx. From 811a7d92d62c682d8229f63f19c0b425ef9c12aa Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Sat, 30 Mar 2019 12:21:06 -0500 Subject: [PATCH 05/14] Add Go module support --- go.mod | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 go.mod diff --git a/go.mod b/go.mod new file mode 100644 index 00000000..b1ed8c92 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module github.com/jackc/chunkreader + +go 1.12 From 517cfde605cd1f91edc0a62affca418114388fc3 Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Sat, 30 Mar 2019 12:21:36 -0500 Subject: [PATCH 06/14] Add Travis CI --- .travis.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .travis.yml diff --git a/.travis.yml b/.travis.yml new file mode 100644 index 00000000..e176228e --- /dev/null +++ b/.travis.yml @@ -0,0 +1,9 @@ +language: go + +go: + - 1.x + - tip + +matrix: + allow_failures: + - go: tip From ecdcf4a36773147f7fb46573cbb1ee7a6130ba0f Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Sat, 8 Jun 2019 18:06:29 -0500 Subject: [PATCH 07/14] Rename Option to Config --- chunkreader.go | 25 +++++++++++++------------ chunkreader_test.go | 6 +++--- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/chunkreader.go b/chunkreader.go index fedd41e5..43363e42 100644 --- a/chunkreader.go +++ b/chunkreader.go @@ -12,31 +12,32 @@ type ChunkReader struct { buf []byte rp, wp int // buf read position and write position - options Options + config Config } -type Options struct { +// Config contains configuration parameters for ChunkReader. +type Config struct { MinBufLen int // Minimum buffer length } func NewChunkReader(r io.Reader) *ChunkReader { - cr, err := NewChunkReaderEx(r, Options{}) + cr, err := NewChunkReaderEx(r, Config{}) if err != nil { - panic("default options can't be bad") + panic("default config can't be bad") } return cr } -func NewChunkReaderEx(r io.Reader, options Options) (*ChunkReader, error) { - if options.MinBufLen == 0 { - options.MinBufLen = 4096 +func NewChunkReaderEx(r io.Reader, config Config) (*ChunkReader, error) { + if config.MinBufLen == 0 { + config.MinBufLen = 4096 } return &ChunkReader{ - r: r, - buf: make([]byte, options.MinBufLen), - options: options, + r: r, + buf: make([]byte, config.MinBufLen), + config: config, }, nil } @@ -78,8 +79,8 @@ func (r *ChunkReader) appendAtLeast(fillLen int) error { } func (r *ChunkReader) newBuf(size int) []byte { - if size < r.options.MinBufLen { - size = r.options.MinBufLen + if size < r.config.MinBufLen { + size = r.config.MinBufLen } return make([]byte, size) } diff --git a/chunkreader_test.go b/chunkreader_test.go index 3be07e3c..66515e87 100644 --- a/chunkreader_test.go +++ b/chunkreader_test.go @@ -7,7 +7,7 @@ import ( func TestChunkReaderNextDoesNotReadIfAlreadyBuffered(t *testing.T) { server := &bytes.Buffer{} - r, err := NewChunkReaderEx(server, Options{MinBufLen: 4}) + r, err := NewChunkReaderEx(server, Config{MinBufLen: 4}) if err != nil { t.Fatal(err) } @@ -44,7 +44,7 @@ func TestChunkReaderNextDoesNotReadIfAlreadyBuffered(t *testing.T) { func TestChunkReaderNextExpandsBufAsNeeded(t *testing.T) { server := &bytes.Buffer{} - r, err := NewChunkReaderEx(server, Options{MinBufLen: 4}) + r, err := NewChunkReaderEx(server, Config{MinBufLen: 4}) if err != nil { t.Fatal(err) } @@ -66,7 +66,7 @@ func TestChunkReaderNextExpandsBufAsNeeded(t *testing.T) { func TestChunkReaderDoesNotReuseBuf(t *testing.T) { server := &bytes.Buffer{} - r, err := NewChunkReaderEx(server, Options{MinBufLen: 4}) + r, err := NewChunkReaderEx(server, Config{MinBufLen: 4}) if err != nil { t.Fatal(err) } From 4e6b8011b67f7ee75e0cd8710295835b6a0c0abd Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Sat, 8 Jun 2019 18:10:49 -0500 Subject: [PATCH 08/14] Shorten constructor function names --- chunkreader.go | 8 +++++--- chunkreader_test.go | 6 +++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/chunkreader.go b/chunkreader.go index 43363e42..b47747f2 100644 --- a/chunkreader.go +++ b/chunkreader.go @@ -20,8 +20,9 @@ type Config struct { MinBufLen int // Minimum buffer length } -func NewChunkReader(r io.Reader) *ChunkReader { - cr, err := NewChunkReaderEx(r, Config{}) +// New creates and returns a new ChunkReader for r with default configuration. +func New(r io.Reader) *ChunkReader { + cr, err := NewConfig(r, Config{}) if err != nil { panic("default config can't be bad") } @@ -29,7 +30,8 @@ func NewChunkReader(r io.Reader) *ChunkReader { return cr } -func NewChunkReaderEx(r io.Reader, config Config) (*ChunkReader, error) { +// NewConfig creates and a new ChunkReader for r configured by config. +func NewConfig(r io.Reader, config Config) (*ChunkReader, error) { if config.MinBufLen == 0 { config.MinBufLen = 4096 } diff --git a/chunkreader_test.go b/chunkreader_test.go index 66515e87..67a20af2 100644 --- a/chunkreader_test.go +++ b/chunkreader_test.go @@ -7,7 +7,7 @@ import ( func TestChunkReaderNextDoesNotReadIfAlreadyBuffered(t *testing.T) { server := &bytes.Buffer{} - r, err := NewChunkReaderEx(server, Config{MinBufLen: 4}) + r, err := NewConfig(server, Config{MinBufLen: 4}) if err != nil { t.Fatal(err) } @@ -44,7 +44,7 @@ func TestChunkReaderNextDoesNotReadIfAlreadyBuffered(t *testing.T) { func TestChunkReaderNextExpandsBufAsNeeded(t *testing.T) { server := &bytes.Buffer{} - r, err := NewChunkReaderEx(server, Config{MinBufLen: 4}) + r, err := NewConfig(server, Config{MinBufLen: 4}) if err != nil { t.Fatal(err) } @@ -66,7 +66,7 @@ func TestChunkReaderNextExpandsBufAsNeeded(t *testing.T) { func TestChunkReaderDoesNotReuseBuf(t *testing.T) { server := &bytes.Buffer{} - r, err := NewChunkReaderEx(server, Config{MinBufLen: 4}) + r, err := NewConfig(server, Config{MinBufLen: 4}) if err != nil { t.Fatal(err) } From 21088f2cb5965119897433279957e2ad2b301ddd Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Sat, 8 Jun 2019 18:29:13 -0500 Subject: [PATCH 09/14] Improve documentation --- README.md | 2 +- chunkreader.go | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index bcc9ac6b..01209bfa 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,6 @@ # chunkreader -Package chunkreader provides an opinionated, efficient buffered reader. +Package chunkreader provides an io.Reader wrapper that minimizes IO reads and memory allocations. Extracted from original implementation in https://github.com/jackc/pgx. diff --git a/chunkreader.go b/chunkreader.go index b47747f2..36304fd5 100644 --- a/chunkreader.go +++ b/chunkreader.go @@ -1,11 +1,17 @@ -// Package chunkreader provides an opinionated, efficient buffered reader. +// Package chunkreader provides an io.Reader wrapper that minimizes IO reads and memory allocations. package chunkreader import ( "io" ) -// ChunkReader is a io.Reader wrapper that minimizes reads and memory allocations. +// ChunkReader is a io.Reader wrapper that minimizes IO reads and memory allocations. It allocates memory in chunks and +// will read as much as will fit in the current buffer in a single call regardless of how large a read is actually +// requested. The memory returned via Next is owned by the caller. This avoids the need for an additional copy. +// +// The downside of this approach is that a large buffer can be pinned in memory even if only a small slice is +// referenced. For example, an entire 4096 byte block could be pinned in memory by even a 1 byte slice. In these rare +// cases it would be advantageous to copy the bytes to another slice. type ChunkReader struct { r io.Reader @@ -43,8 +49,8 @@ func NewConfig(r io.Reader, config Config) (*ChunkReader, error) { }, nil } -// Next returns buf filled with the next n bytes. If an error occurs, buf will -// be nil. +// Next returns buf filled with the next n bytes. The caller gains ownership of buf. It is not necessary to make a copy +// of buf. If an error occurs, buf will be nil. func (r *ChunkReader) Next(n int) (buf []byte, err error) { // n bytes already in buf if (r.wp - r.rp) >= n { From 2c463c0e7d0d0876517f087ce2cce66a46182141 Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Sat, 8 Jun 2019 18:32:30 -0500 Subject: [PATCH 10/14] Release v2 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index b1ed8c92..a1384b40 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module github.com/jackc/chunkreader +module github.com/jackc/chunkreader/v2 go 1.12 From f76af93c210584fb9c059bff875060e720c6004d Mon Sep 17 00:00:00 2001 From: Artemiy Ryabinkov Date: Thu, 8 Aug 2019 13:41:51 +0300 Subject: [PATCH 11/14] Increase buffer size to 8KB Signed-off-by: Artemiy Ryabinkov --- chunkreader.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chunkreader.go b/chunkreader.go index 36304fd5..c45b75aa 100644 --- a/chunkreader.go +++ b/chunkreader.go @@ -39,7 +39,7 @@ func New(r io.Reader) *ChunkReader { // NewConfig creates and a new ChunkReader for r configured by config. func NewConfig(r io.Reader, config Config) (*ChunkReader, error) { if config.MinBufLen == 0 { - config.MinBufLen = 4096 + config.MinBufLen = 8192 } return &ChunkReader{ From e204afcc8c18b630476abe8e28032fe2b5762825 Mon Sep 17 00:00:00 2001 From: Artemiy Ryabinkov Date: Thu, 8 Aug 2019 13:43:26 +0300 Subject: [PATCH 12/14] Add explanation for default buffer size Signed-off-by: Artemiy Ryabinkov --- chunkreader.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/chunkreader.go b/chunkreader.go index c45b75aa..afea1c52 100644 --- a/chunkreader.go +++ b/chunkreader.go @@ -39,6 +39,10 @@ func New(r io.Reader) *ChunkReader { // NewConfig creates and a new ChunkReader for r configured by config. func NewConfig(r io.Reader, config Config) (*ChunkReader, error) { if config.MinBufLen == 0 { + // By historical reasons Postgres currently has 8KB send buffer inside, + // so here we want to have at least the same size buffer. + // @see https://github.com/postgres/postgres/blob/249d64999615802752940e017ee5166e726bc7cd/src/backend/libpq/pqcomm.c#L134 + // @see https://www.postgresql.org/message-id/0cdc5485-cb3c-5e16-4a46-e3b2f7a41322%40ya.ru config.MinBufLen = 8192 } From f5806bc01c49ab9ed8b239419be48df54a08eaa0 Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Mon, 24 Jan 2022 08:10:01 -0600 Subject: [PATCH 13/14] Add a fuzz test Investigating https://github.com/jackc/pgx/issues/938. --- chunkreader_test.go | 32 ++++++++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) diff --git a/chunkreader_test.go b/chunkreader_test.go index 67a20af2..ddc2fbf6 100644 --- a/chunkreader_test.go +++ b/chunkreader_test.go @@ -2,6 +2,7 @@ package chunkreader import ( "bytes" + "math/rand" "testing" ) @@ -94,3 +95,34 @@ func TestChunkReaderDoesNotReuseBuf(t *testing.T) { t.Fatalf("Expected KeepLast to prevent Next from overwriting buf, expected %v but it was %v", src[0:4], n1) } } + +type randomReader struct { + rnd *rand.Rand +} + +// Read reads a random number of random bytes. +func (r *randomReader) Read(p []byte) (n int, err error) { + n = r.rnd.Intn(len(p) + 1) + return r.rnd.Read(p[:n]) +} + +func TestChunkReaderNextFuzz(t *testing.T) { + rr := &randomReader{rnd: rand.New(rand.NewSource(1))} + r, err := NewConfig(rr, Config{MinBufLen: 8192}) + if err != nil { + t.Fatal(err) + } + + randomSizes := rand.New(rand.NewSource(0)) + + for i := 0; i < 100000; i++ { + size := randomSizes.Intn(16384) + 1 + buf, err := r.Next(size) + if err != nil { + t.Fatal(err) + } + if len(buf) != size { + t.Fatalf("Expected to get %v bytes but got %v bytes", size, len(buf)) + } + } +} From fd1a98f85875ad45f74a7dd0a3c646bec0323437 Mon Sep 17 00:00:00 2001 From: Jack Christensen Date: Mon, 21 Feb 2022 14:27:05 -0600 Subject: [PATCH 14/14] Move and clean for import --- .travis.yml | 9 -------- LICENSE | 22 ------------------- README.md | 8 ------- chunkreader.go => chunkreader/chunkreader.go | 0 .../chunkreader_test.go | 0 go.mod | 3 --- 6 files changed, 42 deletions(-) delete mode 100644 .travis.yml delete mode 100644 LICENSE delete mode 100644 README.md rename chunkreader.go => chunkreader/chunkreader.go (100%) rename chunkreader_test.go => chunkreader/chunkreader_test.go (100%) delete mode 100644 go.mod diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index e176228e..00000000 --- a/.travis.yml +++ /dev/null @@ -1,9 +0,0 @@ -language: go - -go: - - 1.x - - tip - -matrix: - allow_failures: - - go: tip diff --git a/LICENSE b/LICENSE deleted file mode 100644 index c1c4f50f..00000000 --- a/LICENSE +++ /dev/null @@ -1,22 +0,0 @@ -Copyright (c) 2019 Jack Christensen - -MIT License - -Permission is hereby granted, free of charge, to any person obtaining -a copy of this software and associated documentation files (the -"Software"), to deal in the Software without restriction, including -without limitation the rights to use, copy, modify, merge, publish, -distribute, sublicense, and/or sell copies of the Software, and to -permit persons to whom the Software is furnished to do so, subject to -the following conditions: - -The above copyright notice and this permission notice shall be -included in all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND -NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE -LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION -OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION -WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/README.md b/README.md deleted file mode 100644 index 01209bfa..00000000 --- a/README.md +++ /dev/null @@ -1,8 +0,0 @@ -[![](https://godoc.org/github.com/jackc/chunkreader?status.svg)](https://godoc.org/github.com/jackc/chunkreader) -[![Build Status](https://travis-ci.org/jackc/chunkreader.svg)](https://travis-ci.org/jackc/chunkreader) - -# chunkreader - -Package chunkreader provides an io.Reader wrapper that minimizes IO reads and memory allocations. - -Extracted from original implementation in https://github.com/jackc/pgx. diff --git a/chunkreader.go b/chunkreader/chunkreader.go similarity index 100% rename from chunkreader.go rename to chunkreader/chunkreader.go diff --git a/chunkreader_test.go b/chunkreader/chunkreader_test.go similarity index 100% rename from chunkreader_test.go rename to chunkreader/chunkreader_test.go diff --git a/go.mod b/go.mod deleted file mode 100644 index a1384b40..00000000 --- a/go.mod +++ /dev/null @@ -1,3 +0,0 @@ -module github.com/jackc/chunkreader/v2 - -go 1.12