New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Added an interactive web interface #154
Conversation
on the command line. The interface is available by visiting localhost:port in a browser. Requirements: * Graphviz must be installed. * Browser must support Javascript. * Tested in recent stable versions of chrome and firefox. Features: * The entry point is a dot graph display (equivalent to "web" output). * Nodes in the graph can be selected by clicking. * A regular expression can also be typed in for selection. * The current selection (either list of nodes or a regexp) can be focused, ignored, or hidden. * Source code or disassembly of the current selection can be displayed.
Codecov Report
@@ Coverage Diff @@
## master #154 +/- ##
=========================================
- Coverage 54.12% 53.1% -1.03%
=========================================
Files 30 32 +2
Lines 5916 6096 +180
=========================================
+ Hits 3202 3237 +35
- Misses 2372 2508 +136
- Partials 342 351 +9
Continue to review full report at Codecov.
|
the page corresponding to command line arguments.
@@ -58,6 +59,7 @@ func parseFlags(o *plugin.Options) (*source, []string, error) { | |||
flagTools := flag.String("tools", os.Getenv("PPROF_TOOLS"), "Path for object tool pathnames") | |||
|
|||
flagTimeout := flag.Int("timeout", -1, "Timeout in seconds for fetching a profile") | |||
flagHTTPPort := flag.Int("http", 0, "Present interactive web based UI at the specified http port") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Would it be possible to make the "http" another command here -
pprof/internal/driver/commands.go
Line 87 in 1b8137e
var pprofCommands = commands{ |
I felt this would integrate more naturally, but maybe I am missing something.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's what I had tried initially, but it became awkward for a few reasons:
-
The usage message for the new command showed up under
Output formats
which seemed odd. -
Commands only support string parameters, whereas I wanted a port number.
-
Making it a command would allow
http _port_
as a command in the interactive shell, which I wasn't sure we wanted to support.
So possible, but not sure it would be better/simpler than the current approach.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, makes sense to me.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm OK either way, but I think having http be listed under Output Formats wouldn't be terrible: we have (web|evince|gv) in there already. We could always explicitly disable the command in the UI if we really wanted to (is there really a problem with it?).
In fact, how about calling this "web" instead of "http", and eliminating the current "web"?
There's always "svg" for people that really want a static svg.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-
I think reusing the name "web" will be problematic since it will change the behavior substantially without warning. E.g., consider somebody who has a script that does "pprof web" a few times to open a bunch of views in a row. The interactive UI will block on the first call. I also am not so excited about "There's always "svg" for people that really want a static svg." If we do that, we are going to force some people to learn a new behavior after they get confused a bit about why things don't work as they used to; and I don't think the win here is big enough to justify that.
-
W.r.t. putting "http" under "Output Formats", I still think that it is better to keep things as they are for the reasons mentioned above (usage message, port number not a string, interactive command misuse).
So unless I hear a stronger complaint than "I'm OK either way", I will keep "http" out of Output Formats.
README.md
Outdated
## Run pprof via a web interface | ||
|
||
If the `-http=port` option is specified, pprof starts a web server at | ||
the specified port that you can visit in your browser to get an interactive |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe mention here that the browser will auto-open.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
README.md
Outdated
``` | ||
pprof -http=[port] [main_binary] profile.pb.gz | ||
|
||
This will run a server that can be visited in the browser. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nit: this line seems redundant given the above text.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
@@ -58,6 +59,7 @@ func parseFlags(o *plugin.Options) (*source, []string, error) { | |||
flagTools := flag.String("tools", os.Getenv("PPROF_TOOLS"), "Path for object tool pathnames") | |||
|
|||
flagTimeout := flag.Int("timeout", -1, "Timeout in seconds for fetching a profile") | |||
flagHTTPPort := flag.Int("http", 0, "Present interactive web based UI at the specified http port") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I see, makes sense to me.
internal/driver/webui.go
Outdated
}) | ||
} | ||
|
||
// disasm generates a web page containing an svg diagram.. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
"disasm" -> "dot"
also fix double full stop at the end.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
internal/driver/webui.go
Outdated
// Convert to svg. | ||
svg, err := dotToSvg(dot.Bytes()) | ||
if err != nil { | ||
reportHandlerError(w, "Failed to execute dot. Is Graphviz installed?", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe change to "failed to execute dot: is Graphviz installed?" to start with lowercase like in other messages.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
|
||
// getFromLegend returns the suffix of an entry in legend that starts | ||
// with param. It returns def if no such entry is found. | ||
func getFromLegend(legend []string, param, def string) string { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Extracting the data from the legend seems a bit fragile. Maybe make pprof fail early in tests if the parameter to extract is not found? Or something else that the tests would detect.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
+1
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I augmented webui_test.go to check that the binary name and profile type are in the generated output.
I didn't do more than that because I was thinking that it would be best to eventually change lower-level code to produce legend info in a struct and leave the stringification to a helper routine. In this code we would avoid the stringification altogether and just get the pieces we need directly from the struct. E.g., I think the header we generate here could be augmented to include info about the displayed counts vs. the overall count.
internal/driver/webhtml.go
Outdated
<button id="disasm">Disasm</button> | ||
<input id="searchbox" type="text" placeholder="Search regexp" autocomplete="off" autocapitalize="none" size=40/> | ||
<button id="focus">Focus</button> | ||
<button id="show">Show</button> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In other UIs we've ordered these as focus, ignore, hide, show, to make it clearer that focus/ignore and hide/show are related pairs.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
internal/driver/webui.go
Outdated
} | ||
} | ||
// No visualizer succeeded, so just print URL. | ||
fmt.Fprintln(os.Stderr, "Visit", u.String(), "in a web browser") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use UI.PrintErr (from plugin.Options). Same for other prints to stderr.
Also, having the URL alone in a single line is easier for cut/paste.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
internal/driver/webui.go
Outdated
vars["show"].value = req.URL.Query().Get("s") | ||
vars["ignore"].value = req.URL.Query().Get("i") | ||
vars["hide"].value = req.URL.Query().Get("h") | ||
_, rpt, err := generateRawReport(ui.prof, args, vars, ui.options) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why manually generate the dot and svg instead of using the existing support for pprof -svg? Seems to me that pushing the report.Generate from generateReport into generateRawReport would eliminate some duplication.
For instance, there is some duplication between dotToSvg() and invokeDot()
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-svg (and -web etc.) generate a standalone svg document, whereas what we want here is an svg that we can embed inside an html.
The regular svg handling code calls Massage, which strips out useful information like width, height, viewBox. We want to avoid that information loss since we need it when the svg is embedded in html.
internal/driver/webui.go
Outdated
// webUI holds the state needed for serving a browser based interface. | ||
type webUI struct { | ||
prof *profile.Profile | ||
options *plugin.Options |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The motivation behind the UI field in plugin.Options is that it would allow errors to be redirected to an interactive UI instead of always printing to stderr.
Would it be possible to redefine options.UI so that it would show such errors somewhere in the UI (either a pop-up or maybe in red in a corner)?
For instance, if you type a function name that doesn't exist and click "focus" we get a print on stderr saying "Focus expression matched no samples". It would be better if that showed up on the browser.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. Instead of getting fancy with the display, I just print the errors below the title, which is prominent enough. It is non-dismissable, which I think is fine, and perhaps better if somebody takes a screenshot.
1. Capture and display errors like "Focus expression matched no samples". 2. Re-ordered buttons to match other interfaces. 3. Use UI.PrintErr to print error messages.
internal/driver/webhtml.go
Outdated
<button id="disasm">Disasm</button> | ||
<input id="searchbox" type="text" placeholder="Search regexp" autocomplete="off" autocapitalize="none" size=40/> | ||
<button id="focus">Focus</button> | ||
<button id="show">Show</button> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
internal/driver/webui.go
Outdated
// webUI holds the state needed for serving a browser based interface. | ||
type webUI struct { | ||
prof *profile.Profile | ||
options *plugin.Options |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done. Instead of getting fancy with the display, I just print the errors below the title, which is prominent enough. It is non-dismissable, which I think is fine, and perhaps better if somebody takes a screenshot.
internal/driver/webui.go
Outdated
} | ||
} | ||
// No visualizer succeeded, so just print URL. | ||
fmt.Fprintln(os.Stderr, "Visit", u.String(), "in a web browser") |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Done.
internal/driver/webui.go
Outdated
vars["show"].value = req.URL.Query().Get("s") | ||
vars["ignore"].value = req.URL.Query().Get("i") | ||
vars["hide"].value = req.URL.Query().Get("h") | ||
_, rpt, err := generateRawReport(ui.prof, args, vars, ui.options) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
-svg (and -web etc.) generate a standalone svg document, whereas what we want here is an svg that we can embed inside an html.
The regular svg handling code calls Massage, which strips out useful information like width, height, viewBox. We want to avoid that information loss since we need it when the svg is embedded in html.
internal/driver/webui.go
Outdated
// Convert to svg. | ||
svg, err := dotToSvg(dot.Bytes()) | ||
if err != nil { | ||
reportHandlerError(w, "failed to execute dot. Is Graphviz installed?", err) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Actually, I switched this back to "Failed..." so it matches commands.go. Also, this messag is printed out as a sentence on its own so it looks better capitalized.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
HTML/JS
internal/driver/webhtml.go
Outdated
var enable = (currentRe != "" || selected.size != 0) | ||
if (buttonsEnabled == enable) return | ||
buttonsEnabled = enable | ||
var d = enable ? false : true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not equivalent "!enable" instead
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remnant of older code. Fixed to use !enable instead.
internal/driver/webhtml.go
Outdated
var graph0 = document.getElementById("graph0") | ||
var svg = graph0.parentElement | ||
|
||
var currentRe = "" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
FYI, Map is an ES6 feature. Relying on it means that the entire code can utilize ES6 (const, let, lambdas, for-of, destructuring, etc.). The same goes for the const at the top, also ES6 only.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Switched to const where possible, let elsewhere.
Switched to "for..of" where possible.
I didn't spot any place where lambdas or destructuring assingment would make things easier to read.
internal/driver/webhtml.go
Outdated
var panLastX = 0 // Last event X coordinate | ||
var panLastY = 0 // Last event Y coordinate | ||
var moved = false // Have we seen significant movement | ||
var touchid = null; // Current touch identifier |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Although the code tries to avoid semicolons, there are still some left, which are not necessary (the only necessary semicolon is when the next line starts with "(" or "[" AFAIR).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed semicolons.
internal/driver/webhtml.go
Outdated
searchAlarm = setTimeout(doSearch, 300) | ||
} | ||
|
||
doSearch = function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
missing "var" (I recommend using "use strict" directive in global functions, which catch this). The same e.g. for the select and unselect functions below.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added 'use strict'; very helpful. I put it just inside my two global functions instead of the top-level, since I presume you had some reason in mind when you mentioned global functions,
internal/driver/webhtml.go
Outdated
|
||
// Make svg pannable and zoomable. | ||
// Call clickHandler(t) if a click event is caught by the pan event handlers. | ||
function initPanAndZoom(svg, clickHandler) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
function statements are used here for global functions, but assignment of function expressions are used for the inner functions, is there any reason?
Unless reassigning a variable with another function, the statements have the advantage of being order independent and can be called before they are defined in the source, so the code can be top-down. The assignments have to be in a correct order and defined before used (unless used in another function and called later), which enforced bottom-up order).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't realize functions could be nested. Fixed.
I could clean up the order of some things now, but I would rather not disturb the diff for very little gain.
internal/driver/webhtml.go
Outdated
var dx = t1.clientX - t2.clientX | ||
var dy = t1.clientY - t2.clientY | ||
return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is Math.hypot() (ES6+)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Switched to Math.hypot.
var graphTemplate = template.Must(template.New("graph").Parse( | ||
`<!DOCTYPE html> | ||
<html> | ||
<head> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
nit: trailing "/" in tags is a HTML error (that is being gracefully ignored, it was used only for now dead XHTML). Only pair tags have to be paired.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed trailing "/" from tags.
internal/driver/webhtml.go
Outdated
</div> | ||
</body> | ||
</html> | ||
<script> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
script should be before closing body (browsers generally ignore closing body and html and insert then at the end anyway, but AFAIK IE 10 for example did not and could ignore the script.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved <script> inside
Also added pprof binary to .gitignore.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. Your comments have been very helpful since I am a Javascript novice.
var graphTemplate = template.Must(template.New("graph").Parse( | ||
`<!DOCTYPE html> | ||
<html> | ||
<head> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed trailing "/" from tags.
internal/driver/webhtml.go
Outdated
</div> | ||
</body> | ||
</html> | ||
<script> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Moved <script> inside
internal/driver/webhtml.go
Outdated
|
||
// Make svg pannable and zoomable. | ||
// Call clickHandler(t) if a click event is caught by the pan event handlers. | ||
function initPanAndZoom(svg, clickHandler) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I didn't realize functions could be nested. Fixed.
I could clean up the order of some things now, but I would rather not disturb the diff for very little gain.
internal/driver/webhtml.go
Outdated
var panLastX = 0 // Last event X coordinate | ||
var panLastY = 0 // Last event Y coordinate | ||
var moved = false // Have we seen significant movement | ||
var touchid = null; // Current touch identifier |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Removed semicolons.
internal/driver/webhtml.go
Outdated
var dx = t1.clientX - t2.clientX | ||
var dy = t1.clientY - t2.clientY | ||
return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2)) | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Switched to Math.hypot.
internal/driver/webhtml.go
Outdated
var graph0 = document.getElementById("graph0") | ||
var svg = graph0.parentElement | ||
|
||
var currentRe = "" |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Switched to const where possible, let elsewhere.
Switched to "for..of" where possible.
I didn't spot any place where lambdas or destructuring assingment would make things easier to read.
internal/driver/webhtml.go
Outdated
searchAlarm = setTimeout(doSearch, 300) | ||
} | ||
|
||
doSearch = function() { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Added 'use strict'; very helpful. I put it just inside my two global functions instead of the top-level, since I presume you had some reason in mind when you mentioned global functions,
internal/driver/webhtml.go
Outdated
var enable = (currentRe != "" || selected.size != 0) | ||
if (buttonsEnabled == enable) return | ||
buttonsEnabled = enable | ||
var d = enable ? false : true |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Remnant of older code. Fixed to use !enable instead.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM. I think we could factor out the common parts of the SVG generation to avoid some duplication, but we can do that separately in the future.
This reverts commit f83a3d8.
* Added an interactive web interface triggered by passing -http=port on the command line. The interface is available by visiting localhost:port in a browser. Requirements: * Graphviz must be installed. * Browser must support Javascript. * Tested in recent stable versions of chrome and firefox. Features: * The entry point is a dot graph display (equivalent to "web" output). * Nodes in the graph can be selected by clicking. * A regular expression can also be typed in for selection. * The current selection (either list of nodes or a regexp) can be focused, ignored, or hidden. * Source code or disassembly of the current selection can be displayed. * Remove unused function. * Skip graph generation test if graphviz is not installed. * Added -http port and the various modes of using pprof to the usage message. * Web interface now supports "show" option. * Web interface automatically opens the browser pointed at the page corresponding to command line arguments. * Some tweaks for firefox. * Handle review comments (better usage message, more testing). * Handled review comments: 1. Capture and display errors like "Focus expression matched no samples". 2. Re-ordered buttons to match other interfaces. 3. Use UI.PrintErr to print error messages. * Handle javascript code review comments (a bunch of cleanups). Also added pprof binary to .gitignore.
The web interface is triggered by passing -http=port on the command line.
The interface is available by visiting localhost:port in a browser.
Requirements:
Features:
can be focused, ignored, shown, or hidden.