Skip to content

Commit 22c71ee

Browse files
znullCopilot
andcommitted
Add test for pool buffer bypass via *os.File WriterTo
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. This test detects the bypass by counting allocations during ioCopier copy operations with an *os.File source (which is the common case when the last pipeline stage is a commandStage). The test currently FAILS, demonstrating the problem. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
1 parent 010dca5 commit 22c71ee

1 file changed

Lines changed: 75 additions & 0 deletions

File tree

pipe/iocopier_test.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
package pipe
2+
3+
import (
4+
"bytes"
5+
"os"
6+
"runtime"
7+
"testing"
8+
)
9+
10+
// TestIOCopierPoolBufferUsed verifies that ioCopier uses the sync.Pool
11+
// buffer rather than allocating a fresh one. On Go 1.26+, *os.File
12+
// implements WriterTo, which causes io.CopyBuffer to bypass the
13+
// provided pool buffer entirely. Instead, File.WriteTo →
14+
// genericWriteTo → io.Copy allocates a fresh 32KB buffer on every call.
15+
func TestIOCopierPoolBufferUsed(t *testing.T) {
16+
const payload = "hello from pipe\n"
17+
18+
// Pre-warm the pool so Get doesn't allocate.
19+
copyBufPool.Put(copyBufPool.New())
20+
21+
// Warm up: run once to stabilize lazy init.
22+
pr, pw, err := os.Pipe()
23+
if err != nil {
24+
t.Fatal(err)
25+
}
26+
go func() {
27+
pw.Write([]byte(payload))
28+
pw.Close()
29+
}()
30+
var warmBuf bytes.Buffer
31+
c := newIOCopier(nopWriteCloser{&warmBuf})
32+
c.Start(nil, Env{}, pr)
33+
c.Wait()
34+
35+
// Now measure: run the copy and check how many bytes were allocated.
36+
// If the pool buffer is bypassed, a fresh 32KB buffer is allocated.
37+
pr, pw, err = os.Pipe()
38+
if err != nil {
39+
t.Fatal(err)
40+
}
41+
go func() {
42+
pw.Write([]byte(payload))
43+
pw.Close()
44+
}()
45+
var buf bytes.Buffer
46+
c = newIOCopier(nopWriteCloser{&buf})
47+
48+
// GC clears sync.Pool, so re-warm it afterward to isolate the
49+
// measurement from pool repopulation overhead.
50+
runtime.GC()
51+
copyBufPool.Put(copyBufPool.New())
52+
53+
var m1, m2 runtime.MemStats
54+
runtime.ReadMemStats(&m1)
55+
56+
c.Start(nil, Env{}, pr)
57+
c.Wait()
58+
59+
runtime.GC()
60+
runtime.ReadMemStats(&m2)
61+
62+
if buf.String() != payload {
63+
t.Fatalf("unexpected output: %q", buf.String())
64+
}
65+
66+
allocBytes := m2.TotalAlloc - m1.TotalAlloc
67+
// A bypassed pool buffer causes ~32KB of allocation.
68+
// With the pool buffer working, we expect well under 32KB.
69+
const maxBytes = 16 * 1024
70+
if allocBytes > maxBytes {
71+
t.Errorf("ioCopier allocated %d bytes during copy (max %d); "+
72+
"pool buffer may be bypassed by *os.File WriterTo",
73+
allocBytes, maxBytes)
74+
}
75+
}

0 commit comments

Comments
 (0)