@@ -30,11 +30,14 @@ import (
3030 "sync"
3131 "sync/atomic"
3232 "time"
33+ "unsafe"
3334
3435 "golang.org/x/net/http2"
3536 "google.golang.org/grpc/codes"
3637 "google.golang.org/grpc/credentials"
3738 "google.golang.org/grpc/internal/channelz"
39+ "google.golang.org/grpc/internal/envconfig"
40+ imem "google.golang.org/grpc/internal/mem"
3841 "google.golang.org/grpc/internal/transport/internal"
3942 "google.golang.org/grpc/keepalive"
4043 "google.golang.org/grpc/mem"
@@ -45,7 +48,30 @@ import (
4548 "google.golang.org/grpc/tap"
4649)
4750
48- const logLevel = 2
51+ const (
52+ logLevel = 2
53+ // recvMsgSize estimates the memory overhead of a recvMsg in the backlog.
54+ // It accounts for the recvMsg struct itself and the slice header of the
55+ // underlying buffer's data.
56+ recvMsgSize = int (unsafe .Sizeof (recvMsg {}) + unsafe .Sizeof ([]byte {}))
57+
58+ // utilizationFactor controls when we consider memory utilization acceptable.
59+ // When backlogHeapSize / payloadSize <= utilizationFactor (meaning at least
60+ // 50% of the heap memory is actual payload data), compaction is skipped.
61+ utilizationFactor = 2
62+ )
63+
64+ var (
65+ // compactionThreshold is approx 57KB (on 64-bit systems). It allows
66+ // accumulating up to 1024 1-byte payloads before triggering compaction.
67+ //
68+ // Because individual payloads <= 1024 bytes are allocated on the heap
69+ // outside mem.BufferPool, waiting for at least 1024 bytes to accumulate
70+ // ensures that compaction coalesces those small heap allocations into a
71+ // single large buffer from mem.BufferPool, enabling buffer reuse while
72+ // avoiding frequent copying for small bursts of frames.
73+ compactionThreshold = imem .BufferPoolingThreshold * (recvMsgSize + 1 )
74+ )
4975
5076func init () {
5177 internal .TimeNowFunc = func () int64 { return time .Now ().UnixNano () }
@@ -71,23 +97,31 @@ type recvBuffer struct {
7197 c chan recvMsg
7298 mu sync.Mutex
7399 backlog []recvMsg
74- err error
100+ // uncompactedSuffixLen tracks the number of consecutive data messages at
101+ // the tail of backlog that have not been compacted.
102+ uncompactedSuffixLen int
103+ // uncompactedBytes tracks the total payload bytes across the trailing
104+ // uncompactedSuffixLen messages.
105+ uncompactedBytes int
106+ err error
107+ bufPool mem.BufferPool
75108}
76109
77110// init allows a recvBuffer to be initialized in-place, which is useful
78111// for resetting a buffer or for avoiding a heap allocation when the buffer
79112// is embedded in another struct.
80- func (b * recvBuffer ) init () {
113+ func (b * recvBuffer ) init (pool mem. BufferPool ) {
81114 b .c = make (chan recvMsg , 1 )
115+ b .bufPool = pool
82116}
83117
84118func (b * recvBuffer ) put (r recvMsg ) {
85119 b .mu .Lock ()
120+ defer b .mu .Unlock ()
86121 if b .err != nil {
87122 // drop the buffer on the floor. Since b.err is not nil, any subsequent reads
88123 // will always return an error, making this buffer inaccessible.
89124 r .buffer .Free ()
90- b .mu .Unlock ()
91125 // An error had occurred earlier, don't accept more
92126 // data or errors.
93127 return
@@ -96,20 +130,84 @@ func (b *recvBuffer) put(r recvMsg) {
96130 if len (b .backlog ) == 0 {
97131 select {
98132 case b .c <- r :
99- b .mu .Unlock ()
100133 return
101134 default :
102135 }
103136 }
104137 b .backlog = append (b .backlog , r )
105- b .mu .Unlock ()
138+ b .compactBacklogLocked (r )
139+ }
140+
141+ func (b * recvBuffer ) compactBacklogLocked (r recvMsg ) {
142+ if ! envconfig .EnableReceiveBufferCompaction {
143+ return
144+ }
145+ if r .buffer == nil {
146+ b .uncompactedBytes = 0
147+ b .uncompactedSuffixLen = 0
148+ return
149+ }
150+
151+ b .uncompactedSuffixLen ++
152+ b .uncompactedBytes += r .buffer .Len ()
153+ backlogHeapSize := b .uncompactedSuffixLen * recvMsgSize + b .uncompactedBytes
154+
155+ // If the memory overhead is less than 50% of the heap usage (e.g., because
156+ // a large DATA frame arrived), the average message size in the suffix is
157+ // large enough that memory bloat is not a concern. Reset suffix tracking.
158+ if backlogHeapSize <= utilizationFactor * b .uncompactedBytes {
159+ b .uncompactedBytes = 0
160+ b .uncompactedSuffixLen = 0
161+ return
162+ }
163+ // Avoid compacting too frequently for short bursts of small frames.
164+ // Wait until we have accumulated at least ~1024 small messages (~57 KB).
165+ if backlogHeapSize <= compactionThreshold {
166+ // Still can accumulate more payloads.
167+ return
168+ }
169+
170+ // Since the memory utilization is less than 50%, the average payload size
171+ // of each recvMsg must be less than recvMsgSize (approx 56 bytes).
172+ // In the worst case for bytes copied (where the average payload is just
173+ // below recvMsgSize), compaction will occur once every:
174+ // compactionThreshold / (recvMsgSize + avg_payload) = ~520 messages,
175+ // copying ~29KB of data.
176+
177+ start := 0
178+ newBuf := b .bufPool .Get (b .uncompactedBytes )
179+ startIdx := len (b .backlog ) - b .uncompactedSuffixLen
180+
181+ for i := startIdx ; i < len (b .backlog ); i ++ {
182+ m := b .backlog [i ]
183+ b .backlog [i ] = recvMsg {}
184+ start += copy ((* newBuf )[start :], m .buffer .ReadOnlyData ())
185+ m .buffer .Free ()
186+ }
187+ b .backlog [startIdx ] = recvMsg {
188+ buffer : mem .NewBuffer (newBuf , b .bufPool ),
189+ }
190+ b .backlog = b .backlog [:startIdx + 1 ]
191+ // After compaction, the suffix is replaced with a single message containing
192+ // the combined payload. The new utilization is close to 1.0 (overhead of
193+ // one recvMsg relative to the large compacted payload), which is well
194+ // below the utilization factor of 2.
195+ b .uncompactedBytes = 0
196+ b .uncompactedSuffixLen = 0
106197}
107198
108199func (b * recvBuffer ) load () {
109200 b .mu .Lock ()
110201 if len (b .backlog ) > 0 {
111202 select {
112203 case b .c <- b .backlog [0 ]:
204+ // backlog[0] is only part of the tracked uncompacted suffix if the
205+ // entire backlog currently consists of the suffix. If an earlier
206+ // compaction or reset occurred, backlog[0] is already compacted.
207+ if envconfig .EnableReceiveBufferCompaction && b .uncompactedSuffixLen == len (b .backlog ) {
208+ b .uncompactedSuffixLen --
209+ b .uncompactedBytes -= b .backlog [0 ].buffer .Len ()
210+ }
113211 b .backlog [0 ] = recvMsg {}
114212 b .backlog = b .backlog [1 :]
115213 default :
0 commit comments