Skip to content

Commit

Permalink
Update style sheet index cache lookup for faster retrieval of element…
Browse files Browse the repository at this point in the history
… style definition, see #293.

- Particularly, performance improvements when using style rules with class names.
- Also fixes hash collisions in tag/id or node lists, previously they could result in the wrong styles being applied.
  • Loading branch information
mikke89 committed Apr 10, 2022
1 parent ba9a654 commit 30d7f60
Show file tree
Hide file tree
Showing 8 changed files with 140 additions and 104 deletions.
16 changes: 4 additions & 12 deletions Include/RmlUi/Core/StyleSheet.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,25 +29,22 @@
#ifndef RMLUI_CORE_STYLESHEET_H
#define RMLUI_CORE_STYLESHEET_H

#include "Traits.h"
#include "PropertyDictionary.h"
#include "Spritesheet.h"
#include "StyleSheetTypes.h"
#include "Traits.h"

namespace Rml {

class Element;
class ElementDefinition;
class StyleSheetNode;
class Decorator;
class FontEffect;
class SpritesheetList;
class Stream;
class StyleSheetContainer;
class StyleSheetParser;
struct PropertySource;
struct Sprite;
struct Spritesheet;

/**
StyleSheet maintains a single stylesheet definition. A stylesheet can be combined with another stylesheet to create
Expand All @@ -61,9 +58,6 @@ class RMLUICORE_API StyleSheet final : public NonCopyMoveable
public:
~StyleSheet();

using NodeList = Vector<const StyleSheetNode*>;
using NodeIndex = UnorderedMap<size_t, NodeList>;

/// Combines this style sheet with another one, producing a new sheet.
UniquePtr<StyleSheet> CombineStyleSheet(const StyleSheet& sheet) const;
/// Merges another style sheet into this.
Expand All @@ -83,9 +77,6 @@ class RMLUICORE_API StyleSheet final : public NonCopyMoveable
/// Returns the compiled element definition for a given element and its hierarchy.
SharedPtr<const ElementDefinition> GetElementDefinition(const Element* element) const;

/// Retrieve the hash key used to look-up applicable nodes in the node index.
static size_t NodeHash(const String& tag, const String& id);

/// Returns a list of instanced decorators from the declarations. The instances are cached for faster future retrieval.
const Vector<SharedPtr<const Decorator>>& InstanceDecorators(const DecoratorDeclarationList& declaration_list, const PropertySource* decorator_source) const;

Expand All @@ -112,10 +103,10 @@ class RMLUICORE_API StyleSheet final : public NonCopyMoveable
SpritesheetList spritesheet_list;

// Map of all styled nodes, that is, they have one or more properties.
NodeIndex styled_node_index;
StyleSheetIndex styled_node_index;

// Index of node sets to element definitions.
using ElementDefinitionCache = UnorderedMap<size_t, SharedPtr<const ElementDefinition>>;
using ElementDefinitionCache = UnorderedMap<StyleSheetIndex::NodeList, SharedPtr<const ElementDefinition>>;
mutable ElementDefinitionCache node_cache;

// Cached decorator instances.
Expand All @@ -127,4 +118,5 @@ class RMLUICORE_API StyleSheet final : public NonCopyMoveable
};

} // namespace Rml

#endif
38 changes: 34 additions & 4 deletions Include/RmlUi/Core/StyleSheetTypes.h
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
Expand All @@ -29,18 +29,20 @@
#ifndef RMLUI_CORE_STYLESHEETTYPES_H
#define RMLUI_CORE_STYLESHEETTYPES_H

#include "Types.h"
#include "PropertyDictionary.h"
#include "Types.h"
#include "Utilities.h"

namespace Rml {

class Decorator;
class DecoratorInstancer;
class StyleSheet;
class StyleSheetNode;

struct KeyframeBlock {
KeyframeBlock(float normalized_time) : normalized_time(normalized_time) {}
float normalized_time; // [0, 1]
float normalized_time; // [0, 1]
PropertyDictionary properties;
};
struct Keyframes {
Expand Down Expand Up @@ -68,12 +70,40 @@ struct DecoratorDeclarationList {

struct MediaBlock {
MediaBlock() {}
MediaBlock(PropertyDictionary _properties, SharedPtr<StyleSheet> _stylesheet) : properties(std::move(_properties)), stylesheet(std::move(_stylesheet)) {}
MediaBlock(PropertyDictionary _properties, SharedPtr<StyleSheet> _stylesheet) :
properties(std::move(_properties)), stylesheet(std::move(_stylesheet))
{}

PropertyDictionary properties; // Media query properties
SharedPtr<StyleSheet> stylesheet;
};
using MediaBlockList = Vector<MediaBlock>;

/**
StyleSheetIndex contains a cached index of all styled nodes for quick lookup when finding applicable style nodes for the current state of a given
element.
*/
struct StyleSheetIndex {
using NodeList = Vector<const StyleSheetNode*>;
using NodeIndex = UnorderedMap<std::size_t, NodeList>;

// The following objects are given in prioritized order. Any nodes in the first object will not be contained in the next one and so on.
NodeIndex ids, classes, tags;
NodeList other;
};
} // namespace Rml

namespace std {
// Hash specialization for the node list, so it can be used as key in UnorderedMap.
template <>
struct hash<::Rml::StyleSheetIndex::NodeList> {
std::size_t operator()(const ::Rml::StyleSheetIndex::NodeList& nodes) const
{
std::size_t seed = 0;
for (const ::Rml::StyleSheetNode* node : nodes)
::Rml::Utilities::HashCombine(seed, node);
return seed;
}
};
} // namespace std
#endif
7 changes: 4 additions & 3 deletions Source/Core/Element.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@
#include "../../Include/RmlUi/Core/PropertyIdSet.h"
#include "../../Include/RmlUi/Core/PropertiesIteratorView.h"
#include "../../Include/RmlUi/Core/PropertyDefinition.h"
#include "../../Include/RmlUi/Core/StyleSheet.h"
#include "../../Include/RmlUi/Core/StyleSheetSpecification.h"
#include "../../Include/RmlUi/Core/TransformPrimitive.h"
#include "Clock.h"
Expand Down Expand Up @@ -1048,7 +1049,7 @@ Element* Element::Closest(const String& selectors) const
{
for (const StyleSheetNode* node : leaf_nodes)
{
if (node->IsApplicable(parent, false))
if (node->IsApplicable(parent))
{
return parent;
}
Expand Down Expand Up @@ -1512,7 +1513,7 @@ static Element* QuerySelectorMatchRecursive(const StyleSheetNodeListRaw& nodes,

for (const StyleSheetNode* node : nodes)
{
if (node->IsApplicable(child, false))
if (node->IsApplicable(child))
return child;
}

Expand All @@ -1534,7 +1535,7 @@ static void QuerySelectorAllMatchRecursive(ElementList& matching_elements, const

for (const StyleSheetNode* node : nodes)
{
if (node->IsApplicable(child, false))
if (node->IsApplicable(child))
{
matching_elements.push_back(child);
break;
Expand Down
5 changes: 5 additions & 0 deletions Source/Core/ElementStyle.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -319,6 +319,11 @@ String ElementStyle::GetClassNames() const
return class_names;
}

const StringList& ElementStyle::GetClassNameList() const
{
return classes;
}

// Sets a local property override on the element to a pre-parsed value.
bool ElementStyle::SetProperty(PropertyId id, const Property& property)
{
Expand Down
2 changes: 2 additions & 0 deletions Source/Core/ElementStyle.h
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,8 @@ class ElementStyle
/// Return the active class list.
/// @return A string containing all the classes on the element, separated by spaces.
String GetClassNames() const;
/// Return the active class list.
const StringList& GetClassNameList() const;

/// Sets a local property override on the element to a pre-parsed value.
/// @param[in] name The name of the new property.
Expand Down
110 changes: 45 additions & 65 deletions Source/Core/StyleSheet.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,19 +32,13 @@
#include "../../Include/RmlUi/Core/Profiling.h"
#include "../../Include/RmlUi/Core/PropertyDefinition.h"
#include "../../Include/RmlUi/Core/StyleSheetSpecification.h"
#include "../../Include/RmlUi/Core/Utilities.h"
#include "ElementDefinition.h"
#include "ElementStyle.h"
#include "StyleSheetNode.h"
#include <algorithm>

namespace Rml {

// Sorts style nodes based on specificity.
inline static bool StyleSheetNodeSort(const StyleSheetNode* lhs, const StyleSheetNode* rhs)
{
return lhs->GetSpecificity() < rhs->GetSpecificity();
}

StyleSheet::StyleSheet()
{
root = MakeUnique<StyleSheetNode>();
Expand Down Expand Up @@ -105,7 +99,7 @@ void StyleSheet::MergeStyleSheet(const StyleSheet& other_sheet)
void StyleSheet::BuildNodeIndex()
{
RMLUI_ZoneScoped;
styled_node_index.clear();
styled_node_index = {};
root->BuildIndex(styled_node_index);
root->SetStructurallyVolatileRecursive(false);
}
Expand Down Expand Up @@ -170,89 +164,75 @@ const Sprite* StyleSheet::GetSprite(const String& name) const
return spritesheet_list.GetSprite(name);
}

size_t StyleSheet::NodeHash(const String& tag, const String& id)
{
size_t seed = 0;
if (!tag.empty())
seed = Hash<String>()(tag);
if(!id.empty())
Utilities::HashCombine(seed, id);
return seed;
}

// Returns the compiled element definition for a given element hierarchy.
SharedPtr<const ElementDefinition> StyleSheet::GetElementDefinition(const Element* element) const
{
RMLUI_ASSERT_NONRECURSIVE;

// See if there are any styles defined for this element.
// Using static to avoid allocations. Make sure we don't call this function recursively.
static Vector< const StyleSheetNode* > applicable_nodes;
applicable_nodes.clear();

const String& tag = element->GetTagName();
const String& id = element->GetId();

// The styled_node_index is hashed with the tag and id of the RCSS rule. However, we must also check
// the rules which don't have them defined, because they apply regardless of tag and id.
Array<size_t, 4> node_hash;
int num_hashes = 2;

node_hash[0] = 0;
node_hash[1] = NodeHash(tag, String());

// If we don't have an id, we can safely skip nodes that define an id. Otherwise, we also check the id nodes.
if (!id.empty())
{
num_hashes = 4;
node_hash[2] = NodeHash(String(), id);
node_hash[3] = NodeHash(tag, id);
}

// The hashes are keys into a set of applicable nodes (given tag and id).
for (int i = 0; i < num_hashes; i++)
{
auto it_nodes = styled_node_index.find(node_hash[i]);
if (it_nodes != styled_node_index.end())
auto AddApplicableNodes = [element](const StyleSheetIndex::NodeIndex& node_index, const String& key) {
auto it_nodes = node_index.find(Hash<String>()(key));
if (it_nodes != node_index.end())
{
const NodeList& nodes = it_nodes->second;
const StyleSheetIndex::NodeList& nodes = it_nodes->second;

// Now see if we satisfy all of the requirements not yet tested: classes, pseudo classes, structural selectors,
// and the full requirements of parent nodes. What this involves is traversing the style nodes backwards,
// trying to match nodes in the element's hierarchy to nodes in the style hierarchy.
for (const StyleSheetNode* node : nodes)
{
if (node->IsApplicable(element, true))
{
// We found a node that has at least one requirement matching the element. Now see if we satisfy the remaining requirements of the
// node, including all ancestor nodes. What this involves is traversing the style nodes backwards, trying to match nodes in the
// element's hierarchy to nodes in the style hierarchy.
if (node->IsApplicable(element))
applicable_nodes.push_back(node);
}
}
}
}
};

// See if there are any styles defined for this element.
const String& tag = element->GetTagName();
const String& id = element->GetId();
const StringList& class_names = element->GetStyle()->GetClassNameList();

// First, look up the indexed requirements.
if (!id.empty())
AddApplicableNodes(styled_node_index.ids, id);

std::sort(applicable_nodes.begin(), applicable_nodes.end(), StyleSheetNodeSort);
for (const String& name : class_names)
AddApplicableNodes(styled_node_index.classes, name);

AddApplicableNodes(styled_node_index.tags, tag);

// Also check all remaining nodes that don't contain any indexed requirements.
for (const StyleSheetNode* node : styled_node_index.other)
{
if (node->IsApplicable(element))
applicable_nodes.push_back(node);
}

// If this element definition won't actually store any information, don't bother with it.
if (applicable_nodes.empty())
return nullptr;

// Check if this puppy has already been cached in the node index.
size_t seed = 0;
for (const StyleSheetNode* node : applicable_nodes)
Utilities::HashCombine(seed, node);
// Sort the applicable nodes by specificity first, then by pointer value in case we have duplicate specificities.
std::sort(applicable_nodes.begin(), applicable_nodes.end(), [](const StyleSheetNode* a, const StyleSheetNode* b) {
const int a_specificity = a->GetSpecificity();
const int b_specificity = b->GetSpecificity();
if (a_specificity == b_specificity)
return a < b;
return a_specificity < b_specificity;
});

auto cache_iterator = node_cache.find(seed);
if (cache_iterator != node_cache.end())
// Check if this puppy has already been cached in the node index.
SharedPtr<const ElementDefinition>& definition = node_cache[applicable_nodes];
if (!definition)
{
SharedPtr<const ElementDefinition>& definition = (*cache_iterator).second;
return definition;
// Otherwise, create a new definition and add it to our cache.
definition = MakeShared<const ElementDefinition>(applicable_nodes);
}

// Create the new definition and add it to our cache.
auto new_definition = MakeShared<const ElementDefinition>(applicable_nodes);
node_cache[seed] = new_definition;

return new_definition;
return definition;
}

} // namespace Rml
Loading

0 comments on commit 30d7f60

Please sign in to comment.