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 7package main 8 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, err 26 } 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, err 36 } 37 gToIntervals, err := pprofMatchingRegions(filter, t) 38 if err != nil { 39 return nil, err 40 } 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 continue 52 } 53 endTime := g.EndTime 54 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, nil 63} 64 65// pprofMatchingRegions returns the time intervals of matching regions 66// 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, nil 70 } 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 continue 77 } 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 and 84 // consider only the outermost regions, 85 // first, we sort based on the start time 86 // 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 c 90 } 91 return cmp.Compare(a.end, b.end) 92 }) 93 var lastTimestamp trace.Time 94 var n int 95 // 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.end 100 n++ 101 } 102 // Otherwise, skip because this region overlaps with a previous region. 103 } 104 gToIntervals[g] = intervals[:n] 105 } 106 return gToIntervals, nil 107} 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 in 112// 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 profile 120// (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-like 128// profile (time spent in syscalls). 129func computePprofSyscall() computePprofFunc { 130 return makeComputePprofFunc(trace.GoSyscall, func(_ string) bool { 131 return true 132 }) 133} 134 135// computePprofSched returns a computePprofFunc that generates a scheduler latency pprof-like profile 136// (time between a goroutine become runnable and actually scheduled for execution). 137func computePprofSched() computePprofFunc { 138 return makeComputePprofFunc(trace.GoRunnable, func(_ string) bool { 139 return true 140 }) 141} 142 143// makeComputePprofFunc returns a computePprofFunc that generates a profile of time goroutines spend 144// 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 continue 155 } 156 stack := ev.Stack() 157 if stack == trace.NoStack { 158 continue 159 } 160 161 // The state transition has to apply to a goroutine. 162 st := ev.StateTransition() 163 if st.Resource.Kind != trace.ResourceGoroutine { 164 continue 165 } 166 id := st.Resource.Goroutine() 167 _, new := st.Goroutine() 168 169 // Check if we're tracking this goroutine. 170 startEv := tracking[id] 171 if startEv == nil { 172 // We're not. Start tracking if the new state 173 // matches what we want and the transition is 174 // for one of the reasons we care about. 175 if new == state && trackReason(st.Reason) { 176 tracking[id] = ev 177 } 178 continue 179 } 180 // We're tracking this goroutine. 181 if new == state { 182 // We're tracking this goroutine, but it's just transitioning 183 // to the same state (this is a no-ip 184 continue 185 } 186 // The goroutine has transitioned out of the state we care about, 187 // so remove it from tracking and record the stack. 188 delete(tracking, id) 189 190 overlapping := pprofOverlappingDuration(gToIntervals, id, interval{startEv.Time(), ev.Time()}) 191 if overlapping > 0 { 192 rec := stacks.getOrAdd(startEv.Stack()) 193 rec.Count++ 194 rec.Time += overlapping 195 } 196 } 197 return stacks.profile(), nil 198 } 199} 200 201// pprofOverlappingDuration returns the overlapping duration between 202// the time intervals in gToIntervals and the specified event. 203// If gToIntervals is nil, this simply returns the event's duration. 204func pprofOverlappingDuration(gToIntervals map[trace.GoID][]interval, id trace.GoID, sample interval) time.Duration { 205 if gToIntervals == nil { // No filtering. 206 return sample.duration() 207 } 208 intervals := gToIntervals[id] 209 if len(intervals) == 0 { 210 return 0 211 } 212 213 var overlapping time.Duration 214 for _, i := range intervals { 215 if o := i.overlap(sample); o > 0 { 216 overlapping += o 217 } 218 } 219 return overlapping 220} 221 222// interval represents a time interval in the trace. 223type interval struct { 224 start, end trace.Time 225} 226 227func (i interval) duration() time.Duration { 228 return i.end.Sub(i.start) 229} 230 231func (i1 interval) overlap(i2 interval) time.Duration { 232 // Assume start1 <= end1 and start2 <= end2 233 if i1.end < i2.start || i2.end < i1.start { 234 return 0 235 } 236 if i1.start < i2.start { // choose the later one 237 i1.start = i2.start 238 } 239 if i1.end > i2.end { // choose the earlier one 240 i1.end = i2.end 241 } 242 return i1.duration() 243} 244 245// pprofMaxStack is the extent of the deduplication we're willing to do. 246// 247// Because slices aren't comparable and we want to leverage maps for deduplication, 248// we have to choose a fixed constant upper bound on the amount of frames we want 249// to support. In practice this is fine because there's a maximum depth to these 250// stacks anyway. 251const pprofMaxStack = 128 252 253// stackMap is a map of trace.Stack to some value V. 254type stackMap struct { 255 // stacks contains the full list of stacks in the set, however 256 // it is insufficient for deduplication because trace.Stack 257 // equality is only optimistic. If two trace.Stacks are equal, 258 // then they are guaranteed to be equal in content. If they are 259 // not equal, then they might still be equal in content. 260 stacks map[trace.Stack]*traceviewer.ProfileRecord 261 262 // pcs is the source-of-truth for deduplication. It is a map of 263 // the actual PCs in the stack to a trace.Stack. 264 pcs map[[pprofMaxStack]uint64]trace.Stack 265} 266 267func newStackMap() *stackMap { 268 return &stackMap{ 269 stacks: make(map[trace.Stack]*traceviewer.ProfileRecord), 270 pcs: make(map[[pprofMaxStack]uint64]trace.Stack), 271 } 272} 273 274func (m *stackMap) getOrAdd(stack trace.Stack) *traceviewer.ProfileRecord { 275 // Fast path: check to see if this exact stack is already in the map. 276 if rec, ok := m.stacks[stack]; ok { 277 return rec 278 } 279 // Slow path: the stack may still be in the map. 280 281 // Grab the stack's PCs as the source-of-truth. 282 var pcs [pprofMaxStack]uint64 283 pcsForStack(stack, &pcs) 284 285 // Check the source-of-truth. 286 var rec *traceviewer.ProfileRecord 287 if existing, ok := m.pcs[pcs]; ok { 288 // In the map. 289 rec = m.stacks[existing] 290 delete(m.stacks, existing) 291 } else { 292 // Not in the map. 293 rec = new(traceviewer.ProfileRecord) 294 } 295 // Insert regardless of whether we have a match in m.pcs. 296 // Even if we have a match, we want to keep the newest version 297 // of that stack, since we're much more likely tos see it again 298 // as we iterate through the trace linearly. Simultaneously, we 299 // are likely to never see the old stack again. 300 m.pcs[pcs] = stack 301 m.stacks[stack] = rec 302 return rec 303} 304 305func (m *stackMap) profile() []traceviewer.ProfileRecord { 306 prof := make([]traceviewer.ProfileRecord, 0, len(m.stacks)) 307 for stack, record := range m.stacks { 308 rec := *record 309 i := 0 310 stack.Frames(func(frame trace.StackFrame) bool { 311 rec.Stack = append(rec.Stack, &trace.Frame{ 312 PC: frame.PC, 313 Fn: frame.Func, 314 File: frame.File, 315 Line: int(frame.Line), 316 }) 317 i++ 318 // Cut this off at pprofMaxStack because that's as far 319 // as our deduplication goes. 320 return i < pprofMaxStack 321 }) 322 prof = append(prof, rec) 323 } 324 return prof 325} 326 327// pcsForStack extracts the first pprofMaxStack PCs from stack into pcs. 328func pcsForStack(stack trace.Stack, pcs *[pprofMaxStack]uint64) { 329 i := 0 330 stack.Frames(func(frame trace.StackFrame) bool { 331 pcs[i] = frame.PC 332 i++ 333 return i < len(pcs) 334 }) 335} 336