Skip to content
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

Merged
merged 10 commits into from Jul 14, 2017
Merged

Added an interactive web interface #154

merged 10 commits into from Jul 14, 2017

Conversation

ghemawat
Copy link
Contributor

@ghemawat ghemawat commented Jul 10, 2017

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:

  • 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, shown, or hidden.
  • Source code or disassembly of the current selection can be displayed.

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-io
Copy link

codecov-io commented Jul 10, 2017

Codecov Report

Merging #154 into master will decrease coverage by 1.02%.
The diff coverage is 14.45%.

Impacted file tree graph

@@            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
Impacted Files Coverage Δ
internal/driver/commands.go 44.5% <0%> (ø) ⬆️
internal/driver/webui.go 10.41% <10.41%> (ø)
internal/report/report.go 23.44% <100%> (+1.19%) ⬆️
internal/driver/driver.go 69.27% <33.33%> (-0.01%) ⬇️
internal/graph/dotgraph.go 90.11% <33.33%> (-0.8%) ⬇️
internal/driver/cli.go 75.75% <50%> (-0.81%) ⬇️
profile/legacy_java_profile.go 73.83% <0%> (-0.87%) ⬇️
fuzz/main.go 0% <0%> (ø)
... and 1 more

Continue to review full report at Codecov.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 4dbd915...a5f0d1f. Read the comment docs.

@@ -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")
Copy link
Collaborator

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 -

var pprofCommands = commands{
?

I felt this would integrate more naturally, but maybe I am missing something.

Copy link
Contributor Author

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:

  1. The usage message for the new command showed up under Output formats which seemed odd.

  2. Commands only support string parameters, whereas I wanted a port number.

  3. 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.

Copy link
Collaborator

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.

Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 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.

  2. 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
Copy link
Collaborator

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.

Copy link
Contributor Author

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.
Copy link
Collaborator

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.

Copy link
Contributor Author

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")
Copy link
Collaborator

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.

})
}

// disasm generates a web page containing an svg diagram..
Copy link
Collaborator

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

// Convert to svg.
svg, err := dotToSvg(dot.Bytes())
if err != nil {
reportHandlerError(w, "Failed to execute dot. Is Graphviz installed?", err)
Copy link
Collaborator

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.

Copy link
Contributor Author

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 {
Copy link
Collaborator

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.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

+1

Copy link
Contributor Author

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.

<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>
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

}
}
// No visualizer succeeded, so just print URL.
fmt.Fprintln(os.Stderr, "Visit", u.String(), "in a web browser")
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

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)
Copy link
Contributor

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()

Copy link
Contributor Author

@ghemawat ghemawat Jul 13, 2017

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.

// webUI holds the state needed for serving a browser based interface.
type webUI struct {
prof *profile.Profile
options *plugin.Options
Copy link
Contributor

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.

Copy link
Contributor Author

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.
<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>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

// webUI holds the state needed for serving a browser based interface.
type webUI struct {
prof *profile.Profile
options *plugin.Options
Copy link
Contributor Author

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.

}
}
// No visualizer succeeded, so just print URL.
fmt.Fprintln(os.Stderr, "Visit", u.String(), "in a web browser")
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done.

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)
Copy link
Contributor Author

@ghemawat ghemawat Jul 13, 2017

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.

// Convert to svg.
svg, err := dotToSvg(dot.Bytes())
if err != nil {
reportHandlerError(w, "failed to execute dot. Is Graphviz installed?", err)
Copy link
Contributor Author

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.

Copy link
Contributor

@josef-jelinek josef-jelinek left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HTML/JS

var enable = (currentRe != "" || selected.size != 0)
if (buttonsEnabled == enable) return
buttonsEnabled = enable
var d = enable ? false : true
Copy link
Contributor

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

Copy link
Contributor Author

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.

var graph0 = document.getElementById("graph0")
var svg = graph0.parentElement

var currentRe = ""
Copy link
Contributor

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.

Copy link
Contributor Author

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.

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
Copy link
Contributor

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).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed semicolons.

searchAlarm = setTimeout(doSearch, 300)
}

doSearch = function() {
Copy link
Contributor

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.

Copy link
Contributor Author

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,


// Make svg pannable and zoomable.
// Call clickHandler(t) if a click event is caught by the pan event handlers.
function initPanAndZoom(svg, clickHandler) {
Copy link
Contributor

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).

Copy link
Contributor Author

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.

var dx = t1.clientX - t2.clientX
var dy = t1.clientY - t2.clientY
return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2))
}
Copy link
Contributor

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+)

Copy link
Contributor Author

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>
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed trailing "/" from tags.

</div>
</body>
</html>
<script>
Copy link
Contributor

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.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved <script> inside

Copy link
Contributor Author

@ghemawat ghemawat left a 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>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed trailing "/" from tags.

</div>
</body>
</html>
<script>
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Moved <script> inside


// Make svg pannable and zoomable.
// Call clickHandler(t) if a click event is caught by the pan event handlers.
function initPanAndZoom(svg, clickHandler) {
Copy link
Contributor Author

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.

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
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removed semicolons.

var dx = t1.clientX - t2.clientX
var dy = t1.clientY - t2.clientY
return Math.sqrt(Math.pow(dx, 2) + Math.pow(dy, 2))
}
Copy link
Contributor Author

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 graph0 = document.getElementById("graph0")
var svg = graph0.parentElement

var currentRe = ""
Copy link
Contributor Author

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.

searchAlarm = setTimeout(doSearch, 300)
}

doSearch = function() {
Copy link
Contributor Author

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,

var enable = (currentRe != "" || selected.size != 0)
if (buttonsEnabled == enable) return
buttonsEnabled = enable
var d = enable ? false : true
Copy link
Contributor Author

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.

Copy link
Contributor

@rauls5382 rauls5382 left a 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.

@aalexand aalexand merged commit f83a3d8 into google:master Jul 14, 2017
nolanmar511 added a commit to nolanmar511/pprof that referenced this pull request Jul 20, 2017
@ghemawat ghemawat deleted the webui branch September 5, 2017 15:17
gmarin13 pushed a commit to gmarin13/pprof that referenced this pull request Dec 17, 2020
* 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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

6 participants