github.com/GuanceCloud/cliutils@v1.1.21/pprofparser/service/parsing/aggregators.go (about) 1 package parsing 2 3 import ( 4 "encoding/json" 5 "fmt" 6 "sort" 7 "strconv" 8 "strings" 9 10 "github.com/GuanceCloud/cliutils/pprofparser/domain/languages" 11 "github.com/GuanceCloud/cliutils/pprofparser/domain/pprof" 12 "github.com/GuanceCloud/cliutils/pprofparser/domain/quantity" 13 "github.com/GuanceCloud/cliutils/pprofparser/tools/filepathtoolkit" 14 "github.com/GuanceCloud/cliutils/pprofparser/tools/logtoolkit" 15 "github.com/GuanceCloud/cliutils/pprofparser/tools/parsetoolkit" 16 "github.com/google/pprof/profile" 17 ) 18 19 // [|lm:System.Private.CoreLib;|ns:System.Diagnostics.Tracing;|ct:EventSource;|fn:DebugCheckEvent] 20 21 type DDFieldTag string 22 23 const UnknownInfo = "<unknown>" 24 25 const ( 26 AssemblyTag DDFieldTag = "|lm:" 27 NamespaceTag DDFieldTag = "|ns:" 28 ClassTag DDFieldTag = "|ct:" 29 MethodTag DDFieldTag = "|fn:" 30 ) 31 32 var ddDotnetFieldIdx = map[DDFieldTag]int{ 33 AssemblyTag: 0, 34 NamespaceTag: 1, 35 ClassTag: 2, 36 MethodTag: 3, 37 } 38 39 type GetPropertyByLine func(lang languages.Lang, line profile.Line) string 40 41 var ( 42 getFuncName = getPropertyCallable(getFuncNameByLine) 43 getMethod = getPropertyCallable(getMethodByLine) 44 getClass = getPropertyCallable(getClassByLine) 45 getNamespace = getPropertyCallable(getNamespaceByLine) 46 getAssembly = getPropertyCallable(getAssemblyByLine) 47 getFuncDisplayStr = getPropertyCallable(GetPrintStrByLine) 48 getDirectory = getPropertyCallable(getDirectoryByLine) 49 getFile = getPropertyCallable(getFileByLine) 50 getPackageName = getPropertyCallable(getPackageNameByLine) 51 getLine = getPropertyCallable(func(lang languages.Lang, line profile.Line) string { 52 return strconv.FormatInt(getLineByLine(lang, line), 10) 53 }) 54 ) 55 56 func getFuncNameByLine(lang languages.Lang, line profile.Line) string { 57 switch lang { 58 case languages.NodeJS: 59 segments := strings.Split(line.Function.Name, ":") 60 if len(segments) > 1 { 61 return segments[len(segments)-2] 62 } 63 return UnknownInfo 64 case languages.DotNet: 65 return getDDDotnetMethodName(line.Function.Name) 66 case languages.PHP: 67 return getPHPBaseFuncName(line.Function.Name) 68 } 69 return line.Function.Name 70 } 71 72 func getDDDotnetMethodName(funcName string) string { 73 pieces := strings.Split(funcName, " ") 74 75 var className, methodName string 76 77 classIdx, fnIdx := ddDotnetFieldIdx[ClassTag], ddDotnetFieldIdx[MethodTag] 78 if classIdx < len(pieces) && strings.HasPrefix(pieces[classIdx], string(ClassTag)) { 79 className = strings.TrimPrefix(pieces[classIdx], string(ClassTag)) 80 } 81 if fnIdx < len(pieces) && strings.HasPrefix(pieces[fnIdx], string(MethodTag)) { 82 methodName = strings.TrimPrefix(pieces[fnIdx], string(MethodTag)) 83 } 84 85 if className == "" || methodName == "" { 86 for _, piece := range pieces { 87 piece = strings.TrimSpace(piece) 88 if className == "" && strings.HasPrefix(piece, string(ClassTag)) { 89 className = strings.TrimPrefix(piece, string(ClassTag)) 90 } 91 if methodName == "" && strings.HasPrefix(piece, string(MethodTag)) { 92 methodName = strings.TrimPrefix(piece, string(MethodTag)) 93 } 94 } 95 } 96 if methodName == "" { 97 return "unknown" 98 } 99 100 if className != "" { 101 return className + "." + methodName 102 } 103 104 return methodName 105 } 106 107 func getDDDotnetField(funcName string, tag DDFieldTag) string { 108 pieces := strings.Split(funcName, " ") 109 idx := ddDotnetFieldIdx[tag] 110 111 tagStr := string(tag) 112 if idx < len(pieces) && strings.HasPrefix(pieces[idx], tagStr) { 113 return strings.TrimPrefix(pieces[idx], tagStr) 114 } 115 for _, piece := range pieces { 116 piece = strings.TrimSpace(piece) 117 if strings.HasPrefix(piece, tagStr) { 118 return strings.TrimPrefix(piece, tagStr) 119 } 120 } 121 return "<unknown>" 122 } 123 124 func getFuncIdentifier(lang languages.Lang, smp *profile.Sample, reverse bool) string { 125 i := 0 126 if reverse { 127 i = len(smp.Location) - 1 128 } 129 if len(smp.Location) > 0 { 130 loc := smp.Location[i] 131 if len(loc.Line) > 0 { 132 return strconv.FormatUint(loc.Line[len(loc.Line)-1].Function.ID, 10) 133 } 134 } 135 return UnknownInfo 136 } 137 138 func getMethodByLine(lang languages.Lang, line profile.Line) string { 139 return getDDDotnetMethodName(line.Function.Name) 140 } 141 142 func getPropertyCallable(getPropertyByLine GetPropertyByLine) GetPropertyFunc { 143 return func(lang languages.Lang, sample *profile.Sample, reverse bool) string { 144 i := 0 145 if reverse { 146 i = len(sample.Location) - 1 147 } 148 149 if len(sample.Location) > 0 { 150 loc := sample.Location[i] 151 if len(loc.Line) > 0 { 152 return getPropertyByLine(lang, loc.Line[len(loc.Line)-1]) 153 } 154 } 155 return UnknownInfo 156 } 157 } 158 159 func getClassByLine(lang languages.Lang, line profile.Line) string { 160 funcName := line.Function.Name 161 switch lang { 162 case languages.DotNet: 163 return getDDDotnetField(funcName, ClassTag) 164 case languages.PHP: 165 funcName = getPHPBaseFuncName(funcName) 166 if pos := strings.LastIndex(funcName, "::"); pos >= 0 { 167 return funcName[:pos] 168 } 169 if pos := strings.Index(funcName, "|"); pos >= 0 { 170 return funcName[:pos] 171 } 172 filename := strings.ReplaceAll(line.Function.Filename, "\\", "/") 173 174 if pos := strings.Index(filename, "/vendor/"); pos >= 0 { 175 filename = filename[pos+len("/vendor/"):] 176 if idx := strings.Index(filename, "/src/"); idx >= 0 { 177 filename = filename[:idx] 178 } else if idx := strings.LastIndexByte(filename, '/'); idx >= 0 { 179 filename = filename[:idx] 180 } 181 return filename 182 } 183 184 return "standard" 185 } 186 187 return UnknownInfo 188 } 189 190 func getNamespaceByLine(lang languages.Lang, line profile.Line) string { 191 switch lang { 192 case languages.DotNet: 193 if namespace := getDDDotnetField(line.Function.Name, NamespaceTag); namespace != "" { 194 return namespace 195 } 196 } 197 return UnknownInfo 198 } 199 200 func getAssemblyByLine(lang languages.Lang, line profile.Line) string { 201 switch lang { 202 case languages.DotNet: 203 if assembly := getDDDotnetField(line.Function.Name, AssemblyTag); assembly != "" { 204 return assembly 205 } 206 } 207 return UnknownInfo 208 } 209 210 func getLineByLine(lang languages.Lang, line profile.Line) int64 { 211 switch lang { 212 case languages.NodeJS: 213 segments := strings.Split(line.Function.Name, ":") 214 if len(segments) > 0 { 215 lineNo := segments[len(segments)-1] 216 if lineNoRegExp.MatchString(lineNo) { 217 lineNum, _ := strconv.ParseInt(lineNo, 10, 64) 218 return lineNum 219 } 220 } 221 } 222 return line.Line 223 } 224 225 func getFileByLine(lang languages.Lang, line profile.Line) string { 226 switch lang { 227 case languages.NodeJS: 228 funcName := line.Function.Name 229 segments := strings.Split(funcName, ":") 230 if len(segments) >= 3 { 231 filename := strings.TrimSpace(strings.Join(segments[:len(segments)-2], ":")) 232 if filename != "" { 233 return filename 234 } 235 } 236 return UnknownInfo 237 case languages.PHP: 238 filename := strings.TrimSpace(line.Function.Filename) 239 if filename == "" { 240 filename = "standard" 241 } 242 return filename 243 } 244 return line.Function.Filename 245 } 246 247 func getDirectoryByLine(lang languages.Lang, line profile.Line) string { 248 return filepathtoolkit.DirName(getFileByLine(lang, line)) 249 } 250 251 func getThreadID(lang languages.Lang, smp *profile.Sample, reverse bool) string { 252 return getThreadIDBySample(smp) 253 } 254 255 func getThreadIDBySample(smp *profile.Sample) string { 256 if tid := parsetoolkit.GetLabel(smp, LabelThreadID); tid != "" { 257 return tid 258 } 259 return UnknownInfo 260 } 261 262 func getThreadName(lang languages.Lang, smp *profile.Sample, reverse bool) string { 263 return getThreadNameBySample(smp) 264 } 265 266 func getThreadNameBySample(smp *profile.Sample) string { 267 if tName := parsetoolkit.GetLabel(smp, LabelThreadName); tName != "" { 268 return tName 269 } 270 return UnknownInfo 271 } 272 273 func getPackageNameByLine(lang languages.Lang, line profile.Line) string { 274 switch lang { 275 case languages.GoLang: 276 packageName, _ := cutGoFuncName(line.Function.Name) 277 return packageName 278 } 279 return UnknownInfo 280 } 281 282 // cutGoFuncName 切割pprof go func 为 package 和 func name 283 // return package name 和 func name 284 func cutGoFuncName(funcName string) (string, string) { 285 pos := strings.LastIndexByte(funcName, '/') 286 packageName := "" 287 if pos > -1 { 288 packageName, funcName = funcName[:pos+1], funcName[pos+1:] 289 } 290 cuts := strings.SplitN(funcName, ".", 2) 291 if len(cuts) < 2 { 292 logtoolkit.Errorf(`func name not contains ".": %s`, funcName) 293 return packageName, cuts[0] 294 } 295 return packageName + cuts[0], cuts[1] 296 } 297 298 func GetPrintStrByLine(lang languages.Lang, line profile.Line) string { 299 switch lang { 300 case languages.GoLang: 301 _, funcName := cutGoFuncName(line.Function.Name) 302 return fmt.Sprintf("%s(%s)", funcName, filepathtoolkit.BaseName(line.Function.Filename)) 303 case languages.NodeJS: 304 // node:internal/timers:listOnTimeout:569 305 // ./node_modules/@pyroscope/nodejs/dist/cjs/index.js:(anonymous):313 306 // :(idle):0 307 segments := strings.Split(line.Function.Name, ":") 308 funcName := "<unknown>" 309 filename := "" 310 if len(segments) == 1 { 311 funcName = segments[0] 312 } else if len(segments) > 1 { 313 funcName = segments[len(segments)-2] 314 filename = strings.TrimSpace(strings.Join(segments[:len(segments)-2], ":")) 315 } 316 baseName := filepathtoolkit.BaseName(filename) 317 if baseName == "" || baseName == "." { 318 return funcName 319 } 320 return fmt.Sprintf("%s(%s)", funcName, baseName) 321 322 case languages.DotNet: 323 return getDDDotnetMethodName(line.Function.Name) 324 case languages.PHP: 325 filename := line.Function.Filename 326 if filename != "" { 327 filename = filepathtoolkit.BaseName(filename) 328 } 329 funcName := getPHPBaseFuncName(line.Function.Name) 330 if filename != "" { 331 return fmt.Sprintf("%s(%s)", funcName, filename) 332 } 333 return funcName 334 default: 335 return fmt.Sprintf("%s(%s)", line.Function.Name, filepathtoolkit.BaseName(line.Function.Filename)) 336 } 337 } 338 339 func getPHPBaseFuncName(funcName string) string { 340 if funcName == "" { 341 return UnknownInfo 342 } 343 pos := strings.LastIndexByte(funcName, '\\') 344 if pos >= 0 && pos < len(funcName)-1 { 345 return funcName[pos+1:] 346 } 347 return funcName 348 } 349 350 func GetSpyPrintStr(funcName, fileName string) string { 351 return fmt.Sprintf("%s(%s)", funcName, filepathtoolkit.BaseName(fileName)) 352 } 353 354 func GetFuncAndLineDisplay(lang languages.Lang, smp *profile.Sample, reverse bool) string { 355 i := 0 356 if reverse { 357 i = len(smp.Location) - 1 358 } 359 if len(smp.Location) > 0 { 360 loc := smp.Location[i] 361 if len(loc.Line) > 0 { 362 line := loc.Line[len(loc.Line)-1] 363 switch lang { 364 case languages.PHP: 365 funcName := getPHPBaseFuncName(line.Function.Name) 366 filename := line.Function.Filename 367 if filename != "" { 368 filename = filepathtoolkit.BaseName(filename) 369 } 370 if filename != "" { 371 return fmt.Sprintf("%s(%s:L#%d)", funcName, filename, line.Line) 372 } 373 return funcName 374 case languages.GoLang: 375 _, funcName := cutGoFuncName(line.Function.Name) 376 return fmt.Sprintf("%s(%s:L#%d)", 377 funcName, filepathtoolkit.BaseName(line.Function.Filename), line.Line) 378 default: 379 return fmt.Sprintf("%s(%s:L#%d)", 380 line.Function.Name, filepathtoolkit.BaseName(line.Function.Filename), line.Line) 381 } 382 } 383 } 384 return "<unknown>" 385 } 386 387 var ( 388 Function = &Aggregator{ 389 Name: "Function", 390 Mapping: []string{pprof.FieldFunctionName}, 391 ShowLanguages: languages.PythonID | languages.GolangID, 392 GetIdentifier: getFuncIdentifier, 393 GetDisplayStr: getFuncDisplayStr, 394 GetMappingFuncs: []GetPropertyFunc{getFuncName}, 395 } 396 397 PHPFunction = &Aggregator{ 398 Name: "Function", 399 Mapping: []string{pprof.FieldFunctionName}, 400 GetIdentifier: getFuncIdentifier, 401 GetDisplayStr: getFuncDisplayStr, 402 GetMappingFuncs: []GetPropertyFunc{getFuncName}, 403 } 404 405 Method = &Aggregator{ 406 Name: "Method", 407 Mapping: []string{pprof.FieldFunctionName}, 408 ShowLanguages: languages.JavaID | languages.DotNetID, 409 GetIdentifier: getMethod, 410 GetDisplayStr: getMethod, 411 GetMappingFuncs: []GetPropertyFunc{getMethod}, 412 } 413 414 Class = &Aggregator{ 415 Name: "Class", 416 Mapping: []string{pprof.FieldClass}, 417 ShowLanguages: languages.DotNetID, 418 GetIdentifier: getClass, 419 GetDisplayStr: getClass, 420 GetMappingFuncs: []GetPropertyFunc{getClass}, 421 } 422 423 Namespace = &Aggregator{ 424 Name: "Namespace", 425 Mapping: []string{pprof.FieldNamespace}, 426 ShowLanguages: languages.DotNetID, 427 GetIdentifier: getNamespace, 428 GetDisplayStr: getNamespace, 429 GetMappingFuncs: []GetPropertyFunc{getNamespace}, 430 } 431 432 Assembly = &Aggregator{ 433 Name: "Assembly", 434 Mapping: []string{pprof.FieldAssembly}, 435 ShowLanguages: languages.DotNetID, 436 GetIdentifier: getAssembly, 437 GetDisplayStr: getAssembly, 438 GetMappingFuncs: []GetPropertyFunc{getAssembly}, 439 } 440 441 PyroNodeFunction = &Aggregator{ 442 Name: "Function", 443 Mapping: []string{pprof.FieldFunctionName}, 444 ShowLanguages: languages.NodeJSID, 445 GetIdentifier: getFuncIdentifier, 446 GetDisplayStr: getFuncDisplayStr, 447 GetMappingFuncs: []GetPropertyFunc{getFuncName}, 448 } 449 450 FunctionLine = &Aggregator{ 451 Name: "Function + Line", 452 Mapping: []string{pprof.FieldFunctionName, pprof.FieldLine}, 453 ShowLanguages: languages.PythonID | languages.GolangID, 454 GetIdentifier: func(lang languages.Lang, smp *profile.Sample, reverse bool) string { 455 i := 0 456 if reverse { 457 i = len(smp.Location) - 1 458 } 459 if len(smp.Location) > 0 { 460 loc := smp.Location[i] 461 if len(loc.Line) > 0 { 462 return fmt.Sprintf("%s###%d###%d", 463 loc.Line[len(loc.Line)-1].Function.Filename, loc.Line[len(loc.Line)-1].Function.ID, loc.Line[len(loc.Line)-1].Line) 464 } 465 } 466 return "<unknown>" 467 }, 468 GetDisplayStr: GetFuncAndLineDisplay, 469 GetMappingFuncs: []GetPropertyFunc{getFuncName, getLine}, 470 } 471 472 Directory = &Aggregator{ 473 Name: "Directory", 474 Mapping: []string{pprof.FieldDirectory}, 475 ShowLanguages: languages.PythonID | languages.GolangID, 476 GetIdentifier: getDirectory, 477 GetDisplayStr: getDirectory, 478 GetMappingFuncs: []GetPropertyFunc{getDirectory}, 479 } 480 481 File = &Aggregator{ 482 Name: "File", 483 Mapping: []string{pprof.FieldFile}, 484 ShowLanguages: languages.PythonID | languages.GolangID, 485 GetIdentifier: getFile, 486 GetDisplayStr: getFile, 487 GetMappingFuncs: []GetPropertyFunc{getFile}, 488 } 489 490 PyroNodeFile = &Aggregator{ 491 Name: "File", 492 Mapping: []string{pprof.FieldFile}, 493 ShowLanguages: languages.NodeJSID, 494 GetIdentifier: getFile, 495 GetDisplayStr: getFile, 496 GetMappingFuncs: []GetPropertyFunc{getFile}, 497 } 498 499 ThreadID = &Aggregator{ 500 Name: "Thread ID", 501 Mapping: []string{pprof.FieldThreadID}, 502 ShowLanguages: languages.PythonID | languages.DotNetID, 503 GetIdentifier: getThreadID, 504 GetDisplayStr: getThreadID, 505 GetMappingFuncs: []GetPropertyFunc{getThreadID}, 506 } 507 508 ThreadName = &Aggregator{ 509 Name: "Thread Name", 510 Mapping: []string{pprof.FieldThreadName}, 511 ShowLanguages: languages.PythonID | languages.DotNetID, 512 GetIdentifier: getThreadName, 513 GetDisplayStr: getThreadName, 514 GetMappingFuncs: []GetPropertyFunc{getThreadName}, 515 } 516 517 Package = &Aggregator{ 518 Name: "Package", 519 Mapping: []string{pprof.FieldPackage}, 520 ShowLanguages: languages.GolangID, 521 GetIdentifier: getPackageName, 522 GetDisplayStr: getPackageName, 523 GetMappingFuncs: []GetPropertyFunc{getPackageName}, 524 } 525 ) 526 527 type GetPropertyFunc func(lang languages.Lang, smp *profile.Sample, reverse bool) string 528 529 type Aggregator struct { 530 Name string 531 Mapping []string 532 533 ShowLanguages languages.LangID 534 535 // GetIdentifier 获取维度的唯一标识 536 GetIdentifier GetPropertyFunc 537 538 // GetDisplayStr 获取维度的显示字符 539 GetDisplayStr GetPropertyFunc 540 541 // GetMappingFuncs, 获取与Mapping字段对应值Func 542 GetMappingFuncs []GetPropertyFunc 543 } 544 545 type AggregatorSelectSlice []*AggregatorSelect 546 547 func (asm AggregatorSelectSlice) CalcPercentAndQuantity(total int64) { 548 for _, aggregatorSelect := range asm { 549 for _, opt := range aggregatorSelect.Options { 550 opt.CalcPercentAndQuantity(total) 551 } 552 } 553 } 554 555 func (asm AggregatorSelectSlice) MarshalJSON() ([]byte, error) { 556 557 JSONMap := make([]*AggregatorSelectForJSON, 0, len(asm)) 558 559 for _, aggregatorSelect := range asm { 560 561 selectForJSON := &AggregatorSelectForJSON{ 562 Dimension: aggregatorSelect.Aggregator.Name, 563 Mapping: aggregatorSelect.Mapping, 564 } 565 566 for _, opt := range aggregatorSelect.Options { 567 selectForJSON.Options = append(selectForJSON.Options, opt) 568 } 569 570 sort.Sort(selectForJSON.Options) 571 572 JSONMap = append(JSONMap, selectForJSON) 573 } 574 return json.Marshal(JSONMap) 575 } 576 577 type OptionSlice []*AggregatorOption 578 579 func (os OptionSlice) Len() int { 580 return len(os) 581 } 582 583 func (os OptionSlice) Less(i, j int) bool { 584 return os[i].Value > os[j].Value 585 } 586 587 func (os OptionSlice) Swap(i, j int) { 588 os[i], os[j] = os[j], os[i] 589 } 590 591 type OptionMap map[string]*AggregatorOption 592 593 type AggregatorSelect struct { 594 Aggregator *Aggregator 595 Mapping []string 596 Options OptionMap 597 } 598 599 type AggregatorSelectForJSON struct { 600 Dimension string `json:"dimension"` 601 Mapping []string `json:"mapping"` 602 Options OptionSlice `json:"data"` 603 } 604 605 type AggregatorOption struct { 606 Title string `json:"title"` 607 Quantity *quantity.Quantity `json:"quantity"` 608 Value int64 `json:"value"` 609 Unit *quantity.Unit `json:"unit"` 610 Percent string `json:"percent"` 611 MappingValues []string `json:"mappingValues"` 612 } 613 614 func (ao *AggregatorOption) CalcPercentAndQuantity(total int64) { 615 616 if total <= 0 { 617 ao.Percent = "100" 618 } else { 619 ao.Percent = fmt.Sprintf("%.2f", float64(ao.Value)/float64(total)*100) 620 } 621 622 if ao.Unit != nil { 623 ao.Quantity = ao.Unit.Quantity(ao.Value) 624 625 // 转成默认单位 626 ao.Quantity.SwitchToDefaultUnit() 627 ao.Value = ao.Quantity.Value 628 ao.Unit = ao.Quantity.Unit 629 } 630 } 631 632 var PythonAggregatorList = []*Aggregator{ 633 Function, 634 FunctionLine, 635 Directory, 636 File, 637 ThreadID, 638 ThreadName, 639 } 640 641 var GoAggregatorList = []*Aggregator{ 642 Function, 643 FunctionLine, 644 Directory, 645 File, 646 Package, 647 } 648 649 var SpyAggregatorList = []*Aggregator{ 650 Function, 651 FunctionLine, 652 Directory, 653 File, 654 ThreadName, 655 } 656 657 var PyroscopeNodeJSAggregatorList = []*Aggregator{ 658 PyroNodeFunction, 659 PyroNodeFile, 660 } 661 662 var DDTraceDotnetAggregatorList = []*Aggregator{ 663 Method, 664 Class, 665 Namespace, 666 Assembly, 667 ThreadID, 668 ThreadName, 669 } 670 671 var DDTracePHPAggregatorList = []*Aggregator{ 672 PHPFunction, 673 FunctionLine, 674 Class, 675 File, 676 Directory, 677 }