fast-cache 0.1.0

Embedded-first thread-per-core in-memory cache with optional Redis-compatible server
Documentation
use std::fs;
use std::process::Command;

use super::CacheGeometry;

const HOT_SLOT_BYTES: usize = 64;
const HOT_L1_BUDGET_NUMERATOR: usize = 3;
const HOT_L1_BUDGET_DENOMINATOR: usize = 5;
const HOT_TARGET_LOAD_NUMERATOR: usize = 4;
const HOT_TARGET_LOAD_DENOMINATOR: usize = 5;

pub(super) struct DefaultShardCount;

pub(super) struct HotTierCapacity;

pub(super) struct CacheGeometryDetector;

pub(super) struct CacheSizeParser<'a> {
    raw: &'a str,
}

struct NearestPowerOfTwo;

enum CacheGeometryProbe {
    Linux,
    MacOs,
    Fallback,
}

enum CacheSizeSuffix {
    Bytes,
    KiB,
    MiB,
}

enum SysctlOutput {
    Ready(Vec<u8>),
    Missing,
}

impl DefaultShardCount {
    pub(super) fn current() -> usize {
        std::thread::available_parallelism()
            .map_or(1, |count| count.get())
            .next_power_of_two()
    }
}

impl HotTierCapacity {
    pub(super) fn from_l1(l1d_bytes: usize) -> usize {
        let target_slots = usize::max(
            4,
            (l1d_bytes.saturating_mul(HOT_L1_BUDGET_NUMERATOR) / HOT_L1_BUDGET_DENOMINATOR)
                / HOT_SLOT_BYTES,
        );
        let slot_count = NearestPowerOfTwo::from(target_slots).clamp(256, 2_048);
        (slot_count.saturating_mul(HOT_TARGET_LOAD_NUMERATOR) / HOT_TARGET_LOAD_DENOMINATOR)
            .clamp(128, 16_384)
    }
}

impl CacheGeometryDetector {
    pub(super) fn detect() -> CacheGeometry {
        Self::probe(CacheGeometryProbe::Linux)
    }

    fn probe(probe: CacheGeometryProbe) -> CacheGeometry {
        match probe {
            CacheGeometryProbe::Linux => match Self::detect_linux() {
                Some(geometry) => geometry,
                None => Self::probe(CacheGeometryProbe::MacOs),
            },
            CacheGeometryProbe::MacOs => match Self::detect_macos() {
                Some(geometry) => geometry,
                None => Self::probe(CacheGeometryProbe::Fallback),
            },
            CacheGeometryProbe::Fallback => Self::fallback(),
        }
    }

    fn detect_linux() -> Option<CacheGeometry> {
        let l1d_bytes = Self::read_sysfs_cache("/sys/devices/system/cpu/cpu0/cache/index0/size")
            .or_else(|| Self::read_sysfs_cache("/sys/devices/system/cpu/cpu0/cache/index1/size"))?;
        let l2_bytes = Self::read_sysfs_cache("/sys/devices/system/cpu/cpu0/cache/index2/size")?;
        let l3_bytes = Self::read_sysfs_cache("/sys/devices/system/cpu/cpu0/cache/index3/size")?;
        Some(CacheGeometry {
            l1d_bytes,
            l2_bytes,
            l3_bytes,
        })
    }

    fn read_sysfs_cache(path: &str) -> Option<usize> {
        let raw = fs::read_to_string(path).ok()?;
        CacheSizeParser::parse(raw.trim())
    }

    fn detect_macos() -> Option<CacheGeometry> {
        let l1d_bytes = Self::read_sysctl_cache("hw.l1dcachesize")?;
        let l2_bytes = Self::read_sysctl_cache("hw.l2cachesize")?;
        let l3_bytes =
            Self::read_sysctl_cache("hw.l3cachesize").unwrap_or_else(|| Self::fallback().l3_bytes);
        Some(CacheGeometry {
            l1d_bytes,
            l2_bytes,
            l3_bytes,
        })
    }

    fn read_sysctl_cache(name: &str) -> Option<usize> {
        match SysctlOutput::read(name) {
            SysctlOutput::Ready(output) => String::from_utf8(output).ok()?.trim().parse().ok(),
            SysctlOutput::Missing => None,
        }
    }

    fn fallback() -> CacheGeometry {
        CacheGeometry {
            l1d_bytes: 64 * 1024,
            l2_bytes: 4 * 1024 * 1024,
            l3_bytes: 16 * 1024 * 1024,
        }
    }
}

impl<'a> CacheSizeParser<'a> {
    pub(super) fn parse(raw: &'a str) -> Option<usize> {
        Self { raw }.parse_inner()
    }

    fn parse_inner(&self) -> Option<usize> {
        match self.raw.is_empty() {
            true => None,
            false => self.parse_non_empty(),
        }
    }

    fn parse_non_empty(&self) -> Option<usize> {
        let digits = self.digits();
        let value = digits.parse::<usize>().ok()?;
        Some(CacheSizeSuffix::from_raw(self.raw, digits.len()).apply(value))
    }

    fn digits(&self) -> String {
        self.raw
            .chars()
            .take_while(|ch| ch.is_ascii_digit())
            .collect::<String>()
    }
}

impl NearestPowerOfTwo {
    fn from(value: usize) -> usize {
        match value {
            0 | 1 => 1,
            value => Self::nearest_non_trivial(value),
        }
    }

    fn nearest_non_trivial(value: usize) -> usize {
        let lower = value.next_power_of_two() >> 1;
        let upper = value.next_power_of_two();
        match value.saturating_sub(lower) <= upper.saturating_sub(value) {
            true => lower.max(1),
            false => upper,
        }
    }
}

impl CacheSizeSuffix {
    fn from_raw(raw: &str, digit_len: usize) -> Self {
        match raw.chars().nth(digit_len).unwrap_or('B') {
            'K' | 'k' => Self::KiB,
            'M' | 'm' => Self::MiB,
            _ => Self::Bytes,
        }
    }

    fn apply(self, value: usize) -> usize {
        match self {
            Self::Bytes => value,
            Self::KiB => value * 1024,
            Self::MiB => value * 1024 * 1024,
        }
    }
}

impl SysctlOutput {
    fn read(name: &str) -> Self {
        match Command::new("sysctl").arg("-n").arg(name).output() {
            Ok(output) if output.status.success() => Self::Ready(output.stdout),
            Ok(_) | Err(_) => Self::Missing,
        }
    }
}