Image caches that find an entry in O(log n), so drawing many small images no longer slows down as the caches fill
Prompt's window had come to spend 35 ms of CPU on every frame after some hours open, most of it in `GlesCanvas::draw_image_data`. Every cache hit looked its key up in the LRU order with `VecDeque::iter().position()` and moved it to the back with `remove`, both linear in the number of entries. A terminal draws each glyph as an image, one per colour it is drawn in, so the cache filled towards its 32 MiB with thousands of small textures, 7573 in the window measured, and every glyph of every frame walked all of them. The software canvas's premultiplied cache and the GLES surface cache did the same. `render/lru.rs` is a least-recently-used cache bounded by a byte budget. Each entry keeps the tick it was last used at and a `BTreeMap` orders the keys by it, so a hit, an insertion and an eviction cost O(log n). `insert` returns the values it evicted, for the GLES caches to delete their textures, and never evicts the one just inserted, so a value larger than the whole budget is still kept; inserting a key again returns its old value. Tests cover the eviction order, a value over the budget and a replaced key. `ImageCache` holds its images and its surfaces in one `Lru` each, with the same budgets of 32 and 16 MiB; `ImageCache::image` uploads a texture only when it is not cached, and `evict_overflow` and the two linear lookups are gone. The software canvas keeps its premultiplied images in an `Lru` in place of `PremulCache`. What is cached and evicted is unchanged.
This commit is contained in:
+31
-73
@@ -28,11 +28,10 @@
|
|||||||
//! (`shared_image_cache`), so a surface that is torn down and recreated
|
//! (`shared_image_cache`), so a surface that is torn down and recreated
|
||||||
//! draws its images without uploading them again.
|
//! draws its images without uploading them again.
|
||||||
|
|
||||||
use std::collections::hash_map::Entry;
|
|
||||||
|
|
||||||
use glow::HasContext;
|
use glow::HasContext;
|
||||||
|
|
||||||
use crate::render::helpers::fingerprint_rgba;
|
use crate::render::helpers::fingerprint_rgba;
|
||||||
|
use crate::render::lru::Lru;
|
||||||
use crate::types::Rect;
|
use crate::types::Rect;
|
||||||
|
|
||||||
use super::helpers::{ ortho_rect, upload_rgba_texture };
|
use super::helpers::{ ortho_rect, upload_rgba_texture };
|
||||||
@@ -49,81 +48,55 @@ type ImageKey = ( u32, u32, u64 );
|
|||||||
/// Content-keyed texture cache shared by every canvas on one GL context.
|
/// Content-keyed texture cache shared by every canvas on one GL context.
|
||||||
pub( super ) struct ImageCache
|
pub( super ) struct ImageCache
|
||||||
{
|
{
|
||||||
map: std::collections::HashMap<ImageKey, ( glow::Texture, u32, u32 )>,
|
images: Lru<ImageKey, glow::Texture>,
|
||||||
/// LRU order of keys, least-recent at the front.
|
|
||||||
lru: std::collections::VecDeque<ImageKey>,
|
|
||||||
/// Estimated GPU bytes held (RGBA8: w × h × 4).
|
|
||||||
bytes: usize,
|
|
||||||
/// Pre-rendered themed surfaces (shadows + fill + insets), keyed by
|
/// Pre-rendered themed surfaces (shadows + fill + insets), keyed by
|
||||||
/// size and style; see `surface_cache.rs`.
|
/// size and style; see `surface_cache.rs`.
|
||||||
surfaces: std::collections::HashMap<u64, ( glow::Texture, u32, u32 )>,
|
surfaces: Lru<u64, ( glow::Texture, u32, u32 )>,
|
||||||
surfaces_lru: std::collections::VecDeque<u64>,
|
|
||||||
surfaces_bytes: usize,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Byte budget for the pre-rendered surface textures.
|
/// Byte budget for the pre-rendered surface textures.
|
||||||
pub const SURFACE_CACHE_MAX_BYTES: usize = 16 * 1024 * 1024;
|
pub const SURFACE_CACHE_MAX_BYTES: usize = 16 * 1024 * 1024;
|
||||||
|
|
||||||
|
/// Estimated GPU bytes of an RGBA8 texture.
|
||||||
|
fn texture_bytes( w: u32, h: u32 ) -> usize
|
||||||
|
{
|
||||||
|
( w as usize ) * ( h as usize ) * 4
|
||||||
|
}
|
||||||
|
|
||||||
impl ImageCache
|
impl ImageCache
|
||||||
{
|
{
|
||||||
fn new() -> Self
|
fn new() -> Self
|
||||||
{
|
{
|
||||||
Self
|
Self { images: Lru::new( IMAGE_CACHE_MAX_BYTES ), surfaces: Lru::new( SURFACE_CACHE_MAX_BYTES ) }
|
||||||
{
|
|
||||||
map: std::collections::HashMap::new(),
|
|
||||||
lru: std::collections::VecDeque::new(),
|
|
||||||
bytes: 0,
|
|
||||||
surfaces: std::collections::HashMap::new(),
|
|
||||||
surfaces_lru: std::collections::VecDeque::new(),
|
|
||||||
surfaces_bytes: 0,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub( super ) fn surface( &mut self, key: u64 ) -> Option<( glow::Texture, u32, u32 )>
|
pub( super ) fn surface( &mut self, key: u64 ) -> Option<( glow::Texture, u32, u32 )>
|
||||||
{
|
{
|
||||||
let hit = *self.surfaces.get( &key )?;
|
self.surfaces.get( &key ).copied()
|
||||||
if let Some( pos ) = self.surfaces_lru.iter().position( |k| *k == key )
|
|
||||||
{
|
|
||||||
self.surfaces_lru.remove( pos );
|
|
||||||
self.surfaces_lru.push_back( key );
|
|
||||||
}
|
|
||||||
Some( hit )
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub( super ) fn insert_surface( &mut self, gl: &glow::Context, key: u64, tex: glow::Texture, w: u32, h: u32 )
|
pub( super ) fn insert_surface( &mut self, gl: &glow::Context, key: u64, tex: glow::Texture, w: u32, h: u32 )
|
||||||
{
|
{
|
||||||
self.surfaces.insert( key, ( tex, w, h ) );
|
for ( old, _, _ ) in self.surfaces.insert( key, ( tex, w, h ), texture_bytes( w, h ) )
|
||||||
self.surfaces_bytes += ( w as usize ) * ( h as usize ) * 4;
|
|
||||||
self.surfaces_lru.push_back( key );
|
|
||||||
while self.surfaces_bytes > SURFACE_CACHE_MAX_BYTES && self.surfaces_lru.len() > 1
|
|
||||||
{
|
{
|
||||||
let Some( old ) = self.surfaces_lru.pop_front() else { break };
|
// SAFETY: `old` was created through `gl` in `end_offscreen` and
|
||||||
if let Some( ( t, ow, oh ) ) = self.surfaces.remove( &old )
|
// just left the cache, so it is deleted exactly once.
|
||||||
{
|
unsafe { gl.delete_texture( old ); }
|
||||||
self.surfaces_bytes = self.surfaces_bytes.saturating_sub( ( ow as usize ) * ( oh as usize ) * 4 );
|
|
||||||
// SAFETY: `t` was created through `gl` in `end_offscreen` and
|
|
||||||
// just left the map, so it is deleted exactly once.
|
|
||||||
unsafe { gl.delete_texture( t ); }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Delete least-recently-drawn textures until the estimated byte total
|
/// The texture for `key`, uploaded by `upload` when it is not cached yet.
|
||||||
/// fits [`IMAGE_CACHE_MAX_BYTES`]. The back of the LRU — the entry the
|
fn image( &mut self, gl: &glow::Context, key: ImageKey, upload: impl FnOnce() -> glow::Texture ) -> glow::Texture
|
||||||
/// current draw is about to use — is never removed.
|
|
||||||
fn evict_overflow( &mut self, gl: &glow::Context )
|
|
||||||
{
|
{
|
||||||
while self.bytes > IMAGE_CACHE_MAX_BYTES && self.lru.len() > 1
|
if let Some( &tex ) = self.images.get( &key ) { return tex; }
|
||||||
|
let tex = upload();
|
||||||
|
for old in self.images.insert( key, tex, texture_bytes( key.0, key.1 ) )
|
||||||
{
|
{
|
||||||
let Some( key ) = self.lru.pop_front() else { break };
|
// SAFETY: `old` was created through `gl` in `draw_image_data`
|
||||||
if let Some( ( tex, w, h ) ) = self.map.remove( &key )
|
// and just left the cache, so it is deleted exactly once.
|
||||||
{
|
unsafe { gl.delete_texture( old ); }
|
||||||
self.bytes = self.bytes.saturating_sub( ( w as usize ) * ( h as usize ) * 4 );
|
|
||||||
// SAFETY: `tex` was created through `gl` in `draw_image_data`
|
|
||||||
// and just left the map, so it is deleted exactly once.
|
|
||||||
unsafe { gl.delete_texture( tex ); }
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
tex
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -174,21 +147,10 @@ impl GlesCanvas
|
|||||||
// depth).
|
// depth).
|
||||||
let cache_key = ( img_w, img_h, fingerprint_rgba( rgba_data ) );
|
let cache_key = ( img_w, img_h, fingerprint_rgba( rgba_data ) );
|
||||||
|
|
||||||
let cache = std::rc::Rc::clone( &self.image_cache );
|
let tex = self.image_cache.borrow_mut().image( &self.gl, cache_key, ||
|
||||||
let mut cache = cache.borrow_mut();
|
|
||||||
let cache = &mut *cache;
|
|
||||||
if let Entry::Vacant( vacant ) = cache.map.entry( cache_key )
|
|
||||||
{
|
{
|
||||||
let tex = upload_rgba_texture( &self.gl, self.version, rgba_data, img_w as i32, img_h as i32 );
|
upload_rgba_texture( &self.gl, self.version, rgba_data, img_w as i32, img_h as i32 )
|
||||||
vacant.insert( ( tex, img_w, img_h ) );
|
} );
|
||||||
cache.bytes += ( img_w as usize ) * ( img_h as usize ) * 4;
|
|
||||||
cache.lru.push_back( cache_key );
|
|
||||||
cache.evict_overflow( &self.gl );
|
|
||||||
} else if let Some( pos ) = cache.lru.iter().position( |k| *k == cache_key )
|
|
||||||
{
|
|
||||||
cache.lru.remove( pos );
|
|
||||||
cache.lru.push_back( cache_key );
|
|
||||||
}
|
|
||||||
|
|
||||||
// Snap to integer pixels. With GL_LINEAR sampling, a
|
// Snap to integer pixels. With GL_LINEAR sampling, a
|
||||||
// fractional `dest.x` / `dest.y` makes every fragment sample
|
// fractional `dest.x` / `dest.y` makes every fragment sample
|
||||||
@@ -205,21 +167,18 @@ impl GlesCanvas
|
|||||||
height: dest.height.round(),
|
height: dest.height.round(),
|
||||||
};
|
};
|
||||||
|
|
||||||
if let Some( ( tex, _, _ ) ) = cache.map.get( &cache_key )
|
|
||||||
{
|
|
||||||
let mvp = ortho_rect( self.width, self.height, dest );
|
let mvp = ortho_rect( self.width, self.height, dest );
|
||||||
let alpha = opacity * self.global_alpha;
|
let alpha = opacity * self.global_alpha;
|
||||||
// SAFETY: see `primitives.rs` module doc. `*tex` is owned by
|
// SAFETY: see `primitives.rs` module doc. `tex` is owned by the
|
||||||
// the shared cache so it outlives the call. Eviction ran
|
// shared cache so it outlives the call: eviction already ran and
|
||||||
// before this borrow and never removes the most-recent key,
|
// never removes the most recent key, which is `cache_key`.
|
||||||
// which is `cache_key`.
|
|
||||||
unsafe
|
unsafe
|
||||||
{
|
{
|
||||||
self.gl.use_program( Some( self.tex_program ) );
|
self.gl.use_program( Some( self.tex_program ) );
|
||||||
self.gl.uniform_matrix_4_f32_slice( Some( &self.u_tex_mvp ), false, &mvp );
|
self.gl.uniform_matrix_4_f32_slice( Some( &self.u_tex_mvp ), false, &mvp );
|
||||||
self.gl.uniform_1_f32( Some( &self.u_tex_opacity ), alpha );
|
self.gl.uniform_1_f32( Some( &self.u_tex_opacity ), alpha );
|
||||||
self.gl.active_texture( glow::TEXTURE0 );
|
self.gl.active_texture( glow::TEXTURE0 );
|
||||||
self.gl.bind_texture( glow::TEXTURE_2D, Some( *tex ) );
|
self.gl.bind_texture( glow::TEXTURE_2D, Some( tex ) );
|
||||||
self.gl.uniform_1_i32( Some( &self.u_tex_sampler ), 0 );
|
self.gl.uniform_1_i32( Some( &self.u_tex_sampler ), 0 );
|
||||||
self.gl.bind_vertex_array( Some( self.quad_vao ) );
|
self.gl.bind_vertex_array( Some( self.quad_vao ) );
|
||||||
self.gl.draw_arrays( glow::TRIANGLES, 0, 6 );
|
self.gl.draw_arrays( glow::TRIANGLES, 0, 6 );
|
||||||
@@ -227,7 +186,6 @@ impl GlesCanvas
|
|||||||
self.gl.bind_texture( glow::TEXTURE_2D, None );
|
self.gl.bind_texture( glow::TEXTURE_2D, None );
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
/// Draw an externally-owned GL texture into `dest`.
|
/// Draw an externally-owned GL texture into `dest`.
|
||||||
///
|
///
|
||||||
|
|||||||
+9
-44
@@ -14,13 +14,13 @@
|
|||||||
//! of four pixels (Argb8888 fallback).
|
//! of four pixels (Argb8888 fallback).
|
||||||
|
|
||||||
use std::cell::RefCell;
|
use std::cell::RefCell;
|
||||||
use std::collections::{ HashMap, VecDeque };
|
|
||||||
|
|
||||||
use tiny_skia::{ IntSize, Pixmap, PixmapPaint, Transform };
|
use tiny_skia::{ IntSize, Pixmap, PixmapPaint, Transform };
|
||||||
|
|
||||||
use crate::types::Rect;
|
use crate::types::Rect;
|
||||||
|
|
||||||
use super::helpers::fingerprint_rgba;
|
use super::helpers::fingerprint_rgba;
|
||||||
|
use super::lru::Lru;
|
||||||
use super::SoftwareCanvas;
|
use super::SoftwareCanvas;
|
||||||
|
|
||||||
/// Byte budget of the premultiplied-image cache, matching the GLES texture
|
/// Byte budget of the premultiplied-image cache, matching the GLES texture
|
||||||
@@ -37,47 +37,11 @@ struct Premul
|
|||||||
opaque: bool,
|
opaque: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Premultiplied copies of the images drawn recently, keyed by size,
|
|
||||||
/// content fingerprint and alpha, so a backdrop or a logo is converted once
|
|
||||||
/// rather than on every frame. Least-recently-drawn eviction; the entry
|
|
||||||
/// being drawn is never evicted.
|
|
||||||
#[ derive( Default ) ]
|
|
||||||
struct PremulCache
|
|
||||||
{
|
|
||||||
entries: HashMap<PremulKey, Premul>,
|
|
||||||
lru: VecDeque<PremulKey>,
|
|
||||||
bytes: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
static PREMUL_CACHE: RefCell<PremulCache> = RefCell::new( PremulCache::default() );
|
/// Premultiplied copies of the images drawn recently, keyed by size,
|
||||||
}
|
/// content fingerprint and alpha, so a backdrop or a logo is converted
|
||||||
|
/// once rather than on every frame.
|
||||||
impl PremulCache
|
static PREMUL_CACHE: RefCell<Lru<PremulKey, Premul>> = RefCell::new( Lru::new( PREMUL_CACHE_MAX_BYTES ) );
|
||||||
{
|
|
||||||
/// Mark `key` as just used. `false` when it is not cached.
|
|
||||||
fn touch( &mut self, key: &PremulKey ) -> bool
|
|
||||||
{
|
|
||||||
let Some( pos ) = self.lru.iter().position( |k| k == key ) else { return false };
|
|
||||||
self.lru.remove( pos );
|
|
||||||
self.lru.push_back( *key );
|
|
||||||
true
|
|
||||||
}
|
|
||||||
|
|
||||||
fn insert( &mut self, key: PremulKey, premul: Premul )
|
|
||||||
{
|
|
||||||
self.bytes += premul.pixmap.data().len();
|
|
||||||
self.entries.insert( key, premul );
|
|
||||||
self.lru.push_back( key );
|
|
||||||
while self.bytes > PREMUL_CACHE_MAX_BYTES && self.lru.len() > 1
|
|
||||||
{
|
|
||||||
let Some( old ) = self.lru.pop_front() else { break };
|
|
||||||
if let Some( p ) = self.entries.remove( &old )
|
|
||||||
{
|
|
||||||
self.bytes = self.bytes.saturating_sub( p.pixmap.data().len() );
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn premultiply( rgba: &[u8], size: IntSize, alpha: f32 ) -> Option<Premul>
|
fn premultiply( rgba: &[u8], size: IntSize, alpha: f32 ) -> Option<Premul>
|
||||||
@@ -144,12 +108,13 @@ impl SoftwareCanvas
|
|||||||
PREMUL_CACHE.with( |cell|
|
PREMUL_CACHE.with( |cell|
|
||||||
{
|
{
|
||||||
let mut cache = cell.borrow_mut();
|
let mut cache = cell.borrow_mut();
|
||||||
if !cache.touch( &key )
|
if cache.get( &key ).is_none()
|
||||||
{
|
{
|
||||||
let Some( premul ) = premultiply( rgba_data, int_size, alpha ) else { return };
|
let Some( premul ) = premultiply( rgba_data, int_size, alpha ) else { return };
|
||||||
cache.insert( key, premul );
|
let bytes = premul.pixmap.data().len();
|
||||||
|
cache.insert( key, premul, bytes );
|
||||||
}
|
}
|
||||||
let Some( src ) = cache.entries.get( &key ) else { return };
|
let Some( src ) = cache.get( &key ) else { return };
|
||||||
|
|
||||||
let unscaled = dest.width as u32 == img_w && dest.height as u32 == img_h;
|
let unscaled = dest.width as u32 == img_w && dest.height as u32 == img_h;
|
||||||
if src.opaque && unscaled && self.clip_mask.is_none()
|
if src.opaque && unscaled && self.clip_mask.is_none()
|
||||||
|
|||||||
@@ -0,0 +1,110 @@
|
|||||||
|
// SPDX-License-Identifier: LGPL-2.1-only
|
||||||
|
// Copyright (C) 2026 Liberux Labs, S. L. <info@liberux.net>
|
||||||
|
|
||||||
|
//! A least-recently-used cache bounded by a byte budget, whose hits, insertions and evictions cost O(log n).
|
||||||
|
|
||||||
|
use std::collections::{ BTreeMap, HashMap };
|
||||||
|
use std::hash::Hash;
|
||||||
|
|
||||||
|
struct Slot<V>
|
||||||
|
{
|
||||||
|
value: V,
|
||||||
|
bytes: usize,
|
||||||
|
used: u64,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub( crate ) struct Lru<K, V>
|
||||||
|
{
|
||||||
|
slots: HashMap<K, Slot<V>>,
|
||||||
|
/// Keys by the tick they were last used at, least recent first.
|
||||||
|
order: BTreeMap<u64, K>,
|
||||||
|
tick: u64,
|
||||||
|
bytes: usize,
|
||||||
|
budget: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<K: Copy + Eq + Hash, V> Lru<K, V>
|
||||||
|
{
|
||||||
|
pub( crate ) fn new( budget: usize ) -> Self
|
||||||
|
{
|
||||||
|
Self { slots: HashMap::new(), order: BTreeMap::new(), tick: 0, bytes: 0, budget }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The value under `key`, which becomes the most recently used.
|
||||||
|
pub( crate ) fn get( &mut self, key: &K ) -> Option<&V>
|
||||||
|
{
|
||||||
|
let slot = self.slots.get_mut( key )?;
|
||||||
|
self.order.remove( &slot.used );
|
||||||
|
self.tick += 1;
|
||||||
|
slot.used = self.tick;
|
||||||
|
self.order.insert( self.tick, *key );
|
||||||
|
Some( &slot.value )
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Store `value` as the most recently used and return the values that left to fit the budget; `value` itself never does, so one larger than the whole budget is still kept.
|
||||||
|
pub( crate ) fn insert( &mut self, key: K, value: V, bytes: usize ) -> Vec<V>
|
||||||
|
{
|
||||||
|
let mut evicted: Vec<V> = self.remove( &key ).into_iter().collect();
|
||||||
|
self.tick += 1;
|
||||||
|
self.order.insert( self.tick, key );
|
||||||
|
self.slots.insert( key, Slot { value, bytes, used: self.tick } );
|
||||||
|
self.bytes += bytes;
|
||||||
|
while self.bytes > self.budget && self.slots.len() > 1
|
||||||
|
{
|
||||||
|
let Some( ( _, oldest ) ) = self.order.pop_first() else { break };
|
||||||
|
if let Some( slot ) = self.slots.remove( &oldest )
|
||||||
|
{
|
||||||
|
self.bytes -= slot.bytes;
|
||||||
|
evicted.push( slot.value );
|
||||||
|
}
|
||||||
|
}
|
||||||
|
evicted
|
||||||
|
}
|
||||||
|
|
||||||
|
fn remove( &mut self, key: &K ) -> Option<V>
|
||||||
|
{
|
||||||
|
let slot = self.slots.remove( key )?;
|
||||||
|
self.order.remove( &slot.used );
|
||||||
|
self.bytes -= slot.bytes;
|
||||||
|
Some( slot.value )
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ cfg( test ) ]
|
||||||
|
mod tests
|
||||||
|
{
|
||||||
|
use super::Lru;
|
||||||
|
|
||||||
|
#[ test ]
|
||||||
|
fn evicts_the_least_recently_used()
|
||||||
|
{
|
||||||
|
let mut lru = Lru::new( 30 );
|
||||||
|
assert!( lru.insert( 1, 'a', 10 ).is_empty() );
|
||||||
|
assert!( lru.insert( 2, 'b', 10 ).is_empty() );
|
||||||
|
assert!( lru.insert( 3, 'c', 10 ).is_empty() );
|
||||||
|
assert_eq!( lru.get( &1 ), Some( &'a' ) );
|
||||||
|
assert_eq!( lru.insert( 4, 'd', 10 ), vec![ 'b' ] );
|
||||||
|
assert_eq!( lru.get( &2 ), None );
|
||||||
|
assert_eq!( lru.get( &1 ), Some( &'a' ) );
|
||||||
|
assert_eq!( lru.get( &3 ), Some( &'c' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ test ]
|
||||||
|
fn keeps_a_value_larger_than_the_budget()
|
||||||
|
{
|
||||||
|
let mut lru = Lru::new( 10 );
|
||||||
|
lru.insert( 1, 'a', 5 );
|
||||||
|
assert_eq!( lru.insert( 2, 'b', 50 ), vec![ 'a' ] );
|
||||||
|
assert_eq!( lru.get( &2 ), Some( &'b' ) );
|
||||||
|
}
|
||||||
|
|
||||||
|
#[ test ]
|
||||||
|
fn replacing_a_key_returns_the_old_value()
|
||||||
|
{
|
||||||
|
let mut lru = Lru::new( 100 );
|
||||||
|
lru.insert( 1, 'a', 10 );
|
||||||
|
assert_eq!( lru.insert( 1, 'z', 10 ), vec![ 'a' ] );
|
||||||
|
assert_eq!( lru.get( &1 ), Some( &'z' ) );
|
||||||
|
assert_eq!( lru.bytes, 10 );
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -47,6 +47,7 @@ pub( crate ) mod primitives;
|
|||||||
pub( crate ) mod text;
|
pub( crate ) mod text;
|
||||||
pub( crate ) mod image;
|
pub( crate ) mod image;
|
||||||
pub( crate ) mod helpers;
|
pub( crate ) mod helpers;
|
||||||
|
pub( crate ) mod lru;
|
||||||
|
|
||||||
// ─── Backend flag ────────────────────────────────────────────────────────────
|
// ─── Backend flag ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user