bleak_house 3.6 → 3.7
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.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
|