1use 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#[derive(Clone, Debug)]
17pub struct SimConfig {
18 pub drive: DriveModel,
20 pub pool_pages: u32,
22 pub lead_radius: u32,
25 pub keep_radius: u32,
27 pub speed_units_per_second: u32,
30 pub max_in_flight: u8,
32 pub tick_micros: u32,
34 pub install_ticks: u32,
37 pub fail_every: u32,
39 pub placement: Placement,
41 pub movable: bool,
43 pub step_budget: u32,
46 pub pinned: Vec<u32>,
48 pub warm_start: bool,
51}
52
53impl SimConfig {
54 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#[derive(Copy, Clone, Debug, PartialEq, Eq)]
79pub struct WorstMiss {
80 pub region: u32,
82 pub need_tick: u32,
84 pub slack_ticks: i64,
86}
87
88#[derive(Clone, Debug, Default, PartialEq, Eq)]
90pub struct SimReport {
91 pub ticks: u32,
93 pub episodes: u32,
95 pub misses: u32,
97 pub miss_ticks: u32,
99 pub stall_ticks: u32,
101 pub min_slack_ticks: Option<i64>,
105 pub min_slack_millis: i64,
107 pub worst_miss: Option<WorstMiss>,
109 pub needed_evictions: u32,
111 pub reads: u32,
113 pub seeks: u32,
115 pub sectors_read: u64,
117 pub wasted_sectors: u64,
119 pub cancelled: u32,
121 pub failed_reads: u32,
123 pub evictions: u32,
125 pub drive_busy_micros: u64,
127 pub run_micros: u64,
129 pub peak_pages: u32,
131 pub max_fragmentation_permille: u32,
133 pub protected_full: u32,
135 pub pages_moved: u32,
137 pub max_step_work: u32,
139 pub total_work: u64,
141 pub checksum: u32,
144}
145
146impl SimReport {
147 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 now_tick: u32,
329 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 ®ion 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 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 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 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 ®ion 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 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
576pub 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 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)] 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 assert!(report.seeks <= 1, "{report:?}");
663 }
664
665 #[test]
666 #[cfg_attr(miri, ignore)] fn a_lead_radius_shorter_than_the_need_closure_is_caught() {
668 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 assert!(report.min_slack_millis > -300, "{report:?}");
675 }
676
677 #[test]
678 #[cfg_attr(miri, ignore)] fn regions_too_big_for_the_drive_miss_and_the_report_says_which() {
680 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)] 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 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)] 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)] 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)] 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)] 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 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)] 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}