cataract 0.1.3 → 0.2.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 +4 -4
- data/.github/workflows/ci-manual-rubies.yml +44 -0
- data/.overcommit.yml +1 -1
- data/.rubocop.yml +96 -4
- data/.rubocop_todo.yml +186 -0
- data/BENCHMARKS.md +62 -141
- data/CHANGELOG.md +20 -0
- data/RAGEL_MIGRATION.md +2 -2
- data/README.md +37 -4
- data/Rakefile +72 -32
- data/cataract.gemspec +4 -1
- data/ext/cataract/cataract.c +59 -50
- data/ext/cataract/cataract.h +5 -3
- data/ext/cataract/css_parser.c +173 -65
- data/ext/cataract/extconf.rb +2 -2
- data/ext/cataract/{merge.c → flatten.c} +526 -468
- data/ext/cataract/shorthand_expander.c +164 -115
- data/lib/cataract/at_rule.rb +8 -9
- data/lib/cataract/declaration.rb +18 -0
- data/lib/cataract/import_resolver.rb +63 -43
- data/lib/cataract/import_statement.rb +49 -0
- data/lib/cataract/pure/byte_constants.rb +69 -0
- data/lib/cataract/pure/flatten.rb +1145 -0
- data/lib/cataract/pure/helpers.rb +35 -0
- data/lib/cataract/pure/imports.rb +268 -0
- data/lib/cataract/pure/parser.rb +1340 -0
- data/lib/cataract/pure/serializer.rb +590 -0
- data/lib/cataract/pure/specificity.rb +206 -0
- data/lib/cataract/pure.rb +153 -0
- data/lib/cataract/rule.rb +69 -15
- data/lib/cataract/stylesheet.rb +356 -49
- data/lib/cataract/version.rb +1 -1
- data/lib/cataract.rb +43 -26
- metadata +14 -26
- 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
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: cataract
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.2.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- James Cook
|
|
@@ -21,12 +21,14 @@ extra_rdoc_files: []
|
|
|
21
21
|
files:
|
|
22
22
|
- ".clang-tidy"
|
|
23
23
|
- ".github/workflows/ci-macos.yml"
|
|
24
|
+
- ".github/workflows/ci-manual-rubies.yml"
|
|
24
25
|
- ".github/workflows/ci.yml"
|
|
25
26
|
- ".github/workflows/docs.yml"
|
|
26
27
|
- ".github/workflows/test.yml"
|
|
27
28
|
- ".gitignore"
|
|
28
29
|
- ".overcommit.yml"
|
|
29
30
|
- ".rubocop.yml"
|
|
31
|
+
- ".rubocop_todo.yml"
|
|
30
32
|
- BENCHMARKS.md
|
|
31
33
|
- CHANGELOG.md
|
|
32
34
|
- Gemfile
|
|
@@ -34,27 +36,6 @@ files:
|
|
|
34
36
|
- RAGEL_MIGRATION.md
|
|
35
37
|
- README.md
|
|
36
38
|
- Rakefile
|
|
37
|
-
- benchmarks/benchmark_harness.rb
|
|
38
|
-
- benchmarks/benchmark_merging.rb
|
|
39
|
-
- benchmarks/benchmark_optimization_comparison.rb
|
|
40
|
-
- benchmarks/benchmark_parsing.rb
|
|
41
|
-
- benchmarks/benchmark_ragel_removal.rb
|
|
42
|
-
- benchmarks/benchmark_runner.rb
|
|
43
|
-
- benchmarks/benchmark_serialization.rb
|
|
44
|
-
- benchmarks/benchmark_shorthand.rb
|
|
45
|
-
- benchmarks/benchmark_shorthand_expansion.rb
|
|
46
|
-
- benchmarks/benchmark_specificity.rb
|
|
47
|
-
- benchmarks/benchmark_string_allocation.rb
|
|
48
|
-
- benchmarks/benchmark_stylesheet_to_s.rb
|
|
49
|
-
- benchmarks/benchmark_to_s_cached.rb
|
|
50
|
-
- benchmarks/benchmark_value_splitter.rb
|
|
51
|
-
- benchmarks/benchmark_yjit.rb
|
|
52
|
-
- benchmarks/benchmark_yjit_workers.rb
|
|
53
|
-
- benchmarks/profile_to_s.rb
|
|
54
|
-
- benchmarks/speedup_calculator.rb
|
|
55
|
-
- benchmarks/system_metadata.rb
|
|
56
|
-
- benchmarks/templates/benchmarks.md.erb
|
|
57
|
-
- benchmarks/yjit_tests.rb
|
|
58
39
|
- cataract.gemspec
|
|
59
40
|
- cliff.toml
|
|
60
41
|
- docs/files/EXAMPLE.md
|
|
@@ -74,8 +55,8 @@ files:
|
|
|
74
55
|
- ext/cataract/cataract.h
|
|
75
56
|
- ext/cataract/css_parser.c
|
|
76
57
|
- ext/cataract/extconf.rb
|
|
58
|
+
- ext/cataract/flatten.c
|
|
77
59
|
- ext/cataract/import_scanner.c
|
|
78
|
-
- ext/cataract/merge.c
|
|
79
60
|
- ext/cataract/shorthand_expander.c
|
|
80
61
|
- ext/cataract/specificity.c
|
|
81
62
|
- ext/cataract/value_splitter.c
|
|
@@ -99,16 +80,23 @@ files:
|
|
|
99
80
|
- lib/cataract.rb
|
|
100
81
|
- lib/cataract/at_rule.rb
|
|
101
82
|
- lib/cataract/color_conversion.rb
|
|
83
|
+
- lib/cataract/declaration.rb
|
|
102
84
|
- lib/cataract/declarations.rb
|
|
103
85
|
- lib/cataract/import_resolver.rb
|
|
86
|
+
- lib/cataract/import_statement.rb
|
|
87
|
+
- lib/cataract/pure.rb
|
|
88
|
+
- lib/cataract/pure/byte_constants.rb
|
|
89
|
+
- lib/cataract/pure/flatten.rb
|
|
90
|
+
- lib/cataract/pure/helpers.rb
|
|
91
|
+
- lib/cataract/pure/imports.rb
|
|
92
|
+
- lib/cataract/pure/parser.rb
|
|
93
|
+
- lib/cataract/pure/serializer.rb
|
|
94
|
+
- lib/cataract/pure/specificity.rb
|
|
104
95
|
- lib/cataract/rule.rb
|
|
105
96
|
- lib/cataract/stylesheet.rb
|
|
106
97
|
- lib/cataract/stylesheet_scope.rb
|
|
107
98
|
- lib/cataract/version.rb
|
|
108
99
|
- lib/tasks/gem.rake
|
|
109
|
-
- scripts/fuzzer/run.rb
|
|
110
|
-
- scripts/fuzzer/worker.rb
|
|
111
|
-
- scripts/generate_benchmarks_md.rb
|
|
112
100
|
homepage: https://github.com/jamescook/cataract
|
|
113
101
|
licenses:
|
|
114
102
|
- MIT
|
|
@@ -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
|