style: Implement selector matching for :nth-child(An+B of selector list) and :nth-last-child(An+B of selector list)

Since we have been using a single hash map to cache all :nth-child
indices (with no selector list), each different selector will need its
own cache.

As a side note, this patch does not address invalidation.

Differential Revision: https://phabricator.services.mozilla.com/D166266
This commit is contained in:
Zach Hoffman 2023-01-16 11:26:41 +00:00 committed by Martin Robinson
parent a973371cf2
commit 3076481c52
8 changed files with 223 additions and 71 deletions

View file

@ -4,8 +4,8 @@
use crate::attr::CaseSensitivity;
use crate::bloom::BloomFilter;
use crate::nth_index_cache::NthIndexCache;
use crate::parser::SelectorImpl;
use crate::nth_index_cache::{NthIndexCache, NthIndexCacheInner};
use crate::parser::{Selector, SelectorImpl};
use crate::tree::{Element, OpaqueElement};
/// What kind of selector matching mode we should use.
@ -110,8 +110,8 @@ where
matching_mode: MatchingMode,
/// Input with the bloom filter used to fast-reject selectors.
pub bloom_filter: Option<&'a BloomFilter>,
/// An optional cache to speed up nth-index-like selectors.
pub nth_index_cache: Option<&'a mut NthIndexCache>,
/// A cache to speed up nth-index-like selectors.
pub nth_index_cache: &'a mut NthIndexCache,
/// The element which is going to match :scope pseudo-class. It can be
/// either one :scope element, or the scoping element.
///
@ -161,7 +161,7 @@ where
pub fn new(
matching_mode: MatchingMode,
bloom_filter: Option<&'a BloomFilter>,
nth_index_cache: Option<&'a mut NthIndexCache>,
nth_index_cache: &'a mut NthIndexCache,
quirks_mode: QuirksMode,
needs_selector_flags: NeedsSelectorFlags,
) -> Self {
@ -179,7 +179,7 @@ where
pub fn new_for_visited(
matching_mode: MatchingMode,
bloom_filter: Option<&'a BloomFilter>,
nth_index_cache: Option<&'a mut NthIndexCache>,
nth_index_cache: &'a mut NthIndexCache,
visited_handling: VisitedHandlingMode,
quirks_mode: QuirksMode,
needs_selector_flags: NeedsSelectorFlags,
@ -202,6 +202,17 @@ where
}
}
// Grab a reference to the appropriate cache.
#[inline]
pub fn nth_index_cache(
&mut self,
is_of_type: bool,
is_from_end: bool,
selectors: &[Selector<Impl>],
) -> &mut NthIndexCacheInner {
self.nth_index_cache.get(is_of_type, is_from_end, selectors)
}
/// Whether we're matching a nested selector.
#[inline]
pub fn is_nested(&self) -> bool {

View file

@ -7,7 +7,6 @@ use crate::attr::{
ParsedCaseSensitivity,
};
use crate::bloom::{BloomFilter, BLOOM_HASH_MASK};
use crate::nth_index_cache::NthIndexCacheInner;
use crate::parser::{AncestorHashes, Combinator, Component, LocalName, NthSelectorData};
use crate::parser::{NonTSPseudoClass, Selector, SelectorImpl, SelectorIter, SelectorList};
use crate::tree::Element;
@ -328,6 +327,21 @@ where
matches!(result, SelectorMatchingResult::Matched)
}
/// Matches each selector of a list as a complex selector
#[inline(always)]
pub fn list_matches_complex_selector<E: Element>(
list: &[Selector<E::Impl>],
element: &E,
context: &mut MatchingContext<E::Impl>,
) -> bool {
for selector in list {
if matches_complex_selector(selector.iter(), element, context) {
return true;
}
}
false
}
/// Traverse all descendents of the given element and return true as soon as any of them match
/// the given list of selectors.
fn has_children_matching<E: Element>(
@ -802,12 +816,14 @@ where
None => element.is_root(),
},
Component::Nth(ref nth_data) => {
matches_generic_nth_child(element, context.shared, nth_data)
},
Component::NthOf(ref nth_of_data) => {
// TODO(zrhoffman, bug 1808228): Use selectors() when matching
matches_generic_nth_child(element, context.shared, nth_of_data.nth_data())
matches_generic_nth_child(element, context.shared, nth_data, &[])
},
Component::NthOf(ref nth_of_data) => matches_generic_nth_child(
element,
context.shared,
nth_of_data.nth_data(),
nth_of_data.selectors(),
),
Component::Is(ref list) | Component::Where(ref list) => context.shared.nest(|context| {
for selector in &**list {
if matches_complex_selector(selector.iter(), element, context) {
@ -870,6 +886,7 @@ fn matches_generic_nth_child<E>(
element: &E,
context: &mut MatchingContext<E::Impl>,
nth_data: &NthSelectorData,
selectors: &[Selector<E::Impl>],
) -> bool
where
E: Element,
@ -877,19 +894,44 @@ where
if element.ignores_nth_child_selectors() {
return false;
}
/*
* This condition could be bound as element_matches_selectors and used in
* nth_child_index() as element_matches_selectors &&
* list_matches_complex_selector(selectors, &curr, context)
* , but since element_matches_selectors would need still need to be
* computed in that case in order to utilize the cache, there would be no
* performance benefit for building up nth-{,last-}child(.. of ..) caches
* where the element does not match the selector list.
*/
if !selectors.is_empty() && !list_matches_complex_selector(selectors, element, context) {
return false;
}
let NthSelectorData { ty, a, b, .. } = *nth_data;
let is_of_type = ty.is_of_type();
if ty.is_only() {
return matches_generic_nth_child(element, context, &NthSelectorData::first(is_of_type)) &&
matches_generic_nth_child(element, context, &NthSelectorData::last(is_of_type));
debug_assert!(
selectors.is_empty(),
":only-child and :only-of-type cannot have a selector list!"
);
return matches_generic_nth_child(
element,
context,
&NthSelectorData::first(is_of_type),
selectors,
) && matches_generic_nth_child(
element,
context,
&NthSelectorData::last(is_of_type),
selectors,
);
}
let is_from_end = ty.is_from_end();
// It's useful to know whether this can only select the first/last element
// child for optimization purposes, see the `HAS_EDGE_CHILD_SELECTOR` flag.
let is_edge_child_selector = a == 0 && b == 1 && !is_of_type;
let is_edge_child_selector = a == 0 && b == 1 && !is_of_type && selectors.is_empty();
if context.needs_selector_flags() {
element.apply_selector_flags(if is_edge_child_selector {
@ -912,25 +954,36 @@ where
.is_none();
}
// Grab a reference to the appropriate cache.
let mut cache = context
.nth_index_cache
.as_mut()
.map(|c| c.get(is_of_type, is_from_end));
// Lookup or compute the index.
let index = if let Some(i) = cache.as_mut().and_then(|c| c.lookup(element.opaque())) {
let index = if let Some(i) = context
.nth_index_cache(is_of_type, is_from_end, selectors)
.lookup(element.opaque())
{
i
} else {
let i = nth_child_index(element, is_of_type, is_from_end, cache.as_deref_mut());
if let Some(c) = cache.as_mut() {
c.insert(element.opaque(), i)
}
let i = nth_child_index(
element,
context,
selectors,
is_of_type,
is_from_end,
/* check_cache = */ true,
);
context
.nth_index_cache(is_of_type, is_from_end, selectors)
.insert(element.opaque(), i);
i
};
debug_assert_eq!(
index,
nth_child_index(element, is_of_type, is_from_end, None),
nth_child_index(
element,
context,
selectors,
is_of_type,
is_from_end,
/* check_cache = */ false
),
"invalid cache"
);
@ -947,9 +1000,11 @@ where
#[inline]
fn nth_child_index<E>(
element: &E,
context: &mut MatchingContext<E::Impl>,
selectors: &[Selector<E::Impl>],
is_of_type: bool,
is_from_end: bool,
mut cache: Option<&mut NthIndexCacheInner>,
check_cache: bool,
) -> i32
where
E: Element,
@ -960,19 +1015,33 @@ where
// siblings to the left checking the cache in the is_from_end case (this
// matches what Gecko does). The indices-from-the-left is handled during the
// regular look further below.
if let Some(ref mut c) = cache {
if is_from_end && !c.is_empty() {
let mut index: i32 = 1;
let mut curr = element.clone();
while let Some(e) = curr.prev_sibling_element() {
curr = e;
if !is_of_type || element.is_same_type(&curr) {
if let Some(i) = c.lookup(curr.opaque()) {
return i - index;
}
index += 1;
}
if check_cache &&
is_from_end &&
!context
.nth_index_cache(is_of_type, is_from_end, selectors)
.is_empty()
{
let mut index: i32 = 1;
let mut curr = element.clone();
while let Some(e) = curr.prev_sibling_element() {
curr = e;
let matches = if is_of_type {
element.is_same_type(&curr)
} else if !selectors.is_empty() {
list_matches_complex_selector(selectors, &curr, context)
} else {
true
};
if !matches {
continue;
}
if let Some(i) = context
.nth_index_cache(is_of_type, is_from_end, selectors)
.lookup(curr.opaque())
{
return i - index;
}
index += 1;
}
}
@ -987,17 +1056,28 @@ where
};
while let Some(e) = next(curr) {
curr = e;
if !is_of_type || element.is_same_type(&curr) {
// If we're computing indices from the left, check each element in the
// cache. We handle the indices-from-the-right case at the top of this
// function.
if !is_from_end {
if let Some(i) = cache.as_mut().and_then(|c| c.lookup(curr.opaque())) {
return i + index;
}
}
index += 1;
let matches = if is_of_type {
element.is_same_type(&curr)
} else if !selectors.is_empty() {
list_matches_complex_selector(selectors, &curr, context)
} else {
true
};
if !matches {
continue;
}
// If we're computing indices from the left, check each element in the
// cache. We handle the indices-from-the-right case at the top of this
// function.
if !is_from_end && check_cache {
if let Some(i) = context
.nth_index_cache(is_of_type, is_from_end, selectors)
.lookup(curr.opaque())
{
return i + index;
}
}
index += 1;
}
index

View file

@ -2,7 +2,9 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use crate::tree::OpaqueElement;
use std::hash::Hash;
use crate::{parser::Selector, tree::OpaqueElement, SelectorImpl};
use fxhash::FxHashMap;
/// A cache to speed up matching of nth-index-like selectors.
@ -13,20 +15,68 @@ use fxhash::FxHashMap;
#[derive(Default)]
pub struct NthIndexCache {
nth: NthIndexCacheInner,
nth_of_selectors: NthIndexOfSelectorsCaches,
nth_last: NthIndexCacheInner,
nth_last_of_selectors: NthIndexOfSelectorsCaches,
nth_of_type: NthIndexCacheInner,
nth_last_of_type: NthIndexCacheInner,
}
impl NthIndexCache {
/// Gets the appropriate cache for the given parameters.
pub fn get(&mut self, is_of_type: bool, is_from_end: bool) -> &mut NthIndexCacheInner {
match (is_of_type, is_from_end) {
(false, false) => &mut self.nth,
(false, true) => &mut self.nth_last,
(true, false) => &mut self.nth_of_type,
(true, true) => &mut self.nth_last_of_type,
pub fn get<Impl: SelectorImpl>(
&mut self,
is_of_type: bool,
is_from_end: bool,
selectors: &[Selector<Impl>],
) -> &mut NthIndexCacheInner {
if is_of_type {
return if is_from_end {
&mut self.nth_last_of_type
} else {
&mut self.nth_of_type
};
}
if !selectors.is_empty() {
return if is_from_end {
self.nth_last_of_selectors.lookup(selectors)
} else {
self.nth_of_selectors.lookup(selectors)
};
}
if is_from_end {
&mut self.nth_last
} else {
&mut self.nth
}
}
}
#[derive(Hash, Eq, PartialEq)]
struct SelectorListCacheKey(usize);
/// Use the selector list's pointer as the cache key
impl SelectorListCacheKey {
// :nth-child of selectors are reference-counted with `ThinArc`, so we know their pointers are stable.
fn new<Impl: SelectorImpl>(selectors: &[Selector<Impl>]) -> Self {
Self(selectors.as_ptr() as usize)
}
}
/// Use a different map of cached indices per :nth-child's or :nth-last-child's selector list
#[derive(Default)]
pub struct NthIndexOfSelectorsCaches(FxHashMap<SelectorListCacheKey, NthIndexCacheInner>);
/// Get or insert a map of cached incides for the selector list of this
/// particular :nth-child or :nth-last-child pseudoclass
impl NthIndexOfSelectorsCaches {
pub fn lookup<Impl: SelectorImpl>(
&mut self,
selectors: &[Selector<Impl>],
) -> &mut NthIndexCacheInner {
self.0
.entry(SelectorListCacheKey::new(selectors))
.or_default()
}
}

View file

@ -14,7 +14,7 @@ use crate::values::AtomIdent;
use selectors::attr::CaseSensitivity;
use selectors::matching::{self, MatchingContext, MatchingMode, NeedsSelectorFlags};
use selectors::parser::{Combinator, Component, LocalName, SelectorImpl};
use selectors::{Element, NthIndexCache, SelectorList};
use selectors::{Element, SelectorList};
use smallvec::SmallVec;
/// <https://dom.spec.whatwg.org/#dom-element-matches>
@ -26,10 +26,12 @@ pub fn element_matches<E>(
where
E: Element,
{
let mut nth_index_cache = Default::default();
let mut context = MatchingContext::new(
MatchingMode::Normal,
None,
None,
&mut nth_index_cache,
quirks_mode,
NeedsSelectorFlags::No,
);
@ -47,12 +49,12 @@ pub fn element_closest<E>(
where
E: Element,
{
let mut nth_index_cache = NthIndexCache::default();
let mut nth_index_cache = Default::default();
let mut context = MatchingContext::new(
MatchingMode::Normal,
None,
Some(&mut nth_index_cache),
&mut nth_index_cache,
quirks_mode,
NeedsSelectorFlags::No,
);
@ -621,13 +623,13 @@ pub fn query_selector<E, Q>(
{
use crate::invalidation::element::invalidator::TreeStyleInvalidator;
let mut nth_index_cache = Default::default();
let quirks_mode = root.owner_doc().quirks_mode();
let mut nth_index_cache = NthIndexCache::default();
let mut matching_context = MatchingContext::new(
MatchingMode::Normal,
None,
Some(&mut nth_index_cache),
&mut nth_index_cache,
quirks_mode,
NeedsSelectorFlags::No,
);

View file

@ -13,6 +13,7 @@ use crate::stylist::CascadeData;
use selectors::matching::{
MatchingContext, MatchingMode, NeedsSelectorFlags, QuirksMode, VisitedHandlingMode,
};
use selectors::NthIndexCache;
use style_traits::dom::DocumentState;
/// A struct holding the members necessary to invalidate document state
@ -43,11 +44,16 @@ pub struct DocumentStateInvalidationProcessor<'a, E: TElement, I> {
impl<'a, E: TElement, I> DocumentStateInvalidationProcessor<'a, E, I> {
/// Creates a new DocumentStateInvalidationProcessor.
#[inline]
pub fn new(rules: I, document_states_changed: DocumentState, quirks_mode: QuirksMode) -> Self {
pub fn new(
rules: I,
document_states_changed: DocumentState,
nth_index_cache: &'a mut NthIndexCache,
quirks_mode: QuirksMode,
) -> Self {
let mut matching_context = MatchingContext::<'a, E::Impl>::new_for_visited(
MatchingMode::Normal,
None,
None,
nth_index_cache,
VisitedHandlingMode::AllLinksVisitedAndUnvisited,
quirks_mode,
NeedsSelectorFlags::No,

View file

@ -65,7 +65,7 @@ impl<'a, 'b: 'a, E: TElement + 'b> StateAndAttrInvalidationProcessor<'a, 'b, E>
let matching_context = MatchingContext::new_for_visited(
MatchingMode::Normal,
None,
Some(nth_index_cache),
nth_index_cache,
VisitedHandlingMode::AllLinksVisitedAndUnvisited,
shared_context.quirks_mode(),
NeedsSelectorFlags::No,

View file

@ -462,7 +462,7 @@ where
let mut matching_context = MatchingContext::new_for_visited(
MatchingMode::Normal,
Some(bloom_filter),
Some(nth_index_cache),
nth_index_cache,
visited_handling,
self.context.shared.quirks_mode(),
NeedsSelectorFlags::Yes,
@ -538,7 +538,7 @@ where
let mut matching_context = MatchingContext::<'_, E::Impl>::new_for_visited(
MatchingMode::ForStatelessPseudoElement,
Some(bloom_filter),
Some(nth_index_cache),
nth_index_cache,
visited_handling,
self.context.shared.quirks_mode(),
NeedsSelectorFlags::Yes,

View file

@ -1125,6 +1125,7 @@ impl Stylist {
{
debug_assert!(pseudo.is_lazy());
let mut nth_index_cache = Default::default();
// No need to bother setting the selector flags when we're computing
// default styles.
let needs_selector_flags = if rule_inclusion == RuleInclusion::DefaultOnly {
@ -1137,7 +1138,7 @@ impl Stylist {
let mut matching_context = MatchingContext::<'_, E::Impl>::new(
MatchingMode::ForStatelessPseudoElement,
None,
None,
&mut nth_index_cache,
self.quirks_mode,
needs_selector_flags,
);
@ -1166,10 +1167,12 @@ impl Stylist {
let mut visited_rules = None;
if parent_style.visited_style().is_some() {
let mut declarations = ApplicableDeclarationList::new();
let mut nth_index_cache = Default::default();
let mut matching_context = MatchingContext::<'_, E::Impl>::new_for_visited(
MatchingMode::ForStatelessPseudoElement,
None,
None,
&mut nth_index_cache,
VisitedHandlingMode::RelevantLinkVisited,
self.quirks_mode,
needs_selector_flags,
@ -1417,7 +1420,7 @@ impl Stylist {
let mut matching_context = MatchingContext::new(
MatchingMode::Normal,
bloom,
Some(nth_index_cache),
nth_index_cache,
self.quirks_mode,
needs_selector_flags,
);