heap-profiler 0.2.1 → 0.3.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b1e0a1ab7e684de76d217df0eef23331ab4ab32a2b252766999db26ce73670c2
4
- data.tar.gz: 50209c2943414c65167307a67e9454807619e8a9a43cbf74c9a09933c2eabba3
3
+ metadata.gz: e84c345fb6a29dbb5da52ce051ae7ca87b220adaea020458b4fc44f9343ad2d2
4
+ data.tar.gz: ca8aa6b5a67209444ece911c598ff4d0fc075bc68274a2507273bf1f6b35ee8c
5
5
  SHA512:
6
- metadata.gz: 703961acc8ac3cea16270b0905599edb4a2cd954b1ce7de57cd0291bf24e74a2eec5df6d0241bc67c34f8d8b4a92792a13159b38e907962ce64e31588854706e
7
- data.tar.gz: 615928e9bea64f63fe1810c01592047cc70fc3ba118760b96e1991240002d7bbe72cec029ec5e841c9659c405d645972e7c0187bb7cb00e49f3cb3564df16291
6
+ metadata.gz: cd7ff25272e3ebf873603eb88db635772e0efc0642f6fde9addebb534a15c4ab9c4aa10271ebb4efed72be4c9dc0ee434e17d57fe2c5410d10353a1bf6c89ce1
7
+ data.tar.gz: '09bc159b740eb05dd2eb335308ab3f5b4b4842beef822014ac2525f7e7354cac607246cd6095d040853f96fc7171cca4bf4def21df044e4de2615af1a0525be9'
@@ -33,7 +33,7 @@ jobs:
33
33
  runs-on: ubuntu-latest
34
34
  strategy:
35
35
  matrix:
36
- ruby: [ '2.5', '2.6', '2.7' ]
36
+ ruby: [ '2.5', '2.6', '2.7', '3.0' ]
37
37
  name: Ruby ${{ matrix.ruby }} Tests
38
38
  steps:
39
39
  - name: Checkout code
@@ -16,5 +16,8 @@ Naming/FileName:
16
16
  Lint/RescueException:
17
17
  Enabled: false
18
18
 
19
+ Lint/MissingSuper:
20
+ Enabled: false
21
+
19
22
  Metrics/ParameterLists:
20
23
  Enabled: false
@@ -1,43 +1,43 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- heap-profiler (0.2.1)
4
+ heap-profiler (0.3.0)
5
5
 
6
6
  GEM
7
7
  remote: https://rubygems.org/
8
8
  specs:
9
9
  ast (2.4.1)
10
- benchmark-ips (2.8.2)
10
+ benchmark-ips (2.8.4)
11
11
  byebug (11.1.3)
12
- minitest (5.14.1)
13
- parallel (1.19.2)
14
- parser (2.7.1.4)
12
+ minitest (5.14.3)
13
+ parallel (1.20.1)
14
+ parser (3.0.0.0)
15
15
  ast (~> 2.4.1)
16
16
  rainbow (3.0.0)
17
- rake (13.0.1)
17
+ rake (13.0.3)
18
18
  rake-compiler (1.1.1)
19
19
  rake
20
- regexp_parser (1.7.1)
20
+ regexp_parser (2.0.3)
21
21
  rexml (3.2.4)
22
- rubocop (0.86.0)
22
+ rubocop (1.8.1)
23
23
  parallel (~> 1.10)
24
- parser (>= 2.7.0.1)
24
+ parser (>= 3.0.0.0)
25
25
  rainbow (>= 2.2.2, < 4.0)
26
- regexp_parser (>= 1.7)
26
+ regexp_parser (>= 1.8, < 3.0)
27
27
  rexml
28
- rubocop-ast (>= 0.0.3, < 1.0)
28
+ rubocop-ast (>= 1.2.0, < 2.0)
29
29
  ruby-progressbar (~> 1.7)
30
- unicode-display_width (>= 1.4.0, < 2.0)
31
- rubocop-ast (0.2.0)
32
- parser (>= 2.7.0.1)
33
- rubocop-shopify (1.0.4)
34
- rubocop (>= 0.85, < 0.87)
35
- ruby-progressbar (1.10.1)
36
- stackprof (0.2.15)
37
- unicode-display_width (1.7.0)
30
+ unicode-display_width (>= 1.4.0, < 3.0)
31
+ rubocop-ast (1.4.0)
32
+ parser (>= 2.7.1.5)
33
+ rubocop-shopify (1.0.7)
34
+ rubocop (~> 1.4)
35
+ ruby-progressbar (1.11.0)
36
+ stackprof (0.2.16)
37
+ unicode-display_width (2.0.0)
38
38
 
39
39
  PLATFORMS
40
- ruby
40
+ x86_64-darwin-19
41
41
 
42
42
  DEPENDENCIES
43
43
  benchmark-ips
@@ -50,4 +50,4 @@ DEPENDENCIES
50
50
  stackprof
51
51
 
52
52
  BUNDLED WITH
53
- 2.1.4
53
+ 2.2.5
@@ -2,6 +2,8 @@
2
2
 
3
3
  require "mkmf"
4
4
 
5
+ have_func("rb_enc_interned_str", "ruby.h")
6
+
5
7
  $CXXFLAGS += ' -O3 -std=c++1z -Wno-register '
6
8
 
7
9
  create_makefile 'heap_profiler/heap_profiler'
@@ -1,4 +1,5 @@
1
1
  #include "ruby.h"
2
+ #include "ruby/encoding.h"
2
3
  #include "simdjson.h"
3
4
  #include <fstream>
4
5
 
@@ -6,7 +7,7 @@ using namespace simdjson;
6
7
 
7
8
  static VALUE rb_eHeapProfilerError, sym_type, sym_class, sym_address, sym_value,
8
9
  sym_memsize, sym_imemo_type, sym_struct, sym_file, sym_line, sym_shared,
9
- sym_references;
10
+ sym_references, id_uminus;
10
11
 
11
12
  typedef struct {
12
13
  dom::parser *parser;
@@ -83,6 +84,24 @@ static inline int64_t parse_dom_address(dom::element element) {
83
84
  return parse_address(address);
84
85
  }
85
86
 
87
+ static inline VALUE make_symbol(std::string_view string) {
88
+ return ID2SYM(rb_intern2(string.data(), string.size()));
89
+ }
90
+
91
+ static inline VALUE make_string(std::string_view string) {
92
+ return rb_utf8_str_new(string.data(), string.size());
93
+ }
94
+
95
+ # ifdef HAVE_RB_ENC_INTERNED_STR
96
+ static inline VALUE dedup_string(std::string_view string) {
97
+ return rb_enc_interned_str(string.data(), string.size(), rb_utf8_encoding());
98
+ }
99
+ # else
100
+ static inline VALUE dedup_string(std::string_view string) {
101
+ return rb_funcall(make_string(string), id_uminus, 0);
102
+ }
103
+ # endif
104
+
86
105
  static VALUE rb_heap_build_index(VALUE self, VALUE path, VALUE batch_size) {
87
106
  Check_Type(path, T_STRING);
88
107
  Check_Type(batch_size, T_FIXNUM);
@@ -107,14 +126,31 @@ static VALUE rb_heap_build_index(VALUE self, VALUE path, VALUE batch_size) {
107
126
  std::string_view value;
108
127
  if (!object["value"].get(value)) {
109
128
  VALUE address = INT2FIX(parse_dom_address(object["address"]));
110
- VALUE string = rb_utf8_str_new(value.data(), value.size());
129
+ VALUE string = make_string(value);
111
130
  rb_hash_aset(string_index, address, string);
112
131
  }
113
132
  } else if (type == "CLASS" || type == "MODULE") {
133
+ VALUE address = INT2FIX(parse_dom_address(object["address"]));
134
+ VALUE class_name = Qfalse;
135
+
114
136
  std::string_view name;
115
137
  if (!object["name"].get(name)) {
116
- VALUE address = INT2FIX(parse_dom_address(object["address"]));
117
- VALUE class_name = rb_utf8_str_new(name.data(), name.size());
138
+ class_name = dedup_string(name);
139
+ } else {
140
+ std::string_view file;
141
+ uint64_t line;
142
+
143
+ if (!object["file"].get(file) && !object["line"].get(line)) {
144
+ std::string buffer = "<Class ";
145
+ buffer += file;
146
+ buffer += ":";
147
+ buffer += std::to_string(line);
148
+ buffer += ">";
149
+ class_name = dedup_string(buffer);
150
+ }
151
+ }
152
+
153
+ if (RTEST(class_name)) {
118
154
  rb_hash_aset(class_index, address, class_name);
119
155
  }
120
156
  }
@@ -140,7 +176,7 @@ static VALUE make_ruby_object(dom::object object)
140
176
 
141
177
  std::string_view type;
142
178
  if (!object["type"].get(type)) {
143
- rb_hash_aset(hash, sym_type, ID2SYM(rb_intern2(type.data(), type.size())));
179
+ rb_hash_aset(hash, sym_type, make_symbol(type));
144
180
  }
145
181
 
146
182
  std::string_view address;
@@ -164,17 +200,17 @@ static VALUE make_ruby_object(dom::object object)
164
200
  if (type == "IMEMO") {
165
201
  std::string_view imemo_type;
166
202
  if (!object["imemo_type"].get(imemo_type)) {
167
- rb_hash_aset(hash, sym_imemo_type, ID2SYM(rb_intern2(imemo_type.data(), imemo_type.size())));
203
+ rb_hash_aset(hash, sym_imemo_type, make_symbol(imemo_type));
168
204
  }
169
205
  } else if (type == "DATA") {
170
206
  std::string_view _struct;
171
207
  if (!object["struct"].get(_struct)) {
172
- rb_hash_aset(hash, sym_struct, ID2SYM(rb_intern2(_struct.data(), _struct.size())));
208
+ rb_hash_aset(hash, sym_struct, make_symbol(_struct));
173
209
  }
174
210
  } else if (type == "STRING") {
175
211
  std::string_view value;
176
212
  if (!object["value"].get(value)) {
177
- rb_hash_aset(hash, sym_value, rb_utf8_str_new(value.data(), value.size()));
213
+ rb_hash_aset(hash, sym_value, make_string(value));
178
214
  }
179
215
 
180
216
  bool shared;
@@ -196,7 +232,7 @@ static VALUE make_ruby_object(dom::object object)
196
232
 
197
233
  std::string_view file;
198
234
  if (!object["file"].get(file)) {
199
- rb_hash_aset(hash, sym_file, rb_utf8_str_new(file.data(), file.size()));
235
+ rb_hash_aset(hash, sym_file, dedup_string(file));
200
236
  }
201
237
 
202
238
  uint64_t line;
@@ -261,6 +297,7 @@ extern "C" {
261
297
  sym_line = ID2SYM(rb_intern("line"));
262
298
  sym_shared = ID2SYM(rb_intern("shared"));
263
299
  sym_references = ID2SYM(rb_intern("references"));
300
+ id_uminus = rb_intern("-@");
264
301
 
265
302
  VALUE rb_mHeapProfiler = rb_const_get(rb_cObject, rb_intern("HeapProfiler"));
266
303
 
@@ -61,7 +61,7 @@ module HeapProfiler
61
61
  end
62
62
  end
63
63
 
64
- def each_object(since: 0, &block)
64
+ def each_object(since: nil, &block)
65
65
  Parser.load_many(path, since: since, batch_size: 10_000_000, &block)
66
66
  end
67
67
 
@@ -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,15 @@ 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
+ 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
52
-
53
- $stderr.puts("WARNING: Couldn't infer class name of: #{object.inspect}")
54
- nil
51
+ $stderr.puts("WARNING: Couldn't infer class name of: #{object.inspect}")
52
+ nil
53
+ end
54
+ elsif type == :DATA
55
+ DATA_TYPES[object[:struct]]
55
56
  end
56
57
  end
57
58
 
@@ -2,18 +2,28 @@
2
2
 
3
3
  module HeapProfiler
4
4
  module Parser
5
+ CLASS_DEFAULT_PROC = ->(_hash, key) { "<Class#0x#{key.to_s(16)}>" }
6
+
5
7
  class Ruby
6
8
  def build_index(path)
7
9
  require 'json'
8
10
  classes_index = {}
11
+ classes_index.default_proc = CLASS_DEFAULT_PROC
9
12
  strings_index = {}
10
13
 
11
14
  File.open(path).each_line do |line|
12
15
  object = JSON.parse(line, symbolize_names: true)
13
16
  case object[:type]
14
17
  when 'MODULE', 'CLASS'
15
- if (name = object[:name])
16
- classes_index[parse_address(object[:address])] = name
18
+ address = parse_address(object[:address])
19
+
20
+ name = object[:name]
21
+ name ||= if object[:file] && object[:line]
22
+ "<Class #{object[:file]}:#{object[:line]}>"
23
+ end
24
+
25
+ if name
26
+ classes_index[address] = name
17
27
  end
18
28
  when 'STRING'
19
29
  next if object[:shared]
@@ -35,7 +45,9 @@ module HeapProfiler
35
45
  DEFAULT_BATCH_SIZE = 10_000_000 # 10MB
36
46
 
37
47
  def build_index(path, batch_size: DEFAULT_BATCH_SIZE)
38
- _build_index(path, batch_size)
48
+ indexes = _build_index(path, batch_size)
49
+ indexes.first.default_proc = CLASS_DEFAULT_PROC
50
+ indexes
39
51
  end
40
52
 
41
53
  def load_many(path, since: nil, batch_size: DEFAULT_BATCH_SIZE, &block)
@@ -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+')
@@ -1,4 +1,4 @@
1
1
  # frozen_string_literal: true
2
2
  module HeapProfiler
3
- VERSION = "0.2.1"
3
+ VERSION = "0.3.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.2.1
4
+ version: 0.3.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-01-12 00:00:00.000000000 Z
11
+ date: 2021-01-26 00:00:00.000000000 Z
12
12
  dependencies: []
13
13
  description: Make several heap dumps and summarize allocated, retained memory
14
14
  email: