forked from discourse/discourse
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathmemory-analysis
executable file
·169 lines (132 loc) · 3.28 KB
/
memory-analysis
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
#!/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