tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

lru_cache.rs (23503B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 use crate::freelist::{FreeList, FreeListHandle, WeakFreeListHandle};
      6 use std::{mem, num};
      7 
      8 /*
      9  This module implements a least recently used cache structure, which is
     10  used by the texture cache to manage the lifetime of items inside the
     11  texture cache. It has a few special pieces of functionality that the
     12  texture cache requires, but should be usable as a general LRU cache
     13  type if useful in other areas.
     14 
     15  The cache is implemented with two types of backing freelists. These allow
     16  random access to the underlying data, while being efficient in both
     17  memory access and allocation patterns.
     18 
     19  The "entries" freelist stores the elements being cached (for example, the
     20  CacheEntry structure for the texture cache). These elements are stored
     21  in arbitrary order, reusing empty slots in the freelist where possible.
     22 
     23  The "lru_index" freelists store the LRU tracking information. Although the
     24  tracking elements are stored in arbitrary order inside a freelist for
     25  efficiency, they use next/prev links to represent a doubly-linked list,
     26  kept sorted in order of recent use. The next link is also used to store
     27  the current freelist within the array when the element is not occupied.
     28 
     29  The LRU cache allows having multiple LRU "partitions". Every entry is tracked
     30  by exactly one partition at any time; all partitions refer to entries in the
     31  shared freelist. Entries can move between partitions, if replace_or_insert is
     32  called with a new partition index for an existing handle.
     33  The partitioning is used by the texture cache so that, for example, allocating
     34  more glyph entries does not cause eviction of image entries (which go into
     35  a different shared texture). If an existing handle's entry is reallocated with
     36  a new size, it might need to move from a shared texture to a standalone
     37  texture; in this case the handle will move to a different LRU partition.
     38 */
     39 
     40 /// Stores the data supplied by the user to be cached, and an index
     41 /// into the LRU tracking freelist for this element.
     42 #[cfg_attr(feature = "capture", derive(Serialize))]
     43 #[cfg_attr(feature = "replay", derive(Deserialize))]
     44 #[derive(MallocSizeOf)]
     45 struct LRUCacheEntry<T> {
     46    /// The LRU partition that tracks this entry.
     47    partition_index: u8,
     48 
     49    /// The location of the LRU tracking element for this cache entry in the
     50    /// right LRU partition.
     51    lru_index: ItemIndex,
     52 
     53    /// The cached data provided by the caller for this element.
     54    value: T,
     55 }
     56 
     57 /// The main public interface to the LRU cache
     58 #[cfg_attr(feature = "capture", derive(Serialize))]
     59 #[cfg_attr(feature = "replay", derive(Deserialize))]
     60 #[derive(MallocSizeOf)]
     61 pub struct LRUCache<T, M> {
     62    /// A free list of cache entries, and indices into the LRU tracking list
     63    entries: FreeList<LRUCacheEntry<T>, M>,
     64    /// The LRU tracking list, allowing O(1) access to the oldest element
     65    lru: Vec<LRUTracker<FreeListHandle<M>>>,
     66 }
     67 
     68 impl<T, M> LRUCache<T, M> {
     69    /// Construct a new LRU cache
     70    pub fn new(lru_partition_count: usize) -> Self {
     71        assert!(lru_partition_count <= u8::MAX as usize + 1);
     72        LRUCache {
     73            entries: FreeList::new(),
     74            lru: (0..lru_partition_count).map(|_| LRUTracker::new()).collect(),
     75        }
     76    }
     77 
     78    /// Insert a new element into the cache. Returns a weak handle for callers to
     79    /// access the data, since the lifetime is managed by the LRU algorithm and it
     80    /// may be evicted at any time.
     81    pub fn push_new(
     82        &mut self,
     83        partition_index: u8,
     84        value: T,
     85    ) -> WeakFreeListHandle<M> {
     86        // It's a slightly awkward process to insert an element, since we don't know
     87        // the index of the LRU tracking element until we've got a handle for the
     88        // underlying cached data.
     89 
     90        // Insert the data provided by the caller
     91        let handle = self.entries.insert(LRUCacheEntry {
     92            partition_index: 0,
     93            lru_index: ItemIndex(num::NonZeroU32::new(1).unwrap()),
     94            value
     95        });
     96 
     97        // Get a weak handle to return to the caller
     98        let weak_handle = handle.weak();
     99 
    100        // Add an LRU tracking node that owns the strong handle, and store the location
    101        // of this inside the cache entry.
    102        let entry = self.entries.get_mut(&handle);
    103        let lru_index = self.lru[partition_index as usize].push_new(handle);
    104        entry.partition_index = partition_index;
    105        entry.lru_index = lru_index;
    106 
    107        weak_handle
    108    }
    109 
    110    /// Get immutable access to the data at a given slot. Since this takes a weak
    111    /// handle, it may have been evicted, so returns an Option.
    112    pub fn get_opt(
    113        &self,
    114        handle: &WeakFreeListHandle<M>,
    115    ) -> Option<&T> {
    116        self.entries
    117            .get_opt(handle)
    118            .map(|entry| {
    119                &entry.value
    120            })
    121    }
    122 
    123    /// Get mutable access to the data at a given slot. Since this takes a weak
    124    /// handle, it may have been evicted, so returns an Option.
    125    pub fn get_opt_mut(
    126        &mut self,
    127        handle: &WeakFreeListHandle<M>,
    128    ) -> Option<&mut T> {
    129        self.entries
    130            .get_opt_mut(handle)
    131            .map(|entry| {
    132                &mut entry.value
    133            })
    134    }
    135 
    136    /// Return a reference to the oldest item in the cache, keeping it in the cache.
    137    /// If the cache is empty, this will return None.
    138    pub fn peek_oldest(&self, partition_index: u8) -> Option<&T> {
    139        self.lru[partition_index as usize]
    140            .peek_front()
    141            .map(|handle| {
    142                let entry = self.entries.get(handle);
    143                &entry.value
    144            })
    145    }
    146 
    147    /// Remove the oldest item from the cache. This is used to select elements to
    148    /// be evicted. If the cache is empty, this will return None.
    149    pub fn pop_oldest(
    150        &mut self,
    151        partition_index: u8,
    152    ) -> Option<T> {
    153        self.lru[partition_index as usize]
    154            .pop_front()
    155            .map(|handle| {
    156                let entry = self.entries.free(handle);
    157                entry.value
    158            })
    159    }
    160 
    161    /// This is a special case of `push_new`, which is a requirement for the texture
    162    /// cache. Sometimes, we want to replace the content of an existing handle if it
    163    /// exists, or insert a new element if the handle is invalid (for example, if an
    164    /// image is resized and it moves to a new location in the texture atlas). This
    165    /// method returns the old cache entry if it existed, so it can be freed by the caller.
    166    #[must_use]
    167    pub fn replace_or_insert(
    168        &mut self,
    169        handle: &mut WeakFreeListHandle<M>,
    170        partition_index: u8,
    171        data: T,
    172    ) -> Option<T> {
    173        match self.entries.get_opt_mut(handle) {
    174            Some(entry) => {
    175                if entry.partition_index != partition_index {
    176                    // Move to a different partition.
    177                    let strong_handle = self.lru[entry.partition_index as usize].remove(entry.lru_index);
    178                    let lru_index = self.lru[partition_index as usize].push_new(strong_handle);
    179                    entry.partition_index = partition_index;
    180                    entry.lru_index = lru_index;
    181                }
    182                Some(mem::replace(&mut entry.value, data))
    183            }
    184            None => {
    185                *handle = self.push_new(partition_index, data);
    186                None
    187            }
    188        }
    189    }
    190 
    191    /// Manually evict a specific item.
    192    pub fn remove(&mut self, handle: &WeakFreeListHandle<M>) -> Option<T> {
    193        if let Some(entry) = self.entries.get_opt_mut(handle) {
    194            let strong_handle = self.lru[entry.partition_index as usize].remove(entry.lru_index);
    195            return Some(self.entries.free(strong_handle).value);
    196        }
    197 
    198        None
    199    }
    200 
    201    /// This is used by the calling code to signal that the element that this handle
    202    /// references has been used on this frame. Internally, it updates the links in
    203    /// the LRU tracking element to move this item to the end of the LRU list. Returns
    204    /// the underlying data in case the client wants to mutate it.
    205    pub fn touch(
    206        &mut self,
    207        handle: &WeakFreeListHandle<M>,
    208    ) -> Option<&mut T> {
    209        let lru = &mut self.lru;
    210 
    211        self.entries
    212            .get_opt_mut(handle)
    213            .map(|entry| {
    214                lru[entry.partition_index as usize].mark_used(entry.lru_index);
    215                &mut entry.value
    216            })
    217    }
    218 
    219    /// Try to validate that the state of the cache is consistent
    220    #[cfg(test)]
    221    fn validate(&self) {
    222        for lru in &self.lru {
    223            lru.validate();
    224        }
    225    }
    226 }
    227 
    228 /// Index of an LRU tracking element
    229 #[cfg_attr(feature = "capture", derive(Serialize))]
    230 #[cfg_attr(feature = "replay", derive(Deserialize))]
    231 #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, MallocSizeOf)]
    232 struct ItemIndex(num::NonZeroU32);
    233 
    234 impl ItemIndex {
    235    fn as_usize(&self) -> usize {
    236        self.0.get() as usize
    237    }
    238 }
    239 
    240 /// Stores a strong handle controlling the lifetime of the data in the LRU
    241 /// cache, and a doubly-linked list node specifying where in the current LRU
    242 /// order this element exists. These items are themselves backed by a freelist
    243 /// to minimize heap allocations and improve cache access patterns.
    244 #[cfg_attr(feature = "capture", derive(Serialize))]
    245 #[cfg_attr(feature = "replay", derive(Deserialize))]
    246 #[derive(Debug, MallocSizeOf)]
    247 struct Item<H> {
    248    prev: Option<ItemIndex>,
    249    next: Option<ItemIndex>,
    250    handle: Option<H>,
    251 }
    252 
    253 /// Internal implementation of the LRU tracking list
    254 #[cfg_attr(feature = "capture", derive(Serialize))]
    255 #[cfg_attr(feature = "replay", derive(Deserialize))]
    256 #[derive(MallocSizeOf)]
    257 struct LRUTracker<H> {
    258    /// Current head of the list - this is the oldest item that will be evicted next.
    259    head: Option<ItemIndex>,
    260    /// Current tail of the list - this is the most recently used element.
    261    tail: Option<ItemIndex>,
    262    /// As tracking items are removed, they are stored in a freelist, to minimize heap allocations
    263    free_list_head: Option<ItemIndex>,
    264    /// The freelist that stores all the LRU tracking items
    265    items: Vec<Item<H>>,
    266 }
    267 
    268 impl<H> LRUTracker<H> where H: std::fmt::Debug {
    269    /// Construct a new LRU tracker
    270    fn new() -> Self {
    271        // Push a dummy entry in the vec that is never used. This ensures the NonZeroU32
    272        // property is respected, and we never create an ItemIndex(0).
    273        let items = vec![
    274            Item {
    275                prev: None,
    276                next: None,
    277                handle: None,
    278            },
    279        ];
    280 
    281        LRUTracker {
    282            head: None,
    283            tail: None,
    284            free_list_head: None,
    285            items,
    286        }
    287    }
    288 
    289    /// Internal function that takes an item index, and links it to the
    290    /// end of the tracker list (makes it the newest item).
    291    fn link_as_new_tail(
    292        &mut self,
    293        item_index: ItemIndex,
    294    ) {
    295        match (self.head, self.tail) {
    296            (Some(..), Some(tail)) => {
    297                // Both a head and a tail
    298                self.items[item_index.as_usize()].prev = Some(tail);
    299                self.items[item_index.as_usize()].next = None;
    300 
    301                self.items[tail.as_usize()].next = Some(item_index);
    302                self.tail = Some(item_index);
    303            }
    304            (None, None) => {
    305                // No head/tail, currently empty list
    306                self.items[item_index.as_usize()].prev = None;
    307                self.items[item_index.as_usize()].next = None;
    308 
    309                self.head = Some(item_index);
    310                self.tail = Some(item_index);
    311            }
    312            (Some(..), None) | (None, Some(..)) => {
    313                // Invalid state
    314                unreachable!();
    315            }
    316        }
    317    }
    318 
    319    /// Internal function that takes an LRU item index, and removes it from
    320    /// the current doubly linked list. Used during removal of items, and also
    321    /// when items are moved to the back of the list as they're touched.
    322    fn unlink(
    323        &mut self,
    324        item_index: ItemIndex,
    325    ) {
    326        let (next, prev) = {
    327            let item = &self.items[item_index.as_usize()];
    328            (item.next, item.prev)
    329        };
    330 
    331        match next {
    332            Some(next) => {
    333                self.items[next.as_usize()].prev = prev;
    334            }
    335            None => {
    336                debug_assert_eq!(self.tail, Some(item_index));
    337                self.tail = prev;
    338            }
    339        }
    340 
    341        match prev {
    342            Some(prev) => {
    343                self.items[prev.as_usize()].next = next;
    344            }
    345            None => {
    346                debug_assert_eq!(self.head, Some(item_index));
    347                self.head = next;
    348            }
    349        }
    350    }
    351 
    352    /// Push a new LRU tracking item on to the back of the list, marking
    353    /// it as the most recent item.
    354    fn push_new(
    355        &mut self,
    356        handle: H,
    357    ) -> ItemIndex {
    358        // See if there is a slot available in the current free list
    359        let item_index = match self.free_list_head {
    360            Some(index) => {
    361                // Reuse an existing slot
    362                let item = &mut self.items[index.as_usize()];
    363 
    364                assert!(item.handle.is_none());
    365                item.handle = Some(handle);
    366 
    367                self.free_list_head = item.next;
    368 
    369                index
    370            }
    371            None => {
    372                // No free slots available, push to the end of the array
    373                let index = ItemIndex(num::NonZeroU32::new(self.items.len() as u32).unwrap());
    374 
    375                self.items.push(Item {
    376                    prev: None,
    377                    next: None,
    378                    handle: Some(handle),
    379                });
    380 
    381                index
    382            }
    383        };
    384 
    385        // Now link this element into the LRU list
    386        self.link_as_new_tail(item_index);
    387 
    388        item_index
    389    }
    390 
    391    /// Returns a reference to the oldest element, or None if the list is empty.
    392    fn peek_front(&self) -> Option<&H> {
    393        self.head.map(|head| self.items[head.as_usize()].handle.as_ref().unwrap())
    394    }
    395 
    396    /// Remove the oldest element from the front of the LRU list. Returns None
    397    /// if the list is empty.
    398    fn pop_front(
    399        &mut self,
    400    ) -> Option<H> {
    401        let handle = match (self.head, self.tail) {
    402            (Some(head), Some(tail)) => {
    403                let item_index = head;
    404 
    405                // Head and tail are the same - removing the only element
    406                if head == tail {
    407                    self.head = None;
    408                    self.tail = None;
    409                } else {
    410                    // Update the head of the list, popping the first element off
    411                    let new_head = self.items[head.as_usize()].next.unwrap();
    412                    self.head = Some(new_head);
    413                    self.items[new_head.as_usize()].prev = None;
    414                }
    415 
    416                // Add this item to the freelist for later use
    417                self.items[item_index.as_usize()].next = self.free_list_head;
    418                self.free_list_head = Some(item_index);
    419 
    420                // Return the handle to the user
    421                Some(self.items[item_index.as_usize()].handle.take().unwrap())
    422            }
    423            (None, None) => {
    424                // List is empty
    425                None
    426            }
    427            (Some(..), None) | (None, Some(..)) => {
    428                // Invalid state
    429                unreachable!();
    430            }
    431        };
    432 
    433        handle
    434    }
    435 
    436    /// Manually remove an item from the LRU tracking list. This is used
    437    /// when an element switches from one LRU partition to a different one.
    438    fn remove(
    439        &mut self,
    440        index: ItemIndex,
    441    ) -> H {
    442        // Remove from the LRU list
    443        self.unlink(index);
    444 
    445        let handle = self.items[index.as_usize()].handle.take().unwrap();
    446 
    447        // Add LRU item to the freelist for future use.
    448        self.items[index.as_usize()].next = self.free_list_head;
    449        self.free_list_head = Some(index);
    450 
    451        handle
    452    }
    453 
    454    /// Called to mark that an item was used on this frame. It unlinks the
    455    /// tracking item, and then re-links it to the back of the list.
    456    fn mark_used(
    457        &mut self,
    458        index: ItemIndex,
    459    ) {
    460        self.unlink(index);
    461        self.link_as_new_tail(index);
    462    }
    463 
    464    /// Try to validate that the state of the linked lists are consistent
    465    #[cfg(test)]
    466    fn validate(&self) {
    467        use std::collections::HashSet;
    468 
    469        // Must have a valid head/tail or be empty
    470        assert!((self.head.is_none() && self.tail.is_none()) || (self.head.is_some() && self.tail.is_some()));
    471 
    472        // If there is a head, the prev of the head must be none
    473        if let Some(head) = self.head {
    474            assert!(self.items[head.as_usize()].prev.is_none());
    475        }
    476 
    477        // If there is a tail, the next of the tail must be none
    478        if let Some(tail) = self.tail {
    479            assert!(self.items[tail.as_usize()].next.is_none());
    480        }
    481 
    482        // Collect all free and valid items, both in forwards and reverse order
    483        let mut free_items = Vec::new();
    484        let mut free_items_set = HashSet::new();
    485        let mut valid_items_front = Vec::new();
    486        let mut valid_items_front_set = HashSet::new();
    487        let mut valid_items_reverse = Vec::new();
    488        let mut valid_items_reverse_set = HashSet::new();
    489 
    490        let mut current = self.free_list_head;
    491        while let Some(index) = current {
    492            let item = &self.items[index.as_usize()];
    493            free_items.push(index);
    494            assert!(free_items_set.insert(index));
    495            current = item.next;
    496        }
    497 
    498        current = self.head;
    499        while let Some(index) = current {
    500            let item = &self.items[index.as_usize()];
    501            valid_items_front.push(index);
    502            assert!(valid_items_front_set.insert(index));
    503            current = item.next;
    504        }
    505 
    506        current = self.tail;
    507        while let Some(index) = current {
    508            let item = &self.items[index.as_usize()];
    509            valid_items_reverse.push(index);
    510            assert!(!valid_items_reverse_set.contains(&index));
    511            valid_items_reverse_set.insert(index);
    512            current = item.prev;
    513        }
    514 
    515        // Ensure set lengths match the vec lengths (should be enforced by the assert check during insert anyway)
    516        assert_eq!(valid_items_front.len(), valid_items_front_set.len());
    517        assert_eq!(valid_items_reverse.len(), valid_items_reverse_set.len());
    518 
    519        // Length of the array should equal free + valid items count + 1 (dummy entry)
    520        assert_eq!(free_items.len() + valid_items_front.len() + 1, self.items.len());
    521 
    522        // Should be same number of items whether iterating forwards or reverse
    523        assert_eq!(valid_items_front.len(), valid_items_reverse.len());
    524 
    525        // Ensure there are no items considered in the free list that are also in the valid list
    526        assert!(free_items_set.intersection(&valid_items_reverse_set).collect::<HashSet<_>>().is_empty());
    527        assert!(free_items_set.intersection(&valid_items_front_set).collect::<HashSet<_>>().is_empty());
    528 
    529        // Should be the same number of items regardless of iteration direction
    530        assert_eq!(valid_items_front_set.len(), valid_items_reverse_set.len());
    531 
    532        // Ensure that the ordering is exactly the same, regardless of iteration direction
    533        for (i0, i1) in valid_items_front.iter().zip(valid_items_reverse.iter().rev()) {
    534            assert_eq!(i0, i1);
    535        }
    536    }
    537 }
    538 
    539 #[test]
    540 fn test_lru_tracker_push_peek() {
    541    // Push elements, peek and ensure:
    542    // - peek_oldest returns None before first element pushed
    543    // - peek_oldest returns oldest element
    544    // - subsequent calls to peek_oldest return same element (nothing was removed)
    545    struct CacheMarker;
    546    const NUM_ELEMENTS: usize = 50;
    547 
    548    let mut cache: LRUCache<usize, CacheMarker> = LRUCache::new(1);
    549    cache.validate();
    550 
    551    assert_eq!(cache.peek_oldest(0), None);
    552 
    553    for i in 0 .. NUM_ELEMENTS {
    554        cache.push_new(0, i);
    555    }
    556    cache.validate();
    557 
    558    assert_eq!(cache.peek_oldest(0), Some(&0));
    559    assert_eq!(cache.peek_oldest(0), Some(&0));
    560 
    561    cache.pop_oldest(0);
    562    assert_eq!(cache.peek_oldest(0), Some(&1));
    563 }
    564 
    565 #[test]
    566 fn test_lru_tracker_push_pop() {
    567    // Push elements, pop them all off and ensure:
    568    // - Returned in oldest order
    569    // - pop_oldest returns None after last element popped
    570    struct CacheMarker;
    571    const NUM_ELEMENTS: usize = 50;
    572 
    573    let mut cache: LRUCache<usize, CacheMarker> = LRUCache::new(1);
    574    cache.validate();
    575 
    576    for i in 0 .. NUM_ELEMENTS {
    577        cache.push_new(0, i);
    578    }
    579    cache.validate();
    580 
    581    for i in 0 .. NUM_ELEMENTS {
    582        assert_eq!(cache.pop_oldest(0), Some(i));
    583    }
    584    cache.validate();
    585 
    586    assert_eq!(cache.pop_oldest(0), None);
    587 }
    588 
    589 #[test]
    590 fn test_lru_tracker_push_touch_pop() {
    591    // Push elements, touch even handles, pop them all off and ensure:
    592    // - Returned in correct order
    593    // - pop_oldest returns None after last element popped
    594    struct CacheMarker;
    595    const NUM_ELEMENTS: usize = 50;
    596 
    597    let mut cache: LRUCache<usize, CacheMarker> = LRUCache::new(1);
    598    let mut handles = Vec::new();
    599    cache.validate();
    600 
    601    for i in 0 .. NUM_ELEMENTS {
    602        handles.push(cache.push_new(0, i));
    603    }
    604    cache.validate();
    605 
    606    for i in 0 .. NUM_ELEMENTS/2 {
    607        cache.touch(&handles[i*2]);
    608    }
    609    cache.validate();
    610 
    611    for i in 0 .. NUM_ELEMENTS/2 {
    612        assert_eq!(cache.pop_oldest(0), Some(i*2+1));
    613    }
    614    cache.validate();
    615    for i in 0 .. NUM_ELEMENTS/2 {
    616        assert_eq!(cache.pop_oldest(0), Some(i*2));
    617    }
    618    cache.validate();
    619 
    620    assert_eq!(cache.pop_oldest(0), None);
    621 }
    622 
    623 #[test]
    624 fn test_lru_tracker_push_get() {
    625    // Push elements, ensure:
    626    // - get access via weak handles works
    627    struct CacheMarker;
    628    const NUM_ELEMENTS: usize = 50;
    629 
    630    let mut cache: LRUCache<usize, CacheMarker> = LRUCache::new(1);
    631    let mut handles = Vec::new();
    632    cache.validate();
    633 
    634    for i in 0 .. NUM_ELEMENTS {
    635        handles.push(cache.push_new(0, i));
    636    }
    637    cache.validate();
    638 
    639    for i in 0 .. NUM_ELEMENTS/2 {
    640        assert!(cache.get_opt(&handles[i]) == Some(&i));
    641    }
    642    cache.validate();
    643 }
    644 
    645 #[test]
    646 fn test_lru_tracker_push_replace_get() {
    647    // Push elements, replace contents, ensure:
    648    // - each element was replaced with new data correctly
    649    // - replace_or_insert works for invalid handles
    650    struct CacheMarker;
    651    const NUM_ELEMENTS: usize = 50;
    652 
    653    let mut cache: LRUCache<usize, CacheMarker> = LRUCache::new(1);
    654    let mut handles = Vec::new();
    655    cache.validate();
    656 
    657    for i in 0 .. NUM_ELEMENTS {
    658        handles.push(cache.push_new(0, i));
    659    }
    660    cache.validate();
    661 
    662    for i in 0 .. NUM_ELEMENTS {
    663        assert_eq!(cache.replace_or_insert(&mut handles[i], 0, i * 2), Some(i));
    664    }
    665    cache.validate();
    666 
    667    for i in 0 .. NUM_ELEMENTS/2 {
    668        assert!(cache.get_opt(&handles[i]) == Some(&(i * 2)));
    669    }
    670    cache.validate();
    671 
    672    let mut empty_handle = WeakFreeListHandle::invalid();
    673    assert_eq!(cache.replace_or_insert(&mut empty_handle, 0, 100), None);
    674    assert_eq!(cache.get_opt(&empty_handle), Some(&100));
    675 }