blob: d27dfa7aa331001a65d1c3d76ba0c78b973dd610 [file] [log] [blame] [edit]
// Copyright 2014 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// Serving of pprof-like profiles.
package main
import (
"cmp"
"fmt"
"internal/trace"
"internal/trace/traceviewer"
"net/http"
"slices"
"strings"
"time"
)
func pprofByGoroutine(compute computePprofFunc, t *parsedTrace) traceviewer.ProfileFunc {
return func(r *http.Request) ([]traceviewer.ProfileRecord, error) {
name := r.FormValue("name")
gToIntervals, err := pprofMatchingGoroutines(name, t)
if err != nil {
return nil, err
}
return compute(gToIntervals, t.events)
}
}
func pprofByRegion(compute computePprofFunc, t *parsedTrace) traceviewer.ProfileFunc {
return func(r *http.Request) ([]traceviewer.ProfileRecord, error) {
filter, err := newRegionFilter(r)
if err != nil {
return nil, err
}
gToIntervals, err := pprofMatchingRegions(filter, t)
if err != nil {
return nil, err
}
return compute(gToIntervals, t.events)
}
}
// pprofMatchingGoroutines returns the ids of goroutines of the matching name and its interval.
// If the id string is empty, returns nil without an error.
func pprofMatchingGoroutines(name string, t *parsedTrace) (map[trace.GoID][]interval, error) {
res := make(map[trace.GoID][]interval)
for _, g := range t.summary.Goroutines {
if name != "" && g.Name != name {
continue
}
endTime := g.EndTime
if g.EndTime == 0 {
endTime = t.endTime() // Use the trace end time, since the goroutine is still live then.
}
res[g.ID] = []interval{{start: g.StartTime, end: endTime}}
}
if len(res) == 0 {
return nil, fmt.Errorf("failed to find matching goroutines for name: %s", name)
}
return res, nil
}
// pprofMatchingRegions returns the time intervals of matching regions
// grouped by the goroutine id. If the filter is nil, returns nil without an error.
func pprofMatchingRegions(filter *regionFilter, t *parsedTrace) (map[trace.GoID][]interval, error) {
if filter == nil {
return nil, nil
}
gToIntervals := make(map[trace.GoID][]interval)
for _, g := range t.summary.Goroutines {
for _, r := range g.Regions {
if !filter.match(t, r) {
continue
}
gToIntervals[g.ID] = append(gToIntervals[g.ID], regionInterval(t, r))
}
}
for g, intervals := range gToIntervals {
// In order to remove nested regions and
// consider only the outermost regions,
// first, we sort based on the start time
// and then scan through to select only the outermost regions.
slices.SortFunc(intervals, func(a, b interval) int {
if c := cmp.Compare(a.start, b.start); c != 0 {
return c
}
return cmp.Compare(a.end, b.end)
})
var lastTimestamp trace.Time
var n int
// Select only the outermost regions.
for _, i := range intervals {
if lastTimestamp <= i.start {
intervals[n] = i // new non-overlapping region starts.
lastTimestamp = i.end
n++
}
// Otherwise, skip because this region overlaps with a previous region.
}
gToIntervals[g] = intervals[:n]
}
return gToIntervals, nil
}
type computePprofFunc func(gToIntervals map[trace.GoID][]interval, events []trace.Event) ([]traceviewer.ProfileRecord, error)
// computePprofIO returns a computePprofFunc that generates IO pprof-like profile (time spent in
// IO wait, currently only network blocking event).
func computePprofIO() computePprofFunc {
return makeComputePprofFunc(trace.GoWaiting, func(reason string) bool {
return reason == "network"
})
}
// computePprofBlock returns a computePprofFunc that generates blocking pprof-like profile
// (time spent blocked on synchronization primitives).
func computePprofBlock() computePprofFunc {
return makeComputePprofFunc(trace.GoWaiting, func(reason string) bool {
return strings.Contains(reason, "chan") || strings.Contains(reason, "sync") || strings.Contains(reason, "select")
})
}
// computePprofSyscall returns a computePprofFunc that generates a syscall pprof-like
// profile (time spent in syscalls).
func computePprofSyscall() computePprofFunc {
return makeComputePprofFunc(trace.GoSyscall, func(_ string) bool {
return true
})
}
// computePprofSched returns a computePprofFunc that generates a scheduler latency pprof-like profile
// (time between a goroutine become runnable and actually scheduled for execution).
func computePprofSched() computePprofFunc {
return makeComputePprofFunc(trace.GoRunnable, func(_ string) bool {
return true
})
}
// makeComputePprofFunc returns a computePprofFunc that generates a profile of time goroutines spend
// in a particular state for the specified reasons.
func makeComputePprofFunc(state trace.GoState, trackReason func(string) bool) computePprofFunc {
return func(gToIntervals map[trace.GoID][]interval, events []trace.Event) ([]traceviewer.ProfileRecord, error) {
stacks := newStackMap()
tracking := make(map[trace.GoID]*trace.Event)
for i := range events {
ev := &events[i]
// Filter out any non-state-transitions and events without stacks.
if ev.Kind() != trace.EventStateTransition {
continue
}
stack := ev.Stack()
if stack == trace.NoStack {
continue
}
// The state transition has to apply to a goroutine.
st := ev.StateTransition()
if st.Resource.Kind != trace.ResourceGoroutine {
continue
}
id := st.Resource.Goroutine()
_, new := st.Goroutine()
// Check if we're tracking this goroutine.
startEv := tracking[id]
if startEv == nil {
// We're not. Start tracking if the new state
// matches what we want and the transition is
// for one of the reasons we care about.
if new == state && trackReason(st.Reason) {
tracking[id] = ev
}
continue
}
// We're tracking this goroutine.
if new == state {
// We're tracking this goroutine, but it's just transitioning
// to the same state (this is a no-ip
continue
}
// The goroutine has transitioned out of the state we care about,
// so remove it from tracking and record the stack.
delete(tracking, id)
overlapping := pprofOverlappingDuration(gToIntervals, id, interval{startEv.Time(), ev.Time()})
if overlapping > 0 {
rec := stacks.getOrAdd(startEv.Stack())
rec.Count++
rec.Time += overlapping
}
}
return stacks.profile(), nil
}
}
// pprofOverlappingDuration returns the overlapping duration between
// the time intervals in gToIntervals and the specified event.
// If gToIntervals is nil, this simply returns the event's duration.
func pprofOverlappingDuration(gToIntervals map[trace.GoID][]interval, id trace.GoID, sample interval) time.Duration {
if gToIntervals == nil { // No filtering.
return sample.duration()
}
intervals := gToIntervals[id]
if len(intervals) == 0 {
return 0
}
var overlapping time.Duration
for _, i := range intervals {
if o := i.overlap(sample); o > 0 {
overlapping += o
}
}
return overlapping
}
// interval represents a time interval in the trace.
type interval struct {
start, end trace.Time
}
func (i interval) duration() time.Duration {
return i.end.Sub(i.start)
}
func (i1 interval) overlap(i2 interval) time.Duration {
// Assume start1 <= end1 and start2 <= end2
if i1.end < i2.start || i2.end < i1.start {
return 0
}
if i1.start < i2.start { // choose the later one
i1.start = i2.start
}
if i1.end > i2.end { // choose the earlier one
i1.end = i2.end
}
return i1.duration()
}
// pprofMaxStack is the extent of the deduplication we're willing to do.
//
// Because slices aren't comparable and we want to leverage maps for deduplication,
// we have to choose a fixed constant upper bound on the amount of frames we want
// to support. In practice this is fine because there's a maximum depth to these
// stacks anyway.
const pprofMaxStack = 128
// stackMap is a map of trace.Stack to some value V.
type stackMap struct {
// stacks contains the full list of stacks in the set, however
// it is insufficient for deduplication because trace.Stack
// equality is only optimistic. If two trace.Stacks are equal,
// then they are guaranteed to be equal in content. If they are
// not equal, then they might still be equal in content.
stacks map[trace.Stack]*traceviewer.ProfileRecord
// pcs is the source-of-truth for deduplication. It is a map of
// the actual PCs in the stack to a trace.Stack.
pcs map[[pprofMaxStack]uint64]trace.Stack
}
func newStackMap() *stackMap {
return &stackMap{
stacks: make(map[trace.Stack]*traceviewer.ProfileRecord),
pcs: make(map[[pprofMaxStack]uint64]trace.Stack),
}
}
func (m *stackMap) getOrAdd(stack trace.Stack) *traceviewer.ProfileRecord {
// Fast path: check to see if this exact stack is already in the map.
if rec, ok := m.stacks[stack]; ok {
return rec
}
// Slow path: the stack may still be in the map.
// Grab the stack's PCs as the source-of-truth.
var pcs [pprofMaxStack]uint64
pcsForStack(stack, &pcs)
// Check the source-of-truth.
var rec *traceviewer.ProfileRecord
if existing, ok := m.pcs[pcs]; ok {
// In the map.
rec = m.stacks[existing]
delete(m.stacks, existing)
} else {
// Not in the map.
rec = new(traceviewer.ProfileRecord)
}
// Insert regardless of whether we have a match in m.pcs.
// Even if we have a match, we want to keep the newest version
// of that stack, since we're much more likely tos see it again
// as we iterate through the trace linearly. Simultaneously, we
// are likely to never see the old stack again.
m.pcs[pcs] = stack
m.stacks[stack] = rec
return rec
}
func (m *stackMap) profile() []traceviewer.ProfileRecord {
prof := make([]traceviewer.ProfileRecord, 0, len(m.stacks))
for stack, record := range m.stacks {
rec := *record
for i, frame := range slices.Collect(stack.Frames()) {
rec.Stack = append(rec.Stack, &trace.Frame{
PC: frame.PC,
Fn: frame.Func,
File: frame.File,
Line: int(frame.Line),
})
// Cut this off at pprofMaxStack because that's as far
// as our deduplication goes.
if i >= pprofMaxStack {
break
}
}
prof = append(prof, rec)
}
return prof
}
// pcsForStack extracts the first pprofMaxStack PCs from stack into pcs.
func pcsForStack(stack trace.Stack, pcs *[pprofMaxStack]uint64) {
for i, frame := range slices.Collect(stack.Frames()) {
pcs[i] = frame.PC
if i >= len(pcs) {
break
}
}
}