heapy 0.1.0 → 0.2.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +5 -5
- data/.github/workflows/check_changelog.yml +13 -0
- data/.gitignore +3 -0
- data/.travis.yml +1 -1
- data/CHANGELOG.md +23 -0
- data/README.md +79 -14
- data/Rakefile +1 -0
- data/bin/heapy +2 -3
- data/heapy.gemspec +4 -2
- data/lib/heapy.rb +78 -106
- data/lib/heapy/analyzer.rb +160 -0
- data/lib/heapy/diff.rb +105 -0
- data/lib/heapy/version.rb +1 -1
- data/trace.rb +3 -0
- metadata +31 -14
- data/.DS_Store +0 -0
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
|
-
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: a84bd36786d08056d5e172044cbbe30547ca5fc911aeee1a6a46905daef45c7d
|
4
|
+
data.tar.gz: b87cd42efd90097653dbe311955e66c45b961828e8c4538a2604d23f66bc8ba1
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: bc703b95887ec3343cf21fc75e68ade763a763c357b7ef4e3e6dcc94d18751559241a587ebb9160321dfb2a975e51707ac5a030ba450dd76b8cf3ba31f385660
|
7
|
+
data.tar.gz: b13485e91fdbe6922e7777c7a8acfd48d3be5433891e2e994d38fa6c6c59881ae6085e532e9b9a04e82d6a80a005ef73afbe684ee2efe27fcbc85a402aeb5a17
|
@@ -0,0 +1,13 @@
|
|
1
|
+
name: Check Changelog
|
2
|
+
|
3
|
+
on:
|
4
|
+
pull_request:
|
5
|
+
types: [opened, reopened, edited, synchronize]
|
6
|
+
jobs:
|
7
|
+
build:
|
8
|
+
runs-on: ubuntu-latest
|
9
|
+
steps:
|
10
|
+
- uses: actions/checkout@v1
|
11
|
+
- name: Check that CHANGELOG is touched
|
12
|
+
run: |
|
13
|
+
cat $GITHUB_EVENT_PATH | jq .pull_request.title | grep -i '\[\(\(changelog skip\)\|\(ci skip\)\)\]' || git diff remotes/origin/${{ github.base_ref }} --name-only | grep CHANGELOG.md
|
data/.gitignore
CHANGED
data/.travis.yml
CHANGED
data/CHANGELOG.md
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
## HEAD
|
2
|
+
|
3
|
+
## 0.2.0
|
4
|
+
|
5
|
+
- Heapy::Alive is removed (https://github.com/schneems/heapy/pull/27)
|
6
|
+
- New command `heapy diff` (https://github.com/schneems/heapy/pull/26)
|
7
|
+
- The read command now takes a --lines flag that can be used to limit output when showing generational data
|
8
|
+
|
9
|
+
## 0.1.4 - 2018-07-25
|
10
|
+
|
11
|
+
- Bundler is no longer required so heapy can now be used via a simple
|
12
|
+
gem install.
|
13
|
+
|
14
|
+
## 0.1.3 - 2017-09-07
|
15
|
+
|
16
|
+
- I'm really bad at keeping a log of changes on this project sorry
|
17
|
+
- Printing out the memory size for a generation
|
18
|
+
- Printing out the total number of objects for the heap in the summary
|
19
|
+
|
20
|
+
## 0.1.1 - 2015-10-15
|
21
|
+
|
22
|
+
- Less memory retention when parsing large heap dumps.
|
23
|
+
- Improved output format.
|
data/README.md
CHANGED
@@ -1,4 +1,5 @@
|
|
1
1
|
# Heapy (Ruby Heap Dump Inspector)
|
2
|
+
[![Help Contribute to Open Source](https://www.codetriage.com/schneems/heapy/badges/users.svg)](https://www.codetriage.com/schneems/heapy) ![Supports Ruby 2.3+](https://img.shields.io/badge/ruby-2.3+-green.svg)
|
2
3
|
|
3
4
|
A CLI for analyzing Ruby Heap dumps. Thanks to [Sam Saffron](http://samsaffron.com/archive/2015/03/31/debugging-memory-leaks-in-ruby) for the idea and initial code.
|
4
5
|
|
@@ -20,6 +21,34 @@ Or install it yourself as:
|
|
20
21
|
|
21
22
|
## Usage
|
22
23
|
|
24
|
+
### Diff 2 heap dumps
|
25
|
+
|
26
|
+
Run with two inputs to output the values of today.dump that are not present in yesterday.dump
|
27
|
+
|
28
|
+
```
|
29
|
+
$ heapy diff tmp/yesterday.dump tmp/today_morning.dump
|
30
|
+
Allocated STRING 9991 objects of size 399640/491264 (in bytes) at: scratch.rb:24
|
31
|
+
```
|
32
|
+
|
33
|
+
Run with three inputs to show the diff between the first two, but only if the objects are still retained in the third
|
34
|
+
|
35
|
+
```
|
36
|
+
$ heapy diff tmp/yesterday.dump tmp/today_morning.dump tmp/today_afternoon.dump
|
37
|
+
Retained STRING 9991 objects of size 399640/491264 (in bytes) at: scratch.rb:24
|
38
|
+
# ...
|
39
|
+
```
|
40
|
+
|
41
|
+
Pass in the name of an output file and the objects present in today.dump that aren't in yesterday.dump will be written to that file
|
42
|
+
|
43
|
+
```
|
44
|
+
$ heapy diff tmp/yesterday.dump tmp/today.dump --output_diff=output.json
|
45
|
+
Allocated STRING 9991 objects of size 399640/491264 (in bytes) at: scratch.rb:24
|
46
|
+
# ...
|
47
|
+
Writing heap dump diff to output.json
|
48
|
+
```
|
49
|
+
|
50
|
+
### Read a Heap Dump
|
51
|
+
|
23
52
|
Step 1) Generate a heap dump. You could [do this manually](http://samsaffron.com/archive/2015/03/31/debugging-memory-leaks-in-ruby). Or you can use a tool like [derailed_benchmarks](https://github.com/schneems/derailed_benchmarks)
|
24
53
|
|
25
54
|
Step 2) Once you've got the heap dump, you can analyze it using this CLI:
|
@@ -27,31 +56,68 @@ Step 2) Once you've got the heap dump, you can analyze it using this CLI:
|
|
27
56
|
```
|
28
57
|
$ heapy read tmp/2015-10-01T10:18:59-05:00-heap.dump
|
29
58
|
|
30
|
-
Generation:
|
31
|
-
Generation:
|
32
|
-
Generation:
|
33
|
-
Generation:
|
34
|
-
Generation:
|
35
|
-
Generation:
|
59
|
+
Generation: nil object count: 209191
|
60
|
+
Generation: 14 object count: 407
|
61
|
+
Generation: 15 object count: 638
|
62
|
+
Generation: 16 object count: 748
|
63
|
+
Generation: 17 object count: 1023
|
64
|
+
Generation: 18 object count: 805
|
36
65
|
# ...
|
37
66
|
```
|
38
67
|
|
39
|
-
|
68
|
+
NOTE: The reason you may be getting a "nil" generation is these objects were loaded into memory before your code began tracking the allocations. To ensure all allocations are tracked you can execute your ruby script this trick. First create a file `trace.rb` that only starts allocation tracing:
|
40
69
|
|
70
|
+
```
|
71
|
+
# trace.rb
|
72
|
+
require 'objspace'
|
73
|
+
|
74
|
+
ObjectSpace.trace_object_allocations_start
|
75
|
+
```
|
76
|
+
|
77
|
+
Now make sure this command is loaded before you run your script, you can use Ruby's `-I` to specify a load path and `-r` to specify a library to require, in this case our trace file
|
78
|
+
|
79
|
+
```
|
80
|
+
$ ruby -I ./ -r trace script_name.rb
|
81
|
+
```
|
82
|
+
|
83
|
+
If the last line of your file is invalid JSON, make sure that you are closing the file after writing the ruby heap dump to it.
|
84
|
+
|
85
|
+
### Digging into a Generation
|
86
|
+
|
87
|
+
You can drill down into a specific generation. In the previous example, the 17'th generation looks strangely large, you can drill into it:
|
41
88
|
|
42
89
|
```
|
43
90
|
$ heapy read tmp/2015-10-01T10:18:59-05:00-heap.dump 17
|
44
91
|
Analyzing Heap (Generation: 17)
|
45
92
|
-------------------------------
|
46
93
|
|
47
|
-
allocated by memory (in bytes)
|
94
|
+
allocated by memory (44061517) (in bytes)
|
48
95
|
==============================
|
49
|
-
|
50
|
-
|
51
|
-
|
96
|
+
39908512 /app/vendor/ruby-2.2.3/lib/ruby/2.2.0/timeout.rb:79
|
97
|
+
1284993 /app/vendor/ruby-2.2.3/lib/ruby/2.2.0/openssl/buffering.rb:182
|
98
|
+
201068 /app/vendor/bundle/ruby/2.2.0/gems/json-1.8.3/lib/json/common.rb:223
|
99
|
+
189272 /app/vendor/bundle/ruby/2.2.0/gems/newrelic_rpm-3.13.2.302/lib/new_relic/agent/stats_engine/stats_hash.rb:39
|
100
|
+
172531 /app/vendor/ruby-2.2.3/lib/ruby/2.2.0/net/http/header.rb:172
|
101
|
+
92200 /app/vendor/bundle/ruby/2.2.0/gems/activesupport-4.2.3/lib/active_support/core_ext/numeric/conversions.rb:131
|
102
|
+
```
|
103
|
+
|
104
|
+
You can limit the output by passing in a `--lines` value:
|
105
|
+
|
106
|
+
```
|
107
|
+
$ heapy read tmp/2015-10-01T10:18:59-05:00-heap.dump 17 --lines=6
|
52
108
|
```
|
53
109
|
|
54
|
-
|
110
|
+
> Note: Default lines value is 50
|
111
|
+
|
112
|
+
### Reviewing all generations
|
113
|
+
|
114
|
+
If you want to read all generations you can use the "all" directive
|
115
|
+
|
116
|
+
```
|
117
|
+
$ heapy read tmp/2015-10-01T10:18:59-05:00-heap.dump all
|
118
|
+
```
|
119
|
+
|
120
|
+
You can also use T-Lo's online JS based [Heap Analyzer](http://tenderlove.github.io/heap-analyzer/) for visualizations. Another tool is [HARB](https://github.com/csfrancis/harb)
|
55
121
|
|
56
122
|
## Development
|
57
123
|
|
@@ -61,8 +127,7 @@ To install this gem onto your local machine, run `bundle exec rake install`. To
|
|
61
127
|
|
62
128
|
## Contributing
|
63
129
|
|
64
|
-
Bug reports and pull requests are welcome on GitHub at https://github.com/
|
65
|
-
|
130
|
+
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.
|
66
131
|
|
67
132
|
## License
|
68
133
|
|
data/Rakefile
CHANGED
data/bin/heapy
CHANGED
data/heapy.gemspec
CHANGED
@@ -19,7 +19,9 @@ Gem::Specification.new do |spec|
|
|
19
19
|
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
20
20
|
spec.require_paths = ["lib"]
|
21
21
|
|
22
|
-
spec.
|
23
|
-
|
22
|
+
spec.add_dependency "thor"
|
23
|
+
|
24
|
+
spec.add_development_dependency "bundler", "> 1"
|
25
|
+
spec.add_development_dependency "rake", "> 10.0"
|
24
26
|
spec.add_development_dependency "rspec"
|
25
27
|
end
|
data/lib/heapy.rb
CHANGED
@@ -1,133 +1,105 @@
|
|
1
1
|
require 'json'
|
2
|
+
require 'thor'
|
2
3
|
|
3
4
|
require "heapy/version"
|
4
5
|
|
5
6
|
module Heapy
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
@cmd = argv.shift
|
10
|
-
@file = argv.shift
|
11
|
-
@number = argv.shift
|
12
|
-
@args = argv
|
7
|
+
class CLI < Thor
|
8
|
+
def self.exit_on_failure?
|
9
|
+
true
|
13
10
|
end
|
14
11
|
|
15
|
-
|
16
|
-
|
17
|
-
|
12
|
+
desc "read <file> <generation> --lines <number_of_lines>", "Read heap dump file"
|
13
|
+
long_desc <<-DESC
|
14
|
+
When run with only a file input, it will output the generation and count pairs:
|
15
|
+
|
16
|
+
$ heapy read tmp/2015-09-30-heap.dump\x5
|
17
|
+
Generation: nil object count: 209191\x5
|
18
|
+
Generation: 14 object count: 407\x5
|
19
|
+
Generation: 15 object count: 638\x5
|
20
|
+
Generation: 16 object count: 748\x5
|
21
|
+
Generation: 17 object count: 1023\x5
|
22
|
+
Generation: 18 object count: 805\x5
|
23
|
+
|
24
|
+
When run with a file and a number it will output detailed information for that\x5
|
25
|
+
generation:\x5
|
26
|
+
|
27
|
+
$ heapy read tmp/2015-09-30-heap.dump 17\x5
|
28
|
+
|
29
|
+
Analyzing Heap (Generation: 17)\x5
|
30
|
+
-------------------------------\x5
|
31
|
+
|
32
|
+
allocated by memory (44061517) (in bytes)\x5
|
33
|
+
==============================\x5
|
34
|
+
39908512 /app/vendor/ruby-2.2.3/lib/ruby/2.2.0/timeout.rb:79\x5
|
35
|
+
1284993 /app/vendor/ruby-2.2.3/lib/ruby/2.2.0/openssl/buffering.rb:182\x5
|
36
|
+
201068 /app/vendor/bundle/ruby/2.2.0/gems/json-1.8.3/lib/json/common.rb:223\x5
|
37
|
+
189272 /app/vendor/bundle/ruby/2.2.0/gems/newrelic_rpm-3.13.2.302/lib/new_relic/agent/stats_engine/stats_hash.rb:39\x5
|
38
|
+
172531 /app/vendor/ruby-2.2.3/lib/ruby/2.2.0/net/http/header.rb:172\x5
|
39
|
+
92200 /app/vendor/bundle/ruby/2.2.0/gems/activesupport-4.2.3/lib/active_support/core_ext/numeric/conversions.rb:131\x5
|
40
|
+
DESC
|
41
|
+
option :lines, required: false, :type => :numeric
|
42
|
+
def read(file_name, generation = nil)
|
43
|
+
if generation
|
44
|
+
Analyzer.new(file_name).drill_down(generation, options[:lines] || 50)
|
45
|
+
else
|
46
|
+
Analyzer.new(file_name).analyze
|
47
|
+
end
|
48
|
+
end
|
18
49
|
|
19
|
-
|
50
|
+
long_desc <<-DESC
|
51
|
+
Run with two inputs to output the values of today.dump that are not present in yesterday.dump
|
20
52
|
|
21
|
-
|
22
|
-
Generation: 0 object count: 209191
|
23
|
-
Generation: 14 object count: 407
|
24
|
-
Generation: 15 object count: 638
|
25
|
-
Generation: 16 object count: 748
|
26
|
-
Generation: 17 object count: 1023
|
27
|
-
Generation: 18 object count: 805
|
53
|
+
$ heapy diff tmp/yesterday.dump tmp/today.dump\x5
|
28
54
|
|
29
|
-
|
30
|
-
generation:
|
55
|
+
Run with three inputs to show the diff between the first two, but only if the objects are still retained in the third
|
31
56
|
|
32
|
-
|
57
|
+
$ heapy diff tmp/yesterday.dump tmp/today_morning.dump tmp/today_afternoon.dump\x5
|
33
58
|
|
34
|
-
|
35
|
-
-------------------------------
|
59
|
+
Pass in the name of an output file and the objects present in today.dump that aren't in yesterday.dump will be written to that file
|
36
60
|
|
37
|
-
|
38
|
-
==============================
|
39
|
-
/Users/richardschneeman/Documents/projects/codetriage/app/views/layouts/application.html.slim:1 (Memory: 377065, Count: 1 )
|
40
|
-
/Users/richardschneeman/.gem/ruby/2.2.3/gems/actionview-4.2.3/lib/action_view/template.rb:296 (Memory: 35814, Count: 67 )
|
41
|
-
/Users/richardschneeman/.gem/ruby/2.2.3/gems/activerecord-4.2.3/lib/active_record/attribute.rb:5 (Memory: 30672, Count: 426 )
|
61
|
+
$ heapy diff tmp/yesterday.dump tmp/today.dump --output_diff=output.json\x5
|
42
62
|
|
43
|
-
|
63
|
+
DESC
|
64
|
+
desc "diff <before_file> <after_file> <retained_file (optional)> --output_diff=output.json", "Diffs 2 heap dumps"
|
65
|
+
option :output_diff, required: false, :type => :string
|
66
|
+
def diff(before, after, retained = nil)
|
67
|
+
Diff.new(before: before, after: after, retained: retained, output_diff: options[:output_diff] || nil).call
|
44
68
|
end
|
45
69
|
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
help
|
51
|
-
when nil
|
52
|
-
help
|
53
|
-
when "read"
|
54
|
-
if @number
|
55
|
-
Analyzer.new(@file).drill_down(@number)
|
56
|
-
else
|
57
|
-
Analyzer.new(@file).analyze
|
58
|
-
end
|
59
|
-
else
|
60
|
-
help
|
61
|
-
end
|
70
|
+
map %w[--version -v] => :version
|
71
|
+
desc "version", "Show heapy version"
|
72
|
+
def version
|
73
|
+
puts Heapy::VERSION
|
62
74
|
end
|
63
|
-
end
|
64
75
|
|
65
|
-
|
66
|
-
def
|
67
|
-
|
68
|
-
end
|
76
|
+
desc "wat", "Outputs instructions on how to make a manual heap dump"
|
77
|
+
def wat
|
78
|
+
puts <<-HELP
|
69
79
|
|
70
|
-
|
71
|
-
puts ""
|
72
|
-
puts "Analyzing Heap (Generation: #{generation})"
|
73
|
-
puts "-------------------------------"
|
74
|
-
puts ""
|
75
|
-
|
76
|
-
generation = Integer(generation)
|
77
|
-
data = []
|
78
|
-
File.open(@filename) do |f|
|
79
|
-
f.each_line do |line|
|
80
|
-
parsed = JSON.parse(line)
|
81
|
-
data << parsed if parsed["generation"] == generation
|
82
|
-
end
|
83
|
-
end
|
80
|
+
To get a heap dump do this:
|
84
81
|
|
82
|
+
require 'objspace'
|
83
|
+
ObjectSpace.trace_object_allocations_start
|
85
84
|
|
86
|
-
|
87
|
-
|
88
|
-
memsize_hash = {}
|
89
|
-
data.group_by { |row| "#{row["file"]}:#{row["line"]}" }.
|
90
|
-
each do |(k, v)|
|
91
|
-
memsize_hash[k] = {
|
92
|
-
count: v.count,
|
93
|
-
memsize: v.inject(0) { |sum, obj| sum + Integer(obj["memsize"]) }
|
94
|
-
}
|
95
|
-
end
|
96
|
-
|
97
|
-
|
98
|
-
puts "allocated by memory (in bytes)"
|
99
|
-
puts "=============================="
|
100
|
-
memsize_hash.sort {|(k1, v1), (k2, v2)| v2[:memsize] <=> v1[:memsize] }.
|
101
|
-
each do |k,v|
|
102
|
-
puts "#{k} (Memory: #{v[:memsize]}, Count: #{v[:count]} ) "
|
103
|
-
end
|
104
|
-
|
105
|
-
puts ""
|
106
|
-
puts "object count"
|
107
|
-
puts "============"
|
108
|
-
memsize_hash.sort {|(k1, v1), (k2, v2)| v2[:count] <=> v1[:count] }.
|
109
|
-
each do |k,v|
|
110
|
-
puts "#{k} (Memory: #{v[:memsize]}, Count: #{v[:count]} ) "
|
111
|
-
end
|
112
|
-
end
|
85
|
+
# Your code here
|
113
86
|
|
114
|
-
|
115
|
-
puts ""
|
116
|
-
puts "Analyzing Heap"
|
117
|
-
puts "=============="
|
87
|
+
p ObjectSpace.dump_all
|
118
88
|
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
data << JSON.parse(line)
|
123
|
-
end
|
124
|
-
end
|
89
|
+
# => #<File:/path/to/output/heap/dump/here.json>
|
90
|
+
|
91
|
+
This will print the file name of your heap dump.
|
125
92
|
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
93
|
+
If you prefer you can manually pass in an IO object to `ObjectSpace.dump_all`
|
94
|
+
|
95
|
+
io = File.open("/tmp/my_dump.json", "w+")
|
96
|
+
ObjectSpace.dump_all(output: io);
|
97
|
+
io.close
|
98
|
+
|
99
|
+
HELP
|
131
100
|
end
|
132
101
|
end
|
133
102
|
end
|
103
|
+
|
104
|
+
require 'heapy/analyzer'
|
105
|
+
require 'heapy/diff'
|
@@ -0,0 +1,160 @@
|
|
1
|
+
module Heapy
|
2
|
+
|
3
|
+
# Used for inspecting contents of a heap dump
|
4
|
+
#
|
5
|
+
# To glance all contents at a glance run:
|
6
|
+
#
|
7
|
+
# Analyzer.new(file_name).analyze
|
8
|
+
#
|
9
|
+
# To inspect contents of a specific generation run:
|
10
|
+
#
|
11
|
+
# Analyzer.new(file_name).drill_down(generation, Float::INFINITY)
|
12
|
+
class Analyzer
|
13
|
+
def initialize(filename)
|
14
|
+
@filename = filename
|
15
|
+
end
|
16
|
+
|
17
|
+
def read
|
18
|
+
File.open(@filename) do |f|
|
19
|
+
f.each_line do |line|
|
20
|
+
begin
|
21
|
+
parsed = JSON.parse(line)
|
22
|
+
yield parsed
|
23
|
+
rescue JSON::ParserError
|
24
|
+
puts "Could not parse #{line}"
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def drill_down(generation_to_inspect, max_items_to_display)
|
31
|
+
puts ""
|
32
|
+
puts "Analyzing Heap (Generation: #{generation_to_inspect})"
|
33
|
+
puts "-------------------------------"
|
34
|
+
puts ""
|
35
|
+
|
36
|
+
generation_to_inspect = Integer(generation_to_inspect) unless generation_to_inspect == "all"
|
37
|
+
|
38
|
+
memsize_hash = Hash.new { |h, k| h[k] = 0 }
|
39
|
+
count_hash = Hash.new { |h, k| h[k] = 0 }
|
40
|
+
string_count = Hash.new { |h, k| h[k] = Hash.new { |h, k| h[k] = 0 } }
|
41
|
+
|
42
|
+
reference_hash = Hash.new { |h, k| h[k] = 0 }
|
43
|
+
|
44
|
+
read do |parsed|
|
45
|
+
generation = parsed["generation"] || 0
|
46
|
+
if generation_to_inspect == "all".freeze || generation == generation_to_inspect
|
47
|
+
next unless parsed["file"]
|
48
|
+
|
49
|
+
key = "#{ parsed["file"] }:#{ parsed["line"] }"
|
50
|
+
memsize_hash[key] += parsed["memsize"] || 0
|
51
|
+
count_hash[key] += 1
|
52
|
+
|
53
|
+
if parsed["type"] == "STRING".freeze
|
54
|
+
string_count[parsed["value"]][key] += 1 if parsed["value"]
|
55
|
+
end
|
56
|
+
|
57
|
+
if parsed["references"]
|
58
|
+
reference_hash[key] += parsed["references"].length
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
raise "not a valid Generation: #{generation_to_inspect.inspect}" if memsize_hash.empty?
|
64
|
+
|
65
|
+
total_memsize = memsize_hash.inject(0){|count, (k, v)| count += v}
|
66
|
+
|
67
|
+
# /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}}]}
|
68
|
+
puts "allocated by memory (#{total_memsize}) (in bytes)"
|
69
|
+
puts "=============================="
|
70
|
+
memsize_hash = memsize_hash.sort {|(k1, v1), (k2, v2)| v2 <=> v1 }.first(max_items_to_display)
|
71
|
+
longest = memsize_hash.first[1].to_s.length
|
72
|
+
memsize_hash.each do |file_line, memsize|
|
73
|
+
puts " #{memsize.to_s.rjust(longest)} #{file_line}"
|
74
|
+
end
|
75
|
+
|
76
|
+
total_count = count_hash.inject(0){|count, (k, v)| count += v}
|
77
|
+
|
78
|
+
puts ""
|
79
|
+
puts "object count (#{total_count})"
|
80
|
+
puts "=============================="
|
81
|
+
count_hash = count_hash.sort {|(k1, v1), (k2, v2)| v2 <=> v1 }.first(max_items_to_display)
|
82
|
+
longest = count_hash.first[1].to_s.length
|
83
|
+
count_hash.each do |file_line, memsize|
|
84
|
+
puts " #{memsize.to_s.rjust(longest)} #{file_line}"
|
85
|
+
end
|
86
|
+
|
87
|
+
puts ""
|
88
|
+
puts "High Ref Counts"
|
89
|
+
puts "=============================="
|
90
|
+
puts ""
|
91
|
+
|
92
|
+
reference_hash = reference_hash.sort {|(k1, v1), (k2, v2)| v2 <=> v1 }.first(max_items_to_display)
|
93
|
+
longest = count_hash.first[1].to_s.length
|
94
|
+
|
95
|
+
reference_hash.each do |file_line, count|
|
96
|
+
puts " #{count.to_s.rjust(longest)} #{file_line}"
|
97
|
+
end
|
98
|
+
|
99
|
+
if !string_count.empty?
|
100
|
+
puts ""
|
101
|
+
puts "Duplicate strings"
|
102
|
+
puts "=============================="
|
103
|
+
puts ""
|
104
|
+
|
105
|
+
value_count = {}
|
106
|
+
|
107
|
+
string_count.each do |string, location_count_hash|
|
108
|
+
value_count[string] = location_count_hash.values.inject(&:+)
|
109
|
+
end
|
110
|
+
|
111
|
+
value_count = value_count.sort {|(k1, v1), (k2, v2)| v2 <=> v1 }.first(max_items_to_display)
|
112
|
+
longest = value_count.first[1].to_s.length
|
113
|
+
|
114
|
+
value_count.each do |string, c1|
|
115
|
+
|
116
|
+
puts " #{c1.to_s.rjust(longest)} #{string.inspect}"
|
117
|
+
string_count[string].sort {|(k1, v1), (k2, v2)| v2 <=> v1 }.each do |file_line, c2|
|
118
|
+
puts " #{c2.to_s.rjust(longest)} #{file_line}"
|
119
|
+
end
|
120
|
+
puts ""
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
end
|
125
|
+
|
126
|
+
def analyze
|
127
|
+
puts ""
|
128
|
+
puts "Analyzing Heap"
|
129
|
+
puts "=============="
|
130
|
+
default_key = "nil".freeze
|
131
|
+
|
132
|
+
# generation number is key, value is count
|
133
|
+
data = Hash.new {|h, k| h[k] = 0 }
|
134
|
+
mem = Hash.new {|h, k| h[k] = 0 }
|
135
|
+
total_count = 0
|
136
|
+
total_mem = 0
|
137
|
+
|
138
|
+
read do |parsed|
|
139
|
+
data[parsed["generation"] || 0] += 1
|
140
|
+
mem[parsed["generation"] || 0] += parsed["memsize"] || 0
|
141
|
+
end
|
142
|
+
|
143
|
+
data = data.sort {|(k1,v1), (k2,v2)| k1 <=> k2 }
|
144
|
+
max_length = [data.last[0].to_s.length, default_key.length].max
|
145
|
+
data.each do |generation, count|
|
146
|
+
generation = default_key if generation == 0
|
147
|
+
total_count += count
|
148
|
+
total_mem += mem[generation]
|
149
|
+
puts "Generation: #{ generation.to_s.rjust(max_length) } object count: #{ count }, mem: #{(mem[generation].to_f / 1024).round(1)} kb"
|
150
|
+
end
|
151
|
+
|
152
|
+
puts ""
|
153
|
+
puts "Heap total"
|
154
|
+
puts "=============="
|
155
|
+
puts "Generations (active): #{data.length}"
|
156
|
+
puts "Count: #{total_count}"
|
157
|
+
puts "Memory: #{(total_mem.to_f / 1024).round(1)} kb"
|
158
|
+
end
|
159
|
+
end
|
160
|
+
end
|
data/lib/heapy/diff.rb
ADDED
@@ -0,0 +1,105 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'json'
|
4
|
+
module Heapy
|
5
|
+
# Diff 2 dumps example:
|
6
|
+
#
|
7
|
+
# Heapy::Diff.new(before: 'my_dump_1.json', after: 'my_dump_2.json').call
|
8
|
+
#
|
9
|
+
# This will find objects that are present in my_dump_2 that are not present in my_dump_1
|
10
|
+
# this means they were allocated sometime between the two heap dumps.
|
11
|
+
#
|
12
|
+
# Diff 3 dumps example:
|
13
|
+
#
|
14
|
+
# Heapy::Diff.new(before: 'my_dump_1.json', after: 'my_dump_2.json', retained: 'my_dump_3.json').call
|
15
|
+
#
|
16
|
+
# This will find objects that are present in my_dump_2 that are not present in my_dump_1
|
17
|
+
# but only if the objects are still present at the time that my_dump_3 was taken. This does
|
18
|
+
# not guarantee that they're retained forever, but were still present at the time the last
|
19
|
+
# dump was taken.
|
20
|
+
#
|
21
|
+
# You can output the diff of heap dumps by passing in a filename as `output_diff` for example
|
22
|
+
#
|
23
|
+
# Heapy::Diff.new(before: 'my_dump_1.json', after: 'my_dump_2.json', outpu_diff: 'out.json').call
|
24
|
+
class Diff
|
25
|
+
attr_reader :diff
|
26
|
+
|
27
|
+
def initialize(before:, after:, retained: nil, io: STDOUT, output_diff: nil)
|
28
|
+
@before_file = before
|
29
|
+
@after_file = after
|
30
|
+
@retained_file = retained
|
31
|
+
@output_diff_file = output_diff ? File.open(output_diff, "w+") : nil
|
32
|
+
@io = io
|
33
|
+
@diff = Hash.new { |hash, k|
|
34
|
+
hash[k] = {}
|
35
|
+
hash[k]["count"] = 0
|
36
|
+
hash[k]["memsize"] = 0
|
37
|
+
hash[k]
|
38
|
+
}
|
39
|
+
|
40
|
+
@before_address_hash = {}
|
41
|
+
@retained_address_hash = {}
|
42
|
+
end
|
43
|
+
|
44
|
+
|
45
|
+
def call
|
46
|
+
read(@before_file) { |parsed| @before_address_hash[parsed['address']] = true }
|
47
|
+
read(@retained_file) { |parsed| @retained_address_hash[parsed['address']] = true } if @retained_file
|
48
|
+
|
49
|
+
read(@after_file) do |parsed, original_line|
|
50
|
+
address = parsed['address']
|
51
|
+
next if previously_allocated?(address)
|
52
|
+
next if not_retained?(address)
|
53
|
+
|
54
|
+
@output_diff_file.puts original_line if @output_diff_file
|
55
|
+
|
56
|
+
hash = diff["#{parsed['type']},#{parsed['file']},#{parsed['line']}"]
|
57
|
+
hash["count"] += 1
|
58
|
+
hash["memsize"] += parsed["memsize"] || 0
|
59
|
+
hash["type"] ||= parsed["type"]
|
60
|
+
hash["file"] ||= parsed["file"]
|
61
|
+
hash["line"] ||= parsed["line"]
|
62
|
+
end
|
63
|
+
|
64
|
+
@output_diff_file.close if @output_diff_file
|
65
|
+
@before_address_hash.clear
|
66
|
+
@retained_address_hash.clear
|
67
|
+
|
68
|
+
total_memsize = diff.inject(0){|sum,(_,v)| sum + v["memsize"] }
|
69
|
+
|
70
|
+
diff.sort_by do |k,v|
|
71
|
+
v["count"]
|
72
|
+
end.reverse.each do |key, data|
|
73
|
+
@io.puts "#{@retained_file ? "Retained" : "Allocated"} #{data['type']} #{data['count']} objects of size #{data['memsize']}/#{total_memsize} (in bytes) at: #{data['file']}:#{data['line']}"
|
74
|
+
end
|
75
|
+
|
76
|
+
@io.puts "\nWriting heap dump diff to #{@output_diff_file.path}\n" if @output_diff_file
|
77
|
+
end
|
78
|
+
|
79
|
+
private def is_retained?(address)
|
80
|
+
return true if @retained_file.nil?
|
81
|
+
@retained_address_hash[address]
|
82
|
+
end
|
83
|
+
|
84
|
+
private def not_retained?(address)
|
85
|
+
!is_retained?(address)
|
86
|
+
end
|
87
|
+
|
88
|
+
private def previously_allocated?(address)
|
89
|
+
@before_address_hash[address]
|
90
|
+
end
|
91
|
+
|
92
|
+
private def read(filename)
|
93
|
+
File.open(filename) do |f|
|
94
|
+
f.each_line do |line|
|
95
|
+
begin
|
96
|
+
parsed = JSON.parse(line)
|
97
|
+
yield parsed, line
|
98
|
+
rescue JSON::ParserError
|
99
|
+
puts "Could not parse #{line}"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
data/lib/heapy/version.rb
CHANGED
data/trace.rb
ADDED
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,10 +74,11 @@ 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"
|
81
|
+
- CHANGELOG.md
|
67
82
|
- CODE_OF_CONDUCT.md
|
68
83
|
- Gemfile
|
69
84
|
- LICENSE.txt
|
@@ -72,12 +87,15 @@ files:
|
|
72
87
|
- bin/heapy
|
73
88
|
- heapy.gemspec
|
74
89
|
- lib/heapy.rb
|
90
|
+
- lib/heapy/analyzer.rb
|
91
|
+
- lib/heapy/diff.rb
|
75
92
|
- lib/heapy/version.rb
|
93
|
+
- trace.rb
|
76
94
|
homepage: https://github.com/schneems/heapy
|
77
95
|
licenses:
|
78
96
|
- MIT
|
79
97
|
metadata: {}
|
80
|
-
post_install_message:
|
98
|
+
post_install_message:
|
81
99
|
rdoc_options: []
|
82
100
|
require_paths:
|
83
101
|
- lib
|
@@ -92,9 +110,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
92
110
|
- !ruby/object:Gem::Version
|
93
111
|
version: '0'
|
94
112
|
requirements: []
|
95
|
-
|
96
|
-
|
97
|
-
signing_key:
|
113
|
+
rubygems_version: 3.1.2
|
114
|
+
signing_key:
|
98
115
|
specification_version: 4
|
99
116
|
summary: Inspects Ruby heap dumps
|
100
117
|
test_files: []
|
data/.DS_Store
DELETED
Binary file
|