Nav apraksta

interactive.go 11KB

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