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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +24 -0
- data/README.md +59 -28
- data/lib/enable_rails_patches.rb +1 -3
- data/lib/html/includes.css +40 -9
- data/lib/html/includes.js +90 -29
- data/lib/html/includes.scss +32 -4
- data/lib/html/includes.tmpl +74 -2
- data/lib/html/profile_handler.js +1 -1
- data/lib/html/rack-mini-profiler.css +3 -0
- data/lib/html/rack-mini-profiler.js +2 -0
- data/lib/html/vendor.js +10 -2
- data/lib/mini_profiler/asset_version.rb +1 -1
- data/lib/mini_profiler/config.rb +13 -1
- data/lib/mini_profiler/profiler.rb +166 -23
- data/lib/mini_profiler/storage/abstract_store.rb +72 -0
- data/lib/mini_profiler/storage/memory_store.rb +54 -5
- data/lib/mini_profiler/storage/redis_store.rb +136 -2
- data/lib/mini_profiler/timer_struct/page.rb +4 -1
- data/lib/mini_profiler/version.rb +1 -1
- data/lib/mini_profiler_rails/railtie.rb +13 -0
- data/lib/mini_profiler_rails/railtie_methods.rb +6 -0
- data/lib/patches/net_patches.rb +19 -6
- data/lib/prepend_net_http_patch.rb +5 -0
- data/rack-mini-profiler.gemspec +4 -2
- metadata +51 -20
@@ -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
|
62
|
-
@timer_struct_lock
|
63
|
-
@user_view_lock
|
64
|
-
@
|
65
|
-
@
|
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
|
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
|
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
|
@@ -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
|
data/lib/patches/net_patches.rb
CHANGED
@@ -1,13 +1,26 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
if (defined?(Net) && defined?(Net::HTTP))
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
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
|
data/rack-mini-profiler.gemspec
CHANGED
@@ -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.
|
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
|