github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/trace/v2/pprof.go (about)

     1  // Copyright 2014 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Serving of pprof-like profiles.
     6  
     7  package trace
     8  
     9  import (
    10  	"cmp"
    11  	"fmt"
    12  	"net/http"
    13  	"slices"
    14  	"strconv"
    15  	"strings"
    16  	"time"
    17  
    18  	"github.com/go-asm/go/trace"
    19  	"github.com/go-asm/go/trace/traceviewer"
    20  	tracev2 "github.com/go-asm/go/trace/v2"
    21  )
    22  
    23  func pprofByGoroutine(compute computePprofFunc, t *parsedTrace) traceviewer.ProfileFunc {
    24  	return func(r *http.Request) ([]traceviewer.ProfileRecord, error) {
    25  		id := r.FormValue("id")
    26  		gToIntervals, err := pprofMatchingGoroutines(id, t)
    27  		if err != nil {
    28  			return nil, err
    29  		}
    30  		return compute(gToIntervals, t.events)
    31  	}
    32  }
    33  
    34  func pprofByRegion(compute computePprofFunc, t *parsedTrace) traceviewer.ProfileFunc {
    35  	return func(r *http.Request) ([]traceviewer.ProfileRecord, error) {
    36  		filter, err := newRegionFilter(r)
    37  		if err != nil {
    38  			return nil, err
    39  		}
    40  		gToIntervals, err := pprofMatchingRegions(filter, t)
    41  		if err != nil {
    42  			return nil, err
    43  		}
    44  		return compute(gToIntervals, t.events)
    45  	}
    46  }
    47  
    48  // pprofMatchingGoroutines parses the goroutine type id string (i.e. pc)
    49  // and returns the ids of goroutines of the matching type and its interval.
    50  // If the id string is empty, returns nil without an error.
    51  func pprofMatchingGoroutines(id string, t *parsedTrace) (map[tracev2.GoID][]interval, error) {
    52  	if id == "" {
    53  		return nil, nil
    54  	}
    55  	pc, err := strconv.ParseUint(id, 10, 64) // id is string
    56  	if err != nil {
    57  		return nil, fmt.Errorf("invalid goroutine type: %v", id)
    58  	}
    59  	res := make(map[tracev2.GoID][]interval)
    60  	for _, g := range t.summary.Goroutines {
    61  		if g.PC != pc {
    62  			continue
    63  		}
    64  		endTime := g.EndTime
    65  		if g.EndTime == 0 {
    66  			endTime = t.endTime() // Use the trace end time, since the goroutine is still live then.
    67  		}
    68  		res[g.ID] = []interval{{start: g.StartTime, end: endTime}}
    69  	}
    70  	if len(res) == 0 && id != "" {
    71  		return nil, fmt.Errorf("failed to find matching goroutines for ID: %s", id)
    72  	}
    73  	return res, nil
    74  }
    75  
    76  // pprofMatchingRegions returns the time intervals of matching regions
    77  // grouped by the goroutine id. If the filter is nil, returns nil without an error.
    78  func pprofMatchingRegions(filter *regionFilter, t *parsedTrace) (map[tracev2.GoID][]interval, error) {
    79  	if filter == nil {
    80  		return nil, nil
    81  	}
    82  
    83  	gToIntervals := make(map[tracev2.GoID][]interval)
    84  	for _, g := range t.summary.Goroutines {
    85  		for _, r := range g.Regions {
    86  			if !filter.match(t, r) {
    87  				continue
    88  			}
    89  			gToIntervals[g.ID] = append(gToIntervals[g.ID], regionInterval(t, r))
    90  		}
    91  	}
    92  
    93  	for g, intervals := range gToIntervals {
    94  		// In order to remove nested regions and
    95  		// consider only the outermost regions,
    96  		// first, we sort based on the start time
    97  		// and then scan through to select only the outermost regions.
    98  		slices.SortFunc(intervals, func(a, b interval) int {
    99  			if c := cmp.Compare(a.start, b.start); c != 0 {
   100  				return c
   101  			}
   102  			return cmp.Compare(a.end, b.end)
   103  		})
   104  		var lastTimestamp tracev2.Time
   105  		var n int
   106  		// Select only the outermost regions.
   107  		for _, i := range intervals {
   108  			if lastTimestamp <= i.start {
   109  				intervals[n] = i // new non-overlapping region starts.
   110  				lastTimestamp = i.end
   111  				n++
   112  			}
   113  			// Otherwise, skip because this region overlaps with a previous region.
   114  		}
   115  		gToIntervals[g] = intervals[:n]
   116  	}
   117  	return gToIntervals, nil
   118  }
   119  
   120  type computePprofFunc func(gToIntervals map[tracev2.GoID][]interval, events []tracev2.Event) ([]traceviewer.ProfileRecord, error)
   121  
   122  // computePprofIO returns a computePprofFunc that generates IO pprof-like profile (time spent in
   123  // IO wait, currently only network blocking event).
   124  func computePprofIO() computePprofFunc {
   125  	return makeComputePprofFunc(tracev2.GoWaiting, func(reason string) bool {
   126  		return reason == "network"
   127  	})
   128  }
   129  
   130  // computePprofBlock returns a computePprofFunc that generates blocking pprof-like profile
   131  // (time spent blocked on synchronization primitives).
   132  func computePprofBlock() computePprofFunc {
   133  	return makeComputePprofFunc(tracev2.GoWaiting, func(reason string) bool {
   134  		return strings.Contains(reason, "chan") || strings.Contains(reason, "sync") || strings.Contains(reason, "select")
   135  	})
   136  }
   137  
   138  // computePprofSyscall returns a computePprofFunc that generates a syscall pprof-like
   139  // profile (time spent in syscalls).
   140  func computePprofSyscall() computePprofFunc {
   141  	return makeComputePprofFunc(tracev2.GoSyscall, func(_ string) bool {
   142  		return true
   143  	})
   144  }
   145  
   146  // computePprofSched returns a computePprofFunc that generates a scheduler latency pprof-like profile
   147  // (time between a goroutine become runnable and actually scheduled for execution).
   148  func computePprofSched() computePprofFunc {
   149  	return makeComputePprofFunc(tracev2.GoRunnable, func(_ string) bool {
   150  		return true
   151  	})
   152  }
   153  
   154  // makeComputePprofFunc returns a computePprofFunc that generates a profile of time goroutines spend
   155  // in a particular state for the specified reasons.
   156  func makeComputePprofFunc(state tracev2.GoState, trackReason func(string) bool) computePprofFunc {
   157  	return func(gToIntervals map[tracev2.GoID][]interval, events []tracev2.Event) ([]traceviewer.ProfileRecord, error) {
   158  		stacks := newStackMap()
   159  		tracking := make(map[tracev2.GoID]*tracev2.Event)
   160  		for i := range events {
   161  			ev := &events[i]
   162  
   163  			// Filter out any non-state-transitions and events without stacks.
   164  			if ev.Kind() != tracev2.EventStateTransition {
   165  				continue
   166  			}
   167  			stack := ev.Stack()
   168  			if stack == tracev2.NoStack {
   169  				continue
   170  			}
   171  
   172  			// The state transition has to apply to a goroutine.
   173  			st := ev.StateTransition()
   174  			if st.Resource.Kind != tracev2.ResourceGoroutine {
   175  				continue
   176  			}
   177  			id := st.Resource.Goroutine()
   178  			_, new := st.Goroutine()
   179  
   180  			// Check if we're tracking this goroutine.
   181  			startEv := tracking[id]
   182  			if startEv == nil {
   183  				// We're not. Start tracking if the new state
   184  				// matches what we want and the transition is
   185  				// for one of the reasons we care about.
   186  				if new == state && trackReason(st.Reason) {
   187  					tracking[id] = ev
   188  				}
   189  				continue
   190  			}
   191  			// We're tracking this goroutine.
   192  			if new == state {
   193  				// We're tracking this goroutine, but it's just transitioning
   194  				// to the same state (this is a no-ip
   195  				continue
   196  			}
   197  			// The goroutine has transitioned out of the state we care about,
   198  			// so remove it from tracking and record the stack.
   199  			delete(tracking, id)
   200  
   201  			overlapping := pprofOverlappingDuration(gToIntervals, id, interval{startEv.Time(), ev.Time()})
   202  			if overlapping > 0 {
   203  				rec := stacks.getOrAdd(startEv.Stack())
   204  				rec.Count++
   205  				rec.Time += overlapping
   206  			}
   207  		}
   208  		return stacks.profile(), nil
   209  	}
   210  }
   211  
   212  // pprofOverlappingDuration returns the overlapping duration between
   213  // the time intervals in gToIntervals and the specified event.
   214  // If gToIntervals is nil, this simply returns the event's duration.
   215  func pprofOverlappingDuration(gToIntervals map[tracev2.GoID][]interval, id tracev2.GoID, sample interval) time.Duration {
   216  	if gToIntervals == nil { // No filtering.
   217  		return sample.duration()
   218  	}
   219  	intervals := gToIntervals[id]
   220  	if len(intervals) == 0 {
   221  		return 0
   222  	}
   223  
   224  	var overlapping time.Duration
   225  	for _, i := range intervals {
   226  		if o := i.overlap(sample); o > 0 {
   227  			overlapping += o
   228  		}
   229  	}
   230  	return overlapping
   231  }
   232  
   233  // interval represents a time interval in the trace.
   234  type interval struct {
   235  	start, end tracev2.Time
   236  }
   237  
   238  func (i interval) duration() time.Duration {
   239  	return i.end.Sub(i.start)
   240  }
   241  
   242  func (i1 interval) overlap(i2 interval) time.Duration {
   243  	// Assume start1 <= end1 and start2 <= end2
   244  	if i1.end < i2.start || i2.end < i1.start {
   245  		return 0
   246  	}
   247  	if i1.start < i2.start { // choose the later one
   248  		i1.start = i2.start
   249  	}
   250  	if i1.end > i2.end { // choose the earlier one
   251  		i1.end = i2.end
   252  	}
   253  	return i1.duration()
   254  }
   255  
   256  // pprofMaxStack is the extent of the deduplication we're willing to do.
   257  //
   258  // Because slices aren't comparable and we want to leverage maps for deduplication,
   259  // we have to choose a fixed constant upper bound on the amount of frames we want
   260  // to support. In practice this is fine because there's a maximum depth to these
   261  // stacks anyway.
   262  const pprofMaxStack = 128
   263  
   264  // stackMap is a map of tracev2.Stack to some value V.
   265  type stackMap struct {
   266  	// stacks contains the full list of stacks in the set, however
   267  	// it is insufficient for deduplication because tracev2.Stack
   268  	// equality is only optimistic. If two tracev2.Stacks are equal,
   269  	// then they are guaranteed to be equal in content. If they are
   270  	// not equal, then they might still be equal in content.
   271  	stacks map[tracev2.Stack]*traceviewer.ProfileRecord
   272  
   273  	// pcs is the source-of-truth for deduplication. It is a map of
   274  	// the actual PCs in the stack to a tracev2.Stack.
   275  	pcs map[[pprofMaxStack]uint64]tracev2.Stack
   276  }
   277  
   278  func newStackMap() *stackMap {
   279  	return &stackMap{
   280  		stacks: make(map[tracev2.Stack]*traceviewer.ProfileRecord),
   281  		pcs:    make(map[[pprofMaxStack]uint64]tracev2.Stack),
   282  	}
   283  }
   284  
   285  func (m *stackMap) getOrAdd(stack tracev2.Stack) *traceviewer.ProfileRecord {
   286  	// Fast path: check to see if this exact stack is already in the map.
   287  	if rec, ok := m.stacks[stack]; ok {
   288  		return rec
   289  	}
   290  	// Slow path: the stack may still be in the map.
   291  
   292  	// Grab the stack's PCs as the source-of-truth.
   293  	var pcs [pprofMaxStack]uint64
   294  	pcsForStack(stack, &pcs)
   295  
   296  	// Check the source-of-truth.
   297  	var rec *traceviewer.ProfileRecord
   298  	if existing, ok := m.pcs[pcs]; ok {
   299  		// In the map.
   300  		rec = m.stacks[existing]
   301  		delete(m.stacks, existing)
   302  	} else {
   303  		// Not in the map.
   304  		rec = new(traceviewer.ProfileRecord)
   305  	}
   306  	// Insert regardless of whether we have a match in m.pcs.
   307  	// Even if we have a match, we want to keep the newest version
   308  	// of that stack, since we're much more likely tos see it again
   309  	// as we iterate through the trace linearly. Simultaneously, we
   310  	// are likely to never see the old stack again.
   311  	m.pcs[pcs] = stack
   312  	m.stacks[stack] = rec
   313  	return rec
   314  }
   315  
   316  func (m *stackMap) profile() []traceviewer.ProfileRecord {
   317  	prof := make([]traceviewer.ProfileRecord, 0, len(m.stacks))
   318  	for stack, record := range m.stacks {
   319  		rec := *record
   320  		i := 0
   321  		stack.Frames(func(frame tracev2.StackFrame) bool {
   322  			rec.Stack = append(rec.Stack, &trace.Frame{
   323  				PC:   frame.PC,
   324  				Fn:   frame.Func,
   325  				File: frame.File,
   326  				Line: int(frame.Line),
   327  			})
   328  			i++
   329  			// Cut this off at pprofMaxStack because that's as far
   330  			// as our deduplication goes.
   331  			return i < pprofMaxStack
   332  		})
   333  		prof = append(prof, rec)
   334  	}
   335  	return prof
   336  }
   337  
   338  // pcsForStack extracts the first pprofMaxStack PCs from stack into pcs.
   339  func pcsForStack(stack tracev2.Stack, pcs *[pprofMaxStack]uint64) {
   340  	i := 0
   341  	stack.Frames(func(frame tracev2.StackFrame) bool {
   342  		pcs[i] = frame.PC
   343  		i++
   344  		return i < len(pcs)
   345  	})
   346  }