heap-profiler 0.5.0 → 0.6.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.
@@ -143,12 +143,13 @@ module HeapProfiler
143
143
  end
144
144
  end
145
145
 
146
- def top_n(_max)
146
+ def top_n(max)
147
147
  values = @locations_counts.values
148
148
  values.sort! do |a, b|
149
149
  cmp = b.count <=> a.count
150
150
  cmp == 0 ? b.location <=> a.location : cmp
151
151
  end
152
+ values.take(max)
152
153
  end
153
154
  end
154
155
 
@@ -178,6 +179,29 @@ module HeapProfiler
178
179
  end
179
180
  end
180
181
 
182
+ class ShapeEdgeDimension
183
+ def initialize
184
+ @stats = Hash.new(0)
185
+ end
186
+
187
+ def process(_index, object)
188
+ if name = object[:edge_name]
189
+ @stats[name] += 1
190
+ end
191
+ end
192
+
193
+ def top_n(max)
194
+ @stats.sort do |(a_name, a_count), (b_name, b_count)|
195
+ cmp = b_count <=> a_count
196
+ if cmp == 0
197
+ a_name <=> b_name
198
+ else
199
+ cmp
200
+ end
201
+ end.take(max)
202
+ end
203
+ end
204
+
181
205
  def initialize(heap, index)
182
206
  @heap = heap
183
207
  @index = index
@@ -188,6 +212,8 @@ module HeapProfiler
188
212
  metrics.each do |metric|
189
213
  if metric == "strings"
190
214
  dimensions["strings"] = StringDimension.new
215
+ elsif metric == "shape_edges"
216
+ dimensions["shape_edges"] = ShapeEdgeDimension.new
191
217
  else
192
218
  dimensions["total"] = Dimension.new
193
219
  groupings.each do |grouping|
@@ -11,9 +11,18 @@ module HeapProfiler
11
11
  parser.parse!(@argv)
12
12
 
13
13
  begin
14
- if @argv.size == 1
15
- print_report(@argv.first)
14
+ case @argv.first
15
+ when "clean"
16
+ clean_dump(@argv[1])
16
17
  return 0
18
+ when "report"
19
+ print_report(@argv[1])
20
+ return 0
21
+ else
22
+ if @argv.size == 1
23
+ print_report(@argv.first)
24
+ return 0
25
+ end
17
26
  end
18
27
  rescue CapacityError => error
19
28
  STDERR.puts(error.message)
@@ -27,15 +36,43 @@ module HeapProfiler
27
36
 
28
37
  def print_report(path)
29
38
  results = if File.directory?(path)
30
- DiffResults.new(path)
39
+ if @retained_only
40
+ DiffResults.new(path, ["retained"])
41
+ else
42
+ DiffResults.new(path)
43
+ end
31
44
  else
32
45
  HeapResults.new(path)
33
46
  end
34
47
  results.pretty_print(scale_bytes: true, normalize_paths: true)
35
48
  end
36
49
 
50
+ def clean_dump(path)
51
+ require "json"
52
+ errors = index = 0
53
+ clean_path = "#{path}.clean"
54
+ File.open(clean_path, "w+") do |output|
55
+ File.open(path) do |input|
56
+ input.each_line do |line|
57
+ begin
58
+ JSON.parse(line)
59
+ rescue JSON::ParserError
60
+ errors += 1
61
+ $stderr.puts("Invalid JSON found on line #{index}. Skipping")
62
+ else
63
+ output.print(line)
64
+ end
65
+ index += 1
66
+ end
67
+ end
68
+ end
69
+ $stderr.puts("Processed #{index} lines, removed #{errors} invalid lines")
70
+ $stderr.puts("Clean dump available at #{clean_path}")
71
+ end
72
+
37
73
  def print_usage
38
74
  puts "Usage: #{$PROGRAM_NAME} directory_or_heap_dump"
75
+ puts @parser.help
39
76
  end
40
77
 
41
78
  SIZE_UNITS = {
@@ -66,14 +103,30 @@ module HeapProfiler
66
103
 
67
104
  def parser
68
105
  @parser ||= OptionParser.new do |opts|
69
- opts.banner = "Usage: heap-profiler [ARGS]"
70
- opts.separator ""
71
- opts.separator "GLOBAL OPTIONS"
106
+ opts.banner = <<~EOS
107
+ Usage: heap-profiler [SUBCOMMAND] [ARGS]"
108
+
109
+ SUBCOMMANDS
110
+
111
+ report: Produce a full memory report from the provided dump. (default)
112
+
113
+ clean: Remove all malformed lines from the provided heap dump. Can be useful to workaround some ruby bugs.
114
+
115
+ GLOBAL OPTIONS
116
+ EOS
72
117
  opts.separator ""
73
118
 
74
- help = <<~EOS
75
- Sets the simdjson parser batch size. It must be larger than the largest JSON document in the
76
- heap dump, and defaults to 10MB.
119
+ opts.on('-r', '--retained-only', 'Only compute report for memory retentions.') do
120
+ @retained_only = true
121
+ end
122
+
123
+ HeapProfiler::AbstractResults.top_entries_count = 50
124
+ opts.on("-m", "--max=NUM", Integer, "Max number of entries to output. (Defaults to 50)") do |arg|
125
+ HeapProfiler::AbstractResults.top_entries_count = arg
126
+ end
127
+
128
+ help = <<~EOS.lines.join(" ")
129
+ Sets the simdjson parser batch size. It must be larger than the largest JSON document in the heap dump, and defaults to 10MB.
77
130
  EOS
78
131
  opts.on('--batch-size SIZE', help.strip) do |size_string|
79
132
  HeapProfiler::Parser.batch_size = parse_byte_size(size_string)
@@ -31,6 +31,7 @@ module HeapProfiler
31
31
  REGEXP: "Regexp",
32
32
  MATCH: "MatchData",
33
33
  ROOT: "<VM Root>",
34
+ SHAPE: "SHAPE",
34
35
  }.freeze
35
36
 
36
37
  IMEMO_TYPES = Hash.new { |h, k| h[k] = "<#{k || 'unknown'}> (IMEMO)" }
@@ -14,11 +14,17 @@ module HeapProfiler
14
14
  24 => 'YB',
15
15
  }.freeze
16
16
 
17
- METRICS = ["memory", "objects", "strings"].freeze
17
+ METRICS = ["memory", "objects", "strings", "shape_edges"].freeze
18
+ GROUPED_METRICS = ["memory", "objects"]
18
19
  GROUPINGS = ["gem", "file", "location", "class"].freeze
19
20
 
20
21
  attr_reader :types, :dimensions
21
22
 
23
+ @top_entries_count = 50
24
+ class << self
25
+ attr_accessor :top_entries_count
26
+ end
27
+
22
28
  def initialize(*, **)
23
29
  raise NotImplementedError
24
30
  end
@@ -82,7 +88,7 @@ module HeapProfiler
82
88
  "(#{dimensions['total'].objects} objects)"
83
89
 
84
90
  @metrics.each do |metric|
85
- next if metric == "strings"
91
+ next unless GROUPED_METRICS.include?(metric)
86
92
  @groupings.each do |grouping|
87
93
  dump_data(io, dimensions, metric, grouping, options)
88
94
  end
@@ -91,11 +97,15 @@ module HeapProfiler
91
97
  if @metrics.include?("strings")
92
98
  dump_strings(io, dimensions, options)
93
99
  end
100
+
101
+ if @metrics.include?("shape_edges")
102
+ dump_shape_edges(io, dimensions, options)
103
+ end
94
104
  end
95
105
 
96
106
  def dump_data(io, dimensions, metric, grouping, options)
97
107
  print_title io, "#{metric} by #{grouping}"
98
- data = dimensions[grouping].top_n(metric, options.fetch(:top, 50))
108
+ data = dimensions[grouping].top_n(metric, AbstractResults.top_entries_count)
99
109
 
100
110
  scale_data = metric == "memory" && options[:scale_bytes]
101
111
  normalize_paths = options[:normalize_paths]
@@ -112,7 +122,7 @@ module HeapProfiler
112
122
  def dump_strings(io, dimensions, options)
113
123
  normalize_paths = options[:normalize_paths]
114
124
  scale_data = options[:scale_bytes]
115
- top = options.fetch(:top, 50)
125
+ top = AbstractResults.top_entries_count
116
126
 
117
127
  print_title(io, "String Report")
118
128
 
@@ -127,6 +137,19 @@ module HeapProfiler
127
137
  io.puts
128
138
  end
129
139
  end
140
+
141
+ def dump_shape_edges(io, dimensions, _options)
142
+ top = AbstractResults.top_entries_count
143
+
144
+ data = dimensions["shape_edges"].top_n(top)
145
+ unless data.empty?
146
+ print_title(io, "Shape Edges Report")
147
+
148
+ data.each do |edge_name, count|
149
+ print_output io, count, edge_name
150
+ end
151
+ end
152
+ end
130
153
  end
131
154
 
132
155
  class DiffResults < AbstractResults
@@ -160,7 +183,7 @@ module HeapProfiler
160
183
 
161
184
  @types.each do |type|
162
185
  @metrics.each do |metric|
163
- next if metric == "strings"
186
+ next unless GROUPED_METRICS.include?(metric)
164
187
  @groupings.each do |grouping|
165
188
  dump_data(io, dimensions, type, metric, grouping, options)
166
189
  end
@@ -172,11 +195,17 @@ module HeapProfiler
172
195
  dump_strings(io, dimensions[type], type, options)
173
196
  end
174
197
  end
198
+
199
+ if @metrics.include?("shape_edges")
200
+ @types.each do |type|
201
+ dump_shape_edges(io, dimensions[type], type, options)
202
+ end
203
+ end
175
204
  end
176
205
 
177
206
  def dump_data(io, dimensions, type, metric, grouping, options)
178
207
  print_title io, "#{type} #{metric} by #{grouping}"
179
- data = dimensions[type][grouping].top_n(metric, options.fetch(:top, 50))
208
+ data = dimensions[type][grouping].top_n(metric, AbstractResults.top_entries_count)
180
209
 
181
210
  scale_data = metric == "memory" && options[:scale_bytes]
182
211
  normalize_paths = options[:normalize_paths]
@@ -193,7 +222,7 @@ module HeapProfiler
193
222
  def dump_strings(io, dimensions, type, options)
194
223
  normalize_paths = options[:normalize_paths]
195
224
  scale_data = options[:scale_bytes]
196
- top = options.fetch(:top, 50)
225
+ top = AbstractResults.top_entries_count
197
226
 
198
227
  print_title(io, "#{type.capitalize} String Report")
199
228
 
@@ -208,5 +237,18 @@ module HeapProfiler
208
237
  io.puts
209
238
  end
210
239
  end
240
+
241
+ def dump_shape_edges(io, dimensions, _type, _options)
242
+ top = AbstractResults.top_entries_count
243
+
244
+ data = dimensions["shape_edges"].top_n(top)
245
+ unless data.empty?
246
+ print_title(io, "Shape Edges Report")
247
+
248
+ data.each do |edge_name, count|
249
+ print_output io, count, edge_name
250
+ end
251
+ end
252
+ end
211
253
  end
212
254
  end
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module HeapProfiler
3
- VERSION = "0.5.0"
3
+ VERSION = "0.6.0"
4
4
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: heap-profiler
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jean Boussier
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-08-05 00:00:00.000000000 Z
11
+ date: 2022-12-15 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Make several heap dumps and summarize allocated, retained memory
14
14
  email:
@@ -22,7 +22,6 @@ files:
22
22
  - ".github/workflows/tests.yml"
23
23
  - ".gitignore"
24
24
  - ".rubocop.yml"
25
- - ".travis.yml"
26
25
  - Gemfile
27
26
  - Gemfile.lock
28
27
  - LICENSE.txt
@@ -79,7 +78,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
79
78
  - !ruby/object:Gem::Version
80
79
  version: '0'
81
80
  requirements: []
82
- rubygems_version: 3.2.20
81
+ rubygems_version: 3.3.7
83
82
  signing_key:
84
83
  specification_version: 4
85
84
  summary: Ruby heap profiling tool
data/.travis.yml DELETED
@@ -1,6 +0,0 @@
1
- ---
2
- language: ruby
3
- cache: bundler
4
- rvm:
5
- - 2.6.3
6
- before_install: gem install bundler -v 2.1.4