ruby-mass 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +7 -0
- data/CHANGELOG.rdoc +5 -0
- data/Gemfile +18 -0
- data/MIT-LICENSE +20 -0
- data/README.rdoc +177 -0
- data/Rakefile +14 -0
- data/VERSION +1 -0
- data/lib/core_ext.rb +3 -0
- data/lib/core_ext/object.rb +40 -0
- data/lib/mass.rb +234 -0
- data/lib/ruby-mass.rb +3 -0
- data/lib/ruby-mass/version.rb +7 -0
- data/ruby-mass.gemspec +16 -0
- data/script/console +39 -0
- data/test/test_helper.rb +7 -0
- data/test/unit/test_mass.rb +221 -0
- metadata +71 -0
data/.gitignore
ADDED
data/CHANGELOG.rdoc
ADDED
data/Gemfile
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
source "http://rubygems.org"
|
2
|
+
|
3
|
+
gemspec
|
4
|
+
|
5
|
+
group :gem_default do
|
6
|
+
gem "ruby-mass", :path => "."
|
7
|
+
end
|
8
|
+
|
9
|
+
group :gem_development do
|
10
|
+
gem "horo"
|
11
|
+
gem "pry"
|
12
|
+
end
|
13
|
+
|
14
|
+
group :gem_test do
|
15
|
+
gem "minitest"
|
16
|
+
gem "mocha"
|
17
|
+
gem "pry"
|
18
|
+
end
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright (c) 2012 Paul Engel
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.rdoc
ADDED
@@ -0,0 +1,177 @@
|
|
1
|
+
== RubyMass
|
2
|
+
|
3
|
+
Introspect the Ruby Heap by indexing, counting, locating references to and detaching (in order to release) objects - optionally narrowing by namespace.
|
4
|
+
|
5
|
+
=== Introduction
|
6
|
+
|
7
|
+
Memory management is an important factor within every application. It can slow down the performance of your application and even of your system. Unlike other programming languages, such as C or Pascal, Ruby does not let you handle the memory allocation directly.
|
8
|
+
|
9
|
+
The garbage collector (<tt>GC</tt>) controls releasing allocated memory. In order to let the GC free an object, you will have to ensure that there are no references to that object or else your memory usage will grow. Finding out why memory does not get released (memory leaks) is like finding a needle in a haystack.
|
10
|
+
|
11
|
+
RubyMass can help you tackle memory leaks as you can list (and count) which Ruby objects (<tt>Ruby mass :D</tt>) are in memory. Also, you can locate references to a certain object and remove them if wanted. Try out RubyMass and feel free to send in pull requests and/or suggestions.
|
12
|
+
|
13
|
+
<b>Note: RubyMass is successfully tested using Ruby 1.8.7, Ruby 1.9.2 and Ruby 1.9.3</b>
|
14
|
+
|
15
|
+
=== Installation
|
16
|
+
|
17
|
+
==== Using Bundler
|
18
|
+
|
19
|
+
Add RubyMass in <tt>Gemfile</tt> as a gem dependency:
|
20
|
+
|
21
|
+
gem "ruby-mass"
|
22
|
+
|
23
|
+
Run the following in your console to install with Bundler:
|
24
|
+
|
25
|
+
bundle install
|
26
|
+
|
27
|
+
=== Usage
|
28
|
+
|
29
|
+
Using RubyMass is pretty straightforward. All examples are based using the following code:
|
30
|
+
|
31
|
+
class Foo
|
32
|
+
class Bar; end
|
33
|
+
end
|
34
|
+
|
35
|
+
class Thing; end
|
36
|
+
class OneMoreThing; end
|
37
|
+
|
38
|
+
==== Getting an object by object_id
|
39
|
+
|
40
|
+
This is a shortcut to the <tt>ObjectSpace._id2ref</tt> method:
|
41
|
+
|
42
|
+
$ f = Foo.new
|
43
|
+
$ o = Mass[f.object_id]
|
44
|
+
$ f.object_id == o.object_id #=> true
|
45
|
+
|
46
|
+
==== Indexing objects
|
47
|
+
|
48
|
+
Just call <tt>Mass.index</tt> to get a Hash containing class names with object_ids of its instances. You can narrow the result by specifying a namespace.
|
49
|
+
|
50
|
+
$ Mass.index Foo #=> {}
|
51
|
+
|
52
|
+
$ f = Foo.new
|
53
|
+
$ b1 = Foo::Bar.new
|
54
|
+
$ b2 = Foo::Bar.new
|
55
|
+
$ t = Thing.new
|
56
|
+
|
57
|
+
$ f.object_id #=> 2154166500
|
58
|
+
$ b1.object_id #=> 2154037400
|
59
|
+
$ b2.object_id #=> 2154126780
|
60
|
+
$ t.object_id #=> 2154372180
|
61
|
+
|
62
|
+
$ Mass.index Foo #=> {"Foo"=>[2154166500], "Foo::Bar"=>[2154037400, 2154126780]}
|
63
|
+
$ Mass.index Foo::Bar #=> {"Foo::Bar"=>[2154037400, 2154126780]}
|
64
|
+
$ Mass.index Thing #=> {"Thing"=>[2154372180]}
|
65
|
+
$ Mass.index #=> {"String"=>[2151797000, 2151797080, 2151797120, 2151797140, ... a lot of more classes and object_ids}
|
66
|
+
$ Mass.index OneMoreThing #=> {}
|
67
|
+
|
68
|
+
==== Counting objects
|
69
|
+
|
70
|
+
The method <tt>Mass.count</tt> returns a similar Hash as <tt>Mass.index</tt>, but instead of arrays of object_ids it contains the number of objects.
|
71
|
+
Continueing with the previous example, you will get the following results:
|
72
|
+
|
73
|
+
$ Mass.count Foo #=> {"Foo"=>1, "Foo::Bar"=>2}
|
74
|
+
$ Mass.count Foo::Bar #=> {"Foo::Bar"=>2}
|
75
|
+
$ Mass.count Thing #=> {"Thing"=>1}
|
76
|
+
$ Mass.count #=> {"String"=>37589, "Array"=>11883, ... a lot of more classes and amounts}
|
77
|
+
$ Mass.count OneMoreThing #=> {}
|
78
|
+
|
79
|
+
==== Pretty print the amount of objects in memory
|
80
|
+
|
81
|
+
You can also pretty print the result of the <tt>Object.count</tt> method:
|
82
|
+
|
83
|
+
|
84
|
+
$ Mass.print Foo
|
85
|
+
+==================================================+
|
86
|
+
Objects within Foo namespace
|
87
|
+
+==================================================+
|
88
|
+
Foo::Bar: 2
|
89
|
+
Foo: 1
|
90
|
+
+==================================================+
|
91
|
+
|
92
|
+
$ Mass.print
|
93
|
+
+==================================================+
|
94
|
+
Objects within environment
|
95
|
+
+==================================================+
|
96
|
+
String: 37811
|
97
|
+
Array: 11899
|
98
|
+
RubyVM::InstructionSequence: 4225
|
99
|
+
Gem::Version: 2765
|
100
|
+
... a lot of more classes and amounts
|
101
|
+
+==================================================+
|
102
|
+
|
103
|
+
$ Mass.print OneMoreThing
|
104
|
+
+==================================================+
|
105
|
+
Objects within OneMoreThing namespace
|
106
|
+
+==================================================+
|
107
|
+
- no objects instantiated -
|
108
|
+
+==================================================+
|
109
|
+
|
110
|
+
==== Locating object references
|
111
|
+
|
112
|
+
Along with indexing (and counting) objects, locating object references is very important when solving memory leakage. An example:
|
113
|
+
|
114
|
+
class Foo
|
115
|
+
attr_accessor :foo
|
116
|
+
end
|
117
|
+
|
118
|
+
class Bar
|
119
|
+
attr_accessor :fool
|
120
|
+
end
|
121
|
+
|
122
|
+
$ foo1 = Foo.new
|
123
|
+
$ Mass.references(foo1) #=> {}
|
124
|
+
|
125
|
+
$ foo2 = Foo.new
|
126
|
+
$ foo2.object_id #=> 2155577620
|
127
|
+
$ foo2.foo = foo1
|
128
|
+
|
129
|
+
$ bar = Bar.new
|
130
|
+
$ bar.object_id #=> 2155383980
|
131
|
+
$ bar.fool = foo1
|
132
|
+
|
133
|
+
$ Mass.references(foo1) #=> {"Foo#2155577620" => ["@foo"], "Bar#2155383980" => ["@fool"]}
|
134
|
+
$ Mass.references(foo1, Foo) #=> {"Foo#2155577620" => ["@foo"]}
|
135
|
+
$ Mass.references(foo1, Bar) #=> {"Bar#2155383980" => ["@fool"]}
|
136
|
+
$ Mass.references(foo1, Hash) #=> {}
|
137
|
+
|
138
|
+
==== Detaching objects (free allocated memory)
|
139
|
+
|
140
|
+
After locating references to a certain object, you must remove them in order to let the garbage collector free the allocated memory of that object.
|
141
|
+
Just call the <tt>Mass.detach</tt> method and it will remove the object references and invoke <tt>GC.start</tt> instructing the garbage collector to do its job.
|
142
|
+
Continueing with the previous example, you will get the following results:
|
143
|
+
|
144
|
+
$ foo1.object_id #=> 2164554700
|
145
|
+
$ Mass.index(Foo) #=> {"Foo"=>[2155577620, 2164554700]}
|
146
|
+
|
147
|
+
# NOTE: Set foo1 to nil (within the block which only gets yielded after successfully detaching references) as it also is a pointer to the allocated memory
|
148
|
+
$ Mass.detach(foo1){foo1 = nil} #=> true
|
149
|
+
|
150
|
+
# NOTE: This ensures that Mass invokes GC.start but only after verifying that foo1 is nil pointer
|
151
|
+
$ Mass.gc!(foo1)
|
152
|
+
|
153
|
+
$ Mass.index(Foo) #=> {"Foo"=>[2155577620]}
|
154
|
+
|
155
|
+
<b>Note</b>: Use <tt>Mass.detach</tt> at <b>own risk</b>. Be sure you know what you are doing!
|
156
|
+
|
157
|
+
=== Last remarks
|
158
|
+
|
159
|
+
Please check out {test/unit/test_mass.rb}[https://github.com/archan937/ruby-mass/blob/master/test/unit/test_mass.rb]. You can run the unit tests with <tt>rake</tt> within the terminal.
|
160
|
+
Also, the RubyMass repo is provided with <tt>script/console</tt> which you can use for testing purposes.
|
161
|
+
|
162
|
+
=== Contact me
|
163
|
+
|
164
|
+
For support, remarks and requests please mail me at {paul.engel@holder.nl}[mailto:paul.engel@holder.nl].
|
165
|
+
|
166
|
+
=== License
|
167
|
+
|
168
|
+
Copyright (c) 2012 Paul Engel, released under the MIT license
|
169
|
+
|
170
|
+
http://holder.nl – http://codehero.es – http://gettopup.com – http://github.com/archan937 – http://twitter.com/archan937 –
|
171
|
+
{paul.engel@holder.nl}[mailto:paul.engel@holder.nl]
|
172
|
+
|
173
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
174
|
+
|
175
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
176
|
+
|
177
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env rake
|
2
|
+
require "bundler/gem_tasks"
|
3
|
+
require "rake/testtask"
|
4
|
+
|
5
|
+
task :default => :test
|
6
|
+
|
7
|
+
Rake::TestTask.new do |test|
|
8
|
+
test.pattern = "test/**/test_*.rb"
|
9
|
+
end
|
10
|
+
|
11
|
+
desc "Generate documentation for RubyMass"
|
12
|
+
task :rdoc do
|
13
|
+
system "rdoc -f horo -t 'RubyMass documentation' -a README.rdoc lib/*"
|
14
|
+
end
|
data/VERSION
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
0.1.0
|
data/lib/core_ext.rb
ADDED
@@ -0,0 +1,40 @@
|
|
1
|
+
class Object
|
2
|
+
|
3
|
+
# A convenience method for detaching all class instances with detach
|
4
|
+
#
|
5
|
+
def self.detach_all
|
6
|
+
Mass.index(self.class)[self.class.name].each{|object_id| Mass[object_id].detach}
|
7
|
+
end
|
8
|
+
|
9
|
+
# A convenience method for detaching all class instances with detach!
|
10
|
+
#
|
11
|
+
def self.detach_all!
|
12
|
+
Mass.index(self.class)[self.class.name].each{|object_id| Mass[object_id].detach!}
|
13
|
+
end
|
14
|
+
|
15
|
+
# Mass uses this method to derive object references. Override this method for increasing performance.
|
16
|
+
# In order to override this method, you would probably want to use <tt>Mass.references</tt> of a sample instance.
|
17
|
+
#
|
18
|
+
def _reference_instances(*mods)
|
19
|
+
Mass.send(:instances_within, *mods)
|
20
|
+
end
|
21
|
+
|
22
|
+
# A convenience method for Mass.references
|
23
|
+
#
|
24
|
+
def references(*mods)
|
25
|
+
Mass.references(self, *mods)
|
26
|
+
end
|
27
|
+
|
28
|
+
# A convenience method for Mass.detach
|
29
|
+
#
|
30
|
+
def detach(*mods, &block)
|
31
|
+
Mass.detach(self, *mods, &block)
|
32
|
+
end
|
33
|
+
|
34
|
+
# A convenience method for Mass.detach!
|
35
|
+
#
|
36
|
+
def detach!(*mods, &block)
|
37
|
+
Mass.detach!(self, *mods, &block)
|
38
|
+
end
|
39
|
+
|
40
|
+
end
|
data/lib/mass.rb
ADDED
@@ -0,0 +1,234 @@
|
|
1
|
+
# == Mass
|
2
|
+
#
|
3
|
+
# This module offers the following either within the whole Ruby Heap or narrowed by namespace:
|
4
|
+
#
|
5
|
+
# - indexing objects grouped by class
|
6
|
+
# - counting objects grouped by class
|
7
|
+
# - printing objects grouped by class
|
8
|
+
# - locating references to a specified object
|
9
|
+
# - detaching references to a specified object (in order to be able to release the object memory using the GC)
|
10
|
+
#
|
11
|
+
module Mass
|
12
|
+
extend self
|
13
|
+
|
14
|
+
# Returns the object corresponding to the passed object_id.
|
15
|
+
#
|
16
|
+
# ==== Example
|
17
|
+
#
|
18
|
+
# log_line = LogLine.new
|
19
|
+
# object = Mass[log_line.object_id]
|
20
|
+
# log_line.object_id == object.object_id #=> true
|
21
|
+
#
|
22
|
+
def [](object_id)
|
23
|
+
ObjectSpace._id2ref object_id
|
24
|
+
end
|
25
|
+
|
26
|
+
# A helper method after having called <tt>Mass.detach</tt>. It instructs the garbage collector to start when the optional passed variable is nil.
|
27
|
+
#
|
28
|
+
def gc!(variable = nil)
|
29
|
+
GC.start if variable.nil?
|
30
|
+
end
|
31
|
+
|
32
|
+
# Returns a hash containing classes with the object_ids of its instances currently in the Ruby Heap. You can narrow the result by namespace.
|
33
|
+
#
|
34
|
+
def index(*mods)
|
35
|
+
instances_within(*mods).inject({}) do |hash, object|
|
36
|
+
((hash[object.class.name] ||= []) << object.object_id).sort!
|
37
|
+
hash
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
# Returns a hash containing classes with the amount of its instances currently in the Ruby Heap. You can narrow the result by namespace.
|
42
|
+
#
|
43
|
+
def count(*mods)
|
44
|
+
index(*mods).inject({}) do |hash, (key, value)|
|
45
|
+
hash[key] = value.size
|
46
|
+
hash
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Prints all object instances within either the whole environment or narrowed by namespace group by class.
|
51
|
+
#
|
52
|
+
def print(*mods)
|
53
|
+
count(*mods).tap do |stats|
|
54
|
+
puts "\n"
|
55
|
+
puts "=" * 50
|
56
|
+
puts " Objects within #{mods ? "#{mods.collect(&:name).sort} namespace" : "environment"}"
|
57
|
+
puts "=" * 50
|
58
|
+
stats.keys.sort{|a, b| [stats[b], a] <=> [stats[a], b]}.each do |key|
|
59
|
+
puts " #{key}: #{stats[key]}"
|
60
|
+
end
|
61
|
+
puts " - no objects instantiated -" if stats.empty?
|
62
|
+
puts "=" * 50
|
63
|
+
puts "\n"
|
64
|
+
end
|
65
|
+
nil
|
66
|
+
end
|
67
|
+
|
68
|
+
# Returns all references to the passed object. You can narrow the namespace of the objects referencing to the object.
|
69
|
+
#
|
70
|
+
# ==== Example
|
71
|
+
#
|
72
|
+
# class A
|
73
|
+
# attr_accessor :a
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# class B
|
77
|
+
# attr_accessor :a
|
78
|
+
# end
|
79
|
+
#
|
80
|
+
# a1 = A.new
|
81
|
+
# Mass.references(a1) #=> {}
|
82
|
+
#
|
83
|
+
# a2 = A.new
|
84
|
+
# a2.object_id #=> 2152675940
|
85
|
+
# a2.a = a1
|
86
|
+
#
|
87
|
+
# b = B.new
|
88
|
+
# b.object_id #=> 2152681800
|
89
|
+
# b.a = a1
|
90
|
+
#
|
91
|
+
# Mass.references(a1) #=> {"A#2152675940" => ["@a"], "B#2152681800" => ["@a"]}
|
92
|
+
# Mass.references(a1, A) #=> {"A#2152675940" => ["@a"]}
|
93
|
+
# Mass.references(a1, B) #=> {"B#2152681800" => ["@a"]}
|
94
|
+
# Mass.references(a1, Hash) #=> {}
|
95
|
+
#
|
96
|
+
def references(object, *mods)
|
97
|
+
return {} if object.nil?
|
98
|
+
object._reference_instances(*mods).inject({}) do |hash, instance|
|
99
|
+
unless (refs = extract_references(instance, object)).empty?
|
100
|
+
hash["#{instance.class.name}##{instance.object_id}"] = refs.collect(&:to_s).sort{|a, b| a <=> b}
|
101
|
+
end
|
102
|
+
hash
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
# Removes all references to the passed object and yielding block when given and references within entire environment removed successfully. Use at own risk.
|
107
|
+
#
|
108
|
+
def detach(object, *mods, &block)
|
109
|
+
_detach(object, object._reference_instances(*mods), true, &block)
|
110
|
+
end
|
111
|
+
|
112
|
+
# Removes all references to the passed object and yielding block when given and references within passed namespaces removed successfully. Use at own risk.
|
113
|
+
#
|
114
|
+
def detach!(object, *mods, &block)
|
115
|
+
_detach(object, object._reference_instances(*mods), false, &block)
|
116
|
+
end
|
117
|
+
|
118
|
+
private
|
119
|
+
|
120
|
+
# Removes all references to the passed object, yielding block when given and removed references successfully.
|
121
|
+
def _detach(object, instances, check_environment, &block)
|
122
|
+
return false if object.nil?
|
123
|
+
instances = instances.inject([]) do |array, instance|
|
124
|
+
references = extract_references(instance, object)
|
125
|
+
unless references.empty? || array.any?{|x| x[0].object_id == instance.object_id}
|
126
|
+
array << [instance, references]
|
127
|
+
end
|
128
|
+
array
|
129
|
+
end
|
130
|
+
removed = instances.all? do |(instance, references)|
|
131
|
+
references.all? do |name|
|
132
|
+
remove_references(instance, object, name)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
(removed && (!check_environment || references(object).empty?)).tap do |detached|
|
136
|
+
yield if detached && block_given?
|
137
|
+
end
|
138
|
+
end
|
139
|
+
|
140
|
+
# Returns all classes within a certain namespace.
|
141
|
+
#
|
142
|
+
def classes_within(mods, initial_array = nil)
|
143
|
+
(initial_array || mods.select{|mod| mod.is_a? Class}).tap do |array|
|
144
|
+
mods.each do |mod|
|
145
|
+
mod.constants.each do |c|
|
146
|
+
const = mod.const_get c
|
147
|
+
if !array.include?(const) && (const.is_a?(Class) || const.is_a?(Module)) && const.name.match(/^#{mod.name}/)
|
148
|
+
if const.is_a?(Class)
|
149
|
+
array << const
|
150
|
+
end
|
151
|
+
if const.is_a?(Class) || const.is_a?(Module)
|
152
|
+
classes_within([const], array)
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
# Return all instances. You can narrow the results by passing a namespace.
|
161
|
+
#
|
162
|
+
def instances_within(*mods)
|
163
|
+
GC.start
|
164
|
+
[].tap do |array|
|
165
|
+
if mods.empty?
|
166
|
+
ObjectSpace.each_object do |object|
|
167
|
+
array << object
|
168
|
+
end
|
169
|
+
else
|
170
|
+
classes_within(mods).each do |klass|
|
171
|
+
ObjectSpace.each_object(klass) do |object|
|
172
|
+
array << object
|
173
|
+
end
|
174
|
+
end
|
175
|
+
end
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
# Returns whether the variable equals with or references to (either nested or not) the passed object.
|
180
|
+
#
|
181
|
+
def matches?(variable, object)
|
182
|
+
if variable.object_id == object.object_id
|
183
|
+
true
|
184
|
+
elsif variable.is_a?(Array) || variable.is_a?(Hash)
|
185
|
+
variable.any?{|x| matches? x, object}
|
186
|
+
else
|
187
|
+
false
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
# Extracts and returns all references between the passed object and instance.
|
192
|
+
#
|
193
|
+
def extract_references(instance, object)
|
194
|
+
instance.instance_variables.select do |name|
|
195
|
+
matches? instance.instance_variable_get(name), object
|
196
|
+
end
|
197
|
+
end
|
198
|
+
|
199
|
+
# Removes the references between the passed object and instance.
|
200
|
+
#
|
201
|
+
def remove_references(instance_or_variable, object, name = nil)
|
202
|
+
detached = true
|
203
|
+
variable = name ? instance_or_variable.instance_variable_get(name) : instance_or_variable
|
204
|
+
|
205
|
+
if name && variable.object_id == object.object_id
|
206
|
+
instance_or_variable.send :remove_instance_variable, name
|
207
|
+
elsif variable.is_a?(Array)
|
208
|
+
detached = false
|
209
|
+
variable.each do |value|
|
210
|
+
if value.object_id == object.object_id
|
211
|
+
detached = true
|
212
|
+
variable.delete value
|
213
|
+
elsif value.is_a?(Array) || value.is_a?(Hash)
|
214
|
+
detached ||= remove_references(value, object)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
elsif variable.is_a?(Hash)
|
218
|
+
detached = false
|
219
|
+
variable.each do |key, value|
|
220
|
+
if value.object_id == object.object_id
|
221
|
+
detached = true
|
222
|
+
variable.delete key
|
223
|
+
elsif value.is_a?(Array) || value.is_a?(Hash)
|
224
|
+
detached ||= remove_references(value, object)
|
225
|
+
end
|
226
|
+
end
|
227
|
+
else
|
228
|
+
detached = false
|
229
|
+
end
|
230
|
+
|
231
|
+
detached
|
232
|
+
end
|
233
|
+
|
234
|
+
end
|
data/lib/ruby-mass.rb
ADDED
data/ruby-mass.gemspec
ADDED
@@ -0,0 +1,16 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
|
3
|
+
Gem::Specification.new do |gem|
|
4
|
+
gem.authors = ["Paul Engel"]
|
5
|
+
gem.email = ["paul.engel@holder.nl"]
|
6
|
+
gem.description = %q{Introspect the Ruby Heap by indexing, counting, locating references to and detaching (in order to release) objects - optionally narrowing by namespace}
|
7
|
+
gem.summary = %q{Introspect the Ruby Heap by indexing, counting, locating references to and detaching (in order to release) objects - optionally narrowing by namespace}
|
8
|
+
gem.homepage = "https://github.com/archan937/ruby-objects"
|
9
|
+
|
10
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
11
|
+
gem.files = `git ls-files`.split("\n")
|
12
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
13
|
+
gem.name = "ruby-mass"
|
14
|
+
gem.require_paths = ["lib"]
|
15
|
+
gem.version = "0.1.0"
|
16
|
+
end
|
data/script/console
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
require "rubygems"
|
3
|
+
require "bundler"
|
4
|
+
|
5
|
+
Bundler.require :gem_default, :gem_development
|
6
|
+
|
7
|
+
class Foo
|
8
|
+
attr_accessor :foo
|
9
|
+
class Bar
|
10
|
+
attr_accessor :fool
|
11
|
+
end
|
12
|
+
end
|
13
|
+
class Thing
|
14
|
+
attr_accessor :food
|
15
|
+
end
|
16
|
+
class OneMoreThing
|
17
|
+
attr_accessor :thing
|
18
|
+
end
|
19
|
+
|
20
|
+
<<-RUBY
|
21
|
+
f1 = Foo.new; nil
|
22
|
+
f2 = Foo.new
|
23
|
+
f2.foo = f1
|
24
|
+
b = Foo::Bar.new
|
25
|
+
b.fool = f1
|
26
|
+
t = Thing.new
|
27
|
+
t.food = f1
|
28
|
+
object_id = f1.object_id
|
29
|
+
|
30
|
+
Mass.detach(f1){f1 = nil}
|
31
|
+
Mass.gc!(f1)
|
32
|
+
|
33
|
+
Mass[object_id] rescue nil
|
34
|
+
RUBY
|
35
|
+
|
36
|
+
puts "Loading development environment (RubyMass #{RubyMass::VERSION})"
|
37
|
+
puts "[0] pry(main)> defined classes Foo, Foo::Bar, Thing and OneMoreThing"
|
38
|
+
|
39
|
+
Pry.start
|
data/test/test_helper.rb
ADDED
@@ -0,0 +1,221 @@
|
|
1
|
+
require File.expand_path("../../test_helper", __FILE__)
|
2
|
+
|
3
|
+
module Unit
|
4
|
+
class TestMass < MiniTest::Unit::TestCase
|
5
|
+
|
6
|
+
describe Mass do
|
7
|
+
before do
|
8
|
+
class Foo
|
9
|
+
attr_accessor :foo
|
10
|
+
class Bar
|
11
|
+
attr_accessor :fool
|
12
|
+
end
|
13
|
+
end
|
14
|
+
class Thing
|
15
|
+
attr_accessor :food
|
16
|
+
end
|
17
|
+
class OneMoreThing
|
18
|
+
attr_accessor :thing
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
after do
|
23
|
+
GC.start
|
24
|
+
end
|
25
|
+
|
26
|
+
it "should be able to return the object corresponding to the passed object_id" do
|
27
|
+
f = Foo.new
|
28
|
+
o = Mass[f.object_id]
|
29
|
+
|
30
|
+
assert_equal true, f.object_id == o.object_id
|
31
|
+
|
32
|
+
f = nil
|
33
|
+
o = nil
|
34
|
+
end
|
35
|
+
|
36
|
+
it "should be able to index objects" do
|
37
|
+
assert_equal({}, Mass.index(Foo))
|
38
|
+
|
39
|
+
f = Foo.new
|
40
|
+
b1 = Foo::Bar.new
|
41
|
+
b2 = Foo::Bar.new
|
42
|
+
t = Thing.new
|
43
|
+
|
44
|
+
assert_equal({"Unit::TestMass::Foo" => [f.object_id], "Unit::TestMass::Foo::Bar" => [b1.object_id, b2.object_id].sort}, Mass.index(Foo))
|
45
|
+
assert_equal({"Unit::TestMass::Foo::Bar" => [b1.object_id, b2.object_id].sort}, Mass.index(Foo::Bar))
|
46
|
+
assert_equal({"Unit::TestMass::Thing" => [t.object_id]}, Mass.index(Thing))
|
47
|
+
assert_equal({"Unit::TestMass::Foo::Bar" => [b1.object_id, b2.object_id].sort, "Unit::TestMass::Thing" => [t.object_id]}, Mass.index(Foo::Bar, Thing))
|
48
|
+
assert_equal({}, Mass.index(OneMoreThing))
|
49
|
+
|
50
|
+
index = Mass.index
|
51
|
+
assert_equal true, index.keys.include?("String")
|
52
|
+
assert_equal true, index.keys.include?("Array")
|
53
|
+
assert_equal true, index.keys.include?("Hash")
|
54
|
+
assert_equal true, index["String"].size > 1000
|
55
|
+
|
56
|
+
f = nil
|
57
|
+
b1 = nil
|
58
|
+
b2 = nil
|
59
|
+
t = nil
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should be able to count objects" do
|
63
|
+
assert_equal({}, Mass.count(Foo))
|
64
|
+
|
65
|
+
f = Foo.new
|
66
|
+
b1 = Foo::Bar.new
|
67
|
+
b2 = Foo::Bar.new
|
68
|
+
t = Thing.new
|
69
|
+
|
70
|
+
assert_equal({"Unit::TestMass::Foo" => 1, "Unit::TestMass::Foo::Bar" => 2}, Mass.count(Foo))
|
71
|
+
assert_equal({"Unit::TestMass::Foo::Bar" => 2}, Mass.count(Foo::Bar))
|
72
|
+
assert_equal({"Unit::TestMass::Thing" => 1}, Mass.count(Thing))
|
73
|
+
assert_equal({"Unit::TestMass::Foo::Bar" => 2, "Unit::TestMass::Thing" => 1}, Mass.count(Foo::Bar, Thing))
|
74
|
+
assert_equal({}, Mass.count(OneMoreThing))
|
75
|
+
|
76
|
+
count = Mass.count
|
77
|
+
assert_equal true, count.keys.include?("String")
|
78
|
+
assert_equal true, count.keys.include?("Array")
|
79
|
+
assert_equal true, count.keys.include?("Hash")
|
80
|
+
assert_equal true, count["String"] > 1000
|
81
|
+
|
82
|
+
f = nil
|
83
|
+
b1 = nil
|
84
|
+
b2 = nil
|
85
|
+
t = nil
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should tackle nil objects" do
|
89
|
+
assert_equal({}, Mass.references(nil))
|
90
|
+
assert_equal(false, Mass.detach(nil))
|
91
|
+
assert_equal(false, Mass.detach!(nil))
|
92
|
+
end
|
93
|
+
|
94
|
+
# NOTE: The first assertion fails sometimes (maybe multiple test runs related?). If so, retry running the tests.
|
95
|
+
it "should be able to locate object references" do
|
96
|
+
f1 = Foo.new
|
97
|
+
assert_equal({}, Mass.references(f1))
|
98
|
+
|
99
|
+
f2 = Foo.new
|
100
|
+
f2.foo = f1
|
101
|
+
|
102
|
+
assert_equal({"Unit::TestMass::Foo##{f2.object_id}" => ["@foo"]}, Mass.references(f1))
|
103
|
+
assert_equal({"Unit::TestMass::Foo##{f2.object_id}" => ["@foo"]}, Mass.references(f1, Foo))
|
104
|
+
assert_equal({}, Mass.references(f1, Foo::Bar))
|
105
|
+
assert_equal({}, Mass.references(f1, Foo::Bar, OneMoreThing))
|
106
|
+
|
107
|
+
b = Foo::Bar.new
|
108
|
+
b.fool = f1
|
109
|
+
|
110
|
+
t = Thing.new
|
111
|
+
t.food = f1
|
112
|
+
|
113
|
+
assert_equal({
|
114
|
+
"Unit::TestMass::Foo##{f2.object_id}" => ["@foo"],
|
115
|
+
"Unit::TestMass::Foo::Bar##{b.object_id}" => ["@fool"],
|
116
|
+
"Unit::TestMass::Thing##{t.object_id}" => ["@food"]
|
117
|
+
}, Mass.references(f1))
|
118
|
+
|
119
|
+
assert_equal({
|
120
|
+
"Unit::TestMass::Foo##{f2.object_id}" => ["@foo"],
|
121
|
+
"Unit::TestMass::Foo::Bar##{b.object_id}" => ["@fool"]
|
122
|
+
}, Mass.references(f1, Foo))
|
123
|
+
|
124
|
+
assert_equal({
|
125
|
+
"Unit::TestMass::Foo##{f2.object_id}" => ["@foo"],
|
126
|
+
"Unit::TestMass::Foo::Bar##{b.object_id}" => ["@fool"]
|
127
|
+
}, f1.references(Foo, Foo::Bar))
|
128
|
+
|
129
|
+
assert_equal({
|
130
|
+
"Unit::TestMass::Foo::Bar##{b.object_id}" => ["@fool"]
|
131
|
+
}, Mass.references(f1, Foo::Bar))
|
132
|
+
|
133
|
+
assert_equal({
|
134
|
+
"Unit::TestMass::Thing##{t.object_id}" => ["@food"]
|
135
|
+
}, Mass.references(f1, Thing))
|
136
|
+
|
137
|
+
assert_equal({}, Mass.references(f1, Hash, OneMoreThing))
|
138
|
+
|
139
|
+
f1 = nil
|
140
|
+
f2 = nil
|
141
|
+
b = nil
|
142
|
+
t = nil
|
143
|
+
end
|
144
|
+
|
145
|
+
describe "using simple objects" do
|
146
|
+
# NOTE: I don't know why the last assertion fails (maybe test environment related?) as the same steps to succeed within script/console (see contents of the file)
|
147
|
+
it "should be able to detach objects" do
|
148
|
+
f1 = Foo.new
|
149
|
+
object_id = f1.object_id
|
150
|
+
|
151
|
+
assert_equal({}, Mass.references(f1))
|
152
|
+
|
153
|
+
f2 = Foo.new
|
154
|
+
f2.foo = f1
|
155
|
+
b = Foo::Bar.new
|
156
|
+
b.fool = f1
|
157
|
+
t = Thing.new
|
158
|
+
t.food = f1
|
159
|
+
|
160
|
+
assert_equal({
|
161
|
+
"Unit::TestMass::Foo" => [object_id, f2.object_id].sort,
|
162
|
+
"Unit::TestMass::Foo::Bar" => [b.object_id]
|
163
|
+
}, Mass.index(Foo))
|
164
|
+
assert_equal({
|
165
|
+
"Unit::TestMass::Foo##{f2.object_id}" => ["@foo"],
|
166
|
+
"Unit::TestMass::Foo::Bar##{b.object_id}" => ["@fool"],
|
167
|
+
"Unit::TestMass::Thing##{t.object_id}" => ["@food"]
|
168
|
+
}, Mass.references(f1))
|
169
|
+
|
170
|
+
assert_equal(false, Mass.detach(f1, OneMoreThing){f1 = nil})
|
171
|
+
|
172
|
+
assert_equal({
|
173
|
+
"Unit::TestMass::Foo" => [object_id, f2.object_id].sort,
|
174
|
+
"Unit::TestMass::Foo::Bar" => [b.object_id]
|
175
|
+
}, Mass.index(Foo))
|
176
|
+
assert_equal({
|
177
|
+
"Unit::TestMass::Foo##{f2.object_id}" => ["@foo"],
|
178
|
+
"Unit::TestMass::Foo::Bar##{b.object_id}" => ["@fool"],
|
179
|
+
"Unit::TestMass::Thing##{t.object_id}" => ["@food"]
|
180
|
+
}, Mass.references(f1))
|
181
|
+
|
182
|
+
assert_equal(false, Mass.detach(f1, OneMoreThing, Thing){f1 = nil})
|
183
|
+
|
184
|
+
assert_equal({
|
185
|
+
"Unit::TestMass::Foo" => [object_id, f2.object_id].sort,
|
186
|
+
"Unit::TestMass::Foo::Bar" => [b.object_id]
|
187
|
+
}, Mass.index(Foo))
|
188
|
+
assert_equal({
|
189
|
+
"Unit::TestMass::Foo##{f2.object_id}" => ["@foo"],
|
190
|
+
"Unit::TestMass::Foo::Bar##{b.object_id}" => ["@fool"]
|
191
|
+
}, Mass.references(f1))
|
192
|
+
|
193
|
+
assert_equal(false, f1.detach(Foo::Bar){f1 = nil})
|
194
|
+
|
195
|
+
assert_equal({
|
196
|
+
"Unit::TestMass::Foo" => [object_id, f2.object_id].sort,
|
197
|
+
"Unit::TestMass::Foo::Bar" => [b.object_id]
|
198
|
+
}, Mass.index(Foo))
|
199
|
+
assert_equal({
|
200
|
+
"Unit::TestMass::Foo##{f2.object_id}" => ["@foo"]
|
201
|
+
}, Mass.references(f1))
|
202
|
+
|
203
|
+
assert_equal(true, Mass.detach(f1){f1 = nil})
|
204
|
+
|
205
|
+
Mass.gc!(f1)
|
206
|
+
assert_equal({
|
207
|
+
"Unit::TestMass::Foo" => [f2.object_id],
|
208
|
+
"Unit::TestMass::Foo::Bar" => [b.object_id]
|
209
|
+
}, Mass.index(Foo))
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
describe "using complex objects" do
|
214
|
+
it "should be able to detach objects" do
|
215
|
+
# TODO: although RubyMass is used within a very complex project using DataMapper write tests within a similar situation
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
end
|
221
|
+
end
|
metadata
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: ruby-mass
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease:
|
5
|
+
version: 0.1.0
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Paul Engel
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
|
13
|
+
date: 2012-06-11 00:00:00 Z
|
14
|
+
dependencies: []
|
15
|
+
|
16
|
+
description: Introspect the Ruby Heap by indexing, counting, locating references to and detaching (in order to release) objects - optionally narrowing by namespace
|
17
|
+
email:
|
18
|
+
- paul.engel@holder.nl
|
19
|
+
executables: []
|
20
|
+
|
21
|
+
extensions: []
|
22
|
+
|
23
|
+
extra_rdoc_files: []
|
24
|
+
|
25
|
+
files:
|
26
|
+
- .gitignore
|
27
|
+
- CHANGELOG.rdoc
|
28
|
+
- Gemfile
|
29
|
+
- MIT-LICENSE
|
30
|
+
- README.rdoc
|
31
|
+
- Rakefile
|
32
|
+
- VERSION
|
33
|
+
- lib/core_ext.rb
|
34
|
+
- lib/core_ext/object.rb
|
35
|
+
- lib/mass.rb
|
36
|
+
- lib/ruby-mass.rb
|
37
|
+
- lib/ruby-mass/version.rb
|
38
|
+
- ruby-mass.gemspec
|
39
|
+
- script/console
|
40
|
+
- test/test_helper.rb
|
41
|
+
- test/unit/test_mass.rb
|
42
|
+
homepage: https://github.com/archan937/ruby-objects
|
43
|
+
licenses: []
|
44
|
+
|
45
|
+
post_install_message:
|
46
|
+
rdoc_options: []
|
47
|
+
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: "0"
|
56
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ">="
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: "0"
|
62
|
+
requirements: []
|
63
|
+
|
64
|
+
rubyforge_project:
|
65
|
+
rubygems_version: 1.8.17
|
66
|
+
signing_key:
|
67
|
+
specification_version: 3
|
68
|
+
summary: Introspect the Ruby Heap by indexing, counting, locating references to and detaching (in order to release) objects - optionally narrowing by namespace
|
69
|
+
test_files:
|
70
|
+
- test/test_helper.rb
|
71
|
+
- test/unit/test_mass.rb
|