Description
Proposal Details
The Framer has an unexported field that holds a closure responsible for allocating byte slices for reading frame payloads. The existing implementation keeps re-using the same slice, requiring users to copy the contents of data frames before reading the next frame. gRPC Go has a buffer pool which is used to recycle byte slices used during different parts of an RPC. The existing sequence of events when reading a data frame is as follows:
- The framer reads to its own buffer.
- gRPC copies framer's buffer to a new slice from its buffer pool, incurring a copy.
- The buffer from the pool is passed around without needing any further copies.
- The buffer is returned to the pool after it’s contents are processed.
I propose the addition of a method to the Framer to allow setting a custom allocator.
// SetBufferAllocator sets a custom buffer allocator that will be called for
// getting a buffer slice for each frame payload. The allocator must return a
// slice of the requested length. For data frames, calling DataFrame.Data() will
// always return a prefix of the slice returned by the allocator.
func (fr *Framer) SetBufferAllocator(allocator func(uint32) []byte) {
fr.allocator = allocator
}
For gRPC, the allocator will return a buffer from the shared buffer pool every time. To make it easy for callers to re-use the entire slice, I also propose changing the data framer parsing code to read the optional padding length byte to a different buffer than the one from the allocator. This ensures calling DataFrame.Data() returns a prefix of the allocator’s buffer, without reducing its capacity.
With the new framer API, a sample implementation of gRPC’s allocator function will look as follows:
func (b *bufferAllocator) get(size uint32) []byte {
// Try to re-use a buffer if possible.
if b.curBuf != nil && cap(*b.curBuf) >= int(size) {
return (*b.curBuf)[:int(size)]
}
// Can't re-use, recycle the buffer.
if b.curBuf != nil {
b.bufferPool.Put(b.curBuf)
}
// Get a new buffer.
b.curBuf = b.bufferPool.Get(int(size))
return *b.curBuf
}
The new sequence of events when reading a data frame will be:
- Framer requests the allocator for a buffer. The allocator gets a buffer from the pool.
- The framer reads to the allocator's buffer.
- gRPC takes ownership of the allocator's buffer by saving the buffer and setting the allocator’s buffer to nil, avoiding a copy.
Benchmarks
When running a Google Cloud Storage benchmark that runs on a Compute Engine VM and downloads 10 MB objects in a loop until it’s downloaded a fixed amount of data, we’re seeing a ~4% reduction in CPU time.