rack-mini-profiler 1.0.1 → 2.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (68) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +115 -20
  3. data/README.md +126 -45
  4. data/lib/enable_rails_patches.rb +5 -0
  5. data/lib/html/dot.1.1.2.min.js +2 -0
  6. data/lib/html/includes.css +136 -35
  7. data/lib/html/includes.js +1400 -1009
  8. data/lib/html/includes.scss +546 -441
  9. data/lib/html/includes.tmpl +231 -148
  10. data/lib/html/pretty-print.js +810 -0
  11. data/lib/html/profile_handler.js +1 -1
  12. data/lib/html/rack-mini-profiler.css +3 -0
  13. data/lib/html/rack-mini-profiler.js +2 -0
  14. data/lib/html/share.html +0 -1
  15. data/lib/html/speedscope/LICENSE +21 -0
  16. data/lib/html/speedscope/README.md +3 -0
  17. data/lib/html/speedscope/demangle-cpp.1768f4cc.js +4 -0
  18. data/lib/html/speedscope/favicon-16x16.f74b3187.png +0 -0
  19. data/lib/html/speedscope/favicon-32x32.bc503437.png +0 -0
  20. data/lib/html/speedscope/file-format-schema.json +324 -0
  21. data/lib/html/speedscope/fonts/source-code-pro-regular.css +8 -0
  22. data/lib/html/speedscope/fonts/source-code-pro-v13-regular.woff +0 -0
  23. data/lib/html/speedscope/fonts/source-code-pro-v13-regular.woff2 +0 -0
  24. data/lib/html/speedscope/import.cf0fa83f.js +115 -0
  25. data/lib/html/speedscope/index.html +2 -0
  26. data/lib/html/speedscope/release.txt +3 -0
  27. data/lib/html/speedscope/reset.8c46b7a1.css +2 -0
  28. data/lib/html/speedscope/source-map.438fa06b.js +24 -0
  29. data/lib/html/speedscope/speedscope.44364064.js +200 -0
  30. data/lib/html/vendor.js +848 -0
  31. data/lib/mini_profiler/asset_version.rb +3 -2
  32. data/lib/mini_profiler/client_settings.rb +13 -5
  33. data/lib/mini_profiler/config.rb +43 -5
  34. data/lib/mini_profiler/gc_profiler.rb +1 -1
  35. data/lib/mini_profiler/profiler.rb +310 -42
  36. data/lib/mini_profiler/profiling_methods.rb +13 -8
  37. data/lib/mini_profiler/snapshots_transporter.rb +109 -0
  38. data/lib/mini_profiler/storage/abstract_store.rb +79 -1
  39. data/lib/mini_profiler/storage/file_store.rb +3 -3
  40. data/lib/mini_profiler/storage/memcache_store.rb +2 -0
  41. data/lib/mini_profiler/storage/memory_store.rb +54 -5
  42. data/lib/mini_profiler/storage/redis_store.rb +136 -2
  43. data/lib/mini_profiler/timer_struct/custom.rb +1 -0
  44. data/lib/mini_profiler/timer_struct/page.rb +60 -4
  45. data/lib/mini_profiler/timer_struct/request.rb +53 -11
  46. data/lib/mini_profiler/timer_struct/sql.rb +4 -2
  47. data/lib/mini_profiler/version.rb +1 -1
  48. data/lib/mini_profiler_rails/railtie.rb +88 -7
  49. data/lib/mini_profiler_rails/railtie_methods.rb +61 -0
  50. data/lib/patches/db/activerecord.rb +1 -12
  51. data/lib/patches/db/mongo.rb +1 -1
  52. data/lib/patches/db/moped.rb +1 -1
  53. data/lib/patches/db/mysql2.rb +4 -27
  54. data/lib/patches/db/mysql2/alias_method.rb +30 -0
  55. data/lib/patches/db/mysql2/prepend.rb +34 -0
  56. data/lib/patches/db/plucky.rb +4 -4
  57. data/lib/patches/net_patches.rb +18 -8
  58. data/lib/patches/sql_patches.rb +13 -5
  59. data/lib/prepend_mysql2_patch.rb +5 -0
  60. data/lib/prepend_net_http_patch.rb +5 -0
  61. data/lib/rack-mini-profiler.rb +1 -1
  62. data/rack-mini-profiler.gemspec +15 -6
  63. metadata +150 -31
  64. data/lib/html/jquery.1.7.1.js +0 -4
  65. data/lib/html/jquery.tmpl.js +0 -486
  66. data/lib/html/list.css +0 -9
  67. data/lib/html/list.js +0 -38
  68. data/lib/html/list.tmpl +0 -34
@@ -1,5 +1,6 @@
1
+ # frozen_string_literal: true
1
2
  module Rack
2
3
  class MiniProfiler
3
- ASSET_VERSION = '355f78011d9b95de14a5b1014b088681'.freeze
4
+ ASSET_VERSION = '8cccf9fed3d62814b1d70f252554501b'
4
5
  end
5
- end
6
+ end
@@ -42,7 +42,7 @@ module Rack
42
42
  def handle_cookie(result)
43
43
  status, headers, _body = result
44
44
 
45
- if (MiniProfiler.config.authorization_mode == :whitelist && !MiniProfiler.request_authorized?)
45
+ if (MiniProfiler.config.authorization_mode == :allow_authorized && !MiniProfiler.request_authorized?)
46
46
  # this is non-obvious, don't kill the profiling cookie on errors or short requests
47
47
  # this ensures that stuff that never reaches the rails stack does not kill profiling
48
48
  if status.to_i >= 200 && status.to_i < 300 && ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - @start) > 0.1)
@@ -59,7 +59,7 @@ module Rack
59
59
 
60
60
  tokens_changed = false
61
61
 
62
- if MiniProfiler.request_authorized? && MiniProfiler.config.authorization_mode == :whitelist
62
+ if MiniProfiler.request_authorized? && MiniProfiler.config.authorization_mode == :allow_authorized
63
63
  @allowed_tokens ||= @store.allowed_tokens
64
64
  tokens_changed = !@orig_auth_tokens || ((@allowed_tokens - @orig_auth_tokens).length > 0)
65
65
  end
@@ -76,6 +76,7 @@ module Rack
76
76
  settings_string = settings.map { |k, v| "#{k}=#{v}" }.join(",")
77
77
  cookie = { value: settings_string, path: '/', httponly: true }
78
78
  cookie[:secure] = true if @request.ssl?
79
+ cookie[:same_site] = 'Lax'
79
80
  Rack::Utils.set_cookie_header!(headers, COOKIE_NAME, cookie)
80
81
  end
81
82
  end
@@ -89,10 +90,17 @@ module Rack
89
90
  def has_valid_cookie?
90
91
  valid_cookie = !@cookie.nil?
91
92
 
92
- if (MiniProfiler.config.authorization_mode == :whitelist)
93
- @allowed_tokens ||= @store.allowed_tokens
93
+ if (MiniProfiler.config.authorization_mode == :allow_authorized) && valid_cookie
94
+ begin
95
+ @allowed_tokens ||= @store.allowed_tokens
96
+ rescue => e
97
+ if MiniProfiler.config.storage_failure != nil
98
+ MiniProfiler.config.storage_failure.call(e)
99
+ end
100
+ end
94
101
 
95
- valid_cookie = (Array === @orig_auth_tokens) &&
102
+ valid_cookie = @allowed_tokens &&
103
+ (Array === @orig_auth_tokens) &&
96
104
  ((@allowed_tokens & @orig_auth_tokens).length > 0)
97
105
  end
98
106
 
@@ -34,9 +34,11 @@ module Rack
34
34
  end
35
35
  end
36
36
  @enabled = true
37
- @disable_env_dump = false
38
37
  @max_sql_param_length = 0 # disable sql parameter collection by default
39
38
  @skip_sql_param_names = /password/ # skips parameters with the name password by default
39
+ @enable_advanced_debugging_tools = false
40
+ @snapshot_every_n_requests = -1
41
+ @snapshots_limit = 1000
40
42
 
41
43
  # ui parameters
42
44
  @autorized = true
@@ -47,9 +49,14 @@ module Rack
47
49
  @show_trivial = false
48
50
  @show_total_sql_count = false
49
51
  @start_hidden = false
50
- @toggle_shortcut = 'Alt+P'
52
+ @toggle_shortcut = 'alt+p'
51
53
  @html_container = 'body'
52
54
  @position = "top-left"
55
+ @snapshot_hidden_custom_fields = []
56
+ @snapshots_transport_destination_url = nil
57
+ @snapshots_transport_auth_key = nil
58
+ @snapshots_redact_sql_queries = true
59
+ @snapshots_transport_gzip_requests = false
53
60
 
54
61
  self
55
62
  }
@@ -57,20 +64,51 @@ module Rack
57
64
 
58
65
  attr_accessor :authorization_mode, :auto_inject, :backtrace_ignores,
59
66
  :backtrace_includes, :backtrace_remove, :backtrace_threshold_ms,
60
- :base_url_path, :disable_caching, :disable_env_dump, :enabled,
67
+ :base_url_path, :disable_caching, :enabled,
61
68
  :flamegraph_sample_rate, :logger, :pre_authorize_cb, :skip_paths,
62
69
  :skip_schema_queries, :storage, :storage_failure, :storage_instance,
63
- :storage_options, :user_provider
64
- attr_accessor :skip_sql_param_names, :suppress_encoding, :max_sql_param_length
70
+ :storage_options, :user_provider, :enable_advanced_debugging_tools,
71
+ :skip_sql_param_names, :suppress_encoding, :max_sql_param_length
65
72
 
66
73
  # ui accessors
67
74
  attr_accessor :collapse_results, :max_traces_to_show, :position,
68
75
  :show_children, :show_controls, :show_trivial, :show_total_sql_count,
69
76
  :start_hidden, :toggle_shortcut, :html_container
70
77
 
78
+ # snapshot related config
79
+ attr_accessor :snapshot_every_n_requests, :snapshots_limit,
80
+ :snapshot_hidden_custom_fields, :snapshots_transport_destination_url,
81
+ :snapshots_transport_auth_key, :snapshots_redact_sql_queries,
82
+ :snapshots_transport_gzip_requests
83
+
71
84
  # Deprecated options
72
85
  attr_accessor :use_existing_jquery
73
86
 
87
+ attr_reader :assets_url
88
+
89
+ # redefined - since the accessor defines it first
90
+ undef :authorization_mode=
91
+ def authorization_mode=(mode)
92
+ if mode == :whitelist
93
+ warn "[DEPRECATION] `:whitelist` authorization mode is deprecated. Please use `:allow_authorized` instead."
94
+
95
+ mode = :allow_authorized
96
+ end
97
+
98
+ warn <<~DEP unless mode == :allow_authorized || mode == :allow_all
99
+ [DEPRECATION] unknown authorization mode #{mode}. Expected `:allow_all` or `:allow_authorized`.
100
+ DEP
101
+
102
+ @authorization_mode = mode
103
+ end
104
+
105
+ def assets_url=(lmbda)
106
+ if defined?(Rack::MiniProfilerRails)
107
+ Rack::MiniProfilerRails.create_engine
108
+ end
109
+ @assets_url = lmbda
110
+ end
111
+
74
112
  def vertical_position
75
113
  position.include?('bottom') ? 'bottom' : 'top'
76
114
  end
@@ -151,7 +151,7 @@ String stats:
151
151
  body << "#{count} : #{string}\n"
152
152
  end
153
153
 
154
- return [200, { 'Content-Type' => 'text/plain' }, body]
154
+ [200, { 'Content-Type' => 'text/plain' }, body]
155
155
  ensure
156
156
  prev_gc_state ? GC.disable : GC.enable
157
157
  end
@@ -1,10 +1,17 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'cgi'
4
+
3
5
  module Rack
4
6
  class MiniProfiler
5
7
  class << self
6
8
 
7
9
  include Rack::MiniProfiler::ProfilingMethods
10
+ attr_accessor :subscribe_sql_active_record
11
+
12
+ def patch_rails?
13
+ !!defined?(Rack::MINI_PROFILER_ENABLE_RAILS_PATCHES)
14
+ end
8
15
 
9
16
  def generate_id
10
17
  rand(36**20).to_s(36)
@@ -33,9 +40,21 @@ module Rack
33
40
 
34
41
  def current=(c)
35
42
  # we use TLS cause we need access to this from sql blocks and code blocks that have no access to env
43
+ Thread.current[:mini_profiler_snapshot_custom_fields] = nil
44
+ Thread.current[:mp_ongoing_snapshot] = nil
36
45
  Thread.current[:mini_profiler_private] = c
37
46
  end
38
47
 
48
+ def add_snapshot_custom_field(key, value)
49
+ thread_var_key = :mini_profiler_snapshot_custom_fields
50
+ Thread.current[thread_var_key] ||= {}
51
+ Thread.current[thread_var_key][key] = value
52
+ end
53
+
54
+ def get_snapshot_custom_fields
55
+ Thread.current[:mini_profiler_snapshot_custom_fields]
56
+ end
57
+
39
58
  # discard existing results, don't track this request
40
59
  def discard_results
41
60
  self.current.discard = true if current
@@ -62,6 +81,32 @@ module Rack
62
81
  Thread.current[:mp_authorized]
63
82
  end
64
83
 
84
+ def advanced_tools_message
85
+ <<~TEXT
86
+ This feature is disabled by default, to enable set the enable_advanced_debugging_tools option to true in Mini Profiler config.
87
+ TEXT
88
+ end
89
+
90
+ def binds_to_params(binds)
91
+ return if binds.nil? || config.max_sql_param_length == 0
92
+ # map ActiveRecord::Relation::QueryAttribute to [name, value]
93
+ params = binds.map { |c| c.kind_of?(Array) ? [c.first, c.last] : [c.name, c.value] }
94
+ if (skip = config.skip_sql_param_names)
95
+ params.map { |(n, v)| n =~ skip ? [n, nil] : [n, v] }
96
+ else
97
+ params
98
+ end
99
+ end
100
+
101
+ def snapshots_transporter?
102
+ !!config.snapshots_transport_destination_url &&
103
+ !!config.snapshots_transport_auth_key
104
+ end
105
+
106
+ def redact_sql_queries?
107
+ Thread.current[:mp_ongoing_snapshot] == true &&
108
+ Rack::MiniProfiler.config.snapshots_redact_sql_queries
109
+ end
65
110
  end
66
111
 
67
112
  #
@@ -71,7 +116,7 @@ module Rack
71
116
  MiniProfiler.config.merge!(config)
72
117
  @config = MiniProfiler.config
73
118
  @app = app
74
- @config.base_url_path << "/" unless @config.base_url_path.end_with? "/"
119
+ @config.base_url_path += "/" unless @config.base_url_path.end_with? "/"
75
120
  unless @config.storage_instance
76
121
  @config.storage_instance = @config.storage.new(@config.storage_options)
77
122
  end
@@ -84,15 +129,24 @@ module Rack
84
129
 
85
130
  def serve_results(env)
86
131
  request = Rack::Request.new(env)
87
- id = request[:id]
88
- page_struct = @storage.load(id)
89
- unless page_struct
132
+ id = request.params['id']
133
+ is_snapshot = request.params['snapshot']
134
+ is_snapshot = [true, "true"].include?(is_snapshot)
135
+ if is_snapshot
136
+ page_struct = @storage.load_snapshot(id)
137
+ else
138
+ page_struct = @storage.load(id)
139
+ end
140
+ if !page_struct && is_snapshot
141
+ id = ERB::Util.html_escape(id)
142
+ return [404, {}, ["Snapshot with id '#{id}' not found"]]
143
+ elsif !page_struct
90
144
  @storage.set_viewed(user(env), id)
91
- id = ERB::Util.html_escape(request['id'])
145
+ id = ERB::Util.html_escape(id)
92
146
  user_info = ERB::Util.html_escape(user(env))
93
147
  return [404, {}, ["Request not found: #{id} - user #{user_info}"]]
94
148
  end
95
- unless page_struct[:has_user_viewed]
149
+ if !page_struct[:has_user_viewed] && !is_snapshot
96
150
  page_struct[:client_timings] = TimerStruct::Client.init_from_form_data(env, page_struct)
97
151
  page_struct[:has_user_viewed] = true
98
152
  @storage.save(page_struct)
@@ -127,11 +181,13 @@ module Rack
127
181
  file_name = path.sub(@config.base_url_path, '')
128
182
 
129
183
  return serve_results(env) if file_name.eql?('results')
184
+ return handle_snapshots_request(env) if file_name.eql?('snapshots')
185
+ return serve_flamegraph(env) if file_name.eql?('flamegraph')
130
186
 
131
187
  resources_env = env.dup
132
188
  resources_env['PATH_INFO'] = file_name
133
189
 
134
- rack_file = Rack::File.new(MiniProfiler.resources_root, 'Cache-Control' => 'max-age:86400')
190
+ rack_file = Rack::File.new(MiniProfiler.resources_root, 'Cache-Control' => "max-age=#{cache_control_value}")
135
191
  rack_file.call(resources_env)
136
192
  end
137
193
 
@@ -147,11 +203,18 @@ module Rack
147
203
  @config
148
204
  end
149
205
 
150
- def call(env)
206
+ def advanced_debugging_enabled?
207
+ config.enable_advanced_debugging_tools
208
+ end
209
+
210
+ def tool_disabled_message(client_settings)
211
+ client_settings.handle_cookie(text_result(Rack::MiniProfiler.advanced_tools_message))
212
+ end
151
213
 
214
+ def call(env)
152
215
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
153
216
  client_settings = ClientSettings.new(env, @storage, start)
154
- MiniProfiler.deauthorize_request if @config.authorization_mode == :whitelist
217
+ MiniProfiler.deauthorize_request if @config.authorization_mode == :allow_authorized
155
218
 
156
219
  status = headers = body = nil
157
220
  query_string = env['QUERY_STRING']
@@ -160,15 +223,31 @@ module Rack
160
223
  # Someone (e.g. Rails engine) could change the SCRIPT_NAME so we save it
161
224
  env['RACK_MINI_PROFILER_ORIGINAL_SCRIPT_NAME'] = ENV['PASSENGER_BASE_URI'] || env['SCRIPT_NAME']
162
225
 
163
- skip_it = (@config.pre_authorize_cb && !@config.pre_authorize_cb.call(env)) ||
164
- (@config.skip_paths && @config.skip_paths.any? { |p| path.start_with?(p) }) ||
165
- query_string =~ /pp=skip/
226
+ skip_it = /pp=skip/.match?(query_string) || (
227
+ @config.skip_paths &&
228
+ @config.skip_paths.any? do |p|
229
+ if p.instance_of?(String)
230
+ path.start_with?(p)
231
+ elsif p.instance_of?(Regexp)
232
+ p.match?(path)
233
+ end
234
+ end
235
+ )
236
+ if skip_it
237
+ return client_settings.handle_cookie(@app.call(env))
238
+ end
239
+
240
+ skip_it = (@config.pre_authorize_cb && !@config.pre_authorize_cb.call(env))
166
241
 
167
242
  if skip_it || (
168
- @config.authorization_mode == :whitelist &&
243
+ @config.authorization_mode == :allow_authorized &&
169
244
  !client_settings.has_valid_cookie?
170
245
  )
171
- return client_settings.handle_cookie(@app.call(env))
246
+ if take_snapshot?(path)
247
+ return client_settings.handle_cookie(take_snapshot(env, start))
248
+ else
249
+ return client_settings.handle_cookie(@app.call(env))
250
+ end
172
251
  end
173
252
 
174
253
  # handle all /mini-profiler requests here
@@ -195,12 +274,23 @@ module Rack
195
274
 
196
275
  # profile gc
197
276
  if query_string =~ /pp=profile-gc/
277
+ return tool_disabled_message(client_settings) if !advanced_debugging_enabled?
198
278
  current.measure = false if current
199
279
  return client_settings.handle_cookie(Rack::MiniProfiler::GCProfiler.new.profile_gc(@app, env))
200
280
  end
201
281
 
202
282
  # profile memory
203
283
  if query_string =~ /pp=profile-memory/
284
+ return tool_disabled_message(client_settings) if !advanced_debugging_enabled?
285
+
286
+ unless defined?(MemoryProfiler) && MemoryProfiler.respond_to?(:report)
287
+ message = "Please install the memory_profiler gem and require it: add gem 'memory_profiler' to your Gemfile"
288
+ _, _, body = @app.call(env)
289
+ body.close if body.respond_to? :close
290
+
291
+ return client_settings.handle_cookie(text_result(message))
292
+ end
293
+
204
294
  query_params = Rack::Utils.parse_nested_query(query_string)
205
295
  options = {
206
296
  ignore_files: query_params['memory_profiler_ignore_files'],
@@ -256,27 +346,40 @@ module Rack
256
346
  # Prevent response body from being compressed
257
347
  env['HTTP_ACCEPT_ENCODING'] = 'identity' if config.suppress_encoding
258
348
 
259
- if query_string =~ /pp=flamegraph/
260
- unless defined?(Flamegraph) && Flamegraph.respond_to?(:generate)
261
-
262
- flamegraph = "Please install the flamegraph gem and require it: add gem 'flamegraph' to your Gemfile"
263
- status, headers, body = @app.call(env)
349
+ if query_string =~ /pp=(async-)?flamegraph/ || env['HTTP_REFERER'] =~ /pp=async-flamegraph/
350
+ unless defined?(StackProf) && StackProf.respond_to?(:run)
351
+ headers = { 'Content-Type' => 'text/html' }
352
+ message = "Please install the stackprof gem and require it: add gem 'stackprof' to your Gemfile"
353
+ body.close if body.respond_to? :close
354
+ return client_settings.handle_cookie([500, headers, message])
264
355
  else
265
356
  # do not sully our profile with mini profiler timings
266
357
  current.measure = false
267
358
  match_data = query_string.match(/flamegraph_sample_rate=([\d\.]+)/)
268
359
 
269
- mode = query_string =~ /mode=c/ ? :c : :ruby
270
-
271
360
  if match_data && !match_data[1].to_f.zero?
272
361
  sample_rate = match_data[1].to_f
273
362
  else
274
363
  sample_rate = config.flamegraph_sample_rate
275
364
  end
276
- flamegraph = Flamegraph.generate(nil, fidelity: sample_rate, embed_resources: query_string =~ /embed/, mode: mode) do
365
+ flamegraph = StackProf.run(
366
+ mode: :wall,
367
+ raw: true,
368
+ aggregate: false,
369
+ interval: (sample_rate * 1000).to_i
370
+ ) do
277
371
  status, headers, body = @app.call(env)
278
372
  end
279
373
  end
374
+ elsif path == '/rack-mini-profiler/requests'
375
+ blank_page_html = <<~HTML
376
+ <html>
377
+ <head></head>
378
+ <body></body>
379
+ </html>
380
+ HTML
381
+
382
+ status, headers, body = [200, { 'Content-Type' => 'text/html' }, [blank_page_html.dup]]
280
383
  else
281
384
  status, headers, body = @app.call(env)
282
385
  end
@@ -287,7 +390,7 @@ module Rack
287
390
 
288
391
  skip_it = current.discard
289
392
 
290
- if (config.authorization_mode == :whitelist && !MiniProfiler.request_authorized?)
393
+ if (config.authorization_mode == :allow_authorized && !MiniProfiler.request_authorized?)
291
394
  skip_it = true
292
395
  end
293
396
 
@@ -307,12 +410,14 @@ module Rack
307
410
  return client_settings.handle_cookie(dump_exceptions exceptions)
308
411
  end
309
412
 
310
- if query_string =~ /pp=env/ && !config.disable_env_dump
413
+ if query_string =~ /pp=env/
414
+ return tool_disabled_message(client_settings) if !advanced_debugging_enabled?
311
415
  body.close if body.respond_to? :close
312
416
  return client_settings.handle_cookie(dump_env env)
313
417
  end
314
418
 
315
419
  if query_string =~ /pp=analyze-memory/
420
+ return tool_disabled_message(client_settings) if !advanced_debugging_enabled?
316
421
  body.close if body.respond_to? :close
317
422
  return client_settings.handle_cookie(analyze_memory)
318
423
  end
@@ -326,9 +431,12 @@ module Rack
326
431
  page_struct[:user] = user(env)
327
432
  page_struct[:root].record_time((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000)
328
433
 
329
- if flamegraph
434
+ if flamegraph && query_string =~ /pp=flamegraph/
330
435
  body.close if body.respond_to? :close
331
- return client_settings.handle_cookie(self.flamegraph(flamegraph))
436
+ return client_settings.handle_cookie(self.flamegraph(flamegraph, path))
437
+ elsif flamegraph # async-flamegraph
438
+ page_struct[:has_flamegraph] = true
439
+ page_struct[:flamegraph] = flamegraph
332
440
  end
333
441
 
334
442
  begin
@@ -369,7 +477,7 @@ module Rack
369
477
 
370
478
  # inject header
371
479
  if headers.is_a? Hash
372
- headers['X-MiniProfiler-Ids'] = ids_json(env)
480
+ headers['X-MiniProfiler-Ids'] = ids_comma_separated(env)
373
481
  end
374
482
 
375
483
  if current.inject_js && content_type =~ /text\/html/
@@ -397,7 +505,13 @@ module Rack
397
505
  if script.respond_to?(:encoding) && script.respond_to?(:force_encoding)
398
506
  script = script.force_encoding(fragment.encoding)
399
507
  end
400
- fragment.insert(index, script)
508
+
509
+ safe_script = script
510
+ if script.respond_to?(:html_safe)
511
+ safe_script = script.html_safe
512
+ end
513
+
514
+ fragment.insert(index, safe_script)
401
515
  else
402
516
  fragment
403
517
  end
@@ -522,7 +636,7 @@ module Rack
522
636
 
523
637
  def make_link(postfix, env)
524
638
  link = env["PATH_INFO"] + "?" + env["QUERY_STRING"].sub("pp=help", "pp=#{postfix}")
525
- "pp=<a href='#{link}'>#{postfix}</a>"
639
+ "pp=<a href='#{ERB::Util.html_escape(link)}'>#{postfix}</a>"
526
640
  end
527
641
 
528
642
  def help(client_settings, env)
@@ -539,13 +653,14 @@ Append the following to your query string:
539
653
  #{make_link "full-backtrace", env} #{"(*) " if client_settings.backtrace_full?}: enable full backtraces for SQL executed (use pp=normal-backtrace to disable)
540
654
  #{make_link "disable", env} : disable profiling for this session
541
655
  #{make_link "enable", env} : enable profiling for this session (if previously disabled)
542
- #{make_link "profile-gc", env} : perform gc profiling on this request, analyzes ObjectSpace generated by request (ruby 1.9.3 only)
656
+ #{make_link "profile-gc", env} : perform gc profiling on this request, analyzes ObjectSpace generated by request
543
657
  #{make_link "profile-memory", env} : requires the memory_profiler gem, new location based report
544
- #{make_link "flamegraph", env} : works best on Ruby 2.0, a graph representing sampled activity (requires the flamegraph gem).
658
+ #{make_link "flamegraph", env} : a graph representing sampled activity (requires the stackprof gem).
659
+ #{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).
545
660
  #{make_link "flamegraph&flamegraph_sample_rate=1", env}: creates a flamegraph with the specified sample rate (in ms). Overrides value set in config
546
- #{make_link "flamegraph_embed", env} : works best on Ruby 2.0, a graph representing sampled activity (requires the flamegraph gem), embedded resources for use on an intranet.
547
- #{make_link "trace-exceptions", env} : requires Ruby 2.0, will return all the spots where your application raises exceptions
548
- #{make_link "analyze-memory", env} : requires Ruby 2.0, will perform basic memory analysis of heap
661
+ #{make_link "flamegraph_embed", env} : a graph representing sampled activity (requires the stackprof gem), embedded resources for use on an intranet.
662
+ #{make_link "trace-exceptions", env} : will return all the spots where your application raises exceptions
663
+ #{make_link "analyze-memory", env} : will perform basic memory analysis of heap
549
664
  </pre>
550
665
  </body>
551
666
  </html>
@@ -554,9 +669,33 @@ Append the following to your query string:
554
669
  [200, headers, [body]]
555
670
  end
556
671
 
557
- def flamegraph(graph)
672
+ def flamegraph(graph, path)
558
673
  headers = { 'Content-Type' => 'text/html' }
559
- [200, headers, [graph]]
674
+ html = <<~HTML
675
+ <!DOCTYPE html>
676
+ <html>
677
+ <head>
678
+ <style>
679
+ body { margin: 0; height: 100vh; }
680
+ #speedscope-iframe { width: 100%; height: 100%; border: none; }
681
+ </style>
682
+ </head>
683
+ <body>
684
+ <script type="text/javascript">
685
+ var graph = #{JSON.generate(graph)};
686
+ var json = JSON.stringify(graph);
687
+ var blob = new Blob([json], { type: 'text/plain' });
688
+ var objUrl = encodeURIComponent(URL.createObjectURL(blob));
689
+ var iframe = document.createElement('IFRAME');
690
+ iframe.setAttribute('id', 'speedscope-iframe');
691
+ document.body.appendChild(iframe);
692
+ var iframeUrl = '#{@config.base_url_path}speedscope/index.html#profileURL=' + objUrl + '&title=' + 'Flamegraph for #{CGI.escape(path)}';
693
+ iframe.setAttribute('src', iframeUrl);
694
+ </script>
695
+ </body>
696
+ </html>
697
+ HTML
698
+ [200, headers, [html]]
560
699
  end
561
700
 
562
701
  def ids(env)
@@ -568,10 +707,6 @@ Append the following to your query string:
568
707
  all
569
708
  end
570
709
 
571
- def ids_json(env)
572
- ::JSON.generate(ids(env))
573
- end
574
-
575
710
  def ids_comma_separated(env)
576
711
  ids(env).join(",")
577
712
  end
@@ -584,10 +719,20 @@ Append the following to your query string:
584
719
  # * you do not want script to be automatically appended for the current page. You can also call cancel_auto_inject
585
720
  def get_profile_script(env)
586
721
  path = "#{env['RACK_MINI_PROFILER_ORIGINAL_SCRIPT_NAME']}#{@config.base_url_path}"
722
+ version = MiniProfiler::ASSET_VERSION
723
+ if @config.assets_url
724
+ url = @config.assets_url.call('rack-mini-profiler.js', version, env)
725
+ css_url = @config.assets_url.call('rack-mini-profiler.css', version, env)
726
+ end
727
+
728
+ url = "#{path}includes.js?v=#{version}" if !url
729
+ css_url = "#{path}includes.css?v=#{version}" if !css_url
587
730
 
588
731
  settings = {
589
732
  path: path,
590
- version: MiniProfiler::ASSET_VERSION,
733
+ url: url,
734
+ cssUrl: css_url,
735
+ version: version,
591
736
  verticalPosition: @config.vertical_position,
592
737
  horizontalPosition: @config.horizontal_position,
593
738
  showTrivial: @config.show_trivial,
@@ -599,7 +744,8 @@ Append the following to your query string:
599
744
  toggleShortcut: @config.toggle_shortcut,
600
745
  startHidden: @config.start_hidden,
601
746
  collapseResults: @config.collapse_results,
602
- htmlContainer: @config.html_container
747
+ htmlContainer: @config.html_container,
748
+ hiddenCustomFields: @config.snapshot_hidden_custom_fields.join(',')
603
749
  }
604
750
 
605
751
  if current && current.page_struct
@@ -627,5 +773,127 @@ Append the following to your query string:
627
773
  current.inject_js = false
628
774
  end
629
775
 
776
+ def cache_control_value
777
+ 86400
778
+ end
779
+
780
+ private
781
+
782
+ def handle_snapshots_request(env)
783
+ self.current = nil
784
+ MiniProfiler.authorize_request
785
+ status = 200
786
+ headers = { 'Content-Type' => 'text/html' }
787
+ qp = Rack::Utils.parse_nested_query(env['QUERY_STRING'])
788
+ if group_name = qp["group_name"]
789
+ list = @storage.find_snapshots_group(group_name)
790
+ list.each do |snapshot|
791
+ snapshot[:url] = url_for_snapshot(snapshot[:id])
792
+ end
793
+ data = {
794
+ group_name: group_name,
795
+ list: list
796
+ }
797
+ else
798
+ list = @storage.snapshot_groups_overview
799
+ list.each do |group|
800
+ group[:url] = url_for_snapshots_group(group[:name])
801
+ end
802
+ data = {
803
+ page: "overview",
804
+ list: list
805
+ }
806
+ end
807
+ data_html = <<~HTML
808
+ <div style="display: none;" id="snapshots-data">
809
+ #{data.to_json}
810
+ </div>
811
+ HTML
812
+ response = Rack::Response.new([], status, headers)
813
+
814
+ response.write <<~HTML
815
+ <html>
816
+ <head></head>
817
+ <body class="mp-snapshots">
818
+ HTML
819
+ response.write(data_html)
820
+ script = self.get_profile_script(env)
821
+ response.write(script)
822
+ response.write <<~HTML
823
+ </body>
824
+ </html>
825
+ HTML
826
+ response.finish
827
+ end
828
+
829
+ def serve_flamegraph(env)
830
+ request = Rack::Request.new(env)
831
+ id = request.params['id']
832
+ page_struct = @storage.load(id)
833
+
834
+ if !page_struct
835
+ id = ERB::Util.html_escape(id)
836
+ user_info = ERB::Util.html_escape(user(env))
837
+ return [404, {}, ["Request not found: #{id} - user #{user_info}"]]
838
+ end
839
+
840
+ if !page_struct[:flamegraph]
841
+ return [404, {}, ["No flamegraph available for #{ERB::Util.html_escape(id)}"]]
842
+ end
843
+
844
+ self.flamegraph(page_struct[:flamegraph], page_struct[:request_path])
845
+ end
846
+
847
+ def rails_route_from_path(path, method)
848
+ if defined?(Rails) && defined?(ActionController::RoutingError)
849
+ hash = Rails.application.routes.recognize_path(path, method: method)
850
+ if hash && hash[:controller] && hash[:action]
851
+ "#{method} #{hash[:controller]}##{hash[:action]}"
852
+ end
853
+ end
854
+ rescue ActionController::RoutingError
855
+ nil
856
+ end
857
+
858
+ def url_for_snapshots_group(group_name)
859
+ qs = Rack::Utils.build_query({ group_name: group_name })
860
+ "/#{@config.base_url_path.gsub('/', '')}/snapshots?#{qs}"
861
+ end
862
+
863
+ def url_for_snapshot(id)
864
+ qs = Rack::Utils.build_query({ id: id, snapshot: true })
865
+ "/#{@config.base_url_path.gsub('/', '')}/results?#{qs}"
866
+ end
867
+
868
+ def take_snapshot?(path)
869
+ @config.snapshot_every_n_requests > 0 &&
870
+ !path.start_with?(@config.base_url_path) &&
871
+ @storage.should_take_snapshot?(@config.snapshot_every_n_requests)
872
+ end
873
+
874
+ def take_snapshot(env, start)
875
+ MiniProfiler.create_current(env, @config)
876
+ Thread.current[:mp_ongoing_snapshot] = true
877
+ results = @app.call(env)
878
+ status = results[0].to_i
879
+ if status >= 200 && status < 300
880
+ page_struct = current.page_struct
881
+ page_struct[:root].record_time(
882
+ (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000
883
+ )
884
+ custom_fields = MiniProfiler.get_snapshot_custom_fields
885
+ page_struct[:custom_fields] = custom_fields if custom_fields
886
+ if Rack::MiniProfiler.snapshots_transporter?
887
+ Rack::MiniProfiler::SnapshotsTransporter.transport(page_struct)
888
+ else
889
+ @storage.push_snapshot(
890
+ page_struct,
891
+ @config
892
+ )
893
+ end
894
+ end
895
+ self.current = nil
896
+ results
897
+ end
630
898
  end
631
899
  end