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 }