vernier 0.2.1 → 0.3.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,65 @@
1
+ require "tempfile"
2
+ require "vernier"
3
+
4
+ module Vernier
5
+ module Autorun
6
+ class << self
7
+ attr_accessor :collector
8
+ attr_reader :options
9
+ end
10
+ @collector = nil
11
+
12
+ @options = ENV.to_h.select { |k,v| k.start_with?("VERNIER_") }
13
+ @options.transform_keys! do |key|
14
+ key.sub(/\AVERNIER_/, "").downcase.to_sym
15
+ end
16
+ @options.freeze
17
+
18
+ def self.start
19
+ interval = options.fetch(:interval, 500).to_i
20
+
21
+ STDERR.puts("starting profiler with interval #{interval}")
22
+
23
+ @collector = Vernier::Collector.new(:wall, interval:)
24
+ @collector.start
25
+ end
26
+
27
+ def self.stop
28
+ result = @collector.stop
29
+ @collector = nil
30
+ output_path = options[:output]
31
+ output_path ||= Tempfile.create(["profile", ".vernier.json"]).path
32
+ File.write(output_path, Vernier::Output::Firefox.new(result).output)
33
+
34
+ STDERR.puts(result.inspect)
35
+ STDERR.puts("written to #{output_path}")
36
+ end
37
+
38
+ def self.running?
39
+ !!@collector
40
+ end
41
+
42
+ def self.at_exit
43
+ stop if running?
44
+ end
45
+
46
+ def self.toggle
47
+ running? ? stop : start
48
+ end
49
+ end
50
+ end
51
+
52
+ unless Vernier::Autorun.options[:start_paused]
53
+ Vernier::Autorun.start
54
+ end
55
+
56
+ if signal = Vernier::Autorun.options[:signal]
57
+ STDERR.puts "to toggle profiler: kill -#{signal} #{Process.pid}"
58
+ trap(signal) do
59
+ Vernier::Autorun.toggle
60
+ end
61
+ end
62
+
63
+ at_exit do
64
+ Vernier::Autorun.at_exit
65
+ end
@@ -6,44 +6,63 @@ module Vernier
6
6
  class Collector
7
7
  def initialize(mode)
8
8
  @mode = mode
9
+ @markers = []
9
10
  end
10
11
 
11
- def stop
12
- result = finish
13
-
14
- markers = []
15
- marker_list = self.markers
16
- size = marker_list.size
17
- marker_strings = Marker.name_table
12
+ ##
13
+ # Get the current time.
14
+ #
15
+ # This method returns the current time from Process.clock_gettime in
16
+ # integer nanoseconds. It's the same time used by Vernier internals and
17
+ # can be used to generate timestamps for custom markers.
18
+ def current_time
19
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
20
+ end
18
21
 
19
- marker_list.each_with_index do |(tid, id, ts), i|
20
- name = marker_strings[id]
21
- finish = nil
22
- phase = Marker::Phase::INSTANT
22
+ def add_marker(name:, start:, finish:, thread: Thread.current.native_thread_id, phase: Marker::Phase::INTERVAL, data: nil)
23
+ @markers << [thread,
24
+ name,
25
+ start,
26
+ finish,
27
+ phase,
28
+ data]
29
+ end
23
30
 
24
- if id == Marker::Type::GC_EXIT
25
- # skip because these are incorporated in "GC enter"
26
- else
27
- if id == Marker::Type::GC_ENTER
28
- j = i + 1
31
+ ##
32
+ # Record an interval with a category and name. Yields to a block and
33
+ # records the amount of time spent in the block as an interval marker.
34
+ def record_interval(category, name = category)
35
+ start = current_time
36
+ yield
37
+ add_marker(
38
+ name: category,
39
+ start:,
40
+ finish: current_time,
41
+ phase: Marker::Phase::INTERVAL,
42
+ thread: Thread.current.native_thread_id,
43
+ data: { :type => 'UserTiming', :entryType => 'measure', :name => name }
44
+ )
45
+ end
29
46
 
30
- name = "GC pause"
31
- phase = Marker::Phase::INTERVAL
47
+ def stop
48
+ result = finish
32
49
 
33
- while j < size
34
- if marker_list[j][1] == Marker::Type::GC_EXIT
35
- finish = marker_list[j][2]
36
- break
37
- end
50
+ end_time = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
51
+ result.pid = Process.pid
52
+ result.end_time = end_time
38
53
 
39
- j += 1
40
- end
41
- end
54
+ marker_strings = Marker.name_table
42
55
 
43
- markers << [tid, name, ts, finish, phase]
44
- end
56
+ markers = self.markers.map do |(tid, type, phase, ts, te, stack)|
57
+ name = marker_strings[type]
58
+ sym = Marker::MARKER_SYMBOLS[type]
59
+ data = { type: sym }
60
+ data[:cause] = { stack: stack } if stack
61
+ [tid, name, ts, te, phase, data]
45
62
  end
46
63
 
64
+ markers.concat @markers
65
+
47
66
  result.instance_variable_set(:@markers, markers)
48
67
 
49
68
  result
@@ -4,27 +4,27 @@ require_relative "vernier" # Make sure constants are loaded
4
4
 
5
5
  module Vernier
6
6
  module Marker
7
- # These are equal to the marker phase types from gecko-profile.js
8
- module Phase # :nodoc:
9
- INSTANT = 0
10
- INTERVAL = 1
11
- INTERVAL_START = 2
12
- INTERVAL_END = 3
7
+ MARKER_SYMBOLS = []
8
+ Type.constants.each do |name|
9
+ MARKER_SYMBOLS[Type.const_get(name)] = name
13
10
  end
11
+ MARKER_SYMBOLS.freeze
14
12
 
15
13
  MARKER_STRINGS = []
16
14
 
17
- MARKER_STRINGS[Type::GVL_THREAD_STARTED] = "thread started"
18
- MARKER_STRINGS[Type::GVL_THREAD_READY] = "thread ready"
19
- MARKER_STRINGS[Type::GVL_THREAD_RESUMED] = "thread resumed"
20
- MARKER_STRINGS[Type::GVL_THREAD_SUSPENDED] = "thread suspended"
21
- MARKER_STRINGS[Type::GVL_THREAD_EXITED] = "thread exited"
15
+ MARKER_STRINGS[Type::GVL_THREAD_STARTED] = "Thread started"
16
+ MARKER_STRINGS[Type::GVL_THREAD_EXITED] = "Thread exited"
22
17
 
23
18
  MARKER_STRINGS[Type::GC_START] = "GC start"
24
19
  MARKER_STRINGS[Type::GC_END_MARK] = "GC end marking"
25
20
  MARKER_STRINGS[Type::GC_END_SWEEP] = "GC end sweeping"
26
21
  MARKER_STRINGS[Type::GC_ENTER] = "GC enter"
27
22
  MARKER_STRINGS[Type::GC_EXIT] = "GC exit"
23
+ MARKER_STRINGS[Type::GC_PAUSE] = "GC pause"
24
+
25
+ MARKER_STRINGS[Type::THREAD_RUNNING] = "Thread Running"
26
+ MARKER_STRINGS[Type::THREAD_STALLED] = "Thread Stalled"
27
+ MARKER_STRINGS[Type::THREAD_SUSPENDED] = "Thread Suspended"
28
28
 
29
29
  MARKER_STRINGS.freeze
30
30
 
@@ -8,13 +8,15 @@ module Vernier
8
8
  # https://github.com/firefox-devtools/profiler/blob/main/src/types/profile.js
9
9
  class Firefox
10
10
  class Categorizer
11
- attr_reader :categories, :gc_category, :jit_category
11
+ attr_reader :categories
12
12
  def initialize
13
13
  @categories = []
14
+ @categories_by_name = {}
14
15
 
15
16
  add_category(name: "Default", color: "grey")
16
17
  add_category(name: "Idle", color: "transparent")
17
- @gc_category = add_category(name: "GC", color: "red")
18
+
19
+ add_category(name: "GC", color: "red")
18
20
  add_category(
19
21
  name: "stdlib",
20
22
  color: "red",
@@ -39,18 +41,21 @@ module Vernier
39
41
  name: "Application",
40
42
  color: "purple"
41
43
  )
42
- @jit_category = add_category(
43
- name: "JIT",
44
- color: "blue"
45
- )
44
+
45
+ add_category(name: "Thread", color: "grey")
46
46
  end
47
47
 
48
- def add_category(**kw)
49
- category = Category.new(@categories.length, **kw)
48
+ def add_category(name:, **kw)
49
+ category = Category.new(@categories.length, name: name, **kw)
50
50
  @categories << category
51
+ @categories_by_name[name] = category
51
52
  category
52
53
  end
53
54
 
55
+ def get_category(name)
56
+ @categories_by_name[name]
57
+ end
58
+
54
59
  def starts_with(*paths)
55
60
  %r{\A#{Regexp.union(paths)}}
56
61
  end
@@ -92,50 +97,30 @@ module Vernier
92
97
  attr_reader :profile
93
98
 
94
99
  def data
95
- threads = Hash.new {|h,k| h[k] = {
96
- timestamps: [],
97
- weights: [],
98
- samples: [],
99
- categories: [],
100
- markers: [],
101
- }}
102
-
103
- profile.samples.size.times do |i|
104
- tid = profile.sample_threads[i]
105
- thread = threads[tid]
106
-
107
- thread[:timestamps] << profile.timestamps[i]
108
- thread[:weights] << profile.weights[i]
109
- thread[:samples] << profile.samples[i]
110
- thread[:categories] << profile.sample_categories[i]
111
- end
112
-
113
- profile.markers.each do |marker|
114
- threads[marker[0]][:markers] << marker
115
- end
100
+ markers_by_thread = profile.markers.group_by { |marker| marker[0] }
116
101
 
117
102
  thread_data = profile.threads.map do |tid, thread_info|
118
- data = threads[tid]
103
+ markers = markers_by_thread[tid] || []
119
104
  Thread.new(
120
105
  profile,
121
106
  @categorizer,
107
+ markers: markers,
122
108
  **thread_info,
123
- **data
124
109
  ).data
125
110
  end
126
111
 
127
112
  {
128
113
  meta: {
129
114
  interval: 1, # FIXME: memory vs wall
130
- startTime: profile.meta[:started_at] / 1_000_000.0,
131
- endTime: (profile.timestamps&.max || 0) / 1_000_000.0,
115
+ startTime: profile.started_at / 1_000_000.0,
116
+ #endTime: (profile.timestamps&.max || 0) / 1_000_000.0,
132
117
  processType: 0,
133
118
  product: "Ruby/Vernier",
134
119
  stackwalk: 1,
135
120
  version: 28,
136
121
  preprocessedProfileVersion: 47,
137
122
  symbolicated: true,
138
- markerSchema: [],
123
+ markerSchema: marker_schema,
139
124
  sampleUnits: {
140
125
  time: "ms",
141
126
  eventDelay: "ms",
@@ -154,17 +139,33 @@ module Vernier
154
139
  }
155
140
  end
156
141
 
142
+ def marker_schema
143
+ [
144
+ {
145
+ name: "GVL_THREAD_RESUMED",
146
+ display: [ "marker-chart", "marker-table" ],
147
+ data: [
148
+ {
149
+ label: "Description",
150
+ value: "The thread has acquired the GVL and is executing"
151
+ }
152
+ ]
153
+ }
154
+ ]
155
+ end
156
+
157
157
  class Thread
158
158
  attr_reader :profile
159
159
 
160
- def initialize(profile, categorizer, name:, tid:, samples:, weights:, timestamps:, categories:, markers:, started_at:, stopped_at: nil)
160
+ def initialize(profile, categorizer, name:, tid:, samples:, weights:, timestamps: nil, sample_categories: nil, markers:, started_at:, stopped_at: nil)
161
161
  @profile = profile
162
162
  @categorizer = categorizer
163
163
  @tid = tid
164
164
  @name = name
165
165
 
166
+ timestamps ||= [0] * samples.size
166
167
  @samples, @weights, @timestamps = samples, weights, timestamps
167
- @sample_categories = categories
168
+ @sample_categories = sample_categories || ([0] * samples.size)
168
169
  @markers = markers
169
170
 
170
171
  @started_at, @stopped_at = started_at, stopped_at
@@ -184,6 +185,25 @@ module Vernier
184
185
  @filenames = filenames.map do |filename|
185
186
  @strings[filename]
186
187
  end
188
+
189
+ lines = profile.frame_table.fetch(:line)
190
+
191
+ @frame_implementations = filenames.zip(lines).map do |filename, line|
192
+ # Must match strings in `src/profile-logic/profile-data.js`
193
+ # inside the firefox profiler. See `getFriendlyStackTypeName`
194
+ if filename == "<cfunc>"
195
+ @strings["native"]
196
+ else
197
+ # FIXME: We need to get upstream support for JIT frames
198
+ if line == -1
199
+ @strings["yjit"]
200
+ else
201
+ # nil means interpreter
202
+ nil
203
+ end
204
+ end
205
+ end
206
+
187
207
  @frame_categories = filenames.map do |filename|
188
208
  @categorizer.categorize(filename)
189
209
  end
@@ -192,7 +212,7 @@ module Vernier
192
212
  def data
193
213
  {
194
214
  name: @name,
195
- isMainThread: @tid == ::Thread.main.native_thread_id,
215
+ isMainThread: (@tid == ::Thread.main.native_thread_id) || (profile.threads.size == 1),
196
216
  processStartupTime: 0, # FIXME
197
217
  processShutdownTime: nil, # FIXME
198
218
  registerTime: (@started_at - 0) / 1_000_000.0,
@@ -218,26 +238,34 @@ module Vernier
218
238
  end
219
239
 
220
240
  def markers_table
221
- size = @markers.size
222
-
223
241
  string_indexes = []
224
242
  start_times = []
225
243
  end_times = []
226
244
  phases = []
227
245
  categories = []
246
+ data = []
228
247
 
229
- @markers.each_with_index do |(_, name, start, finish, phase), i|
248
+ @markers.each_with_index do |(_, name, start, finish, phase, datum), i|
230
249
  string_indexes << @strings[name]
231
250
  start_times << (start / 1_000_000.0)
232
251
 
233
252
  # Please don't hate me. Divide by 1,000,000 only if finish is not nil
234
253
  end_times << (finish&./(1_000_000.0))
235
254
  phases << phase
236
- categories << (name =~ /GC/ ? gc_category.idx : 0)
255
+
256
+ category = case name
257
+ when /\AGC/ then gc_category.idx
258
+ when /\AThread/ then thread_category.idx
259
+ else
260
+ 0
261
+ end
262
+
263
+ categories << category
264
+ data << datum
237
265
  end
238
266
 
239
267
  {
240
- data: [nil] * start_times.size,
268
+ data: data,
241
269
  name: string_indexes,
242
270
  startTime: start_times,
243
271
  endTime: end_times,
@@ -263,7 +291,6 @@ module Vernier
263
291
  times = (0...size).to_a
264
292
  end
265
293
 
266
- raise unless samples.size == size
267
294
  raise unless weights.size == size
268
295
  raise unless times.size == size
269
296
 
@@ -325,7 +352,7 @@ module Vernier
325
352
  func: funcs,
326
353
  nativeSymbol: none,
327
354
  innerWindowID: none,
328
- implementation: none,
355
+ implementation: @frame_implementations,
329
356
  line: lines,
330
357
  column: none,
331
358
  length: size
@@ -357,7 +384,11 @@ module Vernier
357
384
  private
358
385
 
359
386
  def gc_category
360
- @categorizer.gc_category
387
+ @categorizer.get_category("GC")
388
+ end
389
+
390
+ def thread_category
391
+ @categorizer.get_category("Thread")
361
392
  end
362
393
  end
363
394
  end
@@ -0,0 +1,139 @@
1
+ module Vernier
2
+ class Result
3
+ attr_reader :stack_table, :frame_table, :func_table
4
+ attr_reader :markers
5
+
6
+ attr_accessor :pid, :end_time
7
+ attr_accessor :threads
8
+ attr_accessor :meta
9
+
10
+ # TODO: remove these
11
+ def weights; threads.values.flat_map { _1[:weights] }; end
12
+ def samples; threads.values.flat_map { _1[:samples] }; end
13
+ def sample_categories; threads.values.flat_map { _1[:sample_categories] }; end
14
+
15
+ # Realtime in nanoseconds since the unix epoch
16
+ def started_at
17
+ started_at_mono_ns = meta[:started_at]
18
+ current_time_mono_ns = Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
19
+ current_time_real_ns = Process.clock_gettime(Process::CLOCK_REALTIME, :nanosecond)
20
+ (current_time_real_ns - current_time_mono_ns + started_at_mono_ns)
21
+ end
22
+
23
+ def to_gecko
24
+ Output::Firefox.new(self).output
25
+ end
26
+
27
+ def write(out:)
28
+ File.write(out, to_gecko)
29
+ end
30
+
31
+ def elapsed_seconds
32
+ (end_time - started_at) / 1_000_000_000.0
33
+ end
34
+
35
+ def inspect
36
+ "#<#{self.class} #{elapsed_seconds} seconds, #{threads.count} threads, #{weights.sum} samples, #{samples.uniq.size} unique>"
37
+ end
38
+
39
+ def each_sample
40
+ return enum_for(__method__) unless block_given?
41
+ samples.size.times do |sample_idx|
42
+ weight = weights[sample_idx]
43
+ stack_idx = samples[sample_idx]
44
+ yield stack(stack_idx), weight
45
+ end
46
+ end
47
+
48
+ class BaseType
49
+ attr_reader :result, :idx
50
+ def initialize(result, idx)
51
+ @result = result
52
+ @idx = idx
53
+ end
54
+
55
+ def to_s
56
+ idx.to_s
57
+ end
58
+
59
+ def inspect
60
+ "#<#{self.class}\n#{to_s}>"
61
+ end
62
+ end
63
+
64
+ class Func < BaseType
65
+ def label
66
+ result.func_table[:name][idx]
67
+ end
68
+ alias name label
69
+
70
+ def filename
71
+ result.func_table[:filename][idx]
72
+ end
73
+
74
+ def to_s
75
+ "#{name} at #{filename}"
76
+ end
77
+ end
78
+
79
+ class Frame < BaseType
80
+ def label; func.label; end
81
+ def filename; func.filename; end
82
+ alias name label
83
+
84
+ def func
85
+ func_idx = result.frame_table[:func][idx]
86
+ Func.new(result, func_idx)
87
+ end
88
+
89
+ def line
90
+ result.frame_table[:line][idx]
91
+ end
92
+
93
+ def to_s
94
+ "#{func}:#{line}"
95
+ end
96
+ end
97
+
98
+ class Stack < BaseType
99
+ def each_frame
100
+ return enum_for(__method__) unless block_given?
101
+
102
+ stack_idx = idx
103
+ while stack_idx
104
+ frame_idx = result.stack_table[:frame][stack_idx]
105
+ yield Frame.new(result, frame_idx)
106
+ stack_idx = result.stack_table[:parent][stack_idx]
107
+ end
108
+ end
109
+
110
+ def leaf_frame_idx
111
+ result.stack_table[:frame][idx]
112
+ end
113
+
114
+ def leaf_frame
115
+ Frame.new(result, leaf_frame_idx)
116
+ end
117
+
118
+ def frames
119
+ each_frame.to_a
120
+ end
121
+
122
+ def to_s
123
+ arr = []
124
+ each_frame do |frame|
125
+ arr << frame.to_s
126
+ end
127
+ arr.join("\n")
128
+ end
129
+ end
130
+
131
+ def stack(idx)
132
+ Stack.new(self, idx)
133
+ end
134
+
135
+ def total_bytes
136
+ weights.sum
137
+ end
138
+ end
139
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Vernier
4
- VERSION = "0.2.1"
4
+ VERSION = "0.3.1"
5
5
  end