1use std::cmp::Reverse;
15use std::collections::{BinaryHeap, HashMap};
16use std::sync::Arc;
17use std::sync::atomic::{AtomicBool, AtomicU64, Ordering};
18
19use web_async::Lock;
20
21const ENTRY_OVERHEAD: u64 = 256;
27
28#[derive(Clone, Default)]
39pub struct Pool {
40 inner: Arc<Inner>,
41}
42
43struct Inner {
44 used: AtomicU64,
46 capacity: AtomicU64,
48 epoch: web_async::time::Instant,
50 lru: Lock<Lru>,
51}
52
53impl Default for Inner {
54 fn default() -> Self {
55 Self {
56 used: AtomicU64::new(0),
57 capacity: AtomicU64::new(u64::MAX),
58 epoch: web_async::time::Instant::now(),
59 lru: Lock::default(),
60 }
61 }
62}
63
64#[derive(Default)]
65struct Lru {
66 heap: BinaryHeap<Reverse<(u64, u64)>>,
70 entries: HashMap<u64, Arc<Entry>>,
73 next_id: u64,
74}
75
76pub(crate) struct Entry {
82 id: u64,
83 bytes: AtomicU64,
85 last_access: AtomicU64,
87 pinned: AtomicBool,
89 evict: Box<dyn Fn() + Send + Sync>,
91 epoch: web_async::time::Instant,
92}
93
94impl Entry {
95 fn now(&self) -> u64 {
96 self.epoch.elapsed().as_millis() as u64
97 }
98
99 pub(crate) fn touch(&self) {
101 self.last_access.store(self.now(), Ordering::Relaxed);
102 }
103
104 pub(crate) fn set_pinned(&self, pinned: bool) {
106 self.pinned.store(pinned, Ordering::Relaxed);
107 }
108}
109
110impl Pool {
111 pub fn new(capacity: u64) -> Self {
116 let pool = Self::default();
117 pool.inner.capacity.store(capacity, Ordering::Relaxed);
118 pool
119 }
120
121 pub fn unbounded() -> Self {
123 Self::default()
124 }
125
126 pub fn capacity(&self) -> Option<u64> {
128 match self.inner.capacity.load(Ordering::Relaxed) {
129 u64::MAX => None,
130 capacity => Some(capacity),
131 }
132 }
133
134 pub fn used(&self) -> u64 {
136 self.inner.used.load(Ordering::Relaxed)
137 }
138
139 pub fn resize(&self, capacity: impl Into<Option<u64>>) {
142 let capacity = capacity.into().unwrap_or(u64::MAX);
143 self.inner.capacity.store(capacity, Ordering::Relaxed);
144 self.evict();
145 }
146
147 pub fn same_pool(&self, other: &Self) -> bool {
149 Arc::ptr_eq(&self.inner, &other.inner)
150 }
151
152 #[cfg(test)]
155 pub(crate) fn debug_entries(&self) -> (u64, Vec<(u64, u64, u64, bool)>) {
156 let now = self.inner.epoch.elapsed().as_millis() as u64;
157 let lru = self.inner.lru.lock();
158 let mut entries: Vec<_> = lru
159 .entries
160 .values()
161 .map(|e| {
162 (
163 e.id,
164 e.last_access.load(Ordering::Relaxed),
165 e.bytes.load(Ordering::Relaxed),
166 e.pinned.load(Ordering::Relaxed),
167 )
168 })
169 .collect();
170 entries.sort_unstable();
171 (now, entries)
172 }
173
174 pub(crate) fn register(&self, evict: Box<dyn Fn() + Send + Sync>) -> Charge {
179 let inner = self.inner.clone();
180 let entry = {
181 let mut lru = inner.lru.lock();
182 let id = lru.next_id;
183 lru.next_id += 1;
184
185 let entry = Arc::new(Entry {
186 id,
187 bytes: AtomicU64::new(ENTRY_OVERHEAD),
188 last_access: AtomicU64::new(0),
189 pinned: AtomicBool::new(false),
190 evict,
191 epoch: inner.epoch,
192 });
193 entry.touch();
194
195 lru.entries.insert(id, entry.clone());
196 lru.heap.push(Reverse((entry.last_access.load(Ordering::Relaxed), id)));
197 entry
198 };
199
200 inner.used.fetch_add(ENTRY_OVERHEAD, Ordering::Relaxed);
201 Charge {
202 inner: Some((inner, entry)),
203 }
204 }
205
206 pub(crate) fn evict(&self) {
211 let inner = &self.inner;
212 if inner.used.load(Ordering::Relaxed) <= inner.capacity.load(Ordering::Relaxed) {
213 return;
214 }
215
216 let mut victims = Vec::new();
217 {
218 let mut lru = inner.lru.lock();
219 let mut freed = 0u64;
221 let mut pinned = Vec::new();
224 let mut budget = 2 * lru.heap.len();
228
229 while budget > 0
230 && inner.used.load(Ordering::Relaxed).saturating_sub(freed) > inner.capacity.load(Ordering::Relaxed)
231 {
232 budget -= 1;
233 let Some(Reverse((snapshot, id))) = lru.heap.pop() else {
234 break;
235 };
236 let Some(entry) = lru.entries.get(&id) else {
237 continue;
239 };
240 let access = entry.last_access.load(Ordering::Relaxed);
241 if access != snapshot {
242 lru.heap.push(Reverse((access, id)));
244 continue;
245 }
246 if entry.pinned.load(Ordering::Relaxed) {
247 pinned.push(Reverse((access, id)));
248 continue;
249 }
250
251 let entry = lru.entries.remove(&id).unwrap();
252 freed += entry.bytes.load(Ordering::Relaxed);
253 victims.push(entry);
254 }
255
256 for slot in pinned {
257 lru.heap.push(slot);
258 }
259 }
260
261 for victim in victims {
262 (victim.evict)();
263 }
264 }
265}
266
267impl std::fmt::Debug for Pool {
268 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
269 f.debug_struct("Pool")
270 .field("used", &self.used())
271 .field("capacity", &self.capacity())
272 .finish()
273 }
274}
275
276#[derive(Default)]
283pub(crate) struct Charge {
284 inner: Option<(Arc<Inner>, Arc<Entry>)>,
285}
286
287impl Charge {
288 pub(crate) fn add(&self, n: u64) {
292 if let Some((inner, entry)) = &self.inner {
293 entry.bytes.fetch_add(n, Ordering::Relaxed);
294 inner.used.fetch_add(n, Ordering::Relaxed);
295 entry.touch();
296 }
297 }
298
299 pub(crate) fn sub(&self, n: u64) {
301 if let Some((inner, entry)) = &self.inner {
302 entry.bytes.fetch_sub(n, Ordering::Relaxed);
303 inner.used.fetch_sub(n, Ordering::Relaxed);
304 }
305 }
306
307 pub(crate) fn clear(&self) {
310 if let Some((inner, entry)) = &self.inner {
311 let bytes = entry.bytes.swap(0, Ordering::Relaxed);
312 inner.used.fetch_sub(bytes, Ordering::Relaxed);
313 }
314 }
315
316 pub(crate) fn touch(&self) {
318 if let Some((_, entry)) = &self.inner {
319 entry.touch();
320 }
321 }
322
323 pub(crate) fn entry(&self) -> Option<Arc<Entry>> {
325 self.inner.as_ref().map(|(_, entry)| entry.clone())
326 }
327}
328
329impl Drop for Charge {
330 fn drop(&mut self) {
331 let Some((inner, entry)) = self.inner.take() else {
332 return;
333 };
334 let bytes = entry.bytes.swap(0, Ordering::Relaxed);
335 inner.used.fetch_sub(bytes, Ordering::Relaxed);
336 inner.lru.lock().entries.remove(&entry.id);
338 }
339}
340
341#[cfg(test)]
342mod test {
343 use super::*;
344 use std::time::Duration;
345
346 fn flag() -> (Arc<AtomicBool>, Box<dyn Fn() + Send + Sync>) {
347 let evicted = Arc::new(AtomicBool::new(false));
348 let hook = evicted.clone();
349 (
350 evicted,
351 Box::new(move || {
352 hook.store(true, Ordering::Relaxed);
353 }),
354 )
355 }
356
357 #[test]
358 fn unbounded_never_evicts() {
359 let pool = Pool::unbounded();
360 let (evicted, hook) = flag();
361 let charge = pool.register(hook);
362 charge.add(1 << 40);
363 pool.evict();
364 assert!(!evicted.load(Ordering::Relaxed));
365 assert_eq!(pool.used(), (1 << 40) + ENTRY_OVERHEAD);
366 drop(charge);
367 assert_eq!(pool.used(), 0);
368 }
369
370 #[test]
371 fn detached_charge_is_noop() {
372 let charge = Charge::default();
373 charge.add(123);
374 charge.sub(23);
375 charge.clear();
376 charge.touch();
377 assert!(charge.entry().is_none());
378 }
379
380 #[tokio::test]
381 async fn evicts_least_recently_used() {
382 tokio::time::pause();
383
384 let pool = Pool::new(3 * ENTRY_OVERHEAD + 2500);
385 let (evicted_a, hook_a) = flag();
386 let (evicted_b, hook_b) = flag();
387 let (evicted_c, hook_c) = flag();
388
389 let a = pool.register(hook_a);
390 a.add(1000);
391 tokio::time::advance(Duration::from_millis(10)).await;
392 let b = pool.register(hook_b);
393 b.add(1000);
394 tokio::time::advance(Duration::from_millis(10)).await;
395
396 a.touch();
398 tokio::time::advance(Duration::from_millis(10)).await;
399
400 let c = pool.register(hook_c);
401 c.add(1000);
402 pool.evict();
404
405 assert!(!evicted_a.load(Ordering::Relaxed));
406 assert!(evicted_b.load(Ordering::Relaxed));
407 assert!(!evicted_c.load(Ordering::Relaxed));
408
409 b.clear();
411 assert_eq!(pool.used(), 2 * (1000 + ENTRY_OVERHEAD));
412 }
413
414 #[tokio::test]
415 async fn pinned_entries_survive() {
416 tokio::time::pause();
417
418 let pool = Pool::new(ENTRY_OVERHEAD);
419 let (evicted_a, hook_a) = flag();
420 let (evicted_b, hook_b) = flag();
421
422 let a = pool.register(hook_a);
423 a.entry().unwrap().set_pinned(true);
424 a.add(1000);
425 tokio::time::advance(Duration::from_millis(10)).await;
426
427 let b = pool.register(hook_b);
428 b.add(1000);
429
430 pool.evict();
431 assert!(!evicted_a.load(Ordering::Relaxed));
433 assert!(evicted_b.load(Ordering::Relaxed));
434
435 b.clear();
438 drop(b);
439 pool.evict();
440 assert!(!evicted_a.load(Ordering::Relaxed));
441 }
442
443 #[tokio::test]
444 async fn resize_evicts() {
445 tokio::time::pause();
446
447 let pool = Pool::unbounded();
448 let (evicted_a, hook_a) = flag();
449 let a = pool.register(hook_a);
450 a.add(1000);
451
452 pool.evict();
453 assert!(!evicted_a.load(Ordering::Relaxed));
454
455 pool.resize(100);
456 assert!(evicted_a.load(Ordering::Relaxed));
457
458 pool.resize(None);
460 assert_eq!(pool.capacity(), None);
461 }
462
463 #[tokio::test]
464 async fn touched_entry_is_rekeyed_not_evicted() {
465 tokio::time::pause();
466
467 let pool = Pool::new(2 * ENTRY_OVERHEAD + 1500);
468 let (evicted_a, hook_a) = flag();
469 let (evicted_b, hook_b) = flag();
470
471 let a = pool.register(hook_a);
472 a.add(1000);
473 tokio::time::advance(Duration::from_millis(10)).await;
474 let b = pool.register(hook_b);
475 b.add(1000);
476 tokio::time::advance(Duration::from_millis(10)).await;
477
478 a.touch();
481 tokio::time::advance(Duration::from_millis(10)).await;
482 pool.evict();
483
484 assert!(!evicted_a.load(Ordering::Relaxed));
485 assert!(evicted_b.load(Ordering::Relaxed));
486 }
487
488 #[test]
489 fn dropped_charge_leaves_stale_heap_slot() {
490 let pool = Pool::new(0);
491 let (evicted_a, hook_a) = flag();
492 let a = pool.register(hook_a);
493 drop(a);
494
495 let (evicted_b, hook_b) = flag();
498 let b = pool.register(hook_b);
499 b.add(1);
500 pool.evict();
501 assert!(!evicted_a.load(Ordering::Relaxed));
502 assert!(evicted_b.load(Ordering::Relaxed));
503 }
504}