codekingpro/portable-devtools
114k
1// Copyright 2014 The Go Authors. All rights reserved.2// Use of this source code is governed by a BSD-style3// license that can be found in the LICENSE file.4 5// Serving of pprof-like profiles.6 7package main8 9import (10 "cmp"11 "fmt"12 "internal/trace"13 "internal/trace/traceviewer"14 "net/http"15 "slices"16 "strings"17 "time"18)19 20func pprofByGoroutine(compute computePprofFunc, t *parsedTrace) traceviewer.ProfileFunc {21 return func(r *http.Request) ([]traceviewer.ProfileRecord, error) {22 name := r.FormValue("name")23 gToIntervals, err := pprofMatchingGoroutines(name, t)24 if err != nil {25 return nil, err26 }27 return compute(gToIntervals, t.events)28 }29}30 31func pprofByRegion(compute computePprofFunc, t *parsedTrace) traceviewer.ProfileFunc {32 return func(r *http.Request) ([]traceviewer.ProfileRecord, error) {33 filter, err := newRegionFilter(r)34 if err != nil {35 return nil, err36 }37 gToIntervals, err := pprofMatchingRegions(filter, t)38 if err != nil {39 return nil, err40 }41 return compute(gToIntervals, t.events)42 }43}44 45// pprofMatchingGoroutines returns the ids of goroutines of the matching name and its interval.46// If the id string is empty, returns nil without an error.47func pprofMatchingGoroutines(name string, t *parsedTrace) (map[trace.GoID][]interval, error) {48 res := make(map[trace.GoID][]interval)49 for _, g := range t.summary.Goroutines {50 if name != "" && g.Name != name {51 continue52 }53 endTime := g.EndTime54 if g.EndTime == 0 {55 endTime = t.endTime() // Use the trace end time, since the goroutine is still live then.56 }57 res[g.ID] = []interval{{start: g.StartTime, end: endTime}}58 }59 if len(res) == 0 {60 return nil, fmt.Errorf("failed to find matching goroutines for name: %s", name)61 }62 return res, nil63}64 65// pprofMatchingRegions returns the time intervals of matching regions66// grouped by the goroutine id. If the filter is nil, returns nil without an error.67func pprofMatchingRegions(filter *regionFilter, t *parsedTrace) (map[trace.GoID][]interval, error) {68 if filter == nil {69 return nil, nil70 }71 72 gToIntervals := make(map[trace.GoID][]interval)73 for _, g := range t.summary.Goroutines {74 for _, r := range g.Regions {75 if !filter.match(t, r) {76 continue77 }78 gToIntervals[g.ID] = append(gToIntervals[g.ID], regionInterval(t, r))79 }80 }81 82 for g, intervals := range gToIntervals {83 // In order to remove nested regions and84 // consider only the outermost regions,85 // first, we sort based on the start time86 // and then scan through to select only the outermost regions.87 slices.SortFunc(intervals, func(a, b interval) int {88 if c := cmp.Compare(a.start, b.start); c != 0 {89 return c90 }91 return cmp.Compare(a.end, b.end)92 })93 var lastTimestamp trace.Time94 var n int95 // Select only the outermost regions.96 for _, i := range intervals {97 if lastTimestamp <= i.start {98 intervals[n] = i // new non-overlapping region starts.99 lastTimestamp = i.end100 n++101 }102 // Otherwise, skip because this region overlaps with a previous region.103 }104 gToIntervals[g] = intervals[:n]105 }106 return gToIntervals, nil107}108 109type computePprofFunc func(gToIntervals map[trace.GoID][]interval, events []trace.Event) ([]traceviewer.ProfileRecord, error)110 111// computePprofIO returns a computePprofFunc that generates IO pprof-like profile (time spent in112// IO wait, currently only network blocking event).113func computePprofIO() computePprofFunc {114 return makeComputePprofFunc(trace.GoWaiting, func(reason string) bool {115 return reason == "network"116 })117}118 119// computePprofBlock returns a computePprofFunc that generates blocking pprof-like profile120// (time spent blocked on synchronization primitives).121func computePprofBlock() computePprofFunc {122 return makeComputePprofFunc(trace.GoWaiting, func(reason string) bool {123 return strings.Contains(reason, "chan") || strings.Contains(reason, "sync") || strings.Contains(reason, "select")124 })125}126 127// computePprofSyscall returns a computePprofFunc that generates a syscall pprof-like128// profile (time spent in syscalls).129func computePprofSyscall() computePprofFunc {130 return makeComputePprofFunc(trace.GoSyscall, func(_ string) bool {131 return true132 })133}134 135// computePprofSched returns a computePprofFunc that generates a scheduler latency pprof-like profile136// (time between a goroutine become runnable and actually scheduled for execution).137func computePprofSched() computePprofFunc {138 return makeComputePprofFunc(trace.GoRunnable, func(_ string) bool {139 return true140 })141}142 143// makeComputePprofFunc returns a computePprofFunc that generates a profile of time goroutines spend144// in a particular state for the specified reasons.145func makeComputePprofFunc(state trace.GoState, trackReason func(string) bool) computePprofFunc {146 return func(gToIntervals map[trace.GoID][]interval, events []trace.Event) ([]traceviewer.ProfileRecord, error) {147 stacks := newStackMap()148 tracking := make(map[trace.GoID]*trace.Event)149 for i := range events {150 ev := &events[i]151 152 // Filter out any non-state-transitions and events without stacks.153 if ev.Kind() != trace.EventStateTransition {154 continue155 }156 157 // The state transition has to apply to a goroutine.158 st := ev.StateTransition()159 if st.Resource.Kind != trace.ResourceGoroutine {160 continue161 }162 id := st.Resource.Goroutine()163 _, new := st.Goroutine()164 165 // Check if we're tracking this goroutine.166 startEv := tracking[id]167 if startEv == nil {168 // We're not. Start tracking if the new state169 // matches what we want and the transition is170 // for one of the reasons we care about.171 if new == state && trackReason(st.Reason) {172 tracking[id] = ev173 }174 continue175 }176 // We're tracking this goroutine.177 if new == state {178 // We're tracking this goroutine, but it's just transitioning179 // to the same state (this is a no-ip180 continue181 }182 // The goroutine has transitioned out of the state we care about,183 // so remove it from tracking and record the stack.184 delete(tracking, id)185 186 overlapping := pprofOverlappingDuration(gToIntervals, id, interval{startEv.Time(), ev.Time()})187 if overlapping > 0 {188 rec := stacks.getOrAdd(startEv.Stack())189 rec.Count++190 rec.Time += overlapping191 }192 }193 return stacks.profile(), nil194 }195}196 197// pprofOverlappingDuration returns the overlapping duration between198// the time intervals in gToIntervals and the specified event.199// If gToIntervals is nil, this simply returns the event's duration.200func pprofOverlappingDuration(gToIntervals map[trace.GoID][]interval, id trace.GoID, sample interval) time.Duration {201 if gToIntervals == nil { // No filtering.202 return sample.duration()203 }204 intervals := gToIntervals[id]205 if len(intervals) == 0 {206 return 0207 }208 209 var overlapping time.Duration210 for _, i := range intervals {211 if o := i.overlap(sample); o > 0 {212 overlapping += o213 }214 }215 return overlapping216}217 218// interval represents a time interval in the trace.219type interval struct {220 start, end trace.Time221}222 223func (i interval) duration() time.Duration {224 return i.end.Sub(i.start)225}226 227func (i1 interval) overlap(i2 interval) time.Duration {228 // Assume start1 <= end1 and start2 <= end2229 if i1.end < i2.start || i2.end < i1.start {230 return 0231 }232 if i1.start < i2.start { // choose the later one233 i1.start = i2.start234 }235 if i1.end > i2.end { // choose the earlier one236 i1.end = i2.end237 }238 return i1.duration()239}240 241// pprofMaxStack is the extent of the deduplication we're willing to do.242//243// Because slices aren't comparable and we want to leverage maps for deduplication,244// we have to choose a fixed constant upper bound on the amount of frames we want245// to support. In practice this is fine because there's a maximum depth to these246// stacks anyway.247const pprofMaxStack = 128248 249// stackMap is a map of trace.Stack to some value V.250type stackMap struct {251 // stacks contains the full list of stacks in the set, however252 // it is insufficient for deduplication because trace.Stack253 // equality is only optimistic. If two trace.Stacks are equal,254 // then they are guaranteed to be equal in content. If they are255 // not equal, then they might still be equal in content.256 stacks map[trace.Stack]*traceviewer.ProfileRecord257 258 // pcs is the source-of-truth for deduplication. It is a map of259 // the actual PCs in the stack to a trace.Stack.260 pcs map[[pprofMaxStack]uint64]trace.Stack261}262 263func newStackMap() *stackMap {264 return &stackMap{265 stacks: make(map[trace.Stack]*traceviewer.ProfileRecord),266 pcs: make(map[[pprofMaxStack]uint64]trace.Stack),267 }268}269 270func (m *stackMap) getOrAdd(stack trace.Stack) *traceviewer.ProfileRecord {271 // Fast path: check to see if this exact stack is already in the map.272 if rec, ok := m.stacks[stack]; ok {273 return rec274 }275 // Slow path: the stack may still be in the map.276 277 // Grab the stack's PCs as the source-of-truth.278 var pcs [pprofMaxStack]uint64279 pcsForStack(stack, &pcs)280 281 // Check the source-of-truth.282 var rec *traceviewer.ProfileRecord283 if existing, ok := m.pcs[pcs]; ok {284 // In the map.285 rec = m.stacks[existing]286 delete(m.stacks, existing)287 } else {288 // Not in the map.289 rec = new(traceviewer.ProfileRecord)290 }291 // Insert regardless of whether we have a match in m.pcs.292 // Even if we have a match, we want to keep the newest version293 // of that stack, since we're much more likely tos see it again294 // as we iterate through the trace linearly. Simultaneously, we295 // are likely to never see the old stack again.296 m.pcs[pcs] = stack297 m.stacks[stack] = rec298 return rec299}300 301func (m *stackMap) profile() []traceviewer.ProfileRecord {302 prof := make([]traceviewer.ProfileRecord, 0, len(m.stacks))303 for stack, record := range m.stacks {304 rec := *record305 var i int306 for frame := range stack.Frames() {307 rec.Stack = append(rec.Stack, frame)308 // Cut this off at pprofMaxStack because that's as far309 // as our deduplication goes.310 if i >= pprofMaxStack {311 break312 }313 i++314 }315 prof = append(prof, rec)316 }317 return prof318}319 320// pcsForStack extracts the first pprofMaxStack PCs from stack into pcs.321func pcsForStack(stack trace.Stack, pcs *[pprofMaxStack]uint64) {322 for i, frame := range slices.Collect(stack.Frames()) {323 pcs[i] = frame.PC324 if i >= len(pcs) {325 break326 }327 }328}329 