Optimize single-rule CSS selector matching:

Add a fast path that directly evaluates selectors containing one rule,
  avoiding combinator traversal and switch overhead for common class, ID,
  tag, and pseudo-class selectors.

  Move complex selector traversal into a dedicated function and cache rule
  counts in selector loops. Add focused matching coverage and document the
  Phase 7 benchmark results.
This commit is contained in:
Martín Lucas Golini
2026-07-10 19:51:05 -03:00
parent d29dbb1cc1
commit 660d64c823
4 changed files with 55 additions and 7 deletions
@@ -828,6 +828,19 @@ Use instrumentation from Phase 4 to confirm:
# Phase 7: Add Single-Rule Selector Fast Path # 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 ## Motivation
Many selectors are simple: Many selectors are simple:
+5 -2
View File
@@ -41,14 +41,17 @@ class EE_API StyleSheetSelector {
std::string mName; std::string mName;
Int64 mSpecificity; Int64 mSpecificity;
std::vector<StyleSheetSelectorRule> mSelectorRules; std::vector<StyleSheetSelectorRule> mSelectorRules;
bool mCacheable; bool mCacheable{ true };
bool mStructurallyVolatile; bool mStructurallyVolatile{ false };
bool mIsSingleRule{ false };
void addSelectorRule( std::string& buffer, void addSelectorRule( std::string& buffer,
StyleSheetSelectorRule::PatternMatch& curPatternMatch, StyleSheetSelectorRule::PatternMatch& curPatternMatch,
const StyleSheetSelectorRule::PatternMatch& newPatternMatch ); const StyleSheetSelectorRule::PatternMatch& newPatternMatch );
void parseSelector( std::string selector ); void parseSelector( std::string selector );
bool selectComplex( UIWidget* element, const bool& applyPseudo ) const;
}; };
}}} // namespace EE::UI::CSS }}} // namespace EE::UI::CSS
+16 -5
View File
@@ -3,12 +3,12 @@
namespace EE { namespace UI { namespace CSS { namespace EE { namespace UI { namespace CSS {
StyleSheetSelector::StyleSheetSelector() : mName( "*" ), mSpecificity( 0 ), mCacheable( true ) { StyleSheetSelector::StyleSheetSelector() : mName( "*" ), mSpecificity( 0 ) {
parseSelector( mName ); parseSelector( mName );
} }
StyleSheetSelector::StyleSheetSelector( const std::string& selectorName ) : StyleSheetSelector::StyleSheetSelector( const std::string& selectorName ) :
mName( selectorName ), mSpecificity( 0 ), mCacheable( true ), mStructurallyVolatile( false ) { mName( selectorName ), mSpecificity( 0 ) {
parseSelector( mName ); parseSelector( mName );
} }
@@ -136,6 +136,8 @@ void StyleSheetSelector::parseSelector( std::string selector ) {
} }
} }
} }
mIsSingleRule = mSelectorRules.size() == 1;
} }
bool StyleSheetSelector::isCacheable() const { bool StyleSheetSelector::isCacheable() const {
@@ -150,9 +152,17 @@ bool StyleSheetSelector::select( UIWidget* element, const bool& applyPseudo ) co
if ( mSelectorRules.empty() ) if ( mSelectorRules.empty() )
return false; 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]; const StyleSheetSelectorRule& selectorRule = mSelectorRules[i];
switch ( selectorRule.getPatternMatch() ) { switch ( selectorRule.getPatternMatch() ) {
@@ -235,8 +245,9 @@ std::vector<UIWidget*> StyleSheetSelector::getRelatedElements( UIWidget* element
return elements; return elements;
UIWidget* curElement = element; 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]; const StyleSheetSelectorRule& selectorRule = mSelectorRules[i];
switch ( selectorRule.getPatternMatch() ) { switch ( selectorRule.getPatternMatch() ) {
+21
View File
@@ -2914,6 +2914,27 @@ UTEST( UIHTML, HashedSelectorMatchingAndClassMutation ) {
Engine::destroySingleton(); 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><body><div id="target" class="foo bar"></div></body></html>)html" ) );
auto* target = sceneNode->getRoot()->find( "target" )->asType<UIWidget>();
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 ) { UTEST( UIHTML, ClassIndexedStyleSheetCandidates ) {
Engine::instance()->createWindow( WindowSettings( 1024, 768, "Class Index Test", Engine::instance()->createWindow( WindowSettings( 1024, 768, "Class Index Test",
WindowStyle::Default, WindowBackend::Default, WindowStyle::Default, WindowBackend::Default,