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.
- checksums.yaml +4 -4
- data/README.md +24 -8
- data/bin/vernier +4 -0
- data/examples/minitest.rb +20 -0
- data/examples/ractor.rb +11 -0
- data/examples/rails.rb +125 -0
- data/exe/vernier +38 -0
- data/ext/vernier/vernier.cc +571 -302
- data/lib/vernier/autorun.rb +65 -0
- data/lib/vernier/collector.rb +47 -28
- data/lib/vernier/marker.rb +11 -11
- data/lib/vernier/output/firefox.rb +76 -45
- data/lib/vernier/result.rb +139 -0
- data/lib/vernier/version.rb +1 -1
- data/lib/vernier.rb +6 -133
- data/vernier.gemspec +1 -1
- metadata +12 -5
- data/sig/vernier.rbs +0 -4
@@ -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
|
data/lib/vernier/collector.rb
CHANGED
@@ -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
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
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
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
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
|
-
|
31
|
-
|
47
|
+
def stop
|
48
|
+
result = finish
|
32
49
|
|
33
|
-
|
34
|
-
|
35
|
-
|
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
|
-
|
40
|
-
end
|
41
|
-
end
|
54
|
+
marker_strings = Marker.name_table
|
42
55
|
|
43
|
-
|
44
|
-
|
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
|
data/lib/vernier/marker.rb
CHANGED
@@ -4,27 +4,27 @@ require_relative "vernier" # Make sure constants are loaded
|
|
4
4
|
|
5
5
|
module Vernier
|
6
6
|
module Marker
|
7
|
-
|
8
|
-
|
9
|
-
|
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] = "
|
18
|
-
MARKER_STRINGS[Type::
|
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
|
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
|
-
|
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
|
-
|
43
|
-
|
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
|
-
|
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
|
-
|
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.
|
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
|
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 =
|
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
|
-
|
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:
|
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:
|
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.
|
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
|
data/lib/vernier/version.rb
CHANGED