memory-profiler 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/Gemfile +1 -0
- data/Gemfile.lock +13 -0
- data/LICENSE +14 -0
- data/README +52 -0
- data/Rakefile +46 -0
- data/lib/memory-profiler.rb +333 -0
- metadata +79 -0
data/Gemfile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
gemspec
|
data/Gemfile.lock
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
Copyright 2011 Matthew Kerwin
|
2
|
+
|
3
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
you may not use this file except in compliance with the License.
|
5
|
+
You may obtain a copy of at
|
6
|
+
|
7
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
|
9
|
+
Unless required by applicable law or agreed to in writing, software
|
10
|
+
distributed under the License is distributed on an "AS IS" BASES,
|
11
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
See the License for the specific language governing permissions and
|
13
|
+
limitations under the License.
|
14
|
+
|
data/README
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
-------------------------------------------------------------------------
|
2
|
+
|
3
|
+
The simplest way to use this utility is to copy the file:
|
4
|
+
lib/memory-profiler.rb
|
5
|
+
to your project somewhere, and include it directly.
|
6
|
+
|
7
|
+
However the correct way is to build the gem, so that you can use it in
|
8
|
+
all your projects.
|
9
|
+
|
10
|
+
-------------------------------------------------------------------------
|
11
|
+
|
12
|
+
To build the gem, using rake:
|
13
|
+
|
14
|
+
rake install
|
15
|
+
rake clean
|
16
|
+
|
17
|
+
Then in your code:
|
18
|
+
|
19
|
+
require 'rubygems'
|
20
|
+
require 'memory-profiler'
|
21
|
+
|
22
|
+
-------------------------------------------------------------------------
|
23
|
+
|
24
|
+
Refer to RDoc documentation for more detail, but here's an example for
|
25
|
+
using the utility in your Ruby program:
|
26
|
+
|
27
|
+
# start the daemon, and let us know the file to which it reports
|
28
|
+
puts MemoryProfiler.start_daemon( :limit=>5, :delay=>10, :marshall_size=>true, :sort_by=>:absdelta )
|
29
|
+
|
30
|
+
5.times do
|
31
|
+
blah = Hash.new([])
|
32
|
+
|
33
|
+
# compare memory space before and after executing a block of code
|
34
|
+
rpt = MemoryProfiler.start( :limit=>10 ) do
|
35
|
+
# some activities likely to create object references
|
36
|
+
100.times{ blah[1] << 'aaaaa' }
|
37
|
+
1000.times{ blah[2] << 'bbbbb' }
|
38
|
+
end
|
39
|
+
|
40
|
+
# display the report in a (slightly) readable form
|
41
|
+
puts MemoryProfiler.format(rpt)
|
42
|
+
|
43
|
+
sleep 7
|
44
|
+
end
|
45
|
+
|
46
|
+
# terminate the daemon
|
47
|
+
MemoryProfiler.stop_daemon
|
48
|
+
|
49
|
+
-------------------------------------------------------------------------
|
50
|
+
|
51
|
+
Report any issues to the author <matthew@kerwin.net.au>
|
52
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,46 @@
|
|
1
|
+
gemspec = eval(File.read(Dir["*.gemspec"].first))
|
2
|
+
|
3
|
+
begin
|
4
|
+
require 'bundler'
|
5
|
+
Bundler.setup
|
6
|
+
rescue LoadError => er
|
7
|
+
$stderr.puts "You need to have Bundler installed to be able to build this gem."
|
8
|
+
# Process.exit
|
9
|
+
end
|
10
|
+
|
11
|
+
# This is hacky, but I'm too lazy right now to compare all executables in the $PATH
|
12
|
+
# and try to work out which gem* command is appropriate.
|
13
|
+
if $0 =~ /\brake((?:\d+[.\d]))$/
|
14
|
+
$version = $1
|
15
|
+
else
|
16
|
+
$version = case RUBY_VERSION
|
17
|
+
when /^(1\.\d)\.0$/ then $1
|
18
|
+
when /^1\.8\./ then '1.8'
|
19
|
+
else RUBY_VERSION
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
desc "Validate the gemspec"
|
24
|
+
task :gemspec do
|
25
|
+
gemspec.validate
|
26
|
+
end
|
27
|
+
|
28
|
+
|
29
|
+
desc "Build gem locally"
|
30
|
+
task :build => :gemspec do
|
31
|
+
system "gem#$version build #{gemspec.name}.gemspec"
|
32
|
+
FileUtils.mkdir_p "pkg-#$version"
|
33
|
+
FileUtils.mv "#{gemspec.name}-#{gemspec.version}.gem", "pkg-#$version"
|
34
|
+
end
|
35
|
+
|
36
|
+
desc "Install gem locally"
|
37
|
+
task :install => :build do
|
38
|
+
system "gem#$version install pkg-#$version/#{gemspec.name}-#{gemspec.version}"
|
39
|
+
end
|
40
|
+
|
41
|
+
|
42
|
+
desc "Clean automatically generated files"
|
43
|
+
task :clean do
|
44
|
+
FileUtils.rm_rf "pkg-#$version"
|
45
|
+
end
|
46
|
+
|
@@ -0,0 +1,333 @@
|
|
1
|
+
#!/usr/bin/ruby --
|
2
|
+
# vim: tabstop=2:softtabstop=2:shiftwidth=2:noexpandtab
|
3
|
+
=begin
|
4
|
+
|
5
|
+
Author: Matthew Kerwin <matthew@kerwin.net.au>
|
6
|
+
Version: 1.0
|
7
|
+
Date: 2011-01-28
|
8
|
+
|
9
|
+
|
10
|
+
Copyright 2011 Matthew Kerwin
|
11
|
+
|
12
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
13
|
+
you may not use this file except in compliance with the License.
|
14
|
+
You may obtain a copy of at
|
15
|
+
|
16
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
17
|
+
|
18
|
+
Unless required by applicable law or agreed to in writing, software
|
19
|
+
distributed under the License is distributed on an "AS IS" BASES,
|
20
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
21
|
+
See the License for the specific language governing permissions and
|
22
|
+
limitations under the License.
|
23
|
+
|
24
|
+
=end
|
25
|
+
|
26
|
+
require 'sync'
|
27
|
+
|
28
|
+
module MemoryProfiler
|
29
|
+
DEFAULTS = {
|
30
|
+
# general options
|
31
|
+
:sort_by => :current,
|
32
|
+
:only => [],
|
33
|
+
:ignore => [],
|
34
|
+
:limit => 20,
|
35
|
+
:force_gc => false,
|
36
|
+
# daemon options
|
37
|
+
:delay => 60,
|
38
|
+
:filename => nil,
|
39
|
+
# ObjectSpaceAnalyser options
|
40
|
+
:string_debug => false,
|
41
|
+
:marshall_size => false,
|
42
|
+
}
|
43
|
+
@@daemon_thread = nil
|
44
|
+
@@daemon_sync = Sync.new
|
45
|
+
|
46
|
+
@@start_data = nil
|
47
|
+
@@start_sync = Sync.new
|
48
|
+
|
49
|
+
#
|
50
|
+
# Begins an analysis thread that runs periodically, reporting to a text
|
51
|
+
# file at /tmp/memory_profiler-<pid>.log
|
52
|
+
#
|
53
|
+
# Returns the filename used.
|
54
|
+
#
|
55
|
+
# Options:
|
56
|
+
# :delay => 60 # number of seconds between summaries
|
57
|
+
# :filename => nil # override the generated default
|
58
|
+
# See: #start for other options
|
59
|
+
#
|
60
|
+
def self.start_daemon(opt = {})
|
61
|
+
opt = DEFAULTS.merge(opt)
|
62
|
+
filename = opt[:filename] || "/tmp/memory_profiler-#{Process.pid}.log"
|
63
|
+
@@daemon_sync.synchronize(:EX) do
|
64
|
+
raise 'daemon process already running' if @@daemon_thread
|
65
|
+
@@daemon_thread = Thread.new do
|
66
|
+
prev = Hash.new(0)
|
67
|
+
file = File.open(filename, 'w')
|
68
|
+
loop do
|
69
|
+
begin
|
70
|
+
GC.start if opt[:force_gc]
|
71
|
+
curr = ObjectSpaceAnalyser.analyse(opt)
|
72
|
+
data = self._delta(curr, prev, opt)
|
73
|
+
|
74
|
+
file.puts '-'*80
|
75
|
+
file.puts Time.now.to_s
|
76
|
+
data.each {|k,c,d| file.printf( "%5d %+5d %s\n", c, d, k.name ) }
|
77
|
+
|
78
|
+
prev = curr
|
79
|
+
GC.start if opt[:force_gc]
|
80
|
+
rescue ::Exception => err
|
81
|
+
$stderr.puts "** MemoryProfiler daemon error: #{err}", err.backtrace.map{|b| "\t#{b}" }
|
82
|
+
end
|
83
|
+
sleep opt[:delay]
|
84
|
+
end #loop
|
85
|
+
end #Thread.new
|
86
|
+
end
|
87
|
+
filename
|
88
|
+
end
|
89
|
+
|
90
|
+
#
|
91
|
+
# Terminates the analysis thread started by #start_daemon
|
92
|
+
#
|
93
|
+
def self.stop_daemon
|
94
|
+
@@daemon_sync.synchronize(:EX) do
|
95
|
+
raise 'no daemon process running' unless @@daemon_thread
|
96
|
+
@@daemon_thread.kill
|
97
|
+
@@daemon_thread.join
|
98
|
+
@@daemon_thread = nil
|
99
|
+
end
|
100
|
+
self
|
101
|
+
end
|
102
|
+
|
103
|
+
#
|
104
|
+
# Generates in instantaneous report on the current Ruby ObjectSpace, saved to
|
105
|
+
# a text file at: /tmp/memory_profiler-<pid>-<time>.log
|
106
|
+
#
|
107
|
+
# Returns the filename used.
|
108
|
+
#
|
109
|
+
# See: #start for valid/default options, except that :sort_by may
|
110
|
+
# only have the value :current or :none when using #report
|
111
|
+
#
|
112
|
+
def self.report(opt = {})
|
113
|
+
opt = DEFAULTS.merge(opt)
|
114
|
+
GC.start if opt[:force_gc]
|
115
|
+
|
116
|
+
data = ObjectSpaceAnalyser.analyse(opt)
|
117
|
+
|
118
|
+
if opt[:sort_by] == :current
|
119
|
+
data = data.to_a.sort_by{|k,v| -v }
|
120
|
+
data = data[0,opt[:limit]] if opt[:limit] > 0 and opt[:limit] < data.length
|
121
|
+
elsif opt[:sort_by] != :none
|
122
|
+
warn "MemoryProfiler: invalid option :sort_by => #{opt[:sort_by].inspect}; using :none"
|
123
|
+
end
|
124
|
+
|
125
|
+
filename = opt[:filename] || "/tmp/memory_profiler-#{Process.pid}-#{Time.now.to_i}.log"
|
126
|
+
File.open(filename, 'w') do |f|
|
127
|
+
data.each {|k,c| file.printf( "%5d %s\n", c, k.name ) }
|
128
|
+
end
|
129
|
+
|
130
|
+
GC.start if opt[:force_gc]
|
131
|
+
filename
|
132
|
+
end
|
133
|
+
|
134
|
+
#
|
135
|
+
# If a block is given, executes it and returns a summary. Otherwise,
|
136
|
+
# starts the analyser, and waits for a call to #restart or #stop.
|
137
|
+
#
|
138
|
+
# Returned data is an array of:
|
139
|
+
# [ [Class, current_usage, usage_delta], ... ]
|
140
|
+
#
|
141
|
+
# Options:
|
142
|
+
# :sort_by => :current # how to order classes; :current | :delta | :absdelta | :none
|
143
|
+
#
|
144
|
+
# :only => [] # list of only classes to scan; if empty, scans all classes
|
145
|
+
# :ignore => [] # list of classes to exclude from reports (including sub-classes and modules, but not namespaces)
|
146
|
+
# :limit => 20 # how many of the top classes to report (less than 1 means 'all'); only matters if :sort_by is not :none
|
147
|
+
#
|
148
|
+
# :force_gc => true # if true, forces a garbage collection before and after generating report
|
149
|
+
#
|
150
|
+
# :string_debug => false # see ObjectSpaceAnalyser#analyse
|
151
|
+
# :marshall_size => false # see ObjectSpaceAnalyser#analyse
|
152
|
+
#
|
153
|
+
def self.start(opt = {}, &block)
|
154
|
+
opts = DEFAULTS.merge(opt)
|
155
|
+
if block_given?
|
156
|
+
# get pre-block analysis of ObjectSpace
|
157
|
+
GC.start if opt[:force_gc]
|
158
|
+
prev = ObjectSpaceAnalyser.analyse(opt)
|
159
|
+
GC.start if opt[:force_gc]
|
160
|
+
|
161
|
+
yield
|
162
|
+
|
163
|
+
# get post-block analysis of ObjectSpace
|
164
|
+
GC.start if opt[:force_gc]
|
165
|
+
curr = ObjectSpaceAnalyser.analyse(opt)
|
166
|
+
|
167
|
+
# calculate the differences before and after execution
|
168
|
+
data = self._delta(curr, prev, opt)
|
169
|
+
|
170
|
+
# return it
|
171
|
+
GC.start if opt[:force_gc]
|
172
|
+
data
|
173
|
+
else
|
174
|
+
@@start_sync.synchronize(:EX) do
|
175
|
+
raise 'already started' if @@start_data
|
176
|
+
|
177
|
+
GC.start if opt[:force_gc]
|
178
|
+
@@start_data = [ObjectSpaceAnalyser.analyse(opt), opt]
|
179
|
+
GC.start if opt[:force_gc]
|
180
|
+
end
|
181
|
+
self
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
#
|
186
|
+
# Stops the current analysis and emite the results.
|
187
|
+
#
|
188
|
+
# See: #start
|
189
|
+
#
|
190
|
+
def self.stop
|
191
|
+
prev = nil
|
192
|
+
opt = nil
|
193
|
+
@@start_sync.synchronize(:EX) do
|
194
|
+
raise 'not started' unless @@start_data
|
195
|
+
prev, opt = @@start_data
|
196
|
+
@@start_data = nil
|
197
|
+
end
|
198
|
+
|
199
|
+
# get the current state of affairs
|
200
|
+
GC.start if opt[:force_gc]
|
201
|
+
curr = ObjectSpaceAnalyser.analyse(opt)
|
202
|
+
|
203
|
+
# calculate the differences before and after execution
|
204
|
+
data = self._delta(curr, prev, opt)
|
205
|
+
|
206
|
+
# return it
|
207
|
+
GC.start if opt[:force_gc]
|
208
|
+
data
|
209
|
+
end
|
210
|
+
|
211
|
+
#
|
212
|
+
# Stops the current analysis, emits the results, and immediately starts
|
213
|
+
# a new analysis.
|
214
|
+
#
|
215
|
+
# See: #stop, #start
|
216
|
+
#
|
217
|
+
def self.restart(opt = {})
|
218
|
+
res = self.stop
|
219
|
+
self.start(opt)
|
220
|
+
res
|
221
|
+
end
|
222
|
+
|
223
|
+
# => [ [Class, current, delta], ... ]
|
224
|
+
def self._delta(curr, prev, opt={}) #:nodoc:
|
225
|
+
opt = DEFAULTS.merge(opt)
|
226
|
+
|
227
|
+
# determine the difference between current and previous
|
228
|
+
delta = Hash.new(0)
|
229
|
+
(curr.keys + prev.keys).each do |k|
|
230
|
+
delta[k] = curr[k] - prev[k]
|
231
|
+
end
|
232
|
+
data = delta.map{|k,d| [k, curr[k].to_i, d]}
|
233
|
+
|
234
|
+
# organise data according to given options
|
235
|
+
case opt[:sort_by]
|
236
|
+
when :none
|
237
|
+
opt[:limit] = -1
|
238
|
+
when :current
|
239
|
+
data = data.sort_by{|k,c,d| -( c ) }
|
240
|
+
when :delta
|
241
|
+
data = data.sort_by{|k,c,d| -( d ) }
|
242
|
+
when :absdelta
|
243
|
+
data = data.sort_by{|k,c,d| -( d.abs ) }
|
244
|
+
else
|
245
|
+
warn "memoryProfiler: invalid option :sort_by => #{opt[:sort_by].inspect}; using :none"
|
246
|
+
opt[:limit] = -1
|
247
|
+
end
|
248
|
+
data = data[0,opt[:limit]] if opt[:limit] > 0 and opt[:limit] < data.length
|
249
|
+
|
250
|
+
# return it
|
251
|
+
data
|
252
|
+
end
|
253
|
+
|
254
|
+
#
|
255
|
+
# Formats data, such as that returns by #start , into a printable,
|
256
|
+
# readable string.
|
257
|
+
#
|
258
|
+
def self.format(data)
|
259
|
+
" Curr. Delta Class\n" +
|
260
|
+
" ----- ----- -----\n" +
|
261
|
+
data.map{|k,c,d| sprintf(" %5d %+5d %s\n", c, d, k.name) }.join
|
262
|
+
end
|
263
|
+
|
264
|
+
|
265
|
+
module ObjectSpaceAnalyser
|
266
|
+
#
|
267
|
+
# Returns a hash mapping each Class to its usage.
|
268
|
+
#
|
269
|
+
# If opt[:marshall_size] is true, the usage is estimated using Marshall.dump() for each instance,
|
270
|
+
# otherwise it is a simple instance count.
|
271
|
+
#
|
272
|
+
# If opt[:string_debug] is true, the analyser writes a text file containing every string
|
273
|
+
# in the Ruby ObjectSpace, at: /tmp/memory_profiler-<pid>-string-<time>.log
|
274
|
+
#
|
275
|
+
# Uses opt[:only] and opt[:ignore] , as per MemoryProfiler#start
|
276
|
+
#
|
277
|
+
def self.analyse(opt = {})
|
278
|
+
opt = MemoryProfiler::DEFAULTS.merge(opt)
|
279
|
+
marshall_size = !!opt[:marshall_size]
|
280
|
+
string_debug = !!opt[:string_debug]
|
281
|
+
ign = opt[:ignore]
|
282
|
+
only = opt[:only]
|
283
|
+
|
284
|
+
res = Hash.new(0)
|
285
|
+
str = [] if string_debug
|
286
|
+
ObjectSpace.each_object do |o|
|
287
|
+
if res[o.class] or ((only.empty? or only.any?{|y| o.is_a? y }) and ign.none?{|x| o.is_a? x })
|
288
|
+
res[o.class] += ( marshall_size ? self.__sizeof(o) : 1 )
|
289
|
+
end
|
290
|
+
str.push o.inspect if string_debug and o.class == String
|
291
|
+
end
|
292
|
+
if string_debug
|
293
|
+
self.__save str
|
294
|
+
str = nil
|
295
|
+
end
|
296
|
+
res
|
297
|
+
end
|
298
|
+
|
299
|
+
# Estimates the size of an object using Marshall.dump()
|
300
|
+
# Defaults to 1 if anything goes wrong.
|
301
|
+
def self.__sizeof(o) #:nodoc:
|
302
|
+
Marshall.dump(o).size
|
303
|
+
rescue ::Exception
|
304
|
+
1
|
305
|
+
end
|
306
|
+
|
307
|
+
# a single place where the magic filename is defined
|
308
|
+
def self.__save(str) #:nodoc:
|
309
|
+
File.open("/tmp/memory_profiler-#{Process.pid}-strings-#{Time.now.to_i}.log", 'w') do |f|
|
310
|
+
str.sort.each{|s| f.puts s }
|
311
|
+
end
|
312
|
+
str = nil
|
313
|
+
end
|
314
|
+
end
|
315
|
+
end
|
316
|
+
|
317
|
+
|
318
|
+
if $0 == __FILE__
|
319
|
+
puts MemoryProfiler.start_daemon( :limit=>5, :delay=>10, :marshall_size=>true, :sort_by=>:absdelta )
|
320
|
+
|
321
|
+
5.times do
|
322
|
+
blah = Hash.new([])
|
323
|
+
rpt = MemoryProfiler.start( :limit=>10 ) do
|
324
|
+
100.times{ blah[1] << 'aaaaa' }
|
325
|
+
1000.times{ blah[2] << 'bbbbb' }
|
326
|
+
end
|
327
|
+
puts MemoryProfiler.format(rpt)
|
328
|
+
sleep 7
|
329
|
+
end
|
330
|
+
|
331
|
+
MemoryProfiler.stop_daemon
|
332
|
+
end
|
333
|
+
|
metadata
ADDED
@@ -0,0 +1,79 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: memory-profiler
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 23
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 0
|
9
|
+
- 0
|
10
|
+
version: 1.0.0
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Matthew Kerwin
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-01-30 00:00:00 +10:00
|
19
|
+
default_executable:
|
20
|
+
dependencies: []
|
21
|
+
|
22
|
+
description: A rudimentary memory profiler that uses pure in-VM techniques to analyse the object space and attempt to determine memory usage trends.
|
23
|
+
email:
|
24
|
+
- matthew@kerwin.net.au
|
25
|
+
executables: []
|
26
|
+
|
27
|
+
extensions: []
|
28
|
+
|
29
|
+
extra_rdoc_files: []
|
30
|
+
|
31
|
+
files:
|
32
|
+
- lib/memory-profiler.rb
|
33
|
+
- Gemfile
|
34
|
+
- README
|
35
|
+
- Rakefile
|
36
|
+
- Gemfile.lock
|
37
|
+
- LICENSE
|
38
|
+
has_rdoc: true
|
39
|
+
homepage: http://code.google.com/p/memory-profiler-ruby/
|
40
|
+
licenses:
|
41
|
+
- Apache License 2.0
|
42
|
+
post_install_message:
|
43
|
+
rdoc_options:
|
44
|
+
- --title
|
45
|
+
- Memory Profiler
|
46
|
+
- --main
|
47
|
+
- MemoryProfiler
|
48
|
+
- --line-numbers
|
49
|
+
require_paths:
|
50
|
+
- lib
|
51
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
52
|
+
none: false
|
53
|
+
requirements:
|
54
|
+
- - ">="
|
55
|
+
- !ruby/object:Gem::Version
|
56
|
+
hash: 3
|
57
|
+
segments:
|
58
|
+
- 0
|
59
|
+
version: "0"
|
60
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
61
|
+
none: false
|
62
|
+
requirements:
|
63
|
+
- - ">="
|
64
|
+
- !ruby/object:Gem::Version
|
65
|
+
hash: 23
|
66
|
+
segments:
|
67
|
+
- 1
|
68
|
+
- 3
|
69
|
+
- 6
|
70
|
+
version: 1.3.6
|
71
|
+
requirements: []
|
72
|
+
|
73
|
+
rubyforge_project: mem-prof-ruby
|
74
|
+
rubygems_version: 1.4.2
|
75
|
+
signing_key:
|
76
|
+
specification_version: 3
|
77
|
+
summary: A Ruby Memory Profiler
|
78
|
+
test_files: []
|
79
|
+
|