소스 검색

Clean-up handling of output flag (#86)

For certain formats (eog, evince, gv, web, weblist), the visualizer
would be invoked regardless of whether the user specified a specific
output file to write to. In order to ensure that output is properly
written, the invokeVisualizer function has a check for whether the
output is os.Stdout and alters behavior based on that. This is the
wrong abstraction layer to do that work.

At the core of the problem is that PostProcess is used to both
do data post-processing that is inherent to the format, and also
to invoke some visualization for the data (to Stdout or browser).
We split these two steps apart and make it obvious which is which
by adding a visualizer field to Command.

This seperation of concerns allows us to simplify the code.
Joe Tsai 8 년 전
부모
커밋
daa80a9496
4개의 변경된 파일80개의 추가작업 그리고 88개의 파일을 삭제
  1. 45
    60
      internal/driver/commands.go
  2. 33
    23
      internal/driver/driver.go
  3. 1
    1
      internal/driver/interactive_test.go
  4. 1
    4
      third_party/svg/svg.go

+ 45
- 60
internal/driver/commands.go 파일 보기

@@ -41,6 +41,7 @@ type commands map[string]*command
41 41
 type command struct {
42 42
 	format      int           // report format to generate
43 43
 	postProcess PostProcessor // postprocessing to run on report
44
+	visualizer  PostProcessor // display output using some callback
44 45
 	hasParam    bool          // collect a parameter from the CLI
45 46
 	description string        // single-line description text saying what the command does
46 47
 	usage       string        // multi-line help text saying how the command is used
@@ -64,7 +65,7 @@ func (c *command) help(name string) string {
64 65
 // specialized visualization formats. If the command specified already
65 66
 // exists, it is overwritten.
66 67
 func AddCommand(cmd string, format int, post PostProcessor, desc, usage string) {
67
-	pprofCommands[cmd] = &command{format, post, false, desc, usage}
68
+	pprofCommands[cmd] = &command{format, post, nil, false, desc, usage}
68 69
 }
69 70
 
70 71
 // SetVariableDefault sets the default value for a pprof
@@ -76,7 +77,7 @@ func SetVariableDefault(variable, value string) {
76 77
 }
77 78
 
78 79
 // PostProcessor is a function that applies post-processing to the report output
79
-type PostProcessor func(input []byte, output io.Writer, ui plugin.UI) error
80
+type PostProcessor func(input io.Reader, output io.Writer, ui plugin.UI) error
80 81
 
81 82
 // interactiveMode is true if pprof is running on interactive mode, reading
82 83
 // commands from its shell.
@@ -85,43 +86,43 @@ var interactiveMode = false
85 86
 // pprofCommands are the report generation commands recognized by pprof.
86 87
 var pprofCommands = commands{
87 88
 	// Commands that require no post-processing.
88
-	"comments": {report.Comments, nil, false, "Output all profile comments", ""},
89
-	"disasm":   {report.Dis, nil, true, "Output assembly listings annotated with samples", listHelp("disasm", true)},
90
-	"dot":      {report.Dot, nil, false, "Outputs a graph in DOT format", reportHelp("dot", false, true)},
91
-	"list":     {report.List, nil, true, "Output annotated source for functions matching regexp", listHelp("list", false)},
92
-	"peek":     {report.Tree, nil, true, "Output callers/callees of functions matching regexp", "peek func_regex\nDisplay callers and callees of functions matching func_regex."},
93
-	"raw":      {report.Raw, nil, false, "Outputs a text representation of the raw profile", ""},
94
-	"tags":     {report.Tags, 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."},
95
-	"text":     {report.Text, nil, false, "Outputs top entries in text form", reportHelp("text", true, true)},
96
-	"top":      {report.Text, nil, false, "Outputs top entries in text form", reportHelp("top", true, true)},
97
-	"topproto": {report.TopProto, awayFromTTY("pb.gz"), false, "Outputs top entries in compressed protobuf format", ""},
98
-	"traces":   {report.Traces, nil, false, "Outputs all profile samples in text form", ""},
99
-	"tree":     {report.Tree, nil, false, "Outputs a text rendering of call graph", reportHelp("tree", true, true)},
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 100
 
101 101
 	// Save binary formats to a file
102
-	"callgrind": {report.Callgrind, awayFromTTY("callgraph.out"), false, "Outputs a graph in callgrind format", reportHelp("callgrind", false, true)},
103
-	"proto":     {report.Proto, awayFromTTY("pb.gz"), false, "Outputs the profile in compressed protobuf format", ""},
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", ""},
104 105
 
105 106
 	// Generate report in DOT format and postprocess with dot
106
-	"gif": {report.Dot, invokeDot("gif"), false, "Outputs a graph image in GIF format", reportHelp("gif", false, true)},
107
-	"pdf": {report.Dot, invokeDot("pdf"), false, "Outputs a graph in PDF format", reportHelp("pdf", false, true)},
108
-	"png": {report.Dot, invokeDot("png"), false, "Outputs a graph image in PNG format", reportHelp("png", false, true)},
109
-	"ps":  {report.Dot, invokeDot("ps"), false, "Outputs a graph in PS format", reportHelp("ps", false, true)},
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)},
110 111
 
111 112
 	// Save SVG output into a file
112
-	"svg": {report.Dot, saveSVGToFile(), false, "Outputs a graph in SVG format", reportHelp("svg", false, true)},
113
+	"svg": {report.Dot, massageDotSVG(), awayFromTTY("svg"), false, "Outputs a graph in SVG format", reportHelp("svg", false, true)},
113 114
 
114 115
 	// Visualize postprocessed dot output
115
-	"eog":    {report.Dot, invokeVisualizer(invokeDot("svg"), "svg", []string{"eog"}), false, "Visualize graph through eog", reportHelp("eog", false, false)},
116
-	"evince": {report.Dot, invokeVisualizer(invokeDot("pdf"), "pdf", []string{"evince"}), false, "Visualize graph through evince", reportHelp("evince", false, false)},
117
-	"gv":     {report.Dot, invokeVisualizer(invokeDot("ps"), "ps", []string{"gv --noantialias"}), false, "Visualize graph through gv", reportHelp("gv", false, false)},
118
-	"web":    {report.Dot, invokeVisualizer(saveSVGToFile(), "svg", browsers()), false, "Visualize graph through web browser", reportHelp("web", false, false)},
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)},
119 120
 
120 121
 	// Visualize callgrind output
121
-	"kcachegrind": {report.Callgrind, invokeVisualizer(nil, "grind", kcachegrind), false, "Visualize report in KCachegrind", reportHelp("kcachegrind", false, false)},
122
+	"kcachegrind": {report.Callgrind, nil, invokeVisualizer("grind", kcachegrind), false, "Visualize report in KCachegrind", reportHelp("kcachegrind", false, false)},
122 123
 
123 124
 	// Visualize HTML directly generated by report.
124
-	"weblist": {report.WebList, invokeVisualizer(awayFromTTY("html"), "html", browsers()), true, "Display annotated source in a web browser", listHelp("weblist", false)},
125
+	"weblist": {report.WebList, nil, invokeVisualizer("html", browsers()), true, "Display annotated source in a web browser", listHelp("weblist", false)},
125 126
 }
126 127
 
127 128
 // pprofVariables are the configuration parameters that affect the
@@ -360,70 +361,54 @@ var kcachegrind = []string{"kcachegrind"}
360 361
 // the terminal screen. This is used to avoid dumping binary data on
361 362
 // the screen.
362 363
 func awayFromTTY(format string) PostProcessor {
363
-	return func(input []byte, output io.Writer, ui plugin.UI) error {
364
+	return func(input io.Reader, output io.Writer, ui plugin.UI) error {
364 365
 		if output == os.Stdout && (ui.IsTerminal() || interactiveMode) {
365 366
 			tempFile, err := newTempFile("", "profile", "."+format)
366 367
 			if err != nil {
367 368
 				return err
368 369
 			}
369 370
 			ui.PrintErr("Generating report in ", tempFile.Name())
370
-			_, err = fmt.Fprint(tempFile, string(input))
371
-			return err
371
+			output = tempFile
372 372
 		}
373
-		_, err := fmt.Fprint(output, string(input))
373
+		_, err := io.Copy(output, input)
374 374
 		return err
375 375
 	}
376 376
 }
377 377
 
378 378
 func invokeDot(format string) PostProcessor {
379
-	divert := awayFromTTY(format)
380
-	return func(input []byte, output io.Writer, ui plugin.UI) error {
379
+	return func(input io.Reader, output io.Writer, ui plugin.UI) error {
381 380
 		cmd := exec.Command("dot", "-T"+format)
382
-		var buf bytes.Buffer
383
-		cmd.Stdin, cmd.Stdout, cmd.Stderr = bytes.NewBuffer(input), &buf, os.Stderr
381
+		cmd.Stdin, cmd.Stdout, cmd.Stderr = input, output, os.Stderr
384 382
 		if err := cmd.Run(); err != nil {
385 383
 			return fmt.Errorf("Failed to execute dot. Is Graphviz installed? Error: %v", err)
386 384
 		}
387
-		return divert(buf.Bytes(), output, ui)
385
+		return nil
388 386
 	}
389 387
 }
390 388
 
391
-func saveSVGToFile() PostProcessor {
389
+// massageDotSVG invokes the dot tool to generate an SVG image and alters
390
+// the image to have panning capabilities when viewed in a browser.
391
+func massageDotSVG() PostProcessor {
392 392
 	generateSVG := invokeDot("svg")
393
-	divert := awayFromTTY("svg")
394
-	return func(input []byte, output io.Writer, ui plugin.UI) error {
395
-		baseSVG := &bytes.Buffer{}
393
+	return func(input io.Reader, output io.Writer, ui plugin.UI) error {
394
+		baseSVG := new(bytes.Buffer)
396 395
 		if err := generateSVG(input, baseSVG, ui); err != nil {
397 396
 			return err
398 397
 		}
399
-
400
-		return divert([]byte(svg.Massage(*baseSVG)), output, ui)
398
+		_, err := output.Write([]byte(svg.Massage(baseSVG.String())))
399
+		return err
401 400
 	}
402 401
 }
403 402
 
404
-func invokeVisualizer(format PostProcessor, suffix string, visualizers []string) PostProcessor {
405
-	return func(input []byte, output io.Writer, ui plugin.UI) error {
406
-		if output != os.Stdout {
407
-			if format != nil {
408
-				return format(input, output, ui)
409
-			}
410
-			_, err := fmt.Fprint(output, string(input))
411
-			return err
412
-		}
413
-
403
+func invokeVisualizer(suffix string, visualizers []string) PostProcessor {
404
+	return func(input io.Reader, output io.Writer, ui plugin.UI) error {
414 405
 		tempFile, err := newTempFile(os.TempDir(), "pprof", "."+suffix)
415 406
 		if err != nil {
416 407
 			return err
417 408
 		}
418 409
 		deferDeleteTempFile(tempFile.Name())
419
-		if format != nil {
420
-			if err := format(input, tempFile, ui); err != nil {
421
-				return err
422
-			}
423
-		} else {
424
-			if _, err := fmt.Fprint(tempFile, string(input)); err != nil {
425
-				return err
426
-			}
410
+		if _, err := io.Copy(tempFile, input); err != nil {
411
+			return err
427 412
 		}
428 413
 		tempFile.Close()
429 414
 		// Try visualizers until one is successful

+ 33
- 23
internal/driver/driver.go 파일 보기

@@ -20,7 +20,6 @@ package driver
20 20
 import (
21 21
 	"bytes"
22 22
 	"fmt"
23
-	"io"
24 23
 	"os"
25 24
 	"path/filepath"
26 25
 	"regexp"
@@ -59,20 +58,6 @@ func PProf(eo *plugin.Options) error {
59 58
 func generateReport(p *profile.Profile, cmd []string, vars variables, o *plugin.Options) error {
60 59
 	p = p.Copy() // Prevent modification to the incoming profile.
61 60
 
62
-	var w io.Writer
63
-	switch output := vars["output"].value; output {
64
-	case "":
65
-		w = os.Stdout
66
-	default:
67
-		o.UI.PrintErr("Generating report in ", output)
68
-		outputFile, err := o.Writer.Open(output)
69
-		if err != nil {
70
-			return err
71
-		}
72
-		defer outputFile.Close()
73
-		w = outputFile
74
-	}
75
-
76 61
 	vars = applyCommandOverrides(cmd, vars)
77 62
 
78 63
 	// Delay focus after configuring report to get percentages on all samples.
@@ -91,7 +76,6 @@ func generateReport(p *profile.Profile, cmd []string, vars variables, o *plugin.
91 76
 		panic("unexpected nil command")
92 77
 	}
93 78
 	ropt.OutputFormat = c.format
94
-	post := c.postProcess
95 79
 	if len(cmd) == 2 {
96 80
 		s, err := regexp.Compile(cmd[1])
97 81
 		if err != nil {
@@ -106,21 +90,47 @@ func generateReport(p *profile.Profile, cmd []string, vars variables, o *plugin.
106 90
 			return err
107 91
 		}
108 92
 	}
109
-
110 93
 	if err := aggregate(p, vars); err != nil {
111 94
 		return err
112 95
 	}
113 96
 
114
-	if post == nil {
115
-		return report.Generate(w, rpt, o.Obj)
97
+	// Generate the report.
98
+	dst := new(bytes.Buffer)
99
+	if err := report.Generate(dst, rpt, o.Obj); err != nil {
100
+		return err
116 101
 	}
102
+	src := dst
117 103
 
118
-	// Capture output into buffer and send to postprocessing command.
119
-	buf := &bytes.Buffer{}
120
-	if err := report.Generate(buf, rpt, o.Obj); err != nil {
104
+	// If necessary, perform any data post-processing.
105
+	if c.postProcess != nil {
106
+		dst = new(bytes.Buffer)
107
+		if err := c.postProcess(src, dst, o.UI); err != nil {
108
+			return err
109
+		}
110
+		src = dst
111
+	}
112
+
113
+	// If no output is specified, use default visualizer.
114
+	output := vars["output"].value
115
+	if output == "" {
116
+		if c.visualizer != nil {
117
+			return c.visualizer(src, os.Stdout, o.UI)
118
+		}
119
+		_, err := src.WriteTo(os.Stdout)
120
+		return err
121
+	}
122
+
123
+	// Output to specified file.
124
+	o.UI.PrintErr("Generating report in ", output)
125
+	out, err := os.Create(output)
126
+	if err != nil {
127
+		return err
128
+	}
129
+	if _, err := src.WriteTo(out); err != nil {
130
+		out.Close()
121 131
 		return err
122 132
 	}
123
-	return post(buf.Bytes(), w, o.UI)
133
+	return out.Close()
124 134
 }
125 135
 
126 136
 func applyCommandOverrides(cmd []string, v variables) variables {

+ 1
- 1
internal/driver/interactive_test.go 파일 보기

@@ -80,7 +80,7 @@ func TestShell(t *testing.T) {
80 80
 }
81 81
 
82 82
 var testCommands = commands{
83
-	"check": &command{report.Raw, nil, true, "", ""},
83
+	"check": &command{report.Raw, nil, nil, true, "", ""},
84 84
 }
85 85
 
86 86
 func testVariables(base variables) variables {

+ 1
- 4
third_party/svg/svg.go 파일 보기

@@ -16,7 +16,6 @@
16 16
 package svg
17 17
 
18 18
 import (
19
-	"bytes"
20 19
 	"regexp"
21 20
 	"strings"
22 21
 )
@@ -30,9 +29,7 @@ var (
30 29
 // Massage enhances the SVG output from DOT to provide better
31 30
 // panning inside a web browser. It uses the SVGPan library, which is
32 31
 // embedded into the svgPanJS variable.
33
-func Massage(in bytes.Buffer) string {
34
-	svg := string(in.Bytes())
35
-
32
+func Massage(svg string) string {
36 33
 	// Work around for dot bug which misses quoting some ampersands,
37 34
 	// resulting on unparsable SVG.
38 35
 	svg = strings.Replace(svg, "&;", "&;", -1)