Skip to content

Conversation

@kainosnoema
Copy link
Contributor

We've seen some high memory and GC pressure in our production deployment. One culprit seems to be func (*Request) execSelections, which can create a lot of buffers that have to grow multiple times in large responses.

One important optimization we can make here is buffer pooling. This PR implements sync.Pool for bytes.Buffer and map[string]*fieldToExec to reduce allocations, memory growth, and GC pressure during execution.

Changes:

  • Add internal/exec/pool.go with buffer and field map pooling
  • Update Execute(), execSelections(), execList() to use pools
  • Apply pooling to subscription handling
  • Tests and benchmarks

Isolated Benchmarks

Summary:

  • ListQuery: 111µs → 78µs (29% faster), 124KB → 43KB (65% less memory)
  • ListWithNestedLists: 428µs → 346µs (19% faster), 637KB → 177KB (72% less memory)
  • Concurrent: 219µs → 95µs (56% faster), 659KB → 189KB (71% less memory)
# Without Pooling
BenchmarkMemory_SimpleQuery-12                    115488             10336 ns/op            9925 B/op        126 allocs/op
BenchmarkMemory_ListQuery-12                       10000            111123 ns/op          124602 B/op       2037 allocs/op
BenchmarkMemory_NestedQuery-12                     17557             61133 ns/op           80886 B/op       1074 allocs/op
BenchmarkMemory_ListWithNestedLists-12              2841            428631 ns/op          637248 B/op       9660 allocs/op
BenchmarkMemory_Concurrent-12                       5092            219808 ns/op          659525 B/op      10124 allocs/op
BenchmarkMemory_AllocationsPerOp/Single-12                123319              9433 ns/op            8691 B/op        106 allocs/op
BenchmarkMemory_AllocationsPerOp/List_10-12                23680             42620 ns/op           80833 B/op       1016 allocs/op
BenchmarkMemory_AllocationsPerOp/Nested_Depth3-12          50025             24150 ns/op           37659 B/op        444 allocs/op

# With Pooling
BenchmarkMemory_SimpleQuery-12                    131162              9250 ns/op            7423 B/op        110 allocs/op
BenchmarkMemory_ListQuery-12                       16544             78654 ns/op           43204 B/op       1474 allocs/op
BenchmarkMemory_NestedQuery-12                     23664             59657 ns/op           36693 B/op        810 allocs/op
BenchmarkMemory_ListWithNestedLists-12              3644            346407 ns/op          177613 B/op       6696 allocs/op
BenchmarkMemory_Concurrent-12                      13980             95817 ns/op          189372 B/op       7055 allocs/op
BenchmarkMemory_AllocationsPerOp/Single-12                119922              9149 ns/op            6429 B/op         93 allocs/op
BenchmarkMemory_AllocationsPerOp/List_10-12                39200             30693 ns/op           19197 B/op        655 allocs/op
BenchmarkMemory_AllocationsPerOp/Nested_Depth3-12          58494             20190 ns/op           12544 B/op        309 allocs/op

Real-world Benchmarks (private to our app, includes db i/o)

# Without Pooling
BenchmarkMeQuery-12         	    4756	    708655 ns/op	  104785 B/op	    1377 allocs/op
BenchmarkThreadsQuery-12    	     252	  14639531 ns/op	 5990799 B/op	  101922 allocs/op

# With Pooling
BenchmarkMeQuery-12         	    4786	    693537 ns/op	   91532 B/op	    1351 allocs/op
BenchmarkThreadsQuery-12    	     312	  10358961 ns/op	 5519296 B/op	   98256 allocs/op

kainosnoema and others added 3 commits October 24, 2025 18:17
Implements sync.Pool for bytes.Buffer and map[string]*fieldToExec to
reduce allocations and memory growth during GraphQL execution.

Key improvements:
- Buffer pool: 53-87% faster, 50-100% fewer allocations
- Field map pool: 53% faster, 83% less memory per operation
- GC pressure significantly reduced

Changes:
- Add internal/exec/pool.go with buffer and field map pooling
- Update Execute(), execSelections(), execList() to use pools
- Apply pooling to subscription handling
- Add comprehensive tests and benchmarks

Signed-off-by: Evan Owen <[email protected]>
Co-authored-by: Amp <[email protected]>
Comment on lines +9 to +11
maxBufferCap = 64 * 1024
maxFieldMapSize = 128
newFieldMapSize = 16
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I wonder if these should be configurable through schema options, in case some projects have special requirements...

Comment on lines +56 to +58
for k := range m {
delete(m, k)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We replace this by clear maybe?

Suggested change
for k := range m {
delete(m, k)
}
clear(m)

m[string(rune(i))] = &fieldToExec{}
}

putFieldMap(m) // Should not be added to pool
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldn't you check this instead of only putting a comment?

l := resolver.Len()
entryouts := make([]bytes.Buffer, l)
entryouts := make([]*bytes.Buffer, l)
for i := 0; i < l; i++ {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
for i := 0; i < l; i++ {
for i := range l {

Comment on lines +26 to +31
func putBuffer(buf *bytes.Buffer) {
if buf.Cap() > maxBufferCap {
return
}
bufferPool.Put(buf)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func putBuffer(buf *bytes.Buffer) {
if buf.Cap() > maxBufferCap {
return
}
bufferPool.Put(buf)
}
func putBuffer(buf *bytes.Buffer) {
if buf == nil || buf.Cap() > maxBufferCap {
return
}
bufferPool.Put(buf)
}

Comment on lines +52 to +55
func putFieldMap(m map[string]*fieldToExec) {
if len(m) > maxFieldMapSize {
return
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func putFieldMap(m map[string]*fieldToExec) {
if len(m) > maxFieldMapSize {
return
}
func putFieldMap(m map[string]*fieldToExec) {
if m == nil || len(m) > maxFieldMapSize {
return
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants