heap-profiler 0.2.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -3,7 +3,7 @@ require_relative 'lib/heap_profiler/version'
3
3
 
4
4
  Gem::Specification.new do |spec|
5
5
  spec.name = "heap-profiler"
6
- spec.version = Heap::Profiler::VERSION
6
+ spec.version = HeapProfiler::VERSION
7
7
  spec.authors = ["Jean Boussier"]
8
8
  spec.email = ["jean.boussier@gmail.com"]
9
9
 
@@ -11,9 +11,6 @@ module HeapProfiler
11
11
 
12
12
  def process(_index, object)
13
13
  @objects += 1
14
- unless object[:memsize]
15
- p object
16
- end
17
14
  @memory += object[:memsize]
18
15
  end
19
16
 
@@ -1,4 +1,5 @@
1
1
  # frozen_string_literal: true
2
+ require 'optparse'
2
3
 
3
4
  module HeapProfiler
4
5
  class CLI
@@ -7,13 +8,21 @@ module HeapProfiler
7
8
  end
8
9
 
9
10
  def run
10
- if @argv.size == 1
11
- print_report(@argv.first)
12
- 0
13
- else
14
- print_usage
15
- 1
11
+ parser.parse!(@argv)
12
+
13
+ begin
14
+ if @argv.size == 1
15
+ print_report(@argv.first)
16
+ return 0
17
+ end
18
+ rescue CapacityError => error
19
+ STDERR.puts(error.message)
20
+ STDERR.puts("Current size: #{Parser.batch_size}B")
21
+ STDERR.puts("Try increasing it with --batch-size")
22
+ STDERR.puts
16
23
  end
24
+ print_usage
25
+ 1
17
26
  end
18
27
 
19
28
  def print_report(path)
@@ -28,5 +37,51 @@ module HeapProfiler
28
37
  def print_usage
29
38
  puts "Usage: #{$PROGRAM_NAME} directory_or_heap_dump"
30
39
  end
40
+
41
+ SIZE_UNITS = {
42
+ 'B' => 1,
43
+ 'K' => 1_000,
44
+ 'M' => 1_000_000,
45
+ 'G' => 1_000_000_000,
46
+ }
47
+ def parse_byte_size(size_string)
48
+ if (match = size_string.match(/\A(\d+)(\w)?B?\z/i))
49
+ digits = Float(match[1])
50
+ base = 1
51
+ unit = match[2]&.upcase
52
+ if unit
53
+ base = SIZE_UNITS.fetch(unit) { raise ArgumentError, "Unknown size unit: #{unit}" }
54
+ end
55
+ size = (digits * base).to_i
56
+ if size > 4_000_000_000
57
+ raise ArgumentError, "Batch size can't be bigger than 4G"
58
+ end
59
+ size
60
+ else
61
+ raise ArgumentError, "#{size_string} is not a valid size"
62
+ end
63
+ end
64
+
65
+ private
66
+
67
+ def parser
68
+ @parser ||= OptionParser.new do |opts|
69
+ opts.banner = "Usage: heap-profiler [ARGS]"
70
+ opts.separator ""
71
+ opts.separator "GLOBAL OPTIONS"
72
+ opts.separator ""
73
+
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.
77
+ EOS
78
+ opts.on('--batch-size SIZE', help.strip) do |size_string|
79
+ HeapProfiler::Parser.batch_size = parse_byte_size(size_string)
80
+ rescue ArgumentError => error
81
+ STDERR.puts "Invalid batch-size: #{error.message}"
82
+ exit 1
83
+ end
84
+ end
85
+ end
31
86
  end
32
87
  end
@@ -61,8 +61,8 @@ module HeapProfiler
61
61
  end
62
62
  end
63
63
 
64
- def each_object(since: 0, &block)
65
- Parser.load_many(path, since: since, batch_size: 10_000_000, &block)
64
+ def each_object(since: nil, &block)
65
+ Parser.load_many(path, since: since, &block)
66
66
  end
67
67
 
68
68
  def stats
@@ -34,7 +34,7 @@ module HeapProfiler
34
34
  }.freeze
35
35
 
36
36
  IMEMO_TYPES = Hash.new { |h, k| h[k] = "<#{k || 'unknown'}> (IMEMO)" }
37
- DATA_TYPES = Hash.new { |h, k| h[k] = "<#{(k || 'unknown')}> (DATA)" }
37
+ DATA_TYPES = Hash.new { |h, k| h[k] = "<#{k || 'unknown'}> (DATA)" }
38
38
 
39
39
  def guess_class(object)
40
40
  type = object[:type]
@@ -44,14 +44,19 @@ module HeapProfiler
44
44
 
45
45
  return IMEMO_TYPES[object[:imemo_type]] if type == :IMEMO
46
46
 
47
- class_address = object[:class]
48
- return unless class_address
47
+ class_name = if (class_address = object[:class])
48
+ @classes.fetch(class_address) do
49
+ return DATA_TYPES[object[:struct]] if type == :DATA
49
50
 
50
- @classes.fetch(class_address) do
51
- return DATA_TYPES[object[:struct]] if type == :DATA
51
+ $stderr.puts("WARNING: Couldn't infer class name of: #{object.inspect}")
52
+ nil
53
+ end
54
+ end
52
55
 
53
- $stderr.puts("WARNING: Couldn't infer class name of: #{object.inspect}")
54
- nil
56
+ if type == :DATA && (class_name.nil? || class_name == "Object")
57
+ DATA_TYPES[object[:struct]]
58
+ else
59
+ class_name
55
60
  end
56
61
  end
57
62
 
@@ -2,18 +2,33 @@
2
2
 
3
3
  module HeapProfiler
4
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
+
5
12
  class Ruby
6
13
  def build_index(path)
7
14
  require 'json'
8
15
  classes_index = {}
16
+ classes_index.default_proc = CLASS_DEFAULT_PROC
9
17
  strings_index = {}
10
18
 
11
19
  File.open(path).each_line do |line|
12
20
  object = JSON.parse(line, symbolize_names: true)
13
21
  case object[:type]
14
22
  when 'MODULE', 'CLASS'
15
- if (name = object[:name])
16
- classes_index[parse_address(object[:address])] = name
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
17
32
  end
18
33
  when 'STRING'
19
34
  next if object[:shared]
@@ -32,13 +47,13 @@ module HeapProfiler
32
47
  end
33
48
 
34
49
  class Native
35
- DEFAULT_BATCH_SIZE = 10_000_000 # 10MB
36
-
37
- def build_index(path, batch_size: DEFAULT_BATCH_SIZE)
38
- _build_index(path, batch_size)
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
39
54
  end
40
55
 
41
- def load_many(path, since: nil, batch_size: DEFAULT_BATCH_SIZE, &block)
56
+ def load_many(path, since: nil, batch_size: Parser.batch_size, &block)
42
57
  _load_many(path, since, batch_size, &block)
43
58
  end
44
59
  end
@@ -9,8 +9,6 @@ module HeapProfiler
9
9
  # So we name them at the start of the profile to avoid that.
10
10
  #
11
11
  # See: https://github.com/ruby/ruby/pull/3349
12
- #
13
- # TODO: Could we actually do the dump ourselves? objspace is a extension already.
14
12
  if RUBY_VERSION < '2.8'
15
13
  def name_anonymous_modules!
16
14
  ObjectSpace.each_object(Module) do |mod|
@@ -56,10 +54,14 @@ module HeapProfiler
56
54
  def stop
57
55
  HeapProfiler.name_anonymous_modules!
58
56
  ObjectSpace.trace_object_allocations_stop if @enable_tracing
57
+
58
+ # we can't use partial dump for allocated.heap, because we need old generations
59
+ # as well to build the classes and strings indexes.
59
60
  dump_heap(@allocated_heap)
61
+
60
62
  GC.enable
61
63
  4.times { GC.start }
62
- dump_heap(@retained_heap)
64
+ dump_heap(@retained_heap, partial: true)
63
65
  @allocated_heap.close
64
66
  @retained_heap.close
65
67
  write_info("generation", @generation.to_s)
@@ -84,17 +86,24 @@ module HeapProfiler
84
86
  File.write(File.join(@dir_path, "#{key}.info"), value)
85
87
  end
86
88
 
87
- # ObjectSpace.dump_all does allocate a few objects in itself (https://bugs.ruby-lang.org/issues/17045)
88
- # because of this even en empty block of code will report a handful of allocations.
89
- # To filter them more easily we attribute call `dump_all` from a method with a very specific `file`
90
- # property.
91
- class_eval <<~RUBY, '__hprof', __LINE__
92
- # frozen_string_literal: true
93
- def dump_heap(file)
94
- ObjectSpace.dump_all(output: file)
89
+ if RUBY_VERSION >= '3.0'
90
+ def dump_heap(file, partial: false)
91
+ ObjectSpace.dump_all(output: file, since: partial ? @generation : nil)
95
92
  file.close
96
93
  end
97
- RUBY
94
+ else
95
+ # ObjectSpace.dump_all does allocate a few objects in itself (https://bugs.ruby-lang.org/issues/17045)
96
+ # because of this even en empty block of code will report a handful of allocations.
97
+ # To filter them more easily we attribute call `dump_all` from a method with a very specific `file`
98
+ # property.
99
+ class_eval <<~RUBY, '__hprof', __LINE__
100
+ # frozen_string_literal: true
101
+ def dump_heap(file, partial: false)
102
+ ObjectSpace.dump_all(output: file)
103
+ file.close
104
+ end
105
+ RUBY
106
+ end
98
107
 
99
108
  def open_heap(name)
100
109
  File.open(File.join(@dir_path, "#{name}.heap"), 'w+')
@@ -8,6 +8,7 @@ require "heap_profiler/reporter"
8
8
 
9
9
  module HeapProfiler
10
10
  Error = Class.new(StandardError)
11
+ CapacityError = Class.new(Error)
11
12
 
12
13
  class << self
13
14
  attr_accessor :current_reporter
@@ -1,6 +1,4 @@
1
1
  # frozen_string_literal: true
2
- module Heap
3
- module Profiler
4
- VERSION = "0.2.0"
5
- end
2
+ module HeapProfiler
3
+ VERSION = "0.5.0"
6
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.2.0
4
+ version: 0.5.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: 2020-08-19 00:00:00.000000000 Z
11
+ date: 2021-08-05 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Make several heap dumps and summarize allocated, retained memory
14
14
  email:
@@ -79,7 +79,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
79
79
  - !ruby/object:Gem::Version
80
80
  version: '0'
81
81
  requirements: []
82
- rubygems_version: 3.1.2
82
+ rubygems_version: 3.2.20
83
83
  signing_key:
84
84
  specification_version: 4
85
85
  summary: Ruby heap profiling tool