bleak_house 3.6 → 3.7
Sign up to get free protection for your applications and to get access to all the features.
- data.tar.gz.sig +0 -0
- data/CHANGELOG +2 -0
- data/Manifest +1 -2
- data/README +17 -9
- data/TODO +1 -0
- data/bin/bleak +14 -0
- data/bleak_house.gemspec +8 -7
- data/ext/bleak_house/logger/build_ruby.rb +29 -1
- data/ext/bleak_house/logger/snapshot.c +65 -13
- data/ext/bleak_house/logger/snapshot.h +3 -0
- data/lib/bleak_house/analyzer/analyzer.rb +312 -202
- data/lib/bleak_house/logger.rb +0 -1
- data/lib/bleak_house/logger/source.rb +17 -0
- data/lib/bleak_house/rails/bleak_house.rb +2 -0
- data/lib/bleak_house/rails/dispatcher.rb +30 -9
- data/lib/bleak_house/support/core_extensions.rb +9 -18
- data/test/integration/server_test.rb +6 -4
- data/test/unit/test_bleak_house.rb +27 -10
- metadata +4 -5
- metadata.gz.sig +0 -0
- data/lib/bleak_house/logger/mem_usage.rb +0 -13
- data/test/integration/app/log/bleak_house_production.dump +0 -237342
data.tar.gz.sig
CHANGED
Binary file
|
data/CHANGELOG
CHANGED
data/Manifest
CHANGED
@@ -9,7 +9,7 @@ init.rb
|
|
9
9
|
install.rb
|
10
10
|
lib/bleak_house/analyzer/analyzer.rb
|
11
11
|
lib/bleak_house/analyzer.rb
|
12
|
-
lib/bleak_house/logger/
|
12
|
+
lib/bleak_house/logger/source.rb
|
13
13
|
lib/bleak_house/logger.rb
|
14
14
|
lib/bleak_house/rails/action_controller.rb
|
15
15
|
lib/bleak_house/rails/bleak_house.rb
|
@@ -39,7 +39,6 @@ test/integration/app/config/initializers/inflections.rb
|
|
39
39
|
test/integration/app/config/initializers/mime_types.rb
|
40
40
|
test/integration/app/config/routes.rb
|
41
41
|
test/integration/app/doc/README_FOR_APP
|
42
|
-
test/integration/app/log/bleak_house_production.dump
|
43
42
|
test/integration/app/public/404.html
|
44
43
|
test/integration/app/public/422.html
|
45
44
|
test/integration/app/public/500.html
|
data/README
CHANGED
@@ -5,22 +5,24 @@ A library for finding memory leaks.
|
|
5
5
|
|
6
6
|
== License
|
7
7
|
|
8
|
-
Copyright 2007 Cloudburst, LLC. See the included LICENSE file. Portions copyright 2006 Eric Hodel and used with permission. See the included LICENSE_BSD file.
|
8
|
+
Copyright 2007, 2008 Cloudburst, LLC. Licensed under the AFL 3. See the included LICENSE file. Portions copyright 2006 Eric Hodel and used with permission. See the included LICENSE_BSD file.
|
9
9
|
|
10
|
-
The public certificate for this gem is
|
10
|
+
The public certificate for this gem is here[link:http://rubyforge.org/frs/download.php/25331/evan_weaver-original-public_cert.pem].
|
11
|
+
|
12
|
+
If you like this software, please {make a donation to charity}[link:http://blog.evanweaver.com/donate/] or {recommend Evan}[link:http://www.workingwithrails.com/person/7739-evan-weaver] at Working with Rails.
|
11
13
|
|
12
14
|
== Features
|
13
15
|
|
14
16
|
* leak-proof C instrumentation
|
15
|
-
*
|
16
|
-
*
|
17
|
+
* tracks all objects and symbols
|
18
|
+
* inspection of sample leaked objects (optional)
|
17
19
|
* easy Rails integration
|
18
20
|
|
19
21
|
== Requirements
|
20
22
|
|
21
23
|
* A unix-like operating system
|
22
24
|
* Ruby 1.8.6
|
23
|
-
* Rails 2.
|
25
|
+
* Rails 1.2.x or 2.0.x
|
24
26
|
|
25
27
|
= Usage
|
26
28
|
|
@@ -29,7 +31,7 @@ The public certificate for this gem is at http://rubyforge.org/frs/download.php/
|
|
29
31
|
Install the gem:
|
30
32
|
sudo gem install bleak_house
|
31
33
|
|
32
|
-
The installation takes a long time because it compiles a patched Ruby binary for you. It is installed as <tt>ruby-bleak-house</tt> alongside your regular <tt>ruby</tt>binary.
|
34
|
+
The installation takes a long time because it compiles a patched Ruby binary for you. It is installed as <tt>ruby-bleak-house</tt> alongside your regular <tt>ruby</tt> binary.
|
33
35
|
|
34
36
|
Please see the forum ( http://rubyforge.org/forum/forum.php?forum_id=13983 ) if you have installation problems.
|
35
37
|
|
@@ -40,12 +42,18 @@ To setup a Rails app for profiling, just <tt>require 'bleak_house'</tt> in <tt>c
|
|
40
42
|
Then, to engage the logger (ideally, in a live deployment situation), start a server instance as so:
|
41
43
|
RAILS_ENV=production BLEAK_HOUSE=true ruby-bleak-house ./script/server
|
42
44
|
|
43
|
-
Browse around manually, thrash your entire app with a script, target troublesome controllers/actions, etc. The BleakHouse logger will dump a huge amount of data to <tt>log/bleak_house_production.dump</tt>--keep an eye on your disk space.
|
45
|
+
Browse around manually, thrash your entire app with a script, target troublesome controllers/actions, etc. The BleakHouse logger will dump a huge amount of data to <tt>log/bleak_house_production.dump</tt>--keep an eye on your disk space. I usually aim for a 3GB dump.
|
44
46
|
|
45
47
|
Then, to analyze the dump:
|
46
48
|
bleak path/to/log/bleak_house_production.dump
|
47
49
|
|
48
|
-
Be patient.
|
50
|
+
Be patient; it's quite slow.
|
51
|
+
|
52
|
+
== Sampling object contents
|
53
|
+
|
54
|
+
BleakHouse can sample object contents, which might help you figure out the source of the leak. Just add <tt>SAMPLE_RATE=0.1</tt> to the environment string when you start your server.
|
55
|
+
|
56
|
+
Note that there is a chance this could _introduce_ leaks if you override <tt>inspect</tt> in leaky ways. Unfortunately, the samping is of dubious usefulness and really increases the memory usage of the analyze task. That's why it's off by default.
|
49
57
|
|
50
58
|
= Extras
|
51
59
|
|
@@ -61,7 +69,7 @@ You may get library require errors if you install <tt>ruby-bleak-house</tt> 1.8.
|
|
61
69
|
|
62
70
|
== Reporting problems
|
63
71
|
|
64
|
-
|
72
|
+
The support forum is here[http://rubyforge.org/forum/forum.php?forum_id=13983].
|
65
73
|
|
66
74
|
Patches and contributions are very welcome. Please note that contributors are required to assign copyright for their additions to Cloudburst, LLC.
|
67
75
|
|
data/TODO
CHANGED
data/bin/bleak
CHANGED
@@ -8,5 +8,19 @@ if !ARGV[0]
|
|
8
8
|
else
|
9
9
|
$LOAD_PATH << "#{File.dirname(__FILE__)}/../lib/"
|
10
10
|
require 'bleak_house/analyzer'
|
11
|
+
|
12
|
+
require 'ruby-debug' if ENV['DEBUG']
|
13
|
+
|
14
|
+
if ENV['WATCH_MEM']
|
15
|
+
Thread.new do
|
16
|
+
while sleep(10)
|
17
|
+
free = `free -m`.split("\n")[2].split(" ")[3].to_i
|
18
|
+
if free < 20
|
19
|
+
exit!
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
11
25
|
BleakHouse::Analyzer.run(ARGV[0])
|
12
26
|
end
|
data/bleak_house.gemspec
CHANGED
@@ -1,22 +1,22 @@
|
|
1
1
|
|
2
|
-
# Gem::Specification for Bleak_house-3.
|
2
|
+
# Gem::Specification for Bleak_house-3.7
|
3
3
|
# Originally generated by Echoe
|
4
4
|
|
5
5
|
Gem::Specification.new do |s|
|
6
6
|
s.name = %q{bleak_house}
|
7
|
-
s.version = "3.
|
7
|
+
s.version = "3.7"
|
8
8
|
|
9
9
|
s.specification_version = 2 if s.respond_to? :specification_version=
|
10
10
|
|
11
11
|
s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
|
12
12
|
s.authors = ["Evan Weaver"]
|
13
|
-
s.date = %q{2008-01-
|
13
|
+
s.date = %q{2008-01-17}
|
14
14
|
s.default_executable = %q{bleak}
|
15
15
|
s.description = %q{A library for finding memory leaks.}
|
16
16
|
s.email = %q{}
|
17
17
|
s.executables = ["bleak"]
|
18
18
|
s.extensions = ["ext/bleak_house/logger/extconf.rb"]
|
19
|
-
s.files = ["bin/bleak", "CHANGELOG", "ext/bleak_house/logger/build_logger.rb", "ext/bleak_house/logger/build_ruby.rb", "ext/bleak_house/logger/extconf.rb", "ext/bleak_house/logger/snapshot.c", "ext/bleak_house/logger/snapshot.h", "init.rb", "install.rb", "lib/bleak_house/analyzer/analyzer.rb", "lib/bleak_house/analyzer.rb", "lib/bleak_house/logger/
|
19
|
+
s.files = ["bin/bleak", "CHANGELOG", "ext/bleak_house/logger/build_logger.rb", "ext/bleak_house/logger/build_ruby.rb", "ext/bleak_house/logger/extconf.rb", "ext/bleak_house/logger/snapshot.c", "ext/bleak_house/logger/snapshot.h", "init.rb", "install.rb", "lib/bleak_house/analyzer/analyzer.rb", "lib/bleak_house/analyzer.rb", "lib/bleak_house/logger/source.rb", "lib/bleak_house/logger.rb", "lib/bleak_house/rails/action_controller.rb", "lib/bleak_house/rails/bleak_house.rb", "lib/bleak_house/rails/dispatcher.rb", "lib/bleak_house/rails.rb", "lib/bleak_house/support/core_extensions.rb", "lib/bleak_house.rb", "LICENSE", "LICENSE_BSD", "Manifest", "README", "ruby/gc.c.patch", "ruby/parse.y.patch", "ruby/ruby-1.8.6-p110.tar.bz2", "test/integration/app/app/controllers/application.rb", "test/integration/app/app/controllers/items_controller.rb", "test/integration/app/app/helpers/application_helper.rb", "test/integration/app/app/helpers/items_helper.rb", "test/integration/app/app/views/items/index.rhtml", "test/integration/app/config/boot.rb", "test/integration/app/config/database.yml", "test/integration/app/config/environment.rb", "test/integration/app/config/environments/development.rb", "test/integration/app/config/environments/production.rb", "test/integration/app/config/environments/test.rb", "test/integration/app/config/initializers/inflections.rb", "test/integration/app/config/initializers/mime_types.rb", "test/integration/app/config/routes.rb", "test/integration/app/doc/README_FOR_APP", "test/integration/app/public/404.html", "test/integration/app/public/422.html", "test/integration/app/public/500.html", "test/integration/app/public/dispatch.cgi", "test/integration/app/public/dispatch.fcgi", "test/integration/app/public/dispatch.rb", "test/integration/app/public/favicon.ico", "test/integration/app/public/images/rails.png", "test/integration/app/public/javascripts/application.js", "test/integration/app/public/javascripts/controls.js", "test/integration/app/public/javascripts/dragdrop.js", "test/integration/app/public/javascripts/effects.js", "test/integration/app/public/javascripts/prototype.js", "test/integration/app/public/robots.txt", "test/integration/app/Rakefile", "test/integration/app/README", "test/integration/app/script/about", "test/integration/app/script/console", "test/integration/app/script/destroy", "test/integration/app/script/generate", "test/integration/app/script/performance/benchmarker", "test/integration/app/script/performance/profiler", "test/integration/app/script/performance/request", "test/integration/app/script/plugin", "test/integration/app/script/process/inspector", "test/integration/app/script/process/reaper", "test/integration/app/script/process/spawner", "test/integration/app/script/runner", "test/integration/app/script/server", "test/integration/app/test/functional/items_controller_test.rb", "test/integration/app/test/test_helper.rb", "test/integration/server_test.rb", "test/misc/direct.rb", "test/test_helper.rb", "test/unit/test_bleak_house.rb", "TODO", "bleak_house.gemspec"]
|
20
20
|
s.has_rdoc = true
|
21
21
|
s.homepage = %q{http://blog.evanweaver.com/files/doc/fauna/bleak_house/}
|
22
22
|
s.require_paths = ["lib", "ext"]
|
@@ -25,7 +25,7 @@ Gem::Specification.new do |s|
|
|
25
25
|
s.summary = %q{A library for finding memory leaks.}
|
26
26
|
s.test_files = ["test/integration/server_test.rb", "test/unit/test_bleak_house.rb"]
|
27
27
|
|
28
|
-
s.add_dependency(%q<ccsv>, [">= 0"])
|
28
|
+
s.add_dependency(%q<ccsv>, [">= 0.1"])
|
29
29
|
end
|
30
30
|
|
31
31
|
|
@@ -40,9 +40,10 @@ end
|
|
40
40
|
# p.summary = "A library for finding memory leaks."
|
41
41
|
# p.url = "http://blog.evanweaver.com/files/doc/fauna/bleak_house/"
|
42
42
|
# p.docs_host = 'blog.evanweaver.com:~/www/bax/public/files/doc/'
|
43
|
-
# p.dependencies = ['ccsv']
|
43
|
+
# p.dependencies = ['ccsv >=0.1'] # 'memory >=0.0.2'
|
44
44
|
# p.require_signed = true
|
45
45
|
#
|
46
46
|
# p.rdoc_pattern = /^ext.*\.c|lib.*logger.*rb|analyzer|rails\/bleak_house|^README|^CHANGELOG|^TODO|^LICENSE|^COPYING$/
|
47
|
-
# p.test_pattern = ["test/integration/*.rb", "test/unit/*.rb"]
|
47
|
+
# p.test_pattern = ["test/integration/*.rb", "test/unit/*.rb"]
|
48
|
+
# p.clean_pattern << "**/bleak_house*dump*"
|
48
49
|
# end
|
@@ -5,10 +5,15 @@ if RUBY_PLATFORM =~ /win32|windows/
|
|
5
5
|
raise "Windows is not supported."
|
6
6
|
end
|
7
7
|
|
8
|
+
unless RUBY_VERSION == '1.8.6'
|
9
|
+
raise "Wrong Ruby version, you're at '#{RUBY_VERSION}', need 1.8.6"
|
10
|
+
end
|
11
|
+
|
8
12
|
source_dir = File.expand_path(File.dirname(__FILE__)) + "/../../../ruby"
|
9
13
|
tmp = "/tmp/"
|
10
14
|
|
11
15
|
require 'fileutils'
|
16
|
+
require 'rbconfig'
|
12
17
|
|
13
18
|
def which(basename)
|
14
19
|
# system('which') is not compatible across Linux and BSD
|
@@ -17,7 +22,7 @@ def which(basename)
|
|
17
22
|
path if File.exist? path
|
18
23
|
end
|
19
24
|
end
|
20
|
-
|
25
|
+
|
21
26
|
unless which('ruby-bleak-house')
|
22
27
|
|
23
28
|
Dir.chdir(tmp) do
|
@@ -43,6 +48,29 @@ unless which('ruby-bleak-house')
|
|
43
48
|
system("patch -p0 < \'#{source_dir}/gc.c.patch\' > ../gc.c.patch.log 2>&1")
|
44
49
|
system("patch -p0 < \'#{source_dir}/parse.y.patch\' > ../parse.y.patch.log 2>&1")
|
45
50
|
system("./configure --prefix=#{binary_dir[0..-5]} > ../configure.log 2>&1") # --with-static-linked-ext
|
51
|
+
|
52
|
+
# Patch the makefile for arch/sitedir
|
53
|
+
makefile = File.read('Makefile')
|
54
|
+
%w{arch sitearch sitedir}.each do | key |
|
55
|
+
makefile.gsub!(/#{key} = .*/, "#{key} = #{Config::CONFIG[key]}")
|
56
|
+
end
|
57
|
+
File.open('Makefile', 'w'){|f| f.puts(makefile)}
|
58
|
+
|
59
|
+
# Patch the config.h for constants
|
60
|
+
constants = {
|
61
|
+
'RUBY_LIB' => 'rubylibdir', #define RUBY_LIB "/usr/lib/ruby/1.8"
|
62
|
+
'RUBY_SITE_LIB' => 'sitedir', #define RUBY_SITE_LIB "/usr/lib/ruby/site_ruby"
|
63
|
+
'RUBY_SITE_LIB2' => 'sitelibdir', #define RUBY_SITE_LIB2 "/usr/lib/ruby/site_ruby/1.8"
|
64
|
+
'RUBY_PLATFORM' => 'arch', #define RUBY_PLATFORM "i686-linux"
|
65
|
+
'RUBY_ARCHLIB' => 'topdir', #define RUBY_ARCHLIB "/usr/lib/ruby/1.8/i686-linux"
|
66
|
+
'RUBY_SITE_ARCHLIB' => 'sitearchdir' #define RUBY_SITE_ARCHLIB "/usr/lib/ruby/site_ruby/1.8/i686-linux"
|
67
|
+
}
|
68
|
+
config_h = File.read('config.h')
|
69
|
+
constants.each do | const, key |
|
70
|
+
config_h.gsub!(/#define #{const} .*/, "#define #{const} \"#{Config::CONFIG[key]}\"")
|
71
|
+
end
|
72
|
+
File.open('config.h', 'w'){|f| f.puts(config_h)}
|
73
|
+
|
46
74
|
system("make > ../make.log 2>&1")
|
47
75
|
|
48
76
|
binary = "#{binary_dir}/ruby-bleak-house"
|
@@ -15,13 +15,15 @@ static VALUE heaps_length(VALUE self) {
|
|
15
15
|
}
|
16
16
|
|
17
17
|
/* Count the live objects on the heap and in the symbol table and write a CSV frame to <tt>_logfile</tt>. Set <tt>_specials = true</tt> if you also want to count AST nodes and var scopes; otherwise, use <tt>false</tt>. Note that common classes in the CSV output are hashed to small integers in order to save space.*/
|
18
|
-
static VALUE snapshot(VALUE self, VALUE _logfile, VALUE tag, VALUE _specials) {
|
18
|
+
static VALUE snapshot(VALUE self, VALUE _logfile, VALUE tag, VALUE _specials, VALUE _sampler) {
|
19
19
|
Check_Type(_logfile, T_STRING);
|
20
20
|
Check_Type(tag, T_STRING);
|
21
21
|
|
22
22
|
RVALUE *obj, *obj_end;
|
23
23
|
st_table_entry *sym;
|
24
24
|
|
25
|
+
/* printf("Requested: %f\n", rb_num2dbl(_sampler)); */
|
26
|
+
|
25
27
|
struct heaps_slot * heaps = rb_gc_heap_slots();
|
26
28
|
struct st_table * sym_tbl = rb_parse_sym_tbl();
|
27
29
|
|
@@ -42,9 +44,9 @@ static VALUE snapshot(VALUE self, VALUE _logfile, VALUE tag, VALUE _specials) {
|
|
42
44
|
fprintf(logfile, "-1,%li\n", time(0));
|
43
45
|
|
44
46
|
/* get and write the memory usage */
|
45
|
-
VALUE mem = rb_funcall(self, rb_intern("mem_usage"), 0);
|
47
|
+
/* VALUE mem = rb_funcall(self, rb_intern("mem_usage"), 0);
|
46
48
|
fprintf(logfile, "-2,%li\n", NUM2INT(RARRAY_PTR(mem)[0]));
|
47
|
-
fprintf(logfile, "-3,%li\n", NUM2INT(RARRAY_PTR(mem)[1]));
|
49
|
+
fprintf(logfile, "-3,%li\n", NUM2INT(RARRAY_PTR(mem)[1])); */
|
48
50
|
|
49
51
|
int filled_slots = 0;
|
50
52
|
int free_slots = 0;
|
@@ -81,17 +83,32 @@ static VALUE snapshot(VALUE self, VALUE _logfile, VALUE tag, VALUE _specials) {
|
|
81
83
|
hashed = lookup_builtin(rb_obj_classname((VALUE)obj));
|
82
84
|
}
|
83
85
|
}
|
86
|
+
|
84
87
|
/* write to log */
|
85
|
-
if (hashed <
|
86
|
-
/*
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
88
|
+
if (specials || hashed < BUILTINS_SIZE) {
|
89
|
+
/* write classname */
|
90
|
+
if (hashed < 0) {
|
91
|
+
/* regular classname */
|
92
|
+
fprintf(logfile, "%s", rb_obj_classname((VALUE)obj));
|
93
|
+
} else {
|
94
|
+
/* builtins key */
|
91
95
|
/* 0 is not used for 'hashed' because Ruby's to_i turns any String into 0 */
|
92
|
-
fprintf(logfile, "%i
|
96
|
+
fprintf(logfile, "%i", hashed + 1);
|
93
97
|
}
|
94
|
-
|
98
|
+
|
99
|
+
/* write id */
|
100
|
+
fprintf(logfile, ",%lu", FIX2ULONG(rb_obj_id((VALUE)obj)));
|
101
|
+
|
102
|
+
/* write sample, if requested */
|
103
|
+
if (hashed < BUILTINS_SIZE) {
|
104
|
+
if (rand()/((double)RAND_MAX + 1) < rb_num2dbl(_sampler)) {
|
105
|
+
fprintf(logfile, ",%s", rb_rescue(inspect, (VALUE)obj, handle_exception, Qnil));
|
106
|
+
}
|
107
|
+
}
|
108
|
+
|
109
|
+
/* write newline */
|
110
|
+
fprintf(logfile, "\n");
|
111
|
+
}
|
95
112
|
} else {
|
96
113
|
free_slots ++;
|
97
114
|
}
|
@@ -110,10 +127,45 @@ static VALUE snapshot(VALUE self, VALUE _logfile, VALUE tag, VALUE _specials) {
|
|
110
127
|
fprintf(logfile, "-6,%i\n", free_slots);
|
111
128
|
fclose(logfile);
|
112
129
|
|
113
|
-
|
130
|
+
for (i = 0; i < 3; i++) {
|
131
|
+
/* request GC run */
|
132
|
+
rb_funcall(rb_mGC, rb_intern("start"), 0);
|
133
|
+
}
|
134
|
+
|
114
135
|
return Qtrue;
|
115
136
|
}
|
116
137
|
|
138
|
+
char * inspect(VALUE obj) {
|
139
|
+
VALUE value;
|
140
|
+
char * string;
|
141
|
+
int i, length;
|
142
|
+
|
143
|
+
value = rb_funcall((VALUE)obj, rb_intern("inspect"), 0);
|
144
|
+
string = StringValueCStr(value);
|
145
|
+
length = strlen(string);
|
146
|
+
|
147
|
+
if (length > MAX_SAMPLE_LENGTH) {
|
148
|
+
string[MAX_SAMPLE_LENGTH] = '\0';
|
149
|
+
length = MAX_SAMPLE_LENGTH;
|
150
|
+
}
|
151
|
+
|
152
|
+
/* Replace control characters */
|
153
|
+
for (i = 0; i < length; i++) {
|
154
|
+
if (string[i] == '\n') {
|
155
|
+
string[i] = ' ';
|
156
|
+
} if (string[i] == ',') {
|
157
|
+
string[i] = ' ';
|
158
|
+
}
|
159
|
+
}
|
160
|
+
|
161
|
+
/* result = rb_funcall(result, rb_intern("gsub"), 2, rb_str_new2(","), rb_str_new2(",")); */
|
162
|
+
return string;
|
163
|
+
}
|
164
|
+
|
165
|
+
char * handle_exception(VALUE unused) {
|
166
|
+
return "";
|
167
|
+
}
|
168
|
+
|
117
169
|
int lookup_builtin(char * name) {
|
118
170
|
int i;
|
119
171
|
for (i = 0; i < BUILTINS_SIZE; i++) {
|
@@ -146,7 +198,7 @@ Init_snapshot()
|
|
146
198
|
{
|
147
199
|
rb_mBleakHouse = rb_define_module("BleakHouse");
|
148
200
|
rb_cC = rb_define_class_under(rb_mBleakHouse, "Logger", rb_cObject);
|
149
|
-
rb_define_method(rb_cC, "snapshot", snapshot,
|
201
|
+
rb_define_method(rb_cC, "snapshot", snapshot, 4);
|
150
202
|
rb_define_method(rb_cC, "heaps_used", heaps_used, 0);
|
151
203
|
rb_define_method(rb_cC, "heaps_length", heaps_length, 0);
|
152
204
|
}
|
@@ -50,6 +50,7 @@ static char * builtins[] = {
|
|
50
50
|
|
51
51
|
#define BUILTINS_SIZE 30
|
52
52
|
#define SPECIALS_SIZE 7
|
53
|
+
#define MAX_SAMPLE_LENGTH 100
|
53
54
|
|
54
55
|
typedef struct RVALUE {
|
55
56
|
union {
|
@@ -96,4 +97,6 @@ struct heaps_slot * rb_gc_heap_slots();
|
|
96
97
|
int rb_gc_heaps_used();
|
97
98
|
int rb_gc_heaps_length();
|
98
99
|
|
100
|
+
char * inspect(VALUE);
|
101
|
+
char * handle_exception(VALUE);
|
99
102
|
int lookup_builtin(char *);
|
@@ -1,247 +1,357 @@
|
|
1
1
|
|
2
2
|
require 'ccsv'
|
3
|
+
# require 'memory'
|
3
4
|
require 'fileutils'
|
4
5
|
require 'yaml'
|
5
6
|
require 'pp'
|
6
7
|
|
7
8
|
module BleakHouse
|
8
9
|
|
9
|
-
class Analyzer
|
10
|
-
|
11
|
-
|
12
|
-
-1 =>
|
13
|
-
-2 => 'mem usage/swap',
|
14
|
-
-3 => 'mem usage/real',
|
15
|
-
-4 =>
|
16
|
-
-5 => 'heap/filled',
|
17
|
-
-6 => 'heap/free'
|
18
|
-
}
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
return item if yield(item)
|
32
|
-
i -= 1
|
33
|
-
end
|
34
|
-
end
|
35
|
-
|
36
|
-
def self.calculate!(frame, index, total, obj_count = nil)
|
37
|
-
bsize = frame['births'].size
|
38
|
-
dsize = frame['deaths'].size
|
39
|
-
|
40
|
-
# Avoid divide by zero errors
|
41
|
-
frame['meta']['ratio'] = ratio = (bsize - dsize) / (bsize + dsize + 1).to_f
|
42
|
-
frame['meta']['impact'] = begin
|
43
|
-
result = Math.log10((bsize - dsize).abs.to_i / 10.0)
|
44
|
-
raise Errno::ERANGE if result.nan? or result.infinite?
|
45
|
-
result
|
46
|
-
rescue Errno::ERANGE
|
47
|
-
0
|
48
|
-
end
|
49
|
-
|
50
|
-
puts " F#{index}:#{total} (#{index * 100 / total}%): #{frame['meta']['tag']} (#{obj_count.to_s + ' population, ' if obj_count}#{bsize} births, #{dsize} deaths, ratio #{format('%.2f', frame['meta']['ratio'])}, impact #{format('%.2f', frame['meta']['impact'])})"
|
10
|
+
class Analyzer
|
11
|
+
|
12
|
+
SPECIALS = {
|
13
|
+
-1 => :timestamp,
|
14
|
+
-2 => :'mem usage/swap', # Not used
|
15
|
+
-3 => :'mem usage/real', # Not used
|
16
|
+
-4 => :tag,
|
17
|
+
-5 => :'heap/filled',
|
18
|
+
-6 => :'heap/free'
|
19
|
+
}
|
20
|
+
|
21
|
+
# Might be better as a per-tag skip but that gets kinda complicated
|
22
|
+
initial_skip = (ENV['INITIAL_SKIP'] || 15).to_i
|
23
|
+
INITIAL_SKIP = initial_skip < 2 ? 2 : initial_skip
|
24
|
+
|
25
|
+
DISPLAY_SAMPLES = (ENV['DISPLAY_SAMPLES'] || 5).to_i
|
26
|
+
|
27
|
+
class_key_source = File.dirname(__FILE__) + '/../../../ext/bleak_house/logger/snapshot.h'
|
28
|
+
class_key_string = open(class_key_source).read[/\{(.*?)\}/m, 1]
|
29
|
+
# Skip 0 so that the output of String#to_s is useful
|
30
|
+
CLASS_KEYS = eval("[nil, #{class_key_string} ]").map do |class_name|
|
31
|
+
class_name.to_sym if class_name
|
51
32
|
end
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
33
|
+
|
34
|
+
class << self
|
35
|
+
|
36
|
+
def reverse_detect(array)
|
37
|
+
i = array.size - 1
|
38
|
+
while i >= 0
|
39
|
+
item = array[i]
|
40
|
+
return item if yield(item)
|
41
|
+
i -= 1
|
42
|
+
end
|
61
43
|
end
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
71
|
-
|
72
|
-
|
73
|
-
|
74
|
-
|
75
|
-
frames[0..-2].each_with_index do |frame, index|
|
76
|
-
calculate!(frame, index + 1, frames.size - 1)
|
44
|
+
|
45
|
+
def calculate!(frame, index, total, population = nil)
|
46
|
+
bsize = frame[:births].size
|
47
|
+
dsize = frame[:deaths].size
|
48
|
+
|
49
|
+
# Avoid divide by zero errors
|
50
|
+
frame[:meta][:ratio] = ratio = (bsize - dsize) / (bsize + dsize + 1).to_f
|
51
|
+
frame[:meta][:impact] = begin
|
52
|
+
result = Math.log10((bsize - dsize).abs.to_i / 10.0)
|
53
|
+
raise Errno::ERANGE if result.nan? or result.infinite?
|
54
|
+
result
|
55
|
+
rescue Errno::ERANGE
|
56
|
+
0
|
77
57
|
end
|
58
|
+
|
59
|
+
puts " F#{index}:#{total} (#{index * 100 / total}%): #{frame[:meta][:tag]} (#{population.to_s + ' population, ' if population}#{bsize} births, #{dsize} deaths, ratio #{format('%.2f', frame[:meta][:ratio])}, impact #{format('%.2f', frame[:meta][:impact])})"
|
60
|
+
end
|
61
|
+
|
62
|
+
# Read a frames object from a cache file.
|
63
|
+
def read_cache(cachefile)
|
64
|
+
frames = Marshal.load(File.open(cachefile).read)[0..-2]
|
65
|
+
total_frames = frames.size - 1
|
66
|
+
announce_total(total_frames)
|
78
67
|
|
79
|
-
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
68
|
+
frames[0..-2].each_with_index do |frame, index|
|
69
|
+
calculate!(frame, index + 1, total_frames)
|
70
|
+
end
|
71
|
+
frames
|
72
|
+
end
|
73
|
+
|
74
|
+
def announce_total(total_frames)
|
75
|
+
puts "#{total_frames} frames"
|
84
76
|
|
85
77
|
if total_frames < INITIAL_SKIP * 3
|
86
78
|
puts "Not enough frames for accurate results. Please record at least #{INITIAL_SKIP * 3} frames."
|
87
79
|
exit # Should be exit! but that messes up backticks capturing in the tests
|
88
80
|
end
|
81
|
+
end
|
82
|
+
|
83
|
+
# Cache an object to disk.
|
84
|
+
def write_cache(object, cachefile)
|
85
|
+
Thread.exclusive do
|
86
|
+
File.open(cachefile, 'w') do |f|
|
87
|
+
f.write Marshal.dump(object)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
# Rebuild frames
|
93
|
+
def read(logfile, cachefile)
|
94
|
+
total_frames = `grep '^-1' #{logfile} | wc`.to_i - 2
|
95
|
+
announce_total(total_frames)
|
89
96
|
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
97
|
+
frames = loop(logfile, cachefile, total_frames)
|
98
|
+
end
|
99
|
+
|
100
|
+
# Convert the class and id columns to Fixnums, if possible, and remove memory
|
101
|
+
# addresses from inspection samples.
|
102
|
+
def normalize_row(row)
|
103
|
+
|
104
|
+
# Broken out for speed (we don't want to generate a closure)
|
105
|
+
if (int = row[0].to_i) != 0
|
106
|
+
row[0] = int
|
107
|
+
else
|
108
|
+
row[0] = row[0].to_sym
|
109
|
+
end
|
110
|
+
|
111
|
+
if (int = row[1].to_i) != 0
|
112
|
+
row[1] = int
|
113
|
+
else
|
114
|
+
row[1] = row[1].to_sym
|
115
|
+
end
|
116
|
+
|
117
|
+
if row[2]
|
118
|
+
row[2] = row[2].gsub(/0x[\da-f]{8,16}/, "0xID").to_sym
|
119
|
+
end
|
120
|
+
|
121
|
+
row
|
122
|
+
end
|
123
|
+
|
124
|
+
# Inner loop of the raw log reader. The implementation is kind of a mess.
|
125
|
+
def loop(logfile, cachefile, total_frames)
|
126
|
+
|
127
|
+
frames = []
|
128
|
+
last_population = []
|
129
|
+
frame = nil
|
130
|
+
|
131
|
+
Ccsv.foreach(logfile) do |row|
|
132
|
+
|
133
|
+
class_index, id_or_tag, sampled_content = normalize_row(row)
|
134
|
+
|
135
|
+
# Check for frame headers
|
136
|
+
if class_index < 0
|
137
|
+
|
99
138
|
# Get frame meta-information
|
100
|
-
if
|
101
|
-
|
102
|
-
# The frame has ended; process the last one
|
139
|
+
if SPECIALS[class_index] == :timestamp
|
140
|
+
|
141
|
+
# The frame has ended; process the last one
|
103
142
|
if frame
|
104
|
-
population = frame[
|
143
|
+
population = frame[:objects].keys
|
105
144
|
births = population - last_population
|
106
145
|
deaths = last_population - population
|
107
146
|
last_population = population
|
108
|
-
|
109
|
-
#
|
110
|
-
frame[
|
111
|
-
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
147
|
+
|
148
|
+
# Assign births
|
149
|
+
frame[:births] = [] # Uses an Array to work around a Marshal bug
|
150
|
+
births.each do |key|
|
151
|
+
frame[:births] << [key, frame[:objects][key]]
|
152
|
+
end
|
153
|
+
|
154
|
+
# Assign deaths to previous frame
|
155
|
+
final = frames[-2]
|
156
|
+
if final
|
157
|
+
|
158
|
+
final[:deaths] = [] # Uses an Array to work around a Marshal bug
|
159
|
+
deaths.each do |key|
|
160
|
+
final[:deaths] << [key, [final[:objects][key].first]] # Don't need the sample content for deaths
|
161
|
+
end
|
162
|
+
|
163
|
+
# Try to reduce memory footprint
|
164
|
+
final.delete :objects
|
165
|
+
GC.start
|
166
|
+
sleep 1 # Give the GC thread a chance to do something
|
167
|
+
|
168
|
+
calculate!(final, frames.size - 1, total_frames, population.size)
|
118
169
|
end
|
119
170
|
end
|
120
|
-
|
171
|
+
|
121
172
|
# Set up a new frame
|
122
173
|
frame = {}
|
123
174
|
frames << frame
|
124
|
-
frame[
|
125
|
-
frame[
|
126
|
-
|
127
|
-
#
|
175
|
+
frame[:objects] ||= {}
|
176
|
+
frame[:meta] ||= {}
|
177
|
+
|
178
|
+
# Write out an in-process cache, in case you run out of RAM
|
179
|
+
if frames.size % 20 == 0
|
180
|
+
write_cache(frames, cachefile)
|
181
|
+
end
|
128
182
|
end
|
129
|
-
|
130
|
-
frame[
|
183
|
+
|
184
|
+
frame[:meta][SPECIALS[class_index]] = id_or_tag
|
131
185
|
else
|
132
|
-
#
|
133
|
-
|
186
|
+
# XXX Critical section
|
187
|
+
if sampled_content
|
188
|
+
# Normally object address strings and convert to a symbol
|
189
|
+
frame[:objects][id_or_tag] = [class_index, sampled_content]
|
190
|
+
else
|
191
|
+
frame[:objects][id_or_tag] = [class_index]
|
192
|
+
end
|
134
193
|
end
|
194
|
+
|
135
195
|
end
|
136
|
-
|
137
|
-
|
138
|
-
frames
|
139
|
-
|
140
|
-
#
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
196
|
+
|
197
|
+
# Work around for a Marshal/Hash bug on x86_64
|
198
|
+
frames[-2][:objects] = frames[-2][:objects].to_a
|
199
|
+
|
200
|
+
# Write the cache
|
201
|
+
write_cache(frames, cachefile)
|
202
|
+
|
203
|
+
# Junk last frame (read_cache also does this)
|
204
|
+
frames[0..-2]
|
145
205
|
end
|
146
|
-
|
147
|
-
|
148
|
-
|
149
|
-
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
206
|
+
|
207
|
+
# Convert births back to hashes, necessary due to the Marshal workaround
|
208
|
+
def rehash(frames)
|
209
|
+
frames.each do |frame|
|
210
|
+
frame[:births_hash] = {}
|
211
|
+
frame[:births].each do |key, value|
|
212
|
+
frame[:births_hash][key] = value
|
213
|
+
end
|
214
|
+
frame.delete(:births)
|
154
215
|
end
|
155
|
-
|
216
|
+
nil
|
156
217
|
end
|
157
218
|
|
158
|
-
#
|
159
|
-
|
160
|
-
|
161
|
-
|
162
|
-
# See what objects are still laying around
|
163
|
-
population = frames.last['objects'].reject do |key, value|
|
164
|
-
frames.first['births_hash'][key] == value
|
165
|
-
end
|
219
|
+
# Parses and correlates a BleakHouse::Logger output file.
|
220
|
+
def run(logfile)
|
221
|
+
logfile.chomp!(".cache")
|
222
|
+
cachefile = logfile + ".cache"
|
166
223
|
|
167
|
-
|
224
|
+
unless File.exists? logfile or File.exists? cachefile
|
225
|
+
puts "No data file found: #{logfile}"
|
226
|
+
exit
|
227
|
+
end
|
168
228
|
|
169
|
-
|
170
|
-
|
171
|
-
|
172
|
-
|
173
|
-
|
174
|
-
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
179
|
-
puts "\n#{total_births} total births, #{total_deaths} total deaths, #{population.size} uncollected objects."
|
180
|
-
|
181
|
-
leakers = {}
|
182
|
-
|
183
|
-
# debugger
|
184
|
-
|
185
|
-
# Find the sources of the leftover objects in the final population
|
186
|
-
population.each do |id, klass|
|
187
|
-
leaker = backwards_detect(frames) do |frame|
|
188
|
-
frame['births_hash'][id] == klass
|
189
|
-
end
|
190
|
-
if leaker
|
191
|
-
# debugger
|
192
|
-
tag = leaker['meta']['tag']
|
193
|
-
klass = CLASS_KEYS[klass] if klass.is_a? Fixnum
|
194
|
-
leakers[tag] ||= Hash.new(0)
|
195
|
-
leakers[tag][klass] += 1
|
229
|
+
puts "Working..."
|
230
|
+
|
231
|
+
frames = []
|
232
|
+
|
233
|
+
if File.exist?(cachefile) and (!File.exists? logfile or File.stat(cachefile).mtime > File.stat(logfile).mtime)
|
234
|
+
puts "Using cache"
|
235
|
+
frames = read_cache(cachefile)
|
236
|
+
else
|
237
|
+
frames = read(logfile, cachefile)
|
196
238
|
end
|
197
|
-
|
198
|
-
|
199
|
-
|
200
|
-
|
201
|
-
|
202
|
-
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
239
|
+
|
240
|
+
puts "\nRehashing."
|
241
|
+
|
242
|
+
rehash(frames)
|
243
|
+
|
244
|
+
# See what objects are still laying around
|
245
|
+
population = frames.last[:objects].reject do |key, value|
|
246
|
+
frames.first[:births_hash][key] and frames.first[:births_hash][key].first == value.first
|
247
|
+
end
|
248
|
+
|
249
|
+
puts "\n#{frames.size - 1} full frames. Removing #{INITIAL_SKIP} frames from each end of the run to account for\nstartup overhead and GC lag."
|
250
|
+
|
251
|
+
# Remove border frames
|
252
|
+
frames = frames[INITIAL_SKIP..-INITIAL_SKIP]
|
253
|
+
|
254
|
+
# Sum all births
|
255
|
+
total_births = frames.inject(0) do |births, frame|
|
256
|
+
births + frame[:births_hash].size
|
257
|
+
end
|
258
|
+
|
259
|
+
# Sum all deaths
|
260
|
+
total_deaths = frames.inject(0) do |deaths, frame|
|
261
|
+
deaths + frame[:deaths].size
|
262
|
+
end
|
263
|
+
|
264
|
+
puts "\n#{total_births} total births, #{total_deaths} total deaths, #{population.size} uncollected objects."
|
265
|
+
|
266
|
+
leakers = {}
|
267
|
+
|
268
|
+
# Find the sources of the leftover objects in the final population
|
269
|
+
population.each do |id, value|
|
270
|
+
klass = value[0]
|
271
|
+
content = value[1]
|
272
|
+
leaker = reverse_detect(frames) do |frame|
|
273
|
+
frame[:births_hash][id] and frame[:births_hash][id].first == klass
|
274
|
+
end
|
275
|
+
if leaker
|
276
|
+
tag = leaker[:meta][:tag]
|
277
|
+
klass = CLASS_KEYS[klass] if klass.is_a? Fixnum
|
278
|
+
leakers[tag] ||= Hash.new()
|
279
|
+
leakers[tag][klass] ||= {:count => 0, :contents => []}
|
280
|
+
leakers[tag][klass][:count] += 1
|
281
|
+
leakers[tag][klass][:contents] << content if content
|
217
282
|
end
|
218
283
|
end
|
219
|
-
else
|
220
|
-
puts "\nNo persistent uncollected objects found for any tags."
|
221
|
-
end
|
222
|
-
|
223
|
-
impacts = {}
|
224
|
-
|
225
|
-
frames.each do |frame|
|
226
|
-
impacts[frame['meta']['tag']] ||= []
|
227
|
-
impacts[frame['meta']['tag']] << frame['meta']['impact'] * frame['meta']['ratio']
|
228
|
-
end
|
229
|
-
impacts = impacts.map do |tag, values|
|
230
|
-
[tag, values.inject(0) {|acc, i| acc + i} / values.size.to_f]
|
231
|
-
end.sort_by do |tag, impact|
|
232
|
-
impact.nan? ? 0 : -impact
|
233
|
-
end
|
234
|
-
|
235
|
-
puts "\nTags sorted by average impact * ratio. Impact is the log10 of the size of the"
|
236
|
-
puts "change in object count for a frame:"
|
237
|
-
|
238
|
-
impacts.each do |tag, total|
|
239
|
-
puts " #{format('%.4f', total).rjust(7)}: #{tag}"
|
240
|
-
end
|
241
284
|
|
242
|
-
|
285
|
+
# Sort the leakers
|
286
|
+
leakers = leakers.map do |tag, value|
|
287
|
+
# Sort leakiest classes within each tag
|
288
|
+
[tag, value.sort_by do |klass, hash|
|
289
|
+
-hash[:count]
|
290
|
+
end]
|
291
|
+
end.sort_by do |tag, value|
|
292
|
+
# Sort leakiest tags as a whole
|
293
|
+
Hash[*value.flatten].values.inject(0) {|i, hash| i - hash[:count]}
|
294
|
+
end
|
295
|
+
|
296
|
+
if leakers.any?
|
297
|
+
puts "\nTags sorted by persistent uncollected objects. These objects did not exist at\nstartup, were instantiated by the associated tags during the run, and were\nnever garbage collected:"
|
298
|
+
leakers.each do |tag, value|
|
299
|
+
requests = frames.select do |frame|
|
300
|
+
frame[:meta][:tag] == tag
|
301
|
+
end.size
|
302
|
+
puts " #{tag} leaked per request (#{requests}):"
|
303
|
+
value.each do |klass, hash|
|
304
|
+
puts " #{sprintf('%.1f', hash[:count] / requests.to_f)} #{klass}"
|
305
|
+
|
306
|
+
# Extract most common samples
|
307
|
+
contents = begin
|
308
|
+
hist = Hash.new(0)
|
309
|
+
hash[:contents].each do |content|
|
310
|
+
hist[content] += 1
|
311
|
+
end
|
312
|
+
hist.sort_by do |content, count|
|
313
|
+
-count
|
314
|
+
end[0..DISPLAY_SAMPLES].select do |content, count|
|
315
|
+
ENV['DISPLAY_SAMPLES'] or count > 5
|
316
|
+
end
|
317
|
+
end
|
243
318
|
|
244
|
-
|
245
|
-
|
319
|
+
if contents.any?
|
320
|
+
puts " Inspection samples:"
|
321
|
+
contents.each do |content, count|
|
322
|
+
puts " #{sprintf('%.1f', count / requests.to_f)} #{content}"
|
323
|
+
end
|
324
|
+
end
|
325
|
+
|
326
|
+
end
|
327
|
+
end
|
328
|
+
else
|
329
|
+
puts "\nNo persistent uncollected objects found for any tags."
|
330
|
+
end
|
331
|
+
|
332
|
+
impacts = {}
|
333
|
+
|
334
|
+
frames.each do |frame|
|
335
|
+
impacts[frame[:meta][:tag]] ||= []
|
336
|
+
impacts[frame[:meta][:tag]] << frame[:meta][:impact] * frame[:meta][:ratio]
|
337
|
+
end
|
338
|
+
impacts = impacts.map do |tag, values|
|
339
|
+
[tag, values.inject(0) {|acc, i| acc + i} / values.size.to_f]
|
340
|
+
end.sort_by do |tag, impact|
|
341
|
+
impact.nan? ? 0 : -impact
|
342
|
+
end
|
343
|
+
|
344
|
+
puts "\nTags sorted by average impact * ratio. Impact is the log10 of the size of the"
|
345
|
+
puts "change in object count for a frame:"
|
346
|
+
|
347
|
+
impacts.each do |tag, total|
|
348
|
+
puts " #{format('%.4f', total).rjust(7)}: #{tag}"
|
349
|
+
end
|
350
|
+
|
351
|
+
puts "\nDone"
|
352
|
+
|
353
|
+
end
|
354
|
+
|
355
|
+
end
|
246
356
|
end
|
247
357
|
end
|