rack-mini-profiler 1.0.2 → 3.1.1

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 (76) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +145 -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 +1420 -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 +100 -7
  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} +400 -83
  55. data/lib/mini_profiler_rails/railtie.rb +89 -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 = '90a68676a0c0d704b4438ca3f27d46c4'
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
@@ -8,11 +10,11 @@ module Rack
8
10
  class CacheCleanupThread < Thread
9
11
 
10
12
  def initialize(interval, cycle, store)
11
- super
12
13
  @store = store
13
14
  @interval = interval
14
15
  @cycle = cycle
15
16
  @cycle_count = 1
17
+ super
16
18
  end
17
19
 
18
20
  def should_cleanup?
@@ -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
@@ -71,7 +78,7 @@ module Rack
71
78
  cleanup_cycle = args.fetch(:cleanup_cycle) { CLEANUP_CYCLE }
72
79
  t = CacheCleanupThread.new(cleanup_interval, cleanup_cycle, self) do
73
80
  until Thread.current[:should_exit] do
74
- t.sleepy_run
81
+ Thread.current.sleepy_run
75
82
  end
76
83
  end
77
84
  at_exit { t[:should_exit] = true }
@@ -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