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