rack-mini-profiler 0.10.6 → 2.3.0

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 (74) hide show
  1. checksums.yaml +5 -5
  2. data/CHANGELOG.md +129 -16
  3. data/README.md +116 -63
  4. data/lib/enable_rails_patches.rb +5 -0
  5. data/lib/generators/rack_profiler/install_generator.rb +2 -0
  6. data/lib/generators/rack_profiler/templates/rack_profiler.rb +2 -0
  7. data/lib/html/dot.1.1.2.min.js +2 -0
  8. data/lib/html/includes.css +141 -40
  9. data/lib/html/includes.js +1398 -970
  10. data/lib/html/includes.scss +547 -442
  11. data/lib/html/includes.tmpl +227 -142
  12. data/lib/html/pretty-print.js +810 -0
  13. data/lib/html/profile_handler.js +1 -1
  14. data/lib/html/rack-mini-profiler.css +3 -0
  15. data/lib/html/rack-mini-profiler.js +2 -0
  16. data/lib/html/share.html +0 -1
  17. data/lib/html/speedscope/LICENSE +21 -0
  18. data/lib/html/speedscope/README.md +3 -0
  19. data/lib/html/speedscope/demangle-cpp.1768f4cc.js +4 -0
  20. data/lib/html/speedscope/favicon-16x16.f74b3187.png +0 -0
  21. data/lib/html/speedscope/favicon-32x32.bc503437.png +0 -0
  22. data/lib/html/speedscope/file-format-schema.json +324 -0
  23. data/lib/html/speedscope/import.cf0fa83f.js +115 -0
  24. data/lib/html/speedscope/index.html +2 -0
  25. data/lib/html/speedscope/release.txt +3 -0
  26. data/lib/html/speedscope/reset.8c46b7a1.css +2 -0
  27. data/lib/html/speedscope/source-map.438fa06b.js +24 -0
  28. data/lib/html/speedscope/speedscope.44364064.js +200 -0
  29. data/lib/html/vendor.js +848 -0
  30. data/lib/mini_profiler/asset_version.rb +3 -2
  31. data/lib/mini_profiler/client_settings.rb +27 -16
  32. data/lib/mini_profiler/config.rb +73 -46
  33. data/lib/mini_profiler/context.rb +5 -3
  34. data/lib/mini_profiler/gc_profiler.rb +17 -16
  35. data/lib/mini_profiler/profiler.rb +332 -94
  36. data/lib/mini_profiler/profiling_methods.rb +20 -15
  37. data/lib/mini_profiler/snapshots_transporter.rb +109 -0
  38. data/lib/mini_profiler/storage/abstract_store.rb +80 -0
  39. data/lib/mini_profiler/storage/file_store.rb +18 -13
  40. data/lib/mini_profiler/storage/memcache_store.rb +10 -7
  41. data/lib/mini_profiler/storage/memory_store.rb +63 -13
  42. data/lib/mini_profiler/storage/redis_store.rb +143 -7
  43. data/lib/mini_profiler/timer_struct/base.rb +4 -2
  44. data/lib/mini_profiler/timer_struct/client.rb +9 -8
  45. data/lib/mini_profiler/timer_struct/custom.rb +8 -5
  46. data/lib/mini_profiler/timer_struct/page.rb +79 -24
  47. data/lib/mini_profiler/timer_struct/request.rb +83 -38
  48. data/lib/mini_profiler/timer_struct/sql.rb +25 -22
  49. data/lib/mini_profiler/version.rb +3 -1
  50. data/lib/mini_profiler_rails/railtie.rb +91 -8
  51. data/lib/mini_profiler_rails/railtie_methods.rb +61 -0
  52. data/lib/patches/db/activerecord.rb +5 -14
  53. data/lib/patches/db/mongo.rb +3 -1
  54. data/lib/patches/db/moped.rb +5 -3
  55. data/lib/patches/db/mysql2.rb +8 -6
  56. data/lib/patches/db/neo4j.rb +3 -1
  57. data/lib/patches/db/nobrainer.rb +4 -2
  58. data/lib/patches/db/oracle_enhanced.rb +4 -2
  59. data/lib/patches/db/pg.rb +41 -21
  60. data/lib/patches/db/plucky.rb +7 -5
  61. data/lib/patches/db/riak.rb +15 -13
  62. data/lib/patches/db/rsolr.rb +6 -4
  63. data/lib/patches/db/sequel.rb +2 -0
  64. data/lib/patches/net_patches.rb +20 -8
  65. data/lib/patches/sql_patches.rb +17 -7
  66. data/lib/prepend_net_http_patch.rb +5 -0
  67. data/lib/rack-mini-profiler.rb +3 -3
  68. data/rack-mini-profiler.gemspec +23 -9
  69. metadata +146 -31
  70. data/lib/html/jquery.1.7.1.js +0 -4
  71. data/lib/html/jquery.tmpl.js +0 -486
  72. data/lib/html/list.css +0 -9
  73. data/lib/html/list.js +0 -38
  74. data/lib/html/list.tmpl +0 -34
@@ -1,8 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'cgi'
4
+
1
5
  module Rack
2
6
  class MiniProfiler
3
7
  class << self
4
8
 
5
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
6
15
 
7
16
  def generate_id
8
17
  rand(36**20).to_s(36)
@@ -31,15 +40,27 @@ module Rack
31
40
 
32
41
  def current=(c)
33
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
34
45
  Thread.current[:mini_profiler_private] = c
35
46
  end
36
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
+
37
58
  # discard existing results, don't track this request
38
59
  def discard_results
39
60
  self.current.discard = true if current
40
61
  end
41
62
 
42
- def create_current(env={}, options={})
63
+ def create_current(env = {}, options = {})
43
64
  # profiling the request
44
65
  context = Context.new
45
66
  context.inject_js = config.auto_inject && (!env['HTTP_X_REQUESTED_WITH'].eql? 'XMLHttpRequest')
@@ -60,6 +81,32 @@ module Rack
60
81
  Thread.current[:mp_authorized]
61
82
  end
62
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
63
110
  end
64
111
 
65
112
  #
@@ -69,7 +116,7 @@ module Rack
69
116
  MiniProfiler.config.merge!(config)
70
117
  @config = MiniProfiler.config
71
118
  @app = app
72
- @config.base_url_path << "/" unless @config.base_url_path.end_with? "/"
119
+ @config.base_url_path += "/" unless @config.base_url_path.end_with? "/"
73
120
  unless @config.storage_instance
74
121
  @config.storage_instance = @config.storage.new(@config.storage_options)
75
122
  end
@@ -82,15 +129,24 @@ module Rack
82
129
 
83
130
  def serve_results(env)
84
131
  request = Rack::Request.new(env)
85
- id = request[:id]
86
- page_struct = @storage.load(id)
87
- 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
88
144
  @storage.set_viewed(user(env), id)
89
- id = ERB::Util.html_escape(request['id'])
145
+ id = ERB::Util.html_escape(id)
90
146
  user_info = ERB::Util.html_escape(user(env))
91
147
  return [404, {}, ["Request not found: #{id} - user #{user_info}"]]
92
148
  end
93
- unless page_struct[:has_user_viewed]
149
+ if !page_struct[:has_user_viewed] && !is_snapshot
94
150
  page_struct[:client_timings] = TimerStruct::Client.init_from_form_data(env, page_struct)
95
151
  page_struct[:has_user_viewed] = true
96
152
  @storage.save(page_struct)
@@ -100,21 +156,22 @@ module Rack
100
156
  # If we're an XMLHttpRequest, serve up the contents as JSON
101
157
  if request.xhr?
102
158
  result_json = page_struct.to_json
103
- [200, { 'Content-Type' => 'application/json'}, [result_json]]
159
+ [200, { 'Content-Type' => 'application/json' }, [result_json]]
104
160
  else
105
161
  # Otherwise give the HTML back
106
162
  html = generate_html(page_struct, env)
107
- [200, {'Content-Type' => 'text/html'}, [html]]
163
+ [200, { 'Content-Type' => 'text/html' }, [html]]
108
164
  end
109
165
  end
110
166
 
111
167
  def generate_html(page_struct, env, result_json = page_struct.to_json)
112
- path = "#{env['RACK_MINI_PROFILER_ORIGINAL_SCRIPT_NAME']}#{@config.base_url_path}"
113
- version = MiniProfiler::ASSET_VERSION
114
- json = result_json
115
- includes = get_profile_script(env)
116
- name = page_struct[:name]
117
- duration = page_struct.duration_ms.round(1).to_s
168
+ # double-assigning to suppress "assigned but unused variable" warnings
169
+ path = path = "#{env['RACK_MINI_PROFILER_ORIGINAL_SCRIPT_NAME']}#{@config.base_url_path}"
170
+ version = version = MiniProfiler::ASSET_VERSION
171
+ json = json = result_json
172
+ includes = includes = get_profile_script(env)
173
+ name = name = page_struct[:name]
174
+ duration = duration = page_struct.duration_ms.round(1).to_s
118
175
 
119
176
  MiniProfiler.share_template.result(binding)
120
177
  end
@@ -124,15 +181,15 @@ module Rack
124
181
  file_name = path.sub(@config.base_url_path, '')
125
182
 
126
183
  return serve_results(env) if file_name.eql?('results')
184
+ return handle_snapshots_request(env) if file_name.eql?('snapshots')
127
185
 
128
186
  resources_env = env.dup
129
187
  resources_env['PATH_INFO'] = file_name
130
188
 
131
- rack_file = Rack::File.new(MiniProfiler.resources_root, {'Cache-Control' => 'max-age:86400'})
189
+ rack_file = Rack::File.new(MiniProfiler.resources_root, 'Cache-Control' => "max-age=#{cache_control_value}")
132
190
  rack_file.call(resources_env)
133
191
  end
134
192
 
135
-
136
193
  def current
137
194
  MiniProfiler.current
138
195
  end
@@ -141,15 +198,20 @@ module Rack
141
198
  MiniProfiler.current = c
142
199
  end
143
200
 
144
-
145
201
  def config
146
202
  @config
147
203
  end
148
204
 
205
+ def advanced_debugging_enabled?
206
+ config.enable_advanced_debugging_tools
207
+ end
149
208
 
150
- def call(env)
209
+ def tool_disabled_message(client_settings)
210
+ client_settings.handle_cookie(text_result(Rack::MiniProfiler.advanced_tools_message))
211
+ end
151
212
 
152
- start = Time.now
213
+ def call(env)
214
+ start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
153
215
  client_settings = ClientSettings.new(env, @storage, start)
154
216
  MiniProfiler.deauthorize_request if @config.authorization_mode == :whitelist
155
217
 
@@ -160,15 +222,31 @@ module Rack
160
222
  # Someone (e.g. Rails engine) could change the SCRIPT_NAME so we save it
161
223
  env['RACK_MINI_PROFILER_ORIGINAL_SCRIPT_NAME'] = ENV['PASSENGER_BASE_URI'] || env['SCRIPT_NAME']
162
224
 
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/
225
+ skip_it = /pp=skip/.match?(query_string) || (
226
+ @config.skip_paths &&
227
+ @config.skip_paths.any? do |p|
228
+ if p.instance_of?(String)
229
+ path.start_with?(p)
230
+ elsif p.instance_of?(Regexp)
231
+ p.match?(path)
232
+ end
233
+ end
234
+ )
235
+ if skip_it
236
+ return client_settings.handle_cookie(@app.call(env))
237
+ end
238
+
239
+ skip_it = (@config.pre_authorize_cb && !@config.pre_authorize_cb.call(env))
166
240
 
167
241
  if skip_it || (
168
242
  @config.authorization_mode == :whitelist &&
169
243
  !client_settings.has_valid_cookie?
170
244
  )
171
- return client_settings.handle_cookie(@app.call(env))
245
+ if take_snapshot?(path)
246
+ return client_settings.handle_cookie(take_snapshot(env, start))
247
+ else
248
+ return client_settings.handle_cookie(@app.call(env))
249
+ end
172
250
  end
173
251
 
174
252
  # handle all /mini-profiler requests here
@@ -186,30 +264,32 @@ module Rack
186
264
  end
187
265
 
188
266
  if skip_it || !config.enabled
189
- status,headers,body = @app.call(env)
267
+ status, headers, body = @app.call(env)
190
268
  client_settings.disable_profiling = true
191
- return client_settings.handle_cookie([status,headers,body])
269
+ return client_settings.handle_cookie([status, headers, body])
192
270
  else
193
271
  client_settings.disable_profiling = false
194
272
  end
195
273
 
196
274
  # profile gc
197
275
  if query_string =~ /pp=profile-gc/
276
+ return tool_disabled_message(client_settings) if !advanced_debugging_enabled?
198
277
  current.measure = false if current
199
278
  return client_settings.handle_cookie(Rack::MiniProfiler::GCProfiler.new.profile_gc(@app, env))
200
279
  end
201
280
 
202
281
  # profile memory
203
282
  if query_string =~ /pp=profile-memory/
283
+ return tool_disabled_message(client_settings) if !advanced_debugging_enabled?
204
284
  query_params = Rack::Utils.parse_nested_query(query_string)
205
285
  options = {
206
- :ignore_files => query_params['memory_profiler_ignore_files'],
207
- :allow_files => query_params['memory_profiler_allow_files'],
286
+ ignore_files: query_params['memory_profiler_ignore_files'],
287
+ allow_files: query_params['memory_profiler_allow_files'],
208
288
  }
209
- options[:top]= Integer(query_params['memory_profiler_top']) if query_params.key?('memory_profiler_top')
289
+ options[:top] = Integer(query_params['memory_profiler_top']) if query_params.key?('memory_profiler_top')
210
290
  result = StringIO.new
211
291
  report = MemoryProfiler.report(options) do
212
- _,_,body = @app.call(env)
292
+ _, _, body = @app.call(env)
213
293
  body.close if body.respond_to? :close
214
294
  end
215
295
  report.pretty_print(result)
@@ -233,8 +313,7 @@ module Rack
233
313
  flamegraph = nil
234
314
 
235
315
  trace_exceptions = query_string =~ /pp=trace-exceptions/ && defined? TracePoint
236
- status, headers, body, exceptions,trace = nil
237
-
316
+ status, headers, body, exceptions, trace = nil
238
317
 
239
318
  if trace_exceptions
240
319
  exceptions = []
@@ -258,28 +337,40 @@ module Rack
258
337
  env['HTTP_ACCEPT_ENCODING'] = 'identity' if config.suppress_encoding
259
338
 
260
339
  if query_string =~ /pp=flamegraph/
261
- unless defined?(Flamegraph) && Flamegraph.respond_to?(:generate)
340
+ unless defined?(StackProf) && StackProf.respond_to?(:run)
262
341
 
263
- flamegraph = "Please install the flamegraph gem and require it: add gem 'flamegraph' to your Gemfile"
264
- status,headers,body = @app.call(env)
342
+ flamegraph = "Please install the stackprof gem and require it: add gem 'stackprof' to your Gemfile"
343
+ status, headers, body = @app.call(env)
265
344
  else
266
345
  # do not sully our profile with mini profiler timings
267
346
  current.measure = false
268
347
  match_data = query_string.match(/flamegraph_sample_rate=([\d\.]+)/)
269
348
 
270
- mode = query_string =~ /mode=c/ ? :c : :ruby
271
-
272
349
  if match_data && !match_data[1].to_f.zero?
273
350
  sample_rate = match_data[1].to_f
274
351
  else
275
352
  sample_rate = config.flamegraph_sample_rate
276
353
  end
277
- flamegraph = Flamegraph.generate(nil, :fidelity => sample_rate, :embed_resources => query_string =~ /embed/, :mode => mode) do
278
- status,headers,body = @app.call(env)
354
+ flamegraph = StackProf.run(
355
+ mode: :wall,
356
+ raw: true,
357
+ aggregate: false,
358
+ interval: (sample_rate * 1000).to_i
359
+ ) do
360
+ status, headers, body = @app.call(env)
279
361
  end
280
362
  end
363
+ elsif path == '/rack-mini-profiler/requests'
364
+ blank_page_html = <<~HTML
365
+ <html>
366
+ <head></head>
367
+ <body></body>
368
+ </html>
369
+ HTML
370
+
371
+ status, headers, body = [200, { 'Content-Type' => 'text/html' }, [blank_page_html.dup]]
281
372
  else
282
- status,headers,body = @app.call(env)
373
+ status, headers, body = @app.call(env)
283
374
  end
284
375
  ensure
285
376
  trace.disable if trace
@@ -292,7 +383,7 @@ module Rack
292
383
  skip_it = true
293
384
  end
294
385
 
295
- return client_settings.handle_cookie([status,headers,body]) if skip_it
386
+ return client_settings.handle_cookie([status, headers, body]) if skip_it
296
387
 
297
388
  # we must do this here, otherwise current[:discard] is not being properly treated
298
389
  if trace_exceptions
@@ -308,12 +399,14 @@ module Rack
308
399
  return client_settings.handle_cookie(dump_exceptions exceptions)
309
400
  end
310
401
 
311
- if query_string =~ /pp=env/ && !config.disable_env_dump
402
+ if query_string =~ /pp=env/
403
+ return tool_disabled_message(client_settings) if !advanced_debugging_enabled?
312
404
  body.close if body.respond_to? :close
313
405
  return client_settings.handle_cookie(dump_env env)
314
406
  end
315
407
 
316
408
  if query_string =~ /pp=analyze-memory/
409
+ return tool_disabled_message(client_settings) if !advanced_debugging_enabled?
317
410
  body.close if body.respond_to? :close
318
411
  return client_settings.handle_cookie(analyze_memory)
319
412
  end
@@ -325,14 +418,13 @@ module Rack
325
418
 
326
419
  page_struct = current.page_struct
327
420
  page_struct[:user] = user(env)
328
- page_struct[:root].record_time((Time.now - start) * 1000)
421
+ page_struct[:root].record_time((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000)
329
422
 
330
423
  if flamegraph
331
424
  body.close if body.respond_to? :close
332
- return client_settings.handle_cookie(self.flamegraph(flamegraph))
425
+ return client_settings.handle_cookie(self.flamegraph(flamegraph, path))
333
426
  end
334
427
 
335
-
336
428
  begin
337
429
  @storage.save(page_struct)
338
430
  # no matter what it is, it should be unviewed, otherwise we will miss POST
@@ -340,7 +432,7 @@ module Rack
340
432
 
341
433
  # inject headers, script
342
434
  if status >= 200 && status < 300
343
- result = inject_profiler(env,status,headers,body)
435
+ result = inject_profiler(env, status, headers, body)
344
436
  return client_settings.handle_cookie(result) if result
345
437
  end
346
438
  rescue Exception => e
@@ -356,7 +448,7 @@ module Rack
356
448
  self.current = nil
357
449
  end
358
450
 
359
- def inject_profiler(env,status,headers,body)
451
+ def inject_profiler(env, status, headers, body)
360
452
  # mini profiler is meddling with stuff, we can not cache cause we will get incorrect data
361
453
  # Rack::ETag has already inserted some nonesense in the chain
362
454
  content_type = headers['Content-Type']
@@ -366,12 +458,12 @@ module Rack
366
458
  headers.delete('Date')
367
459
  end
368
460
 
369
- headers['X-MiniProfiler-Original-Cache-Control'] = headers['Cache-Control']
461
+ headers['X-MiniProfiler-Original-Cache-Control'] = headers['Cache-Control'] unless headers['Cache-Control'].nil?
370
462
  headers['Cache-Control'] = "#{"no-store, " if config.disable_caching}must-revalidate, private, max-age=0"
371
463
 
372
464
  # inject header
373
465
  if headers.is_a? Hash
374
- headers['X-MiniProfiler-Ids'] = ids_json(env)
466
+ headers['X-MiniProfiler-Ids'] = ids_comma_separated(env)
375
467
  end
376
468
 
377
469
  if current.inject_js && content_type =~ /text\/html/
@@ -379,7 +471,7 @@ module Rack
379
471
  script = self.get_profile_script(env)
380
472
 
381
473
  if String === body
382
- response.write inject(body,script)
474
+ response.write inject(body, script)
383
475
  else
384
476
  body.each { |fragment| response.write inject(fragment, script) }
385
477
  end
@@ -399,38 +491,44 @@ module Rack
399
491
  if script.respond_to?(:encoding) && script.respond_to?(:force_encoding)
400
492
  script = script.force_encoding(fragment.encoding)
401
493
  end
402
- fragment.insert(index, script)
494
+
495
+ safe_script = script
496
+ if script.respond_to?(:html_safe)
497
+ safe_script = script.html_safe
498
+ end
499
+
500
+ fragment.insert(index, safe_script)
403
501
  else
404
502
  fragment
405
503
  end
406
504
  end
407
505
 
408
506
  def dump_exceptions(exceptions)
409
- body = "Exceptions raised during request\n\n"
507
+ body = "Exceptions raised during request\n\n".dup
410
508
  if exceptions.empty?
411
509
  body << "No exceptions raised"
412
510
  else
413
511
  body << "Exceptions: (#{exceptions.size} total)\n"
414
- exceptions.group_by(&:class).each do |klass, exceptions|
415
- body << " #{klass.name} (#{exceptions.size})\n"
512
+ exceptions.group_by(&:class).each do |klass, exceptions_per_class|
513
+ body << " #{klass.name} (#{exceptions_per_class.size})\n"
416
514
  end
417
515
 
418
516
  body << "\nBacktraces\n"
419
517
  exceptions.each_with_index do |e, i|
420
- body << "##{i+1}: #{e.class} - \"#{e.message}\"\n #{e.backtrace.join("\n ")}\n\n"
518
+ body << "##{i + 1}: #{e.class} - \"#{e.message}\"\n #{e.backtrace.join("\n ")}\n\n"
421
519
  end
422
520
  end
423
521
  text_result(body)
424
522
  end
425
523
 
426
524
  def dump_env(env)
427
- body = "Rack Environment\n---------------\n"
428
- env.each do |k,v|
525
+ body = "Rack Environment\n---------------\n".dup
526
+ env.each do |k, v|
429
527
  body << "#{k}: #{v}\n"
430
528
  end
431
529
 
432
530
  body << "\n\nEnvironment\n---------------\n"
433
- ENV.each do |k,v|
531
+ ENV.each do |k, v|
434
532
  body << "#{k}: #{v}\n"
435
533
  end
436
534
 
@@ -446,9 +544,9 @@ module Rack
446
544
  end
447
545
 
448
546
  def trim_strings(strings, max_size)
449
- strings.sort!{|a,b| b[1] <=> a[1]}
547
+ strings.sort! { |a, b| b[1] <=> a[1] }
450
548
  i = 0
451
- strings.delete_if{|_| (i+=1) > max_size}
549
+ strings.delete_if { |_| (i += 1) > max_size }
452
550
  end
453
551
 
454
552
  def analyze_memory
@@ -467,22 +565,22 @@ module Rack
467
565
 
468
566
  unless str.valid_encoding?
469
567
  # work around bust string with a double conversion
470
- str.encode!("utf-16","utf-8",:invalid => :replace)
471
- str.encode!("utf-8","utf-16")
568
+ str.encode!("utf-16", "utf-8", invalid: :replace)
569
+ str.encode!("utf-8", "utf-16")
472
570
  end
473
571
  end
474
572
 
475
573
  str
476
574
  end
477
575
 
478
- body = "ObjectSpace stats:\n\n"
576
+ body = "ObjectSpace stats:\n\n".dup
479
577
 
480
578
  counts = ObjectSpace.count_objects
481
579
  total_strings = counts[:T_STRING]
482
580
 
483
581
  body << counts
484
- .sort{|a,b| b[1] <=> a[1]}
485
- .map{|k,v| "#{k}: #{v}"}
582
+ .sort { |a, b| b[1] <=> a[1] }
583
+ .map { |k, v| "#{k}: #{v}" }
486
584
  .join("\n")
487
585
 
488
586
  strings = []
@@ -506,29 +604,29 @@ module Rack
506
604
  trim_strings(strings, max_size)
507
605
 
508
606
  body << "\n\n\n1000 Largest strings:\n\n"
509
- body << strings.map{|s,len| "#{s[0..1000]}\n(len: #{len})\n\n"}.join("\n")
607
+ body << strings.map { |s, len| "#{s[0..1000]}\n(len: #{len})\n\n" }.join("\n")
510
608
 
511
609
  body << "\n\n\n1000 Sample strings:\n\n"
512
- body << sample_strings.map{|s,len| "#{s[0..1000]}\n(len: #{len})\n\n"}.join("\n")
610
+ body << sample_strings.map { |s, len| "#{s[0..1000]}\n(len: #{len})\n\n" }.join("\n")
513
611
 
514
612
  body << "\n\n\n1000 Most common strings:\n\n"
515
- body << string_counts.sort{|a,b| b[1] <=> a[1]}[0..max_size].map{|s,len| "#{trunc.call(s)}\n(x #{len})\n\n"}.join("\n")
613
+ body << string_counts.sort { |a, b| b[1] <=> a[1] }[0..max_size].map { |s, len| "#{trunc.call(s)}\n(x #{len})\n\n" }.join("\n")
516
614
 
517
615
  text_result(body)
518
616
  end
519
617
 
520
618
  def text_result(body)
521
- headers = {'Content-Type' => 'text/plain'}
619
+ headers = { 'Content-Type' => 'text/plain' }
522
620
  [200, headers, [body]]
523
621
  end
524
622
 
525
623
  def make_link(postfix, env)
526
624
  link = env["PATH_INFO"] + "?" + env["QUERY_STRING"].sub("pp=help", "pp=#{postfix}")
527
- "pp=<a href='#{link}'>#{postfix}</a>"
625
+ "pp=<a href='#{ERB::Util.html_escape(link)}'>#{postfix}</a>"
528
626
  end
529
627
 
530
628
  def help(client_settings, env)
531
- headers = {'Content-Type' => 'text/html'}
629
+ headers = { 'Content-Type' => 'text/html' }
532
630
  body = "<html><body>
533
631
  <pre style='line-height: 30px; font-size: 16px;'>
534
632
  Append the following to your query string:
@@ -543,9 +641,9 @@ Append the following to your query string:
543
641
  #{make_link "enable", env} : enable profiling for this session (if previously disabled)
544
642
  #{make_link "profile-gc", env} : perform gc profiling on this request, analyzes ObjectSpace generated by request (ruby 1.9.3 only)
545
643
  #{make_link "profile-memory", env} : requires the memory_profiler gem, new location based report
546
- #{make_link "flamegraph", env} : works best on Ruby 2.0, a graph representing sampled activity (requires the flamegraph gem).
644
+ #{make_link "flamegraph", env} : requires Ruby 2.2, a graph representing sampled activity (requires the stackprof gem).
547
645
  #{make_link "flamegraph&flamegraph_sample_rate=1", env}: creates a flamegraph with the specified sample rate (in ms). Overrides value set in config
548
- #{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.
646
+ #{make_link "flamegraph_embed", env} : requires Ruby 2.2, a graph representing sampled activity (requires the stackprof gem), embedded resources for use on an intranet.
549
647
  #{make_link "trace-exceptions", env} : requires Ruby 2.0, will return all the spots where your application raises exceptions
550
648
  #{make_link "analyze-memory", env} : requires Ruby 2.0, will perform basic memory analysis of heap
551
649
  </pre>
@@ -556,9 +654,37 @@ Append the following to your query string:
556
654
  [200, headers, [body]]
557
655
  end
558
656
 
559
- def flamegraph(graph)
560
- headers = {'Content-Type' => 'text/html'}
561
- [200, headers, [graph]]
657
+ def flamegraph(graph, path)
658
+ headers = { 'Content-Type' => 'text/html' }
659
+ if Hash === graph
660
+ html = <<~HTML
661
+ <!DOCTYPE html>
662
+ <html>
663
+ <head>
664
+ <style>
665
+ body { margin: 0; height: 100vh; }
666
+ #speedscope-iframe { width: 100%; height: 100%; border: none; }
667
+ </style>
668
+ </head>
669
+ <body>
670
+ <script type="text/javascript">
671
+ var graph = #{JSON.generate(graph)};
672
+ var json = JSON.stringify(graph);
673
+ var blob = new Blob([json], { type: 'text/plain' });
674
+ var objUrl = encodeURIComponent(URL.createObjectURL(blob));
675
+ var iframe = document.createElement('IFRAME');
676
+ iframe.setAttribute('id', 'speedscope-iframe');
677
+ document.body.appendChild(iframe);
678
+ var iframeUrl = '#{@config.base_url_path}speedscope/index.html#profileURL=' + objUrl + '&title=' + 'Flamegraph for #{CGI.escape(path)}';
679
+ iframe.setAttribute('src', iframeUrl);
680
+ </script>
681
+ </body>
682
+ </html>
683
+ HTML
684
+ [200, headers, [html]]
685
+ else
686
+ [200, headers, [graph]]
687
+ end
562
688
  end
563
689
 
564
690
  def ids(env)
@@ -570,10 +696,6 @@ Append the following to your query string:
570
696
  all
571
697
  end
572
698
 
573
- def ids_json(env)
574
- ::JSON.generate(ids(env))
575
- end
576
-
577
699
  def ids_comma_separated(env)
578
700
  ids(env).join(",")
579
701
  end
@@ -586,21 +708,33 @@ Append the following to your query string:
586
708
  # * you do not want script to be automatically appended for the current page. You can also call cancel_auto_inject
587
709
  def get_profile_script(env)
588
710
  path = "#{env['RACK_MINI_PROFILER_ORIGINAL_SCRIPT_NAME']}#{@config.base_url_path}"
711
+ version = MiniProfiler::ASSET_VERSION
712
+ if @config.assets_url
713
+ url = @config.assets_url.call('rack-mini-profiler.js', version, env)
714
+ css_url = @config.assets_url.call('rack-mini-profiler.css', version, env)
715
+ end
716
+
717
+ url = "#{path}includes.js?v=#{version}" if !url
718
+ css_url = "#{path}includes.css?v=#{version}" if !css_url
589
719
 
590
720
  settings = {
591
- :path => path,
592
- :version => MiniProfiler::ASSET_VERSION,
593
- :verticalPosition => @config.vertical_position,
594
- :horizontalPosition => @config.horizontal_position,
595
- :showTrivial => @config.show_trivial,
596
- :showChildren => @config.show_children,
597
- :maxTracesToShow => @config.max_traces_to_show,
598
- :showControls => @config.show_controls,
599
- :authorized => true,
600
- :toggleShortcut => @config.toggle_shortcut,
601
- :startHidden => @config.start_hidden,
602
- :collapseResults => @config.collapse_results,
603
- :htmlContainer => @config.html_container
721
+ path: path,
722
+ url: url,
723
+ cssUrl: css_url,
724
+ version: version,
725
+ verticalPosition: @config.vertical_position,
726
+ horizontalPosition: @config.horizontal_position,
727
+ showTrivial: @config.show_trivial,
728
+ showChildren: @config.show_children,
729
+ maxTracesToShow: @config.max_traces_to_show,
730
+ showControls: @config.show_controls,
731
+ showTotalSqlCount: @config.show_total_sql_count,
732
+ authorized: true,
733
+ toggleShortcut: @config.toggle_shortcut,
734
+ startHidden: @config.start_hidden,
735
+ collapseResults: @config.collapse_results,
736
+ htmlContainer: @config.html_container,
737
+ hiddenCustomFields: @config.snapshot_hidden_custom_fields.join(',')
604
738
  }
605
739
 
606
740
  if current && current.page_struct
@@ -614,7 +748,7 @@ Append the following to your query string:
614
748
  # TODO : cache this snippet
615
749
  script = IO.read(::File.expand_path('../html/profile_handler.js', ::File.dirname(__FILE__)))
616
750
  # replace the variables
617
- settings.each do |k,v|
751
+ settings.each do |k, v|
618
752
  regex = Regexp.new("\\{#{k.to_s}\\}")
619
753
  script.gsub!(regex, v.to_s)
620
754
  end
@@ -628,5 +762,109 @@ Append the following to your query string:
628
762
  current.inject_js = false
629
763
  end
630
764
 
765
+ def cache_control_value
766
+ 86400
767
+ end
768
+
769
+ private
770
+
771
+ def handle_snapshots_request(env)
772
+ self.current = nil
773
+ MiniProfiler.authorize_request
774
+ status = 200
775
+ headers = { 'Content-Type' => 'text/html' }
776
+ qp = Rack::Utils.parse_nested_query(env['QUERY_STRING'])
777
+ if group_name = qp["group_name"]
778
+ list = @storage.find_snapshots_group(group_name)
779
+ list.each do |snapshot|
780
+ snapshot[:url] = url_for_snapshot(snapshot[:id])
781
+ end
782
+ data = {
783
+ group_name: group_name,
784
+ list: list
785
+ }
786
+ else
787
+ list = @storage.snapshot_groups_overview
788
+ list.each do |group|
789
+ group[:url] = url_for_snapshots_group(group[:name])
790
+ end
791
+ data = {
792
+ page: "overview",
793
+ list: list
794
+ }
795
+ end
796
+ data_html = <<~HTML
797
+ <div style="display: none;" id="snapshots-data">
798
+ #{data.to_json}
799
+ </div>
800
+ HTML
801
+ response = Rack::Response.new([], status, headers)
802
+
803
+ response.write <<~HTML
804
+ <html>
805
+ <head></head>
806
+ <body class="mp-snapshots">
807
+ HTML
808
+ response.write(data_html)
809
+ script = self.get_profile_script(env)
810
+ response.write(script)
811
+ response.write <<~HTML
812
+ </body>
813
+ </html>
814
+ HTML
815
+ response.finish
816
+ end
817
+
818
+ def rails_route_from_path(path, method)
819
+ if defined?(Rails) && defined?(ActionController::RoutingError)
820
+ hash = Rails.application.routes.recognize_path(path, method: method)
821
+ if hash && hash[:controller] && hash[:action]
822
+ "#{method} #{hash[:controller]}##{hash[:action]}"
823
+ end
824
+ end
825
+ rescue ActionController::RoutingError
826
+ nil
827
+ end
828
+
829
+ def url_for_snapshots_group(group_name)
830
+ qs = Rack::Utils.build_query({ group_name: group_name })
831
+ "/#{@config.base_url_path.gsub('/', '')}/snapshots?#{qs}"
832
+ end
833
+
834
+ def url_for_snapshot(id)
835
+ qs = Rack::Utils.build_query({ id: id, snapshot: true })
836
+ "/#{@config.base_url_path.gsub('/', '')}/results?#{qs}"
837
+ end
838
+
839
+ def take_snapshot?(path)
840
+ @config.snapshot_every_n_requests > 0 &&
841
+ !path.start_with?(@config.base_url_path) &&
842
+ @storage.should_take_snapshot?(@config.snapshot_every_n_requests)
843
+ end
844
+
845
+ def take_snapshot(env, start)
846
+ MiniProfiler.create_current(env, @config)
847
+ Thread.current[:mp_ongoing_snapshot] = true
848
+ results = @app.call(env)
849
+ status = results[0].to_i
850
+ if status >= 200 && status < 300
851
+ page_struct = current.page_struct
852
+ page_struct[:root].record_time(
853
+ (Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000
854
+ )
855
+ custom_fields = MiniProfiler.get_snapshot_custom_fields
856
+ page_struct[:custom_fields] = custom_fields if custom_fields
857
+ if Rack::MiniProfiler.snapshots_transporter?
858
+ Rack::MiniProfiler::SnapshotsTransporter.transport(page_struct)
859
+ else
860
+ @storage.push_snapshot(
861
+ page_struct,
862
+ @config
863
+ )
864
+ end
865
+ end
866
+ self.current = nil
867
+ results
868
+ end
631
869
  end
632
870
  end