Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
89 changes: 89 additions & 0 deletions transport/http/createrequest_bench_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
// Copyright (c) 2026 Uber Technologies, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package http

import (
"bytes"
"io"
"net/http"
"net/url"
"testing"

"go.uber.org/yarpc/api/transport"
)

var _benchBody = bytes.Repeat([]byte("x"), 1<<10)

// BenchmarkCreateRequest exercises the createRequest hot path as it runs in
// production: URL string cached in urlStr, header map borrowed from headerPool.
func BenchmarkCreateRequest(b *testing.B) {
tr := NewTransport()
out := tr.NewSingleOutbound("http://localhost:8080")

b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
hdr := headerPool.Get().(http.Header)
hreq, err := out.createRequest(makeReq(), hdr)
if err != nil {
b.Fatal(err)
}
_, _ = io.Copy(io.Discard, hreq.Body)
for k := range hdr {
delete(hdr, k)
}
headerPool.Put(hdr)
}
}

// BenchmarkURLCopy isolates just the url.URL copy + String() cost —
// the part we want to eliminate.
func BenchmarkURLCopy(b *testing.B) {
urlTemplate, _ := url.Parse("http://my-service.prod.uber.internal:8080/v1")
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

what about upgrading yarpc to a modern go? :)

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

sure, will do it as follow-up :) Thanks!

newURL := *urlTemplate
_ = newURL.String()
}
}

// BenchmarkURLStringCached benchmarks the target implementation:
// using a pre-computed URL string avoids the url.URL copy and String() alloc.
func BenchmarkURLStringCached(b *testing.B) {
urlTemplate, _ := url.Parse("http://my-service.prod.uber.internal:8080/v1")
cachedStr := urlTemplate.String() // computed once at construction
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
_ = cachedStr // just a string reference, zero cost
}
}

func makeReq() *transport.Request {
return &transport.Request{
Caller: "myservice",
Service: "downstream",
Encoding: "proto",
Procedure: "MyService/MyMethod",
Body: bytes.NewReader(_benchBody),
}
}
52 changes: 44 additions & 8 deletions transport/http/outbound.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"net/url"
"strconv"
"strings"
"sync"
"time"

"github.com/opentracing/opentracing-go"
Expand Down Expand Up @@ -59,6 +60,17 @@ var (

var defaultURLTemplate, _ = url.Parse("http://localhost")

// _headerPoolInitSize covers the mandatory YARPC core headers (~10) plus
// a handful of typical application headers without growing the map.
const _headerPoolInitSize = 16

// headerPool recycles http.Header maps across outbound calls, eliminating
// one heap allocation (the map bucket array) per request on the hot path.
// The map is cleared before being returned so callers always get an empty map.
var headerPool = sync.Pool{
New: func() any { return make(http.Header, _headerPoolInitSize) },
}

// OutboundOption customizes an HTTP Outbound.
type OutboundOption func(*Outbound)

Expand Down Expand Up @@ -142,6 +154,7 @@ func (t *Transport) NewOutbound(chooser peer.Chooser, opts ...OutboundOption) *O
once: lifecycle.NewOnce(),
chooser: chooser,
urlTemplate: defaultURLTemplate,
urlStr: defaultURLTemplate.String(),
tracer: t.tracer,
transport: t,
bothResponseError: true,
Expand Down Expand Up @@ -191,6 +204,7 @@ func createHTTP1TLSClient(o *Outbound) *http.Client {
ut := *o.urlTemplate
ut.Scheme = "https"
o.urlTemplate = &ut
o.urlStr = o.urlTemplate.String()

return &http.Client{
Transport: h1transport,
Expand Down Expand Up @@ -250,9 +264,13 @@ func (t *Transport) NewSingleOutbound(uri string, opts ...OutboundOption) *Outbo
type Outbound struct {
chooser peer.Chooser
urlTemplate *url.URL
tracer opentracing.Tracer
transport *Transport
sender sender
// urlStr caches urlTemplate.String() to avoid recomputing it on every
// outbound call. It is set once during construction via setURLTemplate
// and is safe to read concurrently without a lock.
urlStr string
tracer opentracing.Tracer
transport *Transport
sender sender

// Headers to add to all outgoing requests.
headers http.Header
Expand Down Expand Up @@ -286,6 +304,7 @@ func (o *Outbound) setURLTemplate(URL string) {
log.Fatalf("failed to configure HTTP outbound: invalid URL template %q: %s", URL, err)
}
o.urlTemplate = parsedURL
o.urlStr = parsedURL.String()
}

// Transports returns the outbound's HTTP transport.
Expand Down Expand Up @@ -357,7 +376,20 @@ func (o *Outbound) call(ctx context.Context, treq *transport.Request) (*transpor
}
ttl := deadline.Sub(start)

hreq, err := o.createRequest(treq)
// Borrow a pre-allocated header map from the pool. It is passed into
// createRequest and becomes hreq.Header. net/http serialises the headers
// to the wire during roundTrip; after that call returns the map is no
// longer read by any code path in this function, so it is safe to clear
// and return here via defer.
hdr := headerPool.Get().(http.Header)
defer func() {
for k := range hdr {
delete(hdr, k)
}
headerPool.Put(hdr)
}()

hreq, err := o.createRequest(treq, hdr)
if err != nil {
return nil, err
}
Expand Down Expand Up @@ -445,9 +477,13 @@ func (o *Outbound) getPeerForRequest(ctx context.Context, treq *transport.Reques
return hpPeer, onFinish, nil
}

func (o *Outbound) createRequest(treq *transport.Request) (*http.Request, error) {
newURL := *o.urlTemplate
hreq, err := http.NewRequest("POST", newURL.String(), treq.Body)
func (o *Outbound) createRequest(treq *transport.Request, hdr http.Header) (*http.Request, error) {
urlStr := o.urlStr
if urlStr == "" {
// Fallback for outbounds constructed directly (e.g. in tests).
urlStr = o.urlTemplate.String()
}
hreq, err := http.NewRequest("POST", urlStr, treq.Body)
if err != nil {
return nil, err
}
Expand All @@ -457,7 +493,7 @@ func (o *Outbound) createRequest(treq *transport.Request) (*http.Request, error)
// header is given along a HTTP/1 request.
// see: https://cs.opensource.google/go/x/net/+/c6fcb2db:http/httpguts/httplex.go;l=203
headers := applicationHeaders.deleteHTTP2PseudoHeadersIfNeeded(treq.Headers)
hreq.Header = applicationHeaders.ToHTTPHeaders(headers, nil)
hreq.Header = applicationHeaders.ToHTTPHeaders(headers, hdr)
return hreq, nil
}

Expand Down
2 changes: 1 addition & 1 deletion transport/http/outbound_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ func TestCreateRequest(t *testing.T) {
if tt.urlTemplate != nil {
o.urlTemplate = tt.urlTemplate
}
hreq, err := o.createRequest(tt.treq)
hreq, err := o.createRequest(tt.treq, make(http.Header))
if tt.wantError {
assert.Error(t, err)
return
Expand Down