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 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