diff --git a/tests/phpunit/tests/html-api/README-performance.md b/tests/phpunit/tests/html-api/README-performance.md new file mode 100644 index 0000000000000..c560cf2294799 --- /dev/null +++ b/tests/phpunit/tests/html-api/README-performance.md @@ -0,0 +1,153 @@ +# CSS Selector Performance Tests + +This directory contains performance tests for the WordPress HTML API CSS selector parsing functionality. + +## Test Files + +### wpCssSelectorPerformance.php +Comprehensive performance tests that measure execution time for various CSS selector parsing scenarios: + +- **Type selectors**: `div`, `span`, `article`, etc. +- **Class selectors**: `.class`, `.my-class`, etc. +- **ID selectors**: `#id`, `#my-id`, etc. +- **Attribute selectors**: `[attr]`, `[attr="value"]`, etc. +- **Compound selectors**: `div.class#id[attr]` +- **Complex selectors**: `div > p`, `nav ul li` +- **Selector lists**: `div, p, span` +- **Unicode selectors**: `.café`, `#résumé`, etc. +- **Escaped selectors**: `.\\31 23`, `#\\2e test` +- **Large selectors**: Complex selectors with many parts +- **Malformed selectors**: Error handling performance + +### wpCssSelectorBenchmark.php +Focused benchmark tests that measure throughput (operations per second) and memory usage: + +- **Throughput tests**: How many selectors can be parsed per second +- **Memory scaling**: How memory usage scales with selector complexity +- **Error handling**: Performance of parsing invalid selectors +- **Real-world scenarios**: Performance with typical framework/theme selectors + +## Running Performance Tests + +### Run All Performance Tests +```bash +vendor/bin/phpunit tests/phpunit/tests/html-api/wpCssSelectorPerformance.php +vendor/bin/phpunit tests/phpunit/tests/html-api/wpCssSelectorBenchmark.php +``` + +### Run Specific Test Groups +```bash +# Run only performance tests +vendor/bin/phpunit --group performance + +# Run only benchmark tests +vendor/bin/phpunit --group benchmark + +# Run both performance and benchmark tests +vendor/bin/phpunit --group performance,benchmark +``` + +### Run Individual Tests +```bash +# Run a specific performance test +vendor/bin/phpunit --filter test_compound_selector_parsing_performance tests/phpunit/tests/html-api/wpCssSelectorPerformance.php + +# Run a specific benchmark test +vendor/bin/phpunit --filter test_simple_selector_parsing_throughput tests/phpunit/tests/html-api/wpCssSelectorBenchmark.php +``` + +## Understanding Results + +### Performance Test Results +Performance tests output timing information to error_log: +``` +Performance [Type Selector Parsing]: avg=0.05ms, min=0.03ms, max=0.12ms, iterations=1000 +``` + +- **avg**: Average execution time in milliseconds +- **min**: Fastest execution time +- **max**: Slowest execution time +- **iterations**: Number of test iterations + +### Benchmark Test Results +Benchmark tests output throughput and memory information: +``` +[BENCHMARK] Simple Selector Parsing: 67543.21 ops/sec, 0.0148ms avg, 0.00KB memory, 0.00KB peak +``` + +- **ops/sec**: Operations (parsings) per second +- **avg**: Average time per operation in milliseconds +- **memory**: Memory used during test in KB +- **peak**: Peak memory usage in KB + +## Performance Expectations + +### Current Performance Thresholds +- **Simple selectors**: > 50,000 ops/sec +- **Compound selectors**: > 10,000 ops/sec +- **Complex selectors**: > 5,000 ops/sec +- **Selector lists**: > 3,000 ops/sec +- **Attribute selectors**: > 8,000 ops/sec +- **Unicode selectors**: > 15,000 ops/sec +- **Error handling**: > 20,000 ops/sec + +### Memory Usage +- Memory usage should scale roughly linearly with selector complexity +- Error handling should not consume excessive memory +- Memory leaks should be avoided during repeated parsing + +## Monitoring Performance + +### In CI/CD +These tests can be integrated into CI/CD pipelines to: +- Monitor performance regressions +- Ensure performance standards are maintained +- Compare performance across different PHP versions + +### Local Development +Use these tests to: +- Measure performance impact of code changes +- Identify performance bottlenecks +- Optimize parsing algorithms + +## Troubleshooting + +### Tests Failing Due to Performance +If performance tests fail consistently: + +1. **Check system load**: High CPU usage can affect timing +2. **Run multiple times**: Single runs may have variance +3. **Update thresholds**: Hardware differences may require adjustment +4. **Profile specific cases**: Use tools like Xdebug to identify bottlenecks + +### Adjusting Performance Thresholds +Thresholds can be adjusted in the test files: +- `wpCssSelectorPerformance.php`: Modify `PERFORMANCE_THRESHOLD_MS` constant +- `wpCssSelectorBenchmark.php`: Modify assertion values in individual tests + +### Adding New Performance Tests +When adding new selector functionality: +1. Add parsing performance tests to `wpCssSelectorPerformance.php` +2. Add throughput benchmarks to `wpCssSelectorBenchmark.php` +3. Update this documentation with new performance expectations + +## Best Practices + +### Writing Performance Tests +- Use realistic test data that represents actual usage +- Include both positive and negative test cases +- Test edge cases and error conditions +- Consider memory usage alongside execution time + +### Interpreting Results +- Look for trends over time, not just individual runs +- Compare relative performance between different selector types +- Consider the context of how selectors are used in real applications +- Monitor both average and peak performance metrics + +### Optimization Guidelines +Based on test results, focus optimization efforts on: +- Most commonly used selector types +- Cases with highest memory usage +- Scenarios with slowest throughput +- Error handling paths that are frequently exercised \ No newline at end of file diff --git a/tests/phpunit/tests/html-api/wpCssSelectorBenchmark.php b/tests/phpunit/tests/html-api/wpCssSelectorBenchmark.php new file mode 100644 index 0000000000000..566c5c7f0ad46 --- /dev/null +++ b/tests/phpunit/tests/html-api/wpCssSelectorBenchmark.php @@ -0,0 +1,334 @@ + $iterations, + 'total_time_ms' => ( $time_end - $time_start ) * 1000, + 'avg_time_ms' => ( ( $time_end - $time_start ) / $iterations ) * 1000, + 'memory_used_kb' => ( $memory_end - $memory_start ) / 1024, + 'peak_memory_kb' => ( $peak_memory_end - $peak_memory_start ) / 1024, + 'ops_per_second' => $iterations / ( $time_end - $time_start ), + ); + } + + /** + * Logs benchmark results in a standardized format. + * + * @param string $test_name Name of the benchmark test. + * @param array $results Results from benchmark(). + */ + private function log_benchmark_results( $test_name, $results ) { + $message = sprintf( + '[BENCHMARK] %s: %.2f ops/sec, %.4fms avg, %.2fKB memory, %.2fKB peak', + $test_name, + $results['ops_per_second'], + $results['avg_time_ms'], + $results['memory_used_kb'], + $results['peak_memory_kb'] + ); + + error_log( $message ); + + // Also output to test result for CI visibility + $this->addToAssertionCount( 1 ); + echo "\n" . $message . "\n"; + } + + /** + * @ticket 62653 + */ + public function test_simple_selector_parsing_throughput() { + $selectors = array( 'div', 'span', 'p', 'a', 'h1', 'section', 'article', 'nav', 'header', 'footer' ); + $selector_count = count( $selectors ); + + $results = $this->benchmark( function() use ( $selectors, $selector_count ) { + $selector = $selectors[ array_rand( $selectors ) ]; + $offset = 0; + WP_CSS_Type_Selector::parse( $selector, $offset ); + } ); + + $this->log_benchmark_results( 'Simple Selector Parsing', $results ); + $this->assertGreaterThan( 50000, $results['ops_per_second'], 'Simple selector parsing should exceed 50K ops/sec' ); + } + + /** + * @ticket 62653 + */ + public function test_compound_selector_parsing_throughput() { + $selectors = array( + 'div.class', + 'p#id', + 'span[attr]', + 'article.post#main', + 'section.content[role]', + 'nav.menu[aria-label]', + 'div.container.fluid', + 'button.btn.primary[type="submit"]', + ); + + $results = $this->benchmark( function() use ( $selectors ) { + $selector = $selectors[ array_rand( $selectors ) ]; + $offset = 0; + WP_CSS_Compound_Selector::parse( $selector, $offset ); + } ); + + $this->log_benchmark_results( 'Compound Selector Parsing', $results ); + $this->assertGreaterThan( 10000, $results['ops_per_second'], 'Compound selector parsing should exceed 10K ops/sec' ); + } + + /** + * @ticket 62653 + */ + public function test_complex_selector_parsing_throughput() { + $selectors = array( + 'div p', + 'article > header', + 'nav ul li', + 'main > section > article', + 'div.container > section.content', + 'nav.menu > ul > li > a', + 'article.post > header.post-header', + 'main.site-main > div.container > section.content > article.post', + ); + + $results = $this->benchmark( function() use ( $selectors ) { + $selector = $selectors[ array_rand( $selectors ) ]; + $offset = 0; + WP_CSS_Complex_Selector::parse( $selector, $offset ); + } ); + + $this->log_benchmark_results( 'Complex Selector Parsing', $results ); + $this->assertGreaterThan( 5000, $results['ops_per_second'], 'Complex selector parsing should exceed 5K ops/sec' ); + } + + /** + * @ticket 62653 + */ + public function test_selector_list_parsing_throughput() { + $selector_lists = array( + 'div, p, span', + 'article.post, section.content', + 'nav > ul, div > p', + 'div.class1, p#id, span[attr]', + 'article.post > header, section.content > p', + 'nav.menu > ul > li, div.content > article > p', + ); + + $results = $this->benchmark( function() use ( $selector_lists ) { + $selector_list = $selector_lists[ array_rand( $selector_lists ) ]; + WP_CSS_Complex_Selector_List::from_selectors( $selector_list ); + } ); + + $this->log_benchmark_results( 'Selector List Parsing', $results ); + $this->assertGreaterThan( 3000, $results['ops_per_second'], 'Selector list parsing should exceed 3K ops/sec' ); + } + + /** + * @ticket 62653 + */ + public function test_attribute_selector_parsing_throughput() { + $selectors = array( + '[href]', + '[data-test]', + '[href="value"]', + '[href^="https"]', + '[href$=".html"]', + '[href*="example"]', + '[href~="word"]', + '[href|="en"]', + '[data-test="complex-value"]', + '[aria-label="Accessibility text"]', + ); + + $results = $this->benchmark( function() use ( $selectors ) { + $selector = $selectors[ array_rand( $selectors ) ]; + $offset = 0; + WP_CSS_Attribute_Selector::parse( $selector, $offset ); + } ); + + $this->log_benchmark_results( 'Attribute Selector Parsing', $results ); + $this->assertGreaterThan( 8000, $results['ops_per_second'], 'Attribute selector parsing should exceed 8K ops/sec' ); + } + + /** + * @ticket 62653 + */ + public function test_unicode_selector_parsing_throughput() { + $selectors = array( + '.café', + '#résumé', + '[title="Élément"]', + 'div.наименование', + 'p.العربية', + 'span.中文', + 'article.日本語', + 'section.한국어', + 'nav.επιλογή', + 'div.элемент', + ); + + $results = $this->benchmark( function() use ( $selectors ) { + $selector = $selectors[ array_rand( $selectors ) ]; + $offset = 0; + if ( $selector[0] === '.' ) { + WP_CSS_Class_Selector::parse( $selector, $offset ); + } elseif ( $selector[0] === '#' ) { + WP_CSS_ID_Selector::parse( $selector, $offset ); + } elseif ( $selector[0] === '[' ) { + WP_CSS_Attribute_Selector::parse( $selector, $offset ); + } else { + WP_CSS_Type_Selector::parse( $selector, $offset ); + } + } ); + + $this->log_benchmark_results( 'Unicode Selector Parsing', $results ); + $this->assertGreaterThan( 15000, $results['ops_per_second'], 'Unicode selector parsing should exceed 15K ops/sec' ); + } + + /** + * @ticket 62653 + */ + public function test_memory_usage_scaling() { + $sizes = array( 10, 100, 1000 ); + $results = array(); + + foreach ( $sizes as $size ) { + // Generate selectors of varying complexity + $selectors = array(); + for ( $i = 0; $i < $size; $i++ ) { + $selectors[] = "div.class{$i}#id{$i}[data-test=\"value{$i}\"]"; + } + + $benchmark_result = $this->benchmark( function() use ( $selectors ) { + foreach ( $selectors as $selector ) { + $offset = 0; + WP_CSS_Compound_Selector::parse( $selector, $offset ); + } + }, 100 ); // Fewer iterations for scaling test + + $results[ $size ] = $benchmark_result; + $this->log_benchmark_results( "Memory Scaling ({$size} selectors)", $benchmark_result ); + } + + // Assert that memory usage scales reasonably + $memory_10 = $results[10]['memory_used_kb']; + $memory_100 = $results[100]['memory_used_kb']; + $memory_1000 = $results[1000]['memory_used_kb']; + + // Memory usage should scale roughly linearly, not exponentially + $scaling_factor_100 = $memory_100 / max( $memory_10, 1 ); + $scaling_factor_1000 = $memory_1000 / max( $memory_100, 1 ); + + $this->assertLessThan( 50, $scaling_factor_100, 'Memory usage should scale reasonably from 10 to 100 selectors' ); + $this->assertLessThan( 50, $scaling_factor_1000, 'Memory usage should scale reasonably from 100 to 1000 selectors' ); + } + + /** + * @ticket 62653 + */ + public function test_parser_error_handling_performance() { + $invalid_selectors = array( + 'div.', + 'div#', + 'div[', + 'div[attr', + 'div[attr=', + 'div[attr="', + 'div[attr="value', + 'div >', + 'div > ', + 'div +', + 'div ~', + 'div[attr="value"i', + 'div[attr=value i', + 'div[attr=="value"]', + 'div[attr~=]', + 'div[attr^=]', + 'div[attr$=]', + 'div[attr*=]', + 'div[attr|=]', + ); + + $results = $this->benchmark( function() use ( $invalid_selectors ) { + $selector = $invalid_selectors[ array_rand( $invalid_selectors ) ]; + $offset = 0; + // These should all return null quickly without throwing exceptions + WP_CSS_Complex_Selector::parse( $selector, $offset ); + } ); + + $this->log_benchmark_results( 'Error Handling Performance', $results ); + $this->assertGreaterThan( 20000, $results['ops_per_second'], 'Error handling should exceed 20K ops/sec' ); + } + + /** + * @ticket 62653 + */ + public function test_real_world_selector_performance() { + // Real-world selectors from popular frameworks and themes + $real_world_selectors = array( + // Bootstrap-style selectors + '.container .row .col-md-6', + '.navbar .navbar-nav .nav-item .nav-link', + '.btn.btn-primary[type="submit"]', + '.form-group .form-control', + '.card .card-header .card-title', + + // WordPress theme selectors + '.site-header .site-navigation .menu-item', + '.site-content .entry-content p', + '.widget-area .widget .widget-title', + 'article.post .entry-meta .posted-on', + '.comment-list .comment .comment-meta', + + // Modern CSS selectors + 'main[role="main"] > section.content', + 'nav[aria-label="Main navigation"] ul', + 'button[aria-expanded="false"]', + 'input[type="email"][required]', + 'div[data-testid="component"]', + ); + + $results = $this->benchmark( function() use ( $real_world_selectors ) { + $selector = $real_world_selectors[ array_rand( $real_world_selectors ) ]; + $offset = 0; + WP_CSS_Complex_Selector::parse( $selector, $offset ); + } ); + + $this->log_benchmark_results( 'Real World Selector Performance', $results ); + $this->assertGreaterThan( 5000, $results['ops_per_second'], 'Real world selector parsing should exceed 5K ops/sec' ); + } +} \ No newline at end of file diff --git a/tests/phpunit/tests/html-api/wpCssSelectorPerformance.php b/tests/phpunit/tests/html-api/wpCssSelectorPerformance.php new file mode 100644 index 0000000000000..9df0a5e65f112 --- /dev/null +++ b/tests/phpunit/tests/html-api/wpCssSelectorPerformance.php @@ -0,0 +1,430 @@ + array_sum( $times ), + 'avg_time' => array_sum( $times ) / count( $times ), + 'min_time' => min( $times ), + 'max_time' => max( $times ), + 'iterations' => $iterations, + ); + } + + /** + * Asserts that performance is within acceptable bounds. + * + * @param array $performance_data Performance data from measure_performance(). + * @param string $test_name Name of the test for error messages. + */ + private function assert_performance_acceptable( $performance_data, $test_name ) { + $avg_time = $performance_data['avg_time']; + + if ( $avg_time > self::PERFORMANCE_THRESHOLD_MS ) { + $this->markTestSkipped( + sprintf( + '%s performance test exceeded threshold: %.2fms average (threshold: %dms)', + $test_name, + $avg_time, + self::PERFORMANCE_THRESHOLD_MS + ) + ); + } + + // Output performance data for CI/monitoring + error_log( + sprintf( + 'Performance [%s]: avg=%.2fms, min=%.2fms, max=%.2fms, iterations=%d', + $test_name, + $performance_data['avg_time'], + $performance_data['min_time'], + $performance_data['max_time'], + $performance_data['iterations'] + ) + ); + } + + /** + * @ticket 62653 + */ + public function test_type_selector_parsing_performance() { + $selectors = array( + 'div', + 'span', + 'a', + 'p', + 'h1', + 'article', + 'section', + 'header', + 'footer', + 'nav', + '*', + 'custom-element', + 'very-long-element-name-with-many-hyphens', + ); + + $performance = $this->measure_performance( function() use ( $selectors ) { + foreach ( $selectors as $selector ) { + $offset = 0; + WP_CSS_Type_Selector::parse( $selector, $offset ); + } + } ); + + $this->assert_performance_acceptable( $performance, 'Type Selector Parsing' ); + } + + /** + * @ticket 62653 + */ + public function test_class_selector_parsing_performance() { + $selectors = array( + '.class', + '.my-class', + '.very-long-class-name-with-many-hyphens', + '.class1', + '.class2', + '.class3', + '.class4', + '.class5', + '.class6', + '.class7', + '.class8', + '.class9', + '.class10', + ); + + $performance = $this->measure_performance( function() use ( $selectors ) { + foreach ( $selectors as $selector ) { + $offset = 0; + WP_CSS_Class_Selector::parse( $selector, $offset ); + } + } ); + + $this->assert_performance_acceptable( $performance, 'Class Selector Parsing' ); + } + + /** + * @ticket 62653 + */ + public function test_id_selector_parsing_performance() { + $selectors = array( + '#id', + '#my-id', + '#very-long-id-name-with-many-hyphens', + '#id1', + '#id2', + '#id3', + '#id4', + '#id5', + '#id6', + '#id7', + '#id8', + '#id9', + '#id10', + ); + + $performance = $this->measure_performance( function() use ( $selectors ) { + foreach ( $selectors as $selector ) { + $offset = 0; + WP_CSS_ID_Selector::parse( $selector, $offset ); + } + } ); + + $this->assert_performance_acceptable( $performance, 'ID Selector Parsing' ); + } + + /** + * @ticket 62653 + */ + public function test_attribute_selector_parsing_performance() { + $selectors = array( + '[href]', + '[data-test]', + '[href="value"]', + '[href^="https"]', + '[href$=".html"]', + '[href*="example"]', + '[href~="word"]', + '[href|="en"]', + '[data-test="complex-value-with-hyphens"]', + '[aria-label="Accessibility text"]', + '[class="multiple classes here"]', + '[data-very-long-attribute-name="value"]', + ); + + $performance = $this->measure_performance( function() use ( $selectors ) { + foreach ( $selectors as $selector ) { + $offset = 0; + WP_CSS_Attribute_Selector::parse( $selector, $offset ); + } + } ); + + $this->assert_performance_acceptable( $performance, 'Attribute Selector Parsing' ); + } + + /** + * @ticket 62653 + */ + public function test_compound_selector_parsing_performance() { + $selectors = array( + 'div.class', + 'div#id', + 'div.class#id', + 'div.class[attr]', + 'div.class#id[attr]', + 'div.class1.class2', + 'div.class1.class2.class3', + 'div#id[attr1][attr2]', + 'div.class1.class2#id[attr1][attr2]', + 'article.post.featured#main[data-id="123"][role="article"]', + '*.class#id[attr]', + 'custom-element.my-class[data-test="value"]', + ); + + $performance = $this->measure_performance( function() use ( $selectors ) { + foreach ( $selectors as $selector ) { + $offset = 0; + WP_CSS_Compound_Selector::parse( $selector, $offset ); + } + } ); + + $this->assert_performance_acceptable( $performance, 'Compound Selector Parsing' ); + } + + /** + * @ticket 62653 + */ + public function test_complex_selector_parsing_performance() { + $selectors = array( + 'div p', + 'div > p', + 'div p a', + 'div > p > a', + 'div p a span', + 'div > p a > span', + 'article.post p.content', + 'nav.menu > ul > li > a', + 'section.content article.post > header.post-header', + 'main.site-main > article.post > section.post-content > p', + 'div.container > section.content > article.post > div.post-body > p.text', + ); + + $performance = $this->measure_performance( function() use ( $selectors ) { + foreach ( $selectors as $selector ) { + $offset = 0; + WP_CSS_Complex_Selector::parse( $selector, $offset ); + } + } ); + + $this->assert_performance_acceptable( $performance, 'Complex Selector Parsing' ); + } + + /** + * @ticket 62653 + */ + public function test_selector_list_parsing_performance() { + $selector_lists = array( + 'div, p, span', + 'div.class, p#id, span[attr]', + 'div > p, section > article, nav > ul', + 'div.class1.class2, p#id[attr], span.class[attr="value"]', + 'article.post, section.content, aside.sidebar, footer.site-footer', + 'nav.menu > ul > li, div.content > p, section.sidebar > aside', + 'div.container > section.content, article.post > header.post-header, footer.site-footer > div.copyright', + ); + + $performance = $this->measure_performance( function() use ( $selector_lists ) { + foreach ( $selector_lists as $selector_list ) { + WP_CSS_Complex_Selector_List::from_selectors( $selector_list ); + } + } ); + + $this->assert_performance_acceptable( $performance, 'Selector List Parsing' ); + } + + /** + * @ticket 62653 + */ + public function test_unicode_selector_parsing_performance() { + $selectors = array( + '.café', + '#résumé', + '[title="Élément"]', + 'div.наименование', + 'p.العربية', + 'span.中文', + 'article.日本語', + 'section.한국어', + 'div.🌟element', + 'p.元素', + 'span.элемент', + 'nav.επιλογή', + ); + + $performance = $this->measure_performance( function() use ( $selectors ) { + foreach ( $selectors as $selector ) { + $offset = 0; + if ( $selector[0] === '.' ) { + WP_CSS_Class_Selector::parse( $selector, $offset ); + } elseif ( $selector[0] === '#' ) { + WP_CSS_ID_Selector::parse( $selector, $offset ); + } elseif ( $selector[0] === '[' ) { + WP_CSS_Attribute_Selector::parse( $selector, $offset ); + } else { + WP_CSS_Type_Selector::parse( $selector, $offset ); + } + } + } ); + + $this->assert_performance_acceptable( $performance, 'Unicode Selector Parsing' ); + } + + /** + * @ticket 62653 + */ + public function test_escaped_selector_parsing_performance() { + $selectors = array( + '.\\31 23', + '#\\31 23', + '[attr="\\31 23"]', + '.\\2e class', + '#\\23 hash', + '[attr="\\22 quote"]', + '.\\41 bc', + '#\\30 30', + '[attr="\\5c backslash"]', + '.\\000061 bc', + '#\\1f0a1', + '[attr="\\1D4B2"]', + ); + + $performance = $this->measure_performance( function() use ( $selectors ) { + foreach ( $selectors as $selector ) { + $offset = 0; + if ( $selector[0] === '.' ) { + WP_CSS_Class_Selector::parse( $selector, $offset ); + } elseif ( $selector[0] === '#' ) { + WP_CSS_ID_Selector::parse( $selector, $offset ); + } elseif ( $selector[0] === '[' ) { + WP_CSS_Attribute_Selector::parse( $selector, $offset ); + } + } + } ); + + $this->assert_performance_acceptable( $performance, 'Escaped Selector Parsing' ); + } + + /** + * @ticket 62653 + */ + public function test_large_selector_parsing_performance() { + // Generate a very large compound selector + $class_parts = array(); + for ( $i = 1; $i <= 50; $i++ ) { + $class_parts[] = '.class' . $i; + } + $large_compound = 'div' . implode( '', $class_parts ); + + // Generate a very deep complex selector + $element_parts = array(); + for ( $i = 1; $i <= 20; $i++ ) { + $element_parts[] = 'div' . $i; + } + $large_complex = implode( ' > ', $element_parts ); + + $selectors = array( + $large_compound, + $large_complex, + implode( ', ', array_slice( $class_parts, 0, 10 ) ), + ); + + $performance = $this->measure_performance( function() use ( $selectors ) { + foreach ( $selectors as $selector ) { + $offset = 0; + if ( strpos( $selector, ',' ) !== false ) { + WP_CSS_Complex_Selector_List::from_selectors( $selector ); + } elseif ( strpos( $selector, ' ' ) !== false || strpos( $selector, '>' ) !== false ) { + WP_CSS_Complex_Selector::parse( $selector, $offset ); + } else { + WP_CSS_Compound_Selector::parse( $selector, $offset ); + } + } + }, 100 ); // Fewer iterations for large selectors + + $this->assert_performance_acceptable( $performance, 'Large Selector Parsing' ); + } + + /** + * @ticket 62653 + */ + public function test_malformed_selector_parsing_performance() { + $malformed_selectors = array( + 'div.', + 'div#', + 'div[', + 'div[attr', + 'div[attr=', + 'div[attr="', + 'div[attr="value', + 'div >', + 'div > ', + 'div +', + 'div ~', + 'div[attr="value"i', + 'div[attr=value i', + 'div[attr=="value"]', + 'div[attr~=]', + 'div[attr^=]', + 'div[attr$=]', + 'div[attr*=]', + 'div[attr|=]', + ); + + $performance = $this->measure_performance( function() use ( $malformed_selectors ) { + foreach ( $malformed_selectors as $selector ) { + $offset = 0; + // Try parsing as different types and expect null results + WP_CSS_Complex_Selector::parse( $selector, $offset ); + } + } ); + + $this->assert_performance_acceptable( $performance, 'Malformed Selector Parsing' ); + } +} \ No newline at end of file