use std::fmt::{self, Display};
use std::sync::RwLock;
use std::thread::ThreadId;
use foldhash::{HashMap, HashMapExt};
use nonempty::NonEmpty;
use rand::prelude::*;
use rand::rng;
use crate::fake::HardwareBuilder;
use crate::pal::{AbstractProcessor, Platform, ProcessorFacade};
use crate::{EfficiencyClass, MemoryRegionId, ProcessorId};
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
pub(crate) struct FakeProcessor {
id: ProcessorId,
memory_region_id: MemoryRegionId,
efficiency_class: EfficiencyClass,
}
impl Display for FakeProcessor {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(
f,
"FakeProcessor({} in region {}, {:?})",
self.id, self.memory_region_id, self.efficiency_class
)
}
}
impl FakeProcessor {
pub(crate) fn new(
id: ProcessorId,
memory_region_id: MemoryRegionId,
efficiency_class: EfficiencyClass,
) -> Self {
Self {
id,
memory_region_id,
efficiency_class,
}
}
}
impl AbstractProcessor for FakeProcessor {
fn id(&self) -> ProcessorId {
self.id
}
fn memory_region_id(&self) -> MemoryRegionId {
self.memory_region_id
}
fn efficiency_class(&self) -> EfficiencyClass {
self.efficiency_class
}
}
#[derive(Clone, Debug, Default)]
struct FakeThreadState {
allowed_processors: Option<NonEmpty<ProcessorId>>,
}
#[derive(Debug)]
pub(crate) struct FakePlatform {
processors: Vec<FakeProcessor>,
max_processor_id: ProcessorId,
max_memory_region_id: MemoryRegionId,
max_processor_time: f64,
thread_states: RwLock<HashMap<ThreadId, FakeThreadState>>,
#[cfg(test)]
processor_id_override: RwLock<Option<ProcessorId>>,
}
impl FakePlatform {
pub(crate) fn from_builder(builder: &HardwareBuilder) -> Self {
let configured_processors = builder.build_processors();
assert!(
!configured_processors.is_empty(),
"at least one processor must be configured"
);
let processors: Vec<FakeProcessor> = configured_processors
.iter()
.map(|p| FakeProcessor::new(p.id, p.memory_region_id, p.efficiency_class))
.collect();
let max_processor_id = processors.iter().map(|p| p.id).max().unwrap_or(0);
let max_memory_region_id = processors
.iter()
.map(|p| p.memory_region_id)
.max()
.unwrap_or(0);
#[expect(
clippy::cast_precision_loss,
reason = "processor count is small enough for precise f64 representation"
)]
let max_processor_time = builder
.build_max_processor_time()
.unwrap_or(processors.len() as f64);
Self {
processors,
max_processor_id,
max_memory_region_id,
max_processor_time,
thread_states: RwLock::new(HashMap::new()),
#[cfg(test)]
processor_id_override: RwLock::new(None),
}
}
fn thread_processor_id(&self, thread_id: ThreadId) -> ProcessorId {
let allowed = {
let states = self
.thread_states
.read()
.expect("thread state lock should never be poisoned");
states
.get(&thread_id)
.and_then(|s| s.allowed_processors.clone())
};
match allowed {
Some(processors) => {
*processors
.iter()
.choose(&mut rng())
.expect("allowed processors is non-empty")
}
None => {
self.processors
.iter()
.choose(&mut rng())
.expect("at least one processor was configured")
.id
}
}
}
#[cfg(test)]
pub(crate) fn set_processor_id_override(&self, id: Option<ProcessorId>) {
*self
.processor_id_override
.write()
.expect("processor_id_override lock should never be poisoned") = id;
}
}
impl Platform for FakePlatform {
fn get_all_processors(&self) -> NonEmpty<ProcessorFacade> {
let facades: Vec<ProcessorFacade> = self
.processors
.iter()
.map(|p| ProcessorFacade::Fake(*p))
.collect();
NonEmpty::from_vec(facades).expect("at least one processor was configured")
}
fn pin_current_thread_to<P>(&self, processors: &NonEmpty<P>)
where
P: AsRef<ProcessorFacade>,
{
let thread_id = std::thread::current().id();
let processor_ids: Vec<ProcessorId> = processors.iter().map(|p| p.as_ref().id()).collect();
let processor_ids = NonEmpty::from_vec(processor_ids)
.expect("processors is non-empty so the resulting vec is also non-empty");
let mut states = self
.thread_states
.write()
.expect("thread state lock should never be poisoned");
let state = states.entry(thread_id).or_default();
state.allowed_processors = Some(processor_ids);
}
fn current_processor_id(&self) -> ProcessorId {
#[cfg(test)]
if let Some(id) = *self.processor_id_override.read().unwrap() {
return id;
}
let thread_id = std::thread::current().id();
self.thread_processor_id(thread_id)
}
fn current_thread_processors(&self) -> NonEmpty<ProcessorId> {
let thread_id = std::thread::current().id();
let states = self
.thread_states
.read()
.expect("thread state lock should never be poisoned");
match states
.get(&thread_id)
.and_then(|s| s.allowed_processors.clone())
{
Some(processors) => processors,
None => {
let all_ids: Vec<ProcessorId> = self.processors.iter().map(|p| p.id).collect();
NonEmpty::from_vec(all_ids).expect("at least one processor was configured")
}
}
}
fn max_processor_id(&self) -> ProcessorId {
self.max_processor_id
}
fn max_memory_region_id(&self) -> MemoryRegionId {
self.max_memory_region_id
}
fn max_processor_time(&self) -> f64 {
self.max_processor_time
}
fn active_processor_count(&self) -> usize {
self.processors.len()
}
}
#[cfg(test)]
#[cfg_attr(coverage_nightly, coverage(off))]
#[allow(clippy::indexing_slicing, reason = "test code, panics are acceptable")]
mod tests {
use new_zealand::nz;
use super::*;
use crate::fake::ProcessorBuilder;
#[test]
fn basic_hardware_creation() {
let builder = HardwareBuilder::from_counts(nz!(4), nz!(1));
let backend = FakePlatform::from_builder(&builder);
assert_eq!(backend.processors.len(), 4);
assert_eq!(backend.max_processor_id, 3);
assert_eq!(backend.max_memory_region_id, 0);
assert_eq!(backend.active_processor_count(), 4);
}
#[test]
fn custom_memory_regions() {
let builder = HardwareBuilder::from_counts(nz!(4), nz!(2));
let backend = FakePlatform::from_builder(&builder);
assert_eq!(backend.processors[0].memory_region_id, 0);
assert_eq!(backend.processors[1].memory_region_id, 1);
assert_eq!(backend.processors[2].memory_region_id, 0);
assert_eq!(backend.processors[3].memory_region_id, 1);
assert_eq!(backend.max_memory_region_id, 1);
}
#[test]
fn custom_processor_configuration() {
let builder = HardwareBuilder::new()
.processor(
ProcessorBuilder::new()
.id(0)
.memory_region(0)
.efficiency_class(EfficiencyClass::Performance),
)
.processor(
ProcessorBuilder::new()
.id(1)
.memory_region(1)
.efficiency_class(EfficiencyClass::Efficiency),
);
let backend = FakePlatform::from_builder(&builder);
assert_eq!(backend.processors.len(), 2);
assert_eq!(
backend.processors[0].efficiency_class,
EfficiencyClass::Performance
);
assert_eq!(
backend.processors[1].efficiency_class,
EfficiencyClass::Efficiency
);
assert_eq!(backend.max_memory_region_id, 1);
}
#[test]
fn max_processor_time_configuration() {
let builder = HardwareBuilder::from_counts(nz!(4), nz!(1)).max_processor_time(2.5);
let backend = FakePlatform::from_builder(&builder);
assert!((backend.max_processor_time - 2.5).abs() < f64::EPSILON);
}
#[test]
fn current_processor_id_is_from_valid_set() {
let builder = HardwareBuilder::from_counts(nz!(4), nz!(1));
let backend = FakePlatform::from_builder(&builder);
let id = backend.current_processor_id();
assert!(id < 4);
}
#[test]
fn get_all_processors_returns_all() {
let builder = HardwareBuilder::from_counts(nz!(4), nz!(1));
let backend = FakePlatform::from_builder(&builder);
let processors = backend.get_all_processors();
assert_eq!(processors.len(), 4);
}
#[test]
fn pin_current_thread_to_restricts_processor_selection() {
let builder = HardwareBuilder::from_counts(nz!(4), nz!(1));
let backend = FakePlatform::from_builder(&builder);
let processor = backend.get_all_processors().head;
let processors = NonEmpty::singleton(processor);
backend.pin_current_thread_to(&processors);
let id = backend.current_processor_id();
assert_eq!(id, processor.id());
}
#[test]
fn current_thread_processors_returns_all_when_not_pinned() {
let builder = HardwareBuilder::from_counts(nz!(4), nz!(1));
let backend = FakePlatform::from_builder(&builder);
let processors = backend.current_thread_processors();
assert_eq!(processors.len(), 4);
}
#[test]
fn current_thread_processors_returns_pinned_set() {
let builder = HardwareBuilder::from_counts(nz!(4), nz!(1));
let backend = FakePlatform::from_builder(&builder);
let all = backend.get_all_processors();
let mut iter = all.iter();
let p0 = *iter.next().unwrap();
let p1 = *iter.next().unwrap();
let pinned = NonEmpty::from((p0, vec![p1]));
backend.pin_current_thread_to(&pinned);
let thread_processors = backend.current_thread_processors();
assert_eq!(thread_processors.len(), 2);
assert!(thread_processors.iter().any(|&id| id == p0.id()));
assert!(thread_processors.iter().any(|&id| id == p1.id()));
}
#[test]
fn thread_processor_id_selects_from_pinned_set() {
let builder = HardwareBuilder::from_counts(nz!(8), nz!(1));
let backend = FakePlatform::from_builder(&builder);
let all = backend.get_all_processors();
let processors_vec: Vec<_> = all.iter().copied().collect();
let p2 = processors_vec[2];
let p3 = processors_vec[3];
let pinned = NonEmpty::from((p2, vec![p3]));
backend.pin_current_thread_to(&pinned);
let id = backend.current_processor_id();
assert!(id == p2.id() || id == p3.id());
}
#[test]
fn fake_processor_display_includes_all_fields() {
let processor = FakeProcessor::new(5, 2, EfficiencyClass::Efficiency);
let display = format!("{processor}");
assert!(display.contains('5'));
assert!(display.contains('2'));
assert!(display.contains("Efficiency"));
}
#[test]
fn fake_processor_efficiency_class_returns_configured_value() {
let perf = FakeProcessor::new(0, 0, EfficiencyClass::Performance);
let eff = FakeProcessor::new(1, 0, EfficiencyClass::Efficiency);
assert_eq!(perf.efficiency_class(), EfficiencyClass::Performance);
assert_eq!(eff.efficiency_class(), EfficiencyClass::Efficiency);
}
#[test]
fn fake_processor_id_returns_configured_value() {
let processor = FakeProcessor::new(42, 0, EfficiencyClass::Performance);
assert_eq!(processor.id(), 42);
}
#[test]
fn fake_processor_memory_region_id_returns_configured_value() {
let processor = FakeProcessor::new(0, 7, EfficiencyClass::Performance);
assert_eq!(processor.memory_region_id(), 7);
}
}