rack-mini-profiler 2.0.4 → 2.3.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +28 -0
  3. data/README.md +53 -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 +93 -3
  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/fonts/source-code-pro-regular.css +8 -0
  18. data/lib/html/speedscope/fonts/source-code-pro-v13-regular.woff +0 -0
  19. data/lib/html/speedscope/fonts/source-code-pro-v13-regular.woff2 +0 -0
  20. data/lib/html/speedscope/import.cf0fa83f.js +115 -0
  21. data/lib/html/speedscope/index.html +2 -0
  22. data/lib/html/speedscope/release.txt +3 -0
  23. data/lib/html/speedscope/reset.8c46b7a1.css +2 -0
  24. data/lib/html/speedscope/source-map.438fa06b.js +24 -0
  25. data/lib/html/speedscope/speedscope.44364064.js +200 -0
  26. data/lib/html/vendor.js +10 -2
  27. data/lib/mini_profiler/asset_version.rb +1 -1
  28. data/lib/mini_profiler/client_settings.rb +3 -2
  29. data/lib/mini_profiler/config.rb +24 -2
  30. data/lib/mini_profiler/profiler.rb +214 -22
  31. data/lib/mini_profiler/profiling_methods.rb +11 -2
  32. data/lib/mini_profiler/snapshots_transporter.rb +109 -0
  33. data/lib/mini_profiler/storage/abstract_store.rb +78 -0
  34. data/lib/mini_profiler/storage/memory_store.rb +54 -5
  35. data/lib/mini_profiler/storage/redis_store.rb +134 -0
  36. data/lib/mini_profiler/timer_struct/page.rb +52 -2
  37. data/lib/mini_profiler/timer_struct/sql.rb +2 -2
  38. data/lib/mini_profiler/version.rb +1 -1
  39. data/lib/mini_profiler_rails/railtie.rb +11 -0
  40. data/lib/patches/db/mysql2.rb +4 -27
  41. data/lib/patches/db/mysql2/alias_method.rb +30 -0
  42. data/lib/patches/db/mysql2/prepend.rb +34 -0
  43. data/lib/prepend_mysql2_patch.rb +5 -0
  44. data/lib/rack-mini-profiler.rb +1 -0
  45. data/rack-mini-profiler.gemspec +6 -4
  46. metadata +63 -14
@@ -41,6 +41,84 @@ 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
+ hash = groups[group_name] ||= {}
62
+ hash[:snapshots_count] ||= 0
63
+ hash[:snapshots_count] += 1
64
+ if !hash[:worst_score] || hash[:worst_score] < snapshot.duration_ms
65
+ groups[group_name][:worst_score] = snapshot.duration_ms
66
+ end
67
+ if !hash[:best_score] || hash[:best_score] > snapshot.duration_ms
68
+ groups[group_name][:best_score] = snapshot.duration_ms
69
+ end
70
+ end
71
+ end
72
+ groups = groups.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 find_snapshots_group(group_name)
80
+ data = []
81
+ fetch_snapshots do |batch|
82
+ batch.each do |snapshot|
83
+ snapshot_group_name = default_snapshot_grouping(snapshot)
84
+ if group_name == snapshot_group_name
85
+ data << {
86
+ id: snapshot[:id],
87
+ duration: snapshot.duration_ms,
88
+ sql_count: snapshot[:sql_count],
89
+ timestamp: snapshot[:started_at],
90
+ custom_fields: snapshot[:custom_fields]
91
+ }
92
+ end
93
+ end
94
+ end
95
+ data.sort_by! { |s| s[:duration] }
96
+ data.reverse!
97
+ data
98
+ end
99
+
100
+ def load_snapshot(id)
101
+ raise NotImplementedError.new("load_snapshot is not implemented")
102
+ end
103
+
104
+ private
105
+
106
+ def default_snapshot_grouping(snapshot)
107
+ group_name = rails_route_from_path(snapshot[:request_path], snapshot[:request_method])
108
+ group_name ||= snapshot[:request_path]
109
+ "#{snapshot[:request_method]} #{group_name}"
110
+ end
111
+
112
+ def rails_route_from_path(path, method)
113
+ if defined?(Rails) && defined?(ActionController::RoutingError)
114
+ hash = Rails.application.routes.recognize_path(path, method: method)
115
+ if hash && hash[:controller] && hash[:action]
116
+ "#{hash[:controller]}##{hash[:action]}"
117
+ end
118
+ end
119
+ rescue ActionController::RoutingError
120
+ nil
121
+ end
44
122
  end
45
123
  end
46
124
  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
@@ -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
@@ -10,6 +10,53 @@ module Rack
10
10
  # :has_many TimerStruct::Sql children
11
11
  # :has_many TimerStruct::Custom children
12
12
  class Page < TimerStruct::Base
13
+ class << self
14
+ def from_hash(hash)
15
+ hash = symbolize_hash(hash)
16
+ if hash.key?(:custom_timing_names)
17
+ hash[:custom_timing_names] = []
18
+ end
19
+ hash.delete(:started_formatted)
20
+ if hash.key?(:duration_milliseconds)
21
+ hash[:duration_milliseconds] = 0
22
+ end
23
+ page = self.allocate
24
+ page.instance_variable_set(:@attributes, hash)
25
+ page
26
+ end
27
+
28
+ private
29
+
30
+ def symbolize_hash(hash)
31
+ new_hash = {}
32
+ hash.each do |k, v|
33
+ sym_k = String === k ? k.to_sym : k
34
+ if Hash === v
35
+ new_hash[sym_k] = symbolize_hash(v)
36
+ elsif Array === v
37
+ new_hash[sym_k] = symbolize_array(v)
38
+ else
39
+ new_hash[sym_k] = v
40
+ end
41
+ end
42
+ new_hash
43
+ end
44
+
45
+ def symbolize_array(array)
46
+ array.map do |item|
47
+ if Array === item
48
+ symbolize_array(item)
49
+ elsif Hash === item
50
+ symbolize_hash(item)
51
+ else
52
+ item
53
+ end
54
+ end
55
+ end
56
+ end
57
+
58
+ attr_reader :attributes
59
+
13
60
  def initialize(env)
14
61
  timer_id = MiniProfiler.generate_id
15
62
  page_name = env['PATH_INFO']
@@ -39,8 +86,11 @@ module Rack
39
86
  executed_scalars: 0,
40
87
  executed_non_queries: 0,
41
88
  custom_timing_names: [],
42
- custom_timing_stats: {}
89
+ custom_timing_stats: {},
90
+ custom_fields: {}
43
91
  )
92
+ self[:request_method] = env['REQUEST_METHOD']
93
+ self[:request_path] = env['PATH_INFO']
44
94
  name = "#{env['REQUEST_METHOD']} http://#{env['SERVER_NAME']}:#{env['SERVER_PORT']}#{env['SCRIPT_NAME']}#{env['PATH_INFO']}"
45
95
  self[:root] = TimerStruct::Request.createRoot(name, self)
46
96
  end
@@ -71,7 +121,7 @@ module Rack
71
121
 
72
122
  def extra_json
73
123
  {
74
- started: '/Date(%d)/' % @attributes[:started_at],
124
+ started_formatted: '/Date(%d)/' % @attributes[:started_at],
75
125
  duration_milliseconds: @attributes[:root][:duration_milliseconds],
76
126
  custom_timing_names: @attributes[:custom_timing_stats].keys.sort
77
127
  }
@@ -38,12 +38,12 @@ module Rack
38
38
  start_millis = ((Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000).to_i - page[:started]) - duration_ms
39
39
  super(
40
40
  execute_type: 3, # TODO
41
- formatted_command_string: ERB::Util.html_escape(query),
41
+ formatted_command_string: query ? ERB::Util.html_escape(query) : nil,
42
42
  stack_trace_snippet: stack_trace,
43
43
  start_milliseconds: start_millis,
44
44
  duration_milliseconds: duration_ms,
45
45
  first_fetch_duration_milliseconds: duration_ms,
46
- parameters: trim_binds(params),
46
+ parameters: query ? trim_binds(params) : nil,
47
47
  parent_timing_id: nil,
48
48
  is_duplicate: false
49
49
  )
@@ -2,6 +2,6 @@
2
2
 
3
3
  module Rack
4
4
  class MiniProfiler
5
- VERSION = '2.0.4'
5
+ VERSION = '2.3.1'
6
6
  end
7
7
  end
@@ -118,6 +118,17 @@ module Rack::MiniProfilerRails
118
118
  @already_initialized = true
119
119
  end
120
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
+
121
132
  def self.subscribe(event, &blk)
122
133
  if ActiveSupport::Notifications.respond_to?(:monotonic_subscribe)
123
134
  ActiveSupport::Notifications.monotonic_subscribe(event) { |*args| blk.call(*args) }
@@ -1,30 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- # The best kind of instrumentation is in the actual db provider, however we don't want to double instrument
4
-
5
- class Mysql2::Result
6
- alias_method :each_without_profiling, :each
7
- def each(*args, &blk)
8
- return each_without_profiling(*args, &blk) unless defined?(@miniprofiler_sql_id)
9
-
10
- start = Process.clock_gettime(Process::CLOCK_MONOTONIC)
11
- result = each_without_profiling(*args, &blk)
12
- elapsed_time = SqlPatches.elapsed_time(start)
13
-
14
- @miniprofiler_sql_id.report_reader_duration(elapsed_time)
15
- result
16
- end
17
- end
18
-
19
- class Mysql2::Client
20
- alias_method :query_without_profiling, :query
21
- def query(*args, &blk)
22
- return query_without_profiling(*args, &blk) unless SqlPatches.should_measure?
23
-
24
- result, record = SqlPatches.record_sql(args[0]) do
25
- query_without_profiling(*args, &blk)
26
- end
27
- result.instance_variable_set("@miniprofiler_sql_id", record) if result
28
- result
29
- end
3
+ if defined?(Rack::MINI_PROFILER_PREPEND_MYSQL2_PATCH)
4
+ require "patches/db/mysql2/prepend"
5
+ else
6
+ require "patches/db/mysql2/alias_method"
30
7
  end