github.com/Kolosok86/http@v0.1.2/internal/profile/profile.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 // Package profile provides a representation of 6 // github.com/google/pprof/proto/profile.proto and 7 // methods to encode/decode/merge profiles in this format. 8 package profile 9 10 import ( 11 "bytes" 12 "compress/gzip" 13 "fmt" 14 "io" 15 "strings" 16 "time" 17 18 "github.com/Kolosok86/http/internal/lazyregexp" 19 ) 20 21 // Profile is an in-memory representation of profile.proto. 22 type Profile struct { 23 SampleType []*ValueType 24 DefaultSampleType string 25 Sample []*Sample 26 Mapping []*Mapping 27 Location []*Location 28 Function []*Function 29 Comments []string 30 31 DropFrames string 32 KeepFrames string 33 34 TimeNanos int64 35 DurationNanos int64 36 PeriodType *ValueType 37 Period int64 38 39 commentX []int64 40 dropFramesX int64 41 keepFramesX int64 42 stringTable []string 43 defaultSampleTypeX int64 44 } 45 46 // ValueType corresponds to Profile.ValueType 47 type ValueType struct { 48 Type string // cpu, wall, inuse_space, etc 49 Unit string // seconds, nanoseconds, bytes, etc 50 51 typeX int64 52 unitX int64 53 } 54 55 // Sample corresponds to Profile.Sample 56 type Sample struct { 57 Location []*Location 58 Value []int64 59 Label map[string][]string 60 NumLabel map[string][]int64 61 NumUnit map[string][]string 62 63 locationIDX []uint64 64 labelX []Label 65 } 66 67 // Label corresponds to Profile.Label 68 type Label struct { 69 keyX int64 70 // Exactly one of the two following values must be set 71 strX int64 72 numX int64 // Integer value for this label 73 } 74 75 // Mapping corresponds to Profile.Mapping 76 type Mapping struct { 77 ID uint64 78 Start uint64 79 Limit uint64 80 Offset uint64 81 File string 82 BuildID string 83 HasFunctions bool 84 HasFilenames bool 85 HasLineNumbers bool 86 HasInlineFrames bool 87 88 fileX int64 89 buildIDX int64 90 } 91 92 // Location corresponds to Profile.Location 93 type Location struct { 94 ID uint64 95 Mapping *Mapping 96 Address uint64 97 Line []Line 98 IsFolded bool 99 100 mappingIDX uint64 101 } 102 103 // Line corresponds to Profile.Line 104 type Line struct { 105 Function *Function 106 Line int64 107 108 functionIDX uint64 109 } 110 111 // Function corresponds to Profile.Function 112 type Function struct { 113 ID uint64 114 Name string 115 SystemName string 116 Filename string 117 StartLine int64 118 119 nameX int64 120 systemNameX int64 121 filenameX int64 122 } 123 124 // Parse parses a profile and checks for its validity. The input 125 // may be a gzip-compressed encoded protobuf or one of many legacy 126 // profile formats which may be unsupported in the future. 127 func Parse(r io.Reader) (*Profile, error) { 128 orig, err := io.ReadAll(r) 129 if err != nil { 130 return nil, err 131 } 132 133 var p *Profile 134 if len(orig) >= 2 && orig[0] == 0x1f && orig[1] == 0x8b { 135 gz, err := gzip.NewReader(bytes.NewBuffer(orig)) 136 if err != nil { 137 return nil, fmt.Errorf("decompressing profile: %v", err) 138 } 139 data, err := io.ReadAll(gz) 140 if err != nil { 141 return nil, fmt.Errorf("decompressing profile: %v", err) 142 } 143 orig = data 144 } 145 if p, err = parseUncompressed(orig); err != nil { 146 if p, err = parseLegacy(orig); err != nil { 147 return nil, fmt.Errorf("parsing profile: %v", err) 148 } 149 } 150 151 if err := p.CheckValid(); err != nil { 152 return nil, fmt.Errorf("malformed profile: %v", err) 153 } 154 return p, nil 155 } 156 157 var errUnrecognized = fmt.Errorf("unrecognized profile format") 158 var errMalformed = fmt.Errorf("malformed profile format") 159 160 func parseLegacy(data []byte) (*Profile, error) { 161 parsers := []func([]byte) (*Profile, error){ 162 parseCPU, 163 parseHeap, 164 parseGoCount, // goroutine, threadcreate 165 parseThread, 166 parseContention, 167 } 168 169 for _, parser := range parsers { 170 p, err := parser(data) 171 if err == nil { 172 p.setMain() 173 p.addLegacyFrameInfo() 174 return p, nil 175 } 176 if err != errUnrecognized { 177 return nil, err 178 } 179 } 180 return nil, errUnrecognized 181 } 182 183 func parseUncompressed(data []byte) (*Profile, error) { 184 p := &Profile{} 185 if err := unmarshal(data, p); err != nil { 186 return nil, err 187 } 188 189 if err := p.postDecode(); err != nil { 190 return nil, err 191 } 192 193 return p, nil 194 } 195 196 var libRx = lazyregexp.New(`([.]so$|[.]so[._][0-9]+)`) 197 198 // setMain scans Mapping entries and guesses which entry is main 199 // because legacy profiles don't obey the convention of putting main 200 // first. 201 func (p *Profile) setMain() { 202 for i := 0; i < len(p.Mapping); i++ { 203 file := strings.TrimSpace(strings.ReplaceAll(p.Mapping[i].File, "(deleted)", "")) 204 if len(file) == 0 { 205 continue 206 } 207 if len(libRx.FindStringSubmatch(file)) > 0 { 208 continue 209 } 210 if strings.HasPrefix(file, "[") { 211 continue 212 } 213 // Swap what we guess is main to position 0. 214 p.Mapping[i], p.Mapping[0] = p.Mapping[0], p.Mapping[i] 215 break 216 } 217 } 218 219 // Write writes the profile as a gzip-compressed marshaled protobuf. 220 func (p *Profile) Write(w io.Writer) error { 221 p.preEncode() 222 b := marshal(p) 223 zw := gzip.NewWriter(w) 224 defer zw.Close() 225 _, err := zw.Write(b) 226 return err 227 } 228 229 // CheckValid tests whether the profile is valid. Checks include, but are 230 // not limited to: 231 // - len(Profile.Sample[n].value) == len(Profile.value_unit) 232 // - Sample.id has a corresponding Profile.Location 233 func (p *Profile) CheckValid() error { 234 // Check that sample values are consistent 235 sampleLen := len(p.SampleType) 236 if sampleLen == 0 && len(p.Sample) != 0 { 237 return fmt.Errorf("missing sample type information") 238 } 239 for _, s := range p.Sample { 240 if len(s.Value) != sampleLen { 241 return fmt.Errorf("mismatch: sample has: %d values vs. %d types", len(s.Value), len(p.SampleType)) 242 } 243 } 244 245 // Check that all mappings/locations/functions are in the tables 246 // Check that there are no duplicate ids 247 mappings := make(map[uint64]*Mapping, len(p.Mapping)) 248 for _, m := range p.Mapping { 249 if m.ID == 0 { 250 return fmt.Errorf("found mapping with reserved ID=0") 251 } 252 if mappings[m.ID] != nil { 253 return fmt.Errorf("multiple mappings with same id: %d", m.ID) 254 } 255 mappings[m.ID] = m 256 } 257 functions := make(map[uint64]*Function, len(p.Function)) 258 for _, f := range p.Function { 259 if f.ID == 0 { 260 return fmt.Errorf("found function with reserved ID=0") 261 } 262 if functions[f.ID] != nil { 263 return fmt.Errorf("multiple functions with same id: %d", f.ID) 264 } 265 functions[f.ID] = f 266 } 267 locations := make(map[uint64]*Location, len(p.Location)) 268 for _, l := range p.Location { 269 if l.ID == 0 { 270 return fmt.Errorf("found location with reserved id=0") 271 } 272 if locations[l.ID] != nil { 273 return fmt.Errorf("multiple locations with same id: %d", l.ID) 274 } 275 locations[l.ID] = l 276 if m := l.Mapping; m != nil { 277 if m.ID == 0 || mappings[m.ID] != m { 278 return fmt.Errorf("inconsistent mapping %p: %d", m, m.ID) 279 } 280 } 281 for _, ln := range l.Line { 282 if f := ln.Function; f != nil { 283 if f.ID == 0 || functions[f.ID] != f { 284 return fmt.Errorf("inconsistent function %p: %d", f, f.ID) 285 } 286 } 287 } 288 } 289 return nil 290 } 291 292 // Aggregate merges the locations in the profile into equivalence 293 // classes preserving the request attributes. It also updates the 294 // samples to point to the merged locations. 295 func (p *Profile) Aggregate(inlineFrame, function, filename, linenumber, address bool) error { 296 for _, m := range p.Mapping { 297 m.HasInlineFrames = m.HasInlineFrames && inlineFrame 298 m.HasFunctions = m.HasFunctions && function 299 m.HasFilenames = m.HasFilenames && filename 300 m.HasLineNumbers = m.HasLineNumbers && linenumber 301 } 302 303 // Aggregate functions 304 if !function || !filename { 305 for _, f := range p.Function { 306 if !function { 307 f.Name = "" 308 f.SystemName = "" 309 } 310 if !filename { 311 f.Filename = "" 312 } 313 } 314 } 315 316 // Aggregate locations 317 if !inlineFrame || !address || !linenumber { 318 for _, l := range p.Location { 319 if !inlineFrame && len(l.Line) > 1 { 320 l.Line = l.Line[len(l.Line)-1:] 321 } 322 if !linenumber { 323 for i := range l.Line { 324 l.Line[i].Line = 0 325 } 326 } 327 if !address { 328 l.Address = 0 329 } 330 } 331 } 332 333 return p.CheckValid() 334 } 335 336 // Print dumps a text representation of a profile. Intended mainly 337 // for debugging purposes. 338 func (p *Profile) String() string { 339 340 ss := make([]string, 0, len(p.Sample)+len(p.Mapping)+len(p.Location)) 341 if pt := p.PeriodType; pt != nil { 342 ss = append(ss, fmt.Sprintf("PeriodType: %s %s", pt.Type, pt.Unit)) 343 } 344 ss = append(ss, fmt.Sprintf("Period: %d", p.Period)) 345 if p.TimeNanos != 0 { 346 ss = append(ss, fmt.Sprintf("Time: %v", time.Unix(0, p.TimeNanos))) 347 } 348 if p.DurationNanos != 0 { 349 ss = append(ss, fmt.Sprintf("Duration: %v", time.Duration(p.DurationNanos))) 350 } 351 352 ss = append(ss, "Samples:") 353 var sh1 string 354 for _, s := range p.SampleType { 355 sh1 = sh1 + fmt.Sprintf("%s/%s ", s.Type, s.Unit) 356 } 357 ss = append(ss, strings.TrimSpace(sh1)) 358 for _, s := range p.Sample { 359 var sv string 360 for _, v := range s.Value { 361 sv = fmt.Sprintf("%s %10d", sv, v) 362 } 363 sv = sv + ": " 364 for _, l := range s.Location { 365 sv = sv + fmt.Sprintf("%d ", l.ID) 366 } 367 ss = append(ss, sv) 368 const labelHeader = " " 369 if len(s.Label) > 0 { 370 ls := labelHeader 371 for k, v := range s.Label { 372 ls = ls + fmt.Sprintf("%s:%v ", k, v) 373 } 374 ss = append(ss, ls) 375 } 376 if len(s.NumLabel) > 0 { 377 ls := labelHeader 378 for k, v := range s.NumLabel { 379 ls = ls + fmt.Sprintf("%s:%v ", k, v) 380 } 381 ss = append(ss, ls) 382 } 383 } 384 385 ss = append(ss, "Locations") 386 for _, l := range p.Location { 387 locStr := fmt.Sprintf("%6d: %#x ", l.ID, l.Address) 388 if m := l.Mapping; m != nil { 389 locStr = locStr + fmt.Sprintf("M=%d ", m.ID) 390 } 391 if len(l.Line) == 0 { 392 ss = append(ss, locStr) 393 } 394 for li := range l.Line { 395 lnStr := "??" 396 if fn := l.Line[li].Function; fn != nil { 397 lnStr = fmt.Sprintf("%s %s:%d s=%d", 398 fn.Name, 399 fn.Filename, 400 l.Line[li].Line, 401 fn.StartLine) 402 if fn.Name != fn.SystemName { 403 lnStr = lnStr + "(" + fn.SystemName + ")" 404 } 405 } 406 ss = append(ss, locStr+lnStr) 407 // Do not print location details past the first line 408 locStr = " " 409 } 410 } 411 412 ss = append(ss, "Mappings") 413 for _, m := range p.Mapping { 414 bits := "" 415 if m.HasFunctions { 416 bits += "[FN]" 417 } 418 if m.HasFilenames { 419 bits += "[FL]" 420 } 421 if m.HasLineNumbers { 422 bits += "[LN]" 423 } 424 if m.HasInlineFrames { 425 bits += "[IN]" 426 } 427 ss = append(ss, fmt.Sprintf("%d: %#x/%#x/%#x %s %s %s", 428 m.ID, 429 m.Start, m.Limit, m.Offset, 430 m.File, 431 m.BuildID, 432 bits)) 433 } 434 435 return strings.Join(ss, "\n") + "\n" 436 } 437 438 // Merge adds profile p adjusted by ratio r into profile p. Profiles 439 // must be compatible (same Type and SampleType). 440 // TODO(rsilvera): consider normalizing the profiles based on the 441 // total samples collected. 442 func (p *Profile) Merge(pb *Profile, r float64) error { 443 if err := p.Compatible(pb); err != nil { 444 return err 445 } 446 447 pb = pb.Copy() 448 449 // Keep the largest of the two periods. 450 if pb.Period > p.Period { 451 p.Period = pb.Period 452 } 453 454 p.DurationNanos += pb.DurationNanos 455 456 p.Mapping = append(p.Mapping, pb.Mapping...) 457 for i, m := range p.Mapping { 458 m.ID = uint64(i + 1) 459 } 460 p.Location = append(p.Location, pb.Location...) 461 for i, l := range p.Location { 462 l.ID = uint64(i + 1) 463 } 464 p.Function = append(p.Function, pb.Function...) 465 for i, f := range p.Function { 466 f.ID = uint64(i + 1) 467 } 468 469 if r != 1.0 { 470 for _, s := range pb.Sample { 471 for i, v := range s.Value { 472 s.Value[i] = int64((float64(v) * r)) 473 } 474 } 475 } 476 p.Sample = append(p.Sample, pb.Sample...) 477 return p.CheckValid() 478 } 479 480 // Compatible determines if two profiles can be compared/merged. 481 // returns nil if the profiles are compatible; otherwise an error with 482 // details on the incompatibility. 483 func (p *Profile) Compatible(pb *Profile) error { 484 if !compatibleValueTypes(p.PeriodType, pb.PeriodType) { 485 return fmt.Errorf("incompatible period types %v and %v", p.PeriodType, pb.PeriodType) 486 } 487 488 if len(p.SampleType) != len(pb.SampleType) { 489 return fmt.Errorf("incompatible sample types %v and %v", p.SampleType, pb.SampleType) 490 } 491 492 for i := range p.SampleType { 493 if !compatibleValueTypes(p.SampleType[i], pb.SampleType[i]) { 494 return fmt.Errorf("incompatible sample types %v and %v", p.SampleType, pb.SampleType) 495 } 496 } 497 498 return nil 499 } 500 501 // HasFunctions determines if all locations in this profile have 502 // symbolized function information. 503 func (p *Profile) HasFunctions() bool { 504 for _, l := range p.Location { 505 if l.Mapping == nil || !l.Mapping.HasFunctions { 506 return false 507 } 508 } 509 return true 510 } 511 512 // HasFileLines determines if all locations in this profile have 513 // symbolized file and line number information. 514 func (p *Profile) HasFileLines() bool { 515 for _, l := range p.Location { 516 if l.Mapping == nil || (!l.Mapping.HasFilenames || !l.Mapping.HasLineNumbers) { 517 return false 518 } 519 } 520 return true 521 } 522 523 func compatibleValueTypes(v1, v2 *ValueType) bool { 524 if v1 == nil || v2 == nil { 525 return true // No grounds to disqualify. 526 } 527 return v1.Type == v2.Type && v1.Unit == v2.Unit 528 } 529 530 // Copy makes a fully independent copy of a profile. 531 func (p *Profile) Copy() *Profile { 532 p.preEncode() 533 b := marshal(p) 534 535 pp := &Profile{} 536 if err := unmarshal(b, pp); err != nil { 537 panic(err) 538 } 539 if err := pp.postDecode(); err != nil { 540 panic(err) 541 } 542 543 return pp 544 } 545 546 // Demangler maps symbol names to a human-readable form. This may 547 // include C++ demangling and additional simplification. Names that 548 // are not demangled may be missing from the resulting map. 549 type Demangler func(name []string) (map[string]string, error) 550 551 // Demangle attempts to demangle and optionally simplify any function 552 // names referenced in the profile. It works on a best-effort basis: 553 // it will silently preserve the original names in case of any errors. 554 func (p *Profile) Demangle(d Demangler) error { 555 // Collect names to demangle. 556 var names []string 557 for _, fn := range p.Function { 558 names = append(names, fn.SystemName) 559 } 560 561 // Update profile with demangled names. 562 demangled, err := d(names) 563 if err != nil { 564 return err 565 } 566 for _, fn := range p.Function { 567 if dd, ok := demangled[fn.SystemName]; ok { 568 fn.Name = dd 569 } 570 } 571 return nil 572 } 573 574 // Empty reports whether the profile contains no samples. 575 func (p *Profile) Empty() bool { 576 return len(p.Sample) == 0 577 } 578 579 // Scale multiplies all sample values in a profile by a constant. 580 func (p *Profile) Scale(ratio float64) { 581 if ratio == 1 { 582 return 583 } 584 ratios := make([]float64, len(p.SampleType)) 585 for i := range p.SampleType { 586 ratios[i] = ratio 587 } 588 p.ScaleN(ratios) 589 } 590 591 // ScaleN multiplies each sample values in a sample by a different amount. 592 func (p *Profile) ScaleN(ratios []float64) error { 593 if len(p.SampleType) != len(ratios) { 594 return fmt.Errorf("mismatched scale ratios, got %d, want %d", len(ratios), len(p.SampleType)) 595 } 596 allOnes := true 597 for _, r := range ratios { 598 if r != 1 { 599 allOnes = false 600 break 601 } 602 } 603 if allOnes { 604 return nil 605 } 606 for _, s := range p.Sample { 607 for i, v := range s.Value { 608 if ratios[i] != 1 { 609 s.Value[i] = int64(float64(v) * ratios[i]) 610 } 611 } 612 } 613 return nil 614 }