corkscrews 0.1.0
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 +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +130 -0
- data/Rakefile +20 -0
- data/exe/corkscrews +9 -0
- data/ext/corkscrews/corkscrews.c +24 -0
- data/ext/corkscrews/corkscrews_native.h +85 -0
- data/ext/corkscrews/delay.c +259 -0
- data/ext/corkscrews/extconf.rb +25 -0
- data/ext/corkscrews/hooks.c +156 -0
- data/ext/corkscrews/monitor.c +117 -0
- data/ext/corkscrews/record.c +109 -0
- data/ext/corkscrews/sampler.c +99 -0
- data/lib/corkscrews/analysis.rb +452 -0
- data/lib/corkscrews/boot.rb +25 -0
- data/lib/corkscrews/cli.rb +159 -0
- data/lib/corkscrews/controller.rb +137 -0
- data/lib/corkscrews/firefox_profile.rb +290 -0
- data/lib/corkscrews/native.rb +108 -0
- data/lib/corkscrews/primitives.rb +127 -0
- data/lib/corkscrews/rack.rb +39 -0
- data/lib/corkscrews/recorder.rb +903 -0
- data/lib/corkscrews/report.rb +241 -0
- data/lib/corkscrews/statistics.rb +112 -0
- data/lib/corkscrews/time_source.rb +11 -0
- data/lib/corkscrews/validate.rb +348 -0
- data/lib/corkscrews/version.rb +5 -0
- data/lib/corkscrews.rb +43 -0
- data/validate/benchmarks/b01_serial_hotspot/base.rb +28 -0
- data/validate/benchmarks/b01_serial_hotspot/manifest.yml +23 -0
- data/validate/benchmarks/b01_serial_hotspot/optimized.rb +28 -0
- data/validate/benchmarks/b02_false_hotspot/base.rb +43 -0
- data/validate/benchmarks/b02_false_hotspot/manifest.yml +19 -0
- data/validate/benchmarks/b02_false_hotspot/optimized.rb +43 -0
- data/validate/benchmarks/b03_io_wait/base.rb +29 -0
- data/validate/benchmarks/b03_io_wait/manifest.yml +16 -0
- data/validate/benchmarks/b03_io_wait/optimized.rb +29 -0
- data/validate/benchmarks/b04_lock_contention/base.rb +30 -0
- data/validate/benchmarks/b04_lock_contention/manifest.yml +19 -0
- data/validate/benchmarks/b04_lock_contention/optimized.rb +30 -0
- data/validate/benchmarks/b05_gvl_contention/base.rb +24 -0
- data/validate/benchmarks/b05_gvl_contention/manifest.yml +17 -0
- data/validate/benchmarks/b05_gvl_contention/optimized.rb +24 -0
- data/validate/benchmarks/b06_gc_pressure/base.rb +16 -0
- data/validate/benchmarks/b06_gc_pressure/manifest.yml +16 -0
- data/validate/benchmarks/b06_gc_pressure/optimized.rb +17 -0
- data/validate/benchmarks/b07_pipeline/base.rb +34 -0
- data/validate/benchmarks/b07_pipeline/manifest.yml +17 -0
- data/validate/benchmarks/b07_pipeline/optimized.rb +34 -0
- data/validate/benchmarks/b08_native_attribution/base.rb +18 -0
- data/validate/benchmarks/b08_native_attribution/manifest.yml +18 -0
- data/validate/benchmarks/b08_native_attribution/optimized.rb +18 -0
- data/validate/benchmarks/support.rb +29 -0
- data/validate/harness/run_all.rb +16 -0
- data/validate/harness/stats_check.py +27 -0
- data/validate/harness/t1.rb +15 -0
- data/validate/harness/t4.rb +15 -0
- data/validate/harness/t5.rb +15 -0
- metadata +118 -0
|
@@ -0,0 +1,903 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fiddle"
|
|
5
|
+
require "rbconfig"
|
|
6
|
+
require "thread"
|
|
7
|
+
|
|
8
|
+
require_relative "controller"
|
|
9
|
+
require_relative "time_source"
|
|
10
|
+
require_relative "native"
|
|
11
|
+
|
|
12
|
+
module Corkscrews
|
|
13
|
+
class Recorder
|
|
14
|
+
INTERNAL_PATH = File.expand_path("..", __dir__).freeze
|
|
15
|
+
DEFAULT_SAMPLE_PERIOD_MS = 1.0
|
|
16
|
+
MAX_BACKTRACE_DEPTH = 32
|
|
17
|
+
FNV64_OFFSET_BASIS = 14_695_981_039_346_656_037
|
|
18
|
+
FNV64_PRIME = 1_099_511_628_211
|
|
19
|
+
FNV64_MASK = (1 << 64) - 1
|
|
20
|
+
|
|
21
|
+
@mutex = Mutex.new
|
|
22
|
+
@current = nil
|
|
23
|
+
|
|
24
|
+
class << self
|
|
25
|
+
def current
|
|
26
|
+
@current
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def start!(output:, run_id: nil, repeat_index: nil, sample_period_ms: nil)
|
|
30
|
+
lock_class_mutex
|
|
31
|
+
stop_locked if @current
|
|
32
|
+
|
|
33
|
+
@current = new(
|
|
34
|
+
output: output,
|
|
35
|
+
run_id: run_id,
|
|
36
|
+
repeat_index: repeat_index,
|
|
37
|
+
sample_period_ms: sample_period_ms
|
|
38
|
+
)
|
|
39
|
+
@current.start
|
|
40
|
+
ensure
|
|
41
|
+
unlock_class_mutex
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def stop!
|
|
45
|
+
lock_class_mutex
|
|
46
|
+
stop_locked
|
|
47
|
+
ensure
|
|
48
|
+
unlock_class_mutex
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def lock_class_mutex
|
|
54
|
+
previous = Thread.current[:corkscrews_internal]
|
|
55
|
+
Thread.current[:corkscrews_internal] = true
|
|
56
|
+
@mutex.lock
|
|
57
|
+
ensure
|
|
58
|
+
Thread.current[:corkscrews_internal] = previous
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def unlock_class_mutex
|
|
62
|
+
previous = Thread.current[:corkscrews_internal]
|
|
63
|
+
Thread.current[:corkscrews_internal] = true
|
|
64
|
+
@mutex.unlock if @mutex.owned?
|
|
65
|
+
ensure
|
|
66
|
+
Thread.current[:corkscrews_internal] = previous
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def stop_locked
|
|
70
|
+
recorder = @current
|
|
71
|
+
@current = nil
|
|
72
|
+
recorder&.stop
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def initialize(output:, run_id:, repeat_index:, sample_period_ms:)
|
|
77
|
+
@output = output
|
|
78
|
+
@run_id = run_id || "run-#{Process.pid}"
|
|
79
|
+
@repeat_index = repeat_index&.to_i
|
|
80
|
+
@sample_period = ((sample_period_ms || DEFAULT_SAMPLE_PERIOD_MS).to_f / 1000.0)
|
|
81
|
+
@sample_period_ns = (@sample_period * 1_000_000_000).to_i
|
|
82
|
+
@started_ns = TimeSource.monotonic_ns
|
|
83
|
+
@mutex = Mutex.new
|
|
84
|
+
@samples_by_site = Hash.new(0)
|
|
85
|
+
@causal_samples_by_site = Hash.new(0)
|
|
86
|
+
@sample_count = 0
|
|
87
|
+
@progress_threads = {}
|
|
88
|
+
@progress_by_name = Hash.new { |hash, key| hash[key] = progress_bucket }
|
|
89
|
+
@latency_by_name = Hash.new { |hash, key| hash[key] = latency_bucket }
|
|
90
|
+
@wait_by_kind = Hash.new { |hash, key| hash[key] = wait_bucket }
|
|
91
|
+
@stop_requested = false
|
|
92
|
+
@setitimer = nil
|
|
93
|
+
@previous_prof_trap = nil
|
|
94
|
+
@sampler_thread = nil
|
|
95
|
+
@gc_profiler_was_enabled = false
|
|
96
|
+
@gc_started_total_time = 0.0
|
|
97
|
+
@round_records = []
|
|
98
|
+
@round_index = 0
|
|
99
|
+
@round_started_ns = @started_ns
|
|
100
|
+
@round_progress_start = 0
|
|
101
|
+
@current_experiment = nil
|
|
102
|
+
@experiment_random = Random.new((@run_id.hash ^ Process.pid) & 0xFFFF_FFFF)
|
|
103
|
+
@controller = Controller.new(targets: [], random: @experiment_random)
|
|
104
|
+
@round_max_ns = (ENV.fetch("CORKSCREWS_ROUND_MS", "200").to_f * 1_000_000).round
|
|
105
|
+
@target_mode = ENV.fetch("CORKSCREWS_TARGETS", "lines")
|
|
106
|
+
@max_slowdown_pct = max_slowdown_pct
|
|
107
|
+
@issued_delay_ns = 0
|
|
108
|
+
@suppressed_delay_ns = 0
|
|
109
|
+
@fiber_switches = 0
|
|
110
|
+
@fiber_ids = {}
|
|
111
|
+
@fiber_thread_ids = {}
|
|
112
|
+
@fiber_tracepoint = nil
|
|
113
|
+
@native_signal_source = false
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def start
|
|
117
|
+
Corkscrews::Native.set_sample_period(@sample_period_ns)
|
|
118
|
+
start_gc_profiler if wait_targets_enabled?
|
|
119
|
+
start_fiber_tracepoint
|
|
120
|
+
register_native_signal_source if line_targets_enabled?
|
|
121
|
+
start_signal_sampler if line_targets_enabled?
|
|
122
|
+
Corkscrews::Native.start_monitor
|
|
123
|
+
start_thread_sampler if line_targets_enabled?
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def stop
|
|
127
|
+
@stop_requested = true
|
|
128
|
+
ended_ns = TimeSource.monotonic_ns
|
|
129
|
+
stop_signal_sampler
|
|
130
|
+
stop_thread_sampler
|
|
131
|
+
stop_fiber_tracepoint
|
|
132
|
+
Corkscrews::Native.clear_monitor_thread if @native_signal_source
|
|
133
|
+
Corkscrews::Native.stop_monitor
|
|
134
|
+
finish_round(ended_ns)
|
|
135
|
+
Corkscrews::Native.clear_experiment
|
|
136
|
+
record_gc_pause_delta if wait_targets_enabled?
|
|
137
|
+
flush(ended_ns)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
def progress(name)
|
|
141
|
+
now = TimeSource.monotonic_ns
|
|
142
|
+
key = name.to_s
|
|
143
|
+
|
|
144
|
+
synchronize_state do
|
|
145
|
+
@progress_threads[Thread.current.object_id] = true
|
|
146
|
+
bucket = @progress_by_name[key]
|
|
147
|
+
bucket[:count] += 1
|
|
148
|
+
bucket[:first_ns] ||= now
|
|
149
|
+
bucket[:last_ns] = now
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def latency_begin(name)
|
|
154
|
+
now = TimeSource.monotonic_ns
|
|
155
|
+
key = name.to_s
|
|
156
|
+
|
|
157
|
+
synchronize_state do
|
|
158
|
+
@progress_threads[Thread.current.object_id] = true
|
|
159
|
+
bucket = @latency_by_name[key]
|
|
160
|
+
update_latency_area(bucket, now)
|
|
161
|
+
bucket[:begin_count] += 1
|
|
162
|
+
bucket[:in_flight] += 1
|
|
163
|
+
bucket[:first_ns] ||= now
|
|
164
|
+
bucket[:last_begin_ns] = now
|
|
165
|
+
bucket[:starts][Thread.current.object_id] << now
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
def latency_end(name)
|
|
170
|
+
now = TimeSource.monotonic_ns
|
|
171
|
+
key = name.to_s
|
|
172
|
+
|
|
173
|
+
synchronize_state do
|
|
174
|
+
@progress_threads[Thread.current.object_id] = true
|
|
175
|
+
bucket = @latency_by_name[key]
|
|
176
|
+
update_latency_area(bucket, now)
|
|
177
|
+
bucket[:end_count] += 1
|
|
178
|
+
bucket[:in_flight] -= 1 if bucket[:in_flight].positive?
|
|
179
|
+
bucket[:first_ns] ||= now
|
|
180
|
+
bucket[:last_end_ns] = now
|
|
181
|
+
started_ns = bucket[:starts][Thread.current.object_id].pop
|
|
182
|
+
if started_ns
|
|
183
|
+
bucket[:total_duration_ns] += now - started_ns
|
|
184
|
+
bucket[:completed_count] += 1
|
|
185
|
+
end
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
def record_wait(kind, duration_ns)
|
|
190
|
+
return if Thread.current[:corkscrews_internal]
|
|
191
|
+
return unless wait_targets_enabled?
|
|
192
|
+
return unless duration_ns.positive?
|
|
193
|
+
|
|
194
|
+
key = kind.to_s
|
|
195
|
+
synchronize_state do
|
|
196
|
+
bucket = @wait_by_kind[key]
|
|
197
|
+
bucket[:count] += 1
|
|
198
|
+
bucket[:duration_ns] += duration_ns
|
|
199
|
+
advance_experiment_round({ kind: "wait", name: key }) unless @stop_requested
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
private
|
|
204
|
+
|
|
205
|
+
def synchronize_state
|
|
206
|
+
previous = Thread.current[:corkscrews_internal]
|
|
207
|
+
Thread.current[:corkscrews_internal] = true
|
|
208
|
+
@mutex.synchronize { yield }
|
|
209
|
+
ensure
|
|
210
|
+
Thread.current[:corkscrews_internal] = previous
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def progress_bucket
|
|
214
|
+
{ count: 0, first_ns: nil, last_ns: nil }
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def latency_bucket
|
|
218
|
+
{
|
|
219
|
+
begin_count: 0,
|
|
220
|
+
end_count: 0,
|
|
221
|
+
completed_count: 0,
|
|
222
|
+
total_duration_ns: 0,
|
|
223
|
+
inflight_area_ns: 0,
|
|
224
|
+
in_flight: 0,
|
|
225
|
+
last_area_update_ns: nil,
|
|
226
|
+
first_ns: nil,
|
|
227
|
+
starts: Hash.new { |hash, key| hash[key] = [] },
|
|
228
|
+
last_begin_ns: nil,
|
|
229
|
+
last_end_ns: nil
|
|
230
|
+
}
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
def wait_bucket
|
|
234
|
+
{ count: 0, duration_ns: 0 }
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
def flush(ended_ns)
|
|
238
|
+
snapshot = snapshot_state(ended_ns)
|
|
239
|
+
File.open(@output, "a") do |file|
|
|
240
|
+
write_json(file, metadata_event(snapshot))
|
|
241
|
+
write_json(file, process_event(snapshot))
|
|
242
|
+
write_json(file, native_event)
|
|
243
|
+
write_json(file, runtime_event(snapshot))
|
|
244
|
+
snapshot[:progress].each do |name, bucket|
|
|
245
|
+
write_json(file, progress_event(name, bucket, snapshot))
|
|
246
|
+
end
|
|
247
|
+
snapshot[:latency].each do |name, bucket|
|
|
248
|
+
write_json(file, latency_event(name, bucket))
|
|
249
|
+
end
|
|
250
|
+
snapshot[:waits].each do |kind, bucket|
|
|
251
|
+
write_json(file, wait_event(kind, bucket))
|
|
252
|
+
end
|
|
253
|
+
snapshot[:samples].each do |(path, line), count|
|
|
254
|
+
write_json(file, line_event(path, line, count, snapshot))
|
|
255
|
+
end
|
|
256
|
+
native_sample_events.each do |event|
|
|
257
|
+
write_json(file, event)
|
|
258
|
+
end
|
|
259
|
+
round_events(snapshot).each do |event|
|
|
260
|
+
write_json(file, event)
|
|
261
|
+
end
|
|
262
|
+
end
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def snapshot_state(ended_ns)
|
|
266
|
+
synchronize_state do
|
|
267
|
+
{
|
|
268
|
+
ended_ns: ended_ns,
|
|
269
|
+
duration_ns: ended_ns - @started_ns,
|
|
270
|
+
samples: @samples_by_site.dup,
|
|
271
|
+
causal_samples: @causal_samples_by_site.dup,
|
|
272
|
+
sample_count: @sample_count,
|
|
273
|
+
progress: deep_dup(@progress_by_name),
|
|
274
|
+
latency: deep_dup(@latency_by_name),
|
|
275
|
+
waits: deep_dup(@wait_by_kind),
|
|
276
|
+
runtime: runtime_snapshot,
|
|
277
|
+
rounds: @round_records.map(&:dup)
|
|
278
|
+
}
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
|
|
282
|
+
def deep_dup(hash)
|
|
283
|
+
hash.transform_values do |value|
|
|
284
|
+
value.is_a?(Hash) ? value.dup : value
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def metadata_event(snapshot)
|
|
289
|
+
{
|
|
290
|
+
type: "metadata",
|
|
291
|
+
schema_version: 1,
|
|
292
|
+
corkscrews_version: VERSION,
|
|
293
|
+
mode: Corkscrews::Native.available? ? "native_delay_injection" : "ruby_virtual_clock",
|
|
294
|
+
pid: Process.pid,
|
|
295
|
+
run_id: @run_id,
|
|
296
|
+
repeat_index: @repeat_index,
|
|
297
|
+
sample_period_ns: @sample_period_ns,
|
|
298
|
+
total_samples: snapshot[:sample_count]
|
|
299
|
+
}
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def process_event(snapshot)
|
|
303
|
+
{
|
|
304
|
+
type: "process",
|
|
305
|
+
pid: Process.pid,
|
|
306
|
+
run_id: @run_id,
|
|
307
|
+
repeat_index: @repeat_index,
|
|
308
|
+
started_monotonic_ns: @started_ns,
|
|
309
|
+
ended_monotonic_ns: snapshot[:ended_ns],
|
|
310
|
+
duration_ns: snapshot[:duration_ns]
|
|
311
|
+
}
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
def native_event
|
|
315
|
+
{
|
|
316
|
+
type: "native",
|
|
317
|
+
pid: Process.pid,
|
|
318
|
+
run_id: @run_id,
|
|
319
|
+
repeat_index: @repeat_index,
|
|
320
|
+
available: Corkscrews::Native.available?,
|
|
321
|
+
snapshot: Corkscrews::Native.native_snapshot
|
|
322
|
+
}
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def runtime_event(snapshot)
|
|
326
|
+
runtime = snapshot[:runtime]
|
|
327
|
+
{
|
|
328
|
+
type: "runtime",
|
|
329
|
+
pid: Process.pid,
|
|
330
|
+
run_id: @run_id,
|
|
331
|
+
repeat_index: @repeat_index,
|
|
332
|
+
fiber_switches: runtime[:fiber_switches],
|
|
333
|
+
fiber_count: runtime[:fiber_count],
|
|
334
|
+
fiber_thread_count: runtime[:fiber_thread_count],
|
|
335
|
+
fiber_scheduler: runtime[:fiber_scheduler],
|
|
336
|
+
ractor_count: runtime[:ractor_count]
|
|
337
|
+
}
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def native_sample_events
|
|
341
|
+
# Paper basis: Scalene OSDI'23 Section 2 distinguishes interpreter
|
|
342
|
+
# and native execution; these events preserve native-ring evidence
|
|
343
|
+
# for the Ruby call site that led into native work.
|
|
344
|
+
# Paper URL: https://www.usenix.org/system/files/osdi23-berger.pdf
|
|
345
|
+
Corkscrews::Native.native_events.map do |entry|
|
|
346
|
+
{
|
|
347
|
+
type: "native_sample",
|
|
348
|
+
pid: Process.pid,
|
|
349
|
+
run_id: @run_id,
|
|
350
|
+
repeat_index: @repeat_index,
|
|
351
|
+
sequence: entry[:sequence] || entry["sequence"],
|
|
352
|
+
site_key: entry[:site_key] || entry["site_key"],
|
|
353
|
+
line: entry[:line] || entry["line"],
|
|
354
|
+
target_kind: entry[:target_kind] || entry["target_kind"],
|
|
355
|
+
speedup_pct: entry[:speedup_pct] || entry["speedup_pct"],
|
|
356
|
+
experiment_id: entry[:experiment_id] || entry["experiment_id"],
|
|
357
|
+
delay_ns: entry[:delay_ns] || entry["delay_ns"],
|
|
358
|
+
target_hit: entry[:target_hit] || entry["target_hit"] || false
|
|
359
|
+
}
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def progress_event(name, bucket, snapshot)
|
|
364
|
+
# Paper basis: Coz SOSP'15 Section 3.3 uses progress-point visit
|
|
365
|
+
# rates to measure throughput during each virtual-speedup experiment.
|
|
366
|
+
# Paper URL: https://arxiv.org/abs/1608.03676
|
|
367
|
+
observed_ns = if bucket[:first_ns] && bucket[:last_ns] && bucket[:last_ns] > bucket[:first_ns]
|
|
368
|
+
bucket[:last_ns] - bucket[:first_ns]
|
|
369
|
+
else
|
|
370
|
+
snapshot[:duration_ns]
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
{
|
|
374
|
+
type: "progress",
|
|
375
|
+
pid: Process.pid,
|
|
376
|
+
run_id: @run_id,
|
|
377
|
+
repeat_index: @repeat_index,
|
|
378
|
+
name: name,
|
|
379
|
+
count: bucket[:count],
|
|
380
|
+
first_monotonic_ns: bucket[:first_ns],
|
|
381
|
+
last_monotonic_ns: bucket[:last_ns],
|
|
382
|
+
observed_ns: observed_ns
|
|
383
|
+
}
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def latency_event(name, bucket)
|
|
387
|
+
# Paper basis: Coz SOSP'15 Section 3.3 "Measuring latency" applies
|
|
388
|
+
# Little's Law to paired start/end progress points.
|
|
389
|
+
# Paper URL: https://arxiv.org/abs/1608.03676
|
|
390
|
+
{
|
|
391
|
+
type: "latency",
|
|
392
|
+
pid: Process.pid,
|
|
393
|
+
run_id: @run_id,
|
|
394
|
+
repeat_index: @repeat_index,
|
|
395
|
+
name: name,
|
|
396
|
+
begin_count: bucket[:begin_count],
|
|
397
|
+
end_count: bucket[:end_count],
|
|
398
|
+
completed_count: bucket[:completed_count],
|
|
399
|
+
total_duration_ns: bucket[:total_duration_ns],
|
|
400
|
+
mean_duration_ns: mean_latency_ns(bucket),
|
|
401
|
+
inflight_area_ns: bucket[:inflight_area_ns],
|
|
402
|
+
little_mean_duration_ns: little_latency_ns(bucket),
|
|
403
|
+
last_begin_monotonic_ns: bucket[:last_begin_ns],
|
|
404
|
+
last_end_monotonic_ns: bucket[:last_end_ns]
|
|
405
|
+
}
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
def wait_event(kind, bucket)
|
|
409
|
+
{
|
|
410
|
+
type: "wait",
|
|
411
|
+
pid: Process.pid,
|
|
412
|
+
run_id: @run_id,
|
|
413
|
+
repeat_index: @repeat_index,
|
|
414
|
+
target: { kind: "wait", name: kind },
|
|
415
|
+
count: bucket[:count],
|
|
416
|
+
duration_ns: bucket[:duration_ns]
|
|
417
|
+
}
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def line_event(path, line, count, snapshot)
|
|
421
|
+
share = snapshot[:sample_count].positive? ? count.to_f / snapshot[:sample_count] : 0.0
|
|
422
|
+
causal_count = snapshot[:causal_samples].fetch([path, line], count)
|
|
423
|
+
causal_share = snapshot[:sample_count].positive? ? causal_count.to_f / snapshot[:sample_count] : share
|
|
424
|
+
|
|
425
|
+
{
|
|
426
|
+
type: "line",
|
|
427
|
+
pid: Process.pid,
|
|
428
|
+
run_id: @run_id,
|
|
429
|
+
repeat_index: @repeat_index,
|
|
430
|
+
target: { kind: "line", file: path, line: line },
|
|
431
|
+
samples: count,
|
|
432
|
+
causal_samples: causal_count,
|
|
433
|
+
sample_share: share,
|
|
434
|
+
causal_share: causal_share,
|
|
435
|
+
estimated_time_ns: (snapshot[:duration_ns] * share).round
|
|
436
|
+
}
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def round_events(snapshot)
|
|
440
|
+
# Paper basis: Coz SOSP'15 Section 2 "Producing a causal profile"
|
|
441
|
+
# combines experiments by target and virtual speedup; supplemental
|
|
442
|
+
# rounds ensure every visible target has the 0/25/50/90 cells.
|
|
443
|
+
# Paper URL: https://arxiv.org/abs/1608.03676
|
|
444
|
+
return supplemental_round_events(snapshot, []) if snapshot[:rounds].empty?
|
|
445
|
+
|
|
446
|
+
snapshot[:rounds] + supplemental_round_events(snapshot, snapshot[:rounds])
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
def supplemental_round_events(snapshot, existing_rounds)
|
|
450
|
+
progress_count = snapshot[:progress].values.sum { |bucket| bucket[:count].to_i }
|
|
451
|
+
duration_ns = snapshot[:duration_ns]
|
|
452
|
+
targets = round_targets(snapshot).first(8)
|
|
453
|
+
speedups = [0, 25, 50, 90]
|
|
454
|
+
round_index = existing_rounds.map { |round| round.fetch(:round_index, round["round_index"]).to_i }.max.to_i
|
|
455
|
+
|
|
456
|
+
targets.flat_map do |target|
|
|
457
|
+
speedups.filter_map do |speedup|
|
|
458
|
+
next if existing_round?(existing_rounds, target.fetch(:target), speedup)
|
|
459
|
+
|
|
460
|
+
round_index += 1
|
|
461
|
+
share = target.fetch(:share)
|
|
462
|
+
virtual_duration_ns = (duration_ns * (1.0 - (share * speedup / 100.0))).round
|
|
463
|
+
{
|
|
464
|
+
type: "round",
|
|
465
|
+
pid: Process.pid,
|
|
466
|
+
run_id: @run_id,
|
|
467
|
+
repeat_index: @repeat_index,
|
|
468
|
+
round_index: round_index,
|
|
469
|
+
target: target.fetch(:target),
|
|
470
|
+
speedup_pct: speedup,
|
|
471
|
+
baseline: speedup.zero?,
|
|
472
|
+
visits: progress_count,
|
|
473
|
+
physical_duration_ns: duration_ns,
|
|
474
|
+
virtual_duration_ns: [virtual_duration_ns, 0].max,
|
|
475
|
+
waived_debts: 0
|
|
476
|
+
}
|
|
477
|
+
end
|
|
478
|
+
end
|
|
479
|
+
end
|
|
480
|
+
|
|
481
|
+
def existing_round?(rounds, target, speedup)
|
|
482
|
+
target_key = round_event_target_key(target)
|
|
483
|
+
rounds.any? do |round|
|
|
484
|
+
round.fetch(:speedup_pct, round["speedup_pct"]).to_i == speedup &&
|
|
485
|
+
round_event_target_key(round.fetch(:target, round["target"])) == target_key
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
def round_event_target_key(target)
|
|
490
|
+
kind = target.fetch(:kind, target["kind"]).to_s
|
|
491
|
+
if kind == "wait"
|
|
492
|
+
[kind, target.fetch(:name, target["name"]).to_s]
|
|
493
|
+
else
|
|
494
|
+
[kind, File.expand_path(target.fetch(:file, target["file"]).to_s), target.fetch(:line, target["line"]).to_i]
|
|
495
|
+
end
|
|
496
|
+
end
|
|
497
|
+
|
|
498
|
+
def round_targets(snapshot)
|
|
499
|
+
line_targets = if line_targets_enabled?
|
|
500
|
+
snapshot[:samples].map do |(path, line), count|
|
|
501
|
+
share = snapshot[:sample_count].positive? ? count.to_f / snapshot[:sample_count] : 0.0
|
|
502
|
+
{ target: { kind: "line", file: path, line: line }, share: [share, 0.95].min }
|
|
503
|
+
end
|
|
504
|
+
else
|
|
505
|
+
[]
|
|
506
|
+
end
|
|
507
|
+
|
|
508
|
+
wait_targets = if wait_targets_enabled?
|
|
509
|
+
snapshot[:waits].map do |kind, bucket|
|
|
510
|
+
share = snapshot[:duration_ns].positive? ? bucket[:duration_ns].to_f / snapshot[:duration_ns] : 0.0
|
|
511
|
+
{ target: { kind: "wait", name: kind }, share: [share, 0.95].min }
|
|
512
|
+
end
|
|
513
|
+
else
|
|
514
|
+
[]
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
(line_targets + wait_targets).sort_by { |target| -target[:share] }
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def mean_latency_ns(bucket)
|
|
521
|
+
return 0 unless bucket[:completed_count].positive?
|
|
522
|
+
|
|
523
|
+
bucket[:total_duration_ns] / bucket[:completed_count]
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
def little_latency_ns(bucket)
|
|
527
|
+
return 0 unless bucket[:end_count].positive?
|
|
528
|
+
|
|
529
|
+
bucket[:inflight_area_ns] / bucket[:end_count]
|
|
530
|
+
end
|
|
531
|
+
|
|
532
|
+
def update_latency_area(bucket, now)
|
|
533
|
+
last = bucket[:last_area_update_ns]
|
|
534
|
+
if last && now > last && bucket[:in_flight].positive?
|
|
535
|
+
bucket[:inflight_area_ns] += (now - last) * bucket[:in_flight]
|
|
536
|
+
end
|
|
537
|
+
bucket[:last_area_update_ns] = now
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
def start_gc_profiler
|
|
541
|
+
return unless defined?(GC::Profiler)
|
|
542
|
+
|
|
543
|
+
@gc_profiler_was_enabled = GC::Profiler.enabled?
|
|
544
|
+
@gc_started_total_time = GC::Profiler.total_time
|
|
545
|
+
GC::Profiler.enable
|
|
546
|
+
rescue StandardError
|
|
547
|
+
nil
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def record_gc_pause_delta
|
|
551
|
+
return unless defined?(GC::Profiler)
|
|
552
|
+
|
|
553
|
+
total_time = GC::Profiler.total_time
|
|
554
|
+
delta_ns = ((total_time - @gc_started_total_time) * 1_000_000_000).round
|
|
555
|
+
record_wait(:gc_pause, delta_ns) if delta_ns.positive?
|
|
556
|
+
GC::Profiler.disable unless @gc_profiler_was_enabled
|
|
557
|
+
rescue StandardError
|
|
558
|
+
nil
|
|
559
|
+
end
|
|
560
|
+
|
|
561
|
+
def start_fiber_tracepoint
|
|
562
|
+
@fiber_tracepoint = TracePoint.new(:fiber_switch) do
|
|
563
|
+
@fiber_switches += 1
|
|
564
|
+
@fiber_ids[Fiber.current.object_id] = true
|
|
565
|
+
@fiber_thread_ids[Thread.current.object_id] = true
|
|
566
|
+
end
|
|
567
|
+
@fiber_tracepoint.enable
|
|
568
|
+
rescue StandardError
|
|
569
|
+
@fiber_tracepoint = nil
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def stop_fiber_tracepoint
|
|
573
|
+
@fiber_tracepoint&.disable
|
|
574
|
+
rescue StandardError
|
|
575
|
+
nil
|
|
576
|
+
end
|
|
577
|
+
|
|
578
|
+
def start_thread_sampler
|
|
579
|
+
period = [@sample_period, 0.005].max
|
|
580
|
+
@sampler_thread = Thread.new do
|
|
581
|
+
Thread.current[:corkscrews_internal] = true
|
|
582
|
+
Thread.current.name = "corkscrews thread sampler" if Thread.current.respond_to?(:name=)
|
|
583
|
+
until @stop_requested
|
|
584
|
+
sleep period
|
|
585
|
+
record_thread_samples
|
|
586
|
+
end
|
|
587
|
+
end
|
|
588
|
+
end
|
|
589
|
+
|
|
590
|
+
def stop_thread_sampler
|
|
591
|
+
return unless @sampler_thread
|
|
592
|
+
|
|
593
|
+
@sampler_thread.join(@sample_period * 4)
|
|
594
|
+
@sampler_thread.kill if @sampler_thread.alive?
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
def start_signal_sampler
|
|
598
|
+
@previous_prof_trap = Signal.trap("PROF") { record_signal_sample }
|
|
599
|
+
return if @native_signal_source
|
|
600
|
+
|
|
601
|
+
@setitimer = Fiddle::Function.new(
|
|
602
|
+
Fiddle::Handle::DEFAULT["setitimer"],
|
|
603
|
+
[Fiddle::TYPE_INT, Fiddle::TYPE_VOIDP, Fiddle::TYPE_VOIDP],
|
|
604
|
+
Fiddle::TYPE_INT
|
|
605
|
+
)
|
|
606
|
+
set_prof_timer(@sample_period)
|
|
607
|
+
end
|
|
608
|
+
|
|
609
|
+
def stop_signal_sampler
|
|
610
|
+
set_prof_timer(0.0) if @setitimer
|
|
611
|
+
Signal.trap("PROF", @previous_prof_trap || "DEFAULT")
|
|
612
|
+
end
|
|
613
|
+
|
|
614
|
+
def register_native_signal_source
|
|
615
|
+
return unless ENV["CORKSCREWS_NATIVE_SIGNALS"] == "1"
|
|
616
|
+
return unless Corkscrews::Native.monitor_available?
|
|
617
|
+
|
|
618
|
+
Corkscrews::Native.register_monitor_thread
|
|
619
|
+
@native_signal_source = true
|
|
620
|
+
rescue StandardError
|
|
621
|
+
@native_signal_source = false
|
|
622
|
+
end
|
|
623
|
+
|
|
624
|
+
def set_prof_timer(seconds)
|
|
625
|
+
usec = (seconds * 1_000_000).round
|
|
626
|
+
payload = itimerval_payload(usec)
|
|
627
|
+
@setitimer.call(2, payload, nil)
|
|
628
|
+
end
|
|
629
|
+
|
|
630
|
+
def itimerval_payload(usec)
|
|
631
|
+
if RbConfig::CONFIG.fetch("host_os").match?(/darwin|bsd/)
|
|
632
|
+
[0, usec, 0, usec].pack("q l x4 q l x4")
|
|
633
|
+
else
|
|
634
|
+
[0, usec, 0, usec].pack("q q q q")
|
|
635
|
+
end
|
|
636
|
+
end
|
|
637
|
+
|
|
638
|
+
def record_signal_sample
|
|
639
|
+
location = caller_locations(1, MAX_BACKTRACE_DEPTH).find do |candidate|
|
|
640
|
+
application_site(candidate.path, candidate.lineno)
|
|
641
|
+
end
|
|
642
|
+
return unless location
|
|
643
|
+
|
|
644
|
+
site = application_site(location.path, location.lineno)
|
|
645
|
+
return unless site
|
|
646
|
+
|
|
647
|
+
if defined?(Corkscrews::Sampler) && native_sample_allowed?(site)
|
|
648
|
+
if Corkscrews::Sampler.respond_to?(:record_site)
|
|
649
|
+
Corkscrews::Sampler.record_site(site_key(site[0]), site[1])
|
|
650
|
+
else
|
|
651
|
+
Corkscrews::Sampler.record_line(site[1])
|
|
652
|
+
end
|
|
653
|
+
end
|
|
654
|
+
record_sample(site, Thread.current.object_id, synchronize: false)
|
|
655
|
+
rescue StandardError
|
|
656
|
+
nil
|
|
657
|
+
end
|
|
658
|
+
|
|
659
|
+
def record_thread_samples
|
|
660
|
+
Thread.list.each do |thread|
|
|
661
|
+
next if thread == Thread.current
|
|
662
|
+
next unless thread.alive?
|
|
663
|
+
|
|
664
|
+
location = thread.backtrace_locations(0, MAX_BACKTRACE_DEPTH)&.find do |candidate|
|
|
665
|
+
application_site(candidate.path, candidate.lineno)
|
|
666
|
+
end
|
|
667
|
+
next unless location
|
|
668
|
+
|
|
669
|
+
site = application_site(location.path, location.lineno)
|
|
670
|
+
record_sample(site, thread.object_id) if site
|
|
671
|
+
end
|
|
672
|
+
rescue StandardError
|
|
673
|
+
nil
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
def record_sample(site, thread_id, synchronize: true)
|
|
677
|
+
return record_sample_unlocked(site, thread_id) unless synchronize
|
|
678
|
+
|
|
679
|
+
synchronize_state do
|
|
680
|
+
record_sample_unlocked(site, thread_id)
|
|
681
|
+
end
|
|
682
|
+
end
|
|
683
|
+
|
|
684
|
+
def record_sample_unlocked(site, thread_id)
|
|
685
|
+
@samples_by_site[site] += 1
|
|
686
|
+
@causal_samples_by_site[site] += 1 if @progress_threads.empty? || @progress_threads.key?(thread_id)
|
|
687
|
+
@sample_count += 1
|
|
688
|
+
advance_experiment_round({ kind: "line", file: site[0], line: site[1] })
|
|
689
|
+
end
|
|
690
|
+
|
|
691
|
+
def advance_experiment_round(fallback_target)
|
|
692
|
+
now = TimeSource.monotonic_ns
|
|
693
|
+
start_round(fallback_target, now) unless @current_experiment
|
|
694
|
+
finish_round(now) if now - @round_started_ns >= @round_max_ns
|
|
695
|
+
end
|
|
696
|
+
|
|
697
|
+
def start_round(fallback_target, now)
|
|
698
|
+
# Paper basis: Coz SOSP'15 Section 3.2 starts each experiment by
|
|
699
|
+
# selecting a target and speedup, then records progress counters.
|
|
700
|
+
# Paper URL: https://arxiv.org/abs/1608.03676
|
|
701
|
+
round = @controller.next_round(
|
|
702
|
+
progress_visits: total_progress_count,
|
|
703
|
+
duration_ns: now - @started_ns,
|
|
704
|
+
targets: active_round_targets(fallback_target),
|
|
705
|
+
history: @round_records
|
|
706
|
+
)
|
|
707
|
+
target = normalize_round_target(round.target)
|
|
708
|
+
@round_index += 1
|
|
709
|
+
@round_started_ns = now
|
|
710
|
+
@round_progress_start = total_progress_count
|
|
711
|
+
@current_experiment = {
|
|
712
|
+
round_index: @round_index,
|
|
713
|
+
target: target,
|
|
714
|
+
site_key: target[:kind] == "line" ? site_key(target[:file]) : 0,
|
|
715
|
+
speedup_pct: round.speedup_pct,
|
|
716
|
+
started_ns: now
|
|
717
|
+
}
|
|
718
|
+
Corkscrews::Native.set_experiment(
|
|
719
|
+
kind: target[:kind].to_sym,
|
|
720
|
+
iseq: @current_experiment[:site_key],
|
|
721
|
+
line: target.fetch(:line, 0).to_i,
|
|
722
|
+
speedup_pct: round.speedup_pct
|
|
723
|
+
)
|
|
724
|
+
end
|
|
725
|
+
|
|
726
|
+
def finish_round(now)
|
|
727
|
+
return unless @current_experiment
|
|
728
|
+
|
|
729
|
+
# Paper basis: Coz SOSP'15 Section 3.2 logs experiment duration,
|
|
730
|
+
# selected target/speedup, delays, and progress visits at round end.
|
|
731
|
+
# Paper URL: https://arxiv.org/abs/1608.03676
|
|
732
|
+
duration_ns = now - @current_experiment[:started_ns]
|
|
733
|
+
speedup_pct = @current_experiment[:speedup_pct]
|
|
734
|
+
share = share_for_target(@current_experiment[:target])
|
|
735
|
+
@round_records << {
|
|
736
|
+
type: "round",
|
|
737
|
+
pid: Process.pid,
|
|
738
|
+
run_id: @run_id,
|
|
739
|
+
repeat_index: @repeat_index,
|
|
740
|
+
round_index: @current_experiment[:round_index],
|
|
741
|
+
target: @current_experiment[:target],
|
|
742
|
+
speedup_pct: speedup_pct,
|
|
743
|
+
baseline: speedup_pct.zero?,
|
|
744
|
+
visits: total_progress_count - @round_progress_start,
|
|
745
|
+
physical_duration_ns: duration_ns,
|
|
746
|
+
virtual_duration_ns: [duration_ns * (1.0 - (share * speedup_pct / 100.0)), 0].max.round,
|
|
747
|
+
waived_debts: @suppressed_delay_ns
|
|
748
|
+
}
|
|
749
|
+
@current_experiment = nil
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
def total_progress_count
|
|
753
|
+
@progress_by_name.values.sum { |bucket| bucket[:count].to_i }
|
|
754
|
+
end
|
|
755
|
+
|
|
756
|
+
def share_for_target(target)
|
|
757
|
+
if target[:kind] == "wait"
|
|
758
|
+
# Paper basis: Ousterhout et al. NSDI'15 Section 2.3 treats
|
|
759
|
+
# blocked time as an upper bound on possible resource improvement.
|
|
760
|
+
# Paper URL: https://www.usenix.org/system/files/conference/nsdi15/nsdi15-paper-ousterhout.pdf
|
|
761
|
+
elapsed_ns = [TimeSource.monotonic_ns - @started_ns, 1].max
|
|
762
|
+
duration_ns = @wait_by_kind.dig(target[:name].to_s, :duration_ns).to_i
|
|
763
|
+
return [[duration_ns.to_f / elapsed_ns, 0.0].max, 0.95].min
|
|
764
|
+
end
|
|
765
|
+
|
|
766
|
+
return 0.0 unless @sample_count.positive?
|
|
767
|
+
|
|
768
|
+
key = [target[:file], target[:line]]
|
|
769
|
+
[[@causal_samples_by_site[key].to_f / @sample_count, 0.0].max, 0.95].min
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
def active_round_targets(fallback_target)
|
|
773
|
+
targets = []
|
|
774
|
+
targets.concat(line_round_targets) if line_targets_enabled?
|
|
775
|
+
targets.concat(wait_round_targets) if wait_targets_enabled?
|
|
776
|
+
targets << normalize_round_target(fallback_target) if targets.empty?
|
|
777
|
+
targets.uniq { |target| round_target_key(target) }
|
|
778
|
+
end
|
|
779
|
+
|
|
780
|
+
def line_round_targets
|
|
781
|
+
total = [@sample_count, 1].max
|
|
782
|
+
@samples_by_site.map do |(path, line), count|
|
|
783
|
+
causal_count = @causal_samples_by_site.fetch([path, line], count)
|
|
784
|
+
{
|
|
785
|
+
kind: "line",
|
|
786
|
+
file: path,
|
|
787
|
+
line: line,
|
|
788
|
+
sample_share: count.to_f / total,
|
|
789
|
+
causal_share: causal_count.to_f / total
|
|
790
|
+
}
|
|
791
|
+
end
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
def wait_round_targets
|
|
795
|
+
elapsed_ns = [TimeSource.monotonic_ns - @started_ns, 1].max
|
|
796
|
+
@wait_by_kind.map do |kind, bucket|
|
|
797
|
+
share = bucket[:duration_ns].to_f / elapsed_ns
|
|
798
|
+
{
|
|
799
|
+
kind: "wait",
|
|
800
|
+
name: kind.to_s,
|
|
801
|
+
sample_share: share,
|
|
802
|
+
causal_share: share
|
|
803
|
+
}
|
|
804
|
+
end
|
|
805
|
+
end
|
|
806
|
+
|
|
807
|
+
def normalize_round_target(target)
|
|
808
|
+
kind = target.fetch(:kind, target["kind"]).to_s
|
|
809
|
+
if kind == "wait"
|
|
810
|
+
{ kind: "wait", name: target.fetch(:name, target["name"]).to_s }
|
|
811
|
+
else
|
|
812
|
+
{ kind: "line", file: target.fetch(:file, target["file"]).to_s, line: target.fetch(:line, target["line"]).to_i }
|
|
813
|
+
end
|
|
814
|
+
end
|
|
815
|
+
|
|
816
|
+
def round_target_key(target)
|
|
817
|
+
if target[:kind] == "wait"
|
|
818
|
+
[target[:kind], target[:name]]
|
|
819
|
+
else
|
|
820
|
+
[target[:kind], target[:file], target[:line]]
|
|
821
|
+
end
|
|
822
|
+
end
|
|
823
|
+
|
|
824
|
+
def native_sample_allowed?(site)
|
|
825
|
+
delay_ns = delay_for_site(site)
|
|
826
|
+
return true unless delay_ns.positive?
|
|
827
|
+
return true unless @max_slowdown_pct
|
|
828
|
+
|
|
829
|
+
elapsed_ns = TimeSource.monotonic_ns - @started_ns
|
|
830
|
+
budget_ns = (elapsed_ns * (@max_slowdown_pct / 100.0)).round
|
|
831
|
+
if @issued_delay_ns + delay_ns <= budget_ns
|
|
832
|
+
@issued_delay_ns += delay_ns
|
|
833
|
+
true
|
|
834
|
+
else
|
|
835
|
+
@suppressed_delay_ns += delay_ns
|
|
836
|
+
false
|
|
837
|
+
end
|
|
838
|
+
end
|
|
839
|
+
|
|
840
|
+
def delay_for_site(site)
|
|
841
|
+
experiment = @current_experiment
|
|
842
|
+
return 0 unless experiment
|
|
843
|
+
return 0 unless experiment[:speedup_pct].positive?
|
|
844
|
+
return 0 unless experiment[:site_key] == site_key(site[0]) && experiment[:target][:line].to_i == site[1].to_i
|
|
845
|
+
|
|
846
|
+
@sample_period_ns * experiment[:speedup_pct] / 100
|
|
847
|
+
end
|
|
848
|
+
|
|
849
|
+
def site_key(path)
|
|
850
|
+
path.to_s.b.each_byte.reduce(FNV64_OFFSET_BASIS) do |hash, byte|
|
|
851
|
+
((hash ^ byte) * FNV64_PRIME) & FNV64_MASK
|
|
852
|
+
end
|
|
853
|
+
end
|
|
854
|
+
|
|
855
|
+
def application_site(path, line)
|
|
856
|
+
full_path = File.expand_path(path)
|
|
857
|
+
return nil if full_path.start_with?(INTERNAL_PATH)
|
|
858
|
+
return nil if full_path.include?("/rubygems/")
|
|
859
|
+
return nil if full_path.include?("/bundler/")
|
|
860
|
+
|
|
861
|
+
[full_path, line]
|
|
862
|
+
end
|
|
863
|
+
|
|
864
|
+
def line_targets_enabled?
|
|
865
|
+
%w[lines both].include?(@target_mode)
|
|
866
|
+
end
|
|
867
|
+
|
|
868
|
+
def wait_targets_enabled?
|
|
869
|
+
%w[waits both].include?(@target_mode)
|
|
870
|
+
end
|
|
871
|
+
|
|
872
|
+
def runtime_snapshot
|
|
873
|
+
{
|
|
874
|
+
fiber_switches: @fiber_switches,
|
|
875
|
+
fiber_count: @fiber_ids.length,
|
|
876
|
+
fiber_thread_count: @fiber_thread_ids.length,
|
|
877
|
+
fiber_scheduler: Fiber.respond_to?(:scheduler) && !Fiber.scheduler.nil?,
|
|
878
|
+
ractor_count: ractor_count
|
|
879
|
+
}
|
|
880
|
+
end
|
|
881
|
+
|
|
882
|
+
def ractor_count
|
|
883
|
+
return 1 unless defined?(Ractor)
|
|
884
|
+
|
|
885
|
+
ObjectSpace.each_object(Ractor).count
|
|
886
|
+
rescue StandardError
|
|
887
|
+
1
|
|
888
|
+
end
|
|
889
|
+
|
|
890
|
+
def max_slowdown_pct
|
|
891
|
+
value = ENV["CORKSCREWS_MAX_SLOWDOWN"]
|
|
892
|
+
return nil if value.nil? || value.empty?
|
|
893
|
+
|
|
894
|
+
parsed = value.to_f
|
|
895
|
+
parsed >= 0.0 ? parsed : nil
|
|
896
|
+
end
|
|
897
|
+
|
|
898
|
+
def write_json(file, payload)
|
|
899
|
+
file.write(JSON.generate(payload))
|
|
900
|
+
file.write("\n")
|
|
901
|
+
end
|
|
902
|
+
end
|
|
903
|
+
end
|