vernier 1.4.0 → 1.6.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.
@@ -29,8 +29,24 @@ module Vernier
29
29
  def self.stop
30
30
  result = @collector.stop
31
31
  @collector = nil
32
+
32
33
  output_path = options[:output]
33
- output_path ||= Tempfile.create(["profile", ".vernier.json.gz"]).path
34
+ unless output_path
35
+ output_dir = options[:output_dir]
36
+ unless output_dir
37
+ if File.writable?(".")
38
+ output_dir = "."
39
+ else
40
+ output_dir = Dir.tmpdir
41
+ end
42
+ end
43
+ prefix = "profile-"
44
+ timestamp = Time.now.strftime("%Y%m%d-%H%M%S")
45
+ suffix = ".vernier.json.gz"
46
+
47
+ output_path = File.expand_path("#{output_dir}/#{prefix}#{timestamp}-#{$$}#{suffix}")
48
+ end
49
+
34
50
  result.write(out: output_path)
35
51
 
36
52
  STDERR.puts(result.inspect)
@@ -31,6 +31,8 @@ module Vernier
31
31
  case hook.to_sym
32
32
  when :rails, :activesupport
33
33
  @hooks << Vernier::Hooks::ActiveSupport.new(self)
34
+ when :memory_usage
35
+ @hooks << Vernier::Hooks::MemoryUsage.new(self)
34
36
  else
35
37
  warn "unknown hook: #{hook.inspect}"
36
38
  end
@@ -97,18 +99,44 @@ module Vernier
97
99
 
98
100
  marker_strings = Marker.name_table
99
101
 
100
- markers = self.markers.map do |(tid, type, phase, ts, te, stack, extra_info)|
101
- name = marker_strings[type]
102
- sym = Marker::MARKER_SYMBOLS[type]
103
- data = { type: sym }
104
- data[:cause] = { stack: stack } if stack
105
- data.merge!(extra_info) if extra_info
106
- [tid, name, ts, te, phase, data]
102
+ markers_by_thread_id = (@markers || []).group_by(&:first)
103
+
104
+ result.threads.each do |tid, thread|
105
+ last_fiber = nil
106
+ markers = []
107
+
108
+ markers.concat markers_by_thread_id.fetch(tid, [])
109
+
110
+ original_markers = thread[:markers] || []
111
+ original_markers += result.gc_markers || []
112
+ original_markers.each do |data|
113
+ type, phase, ts, te, stack, extra_info = data
114
+ if type == Marker::Type::FIBER_SWITCH
115
+ if last_fiber
116
+ start_event = markers[last_fiber]
117
+ markers << [nil, "Fiber Running", start_event[2], ts, Marker::Phase::INTERVAL, start_event[5].merge(type: "Fiber Running", cause: nil)]
118
+ end
119
+ last_fiber = markers.size
120
+ end
121
+ name = marker_strings[type]
122
+ sym = Marker::MARKER_SYMBOLS[type]
123
+ data = { type: sym }
124
+ data[:cause] = { stack: stack } if stack
125
+ data.merge!(extra_info) if extra_info
126
+ markers << [tid, name, ts, te, phase, data]
127
+ end
128
+ if last_fiber
129
+ end_time = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
130
+ start_event = markers[last_fiber]
131
+ markers << [nil, "Fiber Running", start_event[2], end_time, Marker::Phase::INTERVAL, start_event[5].merge(type: "Fiber Running", cause: nil)]
132
+ end
133
+
134
+ thread[:markers] = markers
107
135
  end
108
136
 
109
- markers.concat @markers
137
+ #markers.concat @markers
110
138
 
111
- result.instance_variable_set(:@markers, markers)
139
+ #result.instance_variable_set(:@markers, markers)
112
140
 
113
141
  if @out
114
142
  result.write(out: @out)
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vernier
4
+ module Hooks
5
+ class MemoryUsage
6
+ def initialize(collector)
7
+ @collector = collector
8
+ @tracker = Vernier::MemoryTracker.new
9
+ end
10
+
11
+ def enable
12
+ @tracker.start
13
+ end
14
+
15
+ def disable
16
+ @tracker.stop
17
+ end
18
+
19
+ def firefox_counters
20
+ timestamps, memory = @tracker.results
21
+ memory = ([0] + memory).each_cons(2).map { _2 - _1 }
22
+ {
23
+ name: "memory",
24
+ category: "Memory",
25
+ description: "Memory usage in bytes",
26
+ pid: Process.pid,
27
+ mainThreadIndex: 0,
28
+ samples: {
29
+ time: timestamps.map { _1 / 1_000_000.0 },
30
+ count: memory,
31
+ length: timestamps.length
32
+ }
33
+ }
34
+ end
35
+ end
36
+ end
37
+ end
data/lib/vernier/hooks.rb CHANGED
@@ -3,5 +3,6 @@
3
3
  module Vernier
4
4
  module Hooks
5
5
  autoload :ActiveSupport, "vernier/hooks/active_support"
6
+ autoload :MemoryUsage, "vernier/hooks/memory_usage"
6
7
  end
7
8
  end
@@ -26,6 +26,8 @@ module Vernier
26
26
  MARKER_STRINGS[Type::THREAD_STALLED] = "Thread Stalled"
27
27
  MARKER_STRINGS[Type::THREAD_SUSPENDED] = "Thread Suspended"
28
28
 
29
+ MARKER_STRINGS[Type::FIBER_SWITCH] = "Fiber Switch"
30
+
29
31
  MARKER_STRINGS.freeze
30
32
 
31
33
  ##
@@ -1,6 +1,6 @@
1
1
  module Vernier
2
2
  class Middleware
3
- def initialize(app, permit: ->(_) { true })
3
+ def initialize(app, permit: ->(_env) { true })
4
4
  @app = app
5
5
  @permit = permit
6
6
  end
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "filename_filter"
4
+ require "cgi/util"
5
+
6
+ module Vernier
7
+ module Output
8
+ class FileListing
9
+ class SamplesByLocation
10
+ attr_accessor :self, :total
11
+ def initialize
12
+ @self = @total = 0
13
+ 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
+ end
22
+
23
+ def initialize(profile)
24
+ @profile = profile
25
+ end
26
+
27
+ def samples_by_file
28
+ thread = @profile.main_thread
29
+ if Hash === thread
30
+ # live profile
31
+ stack_table = @profile._stack_table
32
+ filename_filter = FilenameFilter.new
33
+ else
34
+ stack_table = thread.stack_table
35
+ filename_filter = ->(x) { x }
36
+ end
37
+
38
+ weights = thread[:weights]
39
+ samples = thread[:samples]
40
+
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
49
+
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
57
+
58
+ samples_by_file = Hash.new do |h, k|
59
+ h[k] = Hash.new do |h2, k2|
60
+ h2[k2] = SamplesByLocation.new
61
+ end
62
+ end
63
+
64
+ self_samples_by_frame.each do |frame, samples|
65
+ line = stack_table.frame_line_no(frame)
66
+ func_index = stack_table.frame_func_idx(frame)
67
+ filename = stack_table.func_filename(func_index)
68
+
69
+ samples_by_file[filename][line] += samples
70
+ end
71
+
72
+ samples_by_file.transform_keys! do |filename|
73
+ filename_filter.call(filename)
74
+ end
75
+ end
76
+
77
+ def output(template: nil)
78
+ output = +""
79
+
80
+ relevant_files = samples_by_file.select do |k, v|
81
+ next if k.start_with?("gem:")
82
+ next if k.start_with?("rubylib:")
83
+ next if k.start_with?("<")
84
+ v.values.map(&:total).sum > total * 0.01
85
+ end
86
+
87
+ if template == "html"
88
+ html_output(output, relevant_files)
89
+ else
90
+ relevant_files.keys.sort.each do |filename|
91
+ output << "="*80 << "\n"
92
+ output << filename << "\n"
93
+ output << "-"*80 << "\n"
94
+ format_file(output, filename, samples_by_file, total: total)
95
+ end
96
+ output << "="*80 << "\n"
97
+ end
98
+ end
99
+
100
+ def total
101
+ thread = @profile.main_thread
102
+ thread[:weights].sum
103
+ end
104
+
105
+ def format_file(output, filename, all_samples, total:)
106
+ samples = all_samples[filename]
107
+
108
+ # file_name, lines, file_wall, file_cpu, file_idle, file_sort
109
+ output << sprintf(" TOTAL | SELF | LINE SOURCE\n")
110
+ File.readlines(filename).each_with_index do |line, i|
111
+ lineno = i + 1
112
+ calls = samples[lineno]
113
+
114
+ if calls && calls.total > 0
115
+ output << sprintf("%5.1f%% | %5.1f%% | % 4i %s", 100 * calls.total / total.to_f, 100 * calls.self / total.to_f, lineno, line)
116
+ else
117
+ output << sprintf(" | | % 4i %s", lineno, line)
118
+ end
119
+ end
120
+ end
121
+
122
+ def html_output(output, relevant_files)
123
+ output << "<pre>"
124
+ output << " SELF FILE\n"
125
+ relevant_files.sort_by {|k, v| -v.values.map(&:self).sum }.each do |filename, file_contents|
126
+ tmpl = "<details style=\"display:inline-block;vertical-align:top;\"><summary>%s</summary>"
127
+ output << sprintf("% 5.1f%% #{tmpl}\n", file_contents.values.map(&:self).sum * 100 / total.to_f, filename)
128
+ format_file_html(output, filename, relevant_files)
129
+ output << "</details>\n"
130
+ end
131
+ output << "</pre>"
132
+ end
133
+
134
+ def format_file_html(output, filename, relevant_files)
135
+ samples = relevant_files[filename]
136
+
137
+ # file_name, lines, file_wall, file_cpu, file_idle, file_sort
138
+ output << sprintf(" TOTAL | SELF | LINE SOURCE\n")
139
+ File.readlines(filename).each_with_index do |line, i|
140
+ lineno = i + 1
141
+ calls = samples[lineno]
142
+
143
+ if calls && calls.total > 0
144
+ output << sprintf("%5.1f%% | %5.1f%% | % 4i %s", 100 * calls.total / total.to_f, 100 * calls.self / total.to_f, lineno, CGI::escapeHTML(line))
145
+ else
146
+ output << sprintf(" | | % 4i %s", lineno, CGI::escapeHTML(line))
147
+ end
148
+ end
149
+ end
150
+ end
151
+ end
152
+ end
@@ -0,0 +1,30 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Vernier
4
+ module Output
5
+ class FilenameFilter
6
+ def initialize
7
+ @pwd = "#{Dir.pwd}/"
8
+ @gem_regex = %r{\A#{Regexp.union(Gem.path)}/gems/}
9
+ @gem_match_regex = %r{\A#{Regexp.union(Gem.path)}/gems/([a-zA-Z](?:[a-zA-Z0-9\.\_]|-[a-zA-Z])*)-([0-9][0-9A-Za-z\-_\.]*)/(.*)\z}
10
+ @rubylibdir = "#{RbConfig::CONFIG["rubylibdir"]}/"
11
+ end
12
+
13
+ attr_reader :pwd, :gem_regex, :gem_match_regex, :rubylibdir
14
+
15
+ def call(filename)
16
+ if filename.match?(gem_regex)
17
+ gem_match_regex =~ filename
18
+ "gem:#$1-#$2:#$3"
19
+ elsif filename.start_with?(pwd)
20
+ filename.delete_prefix(pwd)
21
+ elsif filename.start_with?(rubylibdir)
22
+ path = filename.delete_prefix(rubylibdir)
23
+ "rubylib:#{RUBY_VERSION}:#{path}"
24
+ else
25
+ filename
26
+ end
27
+ end
28
+ end
29
+ end
30
+ end
@@ -3,6 +3,8 @@
3
3
  require "json"
4
4
  require "rbconfig"
5
5
 
6
+ require_relative "filename_filter"
7
+
6
8
  module Vernier
7
9
  module Output
8
10
  # https://profiler.firefox.com/
@@ -104,15 +106,15 @@ module Vernier
104
106
  attr_reader :profile
105
107
 
106
108
  def data
107
- markers_by_thread = profile.markers.group_by { |marker| marker[0] }
109
+ #markers_by_thread = profile.markers.group_by { |marker| marker[0] }
108
110
 
109
111
  threads = profile.threads.map do |ruby_thread_id, thread_info|
110
- markers = markers_by_thread[ruby_thread_id] || []
112
+ #markers = markers_by_thread[ruby_thread_id] || []
111
113
  Thread.new(
112
114
  ruby_thread_id,
113
115
  profile,
114
116
  @categorizer,
115
- markers: markers,
117
+ #markers: markers,
116
118
  **thread_info,
117
119
  )
118
120
  end
@@ -126,7 +128,7 @@ module Vernier
126
128
  product: "Ruby/Vernier",
127
129
  stackwalk: 1,
128
130
  version: 28,
129
- preprocessedProfileVersion: 47,
131
+ preprocessedProfileVersion: 48,
130
132
  symbolicated: true,
131
133
  markerSchema: marker_schema,
132
134
  sampleUnits: {
@@ -145,11 +147,20 @@ module Vernier
145
147
  initialVisibleThreads: threads.each_index.to_a,
146
148
  initialSelectedThreads: Array(threads.find_index(&:is_start))
147
149
  },
150
+ counters: counter_data,
148
151
  libs: [],
149
152
  threads: threads.map(&:data)
150
153
  }
151
154
  end
152
155
 
156
+ def counter_data
157
+ profile.hooks.flat_map do |hook|
158
+ if hook.respond_to?(:firefox_counters)
159
+ hook.firefox_counters
160
+ end
161
+ end.compact
162
+ end
163
+
153
164
  def marker_schema
154
165
  hook_additions = profile.hooks.flat_map do |hook|
155
166
  if hook.respond_to?(:firefox_marker_schema)
@@ -201,6 +212,18 @@ module Vernier
201
212
  { key: "gc_by", format: "string" },
202
213
  ]
203
214
  },
215
+ {
216
+ name: "FIBER_SWITCH",
217
+ display: [ "marker-chart", "marker-table", "timeline-overview" ],
218
+ tooltipLabel: "{marker.name} - {marker.data.fiber_id}",
219
+ data: [
220
+ {
221
+ label: "Description",
222
+ value: "Switch running Fiber"
223
+ },
224
+ { key: "fiber_id", format: "integer" },
225
+ ]
226
+ },
204
227
  *hook_additions
205
228
  ]
206
229
  end
@@ -303,23 +326,9 @@ module Vernier
303
326
  end
304
327
 
305
328
  def filter_filenames(filenames)
306
- pwd = "#{Dir.pwd}/"
307
- gem_regex = %r{\A#{Regexp.union(Gem.path)}/gems/}
308
- gem_match_regex = %r{\A#{Regexp.union(Gem.path)}/gems/([a-zA-Z](?:[a-zA-Z0-9\.\_]|-[a-zA-Z])*)-([0-9][0-9A-Za-z\-_\.]*)/(.*)\z}
309
- rubylibdir = "#{RbConfig::CONFIG["rubylibdir"]}/"
310
-
329
+ filter = FilenameFilter.new
311
330
  filenames.map do |filename|
312
- if filename.match?(gem_regex)
313
- gem_match_regex =~ filename
314
- "gem:#$1-#$2:#$3"
315
- elsif filename.start_with?(pwd)
316
- filename.delete_prefix(pwd)
317
- elsif filename.start_with?(rubylibdir)
318
- path = filename.delete_prefix(rubylibdir)
319
- "rubylib:#{RUBY_VERSION}:#{path}"
320
- else
321
- filename
322
- end
331
+ filter.call(filename)
323
332
  end
324
333
  end
325
334
 
@@ -478,17 +487,21 @@ module Vernier
478
487
  def frame_table
479
488
  funcs = @stack_table_hash[:frame_table].fetch(:func)
480
489
  lines = @stack_table_hash[:frame_table].fetch(:line)
481
- size = funcs.length
490
+ raise unless lines.size == funcs.size
491
+
492
+ size = funcs.size
482
493
  none = [nil] * size
483
- categories = @frame_categories.map(&:idx)
494
+ default = [0] * size
495
+ unidentified = [-1] * size
484
496
 
485
- raise unless lines.size == funcs.size
497
+ categories = @frame_categories.map(&:idx)
498
+ subcategories = @frame_subcategories
486
499
 
487
500
  {
488
- address: [-1] * size,
489
- inlineDepth: [0] * size,
501
+ address: unidentified,
502
+ inlineDepth: default,
490
503
  category: categories,
491
- subcategory: nil,
504
+ subcategory: subcategories,
492
505
  func: funcs,
493
506
  nativeSymbol: none,
494
507
  innerWindowID: none,
@@ -7,23 +7,75 @@ module Vernier
7
7
  @profile = profile
8
8
  end
9
9
 
10
+ class Table
11
+ def initialize(header)
12
+ @header = header
13
+ @rows = []
14
+ yield self
15
+ end
16
+
17
+ def <<(row)
18
+ @rows << row
19
+ end
20
+
21
+ def to_s
22
+ (
23
+ [
24
+ row_separator,
25
+ format_row(@header),
26
+ row_separator
27
+ ] + @rows.map do |row|
28
+ format_row(row)
29
+ end + [row_separator]
30
+ ).join("\n")
31
+ end
32
+
33
+ def widths
34
+ @widths ||=
35
+ (@rows + [@header]).transpose.map do |col|
36
+ col.map(&:size).max
37
+ end
38
+ end
39
+
40
+ def row_separator
41
+ @row_separator = "+" + widths.map { |i| "-" * (i + 2) }.join("+") + "+"
42
+ end
43
+
44
+ def format_row(row)
45
+ "|" + row.map.with_index { |str, i| " " + str.ljust(widths[i] + 1) }.join("|") + "|"
46
+ end
47
+ end
48
+
10
49
  def output
50
+ thread = @profile.main_thread
51
+ stack_table =
52
+ if thread.respond_to?(:stack_table)
53
+ thread.stack_table
54
+ else
55
+ @profile._stack_table
56
+ end
57
+
11
58
  stack_weights = Hash.new(0)
12
- @profile.samples.zip(@profile.weights) do |stack_idx, weight|
59
+ thread[:samples].zip(thread[:weights]) do |stack_idx, weight|
13
60
  stack_weights[stack_idx] += weight
14
61
  end
15
62
 
63
+ total = stack_weights.values.sum
64
+
16
65
  top_by_self = Hash.new(0)
17
66
  stack_weights.each do |stack_idx, weight|
18
- stack = @profile.stack(stack_idx)
19
- top_by_self[stack.leaf_frame.name] += weight
67
+ frame_idx = stack_table.stack_frame_idx(stack_idx)
68
+ func_idx = stack_table.frame_func_idx(frame_idx)
69
+ name = stack_table.func_name(func_idx)
70
+ top_by_self[name] += weight
20
71
  end
21
72
 
22
- s = +""
23
- top_by_self.sort_by(&:last).reverse.each do |frame, samples|
24
- s << "#{samples}\t#{frame}\n"
25
- end
26
- s
73
+ Table.new %w[Samples % name] do |t|
74
+ top_by_self.sort_by(&:last).reverse.each do |frame, samples|
75
+ pct = 100.0 * samples / total
76
+ t << [samples.to_s, pct.round(1).to_s, frame]
77
+ end
78
+ end.to_s
27
79
  end
28
80
  end
29
81
  end
@@ -0,0 +1,102 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "json"
4
+ require_relative "stack_table_helpers"
5
+
6
+ module Vernier
7
+ class ParsedProfile
8
+ def self.read_file(filename)
9
+ # Print the inverted tree from a Vernier profile
10
+ is_gzip = File.binread(filename, 2) == "\x1F\x8B".b # check for gzip header
11
+
12
+ json = if is_gzip
13
+ require "zlib"
14
+ Zlib::GzipReader.open(filename) { |gz| gz.read }
15
+ else
16
+ File.read filename
17
+ end
18
+
19
+ info = JSON.load json
20
+
21
+ new(info)
22
+ end
23
+
24
+ class StackTable
25
+ def initialize(thread_data)
26
+ @stack_parents = thread_data["stackTable"]["prefix"]
27
+ @stack_frames = thread_data["stackTable"]["frame"]
28
+ @frame_funcs = thread_data["frameTable"]["func"]
29
+ @frame_lines = thread_data["frameTable"]["line"]
30
+ @func_names = thread_data["funcTable"]["name"]
31
+ @func_filenames = thread_data["funcTable"]["fileName"]
32
+ #@func_first_linenos = thread_data["funcTable"]["first"]
33
+ @strings = thread_data["stringArray"]
34
+ end
35
+
36
+ attr_reader :strings
37
+
38
+ def stack_count = @stack_parents.length
39
+ def frame_count = @frame_funcs.length
40
+ def func_count = @func_names.length
41
+
42
+ def stack_parent_idx(idx) = @stack_parents[idx]
43
+ def stack_frame_idx(idx) = @stack_frames[idx]
44
+
45
+ def frame_func_idx(idx) = @frame_funcs[idx]
46
+ def frame_line_no(idx) = @frame_lines[idx]
47
+
48
+ def func_name_idx(idx) = @func_names[idx]
49
+ def func_filename_idx(idx) = @func_filenames[idx]
50
+ def func_name(idx) = @strings[func_name_idx(idx)]
51
+ def func_filename(idx) = @strings[func_filename_idx(idx)]
52
+ def func_first_lineno(idx) = @func_first_lineno[idx]
53
+
54
+ include StackTableHelpers
55
+ end
56
+
57
+ class Thread
58
+ attr_reader :data
59
+
60
+ def initialize(data)
61
+ @data = data
62
+ end
63
+
64
+ def stack_table
65
+ @stack_table ||= StackTable.new(@data)
66
+ end
67
+
68
+ def main_thread?
69
+ @data["isMainThread"]
70
+ end
71
+
72
+ def samples
73
+ @data["samples"]["stack"]
74
+ end
75
+
76
+ def weights
77
+ @data["samples"]["weight"]
78
+ end
79
+
80
+ # Emulate hash
81
+ def [](name)
82
+ send(name)
83
+ end
84
+ end
85
+
86
+ attr_reader :data
87
+ def initialize(data)
88
+ @data = data
89
+ end
90
+
91
+ def threads
92
+ @threads ||=
93
+ @data["threads"].map do |thread_data|
94
+ Thread.new(thread_data)
95
+ end
96
+ end
97
+
98
+ def main_thread
99
+ threads.detect(&:main_thread?)
100
+ end
101
+ end
102
+ end