-
Notifications
You must be signed in to change notification settings - Fork 4.7k
transport: Remove buffer copies while writing HTTP/2 Data frames #8667
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 7 commits
71f988e
168d2e6
fc7097f
ca29c67
b625fa2
651b46c
39ed5a5
bfee28b
aba826c
ae9f421
ffbd989
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 |
|---|---|---|
|
|
@@ -389,8 +389,9 @@ func toIOError(err error) error { | |
| } | ||
|
|
||
| type framer struct { | ||
| writer *bufWriter | ||
| fr *http2.Framer | ||
| writer *bufWriter | ||
| fr *http2.Framer | ||
| headerBuf []byte // cached slice for framer headers to reduce heap allocs. | ||
|
easwars marked this conversation as resolved.
|
||
| } | ||
|
|
||
| var writeBufferPoolMap = make(map[int]*sync.Pool) | ||
|
|
@@ -422,6 +423,44 @@ func newFramer(conn net.Conn, writeBufferSize, readBufferSize int, sharedWriteBu | |
| return f | ||
| } | ||
|
|
||
| // writeData writes a DATA frame. | ||
| // | ||
| // It is the caller's responsibility not to violate the maximum frame size. | ||
| func (f *framer) writeData(streamID uint32, endStream bool, data [][]byte) error { | ||
| var flags http2.Flags | ||
| if endStream { | ||
| flags = http2.FlagDataEndStream | ||
| } | ||
| length := uint32(0) | ||
| for _, d := range data { | ||
| length += uint32(len(d)) | ||
| } | ||
| // TODO: Replace the header write with the framer API being added in | ||
| // https://github.com/golang/go/issues/66655. | ||
| f.headerBuf = append(f.headerBuf[:0], | ||
| byte(length>>16), | ||
| byte(length>>8), | ||
| byte(length), | ||
| byte(http2.FrameData), | ||
| byte(flags), | ||
| byte(streamID>>24), | ||
| byte(streamID>>16), | ||
| byte(streamID>>8), | ||
| byte(streamID)) | ||
|
easwars marked this conversation as resolved.
|
||
| if _, err := f.writer.Write(f.headerBuf); err != nil { | ||
| return err | ||
| } | ||
| for _, d := range data { | ||
| if len(d) == 0 { | ||
| continue | ||
| } | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Would this be a bug if it were zero? I would have expected it to be. If it is, then we should delete it.
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Removed. There should not be any empty buffers in the list, since |
||
| if _, err := f.writer.Write(d); err != nil { | ||
| return err | ||
| } | ||
| } | ||
| return nil | ||
| } | ||
|
|
||
| func getWriteBufferPool(size int) *sync.Pool { | ||
| writeBufferMutex.Lock() | ||
| defer writeBufferMutex.Unlock() | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.
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.
It seems like this buffer can only grow and never shrinks.
cap(l.writeBuf)grows to a large value and then we never need it to be that large ever again?I think we need to have some way to scale this buffer back down.
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.
For point 1, I've updated the code to clear the buffer after calling
Write. This releases references to all the slices and allows them to be GCed.With respect to point 2, I've now set a limit of 64 on the buffer's length. If a buffer is longer than that, it's immediately freed after use instead of being cached.
Background on the 64-element limit: The
BufferSlicefrom the proto codec is 1 element. With a potential gRPC header, the length is almost always 2. While custom codecs might produce larger slices, 64 is a generous limit that covers common cases without caching excessive memory.This change also mitigates a worst-case memory scenario. Since
Peek()filters empty slices, a 16KB http2 Data frame (the max size) could theoretically be split into 16K (16,384) distinct 1-byte slices. In that case, the memory overhead for the slice headers alone would be24 bytes * 16 * 1024(approx. 393KB), with the 64 size limit, the max held memory is approx 1.5KB. Also note that the framer already has a data buffer that grows up to 16KB, and after this change, that buffer should no longer be used for Data frames.