rack-mini-profiler 1.0.2 → 3.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +138 -21
  3. data/README.md +201 -94
  4. data/lib/enable_rails_patches.rb +5 -0
  5. data/lib/generators/rack_mini_profiler/USAGE +9 -0
  6. data/lib/generators/rack_mini_profiler/install_generator.rb +13 -0
  7. data/lib/generators/{rack_profiler/templates/rack_profiler.rb → rack_mini_profiler/templates/rack_mini_profiler.rb} +1 -1
  8. data/lib/generators/rack_profiler/install_generator.rb +6 -3
  9. data/lib/html/dot.1.1.2.min.js +2 -0
  10. data/lib/html/includes.css +144 -45
  11. data/lib/html/includes.js +1423 -1009
  12. data/lib/html/includes.scss +538 -441
  13. data/lib/html/includes.tmpl +231 -148
  14. data/lib/html/pretty-print.js +810 -0
  15. data/lib/html/profile_handler.js +1 -1
  16. data/lib/html/rack-mini-profiler.css +3 -0
  17. data/lib/html/rack-mini-profiler.js +2 -0
  18. data/lib/html/share.html +0 -1
  19. data/lib/html/speedscope/LICENSE +21 -0
  20. data/lib/html/speedscope/README.md +3 -0
  21. data/lib/html/speedscope/demangle-cpp.1768f4cc.js +4 -0
  22. data/lib/html/speedscope/favicon-16x16.f74b3187.png +0 -0
  23. data/lib/html/speedscope/favicon-32x32.bc503437.png +0 -0
  24. data/lib/html/speedscope/file-format-schema.json +324 -0
  25. data/lib/html/speedscope/fonts/source-code-pro-regular.css +8 -0
  26. data/lib/html/speedscope/fonts/source-code-pro-v13-regular.woff +0 -0
  27. data/lib/html/speedscope/fonts/source-code-pro-v13-regular.woff2 +0 -0
  28. data/lib/html/speedscope/import.cf0fa83f.js +115 -0
  29. data/lib/html/speedscope/index.html +2 -0
  30. data/lib/html/speedscope/release.txt +3 -0
  31. data/lib/html/speedscope/reset.8c46b7a1.css +2 -0
  32. data/lib/html/speedscope/source-map.438fa06b.js +24 -0
  33. data/lib/html/speedscope/speedscope.44364064.js +200 -0
  34. data/lib/html/vendor.js +848 -0
  35. data/lib/mini_profiler/asset_version.rb +3 -2
  36. data/lib/mini_profiler/client_settings.rb +15 -7
  37. data/lib/mini_profiler/config.rb +51 -5
  38. data/lib/mini_profiler/gc_profiler.rb +1 -1
  39. data/lib/mini_profiler/profiling_methods.rb +13 -8
  40. data/lib/mini_profiler/snapshots_transporter.rb +109 -0
  41. data/lib/mini_profiler/storage/abstract_store.rb +52 -1
  42. data/lib/mini_profiler/storage/file_store.rb +7 -3
  43. data/lib/mini_profiler/storage/memcache_store.rb +13 -7
  44. data/lib/mini_profiler/storage/memory_store.rb +98 -5
  45. data/lib/mini_profiler/storage/redis_store.rb +226 -3
  46. data/lib/mini_profiler/storage.rb +7 -0
  47. data/lib/mini_profiler/timer_struct/base.rb +2 -0
  48. data/lib/mini_profiler/timer_struct/custom.rb +1 -0
  49. data/lib/mini_profiler/timer_struct/page.rb +60 -4
  50. data/lib/mini_profiler/timer_struct/request.rb +53 -11
  51. data/lib/mini_profiler/timer_struct/sql.rb +6 -2
  52. data/lib/mini_profiler/timer_struct.rb +8 -0
  53. data/lib/mini_profiler/version.rb +2 -1
  54. data/lib/{mini_profiler/profiler.rb → mini_profiler.rb} +394 -82
  55. data/lib/mini_profiler_rails/railtie.rb +88 -7
  56. data/lib/mini_profiler_rails/railtie_methods.rb +61 -0
  57. data/lib/patches/db/activerecord.rb +1 -12
  58. data/lib/patches/db/mongo.rb +1 -1
  59. data/lib/patches/db/moped.rb +1 -1
  60. data/lib/patches/db/mysql2/alias_method.rb +30 -0
  61. data/lib/patches/db/mysql2/prepend.rb +34 -0
  62. data/lib/patches/db/mysql2.rb +4 -27
  63. data/lib/patches/db/plucky.rb +4 -4
  64. data/lib/patches/db/riak.rb +1 -1
  65. data/lib/patches/net_patches.rb +21 -10
  66. data/lib/patches/sql_patches.rb +13 -5
  67. data/lib/prepend_mysql2_patch.rb +5 -0
  68. data/lib/prepend_net_http_patch.rb +5 -0
  69. data/lib/rack-mini-profiler.rb +1 -24
  70. data/rack-mini-profiler.gemspec +17 -8
  71. metadata +156 -32
  72. data/lib/html/jquery.1.7.1.js +0 -4
  73. data/lib/html/jquery.tmpl.js +0 -486
  74. data/lib/html/list.css +0 -9
  75. data/lib/html/list.js +0 -38
  76. data/lib/html/list.tmpl +0 -34
@@ -1,5 +1,6 @@
1
+ # frozen_string_literal: true
1
2
  module Rack
2
3
  class MiniProfiler
3
- ASSET_VERSION = '355f78011d9b95de14a5b1014b088681'.freeze
4
+ ASSET_VERSION = '35a79b300ab5afa978cb59af0b05e059'
4
5
  end
5
- end
6
+ end
@@ -42,7 +42,7 @@ module Rack
42
42
  def handle_cookie(result)
43
43
  status, headers, _body = result
44
44
 
45
- if (MiniProfiler.config.authorization_mode == :whitelist && !MiniProfiler.request_authorized?)
45
+ if (MiniProfiler.config.authorization_mode == :allow_authorized && !MiniProfiler.request_authorized?)
46
46
  # this is non-obvious, don't kill the profiling cookie on errors or short requests
47
47
  # this ensures that stuff that never reaches the rails stack does not kill profiling
48
48
  if status.to_i >= 200 && status.to_i < 300 && ((Process.clock_gettime(Process::CLOCK_MONOTONIC) - @start) > 0.1)
@@ -59,7 +59,7 @@ module Rack
59
59
 
60
60
  tokens_changed = false
61
61
 
62
- if MiniProfiler.request_authorized? && MiniProfiler.config.authorization_mode == :whitelist
62
+ if MiniProfiler.request_authorized? && MiniProfiler.config.authorization_mode == :allow_authorized
63
63
  @allowed_tokens ||= @store.allowed_tokens
64
64
  tokens_changed = !@orig_auth_tokens || ((@allowed_tokens - @orig_auth_tokens).length > 0)
65
65
  end
@@ -74,25 +74,33 @@ module Rack
74
74
  settings["bt"] = @backtrace_level if @backtrace_level
75
75
  settings["a"] = @allowed_tokens.join("|") if @allowed_tokens && MiniProfiler.request_authorized?
76
76
  settings_string = settings.map { |k, v| "#{k}=#{v}" }.join(",")
77
- cookie = { value: settings_string, path: '/', httponly: true }
77
+ cookie = { value: settings_string, path: MiniProfiler.config.cookie_path, httponly: true }
78
78
  cookie[:secure] = true if @request.ssl?
79
+ cookie[:same_site] = 'Lax'
79
80
  Rack::Utils.set_cookie_header!(headers, COOKIE_NAME, cookie)
80
81
  end
81
82
  end
82
83
 
83
84
  def discard_cookie!(headers)
84
85
  if @cookie
85
- Rack::Utils.delete_cookie_header!(headers, COOKIE_NAME, path: '/')
86
+ Rack::Utils.delete_cookie_header!(headers, COOKIE_NAME, path: MiniProfiler.config.cookie_path)
86
87
  end
87
88
  end
88
89
 
89
90
  def has_valid_cookie?
90
91
  valid_cookie = !@cookie.nil?
91
92
 
92
- if (MiniProfiler.config.authorization_mode == :whitelist)
93
- @allowed_tokens ||= @store.allowed_tokens
93
+ if (MiniProfiler.config.authorization_mode == :allow_authorized) && valid_cookie
94
+ begin
95
+ @allowed_tokens ||= @store.allowed_tokens
96
+ rescue => e
97
+ if MiniProfiler.config.storage_failure != nil
98
+ MiniProfiler.config.storage_failure.call(e)
99
+ end
100
+ end
94
101
 
95
- valid_cookie = (Array === @orig_auth_tokens) &&
102
+ valid_cookie = @allowed_tokens &&
103
+ (Array === @orig_auth_tokens) &&
96
104
  ((@allowed_tokens & @orig_auth_tokens).length > 0)
97
105
  end
98
106
 
@@ -17,6 +17,7 @@ module Rack
17
17
  new.instance_eval {
18
18
  @auto_inject = true # automatically inject on every html page
19
19
  @base_url_path = "/mini-profiler-resources/".dup
20
+ @cookie_path = "/".dup
20
21
  @disable_caching = true
21
22
  # called prior to rack chain, to ensure we are allowed to profile
22
23
  @pre_authorize_cb = lambda { |env| true }
@@ -28,15 +29,19 @@ module Rack
28
29
  @authorization_mode = :allow_all
29
30
  @backtrace_threshold_ms = 0
30
31
  @flamegraph_sample_rate = 0.5
32
+ @flamegraph_mode = :wall
31
33
  @storage_failure = Proc.new do |exception|
32
34
  if @logger
33
35
  @logger.warn("MiniProfiler storage failure: #{exception.message}")
34
36
  end
35
37
  end
36
38
  @enabled = true
37
- @disable_env_dump = false
38
39
  @max_sql_param_length = 0 # disable sql parameter collection by default
39
40
  @skip_sql_param_names = /password/ # skips parameters with the name password by default
41
+ @enable_advanced_debugging_tools = false
42
+ @snapshot_every_n_requests = -1
43
+ @max_snapshot_groups = 50
44
+ @max_snapshots_per_group = 15
40
45
 
41
46
  # ui parameters
42
47
  @autorized = true
@@ -47,9 +52,17 @@ module Rack
47
52
  @show_trivial = false
48
53
  @show_total_sql_count = false
49
54
  @start_hidden = false
50
- @toggle_shortcut = 'Alt+P'
55
+ @toggle_shortcut = 'alt+p'
51
56
  @html_container = 'body'
52
57
  @position = "top-left"
58
+ @snapshot_hidden_custom_fields = []
59
+ @snapshots_transport_destination_url = nil
60
+ @snapshots_transport_auth_key = nil
61
+ @snapshots_redact_sql_queries = true
62
+ @snapshots_transport_gzip_requests = false
63
+ @enable_hotwire_turbo_drive_support = false
64
+
65
+ @profile_parameter = "pp"
53
66
 
54
67
  self
55
68
  }
@@ -57,20 +70,53 @@ module Rack
57
70
 
58
71
  attr_accessor :authorization_mode, :auto_inject, :backtrace_ignores,
59
72
  :backtrace_includes, :backtrace_remove, :backtrace_threshold_ms,
60
- :base_url_path, :disable_caching, :disable_env_dump, :enabled,
73
+ :base_url_path, :cookie_path, :disable_caching, :enabled,
61
74
  :flamegraph_sample_rate, :logger, :pre_authorize_cb, :skip_paths,
62
75
  :skip_schema_queries, :storage, :storage_failure, :storage_instance,
63
- :storage_options, :user_provider
64
- attr_accessor :skip_sql_param_names, :suppress_encoding, :max_sql_param_length
76
+ :storage_options, :user_provider, :enable_advanced_debugging_tools,
77
+ :skip_sql_param_names, :suppress_encoding, :max_sql_param_length,
78
+ :content_security_policy_nonce, :enable_hotwire_turbo_drive_support,
79
+ :flamegraph_mode, :profile_parameter
65
80
 
66
81
  # ui accessors
67
82
  attr_accessor :collapse_results, :max_traces_to_show, :position,
68
83
  :show_children, :show_controls, :show_trivial, :show_total_sql_count,
69
84
  :start_hidden, :toggle_shortcut, :html_container
70
85
 
86
+ # snapshot related config
87
+ attr_accessor :snapshot_every_n_requests, :max_snapshots_per_group,
88
+ :snapshot_hidden_custom_fields, :snapshots_transport_destination_url,
89
+ :snapshots_transport_auth_key, :snapshots_redact_sql_queries,
90
+ :snapshots_transport_gzip_requests, :max_snapshot_groups
91
+
71
92
  # Deprecated options
72
93
  attr_accessor :use_existing_jquery
73
94
 
95
+ attr_reader :assets_url
96
+
97
+ # redefined - since the accessor defines it first
98
+ undef :authorization_mode=
99
+ def authorization_mode=(mode)
100
+ if mode == :whitelist
101
+ warn "[DEPRECATION] `:whitelist` authorization mode is deprecated. Please use `:allow_authorized` instead."
102
+
103
+ mode = :allow_authorized
104
+ end
105
+
106
+ warn <<~DEP unless mode == :allow_authorized || mode == :allow_all
107
+ [DEPRECATION] unknown authorization mode #{mode}. Expected `:allow_all` or `:allow_authorized`.
108
+ DEP
109
+
110
+ @authorization_mode = mode
111
+ end
112
+
113
+ def assets_url=(lmbda)
114
+ if defined?(Rack::MiniProfilerRails)
115
+ Rack::MiniProfilerRails.create_engine
116
+ end
117
+ @assets_url = lmbda
118
+ end
119
+
74
120
  def vertical_position
75
121
  position.include?('bottom') ? 'bottom' : 'top'
76
122
  end
@@ -151,7 +151,7 @@ String stats:
151
151
  body << "#{count} : #{string}\n"
152
152
  end
153
153
 
154
- return [200, { 'Content-Type' => 'text/plain' }, body]
154
+ [200, { 'Content-Type' => 'text/plain' }, body]
155
155
  ensure
156
156
  prev_gc_state ? GC.disable : GC.enable
157
157
  end
@@ -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,15 +115,18 @@ 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
 
114
124
  def profile_singleton_method(klass, method, type = :profile, &blk)
115
- profile_method(singleton_class(klass), method, type, &blk)
125
+ profile_method(klass.singleton_class, method, type, &blk)
116
126
  end
117
127
 
118
128
  def unprofile_singleton_method(klass, method)
119
- unprofile_method(singleton_class(klass), method)
129
+ unprofile_method(klass.singleton_class, method)
120
130
  end
121
131
 
122
132
  # Add a custom timing. These are displayed similar to SQL/query time in
@@ -144,14 +154,9 @@ module Rack
144
154
 
145
155
  private
146
156
 
147
- def singleton_class(klass)
148
- class << klass; self; end
149
- end
150
-
151
157
  def clean_method_name(method)
152
158
  method.to_s.gsub(/[\?\!]/, "")
153
159
  end
154
-
155
160
  end
156
161
  end
157
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,11 +36,62 @@ 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
43
43
 
44
+ def should_take_snapshot?(period)
45
+ raise NotImplementedError.new("should_take_snapshot? is not implemented")
46
+ end
47
+
48
+ def push_snapshot(page_struct, group_name, config)
49
+ raise NotImplementedError.new("push_snapshot is not implemented")
50
+ end
51
+
52
+ # returns a hash where the keys are group names and the values
53
+ # are hashes that contain 3 keys:
54
+ # 1. `:worst_score` => the duration of the worst/slowest snapshot in the group (float)
55
+ # 2. `:best_score` => the duration of the best/fastest snapshot in the group (float)
56
+ # 3. `:snapshots_count` => the number of snapshots in the group (integer)
57
+ def fetch_snapshots_overview
58
+ raise NotImplementedError.new("fetch_snapshots_overview is not implemented")
59
+ end
60
+
61
+ # @param group_name [String]
62
+ # @return [Array<Rack::MiniProfiler::TimerStruct::Page>] list of snapshots of the group. Blank array if the group doesn't exist.
63
+ def fetch_snapshots_group(group_name)
64
+ raise NotImplementedError.new("fetch_snapshots_group is not implemented")
65
+ end
66
+
67
+ def load_snapshot(id, group_name)
68
+ raise NotImplementedError.new("load_snapshot is not implemented")
69
+ end
70
+
71
+ def snapshots_overview
72
+ groups = fetch_snapshots_overview.to_a
73
+ groups.sort_by! { |name, hash| hash[:worst_score] }
74
+ groups.reverse!
75
+ groups.map! { |name, hash| hash.merge(name: name) }
76
+ groups
77
+ end
78
+
79
+ def snapshots_group(group_name)
80
+ snapshots = fetch_snapshots_group(group_name)
81
+ data = []
82
+ snapshots.each do |snapshot|
83
+ data << {
84
+ id: snapshot[:id],
85
+ duration: snapshot.duration_ms,
86
+ sql_count: snapshot[:sql_count],
87
+ timestamp: snapshot[:started_at],
88
+ custom_fields: snapshot[:custom_fields]
89
+ }
90
+ end
91
+ data.sort_by! { |s| s[:duration] }
92
+ data.reverse!
93
+ data
94
+ end
44
95
  end
45
96
  end
46
97
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'securerandom'
4
+
3
5
  module Rack
4
6
  class MiniProfiler
5
7
  class FileStore < AbstractStore
@@ -17,9 +19,11 @@ module Rack
17
19
  def [](key)
18
20
  begin
19
21
  data = ::File.open(path(key), "rb") { |f| f.read }
20
- return Marshal.load data
22
+ # rubocop:disable Security/MarshalLoad
23
+ Marshal.load data
24
+ # rubocop:enable Security/MarshalLoad
21
25
  rescue
22
- return nil
26
+ nil
23
27
  end
24
28
  end
25
29
 
@@ -31,7 +35,7 @@ module Rack
31
35
  end
32
36
 
33
37
  private
34
- if RUBY_PLATFORM =~ /mswin(?!ce)|mingw|cygwin|bccwin/
38
+ if Gem.win_platform?
35
39
  def path(key)
36
40
  @path.dup << "/" << @prefix << "_" << key.gsub(/:/, '_')
37
41
  end
@@ -10,8 +10,10 @@ module Rack
10
10
  def initialize(args = nil)
11
11
  require 'dalli' unless defined? Dalli
12
12
  args ||= {}
13
+
13
14
  @prefix = args[:prefix] || "MPMemcacheStore"
14
15
  @prefix += "-#{Rack::MiniProfiler::VERSION}"
16
+
15
17
  @client = args[:client] || Dalli::Client.new
16
18
  @expires_in_seconds = args[:expires_in] || EXPIRES_IN_SECONDS
17
19
  end
@@ -22,7 +24,9 @@ module Rack
22
24
 
23
25
  def load(id)
24
26
  raw = @client.get("#{@prefix}#{id}")
25
- Marshal::load(raw) if raw
27
+ # rubocop:disable Security/MarshalLoad
28
+ Marshal.load(raw) if raw
29
+ # rubocop:enable Security/MarshalLoad
26
30
  end
27
31
 
28
32
  def set_unviewed(user, id)
@@ -63,14 +67,16 @@ module Rack
63
67
  key1, key2, cycle_at = nil
64
68
 
65
69
  if token_info
66
- key1, key2, cycle_at = Marshal::load(token_info)
70
+ # rubocop:disable Security/MarshalLoad
71
+ key1, key2, cycle_at = Marshal.load(token_info)
72
+ # rubocop:enable Security/MarshalLoad
67
73
 
68
- key1 = nil unless key1 && key1.length == 32
69
- key2 = nil unless key2 && key2.length == 32
74
+ key1 = nil unless key1 && key1.length == 32
75
+ key2 = nil unless key2 && key2.length == 32
70
76
 
71
- if key1 && cycle_at && (cycle_at > Process.clock_gettime(Process::CLOCK_MONOTONIC))
72
- return [key1, key2].compact
73
- end
77
+ if key1 && cycle_at && (cycle_at > Process.clock_gettime(Process::CLOCK_MONOTONIC))
78
+ return [key1, key2].compact
79
+ end
74
80
  end
75
81
 
76
82
  timeout = Rack::MiniProfiler::AbstractStore::MAX_TOKEN_AGE
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'securerandom'
4
+
3
5
  module Rack
4
6
  class MiniProfiler
5
7
  class MemoryStore < AbstractStore
@@ -52,17 +54,22 @@ module Rack
52
54
  @expires_in_seconds = args.fetch(:expires_in) { EXPIRES_IN_SECONDS }
53
55
 
54
56
  @token1, @token2, @cycle_at = nil
57
+ @snapshots_cycle = 0
58
+ @snapshot_groups = {}
59
+ @snapshots = []
55
60
 
56
61
  initialize_locks
57
62
  initialize_cleanup_thread(args)
58
63
  end
59
64
 
60
65
  def initialize_locks
61
- @token_lock = Mutex.new
62
- @timer_struct_lock = Mutex.new
63
- @user_view_lock = Mutex.new
64
- @timer_struct_cache = {}
65
- @user_view_cache = {}
66
+ @token_lock = Mutex.new
67
+ @timer_struct_lock = Mutex.new
68
+ @user_view_lock = Mutex.new
69
+ @snapshots_cycle_lock = Mutex.new
70
+ @snapshots_lock = Mutex.new
71
+ @timer_struct_cache = {}
72
+ @user_view_cache = {}
66
73
  end
67
74
 
68
75
  #FIXME: use weak ref, trouble it may be broken in 1.9 so need to use the 'ref' gem
@@ -135,6 +142,92 @@ module Rack
135
142
 
136
143
  end
137
144
  end
145
+
146
+ def should_take_snapshot?(period)
147
+ @snapshots_cycle_lock.synchronize do
148
+ @snapshots_cycle += 1
149
+ if @snapshots_cycle % period == 0
150
+ @snapshots_cycle = 0
151
+ true
152
+ else
153
+ false
154
+ end
155
+ end
156
+ end
157
+
158
+ def push_snapshot(page_struct, group_name, config)
159
+ @snapshots_lock.synchronize do
160
+ group = @snapshot_groups[group_name]
161
+ if !group
162
+ @snapshot_groups[group_name] = {
163
+ worst_score: page_struct.duration_ms,
164
+ best_score: page_struct.duration_ms,
165
+ snapshots: [page_struct]
166
+ }
167
+ if @snapshot_groups.size > config.max_snapshot_groups
168
+ group_keys = @snapshot_groups.keys
169
+ group_keys.sort_by! do |key|
170
+ @snapshot_groups[key][:worst_score]
171
+ end
172
+ group_keys.reverse!
173
+ group_keys.pop(group_keys.size - config.max_snapshot_groups)
174
+ @snapshot_groups = @snapshot_groups.slice(*group_keys)
175
+ end
176
+ else
177
+ snapshots = group[:snapshots]
178
+ snapshots << page_struct
179
+ snapshots.sort_by!(&:duration_ms)
180
+ snapshots.reverse!
181
+ if snapshots.size > config.max_snapshots_per_group
182
+ snapshots.pop(snapshots.size - config.max_snapshots_per_group)
183
+ end
184
+ group[:worst_score] = snapshots[0].duration_ms
185
+ group[:best_score] = snapshots[-1].duration_ms
186
+ end
187
+ end
188
+ end
189
+
190
+ def fetch_snapshots_overview
191
+ @snapshots_lock.synchronize do
192
+ groups = {}
193
+ @snapshot_groups.each do |name, group|
194
+ groups[name] = {
195
+ worst_score: group[:worst_score],
196
+ best_score: group[:best_score],
197
+ snapshots_count: group[:snapshots].size
198
+ }
199
+ end
200
+ groups
201
+ end
202
+ end
203
+
204
+ def fetch_snapshots_group(group_name)
205
+ @snapshots_lock.synchronize do
206
+ group = @snapshot_groups[group_name]
207
+ if group
208
+ group[:snapshots].dup
209
+ else
210
+ []
211
+ end
212
+ end
213
+ end
214
+
215
+ def load_snapshot(id, group_name)
216
+ @snapshots_lock.synchronize do
217
+ group = @snapshot_groups[group_name]
218
+ if group
219
+ group[:snapshots].find { |s| s[:id] == id }
220
+ end
221
+ end
222
+ end
223
+
224
+ private
225
+
226
+ # used in tests only
227
+ def wipe_snapshots_data
228
+ @snapshots_cycle = 0
229
+ @snapshot_groups = {}
230
+ end
138
231
  end
139
232
  end
140
233
  end