FEATURE: Introduce `pp=async-flamegraph` for asynchronous flamegraphs (#494)

FEATURE: Introduce pp=async-flamegraph for asynchronous flamegraphs (#494)

Using ?pp=async-flamegraph causes the flamegraph data to be placed in long-term storage, and made available via a link in the mini_profiler UI. Flamegraph data will also be recorded and stored for all AJAX requests with ?pp=async-flamegraph in the Referer header. This is useful in a few situations:

  • You want to view flamegraphs for AJAX requests made by a Javascript application. By supplying pp=async-flamegraph, flamegraph links for every AJAX request will be made available in the mini-profiler UI.

  • You want to see the HTML result of a request, and view the flamegraph later. The existing ?pp=flamegraph option hides the true output.

  • You are performing the request via a tool like curl, and would like to view the flamegraph later in the browser (you can extract the X-MiniProfiler-Ids header from the response, then view flamegraph in the browser)


When the pp=async-flamegraph parameter is supplied, a new “flamegraph” link is added to the UI. Clicking the link will take you to a URL like /mini-profiler-resources/flamegraph?id=t0x70kt7hy3cmitx7adx, which displays the flamegraph UI.

diff --git a/CHANGELOG.md b/CHANGELOG.md
index 30a09ff..575695a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,5 +1,9 @@
 # CHANGELOG
 
+## Unreleased
+
+- [FEATURE] Introduce `pp=async-flamegraph` for asynchronous flamegraphs
+
 ## 2.3.1 - 2021-01-29
 
 - [FIX] compatability with Ruby 3.0
diff --git a/Gemfile b/Gemfile
index ae6c60a..2e332a6 100644
--- a/Gemfile
+++ b/Gemfile
@@ -5,7 +5,10 @@ ruby '>= 2.4.0'
 
 gemspec
 
-gem 'codecov', require: false, group: :test
+group :test do
+  gem 'codecov', require: false
+  gem 'stackprof', require: false
+end
 
 group :development do
   gem 'guard', platforms: [:mri_22, :mri_23]
diff --git a/README.md b/README.md
index cb833f3..f22932e 100644
--- a/README.md
+++ b/README.md
@@ -171,6 +171,10 @@ To generate [flamegraphs](http://samsaffron.com/archive/2013/03/19/flame-graphs-
 * add the [**stackprof**](https://rubygems.org/gems/stackprof) gem to your Gemfile
 * visit a page in your app with `?pp=flamegraph`
 
+To store flamegraph data for later viewing, append the `?pp=async-flamegraph` parameter. The request will return as normal.
+Flamegraph data for this request, and all subsequent requests made by this page (based on the `REFERER` header) will be stored.
+'flamegraph' links will appear for these requests in the MiniProfiler UI.
+
 ### Memory Profiling
 
 Memory allocations can be measured (using the [memory_profiler](https://github.com/SamSaffron/memory_profiler) gem)
diff --git a/lib/html/includes.js b/lib/html/includes.js
index c306574..f704b43 100644
--- a/lib/html/includes.js
+++ b/lib/html/includes.js
@@ -1213,6 +1213,9 @@ var _MiniProfiler = (function() {
     shareUrl: function shareUrl(id) {
       return options.path + "results?id=" + id;
     },
+    flamegraphUrl: function flamegrapgUrl(id) {
+      return options.path + "flamegraph?id=" + id;
+    },
     moreUrl: function moreUrl(requestName) {
       var requestParts = requestName.split(" ");
       var linkSrc =
diff --git a/lib/html/includes.tmpl b/lib/html/includes.tmpl
index 17e1cf9..5a46d58 100644
--- a/lib/html/includes.tmpl
+++ b/lib/html/includes.tmpl
@@ -142,6 +142,9 @@
 <script id="linksTemplate" type="text/x-dot-tmpl">
   <a href="{{= MiniProfiler.shareUrl(it.page.id) }}" class="profiler-share-profiler-results" target="_blank">share</a>
   <a href="{{= MiniProfiler.moreUrl(it.timing.name) }}" class="profiler-more-actions">more</a>
+  {{? it.page.has_flamegraph}}
+    <a href="{{= MiniProfiler.flamegraphUrl(it.page.id) }}" class="profiler-show-flamegraph" target="_blank">flamegraph</a>
+  {{?}}
   {{? it.custom_link}}
     <a href="{{= it.custom_link }}" class="profiler-custom-link" target="_blank">{{= it.custom_link_name }}</a>
   {{?}}
diff --git a/lib/html/vendor.js b/lib/html/vendor.js
index 55d5bd0..c0e16ee 100644
--- a/lib/html/vendor.js
+++ b/lib/html/vendor.js
@@ -11,7 +11,7 @@ var out=' <div class="profiler-result"> <div class="profiler-button ';if(it.has_
 }
 MiniProfiler.templates["linksTemplate"] = function anonymous(it
 ) {
-var out=' <a href="'+( MiniProfiler.shareUrl(it.page.id) )+'" class="profiler-share-profiler-results" target="_blank">share</a> <a href="'+( MiniProfiler.moreUrl(it.timing.name) )+'" class="profiler-more-actions">more</a> ';if(it.custom_link){out+=' <a href="'+( it.custom_link )+'" class="profiler-custom-link" target="_blank">'+( it.custom_link_name )+'</a> ';}out+=' ';if(it.page.has_trivial_timings){out+=' <a class="profiler-toggle-trivial" data-show-on-load="'+( it.page.has_all_trivial_timings )+'" title="toggles any rows with &lt; '+( it.page.trivial_duration_threshold_milliseconds )+' ms"> show trivial </a> ';}return out;
+var out=' <a href="'+( MiniProfiler.shareUrl(it.page.id) )+'" class="profiler-share-profiler-results" target="_blank">share</a> <a href="'+( MiniProfiler.moreUrl(it.timing.name) )+'" class="profiler-more-actions">more</a> ';if(it.page.has_flamegraph){out+=' <a href="'+( MiniProfiler.flamegraphUrl(it.page.id) )+'" class="profiler-show-flamegraph" target="_blank">flamegraph</a> ';}out+=' ';if(it.custom_link){out+=' <a href="'+( it.custom_link )+'" class="profiler-custom-link" target="_blank">'+( it.custom_link_name )+'</a> ';}out+=' ';if(it.page.has_trivial_timings){out+=' <a class="profiler-toggle-trivial" data-show-on-load="'+( it.page.has_all_trivial_timings )+'" title="toggles any rows with &lt; '+( it.page.trivial_duration_threshold_milliseconds )+' ms"> show trivial </a> ';}return out;
 }
 MiniProfiler.templates["timingTemplate"] = function anonymous(it
 ) {
diff --git a/lib/mini_profiler/profiler.rb b/lib/mini_profiler/profiler.rb
index 3cdbae5..52a57d9 100644
--- a/lib/mini_profiler/profiler.rb
+++ b/lib/mini_profiler/profiler.rb
@@ -182,6 +182,7 @@ module Rack
 
       return serve_results(env) if file_name.eql?('results')
       return handle_snapshots_request(env) if file_name.eql?('snapshots')
+      return serve_flamegraph(env) if file_name.eql?('flamegraph')
 
       resources_env = env.dup
       resources_env['PATH_INFO'] = file_name
@@ -345,11 +346,12 @@ module Rack
         # Prevent response body from being compressed
         env['HTTP_ACCEPT_ENCODING'] = 'identity' if config.suppress_encoding
 
-        if query_string =~ /pp=flamegraph/
+        if query_string =~ /pp=(async-)?flamegraph/ || env['HTTP_REFERER'] =~ /pp=async-flamegraph/
           unless defined?(StackProf) && StackProf.respond_to?(:run)
-
-            flamegraph = "Please install the stackprof gem and require it: add gem 'stackprof' to your Gemfile"
-            status, headers, body = @app.call(env)
+            headers = { 'Content-Type' => 'text/html' }
+            message = "Please install the stackprof gem and require it: add gem 'stackprof' to your Gemfile"
+            body.close if body.respond_to? :close
+            return client_settings.handle_cookie([500, headers, message])
           else
             # do not sully our profile with mini profiler timings
             current.measure = false
@@ -429,9 +431,12 @@ module Rack
       page_struct[:user] = user(env)
       page_struct[:root].record_time((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000)
 
-      if flamegraph
+      if flamegraph && query_string =~ /pp=flamegraph/
         body.close if body.respond_to? :close
         return client_settings.handle_cookie(self.flamegraph(flamegraph, path))
+      elsif flamegraph # async-flamegraph
+        page_struct[:has_flamegraph] = true
+        page_struct[:flamegraph] = flamegraph
       end
 
       begin
@@ -651,6 +656,7 @@ Append the following to your query string:
   #{make_link "profile-gc", env} : perform gc profiling on this request, analyzes ObjectSpace generated by request
   #{make_link "profile-memory", env} : requires the memory_profiler gem, new location based report
   #{make_link "flamegraph", env} : a graph representing sampled activity (requires the stackprof gem).
+  #{make_link "async-flamegraph", env} : store flamegraph data for this page and all its AJAX requests. Flamegraph links will be available in the mini-profiler UI (requires the stackprof gem).
   #{make_link "flamegraph&flamegraph_sample_rate=1", env}: creates a flamegraph with the specified sample rate (in ms). Overrides value set in config
   #{make_link "flamegraph_embed", env} : a graph representing sampled activity (requires the stackprof gem), embedded resources for use on an intranet.
   #{make_link "trace-exceptions", env} : will return all the spots where your application raises exceptions
@@ -665,35 +671,31 @@ Append the following to your query string:
 
     def flamegraph(graph, path)
       headers = { 'Content-Type' => 'text/html' }
-      if Hash === graph
-        html = <<~HTML
-          <!DOCTYPE html>
-          <html>
-            <head>
-              <style>
-                body { margin: 0; height: 100vh; }
-                #speedscope-iframe { width: 100%; height: 100%; border: none; }
-              </style>
-            </head>

[... diff too long, it was truncated ...]

GitHub sha: 020cd4f5

This commit appears in #494 which was approved by OsamaSayegh. It was merged by OsamaSayegh.