vernier 0.2.1 → 0.3.0

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,32 @@ 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:, sample_categories:, 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
166
  @samples, @weights, @timestamps = samples, weights, timestamps
167
- @sample_categories = categories
167
+ @sample_categories = sample_categories
168
168
  @markers = markers
169
169
 
170
170
  @started_at, @stopped_at = started_at, stopped_at
@@ -184,6 +184,25 @@ module Vernier
184
184
  @filenames = filenames.map do |filename|
185
185
  @strings[filename]
186
186
  end
187
+
188
+ lines = profile.frame_table.fetch(:line)
189
+
190
+ @frame_implementations = filenames.zip(lines).map do |filename, line|
191
+ # Must match strings in `src/profile-logic/profile-data.js`
192
+ # inside the firefox profiler. See `getFriendlyStackTypeName`
193
+ if filename == "<cfunc>"
194
+ @strings["native"]
195
+ else
196
+ # FIXME: We need to get upstream support for JIT frames
197
+ if line == -1
198
+ @strings["yjit"]
199
+ else
200
+ # nil means interpreter
201
+ nil
202
+ end
203
+ end
204
+ end
205
+
187
206
  @frame_categories = filenames.map do |filename|
188
207
  @categorizer.categorize(filename)
189
208
  end
@@ -225,19 +244,29 @@ module Vernier
225
244
  end_times = []
226
245
  phases = []
227
246
  categories = []
247
+ data = []
228
248
 
229
- @markers.each_with_index do |(_, name, start, finish, phase), i|
249
+ @markers.each_with_index do |(_, name, start, finish, phase, datum), i|
230
250
  string_indexes << @strings[name]
231
251
  start_times << (start / 1_000_000.0)
232
252
 
233
253
  # Please don't hate me. Divide by 1,000,000 only if finish is not nil
234
254
  end_times << (finish&./(1_000_000.0))
235
255
  phases << phase
236
- categories << (name =~ /GC/ ? gc_category.idx : 0)
256
+
257
+ category = case name
258
+ when /\AGC/ then gc_category.idx
259
+ when /\AThread/ then thread_category.idx
260
+ else
261
+ 0
262
+ end
263
+
264
+ categories << category
265
+ data << datum
237
266
  end
238
267
 
239
268
  {
240
- data: [nil] * start_times.size,
269
+ data: data,
241
270
  name: string_indexes,
242
271
  startTime: start_times,
243
272
  endTime: end_times,
@@ -325,7 +354,7 @@ module Vernier
325
354
  func: funcs,
326
355
  nativeSymbol: none,
327
356
  innerWindowID: none,
328
- implementation: none,
357
+ implementation: @frame_implementations,
329
358
  line: lines,
330
359
  column: none,
331
360
  length: size
@@ -357,7 +386,11 @@ module Vernier
357
386
  private
358
387
 
359
388
  def gc_category
360
- @categorizer.gc_category
389
+ @categorizer.get_category("GC")
390
+ end
391
+
392
+ def thread_category
393
+ @categorizer.get_category("Thread")
361
394
  end
362
395
  end
363
396
  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.0"
5
5
  end