vernier 0.2.1 → 0.3.1

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.
@@ -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