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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
- SHA1:
3
- metadata.gz: b131d0e81d19b2300e8394621ac69b9d1c807799
4
- data.tar.gz: 5b97ee60930ac796ed435cd80d94890f4f5c4357
2
+ SHA256:
3
+ metadata.gz: a84bd36786d08056d5e172044cbbe30547ca5fc911aeee1a6a46905daef45c7d
4
+ data.tar.gz: b87cd42efd90097653dbe311955e66c45b961828e8c4538a2604d23f66bc8ba1
5
5
  SHA512:
6
- metadata.gz: b82a202eed50c2666af6a1189c26a19b9db3769e5ffe3399e4fdf1f18130a553a962673e8517968a88c52a270279411e2d8d5f390de8ba617440d300c159e72b
7
- data.tar.gz: e36598de3f6b07f3b7651433686db9214c93c249ab29c94f0f14c5f6fd15249a00f0fae1f80d5e09e981bb3908953220b241fc6cd19ce03b36419e358cd38cf5
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
@@ -7,3 +7,6 @@
7
7
  /pkg/
8
8
  /spec/reports/
9
9
  /tmp/
10
+ tmp
11
+ .DS_Store
12
+ scratch.rb
@@ -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
@@ -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: 0 object count: 209191
31
- Generation: 14 object count: 407
32
- Generation: 15 object count: 638
33
- Generation: 16 object count: 748
34
- Generation: 17 object count: 1023
35
- Generation: 18 object count: 805
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
- Generally early generations will have a high object count as an app is initialized. Over time however, the object count should stabalize. If however you see it spike up, you can drill down into a specific generation. In the previous example, the 17'th generation looks strangely large, you can drill into it:
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
- /Users/richardschneeman/Documents/projects/codetriage/app/views/layouts/application.html.slim:1 (Memory: 377065, Count: 1 )
50
- /Users/richardschneeman/.gem/ruby/2.2.3/gems/actionview-4.2.3/lib/action_view/template.rb:296 (Memory: 35814, Count: 67 )
51
- /Users/richardschneeman/.gem/ruby/2.2.3/gems/activerecord-4.2.3/lib/active_record/attribute.rb:5 (Memory: 30672, Count: 426 )
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
- You can also use T-Lo's online JS based [Heap Analyzer](http://tenderlove.github.io/heap-analyzer/) for visualizations.
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/[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.
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
@@ -4,3 +4,4 @@ require "rspec/core/rake_task"
4
4
  RSpec::Core::RakeTask.new(:spec)
5
5
 
6
6
  task :default => :spec
7
+ task :test => :spec
data/bin/heapy CHANGED
@@ -1,7 +1,6 @@
1
1
  #!/usr/bin/env ruby
2
2
 
3
- require "bundler/setup"
3
+ require "bundler/setup" if defined?(Bundler)
4
4
  require "heapy"
5
5
 
6
-
7
- Heapy::CLI.new(ARGV).run
6
+ Heapy::CLI.start(ARGV)
@@ -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.add_development_dependency "bundler", "~> 1.10"
23
- spec.add_development_dependency "rake", "~> 10.0"
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
@@ -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
- class CLI
8
- def initialize(argv)
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
- def help
16
- puts <<-HALP
17
- $ heapy read <file|command> <number>
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
- When run with only a file, it will output the generation and count pairs:
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
- $ heapy read tmp/2015-09-30-heap.dump
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
- When run with a file and a number it will output detailed information for that
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
- $ heapy read tmp/2015-09-30-heap.dump 17
57
+ $ heapy diff tmp/yesterday.dump tmp/today_morning.dump tmp/today_afternoon.dump\x5
33
58
 
34
- Analyzing Heap (Generation: 17)
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
- allocated by memory (in bytes)
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
- HALP
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
- def run
47
-
48
- case @cmd
49
- when "--help"
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
- class Analyzer
66
- def initialize(filename)
67
- @filename = filename
68
- end
76
+ desc "wat", "Outputs instructions on how to make a manual heap dump"
77
+ def wat
78
+ puts <<-HELP
69
79
 
70
- def drill_down(generation)
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
- # /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}}]}
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
- def analyze
115
- puts ""
116
- puts "Analyzing Heap"
117
- puts "=============="
87
+ p ObjectSpace.dump_all
118
88
 
119
- data = []
120
- File.open(@filename) do |f|
121
- f.each_line do |line|
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
- data.group_by{ |row| row["generation"] }.
127
- sort { |a, b| a[0].to_i <=> b[0].to_i }.
128
- each do |k,v|
129
- puts "Generation: #{k || " 0"} object count: #{v.count}"
130
- end
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
@@ -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
@@ -1,3 +1,3 @@
1
1
  module Heapy
2
- VERSION = "0.1.0"
2
+ VERSION = "0.2.0"
3
3
  end
@@ -0,0 +1,3 @@
1
+ require 'objspace'
2
+
3
+ ObjectSpace.trace_object_allocations_start
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.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: 2015-10-05 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,10 +74,11 @@ 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"
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
- rubyforge_project:
96
- rubygems_version: 2.4.5.1
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