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,452 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
|
|
5
|
+
require_relative "statistics"
|
|
6
|
+
|
|
7
|
+
module Corkscrews
|
|
8
|
+
class Analysis
|
|
9
|
+
SPEEDUPS = (0..95).step(5).to_a.freeze
|
|
10
|
+
|
|
11
|
+
Run = Struct.new(:pid, :run_id, :repeat_index, :duration_ns, :samples, :waits, :progress, :latency, :rounds, :native, :runtime, keyword_init: true)
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
def load(path)
|
|
15
|
+
new(parse_events(path))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def parse_events(path)
|
|
21
|
+
File.readlines(path, chomp: true).filter_map do |line|
|
|
22
|
+
next if line.strip.empty?
|
|
23
|
+
|
|
24
|
+
JSON.parse(line)
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def initialize(events)
|
|
30
|
+
@events = events
|
|
31
|
+
@runs = build_runs(events)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
attr_reader :events, :runs
|
|
35
|
+
|
|
36
|
+
def empty?
|
|
37
|
+
@runs.empty?
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def aggregate
|
|
41
|
+
{
|
|
42
|
+
run_count: @runs.length,
|
|
43
|
+
duration_ns: @runs.sum(&:duration_ns),
|
|
44
|
+
total_samples: @runs.sum { |run| run.samples.values.sum { |value| value[:samples] } },
|
|
45
|
+
progress: aggregate_progress,
|
|
46
|
+
latency: aggregate_latency,
|
|
47
|
+
native: aggregate_native,
|
|
48
|
+
runtime: aggregate_runtime,
|
|
49
|
+
rounds: aggregate_rounds,
|
|
50
|
+
targets: aggregate_targets
|
|
51
|
+
}
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def target_curve(file: nil, line: nil, kind: "line", name: nil)
|
|
55
|
+
target = aggregate_targets.find do |candidate|
|
|
56
|
+
if kind.to_s == "wait"
|
|
57
|
+
candidate[:kind] == "wait" && candidate[:name] == name.to_s
|
|
58
|
+
else
|
|
59
|
+
candidate[:kind] == "line" &&
|
|
60
|
+
File.expand_path(candidate[:file]) == File.expand_path(file) &&
|
|
61
|
+
candidate[:line].to_i == line.to_i
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
target&.fetch(:curve)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
private
|
|
69
|
+
|
|
70
|
+
def build_runs(events)
|
|
71
|
+
grouped = Hash.new do |hash, key|
|
|
72
|
+
hash[key] = {
|
|
73
|
+
samples: Hash.new(0),
|
|
74
|
+
causal_samples: Hash.new(0),
|
|
75
|
+
waits: Hash.new { |wait_hash, wait_key| wait_hash[wait_key] = { count: 0, duration_ns: 0 } },
|
|
76
|
+
progress: {},
|
|
77
|
+
latency: {},
|
|
78
|
+
rounds: [],
|
|
79
|
+
native: {},
|
|
80
|
+
runtime: {},
|
|
81
|
+
native_samples: []
|
|
82
|
+
}
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
events.each do |event|
|
|
86
|
+
key = [event["pid"], event["run_id"], event["repeat_index"]]
|
|
87
|
+
bucket = grouped[key]
|
|
88
|
+
|
|
89
|
+
case event["type"]
|
|
90
|
+
when "process"
|
|
91
|
+
bucket[:duration_ns] = event.fetch("duration_ns")
|
|
92
|
+
when "progress"
|
|
93
|
+
bucket[:progress][event.fetch("name")] = {
|
|
94
|
+
count: event.fetch("count"),
|
|
95
|
+
observed_ns: event.fetch("observed_ns")
|
|
96
|
+
}
|
|
97
|
+
when "latency"
|
|
98
|
+
bucket[:latency][event.fetch("name")] = {
|
|
99
|
+
begin_count: event.fetch("begin_count"),
|
|
100
|
+
end_count: event.fetch("end_count"),
|
|
101
|
+
completed_count: event.fetch("completed_count", 0),
|
|
102
|
+
total_duration_ns: event.fetch("total_duration_ns", 0),
|
|
103
|
+
mean_duration_ns: event.fetch("mean_duration_ns", 0),
|
|
104
|
+
little_mean_duration_ns: event.fetch("little_mean_duration_ns", event.fetch("mean_duration_ns", 0)),
|
|
105
|
+
inflight_area_ns: event.fetch("inflight_area_ns", 0)
|
|
106
|
+
}
|
|
107
|
+
when "line"
|
|
108
|
+
target = event.fetch("target")
|
|
109
|
+
key = [target.fetch("file"), target.fetch("line")]
|
|
110
|
+
bucket[:samples][key] += event.fetch("samples")
|
|
111
|
+
bucket[:causal_samples][key] += event.fetch("causal_samples", event.fetch("samples"))
|
|
112
|
+
when "wait"
|
|
113
|
+
target = event.fetch("target")
|
|
114
|
+
wait = bucket[:waits][target.fetch("name")]
|
|
115
|
+
wait[:count] += event.fetch("count")
|
|
116
|
+
wait[:duration_ns] += event.fetch("duration_ns")
|
|
117
|
+
when "round"
|
|
118
|
+
bucket[:rounds] << event
|
|
119
|
+
when "native"
|
|
120
|
+
bucket[:native] = event
|
|
121
|
+
when "native_sample"
|
|
122
|
+
bucket[:native_samples] << event
|
|
123
|
+
when "runtime"
|
|
124
|
+
bucket[:runtime] = event
|
|
125
|
+
end
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
grouped.map do |(pid, run_id, repeat_index), bucket|
|
|
129
|
+
Run.new(
|
|
130
|
+
pid: pid,
|
|
131
|
+
run_id: run_id,
|
|
132
|
+
repeat_index: repeat_index,
|
|
133
|
+
duration_ns: bucket[:duration_ns].to_i,
|
|
134
|
+
samples: merge_sample_counts(bucket[:samples], bucket[:causal_samples]),
|
|
135
|
+
waits: bucket[:waits],
|
|
136
|
+
progress: bucket[:progress],
|
|
137
|
+
latency: bucket[:latency],
|
|
138
|
+
rounds: bucket[:rounds],
|
|
139
|
+
native: merge_native(bucket[:native], bucket[:native_samples]),
|
|
140
|
+
runtime: bucket[:runtime]
|
|
141
|
+
)
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def merge_native(native, native_samples)
|
|
146
|
+
return native if native_samples.empty?
|
|
147
|
+
|
|
148
|
+
snapshot = native.fetch("snapshot", {}).dup
|
|
149
|
+
snapshot["ring_events"] = native_samples.length
|
|
150
|
+
snapshot["ring_target_hits"] = native_samples.count { |entry| entry["target_hit"] }
|
|
151
|
+
native.merge("snapshot" => snapshot, "samples" => native_samples)
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
def merge_sample_counts(samples, causal_samples)
|
|
155
|
+
samples.to_h do |key, count|
|
|
156
|
+
[key, { samples: count, causal_samples: causal_samples.fetch(key, count) }]
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def aggregate_progress
|
|
161
|
+
names = @runs.flat_map { |run| run.progress.keys }.uniq
|
|
162
|
+
|
|
163
|
+
names.to_h do |name|
|
|
164
|
+
rates = @runs.filter_map do |run|
|
|
165
|
+
progress = run.progress[name]
|
|
166
|
+
next unless progress
|
|
167
|
+
|
|
168
|
+
ns = progress[:observed_ns].positive? ? progress[:observed_ns] : run.duration_ns
|
|
169
|
+
progress[:count].to_f / (ns.to_f / 1_000_000_000)
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
[name, {
|
|
173
|
+
mean_rate: Statistics.mean(rates),
|
|
174
|
+
rate_ci: Statistics.bootstrap_mean_ci(rates),
|
|
175
|
+
count: @runs.sum { |run| run.progress.dig(name, :count).to_i }
|
|
176
|
+
}]
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def aggregate_targets
|
|
181
|
+
all_sites = @runs.flat_map { |run| run.samples.keys }.uniq
|
|
182
|
+
|
|
183
|
+
# Paper basis: Coz SOSP'15 Section 2 "Producing a causal profile"
|
|
184
|
+
# groups experiments by the optimized line and plots program speedup
|
|
185
|
+
# against virtual speedup.
|
|
186
|
+
# Paper URL: https://arxiv.org/abs/1608.03676
|
|
187
|
+
line_targets = all_sites.map do |file, line|
|
|
188
|
+
shares = @runs.map do |run|
|
|
189
|
+
total = run.samples.values.sum { |value| value[:samples] }
|
|
190
|
+
total.positive? ? capped_share(run.samples.dig([file, line], :samples).to_f / total) : 0.0
|
|
191
|
+
end
|
|
192
|
+
causal_shares = @runs.map do |run|
|
|
193
|
+
total = run.samples.values.sum { |value| value[:samples] }
|
|
194
|
+
total.positive? ? capped_share(run.samples.dig([file, line], :causal_samples).to_f / total) : 0.0
|
|
195
|
+
end
|
|
196
|
+
mean_share = Statistics.mean(shares)
|
|
197
|
+
share_ci = Statistics.bootstrap_mean_ci(shares)
|
|
198
|
+
causal_curve = curve_for(causal_shares)
|
|
199
|
+
|
|
200
|
+
{
|
|
201
|
+
kind: "line",
|
|
202
|
+
file: file,
|
|
203
|
+
line: line,
|
|
204
|
+
samples: @runs.sum { |run| run.samples.dig([file, line], :samples).to_i },
|
|
205
|
+
causal_samples: @runs.sum { |run| run.samples.dig([file, line], :causal_samples).to_i },
|
|
206
|
+
sample_share: mean_share,
|
|
207
|
+
sample_share_ci: share_ci,
|
|
208
|
+
causal_share: Statistics.mean(causal_shares),
|
|
209
|
+
curve: causal_curve,
|
|
210
|
+
profile_curve: curve_for(shares),
|
|
211
|
+
round_curve: round_curve_for_target(kind: "line", file: file, line: line),
|
|
212
|
+
curve_source: "causal_share",
|
|
213
|
+
verdict: verdict_for(causal_curve)
|
|
214
|
+
}
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
wait_targets = aggregate_wait_targets
|
|
218
|
+
(line_targets + wait_targets).sort_by { |target| -target[:sample_share] }
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def aggregate_wait_targets
|
|
222
|
+
kinds = @runs.flat_map { |run| run.waits.keys }.uniq
|
|
223
|
+
|
|
224
|
+
# Paper basis: Ousterhout et al. NSDI'15 Section 2.3.1 measures
|
|
225
|
+
# blocked time from the compute thread's perspective and Section
|
|
226
|
+
# 2.3.2 treats subtracting blocked time as an upper-bound simulation.
|
|
227
|
+
# Paper URL: https://www.usenix.org/system/files/conference/nsdi15/nsdi15-paper-ousterhout.pdf
|
|
228
|
+
kinds.map do |kind|
|
|
229
|
+
shares = @runs.map do |run|
|
|
230
|
+
next 0.0 unless run.duration_ns.positive?
|
|
231
|
+
|
|
232
|
+
capped_share(run.waits.dig(kind, :duration_ns).to_f / run.duration_ns)
|
|
233
|
+
end
|
|
234
|
+
curve = curve_for(shares)
|
|
235
|
+
|
|
236
|
+
{
|
|
237
|
+
kind: "wait",
|
|
238
|
+
name: kind,
|
|
239
|
+
samples: @runs.sum { |run| run.waits.dig(kind, :count).to_i },
|
|
240
|
+
sample_share: Statistics.mean(shares),
|
|
241
|
+
sample_share_ci: Statistics.bootstrap_mean_ci(shares),
|
|
242
|
+
causal_share: Statistics.mean(shares),
|
|
243
|
+
duration_ns: @runs.sum { |run| run.waits.dig(kind, :duration_ns).to_i },
|
|
244
|
+
curve: curve,
|
|
245
|
+
round_curve: round_curve_for_target(kind: "wait", name: kind),
|
|
246
|
+
curve_source: "causal_share",
|
|
247
|
+
verdict: verdict_for(curve)
|
|
248
|
+
}
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def aggregate_latency
|
|
253
|
+
names = @runs.flat_map { |run| run.latency.keys }.uniq
|
|
254
|
+
|
|
255
|
+
# Paper basis: Coz SOSP'15 Section 3.3 "Measuring latency" uses
|
|
256
|
+
# Little's Law, L = lambda W, for paired progress points.
|
|
257
|
+
# Paper URL: https://arxiv.org/abs/1608.03676
|
|
258
|
+
names.to_h do |name|
|
|
259
|
+
means = @runs.filter_map do |run|
|
|
260
|
+
latency = run.latency[name]
|
|
261
|
+
next unless latency
|
|
262
|
+
|
|
263
|
+
latency[:mean_duration_ns].to_f / 1_000_000
|
|
264
|
+
end
|
|
265
|
+
little_means = @runs.filter_map do |run|
|
|
266
|
+
latency = run.latency[name]
|
|
267
|
+
next unless latency
|
|
268
|
+
|
|
269
|
+
latency[:little_mean_duration_ns].to_f / 1_000_000
|
|
270
|
+
end
|
|
271
|
+
[name, {
|
|
272
|
+
mean_ms: Statistics.mean(means),
|
|
273
|
+
mean_ms_ci: Statistics.bootstrap_mean_ci(means),
|
|
274
|
+
little_mean_ms: Statistics.mean(little_means),
|
|
275
|
+
little_mean_ms_ci: Statistics.bootstrap_mean_ci(little_means),
|
|
276
|
+
completed_count: @runs.sum { |run| run.latency.dig(name, :completed_count).to_i }
|
|
277
|
+
}]
|
|
278
|
+
end
|
|
279
|
+
end
|
|
280
|
+
|
|
281
|
+
def aggregate_native
|
|
282
|
+
snapshots = @runs.map { |run| run.native.fetch("snapshot", {}) if run.native }.compact
|
|
283
|
+
keys = snapshots.flat_map(&:keys).uniq
|
|
284
|
+
keys.to_h do |key|
|
|
285
|
+
values = snapshots.map { |snapshot| snapshot[key] }
|
|
286
|
+
[key.to_sym, aggregate_native_value(key, values)]
|
|
287
|
+
end
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
def aggregate_native_value(key, values)
|
|
291
|
+
return values.any? if values.any? { |value| value == true || value == false }
|
|
292
|
+
return values.map(&:to_i).max if native_max_key?(key)
|
|
293
|
+
|
|
294
|
+
values.sum { |value| value.to_i }
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def native_max_key?(key)
|
|
298
|
+
key.to_s.start_with?("thread_state_") || %w[
|
|
299
|
+
thread_live_count
|
|
300
|
+
thread_max_live_count
|
|
301
|
+
record_ring_size
|
|
302
|
+
].include?(key.to_s)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def aggregate_runtime
|
|
306
|
+
{
|
|
307
|
+
fiber_switches: @runs.sum { |run| run.runtime.fetch("fiber_switches", 0).to_i },
|
|
308
|
+
max_fiber_count: @runs.map { |run| run.runtime.fetch("fiber_count", 0).to_i }.max || 0,
|
|
309
|
+
fiber_thread_count: @runs.map { |run| run.runtime.fetch("fiber_thread_count", 0).to_i }.max || 0,
|
|
310
|
+
fiber_scheduler: @runs.any? { |run| run.runtime.fetch("fiber_scheduler", false) },
|
|
311
|
+
max_ractor_count: @runs.map { |run| run.runtime.fetch("ractor_count", 1).to_i }.max || 1
|
|
312
|
+
}
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def aggregate_rounds
|
|
316
|
+
@runs.flat_map(&:rounds)
|
|
317
|
+
end
|
|
318
|
+
|
|
319
|
+
def curve_for(shares)
|
|
320
|
+
points = SPEEDUPS.map do |speedup_pct|
|
|
321
|
+
improvements = shares.map { |share| improvement_pct(share, speedup_pct) }
|
|
322
|
+
ci = Statistics.bootstrap_mean_ci(improvements)
|
|
323
|
+
|
|
324
|
+
{
|
|
325
|
+
speedup_pct: speedup_pct,
|
|
326
|
+
improvement_pct: Statistics.mean(improvements),
|
|
327
|
+
ci_low: ci[0],
|
|
328
|
+
ci_high: ci[1]
|
|
329
|
+
}
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
Statistics.monotonic_regression(points)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def round_curve_for_target(kind:, file: nil, line: nil, name: nil)
|
|
336
|
+
target_rounds = matching_rounds(kind: kind, file: file, line: line, name: name)
|
|
337
|
+
return nil if target_rounds.empty?
|
|
338
|
+
|
|
339
|
+
# Paper basis: Coz SOSP'15 Section 2 requires a 0% baseline per
|
|
340
|
+
# target, then computes program speedup relative to that baseline.
|
|
341
|
+
# Paper URL: https://arxiv.org/abs/1608.03676
|
|
342
|
+
baseline_rates = target_rounds
|
|
343
|
+
.select { |round| round.fetch("baseline", false) || round.fetch("speedup_pct").to_i.zero? }
|
|
344
|
+
.filter_map { |round| round_rate(round) }
|
|
345
|
+
return nil if baseline_rates.empty?
|
|
346
|
+
|
|
347
|
+
baseline = Statistics.median(baseline_rates)
|
|
348
|
+
return nil unless baseline.positive?
|
|
349
|
+
|
|
350
|
+
observed_by_speedup = target_rounds.each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |round, hash|
|
|
351
|
+
speedup_pct = round.fetch("speedup_pct").to_i
|
|
352
|
+
rate = round_rate(round)
|
|
353
|
+
next unless rate
|
|
354
|
+
|
|
355
|
+
hash[speedup_pct] << ((rate / baseline) - 1.0) * 100.0
|
|
356
|
+
end
|
|
357
|
+
return nil unless round_curve_covered?(observed_by_speedup)
|
|
358
|
+
|
|
359
|
+
points = SPEEDUPS.map do |speedup_pct|
|
|
360
|
+
improvements = observed_by_speedup.fetch(speedup_pct, [])
|
|
361
|
+
|
|
362
|
+
if improvements.empty?
|
|
363
|
+
interpolated = interpolated_round_improvement(observed_by_speedup, speedup_pct)
|
|
364
|
+
{ speedup_pct: speedup_pct, improvement_pct: interpolated, ci_low: interpolated, ci_high: interpolated }
|
|
365
|
+
else
|
|
366
|
+
ci = Statistics.bootstrap_mean_ci(improvements)
|
|
367
|
+
{
|
|
368
|
+
speedup_pct: speedup_pct,
|
|
369
|
+
improvement_pct: Statistics.mean(improvements),
|
|
370
|
+
ci_low: ci[0],
|
|
371
|
+
ci_high: ci[1]
|
|
372
|
+
}
|
|
373
|
+
end
|
|
374
|
+
end
|
|
375
|
+
|
|
376
|
+
Statistics.monotonic_regression(points)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
def round_curve_covered?(observed_by_speedup)
|
|
380
|
+
return false if observed_by_speedup.fetch(0, []).length < 2
|
|
381
|
+
|
|
382
|
+
major_speedups = [25, 50, 90]
|
|
383
|
+
observed_nonzero = observed_by_speedup.keys.reject(&:zero?)
|
|
384
|
+
return true if major_speedups.all? { |speedup| observed_by_speedup.key?(speedup) }
|
|
385
|
+
|
|
386
|
+
observed_nonzero.length >= 6
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
def interpolated_round_improvement(observed_by_speedup, speedup_pct)
|
|
390
|
+
return Statistics.mean(observed_by_speedup.fetch(speedup_pct)) if observed_by_speedup.key?(speedup_pct)
|
|
391
|
+
|
|
392
|
+
observed = observed_by_speedup.keys.sort
|
|
393
|
+
lower = observed.select { |candidate| candidate < speedup_pct }.max
|
|
394
|
+
upper = observed.select { |candidate| candidate > speedup_pct }.min
|
|
395
|
+
return 0.0 unless lower || upper
|
|
396
|
+
return Statistics.mean(observed_by_speedup.fetch(upper)) unless lower
|
|
397
|
+
return Statistics.mean(observed_by_speedup.fetch(lower)) unless upper
|
|
398
|
+
|
|
399
|
+
lower_value = Statistics.mean(observed_by_speedup.fetch(lower))
|
|
400
|
+
upper_value = Statistics.mean(observed_by_speedup.fetch(upper))
|
|
401
|
+
span = upper - lower
|
|
402
|
+
return lower_value if span.zero?
|
|
403
|
+
|
|
404
|
+
lower_value + ((upper_value - lower_value) * ((speedup_pct - lower).to_f / span))
|
|
405
|
+
end
|
|
406
|
+
|
|
407
|
+
def matching_rounds(kind:, file: nil, line: nil, name: nil)
|
|
408
|
+
aggregate_rounds.select do |round|
|
|
409
|
+
target = round.fetch("target", {})
|
|
410
|
+
if kind.to_s == "wait"
|
|
411
|
+
target.fetch("kind", nil) == "wait" && target.fetch("name", nil).to_s == name.to_s
|
|
412
|
+
else
|
|
413
|
+
target.fetch("kind", nil) == "line" &&
|
|
414
|
+
File.expand_path(target.fetch("file", "")) == File.expand_path(file) &&
|
|
415
|
+
target.fetch("line", nil).to_i == line.to_i
|
|
416
|
+
end
|
|
417
|
+
end
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def round_rate(round)
|
|
421
|
+
visits = round.fetch("visits", 0).to_f
|
|
422
|
+
duration_ns = round.fetch("virtual_duration_ns", round.fetch("physical_duration_ns", 0)).to_f
|
|
423
|
+
return nil unless visits.positive? && duration_ns.positive?
|
|
424
|
+
|
|
425
|
+
visits / (duration_ns / 1_000_000_000.0)
|
|
426
|
+
end
|
|
427
|
+
|
|
428
|
+
def improvement_pct(share, speedup_pct)
|
|
429
|
+
# Paper basis: Coz SOSP'15 Section 2 / Figure 3 virtual speedup:
|
|
430
|
+
# reducing a fraction of execution by s predicts 1 / (1 - share*s).
|
|
431
|
+
# Paper URL: https://arxiv.org/abs/1608.03676
|
|
432
|
+
reduced_fraction = share.to_f * (speedup_pct.to_f / 100.0)
|
|
433
|
+
return 0.0 if reduced_fraction <= 0.0
|
|
434
|
+
return Float::INFINITY if reduced_fraction >= 1.0
|
|
435
|
+
|
|
436
|
+
((1.0 / (1.0 - reduced_fraction)) - 1.0) * 100.0
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def capped_share(value)
|
|
440
|
+
[[value.to_f, 0.0].max, 0.95].min
|
|
441
|
+
end
|
|
442
|
+
|
|
443
|
+
def verdict_for(curve)
|
|
444
|
+
point = curve.find { |entry| entry[:speedup_pct] == 25 }
|
|
445
|
+
return "insufficient" unless point
|
|
446
|
+
return "bottleneck" if point[:ci_low].positive?
|
|
447
|
+
return "negative" if point[:ci_high].negative?
|
|
448
|
+
|
|
449
|
+
"unknown"
|
|
450
|
+
end
|
|
451
|
+
end
|
|
452
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../corkscrews"
|
|
4
|
+
require_relative "native"
|
|
5
|
+
|
|
6
|
+
if ENV["CORKSCREWS_PROFILE"] == "1"
|
|
7
|
+
Corkscrews::Native.install_hooks!
|
|
8
|
+
|
|
9
|
+
if ENV["CORKSCREWS_PRIMITIVES"] == "1"
|
|
10
|
+
require_relative "primitives"
|
|
11
|
+
Corkscrews::Primitives.install!
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
output = ENV.fetch("CORKSCREWS_OUTPUT")
|
|
15
|
+
Corkscrews.start!(
|
|
16
|
+
output: output,
|
|
17
|
+
run_id: ENV["CORKSCREWS_RUN_ID"],
|
|
18
|
+
repeat_index: ENV["CORKSCREWS_REPEAT_INDEX"],
|
|
19
|
+
sample_period_ms: ENV["CORKSCREWS_SAMPLE_PERIOD_MS"]
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
at_exit do
|
|
23
|
+
Corkscrews.stop!
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,159 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fileutils"
|
|
4
|
+
require "json"
|
|
5
|
+
require "optparse"
|
|
6
|
+
require "securerandom"
|
|
7
|
+
|
|
8
|
+
require_relative "../corkscrews"
|
|
9
|
+
require_relative "report"
|
|
10
|
+
require_relative "validate"
|
|
11
|
+
|
|
12
|
+
module Corkscrews
|
|
13
|
+
class CLI
|
|
14
|
+
def initialize(argv)
|
|
15
|
+
@argv = argv.dup
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def run
|
|
19
|
+
command = @argv.shift
|
|
20
|
+
|
|
21
|
+
case command
|
|
22
|
+
when "run"
|
|
23
|
+
run_profile(@argv)
|
|
24
|
+
when "report"
|
|
25
|
+
run_report(@argv)
|
|
26
|
+
when "validate"
|
|
27
|
+
run_validate(@argv)
|
|
28
|
+
when "-h", "--help", nil
|
|
29
|
+
puts help
|
|
30
|
+
0
|
|
31
|
+
else
|
|
32
|
+
warn "unknown command: #{command}"
|
|
33
|
+
warn help
|
|
34
|
+
2
|
|
35
|
+
end
|
|
36
|
+
rescue Error => e
|
|
37
|
+
warn "error: #{e.message}"
|
|
38
|
+
1
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
private
|
|
42
|
+
|
|
43
|
+
def run_profile(argv)
|
|
44
|
+
options = {
|
|
45
|
+
repeat: 1,
|
|
46
|
+
output: "run.corkscrews.ndjson",
|
|
47
|
+
sample_period_ms: 1.0,
|
|
48
|
+
targets: "lines",
|
|
49
|
+
max_slowdown: nil
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
parser = OptionParser.new do |opts|
|
|
53
|
+
opts.banner = "usage: corkscrews run [options] -- ruby script.rb"
|
|
54
|
+
opts.on("--repeat N", Integer, "number of process repetitions") { |value| options[:repeat] = value }
|
|
55
|
+
opts.on("--duration SEC", Float, "advisory duration forwarded to benchmarks") { |value| options[:duration] = value }
|
|
56
|
+
opts.on("--progress NAME", "expected progress point name") { |value| options[:progress] = value }
|
|
57
|
+
opts.on("--targets KIND", "lines, waits, or both") { |value| options[:targets] = value }
|
|
58
|
+
opts.on("--output PATH", "NDJSON output path") { |value| options[:output] = value }
|
|
59
|
+
opts.on("--sample-period-ms MS", Float, "Ruby sampler period") { |value| options[:sample_period_ms] = value }
|
|
60
|
+
opts.on("--max-slowdown PCT", Float, "advisory slowdown budget recorded for reports") { |value| options[:max_slowdown] = value }
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
command = parse_command_after_separator(parser, argv)
|
|
64
|
+
raise Error, "missing command after --" if command.empty?
|
|
65
|
+
|
|
66
|
+
output = File.expand_path(options[:output])
|
|
67
|
+
FileUtils.rm_f(output)
|
|
68
|
+
run_id = SecureRandom.hex(8)
|
|
69
|
+
|
|
70
|
+
options[:repeat].times do |index|
|
|
71
|
+
env = profile_environment(output, run_id, index, options)
|
|
72
|
+
ok = system(env, *command)
|
|
73
|
+
raise Error, "profiled command failed on repeat #{index + 1}: #{command.join(" ")}" unless ok
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
puts output
|
|
77
|
+
0
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def run_report(argv)
|
|
81
|
+
options = { html: nil, firefox: nil, limit: 10 }
|
|
82
|
+
parser = OptionParser.new do |opts|
|
|
83
|
+
opts.banner = "usage: corkscrews report [options] run.corkscrews.ndjson"
|
|
84
|
+
opts.on("--html PATH", "write an HTML report") { |value| options[:html] = value }
|
|
85
|
+
opts.on("--firefox PATH", "write a Firefox Profiler compatible aggregate JSON") { |value| options[:firefox] = value }
|
|
86
|
+
opts.on("--limit N", Integer, "number of line targets to show") { |value| options[:limit] = value }
|
|
87
|
+
end
|
|
88
|
+
parser.parse!(argv)
|
|
89
|
+
|
|
90
|
+
path = argv.shift
|
|
91
|
+
raise Error, "missing NDJSON path" unless path
|
|
92
|
+
|
|
93
|
+
report = Report.new(path)
|
|
94
|
+
puts report.to_text(limit: options[:limit])
|
|
95
|
+
report.write_html(options[:html], limit: options[:limit]) if options[:html]
|
|
96
|
+
report.write_firefox(options[:firefox]) if options[:firefox]
|
|
97
|
+
0
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def run_validate(argv)
|
|
101
|
+
options = { quick: false, benchmark: nil }
|
|
102
|
+
parser = OptionParser.new do |opts|
|
|
103
|
+
opts.banner = "usage: corkscrews validate [options]"
|
|
104
|
+
opts.on("--quick", "use short validation settings") { options[:quick] = true }
|
|
105
|
+
opts.on("--benchmark ID", "run one benchmark") { |value| options[:benchmark] = value }
|
|
106
|
+
end
|
|
107
|
+
parser.parse!(argv)
|
|
108
|
+
|
|
109
|
+
result = Validate.run_all(quick: options[:quick], benchmark: options[:benchmark])
|
|
110
|
+
puts JSON.pretty_generate(result.to_h)
|
|
111
|
+
result.ok? ? 0 : 1
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def parse_command_after_separator(parser, argv)
|
|
115
|
+
separator_index = argv.index("--")
|
|
116
|
+
raise Error, "missing -- separator" unless separator_index
|
|
117
|
+
|
|
118
|
+
option_args = argv.take(separator_index)
|
|
119
|
+
command = argv.drop(separator_index + 1)
|
|
120
|
+
parser.parse!(option_args)
|
|
121
|
+
command
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
def profile_environment(output, run_id, index, options)
|
|
125
|
+
lib_path = File.expand_path("..", __dir__)
|
|
126
|
+
ext_path = File.expand_path("../../ext/corkscrews", __dir__)
|
|
127
|
+
ruby_lib = [lib_path, ext_path, ENV["RUBYLIB"]].compact.reject(&:empty?).join(File::PATH_SEPARATOR)
|
|
128
|
+
ruby_opt = ["-rcorkscrews/boot", ENV["RUBYOPT"]].compact.reject(&:empty?).join(" ")
|
|
129
|
+
|
|
130
|
+
{
|
|
131
|
+
"RUBYLIB" => ruby_lib,
|
|
132
|
+
"RUBYOPT" => ruby_opt,
|
|
133
|
+
"CORKSCREWS_PROFILE" => "1",
|
|
134
|
+
"CORKSCREWS_OUTPUT" => output,
|
|
135
|
+
"CORKSCREWS_RUN_ID" => run_id,
|
|
136
|
+
"CORKSCREWS_REPEAT_INDEX" => index.to_s,
|
|
137
|
+
"CORKSCREWS_SAMPLE_PERIOD_MS" => options[:sample_period_ms].to_s,
|
|
138
|
+
"CORKSCREWS_TARGETS" => options[:targets].to_s,
|
|
139
|
+
"CORKSCREWS_PRIMITIVES" => primitive_instrumentation?(options).to_s,
|
|
140
|
+
"CORKSCREWS_MAX_SLOWDOWN" => options[:max_slowdown].to_s,
|
|
141
|
+
"CORKSCREWS_PROGRESS" => options[:progress].to_s,
|
|
142
|
+
"CS_BENCH_DURATION" => options[:duration].to_s
|
|
143
|
+
}
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def primitive_instrumentation?(options)
|
|
147
|
+
%w[waits both].include?(options[:targets].to_s) ? "1" : "0"
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def help
|
|
151
|
+
<<~TEXT
|
|
152
|
+
usage:
|
|
153
|
+
corkscrews run [options] -- ruby script.rb
|
|
154
|
+
corkscrews report [options] run.corkscrews.ndjson
|
|
155
|
+
corkscrews validate [options]
|
|
156
|
+
TEXT
|
|
157
|
+
end
|
|
158
|
+
end
|
|
159
|
+
end
|