cataract 0.1.2 → 0.1.4
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/.github/workflows/ci-manual-rubies.yml +27 -0
- data/.overcommit.yml +1 -1
- data/.rubocop.yml +62 -0
- data/.rubocop_todo.yml +186 -0
- data/BENCHMARKS.md +60 -139
- data/CHANGELOG.md +14 -0
- data/README.md +30 -2
- data/Rakefile +49 -22
- data/cataract.gemspec +4 -1
- data/ext/cataract/cataract.c +47 -47
- data/ext/cataract/css_parser.c +17 -33
- data/ext/cataract/merge.c +58 -2
- data/lib/cataract/at_rule.rb +8 -9
- data/lib/cataract/declaration.rb +18 -0
- data/lib/cataract/import_resolver.rb +3 -4
- data/lib/cataract/pure/byte_constants.rb +69 -0
- data/lib/cataract/pure/helpers.rb +35 -0
- data/lib/cataract/pure/imports.rb +255 -0
- data/lib/cataract/pure/merge.rb +1146 -0
- data/lib/cataract/pure/parser.rb +1236 -0
- data/lib/cataract/pure/serializer.rb +590 -0
- data/lib/cataract/pure/specificity.rb +206 -0
- data/lib/cataract/pure.rb +130 -0
- data/lib/cataract/rule.rb +22 -13
- data/lib/cataract/stylesheet.rb +14 -9
- data/lib/cataract/version.rb +1 -1
- data/lib/cataract.rb +18 -5
- metadata +12 -25
- data/benchmarks/benchmark_harness.rb +0 -193
- data/benchmarks/benchmark_merging.rb +0 -121
- data/benchmarks/benchmark_optimization_comparison.rb +0 -168
- data/benchmarks/benchmark_parsing.rb +0 -153
- data/benchmarks/benchmark_ragel_removal.rb +0 -56
- data/benchmarks/benchmark_runner.rb +0 -70
- data/benchmarks/benchmark_serialization.rb +0 -180
- data/benchmarks/benchmark_shorthand.rb +0 -109
- data/benchmarks/benchmark_shorthand_expansion.rb +0 -176
- data/benchmarks/benchmark_specificity.rb +0 -124
- data/benchmarks/benchmark_string_allocation.rb +0 -151
- data/benchmarks/benchmark_stylesheet_to_s.rb +0 -62
- data/benchmarks/benchmark_to_s_cached.rb +0 -55
- data/benchmarks/benchmark_value_splitter.rb +0 -54
- data/benchmarks/benchmark_yjit.rb +0 -158
- data/benchmarks/benchmark_yjit_workers.rb +0 -61
- data/benchmarks/profile_to_s.rb +0 -23
- data/benchmarks/speedup_calculator.rb +0 -83
- data/benchmarks/system_metadata.rb +0 -81
- data/benchmarks/templates/benchmarks.md.erb +0 -221
- data/benchmarks/yjit_tests.rb +0 -141
- data/scripts/fuzzer/run.rb +0 -828
- data/scripts/fuzzer/worker.rb +0 -99
- data/scripts/generate_benchmarks_md.rb +0 -155
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
# Benchmark string allocation optimization impact
|
|
5
|
-
# This compares parsing performance with rb_str_buf_new vs rb_str_new_cstr
|
|
6
|
-
#
|
|
7
|
-
# Usage:
|
|
8
|
-
# 1. Run without optimization:
|
|
9
|
-
# rake compile && ruby test/benchmarks/benchmark_string_allocation.rb
|
|
10
|
-
#
|
|
11
|
-
# 2. Recompile with optimization:
|
|
12
|
-
# CFLAGS="-DUSE_STR_BUF_OPTIMIZATION" rake compile
|
|
13
|
-
#
|
|
14
|
-
# 3. Run with optimization:
|
|
15
|
-
# ruby test/benchmarks/benchmark_string_allocation.rb
|
|
16
|
-
#
|
|
17
|
-
# The benchmark will automatically detect which version is running and save
|
|
18
|
-
# results to a JSON file for comparison.
|
|
19
|
-
|
|
20
|
-
require 'benchmark/ips'
|
|
21
|
-
require 'fileutils'
|
|
22
|
-
|
|
23
|
-
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
24
|
-
require 'cataract'
|
|
25
|
-
|
|
26
|
-
# State files for benchmark-ips to compare across runs
|
|
27
|
-
# Store in hidden directory to keep them out of the way
|
|
28
|
-
# Use separate files for each test so we only compare like-to-like
|
|
29
|
-
RESULTS_DIR = File.expand_path('.benchmark_results', __dir__)
|
|
30
|
-
FileUtils.mkdir_p(RESULTS_DIR)
|
|
31
|
-
|
|
32
|
-
RESULTS_FILE_PARSE = File.join(RESULTS_DIR, 'string_allocation_parse.json')
|
|
33
|
-
RESULTS_FILE_ITERATE = File.join(RESULTS_DIR, 'string_allocation_iterate.json')
|
|
34
|
-
RESULTS_FILE_10X = File.join(RESULTS_DIR, 'string_allocation_10x.json')
|
|
35
|
-
|
|
36
|
-
# Large CSS fixture - using Bootstrap 5 CSS for realistic benchmark
|
|
37
|
-
LARGE_CSS_FIXTURE = File.read(File.expand_path('../test/fixtures/bootstrap.css', __dir__))
|
|
38
|
-
|
|
39
|
-
# Detect which version we're running by checking the compile-time constant
|
|
40
|
-
actual_mode = Cataract::STRING_ALLOC_MODE
|
|
41
|
-
# Label based on what's actually running (buffer is the default/production mode)
|
|
42
|
-
mode_label = actual_mode == :buffer ? 'buffer' : 'dynamic'
|
|
43
|
-
|
|
44
|
-
puts '=' * 80
|
|
45
|
-
puts 'String Allocation Optimization Benchmark'
|
|
46
|
-
puts '=' * 80
|
|
47
|
-
puts "Ruby version: #{RUBY_VERSION}"
|
|
48
|
-
puts "String allocation mode: #{actual_mode.inspect}"
|
|
49
|
-
if actual_mode == :buffer
|
|
50
|
-
puts ' → Using rb_str_buf_new (pre-allocated buffers, production default)'
|
|
51
|
-
else
|
|
52
|
-
puts ' → Using rb_str_new_cstr (dynamic allocation, disabled for comparison)'
|
|
53
|
-
end
|
|
54
|
-
puts '=' * 80
|
|
55
|
-
puts
|
|
56
|
-
puts 'This benchmark focuses on at-rules that build selector strings:'
|
|
57
|
-
puts ' - @font-face (large descriptor blocks)'
|
|
58
|
-
puts ' - @property (selector with prelude)'
|
|
59
|
-
puts ' - @keyframes (selector building)'
|
|
60
|
-
puts ' - @page (selector with pseudo)'
|
|
61
|
-
puts ' - @counter-style (selector with name)'
|
|
62
|
-
puts
|
|
63
|
-
puts "CSS fixture: #{LARGE_CSS_FIXTURE.lines.count} lines, #{LARGE_CSS_FIXTURE.bytesize} bytes"
|
|
64
|
-
puts '=' * 80
|
|
65
|
-
puts
|
|
66
|
-
|
|
67
|
-
parser = Cataract::Stylesheet.new
|
|
68
|
-
parser.add_block(LARGE_CSS_FIXTURE)
|
|
69
|
-
GC.start
|
|
70
|
-
# Verify we actually parsed everything
|
|
71
|
-
raise 'Parse failed' if parser.rules_count.zero?
|
|
72
|
-
|
|
73
|
-
puts "\n#{'=' * 80}"
|
|
74
|
-
puts 'TEST 1: Parse CSS with many at-rules'
|
|
75
|
-
puts '=' * 80
|
|
76
|
-
|
|
77
|
-
Benchmark.ips do |x|
|
|
78
|
-
x.config(time: 10, warmup: 2)
|
|
79
|
-
|
|
80
|
-
x.report(mode_label) do
|
|
81
|
-
parser = Cataract::Stylesheet.new
|
|
82
|
-
parser.add_block(LARGE_CSS_FIXTURE)
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
x.save! RESULTS_FILE_PARSE
|
|
86
|
-
x.compare!
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
GC.start
|
|
90
|
-
|
|
91
|
-
puts "\n#{'=' * 80}"
|
|
92
|
-
puts 'TEST 2: Parse + iterate through all rules'
|
|
93
|
-
puts '=' * 80
|
|
94
|
-
|
|
95
|
-
Benchmark.ips do |x|
|
|
96
|
-
x.config(time: 10, warmup: 2)
|
|
97
|
-
|
|
98
|
-
x.report(mode_label) do
|
|
99
|
-
parser = Cataract::Stylesheet.new
|
|
100
|
-
parser.add_block(LARGE_CSS_FIXTURE)
|
|
101
|
-
|
|
102
|
-
count = 0
|
|
103
|
-
parser.select(&:selector?).each do |rule|
|
|
104
|
-
# Force string to be used
|
|
105
|
-
_ = rule.selector.length
|
|
106
|
-
_ = rule.declarations.to_s
|
|
107
|
-
count += 1
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
raise 'No rules found' if count.zero?
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
x.save! RESULTS_FILE_ITERATE
|
|
114
|
-
x.compare!
|
|
115
|
-
end
|
|
116
|
-
|
|
117
|
-
GC.start
|
|
118
|
-
|
|
119
|
-
puts "\n#{'=' * 80}"
|
|
120
|
-
puts 'TEST 3: Multiple parse operations (10x)'
|
|
121
|
-
puts '=' * 80
|
|
122
|
-
|
|
123
|
-
Benchmark.ips do |x|
|
|
124
|
-
x.config(time: 10, warmup: 2)
|
|
125
|
-
|
|
126
|
-
x.report(mode_label) do
|
|
127
|
-
10.times do
|
|
128
|
-
parser = Cataract::Stylesheet.new
|
|
129
|
-
parser.add_block(LARGE_CSS_FIXTURE)
|
|
130
|
-
end
|
|
131
|
-
end
|
|
132
|
-
|
|
133
|
-
x.save! RESULTS_FILE_10X
|
|
134
|
-
x.compare!
|
|
135
|
-
end
|
|
136
|
-
|
|
137
|
-
puts "\n#{'=' * 80}"
|
|
138
|
-
puts 'Results saved to:'
|
|
139
|
-
puts " - #{RESULTS_FILE_PARSE}"
|
|
140
|
-
puts " - #{RESULTS_FILE_ITERATE}"
|
|
141
|
-
puts " - #{RESULTS_FILE_10X}"
|
|
142
|
-
puts ''
|
|
143
|
-
puts 'To compare dynamic vs buffer (default):'
|
|
144
|
-
puts ' 1. Run with dynamic: DISABLE_STR_BUF_OPTIMIZATION=1 rake compile && ruby test/benchmarks/benchmark_string_allocation.rb'
|
|
145
|
-
puts ' 2. Run with buffer: rake compile && ruby test/benchmarks/benchmark_string_allocation.rb'
|
|
146
|
-
puts ' 3. Each test will automatically compare buffer vs dynamic'
|
|
147
|
-
puts ''
|
|
148
|
-
puts 'The benchmark verifies the compilation mode via Cataract::STRING_ALLOC_MODE'
|
|
149
|
-
puts ' :dynamic = rb_str_new_cstr (dynamic allocation)'
|
|
150
|
-
puts ' :buffer = rb_str_buf_new (pre-allocated, production default)'
|
|
151
|
-
puts '=' * 80
|
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'benchmark/ips'
|
|
4
|
-
|
|
5
|
-
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
6
|
-
require 'cataract'
|
|
7
|
-
|
|
8
|
-
puts '=' * 60
|
|
9
|
-
puts 'Stylesheet#to_s: Ruby vs C Implementation'
|
|
10
|
-
puts '=' * 60
|
|
11
|
-
puts ''
|
|
12
|
-
|
|
13
|
-
bootstrap_css = File.read('test/fixtures/bootstrap.css')
|
|
14
|
-
stylesheet = Cataract.parse_css(bootstrap_css)
|
|
15
|
-
|
|
16
|
-
puts "Parsing bootstrap.css: #{stylesheet.size} rules"
|
|
17
|
-
puts ''
|
|
18
|
-
|
|
19
|
-
# Verify both versions produce same output
|
|
20
|
-
ruby_output = stylesheet.to_s
|
|
21
|
-
c_output = Cataract._stylesheet_to_s_c(stylesheet.rules, stylesheet.charset)
|
|
22
|
-
|
|
23
|
-
if ruby_output == c_output
|
|
24
|
-
puts '✓ Ruby and C versions produce identical output'
|
|
25
|
-
else
|
|
26
|
-
puts '✗ WARNING: Ruby and C versions produce different output!'
|
|
27
|
-
puts "Ruby length: #{ruby_output.length}"
|
|
28
|
-
puts "C length: #{c_output.length}"
|
|
29
|
-
|
|
30
|
-
# Find first difference
|
|
31
|
-
ruby_output.chars.each_with_index do |char, i|
|
|
32
|
-
next unless char != c_output[i]
|
|
33
|
-
|
|
34
|
-
puts "First difference at position #{i}:"
|
|
35
|
-
puts " Ruby: #{ruby_output[(i - 20)..(i + 20)].inspect}"
|
|
36
|
-
puts " C: #{c_output[(i - 20)..(i + 20)].inspect}"
|
|
37
|
-
break
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
puts ''
|
|
42
|
-
puts 'Benchmarking to_s only (cache cleared each iteration)...'
|
|
43
|
-
puts ''
|
|
44
|
-
|
|
45
|
-
# Parse once outside benchmark
|
|
46
|
-
PARSER = Cataract.parse_css(bootstrap_css)
|
|
47
|
-
|
|
48
|
-
Benchmark.ips do |x|
|
|
49
|
-
x.config(time: 10, warmup: 3)
|
|
50
|
-
|
|
51
|
-
x.report('Ruby (serialize_to_css)') do
|
|
52
|
-
PARSER.instance_variable_set(:@serialized, nil) # Clear cache
|
|
53
|
-
PARSER.to_s
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
x.report('C (_stylesheet_to_s_c)') do
|
|
57
|
-
PARSER.instance_variable_set(:@serialized, nil) # Clear cache (apples-to-apples)
|
|
58
|
-
Cataract._stylesheet_to_s_c(PARSER.rules, PARSER.charset)
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
x.compare!
|
|
62
|
-
end
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'benchmark/ips'
|
|
4
|
-
|
|
5
|
-
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
6
|
-
require 'cataract'
|
|
7
|
-
|
|
8
|
-
puts '=' * 60
|
|
9
|
-
puts 'CACHED to_s BENCHMARK'
|
|
10
|
-
puts '=' * 60
|
|
11
|
-
puts ''
|
|
12
|
-
|
|
13
|
-
bootstrap_css = File.read('test/fixtures/bootstrap.css')
|
|
14
|
-
|
|
15
|
-
puts 'Comparing:'
|
|
16
|
-
puts ' - Fresh parse → to_s (first call, uncached)'
|
|
17
|
-
puts ' - Repeated to_s (cached, should be instant)'
|
|
18
|
-
puts ''
|
|
19
|
-
|
|
20
|
-
Benchmark.ips do |x|
|
|
21
|
-
x.config(time: 10, warmup: 3)
|
|
22
|
-
|
|
23
|
-
x.report('parse + to_s (uncached)') do
|
|
24
|
-
stylesheet = Cataract.parse_css(bootstrap_css)
|
|
25
|
-
stylesheet.to_s
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
x.report('to_s (cached)') do
|
|
29
|
-
# Parse once outside the benchmark
|
|
30
|
-
stylesheet = Cataract.parse_css(bootstrap_css)
|
|
31
|
-
stylesheet.to_s # Prime cache
|
|
32
|
-
|
|
33
|
-
# Now benchmark cached access
|
|
34
|
-
stylesheet.to_s
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
x.compare!
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
puts ''
|
|
41
|
-
puts '=' * 60
|
|
42
|
-
puts 'Real-world scenario: Multiple to_s calls'
|
|
43
|
-
puts '=' * 60
|
|
44
|
-
puts ''
|
|
45
|
-
|
|
46
|
-
stylesheet = Cataract.parse_css(bootstrap_css)
|
|
47
|
-
|
|
48
|
-
puts 'Calling to_s 10 times...'
|
|
49
|
-
require 'benchmark'
|
|
50
|
-
time = Benchmark.measure do
|
|
51
|
-
10.times { stylesheet.to_s }
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
puts "Total time: #{time.real.round(4)}s"
|
|
55
|
-
puts 'First call does all work, next 9 are free!'
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'benchmark/ips'
|
|
4
|
-
|
|
5
|
-
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
6
|
-
require 'cataract'
|
|
7
|
-
|
|
8
|
-
# Get current git branch
|
|
9
|
-
branch = `git rev-parse --abbrev-ref HEAD 2>/dev/null`.strip
|
|
10
|
-
branch = 'unknown' if branch.empty?
|
|
11
|
-
|
|
12
|
-
puts '=' * 60
|
|
13
|
-
puts "Value Splitter Benchmark: #{branch}"
|
|
14
|
-
puts '=' * 60
|
|
15
|
-
puts ''
|
|
16
|
-
|
|
17
|
-
# Test cases covering different scenarios
|
|
18
|
-
test_cases = {
|
|
19
|
-
'simple' => '1px 2px 3px 4px',
|
|
20
|
-
'functions' => '10px calc(100% - 20px) 5px',
|
|
21
|
-
'rgb' => 'rgb(255, 0, 0) blue rgba(0, 0, 0, 0.5)',
|
|
22
|
-
'quotes' => "'Helvetica Neue', Arial, sans-serif",
|
|
23
|
-
'complex' => "10px calc(100% - 20px) 'Font Name' rgb(255, 0, 0)",
|
|
24
|
-
'long' => '1px 2px 3px 4px 5px 6px 7px 8px 9px 10px 11px 12px 13px 14px 15px'
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
puts 'Test cases:'
|
|
28
|
-
test_cases.each do |name, value|
|
|
29
|
-
result = Cataract.split_value(value)
|
|
30
|
-
puts " #{name}: '#{value}' => #{result.length} tokens"
|
|
31
|
-
end
|
|
32
|
-
puts ''
|
|
33
|
-
|
|
34
|
-
Benchmark.ips do |x|
|
|
35
|
-
x.config(time: 10, warmup: 3)
|
|
36
|
-
|
|
37
|
-
test_cases.each do |name, value|
|
|
38
|
-
x.report("#{branch}:#{name.ljust(12)}") do
|
|
39
|
-
Cataract.split_value(value)
|
|
40
|
-
end
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
x.compare!
|
|
44
|
-
|
|
45
|
-
# Save results to file for cross-branch comparison
|
|
46
|
-
x.save! 'test/.benchmark_results/value_splitter.json'
|
|
47
|
-
x.hold! 'test/.benchmark_results/value_splitter.json'
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
puts ''
|
|
51
|
-
puts '=' * 60
|
|
52
|
-
puts 'Results saved to test/.benchmark_results/value_splitter.json'
|
|
53
|
-
puts 'Switch git branches and run again to compare!'
|
|
54
|
-
puts '=' * 60
|
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'benchmark_harness'
|
|
4
|
-
require 'open3'
|
|
5
|
-
require 'json'
|
|
6
|
-
|
|
7
|
-
# Load the local development version, not installed gem
|
|
8
|
-
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
9
|
-
require 'cataract'
|
|
10
|
-
|
|
11
|
-
# YJIT Benchmark Supervisor
|
|
12
|
-
# Spawns two subprocesses (with/without YJIT) and combines results
|
|
13
|
-
class YjitBenchmark < BenchmarkHarness
|
|
14
|
-
def self.benchmark_name
|
|
15
|
-
'yjit'
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def self.description
|
|
19
|
-
'Ruby-side operations with and without YJIT'
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def self.metadata
|
|
23
|
-
{
|
|
24
|
-
'operations' => [
|
|
25
|
-
'property access',
|
|
26
|
-
'declaration merging',
|
|
27
|
-
'to_s generation',
|
|
28
|
-
'parse + iterate'
|
|
29
|
-
],
|
|
30
|
-
'note' => 'C extension performance is the same regardless of YJIT. This measures Ruby code.'
|
|
31
|
-
}
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def self.speedup_config
|
|
35
|
-
# Compare without YJIT (baseline) vs with YJIT (comparison)
|
|
36
|
-
{
|
|
37
|
-
baseline_matcher: SpeedupCalculator::Matchers.without_yjit,
|
|
38
|
-
comparison_matcher: SpeedupCalculator::Matchers.with_yjit,
|
|
39
|
-
test_case_key: nil # No test_cases array, just operations
|
|
40
|
-
}
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def sanity_checks
|
|
44
|
-
# Verify basic operations work
|
|
45
|
-
decls = Cataract::Declarations.new
|
|
46
|
-
decls['color'] = 'red'
|
|
47
|
-
raise 'Property access failed' unless decls['color']
|
|
48
|
-
|
|
49
|
-
parser = Cataract::Stylesheet.new
|
|
50
|
-
sample_css = 'body { margin: 0; }'
|
|
51
|
-
parser.add_block(sample_css)
|
|
52
|
-
raise 'Parse failed' if parser.rules_count.zero?
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def call
|
|
56
|
-
worker_script = File.expand_path('benchmark_yjit_workers.rb', __dir__)
|
|
57
|
-
|
|
58
|
-
# Clean up any leftover worker files from previous runs
|
|
59
|
-
without_path = File.join(RESULTS_DIR, 'yjit_without.json')
|
|
60
|
-
with_path = File.join(RESULTS_DIR, 'yjit_with.json')
|
|
61
|
-
FileUtils.rm_f(without_path)
|
|
62
|
-
FileUtils.rm_f(with_path)
|
|
63
|
-
|
|
64
|
-
puts 'Running YJIT benchmarks via subprocesses...'
|
|
65
|
-
puts
|
|
66
|
-
|
|
67
|
-
# Run without YJIT
|
|
68
|
-
puts '→ Running without YJIT (--disable-yjit)...'
|
|
69
|
-
stdout_without, status_without = run_subprocess(['ruby', '--disable-yjit', worker_script])
|
|
70
|
-
unless status_without.success?
|
|
71
|
-
raise "Worker without YJIT failed:\n#{stdout_without}"
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
puts stdout_without
|
|
75
|
-
puts
|
|
76
|
-
|
|
77
|
-
# Run with YJIT
|
|
78
|
-
puts '→ Running with YJIT (--yjit)...'
|
|
79
|
-
stdout_with, status_with = run_subprocess(['ruby', '--yjit', worker_script])
|
|
80
|
-
unless status_with.success?
|
|
81
|
-
raise "Worker with YJIT failed:\n#{stdout_with}"
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
puts stdout_with
|
|
85
|
-
puts
|
|
86
|
-
|
|
87
|
-
# Combine results
|
|
88
|
-
combine_worker_results
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
private
|
|
92
|
-
|
|
93
|
-
def run_subprocess(command)
|
|
94
|
-
stdout, stderr, status = Open3.capture3(*command)
|
|
95
|
-
|
|
96
|
-
# Print stderr if present (warnings, etc)
|
|
97
|
-
unless stderr.empty?
|
|
98
|
-
puts "⚠️ stderr: #{stderr}"
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
[stdout, status]
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def combine_worker_results
|
|
105
|
-
without_path = File.join(RESULTS_DIR, 'yjit_without.json')
|
|
106
|
-
with_path = File.join(RESULTS_DIR, 'yjit_with.json')
|
|
107
|
-
|
|
108
|
-
# Check both files exist
|
|
109
|
-
unless File.exist?(without_path) && File.exist?(with_path)
|
|
110
|
-
raise "Worker results not found:\n #{without_path}\n #{with_path}"
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
# Read both JSON files
|
|
114
|
-
without_data = JSON.parse(File.read(without_path))
|
|
115
|
-
with_data = JSON.parse(File.read(with_path))
|
|
116
|
-
|
|
117
|
-
# Combine into single benchmark result
|
|
118
|
-
combined_data = {
|
|
119
|
-
'name' => self.class.benchmark_name,
|
|
120
|
-
'description' => self.class.description,
|
|
121
|
-
'metadata' => self.class.metadata,
|
|
122
|
-
'timestamp' => Time.now.iso8601,
|
|
123
|
-
'results' => []
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
# Merge results from both workers
|
|
127
|
-
combined_data['results'].concat(without_data['results']) if without_data['results']
|
|
128
|
-
combined_data['results'].concat(with_data['results']) if with_data['results']
|
|
129
|
-
|
|
130
|
-
# Calculate speedups using configured strategy
|
|
131
|
-
config = self.class.speedup_config
|
|
132
|
-
if config
|
|
133
|
-
calculator = SpeedupCalculator.new(
|
|
134
|
-
results: combined_data['results'],
|
|
135
|
-
test_cases: combined_data['metadata']['operations'],
|
|
136
|
-
baseline_matcher: config[:baseline_matcher],
|
|
137
|
-
comparison_matcher: config[:comparison_matcher],
|
|
138
|
-
test_case_key: config[:test_case_key]
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
speedup_stats = calculator.calculate
|
|
142
|
-
combined_data['metadata']['speedups'] = speedup_stats if speedup_stats
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
# Write combined file
|
|
146
|
-
combined_path = File.join(RESULTS_DIR, "#{self.class.benchmark_name}.json")
|
|
147
|
-
File.write(combined_path, JSON.pretty_generate(combined_data))
|
|
148
|
-
|
|
149
|
-
# Clean up worker files
|
|
150
|
-
File.delete(without_path)
|
|
151
|
-
File.delete(with_path)
|
|
152
|
-
|
|
153
|
-
puts "✓ Combined results saved to #{combined_path}"
|
|
154
|
-
end
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
# Run if executed directly
|
|
158
|
-
YjitBenchmark.run if __FILE__ == $PROGRAM_NAME
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'benchmark_harness'
|
|
4
|
-
require_relative 'yjit_tests'
|
|
5
|
-
|
|
6
|
-
# Load the local development version, not installed gem
|
|
7
|
-
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
8
|
-
require 'cataract'
|
|
9
|
-
|
|
10
|
-
# Worker benchmark: YJIT disabled
|
|
11
|
-
# Called via subprocess with --disable-yjit flag
|
|
12
|
-
class YjitWithoutBenchmark < BenchmarkHarness
|
|
13
|
-
include YjitTests
|
|
14
|
-
|
|
15
|
-
def self.benchmark_name
|
|
16
|
-
'yjit_without'
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def self.description
|
|
20
|
-
'Ruby-side operations without YJIT'
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def self.metadata
|
|
24
|
-
YjitTests.metadata
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def self.speedup_config
|
|
28
|
-
YjitTests.speedup_config
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Worker benchmark: YJIT enabled
|
|
33
|
-
# Called via subprocess with --yjit flag
|
|
34
|
-
class YjitWithBenchmark < BenchmarkHarness
|
|
35
|
-
include YjitTests
|
|
36
|
-
|
|
37
|
-
def self.benchmark_name
|
|
38
|
-
'yjit_with'
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def self.description
|
|
42
|
-
'Ruby-side operations with YJIT'
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def self.metadata
|
|
46
|
-
YjitTests.metadata
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def self.speedup_config
|
|
50
|
-
YjitTests.speedup_config
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# CLI entry point - run the appropriate worker based on YJIT status
|
|
55
|
-
if __FILE__ == $PROGRAM_NAME
|
|
56
|
-
if defined?(RubyVM::YJIT.enabled?) && RubyVM::YJIT.enabled?
|
|
57
|
-
YjitWithBenchmark.run
|
|
58
|
-
else
|
|
59
|
-
YjitWithoutBenchmark.run
|
|
60
|
-
end
|
|
61
|
-
end
|
data/benchmarks/profile_to_s.rb
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'ruby-prof'
|
|
4
|
-
|
|
5
|
-
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
6
|
-
require 'cataract'
|
|
7
|
-
|
|
8
|
-
bootstrap_css = File.read('test/fixtures/bootstrap.css')
|
|
9
|
-
stylesheet = Cataract.parse_css(bootstrap_css)
|
|
10
|
-
|
|
11
|
-
puts "Profiling Stylesheet#to_s on bootstrap.css (#{stylesheet.size} rules)"
|
|
12
|
-
puts ''
|
|
13
|
-
|
|
14
|
-
# Profile
|
|
15
|
-
RubyProf.start
|
|
16
|
-
|
|
17
|
-
10.times { stylesheet.to_s }
|
|
18
|
-
|
|
19
|
-
result = RubyProf.stop
|
|
20
|
-
|
|
21
|
-
# Print flat profile
|
|
22
|
-
printer = RubyProf::FlatPrinter.new(result)
|
|
23
|
-
printer.print($stdout, min_percent: 1)
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Generic speedup calculator for benchmark results
|
|
4
|
-
# Compares "baseline" vs "comparison" results and calculates speedup stats
|
|
5
|
-
#
|
|
6
|
-
# CONVENTION: Result names must use format "tool_name: test_case_id"
|
|
7
|
-
# Example: "css_parser: CSS1", "cataract: CSS1"
|
|
8
|
-
class SpeedupCalculator
|
|
9
|
-
# @param results [Array<Hash>] Array of benchmark results with 'name' and 'central_tendency'
|
|
10
|
-
# @param test_cases [Array<Hash>] Array of test case metadata to annotate with speedups
|
|
11
|
-
# @param baseline_matcher [Proc] Block that returns true if result is baseline (e.g., css_parser)
|
|
12
|
-
# @param comparison_matcher [Proc] Block that returns true if result is comparison (e.g., cataract)
|
|
13
|
-
# @param test_case_key [Symbol] Key in test_cases hash to match against test case id from result name
|
|
14
|
-
def initialize(results:, test_cases:, baseline_matcher:, comparison_matcher:, test_case_key: nil)
|
|
15
|
-
@results = results
|
|
16
|
-
@test_cases = test_cases
|
|
17
|
-
@baseline_matcher = baseline_matcher
|
|
18
|
-
@comparison_matcher = comparison_matcher
|
|
19
|
-
@test_case_key = test_case_key
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# Calculate speedups and return stats hash
|
|
23
|
-
# Also annotates test_cases with individual speedups if test_case_key provided
|
|
24
|
-
# @return [Hash] { min: Float, max: Float, avg: Float } or nil if no pairs found
|
|
25
|
-
def calculate
|
|
26
|
-
speedups = []
|
|
27
|
-
|
|
28
|
-
# Group results by test case (everything after ':')
|
|
29
|
-
grouped = @results.group_by { |result| extract_test_case(result['name']) }
|
|
30
|
-
|
|
31
|
-
grouped.each do |test_case_id, group_results|
|
|
32
|
-
baseline = group_results.find(&@baseline_matcher)
|
|
33
|
-
comparison = group_results.find(&@comparison_matcher)
|
|
34
|
-
|
|
35
|
-
next unless baseline && comparison
|
|
36
|
-
|
|
37
|
-
speedup = comparison['central_tendency'].to_f / baseline['central_tendency']
|
|
38
|
-
speedups << speedup
|
|
39
|
-
|
|
40
|
-
# Annotate test case metadata if provided
|
|
41
|
-
if @test_case_key && @test_cases
|
|
42
|
-
test_case = @test_cases.find { |tc| tc[@test_case_key.to_s] == test_case_id }
|
|
43
|
-
test_case['speedup'] = speedup.round(2) if test_case
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
return nil if speedups.empty?
|
|
48
|
-
|
|
49
|
-
{
|
|
50
|
-
'min' => speedups.min.round(2),
|
|
51
|
-
'max' => speedups.max.round(2),
|
|
52
|
-
'avg' => (speedups.sum / speedups.size).round(2)
|
|
53
|
-
}
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
private
|
|
57
|
-
|
|
58
|
-
# Extract test case id from result name
|
|
59
|
-
# "css_parser: CSS1" -> "CSS1"
|
|
60
|
-
# "cataract gem: all" -> "all"
|
|
61
|
-
def extract_test_case(name)
|
|
62
|
-
name.split(':').last.strip
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Common matchers
|
|
66
|
-
class Matchers
|
|
67
|
-
def self.css_parser
|
|
68
|
-
->(result) { result['name'].include?('css_parser') }
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def self.cataract
|
|
72
|
-
->(result) { result['name'].include?('cataract') }
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def self.with_yjit
|
|
76
|
-
->(result) { result['name'].include?('YJIT') && !result['name'].include?('no YJIT') }
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def self.without_yjit
|
|
80
|
-
->(result) { result['name'].include?('no YJIT') }
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|