use crate::topology::cpu::MAX_PROCESSOR_ID;
use crate::topology::types::CacheLevel;
use core::mem::size_of;
use std::ffi::c_void;
const RELATION_CACHE: u32 = 2;
const RECORD_HEADER_BYTES: usize = 8;
const CACHE_TYPE_OFFSET: usize = 16;
const CACHE_UNIFIED: u32 = 0;
const CACHE_DATA: u32 = 2;
const CACHE_PREFIX_BYTES: usize = 32;
const GROUP_AFFINITY_BYTES: usize = 16;
const GROUP_MASK_OFFSET: usize = RECORD_HEADER_BYTES + CACHE_PREFIX_BYTES;
const MAX_GROUPS: usize = 64;
const MAX_BUFFER_BYTES: usize = 1024 * 1024;
extern "system" {
fn GetLogicalProcessorInformationEx(
relationship_type: u32,
buffer: *mut c_void,
returned_length: *mut u32,
) -> i32;
}
pub(super) fn detect() -> Option<Box<[CacheLevel]>> {
let mut returned_length = 0u32;
let first_call = unsafe {
GetLogicalProcessorInformationEx(
RELATION_CACHE,
core::ptr::null_mut(),
core::ptr::addr_of_mut!(returned_length),
)
};
if first_call != 0 {
return None;
}
let buffer_length = usize::try_from(returned_length).ok()?;
if buffer_length == 0 || buffer_length > MAX_BUFFER_BYTES {
return None;
}
let mut buffer = vec![0u8; buffer_length];
let second_call = unsafe {
GetLogicalProcessorInformationEx(
RELATION_CACHE,
buffer.as_mut_ptr().cast(),
core::ptr::addr_of_mut!(returned_length),
)
};
if second_call == 0 {
return None;
}
let used = usize::try_from(returned_length).ok()?;
let bytes = buffer.get(..used.min(buffer.len()))?;
parse_records(bytes)
}
fn parse_records(bytes: &[u8]) -> Option<Box<[CacheLevel]>> {
let mut offset = 0usize;
let mut levels = Vec::with_capacity(8);
while offset < bytes.len() {
let relationship = u32::from_ne_bytes(field::<4>(bytes, offset)?);
let record_size =
usize::try_from(u32::from_ne_bytes(field::<4>(bytes, offset + 4)?)).ok()?;
if !(RECORD_HEADER_BYTES..=MAX_BUFFER_BYTES).contains(&record_size)
|| record_size > bytes.len().saturating_sub(offset)
{
return None;
}
if relationship == RELATION_CACHE {
let record = bytes.get(offset..offset + record_size)?;
if let Some(level) = parse_cache_record(record) {
if !levels
.iter()
.any(|existing: &CacheLevel| existing == &level)
{
levels.push(level);
}
}
}
offset = offset.checked_add(record_size)?;
}
(!levels.is_empty()).then(|| levels.into_boxed_slice())
}
fn parse_cache_record(record: &[u8]) -> Option<CacheLevel> {
if record.len() < GROUP_MASK_OFFSET + GROUP_AFFINITY_BYTES {
return None;
}
let level = u32::from(*record.get(RECORD_HEADER_BYTES)?);
if !(1..=3).contains(&level) {
return None;
}
let size_bytes = usize::try_from(u32::from_ne_bytes(field::<4>(record, 12)?)).ok()?;
if size_bytes == 0 {
return None;
}
let cache_type = u32::from_ne_bytes(field::<4>(record, CACHE_TYPE_OFFSET)?);
if cache_type != CACHE_UNIFIED && cache_type != CACHE_DATA {
return None;
}
let line_size = usize::from(u16::from_ne_bytes(field::<2>(record, 10)?));
let group_count = usize::from(u16::from_ne_bytes(field::<2>(record, 38)?));
if !(1..=MAX_GROUPS).contains(&group_count) {
return None;
}
let group_bytes = group_count.checked_mul(GROUP_AFFINITY_BYTES)?;
let group_end = GROUP_MASK_OFFSET.checked_add(group_bytes)?;
if group_end > record.len() {
return None;
}
let mut shared_processors = Vec::with_capacity(group_count);
for group_index in 0..group_count {
let group_offset = GROUP_MASK_OFFSET + group_index * GROUP_AFFINITY_BYTES;
let mask = affinity_mask(record, group_offset)?;
let group = u16::from_ne_bytes(field::<2>(record, group_offset + 8)?);
for bit in 0..usize::BITS as usize {
if mask & (1usize << bit) == 0 {
continue;
}
let processor = usize::from(group).checked_mul(64)?.checked_add(bit)?;
if processor < MAX_PROCESSOR_ID {
shared_processors.push(u32::try_from(processor).ok()?);
}
}
}
if shared_processors.is_empty() {
return None;
}
Some(CacheLevel {
level,
size_bytes,
line_bytes: (line_size > 0).then_some(line_size),
shared_processors: shared_processors.into_boxed_slice(),
})
}
fn affinity_mask(bytes: &[u8], offset: usize) -> Option<usize> {
match size_of::<usize>() {
8 => usize::try_from(u64::from_ne_bytes(field::<8>(bytes, offset)?)).ok(),
4 => usize::try_from(u32::from_ne_bytes(field::<4>(bytes, offset)?)).ok(),
_ => None,
}
}
fn field<const N: usize>(bytes: &[u8], offset: usize) -> Option<[u8; N]> {
bytes.get(offset..offset.checked_add(N)?)?.try_into().ok()
}
#[cfg(test)]
mod tests {
use super::{parse_cache_record, CACHE_TYPE_OFFSET, GROUP_AFFINITY_BYTES, GROUP_MASK_OFFSET};
const CACHE_INSTRUCTION: u32 = 1;
const CACHE_TRACE: u32 = 3;
fn cache_record(level: u8, size_bytes: u32, line_size: u16, cache_type: u32) -> Vec<u8> {
let record_size = GROUP_MASK_OFFSET + GROUP_AFFINITY_BYTES;
let mut record = vec![0u8; record_size];
record[0..4].copy_from_slice(&2u32.to_ne_bytes()); let size_field = u32::try_from(record_size).expect("record size fits in u32");
record[4..8].copy_from_slice(&size_field.to_ne_bytes());
record[8] = level;
record[9] = 8; record[10..12].copy_from_slice(&line_size.to_ne_bytes());
record[12..16].copy_from_slice(&size_bytes.to_ne_bytes());
record[CACHE_TYPE_OFFSET..CACHE_TYPE_OFFSET + 4].copy_from_slice(&cache_type.to_ne_bytes());
record[38..40].copy_from_slice(&1u16.to_ne_bytes()); record[GROUP_MASK_OFFSET..GROUP_MASK_OFFSET + 8].copy_from_slice(&0b11u64.to_ne_bytes()); record
}
#[test]
fn data_and_unified_caches_are_reported() {
let data = parse_cache_record(&cache_record(1, 49152, 64, super::CACHE_DATA))
.expect("data cache is reported");
assert_eq!(data.level, 1);
assert_eq!(data.size_bytes, 49152);
assert_eq!(data.line_bytes, Some(64));
assert_eq!(&*data.shared_processors, &[0, 1]);
let unified = parse_cache_record(&cache_record(3, 37_748_736, 64, super::CACHE_UNIFIED))
.expect("unified cache is reported");
assert_eq!(unified.level, 3);
assert_eq!(unified.size_bytes, 37_748_736);
}
#[test]
fn instruction_and_trace_caches_are_excluded() {
assert!(parse_cache_record(&cache_record(1, 65536, 64, CACHE_INSTRUCTION)).is_none());
assert!(parse_cache_record(&cache_record(1, 65536, 64, CACHE_TRACE)).is_none());
}
#[test]
fn a_split_l1_pair_resolves_to_the_data_cache_alone() {
let records = [
cache_record(1, 49152, 64, super::CACHE_DATA),
cache_record(1, 65536, 64, CACHE_INSTRUCTION),
];
let reported: Vec<_> = records
.iter()
.filter_map(|record| parse_cache_record(record))
.collect();
assert_eq!(reported.len(), 1, "only the data cache survives");
assert_eq!(reported[0].size_bytes, 49152);
}
}