Skip to main content

ntex_bytes/
cache.rs

1//! Page cache statistics.
2use std::sync::{Arc, atomic::AtomicU64, atomic::Ordering::Relaxed};
3
4use crate::{BytePageSize, size::PAGE_SIZES};
5
6const CLASSES: usize = PAGE_SIZES.len();
7
8/// Page cache statistics.
9///
10/// Each thread has its own page cache, see [`crate::set_page_cache_size`].
11/// The cache reports to the metrics that are current on its thread, see
12/// [`PageCacheMetrics::current`]. Statistics are updated with relaxed atomics,
13/// a snapshot is not a consistent cut across all fields.
14///
15/// Updates are a plain load and store, not an atomic read-modify-write, so
16/// metrics must be updated by one thread at a time. If the same metrics are
17/// installed on several threads with [`PageCacheMetrics::set_current`],
18/// concurrent updates can be lost, counters undercount and the `pages` and
19/// `bytes` gauges drift permanently.
20///
21/// A page released on another thread goes to the cache of that thread, so
22/// allocations and releases of the same page can be reported to different
23/// metrics.
24#[derive(Debug, Default)]
25pub struct PageCacheMetrics {
26    classes: [Class; CLASSES],
27}
28
29#[derive(Debug, Default)]
30struct Class {
31    hits: AtomicU64,
32    misses: AtomicU64,
33    cached: AtomicU64,
34    freed: AtomicU64,
35    // pages that entered or left the cache other than by `cached` and `hits`,
36    // the pages gauge is derived, so the hot paths update one counter each
37    added: AtomicU64,
38    removed: AtomicU64,
39}
40
41/// A snapshot of [`PageCacheMetrics`].
42#[derive(Copy, Clone, Debug, PartialEq, Eq)]
43pub struct PageCacheSnapshot {
44    sizes: [PageCacheStats; CLASSES],
45}
46
47/// Page cache statistics of one page size.
48#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
49#[non_exhaustive]
50pub struct PageCacheStats {
51    /// Page size, [`BytePageSize::Unset`] for the totals of all page sizes.
52    pub size: BytePageSize,
53    /// Number of pages taken from the cache.
54    pub hits: u64,
55    /// Number of pages allocated because the cache was empty.
56    pub misses: u64,
57    /// Number of released pages returned to the cache.
58    pub cached: u64,
59    /// Number of released pages freed because the cache was full.
60    pub freed: u64,
61    /// Number of pages in the cache.
62    pub pages: usize,
63    /// Memory held by the pages in the cache, in bytes.
64    pub bytes: usize,
65}
66
67impl PageCacheMetrics {
68    /// Creates new metrics.
69    pub fn new() -> Arc<Self> {
70        Arc::default()
71    }
72
73    /// Returns the metrics of the current thread's page cache.
74    ///
75    /// Each thread has its own metrics, created with the cache, unless other
76    /// metrics were installed with [`PageCacheMetrics::set_current`].
77    pub fn current() -> Arc<Self> {
78        crate::stvec::cache_metrics().unwrap_or_default()
79    }
80
81    /// Installs `metrics` as the metrics of the current thread's page cache.
82    ///
83    /// The pages held by the cache are moved from the previous metrics to
84    /// `metrics`. Metrics must not be current on more than one thread at a
85    /// time, updates are not atomic read-modify-writes.
86    pub fn set_current(metrics: Arc<Self>) {
87        crate::stvec::set_cache_metrics(metrics);
88    }
89
90    /// Returns a snapshot of the statistics.
91    pub fn snapshot(&self) -> PageCacheSnapshot {
92        let mut snapshot = PageCacheSnapshot::default();
93        for (stats, class) in snapshot.sizes.iter_mut().zip(&self.classes) {
94            stats.hits = class.hits.load(Relaxed);
95            stats.misses = class.misses.load(Relaxed);
96            stats.cached = class.cached.load(Relaxed);
97            stats.freed = class.freed.load(Relaxed);
98            let removed = class.removed.load(Relaxed);
99            let hits = stats.hits;
100            // fields are read one by one, a racing update can make it negative
101            stats.pages =
102                (stats.cached + class.added.load(Relaxed)).saturating_sub(hits + removed) as usize;
103            stats.bytes = stats.pages * stats.size.alloc_size();
104        }
105        snapshot
106    }
107
108    pub(crate) fn hit(&self, class: usize) {
109        inc(&self.classes[class].hits, 1);
110    }
111
112    pub(crate) fn miss(&self, class: usize) {
113        inc(&self.classes[class].misses, 1);
114    }
115
116    pub(crate) fn cached(&self, class: usize) {
117        inc(&self.classes[class].cached, 1);
118    }
119
120    pub(crate) fn freed(&self, class: usize) {
121        inc(&self.classes[class].freed, 1);
122    }
123
124    /// Pages leave the cache without being taken, e.g. the cache is dropped.
125    pub(crate) fn remove_pages(&self, class: usize, pages: usize) {
126        if pages != 0 {
127            inc(&self.classes[class].removed, pages as u64);
128        }
129    }
130
131    pub(crate) fn add_pages(&self, class: usize, pages: usize) {
132        if pages != 0 {
133            inc(&self.classes[class].added, pages as u64);
134        }
135    }
136}
137
138// single writer, cheaper than `fetch_add`
139#[inline]
140fn inc(counter: &AtomicU64, n: u64) {
141    counter.store(counter.load(Relaxed).wrapping_add(n), Relaxed);
142}
143
144impl PageCacheSnapshot {
145    /// Returns the statistics of page size `size`, `None` for
146    /// [`BytePageSize::Unset`].
147    pub fn get(&self, size: BytePageSize) -> Option<&PageCacheStats> {
148        self.sizes.get(size as usize)
149    }
150
151    /// Returns the statistics of every page size, in increasing order of size.
152    pub fn iter(&self) -> impl Iterator<Item = &PageCacheStats> {
153        self.sizes.iter()
154    }
155
156    /// Returns the totals of all page sizes.
157    pub fn total(&self) -> PageCacheStats {
158        self.sizes.iter().fold(
159            PageCacheStats {
160                size: BytePageSize::Unset,
161                ..PageCacheStats::default()
162            },
163            |mut total, s| {
164                total.hits += s.hits;
165                total.misses += s.misses;
166                total.cached += s.cached;
167                total.freed += s.freed;
168                total.pages += s.pages;
169                total.bytes += s.bytes;
170                total
171            },
172        )
173    }
174}
175
176impl Default for PageCacheSnapshot {
177    fn default() -> Self {
178        Self {
179            sizes: PAGE_SIZES.map(|size| PageCacheStats {
180                size,
181                ..PageCacheStats::default()
182            }),
183        }
184    }
185}