Skip to content
Closed
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 src/internal/profilerecord/profilerecord.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,3 +26,11 @@ type BlockProfileRecord struct {
Cycles int64
Stack []uintptr
}

type GoroutineInfo struct {
ID uint64
State string
CreatorID uint64
CreationPC uintptr
WaitSince int64 // approx time when the g became blocked, in nanoseconds
}
87 changes: 84 additions & 3 deletions src/runtime/mprof.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)

Expand All @@ -1422,6 +1484,8 @@ func goroutineProfileWithLabelsConcurrent(p []profilerecord.StackRecord, labels
goroutineProfile.active = true
goroutineProfile.records = p
goroutineProfile.labels = labels
goroutineProfile.infos = infos

Copy link
Member

Choose a reason for hiding this comment

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

don't you need to also update doRecordGoroutineProfile so that it updates goroutineProfile.infos?

// 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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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)
}
110 changes: 110 additions & 0 deletions src/runtime/pprof/pprof.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
Expand Down Expand Up @@ -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 {
Copy link
Member

Choose a reason for hiding this comment

The 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(
Copy link
Member

Choose a reason for hiding this comment

The 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 != "" {
Copy link

Copilot AI Aug 9, 2025

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 since the function parameter ensures 'infos' has the same length as 'records'. This check can be simplified to just 'infos[i].State != ""'.

Suggested change
if i < len(infos) && infos[i].State != "" {
if infos[i].State != "" {

Copilot uses AI. Check for mistakes.
state = infos[i].State
}

// Calculate blocked time (same logic as goroutineheader in traceback.go)
blockedStr := ""
if i < len(infos) && infos[i].WaitSince != 0 {
// Check if this is a waiting or syscall state where blocked time applies.
if strings.Contains(state, "waiting") || strings.Contains(state, "syscall") {
waitfor := (runtime_nanotime() - infos[i].WaitSince) / 60e9 // convert to minutes
if waitfor >= 1 {
blockedStr = fmt.Sprintf(", %d minutes", waitfor)
}
}
}

// Get label information using the same format as debug=1.
var labelStr string
if i < len(labels) && labels[i] != nil {
Copy link

Copilot AI Aug 9, 2025

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'.

Suggested change
if i < len(labels) && labels[i] != nil {
if labels[i] != nil {

Copilot uses AI. Check for mistakes.
labelMap := (*labelMap)(labels[i])
if labelMap != nil && len(labelMap.list) > 0 {
labelStr = labelMap.String()
}
}

fmt.Fprintf(w, "goroutine %d [%s%s]%s:\n", goroutineID, state, blockedStr, labelStr)
Copy link
Member

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.

Copy link
Member Author

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


// Print stack trace with runtime filtering (same logic as debug=2).
show := false
frames := runtime.CallersFrames(r.Stack)
for {
frame, more := frames.Next()
name := frame.Function
// Hide runtime.goexit, runtime.main as well as any runtime prior to the
// first non-runtime function.
if name != "runtime.goexit" && name != "runtime.main" && (show || !(strings.HasPrefix(name, "runtime.") || strings.HasPrefix(name, "internal/runtime/"))) {
show = true
if name == "" {
name = "unknown"
}
fmt.Fprintf(w, "%s(...)\n", name)
fmt.Fprintf(w, "\t%s:%d +0x%x\n", frame.File, frame.Line, frame.PC-frame.Entry)
}
if !more {
break
}
}

// Print "created by" information if available (skip main goroutine)
if i < len(infos) && goroutineID != 1 && infos[i].CreationPC != 0 {
Copy link

Copilot AI Aug 9, 2025

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'.

Suggested change
if i < len(infos) && goroutineID != 1 && infos[i].CreationPC != 0 {
if goroutineID != 1 && infos[i].CreationPC != 0 {

Copilot uses AI. Check for mistakes.
if f := runtime.FuncForPC(infos[i].CreationPC - 1); f != nil {
name := f.Name()
file, line := f.FileLine(infos[i].CreationPC - 1)
if infos[i].CreatorID != 0 {
fmt.Fprintf(w, "created by %s in goroutine %d\n", name, infos[i].CreatorID)
} else {
fmt.Fprintf(w, "created by %s\n", name)
}
fmt.Fprintf(w, "\t%s:%d +0x%x\n", file, line, infos[i].CreationPC)
}
}

if i < len(records)-1 {
fmt.Fprintln(w)
}
}

return nil
}

func writeRuntimeProfile(w io.Writer, debug int, name string, fetch func([]profilerecord.StackRecord, []unsafe.Pointer) (int, bool)) error {
// Find out how many records there are (fetch(nil)),
// allocate that many records, and get the data.
Expand Down Expand Up @@ -977,9 +1081,15 @@ func writeProfileInternal(w io.Writer, debug int, name string, runtimeProfile fu
//go:linkname pprof_goroutineProfileWithLabels runtime.pprof_goroutineProfileWithLabels
func pprof_goroutineProfileWithLabels(p []profilerecord.StackRecord, labels []unsafe.Pointer) (n int, ok bool)

//go:linkname pprof_goroutineStacksWithLabels runtime.pprof_goroutineStacksWithLabels
func pprof_goroutineStacksWithLabels(stacks []profilerecord.StackRecord, labels []unsafe.Pointer, infos []profilerecord.GoroutineInfo) (n int, ok bool)

//go:linkname pprof_cyclesPerSecond runtime/pprof.runtime_cyclesPerSecond
func pprof_cyclesPerSecond() int64

//go:linkname runtime_nanotime runtime.nanotime
func runtime_nanotime() int64

//go:linkname pprof_memProfileInternal runtime.pprof_memProfileInternal
func pprof_memProfileInternal(p []profilerecord.MemProfileRecord, inuseZero bool) (n int, ok bool)

Expand Down
Loading