heap-profiler 0.8.0.rc1-aarch64-linux
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/.github/workflows/cibuildgem.yaml +87 -0
- data/.github/workflows/tests.yml +33 -0
- data/.gitignore +11 -0
- data/.rubocop.yml +29 -0
- data/.ruby-version +1 -0
- data/Gemfile +13 -0
- data/Gemfile.lock +69 -0
- data/LICENSE.txt +21 -0
- data/README.md +291 -0
- data/Rakefile +17 -0
- data/TODO.md +3 -0
- data/benchmark/address-parsing.rb +15 -0
- data/benchmark/indexing.rb +17 -0
- data/bin/console +15 -0
- data/bin/generate-report +49 -0
- data/bin/rubocop +29 -0
- data/bin/setup +8 -0
- data/bin/testunit +9 -0
- data/dev.yml +20 -0
- data/exe/heap-profiler +5 -0
- data/ext/heap_profiler/extconf.rb +9 -0
- data/ext/heap_profiler/heap_profiler.cpp +335 -0
- data/ext/heap_profiler/simdjson.cpp +15047 -0
- data/ext/heap_profiler/simdjson.h +32071 -0
- data/heap-profiler.gemspec +31 -0
- data/lib/heap-profiler.rb +6 -0
- data/lib/heap_profiler/3.1/heap_profiler.so +0 -0
- data/lib/heap_profiler/3.2/heap_profiler.so +0 -0
- data/lib/heap_profiler/3.3/heap_profiler.so +0 -0
- data/lib/heap_profiler/3.4/heap_profiler.so +0 -0
- data/lib/heap_profiler/4.0/heap_profiler.so +0 -0
- data/lib/heap_profiler/analyzer.rb +232 -0
- data/lib/heap_profiler/cli.rb +140 -0
- data/lib/heap_profiler/diff.rb +39 -0
- data/lib/heap_profiler/dump.rb +97 -0
- data/lib/heap_profiler/full.rb +12 -0
- data/lib/heap_profiler/index.rb +89 -0
- data/lib/heap_profiler/monochrome.rb +19 -0
- data/lib/heap_profiler/parser.rb +83 -0
- data/lib/heap_profiler/polychrome.rb +93 -0
- data/lib/heap_profiler/reporter.rb +118 -0
- data/lib/heap_profiler/results.rb +256 -0
- data/lib/heap_profiler/runtime.rb +30 -0
- data/lib/heap_profiler/version.rb +4 -0
- metadata +94 -0
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HeapProfiler
|
|
4
|
+
module Parser
|
|
5
|
+
CLASS_DEFAULT_PROC = ->(_hash, key) { "<Class#0x#{key.to_s(16)}>" }
|
|
6
|
+
|
|
7
|
+
class << self
|
|
8
|
+
attr_accessor :batch_size
|
|
9
|
+
end
|
|
10
|
+
self.batch_size = 10_000_000 # 10MB
|
|
11
|
+
|
|
12
|
+
class Ruby
|
|
13
|
+
def build_index(path)
|
|
14
|
+
require 'json'
|
|
15
|
+
classes_index = {}
|
|
16
|
+
classes_index.default_proc = CLASS_DEFAULT_PROC
|
|
17
|
+
strings_index = {}
|
|
18
|
+
|
|
19
|
+
File.open(path).each_line do |line|
|
|
20
|
+
object = JSON.parse(line, symbolize_names: true)
|
|
21
|
+
case object[:type]
|
|
22
|
+
when 'MODULE', 'CLASS'
|
|
23
|
+
address = parse_address(object[:address])
|
|
24
|
+
|
|
25
|
+
name = object[:name]
|
|
26
|
+
name ||= if object[:file] && object[:line]
|
|
27
|
+
"<Class #{object[:file]}:#{object[:line]}>"
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
if name
|
|
31
|
+
classes_index[address] = name
|
|
32
|
+
end
|
|
33
|
+
when 'STRING'
|
|
34
|
+
next if object[:shared]
|
|
35
|
+
if (value = object[:value])
|
|
36
|
+
strings_index[parse_address(object[:address])] = value
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
[classes_index, strings_index]
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def parse_address(address)
|
|
45
|
+
address.to_i(16)
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
class Native
|
|
50
|
+
def build_index(path, batch_size: Parser.batch_size)
|
|
51
|
+
indexes = _build_index(path, batch_size)
|
|
52
|
+
indexes.first.default_proc = CLASS_DEFAULT_PROC
|
|
53
|
+
indexes
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def load_many(path, since: nil, batch_size: Parser.batch_size, &block)
|
|
57
|
+
_load_many(path, since, batch_size, &block)
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
class << self
|
|
62
|
+
def build_index(path)
|
|
63
|
+
current.build_index(path)
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def load_many(path, **kwargs, &block)
|
|
67
|
+
current.load_many(path, **kwargs, &block)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
private
|
|
71
|
+
|
|
72
|
+
def current
|
|
73
|
+
Thread.current[:HeapProfilerParser] ||= Native.new
|
|
74
|
+
end
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
begin
|
|
79
|
+
require "heap_profiler/#{RUBY_VERSION[/^\d+\.\d+/]}/heap_profiler"
|
|
80
|
+
rescue LoadError
|
|
81
|
+
require "heap_profiler/heap_profiler"
|
|
82
|
+
end
|
|
83
|
+
end
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HeapProfiler
|
|
4
|
+
module Polychrome
|
|
5
|
+
class << self
|
|
6
|
+
def path(text)
|
|
7
|
+
blue(text)
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def string(text)
|
|
11
|
+
green(text)
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def line(text)
|
|
15
|
+
cyan(text)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def black(str)
|
|
21
|
+
"\033[30m#{str}\033[0m"
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def red(str)
|
|
25
|
+
"\033[31m#{str}\033[0m"
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def green(str)
|
|
29
|
+
"\033[32m#{str}\033[0m"
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def brown(str)
|
|
33
|
+
"\033[33m#{str}\033[0m"
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def blue(str)
|
|
37
|
+
"\033[34m#{str}\033[0m"
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def magenta(str)
|
|
41
|
+
"\033[35m#{str}\033[0m"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def cyan(str)
|
|
45
|
+
"\033[36m#{str}\033[0m"
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def gray(str)
|
|
49
|
+
"\033[37m#{str}\033[0m"
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def bg_black(str)
|
|
53
|
+
"\033[40m#{str}\033[0m"
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def bg_red(str)
|
|
57
|
+
"\033[41m#{str}\033[0m"
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def bg_green(str)
|
|
61
|
+
"\033[42m#{str}\033[0m"
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def bg_brown(str)
|
|
65
|
+
"\033[43m#{str}\033[0m"
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def bg_blue(str)
|
|
69
|
+
"\033[44m#{str}\033[0m"
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def bg_magenta(str)
|
|
73
|
+
"\033[45m#{str}\033[0m"
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def bg_cyan(str)
|
|
77
|
+
"\033[46m#{str}\033[0m"
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def bg_gray(str)
|
|
81
|
+
"\033[47m#{str}\033[0m"
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def bold(str)
|
|
85
|
+
"\033[1m#{str}\033[22m"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def reverse_color(str)
|
|
89
|
+
"\033[7m#{str}\033[27m"
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
end
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HeapProfiler
|
|
4
|
+
class << self
|
|
5
|
+
# This works around a Ruby bug present until at least 2.7.1
|
|
6
|
+
# ObjectSpace.dump include module and class names in the dump
|
|
7
|
+
# and for anonymous modules and classes this mean naming them.
|
|
8
|
+
#
|
|
9
|
+
# So we name them at the start of the profile to avoid that.
|
|
10
|
+
#
|
|
11
|
+
# See: https://github.com/ruby/ruby/pull/3349
|
|
12
|
+
if RUBY_VERSION < '2.8'
|
|
13
|
+
def name_anonymous_modules!
|
|
14
|
+
ObjectSpace.each_object(Module) do |mod|
|
|
15
|
+
next if mod.singleton_class?
|
|
16
|
+
next if real_mod_name(mod)
|
|
17
|
+
# We have to assign it at the top level to avoid allocating a string for the name
|
|
18
|
+
::Object.const_set(:AnonymousClassOrModule, mod)
|
|
19
|
+
::Object.send(:remove_const, :AnonymousClassOrModule)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
::Module.alias_method(:__real_mod_name, :name)
|
|
24
|
+
def real_mod_name(mod)
|
|
25
|
+
mod.__real_mod_name
|
|
26
|
+
end
|
|
27
|
+
else
|
|
28
|
+
def name_anonymous_modules!
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
class Reporter
|
|
34
|
+
def initialize(dir_path)
|
|
35
|
+
@dir_path = dir_path
|
|
36
|
+
@enable_tracing = !allocation_tracing_enabled?
|
|
37
|
+
@generation = nil
|
|
38
|
+
@partial = true
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def start(partial: true)
|
|
42
|
+
@partial = partial
|
|
43
|
+
FileUtils.mkdir_p(@dir_path)
|
|
44
|
+
ObjectSpace.trace_object_allocations_start if @enable_tracing
|
|
45
|
+
|
|
46
|
+
@allocated_heap = open_heap("allocated")
|
|
47
|
+
@retained_heap = open_heap("retained")
|
|
48
|
+
|
|
49
|
+
HeapProfiler.name_anonymous_modules!
|
|
50
|
+
|
|
51
|
+
GC.start
|
|
52
|
+
GC.disable
|
|
53
|
+
@generation = GC.count
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def stop
|
|
57
|
+
HeapProfiler.name_anonymous_modules!
|
|
58
|
+
ObjectSpace.trace_object_allocations_stop if @enable_tracing
|
|
59
|
+
|
|
60
|
+
# we can't use partial dump for allocated.heap, because we need old generations
|
|
61
|
+
# as well to build the classes and strings indexes.
|
|
62
|
+
dump_heap(@allocated_heap)
|
|
63
|
+
|
|
64
|
+
GC.enable
|
|
65
|
+
GC.start
|
|
66
|
+
dump_heap(@retained_heap, partial: @partial)
|
|
67
|
+
@allocated_heap.close
|
|
68
|
+
@retained_heap.close
|
|
69
|
+
write_info("generation", @partial ? @generation.to_s : "0")
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def run
|
|
73
|
+
start
|
|
74
|
+
begin
|
|
75
|
+
yield
|
|
76
|
+
rescue Exception
|
|
77
|
+
ObjectSpace.trace_object_allocations_stop if @enable_tracing
|
|
78
|
+
GC.enable
|
|
79
|
+
raise
|
|
80
|
+
else
|
|
81
|
+
stop
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
private
|
|
86
|
+
|
|
87
|
+
def write_info(key, value)
|
|
88
|
+
File.write(File.join(@dir_path, "#{key}.info"), value)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
if RUBY_VERSION >= '3.0'
|
|
92
|
+
def dump_heap(file, partial: false)
|
|
93
|
+
ObjectSpace.dump_all(output: file, since: partial ? @generation : nil)
|
|
94
|
+
file.close
|
|
95
|
+
end
|
|
96
|
+
else
|
|
97
|
+
# ObjectSpace.dump_all does allocate a few objects in itself (https://bugs.ruby-lang.org/issues/17045)
|
|
98
|
+
# because of this even en empty block of code will report a handful of allocations.
|
|
99
|
+
# To filter them more easily we attribute call `dump_all` from a method with a very specific `file`
|
|
100
|
+
# property.
|
|
101
|
+
class_eval <<~RUBY, '__hprof', __LINE__
|
|
102
|
+
# frozen_string_literal: true
|
|
103
|
+
def dump_heap(file, partial: false)
|
|
104
|
+
ObjectSpace.dump_all(output: file)
|
|
105
|
+
file.close
|
|
106
|
+
end
|
|
107
|
+
RUBY
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
def open_heap(name)
|
|
111
|
+
File.open(File.join(@dir_path, "#{name}.heap"), 'w+')
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def allocation_tracing_enabled?
|
|
115
|
+
ObjectSpace.allocation_sourceline(Object.new)
|
|
116
|
+
end
|
|
117
|
+
end
|
|
118
|
+
end
|
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module HeapProfiler
|
|
4
|
+
class AbstractResults
|
|
5
|
+
UNIT_PREFIXES = {
|
|
6
|
+
0 => 'B',
|
|
7
|
+
3 => 'kB',
|
|
8
|
+
6 => 'MB',
|
|
9
|
+
9 => 'GB',
|
|
10
|
+
12 => 'TB',
|
|
11
|
+
15 => 'PB',
|
|
12
|
+
18 => 'EB',
|
|
13
|
+
21 => 'ZB',
|
|
14
|
+
24 => 'YB',
|
|
15
|
+
}.freeze
|
|
16
|
+
|
|
17
|
+
METRICS = ["memory", "objects", "strings", "shape_edges"].freeze
|
|
18
|
+
GROUPED_METRICS = ["memory", "objects"]
|
|
19
|
+
GROUPINGS = ["gem", "file", "location", "class"].freeze
|
|
20
|
+
|
|
21
|
+
attr_reader :types, :dimensions
|
|
22
|
+
|
|
23
|
+
@top_entries_count = 50
|
|
24
|
+
class << self
|
|
25
|
+
attr_accessor :top_entries_count
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def initialize(*, **)
|
|
29
|
+
raise NotImplementedError
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def print_title(io, title)
|
|
33
|
+
io.puts
|
|
34
|
+
io.puts title
|
|
35
|
+
io.puts @colorize.line("-----------------------------------")
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def print_output(io, topic, detail)
|
|
39
|
+
io.puts "#{@colorize.path(topic.to_s.rjust(10))} #{detail}"
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def print_output2(io, topic1, topic2, detail)
|
|
43
|
+
io.puts "#{@colorize.path(topic1.to_s.rjust(10))} #{@colorize.path(topic2.to_s.rjust(6))} #{detail}"
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def normalize_path(path)
|
|
47
|
+
@normalize_path ||= {}
|
|
48
|
+
@normalize_path[path] ||= begin
|
|
49
|
+
if %r!(/gems/.*)*/gems/(?<gemname>[^/]+)(?<rest>.*)! =~ path
|
|
50
|
+
"#{gemname}#{rest}"
|
|
51
|
+
elsif %r!ruby/2\.[^/]+/(?<stdlib>[^/.]+)(?<rest>.*)! =~ path
|
|
52
|
+
"ruby/lib/#{stdlib}#{rest}"
|
|
53
|
+
elsif %r!(?<app>[^/]+/(bin|app|lib))(?<rest>.*)! =~ path
|
|
54
|
+
"#{app}#{rest}"
|
|
55
|
+
else
|
|
56
|
+
path
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def scale_bytes(bytes)
|
|
62
|
+
return "0 B" if bytes.zero?
|
|
63
|
+
|
|
64
|
+
scale = Math.log10(bytes).div(3) * 3
|
|
65
|
+
scale = 24 if scale > 24
|
|
66
|
+
format("%.2f #{UNIT_PREFIXES[scale]}", bytes / 10.0**scale)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
class HeapResults < AbstractResults
|
|
71
|
+
def initialize(heap_path, metrics = METRICS, groupings = GROUPINGS)
|
|
72
|
+
@path = heap_path
|
|
73
|
+
@metrics = metrics
|
|
74
|
+
@groupings = groupings
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def pretty_print(io = $stdout, **options)
|
|
78
|
+
heap = Dump.new(@path)
|
|
79
|
+
index = Index.new(heap)
|
|
80
|
+
|
|
81
|
+
color_output = options.fetch(:color_output) { io.respond_to?(:isatty) && io.isatty }
|
|
82
|
+
@colorize = color_output ? Polychrome : Monochrome
|
|
83
|
+
|
|
84
|
+
analyzer = Analyzer.new(heap, index)
|
|
85
|
+
dimensions = analyzer.run(@metrics, @groupings)
|
|
86
|
+
|
|
87
|
+
if dimensions['total']
|
|
88
|
+
io.puts "Total: #{scale_bytes(dimensions['total'].memory)} " \
|
|
89
|
+
"(#{dimensions['total'].objects} objects)"
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
@metrics.each do |metric|
|
|
93
|
+
next unless GROUPED_METRICS.include?(metric)
|
|
94
|
+
@groupings.each do |grouping|
|
|
95
|
+
dump_data(io, dimensions, metric, grouping, options)
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
if @metrics.include?("strings")
|
|
100
|
+
dump_strings(io, dimensions, options)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
if @metrics.include?("shape_edges")
|
|
104
|
+
dump_shape_edges(io, dimensions, options)
|
|
105
|
+
end
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def dump_data(io, dimensions, metric, grouping, options)
|
|
109
|
+
print_title io, "#{metric} by #{grouping}"
|
|
110
|
+
data = dimensions[grouping].top_n(metric, AbstractResults.top_entries_count)
|
|
111
|
+
|
|
112
|
+
scale_data = metric == "memory" && options[:scale_bytes]
|
|
113
|
+
normalize_paths = options[:normalize_paths]
|
|
114
|
+
|
|
115
|
+
if data && !data.empty?
|
|
116
|
+
data.each { |pair| pair[0] = normalize_path(pair[0]) } if normalize_paths
|
|
117
|
+
data.each { |pair| pair[1] = scale_bytes(pair[1]) } if scale_data
|
|
118
|
+
data.each { |k, v| print_output(io, v, k) }
|
|
119
|
+
else
|
|
120
|
+
io.puts "NO DATA"
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def dump_strings(io, dimensions, options)
|
|
125
|
+
normalize_paths = options[:normalize_paths]
|
|
126
|
+
scale_data = options[:scale_bytes]
|
|
127
|
+
top = AbstractResults.top_entries_count
|
|
128
|
+
|
|
129
|
+
print_title(io, "String Report")
|
|
130
|
+
|
|
131
|
+
dimensions["strings"].top_n(top).each do |string|
|
|
132
|
+
memsize = scale_data ? scale_bytes(string.memsize) : string.memsize
|
|
133
|
+
print_output2 io, memsize, string.count, @colorize.string(string.value.inspect)
|
|
134
|
+
string.top_n(top).each do |string_location|
|
|
135
|
+
location = string_location.location
|
|
136
|
+
location = normalize_path(location) if normalize_paths
|
|
137
|
+
print_output2 io, '', string_location.count, location
|
|
138
|
+
end
|
|
139
|
+
io.puts
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def dump_shape_edges(io, dimensions, _options)
|
|
144
|
+
top = AbstractResults.top_entries_count
|
|
145
|
+
|
|
146
|
+
data = dimensions["shape_edges"].top_n(top)
|
|
147
|
+
unless data.empty?
|
|
148
|
+
print_title(io, "Shape Edges Report")
|
|
149
|
+
|
|
150
|
+
data.each do |edge_name, count|
|
|
151
|
+
print_output io, count, edge_name
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
class DiffResults < AbstractResults
|
|
158
|
+
TYPES = ["allocated", "retained"].freeze
|
|
159
|
+
|
|
160
|
+
def initialize(directory, types = TYPES, metrics = METRICS, groupings = GROUPINGS)
|
|
161
|
+
@directory = directory
|
|
162
|
+
@types = types
|
|
163
|
+
@metrics = metrics
|
|
164
|
+
@groupings = groupings
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def pretty_print(io = $stdout, **options)
|
|
168
|
+
diff = Diff.new(@directory)
|
|
169
|
+
heaps = @types.each_with_object({}) { |t, h| h[t] = diff.public_send("#{t}_diff") }
|
|
170
|
+
index = Index.new(diff.allocated)
|
|
171
|
+
|
|
172
|
+
color_output = options.fetch(:color_output) { io.respond_to?(:isatty) && io.isatty }
|
|
173
|
+
@colorize = color_output ? Polychrome : Monochrome
|
|
174
|
+
|
|
175
|
+
dimensions = {}
|
|
176
|
+
heaps.each do |type, heap|
|
|
177
|
+
analyzer = Analyzer.new(heap, index)
|
|
178
|
+
dimensions[type] = analyzer.run(@metrics, @groupings)
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
dimensions.each do |type, metrics|
|
|
182
|
+
io.puts "Total #{type}: #{scale_bytes(metrics['total'].memory)} " \
|
|
183
|
+
"(#{metrics['total'].objects} objects)"
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
@types.each do |type|
|
|
187
|
+
@metrics.each do |metric|
|
|
188
|
+
next unless GROUPED_METRICS.include?(metric)
|
|
189
|
+
@groupings.each do |grouping|
|
|
190
|
+
dump_data(io, dimensions, type, metric, grouping, options)
|
|
191
|
+
end
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
if @metrics.include?("strings")
|
|
196
|
+
@types.each do |type|
|
|
197
|
+
dump_strings(io, dimensions[type], type, options)
|
|
198
|
+
end
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
if @metrics.include?("shape_edges")
|
|
202
|
+
@types.each do |type|
|
|
203
|
+
dump_shape_edges(io, dimensions[type], type, options)
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
def dump_data(io, dimensions, type, metric, grouping, options)
|
|
209
|
+
print_title io, "#{type} #{metric} by #{grouping}"
|
|
210
|
+
data = dimensions[type][grouping].top_n(metric, AbstractResults.top_entries_count)
|
|
211
|
+
|
|
212
|
+
scale_data = metric == "memory" && options[:scale_bytes]
|
|
213
|
+
normalize_paths = options[:normalize_paths]
|
|
214
|
+
|
|
215
|
+
if data && !data.empty?
|
|
216
|
+
data.each { |pair| pair[0] = normalize_path(pair[0]) } if normalize_paths
|
|
217
|
+
data.each { |pair| pair[1] = scale_bytes(pair[1]) } if scale_data
|
|
218
|
+
data.each { |k, v| print_output(io, v, k) }
|
|
219
|
+
else
|
|
220
|
+
io.puts "NO DATA"
|
|
221
|
+
end
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
def dump_strings(io, dimensions, type, options)
|
|
225
|
+
normalize_paths = options[:normalize_paths]
|
|
226
|
+
scale_data = options[:scale_bytes]
|
|
227
|
+
top = AbstractResults.top_entries_count
|
|
228
|
+
|
|
229
|
+
print_title(io, "#{type.capitalize} String Report")
|
|
230
|
+
|
|
231
|
+
dimensions["strings"].top_n(top).each do |string|
|
|
232
|
+
memsize = scale_data ? scale_bytes(string.memsize) : string.memsize
|
|
233
|
+
print_output2 io, memsize, string.count, @colorize.string(string.value.inspect)
|
|
234
|
+
string.top_n(top).each do |string_location|
|
|
235
|
+
location = string_location.location
|
|
236
|
+
location = normalize_path(location) if normalize_paths
|
|
237
|
+
print_output2 io, '', string_location.count, location
|
|
238
|
+
end
|
|
239
|
+
io.puts
|
|
240
|
+
end
|
|
241
|
+
end
|
|
242
|
+
|
|
243
|
+
def dump_shape_edges(io, dimensions, _type, _options)
|
|
244
|
+
top = AbstractResults.top_entries_count
|
|
245
|
+
|
|
246
|
+
data = dimensions["shape_edges"].top_n(top)
|
|
247
|
+
unless data.empty?
|
|
248
|
+
print_title(io, "Shape Edges Report")
|
|
249
|
+
|
|
250
|
+
data.each do |edge_name, count|
|
|
251
|
+
print_output io, count, edge_name
|
|
252
|
+
end
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
end
|
|
256
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "objspace"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
|
|
6
|
+
require "heap_profiler/version"
|
|
7
|
+
require "heap_profiler/reporter"
|
|
8
|
+
|
|
9
|
+
module HeapProfiler
|
|
10
|
+
Error = Class.new(StandardError)
|
|
11
|
+
CapacityError = Class.new(Error)
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
attr_accessor :current_reporter
|
|
15
|
+
|
|
16
|
+
def start(dir, **kwargs)
|
|
17
|
+
return if current_reporter
|
|
18
|
+
self.current_reporter = Reporter.new(dir)
|
|
19
|
+
current_reporter.start(**kwargs)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def stop
|
|
23
|
+
current_reporter&.stop
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def report(dir, &block)
|
|
27
|
+
Reporter.new(dir).run(&block)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: heap-profiler
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.8.0.rc1
|
|
5
|
+
platform: aarch64-linux
|
|
6
|
+
authors:
|
|
7
|
+
- Jean Boussier
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: exe
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-01-14 00:00:00.000000000 Z
|
|
12
|
+
dependencies: []
|
|
13
|
+
description: Make several heap dumps and summarize allocated, retained memory
|
|
14
|
+
email:
|
|
15
|
+
- jean.boussier@gmail.com
|
|
16
|
+
executables:
|
|
17
|
+
- heap-profiler
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- ".github/workflows/cibuildgem.yaml"
|
|
22
|
+
- ".github/workflows/tests.yml"
|
|
23
|
+
- ".gitignore"
|
|
24
|
+
- ".rubocop.yml"
|
|
25
|
+
- ".ruby-version"
|
|
26
|
+
- Gemfile
|
|
27
|
+
- Gemfile.lock
|
|
28
|
+
- LICENSE.txt
|
|
29
|
+
- README.md
|
|
30
|
+
- Rakefile
|
|
31
|
+
- TODO.md
|
|
32
|
+
- benchmark/address-parsing.rb
|
|
33
|
+
- benchmark/indexing.rb
|
|
34
|
+
- bin/console
|
|
35
|
+
- bin/generate-report
|
|
36
|
+
- bin/rubocop
|
|
37
|
+
- bin/setup
|
|
38
|
+
- bin/testunit
|
|
39
|
+
- dev.yml
|
|
40
|
+
- exe/heap-profiler
|
|
41
|
+
- ext/heap_profiler/extconf.rb
|
|
42
|
+
- ext/heap_profiler/heap_profiler.cpp
|
|
43
|
+
- ext/heap_profiler/simdjson.cpp
|
|
44
|
+
- ext/heap_profiler/simdjson.h
|
|
45
|
+
- heap-profiler.gemspec
|
|
46
|
+
- lib/heap-profiler.rb
|
|
47
|
+
- lib/heap_profiler/3.1/heap_profiler.so
|
|
48
|
+
- lib/heap_profiler/3.2/heap_profiler.so
|
|
49
|
+
- lib/heap_profiler/3.3/heap_profiler.so
|
|
50
|
+
- lib/heap_profiler/3.4/heap_profiler.so
|
|
51
|
+
- lib/heap_profiler/4.0/heap_profiler.so
|
|
52
|
+
- lib/heap_profiler/analyzer.rb
|
|
53
|
+
- lib/heap_profiler/cli.rb
|
|
54
|
+
- lib/heap_profiler/diff.rb
|
|
55
|
+
- lib/heap_profiler/dump.rb
|
|
56
|
+
- lib/heap_profiler/full.rb
|
|
57
|
+
- lib/heap_profiler/index.rb
|
|
58
|
+
- lib/heap_profiler/monochrome.rb
|
|
59
|
+
- lib/heap_profiler/parser.rb
|
|
60
|
+
- lib/heap_profiler/polychrome.rb
|
|
61
|
+
- lib/heap_profiler/reporter.rb
|
|
62
|
+
- lib/heap_profiler/results.rb
|
|
63
|
+
- lib/heap_profiler/runtime.rb
|
|
64
|
+
- lib/heap_profiler/version.rb
|
|
65
|
+
homepage: https://github.com/Shopify/heap-profiler
|
|
66
|
+
licenses:
|
|
67
|
+
- MIT
|
|
68
|
+
metadata:
|
|
69
|
+
allowed_push_host: https://rubygems.org/
|
|
70
|
+
homepage_uri: https://github.com/Shopify/heap-profiler
|
|
71
|
+
source_code_uri: https://github.com/Shopify/heap-profiler
|
|
72
|
+
post_install_message:
|
|
73
|
+
rdoc_options: []
|
|
74
|
+
require_paths:
|
|
75
|
+
- lib
|
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: '3.1'
|
|
81
|
+
- - "<"
|
|
82
|
+
- !ruby/object:Gem::Version
|
|
83
|
+
version: 4.1.dev
|
|
84
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
85
|
+
requirements:
|
|
86
|
+
- - ">"
|
|
87
|
+
- !ruby/object:Gem::Version
|
|
88
|
+
version: 1.3.1
|
|
89
|
+
requirements: []
|
|
90
|
+
rubygems_version: 3.3.27
|
|
91
|
+
signing_key:
|
|
92
|
+
specification_version: 4
|
|
93
|
+
summary: Ruby heap profiling tool
|
|
94
|
+
test_files: []
|