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
|
@@ -1,158 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'benchmark_harness'
|
|
4
|
-
require 'open3'
|
|
5
|
-
require 'json'
|
|
6
|
-
|
|
7
|
-
# Load the local development version, not installed gem
|
|
8
|
-
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
9
|
-
require 'cataract'
|
|
10
|
-
|
|
11
|
-
# YJIT Benchmark Supervisor
|
|
12
|
-
# Spawns two subprocesses (with/without YJIT) and combines results
|
|
13
|
-
class YjitBenchmark < BenchmarkHarness
|
|
14
|
-
def self.benchmark_name
|
|
15
|
-
'yjit'
|
|
16
|
-
end
|
|
17
|
-
|
|
18
|
-
def self.description
|
|
19
|
-
'Ruby-side operations with and without YJIT'
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
def self.metadata
|
|
23
|
-
{
|
|
24
|
-
'operations' => [
|
|
25
|
-
'property access',
|
|
26
|
-
'declaration merging',
|
|
27
|
-
'to_s generation',
|
|
28
|
-
'parse + iterate'
|
|
29
|
-
],
|
|
30
|
-
'note' => 'C extension performance is the same regardless of YJIT. This measures Ruby code.'
|
|
31
|
-
}
|
|
32
|
-
end
|
|
33
|
-
|
|
34
|
-
def self.speedup_config
|
|
35
|
-
# Compare without YJIT (baseline) vs with YJIT (comparison)
|
|
36
|
-
{
|
|
37
|
-
baseline_matcher: SpeedupCalculator::Matchers.without_yjit,
|
|
38
|
-
comparison_matcher: SpeedupCalculator::Matchers.with_yjit,
|
|
39
|
-
test_case_key: nil # No test_cases array, just operations
|
|
40
|
-
}
|
|
41
|
-
end
|
|
42
|
-
|
|
43
|
-
def sanity_checks
|
|
44
|
-
# Verify basic operations work
|
|
45
|
-
decls = Cataract::Declarations.new
|
|
46
|
-
decls['color'] = 'red'
|
|
47
|
-
raise 'Property access failed' unless decls['color']
|
|
48
|
-
|
|
49
|
-
parser = Cataract::Stylesheet.new
|
|
50
|
-
sample_css = 'body { margin: 0; }'
|
|
51
|
-
parser.add_block(sample_css)
|
|
52
|
-
raise 'Parse failed' if parser.rules_count.zero?
|
|
53
|
-
end
|
|
54
|
-
|
|
55
|
-
def call
|
|
56
|
-
worker_script = File.expand_path('benchmark_yjit_workers.rb', __dir__)
|
|
57
|
-
|
|
58
|
-
# Clean up any leftover worker files from previous runs
|
|
59
|
-
without_path = File.join(RESULTS_DIR, 'yjit_without.json')
|
|
60
|
-
with_path = File.join(RESULTS_DIR, 'yjit_with.json')
|
|
61
|
-
FileUtils.rm_f(without_path)
|
|
62
|
-
FileUtils.rm_f(with_path)
|
|
63
|
-
|
|
64
|
-
puts 'Running YJIT benchmarks via subprocesses...'
|
|
65
|
-
puts
|
|
66
|
-
|
|
67
|
-
# Run without YJIT
|
|
68
|
-
puts '→ Running without YJIT (--disable-yjit)...'
|
|
69
|
-
stdout_without, status_without = run_subprocess(['ruby', '--disable-yjit', worker_script])
|
|
70
|
-
unless status_without.success?
|
|
71
|
-
raise "Worker without YJIT failed:\n#{stdout_without}"
|
|
72
|
-
end
|
|
73
|
-
|
|
74
|
-
puts stdout_without
|
|
75
|
-
puts
|
|
76
|
-
|
|
77
|
-
# Run with YJIT
|
|
78
|
-
puts '→ Running with YJIT (--yjit)...'
|
|
79
|
-
stdout_with, status_with = run_subprocess(['ruby', '--yjit', worker_script])
|
|
80
|
-
unless status_with.success?
|
|
81
|
-
raise "Worker with YJIT failed:\n#{stdout_with}"
|
|
82
|
-
end
|
|
83
|
-
|
|
84
|
-
puts stdout_with
|
|
85
|
-
puts
|
|
86
|
-
|
|
87
|
-
# Combine results
|
|
88
|
-
combine_worker_results
|
|
89
|
-
end
|
|
90
|
-
|
|
91
|
-
private
|
|
92
|
-
|
|
93
|
-
def run_subprocess(command)
|
|
94
|
-
stdout, stderr, status = Open3.capture3(*command)
|
|
95
|
-
|
|
96
|
-
# Print stderr if present (warnings, etc)
|
|
97
|
-
unless stderr.empty?
|
|
98
|
-
puts "⚠️ stderr: #{stderr}"
|
|
99
|
-
end
|
|
100
|
-
|
|
101
|
-
[stdout, status]
|
|
102
|
-
end
|
|
103
|
-
|
|
104
|
-
def combine_worker_results
|
|
105
|
-
without_path = File.join(RESULTS_DIR, 'yjit_without.json')
|
|
106
|
-
with_path = File.join(RESULTS_DIR, 'yjit_with.json')
|
|
107
|
-
|
|
108
|
-
# Check both files exist
|
|
109
|
-
unless File.exist?(without_path) && File.exist?(with_path)
|
|
110
|
-
raise "Worker results not found:\n #{without_path}\n #{with_path}"
|
|
111
|
-
end
|
|
112
|
-
|
|
113
|
-
# Read both JSON files
|
|
114
|
-
without_data = JSON.parse(File.read(without_path))
|
|
115
|
-
with_data = JSON.parse(File.read(with_path))
|
|
116
|
-
|
|
117
|
-
# Combine into single benchmark result
|
|
118
|
-
combined_data = {
|
|
119
|
-
'name' => self.class.benchmark_name,
|
|
120
|
-
'description' => self.class.description,
|
|
121
|
-
'metadata' => self.class.metadata,
|
|
122
|
-
'timestamp' => Time.now.iso8601,
|
|
123
|
-
'results' => []
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
# Merge results from both workers
|
|
127
|
-
combined_data['results'].concat(without_data['results']) if without_data['results']
|
|
128
|
-
combined_data['results'].concat(with_data['results']) if with_data['results']
|
|
129
|
-
|
|
130
|
-
# Calculate speedups using configured strategy
|
|
131
|
-
config = self.class.speedup_config
|
|
132
|
-
if config
|
|
133
|
-
calculator = SpeedupCalculator.new(
|
|
134
|
-
results: combined_data['results'],
|
|
135
|
-
test_cases: combined_data['metadata']['operations'],
|
|
136
|
-
baseline_matcher: config[:baseline_matcher],
|
|
137
|
-
comparison_matcher: config[:comparison_matcher],
|
|
138
|
-
test_case_key: config[:test_case_key]
|
|
139
|
-
)
|
|
140
|
-
|
|
141
|
-
speedup_stats = calculator.calculate
|
|
142
|
-
combined_data['metadata']['speedups'] = speedup_stats if speedup_stats
|
|
143
|
-
end
|
|
144
|
-
|
|
145
|
-
# Write combined file
|
|
146
|
-
combined_path = File.join(RESULTS_DIR, "#{self.class.benchmark_name}.json")
|
|
147
|
-
File.write(combined_path, JSON.pretty_generate(combined_data))
|
|
148
|
-
|
|
149
|
-
# Clean up worker files
|
|
150
|
-
File.delete(without_path)
|
|
151
|
-
File.delete(with_path)
|
|
152
|
-
|
|
153
|
-
puts "✓ Combined results saved to #{combined_path}"
|
|
154
|
-
end
|
|
155
|
-
end
|
|
156
|
-
|
|
157
|
-
# Run if executed directly
|
|
158
|
-
YjitBenchmark.run if __FILE__ == $PROGRAM_NAME
|
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require_relative 'benchmark_harness'
|
|
4
|
-
require_relative 'yjit_tests'
|
|
5
|
-
|
|
6
|
-
# Load the local development version, not installed gem
|
|
7
|
-
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
8
|
-
require 'cataract'
|
|
9
|
-
|
|
10
|
-
# Worker benchmark: YJIT disabled
|
|
11
|
-
# Called via subprocess with --disable-yjit flag
|
|
12
|
-
class YjitWithoutBenchmark < BenchmarkHarness
|
|
13
|
-
include YjitTests
|
|
14
|
-
|
|
15
|
-
def self.benchmark_name
|
|
16
|
-
'yjit_without'
|
|
17
|
-
end
|
|
18
|
-
|
|
19
|
-
def self.description
|
|
20
|
-
'Ruby-side operations without YJIT'
|
|
21
|
-
end
|
|
22
|
-
|
|
23
|
-
def self.metadata
|
|
24
|
-
YjitTests.metadata
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
def self.speedup_config
|
|
28
|
-
YjitTests.speedup_config
|
|
29
|
-
end
|
|
30
|
-
end
|
|
31
|
-
|
|
32
|
-
# Worker benchmark: YJIT enabled
|
|
33
|
-
# Called via subprocess with --yjit flag
|
|
34
|
-
class YjitWithBenchmark < BenchmarkHarness
|
|
35
|
-
include YjitTests
|
|
36
|
-
|
|
37
|
-
def self.benchmark_name
|
|
38
|
-
'yjit_with'
|
|
39
|
-
end
|
|
40
|
-
|
|
41
|
-
def self.description
|
|
42
|
-
'Ruby-side operations with YJIT'
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
def self.metadata
|
|
46
|
-
YjitTests.metadata
|
|
47
|
-
end
|
|
48
|
-
|
|
49
|
-
def self.speedup_config
|
|
50
|
-
YjitTests.speedup_config
|
|
51
|
-
end
|
|
52
|
-
end
|
|
53
|
-
|
|
54
|
-
# CLI entry point - run the appropriate worker based on YJIT status
|
|
55
|
-
if __FILE__ == $PROGRAM_NAME
|
|
56
|
-
if defined?(RubyVM::YJIT.enabled?) && RubyVM::YJIT.enabled?
|
|
57
|
-
YjitWithBenchmark.run
|
|
58
|
-
else
|
|
59
|
-
YjitWithoutBenchmark.run
|
|
60
|
-
end
|
|
61
|
-
end
|
data/benchmarks/profile_to_s.rb
DELETED
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require 'ruby-prof'
|
|
4
|
-
|
|
5
|
-
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
|
6
|
-
require 'cataract'
|
|
7
|
-
|
|
8
|
-
bootstrap_css = File.read('test/fixtures/bootstrap.css')
|
|
9
|
-
stylesheet = Cataract.parse_css(bootstrap_css)
|
|
10
|
-
|
|
11
|
-
puts "Profiling Stylesheet#to_s on bootstrap.css (#{stylesheet.size} rules)"
|
|
12
|
-
puts ''
|
|
13
|
-
|
|
14
|
-
# Profile
|
|
15
|
-
RubyProf.start
|
|
16
|
-
|
|
17
|
-
10.times { stylesheet.to_s }
|
|
18
|
-
|
|
19
|
-
result = RubyProf.stop
|
|
20
|
-
|
|
21
|
-
# Print flat profile
|
|
22
|
-
printer = RubyProf::FlatPrinter.new(result)
|
|
23
|
-
printer.print($stdout, min_percent: 1)
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
# Generic speedup calculator for benchmark results
|
|
4
|
-
# Compares "baseline" vs "comparison" results and calculates speedup stats
|
|
5
|
-
#
|
|
6
|
-
# CONVENTION: Result names must use format "tool_name: test_case_id"
|
|
7
|
-
# Example: "css_parser: CSS1", "cataract: CSS1"
|
|
8
|
-
class SpeedupCalculator
|
|
9
|
-
# @param results [Array<Hash>] Array of benchmark results with 'name' and 'central_tendency'
|
|
10
|
-
# @param test_cases [Array<Hash>] Array of test case metadata to annotate with speedups
|
|
11
|
-
# @param baseline_matcher [Proc] Block that returns true if result is baseline (e.g., css_parser)
|
|
12
|
-
# @param comparison_matcher [Proc] Block that returns true if result is comparison (e.g., cataract)
|
|
13
|
-
# @param test_case_key [Symbol] Key in test_cases hash to match against test case id from result name
|
|
14
|
-
def initialize(results:, test_cases:, baseline_matcher:, comparison_matcher:, test_case_key: nil)
|
|
15
|
-
@results = results
|
|
16
|
-
@test_cases = test_cases
|
|
17
|
-
@baseline_matcher = baseline_matcher
|
|
18
|
-
@comparison_matcher = comparison_matcher
|
|
19
|
-
@test_case_key = test_case_key
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
# Calculate speedups and return stats hash
|
|
23
|
-
# Also annotates test_cases with individual speedups if test_case_key provided
|
|
24
|
-
# @return [Hash] { min: Float, max: Float, avg: Float } or nil if no pairs found
|
|
25
|
-
def calculate
|
|
26
|
-
speedups = []
|
|
27
|
-
|
|
28
|
-
# Group results by test case (everything after ':')
|
|
29
|
-
grouped = @results.group_by { |result| extract_test_case(result['name']) }
|
|
30
|
-
|
|
31
|
-
grouped.each do |test_case_id, group_results|
|
|
32
|
-
baseline = group_results.find(&@baseline_matcher)
|
|
33
|
-
comparison = group_results.find(&@comparison_matcher)
|
|
34
|
-
|
|
35
|
-
next unless baseline && comparison
|
|
36
|
-
|
|
37
|
-
speedup = comparison['central_tendency'].to_f / baseline['central_tendency']
|
|
38
|
-
speedups << speedup
|
|
39
|
-
|
|
40
|
-
# Annotate test case metadata if provided
|
|
41
|
-
if @test_case_key && @test_cases
|
|
42
|
-
test_case = @test_cases.find { |tc| tc[@test_case_key.to_s] == test_case_id }
|
|
43
|
-
test_case['speedup'] = speedup.round(2) if test_case
|
|
44
|
-
end
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
return nil if speedups.empty?
|
|
48
|
-
|
|
49
|
-
{
|
|
50
|
-
'min' => speedups.min.round(2),
|
|
51
|
-
'max' => speedups.max.round(2),
|
|
52
|
-
'avg' => (speedups.sum / speedups.size).round(2)
|
|
53
|
-
}
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
private
|
|
57
|
-
|
|
58
|
-
# Extract test case id from result name
|
|
59
|
-
# "css_parser: CSS1" -> "CSS1"
|
|
60
|
-
# "cataract gem: all" -> "all"
|
|
61
|
-
def extract_test_case(name)
|
|
62
|
-
name.split(':').last.strip
|
|
63
|
-
end
|
|
64
|
-
|
|
65
|
-
# Common matchers
|
|
66
|
-
class Matchers
|
|
67
|
-
def self.css_parser
|
|
68
|
-
->(result) { result['name'].include?('css_parser') }
|
|
69
|
-
end
|
|
70
|
-
|
|
71
|
-
def self.cataract
|
|
72
|
-
->(result) { result['name'].include?('cataract') }
|
|
73
|
-
end
|
|
74
|
-
|
|
75
|
-
def self.with_yjit
|
|
76
|
-
->(result) { result['name'].include?('YJIT') && !result['name'].include?('no YJIT') }
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
def self.without_yjit
|
|
80
|
-
->(result) { result['name'].include?('no YJIT') }
|
|
81
|
-
end
|
|
82
|
-
end
|
|
83
|
-
end
|
|
@@ -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
|