You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

460 lines
12 KiB

  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. package driver
  15. import (
  16. "fmt"
  17. "io"
  18. "regexp"
  19. "sort"
  20. "strconv"
  21. "strings"
  22. "github.com/google/pprof/internal/plugin"
  23. "github.com/google/pprof/internal/report"
  24. "github.com/google/pprof/profile"
  25. )
  26. var commentStart = "//:" // Sentinel for comments on options
  27. var tailDigitsRE = regexp.MustCompile("[0-9]+$")
  28. // interactive starts a shell to read pprof commands.
  29. func interactive(p *profile.Profile, o *plugin.Options) error {
  30. // Enter command processing loop.
  31. o.UI.SetAutoComplete(newCompleter(functionNames(p)))
  32. pprofVariables.set("compact_labels", "true")
  33. pprofVariables["sample_index"].help += fmt.Sprintf("Or use sample_index=name, with name in %v.\n", sampleTypes(p))
  34. // Do not wait for the visualizer to complete, to allow multiple
  35. // graphs to be visualized simultaneously.
  36. interactiveMode = true
  37. shortcuts := profileShortcuts(p)
  38. // Get all groups in pprofVariables to allow for clearer error messages.
  39. groups := groupOptions(pprofVariables)
  40. greetings(p, o.UI)
  41. for {
  42. input, err := o.UI.ReadLine("(pprof) ")
  43. if err != nil {
  44. if err != io.EOF {
  45. return err
  46. }
  47. if input == "" {
  48. return nil
  49. }
  50. }
  51. for _, input := range shortcuts.expand(input) {
  52. // Process assignments of the form variable=value
  53. if s := strings.SplitN(input, "=", 2); len(s) > 0 {
  54. name := strings.TrimSpace(s[0])
  55. var value string
  56. if len(s) == 2 {
  57. value = s[1]
  58. if comment := strings.LastIndex(value, commentStart); comment != -1 {
  59. value = value[:comment]
  60. }
  61. value = strings.TrimSpace(value)
  62. }
  63. if v := pprofVariables[name]; v != nil {
  64. if name == "sample_index" {
  65. // Error check sample_index=xxx to ensure xxx is a valid sample type.
  66. index, err := p.SampleIndexByName(value)
  67. if err != nil {
  68. o.UI.PrintErr(err)
  69. continue
  70. }
  71. value = p.SampleType[index].Type
  72. }
  73. if err := pprofVariables.set(name, value); err != nil {
  74. o.UI.PrintErr(err)
  75. }
  76. continue
  77. }
  78. // Allow group=variable syntax by converting into variable="".
  79. if v := pprofVariables[value]; v != nil && v.group == name {
  80. if err := pprofVariables.set(value, ""); err != nil {
  81. o.UI.PrintErr(err)
  82. }
  83. continue
  84. } else if okValues := groups[name]; okValues != nil {
  85. o.UI.PrintErr(fmt.Errorf("unrecognized value for %s: %q. Use one of %s", name, value, strings.Join(okValues, ", ")))
  86. continue
  87. }
  88. }
  89. tokens := strings.Fields(input)
  90. if len(tokens) == 0 {
  91. continue
  92. }
  93. switch tokens[0] {
  94. case "o", "options":
  95. printCurrentOptions(p, o.UI)
  96. continue
  97. case "exit", "quit":
  98. return nil
  99. case "help":
  100. commandHelp(strings.Join(tokens[1:], " "), o.UI)
  101. continue
  102. }
  103. args, vars, err := parseCommandLine(tokens)
  104. if err == nil {
  105. err = generateReportWrapper(p, args, vars, o)
  106. }
  107. if err != nil {
  108. o.UI.PrintErr(err)
  109. }
  110. }
  111. }
  112. }
  113. // groupOptions returns a map containing all non-empty groups
  114. // mapped to an array of the option names in that group in
  115. // sorted order.
  116. func groupOptions(vars variables) map[string][]string {
  117. groups := make(map[string][]string)
  118. for name, option := range vars {
  119. group := option.group
  120. if group != "" {
  121. groups[group] = append(groups[group], name)
  122. }
  123. }
  124. for _, names := range groups {
  125. sort.Strings(names)
  126. }
  127. return groups
  128. }
  129. var generateReportWrapper = generateReport // For testing purposes.
  130. // greetings prints a brief welcome and some overall profile
  131. // information before accepting interactive commands.
  132. func greetings(p *profile.Profile, ui plugin.UI) {
  133. numLabelUnits := identifyNumLabelUnits(p, ui)
  134. ropt, err := reportOptions(p, numLabelUnits, pprofVariables)
  135. if err == nil {
  136. rpt := report.New(p, ropt)
  137. ui.Print(strings.Join(report.ProfileLabels(rpt), "\n"))
  138. if rpt.Total() == 0 && len(p.SampleType) > 1 {
  139. ui.Print(`No samples were found with the default sample value type.`)
  140. ui.Print(`Try "sample_index" command to analyze different sample values.`, "\n")
  141. }
  142. }
  143. ui.Print(`Entering interactive mode (type "help" for commands, "o" for options)`)
  144. }
  145. // shortcuts represents composite commands that expand into a sequence
  146. // of other commands.
  147. type shortcuts map[string][]string
  148. func (a shortcuts) expand(input string) []string {
  149. input = strings.TrimSpace(input)
  150. if a != nil {
  151. if r, ok := a[input]; ok {
  152. return r
  153. }
  154. }
  155. return []string{input}
  156. }
  157. var pprofShortcuts = shortcuts{
  158. ":": []string{"focus=", "ignore=", "hide=", "tagfocus=", "tagignore="},
  159. }
  160. // profileShortcuts creates macros for convenience and backward compatibility.
  161. func profileShortcuts(p *profile.Profile) shortcuts {
  162. s := pprofShortcuts
  163. // Add shortcuts for sample types
  164. for _, st := range p.SampleType {
  165. command := fmt.Sprintf("sample_index=%s", st.Type)
  166. s[st.Type] = []string{command}
  167. s["total_"+st.Type] = []string{"mean=0", command}
  168. s["mean_"+st.Type] = []string{"mean=1", command}
  169. }
  170. return s
  171. }
  172. func sampleTypes(p *profile.Profile) []string {
  173. types := make([]string, len(p.SampleType))
  174. for i, t := range p.SampleType {
  175. types[i] = t.Type
  176. }
  177. return types
  178. }
  179. func printCurrentOptions(p *profile.Profile, ui plugin.UI) {
  180. var args []string
  181. type groupInfo struct {
  182. set string
  183. values []string
  184. }
  185. groups := make(map[string]*groupInfo)
  186. for n, o := range pprofVariables {
  187. v := o.stringValue()
  188. comment := ""
  189. if g := o.group; g != "" {
  190. gi, ok := groups[g]
  191. if !ok {
  192. gi = &groupInfo{}
  193. groups[g] = gi
  194. }
  195. if o.boolValue() {
  196. gi.set = n
  197. }
  198. gi.values = append(gi.values, n)
  199. continue
  200. }
  201. switch {
  202. case n == "sample_index":
  203. st := sampleTypes(p)
  204. if v == "" {
  205. // Apply default (last sample index).
  206. v = st[len(st)-1]
  207. }
  208. // Add comments for all sample types in profile.
  209. comment = "[" + strings.Join(st, " | ") + "]"
  210. case n == "source_path":
  211. continue
  212. case n == "nodecount" && v == "-1":
  213. comment = "default"
  214. case v == "":
  215. // Add quotes for empty values.
  216. v = `""`
  217. }
  218. if comment != "" {
  219. comment = commentStart + " " + comment
  220. }
  221. args = append(args, fmt.Sprintf(" %-25s = %-20s %s", n, v, comment))
  222. }
  223. for g, vars := range groups {
  224. sort.Strings(vars.values)
  225. comment := commentStart + " [" + strings.Join(vars.values, " | ") + "]"
  226. args = append(args, fmt.Sprintf(" %-25s = %-20s %s", g, vars.set, comment))
  227. }
  228. sort.Strings(args)
  229. ui.Print(strings.Join(args, "\n"))
  230. }
  231. // parseCommandLine parses a command and returns the pprof command to
  232. // execute and a set of variables for the report.
  233. func parseCommandLine(input []string) ([]string, variables, error) {
  234. cmd, args := input[:1], input[1:]
  235. name := cmd[0]
  236. c := pprofCommands[name]
  237. if c == nil {
  238. // Attempt splitting digits on abbreviated commands (eg top10)
  239. if d := tailDigitsRE.FindString(name); d != "" && d != name {
  240. name = name[:len(name)-len(d)]
  241. cmd[0], args = name, append([]string{d}, args...)
  242. c = pprofCommands[name]
  243. }
  244. }
  245. if c == nil {
  246. return nil, nil, fmt.Errorf("unrecognized command: %q", name)
  247. }
  248. if c.hasParam {
  249. if len(args) == 0 {
  250. return nil, nil, fmt.Errorf("command %s requires an argument", name)
  251. }
  252. cmd = append(cmd, args[0])
  253. args = args[1:]
  254. }
  255. // Copy the variables as options set in the command line are not persistent.
  256. vcopy := pprofVariables.makeCopy()
  257. var focus, ignore string
  258. for i := 0; i < len(args); i++ {
  259. t := args[i]
  260. if _, err := strconv.ParseInt(t, 10, 32); err == nil {
  261. vcopy.set("nodecount", t)
  262. continue
  263. }
  264. switch t[0] {
  265. case '>':
  266. outputFile := t[1:]
  267. if outputFile == "" {
  268. i++
  269. if i >= len(args) {
  270. return nil, nil, fmt.Errorf("unexpected end of line after >")
  271. }
  272. outputFile = args[i]
  273. }
  274. vcopy.set("output", outputFile)
  275. case '-':
  276. if t == "--cum" || t == "-cum" {
  277. vcopy.set("cum", "t")
  278. continue
  279. }
  280. ignore = catRegex(ignore, t[1:])
  281. default:
  282. focus = catRegex(focus, t)
  283. }
  284. }
  285. if name == "tags" {
  286. updateFocusIgnore(vcopy, "tag", focus, ignore)
  287. } else {
  288. updateFocusIgnore(vcopy, "", focus, ignore)
  289. }
  290. if vcopy["nodecount"].intValue() == -1 && (name == "text" || name == "top") {
  291. vcopy.set("nodecount", "10")
  292. }
  293. return cmd, vcopy, nil
  294. }
  295. func updateFocusIgnore(v variables, prefix, f, i string) {
  296. if f != "" {
  297. focus := prefix + "focus"
  298. v.set(focus, catRegex(v[focus].value, f))
  299. }
  300. if i != "" {
  301. ignore := prefix + "ignore"
  302. v.set(ignore, catRegex(v[ignore].value, i))
  303. }
  304. }
  305. func catRegex(a, b string) string {
  306. if a != "" && b != "" {
  307. return a + "|" + b
  308. }
  309. return a + b
  310. }
  311. // commandHelp displays help and usage information for all Commands
  312. // and Variables or a specific Command or Variable.
  313. func commandHelp(args string, ui plugin.UI) {
  314. if args == "" {
  315. help := usage(false)
  316. help = help + `
  317. : Clear focus/ignore/hide/tagfocus/tagignore
  318. type "help <cmd|option>" for more information
  319. `
  320. ui.Print(help)
  321. return
  322. }
  323. if c := pprofCommands[args]; c != nil {
  324. ui.Print(c.help(args))
  325. return
  326. }
  327. if v := pprofVariables[args]; v != nil {
  328. ui.Print(v.help + "\n")
  329. return
  330. }
  331. ui.PrintErr("Unknown command: " + args)
  332. }
  333. // newCompleter creates an autocompletion function for a set of commands.
  334. func newCompleter(fns []string) func(string) string {
  335. return func(line string) string {
  336. v := pprofVariables
  337. switch tokens := strings.Fields(line); len(tokens) {
  338. case 0:
  339. // Nothing to complete
  340. case 1:
  341. // Single token -- complete command name
  342. if match := matchVariableOrCommand(v, tokens[0]); match != "" {
  343. return match
  344. }
  345. case 2:
  346. if tokens[0] == "help" {
  347. if match := matchVariableOrCommand(v, tokens[1]); match != "" {
  348. return tokens[0] + " " + match
  349. }
  350. return line
  351. }
  352. fallthrough
  353. default:
  354. // Multiple tokens -- complete using functions, except for tags
  355. if cmd := pprofCommands[tokens[0]]; cmd != nil && tokens[0] != "tags" {
  356. lastTokenIdx := len(tokens) - 1
  357. lastToken := tokens[lastTokenIdx]
  358. if strings.HasPrefix(lastToken, "-") {
  359. lastToken = "-" + functionCompleter(lastToken[1:], fns)
  360. } else {
  361. lastToken = functionCompleter(lastToken, fns)
  362. }
  363. return strings.Join(append(tokens[:lastTokenIdx], lastToken), " ")
  364. }
  365. }
  366. return line
  367. }
  368. }
  369. // matchVariableOrCommand attempts to match a string token to the prefix of a Command.
  370. func matchVariableOrCommand(v variables, token string) string {
  371. token = strings.ToLower(token)
  372. found := ""
  373. for cmd := range pprofCommands {
  374. if strings.HasPrefix(cmd, token) {
  375. if found != "" {
  376. return ""
  377. }
  378. found = cmd
  379. }
  380. }
  381. for variable := range v {
  382. if strings.HasPrefix(variable, token) {
  383. if found != "" {
  384. return ""
  385. }
  386. found = variable
  387. }
  388. }
  389. return found
  390. }
  391. // functionCompleter replaces provided substring with a function
  392. // name retrieved from a profile if a single match exists. Otherwise,
  393. // it returns unchanged substring. It defaults to no-op if the profile
  394. // is not specified.
  395. func functionCompleter(substring string, fns []string) string {
  396. found := ""
  397. for _, fName := range fns {
  398. if strings.Contains(fName, substring) {
  399. if found != "" {
  400. return substring
  401. }
  402. found = fName
  403. }
  404. }
  405. if found != "" {
  406. return found
  407. }
  408. return substring
  409. }
  410. func functionNames(p *profile.Profile) []string {
  411. var fns []string
  412. for _, fn := range p.Function {
  413. fns = append(fns, fn.Name)
  414. }
  415. return fns
  416. }