heapy 0.1.2 → 0.1.3

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.
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