heapy 0.1.2 → 0.1.3

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: ababd2495856e5682c0f7f136ee0fe112304bc1a
4
- data.tar.gz: db47b39aa2d4207dc27a1753c1444830aba758bb
3
+ metadata.gz: 3e96b6f7d59e467d22dfbd41cb13ce09e262b951
4
+ data.tar.gz: 2538b65bed82efdf2d1e11cc6f769189fcc31e9a
5
5
  SHA512:
6
- metadata.gz: 5695269c2619549c931188225fd6b52f0d2acd343f92aba069a8e3cd937d0c97d2b6b15f2585bd96ae78c905708b8612a0ff51bcbfb4ce746f6bd6df423c62f4
7
- data.tar.gz: b70fbde861babd816b71ee8da6c8ecafb06192740dc4dcbdbb19ad930578925d62ffbdf295566f9e2d938366e385281303a4b1c16ad86c9486ff6f4bb8131d13
6
+ metadata.gz: 79d5a795873c51551eb56082e5832f03a4f5a9c6d837d14f8b4d0deb2a78c7a7682bef9183d469cb18bd32c0f87e16b596a8c6db61ce7117e62533352735d2f6
7
+ data.tar.gz: 797916602227e525d04af3fb9d83cb373c954a6f3ec3cfec9aa38f1df81ae2e317681565518cd97d5751e85b703e096acd9d6802047c9c040921413b12fa612b
data/.gitignore CHANGED
@@ -7,3 +7,5 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ tmp
11
+ .DS_Store
@@ -1,4 +1,4 @@
1
1
  language: ruby
2
2
  rvm:
3
- - 2.2.3
3
+ - 2.3.2
4
4
  before_install: gem install bundler -v 1.10.6
@@ -1,3 +1,9 @@
1
+ ## 0.1.3
2
+
3
+ - I'm really bad at keeping a log of changes on this project sorry
4
+ - Printing out the memory size for a generation
5
+ - Printing out the total number of objects for the heap in the summary
6
+
1
7
  ## 0.1.1
2
8
 
3
9
  - Less memory retention when parsing large heap dumps.
data/README.md CHANGED
@@ -73,6 +73,8 @@ $ heapy read tmp/2015-10-01T10:18:59-05:00-heap.dump 17
73
73
  92200 /app/vendor/bundle/ruby/2.2.0/gems/activesupport-4.2.3/lib/active_support/core_ext/numeric/conversions.rb:131
74
74
  ```
75
75
 
76
+ ### Reviewing all generations
77
+
76
78
  If you want to read all generations you can use the "all" directive
77
79
 
78
80
  ```
@@ -89,7 +91,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
89
91
 
90
92
  ## Contributing
91
93
 
92
- Bug reports and pull requests are welcome on GitHub at https://github.com/[USERNAME]/heapy. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.
94
+ Bug reports and pull requests are welcome on GitHub at https://github.com/schneems/heapy. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](contributor-covenant.org) code of conduct.
93
95
 
94
96
 
95
97
  ## License
@@ -63,138 +63,8 @@ HALP
63
63
  end
64
64
  end
65
65
  end
66
+ end
66
67
 
67
- class Analyzer
68
- def initialize(filename)
69
- @filename = filename
70
- end
71
-
72
- def read
73
- File.open(@filename) do |f|
74
- f.each_line do |line|
75
- begin
76
- parsed = JSON.parse(line)
77
- yield parsed
78
- rescue JSON::ParserError
79
- puts "Could not parse #{line}"
80
- end
81
- end
82
- end
83
- end
84
-
85
- def drill_down(generation_to_inspect)
86
- puts ""
87
- puts "Analyzing Heap (Generation: #{generation_to_inspect})"
88
- puts "-------------------------------"
89
- puts ""
90
-
91
- generation_to_inspect = Integer(generation_to_inspect) unless generation_to_inspect == "all"
92
-
93
- #
94
- memsize_hash = Hash.new { |h, k| h[k] = 0 }
95
- count_hash = Hash.new { |h, k| h[k] = 0 }
96
- string_count = Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = 0 } }
97
-
98
- reference_hash = Hash.new { |h, k| h[k] = 0 }
99
-
100
- read do |parsed|
101
- generation = parsed["generation"] || 0
102
- if generation_to_inspect == "all".freeze || generation == generation_to_inspect
103
- next unless parsed["file"]
104
-
105
- key = "#{ parsed["file"] }:#{ parsed["line"] }"
106
- memsize_hash[key] += parsed["memsize"] || 0
107
- count_hash[key] += 1
108
-
109
- if parsed["type"] == "STRING".freeze
110
- string_count[parsed["value"]][key] += 1 if parsed["value"]
111
- end
112
-
113
- if parsed["references"]
114
- reference_hash[key] += parsed["references"].length
115
- end
116
- end
117
- end
118
-
119
- raise "not a valid Generation: #{generation_to_inspect.inspect}" if memsize_hash.empty?
120
-
121
- total_memsize = memsize_hash.inject(0){|count, (k, v)| count += v}
122
-
123
- # /Users/richardschneeman/Documents/projects/codetriage/app/views/layouts/application.html.slim:1"=>[{"address"=>"0x7f8a4fbf2328", "type"=>"STRING", "class"=>"0x7f8a4d5dec68", "bytesize"=>223051, "capacity"=>376832, "encoding"=>"UTF-8", "file"=>"/Users/richardschneeman/Documents/projects/codetriage/app/views/layouts/application.html.slim", "line"=>1, "method"=>"new", "generation"=>36, "memsize"=>377065, "flags"=>{"wb_protected"=>true, "old"=>true, "long_lived"=>true, "marked"=>true}}]}
124
- puts "allocated by memory (#{total_memsize}) (in bytes)"
125
- puts "=============================="
126
- memsize_hash = memsize_hash.sort {|(k1, v1), (k2, v2)| v2 <=> v1 }.first(50)
127
- longest = memsize_hash.first[1].to_s.length
128
- memsize_hash.each do |file_line, memsize|
129
- puts " #{memsize.to_s.rjust(longest)} #{file_line}"
130
- end
131
-
132
- total_count = count_hash.inject(0){|count, (k, v)| count += v}
133
-
134
- puts ""
135
- puts "object count (#{total_count})"
136
- puts "=============================="
137
- count_hash = count_hash.sort {|(k1, v1), (k2, v2)| v2 <=> v1 }.first(50)
138
- longest = count_hash.first[1].to_s.length
139
- count_hash.each do |file_line, memsize|
140
- puts " #{memsize.to_s.rjust(longest)} #{file_line}"
141
- end
142
-
143
- puts ""
144
- puts "High Ref Counts"
145
- puts "=============================="
146
- puts ""
147
-
148
- reference_hash = reference_hash.sort {|(k1, v1), (k2, v2)| v2 <=> v1 }.first(50)
149
- longest = count_hash.first[1].to_s.length
150
-
151
- reference_hash.each do |file_line, count|
152
- puts " #{count.to_s.rjust(longest)} #{file_line}"
153
- end
154
-
155
- puts ""
156
- puts "Duplicate strings"
157
- puts "=============================="
158
- puts ""
159
- value_count = {}
160
-
161
- string_count.each do |string, location_count_hash|
162
- value_count[string] = location_count_hash.values.inject(&:+)
163
- end
164
-
165
- value_count = value_count.sort {|(k1, v1), (k2, v2)| v2 <=> v1 }.first(50)
166
- longest = value_count.first[1].to_s.length
167
-
168
- value_count.each do |string, c1|
169
-
170
- puts " #{c1.to_s.rjust(longest)} #{string.inspect}"
171
- string_count[string].sort {|(k1, v1), (k2, v2)| v2 <=> v1 }.each do |file_line, c2|
172
- puts " #{c2.to_s.rjust(longest)} #{file_line}"
173
- end
174
- puts ""
175
- end
176
-
177
- end
178
-
179
- def analyze
180
- puts ""
181
- puts "Analyzing Heap"
182
- puts "=============="
183
- default_key = "nil".freeze
184
-
185
- # generation number is key, value is count
186
- data = Hash.new {|h, k| h[k] = 0 }
68
+ require 'heapy/analyzer'
69
+ require 'heapy/alive'
187
70
 
188
- read do |parsed|
189
- data[parsed["generation"] || 0] += 1
190
- end
191
-
192
- data = data.sort {|(k1,v1), (k2,v2)| k1 <=> k2 }
193
- max_length = [data.last[0].to_s.length, default_key.length].max
194
- data.each do |generation, count|
195
- generation = default_key if generation == 0
196
- puts "Generation: #{ generation.to_s.rjust(max_length) } object count: #{ count }"
197
- end
198
- end
199
- end
200
- end
@@ -0,0 +1,269 @@
1
+ require 'objspace'
2
+ require 'stringio'
3
+
4
+ module Heapy
5
+
6
+ # This is an experimental module and likely to change. Don't use in production.
7
+ #
8
+ # Use at your own risk. APIs are not stable.
9
+ #
10
+ # == What
11
+ #
12
+ # You can use it to trace objects to see if they are still "alive" in memory.
13
+ # Unlike the heapy CLI this is meant to be used in live running code.
14
+ #
15
+ # This works by retaining an object's address in memory, then running GC
16
+ # and taking a heap dump. If the object exists in the heap dump, it is retained.
17
+ # Since we have the whole heap dump we can also do things like find what is retaining
18
+ # your object preventing it from being collected.
19
+ #
20
+ # == Use It
21
+ #
22
+ # You need to first start tracing objects:
23
+ #
24
+ # Heapy::Alive.start_object_trace!(heap_file: "./tmp/heap.json")
25
+ #
26
+ # Next in your code you want to specify the object ato trace
27
+ #
28
+ # string = "hello world"
29
+ # Heapy::Alive.trace_without_retain(string)
30
+ #
31
+ # When the code is done executing you can get a reference to all "tracer"
32
+ # objects by running:
33
+ #
34
+ # Heapy::Alive.traced_objects.each do |tracer|
35
+ # puts tracer.raw_json_hash if tracer.object_retained?
36
+ # end
37
+ #
38
+ # A few helpful methods on `tracer` objects:
39
+ #
40
+ # - `raw_json_hash` returns the hash of the object from the heap dump.
41
+ # - `object_retained?` returns truthy if the object was still present in the heap dump.
42
+ # - `address` a string of the memory address of the object you're tracing.
43
+ # - `tracked_to_s` a string that represents the object you're tracing (default
44
+ # is result of calling inspect on the method). You can pass in a custom representation
45
+ # when initializing the object. Can be useful for when `inspect` on the object you
46
+ # are tracing is too verbose.
47
+ # - `id2ref` returns the original object being traced (if it is still in memory).
48
+ # - `root?` returns false if the tracer isn't the root object.
49
+ #
50
+ # See `ObjectTracker` for more methods.
51
+ #
52
+ # If you want to see what retains an object, you can use `ObectTracker#retained_by`
53
+ # method (caution this is extremely expensive and requires re-walking the whole heap dump:
54
+ #
55
+ # Heapy::Alive.traced_objects.each do |tracer|
56
+ # if tracer.object_retained?
57
+ # puts "Traced: #{tracer.raw_json_hash}"
58
+ # tracer.retained_by.each do |retainer|
59
+ # puts " Retained by: #{retainer.raw_json_hash}"
60
+ # end
61
+ # end
62
+ # end
63
+ #
64
+ # You can iterate up the whole retained tree by using the `retained_by` method on tracers
65
+ # returned. But again it's expensive. If you have large heap dump or if you're tracing a bunch
66
+ # of objects, continuously calling `retained_by` will take lots of time. We also don't
67
+ # do any circular dependency detection so if you have two objects that depend on each other,
68
+ # you may hit an infinite loop.
69
+ #
70
+ # If you know that you'll need the retained objects of the main objects you're tracing you can
71
+ # save re-walking the heap the first N times by using the `retained_by` flag:
72
+ #
73
+ # Heapy::Alive.traced_objects(retained_by: true) do |tracer|
74
+ # # ...
75
+ # end
76
+ #
77
+ # This will pre-fetch the first level of "parents" for each object you're tracing.
78
+ #
79
+ # Did I mention this is all experimental and may change?
80
+ module Alive
81
+ @mutex = Mutex.new
82
+ @retain_hash = {}
83
+ @heap_file = nil
84
+ @started = false
85
+
86
+ def self.address_to_object(address)
87
+ obj_id = address.to_i(16) / 2
88
+ ObjectSpace._id2ref(obj_id)
89
+ rescue RangeError
90
+ nil
91
+ end
92
+
93
+ def self.start_object_trace!(heap_file: "./tmp/heap.json")
94
+ @mutex.synchronize do
95
+ @started ||= true && ObjectSpace.trace_object_allocations_start
96
+ @heap_file ||= heap_file
97
+ end
98
+ end
99
+
100
+ def self.trace_without_retain(object, to_s: nil)
101
+ tracker = ObjectTracker.new(object_id: object.object_id, to_s: to_s || object.inspect)
102
+ @mutex.synchronize do
103
+ @retain_hash[tracker.address] = tracker
104
+ end
105
+ end
106
+
107
+ def self.retained_by(tracer: nil, address: nil)
108
+ target_address = address || tracer.address
109
+ tracer = tracer || @retain_hash[address]
110
+
111
+ raise "not a valid address #{target_address}" if target_address.nil?
112
+
113
+ retainer_array = []
114
+ Analyzer.new(@heap_file).read do |json_hash|
115
+ retainers_from_json_hash(json_hash, target_address: target_address, retainer_array: retainer_array)
116
+ end
117
+
118
+ retainer_array
119
+ end
120
+
121
+ class << self
122
+ private def retainers_from_json_hash(json_hash, retainer_array:, target_address:)
123
+ references = json_hash["references"]
124
+ return unless references
125
+
126
+ references.each do |address|
127
+ next unless address == target_address
128
+
129
+ if json_hash["root"]
130
+ retainer = RootTracker.new(json_hash)
131
+ else
132
+ address = json_hash["address"]
133
+ representation = self.address_to_object(address)&.inspect || "object not traced".freeze
134
+ retainer = ObjectTracker.new(address: address, to_s: representation)
135
+ retainer.raw_json_hash = json_hash
136
+ end
137
+
138
+ retainer_array << retainer
139
+ end
140
+ end
141
+ end
142
+
143
+ private
144
+ @string_io = StringIO.new
145
+ # GIANT BALL OF HACKS || THERE BE DRAGONS
146
+ #
147
+ # There is so much I don't understand on why I need to do the things
148
+ # I'm doing in this method.
149
+ #
150
+ # Also see `living_dead` https://github.com/schneems/living_dead
151
+ def self.gc_start
152
+ # During debugging I found calling "puts" made some things
153
+ # mysteriously work, I have no idea why. If you remove this line
154
+ # then (more) tests fail. Maybe it has something to do with the way
155
+ # GC interacts with IO? I seriously have no idea.
156
+ #
157
+ @string_io.puts "=="
158
+
159
+ # Calling flush so we don't create a memory leak.
160
+ # Funny enough maybe calling flush without `puts` also works?
161
+ # IDK
162
+ #
163
+ @string_io.flush
164
+
165
+ # Calling GC multiple times fixes a different class of things
166
+ # Specifically the singleton_class.instance_eval tests.
167
+ # It might also be related to calling GC in a block, but changing
168
+ # to 1.times brings back failures.
169
+ #
170
+ # Calling 2 times results in eventual failure https://twitter.com/schneems/status/804369346910896128
171
+ # Calling 5 times results in eventual failure https://twitter.com/schneems/status/804382968307445760
172
+ # Trying 10 times
173
+ #
174
+ 10.times { GC.start }
175
+ end
176
+ public
177
+
178
+ def self.traced_objects(retained_by: false)
179
+ raise "You aren't tracing anything call Heapy::Alive.trace_without_retain first" if @retain_hash.empty?
180
+ self.gc_start
181
+
182
+ ObjectSpace.dump_all(output: File.open(@heap_file,'w'))
183
+
184
+ retainer_address_array_hash = {}
185
+
186
+ Analyzer.new(@heap_file).read do |json_hash|
187
+ address = json_hash["address"]
188
+ tracer = @retain_hash[address]
189
+ next unless tracer
190
+ tracer.raw_json_hash = json_hash
191
+
192
+ if retained_by
193
+ retainers_from_json_hash(json_hash, target_address: address, retainer_array: tracer.retained_by)
194
+ end
195
+ end
196
+ @retain_hash.values
197
+ end
198
+
199
+ class RootTracker
200
+ def initialize(json)
201
+ @raw_json_hash = json
202
+ end
203
+
204
+ def references
205
+ []
206
+ end
207
+
208
+ def id2ref
209
+ raise "cannot turn root object into an object"
210
+ end
211
+
212
+ def root?
213
+ true
214
+ end
215
+
216
+ def address
217
+ raise "root does not have an address"
218
+ end
219
+
220
+ def object_retained?
221
+ true
222
+ end
223
+
224
+ def tracked_to_s
225
+ "ROOT"
226
+ end
227
+ end
228
+
229
+ class ObjectTracker
230
+ attr_reader :address, :tracked_to_s
231
+
232
+ def initialize(object_id: nil, address: nil, to_s: )
233
+ if object_id
234
+ @address = "0x#{ (object_id << 1).to_s(16) }"
235
+ else
236
+ @address = address
237
+ end
238
+
239
+ raise "must provide address: #{@address.inspect}" if @address.nil?
240
+
241
+ @tracked_to_s = to_s.dup
242
+ @retained_by = nil
243
+ end
244
+
245
+ def id2ref
246
+ Heapy::Alive.address_to_object(address)
247
+ end
248
+
249
+ def root?
250
+ false
251
+ end
252
+
253
+ def object_retained?
254
+ raw_json_hash && raw_json_hash["address"]
255
+ end
256
+
257
+ def retainer_array
258
+ @retained_by ||= []
259
+ @retained_by
260
+ end
261
+
262
+ def retained_by
263
+ @retained_by || Heapy::Alive.retained_by(tracer: self)
264
+ end
265
+
266
+ attr_accessor :raw_json_hash
267
+ end
268
+ end
269
+ end
@@ -0,0 +1,151 @@
1
+ module Heapy
2
+ class Analyzer
3
+ def initialize(filename)
4
+ @filename = filename
5
+ end
6
+
7
+ def read
8
+ File.open(@filename) do |f|
9
+ f.each_line do |line|
10
+ begin
11
+ parsed = JSON.parse(line)
12
+ yield parsed
13
+ rescue JSON::ParserError
14
+ puts "Could not parse #{line}"
15
+ end
16
+ end
17
+ end
18
+ end
19
+
20
+ def drill_down(generation_to_inspect)
21
+ puts ""
22
+ puts "Analyzing Heap (Generation: #{generation_to_inspect})"
23
+ puts "-------------------------------"
24
+ puts ""
25
+
26
+ generation_to_inspect = Integer(generation_to_inspect) unless generation_to_inspect == "all"
27
+
28
+ #
29
+ memsize_hash = Hash.new { |h, k| h[k] = 0 }
30
+ count_hash = Hash.new { |h, k| h[k] = 0 }
31
+ string_count = Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = 0 } }
32
+
33
+ reference_hash = Hash.new { |h, k| h[k] = 0 }
34
+
35
+ read do |parsed|
36
+ generation = parsed["generation"] || 0
37
+ if generation_to_inspect == "all".freeze || generation == generation_to_inspect
38
+ next unless parsed["file"]
39
+
40
+ key = "#{ parsed["file"] }:#{ parsed["line"] }"
41
+ memsize_hash[key] += parsed["memsize"] || 0
42
+ count_hash[key] += 1
43
+
44
+ if parsed["type"] == "STRING".freeze
45
+ string_count[parsed["value"]][key] += 1 if parsed["value"]
46
+ end
47
+
48
+ if parsed["references"]
49
+ reference_hash[key] += parsed["references"].length
50
+ end
51
+ end
52
+ end
53
+
54
+ raise "not a valid Generation: #{generation_to_inspect.inspect}" if memsize_hash.empty?
55
+
56
+ total_memsize = memsize_hash.inject(0){|count, (k, v)| count += v}
57
+
58
+ # /Users/richardschneeman/Documents/projects/codetriage/app/views/layouts/application.html.slim:1"=>[{"address"=>"0x7f8a4fbf2328", "type"=>"STRING", "class"=>"0x7f8a4d5dec68", "bytesize"=>223051, "capacity"=>376832, "encoding"=>"UTF-8", "file"=>"/Users/richardschneeman/Documents/projects/codetriage/app/views/layouts/application.html.slim", "line"=>1, "method"=>"new", "generation"=>36, "memsize"=>377065, "flags"=>{"wb_protected"=>true, "old"=>true, "long_lived"=>true, "marked"=>true}}]}
59
+ puts "allocated by memory (#{total_memsize}) (in bytes)"
60
+ puts "=============================="
61
+ memsize_hash = memsize_hash.sort {|(k1, v1), (k2, v2)| v2 <=> v1 }.first(50)
62
+ longest = memsize_hash.first[1].to_s.length
63
+ memsize_hash.each do |file_line, memsize|
64
+ puts " #{memsize.to_s.rjust(longest)} #{file_line}"
65
+ end
66
+
67
+ total_count = count_hash.inject(0){|count, (k, v)| count += v}
68
+
69
+ puts ""
70
+ puts "object count (#{total_count})"
71
+ puts "=============================="
72
+ count_hash = count_hash.sort {|(k1, v1), (k2, v2)| v2 <=> v1 }.first(50)
73
+ longest = count_hash.first[1].to_s.length
74
+ count_hash.each do |file_line, memsize|
75
+ puts " #{memsize.to_s.rjust(longest)} #{file_line}"
76
+ end
77
+
78
+ puts ""
79
+ puts "High Ref Counts"
80
+ puts "=============================="
81
+ puts ""
82
+
83
+ reference_hash = reference_hash.sort {|(k1, v1), (k2, v2)| v2 <=> v1 }.first(50)
84
+ longest = count_hash.first[1].to_s.length
85
+
86
+ reference_hash.each do |file_line, count|
87
+ puts " #{count.to_s.rjust(longest)} #{file_line}"
88
+ end
89
+
90
+ if !string_count.empty?
91
+ puts ""
92
+ puts "Duplicate strings"
93
+ puts "=============================="
94
+ puts ""
95
+
96
+ value_count = {}
97
+
98
+ string_count.each do |string, location_count_hash|
99
+ value_count[string] = location_count_hash.values.inject(&:+)
100
+ end
101
+
102
+ value_count = value_count.sort {|(k1, v1), (k2, v2)| v2 <=> v1 }.first(50)
103
+ longest = value_count.first[1].to_s.length
104
+
105
+ value_count.each do |string, c1|
106
+
107
+ puts " #{c1.to_s.rjust(longest)} #{string.inspect}"
108
+ string_count[string].sort {|(k1, v1), (k2, v2)| v2 <=> v1 }.each do |file_line, c2|
109
+ puts " #{c2.to_s.rjust(longest)} #{file_line}"
110
+ end
111
+ puts ""
112
+ end
113
+ end
114
+
115
+ end
116
+
117
+ def analyze
118
+ puts ""
119
+ puts "Analyzing Heap"
120
+ puts "=============="
121
+ default_key = "nil".freeze
122
+
123
+ # generation number is key, value is count
124
+ data = Hash.new {|h, k| h[k] = 0 }
125
+ mem = Hash.new {|h, k| h[k] = 0 }
126
+ total_count = 0
127
+ total_mem = 0
128
+
129
+ read do |parsed|
130
+ data[parsed["generation"] || 0] += 1
131
+ mem[parsed["generation"] || 0] += parsed["memsize"] || 0
132
+ end
133
+
134
+ data = data.sort {|(k1,v1), (k2,v2)| k1 <=> k2 }
135
+ max_length = [data.last[0].to_s.length, default_key.length].max
136
+ data.each do |generation, count|
137
+ generation = default_key if generation == 0
138
+ total_count += count
139
+ total_mem += mem[generation]
140
+ puts "Generation: #{ generation.to_s.rjust(max_length) } object count: #{ count }, mem: #{(mem[generation].to_f / 1024).round(1)} kb"
141
+ end
142
+
143
+ puts ""
144
+ puts "Heap total"
145
+ puts "=============="
146
+ puts "Generations (active): #{data.length}"
147
+ puts "Count: #{total_count}"
148
+ puts "Memory: #{(total_mem.to_f / 1024).round(1)} kb"
149
+ end
150
+ end
151
+ end
@@ -1,3 +1,3 @@
1
1
  module Heapy
2
- VERSION = "0.1.2"
2
+ VERSION = "0.1.3"
3
3
  end
data/scratch.rb CHANGED
@@ -1,16 +1,64 @@
1
- require 'objspace'
2
1
 
3
- ObjectSpace.trace_object_allocations_start
4
2
 
5
- array = []
6
- 10_000.times do |x|
7
- a = "#{x}_foo"
8
- array << a
9
- end
10
3
 
11
- # GC.start
4
+ $LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../lib")))
5
+
6
+ load File.expand_path(File.join(__FILE__, "../lib/heapy.rb"))
7
+
8
+
9
+ # class Foo
10
+ # end
11
+
12
+ # class Bar
13
+ # end
14
+
15
+ # class Baz
16
+ # end
17
+
18
+ # def run
19
+ # foo = Foo.new
20
+ # Heapy::Alive.trace_without_retain(foo)
21
+ # foo.singleton_class
22
+ # foo = nil
23
+
24
+ # bar = Bar.new
25
+ # Heapy::Alive.trace_without_retain(bar)
26
+ # bar.singleton_class
27
+ # bar = nil
12
28
 
13
- file_name = "/tmp/#{Time.now.to_f}-heap.dump"
14
- ObjectSpace.dump_all(output: File.open(file_name, 'w'))
29
+ # baz = Baz.new
30
+ # Heapy::Alive.trace_without_retain(baz)
31
+ # baz.singleton_class
32
+ # baz = nil
33
+ # nil
34
+ # end
15
35
 
16
- puts "bin/heapy read #{file_name}"
36
+ # Heapy::Alive.start_object_trace!
37
+
38
+ # run
39
+
40
+ # objects = Heapy::Alive.traced_objects.each do |obj|
41
+ # puts "Address: #{obj.address} #{obj.tracked_to_s}\n #{obj.raw_json_hash || "not found" }"
42
+ # end
43
+
44
+ Heapy::Alive.start_object_trace!
45
+
46
+ def run
47
+ foo = ""
48
+ Heapy::Alive.trace_without_retain(foo)
49
+ b = []
50
+ b << foo
51
+ b
52
+ end
53
+
54
+ c = run
55
+
56
+ objects = Heapy::Alive.traced_objects.each do |tracer|
57
+ puts "== Address: #{tracer.address} #{tracer.tracked_to_s}\n #{tracer.raw_json_hash || "not found" }"
58
+ # tracer.raw_json_hash["references"].each do |address|
59
+ # puts Heapy::Alive.address_to_object(address)
60
+ # end
61
+ Heapy::Alive.retained_by(tracer: tracer).each do |obj|
62
+ puts obj.inspect
63
+ end
64
+ end
@@ -0,0 +1,31 @@
1
+ arg = ARGV.shift
2
+
3
+ @fail_count = 0
4
+
5
+ require 'fileutils'
6
+ FileUtils.mkdir_p("tmp")
7
+
8
+ def run(file, fail_count: @fail_count)
9
+ cmd = "bundle exec ruby #{file}"
10
+ puts " $ #{ cmd }"
11
+ result = `#{cmd}`
12
+ @fail_count += 1 if result.match(/FAIL/)
13
+ puts " " + result
14
+ end
15
+
16
+ if arg.nil? || arg.downcase == "all"
17
+ puts "== Running all directories (#{`ruby -v`.strip})"
18
+ Dir.glob("weird_memory/**/*.rb").each do |file|
19
+ next if file == __FILE__
20
+ run(file)
21
+ end
22
+ else
23
+ puts "== Running examples in `#{arg}` directory (#{`ruby -v`.strip})"
24
+
25
+ Dir.glob("weird_memory/#{arg}/**/*.rb").each do |file|
26
+ run(file)
27
+ end
28
+ end
29
+
30
+ puts
31
+ puts "Total failed: #{@fail_count}"
@@ -0,0 +1,26 @@
1
+ $LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib")))
2
+
3
+ require 'heapy'
4
+
5
+ Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' })
6
+
7
+ def run
8
+ string = ""
9
+ Heapy::Alive.trace_without_retain(string)
10
+ string.singleton_class
11
+ string
12
+
13
+ return nil
14
+ end
15
+
16
+ run
17
+
18
+ alive_count = Heapy::Alive.traced_objects.select {|tracer|
19
+ tracer.object_retained?
20
+ }.length
21
+ # should return 0, no traced objects are returned
22
+
23
+ expected = 0
24
+ actual = alive_count
25
+ result = expected == actual ? "PASS" : "FAIL"
26
+ puts "#{result}: expected: #{expected}, actual: #{actual}"
@@ -0,0 +1,29 @@
1
+ $LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib")))
2
+
3
+ require 'heapy'
4
+
5
+ Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' })
6
+
7
+ class Runner
8
+
9
+ def run
10
+ string = ""
11
+ Heapy::Alive.trace_without_retain(string)
12
+ string.singleton_class
13
+ string
14
+
15
+ return nil
16
+ end
17
+ end
18
+
19
+ Runner.new.run
20
+
21
+ alive_count = Heapy::Alive.traced_objects.select {|tracer|
22
+ tracer.object_retained?
23
+ }.length
24
+ # should return 0, no traced objects are returned
25
+
26
+ expected = 0
27
+ actual = alive_count
28
+ result = expected == actual ? "PASS" : "FAIL"
29
+ puts "#{result}: expected: #{expected}, actual: #{actual}"
@@ -0,0 +1,28 @@
1
+ $LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib")))
2
+
3
+ require 'heapy'
4
+
5
+ Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' })
6
+
7
+ def run
8
+ -> {
9
+ string = ""
10
+ Heapy::Alive.trace_without_retain(string)
11
+ string.singleton_class
12
+ string
13
+ }.call
14
+
15
+ return nil
16
+ end
17
+
18
+ run
19
+
20
+ alive_count = Heapy::Alive.traced_objects.select {|tracer|
21
+ tracer.object_retained?
22
+ }.length
23
+ # should return 0, no traced objects are returned
24
+
25
+ expected = 0
26
+ actual = alive_count
27
+ result = expected == actual ? "PASS" : "FAIL"
28
+ puts "#{result}: expected: #{expected}, actual: #{actual}"
@@ -0,0 +1,26 @@
1
+ $LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib")))
2
+
3
+ require 'heapy'
4
+
5
+ Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' })
6
+
7
+ def run
8
+ string = ""
9
+ Heapy::Alive.trace_without_retain(string)
10
+ string.singleton_class
11
+ string
12
+
13
+ return nil
14
+ end
15
+
16
+ -> { run }.call
17
+
18
+ alive_count = Heapy::Alive.traced_objects.select {|tracer|
19
+ tracer.object_retained?
20
+ }.length
21
+ # should return 0, no traced objects are returned
22
+
23
+ expected = 0
24
+ actual = alive_count
25
+ result = expected == actual ? "PASS" : "FAIL"
26
+ puts "#{result}: expected: #{expected}, actual: #{actual}"
@@ -0,0 +1,27 @@
1
+ $LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib")))
2
+
3
+ require 'heapy'
4
+
5
+ Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' })
6
+
7
+ def run
8
+ string = ""
9
+ Heapy::Alive.trace_without_retain(string)
10
+ string.singleton_class.instance_eval do
11
+ end
12
+ string
13
+
14
+ return nil
15
+ end
16
+
17
+ run
18
+
19
+ alive_count = Heapy::Alive.traced_objects.select {|tracer|
20
+ tracer.object_retained?
21
+ }.length
22
+ # should return 0, no traced objects are returned
23
+
24
+ expected = 0
25
+ actual = alive_count
26
+ result = expected == actual ? "PASS" : "FAIL"
27
+ puts "#{result}: expected: #{expected}, actual: #{actual}"
@@ -0,0 +1,29 @@
1
+ $LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib")))
2
+
3
+ require 'heapy'
4
+
5
+ Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' })
6
+
7
+ class Runner
8
+ def run
9
+ string = ""
10
+ Heapy::Alive.trace_without_retain(string)
11
+ string.singleton_class.instance_eval do
12
+ end
13
+ string
14
+
15
+ return nil
16
+ end
17
+ end
18
+
19
+ Runner.new.run
20
+
21
+ alive_count = Heapy::Alive.traced_objects.select {|tracer|
22
+ tracer.object_retained?
23
+ }.length
24
+ # should return 0, no traced objects are returned
25
+
26
+ expected = 0
27
+ actual = alive_count
28
+ result = expected == actual ? "PASS" : "FAIL"
29
+ puts "#{result}: expected: #{expected}, actual: #{actual}"
@@ -0,0 +1,29 @@
1
+ $LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib")))
2
+
3
+ require 'heapy'
4
+
5
+ Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' })
6
+
7
+ def run
8
+ -> {
9
+ string = ""
10
+ Heapy::Alive.trace_without_retain(string)
11
+ string.singleton_class.instance_eval do
12
+ end
13
+ string
14
+ }.call
15
+
16
+ return nil
17
+ end
18
+
19
+ run
20
+
21
+ alive_count = Heapy::Alive.traced_objects.select {|tracer|
22
+ tracer.object_retained?
23
+ }.length
24
+ # should return 0, no traced objects are returned
25
+
26
+ expected = 0
27
+ actual = alive_count
28
+ result = expected == actual ? "PASS" : "FAIL"
29
+ puts "#{result}: expected: #{expected}, actual: #{actual}"
@@ -0,0 +1,27 @@
1
+ $LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib")))
2
+
3
+ require 'heapy'
4
+
5
+ Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' })
6
+
7
+ def run
8
+ string = ""
9
+ Heapy::Alive.trace_without_retain(string)
10
+ string.singleton_class.instance_eval do
11
+ end
12
+ string
13
+
14
+ return nil
15
+ end
16
+
17
+ -> { run }.call
18
+
19
+ alive_count = Heapy::Alive.traced_objects.select {|tracer|
20
+ tracer.object_retained?
21
+ }.length
22
+ # should return 0, no traced objects are returned
23
+
24
+ expected = 0
25
+ actual = alive_count
26
+ result = expected == actual ? "PASS" : "FAIL"
27
+ puts "#{result}: expected: #{expected}, actual: #{actual}"
@@ -0,0 +1,25 @@
1
+ $LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib")))
2
+
3
+ require 'heapy'
4
+
5
+ Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' })
6
+
7
+ def run
8
+ string = ""
9
+ Heapy::Alive.trace_without_retain(string)
10
+ string
11
+
12
+ return nil
13
+ end
14
+
15
+ run
16
+
17
+ alive_count = Heapy::Alive.traced_objects.select {|tracer|
18
+ tracer.object_retained?
19
+ }.length
20
+ # should return 0, no traced objects are returned
21
+
22
+ expected = 0
23
+ actual = alive_count
24
+ result = expected == actual ? "PASS" : "FAIL"
25
+ puts "#{result}: expected: #{expected}, actual: #{actual}"
@@ -0,0 +1,27 @@
1
+ $LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib")))
2
+
3
+ require 'heapy'
4
+
5
+ Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' })
6
+
7
+ class Runner
8
+ def run
9
+ string = ""
10
+ Heapy::Alive.trace_without_retain(string)
11
+ string
12
+
13
+ return nil
14
+ end
15
+ end
16
+
17
+ Runner.new.run
18
+
19
+ alive_count = Heapy::Alive.traced_objects.select {|tracer|
20
+ tracer.object_retained?
21
+ }.length
22
+ # should return 0, no traced objects are returned
23
+
24
+ expected = 0
25
+ actual = alive_count
26
+ result = expected == actual ? "PASS" : "FAIL"
27
+ puts "#{result}: expected: #{expected}, actual: #{actual}"
@@ -0,0 +1,26 @@
1
+ $LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib")))
2
+
3
+ require 'heapy'
4
+
5
+ Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' })
6
+
7
+ def run
8
+ -> {
9
+ string = ""
10
+ Heapy::Alive.trace_without_retain(string)
11
+ string
12
+ }.call
13
+ return nil
14
+ end
15
+
16
+ run
17
+
18
+ alive_count = Heapy::Alive.traced_objects.select {|tracer|
19
+ tracer.object_retained?
20
+ }.length
21
+ # should return 0, no traced objects are returned
22
+
23
+ expected = 0
24
+ actual = alive_count
25
+ result = expected == actual ? "PASS" : "FAIL"
26
+ puts "#{result}: expected: #{expected}, actual: #{actual}"
@@ -0,0 +1,25 @@
1
+ $LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib")))
2
+
3
+ require 'heapy'
4
+
5
+ Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' })
6
+
7
+ def run
8
+ string = ""
9
+ Heapy::Alive.trace_without_retain(string)
10
+ string
11
+
12
+ return nil
13
+ end
14
+
15
+ -> {run}.call
16
+
17
+ alive_count = Heapy::Alive.traced_objects.select {|tracer|
18
+ tracer.object_retained?
19
+ }.length
20
+ # should return 0, no traced objects are returned
21
+
22
+ expected = 0
23
+ actual = alive_count
24
+ result = expected == actual ? "PASS" : "FAIL"
25
+ puts "#{result}: expected: #{expected}, actual: #{actual}"
@@ -0,0 +1,28 @@
1
+ $LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib")))
2
+
3
+ require 'heapy'
4
+
5
+ Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' })
6
+
7
+ def run
8
+ array = 1.times.map {
9
+ string = ""
10
+ Heapy::Alive.trace_without_retain(string)
11
+ string
12
+ }
13
+ array = nil
14
+ return nil
15
+ end
16
+
17
+ run
18
+
19
+
20
+ alive_count = Heapy::Alive.traced_objects.select {|tracer|
21
+ tracer.object_retained?
22
+ }.length
23
+ # should return 0, no traced objects are returned
24
+
25
+ expected = 0
26
+ actual = alive_count
27
+ result = expected == actual ? "PASS" : "FAIL"
28
+ puts "#{result}: expected: #{expected}, actual: #{actual}"
@@ -0,0 +1,29 @@
1
+ $LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib")))
2
+
3
+ require 'heapy'
4
+
5
+ Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' })
6
+
7
+ class Runner
8
+ def run
9
+ array = 1.times.map {
10
+ string = ""
11
+ Heapy::Alive.trace_without_retain(string)
12
+ string
13
+ }
14
+ array = nil
15
+ return nil
16
+ end
17
+ end
18
+
19
+ Runner.new.run
20
+
21
+ alive_count = Heapy::Alive.traced_objects.select {|tracer|
22
+ tracer.object_retained?
23
+ }.length
24
+ # should return 0, no traced objects are returned
25
+
26
+ expected = 0
27
+ actual = alive_count
28
+ result = expected == actual ? "PASS" : "FAIL"
29
+ puts "#{result}: expected: #{expected}, actual: #{actual}"
@@ -0,0 +1,30 @@
1
+ $LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib")))
2
+
3
+ require 'heapy'
4
+
5
+ Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' })
6
+
7
+ def run
8
+ -> {
9
+ array = 1.times.map {
10
+ string = ""
11
+ Heapy::Alive.trace_without_retain(string)
12
+ string
13
+ }
14
+ array = nil
15
+ }.call
16
+
17
+ return nil
18
+ end
19
+
20
+ run
21
+
22
+ alive_count = Heapy::Alive.traced_objects.select {|tracer|
23
+ tracer.object_retained?
24
+ }.length
25
+ # should return 0, no traced objects are returned
26
+
27
+ expected = 0
28
+ actual = alive_count
29
+ result = expected == actual ? "PASS" : "FAIL"
30
+ puts "#{result}: expected: #{expected}, actual: #{actual}"
@@ -0,0 +1,29 @@
1
+ $LOAD_PATH.unshift(File.expand_path(File.join(__FILE__, "../../../lib")))
2
+
3
+ require 'heapy'
4
+
5
+
6
+ Heapy::Alive.start_object_trace!(heap_file: ENV.fetch('HEAP_FILE') { 'tmp/heap.json' })
7
+
8
+ def run
9
+ array = 1.times.map {
10
+ string = ""
11
+ Heapy::Alive.trace_without_retain(string)
12
+ string
13
+ }
14
+ array = nil
15
+ return nil
16
+ end
17
+
18
+ -> { run }.call
19
+
20
+ alive_count = Heapy::Alive.traced_objects.select {|tracer|
21
+ tracer.object_retained?
22
+ }.length
23
+ # should return 0, no traced objects are returned
24
+
25
+
26
+ expected = 0
27
+ actual = alive_count
28
+ result = expected == actual ? "PASS" : "FAIL"
29
+ puts "#{result}: expected: #{expected}, actual: #{actual}"
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: heapy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.2
4
+ version: 0.1.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - schneems
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2015-11-23 00:00:00.000000000 Z
11
+ date: 2017-09-07 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -73,9 +73,28 @@ files:
73
73
  - bin/heapy
74
74
  - heapy.gemspec
75
75
  - lib/heapy.rb
76
+ - lib/heapy/alive.rb
77
+ - lib/heapy/analyzer.rb
76
78
  - lib/heapy/version.rb
77
79
  - scratch.rb
78
80
  - trace.rb
81
+ - weird_memory/run.rb
82
+ - weird_memory/singleton_class/singleton_class.rb
83
+ - weird_memory/singleton_class/singleton_class_in_class.rb
84
+ - weird_memory/singleton_class/singleton_class_in_proc.rb
85
+ - weird_memory/singleton_class/singleton_class_method_in_proc.rb
86
+ - weird_memory/singleton_class_instance_eval/singleton_class_instance_eval.rb
87
+ - weird_memory/singleton_class_instance_eval/singleton_class_instance_eval_in_class.rb
88
+ - weird_memory/singleton_class_instance_eval/singleton_class_instance_eval_in_proc.rb
89
+ - weird_memory/singleton_class_instance_eval/singleton_class_instance_eval_method_in_proc.rb
90
+ - weird_memory/string/string.rb
91
+ - weird_memory/string/string_in_class.rb
92
+ - weird_memory/string/string_in_proc.rb
93
+ - weird_memory/string/string_method_in_proc.rb
94
+ - weird_memory/times_map/times_map.rb
95
+ - weird_memory/times_map/times_map_in_class.rb
96
+ - weird_memory/times_map/times_map_in_proc.rb
97
+ - weird_memory/times_map/times_map_method_in_proc.rb
79
98
  homepage: https://github.com/schneems/heapy
80
99
  licenses:
81
100
  - MIT
@@ -96,7 +115,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
96
115
  version: '0'
97
116
  requirements: []
98
117
  rubyforge_project:
99
- rubygems_version: 2.5.0
118
+ rubygems_version: 2.6.13
100
119
  signing_key:
101
120
  specification_version: 4
102
121
  summary: Inspects Ruby heap dumps