Colin Cross | 7bb052a | 2015-02-03 12:59:37 -0800 | [diff] [blame^] | 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 driver implements the core pprof functionality. It can be |
| 6 | // parameterized with a flag implementation, fetch and symbolize |
| 7 | // mechanisms. |
| 8 | package driver |
| 9 | |
| 10 | import ( |
| 11 | "bytes" |
| 12 | "fmt" |
| 13 | "io" |
| 14 | "net/url" |
| 15 | "os" |
| 16 | "path/filepath" |
| 17 | "regexp" |
| 18 | "sort" |
| 19 | "strconv" |
| 20 | "strings" |
| 21 | "sync" |
| 22 | "time" |
| 23 | |
| 24 | "cmd/pprof/internal/commands" |
| 25 | "cmd/pprof/internal/plugin" |
| 26 | "cmd/pprof/internal/profile" |
| 27 | "cmd/pprof/internal/report" |
| 28 | "cmd/pprof/internal/tempfile" |
| 29 | ) |
| 30 | |
| 31 | // PProf acquires a profile, and symbolizes it using a profile |
| 32 | // manager. Then it generates a report formatted according to the |
| 33 | // options selected through the flags package. |
| 34 | func PProf(flagset plugin.FlagSet, fetch plugin.Fetcher, sym plugin.Symbolizer, obj plugin.ObjTool, ui plugin.UI, overrides commands.Commands) error { |
| 35 | // Remove any temporary files created during pprof processing. |
| 36 | defer tempfile.Cleanup() |
| 37 | |
| 38 | f, err := getFlags(flagset, overrides, ui) |
| 39 | if err != nil { |
| 40 | return err |
| 41 | } |
| 42 | |
| 43 | obj.SetConfig(*f.flagTools) |
| 44 | |
| 45 | sources := f.profileSource |
| 46 | if len(sources) > 1 { |
| 47 | source := sources[0] |
| 48 | // If the first argument is a supported object file, treat as executable. |
| 49 | if file, err := obj.Open(source, 0); err == nil { |
| 50 | file.Close() |
| 51 | f.profileExecName = source |
| 52 | sources = sources[1:] |
| 53 | } else if *f.flagBuildID == "" && isBuildID(source) { |
| 54 | f.flagBuildID = &source |
| 55 | sources = sources[1:] |
| 56 | } |
| 57 | } |
| 58 | |
| 59 | // errMu protects concurrent accesses to errset and err. errset is set if an |
| 60 | // error is encountered by one of the goroutines grabbing a profile. |
| 61 | errMu, errset := sync.Mutex{}, false |
| 62 | |
| 63 | // Fetch profiles. |
| 64 | wg := sync.WaitGroup{} |
| 65 | profs := make([]*profile.Profile, len(sources)) |
| 66 | for i, source := range sources { |
| 67 | wg.Add(1) |
| 68 | go func(i int, src string) { |
| 69 | defer wg.Done() |
| 70 | p, grabErr := grabProfile(src, f.profileExecName, *f.flagBuildID, fetch, sym, obj, ui, f) |
| 71 | if grabErr != nil { |
| 72 | errMu.Lock() |
| 73 | defer errMu.Unlock() |
| 74 | errset, err = true, grabErr |
| 75 | return |
| 76 | } |
| 77 | profs[i] = p |
| 78 | }(i, source) |
| 79 | } |
| 80 | wg.Wait() |
| 81 | if errset { |
| 82 | return err |
| 83 | } |
| 84 | |
| 85 | // Merge profiles. |
| 86 | prof := profs[0] |
| 87 | for _, p := range profs[1:] { |
| 88 | if err = prof.Merge(p, 1); err != nil { |
| 89 | return err |
| 90 | } |
| 91 | } |
| 92 | |
| 93 | if *f.flagBase != "" { |
| 94 | // Fetch base profile and subtract from current profile. |
| 95 | base, err := grabProfile(*f.flagBase, f.profileExecName, *f.flagBuildID, fetch, sym, obj, ui, f) |
| 96 | if err != nil { |
| 97 | return err |
| 98 | } |
| 99 | |
| 100 | if err = prof.Merge(base, -1); err != nil { |
| 101 | return err |
| 102 | } |
| 103 | } |
| 104 | |
| 105 | if err := processFlags(prof, ui, f); err != nil { |
| 106 | return err |
| 107 | } |
| 108 | |
| 109 | prof.RemoveUninteresting() |
| 110 | |
| 111 | if *f.flagInteractive { |
| 112 | return interactive(prof, obj, ui, f) |
| 113 | } |
| 114 | |
| 115 | return generate(false, prof, obj, ui, f) |
| 116 | } |
| 117 | |
| 118 | // isBuildID determines if the profile may contain a build ID, by |
| 119 | // checking that it is a string of hex digits. |
| 120 | func isBuildID(id string) bool { |
| 121 | return strings.Trim(id, "0123456789abcdefABCDEF") == "" |
| 122 | } |
| 123 | |
| 124 | // adjustURL updates the profile source URL based on heuristics. It |
| 125 | // will append ?seconds=sec for CPU profiles if not already |
| 126 | // specified. Returns the hostname if the profile is remote. |
| 127 | func adjustURL(source string, sec int, ui plugin.UI) (adjusted, host string, duration time.Duration) { |
| 128 | // If there is a local file with this name, just use it. |
| 129 | if _, err := os.Stat(source); err == nil { |
| 130 | return source, "", 0 |
| 131 | } |
| 132 | |
| 133 | url, err := url.Parse(source) |
| 134 | |
| 135 | // Automatically add http:// to URLs of the form hostname:port/path. |
| 136 | // url.Parse treats "hostname" as the Scheme. |
| 137 | if err != nil || (url.Host == "" && url.Scheme != "" && url.Scheme != "file") { |
| 138 | url, err = url.Parse("http://" + source) |
| 139 | if err != nil { |
| 140 | return source, url.Host, time.Duration(30) * time.Second |
| 141 | } |
| 142 | } |
| 143 | if scheme := strings.ToLower(url.Scheme); scheme == "" || scheme == "file" { |
| 144 | url.Scheme = "" |
| 145 | return url.String(), "", 0 |
| 146 | } |
| 147 | |
| 148 | values := url.Query() |
| 149 | if urlSeconds := values.Get("seconds"); urlSeconds != "" { |
| 150 | if us, err := strconv.ParseInt(urlSeconds, 10, 32); err == nil { |
| 151 | if sec >= 0 { |
| 152 | ui.PrintErr("Overriding -seconds for URL ", source) |
| 153 | } |
| 154 | sec = int(us) |
| 155 | } |
| 156 | } |
| 157 | |
| 158 | switch strings.ToLower(url.Path) { |
| 159 | case "", "/": |
| 160 | // Apply default /profilez. |
| 161 | url.Path = "/profilez" |
| 162 | case "/protoz": |
| 163 | // Rewrite to /profilez?type=proto |
| 164 | url.Path = "/profilez" |
| 165 | values.Set("type", "proto") |
| 166 | } |
| 167 | |
| 168 | if hasDuration(url.Path) { |
| 169 | if sec > 0 { |
| 170 | duration = time.Duration(sec) * time.Second |
| 171 | values.Set("seconds", fmt.Sprintf("%d", sec)) |
| 172 | } else { |
| 173 | // Assume default duration: 30 seconds |
| 174 | duration = 30 * time.Second |
| 175 | } |
| 176 | } |
| 177 | url.RawQuery = values.Encode() |
| 178 | return url.String(), url.Host, duration |
| 179 | } |
| 180 | |
| 181 | func hasDuration(path string) bool { |
| 182 | for _, trigger := range []string{"profilez", "wallz", "/profile"} { |
| 183 | if strings.Contains(path, trigger) { |
| 184 | return true |
| 185 | } |
| 186 | } |
| 187 | return false |
| 188 | } |
| 189 | |
| 190 | // preprocess does filtering and aggregation of a profile based on the |
| 191 | // requested options. |
| 192 | func preprocess(prof *profile.Profile, ui plugin.UI, f *flags) error { |
| 193 | if *f.flagFocus != "" || *f.flagIgnore != "" || *f.flagHide != "" { |
| 194 | focus, ignore, hide, err := compileFocusIgnore(*f.flagFocus, *f.flagIgnore, *f.flagHide) |
| 195 | if err != nil { |
| 196 | return err |
| 197 | } |
| 198 | fm, im, hm := prof.FilterSamplesByName(focus, ignore, hide) |
| 199 | |
| 200 | warnNoMatches(fm, *f.flagFocus, "Focus", ui) |
| 201 | warnNoMatches(im, *f.flagIgnore, "Ignore", ui) |
| 202 | warnNoMatches(hm, *f.flagHide, "Hide", ui) |
| 203 | } |
| 204 | |
| 205 | if *f.flagTagFocus != "" || *f.flagTagIgnore != "" { |
| 206 | focus, err := compileTagFilter(*f.flagTagFocus, ui) |
| 207 | if err != nil { |
| 208 | return err |
| 209 | } |
| 210 | ignore, err := compileTagFilter(*f.flagTagIgnore, ui) |
| 211 | if err != nil { |
| 212 | return err |
| 213 | } |
| 214 | fm, im := prof.FilterSamplesByTag(focus, ignore) |
| 215 | |
| 216 | warnNoMatches(fm, *f.flagTagFocus, "TagFocus", ui) |
| 217 | warnNoMatches(im, *f.flagTagIgnore, "TagIgnore", ui) |
| 218 | } |
| 219 | |
| 220 | return aggregate(prof, f) |
| 221 | } |
| 222 | |
| 223 | func compileFocusIgnore(focus, ignore, hide string) (f, i, h *regexp.Regexp, err error) { |
| 224 | if focus != "" { |
| 225 | if f, err = regexp.Compile(focus); err != nil { |
| 226 | return nil, nil, nil, fmt.Errorf("parsing focus regexp: %v", err) |
| 227 | } |
| 228 | } |
| 229 | |
| 230 | if ignore != "" { |
| 231 | if i, err = regexp.Compile(ignore); err != nil { |
| 232 | return nil, nil, nil, fmt.Errorf("parsing ignore regexp: %v", err) |
| 233 | } |
| 234 | } |
| 235 | |
| 236 | if hide != "" { |
| 237 | if h, err = regexp.Compile(hide); err != nil { |
| 238 | return nil, nil, nil, fmt.Errorf("parsing hide regexp: %v", err) |
| 239 | } |
| 240 | } |
| 241 | return |
| 242 | } |
| 243 | |
| 244 | func compileTagFilter(filter string, ui plugin.UI) (f func(string, string, int64) bool, err error) { |
| 245 | if filter == "" { |
| 246 | return nil, nil |
| 247 | } |
| 248 | if numFilter := parseTagFilterRange(filter); numFilter != nil { |
| 249 | ui.PrintErr("Interpreted '", filter, "' as range, not regexp") |
| 250 | return func(key, val string, num int64) bool { |
| 251 | if val != "" { |
| 252 | return false |
| 253 | } |
| 254 | return numFilter(num, key) |
| 255 | }, nil |
| 256 | } |
| 257 | fx, err := regexp.Compile(filter) |
| 258 | if err != nil { |
| 259 | return nil, err |
| 260 | } |
| 261 | |
| 262 | return func(key, val string, num int64) bool { |
| 263 | if val == "" { |
| 264 | return false |
| 265 | } |
| 266 | return fx.MatchString(key + ":" + val) |
| 267 | }, nil |
| 268 | } |
| 269 | |
| 270 | var tagFilterRangeRx = regexp.MustCompile("([[:digit:]]+)([[:alpha:]]+)") |
| 271 | |
| 272 | // parseTagFilterRange returns a function to checks if a value is |
| 273 | // contained on the range described by a string. It can recognize |
| 274 | // strings of the form: |
| 275 | // "32kb" -- matches values == 32kb |
| 276 | // ":64kb" -- matches values <= 64kb |
| 277 | // "4mb:" -- matches values >= 4mb |
| 278 | // "12kb:64mb" -- matches values between 12kb and 64mb (both included). |
| 279 | func parseTagFilterRange(filter string) func(int64, string) bool { |
| 280 | ranges := tagFilterRangeRx.FindAllStringSubmatch(filter, 2) |
| 281 | if len(ranges) == 0 { |
| 282 | return nil // No ranges were identified |
| 283 | } |
| 284 | v, err := strconv.ParseInt(ranges[0][1], 10, 64) |
| 285 | if err != nil { |
| 286 | panic(fmt.Errorf("Failed to parse int %s: %v", ranges[0][1], err)) |
| 287 | } |
| 288 | value, unit := report.ScaleValue(v, ranges[0][2], ranges[0][2]) |
| 289 | if len(ranges) == 1 { |
| 290 | switch match := ranges[0][0]; filter { |
| 291 | case match: |
| 292 | return func(v int64, u string) bool { |
| 293 | sv, su := report.ScaleValue(v, u, unit) |
| 294 | return su == unit && sv == value |
| 295 | } |
| 296 | case match + ":": |
| 297 | return func(v int64, u string) bool { |
| 298 | sv, su := report.ScaleValue(v, u, unit) |
| 299 | return su == unit && sv >= value |
| 300 | } |
| 301 | case ":" + match: |
| 302 | return func(v int64, u string) bool { |
| 303 | sv, su := report.ScaleValue(v, u, unit) |
| 304 | return su == unit && sv <= value |
| 305 | } |
| 306 | } |
| 307 | return nil |
| 308 | } |
| 309 | if filter != ranges[0][0]+":"+ranges[1][0] { |
| 310 | return nil |
| 311 | } |
| 312 | if v, err = strconv.ParseInt(ranges[1][1], 10, 64); err != nil { |
| 313 | panic(fmt.Errorf("Failed to parse int %s: %v", ranges[1][1], err)) |
| 314 | } |
| 315 | value2, unit2 := report.ScaleValue(v, ranges[1][2], unit) |
| 316 | if unit != unit2 { |
| 317 | return nil |
| 318 | } |
| 319 | return func(v int64, u string) bool { |
| 320 | sv, su := report.ScaleValue(v, u, unit) |
| 321 | return su == unit && sv >= value && sv <= value2 |
| 322 | } |
| 323 | } |
| 324 | |
| 325 | func warnNoMatches(match bool, rx, option string, ui plugin.UI) { |
| 326 | if !match && rx != "" && rx != "." { |
| 327 | ui.PrintErr(option + " expression matched no samples: " + rx) |
| 328 | } |
| 329 | } |
| 330 | |
| 331 | // grabProfile fetches and symbolizes a profile. |
| 332 | func grabProfile(source, exec, buildid string, fetch plugin.Fetcher, sym plugin.Symbolizer, obj plugin.ObjTool, ui plugin.UI, f *flags) (*profile.Profile, error) { |
| 333 | source, host, duration := adjustURL(source, *f.flagSeconds, ui) |
| 334 | remote := host != "" |
| 335 | |
| 336 | if remote { |
| 337 | ui.Print("Fetching profile from ", source) |
| 338 | if duration != 0 { |
| 339 | ui.Print("Please wait... (" + duration.String() + ")") |
| 340 | } |
| 341 | } |
| 342 | |
| 343 | now := time.Now() |
| 344 | // Fetch profile from source. |
| 345 | // Give 50% slack on the timeout. |
| 346 | p, err := fetch(source, duration+duration/2, ui) |
| 347 | if err != nil { |
| 348 | return nil, err |
| 349 | } |
| 350 | |
| 351 | // Update the time/duration if the profile source doesn't include it. |
| 352 | // TODO(rsilvera): Remove this when we remove support for legacy profiles. |
| 353 | if remote { |
| 354 | if p.TimeNanos == 0 { |
| 355 | p.TimeNanos = now.UnixNano() |
| 356 | } |
| 357 | if duration != 0 && p.DurationNanos == 0 { |
| 358 | p.DurationNanos = int64(duration) |
| 359 | } |
| 360 | } |
| 361 | |
| 362 | // Replace executable/buildID with the options provided in the |
| 363 | // command line. Assume the executable is the first Mapping entry. |
| 364 | if exec != "" || buildid != "" { |
| 365 | if len(p.Mapping) == 0 { |
| 366 | // Create a fake mapping to hold the user option, and associate |
| 367 | // all samples to it. |
| 368 | m := &profile.Mapping{ |
| 369 | ID: 1, |
| 370 | } |
| 371 | for _, l := range p.Location { |
| 372 | l.Mapping = m |
| 373 | } |
| 374 | p.Mapping = []*profile.Mapping{m} |
| 375 | } |
| 376 | if exec != "" { |
| 377 | p.Mapping[0].File = exec |
| 378 | } |
| 379 | if buildid != "" { |
| 380 | p.Mapping[0].BuildID = buildid |
| 381 | } |
| 382 | } |
| 383 | |
| 384 | if err := sym(*f.flagSymbolize, source, p, obj, ui); err != nil { |
| 385 | return nil, err |
| 386 | } |
| 387 | |
| 388 | // Save a copy of any remote profiles, unless the user is explicitly |
| 389 | // saving it. |
| 390 | if remote && !f.isFormat("proto") { |
| 391 | prefix := "pprof." |
| 392 | if len(p.Mapping) > 0 && p.Mapping[0].File != "" { |
| 393 | prefix = prefix + filepath.Base(p.Mapping[0].File) + "." |
| 394 | } |
| 395 | if !strings.ContainsRune(host, os.PathSeparator) { |
| 396 | prefix = prefix + host + "." |
| 397 | } |
| 398 | for _, s := range p.SampleType { |
| 399 | prefix = prefix + s.Type + "." |
| 400 | } |
| 401 | |
| 402 | dir := os.Getenv("PPROF_TMPDIR") |
| 403 | tempFile, err := tempfile.New(dir, prefix, ".pb.gz") |
| 404 | if err == nil { |
| 405 | if err = p.Write(tempFile); err == nil { |
| 406 | ui.PrintErr("Saved profile in ", tempFile.Name()) |
| 407 | } |
| 408 | } |
| 409 | if err != nil { |
| 410 | ui.PrintErr("Could not save profile: ", err) |
| 411 | } |
| 412 | } |
| 413 | |
| 414 | if err := p.Demangle(obj.Demangle); err != nil { |
| 415 | ui.PrintErr("Failed to demangle profile: ", err) |
| 416 | } |
| 417 | |
| 418 | if err := p.CheckValid(); err != nil { |
| 419 | return nil, fmt.Errorf("Grab %s: %v", source, err) |
| 420 | } |
| 421 | |
| 422 | return p, nil |
| 423 | } |
| 424 | |
| 425 | type flags struct { |
| 426 | flagInteractive *bool // Accept commands interactively |
| 427 | flagCommands map[string]*bool // pprof commands without parameters |
| 428 | flagParamCommands map[string]*string // pprof commands with parameters |
| 429 | |
| 430 | flagSVGPan *string // URL to fetch the SVG Pan library |
| 431 | flagOutput *string // Output file name |
| 432 | |
| 433 | flagCum *bool // Sort by cumulative data |
| 434 | flagCallTree *bool // generate a context-sensitive call tree |
| 435 | |
| 436 | flagAddresses *bool // Report at address level |
| 437 | flagLines *bool // Report at source line level |
| 438 | flagFiles *bool // Report at file level |
| 439 | flagFunctions *bool // Report at function level [default] |
| 440 | |
| 441 | flagSymbolize *string // Symbolization options (=none to disable) |
| 442 | flagBuildID *string // Override build if for first mapping |
| 443 | |
| 444 | flagNodeCount *int // Max number of nodes to show |
| 445 | flagNodeFraction *float64 // Hide nodes below <f>*total |
| 446 | flagEdgeFraction *float64 // Hide edges below <f>*total |
| 447 | flagTrim *bool // Set to false to ignore NodeCount/*Fraction |
| 448 | flagFocus *string // Restricts to paths going through a node matching regexp |
| 449 | flagIgnore *string // Skips paths going through any nodes matching regexp |
| 450 | flagHide *string // Skips sample locations matching regexp |
| 451 | flagTagFocus *string // Restrict to samples tagged with key:value matching regexp |
| 452 | flagTagIgnore *string // Discard samples tagged with key:value matching regexp |
| 453 | flagDropNegative *bool // Skip negative values |
| 454 | |
| 455 | flagBase *string // Source for base profile to user for comparison |
| 456 | |
| 457 | flagSeconds *int // Length of time for dynamic profiles |
| 458 | |
| 459 | flagTotalDelay *bool // Display total delay at each region |
| 460 | flagContentions *bool // Display number of delays at each region |
| 461 | flagMeanDelay *bool // Display mean delay at each region |
| 462 | |
| 463 | flagInUseSpace *bool // Display in-use memory size |
| 464 | flagInUseObjects *bool // Display in-use object counts |
| 465 | flagAllocSpace *bool // Display allocated memory size |
| 466 | flagAllocObjects *bool // Display allocated object counts |
| 467 | flagDisplayUnit *string // Measurement unit to use on reports |
| 468 | flagDivideBy *float64 // Ratio to divide sample values |
| 469 | |
| 470 | flagSampleIndex *int // Sample value to use in reports. |
| 471 | flagMean *bool // Use mean of sample_index over count |
| 472 | |
| 473 | flagTools *string |
| 474 | profileSource []string |
| 475 | profileExecName string |
| 476 | |
| 477 | extraUsage string |
| 478 | commands commands.Commands |
| 479 | } |
| 480 | |
| 481 | func (f *flags) isFormat(format string) bool { |
| 482 | if fl := f.flagCommands[format]; fl != nil { |
| 483 | return *fl |
| 484 | } |
| 485 | if fl := f.flagParamCommands[format]; fl != nil { |
| 486 | return *fl != "" |
| 487 | } |
| 488 | return false |
| 489 | } |
| 490 | |
| 491 | // String provides a printable representation for the current set of flags. |
| 492 | func (f *flags) String(p *profile.Profile) string { |
| 493 | var ret string |
| 494 | |
| 495 | if ix := *f.flagSampleIndex; ix != -1 { |
| 496 | ret += fmt.Sprintf(" %-25s : %d (%s)\n", "sample_index", ix, p.SampleType[ix].Type) |
| 497 | } |
| 498 | if ix := *f.flagMean; ix { |
| 499 | ret += boolFlagString("mean") |
| 500 | } |
| 501 | if *f.flagDisplayUnit != "minimum" { |
| 502 | ret += stringFlagString("unit", *f.flagDisplayUnit) |
| 503 | } |
| 504 | |
| 505 | switch { |
| 506 | case *f.flagInteractive: |
| 507 | ret += boolFlagString("interactive") |
| 508 | } |
| 509 | for name, fl := range f.flagCommands { |
| 510 | if *fl { |
| 511 | ret += boolFlagString(name) |
| 512 | } |
| 513 | } |
| 514 | |
| 515 | if *f.flagCum { |
| 516 | ret += boolFlagString("cum") |
| 517 | } |
| 518 | if *f.flagCallTree { |
| 519 | ret += boolFlagString("call_tree") |
| 520 | } |
| 521 | |
| 522 | switch { |
| 523 | case *f.flagAddresses: |
| 524 | ret += boolFlagString("addresses") |
| 525 | case *f.flagLines: |
| 526 | ret += boolFlagString("lines") |
| 527 | case *f.flagFiles: |
| 528 | ret += boolFlagString("files") |
| 529 | case *f.flagFunctions: |
| 530 | ret += boolFlagString("functions") |
| 531 | } |
| 532 | |
| 533 | if *f.flagNodeCount != -1 { |
| 534 | ret += intFlagString("nodecount", *f.flagNodeCount) |
| 535 | } |
| 536 | |
| 537 | ret += floatFlagString("nodefraction", *f.flagNodeFraction) |
| 538 | ret += floatFlagString("edgefraction", *f.flagEdgeFraction) |
| 539 | |
| 540 | if *f.flagFocus != "" { |
| 541 | ret += stringFlagString("focus", *f.flagFocus) |
| 542 | } |
| 543 | if *f.flagIgnore != "" { |
| 544 | ret += stringFlagString("ignore", *f.flagIgnore) |
| 545 | } |
| 546 | if *f.flagHide != "" { |
| 547 | ret += stringFlagString("hide", *f.flagHide) |
| 548 | } |
| 549 | |
| 550 | if *f.flagTagFocus != "" { |
| 551 | ret += stringFlagString("tagfocus", *f.flagTagFocus) |
| 552 | } |
| 553 | if *f.flagTagIgnore != "" { |
| 554 | ret += stringFlagString("tagignore", *f.flagTagIgnore) |
| 555 | } |
| 556 | |
| 557 | return ret |
| 558 | } |
| 559 | |
| 560 | func boolFlagString(label string) string { |
| 561 | return fmt.Sprintf(" %-25s : true\n", label) |
| 562 | } |
| 563 | |
| 564 | func stringFlagString(label, value string) string { |
| 565 | return fmt.Sprintf(" %-25s : %s\n", label, value) |
| 566 | } |
| 567 | |
| 568 | func intFlagString(label string, value int) string { |
| 569 | return fmt.Sprintf(" %-25s : %d\n", label, value) |
| 570 | } |
| 571 | |
| 572 | func floatFlagString(label string, value float64) string { |
| 573 | return fmt.Sprintf(" %-25s : %f\n", label, value) |
| 574 | } |
| 575 | |
| 576 | // Utility routines to set flag values. |
| 577 | func newBool(b bool) *bool { |
| 578 | return &b |
| 579 | } |
| 580 | |
| 581 | func newString(s string) *string { |
| 582 | return &s |
| 583 | } |
| 584 | |
| 585 | func newFloat64(fl float64) *float64 { |
| 586 | return &fl |
| 587 | } |
| 588 | |
| 589 | func newInt(i int) *int { |
| 590 | return &i |
| 591 | } |
| 592 | |
| 593 | func (f *flags) usage(ui plugin.UI) { |
| 594 | var commandMsg []string |
| 595 | for name, cmd := range f.commands { |
| 596 | if cmd.HasParam { |
| 597 | name = name + "=p" |
| 598 | } |
| 599 | commandMsg = append(commandMsg, |
| 600 | fmt.Sprintf(" -%-16s %s", name, cmd.Usage)) |
| 601 | } |
| 602 | |
| 603 | sort.Strings(commandMsg) |
| 604 | |
| 605 | text := usageMsgHdr + strings.Join(commandMsg, "\n") + "\n" + usageMsg + "\n" |
| 606 | if f.extraUsage != "" { |
| 607 | text += f.extraUsage + "\n" |
| 608 | } |
| 609 | text += usageMsgVars |
| 610 | ui.Print(text) |
| 611 | } |
| 612 | |
| 613 | func getFlags(flag plugin.FlagSet, overrides commands.Commands, ui plugin.UI) (*flags, error) { |
| 614 | f := &flags{ |
| 615 | flagInteractive: flag.Bool("interactive", false, "Accepts commands interactively"), |
| 616 | flagCommands: make(map[string]*bool), |
| 617 | flagParamCommands: make(map[string]*string), |
| 618 | |
| 619 | // Filename for file-based output formats, stdout by default. |
| 620 | flagOutput: flag.String("output", "", "Output filename for file-based outputs "), |
| 621 | // Comparisons. |
| 622 | flagBase: flag.String("base", "", "Source for base profile for comparison"), |
| 623 | flagDropNegative: flag.Bool("drop_negative", false, "Ignore negative differences"), |
| 624 | |
| 625 | flagSVGPan: flag.String("svgpan", "https://www.cyberz.org/projects/SVGPan/SVGPan.js", "URL for SVGPan Library"), |
| 626 | // Data sorting criteria. |
| 627 | flagCum: flag.Bool("cum", false, "Sort by cumulative data"), |
| 628 | // Graph handling options. |
| 629 | flagCallTree: flag.Bool("call_tree", false, "Create a context-sensitive call tree"), |
| 630 | // Granularity of output resolution. |
| 631 | flagAddresses: flag.Bool("addresses", false, "Report at address level"), |
| 632 | flagLines: flag.Bool("lines", false, "Report at source line level"), |
| 633 | flagFiles: flag.Bool("files", false, "Report at source file level"), |
| 634 | flagFunctions: flag.Bool("functions", false, "Report at function level [default]"), |
| 635 | // Internal options. |
| 636 | flagSymbolize: flag.String("symbolize", "", "Options for profile symbolization"), |
| 637 | flagBuildID: flag.String("buildid", "", "Override build id for first mapping"), |
| 638 | // Filtering options |
| 639 | flagNodeCount: flag.Int("nodecount", -1, "Max number of nodes to show"), |
| 640 | flagNodeFraction: flag.Float64("nodefraction", 0.005, "Hide nodes below <f>*total"), |
| 641 | flagEdgeFraction: flag.Float64("edgefraction", 0.001, "Hide edges below <f>*total"), |
| 642 | flagTrim: flag.Bool("trim", true, "Honor nodefraction/edgefraction/nodecount defaults"), |
| 643 | flagFocus: flag.String("focus", "", "Restricts to paths going through a node matching regexp"), |
| 644 | flagIgnore: flag.String("ignore", "", "Skips paths going through any nodes matching regexp"), |
| 645 | flagHide: flag.String("hide", "", "Skips nodes matching regexp"), |
| 646 | flagTagFocus: flag.String("tagfocus", "", "Restrict to samples with tags in range or matched by regexp"), |
| 647 | flagTagIgnore: flag.String("tagignore", "", "Discard samples with tags in range or matched by regexp"), |
| 648 | // CPU profile options |
| 649 | flagSeconds: flag.Int("seconds", -1, "Length of time for dynamic profiles"), |
| 650 | // Heap profile options |
| 651 | flagInUseSpace: flag.Bool("inuse_space", false, "Display in-use memory size"), |
| 652 | flagInUseObjects: flag.Bool("inuse_objects", false, "Display in-use object counts"), |
| 653 | flagAllocSpace: flag.Bool("alloc_space", false, "Display allocated memory size"), |
| 654 | flagAllocObjects: flag.Bool("alloc_objects", false, "Display allocated object counts"), |
| 655 | flagDisplayUnit: flag.String("unit", "minimum", "Measurement units to display"), |
| 656 | flagDivideBy: flag.Float64("divide_by", 1.0, "Ratio to divide all samples before visualization"), |
| 657 | flagSampleIndex: flag.Int("sample_index", -1, "Index of sample value to report"), |
| 658 | flagMean: flag.Bool("mean", false, "Average sample value over first value (count)"), |
| 659 | // Contention profile options |
| 660 | flagTotalDelay: flag.Bool("total_delay", false, "Display total delay at each region"), |
| 661 | flagContentions: flag.Bool("contentions", false, "Display number of delays at each region"), |
| 662 | flagMeanDelay: flag.Bool("mean_delay", false, "Display mean delay at each region"), |
| 663 | flagTools: flag.String("tools", os.Getenv("PPROF_TOOLS"), "Path for object tool pathnames"), |
| 664 | extraUsage: flag.ExtraUsage(), |
| 665 | } |
| 666 | |
| 667 | // Flags used during command processing |
| 668 | interactive := &f.flagInteractive |
| 669 | svgpan := &f.flagSVGPan |
| 670 | f.commands = commands.PProf(functionCompleter, interactive, svgpan) |
| 671 | |
| 672 | // Override commands |
| 673 | for name, cmd := range overrides { |
| 674 | f.commands[name] = cmd |
| 675 | } |
| 676 | |
| 677 | for name, cmd := range f.commands { |
| 678 | if cmd.HasParam { |
| 679 | f.flagParamCommands[name] = flag.String(name, "", "Generate a report in "+name+" format, matching regexp") |
| 680 | } else { |
| 681 | f.flagCommands[name] = flag.Bool(name, false, "Generate a report in "+name+" format") |
| 682 | } |
| 683 | } |
| 684 | |
| 685 | args := flag.Parse(func() { f.usage(ui) }) |
| 686 | if len(args) == 0 { |
| 687 | return nil, fmt.Errorf("no profile source specified") |
| 688 | } |
| 689 | |
| 690 | f.profileSource = args |
| 691 | |
| 692 | // Instruct legacy heapz parsers to grab historical allocation data, |
| 693 | // instead of the default in-use data. Not available with tcmalloc. |
| 694 | if *f.flagAllocSpace || *f.flagAllocObjects { |
| 695 | profile.LegacyHeapAllocated = true |
| 696 | } |
| 697 | |
| 698 | if profileDir := os.Getenv("PPROF_TMPDIR"); profileDir == "" { |
| 699 | profileDir = os.Getenv("HOME") + "/pprof" |
| 700 | os.Setenv("PPROF_TMPDIR", profileDir) |
| 701 | if err := os.MkdirAll(profileDir, 0755); err != nil { |
| 702 | return nil, fmt.Errorf("failed to access temp dir %s: %v", profileDir, err) |
| 703 | } |
| 704 | } |
| 705 | |
| 706 | return f, nil |
| 707 | } |
| 708 | |
| 709 | func processFlags(p *profile.Profile, ui plugin.UI, f *flags) error { |
| 710 | flagDis := f.isFormat("disasm") |
| 711 | flagPeek := f.isFormat("peek") |
| 712 | flagWebList := f.isFormat("weblist") |
| 713 | flagList := f.isFormat("list") |
| 714 | |
| 715 | if flagDis || flagWebList { |
| 716 | // Collect all samples at address granularity for assembly |
| 717 | // listing. |
| 718 | f.flagNodeCount = newInt(0) |
| 719 | f.flagAddresses = newBool(true) |
| 720 | f.flagLines = newBool(false) |
| 721 | f.flagFiles = newBool(false) |
| 722 | f.flagFunctions = newBool(false) |
| 723 | } |
| 724 | |
| 725 | if flagPeek { |
| 726 | // Collect all samples at function granularity for peek command |
| 727 | f.flagNodeCount = newInt(0) |
| 728 | f.flagAddresses = newBool(false) |
| 729 | f.flagLines = newBool(false) |
| 730 | f.flagFiles = newBool(false) |
| 731 | f.flagFunctions = newBool(true) |
| 732 | } |
| 733 | |
| 734 | if flagList { |
| 735 | // Collect all samples at fileline granularity for source |
| 736 | // listing. |
| 737 | f.flagNodeCount = newInt(0) |
| 738 | f.flagAddresses = newBool(false) |
| 739 | f.flagLines = newBool(true) |
| 740 | f.flagFiles = newBool(false) |
| 741 | f.flagFunctions = newBool(false) |
| 742 | } |
| 743 | |
| 744 | if !*f.flagTrim { |
| 745 | f.flagNodeCount = newInt(0) |
| 746 | f.flagNodeFraction = newFloat64(0) |
| 747 | f.flagEdgeFraction = newFloat64(0) |
| 748 | } |
| 749 | |
| 750 | if oc := countFlagMap(f.flagCommands, f.flagParamCommands); oc == 0 { |
| 751 | f.flagInteractive = newBool(true) |
| 752 | } else if oc > 1 { |
| 753 | f.usage(ui) |
| 754 | return fmt.Errorf("must set at most one output format") |
| 755 | } |
| 756 | |
| 757 | // Apply nodecount defaults for non-interactive mode. The |
| 758 | // interactive shell will apply defaults for the interactive mode. |
| 759 | if *f.flagNodeCount < 0 && !*f.flagInteractive { |
| 760 | switch { |
| 761 | default: |
| 762 | f.flagNodeCount = newInt(80) |
| 763 | case f.isFormat("text"): |
| 764 | f.flagNodeCount = newInt(0) |
| 765 | } |
| 766 | } |
| 767 | |
| 768 | // Apply legacy options and diagnose conflicts. |
| 769 | if rc := countFlags([]*bool{f.flagAddresses, f.flagLines, f.flagFiles, f.flagFunctions}); rc == 0 { |
| 770 | f.flagFunctions = newBool(true) |
| 771 | } else if rc > 1 { |
| 772 | f.usage(ui) |
| 773 | return fmt.Errorf("must set at most one granularity option") |
| 774 | } |
| 775 | |
| 776 | var err error |
| 777 | si, sm := *f.flagSampleIndex, *f.flagMean || *f.flagMeanDelay |
| 778 | si, err = sampleIndex(p, &f.flagTotalDelay, si, 1, "delay", "-total_delay", err) |
| 779 | si, err = sampleIndex(p, &f.flagMeanDelay, si, 1, "delay", "-mean_delay", err) |
| 780 | si, err = sampleIndex(p, &f.flagContentions, si, 0, "contentions", "-contentions", err) |
| 781 | |
| 782 | si, err = sampleIndex(p, &f.flagInUseSpace, si, 1, "inuse_space", "-inuse_space", err) |
| 783 | si, err = sampleIndex(p, &f.flagInUseObjects, si, 0, "inuse_objects", "-inuse_objects", err) |
| 784 | si, err = sampleIndex(p, &f.flagAllocSpace, si, 1, "alloc_space", "-alloc_space", err) |
| 785 | si, err = sampleIndex(p, &f.flagAllocObjects, si, 0, "alloc_objects", "-alloc_objects", err) |
| 786 | |
| 787 | if si == -1 { |
| 788 | // Use last value if none is requested. |
| 789 | si = len(p.SampleType) - 1 |
| 790 | } else if si < 0 || si >= len(p.SampleType) { |
| 791 | err = fmt.Errorf("sample_index value %d out of range [0..%d]", si, len(p.SampleType)-1) |
| 792 | } |
| 793 | |
| 794 | if err != nil { |
| 795 | f.usage(ui) |
| 796 | return err |
| 797 | } |
| 798 | f.flagSampleIndex, f.flagMean = newInt(si), newBool(sm) |
| 799 | return nil |
| 800 | } |
| 801 | |
| 802 | func sampleIndex(p *profile.Profile, flag **bool, |
| 803 | sampleIndex int, |
| 804 | newSampleIndex int, |
| 805 | sampleType, option string, |
| 806 | err error) (int, error) { |
| 807 | if err != nil || !**flag { |
| 808 | return sampleIndex, err |
| 809 | } |
| 810 | *flag = newBool(false) |
| 811 | if sampleIndex != -1 { |
| 812 | return 0, fmt.Errorf("set at most one sample value selection option") |
| 813 | } |
| 814 | if newSampleIndex >= len(p.SampleType) || |
| 815 | p.SampleType[newSampleIndex].Type != sampleType { |
| 816 | return 0, fmt.Errorf("option %s not valid for this profile", option) |
| 817 | } |
| 818 | return newSampleIndex, nil |
| 819 | } |
| 820 | |
| 821 | func countFlags(bs []*bool) int { |
| 822 | var c int |
| 823 | for _, b := range bs { |
| 824 | if *b { |
| 825 | c++ |
| 826 | } |
| 827 | } |
| 828 | return c |
| 829 | } |
| 830 | |
| 831 | func countFlagMap(bms map[string]*bool, bmrxs map[string]*string) int { |
| 832 | var c int |
| 833 | for _, b := range bms { |
| 834 | if *b { |
| 835 | c++ |
| 836 | } |
| 837 | } |
| 838 | for _, s := range bmrxs { |
| 839 | if *s != "" { |
| 840 | c++ |
| 841 | } |
| 842 | } |
| 843 | return c |
| 844 | } |
| 845 | |
| 846 | var usageMsgHdr = "usage: pprof [options] [binary] <profile source> ...\n" + |
| 847 | "Output format (only set one):\n" |
| 848 | |
| 849 | var usageMsg = "Output file parameters (for file-based output formats):\n" + |
| 850 | " -output=f Generate output on file f (stdout by default)\n" + |
| 851 | "Output granularity (only set one):\n" + |
| 852 | " -functions Report at function level [default]\n" + |
| 853 | " -files Report at source file level\n" + |
| 854 | " -lines Report at source line level\n" + |
| 855 | " -addresses Report at address level\n" + |
| 856 | "Comparison options:\n" + |
| 857 | " -base <profile> Show delta from this profile\n" + |
| 858 | " -drop_negative Ignore negative differences\n" + |
| 859 | "Sorting options:\n" + |
| 860 | " -cum Sort by cumulative data\n\n" + |
| 861 | "Dynamic profile options:\n" + |
| 862 | " -seconds=N Length of time for dynamic profiles\n" + |
| 863 | "Profile trimming options:\n" + |
| 864 | " -nodecount=N Max number of nodes to show\n" + |
| 865 | " -nodefraction=f Hide nodes below <f>*total\n" + |
| 866 | " -edgefraction=f Hide edges below <f>*total\n" + |
| 867 | "Sample value selection option (by index):\n" + |
| 868 | " -sample_index Index of sample value to display\n" + |
| 869 | " -mean Average sample value over first value\n" + |
| 870 | "Sample value selection option (for heap profiles):\n" + |
| 871 | " -inuse_space Display in-use memory size\n" + |
| 872 | " -inuse_objects Display in-use object counts\n" + |
| 873 | " -alloc_space Display allocated memory size\n" + |
| 874 | " -alloc_objects Display allocated object counts\n" + |
| 875 | "Sample value selection option (for contention profiles):\n" + |
| 876 | " -total_delay Display total delay at each region\n" + |
| 877 | " -contentions Display number of delays at each region\n" + |
| 878 | " -mean_delay Display mean delay at each region\n" + |
| 879 | "Filtering options:\n" + |
| 880 | " -focus=r Restricts to paths going through a node matching regexp\n" + |
| 881 | " -ignore=r Skips paths going through any nodes matching regexp\n" + |
| 882 | " -tagfocus=r Restrict to samples tagged with key:value matching regexp\n" + |
| 883 | " Restrict to samples with numeric tags in range (eg \"32kb:1mb\")\n" + |
| 884 | " -tagignore=r Discard samples tagged with key:value matching regexp\n" + |
| 885 | " Avoid samples with numeric tags in range (eg \"1mb:\")\n" + |
| 886 | "Miscellaneous:\n" + |
| 887 | " -call_tree Generate a context-sensitive call tree\n" + |
| 888 | " -unit=u Convert all samples to unit u for display\n" + |
| 889 | " -show_bytes Display all space in bytes\n" + |
| 890 | " -divide_by=f Scale all samples by dividing them by f\n" + |
| 891 | " -buildid=id Override build id for main binary in profile\n" + |
| 892 | " -tools=path Search path for object-level tools\n" + |
| 893 | " -help This message" |
| 894 | |
| 895 | var usageMsgVars = "Environment Variables:\n" + |
| 896 | " PPROF_TMPDIR Location for temporary files (default $HOME/pprof)\n" + |
| 897 | " PPROF_TOOLS Search path for object-level tools\n" + |
| 898 | " PPROF_BINARY_PATH Search path for local binary files\n" + |
| 899 | " default: $HOME/pprof/binaries\n" + |
| 900 | " finds binaries by $name and $buildid/$name" |
| 901 | |
| 902 | func aggregate(prof *profile.Profile, f *flags) error { |
| 903 | switch { |
| 904 | case f.isFormat("proto"), f.isFormat("raw"): |
| 905 | // No aggregation for raw profiles. |
| 906 | case f.isFormat("callgrind"): |
| 907 | // Aggregate to file/line for callgrind. |
| 908 | fallthrough |
| 909 | case *f.flagLines: |
| 910 | return prof.Aggregate(true, true, true, true, false) |
| 911 | case *f.flagFiles: |
| 912 | return prof.Aggregate(true, false, true, false, false) |
| 913 | case *f.flagFunctions: |
| 914 | return prof.Aggregate(true, true, false, false, false) |
| 915 | case f.isFormat("weblist"), f.isFormat("disasm"): |
| 916 | return prof.Aggregate(false, true, true, true, true) |
| 917 | } |
| 918 | return nil |
| 919 | } |
| 920 | |
| 921 | // parseOptions parses the options into report.Options |
| 922 | // Returns a function to postprocess the report after generation. |
| 923 | func parseOptions(f *flags) (o *report.Options, p commands.PostProcessor, err error) { |
| 924 | |
| 925 | if *f.flagDivideBy == 0 { |
| 926 | return nil, nil, fmt.Errorf("zero divisor specified") |
| 927 | } |
| 928 | |
| 929 | o = &report.Options{ |
| 930 | CumSort: *f.flagCum, |
| 931 | CallTree: *f.flagCallTree, |
| 932 | PrintAddresses: *f.flagAddresses, |
| 933 | DropNegative: *f.flagDropNegative, |
| 934 | Ratio: 1 / *f.flagDivideBy, |
| 935 | |
| 936 | NodeCount: *f.flagNodeCount, |
| 937 | NodeFraction: *f.flagNodeFraction, |
| 938 | EdgeFraction: *f.flagEdgeFraction, |
| 939 | OutputUnit: *f.flagDisplayUnit, |
| 940 | } |
| 941 | |
| 942 | for cmd, b := range f.flagCommands { |
| 943 | if *b { |
| 944 | pcmd := f.commands[cmd] |
| 945 | o.OutputFormat = pcmd.Format |
| 946 | return o, pcmd.PostProcess, nil |
| 947 | } |
| 948 | } |
| 949 | |
| 950 | for cmd, rx := range f.flagParamCommands { |
| 951 | if *rx != "" { |
| 952 | pcmd := f.commands[cmd] |
| 953 | if o.Symbol, err = regexp.Compile(*rx); err != nil { |
| 954 | return nil, nil, fmt.Errorf("parsing -%s regexp: %v", cmd, err) |
| 955 | } |
| 956 | o.OutputFormat = pcmd.Format |
| 957 | return o, pcmd.PostProcess, nil |
| 958 | } |
| 959 | } |
| 960 | |
| 961 | return nil, nil, fmt.Errorf("no output format selected") |
| 962 | } |
| 963 | |
| 964 | type sampleValueFunc func(*profile.Sample) int64 |
| 965 | |
| 966 | // sampleFormat returns a function to extract values out of a profile.Sample, |
| 967 | // and the type/units of those values. |
| 968 | func sampleFormat(p *profile.Profile, f *flags) (sampleValueFunc, string, string) { |
| 969 | valueIndex := *f.flagSampleIndex |
| 970 | |
| 971 | if *f.flagMean { |
| 972 | return meanExtractor(valueIndex), "mean_" + p.SampleType[valueIndex].Type, p.SampleType[valueIndex].Unit |
| 973 | } |
| 974 | |
| 975 | return valueExtractor(valueIndex), p.SampleType[valueIndex].Type, p.SampleType[valueIndex].Unit |
| 976 | } |
| 977 | |
| 978 | func valueExtractor(ix int) sampleValueFunc { |
| 979 | return func(s *profile.Sample) int64 { |
| 980 | return s.Value[ix] |
| 981 | } |
| 982 | } |
| 983 | |
| 984 | func meanExtractor(ix int) sampleValueFunc { |
| 985 | return func(s *profile.Sample) int64 { |
| 986 | if s.Value[0] == 0 { |
| 987 | return 0 |
| 988 | } |
| 989 | return s.Value[ix] / s.Value[0] |
| 990 | } |
| 991 | } |
| 992 | |
| 993 | func generate(interactive bool, prof *profile.Profile, obj plugin.ObjTool, ui plugin.UI, f *flags) error { |
| 994 | o, postProcess, err := parseOptions(f) |
| 995 | if err != nil { |
| 996 | return err |
| 997 | } |
| 998 | |
| 999 | var w io.Writer |
| 1000 | if *f.flagOutput == "" { |
| 1001 | w = os.Stdout |
| 1002 | } else { |
| 1003 | ui.PrintErr("Generating report in ", *f.flagOutput) |
| 1004 | outputFile, err := os.Create(*f.flagOutput) |
| 1005 | if err != nil { |
| 1006 | return err |
| 1007 | } |
| 1008 | defer outputFile.Close() |
| 1009 | w = outputFile |
| 1010 | } |
| 1011 | |
| 1012 | value, stype, unit := sampleFormat(prof, f) |
| 1013 | o.SampleType = stype |
| 1014 | rpt := report.New(prof, *o, value, unit) |
| 1015 | |
| 1016 | // Do not apply filters if we're just generating a proto, so we |
| 1017 | // still have all the data. |
| 1018 | if o.OutputFormat != report.Proto { |
| 1019 | // Delay applying focus/ignore until after creating the report so |
| 1020 | // the report reflects the total number of samples. |
| 1021 | if err := preprocess(prof, ui, f); err != nil { |
| 1022 | return err |
| 1023 | } |
| 1024 | } |
| 1025 | |
| 1026 | if postProcess == nil { |
| 1027 | return report.Generate(w, rpt, obj) |
| 1028 | } |
| 1029 | |
| 1030 | var dot bytes.Buffer |
| 1031 | if err = report.Generate(&dot, rpt, obj); err != nil { |
| 1032 | return err |
| 1033 | } |
| 1034 | |
| 1035 | return postProcess(&dot, w, ui) |
| 1036 | } |