github.com/GuanceCloud/cliutils@v1.1.21/pprofparser/service/parsing/collapse.go (about) 1 package parsing 2 3 import ( 4 "bufio" 5 "fmt" 6 "github.com/GuanceCloud/cliutils/pprofparser/domain/tracing" 7 "os" 8 "regexp" 9 "strconv" 10 "strings" 11 12 "github.com/GuanceCloud/cliutils/pprofparser/domain/events" 13 "github.com/GuanceCloud/cliutils/pprofparser/domain/parameter" 14 "github.com/GuanceCloud/cliutils/pprofparser/domain/pprof" 15 "github.com/GuanceCloud/cliutils/pprofparser/domain/quantity" 16 "github.com/GuanceCloud/cliutils/pprofparser/service/storage" 17 "github.com/GuanceCloud/cliutils/pprofparser/tools/filepathtoolkit" 18 "github.com/GuanceCloud/cliutils/pprofparser/tools/logtoolkit" 19 "github.com/GuanceCloud/cliutils/pprofparser/tools/parsetoolkit" 20 ) 21 22 /* 23 py-spy profiler output is as below: 24 25 process 95768:"/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/Resources/Python.app/Contents/MacOS/Python fibobacci.py";thread (0x100850580);<module> (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:14);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:5) 1 26 process 95768:"/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/Resources/Python.app/Contents/MacOS/Python fibobacci.py";thread (0x100850580);<module> (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:14);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:7) 1 27 process 95768:"/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/Resources/Python.app/Contents/MacOS/Python fibobacci.py";thread (0x100850580);<module> (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:14);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:5) 1 28 process 95768:"/opt/homebrew/Cellar/python@3.10/3.10.5/Frameworks/Python.framework/Versions/3.10/Resources/Python.app/Contents/MacOS/Python fibobacci.py";thread (0x100850580);<module> (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:14);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:8);fibonacci (/Users/zy/PycharmProjects/pyroscope-demo/fibobacci.py:5) 1 29 30 nginx;/usr/sbin/nginx+0x24678;__libc_start_main;main;ngx_master_process_cycle;/usr/sbin/nginx+0x4f0d8;ngx_spawn_process;/usr/sbin/nginx+0x4fa44;ngx_process_events_and_timers;/usr/sbin/nginx+0x51f54;epoll_pwait 1 31 32 */ 33 34 var processRegExp = regexp.MustCompile(`^process\s+\d+:"`) 35 var threadRexExp = regexp.MustCompile(`^thread\s+\([a-zA-Z\d]+\)$`) 36 var stackTraceRegExp = regexp.MustCompile(`^(\S+)(?: +\(([^:]+):(\d+)\))?`) 37 38 type Collapse struct { 39 workspaceUUID string 40 profiles []*parameter.Profile 41 filterBySpan bool 42 spanIDSet *tracing.SpanIDSet 43 } 44 45 func NewCollapse(workspaceUUID string, profiles []*parameter.Profile, 46 filterBySpan bool, spanIDSet *tracing.SpanIDSet) *Collapse { 47 return &Collapse{ 48 workspaceUUID: workspaceUUID, 49 profiles: profiles, 50 filterBySpan: filterBySpan, 51 spanIDSet: spanIDSet, 52 } 53 } 54 55 func summary(filename string) (map[events.Type]*EventSummary, error) { 56 f, err := os.Open(filename) 57 if err != nil { 58 return nil, fmt.Errorf("open profile file [%s] fail: %w", filename, err) 59 } 60 defer f.Close() 61 62 sampleSummary := &EventSummary{ 63 SummaryValueType: &SummaryValueType{ 64 Type: events.CpuSamples, 65 Unit: quantity.CountUnit, 66 }, 67 Value: 0, 68 } 69 70 spySummaries := map[events.Type]*EventSummary{ 71 events.CpuSamples: sampleSummary, 72 } 73 74 scanner := bufio.NewScanner(f) 75 for scanner.Scan() { 76 line := strings.TrimSpace(scanner.Text()) 77 if len(line) < 2 { 78 continue 79 } 80 81 blankIdx := strings.LastIndexByte(line, ' ') 82 if blankIdx < 0 { 83 logtoolkit.Errorf("py-spy profile doesn't contain any blank [line: %s]", line) 84 continue 85 } 86 n, err := strconv.ParseInt(strings.TrimSpace(line[blankIdx+1:]), 10, 64) 87 if err != nil { 88 logtoolkit.Errorf("resolve sample count fail [line: %s]: %w", line, err) 89 continue 90 } 91 sampleSummary.Value += n 92 } 93 return spySummaries, nil 94 } 95 96 func (p *Collapse) Summary() (map[events.Type]*EventSummary, int64, error) { 97 98 prof := p.profiles[0] 99 100 startNanos, err := prof.StartTime() 101 if err != nil { 102 return nil, 0, fmt.Errorf("resolve Profile start timestamp fail: %w", err) 103 } 104 endNanos, err := prof.EndTime() 105 if err != nil { 106 return nil, 0, fmt.Errorf("resolve Profile end timestamp fail: %w", err) 107 } 108 109 filename := storage.DefaultDiskStorage.GetProfilePath(p.workspaceUUID, prof.ProfileID, startNanos, events.DefaultProfileFilename) 110 111 summaries, err := summary(filename) 112 if err != nil { 113 return nil, 0, fmt.Errorf("resolve collapse summary fail: %w", err) 114 } 115 116 return summaries, endNanos - startNanos, nil 117 } 118 119 func IsCollapseProfile(profiles []*parameter.Profile, workspaceUUID string) (bool, error) { 120 // 当前 py-spy 一次只有一条profile数据 121 if len(profiles) > 1 { 122 return false, nil 123 } 124 125 metadata, err := ReadMetaData(profiles[0], workspaceUUID) 126 if err != nil { 127 return false, fmt.Errorf("read py-spy metadata file fail: %w", err) 128 } 129 130 return metadata.Format == RawFlameGraph || metadata.Format == Collapsed, nil 131 } 132 133 func (p *Collapse) ResolveFlameGraph(_ events.Type) (*pprof.Frame, AggregatorSelectSlice, error) { 134 135 prof := p.profiles[0] 136 137 startNanos, err := prof.StartTime() 138 if err != nil { 139 return nil, nil, fmt.Errorf("invalid profile start: %w", err) 140 } 141 file := storage.DefaultDiskStorage.GetProfilePath(p.workspaceUUID, prof.ProfileID, startNanos, events.DefaultProfileFilename) 142 143 f, err := os.Open(file) 144 if err != nil { 145 return nil, nil, fmt.Errorf("open py-spy profile file fail: %w", err) 146 } 147 defer f.Close() 148 149 scanner := bufio.NewScanner(f) 150 151 rootFrame := &pprof.Frame{ 152 SubFrames: make(pprof.SubFrames), 153 } 154 totalValue := int64(0) 155 156 aggregatorSelects := make(AggregatorSelectSlice, 0, len(SpyAggregatorList)) 157 158 for _, aggregator := range SpyAggregatorList { 159 aggregatorSelects = append(aggregatorSelects, &AggregatorSelect{ 160 Aggregator: aggregator, 161 Mapping: aggregator.Mapping, 162 Options: make(map[string]*AggregatorOption), 163 }) 164 } 165 166 for scanner.Scan() { 167 168 line := strings.TrimSpace(scanner.Text()) 169 if len(line) == 0 { 170 continue 171 } 172 173 stacks := strings.Split(line, ";") 174 if len(stacks) == 0 { 175 continue 176 } 177 178 lastStack := strings.TrimSpace(stacks[len(stacks)-1]) 179 if !stackTraceRegExp.MatchString(lastStack) { 180 logtoolkit.Warnf("The last stacktrace not match with the regexp [%s], the stacktrace [%s]", stackTraceRegExp.String(), lastStack) 181 continue 182 } 183 blankIdx := strings.LastIndexByte(lastStack, ' ') 184 if blankIdx < 0 { 185 logtoolkit.Warnf("Can not find any blank from [%s]", lastStack) 186 continue 187 } 188 189 sampleCount, err := strconv.ParseInt(lastStack[blankIdx+1:], 10, 64) 190 if err != nil { 191 logtoolkit.Warnf("Can not resolve sample count from [%s]", lastStack) 192 continue 193 } 194 totalValue += sampleCount 195 196 currentFrame := rootFrame 197 threadName := "<unknown>" 198 199 for idx, stack := range stacks { 200 stack = strings.TrimSpace(stack) 201 matches := stackTraceRegExp.FindStringSubmatch(stack) 202 if len(matches) != 4 { 203 if processRegExp.MatchString(stack) { 204 continue 205 } else if threadRexExp.MatchString(stack) { 206 threadName = stack 207 continue 208 } else { 209 return nil, nil, fmt.Errorf("resolve stacktrace from profiling file fail") 210 } 211 } 212 213 funcName, codeFile, lineNoStr := matches[1], matches[2], matches[3] 214 215 if codeFile == "" { 216 codeFile = "<unknown>" 217 } 218 219 var lineNo int64 = -1 220 if lineNoStr != "" { 221 lineNo, _ = strconv.ParseInt(lineNoStr, 10, 64) 222 } 223 224 funcIdentifier := fmt.Sprintf("%s###%s###%s###%d", threadName, codeFile, funcName, lineNo) 225 226 if idx == len(stacks)-1 { 227 228 for _, aggregatorSelect := range aggregatorSelects { 229 230 var identifier string 231 var displayStr string 232 var mappingValues []string 233 234 switch aggregatorSelect.Aggregator { 235 case Function: 236 identifier = fmt.Sprintf("%s###%s", codeFile, funcName) 237 displayStr = GetSpyPrintStr(funcName, codeFile) 238 mappingValues = []string{funcName} 239 case FunctionLine: 240 identifier = fmt.Sprintf("%s###%s###%d", codeFile, funcName, lineNo) 241 displayStr = fmt.Sprintf("%s(%s:L#%d)", funcName, filepathtoolkit.BaseName(codeFile), lineNo) 242 mappingValues = []string{funcName, fmt.Sprintf("%d", lineNo)} 243 case Directory: 244 identifier = filepathtoolkit.DirName(codeFile) 245 displayStr = identifier 246 mappingValues = []string{identifier} 247 case File: 248 identifier = codeFile 249 displayStr = codeFile 250 mappingValues = []string{codeFile} 251 case ThreadName: 252 identifier = threadName 253 displayStr = threadName 254 mappingValues = []string{threadName} 255 } 256 257 if _, ok := aggregatorSelect.Options[identifier]; ok { 258 aggregatorSelect.Options[identifier].Value += sampleCount 259 } else { 260 aggregatorSelect.Options[identifier] = &AggregatorOption{ 261 Title: displayStr, 262 Value: sampleCount, 263 Unit: quantity.CountUnit, 264 MappingValues: mappingValues, 265 } 266 } 267 } 268 } 269 270 subFrame, ok := currentFrame.SubFrames[funcIdentifier] 271 272 if ok { 273 subFrame.Value += sampleCount 274 } else { 275 subFrame = &pprof.Frame{ 276 Value: sampleCount, 277 Unit: quantity.CountUnit, 278 Function: funcName, 279 Line: lineNo, 280 File: codeFile, 281 Directory: filepathtoolkit.DirName(codeFile), 282 ThreadID: "", 283 ThreadName: threadName, 284 Package: "", 285 PrintString: GetSpyPrintStr(funcName, codeFile), 286 SubFrames: make(pprof.SubFrames), 287 } 288 currentFrame.SubFrames[funcIdentifier] = subFrame 289 } 290 291 currentFrame = subFrame 292 } 293 } 294 295 rootFrame.Value = totalValue 296 rootFrame.Unit = quantity.CountUnit 297 298 parsetoolkit.CalcPercentAndQuantity(rootFrame, totalValue) 299 aggregatorSelects.CalcPercentAndQuantity(totalValue) 300 return rootFrame, aggregatorSelects, nil 301 } 302 303 func ParseRawFlameGraph(filename string) (*pprof.Frame, AggregatorSelectSlice, error) { 304 f, err := os.Open(filename) 305 if err != nil { 306 return nil, nil, fmt.Errorf("open py-spy profile file fail: %w", err) 307 } 308 defer f.Close() 309 310 scanner := bufio.NewScanner(f) 311 312 rootFrame := &pprof.Frame{ 313 SubFrames: make(pprof.SubFrames), 314 } 315 totalValue := int64(0) 316 317 aggregatorSelects := make(AggregatorSelectSlice, 0, len(SpyAggregatorList)) 318 319 for _, aggregator := range SpyAggregatorList { 320 aggregatorSelects = append(aggregatorSelects, &AggregatorSelect{ 321 Aggregator: aggregator, 322 Mapping: aggregator.Mapping, 323 Options: make(map[string]*AggregatorOption), 324 }) 325 } 326 327 for scanner.Scan() { 328 329 line := strings.TrimSpace(scanner.Text()) 330 if len(line) == 0 { 331 continue 332 } 333 334 stacks := strings.Split(line, ";") 335 if len(stacks) == 0 { 336 continue 337 } 338 339 lastStack := strings.TrimSpace(stacks[len(stacks)-1]) 340 if !stackTraceRegExp.MatchString(lastStack) { 341 logtoolkit.Warnf("The last stacktrace not match with the regexp [%s], the stacktrace [%s]", stackTraceRegExp.String(), lastStack) 342 continue 343 } 344 blankIdx := strings.LastIndexByte(lastStack, ' ') 345 if blankIdx < 0 { 346 logtoolkit.Warnf("Can not find any blank from [%s]", lastStack) 347 continue 348 } 349 350 sampleCount, err := strconv.ParseInt(lastStack[blankIdx+1:], 10, 64) 351 if err != nil { 352 logtoolkit.Warnf("Can not resolve sample count from [%s]", lastStack) 353 continue 354 } 355 totalValue += sampleCount 356 357 currentFrame := rootFrame 358 threadName := "<unknown>" 359 360 for idx, stack := range stacks { 361 stack = strings.TrimSpace(stack) 362 matches := stackTraceRegExp.FindStringSubmatch(stack) 363 if len(matches) != 4 { 364 if processRegExp.MatchString(stack) { 365 continue 366 } else if threadRexExp.MatchString(stack) { 367 threadName = stack 368 continue 369 } else { 370 return nil, nil, fmt.Errorf("resolve stacktrace from profiling file fail") 371 } 372 } 373 374 funcName, codeFile, lineNoStr := matches[1], matches[2], matches[3] 375 376 if codeFile == "" { 377 codeFile = "<unknown>" 378 } 379 380 var lineNo int64 = -1 381 if lineNoStr != "" { 382 lineNo, _ = strconv.ParseInt(lineNoStr, 10, 64) 383 } 384 385 funcIdentifier := fmt.Sprintf("%s###%s###%s###%d", threadName, codeFile, funcName, lineNo) 386 387 if idx == len(stacks)-1 { 388 389 for _, aggregatorSelect := range aggregatorSelects { 390 391 var identifier string 392 var displayStr string 393 var mappingValues []string 394 395 switch aggregatorSelect.Aggregator { 396 case Function: 397 identifier = fmt.Sprintf("%s###%s", codeFile, funcName) 398 displayStr = GetSpyPrintStr(funcName, codeFile) 399 mappingValues = []string{funcName} 400 case FunctionLine: 401 identifier = fmt.Sprintf("%s###%s###%d", codeFile, funcName, lineNo) 402 displayStr = fmt.Sprintf("%s(%s:L#%d)", funcName, filepathtoolkit.BaseName(codeFile), lineNo) 403 mappingValues = []string{funcName, fmt.Sprintf("%d", lineNo)} 404 case Directory: 405 identifier = filepathtoolkit.DirName(codeFile) 406 displayStr = identifier 407 mappingValues = []string{identifier} 408 case File: 409 identifier = codeFile 410 displayStr = codeFile 411 mappingValues = []string{codeFile} 412 case ThreadName: 413 identifier = threadName 414 displayStr = threadName 415 mappingValues = []string{threadName} 416 } 417 418 if _, ok := aggregatorSelect.Options[identifier]; ok { 419 aggregatorSelect.Options[identifier].Value += sampleCount 420 } else { 421 aggregatorSelect.Options[identifier] = &AggregatorOption{ 422 Title: displayStr, 423 Value: sampleCount, 424 Unit: quantity.CountUnit, 425 MappingValues: mappingValues, 426 } 427 } 428 } 429 } 430 431 subFrame, ok := currentFrame.SubFrames[funcIdentifier] 432 433 if ok { 434 subFrame.Value += sampleCount 435 } else { 436 subFrame = &pprof.Frame{ 437 Value: sampleCount, 438 Unit: quantity.CountUnit, 439 Function: funcName, 440 Line: lineNo, 441 File: codeFile, 442 Directory: filepathtoolkit.DirName(codeFile), 443 ThreadID: "", 444 ThreadName: threadName, 445 Package: "", 446 PrintString: GetSpyPrintStr(funcName, codeFile), 447 SubFrames: make(pprof.SubFrames), 448 } 449 currentFrame.SubFrames[funcIdentifier] = subFrame 450 } 451 452 currentFrame = subFrame 453 } 454 } 455 456 rootFrame.Value = totalValue 457 rootFrame.Unit = quantity.CountUnit 458 459 parsetoolkit.CalcPercentAndQuantity(rootFrame, totalValue) 460 aggregatorSelects.CalcPercentAndQuantity(totalValue) 461 return rootFrame, aggregatorSelects, nil 462 }