heapy 0.1.4 → 0.2.0
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 +4 -4
- data/.github/workflows/check_changelog.yml +13 -0
- data/.gitignore +1 -0
- data/CHANGELOG.md +8 -0
- data/README.md +38 -4
- data/bin/heapy +1 -2
- data/heapy.gemspec +4 -4
- data/lib/heapy.rb +90 -55
- data/lib/heapy/analyzer.rb +15 -6
- data/lib/heapy/diff.rb +105 -0
- data/lib/heapy/version.rb +1 -1
- metadata +30 -35
- data/lib/heapy/alive.rb +0 -269
- data/scratch.rb +0 -64
- data/weird_memory/run.rb +0 -31
- data/weird_memory/singleton_class/singleton_class.rb +0 -26
- data/weird_memory/singleton_class/singleton_class_in_class.rb +0 -29
- data/weird_memory/singleton_class/singleton_class_in_proc.rb +0 -28
- data/weird_memory/singleton_class/singleton_class_method_in_proc.rb +0 -26
- data/weird_memory/singleton_class_instance_eval/singleton_class_instance_eval.rb +0 -27
- data/weird_memory/singleton_class_instance_eval/singleton_class_instance_eval_in_class.rb +0 -29
- data/weird_memory/singleton_class_instance_eval/singleton_class_instance_eval_in_proc.rb +0 -29
- data/weird_memory/singleton_class_instance_eval/singleton_class_instance_eval_method_in_proc.rb +0 -27
- data/weird_memory/string/string.rb +0 -25
- data/weird_memory/string/string_in_class.rb +0 -27
- data/weird_memory/string/string_in_proc.rb +0 -26
- data/weird_memory/string/string_method_in_proc.rb +0 -25
- data/weird_memory/times_map/times_map.rb +0 -28
- data/weird_memory/times_map/times_map_in_class.rb +0 -29
- data/weird_memory/times_map/times_map_in_proc.rb +0 -30
- data/weird_memory/times_map/times_map_method_in_proc.rb +0 -29
data/lib/heapy/version.rb
CHANGED
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.
|
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:
|
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
|
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
|
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
|
-
- ".
|
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: '
|
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
|
-
|
118
|
-
|
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: []
|
data/lib/heapy/alive.rb
DELETED
@@ -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
|