vernier 1.4.0 → 1.5.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
  ##
@@ -0,0 +1,113 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "filename_filter"
4
+
5
+ module Vernier
6
+ module Output
7
+ class FileListing
8
+ class SamplesByLocation
9
+ attr_accessor :self, :total
10
+ def initialize
11
+ @self = @total = 0
12
+ end
13
+
14
+ def +(other)
15
+ ret = SamplesByLocation.new
16
+ ret.self = @self + other.self
17
+ ret.total = @total + other.total
18
+ ret
19
+ end
20
+ end
21
+
22
+ def initialize(profile)
23
+ @profile = profile
24
+ end
25
+
26
+ def output
27
+ output = +""
28
+
29
+ thread = @profile.main_thread
30
+ if Hash === thread
31
+ # live profile
32
+ stack_table = @profile._stack_table
33
+ weights = thread[:weights]
34
+ samples = thread[:samples]
35
+ filename_filter = FilenameFilter.new
36
+ else
37
+ stack_table = thread.stack_table
38
+ weights = thread.weights
39
+ samples = thread.samples
40
+ filename_filter = ->(x) { x }
41
+ end
42
+
43
+ self_samples_by_frame = Hash.new do |h, k|
44
+ h[k] = SamplesByLocation.new
45
+ end
46
+
47
+ total = weights.sum
48
+
49
+ samples.zip(weights).each do |stack_idx, weight|
50
+ # self time
51
+ top_frame_index = stack_table.stack_frame_idx(stack_idx)
52
+ self_samples_by_frame[top_frame_index].self += weight
53
+
54
+ # total time
55
+ while stack_idx
56
+ frame_idx = stack_table.stack_frame_idx(stack_idx)
57
+ self_samples_by_frame[frame_idx].total += weight
58
+ stack_idx = stack_table.stack_parent_idx(stack_idx)
59
+ end
60
+ end
61
+
62
+ samples_by_file = Hash.new do |h, k|
63
+ h[k] = Hash.new do |h2, k2|
64
+ h2[k2] = SamplesByLocation.new
65
+ end
66
+ end
67
+
68
+ self_samples_by_frame.each do |frame, samples|
69
+ line = stack_table.frame_line_no(frame)
70
+ func_index = stack_table.frame_func_idx(frame)
71
+ filename = stack_table.func_filename(func_index)
72
+
73
+ samples_by_file[filename][line] += samples
74
+ end
75
+
76
+ samples_by_file.transform_keys! do |filename|
77
+ filename_filter.call(filename)
78
+ end
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
+ relevant_files.keys.sort.each do |filename|
87
+ output << "="*80 << "\n"
88
+ output << filename << "\n"
89
+ output << "-"*80 << "\n"
90
+ format_file(output, filename, samples_by_file, total: total)
91
+ end
92
+ output << "="*80 << "\n"
93
+ end
94
+
95
+ def format_file(output, filename, all_samples, total:)
96
+ samples = all_samples[filename]
97
+
98
+ # file_name, lines, file_wall, file_cpu, file_idle, file_sort
99
+ output << sprintf(" TOTAL | SELF | LINE SOURCE\n")
100
+ File.readlines(filename).each_with_index do |line, i|
101
+ lineno = i + 1
102
+ calls = samples[lineno]
103
+
104
+ if calls && calls.total > 0
105
+ output << sprintf("%5.1f%% | %5.1f%% | % 4i %s", 100 * calls.total / total.to_f, 100 * calls.self / total.to_f, lineno, line)
106
+ else
107
+ output << sprintf(" | | % 4i %s", lineno, line)
108
+ end
109
+ end
110
+ end
111
+ end
112
+ end
113
+ 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
 
@@ -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
@@ -1,14 +1,9 @@
1
1
  module Vernier
2
2
  class Result
3
- def stack_table=(stack_table)
4
- @stack_table = stack_table
5
- end
6
-
7
- def _stack_table
8
- @stack_table
9
- end
3
+ attr_accessor :stack_table
4
+ alias _stack_table stack_table
10
5
 
11
- attr_reader :markers
6
+ attr_reader :gc_markers
12
7
 
13
8
  attr_accessor :hooks
14
9
 
@@ -60,91 +55,8 @@ module Vernier
60
55
  end
61
56
  end
62
57
 
63
- class BaseType
64
- attr_reader :result, :idx
65
- def initialize(result, idx)
66
- @result = result
67
- @idx = idx
68
- end
69
-
70
- def to_s
71
- idx.to_s
72
- end
73
-
74
- def inspect
75
- "#<#{self.class}\n#{to_s}>"
76
- end
77
- end
78
-
79
- class Func < BaseType
80
- def label
81
- result._stack_table.func_name(idx)
82
- end
83
- alias name label
84
-
85
- def filename
86
- result._stack_table.func_filename(idx)
87
- end
88
-
89
- def to_s
90
- "#{name} at #{filename}"
91
- end
92
- end
93
-
94
- class Frame < BaseType
95
- def label; func.label; end
96
- def filename; func.filename; end
97
- alias name label
98
-
99
- def func
100
- func_idx = result._stack_table.frame_func_idx(idx)
101
- Func.new(result, func_idx)
102
- end
103
-
104
- def line
105
- result._stack_table.frame_line_no(idx)
106
- end
107
-
108
- def to_s
109
- "#{func}:#{line}"
110
- end
111
- end
112
-
113
- class Stack < BaseType
114
- def each_frame
115
- return enum_for(__method__) unless block_given?
116
-
117
- stack_idx = idx
118
- while stack_idx
119
- frame_idx = result._stack_table.stack_frame_idx(stack_idx)
120
- yield Frame.new(result, frame_idx)
121
- stack_idx = result._stack_table.stack_parent_idx(stack_idx)
122
- end
123
- end
124
-
125
- def leaf_frame_idx
126
- result._stack_table.stack_frame_idx(idx)
127
- end
128
-
129
- def leaf_frame
130
- Frame.new(result, leaf_frame_idx)
131
- end
132
-
133
- def frames
134
- each_frame.to_a
135
- end
136
-
137
- def to_s
138
- arr = []
139
- each_frame do |frame|
140
- arr << frame.to_s
141
- end
142
- arr.join("\n")
143
- end
144
- end
145
-
146
58
  def stack(idx)
147
- Stack.new(self, idx)
59
+ stack_table.stack(idx)
148
60
  end
149
61
 
150
62
  def total_bytes