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,193 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'benchmark/ips'
|
|
4
|
-
require 'json'
|
|
5
|
-
require 'fileutils'
|
|
6
|
-
require_relative 'system_metadata'
|
|
7
|
-
require_relative 'speedup_calculator'
|
|
8
|
-
|
|
9
|
-
# Base class for all benchmarks. Provides structure and automatic JSON output.
|
|
10
|
-
#
|
|
11
|
-
# Usage:
|
|
12
|
-
# class MyBenchmark < BenchmarkHarness
|
|
13
|
-
# def self.benchmark_name
|
|
14
|
-
# 'my_benchmark'
|
|
15
|
-
# end
|
|
16
|
-
#
|
|
17
|
-
# def self.description
|
|
18
|
-
# 'What this benchmark measures'
|
|
19
|
-
# end
|
|
20
|
-
#
|
|
21
|
-
# def self.metadata
|
|
22
|
-
# { 'key' => 'value' } # Optional metadata for docs
|
|
23
|
-
# end
|
|
24
|
-
#
|
|
25
|
-
# def self.sanity_checks
|
|
26
|
-
# # Optional: verify code works before benchmarking
|
|
27
|
-
# raise "Sanity check failed!" unless something_works
|
|
28
|
-
# end
|
|
29
|
-
#
|
|
30
|
-
# def self.call
|
|
31
|
-
# run_test_case_1
|
|
32
|
-
# run_test_case_2
|
|
33
|
-
# end
|
|
34
|
-
#
|
|
35
|
-
# private
|
|
36
|
-
#
|
|
37
|
-
# def self.run_test_case_1
|
|
38
|
-
# benchmark('test_case_1') do |x|
|
|
39
|
-
# x.config(time: 5, warmup: 2)
|
|
40
|
-
# x.report('label') { ... }
|
|
41
|
-
# x.compare!
|
|
42
|
-
# end
|
|
43
|
-
# end
|
|
44
|
-
# end
|
|
45
|
-
class BenchmarkHarness
|
|
46
|
-
RESULTS_DIR = File.expand_path('.results', __dir__)
|
|
47
|
-
|
|
48
|
-
class << self
|
|
49
|
-
# Abstract methods - must be implemented by subclasses
|
|
50
|
-
def benchmark_name
|
|
51
|
-
raise NotImplementedError, "#{self} must implement .benchmark_name"
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
def description
|
|
55
|
-
raise NotImplementedError, "#{self} must implement .description"
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
def metadata
|
|
59
|
-
{} # Optional, can be overridden
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def sanity_checks
|
|
63
|
-
# Optional, can be overridden
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def call
|
|
67
|
-
raise NotImplementedError, "#{self} must implement .call"
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
# Optional: Define how to calculate speedups for this benchmark
|
|
71
|
-
# Override this to customize speedup calculation
|
|
72
|
-
#
|
|
73
|
-
# IMPORTANT: Result names must follow convention "tool_name: test_case_id"
|
|
74
|
-
#
|
|
75
|
-
# @return [Hash] Configuration for SpeedupCalculator
|
|
76
|
-
# {
|
|
77
|
-
# baseline_matcher: Proc, # Returns true for baseline results
|
|
78
|
-
# comparison_matcher: Proc, # Returns true for comparison results
|
|
79
|
-
# test_case_key: Symbol # Key in test_cases metadata matching test_case_id
|
|
80
|
-
# }
|
|
81
|
-
def speedup_config
|
|
82
|
-
# Default: compare css_parser (baseline) vs cataract (comparison)
|
|
83
|
-
# Match to test_cases by 'fixture' key
|
|
84
|
-
{
|
|
85
|
-
baseline_matcher: SpeedupCalculator::Matchers.css_parser,
|
|
86
|
-
comparison_matcher: SpeedupCalculator::Matchers.cataract,
|
|
87
|
-
test_case_key: :fixture
|
|
88
|
-
}
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
# Main entry point - handles setup, execution, and cleanup
|
|
92
|
-
def run
|
|
93
|
-
instance = new
|
|
94
|
-
setup
|
|
95
|
-
instance.sanity_checks if instance.respond_to?(:sanity_checks, true)
|
|
96
|
-
instance.call
|
|
97
|
-
finalize(instance)
|
|
98
|
-
rescue StandardError => e
|
|
99
|
-
puts "❌ Benchmark failed: #{e.message}"
|
|
100
|
-
puts e.backtrace.first(5).join("\n")
|
|
101
|
-
exit 1
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
private
|
|
105
|
-
|
|
106
|
-
def setup
|
|
107
|
-
FileUtils.mkdir_p(RESULTS_DIR)
|
|
108
|
-
|
|
109
|
-
# Collect system metadata once per run
|
|
110
|
-
unless File.exist?(File.join(RESULTS_DIR, 'metadata.json'))
|
|
111
|
-
SystemMetadata.collect
|
|
112
|
-
end
|
|
113
|
-
|
|
114
|
-
# Print header
|
|
115
|
-
puts "\n\n"
|
|
116
|
-
puts '=' * 80
|
|
117
|
-
puts "#{benchmark_name.upcase.tr('_', ' ')} BENCHMARK"
|
|
118
|
-
puts "Measures: #{description}"
|
|
119
|
-
puts '=' * 80
|
|
120
|
-
puts
|
|
121
|
-
end
|
|
122
|
-
|
|
123
|
-
def finalize(instance)
|
|
124
|
-
# Combine all JSON files for this benchmark into one
|
|
125
|
-
return unless instance.instance_variable_defined?(:@json_files) && instance.instance_variable_get(:@json_files)&.any?
|
|
126
|
-
|
|
127
|
-
json_files = instance.instance_variable_get(:@json_files)
|
|
128
|
-
|
|
129
|
-
combined_data = {
|
|
130
|
-
'name' => benchmark_name,
|
|
131
|
-
'description' => description,
|
|
132
|
-
'metadata' => metadata,
|
|
133
|
-
'timestamp' => Time.now.iso8601,
|
|
134
|
-
'results' => []
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
# Read all the individual JSON files
|
|
138
|
-
json_files.each do |filename|
|
|
139
|
-
path = File.join(RESULTS_DIR, filename)
|
|
140
|
-
next unless File.exist?(path)
|
|
141
|
-
|
|
142
|
-
data = JSON.parse(File.read(path))
|
|
143
|
-
combined_data['results'].concat(data) if data.is_a?(Array)
|
|
144
|
-
end
|
|
145
|
-
|
|
146
|
-
# Calculate speedups using configured strategy
|
|
147
|
-
config = speedup_config
|
|
148
|
-
if config
|
|
149
|
-
calculator = SpeedupCalculator.new(
|
|
150
|
-
results: combined_data['results'],
|
|
151
|
-
test_cases: combined_data['metadata']['test_cases'],
|
|
152
|
-
baseline_matcher: config[:baseline_matcher],
|
|
153
|
-
comparison_matcher: config[:comparison_matcher],
|
|
154
|
-
test_case_key: config[:test_case_key]
|
|
155
|
-
)
|
|
156
|
-
|
|
157
|
-
speedup_stats = calculator.calculate
|
|
158
|
-
combined_data['metadata']['speedups'] = speedup_stats if speedup_stats
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
# Write combined file
|
|
162
|
-
combined_path = File.join(RESULTS_DIR, "#{benchmark_name}.json")
|
|
163
|
-
File.write(combined_path, JSON.pretty_generate(combined_data))
|
|
164
|
-
|
|
165
|
-
# Clean up individual files
|
|
166
|
-
json_files.each do |filename|
|
|
167
|
-
File.delete(File.join(RESULTS_DIR, filename))
|
|
168
|
-
end
|
|
169
|
-
|
|
170
|
-
puts "\n✓ Results saved to #{combined_path}"
|
|
171
|
-
end
|
|
172
|
-
end
|
|
173
|
-
|
|
174
|
-
# Instance methods
|
|
175
|
-
protected
|
|
176
|
-
|
|
177
|
-
def benchmark(test_case_name)
|
|
178
|
-
json_filename = "#{self.class.benchmark_name}_#{test_case_name}.json"
|
|
179
|
-
json_path = File.join(RESULTS_DIR, json_filename)
|
|
180
|
-
|
|
181
|
-
Benchmark.ips do |x|
|
|
182
|
-
# Automatically enable JSON output
|
|
183
|
-
x.json!(json_path)
|
|
184
|
-
|
|
185
|
-
# Let the benchmark configure and run
|
|
186
|
-
yield x
|
|
187
|
-
end
|
|
188
|
-
|
|
189
|
-
# Track that we created this file
|
|
190
|
-
@json_files ||= []
|
|
191
|
-
@json_files << json_filename
|
|
192
|
-
end
|
|
193
|
-
end
|
|
@@ -1,121 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'benchmark_harness'
|
|
4
|
-
|
|
5
|
-
# Load the local development version, not installed gem
|
|
6
|
-
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
7
|
-
require 'cataract'
|
|
8
|
-
|
|
9
|
-
# CSS Merging Benchmark
|
|
10
|
-
class MergingBenchmark < BenchmarkHarness
|
|
11
|
-
def self.benchmark_name
|
|
12
|
-
'merging'
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def self.description
|
|
16
|
-
'Time to merge multiple CSS rule sets with same selector'
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def self.metadata
|
|
20
|
-
{
|
|
21
|
-
'test_cases' => [
|
|
22
|
-
{
|
|
23
|
-
'name' => 'No shorthand properties (large)',
|
|
24
|
-
'key' => 'no_shorthand',
|
|
25
|
-
'css' => (".test { color: red; background-color: blue; display: block; position: relative; width: 100px; height: 50px; }\n" * 100)
|
|
26
|
-
},
|
|
27
|
-
{
|
|
28
|
-
'name' => 'Simple properties',
|
|
29
|
-
'key' => 'simple',
|
|
30
|
-
'css' => ".test { color: black; margin: 10px; }\n.test { padding: 5px; }"
|
|
31
|
-
},
|
|
32
|
-
{
|
|
33
|
-
'name' => 'Cascade with specificity',
|
|
34
|
-
'key' => 'cascade',
|
|
35
|
-
'css' => ".test { color: black; }\n#test { color: red; }\n.test { margin: 10px; }"
|
|
36
|
-
},
|
|
37
|
-
{
|
|
38
|
-
'name' => 'Important declarations',
|
|
39
|
-
'key' => 'important',
|
|
40
|
-
'css' => ".test { color: black !important; }\n#test { color: red; }\n.test { margin: 10px; }"
|
|
41
|
-
},
|
|
42
|
-
{
|
|
43
|
-
'name' => 'Shorthand expansion',
|
|
44
|
-
'key' => 'shorthand',
|
|
45
|
-
'css' => ".test { margin: 10px 20px; }\n.test { margin-left: 5px; }\n.test { padding: 1em 2em 3em 4em; }"
|
|
46
|
-
},
|
|
47
|
-
{
|
|
48
|
-
'name' => 'Complex merging',
|
|
49
|
-
'key' => 'complex',
|
|
50
|
-
'css' => "body { margin: 0; padding: 0; }\n.container { width: 100%; margin: 0 auto; }\n#main { background: white; color: black; }\n.button { padding: 10px 20px; border: 1px solid #ccc; }\n.button:hover { background: #f0f0f0; }\n.button.primary { background: blue !important; color: white; }"
|
|
51
|
-
}
|
|
52
|
-
]
|
|
53
|
-
}
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def self.speedup_config
|
|
57
|
-
{
|
|
58
|
-
baseline_matcher: SpeedupCalculator::Matchers.css_parser,
|
|
59
|
-
comparison_matcher: SpeedupCalculator::Matchers.cataract,
|
|
60
|
-
test_case_key: :key
|
|
61
|
-
}
|
|
62
|
-
end
|
|
63
|
-
|
|
64
|
-
def sanity_checks
|
|
65
|
-
require 'css_parser'
|
|
66
|
-
|
|
67
|
-
# Verify merging works correctly
|
|
68
|
-
css = ".test { color: black; }\n.test { margin: 10px; }"
|
|
69
|
-
cataract_rules = Cataract.parse_css(css)
|
|
70
|
-
cataract_merged = Cataract.merge(cataract_rules)
|
|
71
|
-
|
|
72
|
-
raise 'Cataract merge failed' if cataract_merged.rules.empty?
|
|
73
|
-
|
|
74
|
-
merged_decls = cataract_merged.rules.first.declarations
|
|
75
|
-
raise 'Cataract merge incorrect' unless merged_decls.any? { |d| d.property == 'color' }
|
|
76
|
-
end
|
|
77
|
-
|
|
78
|
-
def call
|
|
79
|
-
self.class.metadata['test_cases'].each do |test_case|
|
|
80
|
-
benchmark_test_case(test_case)
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
private
|
|
85
|
-
|
|
86
|
-
def benchmark_test_case(test_case)
|
|
87
|
-
puts '=' * 80
|
|
88
|
-
puts "TEST: #{test_case['name']}"
|
|
89
|
-
puts '=' * 80
|
|
90
|
-
|
|
91
|
-
key = test_case['key']
|
|
92
|
-
css = test_case['css']
|
|
93
|
-
|
|
94
|
-
# Pre-parse the CSS for both implementations
|
|
95
|
-
cataract_rules = Cataract.parse_css(css)
|
|
96
|
-
|
|
97
|
-
parser = CssParser::Parser.new
|
|
98
|
-
parser.add_block!(css)
|
|
99
|
-
rule_sets = []
|
|
100
|
-
parser.each_selector do |selector, declarations, _specificity|
|
|
101
|
-
rule_sets << CssParser::RuleSet.new(selectors: selector, block: declarations)
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
benchmark(key) do |x|
|
|
105
|
-
x.config(time: 5, warmup: 2)
|
|
106
|
-
|
|
107
|
-
x.report("css_parser: #{key}") do
|
|
108
|
-
CssParser.merge(rule_sets)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
x.report("cataract: #{key}") do
|
|
112
|
-
Cataract.merge(cataract_rules)
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
x.compare!
|
|
116
|
-
end
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
# Run if executed directly
|
|
121
|
-
MergingBenchmark.run if __FILE__ == $PROGRAM_NAME
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env ruby
|
|
2
|
-
# frozen_string_literal: true
|
|
3
|
-
|
|
4
|
-
require 'benchmark/ips'
|
|
5
|
-
require 'optparse'
|
|
6
|
-
|
|
7
|
-
# Load the local development version, not installed gem
|
|
8
|
-
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
9
|
-
require 'cataract'
|
|
10
|
-
|
|
11
|
-
# =============================================================================
|
|
12
|
-
# Generic Optimization Comparison Benchmark
|
|
13
|
-
# =============================================================================
|
|
14
|
-
#
|
|
15
|
-
# This benchmark compares performance between different compile-time
|
|
16
|
-
# optimizations using benchmark-ips's hold! functionality.
|
|
17
|
-
#
|
|
18
|
-
# Usage:
|
|
19
|
-
# 1. Compile with baseline configuration:
|
|
20
|
-
# $ rake clean compile
|
|
21
|
-
# $ ruby test/benchmarks/benchmark_optimization_comparison.rb --baseline
|
|
22
|
-
#
|
|
23
|
-
# 2. In same terminal, compile with optimization:
|
|
24
|
-
# $ rake clean && env USE_LIKELY_UNLIKELY=1 rake compile
|
|
25
|
-
# $ ruby test/benchmarks/benchmark_optimization_comparison.rb --optimized
|
|
26
|
-
#
|
|
27
|
-
# 3. benchmark-ips will show comparison results from both runs
|
|
28
|
-
#
|
|
29
|
-
# The benchmark tests merge/cascade performance on bootstrap.css (~10k rules).
|
|
30
|
-
# =============================================================================
|
|
31
|
-
|
|
32
|
-
module OptimizationBenchmark
|
|
33
|
-
def self.run(variant: nil)
|
|
34
|
-
puts '=' * 80
|
|
35
|
-
puts 'OPTIMIZATION COMPARISON BENCHMARK'
|
|
36
|
-
puts '=' * 80
|
|
37
|
-
|
|
38
|
-
# Display current compile-time flags
|
|
39
|
-
puts "\nCurrently compiled with:"
|
|
40
|
-
Cataract::COMPILE_FLAGS.each do |flag, enabled|
|
|
41
|
-
status = enabled ? '✓ ENABLED' : '✗ disabled'
|
|
42
|
-
puts " #{flag}: #{status}"
|
|
43
|
-
end
|
|
44
|
-
puts ''
|
|
45
|
-
|
|
46
|
-
# Load bootstrap.css fixture
|
|
47
|
-
fixtures_dir = File.expand_path('../test/fixtures', __dir__)
|
|
48
|
-
bootstrap_css_path = File.join(fixtures_dir, 'bootstrap.css')
|
|
49
|
-
|
|
50
|
-
unless File.exist?(bootstrap_css_path)
|
|
51
|
-
puts "❌ ERROR: bootstrap.css not found at #{bootstrap_css_path}"
|
|
52
|
-
exit 1
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
bootstrap_css = File.read(bootstrap_css_path)
|
|
56
|
-
puts 'Test file: bootstrap.css'
|
|
57
|
-
puts " Lines: #{bootstrap_css.lines.count}"
|
|
58
|
-
puts " Size: #{bootstrap_css.bytesize} bytes (#{(bootstrap_css.bytesize / 1024.0).round(1)} KB)"
|
|
59
|
-
|
|
60
|
-
# Parse once to get rules for merge benchmark
|
|
61
|
-
puts "\nParsing bootstrap.css to get rules..."
|
|
62
|
-
parser = Cataract::Stylesheet.new
|
|
63
|
-
begin
|
|
64
|
-
parser.add_block(bootstrap_css)
|
|
65
|
-
rules = parser.instance_variable_get(:@raw_rules) # Get raw rules array
|
|
66
|
-
puts " ✅ Parsed successfully (#{rules.length} rules)"
|
|
67
|
-
rescue StandardError => e
|
|
68
|
-
puts " ❌ ERROR: Failed to parse: #{e.message}"
|
|
69
|
-
exit 1
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# Verify merge works before benchmarking
|
|
73
|
-
puts "\nVerifying merge..."
|
|
74
|
-
begin
|
|
75
|
-
merged = Cataract.apply_cascade(rules)
|
|
76
|
-
puts " ✅ Merged successfully (#{merged.length} declarations)"
|
|
77
|
-
rescue StandardError => e
|
|
78
|
-
puts " ❌ ERROR: Failed to merge: #{e.message}"
|
|
79
|
-
exit 1
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
# Auto-detect variant if not specified
|
|
83
|
-
if variant.nil?
|
|
84
|
-
# Check any optimization flag
|
|
85
|
-
has_optimization = Cataract::COMPILE_FLAGS.any? do |flag, enabled|
|
|
86
|
-
enabled && flag != :str_buf_optimization && flag != :debug
|
|
87
|
-
end
|
|
88
|
-
variant = has_optimization ? 'optimized' : 'baseline'
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
puts "\n#{'=' * 80}"
|
|
92
|
-
puts "RUNNING BENCHMARK (variant: #{variant})"
|
|
93
|
-
puts '=' * 80
|
|
94
|
-
puts 'Timing: 20s measurement, 5s warmup'
|
|
95
|
-
puts ''
|
|
96
|
-
|
|
97
|
-
Benchmark.ips do |x|
|
|
98
|
-
x.config(time: 20, warmup: 5)
|
|
99
|
-
|
|
100
|
-
x.report("merge_bootstrap_#{variant}") do
|
|
101
|
-
Cataract.apply_cascade(rules)
|
|
102
|
-
end
|
|
103
|
-
end
|
|
104
|
-
|
|
105
|
-
puts "\n#{'=' * 80}"
|
|
106
|
-
puts "DONE - #{variant}"
|
|
107
|
-
puts '=' * 80
|
|
108
|
-
end
|
|
109
|
-
|
|
110
|
-
def self.print_usage
|
|
111
|
-
puts <<~USAGE
|
|
112
|
-
Usage: #{$PROGRAM_NAME}
|
|
113
|
-
|
|
114
|
-
This benchmark will automatically detect which variant is compiled
|
|
115
|
-
and run the appropriate test.
|
|
116
|
-
|
|
117
|
-
Workflow:
|
|
118
|
-
# 1. Build baseline
|
|
119
|
-
rake clean && rake compile
|
|
120
|
-
ruby test/benchmarks/benchmark_optimization_comparison.rb --baseline
|
|
121
|
-
|
|
122
|
-
# 2. Build with optimization (e.g., LIKELY/UNLIKELY)
|
|
123
|
-
rake clean && USE_LIKELY_UNLIKELY=1 rake compile
|
|
124
|
-
ruby test/benchmarks/benchmark_optimization_comparison.rb --optimized
|
|
125
|
-
|
|
126
|
-
# 3. Compare results (benchmark-ips will show comparison automatically)
|
|
127
|
-
|
|
128
|
-
Specific optimization flags:
|
|
129
|
-
LIKELY/UNLIKELY:
|
|
130
|
-
USE_LIKELY_UNLIKELY=1 rake compile
|
|
131
|
-
|
|
132
|
-
Loop unrolling (raw CFLAGS still work):
|
|
133
|
-
CFLAGS="-funroll-loops" rake compile
|
|
134
|
-
|
|
135
|
-
Aggressive optimization:
|
|
136
|
-
CFLAGS="-O3 -march=native -funroll-loops" rake compile
|
|
137
|
-
USAGE
|
|
138
|
-
end
|
|
139
|
-
end
|
|
140
|
-
|
|
141
|
-
# =============================================================================
|
|
142
|
-
# Main
|
|
143
|
-
# =============================================================================
|
|
144
|
-
|
|
145
|
-
if __FILE__ == $PROGRAM_NAME
|
|
146
|
-
require 'optparse'
|
|
147
|
-
|
|
148
|
-
variant = nil
|
|
149
|
-
|
|
150
|
-
OptionParser.new do |opts|
|
|
151
|
-
opts.banner = "Usage: #{$PROGRAM_NAME} [options]"
|
|
152
|
-
|
|
153
|
-
opts.on('--baseline', 'Run benchmark labeled as baseline') do
|
|
154
|
-
variant = 'baseline'
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
opts.on('--optimized', 'Run benchmark labeled as optimized') do
|
|
158
|
-
variant = 'optimized'
|
|
159
|
-
end
|
|
160
|
-
|
|
161
|
-
opts.on('-h', '--help', 'Show this help message') do
|
|
162
|
-
OptimizationBenchmark.print_usage
|
|
163
|
-
exit 0
|
|
164
|
-
end
|
|
165
|
-
end.parse!
|
|
166
|
-
|
|
167
|
-
OptimizationBenchmark.run(variant: variant)
|
|
168
|
-
end
|
|
@@ -1,153 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'benchmark_harness'
|
|
4
|
-
|
|
5
|
-
# Load the local development version, not installed gem
|
|
6
|
-
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
7
|
-
require 'cataract'
|
|
8
|
-
|
|
9
|
-
# CSS Parsing Performance Benchmark
|
|
10
|
-
class ParsingBenchmark < BenchmarkHarness
|
|
11
|
-
def self.benchmark_name
|
|
12
|
-
'parsing'
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def self.description
|
|
16
|
-
'Time to parse CSS into internal data structures'
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def self.metadata
|
|
20
|
-
instance = new
|
|
21
|
-
{
|
|
22
|
-
'test_cases' => [
|
|
23
|
-
{
|
|
24
|
-
'name' => "Small CSS (#{instance.css1.lines.count} lines, #{(instance.css1.length / 1024.0).round(1)}KB)",
|
|
25
|
-
'fixture' => 'CSS1',
|
|
26
|
-
'lines' => instance.css1.lines.count,
|
|
27
|
-
'bytes' => instance.css1.length
|
|
28
|
-
},
|
|
29
|
-
{
|
|
30
|
-
'name' => "Medium CSS with @media (#{instance.css2.lines.count} lines, #{(instance.css2.length / 1024.0).round(1)}KB)",
|
|
31
|
-
'fixture' => 'CSS2',
|
|
32
|
-
'lines' => instance.css2.lines.count,
|
|
33
|
-
'bytes' => instance.css2.length
|
|
34
|
-
}
|
|
35
|
-
]
|
|
36
|
-
# speedups will be calculated automatically by harness
|
|
37
|
-
}
|
|
38
|
-
end
|
|
39
|
-
|
|
40
|
-
# Uses default speedup_config from harness (css_parser vs cataract, test_case_key: :fixture)
|
|
41
|
-
|
|
42
|
-
def sanity_checks
|
|
43
|
-
# Check css_parser gem is available
|
|
44
|
-
require 'css_parser'
|
|
45
|
-
|
|
46
|
-
# Verify fixtures parse correctly
|
|
47
|
-
parser = Cataract::Stylesheet.new
|
|
48
|
-
parser.add_block(css1)
|
|
49
|
-
raise 'CSS1 sanity check failed: expected rules' if parser.rules_count.zero?
|
|
50
|
-
|
|
51
|
-
parser = Cataract::Stylesheet.new
|
|
52
|
-
parser.add_block(css2)
|
|
53
|
-
raise 'CSS2 sanity check failed: expected rules' if parser.rules_count.zero?
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
def call
|
|
57
|
-
run_css1_benchmark
|
|
58
|
-
run_css2_benchmark
|
|
59
|
-
show_correctness_comparison
|
|
60
|
-
end
|
|
61
|
-
|
|
62
|
-
def css1
|
|
63
|
-
@css1 ||= File.read(File.join(fixtures_dir, 'css1_sample.css'))
|
|
64
|
-
end
|
|
65
|
-
|
|
66
|
-
def css2
|
|
67
|
-
@css2 ||= File.read(File.join(fixtures_dir, 'css2_sample.css'))
|
|
68
|
-
end
|
|
69
|
-
|
|
70
|
-
private
|
|
71
|
-
|
|
72
|
-
def fixtures_dir
|
|
73
|
-
@fixtures_dir ||= File.expand_path('../test/fixtures', __dir__)
|
|
74
|
-
end
|
|
75
|
-
|
|
76
|
-
def run_css1_benchmark
|
|
77
|
-
puts '=' * 80
|
|
78
|
-
puts "TEST: CSS1 (#{css1.lines.count} lines, #{css1.length} chars)"
|
|
79
|
-
puts '=' * 80
|
|
80
|
-
|
|
81
|
-
benchmark('css1') do |x|
|
|
82
|
-
x.config(time: 5, warmup: 2)
|
|
83
|
-
|
|
84
|
-
x.report('css_parser gem: CSS1') do
|
|
85
|
-
parser = CssParser::Parser.new(import: false, io_exceptions: false)
|
|
86
|
-
parser.add_block!(css1)
|
|
87
|
-
end
|
|
88
|
-
|
|
89
|
-
x.report('cataract: CSS1') do
|
|
90
|
-
parser = Cataract::Stylesheet.new
|
|
91
|
-
parser.add_block(css1)
|
|
92
|
-
end
|
|
93
|
-
|
|
94
|
-
x.compare!
|
|
95
|
-
end
|
|
96
|
-
end
|
|
97
|
-
|
|
98
|
-
def run_css2_benchmark
|
|
99
|
-
puts "\n#{'=' * 80}"
|
|
100
|
-
puts "TEST: CSS2 with @media (#{css2.lines.count} lines, #{css2.length} chars)"
|
|
101
|
-
puts '=' * 80
|
|
102
|
-
|
|
103
|
-
benchmark('css2') do |x|
|
|
104
|
-
x.config(time: 5, warmup: 2)
|
|
105
|
-
|
|
106
|
-
x.report('css_parser gem: CSS2') do
|
|
107
|
-
parser = CssParser::Parser.new(import: false, io_exceptions: false)
|
|
108
|
-
parser.add_block!(css2)
|
|
109
|
-
end
|
|
110
|
-
|
|
111
|
-
x.report('cataract: CSS2') do
|
|
112
|
-
parser = Cataract::Stylesheet.new
|
|
113
|
-
parser.add_block(css2)
|
|
114
|
-
end
|
|
115
|
-
|
|
116
|
-
x.compare!
|
|
117
|
-
end
|
|
118
|
-
end
|
|
119
|
-
|
|
120
|
-
def show_correctness_comparison
|
|
121
|
-
puts "\n#{'=' * 80}"
|
|
122
|
-
puts 'CORRECTNESS VALIDATION (CSS2)'
|
|
123
|
-
puts '=' * 80
|
|
124
|
-
|
|
125
|
-
# Test Cataract
|
|
126
|
-
parser = Cataract::Stylesheet.new
|
|
127
|
-
parser.add_block(css2)
|
|
128
|
-
cataract_rules = parser.rules_count
|
|
129
|
-
puts "Cataract found #{cataract_rules} rules"
|
|
130
|
-
|
|
131
|
-
# Test css_parser
|
|
132
|
-
css_parser = CssParser::Parser.new(import: false, io_exceptions: false)
|
|
133
|
-
css_parser.add_block!(css2)
|
|
134
|
-
css_parser_rules = 0
|
|
135
|
-
css_parser.each_selector { css_parser_rules += 1 }
|
|
136
|
-
puts "css_parser found #{css_parser_rules} rules"
|
|
137
|
-
|
|
138
|
-
unless cataract_rules == css_parser_rules
|
|
139
|
-
puts '⚠️ Different number of rules parsed'
|
|
140
|
-
puts ' Note: css_parser has a known bug with ::after pseudo-elements'
|
|
141
|
-
puts ' (it concatenates them with previous rules instead of parsing separately)'
|
|
142
|
-
end
|
|
143
|
-
|
|
144
|
-
# Show sample output
|
|
145
|
-
puts "\nSample Cataract output:"
|
|
146
|
-
parser.select(&:selector?).first(5).each do |rule|
|
|
147
|
-
puts " #{rule.selector}: #{rule.declarations} (spec: #{rule.specificity})"
|
|
148
|
-
end
|
|
149
|
-
end
|
|
150
|
-
end
|
|
151
|
-
|
|
152
|
-
# Run if executed directly
|
|
153
|
-
ParsingBenchmark.run if __FILE__ == $PROGRAM_NAME
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'benchmark/ips'
|
|
4
|
-
|
|
5
|
-
# Load the local development version, not installed gem
|
|
6
|
-
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
7
|
-
require 'cataract'
|
|
8
|
-
|
|
9
|
-
# Get current branch name
|
|
10
|
-
branch = `git rev-parse --abbrev-ref HEAD`.strip
|
|
11
|
-
|
|
12
|
-
puts '=' * 80
|
|
13
|
-
puts 'RAGEL REMOVAL BENCHMARK - Parsing Performance Comparison'
|
|
14
|
-
puts '=' * 80
|
|
15
|
-
puts "Current branch: #{branch}"
|
|
16
|
-
puts
|
|
17
|
-
|
|
18
|
-
# Load bootstrap.css fixture (large real-world CSS file)
|
|
19
|
-
fixtures_dir = File.expand_path('../test/fixtures', __dir__)
|
|
20
|
-
bootstrap_css = File.read(File.join(fixtures_dir, 'bootstrap.css'))
|
|
21
|
-
|
|
22
|
-
puts "Bootstrap CSS: #{bootstrap_css.lines.count} lines, #{bootstrap_css.bytesize} bytes"
|
|
23
|
-
puts
|
|
24
|
-
|
|
25
|
-
# Verify parsing works
|
|
26
|
-
puts 'Verifying parsing works...'
|
|
27
|
-
parser = Cataract::Stylesheet.new
|
|
28
|
-
parser.add_block(bootstrap_css)
|
|
29
|
-
puts " ✅ Parsed successfully (#{parser.rules_count} rules)"
|
|
30
|
-
puts
|
|
31
|
-
|
|
32
|
-
puts '=' * 80
|
|
33
|
-
puts 'BENCHMARK: Bootstrap CSS Parsing'
|
|
34
|
-
puts '=' * 80
|
|
35
|
-
puts
|
|
36
|
-
|
|
37
|
-
Benchmark.ips do |x|
|
|
38
|
-
x.config(time: 5, warmup: 2)
|
|
39
|
-
|
|
40
|
-
x.report("#{branch}:parse_bootstrap") do
|
|
41
|
-
parser = Cataract::Stylesheet.new
|
|
42
|
-
parser.add_block(bootstrap_css)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
x.compare!
|
|
46
|
-
|
|
47
|
-
# Save results to file for cross-branch comparison
|
|
48
|
-
x.save! 'test/.benchmark_results/ragel_removal.json'
|
|
49
|
-
x.hold! 'test/.benchmark_results/ragel_removal.json'
|
|
50
|
-
end
|
|
51
|
-
|
|
52
|
-
puts
|
|
53
|
-
puts '=' * 80
|
|
54
|
-
puts 'Results saved to test/.benchmark_results/ragel_removal.json'
|
|
55
|
-
puts 'Switch git branches, recompile, and run again to compare!'
|
|
56
|
-
puts '=' * 80
|