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.
Files changed (59) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE.txt +21 -0
  3. data/README.md +130 -0
  4. data/Rakefile +20 -0
  5. data/exe/corkscrews +9 -0
  6. data/ext/corkscrews/corkscrews.c +24 -0
  7. data/ext/corkscrews/corkscrews_native.h +85 -0
  8. data/ext/corkscrews/delay.c +259 -0
  9. data/ext/corkscrews/extconf.rb +25 -0
  10. data/ext/corkscrews/hooks.c +156 -0
  11. data/ext/corkscrews/monitor.c +117 -0
  12. data/ext/corkscrews/record.c +109 -0
  13. data/ext/corkscrews/sampler.c +99 -0
  14. data/lib/corkscrews/analysis.rb +452 -0
  15. data/lib/corkscrews/boot.rb +25 -0
  16. data/lib/corkscrews/cli.rb +159 -0
  17. data/lib/corkscrews/controller.rb +137 -0
  18. data/lib/corkscrews/firefox_profile.rb +290 -0
  19. data/lib/corkscrews/native.rb +108 -0
  20. data/lib/corkscrews/primitives.rb +127 -0
  21. data/lib/corkscrews/rack.rb +39 -0
  22. data/lib/corkscrews/recorder.rb +903 -0
  23. data/lib/corkscrews/report.rb +241 -0
  24. data/lib/corkscrews/statistics.rb +112 -0
  25. data/lib/corkscrews/time_source.rb +11 -0
  26. data/lib/corkscrews/validate.rb +348 -0
  27. data/lib/corkscrews/version.rb +5 -0
  28. data/lib/corkscrews.rb +43 -0
  29. data/validate/benchmarks/b01_serial_hotspot/base.rb +28 -0
  30. data/validate/benchmarks/b01_serial_hotspot/manifest.yml +23 -0
  31. data/validate/benchmarks/b01_serial_hotspot/optimized.rb +28 -0
  32. data/validate/benchmarks/b02_false_hotspot/base.rb +43 -0
  33. data/validate/benchmarks/b02_false_hotspot/manifest.yml +19 -0
  34. data/validate/benchmarks/b02_false_hotspot/optimized.rb +43 -0
  35. data/validate/benchmarks/b03_io_wait/base.rb +29 -0
  36. data/validate/benchmarks/b03_io_wait/manifest.yml +16 -0
  37. data/validate/benchmarks/b03_io_wait/optimized.rb +29 -0
  38. data/validate/benchmarks/b04_lock_contention/base.rb +30 -0
  39. data/validate/benchmarks/b04_lock_contention/manifest.yml +19 -0
  40. data/validate/benchmarks/b04_lock_contention/optimized.rb +30 -0
  41. data/validate/benchmarks/b05_gvl_contention/base.rb +24 -0
  42. data/validate/benchmarks/b05_gvl_contention/manifest.yml +17 -0
  43. data/validate/benchmarks/b05_gvl_contention/optimized.rb +24 -0
  44. data/validate/benchmarks/b06_gc_pressure/base.rb +16 -0
  45. data/validate/benchmarks/b06_gc_pressure/manifest.yml +16 -0
  46. data/validate/benchmarks/b06_gc_pressure/optimized.rb +17 -0
  47. data/validate/benchmarks/b07_pipeline/base.rb +34 -0
  48. data/validate/benchmarks/b07_pipeline/manifest.yml +17 -0
  49. data/validate/benchmarks/b07_pipeline/optimized.rb +34 -0
  50. data/validate/benchmarks/b08_native_attribution/base.rb +18 -0
  51. data/validate/benchmarks/b08_native_attribution/manifest.yml +18 -0
  52. data/validate/benchmarks/b08_native_attribution/optimized.rb +18 -0
  53. data/validate/benchmarks/support.rb +29 -0
  54. data/validate/harness/run_all.rb +16 -0
  55. data/validate/harness/stats_check.py +27 -0
  56. data/validate/harness/t1.rb +15 -0
  57. data/validate/harness/t4.rb +15 -0
  58. data/validate/harness/t5.rb +15 -0
  59. metadata +118 -0
@@ -0,0 +1,241 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "cgi"
4
+ require "json"
5
+
6
+ require_relative "analysis"
7
+ require_relative "firefox_profile"
8
+
9
+ module Corkscrews
10
+ class Report
11
+ def initialize(path)
12
+ @path = path
13
+ @analysis = Analysis.load(path)
14
+ end
15
+
16
+ def to_text(limit: 10)
17
+ aggregate = @analysis.aggregate
18
+ lines = []
19
+ lines << "corkscrews report: #{@path}"
20
+ lines << "runs: #{aggregate[:run_count]} samples: #{aggregate[:total_samples]} duration: #{format_seconds(aggregate[:duration_ns])}"
21
+ unless aggregate[:native].empty?
22
+ lines << "native: samples=#{aggregate[:native][:samples].to_i} target_hits=#{aggregate[:native][:target_hits].to_i} ring_events=#{aggregate[:native][:ring_events].to_i} monitor_ticks=#{aggregate[:native][:monitor_ticks].to_i} monitor_signals=#{aggregate[:native][:monitor_signals].to_i} monitor_signal_failures=#{aggregate[:native][:monitor_signal_failures].to_i} live_threads=#{aggregate[:native][:thread_live_count].to_i} max_live_threads=#{aggregate[:native][:thread_max_live_count].to_i} debt_settled_ns=#{aggregate[:native][:debt_settled_ns].to_i} gc_pause_ns=#{aggregate[:native][:gc_pause_ns].to_i}"
23
+ end
24
+ lines << "runtime: fiber_switches=#{aggregate[:runtime][:fiber_switches]} max_fibers=#{aggregate[:runtime][:max_fiber_count]} fiber_threads=#{aggregate[:runtime][:fiber_thread_count]} fiber_scheduler=#{aggregate[:runtime][:fiber_scheduler]} max_ractors=#{aggregate[:runtime][:max_ractor_count]}"
25
+ lines << ""
26
+
27
+ if aggregate[:progress].empty?
28
+ lines << "progress: none"
29
+ else
30
+ lines << "progress:"
31
+ aggregate[:progress].each do |name, stats|
32
+ ci = stats[:rate_ci]
33
+ lines << format(
34
+ " %-16s rate=%10.2f/s ci=[%10.2f,%10.2f] count=%d",
35
+ name,
36
+ stats[:mean_rate],
37
+ ci[0],
38
+ ci[1],
39
+ stats[:count]
40
+ )
41
+ end
42
+ end
43
+
44
+ unless aggregate[:latency].empty?
45
+ lines << ""
46
+ lines << "latency:"
47
+ aggregate[:latency].each do |name, stats|
48
+ ci = stats[:mean_ms_ci]
49
+ little_ci = stats[:little_mean_ms_ci]
50
+ lines << format(
51
+ " %-16s mean=%8.3fms little=%8.3fms ci=[%8.3f,%8.3f] little_ci=[%8.3f,%8.3f] completed=%d",
52
+ name,
53
+ stats[:mean_ms],
54
+ stats[:little_mean_ms],
55
+ ci[0],
56
+ ci[1],
57
+ little_ci[0],
58
+ little_ci[1],
59
+ stats[:completed_count]
60
+ )
61
+ end
62
+ end
63
+
64
+ lines << ""
65
+ lines << "top targets:"
66
+ lines << " share causal verdict samples target predicted improvement at 25% / 50% / 90%"
67
+ aggregate[:targets].first(limit).each do |target|
68
+ curve = target[:curve]
69
+ i25 = curve.find { |point| point[:speedup_pct] == 25 }[:improvement_pct]
70
+ i50 = curve.find { |point| point[:speedup_pct] == 50 }[:improvement_pct]
71
+ i90 = curve.find { |point| point[:speedup_pct] == 90 }[:improvement_pct]
72
+ lines << format(
73
+ " %5.1f%% %5.1f%% %-10s %7d %-62s %6.2f%% / %6.2f%% / %6.2f%%",
74
+ target[:sample_share] * 100.0,
75
+ target[:causal_share].to_f * 100.0,
76
+ target[:verdict],
77
+ target[:samples],
78
+ target_label(target),
79
+ i25,
80
+ i50,
81
+ i90
82
+ )
83
+ end
84
+
85
+ disagreements = disagreement_targets(aggregate[:targets]).first(5)
86
+ unless disagreements.empty?
87
+ lines << ""
88
+ lines << "disagreement highlights:"
89
+ disagreements.each do |target|
90
+ lines << format(
91
+ " %-70s observed=%5.1f%% causal=%5.1f%% verdict=%s",
92
+ target_label(target),
93
+ target[:sample_share] * 100.0,
94
+ target[:causal_share].to_f * 100.0,
95
+ target[:verdict]
96
+ )
97
+ end
98
+ end
99
+
100
+ lines.join("\n")
101
+ end
102
+
103
+ def write_firefox(path)
104
+ aggregate = @analysis.aggregate
105
+ File.write(path, JSON.pretty_generate(FirefoxProfile.build(aggregate)))
106
+ end
107
+
108
+ def write_html(path, limit: 20)
109
+ aggregate = @analysis.aggregate
110
+ File.write(path, html_document(aggregate, limit))
111
+ end
112
+
113
+ private
114
+
115
+ def format_seconds(ns)
116
+ format("%.3fs", ns.to_f / 1_000_000_000)
117
+ end
118
+
119
+ def html_document(aggregate, limit)
120
+ rows = aggregate[:targets].first(limit).map do |target|
121
+ curve = target[:curve]
122
+ i25 = curve.find { |point| point[:speedup_pct] == 25 }[:improvement_pct]
123
+ i50 = curve.find { |point| point[:speedup_pct] == 50 }[:improvement_pct]
124
+ i90 = curve.find { |point| point[:speedup_pct] == 90 }[:improvement_pct]
125
+
126
+ <<~HTML
127
+ <tr>
128
+ <td>#{escape(target_label(target))}</td>
129
+ <td>#{format("%.1f%%", target[:sample_share] * 100.0)}</td>
130
+ <td>#{format("%.1f%%", target[:causal_share].to_f * 100.0)}</td>
131
+ <td>#{escape(target[:verdict])}</td>
132
+ <td>#{target[:samples]}</td>
133
+ <td>#{format("%.2f%%", i25)}</td>
134
+ <td>#{format("%.2f%%", i50)}</td>
135
+ <td>#{format("%.2f%%", i90)}</td>
136
+ <td>#{curve_svg(curve)}</td>
137
+ </tr>
138
+ HTML
139
+ end.join
140
+
141
+ latency_rows = aggregate[:latency].map do |name, stats|
142
+ <<~HTML
143
+ <tr>
144
+ <td>#{escape(name)}</td>
145
+ <td>#{format("%.3fms", stats[:mean_ms])}</td>
146
+ <td>#{format("%.3fms", stats[:little_mean_ms])}</td>
147
+ <td>#{format("[%.3f, %.3f]", *stats[:mean_ms_ci])}</td>
148
+ <td>#{format("[%.3f, %.3f]", *stats[:little_mean_ms_ci])}</td>
149
+ <td>#{stats[:completed_count]}</td>
150
+ </tr>
151
+ HTML
152
+ end.join
153
+
154
+ <<~HTML
155
+ <!doctype html>
156
+ <html lang="en">
157
+ <head>
158
+ <meta charset="utf-8">
159
+ <title>corkscrews report</title>
160
+ <style>
161
+ body { font-family: system-ui, sans-serif; margin: 2rem; color: #1f2933; }
162
+ table { border-collapse: collapse; width: 100%; }
163
+ th, td { border-bottom: 1px solid #d8dee9; padding: 0.5rem; text-align: left; }
164
+ th { background: #eef2f7; }
165
+ .meta { margin-bottom: 1rem; color: #52606d; }
166
+ </style>
167
+ </head>
168
+ <body>
169
+ <h1>corkscrews report</h1>
170
+ <p class="meta">runs: #{aggregate[:run_count]} samples: #{aggregate[:total_samples]} duration: #{format_seconds(aggregate[:duration_ns])}</p>
171
+ <p class="meta">fiber switches: #{aggregate[:runtime][:fiber_switches]} max fibers: #{aggregate[:runtime][:max_fiber_count]} fiber threads: #{aggregate[:runtime][:fiber_thread_count]} scheduler: #{aggregate[:runtime][:fiber_scheduler]} max ractors: #{aggregate[:runtime][:max_ractor_count]}</p>
172
+ <table>
173
+ <thead>
174
+ <tr>
175
+ <th>Target</th>
176
+ <th>Sample share</th>
177
+ <th>Causal share</th>
178
+ <th>Verdict</th>
179
+ <th>Samples</th>
180
+ <th>25% virtual speedup</th>
181
+ <th>50% virtual speedup</th>
182
+ <th>90% virtual speedup</th>
183
+ <th>Curve</th>
184
+ </tr>
185
+ </thead>
186
+ <tbody>
187
+ #{rows}
188
+ </tbody>
189
+ </table>
190
+ <h2>Latency</h2>
191
+ <table>
192
+ <thead>
193
+ <tr>
194
+ <th>Name</th>
195
+ <th>Mean</th>
196
+ <th>Little mean</th>
197
+ <th>CI</th>
198
+ <th>Little CI</th>
199
+ <th>Completed</th>
200
+ </tr>
201
+ </thead>
202
+ <tbody>
203
+ #{latency_rows}
204
+ </tbody>
205
+ </table>
206
+ </body>
207
+ </html>
208
+ HTML
209
+ end
210
+
211
+ def escape(value)
212
+ CGI.escapeHTML(value.to_s)
213
+ end
214
+
215
+ def target_label(target)
216
+ if target[:kind] == "wait"
217
+ "wait:#{target[:name]}"
218
+ else
219
+ "#{target[:file]}:#{target[:line]}"
220
+ end
221
+ end
222
+
223
+ def disagreement_targets(targets)
224
+ targets.select do |target|
225
+ target[:sample_share].to_f >= 0.05 && target[:causal_share].to_f <= target[:sample_share].to_f / 3.0
226
+ end
227
+ end
228
+
229
+ def curve_svg(curve)
230
+ max_y = [curve.map { |point| point[:improvement_pct].to_f }.max.to_f, 1.0].max
231
+ points = curve.map do |point|
232
+ x = (point[:speedup_pct].to_f / 95.0) * 160.0
233
+ y = 40.0 - ((point[:improvement_pct].to_f / max_y) * 36.0)
234
+ "#{format("%.1f", x)},#{format("%.1f", y)}"
235
+ end.join(" ")
236
+
237
+ %(<svg width="170" height="44" viewBox="0 0 170 44" role="img" aria-label="virtual speedup curve"><polyline points="#{points}" fill="none" stroke="#1f6feb" stroke-width="2"/></svg>)
238
+ end
239
+
240
+ end
241
+ end
@@ -0,0 +1,112 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Corkscrews
4
+ module Statistics
5
+ module_function
6
+
7
+ def mean(values)
8
+ return 0.0 if values.empty?
9
+
10
+ values.sum.to_f / values.length
11
+ end
12
+
13
+ def percentile(values, pct)
14
+ return 0.0 if values.empty?
15
+
16
+ sorted = values.sort
17
+ index = (pct * (sorted.length - 1)).round
18
+ sorted.fetch(index)
19
+ end
20
+
21
+ def percentile_ci(values, lower: 0.025, upper: 0.975)
22
+ [percentile(values, lower), percentile(values, upper)]
23
+ end
24
+
25
+ def median(values)
26
+ return 0.0 if values.empty?
27
+
28
+ sorted = values.sort
29
+ middle = sorted.length / 2
30
+ return sorted.fetch(middle).to_f if sorted.length.odd?
31
+
32
+ (sorted.fetch(middle - 1) + sorted.fetch(middle)).to_f / 2.0
33
+ end
34
+
35
+ def bootstrap_mean_ci(values, iterations: 1_000, random: Random.new(12_345))
36
+ return [0.0, 0.0] if values.empty?
37
+ return [values.first.to_f, values.first.to_f] if values.length == 1
38
+
39
+ # Paper basis: Kalibera & Jones, "Quantifying Performance Changes
40
+ # with Effect Size Confidence Intervals" (arXiv:2007.10899),
41
+ # motivates reporting uncertainty for performance effect sizes.
42
+ # Paper URL: https://arxiv.org/abs/2007.10899
43
+ means = Array.new(iterations) do
44
+ sample = Array.new(values.length) { values.fetch(random.rand(values.length)) }
45
+ mean(sample)
46
+ end
47
+ percentile_ci(means)
48
+ end
49
+
50
+ def monotonic_regression(points)
51
+ ordered = points.sort_by { |point| point[:speedup_pct] }
52
+ blocks = []
53
+
54
+ # Paper basis: Coz SOSP'15 Section 2 plots increasing virtual
55
+ # speedup on the x-axis; this pool-adjacent-violators pass enforces
56
+ # the physical monotonicity expected from that curve.
57
+ # Paper URL: https://arxiv.org/abs/1608.03676
58
+ ordered.each do |point|
59
+ blocks << {
60
+ start: point[:speedup_pct],
61
+ finish: point[:speedup_pct],
62
+ weight: 1.0,
63
+ value: point[:improvement_pct].to_f
64
+ }
65
+
66
+ while blocks.length >= 2 && blocks[-2][:value] > blocks[-1][:value]
67
+ right = blocks.pop
68
+ left = blocks.pop
69
+ weight = left[:weight] + right[:weight]
70
+ blocks << {
71
+ start: left[:start],
72
+ finish: right[:finish],
73
+ weight: weight,
74
+ value: ((left[:value] * left[:weight]) + (right[:value] * right[:weight])) / weight
75
+ }
76
+ end
77
+ end
78
+
79
+ blocks.flat_map do |block|
80
+ ordered
81
+ .select { |point| point[:speedup_pct] >= block[:start] && point[:speedup_pct] <= block[:finish] }
82
+ .map { |point| point.merge(improvement_pct: block[:value]) }
83
+ end
84
+ end
85
+
86
+ def kendall_tau(left, right)
87
+ return 0.0 unless left.length == right.length
88
+ return 1.0 if left.length < 2
89
+
90
+ concordant = 0
91
+ discordant = 0
92
+ (0...(left.length - 1)).each do |i|
93
+ ((i + 1)...left.length).each do |j|
94
+ left_delta = left[i] <=> left[j]
95
+ right_delta = right[i] <=> right[j]
96
+ next if left_delta.zero? || right_delta.zero?
97
+
98
+ if left_delta == right_delta
99
+ concordant += 1
100
+ else
101
+ discordant += 1
102
+ end
103
+ end
104
+ end
105
+
106
+ total = concordant + discordant
107
+ return 0.0 if total.zero?
108
+
109
+ (concordant - discordant).to_f / total
110
+ end
111
+ end
112
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Corkscrews
4
+ module TimeSource
5
+ module_function
6
+
7
+ def monotonic_ns
8
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :nanosecond)
9
+ end
10
+ end
11
+ end