Home | History | Annotate | Download | only in driver
      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
      6 
      7 import (
      8 	"fmt"
      9 	"io"
     10 	"regexp"
     11 	"sort"
     12 	"strconv"
     13 	"strings"
     14 
     15 	"cmd/pprof/internal/commands"
     16 	"cmd/pprof/internal/plugin"
     17 	"internal/pprof/profile"
     18 )
     19 
     20 var profileFunctionNames = []string{}
     21 
     22 // functionCompleter replaces provided substring with a function
     23 // name retrieved from a profile if a single match exists. Otherwise,
     24 // it returns unchanged substring. It defaults to no-op if the profile
     25 // is not specified.
     26 func functionCompleter(substring string) string {
     27 	found := ""
     28 	for _, fName := range profileFunctionNames {
     29 		if strings.Contains(fName, substring) {
     30 			if found != "" {
     31 				return substring
     32 			}
     33 			found = fName
     34 		}
     35 	}
     36 	if found != "" {
     37 		return found
     38 	}
     39 	return substring
     40 }
     41 
     42 // updateAutoComplete enhances autocompletion with information that can be
     43 // retrieved from the profile
     44 func updateAutoComplete(p *profile.Profile) {
     45 	profileFunctionNames = nil // remove function names retrieved previously
     46 	for _, fn := range p.Function {
     47 		profileFunctionNames = append(profileFunctionNames, fn.Name)
     48 	}
     49 }
     50 
     51 // splitCommand splits the command line input into tokens separated by
     52 // spaces. Takes care to separate commands of the form 'top10' into
     53 // two tokens: 'top' and '10'
     54 func splitCommand(input string) []string {
     55 	fields := strings.Fields(input)
     56 	if num := strings.IndexAny(fields[0], "0123456789"); num != -1 {
     57 		inputNumber := fields[0][num:]
     58 		fields[0] = fields[0][:num]
     59 		fields = append([]string{fields[0], inputNumber}, fields[1:]...)
     60 	}
     61 	return fields
     62 }
     63 
     64 // interactive displays a prompt and reads commands for profile
     65 // manipulation/visualization.
     66 func interactive(p *profile.Profile, obj plugin.ObjTool, ui plugin.UI, f *flags) error {
     67 	updateAutoComplete(p)
     68 
     69 	// Enter command processing loop.
     70 	ui.Print("Entering interactive mode (type \"help\" for commands)")
     71 	ui.SetAutoComplete(commands.NewCompleter(f.commands))
     72 
     73 	for {
     74 		input, err := readCommand(p, ui, f)
     75 		if err != nil {
     76 			if err != io.EOF {
     77 				return err
     78 			}
     79 			if input == "" {
     80 				return nil
     81 			}
     82 		}
     83 		// Process simple commands.
     84 		switch input {
     85 		case "":
     86 			continue
     87 		case ":":
     88 			f.flagFocus = newString("")
     89 			f.flagIgnore = newString("")
     90 			f.flagTagFocus = newString("")
     91 			f.flagTagIgnore = newString("")
     92 			f.flagHide = newString("")
     93 			continue
     94 		}
     95 
     96 		fields := splitCommand(input)
     97 		// Process report generation commands.
     98 		if _, ok := f.commands[fields[0]]; ok {
     99 			if err := generateReport(p, fields, obj, ui, f); err != nil {
    100 				if err == io.EOF {
    101 					return nil
    102 				}
    103 				ui.PrintErr(err)
    104 			}
    105 			continue
    106 		}
    107 
    108 		switch cmd := fields[0]; cmd {
    109 		case "help":
    110 			commandHelp(fields, ui, f)
    111 			continue
    112 		case "exit", "quit":
    113 			return nil
    114 		}
    115 
    116 		// Process option settings.
    117 		if of, err := optFlags(p, input, f); err == nil {
    118 			f = of
    119 		} else {
    120 			ui.PrintErr("Error: ", err.Error())
    121 		}
    122 	}
    123 }
    124 
    125 func generateReport(p *profile.Profile, cmd []string, obj plugin.ObjTool, ui plugin.UI, f *flags) error {
    126 	prof := p.Copy()
    127 
    128 	cf, err := cmdFlags(prof, cmd, ui, f)
    129 	if err != nil {
    130 		return err
    131 	}
    132 
    133 	return generate(true, prof, obj, ui, cf)
    134 }
    135 
    136 // validateRegex checks if a string is a valid regular expression.
    137 func validateRegex(v string) error {
    138 	_, err := regexp.Compile(v)
    139 	return err
    140 }
    141 
    142 // readCommand prompts for and reads the next command.
    143 func readCommand(p *profile.Profile, ui plugin.UI, f *flags) (string, error) {
    144 	//ui.Print("Options:\n", f.String(p))
    145 	s, err := ui.ReadLine()
    146 	return strings.TrimSpace(s), err
    147 }
    148 
    149 func commandHelp(_ []string, ui plugin.UI, f *flags) error {
    150 	help := `
    151  Commands:
    152    cmd [n] [--cum] [focus_regex]* [-ignore_regex]*
    153        Produce a text report with the top n entries.
    154        Include samples matching focus_regex, and exclude ignore_regex.
    155        Add --cum to sort using cumulative data.
    156        Available commands:
    157 `
    158 	var commands []string
    159 	for name, cmd := range f.commands {
    160 		commands = append(commands, fmt.Sprintf("         %-12s %s", name, cmd.Usage))
    161 	}
    162 	sort.Strings(commands)
    163 
    164 	help = help + strings.Join(commands, "\n") + `
    165    peek func_regex
    166        Display callers and callees of functions matching func_regex.
    167 
    168    dot [n] [focus_regex]* [-ignore_regex]* [>file]
    169        Produce an annotated callgraph with the top n entries.
    170        Include samples matching focus_regex, and exclude ignore_regex.
    171        For other outputs, replace dot with:
    172        - Graphic formats: dot, svg, pdf, ps, gif, png (use > to name output file)
    173        - Graph viewer:    gv, web, evince, eog
    174 
    175    callgrind [n] [focus_regex]* [-ignore_regex]* [>file]
    176        Produce a file in callgrind-compatible format.
    177        Include samples matching focus_regex, and exclude ignore_regex.
    178 
    179    weblist func_regex [-ignore_regex]*
    180        Show annotated source with interspersed assembly in a web browser.
    181 
    182    list func_regex [-ignore_regex]*
    183        Print source for routines matching func_regex, and exclude ignore_regex.
    184 
    185    disasm func_regex [-ignore_regex]*
    186        Disassemble routines matching func_regex, and exclude ignore_regex.
    187 
    188    tags tag_regex [-ignore_regex]*
    189        List tags with key:value matching tag_regex and exclude ignore_regex.
    190 
    191    quit/exit/^D
    192  	     Exit pprof.
    193 
    194    option=value
    195        The following options can be set individually:
    196            cum/flat:           Sort entries based on cumulative or flat data
    197            call_tree:          Build context-sensitive call trees
    198            nodecount:          Max number of entries to display
    199            nodefraction:       Min frequency ratio of nodes to display
    200            edgefraction:       Min frequency ratio of edges to display
    201            focus/ignore:       Regexp to include/exclude samples by name/file
    202            tagfocus/tagignore: Regexp or value range to filter samples by tag
    203                                eg "1mb", "1mb:2mb", ":64kb"
    204 
    205            functions:          Level of aggregation for sample data
    206            files:
    207            lines:
    208            addresses:
    209 
    210            unit:               Measurement unit to use on reports
    211 
    212            Sample value selection by index:
    213             sample_index:      Index of sample value to display
    214             mean:              Average sample value over first value
    215 
    216            Sample value selection by name:
    217             alloc_space        for heap profiles
    218             alloc_objects
    219             inuse_space
    220             inuse_objects
    221 
    222             total_delay        for contention profiles
    223             mean_delay
    224             contentions
    225 
    226    :   Clear focus/ignore/hide/tagfocus/tagignore`
    227 
    228 	ui.Print(help)
    229 	return nil
    230 }
    231 
    232 // cmdFlags parses the options of an interactive command and returns
    233 // an updated flags object.
    234 func cmdFlags(prof *profile.Profile, input []string, ui plugin.UI, f *flags) (*flags, error) {
    235 	cf := *f
    236 
    237 	var focus, ignore string
    238 	output := *cf.flagOutput
    239 	nodeCount := *cf.flagNodeCount
    240 	cmd := input[0]
    241 
    242 	// Update output flags based on parameters.
    243 	tokens := input[1:]
    244 	for p := 0; p < len(tokens); p++ {
    245 		t := tokens[p]
    246 		if t == "" {
    247 			continue
    248 		}
    249 		if c, err := strconv.ParseInt(t, 10, 32); err == nil {
    250 			nodeCount = int(c)
    251 			continue
    252 		}
    253 		switch t[0] {
    254 		case '>':
    255 			if len(t) > 1 {
    256 				output = t[1:]
    257 				continue
    258 			}
    259 			// find next token
    260 			for p++; p < len(tokens); p++ {
    261 				if tokens[p] != "" {
    262 					output = tokens[p]
    263 					break
    264 				}
    265 			}
    266 		case '-':
    267 			if t == "--cum" || t == "-cum" {
    268 				cf.flagCum = newBool(true)
    269 				continue
    270 			}
    271 			ignore = catRegex(ignore, t[1:])
    272 		default:
    273 			focus = catRegex(focus, t)
    274 		}
    275 	}
    276 
    277 	pcmd, ok := f.commands[cmd]
    278 	if !ok {
    279 		return nil, fmt.Errorf("Unexpected parse failure: %v", input)
    280 	}
    281 	// Reset flags
    282 	cf.flagCommands = make(map[string]*bool)
    283 	cf.flagParamCommands = make(map[string]*string)
    284 
    285 	if !pcmd.HasParam {
    286 		cf.flagCommands[cmd] = newBool(true)
    287 
    288 		switch cmd {
    289 		case "tags":
    290 			cf.flagTagFocus = newString(focus)
    291 			cf.flagTagIgnore = newString(ignore)
    292 		default:
    293 			cf.flagFocus = newString(catRegex(*cf.flagFocus, focus))
    294 			cf.flagIgnore = newString(catRegex(*cf.flagIgnore, ignore))
    295 		}
    296 	} else {
    297 		if focus == "" {
    298 			focus = "."
    299 		}
    300 		cf.flagParamCommands[cmd] = newString(focus)
    301 		cf.flagIgnore = newString(catRegex(*cf.flagIgnore, ignore))
    302 	}
    303 
    304 	if nodeCount < 0 {
    305 		switch cmd {
    306 		case "text", "top":
    307 			// Default text/top to 10 nodes on interactive mode
    308 			nodeCount = 10
    309 		default:
    310 			nodeCount = 80
    311 		}
    312 	}
    313 
    314 	cf.flagNodeCount = newInt(nodeCount)
    315 	cf.flagOutput = newString(output)
    316 
    317 	// Do regular flags processing
    318 	if err := processFlags(prof, ui, &cf); err != nil {
    319 		cf.usage(ui)
    320 		return nil, err
    321 	}
    322 
    323 	return &cf, nil
    324 }
    325 
    326 func catRegex(a, b string) string {
    327 	if a == "" {
    328 		return b
    329 	}
    330 	if b == "" {
    331 		return a
    332 	}
    333 	return a + "|" + b
    334 }
    335 
    336 // optFlags parses an interactive option setting and returns
    337 // an updated flags object.
    338 func optFlags(p *profile.Profile, input string, f *flags) (*flags, error) {
    339 	inputs := strings.SplitN(input, "=", 2)
    340 	option := strings.ToLower(strings.TrimSpace(inputs[0]))
    341 	var value string
    342 	if len(inputs) == 2 {
    343 		value = strings.TrimSpace(inputs[1])
    344 	}
    345 
    346 	of := *f
    347 
    348 	var err error
    349 	var bv bool
    350 	var uv uint64
    351 	var fv float64
    352 
    353 	switch option {
    354 	case "cum":
    355 		if bv, err = parseBool(value); err != nil {
    356 			return nil, err
    357 		}
    358 		of.flagCum = newBool(bv)
    359 	case "flat":
    360 		if bv, err = parseBool(value); err != nil {
    361 			return nil, err
    362 		}
    363 		of.flagCum = newBool(!bv)
    364 	case "call_tree":
    365 		if bv, err = parseBool(value); err != nil {
    366 			return nil, err
    367 		}
    368 		of.flagCallTree = newBool(bv)
    369 	case "unit":
    370 		of.flagDisplayUnit = newString(value)
    371 	case "sample_index":
    372 		if uv, err = strconv.ParseUint(value, 10, 32); err != nil {
    373 			return nil, err
    374 		}
    375 		if ix := int(uv); ix < 0 || ix >= len(p.SampleType) {
    376 			return nil, fmt.Errorf("sample_index out of range [0..%d]", len(p.SampleType)-1)
    377 		}
    378 		of.flagSampleIndex = newInt(int(uv))
    379 	case "mean":
    380 		if bv, err = parseBool(value); err != nil {
    381 			return nil, err
    382 		}
    383 		of.flagMean = newBool(bv)
    384 	case "nodecount":
    385 		if uv, err = strconv.ParseUint(value, 10, 32); err != nil {
    386 			return nil, err
    387 		}
    388 		of.flagNodeCount = newInt(int(uv))
    389 	case "nodefraction":
    390 		if fv, err = strconv.ParseFloat(value, 64); err != nil {
    391 			return nil, err
    392 		}
    393 		of.flagNodeFraction = newFloat64(fv)
    394 	case "edgefraction":
    395 		if fv, err = strconv.ParseFloat(value, 64); err != nil {
    396 			return nil, err
    397 		}
    398 		of.flagEdgeFraction = newFloat64(fv)
    399 	case "focus":
    400 		if err = validateRegex(value); err != nil {
    401 			return nil, err
    402 		}
    403 		of.flagFocus = newString(value)
    404 	case "ignore":
    405 		if err = validateRegex(value); err != nil {
    406 			return nil, err
    407 		}
    408 		of.flagIgnore = newString(value)
    409 	case "tagfocus":
    410 		if err = validateRegex(value); err != nil {
    411 			return nil, err
    412 		}
    413 		of.flagTagFocus = newString(value)
    414 	case "tagignore":
    415 		if err = validateRegex(value); err != nil {
    416 			return nil, err
    417 		}
    418 		of.flagTagIgnore = newString(value)
    419 	case "hide":
    420 		if err = validateRegex(value); err != nil {
    421 			return nil, err
    422 		}
    423 		of.flagHide = newString(value)
    424 	case "addresses", "files", "lines", "functions":
    425 		if bv, err = parseBool(value); err != nil {
    426 			return nil, err
    427 		}
    428 		if !bv {
    429 			return nil, fmt.Errorf("select one of addresses/files/lines/functions")
    430 		}
    431 		setGranularityToggle(option, &of)
    432 	default:
    433 		if ix := findSampleIndex(p, "", option); ix >= 0 {
    434 			of.flagSampleIndex = newInt(ix)
    435 		} else if ix := findSampleIndex(p, "total_", option); ix >= 0 {
    436 			of.flagSampleIndex = newInt(ix)
    437 			of.flagMean = newBool(false)
    438 		} else if ix := findSampleIndex(p, "mean_", option); ix >= 1 {
    439 			of.flagSampleIndex = newInt(ix)
    440 			of.flagMean = newBool(true)
    441 		} else {
    442 			return nil, fmt.Errorf("unrecognized command: %s", input)
    443 		}
    444 	}
    445 	return &of, nil
    446 }
    447 
    448 // parseBool parses a string as a boolean value.
    449 func parseBool(v string) (bool, error) {
    450 	switch strings.ToLower(v) {
    451 	case "true", "t", "yes", "y", "1", "":
    452 		return true, nil
    453 	case "false", "f", "no", "n", "0":
    454 		return false, nil
    455 	}
    456 	return false, fmt.Errorf(`illegal input "%s" for bool value`, v)
    457 }
    458 
    459 func findSampleIndex(p *profile.Profile, prefix, sampleType string) int {
    460 	if !strings.HasPrefix(sampleType, prefix) {
    461 		return -1
    462 	}
    463 	sampleType = strings.TrimPrefix(sampleType, prefix)
    464 	for i, r := range p.SampleType {
    465 		if r.Type == sampleType {
    466 			return i
    467 		}
    468 	}
    469 	return -1
    470 }
    471 
    472 // setGranularityToggle manages the set of granularity options. These
    473 // operate as a toggle; turning one on turns the others off.
    474 func setGranularityToggle(o string, fl *flags) {
    475 	t, f := newBool(true), newBool(false)
    476 	fl.flagFunctions = f
    477 	fl.flagFiles = f
    478 	fl.flagLines = f
    479 	fl.flagAddresses = f
    480 	switch o {
    481 	case "functions":
    482 		fl.flagFunctions = t
    483 	case "files":
    484 		fl.flagFiles = t
    485 	case "lines":
    486 		fl.flagLines = t
    487 	case "addresses":
    488 		fl.flagAddresses = t
    489 	default:
    490 		panic(fmt.Errorf("unexpected option %s", o))
    491 	}
    492 }
    493