cataract 0.1.3 → 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.
Files changed (53) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci-manual-rubies.yml +27 -0
  3. data/.overcommit.yml +1 -1
  4. data/.rubocop.yml +62 -0
  5. data/.rubocop_todo.yml +186 -0
  6. data/BENCHMARKS.md +60 -139
  7. data/CHANGELOG.md +10 -0
  8. data/README.md +30 -2
  9. data/Rakefile +49 -22
  10. data/cataract.gemspec +4 -1
  11. data/ext/cataract/cataract.c +47 -47
  12. data/ext/cataract/css_parser.c +17 -33
  13. data/ext/cataract/merge.c +6 -0
  14. data/lib/cataract/at_rule.rb +8 -9
  15. data/lib/cataract/declaration.rb +18 -0
  16. data/lib/cataract/import_resolver.rb +3 -4
  17. data/lib/cataract/pure/byte_constants.rb +69 -0
  18. data/lib/cataract/pure/helpers.rb +35 -0
  19. data/lib/cataract/pure/imports.rb +255 -0
  20. data/lib/cataract/pure/merge.rb +1146 -0
  21. data/lib/cataract/pure/parser.rb +1236 -0
  22. data/lib/cataract/pure/serializer.rb +590 -0
  23. data/lib/cataract/pure/specificity.rb +206 -0
  24. data/lib/cataract/pure.rb +130 -0
  25. data/lib/cataract/rule.rb +22 -13
  26. data/lib/cataract/stylesheet.rb +14 -9
  27. data/lib/cataract/version.rb +1 -1
  28. data/lib/cataract.rb +18 -5
  29. metadata +12 -25
  30. data/benchmarks/benchmark_harness.rb +0 -193
  31. data/benchmarks/benchmark_merging.rb +0 -121
  32. data/benchmarks/benchmark_optimization_comparison.rb +0 -168
  33. data/benchmarks/benchmark_parsing.rb +0 -153
  34. data/benchmarks/benchmark_ragel_removal.rb +0 -56
  35. data/benchmarks/benchmark_runner.rb +0 -70
  36. data/benchmarks/benchmark_serialization.rb +0 -180
  37. data/benchmarks/benchmark_shorthand.rb +0 -109
  38. data/benchmarks/benchmark_shorthand_expansion.rb +0 -176
  39. data/benchmarks/benchmark_specificity.rb +0 -124
  40. data/benchmarks/benchmark_string_allocation.rb +0 -151
  41. data/benchmarks/benchmark_stylesheet_to_s.rb +0 -62
  42. data/benchmarks/benchmark_to_s_cached.rb +0 -55
  43. data/benchmarks/benchmark_value_splitter.rb +0 -54
  44. data/benchmarks/benchmark_yjit.rb +0 -158
  45. data/benchmarks/benchmark_yjit_workers.rb +0 -61
  46. data/benchmarks/profile_to_s.rb +0 -23
  47. data/benchmarks/speedup_calculator.rb +0 -83
  48. data/benchmarks/system_metadata.rb +0 -81
  49. data/benchmarks/templates/benchmarks.md.erb +0 -221
  50. data/benchmarks/yjit_tests.rb +0 -141
  51. data/scripts/fuzzer/run.rb +0 -828
  52. data/scripts/fuzzer/worker.rb +0 -99
  53. data/scripts/generate_benchmarks_md.rb +0 -155
@@ -1,81 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require 'json'
4
- require 'fileutils'
5
-
6
- # Collects system metadata for benchmark runs
7
- class SystemMetadata
8
- RESULTS_DIR = File.expand_path('.results', __dir__)
9
-
10
- def self.collect
11
- metadata = {
12
- 'ruby_version' => RUBY_VERSION,
13
- 'ruby_description' => RUBY_DESCRIPTION,
14
- 'platform' => RUBY_PLATFORM,
15
- 'cpu' => detect_cpu,
16
- 'memory' => detect_memory,
17
- 'os' => detect_os,
18
- 'timestamp' => Time.now.iso8601
19
- }
20
-
21
- FileUtils.mkdir_p(RESULTS_DIR)
22
- File.write(File.join(RESULTS_DIR, 'metadata.json'), JSON.pretty_generate(metadata))
23
-
24
- metadata
25
- end
26
-
27
- def self.detect_cpu
28
- if RUBY_PLATFORM.include?('darwin')
29
- `sysctl -n machdep.cpu.brand_string`.strip
30
- elsif File.exist?('/proc/cpuinfo')
31
- cpuinfo = File.read('/proc/cpuinfo')
32
- if (match = cpuinfo.match(/model name\s*:\s*(.+)/))
33
- match[1].strip
34
- else
35
- 'Unknown'
36
- end
37
- else
38
- 'Unknown'
39
- end
40
- rescue StandardError
41
- 'Unknown'
42
- end
43
-
44
- def self.detect_memory
45
- if RUBY_PLATFORM.include?('darwin')
46
- # Output in GB
47
- bytes = `sysctl -n hw.memsize`.strip.to_i
48
- "#{bytes / (1024 * 1024 * 1024)}GB"
49
- elsif File.exist?('/proc/meminfo')
50
- meminfo = File.read('/proc/meminfo')
51
- if (match = meminfo.match(/MemTotal:\s+(\d+)\s+kB/))
52
- kb = match[1].to_i
53
- "#{kb / (1024 * 1024)}GB"
54
- else
55
- 'Unknown'
56
- end
57
- else
58
- 'Unknown'
59
- end
60
- rescue StandardError
61
- 'Unknown'
62
- end
63
-
64
- def self.detect_os
65
- if RUBY_PLATFORM.include?('darwin')
66
- version = `sw_vers -productVersion`.strip
67
- "macOS #{version}"
68
- elsif File.exist?('/etc/os-release')
69
- os_release = File.read('/etc/os-release')
70
- if (match = os_release.match(/PRETTY_NAME="(.+)"/))
71
- match[1]
72
- else
73
- RUBY_PLATFORM
74
- end
75
- else
76
- RUBY_PLATFORM
77
- end
78
- rescue StandardError
79
- RUBY_PLATFORM
80
- end
81
- end
@@ -1,221 +0,0 @@
1
- <!-- AUTO-GENERATED FILE - DO NOT EDIT -->
2
- <!-- This file is automatically generated from benchmark results. -->
3
- <!-- To regenerate: rake benchmark:generate_docs -->
4
-
5
- # Performance Benchmarks
6
-
7
- Comprehensive performance comparison between Cataract and css_parser gem.
8
-
9
- ## Test Environment
10
-
11
- - **Ruby**: <%= metadata['ruby_description'] %>
12
- - **CPU**: <%= metadata['cpu'] %>
13
- - **Memory**: <%= metadata['memory'] %>
14
- - **OS**: <%= metadata['os'] %>
15
- - **Generated**: <%= metadata['timestamp'] %>
16
-
17
- <%- if parsing_data -%>
18
- <details>
19
- <summary><h2>CSS Parsing</h2></summary>
20
-
21
- Performance of parsing CSS into internal data structures.
22
-
23
- <%= parsing_data['description'] %>
24
-
25
- <%- parsing_data['metadata']['test_cases'].each do |test_case| -%>
26
- ### <%= test_case['name'] %>
27
-
28
- <%- results = parsing_data['results'].select { |r| r['name'].include?(test_case['fixture']) } -%>
29
- <%- css_parser_result = results.find { |r| r['name'].include?('css_parser') } -%>
30
- <%- cataract_result = results.find { |r| r['name'].include?('cataract') } -%>
31
- <%- if css_parser_result && cataract_result -%>
32
- <%- speedup = cataract_result['central_tendency'] / css_parser_result['central_tendency'] -%>
33
-
34
- | Parser | Speed | Time per operation |
35
- |--------|-------|-------------------|
36
- | css_parser | <%= format_ips(css_parser_result, short: true) %> | <%= format_time_per_op(css_parser_result) %> |
37
- | **Cataract** | **<%= format_ips(cataract_result, short: true) %>** | **<%= format_time_per_op(cataract_result) %>** |
38
- | **Speedup** | **<%= format_speedup(speedup) %>** | |
39
-
40
- <%- end -%>
41
- <%- end -%>
42
-
43
- </details>
44
-
45
- ---
46
- <%- end -%>
47
-
48
- <%- if serialization_data -%>
49
- <details>
50
- <summary><h2>CSS Serialization (to_s)</h2></summary>
51
-
52
- Performance of converting parsed CSS back to string format.
53
-
54
- <%= serialization_data['description'] %>
55
-
56
- <%- serialization_data['metadata']['test_cases'].each do |test_case| -%>
57
- ### <%= test_case['name'] %>
58
-
59
- <%- results = serialization_data['results'].select { |r| r['name'].include?(test_case['key']) } -%>
60
- <%- css_parser_result = results.find { |r| r['name'].include?('css_parser') } -%>
61
- <%- cataract_result = results.find { |r| r['name'].include?('cataract') } -%>
62
- <%- if css_parser_result && cataract_result -%>
63
- <%- speedup = cataract_result['central_tendency'] / css_parser_result['central_tendency'] -%>
64
-
65
- | Parser | Speed | Time per operation |
66
- |--------|-------|-------------------|
67
- | css_parser | <%= format_ips(css_parser_result, short: true) %> | <%= format_time_per_op(css_parser_result) %> |
68
- | **Cataract** | **<%= format_ips(cataract_result, short: true) %>** | **<%= format_time_per_op(cataract_result) %>** |
69
- | **Speedup** | **<%= format_speedup(speedup) %>** | |
70
-
71
- <%- end -%>
72
- <%- end -%>
73
-
74
- </details>
75
-
76
- ---
77
- <%- end -%>
78
-
79
- <%- if specificity_data -%>
80
- <details>
81
- <summary><h2>Specificity Calculation</h2></summary>
82
-
83
- Performance of calculating CSS selector specificity values.
84
-
85
- <%= specificity_data['description'] %>
86
-
87
- | Test Case | Speedup |
88
- |-----------|---------|
89
- <%- specificity_data['metadata']['test_cases'].each do |test_case| -%>
90
- | <%= test_case['name'] %> | **<%= format_speedup(test_case['speedup']) %>** |
91
- <%- end -%>
92
-
93
- **Summary:** <%= format_speedup(specificity_data['metadata']['speedups']['min']) %> to <%= format_speedup(specificity_data['metadata']['speedups']['max']) %> (avg <%= format_speedup(specificity_data['metadata']['speedups']['avg']) %>)
94
-
95
- </details>
96
-
97
- ---
98
- <%- end -%>
99
-
100
- <%- if merging_data -%>
101
- <details>
102
- <summary><h2>CSS Merging</h2></summary>
103
-
104
- Performance of merging multiple CSS rule sets with the same selector.
105
-
106
- <%= merging_data['description'] %>
107
-
108
- | Test Case | Speedup |
109
- |-----------|---------|
110
- <%- merging_data['metadata']['test_cases'].each do |test_case| -%>
111
- | <%= test_case['name'] %> | **<%= format_speedup(test_case['speedup']) %>** |
112
- <%- end -%>
113
-
114
- **Summary:** <%= format_speedup(merging_data['metadata']['speedups']['min']) %> to <%= format_speedup(merging_data['metadata']['speedups']['max']) %> (avg <%= format_speedup(merging_data['metadata']['speedups']['avg']) %>)
115
-
116
- ### What's Being Tested
117
- - Specificity-based CSS cascade (ID > class > element)
118
- - `!important` declaration handling
119
- - Shorthand property expansion (e.g., `margin` → `margin-top`, `margin-right`, etc.)
120
- - Shorthand property creation from longhand properties
121
-
122
- </details>
123
-
124
- ---
125
- <%- end -%>
126
-
127
- <%- if yjit_data -%>
128
- <details>
129
- <summary><h2>YJIT Impact</h2></summary>
130
-
131
- Impact of Ruby's YJIT JIT compiler on Ruby-side operations. The C extension performance is the same regardless of YJIT.
132
-
133
- <%= yjit_data['description'] %>
134
-
135
- ### Operations Per Second
136
-
137
- | Operation | Without YJIT | With YJIT | YJIT Improvement |
138
- |-----------|--------------|-----------|------------------|
139
- <%- yjit_data['metadata']['operations'].each do |operation| -%>
140
- <%- results = yjit_data['results'].select { |r| r['name'].include?(operation) } -%>
141
- <%- no_yjit = results.find { |r| r['name'].include?('no YJIT') } -%>
142
- <%- with_yjit = results.find { |r| r['name'].include?('YJIT') && !r['name'].include?('no YJIT') } -%>
143
- <%- if no_yjit && with_yjit -%>
144
- <%- improvement = with_yjit['central_tendency'] / no_yjit['central_tendency'] -%>
145
- <%- pct = ((improvement - 1) * 100).round -%>
146
- | <%= operation %> | <%= format_ips(no_yjit, short: true) %> | <%= format_ips(with_yjit, short: true) %> | **<%= format_speedup(improvement) %>** (<%= pct %>% faster) |
147
- <%- end -%>
148
- <%- end -%>
149
-
150
- ### Key Takeaways
151
- - YJIT provides significant performance boost for Ruby-side operations
152
- - Greatest impact on declaration merging
153
- - Parse + iterate benefits least since most work is in C
154
- - Recommended: Enable YJIT in production (`--yjit` flag or `RUBY_YJIT_ENABLE=1`)
155
-
156
- </details>
157
-
158
- ---
159
- <%- end -%>
160
-
161
- ## Summary
162
-
163
- ### Performance Highlights
164
-
165
- | Category | Min Speedup | Max Speedup | Avg Speedup |
166
- |----------|-------------|-------------|-------------|
167
- <%- if parsing_data && parsing_data['metadata']['speedups'] -%>
168
- <%- sp = parsing_data['metadata']['speedups'] -%>
169
- | **Parsing** | <%= format_speedup(sp['min']) %> | <%= format_speedup(sp['max']) %> | <%= format_speedup(sp['avg']) %> |
170
- <%- end -%>
171
- <%- if serialization_data && serialization_data['metadata']['speedups'] -%>
172
- <%- sp = serialization_data['metadata']['speedups'] -%>
173
- | **Serialization** | <%= format_speedup(sp['min']) %> | <%= format_speedup(sp['max']) %> | <%= format_speedup(sp['avg']) %> |
174
- <%- end -%>
175
- <%- if specificity_data && specificity_data['metadata']['speedups'] -%>
176
- <%- sp = specificity_data['metadata']['speedups'] -%>
177
- | **Specificity** | <%= format_speedup(sp['min']) %> | <%= format_speedup(sp['max']) %> | <%= format_speedup(sp['avg']) %> |
178
- <%- end -%>
179
- <%- if merging_data && merging_data['metadata']['speedups'] -%>
180
- <%- sp = merging_data['metadata']['speedups'] -%>
181
- | **Merging** | <%= format_speedup(sp['min']) %> | <%= format_speedup(sp['max']) %> | <%= format_speedup(sp['avg']) %> |
182
- <%- end -%>
183
-
184
- ### Implementation Notes
185
-
186
- 1. **C Extension**: Critical paths (parsing, specificity, merging, serialization) implemented in C
187
- 2. **Efficient Data Structures**: Rules grouped by media query for O(1) lookups
188
- 3. **Memory Efficient**: Pre-allocated string buffers, minimal Ruby object allocations
189
- 4. **Optimized Algorithms**: Purpose-built CSS specificity calculator
190
-
191
- ### Use Cases
192
-
193
- - **Large CSS files**: Handles complex stylesheets efficiently
194
- - **Specificity calculations**: Optimized for selector analysis
195
- - **High-volume processing**: Reduced allocations minimize GC pressure
196
- - **Production applications**: Tested with Bootstrap CSS and real-world stylesheets
197
-
198
- ---
199
-
200
- ## Running Benchmarks
201
-
202
- ```bash
203
- # All benchmarks
204
- rake benchmark 2>&1 | tee benchmark_output.txt
205
-
206
- # Individual benchmarks
207
- rake benchmark:parsing
208
- rake benchmark:serialization
209
- rake benchmark:specificity
210
- rake benchmark:merging
211
- rake benchmark:yjit
212
-
213
- # Generate documentation
214
- rake benchmark:generate_docs
215
- ```
216
-
217
- ## Notes
218
-
219
- - All benchmarks use benchmark-ips with 3s warmup and 5-10s measurement periods
220
- - Measurements are median i/s (iterations per second) with standard deviation
221
- - css_parser gem must be installed for comparison benchmarks
@@ -1,141 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- # Shared test logic for YJIT benchmarks
4
- # Extended by both YjitWithoutBenchmark and YjitWithBenchmark
5
- module YjitTests
6
- SAMPLE_CSS = <<~CSS
7
- body { margin: 0; padding: 0; font-family: Arial, sans-serif; }
8
- .header { color: #333; padding: 20px; background: #f8f9fa; }
9
- .container { max-width: 1200px; margin: 0 auto; }
10
- div p { line-height: 1.6; }
11
- .container > .item { margin-bottom: 20px; }
12
- h1 + p { margin-top: 0; font-size: 1.2em; }
13
- CSS
14
-
15
- def self.metadata
16
- {
17
- 'operations' => [
18
- 'property access',
19
- 'declaration merging',
20
- 'to_s generation',
21
- 'parse + iterate'
22
- ],
23
- 'note' => 'C extension performance is the same regardless of YJIT. This measures Ruby code.'
24
- }
25
- end
26
-
27
- def self.speedup_config
28
- # Compare without YJIT (baseline) vs with YJIT (comparison)
29
- {
30
- baseline_matcher: SpeedupCalculator::Matchers.without_yjit,
31
- comparison_matcher: SpeedupCalculator::Matchers.with_yjit,
32
- test_case_key: nil # No test_cases array, just operations
33
- }
34
- end
35
-
36
- def sanity_checks
37
- # Verify basic operations work
38
- decls = Cataract::Declarations.new
39
- decls['color'] = 'red'
40
- raise 'Property access failed' unless decls['color']
41
-
42
- parser = Cataract::Stylesheet.new
43
- parser.add_block(SAMPLE_CSS)
44
- raise 'Parse failed' if parser.rules_count.zero?
45
- end
46
-
47
- def call
48
- run_property_access_benchmark
49
- run_declaration_merging_benchmark
50
- run_to_s_benchmark
51
- run_parse_iterate_benchmark
52
- end
53
-
54
- private
55
-
56
- def yjit_label
57
- @yjit_label ||= defined?(RubyVM::YJIT.enabled?) && RubyVM::YJIT.enabled? ? 'YJIT' : 'no YJIT'
58
- end
59
-
60
- def run_property_access_benchmark
61
- puts '=' * 80
62
- puts "TEST: Property access (get/set) - #{yjit_label}"
63
- puts '=' * 80
64
-
65
- benchmark('property_access') do |x|
66
- x.config(time: 3, warmup: 1)
67
-
68
- x.report("#{yjit_label}: property access") do
69
- decls = Cataract::Declarations.new
70
- decls['color'] = 'red'
71
- decls['background'] = 'blue'
72
- decls['font-size'] = '16px'
73
- decls['margin'] = '10px'
74
- decls['padding'] = '5px'
75
- _ = decls['color']
76
- _ = decls['background']
77
- _ = decls['font-size']
78
- end
79
- end
80
- end
81
-
82
- def run_declaration_merging_benchmark
83
- puts "\n#{'=' * 80}"
84
- puts "TEST: Declaration merging - #{yjit_label}"
85
- puts '=' * 80
86
-
87
- benchmark('declaration_merging') do |x|
88
- x.config(time: 3, warmup: 1)
89
-
90
- x.report("#{yjit_label}: declaration merging") do
91
- decls1 = Cataract::Declarations.new
92
- decls1['color'] = 'red'
93
- decls1['font-size'] = '16px'
94
-
95
- decls2 = Cataract::Declarations.new
96
- decls2['background'] = 'blue'
97
- decls2['margin'] = '10px'
98
-
99
- decls1.merge(decls2)
100
- end
101
- end
102
- end
103
-
104
- def run_to_s_benchmark
105
- puts "\n#{'=' * 80}"
106
- puts "TEST: to_s generation - #{yjit_label}"
107
- puts '=' * 80
108
-
109
- benchmark('to_s') do |x|
110
- x.config(time: 3, warmup: 1)
111
-
112
- x.report("#{yjit_label}: to_s generation") do
113
- decls = Cataract::Declarations.new
114
- decls['color'] = 'red'
115
- decls['background'] = 'blue'
116
- decls['font-size'] = '16px'
117
- decls['margin'] = '10px'
118
- decls['padding'] = '5px'
119
- decls.to_s
120
- end
121
- end
122
- end
123
-
124
- def run_parse_iterate_benchmark
125
- puts "\n#{'=' * 80}"
126
- puts "TEST: Parse + iterate - #{yjit_label}"
127
- puts '=' * 80
128
-
129
- benchmark('parse_iterate') do |x|
130
- x.config(time: 3, warmup: 1)
131
-
132
- x.report("#{yjit_label}: parse + iterate") do
133
- parser = Cataract::Stylesheet.new
134
- parser.add_block(SAMPLE_CSS)
135
- parser.select(&:selector?).each do |rule|
136
- _ = rule.declarations
137
- end
138
- end
139
- end
140
- end
141
- end