-
Notifications
You must be signed in to change notification settings - Fork 129
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
Replace the custom flamegraph viewer with speedscope #100
base: master
Are you sure you want to change the base?
Conversation
I haven't looked at this code, but in theory this is fine by me. I'm not using or maintaining this library anymore. @itsderek23 and @tenderlove are running the show here now. |
This is really awesome. I'm 👍 on this, but I need to test it with our app first |
I have some large samples sitting around somewhere that I have run against https://github.com/ManageIQ/manageiq , so I can give that a shot at some point tonight and report back. (Update: I will do this tomorrow... it is late 🌝 ) |
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.
So I think I might have more to say, but since I don't even have merge rights and have just been "nerd sniped" into reviewing because I use this repo myself regularly, I gave some comments.
Mostly personal opinion here, so feel free to take it or leave it.
Seems cool regardless!
-Nick
bin/stackprof
Outdated
puts("open file://#{File.expand_path('../../lib/stackprof/flamegraph/viewer.html', __FILE__)}?data=#{File.expand_path(file)}") | ||
exit | ||
} | ||
o.on('--flamegraph', "open a viewer for the flamegraph of the given profile"){ options[:format] = :flamegraph } |
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 am personally 👎 on this options consolidation change (not that I have any real pull in the final say...)
Reason being is I have used this to integrate with other repos, and having this in two separate steps is kind of ideal when you want to just save it to a path your your choosing (currently this forces the use of Tmpdir
), and open at your convenience. Allowing you to choose a dir also allows you to organize your samples, even when using the private Stackprof.print_flamegraph
interface, and now that is not available.
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.
To maybe meet halfway:
Could you keep the options that exist currently, but change --flamegraph-viewer
to optionally accept a file
, and if one isn't provided, it will run a form of the Tmpdir
code you currently have and open the file.
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 for the feedback! I don't have strong opinions here, and happy to do whatever would yield the best user experience here.
Open to guidance for what the commandline flags should be here and what the semantics should be.
and having this in two separate steps is kind of ideal when you want to just save it to a path your your choosing
My understanding is that if you want to collect multiple profiles for later viewing, you can collect them as the raw stackprof output into a directory of your choosing, then run the --flamegraph
command to view them. I'm not sure what you would want to do with the intermediate form here other than open them to view as a flamegraph.
The intermediate form is a JavaScript ball that isn't usable (AFAIK) from anything except the flamegraph viewer.
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 intermediate form is a JavaScript ball that isn't usable (AFAIK) from anything except the flamegraph viewer.
I always kind of thought of the --flamegraph
flag command as the "compile" step of the flamegraph, and the --flamegraph-viewer
flag as the execution. The "compile" step could be slow, depending on how big your stackprof blob is (you were talking about 100MB blobs, and I know the feeling myself), you might not want to repeat that process if you are looking at data over time, or with a before and after.
Sidenote (and I am NOT suggesting doing this in this PR), in regards to the "Javascript ball" being unusable, I have wondered if it made sense to have a option flag to make it a single .html
file artifact instead of javsacript file but inlining the scripts (so speedscope
in this case). Seems like it would avoid the whole viewer
issue, and it is what the original perl
version of the flamegraph
does as well by making it an 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 don't have any strong feelings about this change, but I'd prefer not to break people's workflow.
lib/stackprof/report.rb
Outdated
|
||
flamegraph_row(f, x - row_width, y, row_width, row_prev) | ||
end | ||
if not `which xdg-open`.empty? |
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.
To avoid shelling out too much, could you implement this:
https://stackoverflow.com/a/5471032/3574689
Not that stackprof
has Windows support anyway, but would save this being something needing to be changed in the future if that ever becomes the case.
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 commands being checked (open
and xdg-open
) here are unlikely to be implemented in Windows. This will still need to be modified for Windows support to run some command that will successfully open the browser, so I'm not sure if the proposed solution helps much on Windows.
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.
Made most of my case over in this comment, but did want to emphasize that this and the other comment was more of a suggestion, not a requirement or even a "strongly worded request".
@@ -0,0 +1,16 @@ | |||
#!/bin/bash |
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.
Could this possibly turned into a Ruby script/rake
task?
The Gem::Package::TarReader
should be able to handle the Tar
portion of things (cross platform), and then you can basically use FileUtils
for the rest.
Obviously npm
is still required and would need a shell out, but that is just a given and as you have stated it is just a developer dependency.
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.
It could be, but I'm not sure it's worth doing -- is the target benefit to make it possible for a Windows-only maintainer to run the update 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.
So there are a few non-"but what about WIN-DOSE" reasons I tend to always push for this myself, and it very much is pedantic in most cases... BUT YOU ASKED FOR IT! (he says sarcastically):
(I am writing a lot here, but again this is just an explanation to my personal opinionated stance and just what I do myself and why. Zero pressure to make the change and feel free to take it or leave it.)
- Shelling out has some cost associated with it. Next to nothing 99% of the time, but depending on how much you need to do it, it can add up. In your case, nothing here of note or value where this is actually an issue, just an FYI.
- When I use "Windows" as an example, bit is more about removing inconsistencies between platforms, and since we know Ruby is going to be used by who ever is running this script, we can assume it will be there as a more consistent constant (#redundantWordsAreRedundant). For a few examples in your case:
- A user might have some weird version of
/bin/bash
(probably a bad example for this use case...) which
might not exist on the$PATH
on some machines, or the user might be running withsudo
- weird user aliases being loaded that mess with the calls in the script
- A user might have some weird version of
- Specifically to the
rake
suggestion, this is simply for dev/maintainer consistency. I know that you did put a README together for thespeedscope
stuff specifically, but from personal experience, most people don't RTFM. But assuming they would at least do arake -T
when they clone the project (whichrake
is pretty much accepted as the build pipeline tool for ruby projects), they can get at a glance what general dev tasks are available.- Further more, if it is written in ruby, we then are just doing
Ruby -> npm
for that one time we need to shell out, instead ofRuby -> shell -> npm/tar/...
. Less moving parts and less places for failure.
- Further more, if it is written in ruby, we then are just doing
</2cents>
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.
@tenderlove Do you have opinions on this front? I don't spend too much time in the ruby ecosystem, so I'm not sure what the expectations are on this front. I'm happy to do this if you feel strongly, but would otherwise bias towards not doing it, since not doing it less work 😅
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.
@jlfwong I think you meant to direct this at me, but I can put something together for you if it is decided this would be preferred.
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.
Sorry, I misread the past comment and just noticed your true intent. Again, apologize for the extra noise.
I did, however, put together some sample code to show how this could be done, but again, no pressure to actually implement this, and it was mostly a interesting exercise for myself.
# Rakefile (I personally put this starting around line 11)
def untar(tarfile)
# Little bit of a hack with the `.new` to get this to work
Gem::Package.new("").open_tar_gz tarfile do |tar|
tar.each { |file| yield file }
end
end
desc "Update speedscope assets"
task :update_speedscope do
rm_rf "vendor/speedscope"
mkdir_p "vendor/speedscope"
cd "vendor/speedscope" do
sh "npm pack speedscope"
File.open Dir.glob("speedscope*.tgz").first do |tarfile|
untar tarfile do |file|
next unless file.full_name =~ /^package\/(LICENSE|dist\/release\/.*(html|css|js|png))$/
File.write File.basename(file.full_name), file.read
end
rm tarfile.path
end
end
end
One note, this does change the scoping of the resulting directory structure from vendor/speedscope/speedscope
to just vendor/speedscope
, since the nested dir seemed redundant (but I could be missing something). I would assume that some changes in report.rb
would be necessary if this is a reasonable change, otherwise a few tweaks to this task could be made to emulate what already exists.
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 for writing up sample code! I always appreciate it when people are willing to take the time to write code to support their ideas.
That said, I'm not planning on changing this to a rake task unless this is considered a blocker for merging.
My reasoning here is that I'm intending to make similar changes to several different profilers in many different languages (e.g. pyflame), and would prefer the update scripts to look as similar as possible. Changing them to be specific to the language of the profiler would make that more difficult.
lib/stackprof/report.rb
Outdated
File.open(tmp_js_path, "w") do |tmp_js_file| | ||
tmp_js_file.write(js_source) | ||
end | ||
puts "Creating temp file #{tmp_js_path}" |
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.
Pedantic: Seems like this should be removed, or possibly hidden behind a --debug
/--verbose
flag.
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.
Yep! Printing the temp file for the HTML file is useful if the browser open fails for some reason, but I agree this path isn't particularly helpful for anything other than my personal debugging while writing this :)
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.
Noticed this after I re-read my review, but maybe putting this down as an else
case for when you are doing the which
checks and which
can't find a program doing an "open", so at least the output file is being printed and something is shown as an output.
I have not, but have this integrated into https://github.com/jlfwong/rack-mini-profiler, which we use to profile a sinatra app regularly at Figma. We also use speedscope at Figma to open 100MB+ profiles generated by Chrome, which it handles relatively gracefully. |
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.
We need to make sure the viewer and JSON are separate (it looks like this PR will do that), but also ensure we can print the flamegraph JSON data to an IO of our choosing. I think this PR removes the that ability (though I could be wrong because the diff is pretty large 😅).
As long as we have an API that can ensure those things, then I'm happy. 😊
bin/stackprof
Outdated
puts("open file://#{File.expand_path('../../lib/stackprof/flamegraph/viewer.html', __FILE__)}?data=#{File.expand_path(file)}") | ||
exit | ||
} | ||
o.on('--flamegraph', "open a viewer for the flamegraph of the given profile"){ options[:format] = :flamegraph } |
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 don't have any strong feelings about this change, but I'd prefer not to break people's workflow.
@@ -80,65 +83,36 @@ def print_stackcollapse | |||
end | |||
end | |||
|
|||
def print_flamegraph(f=STDOUT, skip_common=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.
We need to maintain this API. We're using flamegraphs in production by serving a static asset (the flamegraph viewer) and it makes a request to and endpoint that serves up the flamegraph JSON for a particular page. We use this method to print the flamegraph JSON to the socket
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.
Can you elaborate on the workflow you use? This function as it currently exists doesn't print JSON directly, but rather prints a JavaScript function invocation whose only argument happens to be valid JSON.
Do you take the output of this and strip the flamegraph(
at the beginning and the trailing )
at the end then serve it as true JSON?
Or, when you say
it makes a request to and endpoint that serves up the flamegraph JSON for a particular page
do you mean that you set up the flamegraph viewer to include a <script>
which references the exact output of print_flamegraph
?
Both before and after this PR, the data and the viewer are separated, and before and after this PR the data is valid JavaScript but not valid JSON without modification.
So I think the request here is that "the step for extracting the JavaScript file and the step for opening the browser need to be separate steps so that consumers of stackprof as a library can serve the JavaScript file manually".
If that's the request, I'm going to open up a different code pathway for this.
Speedscope has multiple ways of loading data.
- Dropping local files in
- Browsing for local files
- Specifying files via (possibly CORS) XHR URLs, which would must not contain the JavaScript function invocation wrapper (documented here: https://github.com/jlfwong/speedscope#importing-via-url)
- Specifying files via
file://
protocol script URLs which must contain the Javascript function invocation wrapper, which is what this PR does since you can't make XHR requests tofile://
protocol URLs.
So if I understand you correctly, here's a proposed remediation:
- Add a method called
get_speedscope_json
which returns a JSON string (not a JavaScript string) which could then be served over HTTP - Use that method in
view_flamegraph_in_browser
to retain the behavior in the PR as it stands
Then to upgrade your integration with stackprof, it would require changing the call-site to use get_speedscope_json
instead of print_flamegraph_json
, and also change the static assets that are being served right now to serve speedscope rather than serving the existing flamegraph viewer.
How does that path forward sound to you?
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.
Yes, this sounds perfect!
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.
This should be complete now
I've tried integrating this in to production, but it seems like |
Hmm. It's going to be a bit tricky, but it's doable. The reason it downloads other things is to speed up loading so that it can present a UI and allow users to browse for a file without needing to wait for all the code that does the actual importing. If I'm going down that path anyway, would you prefer that everything is inlined into the I'll look into it when I change the codepaths to split |
@tenderlove To be clear, are you talking about I'm curious how you have this set up with the current flamechart viewer, given that I thought it loads scripts dynamically too? |
@tenderlove My working assumption is that you have a |
o.on('--flamegraph-viewer [f.js]', String, "open html viewer for flamegraph output\n\n"){ |file| | ||
puts("open file://#{File.expand_path('../../lib/stackprof/flamegraph/viewer.html', __FILE__)}?data=#{File.expand_path(file)}") | ||
o.on('--flamegraph', "output format for consumption by --flamegraph-viewer"){ options[:format] = :flamegraph } | ||
o.on('--flamegraph-viewer [profile-path]', "open a viewer for the flamegraph of the given profile"){ |f| |
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.
Okay, I switched this back to preserve the original switches, with the added ability to use --flamegraph-viewer
directly on a profile file without needing to do the compile step first, but also supporting doing the compile step first to preserve people's existing workflows
Okay, @tenderlove I updated the PR using a build which shouldn't do any dynamic script loading. The work is based on an open PR on speedscope, so I pulled in the tarball for it manually rather than using Here's the relevant PR on speedscope if you're curious: jlfwong/speedscope#113 |
@tenderlove I'm also interested to know if the CSP policy you have set up prohibits the use of |
@tenderlove ^ ping |
@jlfwong hey, sorry it took so long to get back to you. Yes, our CSP won't allow My goal is that folks at work can just click a link and see flame graphs of the page. I really want this in stackprof because it's hands down better than the existing one. Maybe we could keep an API that outputs data that will work with the old viewer? |
@tenderlove Got it. Yeah, that makes sense!
Definitely a noble goal :) A couple of alternative options to potentially consider. You've probably thought of these already, but just to make sure that these are all show-stoppers:
If those are both no-gos, then I'll look into either preserving the existing flamechart viewer or into switching the WebGL abstraction I'm using to one which doesn't use |
@tenderlove Okay, I've updated the PR to use a version which does not use eval. To validate that it was working, I opened it via a local server and specified the following header:
I was able to validate that the version before the removal of Can you see if this now works for GitHub's content security policy? If it turns out that I'm wrong and that both the eval calls and the dynamic file loading were causing issues, I can easily switch this to a build that has both removed. |
@tenderlove ping! ^ |
This seems rather interesting, is this proposal alive? |
This PR replaces the custom flamegraph viewer introduced by @aroben in #74 (which sounds like it was a huge improvement over the existing SVG viewer!) with an integration of a high performance, WebGL-based, language agnostic profile viewer that I've been working on called speedscope: https://github.com/jlfwong/speedscope.
If you don't think this is a good fit for this repository, no worries! I'm happy to either just close this PR, or to change it to be a dramatically reduced version which just makes it easier to output a format that speedscope can consume (basically just the
JSON
serialized version of a stackprof profile; speedscope has built-in code to handle import of stackprof profiles: https://github.com/jlfwong/speedscope/blob/master/import/stackprof.ts)A script is included (
vendor/speedscope/update.sh
) to handle updating speedscope in the future to pull in the latest version.It should be able to easily handle profiles at least as large as the existing viewer, and zooming & panning should remain 60fps within those very large profiles through efficient use of the GPU for rendering.
Test Plan:
Ran the following:
I also preserved the original workflow of doing a separate compilation & opening step:
This should open the profile in-browser in both Linux and OS X using whatever your default configured browser is. I've only tested on OS X to date on Firefox & Chrome, but it shouldn't be OS dependent.
Screenshots: