rack-mini-profiler 2.0.3 → 2.3.0

Sign up to get free protection for your applications and to get access to all the features.
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