diff --git a/.agent/plans/eepp_css_selector_optimization_plan.md b/.agent/plans/eepp_css_selector_optimization_plan.md index 0771d2c53..b27a7c25a 100644 --- a/.agent/plans/eepp_css_selector_optimization_plan.md +++ b/.agent/plans/eepp_css_selector_optimization_plan.md @@ -828,6 +828,19 @@ Use instrumentation from Phase 4 to confirm: # Phase 7: Add Single-Rule Selector Fast Path +**Status: Implemented** + +## Implementation State + +- `StyleSheetSelector` records whether parsing produced exactly one rule. +- Single-rule selectors call `StyleSheetSelectorRule::matches()` directly, while selectors with + combinators retain the existing traversal logic in `selectComplex()`. +- A focused test covers class, ID, tag+class, and pseudo-class selectors with pseudo matching both + enabled and disabled. +- Across seven release benchmark runs, `Benchmark.CSSSelectorMatching` had a 93.0 ms median for + 20.48 million selector calls. Compared with the earlier Phase 5 median of approximately 111.4 ms, + this is an indicative 16.6% reduction. + ## Motivation Many selectors are simple: diff --git a/include/eepp/ui/css/stylesheetselector.hpp b/include/eepp/ui/css/stylesheetselector.hpp index 784e2ecc8..17443351a 100644 --- a/include/eepp/ui/css/stylesheetselector.hpp +++ b/include/eepp/ui/css/stylesheetselector.hpp @@ -41,14 +41,17 @@ class EE_API StyleSheetSelector { std::string mName; Int64 mSpecificity; std::vector mSelectorRules; - bool mCacheable; - bool mStructurallyVolatile; + bool mCacheable{ true }; + bool mStructurallyVolatile{ false }; + bool mIsSingleRule{ false }; void addSelectorRule( std::string& buffer, StyleSheetSelectorRule::PatternMatch& curPatternMatch, const StyleSheetSelectorRule::PatternMatch& newPatternMatch ); void parseSelector( std::string selector ); + + bool selectComplex( UIWidget* element, const bool& applyPseudo ) const; }; }}} // namespace EE::UI::CSS diff --git a/src/eepp/ui/css/stylesheetselector.cpp b/src/eepp/ui/css/stylesheetselector.cpp index fba992609..a673cd37a 100644 --- a/src/eepp/ui/css/stylesheetselector.cpp +++ b/src/eepp/ui/css/stylesheetselector.cpp @@ -3,12 +3,12 @@ namespace EE { namespace UI { namespace CSS { -StyleSheetSelector::StyleSheetSelector() : mName( "*" ), mSpecificity( 0 ), mCacheable( true ) { +StyleSheetSelector::StyleSheetSelector() : mName( "*" ), mSpecificity( 0 ) { parseSelector( mName ); } StyleSheetSelector::StyleSheetSelector( const std::string& selectorName ) : - mName( selectorName ), mSpecificity( 0 ), mCacheable( true ), mStructurallyVolatile( false ) { + mName( selectorName ), mSpecificity( 0 ) { parseSelector( mName ); } @@ -136,6 +136,8 @@ void StyleSheetSelector::parseSelector( std::string selector ) { } } } + + mIsSingleRule = mSelectorRules.size() == 1; } bool StyleSheetSelector::isCacheable() const { @@ -150,9 +152,17 @@ bool StyleSheetSelector::select( UIWidget* element, const bool& applyPseudo ) co if ( mSelectorRules.empty() ) return false; - UIWidget* curElement = element; + if ( mIsSingleRule ) + return mSelectorRules[0].matches( element, applyPseudo ); - for ( size_t i = 0; i < mSelectorRules.size(); i++ ) { + return selectComplex( element, applyPseudo ); +} + +bool StyleSheetSelector::selectComplex( UIWidget* element, const bool& applyPseudo ) const { + UIWidget* curElement = element; + const size_t ruleCount = mSelectorRules.size(); + + for ( size_t i = 0; i < ruleCount; i++ ) { const StyleSheetSelectorRule& selectorRule = mSelectorRules[i]; switch ( selectorRule.getPatternMatch() ) { @@ -235,8 +245,9 @@ std::vector StyleSheetSelector::getRelatedElements( UIWidget* element return elements; UIWidget* curElement = element; + const size_t ruleCount = mSelectorRules.size(); - for ( size_t i = 0; i < mSelectorRules.size(); i++ ) { + for ( size_t i = 0; i < ruleCount; i++ ) { const StyleSheetSelectorRule& selectorRule = mSelectorRules[i]; switch ( selectorRule.getPatternMatch() ) { diff --git a/src/tests/unit_tests/uihtml_tests.cpp b/src/tests/unit_tests/uihtml_tests.cpp index b51920c10..7d716e05f 100644 --- a/src/tests/unit_tests/uihtml_tests.cpp +++ b/src/tests/unit_tests/uihtml_tests.cpp @@ -2914,6 +2914,27 @@ UTEST( UIHTML, HashedSelectorMatchingAndClassMutation ) { Engine::destroySingleton(); } +UTEST( UIHTML, SingleRuleSelectorMatching ) { + Engine::instance()->createWindow( WindowSettings( 1024, 768, "Single Rule Selector Test", + WindowStyle::Default, WindowBackend::Default, + 32, {}, 1, false, true ), + ContextSettings( false, 0, 0, GLv_default, true, false ) ); + + UISceneNode* sceneNode = init_test_inline_block(); + sceneNode->loadLayoutFromString( HTMLFormatter::HTMLtoXML( + R"html(
)html" ) ); + auto* target = sceneNode->getRoot()->find( "target" )->asType(); + ASSERT_TRUE( target != nullptr ); + + EXPECT_TRUE( StyleSheetSelector( ".foo" ).select( target, false ) ); + EXPECT_TRUE( StyleSheetSelector( "#target" ).select( target, false ) ); + EXPECT_TRUE( StyleSheetSelector( "div.foo" ).select( target, false ) ); + EXPECT_TRUE( StyleSheetSelector( ".foo:hover" ).select( target, false ) ); + EXPECT_FALSE( StyleSheetSelector( ".foo:hover" ).select( target, true ) ); + + Engine::destroySingleton(); +} + UTEST( UIHTML, ClassIndexedStyleSheetCandidates ) { Engine::instance()->createWindow( WindowSettings( 1024, 768, "Class Index Test", WindowStyle::Default, WindowBackend::Default,