lib/scrap.rb
require "scrap/version"
module Scrap
class Middleware
COMMIFY_REGEX = /(\d)(?=(\d\d\d)+(?!\d))/
CRLF = "\r\n"
def initialize(app)
@@app = app
end
def call(env)
self.class.call(env)
end
@@gc_stats = {}
@@last_gc_run = nil
@@last_gc_mem = nil
@@requests_processed = 0
@@request_list = []
@@alive_at = nil
@@gc_stats_enabled = nil
@@config = nil
def self.config
@@config ||= YAML::load open(File.join(Rails.root, "config", "scrap.yml")).read
rescue Errno::ENOENT
@@config = {}
rescue
puts "[scrap] scrap.yml: #{$!.message}"
@@config = {}
end
def self.call(env)
if !@@gc_stats_enabled
GC.enable_stats if GC.respond_to? :enable_stats # for REE
GC::Profiler.enable if defined? GC::Profiler # for 1.9.3+
@@gc_stats_enabled = true
end
@@requests_processed += 1
@@last_gc_run ||= @@alive_at ||= Time.now.to_f
@@last_gc_mem ||= get_usage
req = sprintf("<p>vsize:[%-10.2fMB] rss:[%-10.2fMB] %s %s</p>", get_usage[:virtual], get_usage[:real], env["REQUEST_METHOD"], env["PATH_INFO"])
req << "<pre>#{ObjectSpace.statistics}</pre>" if ObjectSpace.respond_to? :statistics
req << "<pre>#{readable_gc_stat}</pre>" if GC.respond_to? :stat
@@request_list.unshift req
@@request_list.pop if @@request_list.length > (config["max_requests"] || 150)
if env["PATH_INFO"] == "/stats/scrap"
gc_stats
else
@@app.call(env)
end
end
def self.gc_stats
collected = nil
puts "Respond to? #{ObjectSpace.respond_to? :live_objects}"
if ObjectSpace.respond_to? :live_objects
live = ObjectSpace.live_objects
GC.start
collected = live - ObjectSpace.live_objects
else
GC.start
end
usage = get_usage
virtual_mem_delta = usage[:virtual] - @@last_gc_mem[:virtual]
real_mem_delta = usage[:real] - @@last_gc_mem[:real]
time_delta = Time.now.to_f - @@last_gc_run
s = ''
s << '<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN"' << CRLF
s << ' "http://www.w3.org/TR/html4/strict.dtd">' << CRLF
s << '<html><head>' << CRLF
s << "<title>[#{$$}] Garbage Report</title>" << CRLF
s << '<style type="text/css"> body { font-family: monospace; color: #222; } td { border-bottom: 1px solid #eee; padding: 1px 9px; } td.t { background: #fafafa; } tr:hover td { background: #fafaf0; border-color: #e0e0dd; } h1,h2,h3 { border-bottom: 1px solid #ddd; font-family: sans-serif; } </style>' << CRLF
s << '<body>' << CRLF
s << "<h1>Scrap - PID #{$$}</h1>" << CRLF
s << '<table>' << CRLF
s << sprintf('<tr><td class="t">Virtual Memory usage:</td><td>%2.2fMB</td></tr>', usage[:virtual])
s << sprintf('<tr><td class="t">Real Memory usage:</td><td>%2.2fMB</td></tr>', usage[:real])
s << sprintf('<tr><td class="t">Vsize Delta:</td><td>%2.2fMB</td></tr>', virtual_mem_delta)
s << sprintf('<tr><td class="t">RSS Delta:</td><td>%2.2fMB</td></tr>', real_mem_delta)
s << sprintf('<tr><td class="t">Last Scrap req:</td><td>%2.2f seconds ago</td></tr>', time_delta)
s << sprintf('<tr><td class="t">Requests processed:</td><td>%s</td></tr>', @@requests_processed)
s << sprintf('<tr><td class="t">Alive for:</td><td>%2.2f seconds</td></tr>', Time.now.to_f - @@alive_at)
if GC.respond_to? :time
s << sprintf('<tr><td class="t">Total time spent in GC:</td><td>%2.2f seconds</td></tr>', GC.time / 1000000.0)
elsif defined? GC::Profiler
s << sprintf('<tr><td class="t">Total time spent in GC:</td><td>%2.2f seconds</td></tr>', GC::Profiler.total_time / 1000.0)
end
if collected
s << sprintf('<tr><td class="t">Collected objects:</td><td>%2d</td></tr>', collected)
s << sprintf('<tr><td class="t">Live objects:</td><td>%2d</td></tr>', ObjectSpace.live_objects)
end
s << '</table>' << CRLF
s << "<h3>Top #{config["max_objects"]} deltas since last request</h3>"
s << '<table border="0">'
memcheck(config["max_objects"], Object, :deltas).each do |v|
next if v.last == 0
s << "<tr><td class='t'>#{v.first}</td><td>#{sprintf("%s%s", v.last >= 0 ? "+" : "-", commify(v.last))}</td></tr>"
end
s << '</table>'
s << "<h3>Top #{config["max_objects"]} objects</h3>"
s << '<table border="0">'
memcheck(config["max_objects"]).each do |v|
s << "<tr><td class='t'>#{v.first}</td><td>#{commify v.last}</td></tr>"
end
s << '</table>'
(config["classes"] || {}).each do |klass, val|
puts val.inspect
opts = val === true ? {"print_objects" => true} : val
add_os(klass.constantize, s, opts)
end
s << '<h3>Request history</h3>'
@@request_list.each do |req|
s << req
end
s << '</body></html>'
@@last_gc_run = Time.now.to_f
@@last_gc_mem = usage
@@requests_processed = 0
[200, {"Content-Type" => "text/html"}, [s]]
end
def self.get_usage
usage = Hash.new("N/A")
begin
stat = `cat /proc/#{$$}/stat`.split(" ")
usage[:virtual] = stat[22].to_i / (1024 * 1024).to_f
usage[:real] = stat[23].to_i * (`getconf PAGESIZE`.to_f) / (1024 * 1024).to_f
rescue
# pass
end
return usage
end
def self.add_os(c, s, options = {})
print_objects = options["print_objects"]
small = options["small"]
min = options["min"]
show_fields = options["show_fields"]
ct = ObjectSpace.each_object(c) {}
return if min and ct < min
if small
s << "#{c} (#{ct})<br />"
else
s << "<h3>#{c} (#{ct})</h3>"
end
return if !print_objects or ct == 0
s << CRLF
s << '<table>'
val = ObjectSpace.each_object(c) do |m|
s << '<tr><td class="t">' << "<#{m.class.to_s}:#{sprintf("0x%.8x", m.object_id)}></td>"
if show_fields then
show_fields.each do |field|
v = m.attributes[field.to_s]
if v.blank?
s << '<td> </td>'
else
s << "<td>#{field}: #{v}</td>"
end
end
end
s << '</tr>'
end
s << '</table>' << CRLF
end
def self.memcheck(top, klass = Object, mode = :normal)
top ||= 50
os = Hash.new(0)
ObjectSpace.each_object(klass) do |o|
begin;
# If this is true, it's an association proxy, and we don't want to invoke method_missing on it,
# as it will result in a load of the association proxy from the DB, doing extra DB work and
# potentially creating a lot of AR objects. Hackalicious.
next if o.respond_to? :proxy_respond_to?
os[o.class.to_s] += 1 if o.respond_to? :class
rescue; end
end
if mode == :deltas then
os2 = Hash.new(0)
os.each do |k, v|
os2[k] = v - (@@gc_stats[k] || 0)
@@gc_stats[k] = v
end
sorted = os2.sort_by{|k,v| -v }.first(top)
else
sorted = os.sort_by{|k,v| -v }.first(top)
end
end
def self.readable_gc_stat
stat = GC.stat
"GC cycles so far: #{stat[:count]}\nNumber of heaps : #{stat[:heap_used]}\nNumber of objects : #{ObjectSpace.count_objects[:TOTAL]}\nHeap length : #{stat[:heap_length]}\nHeap increment : #{stat[:heap_increment]}\nHeap live num : #{stat[:heap_live_num]}\nHeap free num : #{stat[:heap_free_num]}\nHeap final num : #{stat[:heap_final_num]}\n"
end
def self.commify(i)
i.to_s.gsub(COMMIFY_REGEX, "\\1,")
end
end
require "scrap/engine" if defined? Rails
end