rack-mini-profiler 2.0.3 → 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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -0
  3. data/README.md +43 -5
  4. data/lib/html/includes.css +38 -0
  5. data/lib/html/includes.js +265 -175
  6. data/lib/html/includes.scss +35 -4
  7. data/lib/html/includes.tmpl +92 -2
  8. data/lib/html/profile_handler.js +1 -1
  9. data/lib/html/rack-mini-profiler.css +3 -0
  10. data/lib/html/rack-mini-profiler.js +2 -0
  11. data/lib/html/speedscope/LICENSE +21 -0
  12. data/lib/html/speedscope/README.md +3 -0
  13. data/lib/html/speedscope/demangle-cpp.1768f4cc.js +4 -0
  14. data/lib/html/speedscope/favicon-16x16.f74b3187.png +0 -0
  15. data/lib/html/speedscope/favicon-32x32.bc503437.png +0 -0
  16. data/lib/html/speedscope/file-format-schema.json +324 -0
  17. data/lib/html/speedscope/import.cf0fa83f.js +115 -0
  18. data/lib/html/speedscope/index.html +2 -0
  19. data/lib/html/speedscope/release.txt +3 -0
  20. data/lib/html/speedscope/reset.8c46b7a1.css +2 -0
  21. data/lib/html/speedscope/source-map.438fa06b.js +24 -0
  22. data/lib/html/speedscope/speedscope.44364064.js +200 -0
  23. data/lib/html/vendor.js +10 -2
  24. data/lib/mini_profiler/asset_version.rb +1 -1
  25. data/lib/mini_profiler/client_settings.rb +3 -2
  26. data/lib/mini_profiler/config.rb +24 -2
  27. data/lib/mini_profiler/profiler.rb +214 -22
  28. data/lib/mini_profiler/profiling_methods.rb +8 -1
  29. data/lib/mini_profiler/snapshots_transporter.rb +109 -0
  30. data/lib/mini_profiler/storage/abstract_store.rb +78 -0
  31. data/lib/mini_profiler/storage/memory_store.rb +54 -5
  32. data/lib/mini_profiler/storage/redis_store.rb +134 -0
  33. data/lib/mini_profiler/timer_struct/page.rb +52 -2
  34. data/lib/mini_profiler/timer_struct/sql.rb +2 -2
  35. data/lib/mini_profiler/version.rb +1 -1
  36. data/lib/mini_profiler_rails/railtie.rb +11 -0
  37. data/lib/mini_profiler_rails/railtie_methods.rb +1 -1
  38. data/lib/rack-mini-profiler.rb +1 -0
  39. data/rack-mini-profiler.gemspec +5 -4
  40. metadata +43 -14
@@ -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
@@ -38,9 +40,21 @@ module Rack
38
40
 
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
43
+ Thread.current[:mini_profiler_snapshot_custom_fields] = nil
44
+ Thread.current[:mp_ongoing_snapshot] = nil
41
45
  Thread.current[:mini_profiler_private] = c
42
46
  end
43
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
+
44
58
  # discard existing results, don't track this request
45
59
  def discard_results
46
60
  self.current.discard = true if current
@@ -83,6 +97,16 @@ module Rack
83
97
  params
84
98
  end
85
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
86
110
  end
87
111
 
88
112
  #
@@ -106,14 +130,23 @@ module Rack
106
130
  def serve_results(env)
107
131
  request = Rack::Request.new(env)
108
132
  id = request.params['id']
109
- page_struct = @storage.load(id)
110
- unless page_struct
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
111
144
  @storage.set_viewed(user(env), id)
112
- id = ERB::Util.html_escape(request.params['id'])
145
+ id = ERB::Util.html_escape(id)
113
146
  user_info = ERB::Util.html_escape(user(env))
114
147
  return [404, {}, ["Request not found: #{id} - user #{user_info}"]]
115
148
  end
116
- unless page_struct[:has_user_viewed]
149
+ if !page_struct[:has_user_viewed] && !is_snapshot
117
150
  page_struct[:client_timings] = TimerStruct::Client.init_from_form_data(env, page_struct)
118
151
  page_struct[:has_user_viewed] = true
119
152
  @storage.save(page_struct)
@@ -148,11 +181,12 @@ module Rack
148
181
  file_name = path.sub(@config.base_url_path, '')
149
182
 
150
183
  return serve_results(env) if file_name.eql?('results')
184
+ return handle_snapshots_request(env) if file_name.eql?('snapshots')
151
185
 
152
186
  resources_env = env.dup
153
187
  resources_env['PATH_INFO'] = file_name
154
188
 
155
- rack_file = Rack::File.new(MiniProfiler.resources_root, 'Cache-Control' => "max-age:#{cache_control_value}")
189
+ rack_file = Rack::File.new(MiniProfiler.resources_root, 'Cache-Control' => "max-age=#{cache_control_value}")
156
190
  rack_file.call(resources_env)
157
191
  end
158
192
 
@@ -177,7 +211,6 @@ module Rack
177
211
  end
178
212
 
179
213
  def call(env)
180
-
181
214
  start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
182
215
  client_settings = ClientSettings.new(env, @storage, start)
183
216
  MiniProfiler.deauthorize_request if @config.authorization_mode == :whitelist
@@ -189,15 +222,31 @@ module Rack
189
222
  # Someone (e.g. Rails engine) could change the SCRIPT_NAME so we save it
190
223
  env['RACK_MINI_PROFILER_ORIGINAL_SCRIPT_NAME'] = ENV['PASSENGER_BASE_URI'] || env['SCRIPT_NAME']
191
224
 
192
- skip_it = (@config.pre_authorize_cb && !@config.pre_authorize_cb.call(env)) ||
193
- (@config.skip_paths && @config.skip_paths.any? { |p| path.start_with?(p) }) ||
194
- 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))
195
240
 
196
241
  if skip_it || (
197
242
  @config.authorization_mode == :whitelist &&
198
243
  !client_settings.has_valid_cookie?
199
244
  )
200
- 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
201
250
  end
202
251
 
203
252
  # handle all /mini-profiler requests here
@@ -288,23 +337,26 @@ module Rack
288
337
  env['HTTP_ACCEPT_ENCODING'] = 'identity' if config.suppress_encoding
289
338
 
290
339
  if query_string =~ /pp=flamegraph/
291
- unless defined?(Flamegraph) && Flamegraph.respond_to?(:generate)
340
+ unless defined?(StackProf) && StackProf.respond_to?(:run)
292
341
 
293
- flamegraph = "Please install the flamegraph gem and require it: add gem 'flamegraph' to your Gemfile"
342
+ flamegraph = "Please install the stackprof gem and require it: add gem 'stackprof' to your Gemfile"
294
343
  status, headers, body = @app.call(env)
295
344
  else
296
345
  # do not sully our profile with mini profiler timings
297
346
  current.measure = false
298
347
  match_data = query_string.match(/flamegraph_sample_rate=([\d\.]+)/)
299
348
 
300
- mode = query_string =~ /mode=c/ ? :c : :ruby
301
-
302
349
  if match_data && !match_data[1].to_f.zero?
303
350
  sample_rate = match_data[1].to_f
304
351
  else
305
352
  sample_rate = config.flamegraph_sample_rate
306
353
  end
307
- flamegraph = Flamegraph.generate(nil, fidelity: sample_rate, embed_resources: query_string =~ /embed/, mode: mode) do
354
+ flamegraph = StackProf.run(
355
+ mode: :wall,
356
+ raw: true,
357
+ aggregate: false,
358
+ interval: (sample_rate * 1000).to_i
359
+ ) do
308
360
  status, headers, body = @app.call(env)
309
361
  end
310
362
  end
@@ -370,7 +422,7 @@ module Rack
370
422
 
371
423
  if flamegraph
372
424
  body.close if body.respond_to? :close
373
- return client_settings.handle_cookie(self.flamegraph(flamegraph))
425
+ return client_settings.handle_cookie(self.flamegraph(flamegraph, path))
374
426
  end
375
427
 
376
428
  begin
@@ -589,9 +641,9 @@ Append the following to your query string:
589
641
  #{make_link "enable", env} : enable profiling for this session (if previously disabled)
590
642
  #{make_link "profile-gc", env} : perform gc profiling on this request, analyzes ObjectSpace generated by request (ruby 1.9.3 only)
591
643
  #{make_link "profile-memory", env} : requires the memory_profiler gem, new location based report
592
- #{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).
593
645
  #{make_link "flamegraph&flamegraph_sample_rate=1", env}: creates a flamegraph with the specified sample rate (in ms). Overrides value set in config
594
- #{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.
595
647
  #{make_link "trace-exceptions", env} : requires Ruby 2.0, will return all the spots where your application raises exceptions
596
648
  #{make_link "analyze-memory", env} : requires Ruby 2.0, will perform basic memory analysis of heap
597
649
  </pre>
@@ -602,9 +654,37 @@ Append the following to your query string:
602
654
  [200, headers, [body]]
603
655
  end
604
656
 
605
- def flamegraph(graph)
657
+ def flamegraph(graph, path)
606
658
  headers = { 'Content-Type' => 'text/html' }
607
- [200, headers, [graph]]
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
608
688
  end
609
689
 
610
690
  def ids(env)
@@ -628,10 +708,20 @@ Append the following to your query string:
628
708
  # * you do not want script to be automatically appended for the current page. You can also call cancel_auto_inject
629
709
  def get_profile_script(env)
630
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
631
719
 
632
720
  settings = {
633
721
  path: path,
634
- version: MiniProfiler::ASSET_VERSION,
722
+ url: url,
723
+ cssUrl: css_url,
724
+ version: version,
635
725
  verticalPosition: @config.vertical_position,
636
726
  horizontalPosition: @config.horizontal_position,
637
727
  showTrivial: @config.show_trivial,
@@ -643,7 +733,8 @@ Append the following to your query string:
643
733
  toggleShortcut: @config.toggle_shortcut,
644
734
  startHidden: @config.start_hidden,
645
735
  collapseResults: @config.collapse_results,
646
- htmlContainer: @config.html_container
736
+ htmlContainer: @config.html_container,
737
+ hiddenCustomFields: @config.snapshot_hidden_custom_fields.join(',')
647
738
  }
648
739
 
649
740
  if current && current.page_struct
@@ -674,5 +765,106 @@ Append the following to your query string:
674
765
  def cache_control_value
675
766
  86400
676
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
677
869
  end
678
870
  end
@@ -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)
@@ -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