Skip to content
Merged
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
8 changes: 8 additions & 0 deletions core/observe/statserver.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"net"
"net/http"
"net/http/pprof"
"runtime"
"strings"
"time"

Expand Down Expand Up @@ -107,6 +108,13 @@ func NewStatServer(addr string, configProvider ConfigProvider, statsProvider Sta
mux.Handle("/tls-bridge/unread", o.tlsBridgeUnread)
}
if o.pprof {
// /debug/pprof/block and /mutex stay empty without these: the handlers
// exist, but the runtime records nothing unless asked. Both cost only
// when the event they measure happens — a blocked goroutine or a contended
// mutex — so they ride the same gate as the handlers rather than running
// on deployments where pprof is off.
runtime.SetBlockProfileRate(1)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

suggestion: A rate of 1 for both recorders means every blocking event and every contended unlock gets recorded, and each one walks the stack. "Costs only when the event happens" is true, but in a proxy those events happen all the time: channel and select waits in the HTTP/2 transport and server, sync.Cond in the body pipes, WaitGroups. The runtime docs suggest sampled values for long-running processes. For example:

runtime.SetBlockProfileRate(int(10 * time.Microsecond)) // one sample per ~10µs blocked
runtime.SetMutexProfileFraction(100)                     // ~1% of contention events

The profiles still catch any real contention problem, at a small fraction of the per-event cost. pprof scales sampled counts back up, so the output stays comparable. This is only enabled on loopback-only (laptop) installs, so it is not blocking. Also, as a process-global side effect of building a server, it is worth one line in the WithPprof doc comment: "also enables block/mutex profiling for the whole process".

runtime.SetMutexProfileFraction(1)
pprofGuard := func(h http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
if !loopbackHost(r.Host) {
Expand Down
29 changes: 29 additions & 0 deletions core/observe/statserver_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@ import (
"net"
"net/http"
"net/http/httptest"
"runtime"
"strings"
"testing"
"time"

"github.com/rossoctl/cortex/core/auth"
"github.com/rossoctl/cortex/core/config"
Expand Down Expand Up @@ -241,6 +243,33 @@ func TestStatServerPprofServesProfiles(t *testing.T) {
}
}

// /debug/pprof/block and /mutex answer 200 but stay empty forever unless the
// runtime is told to record contention — the handlers exist either way. WithPprof
// turns the recorders on with the handlers, so the endpoints can show something.
func TestStatServerPprofEnablesContentionProfiles(t *testing.T) {
handler := serveMuxOpts(newTestConfig(), auth.NewStats(), WithPprof(true))

// Mutex has a getter; block does not, so block is proven by recording one
// event: a channel receive that waits is a block, and at rate 0 it would
// never reach the profile.
ch := make(chan struct{})
go func() { time.Sleep(100 * time.Millisecond); close(ch) }()
<-ch

req := httptest.NewRequest(http.MethodGet, "/debug/pprof/block?debug=1", nil)
req.Host = "localhost:47802" // a loopback Host, as a local client sends
w := httptest.NewRecorder()
handler.ServeHTTP(w, req)
// The blocked receive's stack names this test; an empty profile carries only
// the header lines.
if !strings.Contains(w.Body.String(), "TestStatServerPprofEnablesContentionProfiles") {
t.Error("a 100ms blocked receive is absent from /debug/pprof/block: the recorder is off")
}
if prev := runtime.SetMutexProfileFraction(1); prev == 0 {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: runtime.SetMutexProfileFraction(1) reads the previous value but also sets it. If WithPprof regressed, the test would still flag it, but it would also quietly switch mutex profiling on for the rest of the package's tests. A negative argument only reads the value and leaves it unchanged (per the runtime docs: "If rate < 0, it returns the current rate without changing it"):

if runtime.SetMutexProfileFraction(-1) == 0 {

The comment above ("Mutex has a getter") then matches what the code does.

t.Error("WithPprof left mutex profiling off: /debug/pprof/mutex would stay empty")
}
}

// A page can rebind a DNS name to 127.0.0.1 and have a browser read a loopback
// port, so the profiles refuse any Host that is not exactly loopback — the same
// defense as the session API's erase guards.
Expand Down
8 changes: 7 additions & 1 deletion core/pipeline/model.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import (
"fmt"
"log/slog"
"strings"
"unsafe"

"github.com/tidwall/gjson"
"github.com/tidwall/sjson"
Expand Down Expand Up @@ -115,7 +116,12 @@ func requestModel(body []byte) (string, error) {
}
var model gjson.Result
exact, anyCase := 0, 0
gjson.ParseBytes(body).ForEach(func(key, value gjson.Result) bool {
// Zero-copy: ParseBytes is Parse(string(body)) — a full copy of every routed
// request just to scan its top-level keys. The alias is safe because gjson
// never writes the string it parses, the body is not written concurrently on
// the request path, and nothing that outlives the scan points into it: the
// only string that escapes, below, is cloned.
gjson.Parse(unsafe.String(unsafe.SliceData(body), len(body))).ForEach(func(key, value gjson.Result) bool {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

nit: The comment above the strings.Clone further down (around L145: "Cloned off gjson's parse copy of the body: Result.String() is a substring of that copy …") is now out of date. There is no parse copy anymore, and model.String() is a substring of an alias of c.Body itself. That also makes the clone more important, not just about pinning. Without it, the returned string would share bytes with a mutable []byte, so a later write to the body (for example an in-place sjson set, or a plugin reusing the buffer) would change a Go string, which is meant to be immutable. Consider rewording it to something like: "Cloned off the body: Result.String() aliases c.Body (see the zero-copy scan above), so an uncloned name would both pin the body and change if the body were written."

if key.Type != gjson.String || !strings.EqualFold(key.String(), "model") {
return true
}
Expand Down
24 changes: 24 additions & 0 deletions core/pipeline/model_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -282,3 +282,27 @@ func TestRequestModelDoesNotPinTheBody(t *testing.T) {
t.Errorf("name = %q", names[0])
}
}

// TestRequestModelDoesNotCopyTheBody is the allocation regression for the same
// scan: ParseBytes is Parse(string(body)), so reading the top-level keys used to
// copy the whole body once per routed request — ~1GB/hour of churn on a live
// proxy, third behind tool-prune's copies. The scan now runs over an alias of
// the body, so one call on a 4MiB body must allocate well under the body's size.
func TestRequestModelDoesNotCopyTheBody(t *testing.T) {
pad := strings.Repeat("x", 4<<20)
body := []byte(`{"model":"claude-opus-5-5","pad":"` + pad + `"}`)
if _, err := requestModel(body); err != nil { // warm: page in gjson's code
t.Fatalf("requestModel: %v", err)
}
runtime.GC()
var before runtime.MemStats
runtime.ReadMemStats(&before)
if _, err := requestModel(body); err != nil {
t.Fatalf("requestModel: %v", err)
}
var after runtime.MemStats
runtime.ReadMemStats(&after)
if got := after.TotalAlloc - before.TotalAlloc; got > 64<<10 {
t.Errorf("requestModel on a %dMiB body allocated %dKiB; the scan copies the body", len(body)>>20, got>>10)
}
}
Loading