rublique 0.0.1
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/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:
|