forked from discourse/discourse
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
DEV: add debugging scripts for memory leaks
These scripts are somewhat rough but I needed them to help debug a memory leak we have noticed in rails 6. The biggest object script finds all the biggest objects we have in memory after boot. The test memory leak runs a very simple iteration through all multisites and observed memory.
- Loading branch information
1 parent
45a55a3
commit 038a38a
Showing
2 changed files
with
144 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,36 @@ | ||
# frozen_string_literal: true | ||
|
||
# simple script to measure largest objects in memory post boot | ||
|
||
if ENV['RAILS_ENV'] != "production" | ||
exec "RAILS_ENV=production ruby #{__FILE__}" | ||
end | ||
|
||
require 'objspace' | ||
|
||
ObjectSpace.trace_object_allocations do | ||
|
||
require File.expand_path("../../config/environment", __FILE__) | ||
|
||
Rails.application.routes.recognize_path('abc') rescue nil | ||
|
||
# load up the yaml for the localization bits, in master process | ||
I18n.t(:posts) | ||
|
||
RailsMultisite::ConnectionManagement.each_connection do | ||
(ActiveRecord::Base.connection.tables - %w[schema_migrations versions]).each do |table| | ||
table.classify.constantize.first rescue nil | ||
end | ||
end | ||
|
||
end | ||
|
||
5.times do | ||
GC.start(full_mark: true, immediate_sweep: true) | ||
end | ||
|
||
[String, Array, Hash].each do |klass| | ||
ObjectSpace.each_object(klass).sort { |a, b| b.length <=> a.length }.first(50).each do |obj| | ||
puts "#{klass} size: #{obj.length} #{ObjectSpace.allocation_sourcefile(obj)} #{ObjectSpace.allocation_sourceline(obj)}" | ||
end | ||
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,108 @@ | ||
# frozen_string_literal: true | ||
|
||
# simple test to check for memory leaks | ||
# | ||
# this performs a trivial operation walking all multisites and grabbing first topic / localizing | ||
# the expectation is that RSS will remain static no matter how many iterations run | ||
|
||
if ENV['RAILS_ENV'] != "production" | ||
exec "RAILS_ENV=production ruby #{__FILE__}" | ||
end | ||
|
||
if !ENV['LD_PRELOAD'] | ||
exec "LD_PRELOAD=/usr/lib/libjemalloc.so.1 ruby #{__FILE__}" | ||
end | ||
|
||
if ENV['LD_PRELOAD'].include?("jemalloc") | ||
# for 3.6.0 we need a patch jemal 1.1.0 gem (1.1.1 does not support 3.6.0) | ||
# however ffi is a problem so we need to patch the gem | ||
require 'jemal' | ||
|
||
$jemalloc = true | ||
end | ||
|
||
if ENV['LD_PRELOAD'].include?("mwrap") | ||
$mwrap = true | ||
require 'mwrap' | ||
end | ||
|
||
def bin_diff(current) | ||
$baseline[:arenas].each_with_index do |arena, i| | ||
next if !arena || !arena[:bins] | ||
arena[:bins].each do |size, stat| | ||
allocated = (current.dig(:arenas, i, :bins, size, :allocated) || 0) | ||
diff = allocated - stat[:allocated] | ||
puts "bin #{size} delta #{diff}" | ||
end | ||
end | ||
end | ||
|
||
require File.expand_path("../../config/environment", __FILE__) | ||
|
||
Rails.application.routes.recognize_path('abc') rescue nil | ||
I18n.t(:posts) | ||
|
||
def rss | ||
`ps -o rss -p #{$$}`.chomp.split("\n").last.to_i | ||
end | ||
|
||
def loop_sites | ||
RailsMultisite::ConnectionManagement.each_connection do | ||
yield | ||
end | ||
end | ||
|
||
def biggest_klass(klass) | ||
ObjectSpace.each_object(klass).max { |a, b| a.length <=> b.length } | ||
end | ||
|
||
def iter(warmup: false) | ||
loop_sites { Topic.first; I18n.t('too_late_to_edit') } | ||
if !warmup | ||
GC.start(full_mark: true, immediate_sweep: true) | ||
|
||
if $jemalloc | ||
jemal_stats = Jemal.stats | ||
jedelta = "(jdelta #{jemal_stats[:active] - $baseline_jemalloc_active})" | ||
end | ||
|
||
if $mwrap | ||
mwrap_delta = (Mwrap.total_bytes_allocated - Mwrap.total_bytes_freed) - $mwrap_baseline | ||
mwrap_delta = "(mwrap delta #{mwrap_delta})" | ||
end | ||
|
||
rss_delta = rss - $baseline_rss | ||
array_delta = biggest_klass(Array).length - $biggest_array_length | ||
puts "rss: #{rss} (#{rss_delta}) #{mwrap_delta}#{jedelta} heap_delta: #{GC.stat[:heap_live_slots] - $baseline_slots} array_delta: #{array_delta}" | ||
|
||
if $jemalloc | ||
bin_diff(jemal_stats) | ||
end | ||
end | ||
|
||
end | ||
|
||
iter(warmup: true) | ||
4.times do | ||
GC.start(full_mark: true, immediate_sweep: true) | ||
end | ||
|
||
if $jemalloc | ||
$baseline = Jemal.stats | ||
$baseline_jemalloc_active = $baseline[:active] | ||
4.times do | ||
GC.start(full_mark: true, immediate_sweep: true) | ||
end | ||
end | ||
|
||
if $mwrap | ||
$mwrap_baseline = Mwrap.total_bytes_allocated - Mwrap.total_bytes_freed | ||
end | ||
|
||
$baseline_slots = GC.stat[:heap_live_slots] | ||
$baseline_rss = rss | ||
$biggest_array_length = biggest_klass(Array).length | ||
|
||
100000.times do | ||
iter | ||
end |