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
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.1.3
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