rublique 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/README +83 -0
- data/Rakefile +33 -0
- data/bin/rublique_analyzer +88 -0
- data/lib/rublique.rb +108 -0
- data/lib/rublique_dispatcher.rb +44 -0
- data/lib/rublique_logger.rb +38 -0
- metadata +61 -0
data/README
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
Hello. I'm going to answer the one question I expect to get asked regarding
|
2
|
+
this package. Hopefully everything else you can figure out from the RDoc.
|
3
|
+
|
4
|
+
That question is:
|
5
|
+
|
6
|
+
How do I use this with Rails?
|
7
|
+
|
8
|
+
Well, for now, it's an ugly hack. I don't know Rails internals well enough
|
9
|
+
to come up with a better solution.
|
10
|
+
|
11
|
+
The Rublique distribution includes a rublique_dispatcher.rb file which contains
|
12
|
+
a replacement implementation for Dispatcher.dispatch compatible with
|
13
|
+
Rails 1.1.6 (other versions may work. I don't know)
|
14
|
+
|
15
|
+
If you know a better way to do this, than for the love of God please e-mail
|
16
|
+
me at tony@clickcaster.com (and no, sticking it in environment.rb won't work)
|
17
|
+
|
18
|
+
First, find your gems directory (/usr/lib/ruby/gems, /usr/local/lib/ruby/gems,
|
19
|
+
/opt/local/lib/ruby/gems, etc. depending on your platform)
|
20
|
+
|
21
|
+
Then, look underr gems/1.8/gems/rails-1.1.6/lib directory and you'll find
|
22
|
+
dispatcher.rb
|
23
|
+
|
24
|
+
At the BOTTOM of this file, add:
|
25
|
+
|
26
|
+
require 'rublique_dispatcher'
|
27
|
+
|
28
|
+
Then restart your Rails application.
|
29
|
+
|
30
|
+
This will log to RAILS_ROOT/log/rublique.log by default. The format is a
|
31
|
+
series of newline delimited JSON arrays, containing:
|
32
|
+
|
33
|
+
[timestamp,{section breakdown}]
|
34
|
+
|
35
|
+
The section breakdown is a JSON object which keys code section names (which
|
36
|
+
with the Rails instrumentation will be in the form of "controller/action" or
|
37
|
+
"rails" for anything done outside the dispatcher. Each section name keys
|
38
|
+
to another JSON object, with class names that key to an object count.
|
39
|
+
|
40
|
+
The object count represents how many objects of a given class were created
|
41
|
+
in that particular section minus how many objects of a given class originally
|
42
|
+
created within that section were garbage collected since the last time
|
43
|
+
the object counts were logged. By default these deltas are logged every
|
44
|
+
10 requests, and right now there's no good way to change that besides
|
45
|
+
hand-editing rublique_dispatcher.rb (this is a 0.0.1 release, after all)
|
46
|
+
|
47
|
+
That information probably isn't very useful to you, but fortunately Rublique
|
48
|
+
includes a log analyzer tool that outputs CSV files you can import into
|
49
|
+
the graphing tool of your choice.
|
50
|
+
|
51
|
+
Run:
|
52
|
+
|
53
|
+
rublique_analyzer -s rublique.log
|
54
|
+
|
55
|
+
to output a breakdown of net object creation by code section over time. The
|
56
|
+
first column represents time in seconds, and the others object counts.
|
57
|
+
|
58
|
+
To inspect a particular section, use:
|
59
|
+
|
60
|
+
rublique_analyzer -c controller/action rublique.log
|
61
|
+
|
62
|
+
This will give you a CSV breakdown of object allocation by class per code
|
63
|
+
section over time.
|
64
|
+
|
65
|
+
---
|
66
|
+
|
67
|
+
Known bugs:
|
68
|
+
|
69
|
+
Rublique is neither thread safe nor reentrant. This can lead to some erroneous
|
70
|
+
numbers when used in a threaded environment (which is pretty much guaranteed
|
71
|
+
with Rails)
|
72
|
+
|
73
|
+
Rublique provides no interface for nor keeps a stack of code sections. This
|
74
|
+
means it presently only works within a dispatcher-like framework where
|
75
|
+
you have an outer environment section which dispatches to various inner
|
76
|
+
processing sections. Sections can be no deeper than two levels.
|
77
|
+
|
78
|
+
Because of this, things like Rails components will mess up Rublique's
|
79
|
+
housekeeping. Rublique really needs to keep a stack of code sections, and have
|
80
|
+
a .pop method to move down a level. That will have to wait for a future
|
81
|
+
version, sorry folks.
|
82
|
+
|
83
|
+
For now: don't use components!
|
data/Rakefile
ADDED
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'rake'
|
2
|
+
require 'rake/rdoctask'
|
3
|
+
require 'rake/gempackagetask'
|
4
|
+
require 'spec/rake/spectask'
|
5
|
+
|
6
|
+
Spec::Rake::SpecTask.new(:spec) do |task|
|
7
|
+
task.spec_files = FileList['**/*_spec.rb']
|
8
|
+
end
|
9
|
+
|
10
|
+
Rake::RDocTask.new(:rdoc) do |task|
|
11
|
+
task.rdoc_dir = 'doc'
|
12
|
+
task.title = 'Rublique'
|
13
|
+
task.rdoc_files.include('lib/**/*.rb')
|
14
|
+
end
|
15
|
+
|
16
|
+
spec = Gem::Specification.new do |s|
|
17
|
+
s.name = %q{rublique}
|
18
|
+
s.version = "0.0.1"
|
19
|
+
s.date = %q{2006-12-19}
|
20
|
+
s.summary = %q{Rublique monitors object lifetimes across various code sections, outputting a logfile of object deltas which can be used to generate CSV files of object use over time}
|
21
|
+
s.email = %q{tony@clickcaster.com}
|
22
|
+
s.homepage = %q{http://rublique.rubyforge.org}
|
23
|
+
s.rubyforge_project = %q{rublique}
|
24
|
+
s.has_rdoc = true
|
25
|
+
s.authors = ["Tony Arcieri"]
|
26
|
+
s.files = ["README", "Rakefile", "lib", "lib/rublique.rb", "lib/rublique_logger.rb", "lib/rublique_dispatcher.rb", "bin", "bin/rublique_analyzer"]
|
27
|
+
s.add_dependency('json', '>= 0.4.0')
|
28
|
+
s.executables << 'rublique_analyzer'
|
29
|
+
end
|
30
|
+
|
31
|
+
Rake::GemPackageTask.new(spec) do |pkg|
|
32
|
+
pkg.need_tar = true
|
33
|
+
end
|
@@ -0,0 +1,88 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require 'rubygems'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'fjson'
|
6
|
+
rescue
|
7
|
+
require 'json'
|
8
|
+
end
|
9
|
+
|
10
|
+
logfile_name = ARGV.pop
|
11
|
+
mode = nil
|
12
|
+
|
13
|
+
case ARGV.shift
|
14
|
+
when '-s'
|
15
|
+
mode = :sections
|
16
|
+
when '-c'
|
17
|
+
mode = :classes
|
18
|
+
section_name = ARGV.shift
|
19
|
+
end
|
20
|
+
|
21
|
+
if !logfile_name || !mode || (mode == :classes && !section_name)
|
22
|
+
puts <<EOD
|
23
|
+
Usage: #{$0} -s <logfile>
|
24
|
+
#{$0} -c <section> <logfile>
|
25
|
+
|
26
|
+
#{$0} outputs a CSV analysis of a Rublique log:
|
27
|
+
|
28
|
+
-s Outputs a timestamp followed by every section in the logfile and
|
29
|
+
the total objects allocated by that section at that time
|
30
|
+
|
31
|
+
-c section Outputs a timestamp followed by how many instances of every class
|
32
|
+
are presently allocated within that section
|
33
|
+
EOD
|
34
|
+
exit
|
35
|
+
end
|
36
|
+
|
37
|
+
logfile = File.open logfile_name, 'r'
|
38
|
+
|
39
|
+
initial_time = nil
|
40
|
+
output_data = []
|
41
|
+
previous_totals = Hash.new(0)
|
42
|
+
|
43
|
+
logfile.each_line do |line|
|
44
|
+
date, breakdown = JSON.parse line
|
45
|
+
|
46
|
+
if initial_time
|
47
|
+
tdelta = (Time.parse(date) - initial_time).to_i
|
48
|
+
else
|
49
|
+
tdelta = 0
|
50
|
+
initial_time = Time.parse(date)
|
51
|
+
end
|
52
|
+
|
53
|
+
case mode
|
54
|
+
when :sections
|
55
|
+
totals = breakdown.inject(previous_totals) do |h, action|
|
56
|
+
name, objects = action
|
57
|
+
h[name] += objects.values.inject(0) { |a,v| a + v }
|
58
|
+
h
|
59
|
+
end
|
60
|
+
when :classes
|
61
|
+
next if breakdown[section_name].nil?
|
62
|
+
|
63
|
+
totals = breakdown[section_name].inject(previous_totals) do |h, classcounts|
|
64
|
+
classname, count = classcounts
|
65
|
+
h[classname] += count
|
66
|
+
h
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
previous_totals = totals.dup
|
71
|
+
output_data << [tdelta, totals]
|
72
|
+
end
|
73
|
+
|
74
|
+
all_columns = output_data.last[1].keys.sort
|
75
|
+
|
76
|
+
lifetime_totals = all_columns.inject({}) do |h,c|
|
77
|
+
h[c] = output_data.inject(0) { |a,o| o[1][c] + a }
|
78
|
+
h
|
79
|
+
end
|
80
|
+
|
81
|
+
ranked_columns = lifetime_totals.sort { |a,b| b[1] <=> a[1] }.map { |a| a.shift }
|
82
|
+
|
83
|
+
puts "seconds," + ranked_columns.join(',')
|
84
|
+
|
85
|
+
output_data.each do |snapshot|
|
86
|
+
time, totals = snapshot
|
87
|
+
puts time.to_s + ',' + ranked_columns.map { |c| totals[c].to_s }.join(',')
|
88
|
+
end
|
data/lib/rublique.rb
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
class Rublique
|
4
|
+
class << self
|
5
|
+
# A map of what objects belong to what tag
|
6
|
+
@@object_map = {}
|
7
|
+
|
8
|
+
# A map of what classes belong(ed) to what ID
|
9
|
+
@@class_map = {}
|
10
|
+
|
11
|
+
# A list of object counts, broken down by tag
|
12
|
+
@@object_breakdown = {}
|
13
|
+
|
14
|
+
# Grab the #object_id method from Ojbect, in case it's been overridden
|
15
|
+
# Objects like Builder::XmlMarkup do this
|
16
|
+
@@object_id_method = Object.new.method(:object_id).unbind
|
17
|
+
|
18
|
+
# Grab the #class method from Object, in case it's been overridden
|
19
|
+
@@class_method = Object.new.method(:class).unbind
|
20
|
+
|
21
|
+
# Take a snapshot of the current ObjectSpace, returning active object IDs and their associated tag
|
22
|
+
def snapshot(tag)
|
23
|
+
# Unknown stuff in ObjectSpace gets dumped here
|
24
|
+
new_objects = {}
|
25
|
+
|
26
|
+
# A list of stuff we know about, so we can see what got GCed
|
27
|
+
object_list = @@object_map.dup
|
28
|
+
|
29
|
+
ObjectSpace.each_object do |obj|
|
30
|
+
# Bind Object#object_id to the object and call it to get its id
|
31
|
+
obj_id = @@object_id_method.bind(obj).call
|
32
|
+
|
33
|
+
if object_list.has_key? obj_id
|
34
|
+
# Delete known objects, since this list stores GCed objects
|
35
|
+
object_list.delete obj_id
|
36
|
+
next
|
37
|
+
end
|
38
|
+
|
39
|
+
# Add to the new objects collection if unknown, with the given tag
|
40
|
+
new_objects[obj_id] = tag
|
41
|
+
|
42
|
+
# Check to see if Object#class has been overridden
|
43
|
+
if obj.class.class == Class
|
44
|
+
@@class_map[obj_id] = obj.class
|
45
|
+
else
|
46
|
+
# Capture the real class of objects which override #class
|
47
|
+
@@class_map[obj_id] = @@class_method.bind(obj).call
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
# Remove objects that are no longer in ObjectSpace
|
52
|
+
object_list.each_key { |key| @@object_map.delete key }
|
53
|
+
|
54
|
+
@@object_map.merge! new_objects
|
55
|
+
end
|
56
|
+
|
57
|
+
# Return a hash of object IDs to their associated tag
|
58
|
+
def objects
|
59
|
+
@@object_map
|
60
|
+
end
|
61
|
+
|
62
|
+
# Build a hash of hashes which maps a tag to its respective object IDs and those IDs to their class
|
63
|
+
def breakdown
|
64
|
+
@@object_map.to_a.inject({}) do |tag_hash, object_mapping|
|
65
|
+
obj, tag = object_mapping
|
66
|
+
|
67
|
+
if tag_hash.has_key? tag
|
68
|
+
tag_hash[tag][obj] = @@class_map[obj]
|
69
|
+
else
|
70
|
+
tag_hash[tag] = { obj => @@class_map[obj] }
|
71
|
+
end
|
72
|
+
|
73
|
+
tag_hash
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
# Build a hash of hashes which records total objects created and destroyed since the last delta was taken, by class by code section
|
78
|
+
def delta
|
79
|
+
old_object_breakdown = @@object_breakdown
|
80
|
+
@@object_breakdown = nil
|
81
|
+
|
82
|
+
# Try to GC our own overhead
|
83
|
+
ObjectSpace.garbage_collect
|
84
|
+
|
85
|
+
@@object_breakdown = breakdown
|
86
|
+
|
87
|
+
@@object_breakdown.keys.inject({}) do |d, tag|
|
88
|
+
old_objects = old_object_breakdown[tag] || {}
|
89
|
+
new_objects = @@object_breakdown[tag]
|
90
|
+
|
91
|
+
# Find objects which have been garbage collected, note them, and delete them from the class_map
|
92
|
+
d[tag] = (old_objects.keys - new_objects.keys).inject(Hash.new(0)) do |h,obj|
|
93
|
+
h[@@class_map[obj]] -= 1
|
94
|
+
@@class_map.delete obj
|
95
|
+
h
|
96
|
+
end
|
97
|
+
|
98
|
+
new_objects.keys.inject(d[tag]) do |h,obj|
|
99
|
+
unless old_objects.has_key? obj
|
100
|
+
h[@@class_map[obj]] += 1
|
101
|
+
end
|
102
|
+
h
|
103
|
+
end.delete_if { |k,v| v == 0 }
|
104
|
+
d
|
105
|
+
end.delete_if { |k,v| v.empty? }
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'rublique'
|
2
|
+
require 'rublique_logger'
|
3
|
+
|
4
|
+
RUBLIQUE_LOG_INTERVAL = 10
|
5
|
+
|
6
|
+
# Override the Rails 1.1.6 dispatcher to use Rublique
|
7
|
+
class Dispatcher
|
8
|
+
# Set the Rublique logfile path if we haven't already
|
9
|
+
@@logfile_path_set = false
|
10
|
+
|
11
|
+
# Count the number of requests we've received before logging a delta
|
12
|
+
@@request_count = RUBLIQUE_LOG_INTERVAL
|
13
|
+
|
14
|
+
def self.dispatch(cgi = nil, session_options = ActionController::CgiRequest::DEFAULT_SESSION_OPTIONS, output = $stdout)
|
15
|
+
# Set the logfile path to within RAILS_ROOT
|
16
|
+
unless @@logfile_path_set
|
17
|
+
RubliqueLogger.file = RAILS_ROOT + '/log/rublique.log'
|
18
|
+
@@logfile_path_set = true
|
19
|
+
end
|
20
|
+
|
21
|
+
if cgi ||= new_cgi(output)
|
22
|
+
request, response = ActionController::CgiRequest.new(cgi, session_options), ActionController::CgiResponse.new(cgi)
|
23
|
+
prepare_application
|
24
|
+
|
25
|
+
# Locate the appropriate controller through routes
|
26
|
+
controller = ActionController::Routing::Routes.recognize!(request)
|
27
|
+
|
28
|
+
Rublique.snapshot('rails')
|
29
|
+
controller.process(request, response).out(output)
|
30
|
+
Rublique.snapshot(controller.controller_name + '/' + controller.action_name)
|
31
|
+
|
32
|
+
@@request_count += 1
|
33
|
+
@@request_count = 0 if @@request_count > RUBLIQUE_LOG_INTERVAL
|
34
|
+
RubliqueLogger.log if @@request_count == 0
|
35
|
+
end
|
36
|
+
rescue Object => exception
|
37
|
+
failsafe_response(output, '500 Internal Server Error', exception) do
|
38
|
+
ActionController::Base.process_with_exception(request, response, exception).out(output)
|
39
|
+
end
|
40
|
+
ensure
|
41
|
+
# Do not give a failsafe response here.
|
42
|
+
reset_after_dispatch
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'rublique'
|
3
|
+
|
4
|
+
begin
|
5
|
+
require 'fjson'
|
6
|
+
rescue
|
7
|
+
require 'json'
|
8
|
+
end
|
9
|
+
|
10
|
+
# A singleton for writing logs Rublique deltas in JSON format to be used in
|
11
|
+
# conjunction with rublique_analyzer
|
12
|
+
class RubliqueLogger
|
13
|
+
class << self
|
14
|
+
# Path to logfile
|
15
|
+
@@path = '/tmp/rublique.log'
|
16
|
+
|
17
|
+
# Logfile handle
|
18
|
+
@@log = nil
|
19
|
+
|
20
|
+
# Set the logfile name. May only be done before .log is called
|
21
|
+
def file=(path)
|
22
|
+
raise 'Cannot change Railique log path after logfile has been opened' unless @@log.nil?
|
23
|
+
@@path = path
|
24
|
+
end
|
25
|
+
|
26
|
+
# Log a Rublique delta
|
27
|
+
def log
|
28
|
+
if @@log.nil?
|
29
|
+
@@log = File.open(@@path, 'a+')
|
30
|
+
@@log.sync = true
|
31
|
+
end
|
32
|
+
|
33
|
+
delta = Rublique.delta
|
34
|
+
@@log.write [Time.now.strftime('%Y-%m-%d %H:%M:%S'), delta].to_json + "\n" unless delta.empty?
|
35
|
+
delta
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
metadata
ADDED
@@ -0,0 +1,61 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
rubygems_version: 0.9.0
|
3
|
+
specification_version: 1
|
4
|
+
name: rublique
|
5
|
+
version: !ruby/object:Gem::Version
|
6
|
+
version: 0.0.1
|
7
|
+
date: 2006-12-19 00:00:00 -07:00
|
8
|
+
summary: Rublique monitors object lifetimes across various code sections, outputting a logfile of object deltas which can be used to generate CSV files of object use over time
|
9
|
+
require_paths:
|
10
|
+
- lib
|
11
|
+
email: tony@clickcaster.com
|
12
|
+
homepage: http://rublique.rubyforge.org
|
13
|
+
rubyforge_project: rublique
|
14
|
+
description:
|
15
|
+
autorequire:
|
16
|
+
default_executable:
|
17
|
+
bindir: bin
|
18
|
+
has_rdoc: true
|
19
|
+
required_ruby_version: !ruby/object:Gem::Version::Requirement
|
20
|
+
requirements:
|
21
|
+
- - ">"
|
22
|
+
- !ruby/object:Gem::Version
|
23
|
+
version: 0.0.0
|
24
|
+
version:
|
25
|
+
platform: ruby
|
26
|
+
signing_key:
|
27
|
+
cert_chain:
|
28
|
+
post_install_message:
|
29
|
+
authors:
|
30
|
+
- Tony Arcieri
|
31
|
+
files:
|
32
|
+
- README
|
33
|
+
- Rakefile
|
34
|
+
- lib
|
35
|
+
- lib/rublique.rb
|
36
|
+
- lib/rublique_logger.rb
|
37
|
+
- lib/rublique_dispatcher.rb
|
38
|
+
- bin
|
39
|
+
- bin/rublique_analyzer
|
40
|
+
test_files: []
|
41
|
+
|
42
|
+
rdoc_options: []
|
43
|
+
|
44
|
+
extra_rdoc_files: []
|
45
|
+
|
46
|
+
executables:
|
47
|
+
- rublique_analyzer
|
48
|
+
extensions: []
|
49
|
+
|
50
|
+
requirements: []
|
51
|
+
|
52
|
+
dependencies:
|
53
|
+
- !ruby/object:Gem::Dependency
|
54
|
+
name: json
|
55
|
+
version_requirement:
|
56
|
+
version_requirements: !ruby/object:Gem::Version::Requirement
|
57
|
+
requirements:
|
58
|
+
- - ">="
|
59
|
+
- !ruby/object:Gem::Version
|
60
|
+
version: 0.4.0
|
61
|
+
version:
|