discourse/script/memory-analysis
Sam Saffron 30990006a9 DEV: enable frozen string literal on all files
This reduces chances of errors where consumers of strings mutate inputs
and reduces memory usage of the app.

Test suite passes now, but there may be some stuff left, so we will run
a few sites on a branch prior to merging
2019-05-13 09:31:32 +08:00

170 lines
3.3 KiB
Ruby
Executable File

#!/usr/bin/env ruby
# frozen_string_literal: true
require 'fileutils'
require 'pathname'
require 'tmpdir'
require 'json'
require 'set'
def usage
STDERR.puts "Usage: memory-analysis [PID|DUMPFILE]"
exit 1
end
if ARGV.length != 1
usage
end
dumpfile = ARGV[0]
if !File.exist?(dumpfile)
pid = dumpfile.to_i
usage if pid == 0
time = Time.now.utc
utc = time.strftime("%Y-%m-%d-%H-%M-%S")
dumpfile = "#{Pathname.new(Dir.tmpdir).realpath}/#{pid}_#{utc}.dump"
puts "Dumping heap for pid #{pid} to #{dumpfile}"
puts
`rbtrace -p #{pid} -e 'Thread.new{GC.start;require "objspace";io=File.open("#{dumpfile}", "w"); ObjectSpace.dump_all(output: io); io.close}'`
old_size = 0
found = false
20.times do
sleep 0.2
found = File.exist?(dumpfile)
break if found
end
if !found
STDERR.puts "Unable to find dumpfile #{dumpfile}, is rbtrace running properly, did you pick the right pid?"
usage
end
while true
sleep 0.5
size = File.size(dumpfile)
if size == old_size && size > 0
break
end
old_size = size
end
end
puts "Processing heap dump"
class Stats
def initialize
@classes = {}
@class_stats = {}
@type_stats = {}
@threads = Set.new
@thread_owners = {}
end
def print
puts "Stats by Type"
puts "-" * 20
puts
@type_stats.sort_by { |_, (_, size)| -size }.each do |k, (count, size)|
puts "#{k} Count: #{count} Size: #{size}"
end
puts
puts "Stats by Class"
puts "-" * 20
@class_stats.sort_by { |_, (_, size)| -size }.each do |k, (count, size)|
puts "#{@classes[k] || k} Count: #{count} Size: #{size}"
end
puts "Thread Stats"
puts "-" * 20
@thread_owners.sort_by { |_ , count| -count }.each do |name, count|
puts "#{count} refs from #{name}"
end
end
def thread_class
@thread_class ||=
begin
@classes.find do |addr, n|
n == "Thread"
end.first
end
end
def detect_threads(line)
parsed = JSON.parse(line)
if parsed["class"] == thread_class
@threads << parsed["address"]
end
end
def detect_thread_owners(line)
parsed = JSON.parse(line)
if refs = parsed["references"]
i = 0
while i < refs.length
if @threads.include?(refs[i])
klass = @classes[parsed["class"]] || "#{parsed["type"]} #{parsed["address"]}"
@thread_owners[klass] ||= 0
@thread_owners[klass] += 1
end
i += 1
end
end
end
def injest(line)
parsed = JSON.parse(line)
if parsed["type"] == "CLASS"
@classes[parsed["address"]] = parsed["name"]
end
type_stat = @type_stats[parsed["type"]] ||= [0, 0]
if klass = parsed["class"]
class_stat = @class_stats[parsed["class"]] ||= [0, 0]
class_stat[0] += 1
class_stat[1] += parsed["memsize"] || 0
end
type_stat[0] += 1
type_stat[1] += parsed["memsize"] || 0
end
end
def process_dumpfile(dumpfile)
stats = Stats.new
File.open(dumpfile).each_line do |line|
stats.injest(line)
end
puts "pass 1 done"
File.open(dumpfile).each_line do |line|
stats.detect_threads(line)
end
puts "pass 2 done"
File.open(dumpfile).each_line do |line|
stats.detect_thread_owners(line)
end
stats
end
stats = process_dumpfile(dumpfile)
stats.print