/* This Source Code Form is subject to the terms of the Mozilla Public *License,v.2.0.IfacopyoftheMPLwasnotdistributedwiththis
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */
#![allow(unsafe_code)]
usecrate::applicable_declarations::CascadePriority; usecrate::shared_lock::StylesheetGuards; usecrate::stylesheets::layer_rule::LayerOrder; use malloc_size_of::{MallocShallowSizeOf, MallocSizeOf, MallocSizeOfOps}; use parking_lot::RwLock; use smallvec::SmallVec; use std::fmt; use std::hash; use std::io::Write; use std::mem; use std::ptr; use std::sync::atomic::{self, AtomicPtr, AtomicUsize, Ordering};
/// The rule tree, the structure servo uses to preserve the results of selector /// matching. /// /// This is organized as a tree of rules. When a node matches a set of rules, /// they're inserted in order in the tree, starting with the less specific one. /// /// When a rule is inserted in the tree, other elements may share the path up to /// a given rule. If that's the case, we don't duplicate child nodes, but share /// them. /// /// When the rule node refcount drops to zero, it doesn't get freed. It gets /// instead put into a free list, and it is potentially GC'd after a while. /// /// That way, a rule node that represents a likely-to-match-again rule (like a /// :hover rule) can be reused if we haven't GC'd it yet. #[derive(Debug)] pubstruct RuleTree {
root: StrongRuleNode,
}
impl Drop for RuleTree { fn drop(&mutself) { unsafe { self.swap_free_list_and_gc(ptr::null_mut()) }
}
}
whilelet Some(node) = stack.pop() {
n += unsafe { ops.malloc_size_of(&*node.p) }; let children = node.p.children.read();
children.shallow_size_of(ops); for c in &*children {
stack.push(unsafe { c.upgrade() });
}
}
n
}
}
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)] struct ChildKey(CascadePriority, ptr::NonNull<()>); unsafeimpl Send for ChildKey {} unsafeimpl Sync for ChildKey {}
impl RuleTree { /// Construct a new rule tree. pubfn new() -> Self {
RuleTree {
root: StrongRuleNode::new(Box::new(RuleNode::root())),
}
}
/// Get the root rule node. pubfn root(&self) -> &StrongRuleNode {
&self.root
}
/// This can only be called when no other threads is accessing this tree. pubfn gc(&self) { unsafe { self.swap_free_list_and_gc(RuleNode::DANGLING_PTR) }
}
/// This can only be called when no other threads is accessing this tree. pubfn maybe_gc(&self) { #[cfg(debug_assertions)] self.maybe_dump_stats();
let should_dump = LAST_STATS.with(|s| { let now = Instant::now(); if now.duration_since(s.get()) < RULE_TREE_STATS_INTERVAL { returnfalse;
}
s.set(now); true
});
// Reset the approximate free count to zero, as we are going to steal // the free list.
root.approximate_free_count.store(0, Ordering::Relaxed);
// Steal the free list head. Memory loads on nodes while iterating it // must observe any prior changes that occured so this requires // acquire ordering, but there are no writes that need to be kept // before this swap so there is no need for release. letmut head = root.next_free.swap(ptr, Ordering::Acquire);
while head != RuleNode::DANGLING_PTR {
debug_assert!(!head.is_null());
letmut node = UnsafeBox::from_raw(head);
// The root node cannot go on the free list.
debug_assert!(node.root.is_some());
// The refcount of nodes on the free list never goes below 1.
debug_assert!(node.refcount.load(Ordering::Relaxed) > 0);
// No one else is currently writing to that field. Get the address // of the next node in the free list and replace it with null, // other threads will now consider that this node is not on the // free list.
head = node.next_free.swap(ptr::null_mut(), Ordering::Relaxed);
// This release write synchronises with the acquire fence in // `WeakRuleNode::upgrade`, making sure that if `upgrade` observes // decrements the refcount to 0, it will also observe the // `node.next_free` swap to null above. if node.refcount.fetch_sub(1, Ordering::Release) == 1 { // And given it observed the null swap above, it will need // `pretend_to_be_on_free_list` to finish its job, writing // `RuleNode::DANGLING_PTR` in `node.next_free`.
RuleNode::pretend_to_be_on_free_list(&node);
// Drop this node now that we just observed its refcount going // down to zero.
RuleNode::drop_without_free_list(&mut node);
}
}
}
}
/// The number of RuleNodes added to the free list before we will consider /// doing a GC when calling maybe_gc(). (The value is copied from Gecko, /// where it likely did not result from a rigorous performance analysis.) const RULE_TREE_GC_INTERVAL: usize = 300;
/// A node in the rule tree. struct RuleNode { /// The root node. Only the root has no root pointer, for obvious reasons.
root: Option<WeakRuleNode>,
/// The parent rule node. Only the root has no parent.
parent: Option<StrongRuleNode>,
/// The actual style source, either coming from a selector in a StyleRule, /// or a raw property declaration block (like the style attribute). /// /// None for the root node.
source: Option<StyleSource>,
/// The cascade level + layer order this rule is positioned at.
cascade_priority: CascadePriority,
/// The refcount of this node. /// /// Starts at one. Incremented in `StrongRuleNode::clone` and /// `WeakRuleNode::upgrade`. Decremented in `StrongRuleNode::drop` /// and `RuleTree::swap_free_list_and_gc`. /// /// If a non-root node's refcount reaches zero, it is incremented back to at /// least one in `RuleNode::pretend_to_be_on_free_list` until the caller who /// observed it dropping to zero had a chance to try to remove it from its /// parent's children list. /// /// The refcount should never be decremented to zero if the value in /// `next_free` is not null.
refcount: AtomicUsize,
/// Only used for the root, stores the number of free rule nodes that are /// around.
approximate_free_count: AtomicUsize,
/// The children of a given rule node. Children remove themselves from here /// when they go away.
children: RwLock<Map<ChildKey, WeakRuleNode>>,
/// This field has two different meanings depending on whether this is the /// root node or not. /// /// If it is the root, it represents the head of the free list. It may be /// null, which means the free list is gone because the tree was dropped, /// and it may be `RuleNode::DANGLING_PTR`, which means the free list is /// empty. /// /// If it is not the root node, this field is either null if the node is /// not on the free list, `RuleNode::DANGLING_PTR` if it is the last item /// on the free list or the node is pretending to be on the free list, or /// any valid non-null pointer representing the next item on the free list /// after this one. /// /// See `RuleNode::push_on_free_list`, `swap_free_list_and_gc`, and /// `WeakRuleNode::upgrade`. /// /// Two threads should never attempt to put the same node on the free list /// both at the same time.
next_free: AtomicPtr<RuleNode>,
}
// On Gecko builds, hook into the leak checking machinery. #[cfg(feature = "gecko_refcount_logging")] mod gecko_leak_checking { usesuper::RuleNode; use std::mem::size_of; use std::os::raw::{c_char, c_void};
/// Logs the creation of a heap-allocated object to Gecko's leak-checking machinery. pub(super) fn log_ctor(ptr: *const RuleNode) { let s = NAME as *const [u8] as *const u8 as *const c_char; unsafe {
NS_LogCtor(ptr as *mut c_void, s, size_of::<RuleNode>() as u32);
}
}
/// Logs the destruction of a heap-allocated object to Gecko's leak-checking machinery. pub(super) fn log_dtor(ptr: *const RuleNode) { let s = NAME as *const [u8] as *const u8 as *const c_char; unsafe {
NS_LogDtor(ptr as *mut c_void, s, size_of::<RuleNode>() as u32);
}
}
}
fn key(&self) -> ChildKey {
ChildKey( self.cascade_priority, self.source
.as_ref()
.expect("Called key() on the root node")
.key(),
)
}
/// Drops a node without ever putting it on the free list. /// /// Note that the node may not be dropped if we observe that its refcount /// isn't zero anymore when we write-lock its parent's children map to /// remove it. /// /// This loops over parents of dropped nodes if their own refcount reaches /// zero to avoid recursion when dropping deep hierarchies of nodes. /// /// For non-root nodes, this should always be preceded by a call of /// `RuleNode::pretend_to_be_on_free_list`. unsafefn drop_without_free_list(this: &mut UnsafeBox<Self>) { // We clone the box and shadow the original one to be able to loop // over its ancestors if they also need to be dropped. letmut this = UnsafeBox::clone(this); loop { // If the node has a parent, we need to remove it from its parent's // children list. iflet Some(parent) = this.parent.as_ref() {
debug_assert!(!this.next_free.load(Ordering::Relaxed).is_null());
// We lock the parent's children list, which means no other // thread will have any more opportunity to resurrect the node // anymore. letmut children = parent.p.children.write();
// We decrement the counter to remove the "pretend to be // on the free list" reference. let old_refcount = this.refcount.fetch_sub(1, Ordering::Release);
debug_assert!(old_refcount != 0); if old_refcount != 1 { // Other threads resurrected this node and those references // are still alive, we have nothing to do anymore. return;
}
// We finally remove the node from its parent's children list, // there are now no other references to it and it cannot // be resurrected anymore even after we unlock the list.
debug!( "Remove from child list: {:?}, parent: {:?}",
this.as_mut_ptr(),
this.parent.as_ref().map(|p| p.p.as_mut_ptr())
); let weak = children.remove(&this.key(), |node| node.p.key()).unwrap();
assert_eq!(weak.p.as_mut_ptr(), this.as_mut_ptr());
} else {
debug_assert_eq!(this.next_free.load(Ordering::Relaxed), ptr::null_mut());
debug_assert_eq!(this.refcount.load(Ordering::Relaxed), 0);
}
// We are going to drop this node for good this time, as per the // usual refcounting protocol we need an acquire fence here before // we run the destructor. // // See https://github.com/rust-lang/rust/pull/41714#issuecomment-298996916 // for why it doesn't matter whether this is a load or a fence.
atomic::fence(Ordering::Acquire);
// Remove the parent reference from the child to avoid // recursively dropping it and putting it on the free list. let parent = UnsafeBox::deref_mut(&mut this).parent.take();
// We now drop the actual box and its contents, no one should // access the current value in `this` anymore.
log_drop(&*this);
UnsafeBox::drop(&mut this);
iflet Some(parent) = parent { // We will attempt to drop the node's parent without the free // list, so we clone the inner unsafe box and forget the // original parent to avoid running its `StrongRuleNode` // destructor which would attempt to use the free list if it // still exists.
this = UnsafeBox::clone(&parent.p);
mem::forget(parent); if this.refcount.fetch_sub(1, Ordering::Release) == 1 {
debug_assert_eq!(this.next_free.load(Ordering::Relaxed), ptr::null_mut()); if this.root.is_some() {
RuleNode::pretend_to_be_on_free_list(&this);
} // Parent also reached refcount zero, we loop to drop it. continue;
}
}
return;
}
}
/// Pushes this node on the tree's free list. Returns false if the free list /// is gone. Should only be called after we decremented a node's refcount /// to zero and pretended to be on the free list. unsafefn push_on_free_list(this: &UnsafeBox<Self>) -> bool { let root = &this.root.as_ref().unwrap().p;
// Increment the approximate free count by one.
root.approximate_free_count.fetch_add(1, Ordering::Relaxed);
// If the compare-exchange operation fails in the loop, we will retry // with the new head value, so this can be a relaxed load. letmut head = root.next_free.load(Ordering::Relaxed);
while !head.is_null() { // Two threads can never attempt to push the same node on the free // list both at the same time, so whoever else pushed a node on the // free list cannot have done so with this node.
debug_assert_ne!(head, this.as_mut_ptr());
// Store the current head of the free list in this node.
this.next_free.store(head, Ordering::Relaxed);
// Any thread acquiring the free list must observe the previous // next_free changes that occured, hence the release ordering // on success. match root.next_free.compare_exchange_weak(
head,
this.as_mut_ptr(),
Ordering::Release,
Ordering::Relaxed,
) {
Ok(_) => { // This node is now on the free list, caller should not use // the node anymore. returntrue;
},
Err(new_head) => head = new_head,
}
}
// Tree was dropped and free list has been destroyed. We did not push // this node on the free list but we still pretend to be on the free // list to be ready to call `drop_without_free_list`. false
}
/// Makes the node pretend to be on the free list. This will increment the /// refcount by 1 and store `Self::DANGLING_PTR` in `next_free`. This /// method should only be called after caller decremented the refcount to /// zero, with the null pointer stored in `next_free`. unsafefn pretend_to_be_on_free_list(this: &UnsafeBox<Self>) {
debug_assert_eq!(this.next_free.load(Ordering::Relaxed), ptr::null_mut());
this.refcount.fetch_add(1, Ordering::Relaxed);
this.next_free.store(Self::DANGLING_PTR, Ordering::Release);
}
debug_assert!( self.p.cascade_priority <= cascade_priority, "Should be ordered (instead {:?} > {:?}), from {:?} and {:?}", self.p.cascade_priority,
cascade_priority, self.p.source,
source,
);
let key = ChildKey(cascade_priority, source.key()); let children = self.p.children.upgradable_read(); iflet Some(child) = children.get(&key, |node| node.p.key()) { // Sound to call because we read-locked the parent's children. returnunsafe { child.upgrade() };
} letmut children = RwLockUpgradableReadGuard::upgrade(children); match children.entry(key, |node| node.p.key()) {
Entry::Occupied(child) => { // Sound to call because we write-locked the parent's children. unsafe { child.upgrade() }
},
Entry::Vacant(entry) => unsafe { let node = StrongRuleNode::new(Box::new(RuleNode::new(
root.downgrade(), self.clone(),
source,
cascade_priority,
))); // Sound to call because we still own a strong reference to // this node, through the `node` variable itself that we are // going to return to the caller.
entry.insert(node.downgrade());
node
},
}
}
/// Get the style source corresponding to this rule node. May return `None` /// if it's the root node, which means that the node hasn't matched any /// rules. pubfn style_source(&self) -> Option<&StyleSource> { self.p.source.as_ref()
}
/// Returns whether this node has any child, only intended for testing /// purposes. pubunsafefn has_children_for_testing(&self) -> bool {
!self.p.children.read().is_empty()
}
if !should_drop { // The refcount didn't even drop zero yet, there is nothing for us // to do anymore. return;
}
unsafe { if node.root.is_some() { // This is a non-root node and we just observed the refcount // dropping to zero, we need to pretend to be on the free list // to unstuck any thread who tried to resurrect this node first // through `WeakRuleNode::upgrade`.
RuleNode::pretend_to_be_on_free_list(&self.p);
// Attempt to push the node on the free list. This may fail // if the free list is gone. if RuleNode::push_on_free_list(&self.p) { return;
}
}
// Either this was the last reference of the root node, or the // tree rule is gone and there is no free list anymore. Drop the // node.
RuleNode::drop_without_free_list(&mutself.p);
}
}
}
impl WeakRuleNode { /// Upgrades this weak node reference, returning a strong one. /// /// Must be called with items stored in a node's children list. The children /// list must at least be read-locked when this is called. unsafefn upgrade(&self) -> StrongRuleNode {
debug!("Upgrading weak node: {:p}", &*self.p);
ifself.p.refcount.fetch_add(1, Ordering::Relaxed) == 0 { // We observed a refcount of 0, we need to wait for this node to // be put on the free list. Resetting the `next_free` pointer to // null is only done in `RuleNode::drop_without_free_list`, just // before a release refcount decrement, so this acquire fence here // makes sure that we observed the write to null before we loop // until there is a non-null value.
atomic::fence(Ordering::Acquire); whileself.p.next_free.load(Ordering::Relaxed).is_null() {}
}
StrongRuleNode::from_unsafe_box(UnsafeBox::clone(&self.p))
}
}
impl fmt::Debug for StrongRuleNode { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
(&*self.p as *const RuleNode).fmt(f)
}
}
impl Eq for StrongRuleNode {} impl PartialEq for StrongRuleNode { fn eq(&self, other: &Self) -> bool {
&*self.p as *const RuleNode == &*other.p
}
}
impl hash::Hash for StrongRuleNode { fn hash<H>(&self, state: &mut H) where
H: hash::Hasher,
{
(&*self.p as *const RuleNode).hash(state)
}
}
// Large pages generate thousands of RuleNode objects.
size_of_test!(RuleNode, 80); // StrongRuleNode should be pointer-sized even inside an option.
size_of_test!(Option<StrongRuleNode>, 8);
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.14 Sekunden
(vorverarbeitet am 2026-06-19)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.