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.
Files changed (58) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci-manual-rubies.yml +44 -0
  3. data/.overcommit.yml +1 -1
  4. data/.rubocop.yml +96 -4
  5. data/.rubocop_todo.yml +186 -0
  6. data/BENCHMARKS.md +62 -141
  7. data/CHANGELOG.md +20 -0
  8. data/RAGEL_MIGRATION.md +2 -2
  9. data/README.md +37 -4
  10. data/Rakefile +72 -32
  11. data/cataract.gemspec +4 -1
  12. data/ext/cataract/cataract.c +59 -50
  13. data/ext/cataract/cataract.h +5 -3
  14. data/ext/cataract/css_parser.c +173 -65
  15. data/ext/cataract/extconf.rb +2 -2
  16. data/ext/cataract/{merge.c → flatten.c} +526 -468
  17. data/ext/cataract/shorthand_expander.c +164 -115
  18. data/lib/cataract/at_rule.rb +8 -9
  19. data/lib/cataract/declaration.rb +18 -0
  20. data/lib/cataract/import_resolver.rb +63 -43
  21. data/lib/cataract/import_statement.rb +49 -0
  22. data/lib/cataract/pure/byte_constants.rb +69 -0
  23. data/lib/cataract/pure/flatten.rb +1145 -0
  24. data/lib/cataract/pure/helpers.rb +35 -0
  25. data/lib/cataract/pure/imports.rb +268 -0
  26. data/lib/cataract/pure/parser.rb +1340 -0
  27. data/lib/cataract/pure/serializer.rb +590 -0
  28. data/lib/cataract/pure/specificity.rb +206 -0
  29. data/lib/cataract/pure.rb +153 -0
  30. data/lib/cataract/rule.rb +69 -15
  31. data/lib/cataract/stylesheet.rb +356 -49
  32. data/lib/cataract/version.rb +1 -1
  33. data/lib/cataract.rb +43 -26
  34. metadata +14 -26
  35. data/benchmarks/benchmark_harness.rb +0 -193
  36. data/benchmarks/benchmark_merging.rb +0 -121
  37. data/benchmarks/benchmark_optimization_comparison.rb +0 -168
  38. data/benchmarks/benchmark_parsing.rb +0 -153
  39. data/benchmarks/benchmark_ragel_removal.rb +0 -56
  40. data/benchmarks/benchmark_runner.rb +0 -70
  41. data/benchmarks/benchmark_serialization.rb +0 -180
  42. data/benchmarks/benchmark_shorthand.rb +0 -109
  43. data/benchmarks/benchmark_shorthand_expansion.rb +0 -176
  44. data/benchmarks/benchmark_specificity.rb +0 -124
  45. data/benchmarks/benchmark_string_allocation.rb +0 -151
  46. data/benchmarks/benchmark_stylesheet_to_s.rb +0 -62
  47. data/benchmarks/benchmark_to_s_cached.rb +0 -55
  48. data/benchmarks/benchmark_value_splitter.rb +0 -54
  49. data/benchmarks/benchmark_yjit.rb +0 -158
  50. data/benchmarks/benchmark_yjit_workers.rb +0 -61
  51. data/benchmarks/profile_to_s.rb +0 -23
  52. data/benchmarks/speedup_calculator.rb +0 -83
  53. data/benchmarks/system_metadata.rb +0 -81
  54. data/benchmarks/templates/benchmarks.md.erb +0 -221
  55. data/benchmarks/yjit_tests.rb +0 -141
  56. data/scripts/fuzzer/run.rb +0 -828
  57. data/scripts/fuzzer/worker.rb +0 -99
  58. data/scripts/generate_benchmarks_md.rb +0 -155
@@ -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
@@ -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
@@ -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