Skip to main content
← PSoXide SDK guides and examples · SDK f2fec1c301f1 · All Cargo features enabled; check feature gates before use.

psx_residency_sim/
run.rs

1//! The route simulator: the real engine against the drive model.
2
3use crate::drive::DriveModel;
4use crate::world::{RegionGraph, Route, TICKS_PER_SECOND};
5use alloc::boxed::Box;
6use alloc::vec;
7use alloc::vec::Vec;
8use psx_residency::{
9    Category, CategoryConfig, Class, Config, Eviction, Extent, Host, Placement, PoolConfig,
10    ReadOutcome, ReadRequest, RequestId, Residency, ResourceKey, Submit, Wanted, PAGE_BYTES,
11};
12
13type Engine = Residency<256, 256, 1>;
14
15/// Simulator knobs.
16#[derive(Clone, Debug)]
17pub struct SimConfig {
18    /// The drive.
19    pub drive: DriveModel,
20    /// Pages in the region pool.
21    pub pool_pages: u32,
22    /// Regions within this walk distance of the player are requested (lead
23    /// ring), nearest first.
24    pub lead_radius: u32,
25    /// Unwanted regions within this distance are in the hysteresis band.
26    pub keep_radius: u32,
27    /// Player speed assumed for lead deadlines, units per second. The default
28    /// is the design's `v_max` of 470, a design estimate, not a measurement.
29    pub speed_units_per_second: u32,
30    /// Transport depth.
31    pub max_in_flight: u8,
32    /// Microseconds per simulated tick (16 667: 60 Hz).
33    pub tick_micros: u32,
34    /// Ticks of install work after a region lands (0: none, the region is
35    /// resident when read). The design lists the install cost as unmeasured.
36    pub install_ticks: u32,
37    /// Every Nth read fails (0: none).
38    pub fail_every: u32,
39    /// Placement policy of the pool.
40    pub placement: Placement,
41    /// Whether compaction may move regions.
42    pub movable: bool,
43    /// Bookkeeping budget per step, passed on as the `step_budget` of the
44    /// [`psx_residency::Config`].
45    pub step_budget: u32,
46    /// Regions pinned for the whole run (the home pin), at demand class.
47    pub pinned: Vec<u32>,
48    /// Load the start region's needs before the route begins (a loading
49    /// screen at boot), not counted in the report.
50    pub warm_start: bool,
51}
52
53impl SimConfig {
54    /// Defaults: double-speed nominal drive, queue depth 2, 60 Hz ticks, no
55    /// install cost, no failures, best-fit, movable, warm start, keep radius
56    /// one quarter beyond the lead radius.
57    pub fn new(pool_pages: u32, lead_radius: u32) -> Self {
58        Self {
59            drive: DriveModel::double_speed(),
60            pool_pages,
61            lead_radius,
62            keep_radius: CategoryConfig::keep_radius_for(lead_radius, 0),
63            speed_units_per_second: 470,
64            max_in_flight: 2,
65            tick_micros: 16_667,
66            install_ticks: 0,
67            fail_every: 0,
68            placement: Placement::BestFit,
69            movable: true,
70            step_budget: u32::MAX,
71            pinned: Vec::new(),
72            warm_start: true,
73        }
74    }
75}
76
77/// The worst deadline miss of a run.
78#[derive(Copy, Clone, Debug, PartialEq, Eq)]
79pub struct WorstMiss {
80    /// The region that was late.
81    pub region: u32,
82    /// Tick it became needed.
83    pub need_tick: u32,
84    /// Negative: ticks the player waited.
85    pub slack_ticks: i64,
86}
87
88/// What a simulation found.
89#[derive(Clone, Debug, Default, PartialEq, Eq)]
90pub struct SimReport {
91    /// Ticks simulated (route length).
92    pub ticks: u32,
93    /// Times a region became needed.
94    pub episodes: u32,
95    /// Episodes where the region was not resident when needed (a stall).
96    pub misses: u32,
97    /// Region-ticks needed but not resident.
98    pub miss_ticks: u32,
99    /// Ticks with at least one unmet need (the player would be held).
100    pub stall_ticks: u32,
101    /// Smallest slack over all episodes, in ticks: how long before it was
102    /// needed the region was resident (negative: late). `None` without
103    /// episodes.
104    pub min_slack_ticks: Option<i64>,
105    /// The same in milliseconds (0 without episodes).
106    pub min_slack_millis: i64,
107    /// The worst miss, if any.
108    pub worst_miss: Option<WorstMiss>,
109    /// Regions evicted while the player needed them (must be 0).
110    pub needed_evictions: u32,
111    /// Reads started on the drive.
112    pub reads: u32,
113    /// Reads that needed a seek.
114    pub seeks: u32,
115    /// Sectors read.
116    pub sectors_read: u64,
117    /// Sectors of regions evicted before the player ever needed them.
118    pub wasted_sectors: u64,
119    /// Reads cancelled for a demand.
120    pub cancelled: u32,
121    /// Reads that failed.
122    pub failed_reads: u32,
123    /// Regions evicted.
124    pub evictions: u32,
125    /// Microseconds the drive was busy.
126    pub drive_busy_micros: u64,
127    /// Microseconds simulated.
128    pub run_micros: u64,
129    /// Most pages in use at once.
130    pub peak_pages: u32,
131    /// Worst free-space fragmentation seen, in permille.
132    pub max_fragmentation_permille: u32,
133    /// Requests that found no room (summed over steps).
134    pub protected_full: u32,
135    /// Pages moved by compaction.
136    pub pages_moved: u32,
137    /// Most bookkeeping work units in one step.
138    pub max_step_work: u32,
139    /// Work units over the run.
140    pub total_work: u64,
141    /// A fingerprint of every admission, eviction and request, for
142    /// determinism checks.
143    pub checksum: u32,
144}
145
146impl SimReport {
147    /// Share of the run the drive was busy, in permille.
148    pub fn drive_utilization_permille(&self) -> u32 {
149        (self.drive_busy_micros * 1000)
150            .checked_div(self.run_micros)
151            .unwrap_or(0) as u32
152    }
153}
154
155struct Queued {
156    request: ReadRequest,
157    submitted_us: u64,
158    order: u32,
159}
160
161struct Current {
162    request: ReadRequest,
163    start_us: u64,
164    finish_us: u64,
165    outcome: ReadOutcome,
166}
167
168struct Transport {
169    drive: DriveModel,
170    queue: Vec<Queued>,
171    current: Option<Current>,
172    free_at_us: u64,
173    outcomes: Vec<(RequestId, ResourceKey, ReadOutcome)>,
174    fail_every: u32,
175    submissions: u32,
176    reads: u32,
177    seeks: u32,
178    sectors: u64,
179    busy_us: u64,
180    failed: u32,
181    cancelled: u32,
182}
183
184impl Transport {
185    fn advance(&mut self, now_us: u64) {
186        loop {
187            if self.current.is_none() {
188                let next = self
189                    .queue
190                    .iter()
191                    .enumerate()
192                    .min_by_key(|(_, q)| (q.request.class, q.request.deadline_ticks, q.order))
193                    .map(|(index, _)| index);
194                let Some(index) = next else { return };
195                let start = self.free_at_us.max(self.queue[index].submitted_us);
196                if start > now_us {
197                    return;
198                }
199                let queued = self.queue.remove(index);
200                let (micros, seeked) = self.drive.read(queued.request.lba, queued.request.sectors);
201                self.reads += 1;
202                self.seeks += seeked as u32;
203                self.sectors += queued.request.sectors as u64;
204                let fails = self.fail_every != 0 && self.reads.is_multiple_of(self.fail_every);
205                self.current = Some(Current {
206                    request: queued.request,
207                    start_us: start,
208                    finish_us: start + micros,
209                    outcome: if fails {
210                        ReadOutcome::Failed
211                    } else {
212                        ReadOutcome::Done
213                    },
214                });
215            }
216            let current = self.current.as_ref().unwrap();
217            if current.finish_us > now_us {
218                return;
219            }
220            let current = self.current.take().unwrap();
221            self.busy_us += current.finish_us - current.start_us;
222            self.free_at_us = current.finish_us;
223            match current.outcome {
224                ReadOutcome::Failed => self.failed += 1,
225                ReadOutcome::Cancelled => self.cancelled += 1,
226                ReadOutcome::Done => {}
227            }
228            self.outcomes
229                .push((current.request.id, current.request.key, current.outcome));
230        }
231    }
232}
233
234struct SimHost<'a> {
235    graph: &'a RegionGraph,
236    distances: &'a [u32],
237    transport: &'a mut Transport,
238    now_us: u64,
239}
240
241impl Host for SimHost<'_> {
242    fn extent(&self, key: ResourceKey) -> Option<Extent> {
243        let id = key.index();
244        if id as usize >= self.graph.region_count() {
245            return None;
246        }
247        let region = self.graph.region(id);
248        Some(Extent {
249            lba: region.lba,
250            sectors: region.sectors,
251            pages: region.sectors,
252            bytes: region.sectors * PAGE_BYTES as u32,
253        })
254    }
255
256    fn walk_distance(&self, key: ResourceKey) -> u32 {
257        self.distances
258            .get(key.index() as usize)
259            .copied()
260            .unwrap_or(u32::MAX)
261    }
262
263    fn submit(&mut self, request: &ReadRequest) -> Submit {
264        self.transport.submissions += 1;
265        let order = self.transport.submissions;
266        self.transport.queue.push(Queued {
267            request: *request,
268            submitted_us: self.now_us,
269            order,
270        });
271        Submit::Accepted
272    }
273
274    fn cancel(&mut self, request: RequestId) {
275        let transport = &mut *self.transport;
276        if let Some(index) = transport.queue.iter().position(|q| q.request.id == request) {
277            let queued = transport.queue.remove(index);
278            transport.cancelled += 1;
279            transport
280                .outcomes
281                .push((request, queued.request.key, ReadOutcome::Cancelled));
282        } else if let Some(current) = transport.current.as_mut() {
283            if current.request.id == request {
284                current.finish_us = self.now_us + transport.drive.abort_micros() as u64;
285                current.outcome = ReadOutcome::Cancelled;
286                transport.drive.lose_head();
287            }
288        }
289    }
290
291    fn promote(&mut self, request: RequestId, class: Class) {
292        if let Some(queued) = self
293            .transport
294            .queue
295            .iter_mut()
296            .find(|q| q.request.id == request)
297        {
298            queued.request.class = class;
299        }
300    }
301}
302
303struct Fnv(u32);
304
305impl Fnv {
306    fn feed(&mut self, value: u32) {
307        for byte in value.to_le_bytes() {
308            self.0 = (self.0 ^ byte as u32).wrapping_mul(0x0100_0193);
309        }
310    }
311}
312
313struct Sim<'a> {
314    graph: &'a RegionGraph,
315    config: &'a SimConfig,
316    engine: Box<Engine>,
317    transport: Transport,
318    distance_cache: Vec<Option<Vec<u32>>>,
319    resident: Vec<bool>,
320    admitted_at: Vec<u32>,
321    used: Vec<bool>,
322    in_need: Vec<bool>,
323    need_tick: Vec<Option<u32>>,
324    installing: Option<(ResourceKey, u32)>,
325    report: SimReport,
326    hash: Fnv,
327    /// Absolute tick counter (warm-up included).
328    now_tick: u32,
329    /// Absolute tick at which the route begins (`u32::MAX` while warming up).
330    origin: u32,
331}
332
333fn region_key(id: u32) -> ResourceKey {
334    ResourceKey::new(Category::WORLD_REGION, id).expect("region id fits a key")
335}
336
337impl<'a> Sim<'a> {
338    fn new(graph: &'a RegionGraph, config: &'a SimConfig) -> Self {
339        assert!(
340            graph.region_count() <= 256,
341            "the simulator tracks 256 regions"
342        );
343        let mut engine_config = Config::<1>::new();
344        engine_config.pools[0] = PoolConfig {
345            capacity_pages: config.pool_pages,
346            placement: config.placement,
347        };
348        engine_config.categories[0] = CategoryConfig {
349            pool: 0,
350            max_pages: config.pool_pages,
351            max_entries: 256,
352            movable: config.movable,
353            installs: config.install_ticks > 0,
354            keep_radius: config.keep_radius,
355        };
356        engine_config.max_in_flight = config.max_in_flight;
357        engine_config.step_budget = config.step_budget;
358        let regions = graph.region_count();
359        let mut engine = Box::new(Engine::new(engine_config));
360        for &region in &config.pinned {
361            assert!(engine.pin(region_key(region), Class::Demand));
362        }
363        Self {
364            graph,
365            config,
366            engine,
367            transport: Transport {
368                drive: config.drive.clone(),
369                queue: Vec::new(),
370                current: None,
371                free_at_us: 0,
372                outcomes: Vec::new(),
373                fail_every: config.fail_every,
374                submissions: 0,
375                reads: 0,
376                seeks: 0,
377                sectors: 0,
378                busy_us: 0,
379                failed: 0,
380                cancelled: 0,
381            },
382            distance_cache: vec![None; regions],
383            resident: vec![false; regions],
384            admitted_at: vec![0; regions],
385            used: vec![false; regions],
386            in_need: vec![false; regions],
387            need_tick: vec![None; regions],
388            installing: None,
389            report: SimReport::default(),
390            hash: Fnv(0x811C_9DC5),
391            now_tick: 0,
392            origin: u32::MAX,
393        }
394    }
395
396    fn counting(&self) -> bool {
397        self.now_tick >= self.origin
398    }
399
400    fn note_resident(&mut self, region: u32) {
401        let now = self.engine.is_resident(region_key(region));
402        let index = region as usize;
403        if now && !self.resident[index] {
404            self.admitted_at[index] = self.now_tick;
405            self.used[index] = false;
406            self.hash.feed(self.now_tick << 8 | 1);
407            self.hash.feed(region);
408        }
409        self.resident[index] = now;
410    }
411
412    /// One simulated tick with the player in `player`.
413    fn tick(&mut self, player: u32) {
414        let now_us = self.now_tick as u64 * self.config.tick_micros as u64;
415        self.transport.advance(now_us);
416        let outcomes = core::mem::take(&mut self.transport.outcomes);
417        for (id, key, outcome) in outcomes {
418            self.engine.complete_read(id, outcome);
419            self.note_resident(key.index());
420        }
421        // Install work, one payload at a time.
422        if self.config.install_ticks > 0 {
423            if let Some((key, done)) = self.installing {
424                if self.now_tick >= done {
425                    self.engine.finish_install(key);
426                    self.note_resident(key.index());
427                    self.installing = None;
428                }
429            }
430            if self.installing.is_none() {
431                if let Some(key) = self.engine.next_install(None) {
432                    self.engine.begin_install(key);
433                    self.installing = Some((key, self.now_tick + self.config.install_ticks));
434                }
435            }
436        }
437
438        // What the player wants this tick.
439        if self.distance_cache[player as usize].is_none() {
440            self.distance_cache[player as usize] = Some(self.graph.distances_from(player));
441        }
442        let distances = self.distance_cache[player as usize].take().unwrap();
443        let needs = self.graph.needs(player);
444        let mut wanted = Vec::new();
445        let mut needed = vec![false; self.graph.region_count()];
446        for &region in needs {
447            needed[region as usize] = true;
448            wanted.push(Wanted::new(
449                region_key(region),
450                Class::Demand,
451                distances[region as usize],
452            ));
453        }
454        for (region, &distance) in distances.iter().enumerate() {
455            if !needed[region] && distance <= self.config.lead_radius {
456                let deadline = (distance as u64 * TICKS_PER_SECOND as u64
457                    / self.config.speed_units_per_second.max(1) as u64)
458                    as u32;
459                wanted.push(
460                    Wanted::new(region_key(region as u32), Class::Lead, distance)
461                        .with_deadline_ticks(deadline),
462                );
463            }
464        }
465
466        let mut evictions: Vec<Eviction> = Vec::with_capacity(16);
467        {
468            let mut scratch = [Eviction::NONE; 16];
469            let mut host = SimHost {
470                graph: self.graph,
471                distances: &distances,
472                transport: &mut self.transport,
473                now_us,
474            };
475            let report = self.engine.step(&wanted, &mut host, &mut scratch);
476            for eviction in &scratch[..report.eviction_count] {
477                evictions.push(*eviction);
478            }
479            if self.counting() {
480                let stats = report.stats;
481                self.report.protected_full += stats.protected_full;
482                self.report.pages_moved += stats.pages_moved;
483                self.report.max_step_work = self.report.max_step_work.max(stats.work_used);
484                self.report.total_work += stats.work_used as u64;
485            }
486        }
487        self.distance_cache[player as usize] = Some(distances);
488
489        for eviction in &evictions {
490            let region = eviction.key.index();
491            if self.counting() {
492                self.report.evictions += 1;
493                if needed[region as usize] {
494                    self.report.needed_evictions += 1;
495                }
496                if !self.used[region as usize] {
497                    self.report.wasted_sectors += self.graph.region(region).sectors as u64;
498                }
499            }
500            self.hash.feed(self.now_tick << 8 | 2);
501            self.hash.feed(region);
502            self.note_resident(region);
503        }
504
505        if self.counting() {
506            self.account_needs(&needed);
507            let pool = self.engine.pool(0).unwrap();
508            self.report.peak_pages = self.report.peak_pages.max(pool.used_pages());
509            self.report.max_fragmentation_permille = self
510                .report
511                .max_fragmentation_permille
512                .max(pool.fragmentation().permille());
513        }
514        self.now_tick += 1;
515    }
516
517    fn account_needs(&mut self, needed: &[bool]) {
518        let route_tick = self.now_tick - self.origin;
519        let mut stalled = false;
520        for (region, &is_needed) in needed.iter().enumerate() {
521            let resident = self.resident[region];
522            if is_needed {
523                if resident {
524                    self.used[region] = true;
525                } else {
526                    self.report.miss_ticks += 1;
527                    stalled = true;
528                }
529                if !self.in_need[region] {
530                    self.report.episodes += 1;
531                    if resident {
532                        let slack = self.now_tick as i64 - self.admitted_at[region] as i64;
533                        self.record_slack(region as u32, route_tick, slack);
534                    } else {
535                        self.need_tick[region] = Some(route_tick);
536                    }
537                } else if resident {
538                    if let Some(since) = self.need_tick[region].take() {
539                        let slack = -(route_tick as i64 - since as i64);
540                        self.record_slack(region as u32, since, slack);
541                    }
542                }
543            } else if let Some(since) = self.need_tick[region].take() {
544                // Left the need set without ever being resident.
545                let slack = -(route_tick as i64 - since as i64);
546                self.record_slack(region as u32, since, slack);
547            }
548            self.in_need[region] = is_needed;
549        }
550        if stalled {
551            self.report.stall_ticks += 1;
552        }
553    }
554
555    fn record_slack(&mut self, region: u32, need_tick: u32, slack: i64) {
556        if slack < 0 {
557            self.report.misses += 1;
558            if self
559                .report
560                .worst_miss
561                .is_none_or(|worst| slack < worst.slack_ticks)
562            {
563                self.report.worst_miss = Some(WorstMiss {
564                    region,
565                    need_tick,
566                    slack_ticks: slack,
567                });
568            }
569        }
570        if self.report.min_slack_ticks.is_none_or(|min| slack < min) {
571            self.report.min_slack_ticks = Some(slack);
572        }
573    }
574}
575
576/// Replay `route` over `graph` against the real residency engine and the
577/// drive model, and report deadline slack and misses.
578///
579/// Each tick the player's region fixes the wanted list: the regions the graph
580/// says it needs at demand class, every other region within the lead radius at
581/// lead class with a deadline of its walk distance at the assumed speed. The
582/// engine steps once per tick; the drive model decides when each read lands.
583pub fn simulate(graph: &RegionGraph, route: &Route, config: &SimConfig) -> SimReport {
584    let mut sim = Sim::new(graph, config);
585    let Some(start) = route
586        .region_at(0)
587        .or_else(|| route.samples().first().map(|s| s.region))
588    else {
589        return SimReport::default();
590    };
591    if config.warm_start {
592        let limit = 10 * 60 * TICKS_PER_SECOND;
593        while sim.now_tick < limit {
594            sim.tick(start);
595            if graph.needs(start).iter().all(|&r| sim.resident[r as usize]) {
596                break;
597            }
598        }
599    }
600    sim.origin = sim.now_tick;
601    let before = (
602        sim.transport.reads,
603        sim.transport.seeks,
604        sim.transport.sectors,
605        sim.transport.busy_us,
606        sim.transport.failed,
607        sim.transport.cancelled,
608    );
609    for route_tick in 0..route.end_tick() {
610        let player = route.region_at(route_tick).unwrap_or(start);
611        sim.tick(player);
612    }
613    // Needs still unresolved at the end are misses.
614    for region in 0..graph.region_count() {
615        if let Some(since) = sim.need_tick[region].take() {
616            let slack = -(route.end_tick() as i64 - since as i64);
617            sim.record_slack(region as u32, since, slack);
618        }
619    }
620    let mut report = sim.report.clone();
621    report.ticks = route.end_tick();
622    report.reads = sim.transport.reads - before.0;
623    report.seeks = sim.transport.seeks - before.1;
624    report.sectors_read = sim.transport.sectors - before.2;
625    report.drive_busy_micros = sim.transport.busy_us - before.3;
626    report.failed_reads = sim.transport.failed - before.4;
627    report.cancelled = sim.transport.cancelled - before.5;
628    report.run_micros = route.end_tick() as u64 * config.tick_micros as u64;
629    report.min_slack_millis = report
630        .min_slack_ticks
631        .map_or(0, |ticks| ticks * config.tick_micros as i64 / 1000);
632    report.checksum = sim.hash.0;
633    report
634}
635
636#[cfg(test)]
637mod tests {
638    use super::*;
639    use crate::drive::Variance;
640    use crate::world::RegionGraph;
641
642    fn chain(count: u32, sectors: u32, length: u32) -> (RegionGraph, Route) {
643        let mut graph = RegionGraph::chain(count, sectors, length);
644        graph.layout_in_order(1_000, 0);
645        graph.needs_with_neighbors();
646        let path: Vec<u32> = (0..count).collect();
647        let route = Route::walk(&graph, &path, 375);
648        (graph, route)
649    }
650
651    #[test]
652    #[cfg_attr(miri, ignore)] // exhaustive sweep, runs natively
653    fn a_sprint_down_a_chain_of_sixteen_sector_regions_has_slack() {
654        let (graph, route) = chain(12, 16, 400);
655        let report = simulate(&graph, &route, &SimConfig::new(16 * 8, 900));
656        assert_eq!(report.misses, 0, "{report:?}");
657        assert_eq!(report.stall_ticks, 0);
658        assert_eq!(report.needed_evictions, 0);
659        assert!(report.min_slack_millis > 0);
660        // Contiguous regions chain without a seek: at most the first read
661        // seeks.
662        assert!(report.seeks <= 1, "{report:?}");
663    }
664
665    #[test]
666    #[cfg_attr(miri, ignore)] // exhaustive sweep, runs natively
667    fn a_lead_radius_shorter_than_the_need_closure_is_caught() {
668        // Entering region n needs region n + 1, two edges (800 units) ahead of
669        // region n - 1; a lead radius of 700 asks for it one region too late.
670        let (graph, route) = chain(24, 16, 400);
671        let report = simulate(&graph, &route, &SimConfig::new(16 * 6, 700));
672        assert!(report.misses > 20, "{report:?}");
673        // The late read still costs one region of transfer (104 ms), not more.
674        assert!(report.min_slack_millis > -300, "{report:?}");
675    }
676
677    #[test]
678    #[cfg_attr(miri, ignore)] // exhaustive sweep, runs natively
679    fn regions_too_big_for_the_drive_miss_and_the_report_says_which() {
680        // 64 sectors take 415 ms to read; one is needed every 107 ms.
681        let (graph, route) = chain(12, 64, 100);
682        let mut config = SimConfig::new(64 * 6, 300);
683        config.speed_units_per_second = 375;
684        let report = simulate(&graph, &route, &config);
685        assert!(report.misses > 0, "{report:?}");
686        assert!(report.stall_ticks > 0);
687        let worst = report.worst_miss.unwrap();
688        assert!(worst.slack_ticks < 0);
689        assert!(report.min_slack_millis < 0);
690        assert!(report.drive_utilization_permille() > 800);
691    }
692
693    #[test]
694    #[cfg_attr(miri, ignore)] // exhaustive sweep, runs natively
695    fn the_same_inputs_give_the_same_report_and_seeded_variance_changes_it() {
696        let (graph, route) = chain(10, 16, 400);
697        let mut config = SimConfig::new(16 * 8, 900);
698        let a = simulate(&graph, &route, &config);
699        let b = simulate(&graph, &route, &config);
700        assert_eq!(a, b);
701        config.drive = DriveModel::double_speed().with_variance(Variance::Seeded(3));
702        // A scattered layout makes every read seek, so variance shows.
703        let (mut scattered, route2) = chain(10, 16, 400);
704        for region in 0..10 {
705            scattered.place(region, 1_000 + (region * 7919) % 10_000);
706        }
707        let nominal = simulate(&scattered, &route2, &SimConfig::new(16 * 8, 900));
708        let varied = simulate(&scattered, &route2, &config);
709        assert_ne!(nominal.checksum, varied.checksum);
710        assert!(varied.drive_busy_micros >= nominal.drive_busy_micros);
711        let again = simulate(&scattered, &route2, &config);
712        assert_eq!(varied, again);
713    }
714
715    #[test]
716    #[cfg_attr(miri, ignore)] // exhaustive sweep, runs natively
717    fn the_pessimistic_corner_is_slower_than_the_nominal_one() {
718        let (mut graph, route) = chain(10, 16, 400);
719        for region in 0..10 {
720            graph.place(region, 1_000 + ((region * 4_001) % 20_000));
721        }
722        let nominal = simulate(&graph, &route, &SimConfig::new(16 * 8, 900));
723        let mut config = SimConfig::new(16 * 8, 900);
724        config.drive = DriveModel::double_speed().with_variance(Variance::Pessimistic);
725        let pessimistic = simulate(&graph, &route, &config);
726        assert!(pessimistic.drive_busy_micros > nominal.drive_busy_micros);
727        assert!(pessimistic.min_slack_ticks <= nominal.min_slack_ticks);
728    }
729
730    #[test]
731    #[cfg_attr(miri, ignore)] // exhaustive sweep, runs natively
732    fn backtracking_inside_the_keep_radius_does_not_read_again() {
733        let (graph, _) = chain(6, 16, 400);
734        let path = [0, 1, 2, 3, 2, 1, 2, 3];
735        let route = Route::walk(&graph, &path, 375);
736        let config = SimConfig::new(16 * 8, 900);
737        let report = simulate(&graph, &route, &config);
738        assert_eq!(report.misses, 0);
739        let straight = simulate(&graph, &Route::walk(&graph, &[0, 1, 2, 3], 375), &config);
740        assert_eq!(report.reads, straight.reads, "no region is read twice");
741    }
742
743    #[test]
744    #[cfg_attr(miri, ignore)] // exhaustive sweep, runs natively
745    fn a_pool_smaller_than_the_world_evicts_behind_the_player_and_never_what_is_needed() {
746        let (graph, route) = chain(24, 16, 400);
747        let report = simulate(&graph, &route, &SimConfig::new(16 * 6, 900));
748        assert_eq!(report.misses, 0, "{report:?}");
749        assert_eq!(report.needed_evictions, 0);
750        assert!(report.evictions >= 10, "{report:?}");
751        assert!(report.peak_pages <= 16 * 6);
752    }
753
754    #[test]
755    #[cfg_attr(miri, ignore)] // exhaustive sweep, runs natively
756    fn failing_reads_are_retried_and_the_run_still_completes() {
757        let (graph, route) = chain(10, 16, 400);
758        let mut config = SimConfig::new(16 * 8, 900);
759        config.fail_every = 4;
760        let report = simulate(&graph, &route, &config);
761        assert!(report.failed_reads > 0, "{report:?}");
762        // The retry backoff (16 windows = 267 ms) costs slack but nothing is
763        // abandoned: the route ends with its needs resident.
764        let clean = simulate(&graph, &route, &SimConfig::new(16 * 8, 900));
765        assert!(report.reads > clean.reads, "failed reads were read again");
766    }
767
768    #[test]
769    #[cfg_attr(miri, ignore)] // exhaustive sweep, runs natively
770    fn install_time_eats_slack() {
771        let (graph, route) = chain(12, 16, 400);
772        let free = simulate(&graph, &route, &SimConfig::new(16 * 8, 900));
773        let mut config = SimConfig::new(16 * 8, 900);
774        config.install_ticks = 6;
775        let slow = simulate(&graph, &route, &config);
776        assert!(slow.min_slack_ticks <= free.min_slack_ticks);
777        assert_eq!(slow.needed_evictions, 0);
778    }
779
780    #[test]
781    fn an_empty_route_reports_nothing() {
782        let (graph, _) = chain(3, 4, 100);
783        let report = simulate(&graph, &Route::default(), &SimConfig::new(32, 300));
784        assert_eq!(report, SimReport::default());
785    }
786}