heapy 0.1.4 → 0.2.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (31) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/check_changelog.yml +13 -0
  3. data/.gitignore +1 -0
  4. data/CHANGELOG.md +8 -0
  5. data/README.md +38 -4
  6. data/bin/heapy +1 -2
  7. data/heapy.gemspec +4 -4
  8. data/lib/heapy.rb +90 -55
  9. data/lib/heapy/analyzer.rb +15 -6
  10. data/lib/heapy/diff.rb +105 -0
  11. data/lib/heapy/version.rb +1 -1
  12. metadata +30 -35
  13. data/lib/heapy/alive.rb +0 -269
  14. data/scratch.rb +0 -64
  15. data/weird_memory/run.rb +0 -31
  16. data/weird_memory/singleton_class/singleton_class.rb +0 -26
  17. data/weird_memory/singleton_class/singleton_class_in_class.rb +0 -29
  18. data/weird_memory/singleton_class/singleton_class_in_proc.rb +0 -28
  19. data/weird_memory/singleton_class/singleton_class_method_in_proc.rb +0 -26
  20. data/weird_memory/singleton_class_instance_eval/singleton_class_instance_eval.rb +0 -27
  21. data/weird_memory/singleton_class_instance_eval/singleton_class_instance_eval_in_class.rb +0 -29
  22. data/weird_memory/singleton_class_instance_eval/singleton_class_instance_eval_in_proc.rb +0 -29
  23. data/weird_memory/singleton_class_instance_eval/singleton_class_instance_eval_method_in_proc.rb +0 -27
  24. data/weird_memory/string/string.rb +0 -25
  25. data/weird_memory/string/string_in_class.rb +0 -27
  26. data/weird_memory/string/string_in_proc.rb +0 -26
  27. data/weird_memory/string/string_method_in_proc.rb +0 -25
  28. data/weird_memory/times_map/times_map.rb +0 -28
  29. data/weird_memory/times_map/times_map_in_class.rb +0 -29
  30. data/weird_memory/times_map/times_map_in_proc.rb +0 -30
  31. data/weird_memory/times_map/times_map_method_in_proc.rb +0 -29
@@ -1,3 +1,3 @@
1
1
  module Heapy
2
- VERSION = "0.1.4"
2
+ VERSION = "0.2.0"
3
3
  end
metadata CHANGED
@@ -1,41 +1,55 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: heapy
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.4
4
+ version: 0.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - schneems
8
- autorequire:
8
+ autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2018-07-25 00:00:00.000000000 Z
11
+ date: 2020-08-26 00:00:00.000000000 Z
12
12
  dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: thor
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
13
27
  - !ruby/object:Gem::Dependency
14
28
  name: bundler
15
29
  requirement: !ruby/object:Gem::Requirement
16
30
  requirements:
17
- - - "~>"
31
+ - - ">"
18
32
  - !ruby/object:Gem::Version
19
- version: '1.10'
33
+ version: '1'
20
34
  type: :development
21
35
  prerelease: false
22
36
  version_requirements: !ruby/object:Gem::Requirement
23
37
  requirements:
24
- - - "~>"
38
+ - - ">"
25
39
  - !ruby/object:Gem::Version
26
- version: '1.10'
40
+ version: '1'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: rake
29
43
  requirement: !ruby/object:Gem::Requirement
30
44
  requirements:
31
- - - "~>"
45
+ - - ">"
32
46
  - !ruby/object:Gem::Version
33
47
  version: '10.0'
34
48
  type: :development
35
49
  prerelease: false
36
50
  version_requirements: !ruby/object:Gem::Requirement
37
51
  requirements:
38
- - - "~>"
52
+ - - ">"
39
53
  - !ruby/object:Gem::Version
40
54
  version: '10.0'
41
55
  - !ruby/object:Gem::Dependency
@@ -60,7 +74,7 @@ executables:
60
74
  extensions: []
61
75
  extra_rdoc_files: []
62
76
  files:
63
- - ".DS_Store"
77
+ - ".github/workflows/check_changelog.yml"
64
78
  - ".gitignore"
65
79
  - ".rspec"
66
80
  - ".travis.yml"
@@ -73,50 +87,31 @@ files:
73
87
  - bin/heapy
74
88
  - heapy.gemspec
75
89
  - lib/heapy.rb
76
- - lib/heapy/alive.rb
77
90
  - lib/heapy/analyzer.rb
91
+ - lib/heapy/diff.rb
78
92
  - lib/heapy/version.rb
79
- - scratch.rb
80
93
  - 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
98
94
  homepage: https://github.com/schneems/heapy
99
95
  licenses:
100
96
  - MIT
101
97
  metadata: {}
102
- post_install_message:
98
+ post_install_message:
103
99
  rdoc_options: []
104
100
  require_paths:
105
101
  - lib
106
102
  required_ruby_version: !ruby/object:Gem::Requirement
107
103
  requirements:
108
- - - "~>"
104
+ - - ">="
109
105
  - !ruby/object:Gem::Version
110
- version: '2.3'
106
+ version: '0'
111
107
  required_rubygems_version: !ruby/object:Gem::Requirement
112
108
  requirements:
113
109
  - - ">="
114
110
  - !ruby/object:Gem::Version
115
111
  version: '0'
116
112
  requirements: []
117
- rubyforge_project:
118
- rubygems_version: 2.7.6
119
- signing_key:
113
+ rubygems_version: 3.1.2
114
+ signing_key:
120
115
  specification_version: 4
121
116
  summary: Inspects Ruby heap dumps
122
117
  test_files: []
@@ -1,269 +0,0 @@
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
data/scratch.rb DELETED
@@ -1,64 +0,0 @@
1
-
2
-
3
-
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
28
-
29
- # baz = Baz.new
30
- # Heapy::Alive.trace_without_retain(baz)
31
- # baz.singleton_class
32
- # baz = nil
33
- # nil
34
- # end
35
-
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