Skip to main content

ntex_bytes/
cache.rs

1//! Per-thread page cache and its statistics.
2use std::cell::Cell;
3use std::sync::{Arc, atomic::AtomicU64, atomic::Ordering::Relaxed};
4
5use crate::stvec::{Header, StorageVec};
6use crate::{BytePageSize, size::PAGE_SIZES};
7
8/// Number of page sizes, `BytePageSize::Unset` excluded.
9const CLASSES: usize = PAGE_SIZES.len();
10const _: () = assert!(CLASSES == BytePageSize::Unset as usize);
11
12/// Default number of cached pages per page size, fewer pages are cached
13/// for larger sizes. Reads start at 4 KiB pages and adapt up to 64 KiB
14/// pages under load, 16 KiB is the default page size of writes.
15const DEFAULT_PAGES_CACHE: [usize; CLASSES] = [128, 64, 64, 32, 16, 8, 16, 2, 1];
16
17thread_local! {
18    static CACHE: Cell<Option<Box<Cache>>> = Cell::new(Some(Box::default()));
19}
20
21struct Cache {
22    limits: [usize; CLASSES],
23    pages: [Vec<StorageVec>; CLASSES],
24    metrics: Arc<PageCacheMetrics>,
25}
26
27impl Default for Cache {
28    fn default() -> Self {
29        Self {
30            limits: DEFAULT_PAGES_CACHE,
31            pages: Default::default(),
32            metrics: PageCacheMetrics::new(),
33        }
34    }
35}
36
37impl Drop for Cache {
38    fn drop(&mut self) {
39        for (class, pages) in self.pages.iter().enumerate() {
40            self.metrics.remove_pages(class, pages.len());
41        }
42    }
43}
44
45impl Cache {
46    /// Sets the limit of `class`, the pages above the limit are released.
47    fn set_limit(&mut self, class: usize, limit: usize) {
48        self.limits[class] = limit;
49        let pages = &mut self.pages[class];
50        if pages.len() > limit {
51            self.metrics.remove_pages(class, pages.len() - limit);
52            // the cache is taken out of the thread-local, the released pages
53            // are freed instead of being cached again
54            pages.truncate(limit);
55        }
56    }
57}
58
59/// Runs `f` with the cache of the current thread.
60///
61/// Returns `None` if the cache is in use or the thread-local is being
62/// destroyed.
63#[inline]
64fn with_cache<R>(f: impl FnOnce(&mut Cache) -> R) -> Option<R> {
65    CACHE
66        .try_with(|c| {
67            let mut cst = c.take()?;
68            let res = f(&mut cst);
69            c.set(Some(cst));
70            Some(res)
71        })
72        .ok()
73        .flatten()
74}
75
76/// Takes a page of size `size` from the cache.
77#[inline]
78pub(crate) fn take_page(size: BytePageSize) -> Option<StorageVec> {
79    let class = size as usize;
80    with_cache(|cst| {
81        let page = cst.pages[class].pop();
82        if page.is_some() {
83            cst.metrics.hit(class);
84        } else {
85            cst.metrics.miss(class);
86        }
87        page
88    })
89    .flatten()
90}
91
92/// Puts a released page of size `size` to the cache.
93///
94/// Returns `false` if the page is not cached and must be freed.
95///
96/// # Safety
97///
98/// `ptr` must point to a live `Header` of page size `size` without
99/// references.
100#[inline]
101pub(crate) unsafe fn put_page(ptr: *mut Header, size: BytePageSize) -> bool {
102    let class = size as usize;
103    with_cache(|cst| {
104        if cst.pages[class].len() < cst.limits[class] {
105            cst.pages[class].push(unsafe { StorageVec::recycle(ptr) });
106            cst.metrics.cached(class);
107            true
108        } else {
109            cst.metrics.freed(class);
110            false
111        }
112    })
113    .unwrap_or(false)
114}
115
116/// Sets the cache limit of every page size, see [`crate::set_pages_cache`].
117pub(crate) fn set_pages_cache(count: usize) {
118    with_cache(|cst| {
119        for class in 0..CLASSES {
120            cst.set_limit(class, count);
121        }
122    });
123}
124
125/// Sets the cache limit of page size `size`, see [`crate::set_page_cache_size`].
126pub(crate) fn set_page_cache_size(size: BytePageSize, count: usize) {
127    if size != BytePageSize::Unset {
128        with_cache(|cst| cst.set_limit(size as usize, count));
129    }
130}
131
132#[cfg(test)]
133/// Replaces the cache of the current thread with an empty one.
134pub(crate) fn reset() {
135    CACHE.with(|c| c.set(Some(Box::default())));
136}
137
138#[cfg(test)]
139/// Returns the number of cached pages of size `size`.
140pub(crate) fn cached_pages(size: BytePageSize) -> usize {
141    with_cache(|cst| cst.pages[size as usize].len()).unwrap()
142}
143
144/// Page cache statistics.
145///
146/// Each thread has its own page cache, see [`crate::set_page_cache_size`].
147/// The cache reports to the metrics that are current on its thread, see
148/// [`PageCacheMetrics::current`]. Statistics are updated with relaxed atomics,
149/// a snapshot is not a consistent cut across all fields.
150///
151/// Updates are a plain load and store, not an atomic read-modify-write, so
152/// metrics must be updated by one thread at a time. If the same metrics are
153/// installed on several threads with [`PageCacheMetrics::set_current`],
154/// concurrent updates can be lost, counters undercount and the `pages` and
155/// `bytes` gauges drift permanently.
156///
157/// A page released on another thread goes to the cache of that thread, so
158/// allocations and releases of the same page can be reported to different
159/// metrics.
160#[derive(Debug, Default)]
161pub struct PageCacheMetrics {
162    classes: [Class; CLASSES],
163}
164
165#[derive(Debug, Default)]
166struct Class {
167    hits: AtomicU64,
168    misses: AtomicU64,
169    cached: AtomicU64,
170    freed: AtomicU64,
171    // pages that entered or left the cache other than by `cached` and `hits`,
172    // the pages gauge is derived, so the hot paths update one counter each
173    added: AtomicU64,
174    removed: AtomicU64,
175}
176
177/// A snapshot of [`PageCacheMetrics`].
178#[derive(Copy, Clone, Debug, PartialEq, Eq)]
179pub struct PageCacheSnapshot {
180    sizes: [PageCacheStats; CLASSES],
181}
182
183/// Page cache statistics of one page size.
184#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
185#[non_exhaustive]
186pub struct PageCacheStats {
187    /// Page size, [`BytePageSize::Unset`] for the totals of all page sizes.
188    pub size: BytePageSize,
189    /// Number of pages taken from the cache.
190    pub hits: u64,
191    /// Number of pages allocated because the cache was empty.
192    pub misses: u64,
193    /// Number of released pages returned to the cache.
194    pub cached: u64,
195    /// Number of released pages freed because the cache was full.
196    pub freed: u64,
197    /// Number of pages in the cache.
198    pub pages: usize,
199    /// Memory held by the pages in the cache, in bytes.
200    pub bytes: usize,
201}
202
203impl PageCacheMetrics {
204    /// Creates new metrics.
205    pub fn new() -> Arc<Self> {
206        Arc::default()
207    }
208
209    /// Returns the metrics of the current thread's page cache.
210    ///
211    /// Each thread has its own metrics, created with the cache, unless other
212    /// metrics were installed with [`PageCacheMetrics::set_current`].
213    pub fn current() -> Arc<Self> {
214        with_cache(|cst| cst.metrics.clone()).unwrap_or_default()
215    }
216
217    /// Installs `metrics` as the metrics of the current thread's page cache.
218    ///
219    /// The pages held by the cache are moved from the previous metrics to
220    /// `metrics`. Metrics must not be current on more than one thread at a
221    /// time, updates are not atomic read-modify-writes.
222    pub fn set_current(metrics: Arc<Self>) {
223        with_cache(|cst| {
224            for (class, pages) in cst.pages.iter().enumerate() {
225                cst.metrics.remove_pages(class, pages.len());
226                metrics.add_pages(class, pages.len());
227            }
228            cst.metrics = metrics;
229        });
230    }
231
232    /// Returns a snapshot of the statistics.
233    pub fn snapshot(&self) -> PageCacheSnapshot {
234        let mut snapshot = PageCacheSnapshot::default();
235        for (stats, class) in snapshot.sizes.iter_mut().zip(&self.classes) {
236            stats.hits = class.hits.load(Relaxed);
237            stats.misses = class.misses.load(Relaxed);
238            stats.cached = class.cached.load(Relaxed);
239            stats.freed = class.freed.load(Relaxed);
240            let removed = class.removed.load(Relaxed);
241            let hits = stats.hits;
242            // fields are read one by one, a racing update can make it negative
243            stats.pages =
244                (stats.cached + class.added.load(Relaxed)).saturating_sub(hits + removed) as usize;
245            stats.bytes = stats.pages * stats.size.alloc_size();
246        }
247        snapshot
248    }
249
250    pub(crate) fn hit(&self, class: usize) {
251        inc(&self.classes[class].hits, 1);
252    }
253
254    pub(crate) fn miss(&self, class: usize) {
255        inc(&self.classes[class].misses, 1);
256    }
257
258    pub(crate) fn cached(&self, class: usize) {
259        inc(&self.classes[class].cached, 1);
260    }
261
262    pub(crate) fn freed(&self, class: usize) {
263        inc(&self.classes[class].freed, 1);
264    }
265
266    /// Pages leave the cache without being taken, e.g. the cache is dropped.
267    pub(crate) fn remove_pages(&self, class: usize, pages: usize) {
268        if pages != 0 {
269            inc(&self.classes[class].removed, pages as u64);
270        }
271    }
272
273    pub(crate) fn add_pages(&self, class: usize, pages: usize) {
274        if pages != 0 {
275            inc(&self.classes[class].added, pages as u64);
276        }
277    }
278}
279
280// single writer, cheaper than `fetch_add`
281#[inline]
282fn inc(counter: &AtomicU64, n: u64) {
283    counter.store(counter.load(Relaxed).wrapping_add(n), Relaxed);
284}
285
286impl PageCacheSnapshot {
287    /// Returns the statistics of page size `size`, `None` for
288    /// [`BytePageSize::Unset`].
289    pub fn get(&self, size: BytePageSize) -> Option<&PageCacheStats> {
290        self.sizes.get(size as usize)
291    }
292
293    /// Returns the statistics of every page size, in increasing order of size.
294    pub fn iter(&self) -> impl Iterator<Item = &PageCacheStats> {
295        self.sizes.iter()
296    }
297
298    /// Returns the totals of all page sizes.
299    pub fn total(&self) -> PageCacheStats {
300        self.sizes.iter().fold(
301            PageCacheStats {
302                size: BytePageSize::Unset,
303                ..PageCacheStats::default()
304            },
305            |mut total, s| {
306                total.hits += s.hits;
307                total.misses += s.misses;
308                total.cached += s.cached;
309                total.freed += s.freed;
310                total.pages += s.pages;
311                total.bytes += s.bytes;
312                total
313            },
314        )
315    }
316}
317
318impl Default for PageCacheSnapshot {
319    fn default() -> Self {
320        Self {
321            sizes: PAGE_SIZES.map(|size| PageCacheStats {
322                size,
323                ..PageCacheStats::default()
324            }),
325        }
326    }
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332    use crate::*;
333
334    #[test]
335    fn cached() {
336        reset();
337
338        let mut st = StorageVec::sized(BytePageSize::Size8);
339        assert_eq!(st.page_size(), BytePageSize::Size8);
340
341        st.put_u8(b'h');
342        let addr = st.0;
343        drop(st);
344
345        let st = StorageVec::sized(BytePageSize::Size8);
346        assert_eq!(addr, st.0);
347    }
348
349    #[test]
350    fn cache_metrics() {
351        reset();
352        let metrics = PageCacheMetrics::current();
353        assert_eq!(metrics.snapshot(), PageCacheSnapshot::default());
354
355        let size = BytePageSize::Size8;
356        let st = StorageVec::sized(size);
357        let s = *metrics.snapshot().get(size).unwrap();
358        assert_eq!(
359            (s.size, s.hits, s.misses, s.cached, s.pages),
360            (size, 0, 1, 0, 0)
361        );
362
363        drop(st);
364        let s = *metrics.snapshot().get(size).unwrap();
365        assert_eq!(
366            (s.hits, s.misses, s.cached, s.freed, s.pages),
367            (0, 1, 1, 0, 1)
368        );
369        assert_eq!(s.bytes, 8 * 1024);
370
371        let st = StorageVec::sized(size);
372        let s = *metrics.snapshot().get(size).unwrap();
373        assert_eq!(
374            (s.hits, s.misses, s.cached, s.pages, s.bytes),
375            (1, 1, 1, 0, 0)
376        );
377
378        crate::set_page_cache_size(size, 0);
379        drop(st);
380        let s = *metrics.snapshot().get(size).unwrap();
381        assert_eq!((s.cached, s.freed, s.pages), (1, 1, 0));
382
383        // other page sizes are separate, non-page buffers are not counted
384        drop(StorageVec::sized(BytePageSize::Size4));
385        drop(StorageVec::with_capacity(100));
386        let snapshot = metrics.snapshot();
387        let s = *snapshot.get(BytePageSize::Size4).unwrap();
388        assert_eq!((s.misses, s.cached, s.pages), (1, 1, 1));
389        assert!(snapshot.get(BytePageSize::Unset).is_none());
390        assert_eq!(snapshot.iter().count(), 9);
391        let total = snapshot.total();
392        assert_eq!(total.size, BytePageSize::Unset);
393        assert_eq!(
394            (total.hits, total.misses, total.cached, total.freed),
395            (1, 2, 2, 1)
396        );
397        assert_eq!((total.pages, total.bytes), (1, 4 * 1024));
398
399        // a replaced cache leaves the metrics
400        reset();
401        assert_eq!(metrics.snapshot().total().pages, 0);
402        assert!(!Arc::ptr_eq(&metrics, &PageCacheMetrics::current()));
403    }
404
405    #[test]
406    fn cache_metrics_set_current() {
407        reset();
408        let size = BytePageSize::Size16;
409        let old = PageCacheMetrics::current();
410        drop(StorageVec::sized(size));
411        assert_eq!(old.snapshot().get(size).unwrap().pages, 1);
412
413        let metrics = PageCacheMetrics::new();
414        PageCacheMetrics::set_current(metrics.clone());
415        assert!(Arc::ptr_eq(&metrics, &PageCacheMetrics::current()));
416        assert_eq!(old.snapshot().get(size).unwrap().pages, 0);
417        assert_eq!(metrics.snapshot().get(size).unwrap().pages, 1);
418
419        drop(StorageVec::sized(size));
420        let s = *metrics.snapshot().get(size).unwrap();
421        assert_eq!((s.hits, s.misses, s.cached, s.pages), (1, 0, 1, 1));
422        assert_eq!(old.snapshot().get(size).unwrap().hits, 0);
423
424        // other threads have their own metrics
425        let other = std::thread::spawn(PageCacheMetrics::current)
426            .join()
427            .unwrap();
428        assert!(!Arc::ptr_eq(&metrics, &other));
429        reset();
430    }
431
432    #[test]
433    fn default_cache_limit_per_page_size() {
434        assert_eq!(DEFAULT_PAGES_CACHE, [128, 64, 64, 32, 16, 8, 16, 2, 1]);
435        for size in crate::size::PAGE_SIZES {
436            reset();
437
438            let limit = DEFAULT_PAGES_CACHE[size as usize];
439            let pages: Vec<_> = (0..limit + 4).map(|_| StorageVec::sized(size)).collect();
440            drop(pages);
441            assert_eq!(cached_pages(size), limit, "{size:?}");
442        }
443        reset();
444    }
445
446    #[test]
447    #[allow(deprecated)]
448    fn pages_cache_size() {
449        reset();
450
451        crate::set_pages_cache(1);
452        drop((
453            StorageVec::sized(BytePageSize::Size4),
454            StorageVec::sized(BytePageSize::Size4),
455        ));
456        assert_eq!(cached_pages(BytePageSize::Size4), 1);
457
458        crate::set_pages_cache(0);
459        drop(StorageVec::sized(BytePageSize::Size8));
460        assert_eq!(cached_pages(BytePageSize::Size8), 0);
461
462        // the cache is in use, the page is freed
463        crate::set_pages_cache(16);
464        let st = StorageVec::sized(BytePageSize::Size16);
465        let cache = CACHE.with(Cell::take);
466        drop(st);
467        CACHE.with(|c| c.set(cache));
468        assert_eq!(cached_pages(BytePageSize::Size16), 0);
469
470        // the setting is ignored while the cache is in use
471        let cache = CACHE.with(Cell::take);
472        crate::set_pages_cache(3);
473        CACHE.with(|c| c.set(cache));
474        assert_eq!(cache_limits(), [16; CLASSES]);
475    }
476
477    fn cache_limits() -> [usize; CLASSES] {
478        CACHE.with(|c| {
479            let cst = c.take().unwrap();
480            let limits = cst.limits;
481            c.set(Some(cst));
482            limits
483        })
484    }
485
486    #[test]
487    #[allow(deprecated)]
488    fn page_cache_size() {
489        reset();
490
491        crate::set_page_cache_size(BytePageSize::Size64, 1);
492        crate::set_page_cache_size(BytePageSize::Unset, 5);
493        let mut expected = DEFAULT_PAGES_CACHE;
494        expected[BytePageSize::Size64 as usize] = 1;
495        assert_eq!(cache_limits(), expected);
496
497        drop((
498            StorageVec::sized(BytePageSize::Size64),
499            StorageVec::sized(BytePageSize::Size64),
500            StorageVec::sized(BytePageSize::Size48),
501            StorageVec::sized(BytePageSize::Size48),
502        ));
503        assert_eq!(cached_pages(BytePageSize::Size64), 1);
504        assert_eq!(cached_pages(BytePageSize::Size48), 2);
505
506        // the setting is ignored while the cache is in use
507        let cache = CACHE.with(Cell::take);
508        crate::set_page_cache_size(BytePageSize::Size48, 3);
509        CACHE.with(|c| c.set(cache));
510        assert_eq!(cache_limits(), expected);
511
512        crate::set_pages_cache(2);
513        assert_eq!(cache_limits(), [2; CLASSES]);
514        reset();
515    }
516
517    #[test]
518    #[allow(deprecated)]
519    fn lowered_limit_releases_pages() {
520        reset();
521        let metrics = PageCacheMetrics::current();
522        let pages = |size| metrics.snapshot().get(size).unwrap().pages;
523
524        let (s4, s64) = (BytePageSize::Size4, BytePageSize::Size64);
525        drop([0; 4].map(|_| StorageVec::sized(s64)));
526        drop([0; 4].map(|_| StorageVec::sized(s4)));
527        assert_eq!((cached_pages(s64), pages(s64)), (4, 4));
528
529        crate::set_page_cache_size(s64, 1);
530        assert_eq!((cached_pages(s64), pages(s64)), (1, 1));
531        assert_eq!((cached_pages(s4), pages(s4)), (4, 4));
532        crate::set_page_cache_size(s64, 0);
533        assert_eq!((cached_pages(s64), pages(s64)), (0, 0));
534
535        // released pages are not counted as freed by a full cache
536        crate::set_pages_cache(1);
537        assert_eq!((cached_pages(s4), pages(s4)), (1, 1));
538        let total = metrics.snapshot().total();
539        assert_eq!((total.cached, total.freed, total.pages), (8, 0, 1));
540
541        // a raised limit keeps the cached pages
542        crate::set_page_cache_size(s4, 8);
543        assert_eq!((cached_pages(s4), pages(s4)), (1, 1));
544        reset();
545    }
546
547    #[test]
548    fn bytes_mut_with_page_size() {
549        reset();
550
551        let mut buf = BytesMut::with_page_size(BytePageSize::Size8);
552        assert_eq!(buf.page_size(), BytePageSize::Size8);
553        assert_eq!(buf.capacity(), BytePageSize::Size8.capacity());
554        let ptr = buf.as_ptr();
555
556        // split off data keeps the page until the last reference is dropped
557        buf.extend_from_slice(&[1; 1000]);
558        let head = buf.split_to(500);
559        drop(buf);
560        assert_eq!(cached_pages(BytePageSize::Size8), 0);
561        drop(head);
562        assert_eq!(cached_pages(BytePageSize::Size8), 1);
563
564        let buf = BytesMut::with_page_size(BytePageSize::Size8);
565        assert_eq!(buf.as_ptr(), ptr);
566        assert_eq!(buf.capacity(), BytePageSize::Size8.capacity());
567        assert_eq!(cached_pages(BytePageSize::Size8), 0);
568
569        // `Unset` creates a `Size64` page
570        let buf = BytesMut::with_page_size(BytePageSize::Unset);
571        assert_eq!(buf.page_size(), BytePageSize::Size64);
572        assert_eq!(buf.capacity(), BytePageSize::Size64.capacity());
573        drop(buf);
574        assert_eq!(cached_pages(BytePageSize::Size64), 1);
575
576        // buffers without a page size are not cached
577        let buf = BytesMut::with_capacity(BytePageSize::Size64.capacity() + 1);
578        assert_eq!(buf.page_size(), BytePageSize::Unset);
579        assert_eq!(BytesMut::new().page_size(), BytePageSize::Unset);
580        assert_eq!(BytesMut::with_capacity(64).page_size(), BytePageSize::Unset);
581        assert_eq!(
582            BytesMut::copy_from_slice(b"hello").page_size(),
583            BytePageSize::Unset
584        );
585        drop(buf);
586        for size in crate::size::PAGE_SIZES {
587            let expected = usize::from(size == BytePageSize::Size64);
588            assert_eq!(cached_pages(size), expected);
589        }
590    }
591}