Przeglądaj źródła

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 lat temu
rodzic
commit
daa80a9496

+ 45
- 60
internal/driver/commands.go Wyświetl plik

41
 type command struct {
41
 type command struct {
42
 	format      int           // report format to generate
42
 	format      int           // report format to generate
43
 	postProcess PostProcessor // postprocessing to run on report
43
 	postProcess PostProcessor // postprocessing to run on report
44
+	visualizer  PostProcessor // display output using some callback
44
 	hasParam    bool          // collect a parameter from the CLI
45
 	hasParam    bool          // collect a parameter from the CLI
45
 	description string        // single-line description text saying what the command does
46
 	description string        // single-line description text saying what the command does
46
 	usage       string        // multi-line help text saying how the command is used
47
 	usage       string        // multi-line help text saying how the command is used
64
 // specialized visualization formats. If the command specified already
65
 // specialized visualization formats. If the command specified already
65
 // exists, it is overwritten.
66
 // exists, it is overwritten.
66
 func AddCommand(cmd string, format int, post PostProcessor, desc, usage string) {
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
 // SetVariableDefault sets the default value for a pprof
71
 // SetVariableDefault sets the default value for a pprof
76
 }
77
 }
77
 
78
 
78
 // PostProcessor is a function that applies post-processing to the report output
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
 // interactiveMode is true if pprof is running on interactive mode, reading
82
 // interactiveMode is true if pprof is running on interactive mode, reading
82
 // commands from its shell.
83
 // commands from its shell.
85
 // pprofCommands are the report generation commands recognized by pprof.
86
 // pprofCommands are the report generation commands recognized by pprof.
86
 var pprofCommands = commands{
87
 var pprofCommands = commands{
87
 	// Commands that require no post-processing.
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
 	// Save binary formats to a file
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
 	// Generate report in DOT format and postprocess with dot
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
 	// Save SVG output into a file
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
 	// Visualize postprocessed dot output
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
 	// Visualize callgrind output
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
 	// Visualize HTML directly generated by report.
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
 // pprofVariables are the configuration parameters that affect the
128
 // pprofVariables are the configuration parameters that affect the
360
 // the terminal screen. This is used to avoid dumping binary data on
361
 // the terminal screen. This is used to avoid dumping binary data on
361
 // the screen.
362
 // the screen.
362
 func awayFromTTY(format string) PostProcessor {
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
 		if output == os.Stdout && (ui.IsTerminal() || interactiveMode) {
365
 		if output == os.Stdout && (ui.IsTerminal() || interactiveMode) {
365
 			tempFile, err := newTempFile("", "profile", "."+format)
366
 			tempFile, err := newTempFile("", "profile", "."+format)
366
 			if err != nil {
367
 			if err != nil {
367
 				return err
368
 				return err
368
 			}
369
 			}
369
 			ui.PrintErr("Generating report in ", tempFile.Name())
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
 		return err
374
 		return err
375
 	}
375
 	}
376
 }
376
 }
377
 
377
 
378
 func invokeDot(format string) PostProcessor {
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
 		cmd := exec.Command("dot", "-T"+format)
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
 		if err := cmd.Run(); err != nil {
382
 		if err := cmd.Run(); err != nil {
385
 			return fmt.Errorf("Failed to execute dot. Is Graphviz installed? Error: %v", err)
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
 	generateSVG := invokeDot("svg")
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
 		if err := generateSVG(input, baseSVG, ui); err != nil {
395
 		if err := generateSVG(input, baseSVG, ui); err != nil {
397
 			return err
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
 		tempFile, err := newTempFile(os.TempDir(), "pprof", "."+suffix)
405
 		tempFile, err := newTempFile(os.TempDir(), "pprof", "."+suffix)
415
 		if err != nil {
406
 		if err != nil {
416
 			return err
407
 			return err
417
 		}
408
 		}
418
 		deferDeleteTempFile(tempFile.Name())
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
 		tempFile.Close()
413
 		tempFile.Close()
429
 		// Try visualizers until one is successful
414
 		// Try visualizers until one is successful

+ 33
- 23
internal/driver/driver.go Wyświetl plik

20
 import (
20
 import (
21
 	"bytes"
21
 	"bytes"
22
 	"fmt"
22
 	"fmt"
23
-	"io"
24
 	"os"
23
 	"os"
25
 	"path/filepath"
24
 	"path/filepath"
26
 	"regexp"
25
 	"regexp"
59
 func generateReport(p *profile.Profile, cmd []string, vars variables, o *plugin.Options) error {
58
 func generateReport(p *profile.Profile, cmd []string, vars variables, o *plugin.Options) error {
60
 	p = p.Copy() // Prevent modification to the incoming profile.
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
 	vars = applyCommandOverrides(cmd, vars)
61
 	vars = applyCommandOverrides(cmd, vars)
77
 
62
 
78
 	// Delay focus after configuring report to get percentages on all samples.
63
 	// Delay focus after configuring report to get percentages on all samples.
91
 		panic("unexpected nil command")
76
 		panic("unexpected nil command")
92
 	}
77
 	}
93
 	ropt.OutputFormat = c.format
78
 	ropt.OutputFormat = c.format
94
-	post := c.postProcess
95
 	if len(cmd) == 2 {
79
 	if len(cmd) == 2 {
96
 		s, err := regexp.Compile(cmd[1])
80
 		s, err := regexp.Compile(cmd[1])
97
 		if err != nil {
81
 		if err != nil {
106
 			return err
90
 			return err
107
 		}
91
 		}
108
 	}
92
 	}
109
-
110
 	if err := aggregate(p, vars); err != nil {
93
 	if err := aggregate(p, vars); err != nil {
111
 		return err
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
 		return err
131
 		return err
122
 	}
132
 	}
123
-	return post(buf.Bytes(), w, o.UI)
133
+	return out.Close()
124
 }
134
 }
125
 
135
 
126
 func applyCommandOverrides(cmd []string, v variables) variables {
136
 func applyCommandOverrides(cmd []string, v variables) variables {

+ 1
- 1
internal/driver/interactive_test.go Wyświetl plik

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

+ 1
- 4
third_party/svg/svg.go Wyświetl plik

16
 package svg
16
 package svg
17
 
17
 
18
 import (
18
 import (
19
-	"bytes"
20
 	"regexp"
19
 	"regexp"
21
 	"strings"
20
 	"strings"
22
 )
21
 )
30
 // Massage enhances the SVG output from DOT to provide better
29
 // Massage enhances the SVG output from DOT to provide better
31
 // panning inside a web browser. It uses the SVGPan library, which is
30
 // panning inside a web browser. It uses the SVGPan library, which is
32
 // embedded into the svgPanJS variable.
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
 	// Work around for dot bug which misses quoting some ampersands,
33
 	// Work around for dot bug which misses quoting some ampersands,
37
 	// resulting on unparsable SVG.
34
 	// resulting on unparsable SVG.
38
 	svg = strings.Replace(svg, "&;", "&;", -1)
35
 	svg = strings.Replace(svg, "&;", "&;", -1)