mirror of
https://github.com/SpartanJ/eepp.git
synced 2026-10-02 11:10:31 +03:00
Optimize CSS attribute selector matching:
Resolve standard property definitions and data-attribute classification when selectors are parsed, avoiding repeated specification lookups and attribute-name checks during matching. Fast-path existence-only data selectors without retrieving their values, while preserving all attribute operator behavior. Add standard-property coverage and an attribute-focused benchmark showing a 41.7% reduction in matching time.
This commit is contained in:
@@ -907,6 +907,23 @@ with both `applyPseudo == false` and `applyPseudo == true` where applicable.
|
|||||||
|
|
||||||
# Phase 8: Reduce Attribute Selector Cost
|
# Phase 8: Reduce Attribute Selector Cost
|
||||||
|
|
||||||
|
**Status: Implemented**
|
||||||
|
|
||||||
|
## Implementation State
|
||||||
|
|
||||||
|
- Attribute names are hashed once during parsing to resolve and cache the standard
|
||||||
|
`PropertyDefinition`, avoiding repeated stylesheet-specification lookups while matching.
|
||||||
|
- Data-attribute classification is cached in each parsed attribute selector.
|
||||||
|
- Existence-only data selectors return after the map lookup without constructing or reading a
|
||||||
|
value string; value comparisons continue to use the stored `StyleSheetProperty` string directly.
|
||||||
|
- Tag, ID, and class checks remain ahead of attribute matching so unrelated elements reject early.
|
||||||
|
- Existing coverage exercises all supported data-attribute operators, including empty-value
|
||||||
|
existence. Additional checks cover standard-property existence, exact matching, and unknown
|
||||||
|
properties.
|
||||||
|
- `Benchmark.CSSAttributeSelectorMatching` covers all supported operators and a standard property.
|
||||||
|
Across seven release runs of 5.12 million selector calls, the median decreased from 155.9 ms to
|
||||||
|
90.9 ms, a 41.7% reduction.
|
||||||
|
|
||||||
## Motivation
|
## Motivation
|
||||||
|
|
||||||
Attribute selectors are less common than class selectors but expensive when evaluated.
|
Attribute selectors are less common than class selectors but expensive when evaluated.
|
||||||
|
|||||||
@@ -114,8 +114,10 @@ class EE_API StyleSheetSelectorRule {
|
|||||||
|
|
||||||
struct AttributeSelector {
|
struct AttributeSelector {
|
||||||
std::string name;
|
std::string name;
|
||||||
AttributeOperator op{ AttributeOperator::None };
|
|
||||||
std::string value;
|
std::string value;
|
||||||
|
const PropertyDefinition* propertyDefinition{ nullptr };
|
||||||
|
AttributeOperator op{ AttributeOperator::None };
|
||||||
|
bool isDataAttribute{ false };
|
||||||
};
|
};
|
||||||
|
|
||||||
static PseudoClasses toPseudoClass( std::string_view cls );
|
static PseudoClasses toPseudoClass( std::string_view cls );
|
||||||
|
|||||||
@@ -9,12 +9,14 @@
|
|||||||
#include <eepp/system/sys.hpp>
|
#include <eepp/system/sys.hpp>
|
||||||
#include <eepp/ui/css/stylesheetparser.hpp>
|
#include <eepp/ui/css/stylesheetparser.hpp>
|
||||||
#include <eepp/ui/css/stylesheetselector.hpp>
|
#include <eepp/ui/css/stylesheetselector.hpp>
|
||||||
|
#include <eepp/ui/uihtmlwidget.hpp>
|
||||||
#include <eepp/ui/uimarkdownview.hpp>
|
#include <eepp/ui/uimarkdownview.hpp>
|
||||||
#include <eepp/ui/uiscenenode.hpp>
|
#include <eepp/ui/uiscenenode.hpp>
|
||||||
#include <eepp/ui/uiscrollview.hpp>
|
#include <eepp/ui/uiscrollview.hpp>
|
||||||
#include <eepp/ui/uithememanager.hpp>
|
#include <eepp/ui/uithememanager.hpp>
|
||||||
#include <eepp/window/engine.hpp>
|
#include <eepp/window/engine.hpp>
|
||||||
|
|
||||||
|
#include <array>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
|
|
||||||
using namespace EE;
|
using namespace EE;
|
||||||
@@ -150,6 +152,53 @@ UTEST( Benchmark, CSSClassIndexLookup ) {
|
|||||||
Engine::destroySingleton();
|
Engine::destroySingleton();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UTEST( Benchmark, CSSAttributeSelectorMatching ) {
|
||||||
|
Engine::instance()->createWindow( WindowSettings( 800, 600, "CSS attribute selector bench",
|
||||||
|
WindowStyle::Default, WindowBackend::Default,
|
||||||
|
32, {}, 1, false, true ),
|
||||||
|
ContextSettings( false, 0, 0, GLv_default, true, false ) );
|
||||||
|
UIHTMLWidget* widget = UIHTMLWidget::New();
|
||||||
|
widget->setDataProperty( "data-empty", "" );
|
||||||
|
widget->setDataProperty( "data-role", "hero" );
|
||||||
|
widget->setDataProperty( "data-tags", "featured primary" );
|
||||||
|
widget->setDataProperty( "data-lang", "en-US" );
|
||||||
|
widget->setDataProperty( "data-id", "user-42" );
|
||||||
|
|
||||||
|
static constexpr int selectorCount = 1024;
|
||||||
|
const std::array<std::string, 8> selectorNames = { "[data-empty]",
|
||||||
|
"[data-role=\"hero\"]",
|
||||||
|
"[data-tags~=\"featured\"]",
|
||||||
|
"[data-lang|=\"en\"]",
|
||||||
|
"[data-id^=\"user-\"]",
|
||||||
|
"[data-id$=\"-42\"]",
|
||||||
|
"[data-id*=\"ser\"]",
|
||||||
|
"[width]" };
|
||||||
|
std::vector<StyleSheetSelector> selectors;
|
||||||
|
selectors.reserve( selectorCount );
|
||||||
|
for ( int i = 0; i < selectorCount; ++i )
|
||||||
|
selectors.emplace_back( selectorNames[i % selectorNames.size()] );
|
||||||
|
|
||||||
|
const int matchingIterations = getSelectorMatchingIterations();
|
||||||
|
Uint64 matchCount = 0;
|
||||||
|
Clock matchingClock;
|
||||||
|
for ( int iteration = 0; iteration < matchingIterations; ++iteration ) {
|
||||||
|
for ( const auto& selector : selectors )
|
||||||
|
matchCount += selector.select( widget, false );
|
||||||
|
}
|
||||||
|
const Time matchingElapsed = matchingClock.getElapsedTime();
|
||||||
|
|
||||||
|
EXPECT_EQ( static_cast<Uint64>( matchingIterations ) * selectorCount, matchCount );
|
||||||
|
UTEST_PRINT_INFO(
|
||||||
|
String::format( "Attribute selector matching: %lld us", matchingElapsed.asMicroseconds() )
|
||||||
|
.c_str() );
|
||||||
|
UTEST_PRINT_INFO(
|
||||||
|
String::format( "Attribute selector calls: %d", selectorCount * matchingIterations )
|
||||||
|
.c_str() );
|
||||||
|
|
||||||
|
widget->close();
|
||||||
|
Engine::destroySingleton();
|
||||||
|
}
|
||||||
|
|
||||||
static int getMarkdownFlushIterations() {
|
static int getMarkdownFlushIterations() {
|
||||||
if ( const char* env = std::getenv( "EE_MARKDOWN_BENCH_FLUSH_ITERATIONS" ) ) {
|
if ( const char* env = std::getenv( "EE_MARKDOWN_BENCH_FLUSH_ITERATIONS" ) ) {
|
||||||
Int32 val = markdownFlushIterations;
|
Int32 val = markdownFlushIterations;
|
||||||
|
|||||||
@@ -168,8 +168,12 @@ void StyleSheetSelectorRule::parseFragment( const std::string& selectorFragment
|
|||||||
attr.name = String::trim( buffer );
|
attr.name = String::trim( buffer );
|
||||||
attr.op = AttributeOperator::None;
|
attr.op = AttributeOperator::None;
|
||||||
}
|
}
|
||||||
|
attr.isDataAttribute = isDataAttributeName( attr.name );
|
||||||
|
if ( !attr.isDataAttribute )
|
||||||
|
attr.propertyDefinition =
|
||||||
|
StyleSheetSpecification::instance()->getProperty( String::hash( attr.name ) );
|
||||||
|
|
||||||
mAttributeSelectors.push_back( attr );
|
mAttributeSelectors.emplace_back( std::move( attr ) );
|
||||||
mSpecificity += SpecificityClass;
|
mSpecificity += SpecificityClass;
|
||||||
buffer.clear();
|
buffer.clear();
|
||||||
return;
|
return;
|
||||||
@@ -356,57 +360,57 @@ bool StyleSheetSelectorRule::matches( UIWidget* element, const bool& applyPseudo
|
|||||||
|
|
||||||
if ( !mAttributeSelectors.empty() ) {
|
if ( !mAttributeSelectors.empty() ) {
|
||||||
for ( const auto& attr : mAttributeSelectors ) {
|
for ( const auto& attr : mAttributeSelectors ) {
|
||||||
bool attrExists = false;
|
const std::string* elVal;
|
||||||
std::string elValStorage;
|
std::string elValStorage;
|
||||||
const std::string* elVal = &elValStorage;
|
|
||||||
|
|
||||||
if ( element->isType( UI_TYPE_HTML_WIDGET ) && isDataAttributeName( attr.name ) ) {
|
if ( attr.isDataAttribute && element->isType( UI_TYPE_HTML_WIDGET ) ) {
|
||||||
auto* htmlElement = element->asType<UIHTMLWidget>();
|
auto* htmlElement = element->asType<UIHTMLWidget>();
|
||||||
const auto* property = htmlElement->getDataProperty( attr.name );
|
const auto* property = htmlElement->getDataProperty( attr.name );
|
||||||
attrExists = property != nullptr;
|
if ( property == nullptr )
|
||||||
if ( attrExists )
|
return false;
|
||||||
elVal = &property->value();
|
if ( attr.op == AttributeOperator::None )
|
||||||
|
continue;
|
||||||
|
elVal = &property->value();
|
||||||
} else {
|
} else {
|
||||||
elValStorage = element->getPropertyString( attr.name );
|
elValStorage = element->getPropertyString( attr.propertyDefinition );
|
||||||
attrExists = !elValStorage.empty();
|
if ( elValStorage.empty() )
|
||||||
|
return false;
|
||||||
|
if ( attr.op == AttributeOperator::None )
|
||||||
|
continue;
|
||||||
|
elVal = &elValStorage;
|
||||||
}
|
}
|
||||||
|
|
||||||
if ( !attrExists )
|
switch ( attr.op ) {
|
||||||
return false;
|
case AttributeOperator::Exact: // =
|
||||||
|
if ( *elVal != attr.value )
|
||||||
if ( attr.op != AttributeOperator::None ) {
|
return false;
|
||||||
switch ( attr.op ) {
|
break;
|
||||||
case AttributeOperator::Exact: // =
|
case AttributeOperator::StartsWith: // ^=
|
||||||
if ( *elVal != attr.value )
|
if ( !String::startsWith( *elVal, attr.value ) )
|
||||||
return false;
|
return false;
|
||||||
break;
|
break;
|
||||||
case AttributeOperator::StartsWith: // ^=
|
case AttributeOperator::EndsWith: // $=
|
||||||
if ( !String::startsWith( *elVal, attr.value ) )
|
if ( !String::endsWith( *elVal, attr.value ) )
|
||||||
return false;
|
return false;
|
||||||
break;
|
break;
|
||||||
case AttributeOperator::EndsWith: // $=
|
case AttributeOperator::Contains: // *=
|
||||||
if ( !String::endsWith( *elVal, attr.value ) )
|
if ( elVal->find( attr.value ) == std::string::npos )
|
||||||
return false;
|
return false;
|
||||||
break;
|
break;
|
||||||
case AttributeOperator::Contains: // *=
|
case AttributeOperator::ContainsWord: { // ~= (Space-separated word check)
|
||||||
if ( elVal->find( attr.value ) == std::string::npos )
|
if ( !containsWord( *elVal, attr.value ) ) {
|
||||||
return false;
|
return false;
|
||||||
break;
|
|
||||||
case AttributeOperator::ContainsWord: { // ~= (Space-separated word check)
|
|
||||||
if ( !containsWord( *elVal, attr.value ) ) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
case AttributeOperator::StartsWithDash: // |= (Exact match or starts with value
|
break;
|
||||||
// + "-")
|
|
||||||
if ( !startsWithDashMatch( *elVal, attr.value ) ) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
}
|
||||||
|
case AttributeOperator::StartsWithDash: // |= (Exact match or starts with value
|
||||||
|
// + "-")
|
||||||
|
if ( !startsWithDashMatch( *elVal, attr.value ) ) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1552,6 +1552,10 @@ UTEST( UIHTML, DataProperties ) {
|
|||||||
EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-id*=\"ser\"]" ).size(), (size_t)1 );
|
EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-id*=\"ser\"]" ).size(), (size_t)1 );
|
||||||
EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-empty]" ).size(), (size_t)1 );
|
EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-empty]" ).size(), (size_t)1 );
|
||||||
EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-missing]" ).size(), (size_t)0 );
|
EXPECT_EQ( sceneNode->getRoot()->querySelectorAll( "[data-missing]" ).size(), (size_t)0 );
|
||||||
|
EXPECT_TRUE( StyleSheetSelector( "[width]" ).select( target, false ) );
|
||||||
|
EXPECT_TRUE( StyleSheetSelector( "[width=\"" + target->getPropertyString( "width" ) + "\"]" )
|
||||||
|
.select( target, false ) );
|
||||||
|
EXPECT_FALSE( StyleSheetSelector( "[unknown-property]" ).select( target, false ) );
|
||||||
|
|
||||||
EXPECT_TRUE( target->getDataPropertyString( "data-language" ) == "cpp" );
|
EXPECT_TRUE( target->getDataPropertyString( "data-language" ) == "cpp" );
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user