-
Notifications
You must be signed in to change notification settings - Fork 491
Add buffer pooling for reduced allocs, GC pressure, and improved performance #691
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
base: main
Are you sure you want to change the base?
Changes from 3 commits
7e06267
45cdc80
ec053b1
3ba6e6b
70da480
512479d
776ce5e
1d51c41
a2fae60
e3da7f9
c651c21
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,60 @@ | ||
| package exec | ||
|
|
||
| import ( | ||
| "bytes" | ||
| "sync" | ||
| ) | ||
|
|
||
| const ( | ||
| maxBufferCap = 64 * 1024 | ||
| maxFieldMapSize = 128 | ||
| newFieldMapSize = 16 | ||
|
||
| ) | ||
|
|
||
| var bufferPool = sync.Pool{ | ||
| New: func() interface{} { | ||
| return new(bytes.Buffer) | ||
| }, | ||
| } | ||
|
|
||
| func getBuffer() *bytes.Buffer { | ||
| buf := bufferPool.Get().(*bytes.Buffer) | ||
| buf.Reset() | ||
| return buf | ||
| } | ||
|
|
||
| func putBuffer(buf *bytes.Buffer) { | ||
| if buf.Cap() > maxBufferCap { | ||
| return | ||
| } | ||
| bufferPool.Put(buf) | ||
| } | ||
kainosnoema marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| func copyBuffer(buf *bytes.Buffer) []byte { | ||
| if buf.Len() == 0 { | ||
| return nil | ||
| } | ||
| result := make([]byte, buf.Len()) | ||
| copy(result, buf.Bytes()) | ||
| return result | ||
| } | ||
|
|
||
| var fieldMapPool = sync.Pool{ | ||
| New: func() interface{} { | ||
| return make(map[string]*fieldToExec, newFieldMapSize) | ||
| }, | ||
| } | ||
|
|
||
| func getFieldMap() map[string]*fieldToExec { | ||
| return fieldMapPool.Get().(map[string]*fieldToExec) | ||
| } | ||
|
|
||
| func putFieldMap(m map[string]*fieldToExec) { | ||
| if len(m) > maxFieldMapSize { | ||
| return | ||
| } | ||
kainosnoema marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| for k := range m { | ||
| delete(m, k) | ||
| } | ||
kainosnoema marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| fieldMapPool.Put(m) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,93 @@ | ||
| package exec | ||
|
|
||
| import ( | ||
| "testing" | ||
| ) | ||
|
|
||
| func TestBufferPool(t *testing.T) { | ||
| t.Run("resets buffer before returning", func(t *testing.T) { | ||
| buf := getBuffer() | ||
| buf.WriteString("test data") | ||
| putBuffer(buf) | ||
|
|
||
| buf2 := getBuffer() | ||
| if buf2.Len() != 0 { | ||
| t.Errorf("expected reset buffer, got length %d", buf2.Len()) | ||
| } | ||
| putBuffer(buf2) | ||
| }) | ||
|
|
||
| t.Run("copyBuffer copies data correctly", func(t *testing.T) { | ||
| buf := getBuffer() | ||
| buf.WriteString("test data") | ||
|
|
||
| copied := copyBuffer(buf) | ||
| if string(copied) != "test data" { | ||
| t.Errorf("expected 'test data', got %q", string(copied)) | ||
| } | ||
|
|
||
| // Original buffer should be unchanged | ||
| if buf.String() != "test data" { | ||
| t.Errorf("original buffer modified") | ||
| } | ||
|
|
||
| putBuffer(buf) | ||
| }) | ||
|
|
||
| t.Run("copyBuffer returns nil for empty buffer", func(t *testing.T) { | ||
| buf := getBuffer() | ||
| copied := copyBuffer(buf) | ||
| if copied != nil { | ||
| t.Errorf("expected nil for empty buffer, got %v", copied) | ||
| } | ||
| putBuffer(buf) | ||
| }) | ||
|
|
||
| t.Run("does not pool oversized buffers", func(t *testing.T) { | ||
| buf := getBuffer() | ||
| large := make([]byte, 65*1024) | ||
| buf.Write(large) | ||
|
|
||
| if buf.Cap() <= maxBufferCap { | ||
| t.Skip("buffer didn't grow large enough for test") | ||
| } | ||
|
|
||
| putBuffer(buf) // Should not be added to pool | ||
|
|
||
| buf2 := getBuffer() | ||
| if buf2.Cap() > maxBufferCap { | ||
| t.Errorf("got oversized buffer from pool, capacity: %d", buf2.Cap()) | ||
| } | ||
| putBuffer(buf2) | ||
| }) | ||
| } | ||
|
|
||
| func TestFieldMapPool(t *testing.T) { | ||
| t.Run("clears map before returning", func(t *testing.T) { | ||
| m := getFieldMap() | ||
| m["test"] = &fieldToExec{} | ||
| m["foo"] = &fieldToExec{} | ||
| putFieldMap(m) | ||
|
|
||
| m2 := getFieldMap() | ||
| if len(m2) != 0 { | ||
| t.Errorf("expected cleared map, got length %d", len(m2)) | ||
| } | ||
| putFieldMap(m2) | ||
| }) | ||
|
|
||
| t.Run("does not pool oversized maps", func(t *testing.T) { | ||
| m := getFieldMap() | ||
| for i := 0; i < 129; i++ { | ||
| m[string(rune(i))] = &fieldToExec{} | ||
| } | ||
|
|
||
| putFieldMap(m) // Should not be added to pool | ||
kainosnoema marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| m2 := getFieldMap() | ||
| if len(m2) != 0 { | ||
| t.Errorf("got non-empty map from pool, length: %d", len(m2)) | ||
| } | ||
| putFieldMap(m2) | ||
| }) | ||
| } | ||
Uh oh!
There was an error while loading. Please reload this page.