1use std::{
4 collections::{BTreeMap, BTreeSet, VecDeque},
5 time::Duration,
6};
7
8use serde::{Deserialize, Serialize};
9
10use super::OffloadUnitId;
11
12#[derive(Debug, Default, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)]
14pub struct BackgroundPrefetchReport {
15 submitted: u64,
16 coalesced: u64,
17 started: u64,
18 completed: u64,
19 cancelled: u64,
20 failed: u64,
21 queue_capacity: usize,
22 peak_queue_occupancy: usize,
23 backpressure_count: u64,
24 backpressure_duration: Duration,
25 demand_waits: u64,
26 demand_wait_duration: Duration,
27 ready_before_demand: u64,
28 in_flight_at_demand: u64,
29 evicted_before_use: u64,
30}
31
32impl BackgroundPrefetchReport {
33 pub const fn submitted(self) -> u64 {
35 self.submitted
36 }
37
38 pub const fn coalesced(self) -> u64 {
40 self.coalesced
41 }
42
43 pub const fn started(self) -> u64 {
45 self.started
46 }
47
48 pub const fn completed(self) -> u64 {
50 self.completed
51 }
52
53 pub const fn cancelled(self) -> u64 {
55 self.cancelled
56 }
57
58 pub const fn failed(self) -> u64 {
60 self.failed
61 }
62
63 pub const fn queue_capacity(self) -> usize {
65 self.queue_capacity
66 }
67
68 pub const fn peak_queue_occupancy(self) -> usize {
70 self.peak_queue_occupancy
71 }
72
73 pub const fn backpressure_count(self) -> u64 {
75 self.backpressure_count
76 }
77
78 pub const fn backpressure_duration(self) -> Duration {
80 self.backpressure_duration
81 }
82
83 pub const fn demand_waits(self) -> u64 {
85 self.demand_waits
86 }
87
88 pub const fn demand_wait_duration(self) -> Duration {
90 self.demand_wait_duration
91 }
92
93 pub const fn ready_before_demand(self) -> u64 {
95 self.ready_before_demand
96 }
97
98 pub const fn in_flight_at_demand(self) -> u64 {
100 self.in_flight_at_demand
101 }
102
103 pub const fn evicted_before_use(self) -> u64 {
105 self.evicted_before_use
106 }
107}
108
109#[derive(Debug, Clone, Eq, PartialEq)]
111pub struct PrefetchWork {
112 generation: u64,
113 id: OffloadUnitId,
114}
115
116impl PrefetchWork {
117 pub const fn generation(&self) -> u64 {
119 self.generation
120 }
121
122 pub const fn id(&self) -> &OffloadUnitId {
124 &self.id
125 }
126}
127
128#[derive(Debug, Clone, Eq, PartialEq)]
130pub enum PrefetchAdmission {
131 Admitted(PrefetchWork),
133 Coalesced,
135 AtCapacity,
137}
138
139#[derive(Debug, Clone, Copy, Eq, PartialEq)]
141pub enum PrefetchDemandObservation {
142 Queued,
144 InFlight,
146 Ready,
148 Failed,
150 Unscheduled,
152}
153
154impl PrefetchDemandObservation {
155 pub const fn is_pending(self) -> bool {
157 matches!(self, Self::Queued | Self::InFlight)
158 }
159}
160
161#[derive(Debug, Clone, Eq, PartialEq)]
163pub enum PrefetchDemandResolution<E> {
164 Ready,
166 Failed(E),
168 Unscheduled,
170}
171
172#[derive(Debug, Clone, Copy, Eq, PartialEq)]
174pub enum PrefetchCompletion {
175 Published,
177 Failed,
179 Discarded,
181}
182
183#[derive(Debug)]
190pub struct PrefetchExecutionState<E> {
191 generation: u64,
192 queue_capacity: usize,
193 queue: VecDeque<PrefetchWork>,
194 queued: BTreeSet<OffloadUnitId>,
195 in_flight: BTreeMap<OffloadUnitId, u64>,
196 completed: BTreeSet<OffloadUnitId>,
197 failures: BTreeMap<OffloadUnitId, E>,
198 report: BackgroundPrefetchReport,
199}
200
201impl<E> PrefetchExecutionState<E> {
202 pub fn new(queue_capacity: usize) -> Result<Self, PrefetchStateError> {
204 if queue_capacity == 0 {
205 return Err(PrefetchStateError::ZeroQueueCapacity);
206 }
207 Ok(Self {
208 generation: 0,
209 queue_capacity,
210 queue: VecDeque::new(),
211 queued: BTreeSet::new(),
212 in_flight: BTreeMap::new(),
213 completed: BTreeSet::new(),
214 failures: BTreeMap::new(),
215 report: BackgroundPrefetchReport {
216 queue_capacity,
217 ..BackgroundPrefetchReport::default()
218 },
219 })
220 }
221
222 pub fn admit(&mut self, id: OffloadUnitId, resident: bool) -> PrefetchAdmission {
228 if self.queued.contains(&id) || self.in_flight.contains_key(&id) {
229 self.report.coalesced = self.report.coalesced.saturating_add(1);
230 return PrefetchAdmission::Coalesced;
231 }
232
233 if self.completed.contains(&id) && !resident {
234 self.completed.remove(&id);
235 self.report.evicted_before_use = self.report.evicted_before_use.saturating_add(1);
236 }
237 if resident {
238 self.failures.remove(&id);
239 self.completed.insert(id);
240 self.report.coalesced = self.report.coalesced.saturating_add(1);
241 return PrefetchAdmission::Coalesced;
242 }
243 if self.queue.len() == self.queue_capacity {
244 return PrefetchAdmission::AtCapacity;
245 }
246
247 self.failures.remove(&id);
250 let work = PrefetchWork {
251 generation: self.generation,
252 id,
253 };
254 self.queued.insert(work.id.clone());
255 self.queue.push_back(work.clone());
256 self.report.submitted = self.report.submitted.saturating_add(1);
257 self.report.peak_queue_occupancy = self.report.peak_queue_occupancy.max(self.queue.len());
258 PrefetchAdmission::Admitted(work)
259 }
260
261 pub fn rollback_admission(&mut self, work: &PrefetchWork) -> Result<(), PrefetchStateError> {
263 let Some(position) = self.queue.iter().position(|queued| queued == work) else {
264 return Err(PrefetchStateError::WorkNotQueued {
265 id: work.id.clone(),
266 generation: work.generation,
267 });
268 };
269 self.queue.remove(position);
270 self.queued.remove(&work.id);
271 self.report.submitted = self.report.submitted.saturating_sub(1);
272 Ok(())
273 }
274
275 pub fn begin_next(&mut self) -> Option<PrefetchWork> {
277 let work = self.queue.pop_front()?;
278 self.queued.remove(&work.id);
279 self.in_flight.insert(work.id.clone(), work.generation);
280 self.report.started = self.report.started.saturating_add(1);
281 Some(work)
282 }
283
284 pub fn complete(
286 &mut self,
287 work: PrefetchWork,
288 result: Result<(), E>,
289 ) -> Result<PrefetchCompletion, PrefetchStateError> {
290 let Some(active_generation) = self.in_flight.get(&work.id).copied() else {
291 return Err(PrefetchStateError::WorkNotInFlight {
292 id: work.id,
293 generation: work.generation,
294 });
295 };
296 if active_generation != work.generation {
297 return Err(PrefetchStateError::CompletionGenerationMismatch {
298 id: work.id,
299 expected: active_generation,
300 actual: work.generation,
301 });
302 }
303 self.in_flight.remove(&work.id);
304 if work.generation != self.generation {
305 self.report.cancelled = self.report.cancelled.saturating_add(1);
306 return Ok(PrefetchCompletion::Discarded);
307 }
308 match result {
309 Ok(()) => {
310 self.completed.insert(work.id);
311 self.report.completed = self.report.completed.saturating_add(1);
312 Ok(PrefetchCompletion::Published)
313 }
314 Err(error) => {
315 self.failures.insert(work.id, error);
316 self.report.failed = self.report.failed.saturating_add(1);
317 Ok(PrefetchCompletion::Failed)
318 }
319 }
320 }
321
322 pub fn observe_demand(&mut self, id: &OffloadUnitId) -> PrefetchDemandObservation {
324 if self.queued.contains(id) {
325 PrefetchDemandObservation::Queued
326 } else if self.in_flight.contains_key(id) {
327 self.report.in_flight_at_demand = self.report.in_flight_at_demand.saturating_add(1);
328 PrefetchDemandObservation::InFlight
329 } else if self.failures.contains_key(id) {
330 PrefetchDemandObservation::Failed
331 } else if self.completed.contains(id) {
332 PrefetchDemandObservation::Ready
333 } else {
334 PrefetchDemandObservation::Unscheduled
335 }
336 }
337
338 pub fn is_pending(&self, id: &OffloadUnitId) -> bool {
340 self.queued.contains(id) || self.in_flight.contains_key(id)
341 }
342
343 pub fn resolve_demand(
345 &mut self,
346 id: &OffloadUnitId,
347 waited: Option<Duration>,
348 ) -> Result<PrefetchDemandResolution<E>, PrefetchStateError> {
349 if self.is_pending(id) {
350 return Err(PrefetchStateError::DemandStillPending { id: id.clone() });
351 }
352 if let Some(duration) = waited {
353 self.report.demand_waits = self.report.demand_waits.saturating_add(1);
354 self.report.demand_wait_duration =
355 self.report.demand_wait_duration.saturating_add(duration);
356 }
357 if let Some(error) = self.failures.remove(id) {
358 return Ok(PrefetchDemandResolution::Failed(error));
359 }
360 if self.completed.remove(id) {
361 self.report.ready_before_demand = self.report.ready_before_demand.saturating_add(1);
362 return Ok(PrefetchDemandResolution::Ready);
363 }
364 Ok(PrefetchDemandResolution::Unscheduled)
365 }
366
367 pub fn begin_backpressure(&mut self) {
369 self.report.backpressure_count = self.report.backpressure_count.saturating_add(1);
370 }
371
372 pub fn finish_backpressure(&mut self, duration: Duration) {
374 self.report.backpressure_duration =
375 self.report.backpressure_duration.saturating_add(duration);
376 }
377
378 pub fn cancel_all(&mut self) -> Result<(), PrefetchStateError> {
384 self.generation = self
385 .generation
386 .checked_add(1)
387 .ok_or(PrefetchStateError::GenerationExhausted)?;
388 self.report.cancelled = self
389 .report
390 .cancelled
391 .saturating_add(self.queue.len() as u64);
392 self.queue.clear();
393 self.queued.clear();
394 Ok(())
395 }
396
397 pub fn finish_cancellation(
402 &mut self,
403 ) -> Result<Option<(OffloadUnitId, E)>, PrefetchStateError> {
404 if !self.is_idle() {
405 return Err(PrefetchStateError::CancellationStillInFlight);
406 }
407 self.completed.clear();
408 let failure = self.failures.pop_first();
409 self.failures.clear();
410 Ok(failure)
411 }
412
413 pub fn is_idle(&self) -> bool {
415 self.queue.is_empty() && self.in_flight.is_empty()
416 }
417
418 pub const fn generation(&self) -> u64 {
420 self.generation
421 }
422
423 pub const fn report(&self) -> BackgroundPrefetchReport {
425 self.report
426 }
427}
428
429#[derive(Debug, Clone, Eq, PartialEq, thiserror::Error)]
431pub enum PrefetchStateError {
432 #[error("background prefetch queue capacity must be nonzero")]
434 ZeroQueueCapacity,
435 #[error("background prefetch cancellation generation exhausted")]
437 GenerationExhausted,
438 #[error("prefetch work {id} generation {generation} is not queued")]
440 WorkNotQueued {
441 id: OffloadUnitId,
443 generation: u64,
445 },
446 #[error("prefetch work {id} generation {generation} is not in flight")]
448 WorkNotInFlight {
449 id: OffloadUnitId,
451 generation: u64,
453 },
454 #[error("prefetch completion generation mismatch for {id}: expected {expected}, got {actual}")]
456 CompletionGenerationMismatch {
457 id: OffloadUnitId,
459 expected: u64,
461 actual: u64,
463 },
464 #[error("prefetch demand for {id} is still pending")]
466 DemandStillPending {
467 id: OffloadUnitId,
469 },
470 #[error("background prefetch cancellation still owns in-flight work")]
472 CancellationStillInFlight,
473}
474
475#[cfg(test)]
476mod tests {
477 use super::*;
478
479 fn id(value: &str) -> OffloadUnitId {
480 OffloadUnitId::new(value).unwrap()
481 }
482
483 #[derive(Debug, Default)]
484 struct MockBackend {
485 executed: Vec<OffloadUnitId>,
486 }
487
488 impl MockBackend {
489 fn execute(
490 &mut self,
491 state: &mut PrefetchExecutionState<&'static str>,
492 result: Result<(), &'static str>,
493 ) -> PrefetchCompletion {
494 let work = state.begin_next().expect("mock backend has admitted work");
495 self.executed.push(work.id().clone());
496 state.complete(work, result).unwrap()
497 }
498 }
499
500 #[test]
501 fn mock_backend_reuses_fifo_admission_coalescing_and_exact_completion() {
502 let mut state = PrefetchExecutionState::new(2).unwrap();
503 let first = id("layer.0");
504 let second = id("layer.1");
505 let third = id("layer.2");
506
507 assert!(matches!(
508 state.admit(first.clone(), false),
509 PrefetchAdmission::Admitted(_)
510 ));
511 assert_eq!(
512 state.admit(first.clone(), false),
513 PrefetchAdmission::Coalesced
514 );
515 assert!(matches!(
516 state.admit(second.clone(), false),
517 PrefetchAdmission::Admitted(_)
518 ));
519 assert_eq!(
520 state.admit(third.clone(), false),
521 PrefetchAdmission::AtCapacity
522 );
523
524 let mut backend = MockBackend::default();
525 assert_eq!(
526 backend.execute(&mut state, Ok(())),
527 PrefetchCompletion::Published
528 );
529 assert!(matches!(
530 state.admit(third.clone(), false),
531 PrefetchAdmission::Admitted(_)
532 ));
533 backend.execute(&mut state, Ok(()));
534 backend.execute(&mut state, Ok(()));
535 assert_eq!(backend.executed, [first, second, third]);
536 assert_eq!(state.report().submitted(), 3);
537 assert_eq!(state.report().coalesced(), 1);
538 assert_eq!(state.report().peak_queue_occupancy(), 2);
539 }
540
541 #[test]
542 fn cancellation_discards_queue_but_retains_exact_in_flight_ownership() {
543 let mut state = PrefetchExecutionState::<()>::new(2).unwrap();
544 let active = id("layer.0");
545 let queued = id("layer.1");
546 state.admit(active.clone(), false);
547 state.admit(queued, false);
548 let work = state.begin_next().unwrap();
549
550 state.cancel_all().unwrap();
551 assert!(!state.is_idle());
552 assert!(matches!(
553 state.finish_cancellation(),
554 Err(PrefetchStateError::CancellationStillInFlight)
555 ));
556 assert_eq!(
557 state.complete(work, Ok(())).unwrap(),
558 PrefetchCompletion::Discarded
559 );
560 assert!(state.is_idle());
561 assert_eq!(state.finish_cancellation().unwrap(), None);
562 assert_eq!(state.report().cancelled(), 2);
563 assert_eq!(state.report().completed(), 0);
564 assert_eq!(
565 state.observe_demand(&active),
566 PrefetchDemandObservation::Unscheduled
567 );
568 }
569
570 #[test]
571 fn failure_is_delivered_once_and_a_new_attempt_supersedes_it() {
572 let mut state = PrefetchExecutionState::new(1).unwrap();
573 let unit = id("layer.0");
574 state.admit(unit.clone(), false);
575 let work = state.begin_next().unwrap();
576 state.complete(work, Err("disk read failed")).unwrap();
577 assert_eq!(
578 state.observe_demand(&unit),
579 PrefetchDemandObservation::Failed
580 );
581
582 assert!(matches!(
583 state.admit(unit.clone(), false),
584 PrefetchAdmission::Admitted(_)
585 ));
586 assert_eq!(
587 state.observe_demand(&unit),
588 PrefetchDemandObservation::Queued
589 );
590 let work = state.begin_next().unwrap();
591 state.complete(work, Ok(())).unwrap();
592 assert_eq!(
593 state
594 .resolve_demand(&unit, Some(Duration::from_millis(3)))
595 .unwrap(),
596 PrefetchDemandResolution::Ready
597 );
598 assert_eq!(state.report().failed(), 1);
599 assert_eq!(state.report().completed(), 1);
600 assert_eq!(state.report().demand_waits(), 1);
601 }
602
603 #[test]
604 fn rollback_and_residency_observation_preserve_admission_accounting() {
605 let mut state = PrefetchExecutionState::<()>::new(1).unwrap();
606 let unit = id("layer.0");
607 let PrefetchAdmission::Admitted(work) = state.admit(unit.clone(), false) else {
608 panic!("missing unit should be admitted");
609 };
610 state.rollback_admission(&work).unwrap();
611 assert_eq!(state.report().submitted(), 0);
612 assert!(state.is_idle());
613
614 assert_eq!(
615 state.admit(unit.clone(), true),
616 PrefetchAdmission::Coalesced
617 );
618 assert_eq!(
619 state.observe_demand(&unit),
620 PrefetchDemandObservation::Ready
621 );
622 assert_eq!(
623 state.resolve_demand(&unit, None).unwrap(),
624 PrefetchDemandResolution::Ready
625 );
626 assert_eq!(state.report().ready_before_demand(), 1);
627 }
628
629 #[test]
630 fn report_serialization_round_trip_preserves_stable_fields() {
631 let mut state = PrefetchExecutionState::<()>::new(3).unwrap();
632 state.begin_backpressure();
633 assert_eq!(state.report().backpressure_count(), 1);
634 assert_eq!(state.report().backpressure_duration(), Duration::ZERO);
635 state.finish_backpressure(Duration::from_millis(7));
636 state.admit(id("layer.0"), false);
637 let report = state.report();
638 let encoded = serde_json::to_string(&report).unwrap();
639 let decoded: BackgroundPrefetchReport = serde_json::from_str(&encoded).unwrap();
640 assert_eq!(decoded, report);
641 assert_eq!(decoded.queue_capacity(), 3);
642 assert_eq!(decoded.backpressure_count(), 1);
643 assert_eq!(decoded.backpressure_duration(), Duration::from_millis(7));
644 }
645}