selinux/
fifo_cache.rs

1// Copyright 2023 The Fuchsia Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5use indexmap::IndexMap;
6use std::hash::Hash;
7use std::ops::Add;
8
9#[cfg(test)]
10use indexmap::map::Iter;
11
12/// Describes the performance statistics of a cache implementation.
13#[derive(Default, Debug, Clone, PartialEq)]
14pub struct CacheStats {
15    /// Cumulative count of lookups performed on the cache.
16    pub lookups: u64,
17    /// Cumulative count of lookups that returned data from an existing cache entry.
18    pub hits: u64,
19    /// Cumulative count of lookups that did not match any existing cache entry.
20    pub misses: u64,
21    /// Cumulative count of insertions into the cache.
22    pub allocs: u64,
23    /// Cumulative count of evictions from the cache, to make space for a new insertion.
24    pub reclaims: u64,
25    /// Cumulative count of evictions from the cache due to no longer being deemed relevant.
26    /// This is not used in our current implementation.
27    pub frees: u64,
28}
29
30impl Add for &CacheStats {
31    type Output = CacheStats;
32
33    fn add(self, other: &CacheStats) -> CacheStats {
34        CacheStats {
35            lookups: self.lookups + other.lookups,
36            hits: self.hits + other.hits,
37            misses: self.misses + other.misses,
38            allocs: self.allocs + other.allocs,
39            reclaims: self.reclaims + other.reclaims,
40            frees: self.frees + other.frees,
41        }
42    }
43}
44
45/// An interface through which statistics may be obtained from each cache.
46pub trait HasCacheStats {
47    /// Returns statistics for the cache implementation.
48    fn cache_stats(&self) -> CacheStats;
49}
50
51/// Associative FIFO cache with capacity defined at creation.
52///
53/// Lookups in the cache are O(1), as are evictions.
54///
55/// This implementation is thread-hostile; it expects all operations to be executed on the same
56/// thread.
57pub(super) struct FifoCache<A: Hash + Eq, R> {
58    cache: IndexMap<A, R>,
59    capacity: usize,
60    oldest_index: usize,
61    stats: CacheStats,
62}
63
64impl<A: Hash + Eq, R> FifoCache<A, R> {
65    pub fn with_capacity(capacity: usize) -> Self {
66        assert!(capacity > 0, "cannot instantiate fixed access vector cache of size 0");
67
68        Self {
69            // Request `capacity` plus one element working-space for insertions that trigger
70            // an eviction.
71            cache: IndexMap::with_capacity(capacity + 1),
72            capacity,
73            oldest_index: 0,
74            stats: CacheStats::default(),
75        }
76    }
77
78    /// Returns true if the cache has reached capacity.
79    #[inline]
80    pub fn is_full(&self) -> bool {
81        self.cache.len() == self.capacity
82    }
83
84    /// Returns the capacity with which this instance was initialized.
85    pub fn capacity(&self) -> usize {
86        self.capacity
87    }
88
89    /// Searches the cache and returns the index of a [`QueryAndResult`] matching
90    /// the given `source_sid`, `target_sid`, and `target_class` (or returns `None`).
91    #[inline]
92    pub fn get(&mut self, query_args: &A) -> Option<&mut R> {
93        self.stats.lookups += 1;
94
95        let result = self.cache.get_mut(query_args);
96
97        if result.is_some() {
98            self.stats.hits += 1;
99        } else {
100            self.stats.misses += 1;
101        }
102
103        result
104    }
105
106    /// Inserts the specified `query` and `result` into the cache, evicting the oldest existing
107    /// entry if capacity has been reached.
108    #[inline]
109    pub fn insert(&mut self, query: A, result: R) -> &mut R {
110        self.stats.allocs += 1;
111
112        // If the cache is already full then it will be necessary to evict an existing entry.
113        // Eviction is performed after inserting the new entry, to allow the eviction operation to
114        // be implemented via swap-into-place.
115        let must_evict = self.is_full();
116
117        // Insert the entry, at the end of the `IndexMap` queue, then evict the oldest element.
118        let (mut index, _) = self.cache.insert_full(query, result);
119        if must_evict {
120            // The final element in the ordered container is the newly-added entry, so we can simply
121            // swap it with the oldest element, and then remove the final element, to achieve FIFO
122            // eviction.
123            assert_eq!(index, self.capacity);
124
125            self.cache.swap_remove_index(self.oldest_index);
126            self.stats.reclaims += 1;
127
128            index = self.oldest_index;
129
130            self.oldest_index += 1;
131            if self.oldest_index == self.capacity {
132                self.oldest_index = 0;
133            }
134        }
135
136        self.cache.get_index_mut(index).map(|(_, v)| v).expect("invalid index after insert!")
137    }
138
139    /// Returns an iterator over the cache elements, for use by tests.
140    #[cfg(test)]
141    #[allow(dead_code)] // Only used by `access_vector_cache.rs` tests when built for Fuchsia.
142    pub fn iter(&self) -> Iter<'_, A, R> {
143        self.cache.iter()
144    }
145}
146
147impl<A: Hash + Eq, R> HasCacheStats for FifoCache<A, R> {
148    fn cache_stats(&self) -> CacheStats {
149        self.stats.clone()
150    }
151}