heap-profiler 0.2.1 → 0.3.0

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