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 CHANGED
Binary file
data/CHANGELOG CHANGED
@@ -1,4 +1,6 @@
1
1
 
2
+ v3.7. Sample object contents. Restore Rails 1.2.x compatibility.
3
+
2
4
  v3.6. Rails 2.0.2 compatibility.
3
5
 
4
6
  v3.5.1. Update bundled Ruby to patchlevel 110. Delete your current ruby-bleak-house and reinstall.
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/mem_usage.rb
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 at http://rubyforge.org/frs/download.php/25331/evan_weaver-original-public_cert.pem.
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
- * fast logging
16
- * complete tracking of object and symbol history
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.0.2 or greater
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
- * http://rubyforge.org/forum/forum.php?forum_id=13983
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
@@ -1,2 +1,3 @@
1
1
 
2
2
  * Better docs on what the tags mean
3
+ * Use per-frame file storage to avoid out-of-memory problems
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.6
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.6"
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-02}
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/mem_usage.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/log/bleak_house_production.dump", "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"]
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 < 0) {
86
- /* regular classname */
87
- fprintf(logfile, "%s,%lu\n", rb_obj_classname((VALUE)obj), FIX2ULONG(rb_obj_id((VALUE)obj)));
88
- } else {
89
- /* builtins key */
90
- if (specials || hashed < BUILTINS_SIZE) {
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,%lu\n", hashed + 1, FIX2ULONG(rb_obj_id((VALUE)obj)));
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
- rb_funcall(rb_mGC, rb_intern("start"), 0); /* request GC run */
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, 3);
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
- MAGIC_KEYS = {
12
- -1 => 'timestamp',
13
- -2 => 'mem usage/swap',
14
- -3 => 'mem usage/real',
15
- -4 => 'tag',
16
- -5 => 'heap/filled',
17
- -6 => 'heap/free'
18
- }
19
-
20
- INITIAL_SKIP = 15 # XXX Might be better as a per-tag skip but that gets kinda complicated
21
-
22
- CLASS_KEYS = eval('[nil, ' + # Skip 0 so that the output of String#to_s is useful
23
- open(
24
- File.dirname(__FILE__) + '/../../../ext/bleak_house/logger/snapshot.h'
25
- ).read[/\{(.*?)\}/m, 1] + ']')
26
-
27
- def self.backwards_detect(array)
28
- i = array.size - 1
29
- while i >= 0
30
- item = array[i]
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
- # Parses and correlates a BleakHouse::Logger output file.
54
- def self.run(logfile)
55
- logfile.chomp!(".cache")
56
- cachefile = logfile + ".cache"
57
-
58
- unless File.exists? logfile or File.exists? cachefile
59
- puts "No data file found: #{logfile}"
60
- exit
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
- puts "Working..."
64
-
65
- frames = []
66
- last_population = []
67
- frame = nil
68
- ix = nil
69
-
70
- if File.exist?(cachefile) and (!File.exists? logfile or File.stat(cachefile).mtime > File.stat(logfile).mtime)
71
- # Cache is fresh
72
- puts "Using cache"
73
- frames = Marshal.load(File.open(cachefile).read)
74
- puts "#{frames.size - 1} frames"
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
- else
80
- # Rebuild frames
81
- total_frames = `grep '^-1' #{logfile} | wc`.to_i - 2
82
-
83
- puts "#{total_frames} frames"
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
- Ccsv.foreach(logfile) do |row|
91
-
92
- # Stupid is fast
93
- i = row[0].to_i
94
- row[0] = i if i != 0
95
- i = row[1].to_i
96
- row[1] = i if i != 0
97
-
98
- if row[0].to_i < 0
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 MAGIC_KEYS[row[0]] == 'timestamp'
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['objects'].keys
143
+ population = frame[:objects].keys
105
144
  births = population - last_population
106
145
  deaths = last_population - population
107
146
  last_population = population
108
-
109
- # assign births
110
- frame['births'] = frame['objects'].slice(births).to_a # Work around a Marshal bug
111
-
112
- # assign deaths to previous frame
113
- if final = frames[-2]
114
- final['deaths'] = final['objects'].slice(deaths).to_a # Work around a Marshal bug
115
- obj_count = final['objects'].size
116
- final.delete 'objects'
117
- calculate!(final, frames.size - 1, total_frames, obj_count)
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['objects'] ||= {}
125
- frame['meta'] ||= {}
126
-
127
- #puts " Frame #{frames.size} opened"
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['meta'][MAGIC_KEYS[row[0]]] = row[1]
183
+
184
+ frame[:meta][SPECIALS[class_index]] = id_or_tag
131
185
  else
132
- # Assign live objects
133
- frame['objects'][row[1]] = row[0]
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
- frames = frames[0..-2]
138
- frames.last['objects'] = frames.last['objects'].to_a # Work around a Marshal bug x86-64
139
-
140
- # Cache the result
141
- File.open(cachefile, 'w') do |f|
142
- f.write Marshal.dump(frames)
143
- end
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
- puts "\nRehashing."
148
-
149
- # Convert births back to hashes, necessary due to the Marshal workaround
150
- frames.each do |frame|
151
- frame['births_hash'] = {}
152
- frame['births'].each do |key, value|
153
- frame['births_hash'][key] = value
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
- frame.delete('births')
216
+ nil
156
217
  end
157
218
 
158
- # require 'ruby-debug'; Debugger.start
159
- #
160
- # debugger
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
- 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."
224
+ unless File.exists? logfile or File.exists? cachefile
225
+ puts "No data file found: #{logfile}"
226
+ exit
227
+ end
168
228
 
169
- # Remove border frames
170
- frames = frames[INITIAL_SKIP..-INITIAL_SKIP]
171
-
172
- total_births = frames.inject(0) do |births, frame|
173
- births + frame['births_hash'].size
174
- end
175
- total_deaths = frames.inject(0) do |deaths, frame|
176
- deaths + frame['deaths'].size
177
- end
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
- end
198
-
199
- # Sort
200
- leakers = leakers.map do |tag, value|
201
- [tag, value.sort_by do |klass, count|
202
- -count
203
- end]
204
- end.sort_by do |tag, value|
205
- Hash[*value.flatten].values.inject(0) {|i, v| i - v}
206
- end
207
-
208
- if leakers.any?
209
- puts "\nTags sorted by persistent uncollected objects. These objects did not exist at\nstartup, were instantiated by the associated tags, and were never garbage\ncollected:"
210
- leakers.each do |tag, value|
211
- requests = frames.select do |frame|
212
- frame['meta']['tag'] == tag
213
- end.size
214
- puts " #{tag} leaked (over #{requests} requests):"
215
- value.each do |klass, count|
216
- puts " #{count} #{klass}"
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
- puts "\nDone"
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
- end
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