Bug 1931432: Fix up how :empty, :first-child and :last-child invalidation are skipped. r=firefox-style-system-reviewers,emilio

Previous go at optimizing in Bug 1896380 was not quite correct.
For example, given `.anchor:has(:first-child)`, when `.anchor`
has a new child inserted, we need to be able to invalidate (Which
we did not).
Thankfully, we can reason that the previously-optimized pseudo-
classes, i.e. :empty, :first-child, :last-child, can be trivially
rejected from activating the invalidation machinery by by examining
its descendants/siblings - e.g. `:has(:first-child)` is irrelevant
to elements we know aren't in the first sibling position.

Differential Revision: https://phabricator.services.mozilla.com/D229670
This commit is contained in:
David Shin
2024-11-26 18:14:43 +00:00
parent 6b0226de96
commit 055e8eca9e
7 changed files with 167 additions and 25 deletions

View File

@@ -160,7 +160,7 @@ void RestyleManager::ContentAppended(nsIContent* aFirstNewContent) {
PostRestyleEvent(element, RestyleHint::RestyleSubtree(),
nsChangeHint(0));
StyleSet()->MaybeInvalidateRelativeSelectorForNthEdgeDependency(
*element);
*element, StyleRelativeSelectorNthEdgeInvalidateFor::Last);
break;
}
}
@@ -218,7 +218,7 @@ void RestyleManager::MaybeRestyleForEdgeChildChange(nsINode* aContainer,
PostRestyleEvent(element, RestyleHint::RestyleSubtree(),
nsChangeHint(0));
StyleSet()->MaybeInvalidateRelativeSelectorForNthEdgeDependency(
*element);
*element, StyleRelativeSelectorNthEdgeInvalidateFor::First);
}
break;
}
@@ -237,7 +237,7 @@ void RestyleManager::MaybeRestyleForEdgeChildChange(nsINode* aContainer,
PostRestyleEvent(element, RestyleHint::RestyleSubtree(),
nsChangeHint(0));
StyleSet()->MaybeInvalidateRelativeSelectorForNthEdgeDependency(
*element);
*element, StyleRelativeSelectorNthEdgeInvalidateFor::Last);
}
break;
}
@@ -541,7 +541,7 @@ void RestyleManager::ContentWillBeRemoved(nsIContent* aOldChild) {
PostRestyleEvent(element, RestyleHint::RestyleSubtree(),
nsChangeHint(0));
StyleSet()->MaybeInvalidateRelativeSelectorForNthEdgeDependency(
*element);
*element, StyleRelativeSelectorNthEdgeInvalidateFor::First);
}
break;
}
@@ -560,7 +560,7 @@ void RestyleManager::ContentWillBeRemoved(nsIContent* aOldChild) {
PostRestyleEvent(element, RestyleHint::RestyleSubtree(),
nsChangeHint(0));
StyleSet()->MaybeInvalidateRelativeSelectorForNthEdgeDependency(
*element);
*element, StyleRelativeSelectorNthEdgeInvalidateFor::Last);
}
break;
}

View File

@@ -1442,9 +1442,10 @@ void ServoStyleSet::MaybeInvalidateRelativeSelectorForEmptyDependency(
}
void ServoStyleSet::MaybeInvalidateRelativeSelectorForNthEdgeDependency(
const Element& aElement) {
const Element& aElement,
StyleRelativeSelectorNthEdgeInvalidateFor aInvalidateFor) {
Servo_StyleSet_MaybeInvalidateRelativeSelectorNthEdgeDependency(
mRawData.get(), &aElement);
mRawData.get(), &aElement, aInvalidateFor);
}
void ServoStyleSet::MaybeInvalidateRelativeSelectorForNthDependencyFromSibling(

View File

@@ -31,6 +31,7 @@ namespace mozilla {
enum class MediaFeatureChangeReason : uint8_t;
enum class StylePageSizeOrientation : uint8_t;
enum class StyleRuleChangeKind : uint32_t;
enum class StyleRelativeSelectorNthEdgeInvalidateFor : uint8_t;
class ErrorResult;
@@ -514,7 +515,8 @@ class ServoStyleSet {
* by a selector that can only selector first/last child, that
* might require us to restyle the relative selector it refers to.
*/
void MaybeInvalidateRelativeSelectorForNthEdgeDependency(const dom::Element&);
void MaybeInvalidateRelativeSelectorForNthEdgeDependency(
const dom::Element&, StyleRelativeSelectorNthEdgeInvalidateFor);
/**
* Maybe invalidate if a state change on an element that might be selected by

View File

@@ -291,25 +291,33 @@ pub struct InvalidationMap {
/// Tree-structural pseudoclasses that we care about for (Relative selector) invalidation.
/// Specifically, we need to store information on ones that don't generate the inner selector.
/// Given the nature of these selectors:
/// * These are only relevant during DOM mutation invalidations
/// * Some invalidations may be optimized away.
#[derive(Clone, Copy, Debug, MallocSizeOf)]
pub struct TSStateForInvalidation(u8);
bitflags! {
impl TSStateForInvalidation : u8 {
/// :empty
/// :empty. This only needs to be considered for DOM mutation, and for
/// elements that do not have any children.
const EMPTY = 1 << 0;
/// :nth and related selectors, without of.
const NTH = 1 << 1;
/// "Simple" edge child selectors, like :first-child, :last-child, etc.
/// Excludes :*-of-type as well as :only-child.
const NTH_EDGE = 1 << 2;
/// :first-child. This only needs to be considered for DOM mutation, and
/// for elements that have no previous sibling.
const NTH_EDGE_FIRST = 1 << 2;
/// :last-child. This only needs to be considered for DOM mutation,
/// and for elements have no next sibling.
const NTH_EDGE_LAST = 1 << 3;
}
}
impl TSStateForInvalidation {
/// Return true if this state invalidation should not result in a blanket
pub fn avoid_blanket_invalidation_on_dom_mutation(&self) -> bool {
(Self::EMPTY | Self::NTH_EDGE).contains(*self)
/// Return true if this state invalidation could be skipped (As per comment
/// in the definition of this bitflags)
pub fn may_be_optimized(&self) -> bool {
(Self::EMPTY | Self::NTH_EDGE_FIRST | Self::NTH_EDGE_LAST).contains(*self)
}
}
@@ -1166,7 +1174,11 @@ fn on_simple_selector<C: Collector>(
Component::Empty => Ok(ComponentVisitResult::Handled(TSStateForInvalidation::EMPTY)),
Component::Nth(data) => {
let kind = if data.is_simple_edge() {
TSStateForInvalidation::NTH_EDGE
if data.ty.is_from_end() {
TSStateForInvalidation::NTH_EDGE_LAST
} else {
TSStateForInvalidation::NTH_EDGE_FIRST
}
} else {
TSStateForInvalidation::NTH
};

View File

@@ -11,7 +11,7 @@ use crate::gecko_bindings::structs::ServoElementSnapshotTable;
use crate::invalidation::element::element_wrapper::ElementWrapper;
use crate::invalidation::element::invalidation_map::{
Dependency, DependencyInvalidationKind, NormalDependencyInvalidationKind,
RelativeDependencyInvalidationKind, RelativeSelectorInvalidationMap,
RelativeDependencyInvalidationKind, RelativeSelectorInvalidationMap, TSStateForInvalidation,
};
use crate::invalidation::element::invalidator::{
DescendantInvalidationLists, Invalidation, InvalidationProcessor, InvalidationResult,
@@ -515,14 +515,46 @@ where
if !operation.accept(&dependency.dep, element) {
return true;
}
// This section contain potential optimization for not running full invalidation -
// consult documentation in `TSStateForInvalidation`.
if dependency.state.may_be_optimized() {
debug_assert!(
self.optimization_context.is_some(),
"Optimization context not available for DOM mutation?"
);
if operation.is_side_effect() {
// Side effect operations act on element not being mutated, so they can't
// change the match outcome of these optimizable pseudoclasses.
return true;
}
if dependency.state.contains(TSStateForInvalidation::EMPTY) &&
element.first_element_child().is_some()
{
return true;
}
let sibling_traversal_map = self
.optimization_context
.as_ref()
.unwrap()
.sibling_traversal_map;
if dependency
.state
.avoid_blanket_invalidation_on_dom_mutation()
.contains(TSStateForInvalidation::NTH_EDGE_FIRST) &&
sibling_traversal_map.prev_sibling_for(&element).is_some()
{
// We assume here that these dependencies are handled elsewhere,
// in a more constrained manner.
return true;
}
if dependency
.state
.contains(TSStateForInvalidation::NTH_EDGE_LAST) &&
sibling_traversal_map.next_sibling_for(&element).is_some()
{
return true;
}
}
self.add_dependency(&dependency.dep, element, scope);
true
},

View File

@@ -7497,15 +7497,26 @@ pub extern "C" fn Servo_StyleSet_MaybeInvalidateRelativeSelectorEmptyDependency(
);
}
/// Which edge side should the invalidation run for?
#[repr(u8)]
pub enum RelativeSelectorNthEdgeInvalidateFor {
First,
Last
}
#[no_mangle]
pub extern "C" fn Servo_StyleSet_MaybeInvalidateRelativeSelectorNthEdgeDependency(
raw_data: &PerDocumentStyleData,
element: &RawGeckoElement,
invalidate_for: RelativeSelectorNthEdgeInvalidateFor,
) {
invalidate_relative_selector_ts_dependency(
&raw_data.borrow().stylist,
GeckoElement(element),
TSStateForInvalidation::NTH_EDGE,
match invalidate_for {
RelativeSelectorNthEdgeInvalidateFor::First => TSStateForInvalidation::NTH_EDGE_FIRST,
RelativeSelectorNthEdgeInvalidateFor::Last => TSStateForInvalidation::NTH_EDGE_LAST,
},
);
}

View File

@@ -0,0 +1,84 @@
<!DOCTYPE html>
<title>CSS Selectors Invalidation: :has() containing :empty, :first-child, :last-child, pseudoclasses only</title>
<link rel="author" title="David Shin" href="mailto:dshin@mozilla.com">
<link rel="help" href="https://drafts.csswg.org/selectors/#relational">
<link rel="help" href="https://bugzilla.mozilla.org/show_bug.cgi?id=1931432">
<script src="/resources/testharness.js"></script>
<script src="/resources/testharnessreport.js"></script>
<style>
#subject {
color: grey;
}
#subject.empty:has(:empty) {
color: red;
}
#subject.first:has(:first-child) {
color: orange;
}
#subject.last:has(:last-child) {
color: yellow;
}
/* :empty :empty never matches, so use something else. */
#subject.empty-subtree:has(:first-child :empty) {
color: green;
}
#subject.first-subtree:has(:first-child :first-child) {
color: blue;
}
#subject.last-subtree:has(:last-child :last-child) {
color: purple;
}
</style>
<div id="subject"></div>
<script>
const grey = "rgb(128, 128, 128)";
const red = "rgb(255, 0, 0)";
const orange = "rgb(255, 165, 0)";
const yellow = "rgb(255, 255, 0)";
const green = "rgb(0, 128, 0)";
const blue = 'rgb(0, 0, 255)';
const purple = 'rgb(128, 0, 128)';
function testColor(test_name, color) {
test(function() {
assert_equals(getComputedStyle(subject).color, color);
}, test_name);
}
function runTest(test_class, child, insert, color_after_add, desc) {
subject.classList.add(test_class);
testColor(desc + ' color initial', grey);
insert(child);
testColor(desc + ' color after adding', color_after_add);
subject.replaceChildren();
testColor(desc + ' color after removing', grey);
subject.classList.remove(test_class);
}
function createSimpleTree() {
const root = document.createElement('div');
root.replaceChildren(document.createElement('div'), document.createElement('div'));
return root;
}
const tests = {
':empty': {cls: 'empty', child: document.createElement('div'), color_after_add: red},
':first-child': {cls: 'first', child: document.createElement('div'), color_after_add: orange},
':last-child': {cls: 'last', child: document.createElement('div'), color_after_add: yellow},
':empty subtree': {cls: 'empty-subtree', child: createSimpleTree(), color_after_add: green},
':first-child subtree': {cls: 'first-subtree', child: createSimpleTree(), color_after_add: blue},
':last-child subtree': {cls: 'last-subtree', child: createSimpleTree(), color_after_add: purple},
};
const element = document.createElement('div');
for (const [t, options] of Object.entries(tests)) {
runTest(options.cls, options.child, (e) => subject.appendChild(e), options.color_after_add, t + ' append');
runTest(options.cls, options.child, (e) => subject.insertBefore(e, null), options.color_after_add, t + ' insert');
}
</script>