Skip to main content

blitz_dom/node/
attributes.rs

1use std::ops::{Deref, DerefMut};
2
3use markup5ever::QualName;
4
5/// An attribute's value, interned so identical values are stored once.
6///
7/// This was a plain `String`, which meant one separate heap allocation per
8/// attribute per element with no sharing between them. A census over the
9/// application's own transcript markup
10/// (`blitz-tests/tests/attribute_value_duplication.rs`) found **777 attribute
11/// values of which 54 were distinct**: 14.4x duplication, and 91.3% of the
12/// value bytes were a copy of a string already in the tree. One `class` string
13/// appeared 24 times. That is what a Tailwind UI looks like in memory, and it
14/// is the shape Blink shares through `ElementDataCache` for the same reason
15/// (`element_data.h:172`, "very common for many elements to have duplicate
16/// sets of attributes (ex. the same classes)").
17///
18/// `Atom` is the right tool and was already in the dependency graph, because
19/// `QualName` above is built from it. It is 8 bytes against `String`'s 24,
20/// stores up to 7 bytes inline with no heap allocation at all, and interns
21/// anything longer in a refcounted global table with per-bucket locks rather
22/// than one global one.
23///
24/// The trade is a hash and a possible lock acquisition per *write*, against a
25/// heap allocation and a memcpy per write today, and equality becoming a
26/// pointer comparison rather than a memcmp. Reads are unaffected: this derefs
27/// to `str`, so every `&attr.value`, `.as_str()`, `.parse()` and `==` call
28/// site continues to compile and mean the same thing.
29///
30/// `Atom` is generic over a set of strings interned at compile time. We have
31/// none to pre-intern: attribute *names* are already atoms via `QualName`, and
32/// values are arbitrary author strings, so every one of ours takes the dynamic
33/// path. `EmptyStaticAtomSet` is the crate's own declaration of that case.
34///
35/// Named `AttrAtom` rather than the more obvious `AttrValue`, because stylo
36/// already exports an `AttrValue` enum that `document.rs` uses in the same
37/// breath as this type. Two different things under one name in one file is how
38/// a later reader loses an afternoon.
39pub type AttrAtom = string_cache::Atom<string_cache::EmptyStaticAtomSet>;
40
41/// A tag attribute, e.g. `class="test"` in `<div class="test" ...>`.
42#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug)]
43pub struct Attribute {
44    /// The name of the attribute (e.g. the `class` in `<div class="test">`)
45    pub name: QualName,
46    /// The value of the attribute (e.g. the `"test"` in `<div class="test">`)
47    pub value: AttrAtom,
48}
49
50#[derive(Clone, Debug)]
51pub struct Attributes {
52    inner: Vec<Attribute>,
53}
54
55impl Attributes {
56    pub fn new(inner: Vec<Attribute>) -> Self {
57        Self { inner }
58    }
59
60    pub fn get(&mut self, name: &QualName) -> Option<&Attribute> {
61        self.inner.iter().find(|attr| attr.name == *name)
62    }
63
64    /// Set `name` to `value`, replacing any existing value.
65    ///
66    /// This used to `clear()` and `push_str()` into the existing `String`,
67    /// reusing its allocation. An interned value cannot be edited in place, so
68    /// it is replaced instead. That is not the regression it looks like: the
69    /// old path still memcpy'd the bytes and only avoided the allocation when
70    /// the new value happened to fit the old capacity, whereas interning
71    /// usually finds the string already present and takes a refcount. A
72    /// re-set to the value it already holds is now free, which is the common
73    /// case when a framework rewrites `class` with an unchanged string.
74    pub fn set(&mut self, name: QualName, value: &str) {
75        let existing_attr = self.inner.iter_mut().find(|a| a.name == name);
76        if let Some(existing_attr) = existing_attr {
77            existing_attr.value = AttrAtom::from(value);
78        } else {
79            self.push(Attribute {
80                name: name.clone(),
81                value: AttrAtom::from(value),
82            });
83        }
84    }
85
86    pub fn remove(&mut self, name: &QualName) -> Option<Attribute> {
87        let idx = self.inner.iter().position(|attr| attr.name == *name);
88        idx.map(|idx| self.inner.remove(idx))
89    }
90}
91
92impl Deref for Attributes {
93    type Target = Vec<Attribute>;
94    fn deref(&self) -> &Self::Target {
95        &self.inner
96    }
97}
98impl DerefMut for Attributes {
99    fn deref_mut(&mut self) -> &mut Self::Target {
100        &mut self.inner
101    }
102}