heap-profiler 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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