-
Notifications
You must be signed in to change notification settings - Fork 2
runtime/pprof: add debug=26257 goroutine profile with labels and reduced STW #5
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 |
|---|---|---|
|
|
@@ -1320,19 +1320,59 @@ func goroutineProfileWithLabels(p []profilerecord.StackRecord, labels []unsafe.P | |
| labels = nil | ||
| } | ||
|
|
||
| return goroutineProfileWithLabelsConcurrent(p, labels) | ||
| return goroutineProfileWithLabelsConcurrent(p, labels, nil) | ||
| } | ||
|
|
||
| var goroutineProfile = struct { | ||
| sema uint32 | ||
| active bool | ||
| offset atomic.Int64 | ||
| records []profilerecord.StackRecord | ||
| labels []unsafe.Pointer | ||
| // labels, if non-nil, should have the same length as records. | ||
| labels []unsafe.Pointer | ||
| // infos, if non-nil, should have the same length as records. | ||
| infos []profilerecord.GoroutineInfo | ||
| }{ | ||
| sema: 1, | ||
| } | ||
|
|
||
| // goroutineStatusString converts goroutine status to human-readable string | ||
| // Uses the same logic as goroutineheader() in traceback.go | ||
| func goroutineStatusString(gp1 *g) string { | ||
| gpstatus := readgstatus(gp1) | ||
| gpstatus &^= _Gscan // drop the scan bit | ||
|
|
||
| // Basic string status | ||
| var status string | ||
| switch gpstatus { | ||
| case _Gidle: | ||
| status = "idle" | ||
| case _Grunnable: | ||
| status = "runnable" | ||
| case _Grunning: | ||
| status = "running" | ||
| case _Gsyscall: | ||
| status = "syscall" | ||
| case _Gwaiting: | ||
| status = "waiting" | ||
| case _Gdead: | ||
| status = "dead" | ||
| case _Gcopystack: | ||
| status = "copystack" | ||
| case _Gpreempted: | ||
| status = "preempted" | ||
| default: | ||
| status = "unknown" | ||
| } | ||
|
|
||
| // Override with wait reason if available (same logic as goroutineheader) | ||
| if gpstatus == _Gwaiting && gp1.waitreason != waitReasonZero { | ||
| status = gp1.waitreason.String() | ||
| } | ||
|
|
||
| return status | ||
| } | ||
|
|
||
| // goroutineProfileState indicates the status of a goroutine's stack for the | ||
| // current in-progress goroutine profile. Goroutines' stacks are initially | ||
| // "Absent" from the profile, and end up "Satisfied" by the time the profile is | ||
|
|
@@ -1366,7 +1406,18 @@ func (p *goroutineProfileStateHolder) CompareAndSwap(old, new goroutineProfileSt | |
| return (*atomic.Uint32)(p).CompareAndSwap(uint32(old), uint32(new)) | ||
| } | ||
|
|
||
| func goroutineProfileWithLabelsConcurrent(p []profilerecord.StackRecord, labels []unsafe.Pointer) (n int, ok bool) { | ||
| // goroutineProfileWithLabelsConcurrent collects the stacks of all user | ||
| // goroutines into the passed slice, provided it has sufficeient length to do | ||
| // so, returning the number collected and ok=true. If the passed slice does | ||
| // not have sufficient length, it returns the required length and ok=false | ||
| // instead. | ||
| // | ||
| // Additional information about each goroutine can optionally be collected into | ||
| // the labels and/or infos slices if they are non-nil. Their length must match | ||
| // len(p) if non-nil. | ||
| func goroutineProfileWithLabelsConcurrent( | ||
| p []profilerecord.StackRecord, labels []unsafe.Pointer, infos []profilerecord.GoroutineInfo, | ||
| ) (n int, ok bool) { | ||
| if len(p) == 0 { | ||
| // An empty slice is obviously too small. Return a rough | ||
| // allocation estimate without bothering to STW. As long as | ||
|
|
@@ -1411,6 +1462,17 @@ func goroutineProfileWithLabelsConcurrent(p []profilerecord.StackRecord, labels | |
| if labels != nil { | ||
| labels[0] = ourg.labels | ||
| } | ||
|
|
||
| // If extended goroutine info collection is enabled, gather the additional | ||
| // fields as well. | ||
| if len(infos) > 0 { | ||
| infos[0].ID = ourg.goid | ||
| infos[0].CreatorID = ourg.parentGoid | ||
| infos[0].CreationPC = ourg.gopc | ||
| infos[0].State = goroutineStatusString(ourg) | ||
| infos[0].WaitSince = ourg.waitsince | ||
| } | ||
|
|
||
| ourg.goroutineProfiled.Store(goroutineProfileSatisfied) | ||
| goroutineProfile.offset.Store(1) | ||
|
|
||
|
|
@@ -1422,6 +1484,8 @@ func goroutineProfileWithLabelsConcurrent(p []profilerecord.StackRecord, labels | |
| goroutineProfile.active = true | ||
| goroutineProfile.records = p | ||
| goroutineProfile.labels = labels | ||
| goroutineProfile.infos = infos | ||
|
|
||
|
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. don't you need to also update |
||
| // The finalizer goroutine needs special handling because it can vary over | ||
| // time between being a user goroutine (eligible for this profile) and a | ||
| // system goroutine (to be excluded). Pick one before restarting the world. | ||
|
|
@@ -1453,6 +1517,7 @@ func goroutineProfileWithLabelsConcurrent(p []profilerecord.StackRecord, labels | |
| goroutineProfile.active = false | ||
| goroutineProfile.records = nil | ||
| goroutineProfile.labels = nil | ||
| goroutineProfile.infos = nil | ||
| startTheWorld(stw) | ||
|
|
||
| // Restore the invariant that every goroutine struct in allgs has its | ||
|
|
@@ -1575,9 +1640,20 @@ func doRecordGoroutineProfile(gp1 *g, pcbuf []uintptr) { | |
| // to avoid schedule delays. | ||
| systemstack(func() { saveg(^uintptr(0), ^uintptr(0), gp1, &goroutineProfile.records[offset], pcbuf) }) | ||
|
|
||
| // If label collection is enabled, collect the labels. | ||
| if goroutineProfile.labels != nil { | ||
| goroutineProfile.labels[offset] = gp1.labels | ||
| } | ||
|
|
||
| // If extended goroutine info collection is enabled and there is sufficient | ||
| // capacity to do so, gather the additional goroutine fields as well. | ||
| if goroutineProfile.infos != nil && offset < len(goroutineProfile.infos) { | ||
| goroutineProfile.infos[offset].ID = gp1.goid | ||
| goroutineProfile.infos[offset].CreatorID = gp1.parentGoid | ||
| goroutineProfile.infos[offset].CreationPC = gp1.gopc | ||
| goroutineProfile.infos[offset].State = goroutineStatusString(gp1) | ||
| goroutineProfile.infos[offset].WaitSince = gp1.waitsince | ||
| } | ||
| } | ||
|
|
||
| func goroutineProfileWithLabelsSync(p []profilerecord.StackRecord, labels []unsafe.Pointer) (n int, ok bool) { | ||
|
|
@@ -1733,3 +1809,8 @@ func Stack(buf []byte, all bool) int { | |
| } | ||
| return n | ||
| } | ||
|
|
||
| //go:linkname pprof_goroutineStacksWithLabels | ||
| func pprof_goroutineStacksWithLabels(stacks []profilerecord.StackRecord, labels []unsafe.Pointer, infos []profilerecord.GoroutineInfo) (n int, ok bool) { | ||
| return goroutineProfileWithLabelsConcurrent(stacks, labels, infos) | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -749,6 +749,9 @@ func countGoroutine() int { | |||||
|
|
||||||
| // writeGoroutine writes the current runtime GoroutineProfile to w. | ||||||
| func writeGoroutine(w io.Writer, debug int) error { | ||||||
| if debug == 26257 { | ||||||
| return writeGoroutineStacksWithLabels(w) | ||||||
| } | ||||||
| if debug >= 2 { | ||||||
| return writeGoroutineStacks(w) | ||||||
| } | ||||||
|
|
@@ -776,6 +779,107 @@ func writeGoroutineStacks(w io.Writer) error { | |||||
| return err | ||||||
| } | ||||||
|
|
||||||
| // writeGoroutineStacksWithLabels writes individual goroutine stack traces like | ||||||
| // writeGoroutineStacks, but uses the concurrent, reduced-stop time approach of | ||||||
| // goroutineProfileWithLabelsConcurrent. It includes the ID, state, and labels | ||||||
| // for each goroutine, although as these are captured after the world is resumed | ||||||
| // they are not guaranteed to be entirely consistent as of a single point in | ||||||
| // time. | ||||||
| func writeGoroutineStacksWithLabels(w io.Writer) error { | ||||||
| n, ok := pprof_goroutineStacksWithLabels(nil, nil, nil) | ||||||
|
|
||||||
| for { | ||||||
|
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. are you confident in the robustness of this loop? Maybe worth to additionally put in a growth factor of (say) 2 on each iteration. |
||||||
| // Allocate slices for individual goroutine data | ||||||
| stacks := make([]profilerecord.StackRecord, n+10) | ||||||
| labels := make([]unsafe.Pointer, n+10) | ||||||
| infos := make([]profilerecord.GoroutineInfo, n+10) | ||||||
|
|
||||||
| n, ok = pprof_goroutineStacksWithLabels(stacks, labels, infos) | ||||||
| if ok { | ||||||
| return printGoroutineStacksWithLabels(w, stacks[:n], labels[:n], infos[:n]) | ||||||
| } | ||||||
| // Profile grew; try again with larger slices | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| // printGoroutineStacksWithLabels formats goroutine records in a format similar | ||||||
| // to runtime.Stack, but also includes labels as well. | ||||||
| func printGoroutineStacksWithLabels( | ||||||
|
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. Is there a smaller diff that takes the existing print code and adds an argument for it? |
||||||
| w io.Writer, records []profilerecord.StackRecord, labels []unsafe.Pointer, infos []profilerecord.GoroutineInfo, | ||||||
| ) error { | ||||||
| for i, r := range records { | ||||||
| goroutineID := infos[i].ID | ||||||
| state := "unknown" | ||||||
| if i < len(infos) && infos[i].State != "" { | ||||||
|
||||||
| if i < len(infos) && infos[i].State != "" { | |
| if infos[i].State != "" { |
dt marked this conversation as resolved.
Show resolved
Hide resolved
Copilot
AI
Aug 9, 2025
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.
Similar to the infos check, 'i < len(labels)' is redundant since labels has the same length as records. This can be simplified to 'labels[i] != nil'.
| if i < len(labels) && labels[i] != nil { | |
| if labels[i] != nil { |
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.
one side-effect of the format change is that panicparse and friends won't accept this any more. Can you think of a way to squeeze the labels in in a way that doesn't "nominally" change the format? I can't - but thought I'd ask.
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.
if we wanted to use backtrace/debug=2 format we could inject them inside the []; I previously had assumed that was strictly state[, waitSince] but looking at the 1.25 changes that add bubble info there, and the scanning states, I realized that it is expected that any number of comma separated fields can be in the [] so we could put labels there. But we can never get the args in a resume-the-world safe way to I think maybe trying to make debug=2 without STW is a fool's errand and we should focus on getting what we want (state, creator, waitSince) in debug=0/1, ala golang#74954
Copilot
AI
Aug 9, 2025
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.
The condition 'i < len(infos)' is redundant as infos has the same length as records. This can be simplified to 'goroutineID != 1 && infos[i].CreationPC != 0'.
| if i < len(infos) && goroutineID != 1 && infos[i].CreationPC != 0 { | |
| if goroutineID != 1 && infos[i].CreationPC != 0 { |
Uh oh!
There was an error while loading. Please reload this page.