rack-mini-profiler 2.1.0 → 2.3.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (42) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +26 -0
  3. data/README.md +37 -9
  4. data/lib/html/includes.css +3 -1
  5. data/lib/html/includes.js +225 -179
  6. data/lib/html/includes.scss +4 -1
  7. data/lib/html/includes.tmpl +29 -8
  8. data/lib/html/profile_handler.js +1 -1
  9. data/lib/html/speedscope/LICENSE +21 -0
  10. data/lib/html/speedscope/README.md +3 -0
  11. data/lib/html/speedscope/demangle-cpp.1768f4cc.js +4 -0
  12. data/lib/html/speedscope/favicon-16x16.f74b3187.png +0 -0
  13. data/lib/html/speedscope/favicon-32x32.bc503437.png +0 -0
  14. data/lib/html/speedscope/file-format-schema.json +324 -0
  15. data/lib/html/speedscope/fonts/source-code-pro-regular.css +8 -0
  16. data/lib/html/speedscope/fonts/source-code-pro-v13-regular.woff +0 -0
  17. data/lib/html/speedscope/fonts/source-code-pro-v13-regular.woff2 +0 -0
  18. data/lib/html/speedscope/import.cf0fa83f.js +115 -0
  19. data/lib/html/speedscope/index.html +2 -0
  20. data/lib/html/speedscope/release.txt +3 -0
  21. data/lib/html/speedscope/reset.8c46b7a1.css +2 -0
  22. data/lib/html/speedscope/source-map.438fa06b.js +24 -0
  23. data/lib/html/speedscope/speedscope.44364064.js +200 -0
  24. data/lib/html/vendor.js +5 -5
  25. data/lib/mini_profiler/asset_version.rb +1 -1
  26. data/lib/mini_profiler/client_settings.rb +6 -5
  27. data/lib/mini_profiler/config.rb +28 -2
  28. data/lib/mini_profiler/profiler.rb +104 -25
  29. data/lib/mini_profiler/profiling_methods.rb +11 -2
  30. data/lib/mini_profiler/snapshots_transporter.rb +109 -0
  31. data/lib/mini_profiler/storage/abstract_store.rb +14 -8
  32. data/lib/mini_profiler/timer_struct/page.rb +57 -4
  33. data/lib/mini_profiler/timer_struct/sql.rb +2 -2
  34. data/lib/mini_profiler/version.rb +1 -1
  35. data/lib/mini_profiler_rails/railtie.rb +1 -1
  36. data/lib/patches/db/mysql2.rb +4 -27
  37. data/lib/patches/db/mysql2/alias_method.rb +30 -0
  38. data/lib/patches/db/mysql2/prepend.rb +34 -0
  39. data/lib/prepend_mysql2_patch.rb +5 -0
  40. data/lib/rack-mini-profiler.rb +1 -0
  41. data/rack-mini-profiler.gemspec +6 -4
  42. metadata +61 -14
@@ -52,6 +52,11 @@ module Rack
52
52
  @toggle_shortcut = 'alt+p'
53
53
  @html_container = 'body'
54
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
55
60
 
56
61
  self
57
62
  }
@@ -63,19 +68,40 @@ module Rack
63
68
  :flamegraph_sample_rate, :logger, :pre_authorize_cb, :skip_paths,
64
69
  :skip_schema_queries, :storage, :storage_failure, :storage_instance,
65
70
  :storage_options, :user_provider, :enable_advanced_debugging_tools,
66
- :snapshot_every_n_requests, :snapshots_limit
67
- attr_accessor :skip_sql_param_names, :suppress_encoding, :max_sql_param_length
71
+ :skip_sql_param_names, :suppress_encoding, :max_sql_param_length
68
72
 
69
73
  # ui accessors
70
74
  attr_accessor :collapse_results, :max_traces_to_show, :position,
71
75
  :show_children, :show_controls, :show_trivial, :show_total_sql_count,
72
76
  :start_hidden, :toggle_shortcut, :html_container
73
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
+
74
84
  # Deprecated options
75
85
  attr_accessor :use_existing_jquery
76
86
 
77
87
  attr_reader :assets_url
78
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
+
79
105
  def assets_url=(lmbda)
80
106
  if defined?(Rack::MiniProfilerRails)
81
107
  Rack::MiniProfilerRails.create_engine
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'cgi'
4
+
3
5
  module Rack
4
6
  class MiniProfiler
5
7
  class << self
@@ -39,6 +41,7 @@ module Rack
39
41
  def current=(c)
40
42
  # we use TLS cause we need access to this from sql blocks and code blocks that have no access to env
41
43
  Thread.current[:mini_profiler_snapshot_custom_fields] = nil
44
+ Thread.current[:mp_ongoing_snapshot] = nil
42
45
  Thread.current[:mini_profiler_private] = c
43
46
  end
44
47
 
@@ -94,6 +97,16 @@ module Rack
94
97
  params
95
98
  end
96
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
97
110
  end
98
111
 
99
112
  #
@@ -169,6 +182,7 @@ module Rack
169
182
 
170
183
  return serve_results(env) if file_name.eql?('results')
171
184
  return handle_snapshots_request(env) if file_name.eql?('snapshots')
185
+ return serve_flamegraph(env) if file_name.eql?('flamegraph')
172
186
 
173
187
  resources_env = env.dup
174
188
  resources_env['PATH_INFO'] = file_name
@@ -200,7 +214,7 @@ module Rack
200
214
  def call(env)
201
215
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
202
216
  client_settings = ClientSettings.new(env, @storage, start)
203
- MiniProfiler.deauthorize_request if @config.authorization_mode == :whitelist
217
+ MiniProfiler.deauthorize_request if @config.authorization_mode == :allow_authorized
204
218
 
205
219
  status = headers = body = nil
206
220
  query_string = env['QUERY_STRING']
@@ -226,7 +240,7 @@ module Rack
226
240
  skip_it = (@config.pre_authorize_cb && !@config.pre_authorize_cb.call(env))
227
241
 
228
242
  if skip_it || (
229
- @config.authorization_mode == :whitelist &&
243
+ @config.authorization_mode == :allow_authorized &&
230
244
  !client_settings.has_valid_cookie?
231
245
  )
232
246
  if take_snapshot?(path)
@@ -268,6 +282,15 @@ module Rack
268
282
  # profile memory
269
283
  if query_string =~ /pp=profile-memory/
270
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
+
271
294
  query_params = Rack::Utils.parse_nested_query(query_string)
272
295
  options = {
273
296
  ignore_files: query_params['memory_profiler_ignore_files'],
@@ -323,24 +346,28 @@ module Rack
323
346
  # Prevent response body from being compressed
324
347
  env['HTTP_ACCEPT_ENCODING'] = 'identity' if config.suppress_encoding
325
348
 
326
- if query_string =~ /pp=flamegraph/
327
- unless defined?(Flamegraph) && Flamegraph.respond_to?(:generate)
328
-
329
- flamegraph = "Please install the flamegraph gem and require it: add gem 'flamegraph' to your Gemfile"
330
- 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])
331
355
  else
332
356
  # do not sully our profile with mini profiler timings
333
357
  current.measure = false
334
358
  match_data = query_string.match(/flamegraph_sample_rate=([\d\.]+)/)
335
359
 
336
- mode = query_string =~ /mode=c/ ? :c : :ruby
337
-
338
360
  if match_data && !match_data[1].to_f.zero?
339
361
  sample_rate = match_data[1].to_f
340
362
  else
341
363
  sample_rate = config.flamegraph_sample_rate
342
364
  end
343
- 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
344
371
  status, headers, body = @app.call(env)
345
372
  end
346
373
  end
@@ -363,7 +390,7 @@ module Rack
363
390
 
364
391
  skip_it = current.discard
365
392
 
366
- if (config.authorization_mode == :whitelist && !MiniProfiler.request_authorized?)
393
+ if (config.authorization_mode == :allow_authorized && !MiniProfiler.request_authorized?)
367
394
  skip_it = true
368
395
  end
369
396
 
@@ -404,9 +431,12 @@ module Rack
404
431
  page_struct[:user] = user(env)
405
432
  page_struct[:root].record_time((Process.clock_gettime(Process::CLOCK_MONOTONIC) - start) * 1000)
406
433
 
407
- if flamegraph
434
+ if flamegraph && query_string =~ /pp=flamegraph/
408
435
  body.close if body.respond_to? :close
409
- 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
410
440
  end
411
441
 
412
442
  begin
@@ -623,13 +653,14 @@ Append the following to your query string:
623
653
  #{make_link "full-backtrace", env} #{"(*) " if client_settings.backtrace_full?}: enable full backtraces for SQL executed (use pp=normal-backtrace to disable)
624
654
  #{make_link "disable", env} : disable profiling for this session
625
655
  #{make_link "enable", env} : enable profiling for this session (if previously disabled)
626
- #{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
627
657
  #{make_link "profile-memory", env} : requires the memory_profiler gem, new location based report
628
- #{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).
629
660
  #{make_link "flamegraph&flamegraph_sample_rate=1", env}: creates a flamegraph with the specified sample rate (in ms). Overrides value set in config
630
- #{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.
631
- #{make_link "trace-exceptions", env} : requires Ruby 2.0, will return all the spots where your application raises exceptions
632
- #{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
633
664
  </pre>
634
665
  </body>
635
666
  </html>
@@ -638,9 +669,33 @@ Append the following to your query string:
638
669
  [200, headers, [body]]
639
670
  end
640
671
 
641
- def flamegraph(graph)
672
+ def flamegraph(graph, path)
642
673
  headers = { 'Content-Type' => 'text/html' }
643
- [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]]
644
699
  end
645
700
 
646
701
  def ids(env)
@@ -689,7 +744,8 @@ Append the following to your query string:
689
744
  toggleShortcut: @config.toggle_shortcut,
690
745
  startHidden: @config.start_hidden,
691
746
  collapseResults: @config.collapse_results,
692
- htmlContainer: @config.html_container
747
+ htmlContainer: @config.html_container,
748
+ hiddenCustomFields: @config.snapshot_hidden_custom_fields.join(',')
693
749
  }
694
750
 
695
751
  if current && current.page_struct
@@ -770,6 +826,24 @@ Append the following to your query string:
770
826
  response.finish
771
827
  end
772
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
+
773
847
  def rails_route_from_path(path, method)
774
848
  if defined?(Rails) && defined?(ActionController::RoutingError)
775
849
  hash = Rails.application.routes.recognize_path(path, method: method)
@@ -799,6 +873,7 @@ Append the following to your query string:
799
873
 
800
874
  def take_snapshot(env, start)
801
875
  MiniProfiler.create_current(env, @config)
876
+ Thread.current[:mp_ongoing_snapshot] = true
802
877
  results = @app.call(env)
803
878
  status = results[0].to_i
804
879
  if status >= 200 && status < 300
@@ -808,10 +883,14 @@ Append the following to your query string:
808
883
  )
809
884
  custom_fields = MiniProfiler.get_snapshot_custom_fields
810
885
  page_struct[:custom_fields] = custom_fields if custom_fields
811
- @storage.push_snapshot(
812
- page_struct,
813
- @config
814
- )
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
815
894
  end
816
895
  self.current = nil
817
896
  results
@@ -7,7 +7,14 @@ module Rack
7
7
  def record_sql(query, elapsed_ms, params = nil)
8
8
  return unless current && current.current_timer
9
9
  c = current
10
- c.current_timer.add_sql(query, elapsed_ms, c.page_struct, params, c.skip_backtrace, c.full_backtrace)
10
+ c.current_timer.add_sql(
11
+ redact_sql_queries? ? nil : query,
12
+ elapsed_ms,
13
+ c.page_struct,
14
+ redact_sql_queries? ? nil : params,
15
+ c.skip_backtrace,
16
+ c.full_backtrace
17
+ )
11
18
  end
12
19
 
13
20
  def start_step(name)
@@ -108,6 +115,9 @@ module Rack
108
115
  end
109
116
  end
110
117
  end
118
+ if klass.respond_to?(:ruby2_keywords, true)
119
+ klass.send(:ruby2_keywords, with_profiling)
120
+ end
111
121
  klass.send :alias_method, method, with_profiling
112
122
  end
113
123
 
@@ -147,7 +157,6 @@ module Rack
147
157
  def clean_method_name(method)
148
158
  method.to_s.gsub(/[\?\!]/, "")
149
159
  end
150
-
151
160
  end
152
161
  end
153
162
  end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ class ::Rack::MiniProfiler::SnapshotsTransporter
4
+ @@transported_snapshots_count = 0
5
+ @@successful_http_requests_count = 0
6
+ @@failed_http_requests_count = 0
7
+
8
+ class << self
9
+ def transported_snapshots_count
10
+ @@transported_snapshots_count
11
+ end
12
+ def successful_http_requests_count
13
+ @@successful_http_requests_count
14
+ end
15
+ def failed_http_requests_count
16
+ @@failed_http_requests_count
17
+ end
18
+
19
+ def transport(snapshot)
20
+ @transporter ||= self.new(Rack::MiniProfiler.config)
21
+ @transporter.ship(snapshot)
22
+ end
23
+ end
24
+
25
+ attr_reader :buffer
26
+ attr_accessor :max_buffer_size, :gzip_requests
27
+
28
+ def initialize(config)
29
+ @uri = URI(config.snapshots_transport_destination_url)
30
+ @auth_key = config.snapshots_transport_auth_key
31
+ @gzip_requests = config.snapshots_transport_gzip_requests
32
+ @thread = nil
33
+ @thread_mutex = Mutex.new
34
+ @buffer = []
35
+ @buffer_mutex = Mutex.new
36
+ @max_buffer_size = 100
37
+ @consecutive_failures_count = 0
38
+ @testing = false
39
+ end
40
+
41
+ def ship(snapshot)
42
+ @buffer_mutex.synchronize do
43
+ @buffer << snapshot
44
+ @buffer.shift if @buffer.size > @max_buffer_size
45
+ end
46
+ @thread_mutex.synchronize { start_thread }
47
+ end
48
+
49
+ def flush_buffer
50
+ buffer_content = @buffer_mutex.synchronize do
51
+ @buffer.dup if @buffer.size > 0
52
+ end
53
+ if buffer_content
54
+ headers = {
55
+ 'Content-Type' => 'application/json',
56
+ 'Mini-Profiler-Transport-Auth' => @auth_key
57
+ }
58
+ json = { snapshots: buffer_content }.to_json
59
+ body = if @gzip_requests
60
+ require 'zlib'
61
+ io = StringIO.new
62
+ gzip_writer = Zlib::GzipWriter.new(io)
63
+ gzip_writer.write(json)
64
+ gzip_writer.close
65
+ headers['Content-Encoding'] = 'gzip'
66
+ io.string
67
+ else
68
+ json
69
+ end
70
+ request = Net::HTTP::Post.new(@uri, headers)
71
+ request.body = body
72
+ http = Net::HTTP.new(@uri.hostname, @uri.port)
73
+ http.use_ssl = @uri.scheme == 'https'
74
+ res = http.request(request)
75
+ if res.code.to_i == 200
76
+ @@successful_http_requests_count += 1
77
+ @@transported_snapshots_count += buffer_content.size
78
+ @buffer_mutex.synchronize do
79
+ @buffer -= buffer_content
80
+ end
81
+ @consecutive_failures_count = 0
82
+ else
83
+ @@failed_http_requests_count += 1
84
+ @consecutive_failures_count += 1
85
+ end
86
+ end
87
+ end
88
+
89
+ def requests_interval
90
+ [30 + backoff_delay, 60 * 60].min
91
+ end
92
+
93
+ private
94
+
95
+ def backoff_delay
96
+ return 0 if @consecutive_failures_count == 0
97
+ 2**@consecutive_failures_count
98
+ end
99
+
100
+ def start_thread
101
+ return if @thread&.alive? || @testing
102
+ @thread = Thread.new do
103
+ while true
104
+ sleep requests_interval
105
+ flush_buffer
106
+ end
107
+ end
108
+ end
109
+ end
@@ -36,7 +36,7 @@ module Rack
36
36
  ""
37
37
  end
38
38
 
39
- # a list of tokens that are permitted to access profiler in whitelist mode
39
+ # a list of tokens that are permitted to access profiler in explicit mode
40
40
  def allowed_tokens
41
41
  raise NotImplementedError.new("allowed_tokens is not implemented")
42
42
  end
@@ -58,17 +58,21 @@ module Rack
58
58
  fetch_snapshots do |batch|
59
59
  batch.each do |snapshot|
60
60
  group_name = default_snapshot_grouping(snapshot)
61
- if !groups[group_name] || groups[group_name] < snapshot.duration_ms
62
- groups[group_name] = snapshot.duration_ms
61
+ hash = groups[group_name] ||= {}
62
+ hash[:snapshots_count] ||= 0
63
+ hash[:snapshots_count] += 1
64
+ if !hash[:worst_score] || hash[:worst_score] < snapshot.duration_ms
65
+ groups[group_name][:worst_score] = snapshot.duration_ms
66
+ end
67
+ if !hash[:best_score] || hash[:best_score] > snapshot.duration_ms
68
+ groups[group_name][:best_score] = snapshot.duration_ms
63
69
  end
64
70
  end
65
71
  end
66
72
  groups = groups.to_a
67
- groups.sort_by! { |name, score| score }
73
+ groups.sort_by! { |name, hash| hash[:worst_score] }
68
74
  groups.reverse!
69
- groups.map! do |name, score|
70
- { name: name, worst_score: score }
71
- end
75
+ groups.map! { |name, hash| hash.merge(name: name) }
72
76
  groups
73
77
  end
74
78
 
@@ -81,7 +85,9 @@ module Rack
81
85
  data << {
82
86
  id: snapshot[:id],
83
87
  duration: snapshot.duration_ms,
84
- timestamp: snapshot[:started_at]
88
+ sql_count: snapshot[:sql_count],
89
+ timestamp: snapshot[:started_at],
90
+ custom_fields: snapshot[:custom_fields]
85
91
  }
86
92
  end
87
93
  end