Skip to main content

blitz_dom/node/
attributes.rs

1use std::ops::{Deref, DerefMut};
2use std::sync::{Arc, Mutex, RwLock, Weak};
3
4use markup5ever::QualName;
5
6/// An attribute's value, interned so identical values are stored once.
7///
8/// This was a plain `String`, which meant one separate heap allocation per
9/// attribute per element with no sharing between them. A census over the
10/// application's own transcript markup
11/// (`blitz-tests/tests/attribute_value_duplication.rs`) found **777 attribute
12/// values of which 54 were distinct**: 14.4x duplication, and 91.3% of the
13/// value bytes were a copy of a string already in the tree. One `class` string
14/// appeared 24 times. That is what a Tailwind UI looks like in memory, and it
15/// is the shape Blink shares through `ElementDataCache` for the same reason
16/// (`element_data.h:172`, "very common for many elements to have duplicate
17/// sets of attributes").
18///
19/// `Atom` is the right tool and was already in the dependency graph, because
20/// `QualName` above is built from it. It is 8 bytes against `String`'s 24,
21/// stores up to 7 bytes inline with no heap allocation at all, and interns
22/// anything longer in a refcounted global table with per-bucket locks rather
23/// than one global one.
24///
25/// The trade is a hash and a possible lock acquisition per *write*, against a
26/// heap allocation and a memcpy per write today, and equality becoming a
27/// pointer comparison rather than a memcmp. Reads are unaffected: this derefs
28/// to `str`, so every `&attr.value`, `.as_str()`, `.parse()` and `==` call
29/// site continues to compile and mean the same thing.
30///
31/// `Atom` is generic over a set of strings interned at compile time. We have
32/// none to pre-intern: attribute *names* are already atoms via `QualName`, and
33/// values are arbitrary author strings, so every one of ours takes the dynamic
34/// path. `EmptyStaticAtomSet` is the crate's own declaration of that case.
35///
36/// Named `AttrAtom` rather than the more obvious `AttrValue`, because stylo
37/// already exports an `AttrValue` enum that `document.rs` uses in the same
38/// breath as this type. Two different things under one name in one file is how
39/// a later reader loses an afternoon.
40pub type AttrAtom = string_cache::Atom<string_cache::EmptyStaticAtomSet>;
41
42/// A tag attribute, e.g. `class="test"` in `<div class="test" ...>`.
43#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Debug)]
44pub struct Attribute {
45    /// The name of the attribute (e.g. the `class` in `<div class="test">`)
46    pub name: QualName,
47    /// The value of the attribute (e.g. the `"test"` in `<div class="test">`)
48    pub value: AttrAtom,
49}
50
51/// Identity and retained contents of an attribute exposed as an Attr.
52///
53/// Elements hold weak handles. A retained Attr owns its handle and continues
54/// to hold its last value after removal. Cloning an element creates new
55/// attribute identities.
56#[derive(Debug)]
57pub struct AttributeNode {
58    pub name: QualName,
59    pub value: AttrAtom,
60    pub attached: bool,
61}
62
63#[derive(Debug)]
64pub struct Attributes {
65    inner: Vec<Attribute>,
66    handles: Mutex<Vec<Weak<RwLock<AttributeNode>>>>,
67}
68
69impl Clone for Attributes {
70    fn clone(&self) -> Self {
71        Self::new(self.inner.clone())
72    }
73}
74
75impl Drop for Attributes {
76    fn drop(&mut self) {
77        for handle in self
78            .handles
79            .get_mut()
80            .unwrap()
81            .iter()
82            .filter_map(Weak::upgrade)
83        {
84            handle.write().unwrap().attached = false;
85        }
86    }
87}
88
89fn same_expanded_name(left: &QualName, right: &QualName) -> bool {
90    left.ns == right.ns && left.local == right.local
91}
92
93impl Attributes {
94    pub fn new(inner: Vec<Attribute>) -> Self {
95        Self {
96            inner,
97            handles: Mutex::new(Vec::new()),
98        }
99    }
100
101    pub fn get(&mut self, name: &QualName) -> Option<&Attribute> {
102        self.inner
103            .iter()
104            .find(|attr| same_expanded_name(&attr.name, name))
105    }
106
107    /// Get the stable identity of an existing attribute.
108    pub fn attribute_node(&self, name: &QualName) -> Option<Arc<RwLock<AttributeNode>>> {
109        let attr = self
110            .inner
111            .iter()
112            .find(|attr| same_expanded_name(&attr.name, name))?;
113        let mut handles = self.handles.lock().unwrap();
114        handles.retain(|handle| handle.strong_count() != 0);
115        for handle in handles.iter().filter_map(Weak::upgrade) {
116            let matches = {
117                let node = handle.read().unwrap();
118                node.attached && same_expanded_name(&node.name, &attr.name)
119            };
120            if matches {
121                return Some(handle);
122            }
123        }
124        let handle = Arc::new(RwLock::new(AttributeNode {
125            name: attr.name.clone(),
126            value: attr.value.clone(),
127            attached: true,
128        }));
129        handles.push(Arc::downgrade(&handle));
130        Some(handle)
131    }
132
133    /// Detach an identity without removing the backing attribute.
134    ///
135    /// setAttributeNode uses this before replacing the value, so the returned
136    /// old Attr retains the old value rather than the replacement's value.
137    pub fn detach_attribute_node(&self, name: &QualName) {
138        self.handles.lock().unwrap().retain(|weak| {
139            let Some(handle) = weak.upgrade() else {
140                return false;
141            };
142            let mut node = handle.write().unwrap();
143            if same_expanded_name(&node.name, name) {
144                node.attached = false;
145                false
146            } else {
147                true
148            }
149        });
150    }
151
152    /// Bind the identity supplied to setAttributeNode after the mutation.
153    pub fn bind_attribute_node(&self, handle: &Arc<RwLock<AttributeNode>>) {
154        let name = handle.read().unwrap().name.clone();
155        self.detach_attribute_node(&name);
156        handle.write().unwrap().attached = true;
157        self.handles.lock().unwrap().push(Arc::downgrade(handle));
158    }
159
160    /// Set `name` to `value`, replacing any existing value.
161    ///
162    /// This used to `clear()` and `push_str()` into the existing `String`,
163    /// reusing its allocation. An interned value cannot be edited in place, so
164    /// it is replaced instead. That is not the regression it looks like: the
165    /// old path still memcpy'd the bytes and only avoided the allocation when
166    /// the new value happened to fit the old capacity, whereas interning
167    /// usually finds the string already present and takes a refcount. A
168    /// re-set to the value it already holds is now free, which is the common
169    /// case when a framework rewrites `class` with an unchanged string.
170    pub fn set(&mut self, name: QualName, value: &str) {
171        let existing_attr = self
172            .inner
173            .iter_mut()
174            .find(|attr| same_expanded_name(&attr.name, &name));
175        let value = AttrAtom::from(value);
176        if let Some(existing_attr) = existing_attr {
177            existing_attr.name = name.clone();
178            existing_attr.value = value.clone();
179        } else {
180            self.inner.push(Attribute {
181                name: name.clone(),
182                value: value.clone(),
183            });
184        }
185        self.handles.get_mut().unwrap().retain(|weak| {
186            let Some(handle) = weak.upgrade() else {
187                return false;
188            };
189            let mut node = handle.write().unwrap();
190            if node.attached && same_expanded_name(&node.name, &name) {
191                node.name = name.clone();
192                node.value = value.clone();
193            }
194            true
195        });
196    }
197
198    pub fn remove(&mut self, name: &QualName) -> Option<Attribute> {
199        let idx = self
200            .inner
201            .iter()
202            .position(|attr| same_expanded_name(&attr.name, name))?;
203        self.detach_attribute_node(name);
204        Some(self.inner.remove(idx))
205    }
206}
207
208impl Deref for Attributes {
209    type Target = Vec<Attribute>;
210    fn deref(&self) -> &Self::Target {
211        &self.inner
212    }
213}
214impl DerefMut for Attributes {
215    fn deref_mut(&mut self) -> &mut Self::Target {
216        &mut self.inner
217    }
218}