rack-mini-profiler 2.0.0 → 2.1.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.
@@ -41,6 +41,78 @@ module Rack
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, config)
49
+ raise NotImplementedError.new("push_snapshot is not implemented")
50
+ end
51
+
52
+ def fetch_snapshots(batch_size: 200, &blk)
53
+ raise NotImplementedError.new("fetch_snapshots is not implemented")
54
+ end
55
+
56
+ def snapshot_groups_overview
57
+ groups = {}
58
+ fetch_snapshots do |batch|
59
+ batch.each do |snapshot|
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
63
+ end
64
+ end
65
+ end
66
+ groups = groups.to_a
67
+ groups.sort_by! { |name, score| score }
68
+ groups.reverse!
69
+ groups.map! do |name, score|
70
+ { name: name, worst_score: score }
71
+ end
72
+ groups
73
+ end
74
+
75
+ def find_snapshots_group(group_name)
76
+ data = []
77
+ fetch_snapshots do |batch|
78
+ batch.each do |snapshot|
79
+ snapshot_group_name = default_snapshot_grouping(snapshot)
80
+ if group_name == snapshot_group_name
81
+ data << {
82
+ id: snapshot[:id],
83
+ duration: snapshot.duration_ms,
84
+ timestamp: snapshot[:started_at]
85
+ }
86
+ end
87
+ end
88
+ end
89
+ data.sort_by! { |s| s[:duration] }
90
+ data.reverse!
91
+ data
92
+ end
93
+
94
+ def load_snapshot(id)
95
+ raise NotImplementedError.new("load_snapshot is not implemented")
96
+ end
97
+
98
+ private
99
+
100
+ def default_snapshot_grouping(snapshot)
101
+ group_name = rails_route_from_path(snapshot[:request_path], snapshot[:request_method])
102
+ group_name ||= snapshot[:request_path]
103
+ "#{snapshot[:request_method]} #{group_name}"
104
+ end
105
+
106
+ def rails_route_from_path(path, method)
107
+ if defined?(Rails) && defined?(ActionController::RoutingError)
108
+ hash = Rails.application.routes.recognize_path(path, method: method)
109
+ if hash && hash[:controller] && hash[:action]
110
+ "#{hash[:controller]}##{hash[:action]}"
111
+ end
112
+ end
113
+ rescue ActionController::RoutingError
114
+ nil
115
+ end
44
116
  end
45
117
  end
46
118
  end
@@ -52,17 +52,21 @@ module Rack
52
52
  @expires_in_seconds = args.fetch(:expires_in) { EXPIRES_IN_SECONDS }
53
53
 
54
54
  @token1, @token2, @cycle_at = nil
55
+ @snapshots_cycle = 0
56
+ @snapshots = []
55
57
 
56
58
  initialize_locks
57
59
  initialize_cleanup_thread(args)
58
60
  end
59
61
 
60
62
  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 = {}
63
+ @token_lock = Mutex.new
64
+ @timer_struct_lock = Mutex.new
65
+ @user_view_lock = Mutex.new
66
+ @snapshots_cycle_lock = Mutex.new
67
+ @snapshots_lock = Mutex.new
68
+ @timer_struct_cache = {}
69
+ @user_view_cache = {}
66
70
  end
67
71
 
68
72
  #FIXME: use weak ref, trouble it may be broken in 1.9 so need to use the 'ref' gem
@@ -135,6 +139,51 @@ module Rack
135
139
 
136
140
  end
137
141
  end
142
+
143
+ def should_take_snapshot?(period)
144
+ @snapshots_cycle_lock.synchronize do
145
+ @snapshots_cycle += 1
146
+ if @snapshots_cycle % period == 0
147
+ @snapshots_cycle = 0
148
+ true
149
+ else
150
+ false
151
+ end
152
+ end
153
+ end
154
+
155
+ def push_snapshot(page_struct, config)
156
+ @snapshots_lock.synchronize do
157
+ @snapshots << page_struct
158
+ @snapshots.sort_by! { |s| s.duration_ms }
159
+ @snapshots.reverse!
160
+ if @snapshots.size > config.snapshots_limit
161
+ @snapshots.slice!(-1)
162
+ end
163
+ end
164
+ end
165
+
166
+ def fetch_snapshots(batch_size: 200, &blk)
167
+ @snapshots_lock.synchronize do
168
+ @snapshots.each_slice(batch_size) do |batch|
169
+ blk.call(batch)
170
+ end
171
+ end
172
+ end
173
+
174
+ def load_snapshot(id)
175
+ @snapshots_lock.synchronize do
176
+ @snapshots.find { |s| s[:id] == id }
177
+ end
178
+ end
179
+
180
+ private
181
+
182
+ # used in tests only
183
+ def wipe_snapshots_data
184
+ @snapshots_cycle = 0
185
+ @snapshots = []
186
+ end
138
187
  end
139
188
  end
140
189
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'digest'
4
+
3
5
  module Rack
4
6
  class MiniProfiler
5
7
  class RedisStore < AbstractStore
@@ -33,7 +35,7 @@ module Rack
33
35
 
34
36
  def set_unviewed(user, id)
35
37
  key = user_key(user)
36
- if redis.exists(prefixed_id(id))
38
+ if redis.call([:exists, prefixed_id(id)]) == 1
37
39
  expire_at = Process.clock_gettime(Process::CLOCK_MONOTONIC).to_i + redis.ttl(prefixed_id(id))
38
40
  redis.zadd(key, expire_at, id)
39
41
  end
@@ -44,7 +46,7 @@ module Rack
44
46
  key = user_key(user)
45
47
  redis.del(key)
46
48
  ids.each do |id|
47
- if redis.exists(prefixed_id(id))
49
+ if redis.call([:exists, prefixed_id(id)]) == 1
48
50
  expire_at = Process.clock_gettime(Process::CLOCK_MONOTONIC).to_i + redis.ttl(prefixed_id(id))
49
51
  redis.zadd(key, expire_at, id)
50
52
  end
@@ -108,6 +110,106 @@ unviewed_ids: #{get_unviewed_ids(user)}
108
110
  [key1, key2].compact
109
111
  end
110
112
 
113
+ COUNTER_LUA = <<~LUA
114
+ if redis.call("INCR", KEYS[1]) % ARGV[1] == 0 then
115
+ redis.call("DEL", KEYS[1])
116
+ return 1
117
+ else
118
+ return 0
119
+ end
120
+ LUA
121
+
122
+ COUNTER_LUA_SHA = Digest::SHA1.hexdigest(COUNTER_LUA)
123
+
124
+ def should_take_snapshot?(period)
125
+ 1 == cached_redis_eval(
126
+ COUNTER_LUA,
127
+ COUNTER_LUA_SHA,
128
+ reraise: false,
129
+ keys: [snapshot_counter_key()],
130
+ argv: [period]
131
+ )
132
+ end
133
+
134
+ def push_snapshot(page_struct, config)
135
+ zset_key = snapshot_zset_key()
136
+ hash_key = snapshot_hash_key()
137
+
138
+ id = page_struct[:id]
139
+ score = page_struct.duration_ms
140
+ limit = config.snapshots_limit
141
+ bytes = Marshal.dump(page_struct)
142
+
143
+ lua = <<~LUA
144
+ local zset_key = KEYS[1]
145
+ local hash_key = KEYS[2]
146
+ local id = ARGV[1]
147
+ local score = tonumber(ARGV[2])
148
+ local bytes = ARGV[3]
149
+ local limit = tonumber(ARGV[4])
150
+ redis.call("ZADD", zset_key, score, id)
151
+ redis.call("HSET", hash_key, id, bytes)
152
+ if redis.call("ZCARD", zset_key) > limit then
153
+ local lowest_snapshot_id = redis.call("ZRANGE", zset_key, 0, 0)[1]
154
+ redis.call("ZREM", zset_key, lowest_snapshot_id)
155
+ redis.call("HDEL", hash_key, lowest_snapshot_id)
156
+ end
157
+ LUA
158
+ redis.eval(
159
+ lua,
160
+ keys: [zset_key, hash_key],
161
+ argv: [id, score, bytes, limit]
162
+ )
163
+ end
164
+
165
+ def fetch_snapshots(batch_size: 200, &blk)
166
+ zset_key = snapshot_zset_key()
167
+ hash_key = snapshot_hash_key()
168
+ iteration = 0
169
+ corrupt_snapshots = []
170
+ while true
171
+ ids = redis.zrange(
172
+ zset_key,
173
+ batch_size * iteration,
174
+ batch_size * iteration + batch_size - 1
175
+ )
176
+ break if ids.size == 0
177
+ batch = redis.mapped_hmget(hash_key, *ids).to_a
178
+ batch.map! do |id, bytes|
179
+ begin
180
+ Marshal.load(bytes)
181
+ rescue
182
+ corrupt_snapshots << id
183
+ nil
184
+ end
185
+ end
186
+ batch.compact!
187
+ blk.call(batch) if batch.size != 0
188
+ break if ids.size < batch_size
189
+ iteration += 1
190
+ end
191
+ if corrupt_snapshots.size > 0
192
+ redis.pipelined do
193
+ redis.zrem(zset_key, corrupt_snapshots)
194
+ redis.hdel(hash_key, corrupt_snapshots)
195
+ end
196
+ end
197
+ end
198
+
199
+ def load_snapshot(id)
200
+ hash_key = snapshot_hash_key()
201
+ bytes = redis.hget(hash_key, id)
202
+ begin
203
+ Marshal.load(bytes)
204
+ rescue
205
+ redis.pipelined do
206
+ redis.zrem(snapshot_zset_key(), id)
207
+ redis.hdel(hash_key, id)
208
+ end
209
+ nil
210
+ end
211
+ end
212
+
111
213
  private
112
214
 
113
215
  def user_key(user)
@@ -125,6 +227,38 @@ unviewed_ids: #{get_unviewed_ids(user)}
125
227
  end
126
228
  end
127
229
 
230
+ def snapshot_counter_key
231
+ @snapshot_counter_key ||= "#{@prefix}-mini-profiler-snapshots-counter"
232
+ end
233
+
234
+ def snapshot_zset_key
235
+ @snapshot_zset_key ||= "#{@prefix}-mini-profiler-snapshots-zset"
236
+ end
237
+
238
+ def snapshot_hash_key
239
+ @snapshot_hash_key ||= "#{@prefix}-mini-profiler-snapshots-hash"
240
+ end
241
+
242
+ def cached_redis_eval(script, script_sha, reraise: true, argv: [], keys: [])
243
+ begin
244
+ redis.evalsha(script_sha, argv: argv, keys: keys)
245
+ rescue ::Redis::CommandError => e
246
+ if e.message.start_with?('NOSCRIPT')
247
+ redis.eval(script, argv: argv, keys: keys)
248
+ else
249
+ raise e if reraise
250
+ end
251
+ end
252
+ end
253
+
254
+ # only used in tests
255
+ def wipe_snapshots_data
256
+ redis.pipelined do
257
+ redis.del(snapshot_counter_key())
258
+ redis.del(snapshot_zset_key())
259
+ redis.del(snapshot_hash_key())
260
+ end
261
+ end
128
262
  end
129
263
  end
130
264
  end
@@ -39,8 +39,11 @@ module Rack
39
39
  executed_scalars: 0,
40
40
  executed_non_queries: 0,
41
41
  custom_timing_names: [],
42
- custom_timing_stats: {}
42
+ custom_timing_stats: {},
43
+ custom_fields: {}
43
44
  )
45
+ self[:request_method] = env['REQUEST_METHOD']
46
+ self[:request_path] = env['PATH_INFO']
44
47
  name = "#{env['REQUEST_METHOD']} http://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{env['SCRIPT_NAME']}#{env['PATH_INFO']}"
45
48
  self[:root] = TimerStruct::Request.createRoot(name, self)
46
49
  end
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rack
4
4
  class MiniProfiler
5
- VERSION = '2.0.0'
5
+ VERSION = '2.1.0'
6
6
  end
7
7
  end
@@ -30,6 +30,8 @@ module Rack::MiniProfilerRails
30
30
 
31
31
  if serves_static_assets?(app)
32
32
  c.skip_paths << app.config.assets.prefix
33
+ wp_assets_path = get_webpacker_assets_path()
34
+ c.skip_paths << wp_assets_path if wp_assets_path
33
35
  end
34
36
 
35
37
  unless Rails.env.development? || Rails.env.test?
@@ -116,6 +118,17 @@ module Rack::MiniProfilerRails
116
118
  @already_initialized = true
117
119
  end
118
120
 
121
+ def self.create_engine
122
+ return if defined?(Rack::MiniProfilerRails::Engine)
123
+ klass = Class.new(::Rails::Engine) do
124
+ engine_name 'rack-mini-profiler'
125
+ config.assets.paths << File.expand_path('../../html', __FILE__)
126
+ config.assets.precompile << 'rack-mini-profiler.js'
127
+ config.assets.precompile << 'rack-mini-profiler.css'
128
+ end
129
+ Rack::MiniProfilerRails.const_set("Engine", klass)
130
+ end
131
+
119
132
  def self.subscribe(event, &blk)
120
133
  if ActiveSupport::Notifications.respond_to?(:monotonic_subscribe)
121
134
  ActiveSupport::Notifications.monotonic_subscribe(event) { |*args| blk.call(*args) }
@@ -51,5 +51,11 @@ module Rack::MiniProfilerRailsMethods
51
51
  child[start] + child[duration] <= node[start] + node[duration]
52
52
  end
53
53
 
54
+ def get_webpacker_assets_path
55
+ if defined?(Webpacker) && Webpacker.config.config_path.exist?
56
+ Webpacker.config.public_output_path.to_s.gsub(Webpacker.config.public_path.to_s, "")
57
+ end
58
+ end
59
+
54
60
  extend self
55
61
  end
@@ -1,13 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  if (defined?(Net) && defined?(Net::HTTP))
4
- module NetHTTPWithMiniProfiler
5
- def request(request, *args, &block)
6
- Rack::MiniProfiler.step("Net::HTTP #{request.method} #{request.path}") do
7
- super
4
+
5
+ if defined?(Rack::MINI_PROFILER_PREPEND_NET_HTTP_PATCH)
6
+ module NetHTTPWithMiniProfiler
7
+ def request(request, *args, &block)
8
+ Rack::MiniProfiler.step("Net::HTTP #{request.method} #{request.path}") do
9
+ super
10
+ end
11
+ end
12
+ end
13
+ Net::HTTP.prepend(NetHTTPWithMiniProfiler)
14
+ else
15
+ Net::HTTP.class_eval do
16
+ def request_with_mini_profiler(*args, &block)
17
+ request = args[0]
18
+ Rack::MiniProfiler.step("Net::HTTP #{request.method} #{request.path}") do
19
+ request_without_mini_profiler(*args, &block)
20
+ end
8
21
  end
22
+ alias request_without_mini_profiler request
23
+ alias request request_with_mini_profiler
9
24
  end
10
25
  end
11
-
12
- Net::HTTP.prepend(NetHTTPWithMiniProfiler)
13
26
  end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Rack
4
+ MINI_PROFILER_PREPEND_NET_HTTP_PATCH = true
5
+ end
@@ -21,7 +21,7 @@ Gem::Specification.new do |s|
21
21
  "CHANGELOG.md"
22
22
  ]
23
23
  s.add_runtime_dependency 'rack', '>= 1.2.0'
24
- s.required_ruby_version = '>= 2.3.0'
24
+ s.required_ruby_version = '>= 2.4.0'
25
25
 
26
26
  s.metadata = {
27
27
  'source_code_uri' => 'https://github.com/MiniProfiler/rack-mini-profiler',
@@ -30,7 +30,6 @@ Gem::Specification.new do |s|
30
30
 
31
31
  s.add_development_dependency 'rake', '< 11'
32
32
  s.add_development_dependency 'rack-test'
33
- s.add_development_dependency 'activerecord', '~> 3.0'
34
33
  s.add_development_dependency 'dalli'
35
34
  s.add_development_dependency 'rspec', '~> 3.6.0'
36
35
  s.add_development_dependency 'redis'
@@ -40,6 +39,9 @@ Gem::Specification.new do |s|
40
39
  s.add_development_dependency 'mini_racer'
41
40
  s.add_development_dependency 'nokogiri'
42
41
  s.add_development_dependency 'rubocop-discourse'
42
+ s.add_development_dependency 'listen'
43
+ s.add_development_dependency 'webpacker', '~> 5.1'
44
+ s.add_development_dependency 'rails', '~> 5.1'
43
45
 
44
46
  s.require_paths = ["lib"]
45
47
  end