Skip to main content

taffy/tree/
cache.rs

1//! A cache for storing the results of layout computation
2
3#![allow(clippy::unusual_byte_groupings)]
4
5use crate::geometry::Size;
6use crate::style::AvailableSpace;
7use crate::tree::{CollapsibleMarginSet, LayoutInput, LayoutOutput, RunMode, SizingMode};
8use crate::RequestedAxis;
9
10/// The number of cache entries for each node in the tree
11const CACHE_SIZE: usize = 9;
12
13// Manually written-out results of float to u32 bit casts because
14// `f32::to_bits` is not yet const at our MSRV.
15
16/// `f32::INFINITY` as a u32
17const INFINITY_BITS: u32 = 0b_0_11111111_00000000000000000000000_u32;
18/// `f32::NEG_INFINITY` as a u32
19const NEG_INFINITY_BITS: u32 = 0b_1_11111111_00000000000000000000000_u32;
20
21// The `CacheKey` encodes two f32s as a u64. We know that the f32s will always be
22// non-negative, so we pack two extra bits encoding the `RequestedAxis` into the
23// sign bits of the f32s. These constants help to encode and decode those bits.
24
25/// The sign bit of the first f32
26const SIGN_BIT_1: u64 = 1u64 << 63;
27/// The sign bit of the second f32
28const SIGN_BIT_2: u64 = 1u64 << 31;
29/// Mask of both sign bits (used to compute NON_SIGN_BITS_MASK)
30const BOTH_SIGN_BITS_MASK: u64 = SIGN_BIT_1 | SIGN_BIT_2;
31/// Mask of excluding the sign bits (used when setting/getting the size excluding the packed bits)
32const NON_SIGN_BITS_MASK: u64 = !BOTH_SIGN_BITS_MASK;
33
34/// Mask which includes only the bits which encode the x-axis value that we can use to ignore the
35/// y-axis value when comparing a cache key.
36const X_AXIS_VALUE_MASK: u64 = (u32::MAX as u64) << 32;
37
38/// Pack `Option<f32>` into `u32`
39#[inline(always)]
40fn option_cache_key(input: Option<f32>) -> u32 {
41    match input {
42        Some(value) => value.to_bits(),
43        None => INFINITY_BITS,
44    }
45}
46
47/// Pack `Size<Option<f32>>` into `u64`
48#[inline(always)]
49fn size_option_cache_key(input: Size<Option<f32>>) -> u64 {
50    (option_cache_key(input.width) as u64) << 32 | option_cache_key(input.height) as u64
51}
52
53/// Pack `AvailableSpace` into `u32`
54#[inline(always)]
55fn available_space_cache_key(input: AvailableSpace) -> u32 {
56    match input {
57        AvailableSpace::Definite(value) => (-value).to_bits(),
58        AvailableSpace::MinContent => NEG_INFINITY_BITS,
59        AvailableSpace::MaxContent => INFINITY_BITS,
60    }
61}
62
63/// Pack `Size<AvailableSpace>` into `u64`
64#[inline(always)]
65#[allow(dead_code)]
66fn size_available_space_cache_key(input: Size<AvailableSpace>) -> u64 {
67    (available_space_cache_key(input.width) as u64) << 32 | available_space_cache_key(input.height) as u64
68}
69
70/// Encodes combination of a `known_dimension` (Option<f32>) and `AvailableSpace` in
71/// a single dimension into a cache key in a single dimension.
72#[inline(always)]
73fn mixed_cache_key(kd: Option<f32>, avs: AvailableSpace) -> u32 {
74    kd.map(|kd| kd.to_bits()).unwrap_or_else(|| available_space_cache_key(avs))
75}
76
77/// Encodes combination of a `known_dimension` (Option<f32>) and `AvailableSpace` in
78/// two dimensions into a cache key in a single dimension.
79#[inline(always)]
80fn size_mixed_cache_key(kd: Size<Option<f32>>, avs: Size<AvailableSpace>) -> u64 {
81    (mixed_cache_key(kd.width, avs.width) as u64) << 32 | mixed_cache_key(kd.height, avs.height) as u64
82}
83
84/// Space-optimised cache key that packs bits into as small a size as possible
85#[derive(Debug, Clone, Copy, PartialEq, Eq)]
86#[cfg_attr(feature = "serde", derive(Serialize))]
87struct CacheKey {
88    /// The initial cached size of the node itself
89    kd_available_space: u64,
90    /// The initial cached size of the parent's node
91    parent_size: u64,
92    /// Whether each known dimension is definite. Normalized such that an axis
93    /// without a known dimension is always `true`.
94    known_dimensions_are_definite: Size<bool>,
95}
96
97impl CacheKey {
98    #[inline(always)]
99    #[allow(dead_code)]
100    /// Return the parent size with the extra bits that encode the requested axis masked out
101    fn parent_size(&self) -> u64 {
102        self.parent_size & NON_SIGN_BITS_MASK
103    }
104
105    /// Return the parent size with the extra bits that encode the requested axis masked out
106    /// And the y-axis value masked out
107    fn x_axis_parent_size(&self) -> u64 {
108        self.parent_size & (X_AXIS_VALUE_MASK & NON_SIGN_BITS_MASK)
109    }
110
111    /// Return the bits that encode the requested axis
112    fn requested_axis_bits(&self) -> u64 {
113        self.parent_size & BOTH_SIGN_BITS_MASK
114    }
115
116    /// Whether a cached entry with this key contains a valid size for the axis requested by `other`.
117    /// Sizes computed for a single axis may contain garbage values in the other axis, so an entry
118    /// is only usable if it was computed for the same axis (or for both axes).
119    fn size_is_valid_for(&self, other: &CacheKey) -> bool {
120        let entry_axis = self.requested_axis_bits();
121        entry_axis == BOTH_SIGN_BITS_MASK || entry_axis == other.requested_axis_bits()
122    }
123}
124
125impl From<&LayoutInput> for CacheKey {
126    fn from(input: &LayoutInput) -> Self {
127        // Pack axis enum into spare bits in the known_dimensions and available_space values
128        let extra_bits = match input.axis {
129            RequestedAxis::Horizontal => SIGN_BIT_1,
130            RequestedAxis::Vertical => SIGN_BIT_2,
131            RequestedAxis::Both => SIGN_BIT_1 | SIGN_BIT_2,
132        };
133
134        Self {
135            kd_available_space: size_mixed_cache_key(input.known_dimensions, input.available_space),
136            parent_size: (size_option_cache_key(input.parent_size) & NON_SIGN_BITS_MASK) | extra_bits,
137            known_dimensions_are_definite: input
138                .known_dimensions_are_definite
139                .zip_map(input.known_dimensions, |is_definite, kd| is_definite || kd.is_none()),
140        }
141    }
142}
143
144/// The layout inputs that affect a node's layout but are not part of the [`CacheKey`]: the
145/// [`SizingMode`] and whether each of the node's vertical margins is collapsible, packed into a `u8`.
146///
147/// These are determined by the layout algorithm of the node's parent and by the node's own style,
148/// so they only change for a given node when one of those changes. Rather than keying every entry
149/// on them, a [`Cache`] records the single mode that all of its entries were computed with.
150#[derive(Debug, Clone, Copy, PartialEq, Eq)]
151#[cfg_attr(feature = "serde", derive(Serialize))]
152struct CacheMode(u8);
153
154impl CacheMode {
155    /// Set if the sizing mode is `SizingMode::InherentSize`
156    const INHERENT_SIZE_BIT: u8 = 0b001;
157    /// Set if the node's top margin is collapsible
158    const COLLAPSIBLE_START_MARGIN_BIT: u8 = 0b010;
159    /// Set if the node's bottom margin is collapsible
160    const COLLAPSIBLE_END_MARGIN_BIT: u8 = 0b100;
161
162    /// The mode of a cache that has never been stored to. Which mode this is does not matter,
163    /// as such a cache has no entries.
164    const INITIAL: Self = Self(0);
165}
166
167impl From<&LayoutInput> for CacheMode {
168    #[inline(always)]
169    fn from(input: &LayoutInput) -> Self {
170        let inherent_size = matches!(input.sizing_mode, SizingMode::InherentSize) as u8;
171        let collapsible = input.vertical_margins_are_collapsible;
172        Self(
173            (inherent_size * Self::INHERENT_SIZE_BIT)
174                | (collapsible.start as u8 * Self::COLLAPSIBLE_START_MARGIN_BIT)
175                | (collapsible.end as u8 * Self::COLLAPSIBLE_END_MARGIN_BIT),
176        )
177    }
178}
179
180#[cfg(all(debug_assertions, feature = "std"))]
181std::thread_local! {
182    /// See [`cache_mode_change_evictions`]
183    static MODE_CHANGE_EVICTIONS: core::cell::Cell<usize> = const { core::cell::Cell::new(0) };
184}
185
186/// The number of times that the current thread has stored a result into a non-empty [`Cache`]
187/// whose entries were computed with a different `sizing_mode` or `vertical_margins_are_collapsible`,
188/// dropping those entries.
189///
190/// This is expected when the layout algorithm of a node's parent has changed since the node was
191/// last laid out. It must not happen while laying out a tree whose caches started out empty: that
192/// would mean that Taffy lays a node out with more than one mode in a single pass, with each
193/// change of mode throwing away the node's cache. Taffy's test suite uses this function to check
194/// that. Only available in debug builds.
195#[doc(hidden)]
196#[cfg(all(debug_assertions, feature = "std"))]
197pub fn cache_mode_change_evictions() -> usize {
198    MODE_CHANGE_EVICTIONS.with(|count| count.get())
199}
200
201/// Cached intermediate layout results
202#[derive(Debug, Clone, PartialEq)]
203#[cfg_attr(feature = "serde", derive(Serialize))]
204pub(crate) struct CacheEntry<T> {
205    /// The key for the cache entry
206    key: CacheKey,
207    /// The cached size and baselines of the item
208    content: T,
209}
210
211/// A cache for caching the results of a sizing a Grid Item or Flexbox Item
212#[derive(Debug, Clone, PartialEq)]
213#[cfg_attr(feature = "serde", derive(Serialize))]
214pub struct Cache {
215    /// The cache entry for the node's final layout
216    final_layout_entry: Option<CacheEntry<LayoutOutput>>,
217    /// The cache entries for the node's preliminary size measurements
218    measure_entries: [Option<CacheEntry<Size<f32>>>; CACHE_SIZE],
219    /// Tracks which measure entries have been used since the eviction cursor last passed them
220    recently_used_entries: u16,
221    /// The next measure entry to consider replacing
222    next_measure_entry: u8,
223    /// Tracks if all cache entries are empty
224    is_empty: bool,
225    /// The `sizing_mode` and `vertical_margins_are_collapsible` inputs that all of the cache's
226    /// entries were computed with. Results are only retrieved for inputs that have the same mode,
227    /// and storing a result that was computed with a different mode drops the existing entries.
228    mode: CacheMode,
229}
230
231impl Default for Cache {
232    fn default() -> Self {
233        Self::new()
234    }
235}
236
237impl Cache {
238    /// Create a new empty cache
239    pub const fn new() -> Self {
240        /// Workaround for `Option<CacheEntry<_>>` not being `Copy` (required for array repeat expressions)
241        const NONE_MEASURE_ENTRY: Option<CacheEntry<Size<f32>>> = None;
242        Self {
243            final_layout_entry: None,
244            measure_entries: [NONE_MEASURE_ENTRY; CACHE_SIZE],
245            recently_used_entries: 0,
246            next_measure_entry: 0,
247            is_empty: true,
248            mode: CacheMode::INITIAL,
249        }
250    }
251
252    /// Try to retrieve a cached result from the cache
253    #[inline]
254    pub fn get(&mut self, input: &LayoutInput) -> Option<LayoutOutput> {
255        if self.mode != CacheMode::from(input) {
256            return None;
257        }
258        let key = CacheKey::from(input);
259        match input.run_mode {
260            RunMode::PerformLayout => {
261                self.final_layout_entry.as_ref().filter(|entry| entry.key == key).map(|e| e.content.clone())
262            }
263            RunMode::ComputeSize => {
264                for (index, entry) in self.measure_entries.iter().enumerate() {
265                    let Some(entry) = entry else { continue };
266                    if entry.key.kd_available_space == key.kd_available_space
267                        && entry.key.known_dimensions_are_definite == key.known_dimensions_are_definite
268                        && (entry.key.x_axis_parent_size() == key.x_axis_parent_size())
269                        && entry.key.size_is_valid_for(&key)
270                    {
271                        self.recently_used_entries |= 1 << index;
272                        return Some(LayoutOutput::from_outer_size(entry.content));
273                    }
274                }
275
276                None
277            }
278            RunMode::PerformHiddenLayout => None,
279        }
280    }
281
282    /// Store a computed size in the cache
283    pub fn store(&mut self, input: &LayoutInput, layout_output: LayoutOutput) {
284        if input.run_mode == RunMode::PerformHiddenLayout {
285            return;
286        }
287        let mode = CacheMode::from(input);
288        if self.mode != mode {
289            #[cfg(all(debug_assertions, feature = "std"))]
290            if !self.is_empty {
291                MODE_CHANGE_EVICTIONS.with(|count| count.set(count.get() + 1));
292            }
293            self.clear();
294            self.mode = mode;
295        }
296        let key = CacheKey::from(input);
297        match input.run_mode {
298            RunMode::PerformLayout => {
299                self.is_empty = false;
300                self.final_layout_entry = Some(CacheEntry { key, content: layout_output })
301            }
302            RunMode::ComputeSize => {
303                // Measure entries only store the size, and cache hits are reconstructed with
304                // `LayoutOutput::from_outer_size`, which resets the margin-collapse metadata
305                // (`top_margin`, `bottom_margin`, `margins_can_collapse_through`). Results that
306                // carry such metadata cannot be reconstructed from their size, so don't cache them.
307                if layout_output.margins_can_collapse_through
308                    || layout_output.top_margin != CollapsibleMarginSet::ZERO
309                    || layout_output.bottom_margin != CollapsibleMarginSet::ZERO
310                {
311                    return;
312                }
313                self.is_empty = false;
314                if let Some(index) =
315                    self.measure_entries.iter().position(|entry| entry.as_ref().is_some_and(|entry| entry.key == key))
316                {
317                    self.measure_entries[index].as_mut().unwrap().content = layout_output.size;
318                    self.recently_used_entries |= 1 << index;
319                    return;
320                }
321                while self.recently_used_entries & (1 << self.next_measure_entry) != 0 {
322                    self.recently_used_entries &= !(1 << self.next_measure_entry);
323                    self.next_measure_entry += 1;
324                    if self.next_measure_entry == CACHE_SIZE as u8 {
325                        self.next_measure_entry = 0;
326                    }
327                }
328                let entry_index = self.next_measure_entry as usize;
329                self.measure_entries[entry_index] = Some(CacheEntry { key, content: layout_output.size });
330                self.recently_used_entries |= 1 << entry_index;
331                self.next_measure_entry += 1;
332                if self.next_measure_entry == CACHE_SIZE as u8 {
333                    self.next_measure_entry = 0;
334                }
335            }
336            RunMode::PerformHiddenLayout => {}
337        }
338    }
339
340    /// Clear all cache entries and reports clear operation outcome ([`ClearState`])
341    pub fn clear(&mut self) -> ClearState {
342        if self.is_empty {
343            return ClearState::AlreadyEmpty;
344        }
345        self.is_empty = true;
346        self.final_layout_entry = None;
347        /// Workaround for `Option<CacheEntry<_>>` not being `Copy` (required for array repeat expressions)
348        const NONE_MEASURE_ENTRY: Option<CacheEntry<Size<f32>>> = None;
349        self.measure_entries = [NONE_MEASURE_ENTRY; CACHE_SIZE];
350        self.recently_used_entries = 0;
351        self.next_measure_entry = 0;
352        ClearState::Cleared
353    }
354
355    /// Returns true if all cache entries are None, else false
356    pub fn is_empty(&self) -> bool {
357        self.final_layout_entry.is_none() && !self.measure_entries.iter().any(|entry| entry.is_some())
358    }
359}
360
361/// Clear operation outcome. See [`Cache::clear`]
362pub enum ClearState {
363    /// Cleared some values
364    Cleared,
365    /// Everything was already cleared
366    AlreadyEmpty,
367}
368
369#[cfg(test)]
370mod tests {
371    use super::*;
372    use crate::geometry::Line;
373    use crate::tree::SizingMode;
374
375    fn input(width: f32) -> LayoutInput {
376        LayoutInput {
377            run_mode: RunMode::ComputeSize,
378            sizing_mode: SizingMode::InherentSize,
379            axis: RequestedAxis::Both,
380            known_dimensions: Size { width: Some(width), height: None },
381            known_dimensions_are_definite: Size { width: true, height: true },
382            parent_size: Size::NONE,
383            available_space: Size { width: AvailableSpace::MaxContent, height: AvailableSpace::MaxContent },
384            vertical_margins_are_collapsible: Line::FALSE,
385        }
386    }
387
388    fn output(width: f32) -> LayoutOutput {
389        LayoutOutput::from_outer_size(Size { width, height: width })
390    }
391
392    #[test]
393    fn recently_used_measure_entries_get_a_second_chance() {
394        let mut cache = Cache::new();
395        for width in 0..CACHE_SIZE {
396            cache.store(&input(width as f32), output(width as f32));
397        }
398        cache.store(&input(CACHE_SIZE as f32), output(CACHE_SIZE as f32));
399
400        assert_eq!(cache.get(&input(1.0)), Some(output(1.0)));
401        cache.store(&input((CACHE_SIZE + 1) as f32), output((CACHE_SIZE + 1) as f32));
402
403        assert_eq!(cache.get(&input(1.0)), Some(output(1.0)));
404        assert_eq!(cache.get(&input(2.0)), None);
405    }
406
407    #[test]
408    fn storing_an_existing_measurement_updates_it_in_place() {
409        let mut cache = Cache::new();
410        cache.store(&input(1.0), output(1.0));
411        cache.store(&input(2.0), output(2.0));
412        cache.store(&input(1.0), output(3.0));
413
414        assert_eq!(cache.measure_entries.iter().flatten().count(), 2);
415        assert_eq!(cache.get(&input(1.0)), Some(output(3.0)));
416    }
417
418    #[test]
419    fn measurements_with_margin_collapse_metadata_are_not_cached() {
420        let mut cache = Cache::new();
421
422        let mut collapse_through = output(1.0);
423        collapse_through.margins_can_collapse_through = true;
424        cache.store(&input(1.0), collapse_through);
425        assert_eq!(cache.get(&input(1.0)), None);
426
427        let mut carried_margin = output(2.0);
428        carried_margin.top_margin = CollapsibleMarginSet::from_margin(10.0);
429        cache.store(&input(2.0), carried_margin);
430        assert_eq!(cache.get(&input(2.0)), None);
431    }
432
433    #[test]
434    fn retrieving_a_measurement_only_marks_its_slot_as_used() {
435        let mut cache = Cache::new();
436        cache.store(&input(1.0), output(1.0));
437        cache.store(&input(2.0), output(2.0));
438        cache.recently_used_entries = 0;
439        let entries = cache.measure_entries.clone();
440
441        assert_eq!(cache.get(&input(1.0)), Some(output(1.0)));
442        assert_eq!(cache.measure_entries, entries);
443        assert_ne!(cache.recently_used_entries, 0);
444    }
445
446    fn perform_layout_input(width: f32, height: f32) -> LayoutInput {
447        LayoutInput {
448            run_mode: RunMode::PerformLayout,
449            known_dimensions: Size { width: Some(width), height: Some(height) },
450            parent_size: Size { width: Some(width), height: Some(height) },
451            ..input(width)
452        }
453    }
454
455    /// Inputs that differ from `input`/`perform_layout_input` only in their mode
456    fn other_modes(input: LayoutInput) -> [LayoutInput; 4] {
457        [
458            LayoutInput { sizing_mode: SizingMode::ContentSize, ..input },
459            LayoutInput { vertical_margins_are_collapsible: Line::TRUE, ..input },
460            LayoutInput { vertical_margins_are_collapsible: Line { start: true, end: false }, ..input },
461            LayoutInput { vertical_margins_are_collapsible: Line { start: false, end: true }, ..input },
462        ]
463    }
464
465    #[test]
466    fn cache_mode_distinguishes_every_combination_of_inputs() {
467        let mut modes = [CacheMode::INITIAL; 8];
468        for (index, mode) in modes.iter_mut().enumerate() {
469            *mode = CacheMode::from(&LayoutInput {
470                sizing_mode: if index & 1 == 0 { SizingMode::ContentSize } else { SizingMode::InherentSize },
471                vertical_margins_are_collapsible: Line { start: index & 2 != 0, end: index & 4 != 0 },
472                ..input(1.0)
473            });
474        }
475        for (index, mode) in modes.iter().enumerate() {
476            assert!(!modes[..index].contains(mode));
477        }
478    }
479
480    #[test]
481    fn retrieving_with_a_different_mode_misses_and_keeps_the_entries() {
482        let mut cache = Cache::new();
483        cache.store(&perform_layout_input(100.0, 50.0), output(1.0));
484        cache.store(&input(1.0), output(2.0));
485
486        for other in other_modes(perform_layout_input(100.0, 50.0)) {
487            assert_eq!(cache.get(&other), None);
488        }
489        for other in other_modes(input(1.0)) {
490            assert_eq!(cache.get(&other), None);
491        }
492
493        assert_eq!(cache.get(&perform_layout_input(100.0, 50.0)), Some(output(1.0)));
494        assert_eq!(cache.get(&input(1.0)), Some(output(2.0)));
495    }
496
497    #[test]
498    fn storing_with_a_different_mode_drops_the_existing_entries() {
499        for other in other_modes(input(3.0)) {
500            let mut cache = Cache::new();
501            cache.store(&perform_layout_input(100.0, 50.0), output(1.0));
502            cache.store(&input(1.0), output(2.0));
503
504            cache.store(&other, output(3.0));
505            assert_eq!(cache.get(&other), Some(output(3.0)));
506            assert!(cache.final_layout_entry.is_none());
507            assert_eq!(cache.measure_entries.iter().flatten().count(), 1);
508
509            // The old entries are gone, even for inputs that have the mode that the cache had before
510            cache.store(&input(4.0), output(4.0));
511            assert_eq!(cache.get(&perform_layout_input(100.0, 50.0)), None);
512            assert_eq!(cache.get(&input(1.0)), None);
513            assert_eq!(cache.get(&other), None);
514            assert_eq!(cache.get(&input(4.0)), Some(output(4.0)));
515        }
516
517        for other in other_modes(perform_layout_input(100.0, 50.0)) {
518            let mut cache = Cache::new();
519            cache.store(&perform_layout_input(100.0, 50.0), output(1.0));
520            cache.store(&input(1.0), output(2.0));
521
522            cache.store(&other, output(3.0));
523            assert_eq!(cache.get(&other), Some(output(3.0)));
524            assert!(cache.measure_entries.iter().all(|entry| entry.is_none()));
525            assert_eq!(cache.get(&perform_layout_input(100.0, 50.0)), None);
526        }
527    }
528
529    #[test]
530    fn hidden_layouts_do_not_affect_the_cache() {
531        let mut cache = Cache::new();
532        cache.store(&input(1.0), output(1.0));
533        cache.store(&LayoutInput::HIDDEN, output(2.0));
534        assert_eq!(cache.get(&input(1.0)), Some(output(1.0)));
535    }
536
537    #[test]
538    #[cfg(all(debug_assertions, feature = "std"))]
539    fn mode_change_evictions_are_counted_for_non_empty_caches_only() {
540        let content_size = LayoutInput { sizing_mode: SizingMode::ContentSize, ..input(1.0) };
541        let evictions = cache_mode_change_evictions();
542
543        let mut cache = Cache::new();
544        cache.store(&content_size, output(1.0));
545        cache.store(&LayoutInput { sizing_mode: SizingMode::ContentSize, ..input(2.0) }, output(2.0));
546        assert_eq!(cache_mode_change_evictions(), evictions);
547
548        cache.store(&input(1.0), output(1.0));
549        assert_eq!(cache_mode_change_evictions(), evictions + 1);
550
551        cache.clear();
552        cache.store(&content_size, output(1.0));
553        assert_eq!(cache_mode_change_evictions(), evictions + 1);
554    }
555}