Skip to content

Conversation

arjan-bal
Copy link
Contributor

In Go, creating a closure results in a heap allocation if the compiler determines the closure might outlive the function in which it was created. This change removes two such closures, replacing them with interfaces that are implemented by the ClientStream and ServerStream structs.

While this pattern may slightly reduce readability, the performance benefit is worthwhile, as this transport code is executed for every new stream. This reduces allocs/unary RPC by 2.5%.

Testing

# test command
 go run benchmark/benchmain/main.go -benchtime=60s -workloads=unary \
   -compression=off -maxConcurrentCalls=500 -trace=off \
   -reqSizeBytes=100 -respSizeBytes=100 -networkMode=Local -resultFile="${RUN_NAME}"   -recvBufferPool=simple

# results
go run benchmark/benchresult/main.go unary-before unary-after       
               Title       Before        After Percentage
            TotalOps      7593738      7708364     1.51%
             SendOps            0            0      NaN%
             RecvOps            0            0      NaN%
            Bytes/op     10218.45     10185.84    -0.32%
           Allocs/op       164.85       160.84    -2.43%
             ReqT/op 101249840.00 102778186.67     1.51%
            RespT/op 101249840.00 102778186.67     1.51%
            50th-Lat   3.617561ms   3.568623ms    -1.35%
            90th-Lat   5.218682ms   5.131828ms    -1.66%
            99th-Lat   6.052632ms   5.950261ms    -1.69%
             Avg-Lat   3.948414ms   3.889006ms    -1.50%
           GoVersion     go1.24.4     go1.24.4
         GrpcVersion   1.77.0-dev   1.77.0-dev

RELEASE NOTES: N/A

@arjan-bal arjan-bal added this to the 1.77 Release milestone Oct 6, 2025
@arjan-bal arjan-bal added Type: Performance Performance improvements (CPU, network, memory, etc) Area: Transport Includes HTTP/2 client/server and HTTP server handler transports and advanced transport features. labels Oct 6, 2025
Copy link

codecov bot commented Oct 6, 2025

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 82.03%. Comparing base (ece7397) to head (9fee045).

Additional details and impacted files
@@            Coverage Diff             @@
##           master    #8630      +/-   ##
==========================================
- Coverage   82.17%   82.03%   -0.15%     
==========================================
  Files         415      415              
  Lines       40709    40726      +17     
==========================================
- Hits        33453    33409      -44     
- Misses       5883     5933      +50     
- Partials     1373     1384      +11     
Files with missing lines Coverage Δ
internal/transport/client_stream.go 100.00% <100.00%> (ø)
internal/transport/handler_server.go 91.22% <100.00%> (+0.37%) ⬆️
internal/transport/http2_client.go 91.92% <100.00%> (+0.13%) ⬆️
internal/transport/http2_server.go 90.57% <100.00%> (-0.29%) ⬇️
internal/transport/server_stream.go 95.58% <100.00%> (+0.27%) ⬆️
internal/transport/transport.go 83.87% <100.00%> (-6.86%) ⬇️

... and 25 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Member

@dfawley dfawley left a comment

Choose a reason for hiding this comment

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

In general this approach LGTM. One question to maybe simplify things. Thanks for the improvements.

// Callback to state application's intentions to read data. This
// is used to adjust flow control, if needed.
requestRead func(int)
readRequester readRequester
Copy link
Member

Choose a reason for hiding this comment

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

Does it make sense to embed some of these things instead so that we don't have to implement the trampoline methods?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The updateWindow method on the new windowHandler interface takes a single parameter, while the corresponding methods on http2Client and http2Server expect a second parameter for the stream itself. The intermediate methods on ServerStream and ClientStream supply this extra parameter before calling the underlying http2 methods.

func (s *ServerStream) updateWindow(n int) {
	// The receiver 's' is passed as the required stream parameter.
	s.st.updateWindow(s, uint32(n))
}

I couldn't find a simple way to remove these one-line wrapper methods without changing the windowHandler interface to accept the extra *Stream parameter. That change would require callers like transportReader to hold a reference to the stream just for this call, which feels like a worse design.

@dfawley dfawley removed their assignment Oct 6, 2025
@easwars easwars assigned arjan-bal and unassigned easwars Oct 6, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Area: Transport Includes HTTP/2 client/server and HTTP server handler transports and advanced transport features. Type: Performance Performance improvements (CPU, network, memory, etc)
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants