diff --git a/Gemfile.lock b/Gemfile.lock index e5d8048d2..b00ea6668 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -230,7 +230,7 @@ GEM metaclass (~> 0.0.1) mock_redis (0.13.2) moneta (0.8.0) - msgpack (0.5.10) + msgpack (0.5.11) multi_json (1.10.1) multi_xml (0.5.5) multipart-post (2.0.0) diff --git a/app/controllers/admin/diagnostics_controller.rb b/app/controllers/admin/diagnostics_controller.rb index 83d1fb292..7b0ff00de 100644 --- a/app/controllers/admin/diagnostics_controller.rb +++ b/app/controllers/admin/diagnostics_controller.rb @@ -3,7 +3,25 @@ class Admin::DiagnosticsController < Admin::AdminController skip_before_filter :check_xhr def memory_stats - render text: memory_report(class_report: params.key?(:full)), content_type: Mime::TEXT + text = nil + + if params.key?(:diff) + if !File.exists?(snapshot_filename) + text = "No initial snapshot exists" + else + filename = snapshot_filename + ".new" + snapshot_current_process(filename) + + text = compare(snapshot_filename, filename) + end + elsif params.key?(:snapshot) + snapshot_current_process + text = "Writing snapshot to: #{snapshot_filename}\n\nTo get a diff use ?diff=1" + else + text = memory_report(class_report: params.key?(:full)) + end + + render text: text, content_type: Mime::TEXT end def dump_heap @@ -24,6 +42,80 @@ class Admin::DiagnosticsController < Admin::AdminController protected + def compare(from, to) + from = Marshal::load(IO.binread(from)); + to = Marshal::load(IO.binread(to)); + + diff = from - to + + report = "#{diff.length} objects have leaked\n" + + + require 'objspace' + diff = diff.map do |id| + ObjectSpace._id2ref(id) rescue nil + end.compact! + + report << "Summary:\n" + + summary = {} + diff.each do |obj| + begin + summary[obj.class] ||= 0 + summary[obj.class] += 1 + rescue + # don't care + end + end + + report << summary.sort{|a,b| b[1] <=> a[1]}[0..50].map{|k,v| + "#{k}: #{v}" + }.join("\n") + + report << "\nSample Items:\n" + + diff[0..5000].each do |v| + report << "#{v.class}: #{String === v ? v[0..300] : (40 + ObjectSpace.memsize_of(v)).to_s + " bytes"}\n" rescue nil + end + + report + end + + def snapshot_path + "#{Rails.root}/tmp/mem_snapshots" + end + + def snapshot_filename + "#{snapshot_path}/#{Process.pid}.snapshot" + end + + def snapshot_current_process(filename=nil) + filename ||= snapshot_filename + pid=fork do + snapshot(filename) + end + + Process.wait(pid) + end + + def snapshot(filename) + require 'objspace' + FileUtils.mkdir_p snapshot_path + object_ids = [] + + full_gc + + ObjectSpace.each_object do |o| + begin + object_ids << o.object_id + rescue + # skip + end + end + + IO.binwrite(filename, Marshal::dump(object_ids)) + end + def memory_report(opts={}) begin # ruby 2.1 @@ -74,4 +166,26 @@ Classes: TEXT end + + + def full_gc + # gc start may not collect everything + GC.start while new_count = decreased_count(new_count) + end + + def decreased_count(old) + count = count_objects + if !old || count < old + count + else + nil + end + end + + def count_objects + i = 0 + ObjectSpace.each_object do |obj| + i += 1 + end + end end