bleak_house 3.1 → 3.2
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 +1 -0
- data/CHANGELOG +30 -5
- data/LICENSE_BSD +10 -0
- data/LICENSE_RUBY +53 -0
- data/Manifest +31 -18
- data/README +66 -13
- data/Rakefile +16 -56
- data/TODO +2 -0
- data/bin/bleak +15 -0
- data/ext/bleak_house/logger/extconf.rb +4 -0
- data/ext/bleak_house/logger/snapshot.c +153 -0
- data/ext/bleak_house/logger/snapshot.h +99 -0
- data/init.rb +2 -1
- data/install.rb +2 -1
- data/lib/bleak_house.rb +4 -10
- data/lib/bleak_house/analyzer.rb +5 -0
- data/lib/bleak_house/analyzer/analyzer.rb +203 -0
- data/lib/bleak_house/logger.rb +3 -0
- data/lib/bleak_house/logger/mem_usage.rb +13 -0
- data/lib/bleak_house/rails.rb +6 -0
- data/lib/bleak_house/{action_controller.rb → rails/action_controller.rb} +3 -2
- data/lib/bleak_house/rails/bleak_house.rb +58 -0
- data/lib/bleak_house/rails/dispatcher.rb +19 -0
- data/lib/bleak_house/support/build.rb +53 -0
- data/lib/bleak_house/{support_methods.rb → support/core_extensions.rb} +23 -0
- data/lib/bleak_house/{rake_task_redefine_task.rb → support/rake.rb} +0 -0
- data/lib/vendor/lightcsv.rb +168 -0
- data/patches/gc.c.patch +27 -0
- data/patches/parse.y.patch +16 -0
- data/test/misc/direct.rb +13 -0
- data/test/unit/test_bleak_house.rb +34 -0
- metadata +78 -50
- metadata.gz.sig +0 -0
- data/lib/bleak_house/analyze.rb +0 -139
- data/lib/bleak_house/bleak_house.rb +0 -33
- data/lib/bleak_house/dispatcher.rb +0 -23
- data/lib/bleak_house/gruff_hacks.rb +0 -56
- data/lib/bleak_house/mem_logger.rb +0 -54
- data/tasks/bleak_house_tasks.rake +0 -13
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
#include "ruby.h"
|
|
2
|
+
#include "env.h"
|
|
3
|
+
#include "node.h"
|
|
4
|
+
#include "st.h"
|
|
5
|
+
#include "re.h"
|
|
6
|
+
#include "util.h"
|
|
7
|
+
#include "intern.h"
|
|
8
|
+
#include "string.h"
|
|
9
|
+
|
|
10
|
+
/* Histogram of most common Rails classes */
|
|
11
|
+
static char * builtins[] = {
|
|
12
|
+
"String",
|
|
13
|
+
"Array",
|
|
14
|
+
"Hash",
|
|
15
|
+
"Class",
|
|
16
|
+
"Regexp",
|
|
17
|
+
"Proc",
|
|
18
|
+
"ActionController::Routing::DividerSegment",
|
|
19
|
+
"Gem::Version",
|
|
20
|
+
"Gem::Version::Requirement",
|
|
21
|
+
"Bignum",
|
|
22
|
+
"Symbol",
|
|
23
|
+
"Time",
|
|
24
|
+
"MatchData",
|
|
25
|
+
"Gem::Specification",
|
|
26
|
+
"ActionController::Routing::StaticSegment",
|
|
27
|
+
"Gem::Dependency",
|
|
28
|
+
"Module",
|
|
29
|
+
"ActionController::Routing::DynamicSegment",
|
|
30
|
+
"Range",
|
|
31
|
+
"ActionController::Routing::Route",
|
|
32
|
+
"Float",
|
|
33
|
+
"HashWithIndifferentAccess",
|
|
34
|
+
"Method",
|
|
35
|
+
"Enumerable",
|
|
36
|
+
"Comparable",
|
|
37
|
+
"Set",
|
|
38
|
+
"File",
|
|
39
|
+
"Object",
|
|
40
|
+
"NameError",
|
|
41
|
+
"Thread",
|
|
42
|
+
"_node",
|
|
43
|
+
"_none",
|
|
44
|
+
"_blktag",
|
|
45
|
+
"_undef",
|
|
46
|
+
"_varmap",
|
|
47
|
+
"_scope",
|
|
48
|
+
"_unknown"
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
#define BUILTINS_SIZE 30
|
|
52
|
+
#define SPECIALS_SIZE 7
|
|
53
|
+
|
|
54
|
+
typedef struct RVALUE {
|
|
55
|
+
union {
|
|
56
|
+
struct {
|
|
57
|
+
unsigned long flags; /* always 0 for freed obj */
|
|
58
|
+
struct RVALUE *next;
|
|
59
|
+
} free;
|
|
60
|
+
struct RBasic basic;
|
|
61
|
+
struct RObject object;
|
|
62
|
+
struct RClass klass;
|
|
63
|
+
struct RFloat flonum;
|
|
64
|
+
struct RString string;
|
|
65
|
+
struct RArray array;
|
|
66
|
+
struct RRegexp regexp;
|
|
67
|
+
struct RHash hash;
|
|
68
|
+
struct RData data;
|
|
69
|
+
struct RStruct rstruct;
|
|
70
|
+
struct RBignum bignum;
|
|
71
|
+
struct RFile file;
|
|
72
|
+
struct RNode node;
|
|
73
|
+
struct RMatch match;
|
|
74
|
+
struct RVarmap varmap;
|
|
75
|
+
struct SCOPE scope;
|
|
76
|
+
} as;
|
|
77
|
+
} RVALUE;
|
|
78
|
+
|
|
79
|
+
struct heaps_slot {
|
|
80
|
+
void *membase;
|
|
81
|
+
RVALUE *slot;
|
|
82
|
+
int limit;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
typedef struct st_table_entry st_table_entry;
|
|
86
|
+
|
|
87
|
+
struct st_table_entry {
|
|
88
|
+
unsigned int hash;
|
|
89
|
+
st_data_t key;
|
|
90
|
+
st_data_t record;
|
|
91
|
+
st_table_entry *next;
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
struct st_table * rb_parse_sym_tbl();
|
|
95
|
+
struct heaps_slot * rb_gc_heap_slots();
|
|
96
|
+
int rb_gc_heaps_used();
|
|
97
|
+
int rb_gc_heaps_length();
|
|
98
|
+
|
|
99
|
+
int lookup_builtin(char *);
|
data/init.rb
CHANGED
data/install.rb
CHANGED
|
@@ -1 +1,2 @@
|
|
|
1
|
-
|
|
1
|
+
|
|
2
|
+
puts "Please use the gem version of BleakHouse from now on."
|
data/lib/bleak_house.rb
CHANGED
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
|
|
2
|
-
|
|
2
|
+
require 'bleak_house/logger'
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
require 'bleak_house/mem_logger'
|
|
8
|
-
require 'bleak_house/dispatcher'
|
|
9
|
-
require 'bleak_house/action_controller'
|
|
10
|
-
|
|
11
|
-
BleakHouse.warn "enabled (log/#{RAILS_ENV}_bleak_house.log) (#{BleakHouse.log_interval} requests per frame)"
|
|
12
|
-
|
|
4
|
+
if ENV['RAILS_ENV'] and ENV['BLEAK_HOUSE']
|
|
5
|
+
require 'bleak_house/rails'
|
|
6
|
+
BleakHouse::Rails.warn "enabled (log/bleak_house_#{RAILS_ENV}.dump)"
|
|
13
7
|
end
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
|
|
2
|
+
require 'rubygems'
|
|
3
|
+
require 'fileutils'
|
|
4
|
+
require 'yaml'
|
|
5
|
+
require 'pp'
|
|
6
|
+
|
|
7
|
+
module BleakHouse
|
|
8
|
+
|
|
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 = 10
|
|
21
|
+
|
|
22
|
+
CLASS_KEYS = eval('[nil, ' + # skip 0
|
|
23
|
+
open(
|
|
24
|
+
File.dirname(__FILE__) + '/../../../ext/bleak_house/logger/snapshot.h'
|
|
25
|
+
).read[/\{(.*?)\}/m, 1] + ']')
|
|
26
|
+
|
|
27
|
+
def self.calculate!(frame, index, total)
|
|
28
|
+
bsize = frame['births'].size
|
|
29
|
+
dsize = frame['deaths'].size
|
|
30
|
+
|
|
31
|
+
# Avoid divide by zero errors
|
|
32
|
+
frame['meta']['ratio'] = ratio = (bsize - dsize) / (bsize + dsize + 1).to_f
|
|
33
|
+
frame['meta']['impact'] = begin
|
|
34
|
+
Math.log10((bsize - dsize).abs.to_i / 10.0)
|
|
35
|
+
rescue Errno::ERANGE
|
|
36
|
+
0
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
puts " #{index * 100 / total}%: #{frame['meta']['tag']} (#{bsize} births, #{dsize} deaths, ratio #{format('%.2f', frame['meta']['ratio'])}, impact #{format('%.2f', frame['meta']['impact'])})"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
# Parses and correlates a BleakHouse::Logger output file.
|
|
43
|
+
def self.run(logfile)
|
|
44
|
+
unless File.exists? logfile
|
|
45
|
+
puts "No data file found: #{logfile}"
|
|
46
|
+
exit
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
puts "Working..."
|
|
50
|
+
|
|
51
|
+
cachefile = logfile + ".cache"
|
|
52
|
+
frames = []
|
|
53
|
+
last_population = []
|
|
54
|
+
frame = nil
|
|
55
|
+
ix = nil
|
|
56
|
+
|
|
57
|
+
if File.exist?(cachefile) and File.stat(cachefile).mtime > File.stat(logfile).mtime
|
|
58
|
+
# Cache is fresh
|
|
59
|
+
puts "Using cache"
|
|
60
|
+
frames = Marshal.load(File.open(cachefile).read)
|
|
61
|
+
|
|
62
|
+
puts "#{frames.size} frames"
|
|
63
|
+
|
|
64
|
+
frames[0..-3].each_with_index do |frame, index|
|
|
65
|
+
calculate!(frame, index + 1, frames.size - 1)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
else
|
|
69
|
+
# Rebuild frames
|
|
70
|
+
total_frames = `grep '^-1' #{logfile} | wc`.to_i
|
|
71
|
+
|
|
72
|
+
puts "#{total_frames} frames"
|
|
73
|
+
|
|
74
|
+
Ccsv.foreach(logfile) do |row|
|
|
75
|
+
|
|
76
|
+
# Stupid is fast
|
|
77
|
+
i = row[0].to_i
|
|
78
|
+
row[0] = i if i != 0
|
|
79
|
+
i = row[1].to_i
|
|
80
|
+
row[1] = i if i != 0
|
|
81
|
+
|
|
82
|
+
if row[0].to_i < 0
|
|
83
|
+
# Get frame meta-information
|
|
84
|
+
if MAGIC_KEYS[row[0]] == 'timestamp'
|
|
85
|
+
|
|
86
|
+
# The frame has ended; process the last one
|
|
87
|
+
if frame
|
|
88
|
+
population = frame['objects'].keys
|
|
89
|
+
births = population - last_population
|
|
90
|
+
deaths = last_population - population
|
|
91
|
+
last_population = population
|
|
92
|
+
|
|
93
|
+
# assign births
|
|
94
|
+
frame['births'] = frame['objects'].slice(births)
|
|
95
|
+
|
|
96
|
+
# assign deaths to previous frame
|
|
97
|
+
if final = frames[-2]
|
|
98
|
+
final['deaths'] = final['objects'].slice(deaths)
|
|
99
|
+
final.delete 'objects'
|
|
100
|
+
calculate!(final, frames.size - 1, total_frames)
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Set up a new frame
|
|
105
|
+
frame = {}
|
|
106
|
+
frames << frame
|
|
107
|
+
frame['objects'] ||= {}
|
|
108
|
+
frame['meta'] ||= {}
|
|
109
|
+
|
|
110
|
+
#puts " Frame #{frames.size} opened"
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
frame['meta'][MAGIC_KEYS[row[0]]] = row[1]
|
|
114
|
+
else
|
|
115
|
+
# Assign live objects
|
|
116
|
+
frame['objects'][row[1]] = row[0]
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
# Cache the result
|
|
121
|
+
File.open(cachefile, 'w') do |f|
|
|
122
|
+
f.write Marshal.dump(frames)
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
# See what objects are still laying around
|
|
128
|
+
population = frames.last['objects'].reject do |key, value|
|
|
129
|
+
frames.first['births'][key] == value
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# Remove bogus frames
|
|
133
|
+
frames = frames[INITIAL_SKIP..-3]
|
|
134
|
+
|
|
135
|
+
total_births = frames.inject(0) do |births, frame|
|
|
136
|
+
births + frame['births'].size
|
|
137
|
+
end
|
|
138
|
+
total_deaths = frames.inject(0) do |deaths, frame|
|
|
139
|
+
deaths + frame['deaths'].size
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
puts "\n#{total_births} births, #{total_deaths} deaths."
|
|
143
|
+
|
|
144
|
+
leakers = {}
|
|
145
|
+
|
|
146
|
+
# Find the sources of the leftover objects in the final population
|
|
147
|
+
population.each do |id, klass|
|
|
148
|
+
leaker = frames.detect do |frame|
|
|
149
|
+
frame['births'][id] == klass
|
|
150
|
+
end
|
|
151
|
+
if leaker
|
|
152
|
+
tag = leaker['meta']['tag']
|
|
153
|
+
klass = CLASS_KEYS[klass] if klass.is_a? Fixnum
|
|
154
|
+
leakers[tag] ||= Hash.new(0)
|
|
155
|
+
leakers[tag][klass] += 1
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
# Sort
|
|
160
|
+
leakers = leakers.map do |tag, value|
|
|
161
|
+
[tag, value.sort_by do |klass, count|
|
|
162
|
+
-count
|
|
163
|
+
end]
|
|
164
|
+
end.sort_by do |tag, value|
|
|
165
|
+
Hash[*value.flatten].values.inject(0) {|i, v| i - v}
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
puts "\nTags sorted by immortal leaks:"
|
|
169
|
+
leakers.each do |tag, value|
|
|
170
|
+
requests = frames.select do |frame|
|
|
171
|
+
frame['meta']['tag'] == tag
|
|
172
|
+
end.size
|
|
173
|
+
puts " #{tag} leaks, averaged over #{requests} requests:"
|
|
174
|
+
value.each do |klass, count|
|
|
175
|
+
count = count/requests
|
|
176
|
+
puts " #{count} #{klass}" if count > 0
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
impacts = {}
|
|
181
|
+
|
|
182
|
+
frames.each do |frame|
|
|
183
|
+
impacts[frame['meta']['tag']] ||= []
|
|
184
|
+
impacts[frame['meta']['tag']] << frame['meta']['impact'] * frame['meta']['ratio']
|
|
185
|
+
end
|
|
186
|
+
impacts = impacts.map do |tag, values|
|
|
187
|
+
[tag, values.inject(0) {|acc, i| acc + i} / values.size.to_f]
|
|
188
|
+
end.sort_by do |tag, impact|
|
|
189
|
+
-impact
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
puts "\nTags sorted by impact * ratio:"
|
|
193
|
+
|
|
194
|
+
impacts.each do |tag, total|
|
|
195
|
+
puts " #{format('%.4f', total).rjust(7)}: #{tag}"
|
|
196
|
+
end
|
|
197
|
+
|
|
198
|
+
puts "\nBye"
|
|
199
|
+
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
end
|
|
203
|
+
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
|
|
2
|
+
module BleakHouse
|
|
3
|
+
|
|
4
|
+
class Logger
|
|
5
|
+
|
|
6
|
+
# Returns an array of the running process's real and virtual memory usage, in kilobytes.
|
|
7
|
+
def mem_usage
|
|
8
|
+
a = `ps -o vsz,rss -p #{Process.pid}`.split(/\s+/)[-2..-1].map{|el| el.to_i}
|
|
9
|
+
[a.first - a.last, a.last]
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -1,14 +1,15 @@
|
|
|
1
1
|
|
|
2
|
+
# Override ActionController::Base.process and process_with_exception to make sure the request tag for the snapshot gets set as a side-effect of request processing.
|
|
2
3
|
class ActionController::Base
|
|
3
4
|
class << self
|
|
4
5
|
def process_with_bleak_house(request, *args)
|
|
5
|
-
BleakHouse.set_request_name request
|
|
6
|
+
BleakHouse::Rails.set_request_name request
|
|
6
7
|
process_without_bleak_house(request, *args)
|
|
7
8
|
end
|
|
8
9
|
alias_method_chain :process, :bleak_house
|
|
9
10
|
|
|
10
11
|
def process_with_exception_with_bleak_house(request, *args)
|
|
11
|
-
BleakHouse.set_request_name request, "/error"
|
|
12
|
+
BleakHouse::Rails.set_request_name request, "/error"
|
|
12
13
|
process_with_exception_without_bleak_house(request, *args)
|
|
13
14
|
end
|
|
14
15
|
alias_method_chain :process_with_exception, :bleak_house
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
|
|
2
|
+
module BleakHouse
|
|
3
|
+
module Rails
|
|
4
|
+
class << self
|
|
5
|
+
|
|
6
|
+
def last_request_name
|
|
7
|
+
@@last_request_name
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def last_request_name=(obj)
|
|
11
|
+
@@last_request_name = obj
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
# Avoid making four more strings on each request.
|
|
15
|
+
CONTROLLER_KEY = 'controller'
|
|
16
|
+
ACTION_KEY = 'action'
|
|
17
|
+
GSUB_SEARCH = '/'
|
|
18
|
+
GSUB_REPLACEMENT = '__'
|
|
19
|
+
|
|
20
|
+
# Sets the request name on the BleakHouse object to match this Rails request. Called from <tt>ActionController::Base.process</tt>. Assign to <tt>last_request_name</tt> yourself if you are not using BleakHouse within Rails.
|
|
21
|
+
def set_request_name(request, other = nil)
|
|
22
|
+
self.last_request_name = "#{
|
|
23
|
+
request.parameters[CONTROLLER_KEY].gsub(GSUB_SEARCH, GSUB_REPLACEMENT) # mangle namespaced controller names
|
|
24
|
+
}/#{
|
|
25
|
+
request.parameters[ACTION_KEY]
|
|
26
|
+
}/#{
|
|
27
|
+
request.request_method
|
|
28
|
+
}#{
|
|
29
|
+
other
|
|
30
|
+
}"
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def debug(s) #:nodoc:
|
|
34
|
+
s = "** bleak_house: #{s}"
|
|
35
|
+
RAILS_DEFAULT_LOGGER.debug s if RAILS_DEFAULT_LOGGER
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def warn(s) #:nodoc:
|
|
39
|
+
s = "** bleak_house: #{s}"
|
|
40
|
+
if RAILS_DEFAULT_LOGGER
|
|
41
|
+
RAILS_DEFAULT_LOGGER.warn s
|
|
42
|
+
else
|
|
43
|
+
$stderr.puts s
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
LOGFILE = "#{RAILS_ROOT}/log/bleak_house_#{RAILS_ENV}.dump"
|
|
49
|
+
if File.exists?(LOGFILE)
|
|
50
|
+
File.rename(LOGFILE, "#{LOGFILE}.old")
|
|
51
|
+
warn "renamed old logfile"
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
WITH_SPECIALS = false
|
|
55
|
+
|
|
56
|
+
MEMLOGGER = Logger.new
|
|
57
|
+
end
|
|
58
|
+
end
|