vernier 1.4.0 → 1.5.0

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