rack-mini-profiler 1.0.2 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +138 -21
  3. data/README.md +201 -94
  4. data/lib/enable_rails_patches.rb +5 -0
  5. data/lib/generators/rack_mini_profiler/USAGE +9 -0
  6. data/lib/generators/rack_mini_profiler/install_generator.rb +13 -0
  7. data/lib/generators/{rack_profiler/templates/rack_profiler.rb → rack_mini_profiler/templates/rack_mini_profiler.rb} +1 -1
  8. data/lib/generators/rack_profiler/install_generator.rb +6 -3
  9. data/lib/html/dot.1.1.2.min.js +2 -0
  10. data/lib/html/includes.css +144 -45
  11. data/lib/html/includes.js +1423 -1009
  12. data/lib/html/includes.scss +538 -441
  13. data/lib/html/includes.tmpl +231 -148
  14. data/lib/html/pretty-print.js +810 -0
  15. data/lib/html/profile_handler.js +1 -1
  16. data/lib/html/rack-mini-profiler.css +3 -0
  17. data/lib/html/rack-mini-profiler.js +2 -0
  18. data/lib/html/share.html +0 -1
  19. data/lib/html/speedscope/LICENSE +21 -0
  20. data/lib/html/speedscope/README.md +3 -0
  21. data/lib/html/speedscope/demangle-cpp.1768f4cc.js +4 -0
  22. data/lib/html/speedscope/favicon-16x16.f74b3187.png +0 -0
  23. data/lib/html/speedscope/favicon-32x32.bc503437.png +0 -0
  24. data/lib/html/speedscope/file-format-schema.json +324 -0
  25. data/lib/html/speedscope/fonts/source-code-pro-regular.css +8 -0
  26. data/lib/html/speedscope/fonts/source-code-pro-v13-regular.woff +0 -0
  27. data/lib/html/speedscope/fonts/source-code-pro-v13-regular.woff2 +0 -0
  28. data/lib/html/speedscope/import.cf0fa83f.js +115 -0
  29. data/lib/html/speedscope/index.html +2 -0
  30. data/lib/html/speedscope/release.txt +3 -0
  31. data/lib/html/speedscope/reset.8c46b7a1.css +2 -0
  32. data/lib/html/speedscope/source-map.438fa06b.js +24 -0
  33. data/lib/html/speedscope/speedscope.44364064.js +200 -0
  34. data/lib/html/vendor.js +848 -0
  35. data/lib/mini_profiler/asset_version.rb +3 -2
  36. data/lib/mini_profiler/client_settings.rb +15 -7
  37. data/lib/mini_profiler/config.rb +51 -5
  38. data/lib/mini_profiler/gc_profiler.rb +1 -1
  39. data/lib/mini_profiler/profiling_methods.rb +13 -8
  40. data/lib/mini_profiler/snapshots_transporter.rb +109 -0
  41. data/lib/mini_profiler/storage/abstract_store.rb +52 -1
  42. data/lib/mini_profiler/storage/file_store.rb +7 -3
  43. data/lib/mini_profiler/storage/memcache_store.rb +13 -7
  44. data/lib/mini_profiler/storage/memory_store.rb +98 -5
  45. data/lib/mini_profiler/storage/redis_store.rb +226 -3
  46. data/lib/mini_profiler/storage.rb +7 -0
  47. data/lib/mini_profiler/timer_struct/base.rb +2 -0
  48. data/lib/mini_profiler/timer_struct/custom.rb +1 -0
  49. data/lib/mini_profiler/timer_struct/page.rb +60 -4
  50. data/lib/mini_profiler/timer_struct/request.rb +53 -11
  51. data/lib/mini_profiler/timer_struct/sql.rb +6 -2
  52. data/lib/mini_profiler/timer_struct.rb +8 -0
  53. data/lib/mini_profiler/version.rb +2 -1
  54. data/lib/{mini_profiler/profiler.rb → mini_profiler.rb} +394 -82
  55. data/lib/mini_profiler_rails/railtie.rb +88 -7
  56. data/lib/mini_profiler_rails/railtie_methods.rb +61 -0
  57. data/lib/patches/db/activerecord.rb +1 -12
  58. data/lib/patches/db/mongo.rb +1 -1
  59. data/lib/patches/db/moped.rb +1 -1
  60. data/lib/patches/db/mysql2/alias_method.rb +30 -0
  61. data/lib/patches/db/mysql2/prepend.rb +34 -0
  62. data/lib/patches/db/mysql2.rb +4 -27
  63. data/lib/patches/db/plucky.rb +4 -4
  64. data/lib/patches/db/riak.rb +1 -1
  65. data/lib/patches/net_patches.rb +21 -10
  66. data/lib/patches/sql_patches.rb +13 -5
  67. data/lib/prepend_mysql2_patch.rb +5 -0
  68. data/lib/prepend_net_http_patch.rb +5 -0
  69. data/lib/rack-mini-profiler.rb +1 -24
  70. data/rack-mini-profiler.gemspec +17 -8
  71. metadata +156 -32
  72. data/lib/html/jquery.1.7.1.js +0 -4
  73. data/lib/html/jquery.tmpl.js +0 -486
  74. data/lib/html/list.css +0 -9
  75. data/lib/html/list.js +0 -38
  76. data/lib/html/list.tmpl +0 -34
@@ -1,10 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'cgi'
4
+ require 'json'
5
+ require 'erb'
6
+
7
+ require 'mini_profiler/timer_struct'
8
+ require 'mini_profiler/storage'
9
+ require 'mini_profiler/config'
10
+ require 'mini_profiler/profiling_methods'
11
+ require 'mini_profiler/context'
12
+ require 'mini_profiler/client_settings'
13
+ require 'mini_profiler/gc_profiler'
14
+ require 'mini_profiler/snapshots_transporter'
15
+
3
16
  module Rack
4
17
  class MiniProfiler
5
18
  class << self
6
19
 
7
20
  include Rack::MiniProfiler::ProfilingMethods
21
+ attr_accessor :subscribe_sql_active_record
22
+
23
+ def patch_rails?
24
+ !!defined?(Rack::MINI_PROFILER_ENABLE_RAILS_PATCHES)
25
+ end
8
26
 
9
27
  def generate_id
10
28
  rand(36**20).to_s(36)
@@ -20,11 +38,11 @@ module Rack
20
38
  end
21
39
 
22
40
  def resources_root
23
- @resources_root ||= ::File.expand_path("../../html", __FILE__)
41
+ @resources_root ||= ::File.expand_path("../html", __FILE__)
24
42
  end
25
43
 
26
44
  def share_template
27
- @share_template ||= ERB.new(::File.read(::File.expand_path("../html/share.html", ::File.dirname(__FILE__))))
45
+ @share_template ||= ERB.new(::File.read(::File.expand_path("html/share.html", ::File.dirname(__FILE__))))
28
46
  end
29
47
 
30
48
  def current
@@ -33,9 +51,21 @@ module Rack
33
51
 
34
52
  def current=(c)
35
53
  # we use TLS cause we need access to this from sql blocks and code blocks that have no access to env
54
+ Thread.current[:mini_profiler_snapshot_custom_fields] = nil
55
+ Thread.current[:mp_ongoing_snapshot] = nil
36
56
  Thread.current[:mini_profiler_private] = c
37
57
  end
38
58
 
59
+ def add_snapshot_custom_field(key, value)
60
+ thread_var_key = :mini_profiler_snapshot_custom_fields
61
+ Thread.current[thread_var_key] ||= {}
62
+ Thread.current[thread_var_key][key] = value
63
+ end
64
+
65
+ def get_snapshot_custom_fields
66
+ Thread.current[:mini_profiler_snapshot_custom_fields]
67
+ end
68
+
39
69
  # discard existing results, don't track this request
40
70
  def discard_results
41
71
  self.current.discard = true if current
@@ -62,6 +92,32 @@ module Rack
62
92
  Thread.current[:mp_authorized]
63
93
  end
64
94
 
95
+ def advanced_tools_message
96
+ <<~TEXT
97
+ This feature is disabled by default, to enable set the enable_advanced_debugging_tools option to true in Mini Profiler config.
98
+ TEXT
99
+ end
100
+
101
+ def binds_to_params(binds)
102
+ return if binds.nil? || config.max_sql_param_length == 0
103
+ # map ActiveRecord::Relation::QueryAttribute to [name, value]
104
+ params = binds.map { |c| c.kind_of?(Array) ? [c.first, c.last] : [c.name, c.value] }
105
+ if (skip = config.skip_sql_param_names)
106
+ params.map { |(n, v)| n =~ skip ? [n, nil] : [n, v] }
107
+ else
108
+ params
109
+ end
110
+ end
111
+
112
+ def snapshots_transporter?
113
+ !!config.snapshots_transport_destination_url &&
114
+ !!config.snapshots_transport_auth_key
115
+ end
116
+
117
+ def redact_sql_queries?
118
+ Thread.current[:mp_ongoing_snapshot] == true &&
119
+ Rack::MiniProfiler.config.snapshots_redact_sql_queries
120
+ end
65
121
  end
66
122
 
67
123
  #
@@ -71,7 +127,7 @@ module Rack
71
127
  MiniProfiler.config.merge!(config)
72
128
  @config = MiniProfiler.config
73
129
  @app = app
74
- @config.base_url_path << "/" unless @config.base_url_path.end_with? "/"
130
+ @config.base_url_path += "/" unless @config.base_url_path.end_with? "/"
75
131
  unless @config.storage_instance
76
132
  @config.storage_instance = @config.storage.new(@config.storage_options)
77
133
  end
@@ -84,15 +140,24 @@ module Rack
84
140
 
85
141
  def serve_results(env)
86
142
  request = Rack::Request.new(env)
87
- id = request[:id]
88
- page_struct = @storage.load(id)
89
- unless page_struct
143
+ id = request.params['id']
144
+ group_name = request.params['group']
145
+ is_snapshot = group_name && group_name.size > 0
146
+ if is_snapshot
147
+ page_struct = @storage.load_snapshot(id, group_name)
148
+ else
149
+ page_struct = @storage.load(id)
150
+ end
151
+ if !page_struct && is_snapshot
152
+ id = ERB::Util.html_escape(id)
153
+ return [404, {}, ["Snapshot with id '#{id}' not found"]]
154
+ elsif !page_struct
90
155
  @storage.set_viewed(user(env), id)
91
- id = ERB::Util.html_escape(request['id'])
156
+ id = ERB::Util.html_escape(id)
92
157
  user_info = ERB::Util.html_escape(user(env))
93
158
  return [404, {}, ["Request not found: #{id} - user #{user_info}"]]
94
159
  end
95
- unless page_struct[:has_user_viewed]
160
+ if !page_struct[:has_user_viewed] && !is_snapshot
96
161
  page_struct[:client_timings] = TimerStruct::Client.init_from_form_data(env, page_struct)
97
162
  page_struct[:has_user_viewed] = true
98
163
  @storage.save(page_struct)
@@ -127,11 +192,13 @@ module Rack
127
192
  file_name = path.sub(@config.base_url_path, '')
128
193
 
129
194
  return serve_results(env) if file_name.eql?('results')
195
+ return handle_snapshots_request(env) if file_name.eql?('snapshots')
196
+ return serve_flamegraph(env) if file_name.eql?('flamegraph')
130
197
 
131
198
  resources_env = env.dup
132
199
  resources_env['PATH_INFO'] = file_name
133
200
 
134
- rack_file = Rack::File.new(MiniProfiler.resources_root, 'Cache-Control' => 'max-age:86400')
201
+ rack_file = Rack::File.new(MiniProfiler.resources_root, 'Cache-Control' => "max-age=#{cache_control_value}")
135
202
  rack_file.call(resources_env)
136
203
  end
137
204
 
@@ -147,11 +214,18 @@ module Rack
147
214
  @config
148
215
  end
149
216
 
150
- def call(env)
217
+ def advanced_debugging_enabled?
218
+ config.enable_advanced_debugging_tools
219
+ end
220
+
221
+ def tool_disabled_message(client_settings)
222
+ client_settings.handle_cookie(text_result(Rack::MiniProfiler.advanced_tools_message))
223
+ end
151
224
 
225
+ def call(env)
152
226
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
153
227
  client_settings = ClientSettings.new(env, @storage, start)
154
- MiniProfiler.deauthorize_request if @config.authorization_mode == :whitelist
228
+ MiniProfiler.deauthorize_request if @config.authorization_mode == :allow_authorized
155
229
 
156
230
  status = headers = body = nil
157
231
  query_string = env['QUERY_STRING']
@@ -160,15 +234,31 @@ module Rack
160
234
  # Someone (e.g. Rails engine) could change the SCRIPT_NAME so we save it
161
235
  env['RACK_MINI_PROFILER_ORIGINAL_SCRIPT_NAME'] = ENV['PASSENGER_BASE_URI'] || env['SCRIPT_NAME']
162
236
 
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/
237
+ skip_it = /#{@config.profile_parameter}=skip/.match?(query_string) || (
238
+ @config.skip_paths &&
239
+ @config.skip_paths.any? do |p|
240
+ if p.instance_of?(String)
241
+ path.start_with?(p)
242
+ elsif p.instance_of?(Regexp)
243
+ p.match?(path)
244
+ end
245
+ end
246
+ )
247
+ if skip_it
248
+ return client_settings.handle_cookie(@app.call(env))
249
+ end
250
+
251
+ skip_it = (@config.pre_authorize_cb && !@config.pre_authorize_cb.call(env))
166
252
 
167
253
  if skip_it || (
168
- @config.authorization_mode == :whitelist &&
254
+ @config.authorization_mode == :allow_authorized &&
169
255
  !client_settings.has_valid_cookie?
170
256
  )
171
- return client_settings.handle_cookie(@app.call(env))
257
+ if take_snapshot?(path)
258
+ return client_settings.handle_cookie(take_snapshot(env, start))
259
+ else
260
+ return client_settings.handle_cookie(@app.call(env))
261
+ end
172
262
  end
173
263
 
174
264
  # handle all /mini-profiler requests here
@@ -176,11 +266,11 @@ module Rack
176
266
 
177
267
  has_disable_cookie = client_settings.disable_profiling?
178
268
  # manual session disable / enable
179
- if query_string =~ /pp=disable/ || has_disable_cookie
269
+ if query_string =~ /#{@config.profile_parameter}=disable/ || has_disable_cookie
180
270
  skip_it = true
181
271
  end
182
272
 
183
- if query_string =~ /pp=enable/
273
+ if query_string =~ /#{@config.profile_parameter}=enable/
184
274
  skip_it = false
185
275
  config.enabled = true
186
276
  end
@@ -194,13 +284,26 @@ module Rack
194
284
  end
195
285
 
196
286
  # profile gc
197
- if query_string =~ /pp=profile-gc/
287
+ if query_string =~ /#{@config.profile_parameter}=profile-gc/
288
+ return tool_disabled_message(client_settings) if !advanced_debugging_enabled?
198
289
  current.measure = false if current
199
290
  return client_settings.handle_cookie(Rack::MiniProfiler::GCProfiler.new.profile_gc(@app, env))
200
291
  end
201
292
 
202
293
  # profile memory
203
- if query_string =~ /pp=profile-memory/
294
+ if query_string =~ /#{@config.profile_parameter}=profile-memory/
295
+ return tool_disabled_message(client_settings) if !advanced_debugging_enabled?
296
+
297
+ unless defined?(MemoryProfiler) && MemoryProfiler.respond_to?(:report)
298
+ message = "Please install the memory_profiler gem and require it: add gem 'memory_profiler' to your Gemfile"
299
+ status, headers, body = @app.call(env)
300
+ body.close if body.respond_to? :close
301
+
302
+ return client_settings.handle_cookie(
303
+ text_result(message, status: 500, headers: headers)
304
+ )
305
+ end
306
+
204
307
  query_params = Rack::Utils.parse_nested_query(query_string)
205
308
  options = {
206
309
  ignore_files: query_params['memory_profiler_ignore_files'],
@@ -218,12 +321,12 @@ module Rack
218
321
 
219
322
  MiniProfiler.create_current(env, @config)
220
323
 
221
- if query_string =~ /pp=normal-backtrace/
324
+ if query_string =~ /#{@config.profile_parameter}=normal-backtrace/
222
325
  client_settings.backtrace_level = ClientSettings::BACKTRACE_DEFAULT
223
- elsif query_string =~ /pp=no-backtrace/
326
+ elsif query_string =~ /#{@config.profile_parameter}=no-backtrace/
224
327
  current.skip_backtrace = true
225
328
  client_settings.backtrace_level = ClientSettings::BACKTRACE_NONE
226
- elsif query_string =~ /pp=full-backtrace/ || client_settings.backtrace_full?
329
+ elsif query_string =~ /#{@config.profile_parameter}=full-backtrace/ || client_settings.backtrace_full?
227
330
  current.full_backtrace = true
228
331
  client_settings.backtrace_level = ClientSettings::BACKTRACE_FULL
229
332
  elsif client_settings.backtrace_none?
@@ -232,7 +335,7 @@ module Rack
232
335
 
233
336
  flamegraph = nil
234
337
 
235
- trace_exceptions = query_string =~ /pp=trace-exceptions/ && defined? TracePoint
338
+ trace_exceptions = query_string =~ /#{@config.profile_parameter}=trace-exceptions/ && defined? TracePoint
236
339
  status, headers, body, exceptions, trace = nil
237
340
 
238
341
  if trace_exceptions
@@ -256,27 +359,56 @@ module Rack
256
359
  # Prevent response body from being compressed
257
360
  env['HTTP_ACCEPT_ENCODING'] = 'identity' if config.suppress_encoding
258
361
 
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)
264
- else
362
+ if query_string =~ /pp=(async-)?flamegraph/ || env['HTTP_REFERER'] =~ /pp=async-flamegraph/
363
+ if defined?(StackProf) && StackProf.respond_to?(:run)
265
364
  # do not sully our profile with mini profiler timings
266
365
  current.measure = false
267
366
  match_data = query_string.match(/flamegraph_sample_rate=([\d\.]+)/)
268
367
 
269
- mode = query_string =~ /mode=c/ ? :c : :ruby
270
-
271
368
  if match_data && !match_data[1].to_f.zero?
272
369
  sample_rate = match_data[1].to_f
273
370
  else
274
371
  sample_rate = config.flamegraph_sample_rate
275
372
  end
276
- flamegraph = Flamegraph.generate(nil, fidelity: sample_rate, embed_resources: query_string =~ /embed/, mode: mode) do
373
+
374
+ mode_match_data = query_string.match(/flamegraph_mode=([a-zA-Z]+)/)
375
+
376
+ if mode_match_data && [:cpu, :wall, :object, :custom].include?(mode_match_data[1].to_sym)
377
+ mode = mode_match_data[1].to_sym
378
+ else
379
+ mode = config.flamegraph_mode
380
+ end
381
+
382
+ flamegraph = StackProf.run(
383
+ mode: mode,
384
+ raw: true,
385
+ aggregate: false,
386
+ interval: (sample_rate * 1000).to_i
387
+ ) do
277
388
  status, headers, body = @app.call(env)
278
389
  end
390
+ else
391
+ message = "Please install the stackprof gem and require it: add gem 'stackprof' to your Gemfile"
392
+ status, headers, body = @app.call(env)
393
+ body.close if body.respond_to? :close
394
+
395
+ return client_settings.handle_cookie(
396
+ text_result(message, status: status, headers: headers)
397
+ )
279
398
  end
399
+ elsif path == '/rack-mini-profiler/requests'
400
+ blank_page_html = <<~HTML
401
+ <!DOCTYPE html>
402
+ <html>
403
+ <head>
404
+ <title>Rack::MiniProfiler Requests</title>
405
+ </head>
406
+ <body>
407
+ </body>
408
+ </html>
409
+ HTML
410
+
411
+ status, headers, body = [200, { 'Content-Type' => 'text/html' }, [blank_page_html.dup]]
280
412
  else
281
413
  status, headers, body = @app.call(env)
282
414
  end
@@ -287,7 +419,7 @@ module Rack
287
419
 
288
420
  skip_it = current.discard
289
421
 
290
- if (config.authorization_mode == :whitelist && !MiniProfiler.request_authorized?)
422
+ if (config.authorization_mode == :allow_authorized && !MiniProfiler.request_authorized?)
291
423
  skip_it = true
292
424
  end
293
425
 
@@ -307,17 +439,19 @@ module Rack
307
439
  return client_settings.handle_cookie(dump_exceptions exceptions)
308
440
  end
309
441
 
310
- if query_string =~ /pp=env/ && !config.disable_env_dump
442
+ if query_string =~ /#{@config.profile_parameter}=env/
443
+ return tool_disabled_message(client_settings) if !advanced_debugging_enabled?
311
444
  body.close if body.respond_to? :close
312
445
  return client_settings.handle_cookie(dump_env env)
313
446
  end
314
447
 
315
- if query_string =~ /pp=analyze-memory/
448
+ if query_string =~ /#{@config.profile_parameter}=analyze-memory/
449
+ return tool_disabled_message(client_settings) if !advanced_debugging_enabled?
316
450
  body.close if body.respond_to? :close
317
451
  return client_settings.handle_cookie(analyze_memory)
318
452
  end
319
453
 
320
- if query_string =~ /pp=help/
454
+ if query_string =~ /#{@config.profile_parameter}=help/
321
455
  body.close if body.respond_to? :close
322
456
  return client_settings.handle_cookie(help(client_settings, env))
323
457
  end
@@ -326,9 +460,12 @@ module Rack
326
460
  page_struct[:user] = user(env)
327
461
  page_struct[:root].record_time((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000)
328
462
 
329
- if flamegraph
463
+ if flamegraph && query_string =~ /#{@config.profile_parameter}=flamegraph/
330
464
  body.close if body.respond_to? :close
331
- return client_settings.handle_cookie(self.flamegraph(flamegraph))
465
+ return client_settings.handle_cookie(self.flamegraph(flamegraph, path))
466
+ elsif flamegraph # async-flamegraph
467
+ page_struct[:has_flamegraph] = true
468
+ page_struct[:flamegraph] = flamegraph
332
469
  end
333
470
 
334
471
  begin
@@ -369,7 +506,7 @@ module Rack
369
506
 
370
507
  # inject header
371
508
  if headers.is_a? Hash
372
- headers['X-MiniProfiler-Ids'] = ids_json(env)
509
+ headers['X-MiniProfiler-Ids'] = ids_comma_separated(env)
373
510
  end
374
511
 
375
512
  if current.inject_js && content_type =~ /text\/html/
@@ -421,7 +558,7 @@ module Rack
421
558
 
422
559
  body << "\nBacktraces\n"
423
560
  exceptions.each_with_index do |e, i|
424
- body << "##{i + 1}: #{e.class} - \"#{e.message}\"\n #{e.backtrace.join("\n ")}\n\n"
561
+ body << "##{i + 1}: #{e.class} - \"#{e.message.lines.first.chomp}\"\n #{e.backtrace.join("\n ")}\n\n"
425
562
  end
426
563
  end
427
564
  text_result(body)
@@ -521,48 +658,81 @@ module Rack
521
658
  text_result(body)
522
659
  end
523
660
 
524
- def text_result(body)
525
- headers = { 'Content-Type' => 'text/plain' }
526
- [200, headers, [body]]
661
+ def text_result(body, status: 200, headers: nil)
662
+ headers = (headers || {}).merge('Content-Type' => 'text/plain; charset=utf-8')
663
+ [status, headers, [body]]
527
664
  end
528
665
 
529
666
  def make_link(postfix, env)
530
- link = env["PATH_INFO"] + "?" + env["QUERY_STRING"].sub("pp=help", "pp=#{postfix}")
531
- "pp=<a href='#{link}'>#{postfix}</a>"
667
+ link = env["PATH_INFO"] + "?" + env["QUERY_STRING"].sub("#{@config.profile_parameter}=help", "#{@config.profile_parameter}=#{postfix}")
668
+ "#{@config.profile_parameter}=<a href='#{ERB::Util.html_escape(link)}'>#{postfix}</a>"
532
669
  end
533
670
 
534
671
  def help(client_settings, env)
535
672
  headers = { 'Content-Type' => 'text/html' }
536
- body = "<html><body>
537
- <pre style='line-height: 30px; font-size: 16px;'>
538
- Append the following to your query string:
539
-
540
- #{make_link "help", env} : display this screen
541
- #{make_link "env", env} : display the rack environment
542
- #{make_link "skip", env} : skip mini profiler for this request
543
- #{make_link "no-backtrace", env} #{"(*) " if client_settings.backtrace_none?}: don't collect stack traces from all the SQL executed (sticky, use pp=normal-backtrace to enable)
544
- #{make_link "normal-backtrace", env} #{"(*) " if client_settings.backtrace_default?}: collect stack traces from all the SQL executed and filter normally
545
- #{make_link "full-backtrace", env} #{"(*) " if client_settings.backtrace_full?}: enable full backtraces for SQL executed (use pp=normal-backtrace to disable)
546
- #{make_link "disable", env} : disable profiling for this session
547
- #{make_link "enable", env} : enable profiling for this session (if previously disabled)
548
- #{make_link "profile-gc", env} : perform gc profiling on this request, analyzes ObjectSpace generated by request (ruby 1.9.3 only)
549
- #{make_link "profile-memory", env} : requires the memory_profiler gem, new location based report
550
- #{make_link "flamegraph", env} : works best on Ruby 2.0, a graph representing sampled activity (requires the flamegraph gem).
551
- #{make_link "flamegraph&flamegraph_sample_rate=1", env}: creates a flamegraph with the specified sample rate (in ms). Overrides value set in config
552
- #{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.
553
- #{make_link "trace-exceptions", env} : requires Ruby 2.0, will return all the spots where your application raises exceptions
554
- #{make_link "analyze-memory", env} : requires Ruby 2.0, will perform basic memory analysis of heap
555
- </pre>
556
- </body>
557
- </html>
558
- "
559
-
560
- [200, headers, [body]]
561
- end
562
-
563
- def flamegraph(graph)
673
+ html = <<~HTML
674
+ <!DOCTYPE html>
675
+ <html>
676
+ <head>
677
+ <title>Rack::MiniProfiler Help</title>
678
+ </head>
679
+ <body>
680
+ <pre style='line-height: 30px; font-size: 16px'>
681
+ This is the help menu of the <a href='#{Rack::MiniProfiler::SOURCE_CODE_URI}'>rack-mini-profiler</a> gem, append the following to your query string for more options:
682
+
683
+ #{make_link "help", env} : display this screen
684
+ #{make_link "env", env} : display the rack environment
685
+ #{make_link "skip", env} : skip mini profiler for this request
686
+ #{make_link "no-backtrace", env} #{"(*) " if client_settings.backtrace_none?}: don't collect stack traces from all the SQL executed (sticky, use #{@config.profile_parameter}=normal-backtrace to enable)
687
+ #{make_link "normal-backtrace", env} #{"(*) " if client_settings.backtrace_default?}: collect stack traces from all the SQL executed and filter normally
688
+ #{make_link "full-backtrace", env} #{"(*) " if client_settings.backtrace_full?}: enable full backtraces for SQL executed (use #{@config.profile_parameter}=normal-backtrace to disable)
689
+ #{make_link "disable", env} : disable profiling for this session
690
+ #{make_link "enable", env} : enable profiling for this session (if previously disabled)
691
+ #{make_link "profile-gc", env} : perform gc profiling on this request, analyzes ObjectSpace generated by request
692
+ #{make_link "profile-memory", env} : requires the memory_profiler gem, new location based report
693
+ #{make_link "flamegraph", env} : a graph representing sampled activity (requires the stackprof gem).
694
+ #{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).
695
+ #{make_link "flamegraph&flamegraph_sample_rate=1", env}: creates a flamegraph with the specified sample rate (in ms). Overrides value set in config
696
+ #{make_link "flamegraph&flamegraph_mode=cpu", env}: creates a flamegraph with the specified mode (one of cpu, wall, object, or custom). Overrides value set in config
697
+ #{make_link "flamegraph_embed", env} : a graph representing sampled activity (requires the stackprof gem), embedded resources for use on an intranet.
698
+ #{make_link "trace-exceptions", env} : will return all the spots where your application raises exceptions
699
+ #{make_link "analyze-memory", env} : will perform basic memory analysis of heap
700
+ </pre>
701
+ </body>
702
+ </html>
703
+ HTML
704
+
705
+ [200, headers, [html]]
706
+ end
707
+
708
+ def flamegraph(graph, path)
564
709
  headers = { 'Content-Type' => 'text/html' }
565
- [200, headers, [graph]]
710
+ html = <<~HTML
711
+ <!DOCTYPE html>
712
+ <html>
713
+ <head>
714
+ <title>Rack::MiniProfiler Flamegraph</title>
715
+ <style>
716
+ body { margin: 0; height: 100vh; }
717
+ #speedscope-iframe { width: 100%; height: 100%; border: none; }
718
+ </style>
719
+ </head>
720
+ <body>
721
+ <script type="text/javascript">
722
+ var graph = #{JSON.generate(graph)};
723
+ var json = JSON.stringify(graph);
724
+ var blob = new Blob([json], { type: 'text/plain' });
725
+ var objUrl = encodeURIComponent(URL.createObjectURL(blob));
726
+ var iframe = document.createElement('IFRAME');
727
+ iframe.setAttribute('id', 'speedscope-iframe');
728
+ document.body.appendChild(iframe);
729
+ var iframeUrl = '#{@config.base_url_path}speedscope/index.html#profileURL=' + objUrl + '&title=' + 'Flamegraph for #{CGI.escape(path)}';
730
+ iframe.setAttribute('src', iframeUrl);
731
+ </script>
732
+ </body>
733
+ </html>
734
+ HTML
735
+ [200, headers, [html]]
566
736
  end
567
737
 
568
738
  def ids(env)
@@ -574,10 +744,6 @@ Append the following to your query string:
574
744
  all
575
745
  end
576
746
 
577
- def ids_json(env)
578
- ::JSON.generate(ids(env))
579
- end
580
-
581
747
  def ids_comma_separated(env)
582
748
  ids(env).join(",")
583
749
  end
@@ -590,10 +756,24 @@ Append the following to your query string:
590
756
  # * you do not want script to be automatically appended for the current page. You can also call cancel_auto_inject
591
757
  def get_profile_script(env)
592
758
  path = "#{env['RACK_MINI_PROFILER_ORIGINAL_SCRIPT_NAME']}#{@config.base_url_path}"
759
+ version = MiniProfiler::ASSET_VERSION
760
+ if @config.assets_url
761
+ url = @config.assets_url.call('rack-mini-profiler.js', version, env)
762
+ css_url = @config.assets_url.call('rack-mini-profiler.css', version, env)
763
+ end
764
+
765
+ url = "#{path}includes.js?v=#{version}" if !url
766
+ css_url = "#{path}includes.css?v=#{version}" if !css_url
767
+
768
+ content_security_policy_nonce = @config.content_security_policy_nonce ||
769
+ env["action_dispatch.content_security_policy_nonce"] ||
770
+ env["secure_headers_content_security_policy_nonce"]
593
771
 
594
772
  settings = {
595
773
  path: path,
596
- version: MiniProfiler::ASSET_VERSION,
774
+ url: url,
775
+ cssUrl: css_url,
776
+ version: version,
597
777
  verticalPosition: @config.vertical_position,
598
778
  horizontalPosition: @config.horizontal_position,
599
779
  showTrivial: @config.show_trivial,
@@ -605,7 +785,10 @@ Append the following to your query string:
605
785
  toggleShortcut: @config.toggle_shortcut,
606
786
  startHidden: @config.start_hidden,
607
787
  collapseResults: @config.collapse_results,
608
- htmlContainer: @config.html_container
788
+ htmlContainer: @config.html_container,
789
+ hiddenCustomFields: @config.snapshot_hidden_custom_fields.join(','),
790
+ cspNonce: content_security_policy_nonce,
791
+ hotwireTurboDriveSupport: @config.enable_hotwire_turbo_drive_support,
609
792
  }
610
793
 
611
794
  if current && current.page_struct
@@ -617,7 +800,7 @@ Append the following to your query string:
617
800
  end
618
801
 
619
802
  # TODO : cache this snippet
620
- script = IO.read(::File.expand_path('../html/profile_handler.js', ::File.dirname(__FILE__)))
803
+ script = ::File.read(::File.expand_path('html/profile_handler.js', ::File.dirname(__FILE__)))
621
804
  # replace the variables
622
805
  settings.each do |k, v|
623
806
  regex = Regexp.new("\\{#{k.to_s}\\}")
@@ -633,5 +816,134 @@ Append the following to your query string:
633
816
  current.inject_js = false
634
817
  end
635
818
 
819
+ def cache_control_value
820
+ 86400
821
+ end
822
+
823
+ private
824
+
825
+ def handle_snapshots_request(env)
826
+ self.current = nil
827
+ MiniProfiler.authorize_request
828
+ status = 200
829
+ headers = { 'Content-Type' => 'text/html' }
830
+ qp = Rack::Utils.parse_nested_query(env['QUERY_STRING'])
831
+ if group_name = qp["group_name"]
832
+ list = @storage.snapshots_group(group_name)
833
+ list.each do |snapshot|
834
+ snapshot[:url] = url_for_snapshot(snapshot[:id], group_name)
835
+ end
836
+ data = {
837
+ group_name: group_name,
838
+ list: list
839
+ }
840
+ else
841
+ list = @storage.snapshots_overview
842
+ list.each do |group|
843
+ group[:url] = url_for_snapshots_group(group[:name])
844
+ end
845
+ data = {
846
+ page: "overview",
847
+ list: list
848
+ }
849
+ end
850
+ data_html = <<~HTML
851
+ <div style="display: none;" id="snapshots-data">
852
+ #{data.to_json}
853
+ </div>
854
+ HTML
855
+ response = Rack::Response.new([], status, headers)
856
+
857
+ response.write <<~HTML
858
+ <!DOCTYPE html>
859
+ <html>
860
+ <head>
861
+ <title>Rack::MiniProfiler Snapshots</title>
862
+ </head>
863
+ <body class="mp-snapshots">
864
+ HTML
865
+ response.write(data_html)
866
+ script = self.get_profile_script(env)
867
+ response.write(script)
868
+ response.write <<~HTML
869
+ </body>
870
+ </html>
871
+ HTML
872
+ response.finish
873
+ end
874
+
875
+ def serve_flamegraph(env)
876
+ request = Rack::Request.new(env)
877
+ id = request.params['id']
878
+ page_struct = @storage.load(id)
879
+
880
+ if !page_struct
881
+ id = ERB::Util.html_escape(id)
882
+ user_info = ERB::Util.html_escape(user(env))
883
+ return [404, {}, ["Request not found: #{id} - user #{user_info}"]]
884
+ end
885
+
886
+ if !page_struct[:flamegraph]
887
+ return [404, {}, ["No flamegraph available for #{ERB::Util.html_escape(id)}"]]
888
+ end
889
+
890
+ self.flamegraph(page_struct[:flamegraph], page_struct[:request_path])
891
+ end
892
+
893
+ def rails_route_from_path(path, method)
894
+ if defined?(Rails) && defined?(ActionController::RoutingError)
895
+ hash = Rails.application.routes.recognize_path(path, method: method)
896
+ if hash && hash[:controller] && hash[:action]
897
+ "#{hash[:controller]}##{hash[:action]}"
898
+ end
899
+ end
900
+ rescue ActionController::RoutingError
901
+ nil
902
+ end
903
+
904
+ def url_for_snapshots_group(group_name)
905
+ qs = Rack::Utils.build_query({ group_name: group_name })
906
+ "/#{@config.base_url_path.gsub('/', '')}/snapshots?#{qs}"
907
+ end
908
+
909
+ def url_for_snapshot(id, group_name)
910
+ qs = Rack::Utils.build_query({ id: id, group: group_name })
911
+ "/#{@config.base_url_path.gsub('/', '')}/results?#{qs}"
912
+ end
913
+
914
+ def take_snapshot?(path)
915
+ @config.snapshot_every_n_requests > 0 &&
916
+ !path.start_with?(@config.base_url_path) &&
917
+ @storage.should_take_snapshot?(@config.snapshot_every_n_requests)
918
+ end
919
+
920
+ def take_snapshot(env, start)
921
+ MiniProfiler.create_current(env, @config)
922
+ Thread.current[:mp_ongoing_snapshot] = true
923
+ results = @app.call(env)
924
+ status = results[0].to_i
925
+ if status >= 200 && status < 300
926
+ page_struct = current.page_struct
927
+ page_struct[:root].record_time(
928
+ (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000
929
+ )
930
+ custom_fields = MiniProfiler.get_snapshot_custom_fields
931
+ page_struct[:custom_fields] = custom_fields if custom_fields
932
+ if Rack::MiniProfiler.snapshots_transporter?
933
+ Rack::MiniProfiler::SnapshotsTransporter.transport(page_struct)
934
+ else
935
+ group_name = rails_route_from_path(page_struct[:request_path], page_struct[:request_method])
936
+ group_name ||= page_struct[:request_path]
937
+ group_name = "#{page_struct[:request_method]} #{group_name}"
938
+ @storage.push_snapshot(
939
+ page_struct,
940
+ group_name,
941
+ @config
942
+ )
943
+ end
944
+ end
945
+ self.current = nil
946
+ results
947
+ end
636
948
  end
637
949
  end