vernier 1.10.0 → 1.11.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: a504e7ef885ed393a864128d15ca792af4e6ab60f30cdf90afbc36aa5d577b34
4
- data.tar.gz: 87ab42b6cc3ea1effae71f3b95bdc4d90c117abf1aadc3d425bb30b4573dafa9
3
+ metadata.gz: 6e8d610b954485d3bccefa47408932dddd5806b618caa2958d689f8c0c8fbb3d
4
+ data.tar.gz: 2423bf5e06dd1168e21f0a5063050e897137c37bd09ce68a540af9d84b0a7546
5
5
  SHA512:
6
- metadata.gz: f52d1d392009084c1511b7b2cc24c51fdb5da983d0fe1dd60286ed9fe0564229ec409513cd179f2af3ceb96f9a085e2f9f48b556ac5ae6b597aba7c13bd56cf8
7
- data.tar.gz: 7b8255043801305c221a967a25974ff1e5896393adf8c820784dd980a2eeecfa49f63509b7eeddf1c0f81a7279c29ea87ffa88622de306d0d5b430c23a40c109
6
+ metadata.gz: 942b097d843b63457650254b2e9717698939df9ee40c02ff367832d344121e6ffdafffbc2e0824d70229bf5d4187d1cf0eb64491402a4c54c4c5f6dd8fb0b98f
7
+ data.tar.gz: 2f710c6b9eeb8e4e352290902aed1a4142ea5b48df7039ee49de66dc7c9f7411e25cb77319addba69a15f072f08b3820a51535fe19070a4e77341aa6c000a57b
data/.ruby-version CHANGED
@@ -1 +1 @@
1
- 3.4.7
1
+ 4.0
data/Gemfile CHANGED
@@ -12,3 +12,5 @@ gem "rake-compiler"
12
12
  gem "benchmark-ips"
13
13
 
14
14
  gem "minitest", "~> 5.0"
15
+
16
+ gem "zlib", ">= 3.2.1"
@@ -138,6 +138,9 @@ class HeapTracker {
138
138
  if (RTEST(tp_newobj)) {
139
139
  rb_tracepoint_disable(tp_newobj);
140
140
  tp_newobj = Qnil;
141
+ if (tombstones * 10 > object_list.size()) {
142
+ rebuild();
143
+ }
141
144
  }
142
145
  }
143
146
 
@@ -29,6 +29,7 @@ module Vernier
29
29
  result.instance_variable_set(:@threads, {
30
30
  0 => {
31
31
  tid: 0,
32
+ is_main: true,
32
33
  name: "custom",
33
34
  started_at: @started_at,
34
35
  samples: @samples,
@@ -80,6 +81,7 @@ module Vernier
80
81
  result.instance_variable_set(:@threads, {
81
82
  0 => {
82
83
  tid: 0,
84
+ is_main: true,
83
85
  name: "retained memory",
84
86
  started_at: @started_at,
85
87
  samples: samples,
@@ -1,8 +1,12 @@
1
1
  require "json"
2
2
 
3
+ require_relative "../output_helpers"
4
+
3
5
  module Vernier
4
6
  module Output
5
7
  class Cpuprofile
8
+ include OutputHelpers
9
+
6
10
  def initialize(profile)
7
11
  @profile = profile
8
12
  end
@@ -98,7 +102,9 @@ module Vernier
98
102
  line = stack_table.frame_line_no(frame_idx) - 1
99
103
 
100
104
  func_name = stack_table.func_name(func_idx)
105
+ func_name = sanitize_string(func_name) if func_name
101
106
  filename = stack_table.func_filename(func_idx)
107
+ filename = sanitize_string(filename) if filename
102
108
 
103
109
  node_id = nodes.length
104
110
  node = {
@@ -1,23 +1,19 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require_relative "filename_filter"
4
+ require_relative "../output_helpers"
4
5
  require "cgi/escape"
5
6
 
6
7
  module Vernier
7
8
  module Output
8
9
  class FileListing
10
+ include OutputHelpers
11
+
9
12
  class SamplesByLocation
10
13
  attr_accessor :self, :total
11
14
  def initialize
12
15
  @self = @total = 0
13
16
  end
14
-
15
- def +(other)
16
- ret = SamplesByLocation.new
17
- ret.self = @self + other.self
18
- ret.total = @total + other.total
19
- ret
20
- end
21
17
  end
22
18
 
23
19
  def initialize(profile)
@@ -25,6 +21,8 @@ module Vernier
25
21
  end
26
22
 
27
23
  def samples_by_file
24
+ return @samples_by_file if defined?(@samples_by_file)
25
+
28
26
  thread = @profile.main_thread
29
27
  if Hash === thread
30
28
  # live profile
@@ -38,22 +36,9 @@ module Vernier
38
36
  weights = thread[:weights]
39
37
  samples = thread[:samples]
40
38
 
41
- self_samples_by_frame = Hash.new do |h, k|
42
- h[k] = SamplesByLocation.new
43
- end
44
-
45
- samples.zip(weights).each do |stack_idx, weight|
46
- # self time
47
- top_frame_index = stack_table.stack_frame_idx(stack_idx)
48
- self_samples_by_frame[top_frame_index].self += weight
39
+ stack_weights = collapse_stack_weights(samples, weights)
49
40
 
50
- # total time
51
- while stack_idx
52
- frame_idx = stack_table.stack_frame_idx(stack_idx)
53
- self_samples_by_frame[frame_idx].total += weight
54
- stack_idx = stack_table.stack_parent_idx(stack_idx)
55
- end
56
- end
41
+ self_by_frame, total_by_frame = frame_weights(stack_table, stack_weights)
57
42
 
58
43
  samples_by_file = Hash.new do |h, k|
59
44
  h[k] = Hash.new do |h2, k2|
@@ -61,19 +46,64 @@ module Vernier
61
46
  end
62
47
  end
63
48
 
64
- self_samples_by_frame.each do |frame, samples|
49
+ self_by_frame.each_with_index do |self_weight, frame|
50
+ total_weight = total_by_frame[frame]
51
+ next if self_weight == 0 && total_weight == 0
52
+
65
53
  line = stack_table.frame_line_no(frame)
66
54
  func_index = stack_table.frame_func_idx(frame)
67
55
  filename = stack_table.func_filename(func_index)
68
56
 
69
- samples_by_file[filename][line] += samples
57
+ location = samples_by_file[filename][line]
58
+ location.self += self_weight
59
+ location.total += total_weight
70
60
  end
71
61
 
72
- samples_by_file.transform_keys! do |filename|
62
+ @samples_by_file = samples_by_file.transform_keys! do |filename|
73
63
  filename_filter.call(filename)
74
64
  end
75
65
  end
76
66
 
67
+ # Returns [self_weight, total_weight] arrays indexed by frame, from a
68
+ # hash of per-stack weights.
69
+ #
70
+ # The stack table is a prefix tree whose parent is always interned
71
+ # before its children (parent idx < child idx), so total weights are
72
+ # accumulated bottom-up in a single reverse pass.
73
+ private def frame_weights(stack_table, stack_weights)
74
+ # Scatter the sparse weights into an array indexed by stack.
75
+ stack_totals = Array.new(stack_table.stack_count, 0)
76
+ stack_weights.each do |stack_idx, weight|
77
+ stack_totals[stack_idx] = weight
78
+ end
79
+
80
+ # Accumulate each stack's total into its parent, children first,
81
+ # so a single descending pass completes every subtree's total.
82
+ (stack_totals.length - 1).downto(0) do |stack_idx|
83
+ parent_idx = stack_table.stack_parent_idx(stack_idx)
84
+ next unless parent_idx
85
+
86
+ if parent_idx >= stack_idx
87
+ raise "Invalid profile: stack table is not parent-before-child ordered"
88
+ end
89
+
90
+ stack_totals[parent_idx] += stack_totals[stack_idx]
91
+ end
92
+
93
+ self_by_frame = Array.new(stack_table.frame_count, 0)
94
+ total_by_frame = Array.new(stack_table.frame_count, 0)
95
+ stack_weights.each do |stack_idx, weight|
96
+ self_by_frame[stack_table.stack_frame_idx(stack_idx)] += weight
97
+ end
98
+ stack_totals.each_with_index do |total, stack_idx|
99
+ next if total == 0
100
+
101
+ total_by_frame[stack_table.stack_frame_idx(stack_idx)] += total
102
+ end
103
+
104
+ [self_by_frame, total_by_frame]
105
+ end
106
+
77
107
  def output(template: nil)
78
108
  output = +""
79
109
 
@@ -98,8 +128,7 @@ module Vernier
98
128
  end
99
129
 
100
130
  def total
101
- thread = @profile.main_thread
102
- thread[:weights].sum
131
+ @total ||= @profile.main_thread[:weights].sum
103
132
  end
104
133
 
105
134
  def format_file(output, filename, all_samples, total:)
@@ -4,6 +4,7 @@ require "json"
4
4
  require "rbconfig"
5
5
 
6
6
  require_relative "filename_filter"
7
+ require_relative "../output_helpers"
7
8
 
8
9
  module Vernier
9
10
  module Output
@@ -267,6 +268,13 @@ module Vernier
267
268
  end
268
269
 
269
270
  class Thread
271
+ include OutputHelpers
272
+
273
+ SAMPLE_CATEGORY_NAMES = {
274
+ 1 => "Idle",
275
+ 2 => "Stalled"
276
+ }.freeze
277
+
270
278
  attr_reader :profile, :is_start
271
279
 
272
280
  def initialize(ruby_thread_id, profile, categorizer, name:, tid:, samples:, weights:, timestamps: nil, sample_categories: nil, markers:, started_at:, stopped_at: nil, allocations: nil, is_main: nil, is_start: nil)
@@ -355,6 +363,31 @@ module Vernier
355
363
  @frame_subcategories = @stack_table_hash[:frame_table].fetch(:func).map do |func_idx|
356
364
  func_subcategories[func_idx]
357
365
  end
366
+
367
+ @sample_category_idx = SAMPLE_CATEGORY_NAMES.transform_values do |name|
368
+ @categorizer.get_category(name).idx
369
+ end
370
+
371
+ @samples.zip(@sample_categories).each do |sample, raw_category|
372
+ next if raw_category == 0
373
+
374
+ @categorized_stacks[[sample, raw_category]]
375
+ end
376
+
377
+ base_stack_frames = @stack_table_hash[:stack_table].fetch(:frame)
378
+ base_frame_count = @stack_table_hash[:frame_table].fetch(:func).size
379
+ @extra_frames = []
380
+ @categorized_frame_map = {}
381
+
382
+ @categorized_stacks.each_key do |(stack, raw_category)|
383
+ original_frame_idx = base_stack_frames[stack]
384
+ key = [original_frame_idx, raw_category]
385
+ next if @categorized_frame_map.key?(key)
386
+
387
+ new_frame_idx = base_frame_count + @extra_frames.size
388
+ @categorized_frame_map[key] = new_frame_idx
389
+ @extra_frames << [original_frame_idx, @sample_category_idx[raw_category]]
390
+ end
358
391
  end
359
392
 
360
393
  def categorize_filename(filename)
@@ -377,7 +410,7 @@ module Vernier
377
410
  def find_category_and_subcategory(filename, categories)
378
411
  categories.each do |category_name|
379
412
  category = @categorizer.get_category(category_name)
380
- subcategory = category.subcategories.detect {|c| c.matches?(filename) }&.idx
413
+ subcategory = category.subcategories.detect { |c| c.matches?(filename) }&.idx
381
414
  return category, subcategory if subcategory
382
415
  end
383
416
  [nil, nil]
@@ -430,7 +463,7 @@ module Vernier
430
463
  categories = []
431
464
  data = []
432
465
 
433
- @markers.each_with_index do |(_, name, start, finish, phase, datum), i|
466
+ @markers.each do |(_, name, start, finish, phase, datum)|
434
467
  string_indexes << @strings[name]
435
468
  start_times << (start / 1_000_000.0)
436
469
 
@@ -463,12 +496,14 @@ module Vernier
463
496
  end
464
497
 
465
498
  def allocations_table
466
- return nil if !@allocations
499
+ return nil unless @allocations
500
+
467
501
  samples, weights, timestamps = @allocations.values_at(:samples, :weights, :timestamps)
468
- return nil if samples.size == 0
502
+ return nil if samples.empty?
503
+
469
504
  size = samples.size
470
505
  timestamps = timestamps.map { _1 / 1_000_000.0 }
471
- ret = {
506
+ {
472
507
  "time": timestamps,
473
508
  "className": ["Object"]*size,
474
509
  "typeName": ["JSObject"]*size,
@@ -478,7 +513,6 @@ module Vernier
478
513
  "stack": samples,
479
514
  "length": size
480
515
  }
481
- ret
482
516
  end
483
517
 
484
518
  def samples_table
@@ -518,21 +552,22 @@ module Vernier
518
552
  end
519
553
 
520
554
  def stack_table
521
- frames = @stack_table_hash[:stack_table].fetch(:frame).dup
555
+ base_frames = @stack_table_hash[:stack_table].fetch(:frame)
556
+ frames = base_frames.dup
522
557
  prefixes = @stack_table_hash[:stack_table].fetch(:parent).dup
523
- categories = frames.map{|idx| @frame_categories[idx].idx }
524
- subcategories = frames.map{|idx| @frame_subcategories[idx] }
558
+ categories = frames.map { |idx| @frame_categories[idx].idx }
559
+ subcategories = frames.map { |idx| @frame_subcategories[idx] }
525
560
 
526
- @categorized_stacks.each_key do |(stack, category)|
527
- frames << frames[stack]
561
+ @categorized_stacks.each_key do |(stack, raw_category)|
562
+ original_frame_idx = base_frames[stack]
563
+ frames << @categorized_frame_map[[original_frame_idx, raw_category]]
528
564
  prefixes << prefixes[stack]
529
- categories << category
565
+ categories << @sample_category_idx[raw_category]
530
566
  subcategories << 0
531
567
  end
532
568
 
533
- size = frames.length
534
- raise unless frames.size == size
535
- raise unless prefixes.size == size
569
+ raise unless prefixes.size == frames.size
570
+
536
571
  {
537
572
  frame: frames,
538
573
  category: categories,
@@ -543,18 +578,27 @@ module Vernier
543
578
  end
544
579
 
545
580
  def frame_table
546
- funcs = @stack_table_hash[:frame_table].fetch(:func)
547
- lines = @stack_table_hash[:frame_table].fetch(:line)
581
+ funcs = @stack_table_hash[:frame_table].fetch(:func).dup
582
+ lines = @stack_table_hash[:frame_table].fetch(:line).dup
548
583
  raise unless lines.size == funcs.size
549
584
 
585
+ categories = @frame_categories.map(&:idx)
586
+ subcategories = @frame_subcategories.dup
587
+ implementations = @frame_implementations.dup
588
+
589
+ @extra_frames.each do |(frame_idx, category_idx)|
590
+ funcs << funcs[frame_idx]
591
+ lines << lines[frame_idx]
592
+ categories << category_idx
593
+ subcategories << 0
594
+ implementations << implementations[frame_idx]
595
+ end
596
+
550
597
  size = funcs.size
551
598
  none = [nil] * size
552
599
  default = [0] * size
553
600
  unidentified = [-1] * size
554
601
 
555
- categories = @frame_categories.map(&:idx)
556
- subcategories = @frame_subcategories
557
-
558
602
  {
559
603
  address: unidentified,
560
604
  inlineDepth: default,
@@ -563,7 +607,7 @@ module Vernier
563
607
  func: funcs,
564
608
  nativeSymbol: none,
565
609
  innerWindowID: none,
566
- implementation: @frame_implementations,
610
+ implementation: implementations,
567
611
  line: lines,
568
612
  column: none,
569
613
  length: size
@@ -597,21 +641,7 @@ module Vernier
597
641
 
598
642
  def string_table
599
643
  @strings.keys.map do |string|
600
- if string.ascii_only?
601
- string
602
- elsif string.encoding == Encoding::UTF_8
603
- if string.valid_encoding?
604
- string
605
- else
606
- string.scrub
607
- end
608
- elsif string.encoding == Encoding::BINARY
609
- # TODO: We might want to guess UTF-8 and escape the binary more explicitly
610
- string.dup.force_encoding("UTF-8").scrub
611
- else
612
- # TODO: ideally we should attempt to properly re-encode here, but right now I think this is dead code
613
- string.dup.force_encoding("UTF-8").scrub
614
- end
644
+ sanitize_string(string)
615
645
  end
616
646
  end
617
647
 
@@ -1,8 +1,12 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../output_helpers"
4
+
3
5
  module Vernier
4
6
  module Output
5
7
  class Top
8
+ include OutputHelpers
9
+
6
10
  def initialize(profile, row_limit)
7
11
  @profile = profile
8
12
  @row_limit = row_limit
@@ -57,10 +61,10 @@ module Vernier
57
61
  @profile._stack_table
58
62
  end
59
63
 
60
- stack_weights = Hash.new(0)
61
- thread[:samples].zip(thread[:weights]) do |stack_idx, weight|
62
- stack_weights[stack_idx] += weight
63
- end
64
+ samples = thread[:samples]
65
+ weights = thread[:weights]
66
+
67
+ stack_weights = collapse_stack_weights(samples, weights)
64
68
 
65
69
  total = stack_weights.values.sum
66
70
 
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vernier
4
+ module OutputHelpers
5
+ # Collapses parallel samples/weights arrays into a hash of total weight
6
+ # per stack index. Retained-mode profiles have one sample per object and
7
+ # many samples share the same stack, so collapsing first keeps later
8
+ # aggregation proportional to the number of unique stacks.
9
+ def collapse_stack_weights(samples, weights)
10
+ stack_weights = Hash.new(0)
11
+ samples.each_with_index do |stack_idx, idx|
12
+ stack_weights[stack_idx] += weights[idx]
13
+ end
14
+ stack_weights
15
+ end
16
+
17
+ # Returns a string safe to embed in JSON output: valid UTF-8 with any
18
+ # invalid bytes replaced. Method names and paths can carry arbitrary
19
+ # encodings (e.g. BINARY or Shift_JIS source), which would otherwise
20
+ # raise JSON::GeneratorError.
21
+ def sanitize_string(string)
22
+ if string.ascii_only?
23
+ string
24
+ elsif string.encoding == Encoding::UTF_8
25
+ string.valid_encoding? ? string : string.scrub
26
+ else
27
+ # We don't know the real encoding; interpret the bytes as UTF-8
28
+ # and replace anything invalid.
29
+ string.dup.force_encoding(Encoding::UTF_8).scrub
30
+ end
31
+ end
32
+ end
33
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vernier
4
- VERSION = "1.10.0"
4
+ VERSION = "1.11.0"
5
5
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: vernier
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.10.0
4
+ version: 1.11.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - John Hawthorn
@@ -106,6 +106,7 @@ files:
106
106
  - lib/vernier/output/firefox.rb
107
107
  - lib/vernier/output/markdown.rb
108
108
  - lib/vernier/output/top.rb
109
+ - lib/vernier/output_helpers.rb
109
110
  - lib/vernier/parsed_profile.rb
110
111
  - lib/vernier/result.rb
111
112
  - lib/vernier/stack_table.rb
@@ -134,7 +135,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
134
135
  - !ruby/object:Gem::Version
135
136
  version: '0'
136
137
  requirements: []
137
- rubygems_version: 4.0.3
138
+ rubygems_version: 4.0.16
138
139
  specification_version: 4
139
140
  summary: A next generation CRuby profiler
140
141
  test_files: []