dead_bro 0.2.17 → 0.2.18
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/lib/dead_bro/sql_subscriber.rb +91 -16
- data/lib/dead_bro/version.rb +1 -1
- data/lib/dead_bro/view_rendering_subscriber.rb +100 -120
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: aa43ba99a6fb0fd4007b856a24ac5844236f2d0102f9ec71175c81981a91bb17
|
|
4
|
+
data.tar.gz: c91f7fee25858bafd2edabfb162239ea8d8833405a3b4965f55e403a06dbfd05
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: b436b94d8d36d0b3407b09481326e443aa342c4398d4973fb517016bd3ee1f4a159a163d12a76b2294f2e4de36505b0cb568010bd8e0aecbbd0e3b4dc544069e
|
|
7
|
+
data.tar.gz: 7981b5ea511dad34ce67c0a71afe8a09f12045a0ae778311fa42ae0426f3a689d38377528939c6f757192bd973c3d3c782f9f2124831cfac5505d4bf1ab2d177
|
|
@@ -14,8 +14,18 @@ module DeadBro
|
|
|
14
14
|
THREAD_LOCAL_ALLOC_RESULTS_KEY = :dead_bro_sql_alloc_results
|
|
15
15
|
THREAD_LOCAL_BACKTRACE_KEY = :dead_bro_sql_backtraces
|
|
16
16
|
THREAD_LOCAL_EXPLAIN_PENDING_KEY = :dead_bro_explain_pending
|
|
17
|
+
THREAD_LOCAL_CALL_COUNTS_KEY = :dead_bro_sql_call_counts
|
|
18
|
+
THREAD_LOCAL_AGGREGATES_KEY = :dead_bro_sql_aggregates
|
|
17
19
|
MAX_TRACKED_QUERIES = 1000
|
|
18
20
|
|
|
21
|
+
# Number of identical queries within one request that triggers N+1 detection.
|
|
22
|
+
N_PLUS_ONE_THRESHOLD = 5
|
|
23
|
+
|
|
24
|
+
NORMALIZE_PARAM_RE = /\$\d+/.freeze
|
|
25
|
+
NORMALIZE_NUMERIC_RE = /\b\d+(\.\d+)?\b/.freeze
|
|
26
|
+
NORMALIZE_IN_RE = /\bIN\s*\([^)]+\)/i.freeze
|
|
27
|
+
NORMALIZE_SPACE_RE = /\s+/.freeze
|
|
28
|
+
|
|
19
29
|
# Precompiled regexes used by sanitize_sql. Dynamic /.../i literals inside
|
|
20
30
|
# a hot-path method allocate a fresh Regexp on every call — pinning them
|
|
21
31
|
# here removes that allocation entirely.
|
|
@@ -57,6 +67,20 @@ module DeadBro
|
|
|
57
67
|
true
|
|
58
68
|
end
|
|
59
69
|
|
|
70
|
+
def self.normalize_for_n_plus_one(sql)
|
|
71
|
+
return "" unless sql.is_a?(String)
|
|
72
|
+
s = sql.dup
|
|
73
|
+
s.gsub!(NORMALIZE_PARAM_RE, "?")
|
|
74
|
+
s.gsub!(NORMALIZE_NUMERIC_RE, "?")
|
|
75
|
+
s.gsub!(NORMALIZE_IN_RE, "IN (?)")
|
|
76
|
+
s.gsub!(NORMALIZE_SPACE_RE, " ")
|
|
77
|
+
s.strip!
|
|
78
|
+
s.downcase!
|
|
79
|
+
s
|
|
80
|
+
rescue
|
|
81
|
+
sql.to_s.downcase
|
|
82
|
+
end
|
|
83
|
+
|
|
60
84
|
def self.subscribe!
|
|
61
85
|
# Subscribe with a start/finish listener to measure allocations per query
|
|
62
86
|
if ActiveSupport::Notifications.notifier.respond_to?(:subscribe)
|
|
@@ -82,19 +106,31 @@ module DeadBro
|
|
|
82
106
|
duration_ms = ((finished - started) * 1000.0).round(2)
|
|
83
107
|
original_sql = data[:sql]
|
|
84
108
|
|
|
85
|
-
# Only capture a backtrace for queries we actually care about tracing
|
|
86
|
-
# (slow). This skips the ~O(stack-depth) allocation on the 99% of queries
|
|
87
|
-
# that are fast. An N+1 of 100 x 1ms queries no longer eats a thousand
|
|
88
|
-
# frame allocations for traces nobody will read.
|
|
89
109
|
threshold = begin
|
|
90
110
|
DeadBro.configuration.slow_query_threshold_ms
|
|
91
111
|
rescue
|
|
92
112
|
500
|
|
93
113
|
end
|
|
114
|
+
sanitized_sql = sanitize_sql(original_sql)
|
|
94
115
|
captured_trace = (duration_ms >= threshold.to_f) ? capture_app_backtrace : []
|
|
95
116
|
|
|
117
|
+
cc_stack = Thread.current[THREAD_LOCAL_CALL_COUNTS_KEY]
|
|
118
|
+
agg_stack = Thread.current[THREAD_LOCAL_AGGREGATES_KEY]
|
|
119
|
+
call_counts = cc_stack.is_a?(Array) ? cc_stack.last : nil
|
|
120
|
+
aggregates_h = agg_stack.is_a?(Array) ? agg_stack.last : nil
|
|
121
|
+
normalized_key = nil
|
|
122
|
+
|
|
123
|
+
if call_counts && aggregates_h
|
|
124
|
+
normalized_key = normalize_for_n_plus_one(sanitized_sql)
|
|
125
|
+
call_counts[normalized_key] = (call_counts[normalized_key] || 0) + 1
|
|
126
|
+
# Capture backtrace exactly at the N+1 boundary — the stack is still meaningful here.
|
|
127
|
+
if call_counts[normalized_key] == N_PLUS_ONE_THRESHOLD && captured_trace.empty?
|
|
128
|
+
captured_trace = capture_app_backtrace
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
|
|
96
132
|
query_info = {
|
|
97
|
-
sql:
|
|
133
|
+
sql: sanitized_sql,
|
|
98
134
|
name: data[:name],
|
|
99
135
|
duration_ms: duration_ms,
|
|
100
136
|
cached: data[:cached] || false,
|
|
@@ -103,16 +139,40 @@ module DeadBro
|
|
|
103
139
|
allocations: allocations
|
|
104
140
|
}
|
|
105
141
|
|
|
106
|
-
# Run EXPLAIN ANALYZE for slow queries in the background
|
|
107
142
|
if should_explain_query?(duration_ms, original_sql)
|
|
108
|
-
|
|
109
|
-
query_info[:explain_plan] = nil # Placeholder
|
|
110
|
-
# Capture binds if available (type_casted_binds is preferred as they are ready for quoting)
|
|
143
|
+
query_info[:explain_plan] = nil
|
|
111
144
|
binds = data[:type_casted_binds] || data[:binds]
|
|
112
145
|
start_explain_analyze_background(original_sql, data[:connection_id], query_info, binds)
|
|
113
146
|
end
|
|
114
147
|
|
|
115
|
-
|
|
148
|
+
if aggregates_h && normalized_key
|
|
149
|
+
call_count = call_counts[normalized_key]
|
|
150
|
+
if (agg = aggregates_h[normalized_key])
|
|
151
|
+
agg[:count] += 1
|
|
152
|
+
agg[:total_duration_ms] = (agg[:total_duration_ms] + duration_ms).round(2)
|
|
153
|
+
agg[:max_duration_ms] = [agg[:max_duration_ms], duration_ms].max
|
|
154
|
+
agg[:min_duration_ms] = [agg[:min_duration_ms], duration_ms].min
|
|
155
|
+
agg[:total_allocations] += (allocations || 0)
|
|
156
|
+
agg[:cached_count] += 1 if query_info[:cached]
|
|
157
|
+
agg[:n_plus_one] = true if call_count >= N_PLUS_ONE_THRESHOLD
|
|
158
|
+
agg[:backtrace] = captured_trace if agg[:backtrace].empty? && !captured_trace.empty?
|
|
159
|
+
else
|
|
160
|
+
aggregates_h[normalized_key] = {
|
|
161
|
+
sql: sanitized_sql,
|
|
162
|
+
name: data[:name],
|
|
163
|
+
count: 1,
|
|
164
|
+
total_duration_ms: duration_ms,
|
|
165
|
+
max_duration_ms: duration_ms,
|
|
166
|
+
min_duration_ms: duration_ms,
|
|
167
|
+
total_allocations: allocations || 0,
|
|
168
|
+
cached_count: (data[:cached] ? 1 : 0),
|
|
169
|
+
n_plus_one: false,
|
|
170
|
+
backtrace: captured_trace,
|
|
171
|
+
explain_plan: nil
|
|
172
|
+
}
|
|
173
|
+
end
|
|
174
|
+
end
|
|
175
|
+
|
|
116
176
|
if should_continue_tracking?(current, MAX_TRACKED_QUERIES)
|
|
117
177
|
current << query_info
|
|
118
178
|
end
|
|
@@ -126,25 +186,40 @@ module DeadBro
|
|
|
126
186
|
Thread.current[THREAD_LOCAL_ALLOC_RESULTS_KEY] = {}
|
|
127
187
|
Thread.current[THREAD_LOCAL_BACKTRACE_KEY] = {}
|
|
128
188
|
Thread.current[THREAD_LOCAL_EXPLAIN_PENDING_KEY] = []
|
|
189
|
+
(Thread.current[THREAD_LOCAL_CALL_COUNTS_KEY] ||= []) << {}
|
|
190
|
+
(Thread.current[THREAD_LOCAL_AGGREGATES_KEY] ||= []) << {}
|
|
129
191
|
end
|
|
130
192
|
|
|
131
193
|
def self.stop_request_tracking
|
|
132
|
-
# Wait for any pending EXPLAIN ANALYZE queries to complete (with timeout)
|
|
133
|
-
# This must happen BEFORE we get the queries array reference to ensure
|
|
134
|
-
# all explain_plan fields are populated
|
|
135
194
|
wait_for_pending_explains(EXPLAIN_WAIT_TIMEOUT_SECONDS)
|
|
136
195
|
|
|
137
196
|
stack = Thread.current[THREAD_LOCAL_KEY]
|
|
138
|
-
|
|
139
|
-
|
|
197
|
+
raw_queries = (stack.is_a?(Array) && stack.any?) ? stack.pop : []
|
|
198
|
+
|
|
199
|
+
agg_stack = Thread.current[THREAD_LOCAL_AGGREGATES_KEY]
|
|
200
|
+
aggregates_h = (agg_stack.is_a?(Array) && agg_stack.any?) ? agg_stack.pop : {}
|
|
201
|
+
cc_stack = Thread.current[THREAD_LOCAL_CALL_COUNTS_KEY]
|
|
202
|
+
cc_stack.pop if cc_stack.is_a?(Array) && cc_stack.any?
|
|
203
|
+
|
|
204
|
+
# Fold any completed EXPLAIN plans from raw queries into their aggregate entry
|
|
205
|
+
raw_queries.each do |q|
|
|
206
|
+
next unless q[:explain_plan]
|
|
207
|
+
key = normalize_for_n_plus_one(q[:sql].to_s)
|
|
208
|
+
agg = aggregates_h[key]
|
|
209
|
+
agg[:explain_plan] ||= q[:explain_plan] if agg
|
|
210
|
+
end
|
|
211
|
+
|
|
140
212
|
if stack.nil? || stack.empty?
|
|
141
213
|
Thread.current[THREAD_LOCAL_KEY] = nil
|
|
142
214
|
Thread.current[THREAD_LOCAL_ALLOC_START_KEY] = nil
|
|
143
215
|
Thread.current[THREAD_LOCAL_ALLOC_RESULTS_KEY] = nil
|
|
144
216
|
Thread.current[THREAD_LOCAL_BACKTRACE_KEY] = nil
|
|
145
217
|
Thread.current[THREAD_LOCAL_EXPLAIN_PENDING_KEY] = nil
|
|
218
|
+
Thread.current[THREAD_LOCAL_CALL_COUNTS_KEY] = nil
|
|
219
|
+
Thread.current[THREAD_LOCAL_AGGREGATES_KEY] = nil
|
|
146
220
|
end
|
|
147
|
-
|
|
221
|
+
|
|
222
|
+
aggregates_h.values.sort_by { |a| -a[:total_duration_ms] }
|
|
148
223
|
end
|
|
149
224
|
|
|
150
225
|
# Upper bound on pending EXPLAIN threads per request — stops a slow-query
|
data/lib/dead_bro/version.rb
CHANGED
|
@@ -4,167 +4,147 @@ require "active_support/notifications"
|
|
|
4
4
|
|
|
5
5
|
module DeadBro
|
|
6
6
|
class ViewRenderingSubscriber
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
RENDER_PARTIAL_EVENT = "render_partial.action_view"
|
|
7
|
+
RENDER_TEMPLATE_EVENT = "render_template.action_view"
|
|
8
|
+
RENDER_PARTIAL_EVENT = "render_partial.action_view"
|
|
10
9
|
RENDER_COLLECTION_EVENT = "render_collection.action_view"
|
|
11
10
|
|
|
12
|
-
THREAD_LOCAL_KEY
|
|
13
|
-
|
|
11
|
+
THREAD_LOCAL_KEY = :dead_bro_view_events
|
|
12
|
+
THREAD_LOCAL_AGGREGATES_KEY = :dead_bro_view_aggregates
|
|
13
|
+
MAX_TRACKED_EVENTS = 500
|
|
14
14
|
|
|
15
15
|
def self.subscribe!(client: Client.new)
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
view_info = {
|
|
21
|
-
type: "template",
|
|
22
|
-
identifier: safe_identifier(data[:identifier]),
|
|
23
|
-
layout: data[:layout],
|
|
24
|
-
duration_ms: duration_ms,
|
|
25
|
-
virtual_path: data[:virtual_path],
|
|
26
|
-
rendered_at: Time.now.utc.to_i
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
add_view_event(view_info)
|
|
16
|
+
ActiveSupport::Notifications.subscribe(RENDER_TEMPLATE_EVENT) do |_name, started, finished, _uid, data|
|
|
17
|
+
add_view_event(type: "template", identifier: safe_identifier(data[:identifier]),
|
|
18
|
+
duration_ms: ((finished - started) * 1000.0).round(2),
|
|
19
|
+
rendered_at: Time.now.utc.to_i)
|
|
30
20
|
end
|
|
31
21
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
type: "partial",
|
|
38
|
-
identifier: safe_identifier(data[:identifier]),
|
|
39
|
-
layout: data[:layout],
|
|
40
|
-
duration_ms: duration_ms,
|
|
41
|
-
virtual_path: data[:virtual_path],
|
|
42
|
-
cache_key: data[:cache_key],
|
|
43
|
-
rendered_at: Time.now.utc.to_i
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
add_view_event(view_info)
|
|
22
|
+
ActiveSupport::Notifications.subscribe(RENDER_PARTIAL_EVENT) do |_name, started, finished, _uid, data|
|
|
23
|
+
add_view_event(type: "partial", identifier: safe_identifier(data[:identifier]),
|
|
24
|
+
duration_ms: ((finished - started) * 1000.0).round(2),
|
|
25
|
+
cache_key: data[:cache_key],
|
|
26
|
+
rendered_at: Time.now.utc.to_i)
|
|
47
27
|
end
|
|
48
28
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
identifier: safe_identifier(data[:identifier]),
|
|
56
|
-
layout: data[:layout],
|
|
57
|
-
duration_ms: duration_ms,
|
|
58
|
-
virtual_path: data[:virtual_path],
|
|
59
|
-
cache_key: data[:cache_key],
|
|
60
|
-
count: data[:count],
|
|
61
|
-
cached_count: data[:cached_count],
|
|
62
|
-
rendered_at: Time.now.utc.to_i
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
add_view_event(view_info)
|
|
29
|
+
ActiveSupport::Notifications.subscribe(RENDER_COLLECTION_EVENT) do |_name, started, finished, _uid, data|
|
|
30
|
+
add_view_event(type: "collection", identifier: safe_identifier(data[:identifier]),
|
|
31
|
+
duration_ms: ((finished - started) * 1000.0).round(2),
|
|
32
|
+
collection_count: (data[:count] || 0).to_i,
|
|
33
|
+
collection_cached_count: (data[:cached_count] || 0).to_i,
|
|
34
|
+
rendered_at: Time.now.utc.to_i)
|
|
66
35
|
end
|
|
67
36
|
rescue
|
|
68
|
-
# Never raise from instrumentation install
|
|
69
37
|
end
|
|
70
38
|
|
|
71
39
|
def self.start_request_tracking
|
|
72
|
-
Thread.current[THREAD_LOCAL_KEY]
|
|
40
|
+
Thread.current[THREAD_LOCAL_KEY] = true
|
|
41
|
+
Thread.current[THREAD_LOCAL_AGGREGATES_KEY] = {}
|
|
73
42
|
end
|
|
74
43
|
|
|
75
44
|
def self.stop_request_tracking
|
|
76
|
-
events = Thread.current[THREAD_LOCAL_KEY]
|
|
77
45
|
Thread.current[THREAD_LOCAL_KEY] = nil
|
|
78
|
-
|
|
46
|
+
aggregates = Thread.current[THREAD_LOCAL_AGGREGATES_KEY] || {}
|
|
47
|
+
Thread.current[THREAD_LOCAL_AGGREGATES_KEY] = nil
|
|
48
|
+
aggregates.values.sort_by { |a| [-a[:count], -a[:total_duration_ms]] }
|
|
79
49
|
end
|
|
80
50
|
|
|
81
51
|
def self.add_view_event(view_info)
|
|
82
|
-
|
|
83
|
-
|
|
52
|
+
return unless Thread.current[THREAD_LOCAL_KEY]
|
|
53
|
+
aggregates = Thread.current[THREAD_LOCAL_AGGREGATES_KEY]
|
|
54
|
+
return unless aggregates
|
|
55
|
+
|
|
56
|
+
key = view_info[:identifier].to_s
|
|
57
|
+
dur = view_info[:duration_ms].to_f
|
|
58
|
+
rendered_at = view_info[:rendered_at]
|
|
59
|
+
|
|
60
|
+
if (agg = aggregates[key])
|
|
61
|
+
agg[:count] += 1
|
|
62
|
+
agg[:total_duration_ms] = (agg[:total_duration_ms] + dur).round(2)
|
|
63
|
+
agg[:max_duration_ms] = [agg[:max_duration_ms], dur].max
|
|
64
|
+
agg[:min_duration_ms] = [agg[:min_duration_ms], dur].min
|
|
65
|
+
agg[:rendered_at_min] = [agg[:rendered_at_min], rendered_at].compact.min if rendered_at
|
|
66
|
+
agg[:rendered_at_max] = [agg[:rendered_at_max], rendered_at].compact.max if rendered_at
|
|
67
|
+
agg[:cache_hit_count] += 1 if view_info[:cache_key]
|
|
68
|
+
agg[:collection_count] += view_info[:collection_count].to_i
|
|
69
|
+
agg[:collection_cached_count] += view_info[:collection_cached_count].to_i
|
|
70
|
+
else
|
|
71
|
+
return if aggregates.size >= MAX_TRACKED_EVENTS
|
|
72
|
+
aggregates[key] = {
|
|
73
|
+
identifier: key,
|
|
74
|
+
type: view_info[:type],
|
|
75
|
+
count: 1,
|
|
76
|
+
total_duration_ms: dur,
|
|
77
|
+
max_duration_ms: dur,
|
|
78
|
+
min_duration_ms: dur,
|
|
79
|
+
rendered_at_min: rendered_at,
|
|
80
|
+
rendered_at_max: rendered_at,
|
|
81
|
+
cache_hit_count: (view_info[:cache_key] ? 1 : 0),
|
|
82
|
+
collection_count: view_info[:collection_count].to_i,
|
|
83
|
+
collection_cached_count: view_info[:collection_cached_count].to_i
|
|
84
|
+
}
|
|
84
85
|
end
|
|
85
86
|
end
|
|
86
87
|
|
|
87
|
-
# Check if we should continue tracking based on count and time limits
|
|
88
|
-
def self.should_continue_tracking?
|
|
89
|
-
events = Thread.current[THREAD_LOCAL_KEY]
|
|
90
|
-
return false unless events
|
|
91
|
-
|
|
92
|
-
# Check count limit
|
|
93
|
-
return false if events.length >= MAX_TRACKED_EVENTS
|
|
94
|
-
|
|
95
|
-
# Check time limit
|
|
96
|
-
start_time = Thread.current[DeadBro::TRACKING_START_TIME_KEY]
|
|
97
|
-
if start_time
|
|
98
|
-
elapsed_seconds = Time.now - start_time
|
|
99
|
-
return false if elapsed_seconds >= DeadBro::MAX_TRACKING_DURATION_SECONDS
|
|
100
|
-
end
|
|
101
|
-
|
|
102
|
-
true
|
|
103
|
-
end
|
|
104
|
-
|
|
105
88
|
def self.safe_identifier(identifier)
|
|
106
89
|
return "" unless identifier.is_a?(String)
|
|
107
|
-
|
|
108
|
-
# Extract meaningful parts of the file path
|
|
109
|
-
# e.g., "/app/views/users/show.html.erb" -> "users/show.html.erb"
|
|
110
90
|
identifier.split("/").last(3).join("/")
|
|
111
91
|
rescue
|
|
112
92
|
identifier.to_s
|
|
113
93
|
end
|
|
114
94
|
|
|
115
|
-
# Analyze view rendering performance
|
|
116
95
|
def self.analyze_view_performance(view_events)
|
|
117
96
|
return {} if view_events.empty?
|
|
118
97
|
|
|
119
|
-
|
|
98
|
+
total_renders = view_events.sum { |e| e_int(e, :count, 1) }
|
|
99
|
+
total_duration = view_events.sum { |e| e_flt(e, :total_duration_ms) }
|
|
120
100
|
|
|
121
|
-
|
|
122
|
-
|
|
101
|
+
by_type = Hash.new(0)
|
|
102
|
+
view_events.each { |e| by_type[e_str(e, :type)] += e_int(e, :count, 1) }
|
|
123
103
|
|
|
124
|
-
|
|
125
|
-
|
|
104
|
+
slowest = view_events.sort_by { |e| -e_flt(e, :max_duration_ms) }.first(5).map do |e|
|
|
105
|
+
{ identifier: e_str(e, :identifier), duration_ms: e_flt(e, :max_duration_ms), type: e_str(e, :type) }
|
|
106
|
+
end
|
|
126
107
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
.sort_by { |_, count| -count }
|
|
131
|
-
.first(5)
|
|
108
|
+
most_frequent = view_events.sort_by { |e| -e_int(e, :count, 1) }.first(5).map do |e|
|
|
109
|
+
{ identifier: e_str(e, :identifier), count: e_int(e, :count, 1) }
|
|
110
|
+
end
|
|
132
111
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
112
|
+
partials = view_events.select { |e| e_str(e, :type) == "partial" }
|
|
113
|
+
total_partial_renders = partials.sum { |e| e_int(e, :count, 1) }
|
|
114
|
+
total_cache_hits = partials.sum { |e| e_int(e, :cache_hit_count) }
|
|
115
|
+
partial_cache_hit_rate = total_partial_renders > 0 ? (total_cache_hits.to_f / total_partial_renders * 100).round(2) : 0
|
|
137
116
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
collection_cache_hit_rate = (total_collection_items > 0) ?
|
|
143
|
-
(total_cached_items.to_f / total_collection_items * 100).round(2) : 0
|
|
117
|
+
collections = view_events.select { |e| e_str(e, :type) == "collection" }
|
|
118
|
+
total_collection_items = collections.sum { |e| e_int(e, :collection_count) }
|
|
119
|
+
total_cached_items = collections.sum { |e| e_int(e, :collection_cached_count) }
|
|
120
|
+
collection_cache_hit_rate = total_collection_items > 0 ? (total_cached_items.to_f / total_collection_items * 100).round(2) : 0
|
|
144
121
|
|
|
145
122
|
{
|
|
146
|
-
total_views_rendered:
|
|
147
|
-
total_view_duration_ms:
|
|
148
|
-
average_view_duration_ms: (total_duration /
|
|
149
|
-
by_type:
|
|
150
|
-
slowest_views:
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
},
|
|
157
|
-
most_frequent_views: view_frequency.map { |identifier, count|
|
|
158
|
-
{
|
|
159
|
-
identifier: identifier,
|
|
160
|
-
count: count
|
|
161
|
-
}
|
|
162
|
-
},
|
|
163
|
-
partial_cache_hit_rate: cache_hit_rate,
|
|
164
|
-
collection_cache_hit_rate: collection_cache_hit_rate,
|
|
165
|
-
total_collection_items: total_collection_items,
|
|
166
|
-
total_cached_collection_items: total_cached_items
|
|
123
|
+
total_views_rendered: total_renders,
|
|
124
|
+
total_view_duration_ms: total_duration.round(2),
|
|
125
|
+
average_view_duration_ms: total_renders > 0 ? (total_duration / total_renders).round(2) : 0,
|
|
126
|
+
by_type: by_type,
|
|
127
|
+
slowest_views: slowest,
|
|
128
|
+
most_frequent_views: most_frequent,
|
|
129
|
+
partial_cache_hit_rate: partial_cache_hit_rate,
|
|
130
|
+
collection_cache_hit_rate: collection_cache_hit_rate,
|
|
131
|
+
total_collection_items: total_collection_items,
|
|
132
|
+
total_cached_collection_items: total_cached_items
|
|
167
133
|
}
|
|
168
134
|
end
|
|
135
|
+
|
|
136
|
+
private
|
|
137
|
+
|
|
138
|
+
def self.e_int(e, key, default = 0)
|
|
139
|
+
(e[key] || e[key.to_s] || default).to_i
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
def self.e_flt(e, key, default = 0.0)
|
|
143
|
+
(e[key] || e[key.to_s] || default).to_f
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def self.e_str(e, key, default = "")
|
|
147
|
+
(e[key] || e[key.to_s] || default).to_s
|
|
148
|
+
end
|
|
169
149
|
end
|
|
170
150
|
end
|