rack-mini-profiler 1.0.1 → 2.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
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