| // Copyright 2019 The Fuchsia Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| use { |
| super::asset::{Asset, AssetId}, |
| fuchsia_inspect::{self as finspect, NumericProperty}, |
| std::collections::VecDeque, |
| }; |
| |
| /// An LRU cache for `Buffer`s, bounded by the total size of cached VMOs. |
| /// |
| /// `capacity` and `available` are [`u64`] instead of [`usize`] for parity with `mem::Buffer.size`. |
| pub struct Cache { |
| /// Maximum allowed sum of `self.buffers[..].size` in bytes. |
| capacity: u64, |
| /// Bytes available to be used. |
| available: u64, |
| /// Assets ordered by recency of last use. |
| cache: VecDeque<Asset>, |
| |
| inspect_data: AssetCacheInspectData, |
| } |
| |
| impl Cache { |
| /// Creates a new cache instance with the given `capacity` in bytes, and with the given parent |
| /// Inspect node. |
| pub fn new(capacity: u64, parent_inspect_node: &finspect::Node) -> Cache { |
| let inspect_data = AssetCacheInspectData::new(parent_inspect_node, capacity); |
| Cache { capacity, available: capacity, cache: VecDeque::new(), inspect_data } |
| } |
| |
| /// Get the index of the [`Asset`] with ID `id`, if it is cached. |
| /// |
| /// Runs in `O(self.len)` time, which should be fast enough if the cache is small. |
| fn index_of(&self, id: AssetId) -> Option<usize> { |
| // Iterate from most- to least-recently used (back to front). |
| for (index, asset) in self.cache.iter().enumerate().rev() { |
| if asset.id == id { |
| return Some(index); |
| } |
| } |
| None |
| } |
| |
| /// Move the cached [`Asset`] at `index` to the back of the queue (i.e. mark it as |
| /// most-recently used) and return a reference to it. |
| /// |
| /// Returns [`None`] if `index` is out of bounds. |
| fn move_to_back(&mut self, index: usize) -> Option<&Asset> { |
| if index >= self.cache.len() { |
| return None; |
| } |
| |
| // Don't do anything if this is already at the back. |
| if index != self.cache.len() - 1 { |
| if let Some(asset) = self.cache.remove(index) { |
| self.cache.push_back(asset); |
| } |
| } |
| self.cache.back() |
| } |
| |
| /// Get a clone of the cached [`Asset`] with ID `id`. |
| /// Returns [`None`] if cloning fails or the requested [`Asset`] is not cached. |
| pub fn get(&mut self, id: AssetId) -> Option<Asset> { |
| self.index_of(id) |
| .and_then(move |index| self.move_to_back(index)) |
| .and_then(|cached| cached.try_clone().ok()) |
| } |
| |
| /// Remove and return the least recently used cached [`Asset`]. |
| /// Returns [`None`] if the cache is empty. |
| fn pop(&mut self) -> Option<Asset> { |
| let popped = self.cache.pop_front(); |
| if let Some(asset) = &popped { |
| self.available += asset.buffer.size; |
| self.inspect_data.on_pop(&asset); |
| } |
| popped |
| } |
| |
| /// Add a clone of `asset` to the cache, and return the original. |
| /// If `asset` is already cached, calls [`move_to_back`] before returning. |
| /// If cloning `asset` fails or if `asset` is larger than the cache capacity, nothing is cached. |
| /// As many cached [`Assets`] as needed (in LRU order) will be popped to make space for `asset`. |
| /// |
| /// Returns the original given `Asset`, a `bool` indicating whether it is now cached |
| /// successfully (including if it was already cached), and any `Asset`s that were evicted to |
| /// make room for it. |
| pub fn push(&mut self, asset: Asset) -> (Asset, bool, Vec<Asset>) { |
| let mut is_cached = false; |
| let mut evicted = Vec::<Asset>::new(); |
| |
| if let Some(index) = self.index_of(asset.id) { |
| self.move_to_back(index); |
| is_cached = true |
| } else if asset.buffer.size < self.capacity { |
| if let Some(clone) = asset.try_clone().ok() { |
| while clone.buffer.size > self.available { |
| if let Some(popped) = self.pop() { |
| evicted.push(popped); |
| } |
| } |
| is_cached = true; |
| self.available -= clone.buffer.size; |
| self.inspect_data.on_push(&clone); |
| self.cache.push_back(clone); |
| } |
| } |
| (asset, is_cached, evicted) |
| } |
| |
| /// Returns the capacity of the cache in bytes. |
| #[cfg(test)] |
| pub fn capacity_bytes(&self) -> u64 { |
| self.capacity |
| } |
| } |
| |
| /// Inspect data for [AssetCache]. |
| #[allow(dead_code)] |
| pub struct AssetCacheInspectData { |
| /// Root Inspect node for the cache |
| node: finspect::Node, |
| /// Tracks bytes used by the cache |
| used_bytes: finspect::UintProperty, |
| /// Tracks number of assets in the cache |
| count: finspect::UintProperty, |
| } |
| |
| impl AssetCacheInspectData { |
| /// Creates a new instance with the given parent node and cache capacity. |
| fn new(parent_node: &finspect::Node, capacity: u64) -> Self { |
| let node = parent_node.create_child("asset_cache"); |
| node.record_uint("capacity_bytes", capacity); |
| let used = node.create_uint("used_bytes", 0); |
| let count = node.create_uint("count", 0); |
| AssetCacheInspectData { node, used_bytes: used, count } |
| } |
| |
| fn on_pop(&mut self, popped_asset: &Asset) { |
| self.used_bytes.subtract(popped_asset.buffer.size); |
| self.count.subtract(1); |
| } |
| |
| fn on_push(&mut self, pushed_asset: &Asset) { |
| self.used_bytes.add(pushed_asset.buffer.size); |
| self.count.add(1); |
| } |
| } |
| |
| #[cfg(test)] |
| mod tests { |
| use { |
| super::*, fidl_fuchsia_mem as mem, fuchsia_inspect::assert_inspect_tree, |
| fuchsia_zircon as zx, |
| }; |
| |
| /// Creates a `Cache` with some mocked assets. |
| fn mock_cache() -> Cache { |
| let inspector = finspect::Inspector::new(); |
| mock_cache_with_inspector(&inspector) |
| } |
| |
| /// Creates a cache with some mocked assets and the given `Inspector`. |
| fn mock_cache_with_inspector(inspector: &finspect::Inspector) -> Cache { |
| let inspector_root = inspector.root(); |
| let capacity = 3000; |
| Cache { |
| capacity, |
| available: 1000, |
| cache: VecDeque::from(vec![mock_asset(1, 1024, 1000), mock_asset(2, 1024, 1000)]), |
| inspect_data: AssetCacheInspectData::new(inspector_root, capacity), |
| } |
| } |
| |
| fn mock_asset(id: u32, vmo_size: u64, buf_size: u64) -> Asset { |
| assert!(vmo_size > buf_size); |
| Asset { |
| id: AssetId(id), |
| buffer: mem::Buffer { vmo: zx::Vmo::create(vmo_size).unwrap(), size: buf_size }, |
| } |
| } |
| |
| #[test] |
| fn test_index_of_hit() { |
| let cache = mock_cache(); |
| assert_eq!(cache.index_of(AssetId(1)), Some(0)); |
| assert_eq!(cache.index_of(AssetId(2)), Some(1)); |
| } |
| |
| #[test] |
| fn test_index_of_miss() { |
| let cache = mock_cache(); |
| assert!(cache.index_of(AssetId(0)).is_none()); |
| } |
| |
| #[test] |
| fn test_move_to_back() { |
| let mut cache = mock_cache(); |
| let front_id = cache.cache.front().unwrap().id; |
| let moved = cache.move_to_back(0).unwrap(); |
| assert_eq!(moved.id, front_id); |
| assert_ne!(cache.cache.front().unwrap().id, front_id); |
| } |
| |
| #[test] |
| fn test_move_to_back_out_of_bounds() { |
| let mut cache = mock_cache(); |
| assert!(cache.move_to_back(3).is_none()); |
| } |
| |
| #[test] |
| fn test_get_hit() { |
| let mut cache = mock_cache(); |
| let cached = cache.get(AssetId(1)).unwrap(); |
| assert_eq!(cache.cache.back().unwrap().id, cached.id); |
| } |
| |
| #[test] |
| fn test_get_miss() { |
| let mut cache = mock_cache(); |
| let should_be_none = cache.get(AssetId(3)); |
| assert!(should_be_none.is_none()); |
| } |
| |
| #[test] |
| fn test_pop() { |
| let mut cache = mock_cache(); |
| let unused_before = cache.available; |
| let should_be_popped_id = cache.cache.front().unwrap().id; |
| let should_be_popped_size = cache.cache.front().unwrap().buffer.size; |
| cache.pop(); |
| assert!(cache.index_of(should_be_popped_id).is_none()); |
| assert_eq!(cache.available, unused_before + should_be_popped_size); |
| } |
| |
| #[test] |
| fn test_pop_empty() { |
| let inspector = finspect::Inspector::new(); |
| let mut cache = Cache::new(10, inspector.root()); |
| cache.pop(); |
| assert_eq!(cache.available, 10); |
| } |
| |
| #[test] |
| fn test_push_new() { |
| let mut cache = mock_cache(); |
| let unused_before = cache.available; |
| let to_push = mock_asset(3, 1024, 1000); |
| let (cached, is_cached, evicted) = cache.push(to_push); |
| assert_eq!(cached.id, AssetId(3)); |
| assert_eq!(cached.id, cache.cache.back().unwrap().id); |
| assert_eq!(cache.available, unused_before - 1000); |
| assert!(is_cached); |
| assert!(evicted.is_empty()); |
| } |
| |
| #[test] |
| fn test_push_make_space() { |
| let mut cache = mock_cache(); |
| let to_push = mock_asset(3, 2048, 2000); |
| let should_be_popped_id = cache.cache.front().unwrap().id; |
| let (cached, is_cached, evicted) = cache.push(to_push); |
| assert_eq!(cached.id, AssetId(3)); |
| assert_eq!(cached.id, cache.cache.back().unwrap().id); |
| assert_eq!(cache.available, 0); |
| assert!(cache.index_of(should_be_popped_id).is_none()); |
| assert!(is_cached); |
| assert_eq!(evicted.len(), 1); |
| assert_eq!(evicted[0].id, should_be_popped_id); |
| } |
| |
| #[test] |
| fn test_push_cached() { |
| let mut cache = mock_cache(); |
| let unused_before = cache.available; |
| let to_push = cache.cache.front().unwrap().try_clone().unwrap(); |
| let front_id = to_push.id; |
| let (cached, is_cached, evicted) = cache.push(to_push); |
| assert_eq!(cached.id, front_id); |
| assert_eq!(cached.id, cache.cache.back().unwrap().id); |
| assert_eq!(cache.available, unused_before); |
| assert!(is_cached); |
| assert!(evicted.is_empty()); |
| } |
| |
| #[test] |
| fn test_push_does_not_fit() { |
| let mut cache = mock_cache(); |
| let unused_before = cache.available; |
| let too_big = mock_asset(3, 4096, 4000); |
| let (too_big, is_cached, evicted) = cache.push(too_big); |
| assert!(!is_cached); |
| assert!(evicted.is_empty()); |
| assert!(cache.get(too_big.id).is_none()); |
| assert_eq!(cache.available, unused_before); |
| } |
| |
| #[test] |
| fn test_inspect_data() { |
| let inspector = finspect::Inspector::new(); |
| let capacity = 3000; |
| let mut cache = Cache::new(capacity, inspector.root()); |
| assert_inspect_tree!(inspector, root: { |
| asset_cache: { |
| capacity_bytes: 3000u64, |
| used_bytes: 0u64, |
| count: 0u64, |
| } |
| }); |
| |
| let asset = mock_asset(1, 1024, 1000); |
| cache.push(asset); |
| |
| assert_inspect_tree!(inspector, root: contains { |
| asset_cache: { |
| capacity_bytes: 3000u64, |
| used_bytes: 1000u64, |
| count: 1u64, |
| } |
| }); |
| } |
| } |