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}