-
Notifications
You must be signed in to change notification settings - Fork 7
Enable sendfile for network destinations; fix pool bypass on Go 1.26+ #50
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 4 commits
22c71ee
c5839cb
afe1947
6e7976a
60bd6c3
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,121 @@ | ||
| package pipe | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "io" | ||
| "os" | ||
| "runtime" | ||
| "sync/atomic" | ||
| "testing" | ||
| ) | ||
|
|
||
| // TestIOCopierPoolBufferUsed verifies that ioCopier uses the sync.Pool | ||
| // buffer rather than allocating a fresh one. On Go 1.26+, *os.File | ||
| // implements WriterTo, which causes io.CopyBuffer to bypass the | ||
| // provided pool buffer entirely. Instead, File.WriteTo → | ||
| // genericWriteTo → io.Copy allocates a fresh 32KB buffer on every call. | ||
| func TestIOCopierPoolBufferUsed(t *testing.T) { | ||
| const payload = "hello from pipe\n" | ||
|
|
||
| // Pre-warm the pool so Get doesn't allocate. | ||
| copyBufPool.Put(copyBufPool.New()) | ||
|
|
||
| // Warm up: run once to stabilize lazy init. | ||
| pr, pw, err := os.Pipe() | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| go func() { | ||
| pw.Write([]byte(payload)) | ||
| pw.Close() | ||
| }() | ||
| var warmBuf bytes.Buffer | ||
| c := newIOCopier(nopWriteCloser{&warmBuf}) | ||
| c.Start(nil, Env{}, pr) | ||
| c.Wait() | ||
|
||
|
|
||
| // Now measure: run the copy and check how many bytes were allocated. | ||
| // If the pool buffer is bypassed, a fresh 32KB buffer is allocated. | ||
| pr, pw, err = os.Pipe() | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| go func() { | ||
| pw.Write([]byte(payload)) | ||
| pw.Close() | ||
| }() | ||
| var buf bytes.Buffer | ||
| c = newIOCopier(nopWriteCloser{&buf}) | ||
|
|
||
| // GC clears sync.Pool, so re-warm it afterward to isolate the | ||
| // measurement from pool repopulation overhead. | ||
| runtime.GC() | ||
| copyBufPool.Put(copyBufPool.New()) | ||
|
|
||
| var m1, m2 runtime.MemStats | ||
| runtime.ReadMemStats(&m1) | ||
|
|
||
| c.Start(nil, Env{}, pr) | ||
| c.Wait() | ||
|
|
||
| runtime.GC() | ||
| runtime.ReadMemStats(&m2) | ||
|
|
||
| if buf.String() != payload { | ||
| t.Fatalf("unexpected output: %q", buf.String()) | ||
| } | ||
|
|
||
| allocBytes := m2.TotalAlloc - m1.TotalAlloc | ||
| // A bypassed pool buffer causes ~32KB of allocation. | ||
| // With the pool buffer working, we expect well under 32KB. | ||
| const maxBytes = 16 * 1024 | ||
| if allocBytes > maxBytes { | ||
| t.Errorf("ioCopier allocated %d bytes during copy (max %d); "+ | ||
| "pool buffer may be bypassed by *os.File WriterTo", | ||
| allocBytes, maxBytes) | ||
| } | ||
| } | ||
|
|
||
| // readFromWriter is a test writer that implements io.ReaderFrom and | ||
| // records whether ReadFrom was called. | ||
| type readFromWriter struct { | ||
| bytes.Buffer | ||
| readFromCalled atomic.Bool | ||
| } | ||
|
|
||
| func (w *readFromWriter) ReadFrom(r io.Reader) (int64, error) { | ||
| w.readFromCalled.Store(true) | ||
| return w.Buffer.ReadFrom(r) | ||
| } | ||
|
|
||
| func (w *readFromWriter) Close() error { return nil } | ||
|
|
||
| // TestIOCopierUsesReadFrom verifies that ioCopier dispatches to | ||
| // ReaderFrom when the destination writer supports it, even when | ||
| // wrapped in nopWriteCloser (as happens with WithStdout). | ||
| func TestIOCopierUsesReadFrom(t *testing.T) { | ||
| const payload = "hello readfrom\n" | ||
|
|
||
| pr, pw, err := os.Pipe() | ||
| if err != nil { | ||
| t.Fatal(err) | ||
| } | ||
| go func() { | ||
| pw.Write([]byte(payload)) | ||
| pw.Close() | ||
| }() | ||
|
|
||
| w := &readFromWriter{} | ||
| c := newIOCopier(nopWriteCloser{w}) | ||
| c.Start(nil, Env{}, pr) | ||
| c.Wait() | ||
|
|
||
| if w.Buffer.String() != payload { | ||
| t.Fatalf("unexpected output: %q", w.Buffer.String()) | ||
| } | ||
|
|
||
| if !w.readFromCalled.Load() { | ||
| t.Error("ioCopier did not call ReadFrom on destination; " + | ||
| "nopWriteCloser may be hiding the ReaderFrom interface") | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This test is meant to guard against the Go 1.26+ *os.File WriterTo path, but the module targets Go 1.24 (go.mod) where *os.File likely doesn't implement io.WriterTo, so the failure mode may not be exercised in CI. Consider making the test version-independent (e.g., use a custom reader that implements io.WriterTo and would allocate/counter-increment if WriteTo is invoked, or explicitly skip unless the pipe reader implements io.WriterTo).