1 // Copyright 2014 Google Inc. All Rights Reserved. 2 // 3 // Licensed under the Apache License, Version 2.0 (the "License"); 4 // you may not use this file except in compliance with the License. 5 // You may obtain a copy of the License at 6 // 7 // http://www.apache.org/licenses/LICENSE-2.0 8 // 9 // Unless required by applicable law or agreed to in writing, software 10 // distributed under the License is distributed on an "AS IS" BASIS, 11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 // See the License for the specific language governing permissions and 13 // limitations under the License. 14 15 package driver 16 17 import ( 18 "bytes" 19 "fmt" 20 "io" 21 "os" 22 "os/exec" 23 "runtime" 24 "sort" 25 "strconv" 26 "strings" 27 "time" 28 29 "github.com/google/pprof/internal/plugin" 30 "github.com/google/pprof/internal/report" 31 "github.com/google/pprof/third_party/svg" 32 ) 33 34 // commands describes the commands accepted by pprof. 35 type commands map[string]*command 36 37 // command describes the actions for a pprof command. Includes a 38 // function for command-line completion, the report format to use 39 // during report generation, any postprocessing functions, and whether 40 // the command expects a regexp parameter (typically a function name). 41 type command struct { 42 format int // report format to generate 43 postProcess PostProcessor // postprocessing to run on report 44 visualizer PostProcessor // display output using some callback 45 hasParam bool // collect a parameter from the CLI 46 description string // single-line description text saying what the command does 47 usage string // multi-line help text saying how the command is used 48 } 49 50 // help returns a help string for a command. 51 func (c *command) help(name string) string { 52 message := c.description + "\n" 53 if c.usage != "" { 54 message += " Usage:\n" 55 lines := strings.Split(c.usage, "\n") 56 for _, line := range lines { 57 message += fmt.Sprintf(" %s\n", line) 58 } 59 } 60 return message + "\n" 61 } 62 63 // AddCommand adds an additional command to the set of commands 64 // accepted by pprof. This enables extensions to add new commands for 65 // specialized visualization formats. If the command specified already 66 // exists, it is overwritten. 67 func AddCommand(cmd string, format int, post PostProcessor, desc, usage string) { 68 pprofCommands[cmd] = &command{format, post, nil, false, desc, usage} 69 } 70 71 // SetVariableDefault sets the default value for a pprof 72 // variable. This enables extensions to set their own defaults. 73 func SetVariableDefault(variable, value string) { 74 if v := pprofVariables[variable]; v != nil { 75 v.value = value 76 } 77 } 78 79 // PostProcessor is a function that applies post-processing to the report output 80 type PostProcessor func(input io.Reader, output io.Writer, ui plugin.UI) error 81 82 // interactiveMode is true if pprof is running on interactive mode, reading 83 // commands from its shell. 84 var interactiveMode = false 85 86 // pprofCommands are the report generation commands recognized by pprof. 87 var pprofCommands = commands{ 88 // Commands that require no post-processing. 89 "comments": {report.Comments, nil, nil, false, "Output all profile comments", ""}, 90 "disasm": {report.Dis, nil, nil, true, "Output assembly listings annotated with samples", listHelp("disasm", true)}, 91 "dot": {report.Dot, nil, nil, false, "Outputs a graph in DOT format", reportHelp("dot", false, true)}, 92 "list": {report.List, nil, nil, true, "Output annotated source for functions matching regexp", listHelp("list", false)}, 93 "peek": {report.Tree, nil, nil, true, "Output callers/callees of functions matching regexp", "peek func_regex\nDisplay callers and callees of functions matching func_regex."}, 94 "raw": {report.Raw, nil, nil, false, "Outputs a text representation of the raw profile", ""}, 95 "tags": {report.Tags, nil, nil, false, "Outputs all tags in the profile", "tags [tag_regex]* [-ignore_regex]* [>file]\nList tags with key:value matching tag_regex and exclude ignore_regex."}, 96 "text": {report.Text, nil, nil, false, "Outputs top entries in text form", reportHelp("text", true, true)}, 97 "top": {report.Text, nil, nil, false, "Outputs top entries in text form", reportHelp("top", true, true)}, 98 "traces": {report.Traces, nil, nil, false, "Outputs all profile samples in text form", ""}, 99 "tree": {report.Tree, nil, nil, false, "Outputs a text rendering of call graph", reportHelp("tree", true, true)}, 100 101 // Save binary formats to a file 102 "callgrind": {report.Callgrind, nil, awayFromTTY("callgraph.out"), false, "Outputs a graph in callgrind format", reportHelp("callgrind", false, true)}, 103 "proto": {report.Proto, nil, awayFromTTY("pb.gz"), false, "Outputs the profile in compressed protobuf format", ""}, 104 "topproto": {report.TopProto, nil, awayFromTTY("pb.gz"), false, "Outputs top entries in compressed protobuf format", ""}, 105 106 // Generate report in DOT format and postprocess with dot 107 "gif": {report.Dot, invokeDot("gif"), awayFromTTY("gif"), false, "Outputs a graph image in GIF format", reportHelp("gif", false, true)}, 108 "pdf": {report.Dot, invokeDot("pdf"), awayFromTTY("pdf"), false, "Outputs a graph in PDF format", reportHelp("pdf", false, true)}, 109 "png": {report.Dot, invokeDot("png"), awayFromTTY("png"), false, "Outputs a graph image in PNG format", reportHelp("png", false, true)}, 110 "ps": {report.Dot, invokeDot("ps"), awayFromTTY("ps"), false, "Outputs a graph in PS format", reportHelp("ps", false, true)}, 111 112 // Save SVG output into a file 113 "svg": {report.Dot, massageDotSVG(), awayFromTTY("svg"), false, "Outputs a graph in SVG format", reportHelp("svg", false, true)}, 114 115 // Visualize postprocessed dot output 116 "eog": {report.Dot, invokeDot("svg"), invokeVisualizer("svg", []string{"eog"}), false, "Visualize graph through eog", reportHelp("eog", false, false)}, 117 "evince": {report.Dot, invokeDot("pdf"), invokeVisualizer("pdf", []string{"evince"}), false, "Visualize graph through evince", reportHelp("evince", false, false)}, 118 "gv": {report.Dot, invokeDot("ps"), invokeVisualizer("ps", []string{"gv --noantialias"}), false, "Visualize graph through gv", reportHelp("gv", false, false)}, 119 "web": {report.Dot, massageDotSVG(), invokeVisualizer("svg", browsers()), false, "Visualize graph through web browser", reportHelp("web", false, false)}, 120 121 // Visualize callgrind output 122 "kcachegrind": {report.Callgrind, nil, invokeVisualizer("grind", kcachegrind), false, "Visualize report in KCachegrind", reportHelp("kcachegrind", false, false)}, 123 124 // Visualize HTML directly generated by report. 125 "weblist": {report.WebList, nil, invokeVisualizer("html", browsers()), true, "Display annotated source in a web browser", listHelp("weblist", false)}, 126 } 127 128 // pprofVariables are the configuration parameters that affect the 129 // reported generated by pprof. 130 var pprofVariables = variables{ 131 // Filename for file-based output formats, stdout by default. 132 "output": &variable{stringKind, "", "", helpText("Output filename for file-based outputs")}, 133 134 // Comparisons. 135 "drop_negative": &variable{boolKind, "f", "", helpText( 136 "Ignore negative differences", 137 "Do not show any locations with values <0.")}, 138 139 // Comparisons. 140 "positive_percentages": &variable{boolKind, "f", "", helpText( 141 "Ignore negative samples when computing percentages", 142 "Do not count negative samples when computing the total value", 143 "of the profile, used to compute percentages. If set, and the -base", 144 "option is used, percentages reported will be computed against the", 145 "main profile, ignoring the base profile.")}, 146 147 // Graph handling options. 148 "call_tree": &variable{boolKind, "f", "", helpText( 149 "Create a context-sensitive call tree", 150 "Treat locations reached through different paths as separate.")}, 151 152 // Display options. 153 "relative_percentages": &variable{boolKind, "f", "", helpText( 154 "Show percentages relative to focused subgraph", 155 "If unset, percentages are relative to full graph before focusing", 156 "to facilitate comparison with original graph.")}, 157 "unit": &variable{stringKind, "minimum", "", helpText( 158 "Measurement units to display", 159 "Scale the sample values to this unit.", 160 "For time-based profiles, use seconds, milliseconds, nanoseconds, etc.", 161 "For memory profiles, use megabytes, kilobytes, bytes, etc.", 162 "Using auto will scale each value independently to the most natural unit.")}, 163 "compact_labels": &variable{boolKind, "f", "", "Show minimal headers"}, 164 "source_path": &variable{stringKind, "", "", "Search path for source files"}, 165 166 // Filtering options 167 "nodecount": &variable{intKind, "-1", "", helpText( 168 "Max number of nodes to show", 169 "Uses heuristics to limit the number of locations to be displayed.", 170 "On graphs, dotted edges represent paths through nodes that have been removed.")}, 171 "nodefraction": &variable{floatKind, "0.005", "", "Hide nodes below <f>*total"}, 172 "edgefraction": &variable{floatKind, "0.001", "", "Hide edges below <f>*total"}, 173 "trim": &variable{boolKind, "t", "", helpText( 174 "Honor nodefraction/edgefraction/nodecount defaults", 175 "Set to false to get the full profile, without any trimming.")}, 176 "focus": &variable{stringKind, "", "", helpText( 177 "Restricts to samples going through a node matching regexp", 178 "Discard samples that do not include a node matching this regexp.", 179 "Matching includes the function name, filename or object name.")}, 180 "ignore": &variable{stringKind, "", "", helpText( 181 "Skips paths going through any nodes matching regexp", 182 "If set, discard samples that include a node matching this regexp.", 183 "Matching includes the function name, filename or object name.")}, 184 "prune_from": &variable{stringKind, "", "", helpText( 185 "Drops any functions below the matched frame.", 186 "If set, any frames matching the specified regexp and any frames", 187 "below it will be dropped from each sample.")}, 188 "hide": &variable{stringKind, "", "", helpText( 189 "Skips nodes matching regexp", 190 "Discard nodes that match this location.", 191 "Other nodes from samples that include this location will be shown.", 192 "Matching includes the function name, filename or object name.")}, 193 "show": &variable{stringKind, "", "", helpText( 194 "Only show nodes matching regexp", 195 "If set, only show nodes that match this location.", 196 "Matching includes the function name, filename or object name.")}, 197 "tagfocus": &variable{stringKind, "", "", helpText( 198 "Restricts to samples with tags in range or matched by regexp", 199 "Use name=value syntax to limit the matching to a specific tag.", 200 "Numeric tag filter examples: 1kb, 1kb:10kb, memory=32mb:", 201 "String tag filter examples: foo, foo.*bar, mytag=foo.*bar")}, 202 "tagignore": &variable{stringKind, "", "", helpText( 203 "Discard samples with tags in range or matched by regexp", 204 "Use name=value syntax to limit the matching to a specific tag.", 205 "Numeric tag filter examples: 1kb, 1kb:10kb, memory=32mb:", 206 "String tag filter examples: foo, foo.*bar, mytag=foo.*bar")}, 207 "tagshow": &variable{stringKind, "", "", helpText( 208 "Only consider tags matching this regexp", 209 "Discard tags that do not match this regexp")}, 210 "taghide": &variable{stringKind, "", "", helpText( 211 "Skip tags matching this regexp", 212 "Discard tags that match this regexp")}, 213 // Heap profile options 214 "divide_by": &variable{floatKind, "1", "", helpText( 215 "Ratio to divide all samples before visualization", 216 "Divide all samples values by a constant, eg the number of processors or jobs.")}, 217 "mean": &variable{boolKind, "f", "", helpText( 218 "Average sample value over first value (count)", 219 "For memory profiles, report average memory per allocation.", 220 "For time-based profiles, report average time per event.")}, 221 "sample_index": &variable{stringKind, "", "", helpText( 222 "Sample value to report (0-based index or name)", 223 "Profiles contain multiple values per sample.", 224 "Use sample_index=i to select the ith value (starting at 0).")}, 225 "normalize": &variable{boolKind, "f", "", helpText( 226 "Scales profile based on the base profile.")}, 227 228 // Data sorting criteria 229 "flat": &variable{boolKind, "t", "cumulative", helpText("Sort entries based on own weight")}, 230 "cum": &variable{boolKind, "f", "cumulative", helpText("Sort entries based on cumulative weight")}, 231 232 // Output granularity 233 "functions": &variable{boolKind, "t", "granularity", helpText( 234 "Aggregate at the function level.", 235 "Takes into account the filename/lineno where the function was defined.")}, 236 "files": &variable{boolKind, "f", "granularity", "Aggregate at the file level."}, 237 "lines": &variable{boolKind, "f", "granularity", "Aggregate at the source code line level."}, 238 "addresses": &variable{boolKind, "f", "granularity", helpText( 239 "Aggregate at the function level.", 240 "Includes functions' addresses in the output.")}, 241 "noinlines": &variable{boolKind, "f", "granularity", helpText( 242 "Aggregate at the function level.", 243 "Attributes inlined functions to their first out-of-line caller.")}, 244 "addressnoinlines": &variable{boolKind, "f", "granularity", helpText( 245 "Aggregate at the function level, including functions' addresses in the output.", 246 "Attributes inlined functions to their first out-of-line caller.")}, 247 } 248 249 func helpText(s ...string) string { 250 return strings.Join(s, "\n") + "\n" 251 } 252 253 // usage returns a string describing the pprof commands and variables. 254 // if commandLine is set, the output reflect cli usage. 255 func usage(commandLine bool) string { 256 var prefix string 257 if commandLine { 258 prefix = "-" 259 } 260 fmtHelp := func(c, d string) string { 261 return fmt.Sprintf(" %-16s %s", c, strings.SplitN(d, "\n", 2)[0]) 262 } 263 264 var commands []string 265 for name, cmd := range pprofCommands { 266 commands = append(commands, fmtHelp(prefix+name, cmd.description)) 267 } 268 sort.Strings(commands) 269 270 var help string 271 if commandLine { 272 help = " Output formats (select at most one):\n" 273 } else { 274 help = " Commands:\n" 275 commands = append(commands, fmtHelp("o/options", "List options and their current values")) 276 commands = append(commands, fmtHelp("quit/exit/^D", "Exit pprof")) 277 } 278 279 help = help + strings.Join(commands, "\n") + "\n\n" + 280 " Options:\n" 281 282 // Print help for variables after sorting them. 283 // Collect radio variables by their group name to print them together. 284 radioOptions := make(map[string][]string) 285 var variables []string 286 for name, vr := range pprofVariables { 287 if vr.group != "" { 288 radioOptions[vr.group] = append(radioOptions[vr.group], name) 289 continue 290 } 291 variables = append(variables, fmtHelp(prefix+name, vr.help)) 292 } 293 sort.Strings(variables) 294 295 help = help + strings.Join(variables, "\n") + "\n\n" + 296 " Option groups (only set one per group):\n" 297 298 var radioStrings []string 299 for radio, ops := range radioOptions { 300 sort.Strings(ops) 301 s := []string{fmtHelp(radio, "")} 302 for _, op := range ops { 303 s = append(s, " "+fmtHelp(prefix+op, pprofVariables[op].help)) 304 } 305 306 radioStrings = append(radioStrings, strings.Join(s, "\n")) 307 } 308 sort.Strings(radioStrings) 309 return help + strings.Join(radioStrings, "\n") 310 } 311 312 func reportHelp(c string, cum, redirect bool) string { 313 h := []string{ 314 c + " [n] [focus_regex]* [-ignore_regex]*", 315 "Include up to n samples", 316 "Include samples matching focus_regex, and exclude ignore_regex.", 317 } 318 if cum { 319 h[0] += " [-cum]" 320 h = append(h, "-cum sorts the output by cumulative weight") 321 } 322 if redirect { 323 h[0] += " >f" 324 h = append(h, "Optionally save the report on the file f") 325 } 326 return strings.Join(h, "\n") 327 } 328 329 func listHelp(c string, redirect bool) string { 330 h := []string{ 331 c + "<func_regex|address> [-focus_regex]* [-ignore_regex]*", 332 "Include functions matching func_regex, or including the address specified.", 333 "Include samples matching focus_regex, and exclude ignore_regex.", 334 } 335 if redirect { 336 h[0] += " >f" 337 h = append(h, "Optionally save the report on the file f") 338 } 339 return strings.Join(h, "\n") 340 } 341 342 // browsers returns a list of commands to attempt for web visualization. 343 func browsers() []string { 344 cmds := []string{"chrome", "google-chrome", "firefox"} 345 switch runtime.GOOS { 346 case "darwin": 347 return append(cmds, "/usr/bin/open") 348 case "windows": 349 return append(cmds, "cmd /c start") 350 default: 351 userBrowser := os.Getenv("BROWSER") 352 if userBrowser != "" { 353 cmds = append([]string{userBrowser, "sensible-browser"}, cmds...) 354 } else { 355 cmds = append([]string{"sensible-browser"}, cmds...) 356 } 357 return append(cmds, "xdg-open") 358 } 359 } 360 361 var kcachegrind = []string{"kcachegrind"} 362 363 // awayFromTTY saves the output in a file if it would otherwise go to 364 // the terminal screen. This is used to avoid dumping binary data on 365 // the screen. 366 func awayFromTTY(format string) PostProcessor { 367 return func(input io.Reader, output io.Writer, ui plugin.UI) error { 368 if output == os.Stdout && (ui.IsTerminal() || interactiveMode) { 369 tempFile, err := newTempFile("", "profile", "."+format) 370 if err != nil { 371 return err 372 } 373 ui.PrintErr("Generating report in ", tempFile.Name()) 374 output = tempFile 375 } 376 _, err := io.Copy(output, input) 377 return err 378 } 379 } 380 381 func invokeDot(format string) PostProcessor { 382 return func(input io.Reader, output io.Writer, ui plugin.UI) error { 383 cmd := exec.Command("dot", "-T"+format) 384 cmd.Stdin, cmd.Stdout, cmd.Stderr = input, output, os.Stderr 385 if err := cmd.Run(); err != nil { 386 return fmt.Errorf("Failed to execute dot. Is Graphviz installed? Error: %v", err) 387 } 388 return nil 389 } 390 } 391 392 // massageDotSVG invokes the dot tool to generate an SVG image and alters 393 // the image to have panning capabilities when viewed in a browser. 394 func massageDotSVG() PostProcessor { 395 generateSVG := invokeDot("svg") 396 return func(input io.Reader, output io.Writer, ui plugin.UI) error { 397 baseSVG := new(bytes.Buffer) 398 if err := generateSVG(input, baseSVG, ui); err != nil { 399 return err 400 } 401 _, err := output.Write([]byte(svg.Massage(baseSVG.String()))) 402 return err 403 } 404 } 405 406 func invokeVisualizer(suffix string, visualizers []string) PostProcessor { 407 return func(input io.Reader, output io.Writer, ui plugin.UI) error { 408 tempFile, err := newTempFile(os.TempDir(), "pprof", "."+suffix) 409 if err != nil { 410 return err 411 } 412 deferDeleteTempFile(tempFile.Name()) 413 if _, err := io.Copy(tempFile, input); err != nil { 414 return err 415 } 416 tempFile.Close() 417 // Try visualizers until one is successful 418 for _, v := range visualizers { 419 // Separate command and arguments for exec.Command. 420 args := strings.Split(v, " ") 421 if len(args) == 0 { 422 continue 423 } 424 viewer := exec.Command(args[0], append(args[1:], tempFile.Name())...) 425 viewer.Stderr = os.Stderr 426 if err = viewer.Start(); err == nil { 427 // Wait for a second so that the visualizer has a chance to 428 // open the input file. This needs to be done even if we're 429 // waiting for the visualizer as it can be just a wrapper that 430 // spawns a browser tab and returns right away. 431 defer func(t <-chan time.Time) { 432 <-t 433 }(time.After(time.Second)) 434 // On interactive mode, let the visualizer run in the background 435 // so other commands can be issued. 436 if !interactiveMode { 437 return viewer.Wait() 438 } 439 return nil 440 } 441 } 442 return err 443 } 444 } 445 446 // variables describe the configuration parameters recognized by pprof. 447 type variables map[string]*variable 448 449 // variable is a single configuration parameter. 450 type variable struct { 451 kind int // How to interpret the value, must be one of the enums below. 452 value string // Effective value. Only values appropriate for the Kind should be set. 453 group string // boolKind variables with the same Group != "" cannot be set simultaneously. 454 help string // Text describing the variable, in multiple lines separated by newline. 455 } 456 457 const ( 458 // variable.kind must be one of these variables. 459 boolKind = iota 460 intKind 461 floatKind 462 stringKind 463 ) 464 465 // set updates the value of a variable, checking that the value is 466 // suitable for the variable Kind. 467 func (vars variables) set(name, value string) error { 468 v := vars[name] 469 if v == nil { 470 return fmt.Errorf("no variable %s", name) 471 } 472 var err error 473 switch v.kind { 474 case boolKind: 475 var b bool 476 if b, err = stringToBool(value); err == nil { 477 if v.group != "" && !b { 478 err = fmt.Errorf("%q can only be set to true", name) 479 } 480 } 481 case intKind: 482 _, err = strconv.Atoi(value) 483 case floatKind: 484 _, err = strconv.ParseFloat(value, 64) 485 case stringKind: 486 // Remove quotes, particularly useful for empty values. 487 if len(value) > 1 && strings.HasPrefix(value, `"`) && strings.HasSuffix(value, `"`) { 488 value = value[1 : len(value)-1] 489 } 490 } 491 if err != nil { 492 return err 493 } 494 vars[name].value = value 495 if group := vars[name].group; group != "" { 496 for vname, vvar := range vars { 497 if vvar.group == group && vname != name { 498 vvar.value = "f" 499 } 500 } 501 } 502 return err 503 } 504 505 // boolValue returns the value of a boolean variable. 506 func (v *variable) boolValue() bool { 507 b, err := stringToBool(v.value) 508 if err != nil { 509 panic("unexpected value " + v.value + " for bool ") 510 } 511 return b 512 } 513 514 // intValue returns the value of an intKind variable. 515 func (v *variable) intValue() int { 516 i, err := strconv.Atoi(v.value) 517 if err != nil { 518 panic("unexpected value " + v.value + " for int ") 519 } 520 return i 521 } 522 523 // floatValue returns the value of a Float variable. 524 func (v *variable) floatValue() float64 { 525 f, err := strconv.ParseFloat(v.value, 64) 526 if err != nil { 527 panic("unexpected value " + v.value + " for float ") 528 } 529 return f 530 } 531 532 // stringValue returns a canonical representation for a variable. 533 func (v *variable) stringValue() string { 534 switch v.kind { 535 case boolKind: 536 return fmt.Sprint(v.boolValue()) 537 case intKind: 538 return fmt.Sprint(v.intValue()) 539 case floatKind: 540 return fmt.Sprint(v.floatValue()) 541 } 542 return v.value 543 } 544 545 func stringToBool(s string) (bool, error) { 546 switch strings.ToLower(s) { 547 case "true", "t", "yes", "y", "1", "": 548 return true, nil 549 case "false", "f", "no", "n", "0": 550 return false, nil 551 default: 552 return false, fmt.Errorf(`illegal value "%s" for bool variable`, s) 553 } 554 } 555 556 // makeCopy returns a duplicate of a set of shell variables. 557 func (vars variables) makeCopy() variables { 558 varscopy := make(variables, len(vars)) 559 for n, v := range vars { 560 vcopy := *v 561 varscopy[n] = &vcopy 562 } 563 return varscopy 564 } 565