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