memdump 0.1.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 +7 -0
- data/.gitignore +9 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +123 -0
- data/Rakefile +10 -0
- data/bin/memdump +2 -0
- data/lib/memdump.rb +5 -0
- data/lib/memdump/cleanup_references.rb +19 -0
- data/lib/memdump/cli.rb +134 -0
- data/lib/memdump/convert_to_gml.rb +47 -0
- data/lib/memdump/diff.rb +44 -0
- data/lib/memdump/json_dump.rb +23 -0
- data/lib/memdump/remove_node.rb +43 -0
- data/lib/memdump/replace_class_address_by_name.rb +25 -0
- data/lib/memdump/root_of.rb +35 -0
- data/lib/memdump/stats.rb +15 -0
- data/lib/memdump/subgraph_of.rb +26 -0
- data/lib/memdump/version.rb +3 -0
- data/memdump.gemspec +27 -0
- metadata +136 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: bb8daaffee80155c4227920118ed3793e3afe4ab
|
4
|
+
data.tar.gz: b57f30d96b9e35d8519d800cbca852ef6995d965
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 84b9700c70b49d35b7c7910e1a047d4f1fd9aba7161888090bc3190e6e983a835b9417363376c3eabe648763eb537e57d89726440f9b8ac022b0920be60bd2fd
|
7
|
+
data.tar.gz: 206a0adbfd4c9f3b8d10f21c0ad07f911384977f79dee4a22e01af9c3710ab6e430e6f4ade72a37655e24eef2137025cec050dce232d2dacb0db75d1ae8f86e3
|
data/.gitignore
ADDED
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2016 Sylvain Joyeux
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,123 @@
|
|
1
|
+
# Memdump
|
2
|
+
|
3
|
+
Memdump is a set of (basic) tools to create and manipulate Ruby object dumps.
|
4
|
+
|
5
|
+
Since Ruby 2.1, ObjectSpace can be dumped in a JSON file that represents all
|
6
|
+
allocated objects and their relationships. It is a gold mine of information if
|
7
|
+
you want to understand why your application has that many objects and/or a
|
8
|
+
memory leak.
|
9
|
+
|
10
|
+
Processing methods are available as a library, or using the `memdump`
|
11
|
+
command-line tool. Just run `memdump help` for a summary of operations.
|
12
|
+
|
13
|
+
**NOTE** running memdump under jruby really reduces processing times... If you're using rbenv, just do
|
14
|
+
|
15
|
+
```
|
16
|
+
rbenv shell jruby-9.0.5.0
|
17
|
+
```
|
18
|
+
|
19
|
+
in the shell where you run the memdump commands.
|
20
|
+
|
21
|
+
## Installation
|
22
|
+
|
23
|
+
Add this line to your application's Gemfile:
|
24
|
+
|
25
|
+
```ruby
|
26
|
+
gem 'memdump'
|
27
|
+
```
|
28
|
+
|
29
|
+
And then execute:
|
30
|
+
|
31
|
+
$ bundle
|
32
|
+
|
33
|
+
Or install it yourself as:
|
34
|
+
|
35
|
+
$ gem install memdump
|
36
|
+
|
37
|
+
|
38
|
+
## Creating a memory dump
|
39
|
+
|
40
|
+
### Using rbtrace
|
41
|
+
|
42
|
+
The memdump command-line tool can connect to a process where
|
43
|
+
the [excellent rbtrace](https://github.com/tmm1/rbtrace) has been required. Just
|
44
|
+
start your Ruby application with `-r rbtrace`, e.g.
|
45
|
+
|
46
|
+
```
|
47
|
+
ruby -rrbtrace -S syskit run
|
48
|
+
```
|
49
|
+
|
50
|
+
and find out the process PID using e.g. top or ps (in the following, I assume
|
51
|
+
that the PID is 1234)
|
52
|
+
|
53
|
+
Memory dumps are then created with
|
54
|
+
|
55
|
+
```
|
56
|
+
memdump dump 1234 /tmp/mydump
|
57
|
+
```
|
58
|
+
|
59
|
+
Since `dump_all` requires very long, the rbtrace client will return before the
|
60
|
+
end of the dump with `*** timed out waiting for eval response`. Check your
|
61
|
+
application's output for a line saying `sendto(14): No such file or directory
|
62
|
+
[detaching]`
|
63
|
+
|
64
|
+
Additionally, you might want to enable allocation tracing, which adds to the
|
65
|
+
dump the line/file of the point where the object got allocated but is also very
|
66
|
+
costly from a performance point of view, do
|
67
|
+
|
68
|
+
```
|
69
|
+
memdump enable-allocation-trace 1234
|
70
|
+
```
|
71
|
+
|
72
|
+
### Manually
|
73
|
+
|
74
|
+
It is sometimes more beneficial to do the dumps in specific places
|
75
|
+
in your application, something the rbtrace method does not allow you to do. In
|
76
|
+
this case, create memory dumps by calling `ObjectSpace.dump_all`
|
77
|
+
|
78
|
+
~~~ ruby
|
79
|
+
require 'objspace'
|
80
|
+
File.open('/path/to/dump/file', 'w') do |io|
|
81
|
+
ObjectSpace.dump_all(output: io)
|
82
|
+
end
|
83
|
+
~~~
|
84
|
+
|
85
|
+
Allocation tracing is enabled with
|
86
|
+
|
87
|
+
~~~ ruby
|
88
|
+
require 'objspace'
|
89
|
+
ObjectSpace.trace_objects_allocation_start
|
90
|
+
~~~
|
91
|
+
|
92
|
+
## Analyzing the dump
|
93
|
+
|
94
|
+
The first thing you will probably want to do is to run the replace-class command
|
95
|
+
on the dump. It replaces the class attribute, which in the original dump is the
|
96
|
+
reference to the class object, by the class name. This makes reading the dump a
|
97
|
+
lot easier.
|
98
|
+
|
99
|
+
```
|
100
|
+
memdump replace-class /tmp/mydump
|
101
|
+
```
|
102
|
+
|
103
|
+
The most basic analysis is done by running **stats**, which outputs the object
|
104
|
+
count by class. For memory leaks, the **diff** command allows you to output the
|
105
|
+
part of the graph that involves new objects (removing the
|
106
|
+
"old-and-not-referred-to-by-new")
|
107
|
+
|
108
|
+
Beyond that, I usually go back and forth between the memory dump and
|
109
|
+
[gephi](http://gephi.org), a graph analysis application. the **gml** command
|
110
|
+
allows to convert the memory dump into a graph format that gephi can import.
|
111
|
+
From there, use gephi's layouting and filtering algorithms to get an idea of the
|
112
|
+
most likely objects. Then, you can "massage" the dump using the **root_of**,
|
113
|
+
**subgraph_of** and **remove-node** commands to narrow the dump to its most useful
|
114
|
+
parts.
|
115
|
+
|
116
|
+
## Contributing
|
117
|
+
|
118
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/doudou/memdump.
|
119
|
+
|
120
|
+
## License
|
121
|
+
|
122
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
123
|
+
|
data/Rakefile
ADDED
data/bin/memdump
ADDED
data/lib/memdump.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module MemDump
|
2
|
+
def self.cleanup_references(dump)
|
3
|
+
addresses = Set.new
|
4
|
+
records = Array.new
|
5
|
+
dump.each_record do |r|
|
6
|
+
addr = (r['address'] || r['root'])
|
7
|
+
addresses << addr
|
8
|
+
records << r
|
9
|
+
end
|
10
|
+
|
11
|
+
records.each do |r|
|
12
|
+
if references = r['references']
|
13
|
+
references.delete_if { |r| !addresses.include?(r) }
|
14
|
+
end
|
15
|
+
end
|
16
|
+
records
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
data/lib/memdump/cli.rb
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
require 'thor'
|
2
|
+
require 'pathname'
|
3
|
+
require 'memdump/json_dump'
|
4
|
+
|
5
|
+
module MemDump
|
6
|
+
class CLI < Thor
|
7
|
+
desc 'enable-allocation-trace PID', 'enable the tracing of allocations on a running process'
|
8
|
+
def enable_allocation_trace(pid)
|
9
|
+
system('rbtrace', '-p', pid.to_s, '-e', "require \"objspace\"; ObjectSpace.trace_object_allocations_start")
|
10
|
+
end
|
11
|
+
|
12
|
+
desc 'dump PID FILE', 'generate a dump file from a running process'
|
13
|
+
def dump(pid, file)
|
14
|
+
file = File.expand_path(file)
|
15
|
+
system('rbtrace', '-p', pid.to_s, '-e', "require \"objspace\"; File.open(\"#{file}\", 'w') { |io| ObjectSpace.dump_all(output: io) }")
|
16
|
+
end
|
17
|
+
|
18
|
+
desc 'diff SOURCE TARGET OUTPUT', 'generate a memory dump that contains the objects in TARGET not in SOURCE, and all their parents'
|
19
|
+
def diff(source, target, output)
|
20
|
+
require 'memdump/diff'
|
21
|
+
|
22
|
+
STDOUT.sync = true
|
23
|
+
from = MemDump::JSONDump.new(Pathname.new(source))
|
24
|
+
to = MemDump::JSONDump.new(Pathname.new(target))
|
25
|
+
records = MemDump.diff(from, to)
|
26
|
+
File.open(output, 'w') do |io|
|
27
|
+
records.each do |r|
|
28
|
+
io.puts JSON.dump(r)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
desc 'gml DUMP GML', 'converts a memory dump into a graph in the GML format (for processing by e.g. gephi)'
|
34
|
+
def gml(dump_path, gml_path = nil)
|
35
|
+
require 'memdump/convert_to_gml'
|
36
|
+
|
37
|
+
STDOUT.sync = true
|
38
|
+
dump_path = Pathname.new(dump_path)
|
39
|
+
if gml_path
|
40
|
+
gml_path = Pathname.new(gml_path)
|
41
|
+
else
|
42
|
+
gml_path = dump_path.sub_ext('.gml')
|
43
|
+
end
|
44
|
+
|
45
|
+
dump = MemDump::JSONDump.new(dump_path)
|
46
|
+
gml_path.open('w') do |io|
|
47
|
+
MemDump.convert_to_gml(dump, io)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
desc "subgraph_of DUMP ADDRESS", "traces all objects that are reachable from the given object"
|
52
|
+
option :max_depth, desc: 'depth of the subgraph to generate', type: :numeric, default: Float::INFINITY
|
53
|
+
def subgraph_of(dump, address)
|
54
|
+
require 'memdump/subgraph_of'
|
55
|
+
|
56
|
+
STDOUT.sync = true
|
57
|
+
dump = MemDump::JSONDump.new(Pathname.new(dump))
|
58
|
+
MemDump.subgraph_of(dump, address, max_depth: options[:max_depth]).each do |r|
|
59
|
+
puts JSON.dump(r)
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
desc "root_of DUMP ADDRESS", "traces the object with the given address to the root that's holding it alive"
|
64
|
+
def root_of(dump, address)
|
65
|
+
require 'memdump/root_of'
|
66
|
+
|
67
|
+
STDOUT.sync = true
|
68
|
+
dump = MemDump::JSONDump.new(Pathname.new(dump))
|
69
|
+
MemDump.root_of(dump, address).each do |r|
|
70
|
+
puts JSON.dump(r)
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
desc 'replace-class DUMP OUTPUT', 'replaces the class address by the class name'
|
75
|
+
option :add_ref, desc: 'whether a reference to the class object should be added', type: :boolean, default: false
|
76
|
+
def replace_class(dump_path, output_path = nil)
|
77
|
+
require 'memdump/replace_class_address_by_name'
|
78
|
+
|
79
|
+
STDOUT.sync = true
|
80
|
+
dump_path = Pathname.new(dump_path)
|
81
|
+
output_path =
|
82
|
+
if output_path then Pathname.new(output_path)
|
83
|
+
else dump_path
|
84
|
+
end
|
85
|
+
dump = MemDump::JSONDump.new(dump_path)
|
86
|
+
result = MemDump.replace_class_address_by_name(dump, add_reference_to_class: options[:add_ref])
|
87
|
+
output_path.open('w') do |io|
|
88
|
+
result.each do |r|
|
89
|
+
io.puts JSON.dump(r)
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
desc 'cleanup-refs DUMP OUTPUT', "removes references to deleted objects"
|
95
|
+
def cleanup_refs(dump, output)
|
96
|
+
require 'memdump/cleanup_references'
|
97
|
+
|
98
|
+
STDOUT.sync = true
|
99
|
+
dump = MemDump::JSONDump.new(Pathname.new(dump))
|
100
|
+
cleaned = MemDump.cleanup_references(dump)
|
101
|
+
Pathname.new(output).open('w') do |io|
|
102
|
+
cleaned.each do |r|
|
103
|
+
io.puts JSON.dump(r)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
desc 'remove-node DUMP NODE', 'remove all objects that are kept alive by the given node'
|
109
|
+
def remove_node(dump, node_id)
|
110
|
+
require 'memdump/remove_node'
|
111
|
+
|
112
|
+
STDOUT.sync = true
|
113
|
+
dump = MemDump::JSONDump.new(Pathname.new(dump))
|
114
|
+
cleaned = MemDump.remove_node(dump, node_id)
|
115
|
+
cleaned.each do |r|
|
116
|
+
STDOUT.puts JSON.dump(r)
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
desc 'stats DUMP', 'give statistics on the objects present in the dump'
|
121
|
+
def stats(dump)
|
122
|
+
require 'pp'
|
123
|
+
require 'memdump/stats'
|
124
|
+
dump = MemDump::JSONDump.new(Pathname.new(dump))
|
125
|
+
unknown, by_type = MemDump.stats(dump)
|
126
|
+
puts "#{unknown} objects without a known type"
|
127
|
+
by_type.sort_by { |n, v| v }.reverse.each do |n, v|
|
128
|
+
puts "#{n}: #{v}"
|
129
|
+
end
|
130
|
+
end
|
131
|
+
end
|
132
|
+
end
|
133
|
+
|
134
|
+
|
@@ -0,0 +1,47 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module MemDump
|
4
|
+
def self.convert_to_gml(dump, io)
|
5
|
+
nodes = dump.each_record.map do |row|
|
6
|
+
if row['class_address'] # transformed with replace_class_address_by_name
|
7
|
+
name = row['class']
|
8
|
+
else
|
9
|
+
name = row['struct'] || row['root'] || row['type']
|
10
|
+
end
|
11
|
+
|
12
|
+
address = row['address'] || row['root']
|
13
|
+
refs = Hash.new
|
14
|
+
if row_refs = row['references']
|
15
|
+
row_refs.each { |r| refs[r] = nil }
|
16
|
+
end
|
17
|
+
|
18
|
+
[address, refs, name]
|
19
|
+
end
|
20
|
+
|
21
|
+
io.puts "graph"
|
22
|
+
io.puts "["
|
23
|
+
known_addresses = Set.new
|
24
|
+
nodes.each do |address, refs, name|
|
25
|
+
known_addresses << address
|
26
|
+
io.puts " node"
|
27
|
+
io.puts " ["
|
28
|
+
io.puts " id #{address}"
|
29
|
+
io.puts " label \"#{name}\""
|
30
|
+
io.puts " ]"
|
31
|
+
end
|
32
|
+
|
33
|
+
nodes.each do |address, refs, _|
|
34
|
+
refs.each do |ref_address, ref_label|
|
35
|
+
io.puts " edge"
|
36
|
+
io.puts " ["
|
37
|
+
io.puts " source #{address}"
|
38
|
+
io.puts " target #{ref_address}"
|
39
|
+
if ref_label
|
40
|
+
io.puts " label \"#{ref_label}\""
|
41
|
+
end
|
42
|
+
io.puts " ]"
|
43
|
+
end
|
44
|
+
end
|
45
|
+
io.puts "]"
|
46
|
+
end
|
47
|
+
end
|
data/lib/memdump/diff.rb
ADDED
@@ -0,0 +1,44 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
module MemDump
|
4
|
+
def self.diff(from, to)
|
5
|
+
from_objects = Set.new
|
6
|
+
from.each_record { |r| from_objects << (r['address'] || r['root']) }
|
7
|
+
puts "#{from_objects.size} objects found in source dump"
|
8
|
+
|
9
|
+
selected_records = Hash.new
|
10
|
+
remaining_records = Array.new
|
11
|
+
to.each_record do |r|
|
12
|
+
address = (r['address'] || r['root'])
|
13
|
+
if !from_objects.include?(address)
|
14
|
+
selected_records[address] = r
|
15
|
+
r['only_in_target'] = 1
|
16
|
+
else
|
17
|
+
remaining_records << r
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
total = remaining_records.size + selected_records.size
|
22
|
+
count = 0
|
23
|
+
while selected_records.size != count
|
24
|
+
count = selected_records.size
|
25
|
+
puts "#{count}/#{total} records selected so far"
|
26
|
+
remaining_records.delete_if do |r|
|
27
|
+
address = (r['address'] || r['root'])
|
28
|
+
references = r['references']
|
29
|
+
|
30
|
+
if references && references.any? { |r| selected_records.has_key?(r) }
|
31
|
+
selected_records[address] = r
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
puts "#{count}/#{total} records selected"
|
36
|
+
|
37
|
+
selected_records.each_value do |r|
|
38
|
+
if references = r['references']
|
39
|
+
references.delete_if { |a| !selected_records.has_key?(a) }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
selected_records.each_value
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'json'
|
2
|
+
module MemDump
|
3
|
+
class JSONDump
|
4
|
+
def initialize(filename)
|
5
|
+
@filename = filename
|
6
|
+
end
|
7
|
+
|
8
|
+
def each_record
|
9
|
+
return enum_for(__method__) if !block_given?
|
10
|
+
|
11
|
+
if @cached_entries
|
12
|
+
@cached_entries.each(&proc)
|
13
|
+
else
|
14
|
+
@filename.open do |f|
|
15
|
+
f.each_line do |line|
|
16
|
+
yield JSON.parse(line)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
@@ -0,0 +1,43 @@
|
|
1
|
+
module MemDump
|
2
|
+
def self.remove_node(dump, removed_node)
|
3
|
+
remaining_records = Hash.new
|
4
|
+
non_roots = Set.new
|
5
|
+
dump.each_record do |r|
|
6
|
+
address = (r['address'] || r['root'])
|
7
|
+
remaining_records[address] = r
|
8
|
+
|
9
|
+
if refs = r['references']
|
10
|
+
refs.each do |ref_address|
|
11
|
+
non_roots << ref_address
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
roots = remaining_records.each_key.
|
17
|
+
find_all { |a| !non_roots.include?(a) }
|
18
|
+
|
19
|
+
queue = roots.dup
|
20
|
+
selected_records = Hash.new
|
21
|
+
while !queue.empty?
|
22
|
+
address = queue.shift
|
23
|
+
next if address == removed_node
|
24
|
+
|
25
|
+
if record = remaining_records.delete(address)
|
26
|
+
selected_records[address] = record
|
27
|
+
if refs = record['references']
|
28
|
+
refs.each do |ref_address|
|
29
|
+
queue << ref_address
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
selected_records.values.reverse.map do |r|
|
36
|
+
if refs = r['references']
|
37
|
+
refs.delete_if { |a| !selected_records.has_key?(a) }
|
38
|
+
end
|
39
|
+
r
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module MemDump
|
2
|
+
# Replace the address in the 'class' attribute by the class name
|
3
|
+
def self.replace_class_address_by_name(dump, add_reference_to_class: false)
|
4
|
+
class_names = Hash.new
|
5
|
+
dump.each_record do |row|
|
6
|
+
if row['type'] == 'CLASS' || row['type'] == 'MODULE'
|
7
|
+
class_names[row['address']] = row['name']
|
8
|
+
end
|
9
|
+
end
|
10
|
+
|
11
|
+
dump.each_record.map do |r|
|
12
|
+
if klass = r['class']
|
13
|
+
r['class'] = class_names[klass] || klass
|
14
|
+
r['class_address'] = klass
|
15
|
+
if add_reference_to_class
|
16
|
+
(r['references'] ||= Array.new) << klass
|
17
|
+
end
|
18
|
+
end
|
19
|
+
if r['type'] == 'ICLASS'
|
20
|
+
r['class'] = "I(#{r['class']})"
|
21
|
+
end
|
22
|
+
r
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
module MemDump
|
2
|
+
def self.root_of(dump, root_address)
|
3
|
+
remaining_records = Array.new
|
4
|
+
selected_records = Hash.new
|
5
|
+
selected_root = root_address
|
6
|
+
dump.each_record do |r|
|
7
|
+
address = (r['address'] || r['root'])
|
8
|
+
if selected_root == address
|
9
|
+
selected_records[address] = r
|
10
|
+
selected_root = nil;
|
11
|
+
else
|
12
|
+
remaining_records << r
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
count = 0
|
17
|
+
while count != selected_records.size
|
18
|
+
count = selected_records.size
|
19
|
+
remaining_records.delete_if do |r|
|
20
|
+
references = r['references']
|
21
|
+
if references && references.any? { |a| selected_records.has_key?(a) }
|
22
|
+
address = (r['address'] || r['root'])
|
23
|
+
selected_records[address] = r
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
selected_records.values.reverse.each do |r|
|
29
|
+
if refs = r['references']
|
30
|
+
refs.delete_if { |a| !selected_records.has_key?(a) }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module MemDump
|
2
|
+
def self.stats(memdump)
|
3
|
+
unknown_class = 0
|
4
|
+
by_class = Hash.new(0)
|
5
|
+
memdump.each_record do |r|
|
6
|
+
if klass = (r['class'] || r['type'] || r['root'])
|
7
|
+
by_class[klass] += 1
|
8
|
+
else
|
9
|
+
unknown_class += 1
|
10
|
+
end
|
11
|
+
end
|
12
|
+
return unknown_class, by_class
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module MemDump
|
2
|
+
def self.subgraph_of(dump, root_address, max_depth: Float::INFINITY)
|
3
|
+
remaining_records = Hash.new
|
4
|
+
dump.each_record do |r|
|
5
|
+
address = (r['address'] || r['root'])
|
6
|
+
remaining_records[address] = r
|
7
|
+
end
|
8
|
+
|
9
|
+
selected_records = Hash.new
|
10
|
+
queue = [[root_address, 0]]
|
11
|
+
while !queue.empty?
|
12
|
+
address, depth = queue.shift
|
13
|
+
if record = remaining_records.delete(address)
|
14
|
+
selected_records[address] = record
|
15
|
+
if (depth < max_depth) && (refs = record['references'])
|
16
|
+
refs.each do |ref_address|
|
17
|
+
queue << [ref_address, depth + 1]
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
selected_records.values
|
24
|
+
end
|
25
|
+
end
|
26
|
+
|
data/memdump.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'memdump/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "memdump"
|
8
|
+
spec.version = Memdump::VERSION
|
9
|
+
spec.authors = ["Sylvain Joyeux"]
|
10
|
+
spec.email = ["sylvain.joyeux@m4x.org"]
|
11
|
+
|
12
|
+
spec.summary = %q{Tools to manipulate Ruby 2.1+ memory dumps}
|
13
|
+
spec.description = %q{}
|
14
|
+
spec.homepage = "https://github.com/doudou/memdump"
|
15
|
+
spec.license = "MIT"
|
16
|
+
|
17
|
+
spec.files = `git ls-files -z`.split("\x0").reject { |f| f.match(%r{^(test|spec|features)/}) }
|
18
|
+
spec.bindir = "bin"
|
19
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
20
|
+
spec.require_paths = ["lib"]
|
21
|
+
|
22
|
+
spec.add_dependency 'thor'
|
23
|
+
spec.add_dependency 'rbtrace'
|
24
|
+
spec.add_development_dependency "bundler", "~> 1.11"
|
25
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
26
|
+
spec.add_development_dependency "minitest", "~> 5.0"
|
27
|
+
end
|
metadata
ADDED
@@ -0,0 +1,136 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: memdump
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.0
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- Sylvain Joyeux
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2016-04-25 00:00:00.000000000 Z
|
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'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rbtrace
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - ">="
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :runtime
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - ">="
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: bundler
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '1.11'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '1.11'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rake
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '10.0'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '10.0'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: minitest
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: '5.0'
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: '5.0'
|
83
|
+
description: ''
|
84
|
+
email:
|
85
|
+
- sylvain.joyeux@m4x.org
|
86
|
+
executables:
|
87
|
+
- memdump
|
88
|
+
extensions: []
|
89
|
+
extra_rdoc_files: []
|
90
|
+
files:
|
91
|
+
- ".gitignore"
|
92
|
+
- ".travis.yml"
|
93
|
+
- Gemfile
|
94
|
+
- LICENSE.txt
|
95
|
+
- README.md
|
96
|
+
- Rakefile
|
97
|
+
- bin/memdump
|
98
|
+
- lib/memdump.rb
|
99
|
+
- lib/memdump/cleanup_references.rb
|
100
|
+
- lib/memdump/cli.rb
|
101
|
+
- lib/memdump/convert_to_gml.rb
|
102
|
+
- lib/memdump/diff.rb
|
103
|
+
- lib/memdump/json_dump.rb
|
104
|
+
- lib/memdump/remove_node.rb
|
105
|
+
- lib/memdump/replace_class_address_by_name.rb
|
106
|
+
- lib/memdump/root_of.rb
|
107
|
+
- lib/memdump/stats.rb
|
108
|
+
- lib/memdump/subgraph_of.rb
|
109
|
+
- lib/memdump/version.rb
|
110
|
+
- memdump.gemspec
|
111
|
+
homepage: https://github.com/doudou/memdump
|
112
|
+
licenses:
|
113
|
+
- MIT
|
114
|
+
metadata: {}
|
115
|
+
post_install_message:
|
116
|
+
rdoc_options: []
|
117
|
+
require_paths:
|
118
|
+
- lib
|
119
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
120
|
+
requirements:
|
121
|
+
- - ">="
|
122
|
+
- !ruby/object:Gem::Version
|
123
|
+
version: '0'
|
124
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
125
|
+
requirements:
|
126
|
+
- - ">="
|
127
|
+
- !ruby/object:Gem::Version
|
128
|
+
version: '0'
|
129
|
+
requirements: []
|
130
|
+
rubyforge_project:
|
131
|
+
rubygems_version: 2.2.3
|
132
|
+
signing_key:
|
133
|
+
specification_version: 4
|
134
|
+
summary: Tools to manipulate Ruby 2.1+ memory dumps
|
135
|
+
test_files: []
|
136
|
+
has_rdoc:
|