Repository navigation
Fix: zero-copy model scan and live contention profiles #1609
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 all commits
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 |
|---|---|---|
|
|
@@ -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" | ||
|
|
@@ -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 { | ||
|
Collaborator
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. nit: 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. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -5,6 +5,7 @@ import ( | |
| "fmt" | ||
| "log/slog" | ||
| "strings" | ||
| "unsafe" | ||
|
|
||
| "github.com/tidwall/gjson" | ||
| "github.com/tidwall/sjson" | ||
|
|
@@ -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 { | ||
|
Collaborator
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. nit: The comment above the |
||
| if key.Type != gjson.String || !strings.EqualFold(key.String(), "model") { | ||
| return true | ||
| } | ||
|
|
||
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.
suggestion: A rate of
1for 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.Condin the body pipes,WaitGroups. The runtime docs suggest sampled values for long-running processes. For example:The profiles still catch any real contention problem, at a small fraction of the per-event cost.
pprofscales 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 theWithPprofdoc comment: "also enables block/mutex profiling for the whole process".