Skip to content

Commit 06d3b35

Browse files
committed
Replace allocations count by GC time in request logs
Allocations count is often an interesting proxy for performance, but not necessarily the most relevant thing to include in request logs, given they aren't a per thread metric, so the reporting is widely innacurate in multi-threaded environments. Since Ruby 3.1 there is now `GC.total_time` which is a monotonically increasing counter of time spent in GC. It still isn't really a per thread metric, but is is more interesting because it uses the same unit as the response time, allowing to better see when you have a GC pause performance issue.
1 parent 07e40ea commit 06d3b35

File tree

5 files changed

+32
-12
lines changed

5 files changed

+32
-12
lines changed

actionpack/lib/action_controller/log_subscriber.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ def process_action(event)
3333
status = ActionDispatch::ExceptionWrapper.status_code_for_exception(exception_class_name)
3434
end
3535

36-
additions << "Allocations: #{event.allocations}"
36+
additions << "GC: #{event.gc_time.round(1)}ms"
3737

3838
message = +"Completed #{status} #{Rack::Utils::HTTP_STATUS_CODES[status]} in #{event.duration.round}ms" \
3939
" (#{additions.join(" | ")})"

actionview/lib/action_view/log_subscriber.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ def render_template(event)
1818
info do
1919
message = +" Rendered #{from_rails_root(event.payload[:identifier])}"
2020
message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout]
21-
message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
21+
message << " (Duration: #{event.duration.round(1)}ms | GC: #{event.gc_time.round(1)}ms)"
2222
end
2323
end
2424
subscribe_log_level :render_template, :debug
@@ -27,7 +27,7 @@ def render_partial(event)
2727
debug do
2828
message = +" Rendered #{from_rails_root(event.payload[:identifier])}"
2929
message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout]
30-
message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
30+
message << " (Duration: #{event.duration.round(1)}ms | GC: #{event.gc_time.round(1)}ms)"
3131
message << " #{cache_message(event.payload)}" unless event.payload[:cache_hit].nil?
3232
message
3333
end
@@ -37,7 +37,7 @@ def render_partial(event)
3737
def render_layout(event)
3838
info do
3939
message = +" Rendered layout #{from_rails_root(event.payload[:identifier])}"
40-
message << " (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
40+
message << " (Duration: #{event.duration.round(1)}ms | GC: #{event.gc_time.round(1)}ms)"
4141
end
4242
end
4343
subscribe_log_level :render_layout, :info
@@ -48,7 +48,7 @@ def render_collection(event)
4848
debug do
4949
message = +" Rendered collection of #{from_rails_root(identifier)}"
5050
message << " within #{from_rails_root(event.payload[:layout])}" if event.payload[:layout]
51-
message << " #{render_count(event.payload)} (Duration: #{event.duration.round(1)}ms | Allocations: #{event.allocations})"
51+
message << " #{render_count(event.payload)} (Duration: #{event.duration.round(1)}ms | GC: #{event.gc_time.round(1)}ms)"
5252
message
5353
end
5454
end

actionview/test/activerecord/controller_runtime_test.rb

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ def test_log_with_active_record
6868
wait
6969

7070
assert_equal 2, @logger.logged(:info).size
71-
assert_match(/\(Views: [\d.]+ms \| ActiveRecord: [\d.]+ms \(0 queries, 0 cached\) \| Allocations: [\d.]+\)/, @logger.logged(:info)[1])
71+
assert_match(/\(Views: [\d.]+ms \| ActiveRecord: [\d.]+ms \(0 queries, 0 cached\) \| GC: [\d.]+ms\)/, @logger.logged(:info)[1])
7272
end
7373

7474
def test_runtime_reset_before_requests
@@ -77,27 +77,27 @@ def test_runtime_reset_before_requests
7777
wait
7878

7979
assert_equal 2, @logger.logged(:info).size
80-
assert_match(/\(Views: [\d.]+ms \| ActiveRecord: [\d.]+ms \(0 queries, 0 cached\) \| Allocations: [\d.]+\)/, @logger.logged(:info)[1])
80+
assert_match(/\(Views: [\d.]+ms \| ActiveRecord: [\d.]+ms \(0 queries, 0 cached\) \| GC: [\d.]+ms\)/, @logger.logged(:info)[1])
8181
end
8282

8383
def test_log_with_active_record_when_post
8484
post :create
8585
wait
86-
assert_match(/ActiveRecord: ([1-9][\d.]+)ms \(1 query, 0 cached\) \| Allocations: [\d.]+\)/, @logger.logged(:info)[2])
86+
assert_match(/ActiveRecord: ([1-9][\d.]+)ms \(1 query, 0 cached\) \| GC: [\d.]+ms\)/, @logger.logged(:info)[2])
8787
end
8888

8989
def test_log_with_active_record_when_redirecting
9090
get :redirect
9191
wait
9292
assert_equal 3, @logger.logged(:info).size
93-
assert_match(/\(ActiveRecord: [\d.]+ms \(0 queries, 0 cached\) \| Allocations: [\d.]+\)/, @logger.logged(:info)[2])
93+
assert_match(/\(ActiveRecord: [\d.]+ms \(0 queries, 0 cached\) \| GC: [\d.]+ms\)/, @logger.logged(:info)[2])
9494
end
9595

9696
def test_include_time_query_time_after_rendering
9797
get :db_after_render
9898
wait
9999

100100
assert_equal 2, @logger.logged(:info).size
101-
assert_match(/\(Views: [\d.]+ms \| ActiveRecord: ([1-9][\d.]+)ms \(1 query, 0 cached\) \| Allocations: [\d.]+\)/, @logger.logged(:info)[1])
101+
assert_match(/\(Views: [\d.]+ms \| ActiveRecord: ([1-9][\d.]+)ms \(1 query, 0 cached\) \| GC: [\d.]+ms\)/, @logger.logged(:info)[1])
102102
end
103103
end

actionview/test/template/log_subscriber_test.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -186,14 +186,14 @@ def test_render_uncached_outer_partial_with_inner_cached_partial_wont_mix_cache_
186186
wait
187187
*, cached_inner, uncached_outer = @logger.logged(:debug)
188188
assert_match(/Rendered test\/_cached_customer\.erb (.*) \[cache miss\]/, cached_inner)
189-
assert_match(/Rendered test\/_nested_cached_customer\.erb \(Duration: .*?ms \| Allocations: .*?\)$/, uncached_outer)
189+
assert_match(/Rendered test\/_nested_cached_customer\.erb \(Duration: .*?ms \| GC: .*?\)$/, uncached_outer)
190190

191191
# Second render hits the cache for the _cached_customer partial. Outer template's log shouldn't be affected.
192192
@view.render(partial: "test/nested_cached_customer", locals: { cached_customer: Customer.new("Stan") })
193193
wait
194194
*, cached_inner, uncached_outer = @logger.logged(:debug)
195195
assert_match(/Rendered test\/_cached_customer\.erb (.*) \[cache hit\]/, cached_inner)
196-
assert_match(/Rendered test\/_nested_cached_customer\.erb \(Duration: .*?ms \| Allocations: .*?\)$/, uncached_outer)
196+
assert_match(/Rendered test\/_nested_cached_customer\.erb \(Duration: .*?ms \| GC: .*?\)$/, uncached_outer)
197197
end
198198
end
199199

activesupport/lib/active_support/notifications/instrumenter.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,8 @@ def initialize(name, start, ending, transaction_id, payload)
117117
@cpu_time_finish = 0.0
118118
@allocation_count_start = 0
119119
@allocation_count_finish = 0
120+
@gc_time_start = 0
121+
@gc_time_finish = 0
120122
end
121123

122124
def time
@@ -144,12 +146,14 @@ def record # :nodoc:
144146
def start!
145147
@time = now
146148
@cpu_time_start = now_cpu
149+
@gc_time_start = now_gc
147150
@allocation_count_start = now_allocations
148151
end
149152

150153
# Record information at the time this event finishes
151154
def finish!
152155
@cpu_time_finish = now_cpu
156+
@gc_time_finish = now_gc
153157
@end = now
154158
@allocation_count_finish = now_allocations
155159
end
@@ -173,6 +177,12 @@ def allocations
173177
@allocation_count_finish - @allocation_count_start
174178
end
175179

180+
# Returns the time spent in GC (in milliseconds) between the call to #start!
181+
# and the call to #finish!
182+
def gc_time
183+
(@gc_time_finish - @gc_time_start) / 1_000_000.0
184+
end
185+
176186
# Returns the difference in milliseconds between when the execution of the
177187
# event started and when it ended.
178188
#
@@ -206,6 +216,16 @@ def now_cpu
206216
end
207217
end
208218

219+
if GC.respond_to?(:total_time)
220+
def now_gc
221+
GC.total_time
222+
end
223+
else
224+
def now_gc
225+
0
226+
end
227+
end
228+
209229
if GC.stat.key?(:total_allocated_objects)
210230
def now_allocations
211231
GC.stat(:total_allocated_objects)

0 commit comments

Comments
 (0)