#![forbid(unsafe_code)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ReusableComputation {
LayoutSolve,
TextWidth,
TextWrap,
StyleResolution,
IntrinsicMeasure,
}
impl ReusableComputation {
#[must_use]
pub const fn label(&self) -> &'static str {
match self {
Self::LayoutSolve => "layout-solve",
Self::TextWidth => "text-width",
Self::TextWrap => "text-wrap",
Self::StyleResolution => "style-resolution",
Self::IntrinsicMeasure => "intrinsic-measure",
}
}
#[must_use]
pub const fn is_pure(&self) -> bool {
true }
#[must_use]
pub const fn typical_cost_us(&self) -> u32 {
match self {
Self::LayoutSolve => 50, Self::TextWidth => 5, Self::TextWrap => 30, Self::StyleResolution => 10, Self::IntrinsicMeasure => 20, }
}
pub const ALL: &'static [ReusableComputation] = &[
Self::LayoutSolve,
Self::TextWidth,
Self::TextWrap,
Self::StyleResolution,
Self::IntrinsicMeasure,
];
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum NonReusableComputation {
CursorPosition,
AnimationState,
ScrollOffset,
RandomEffect,
SelectionState,
NotificationPosition,
}
impl NonReusableComputation {
#[must_use]
pub const fn label(&self) -> &'static str {
match self {
Self::CursorPosition => "cursor-position",
Self::AnimationState => "animation-state",
Self::ScrollOffset => "scroll-offset",
Self::RandomEffect => "random-effect",
Self::SelectionState => "selection-state",
Self::NotificationPosition => "notification-position",
}
}
#[must_use]
pub const fn reason(&self) -> &'static str {
match self {
Self::CursorPosition => "Focus state changes between frames",
Self::AnimationState => "Time-dependent values are non-repeatable",
Self::ScrollOffset => "User-driven, changes unpredictably",
Self::RandomEffect => "Seed-dependent, intentionally varies",
Self::SelectionState => "Mouse/keyboard selection is frame-local",
Self::NotificationPosition => "Toast dismiss timing is time-dependent",
}
}
pub const ALL: &'static [NonReusableComputation] = &[
Self::CursorPosition,
Self::AnimationState,
Self::ScrollOffset,
Self::RandomEffect,
Self::SelectionState,
Self::NotificationPosition,
];
}
#[derive(Debug, Clone)]
pub struct CacheKeySpec {
pub computation: ReusableComputation,
pub components: Vec<KeyComponent>,
pub invalidation_triggers: Vec<InvalidationTrigger>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum KeyComponent {
Area,
Direction,
ConstraintsHash,
ContentHash,
MaxWidth,
StyleId,
ThemeEpoch,
IntrinsicHash,
}
impl KeyComponent {
#[must_use]
pub const fn label(&self) -> &'static str {
match self {
Self::Area => "area",
Self::Direction => "direction",
Self::ConstraintsHash => "constraints-hash",
Self::ContentHash => "content-hash",
Self::MaxWidth => "max-width",
Self::StyleId => "style-id",
Self::ThemeEpoch => "theme-epoch",
Self::IntrinsicHash => "intrinsic-hash",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum InvalidationTrigger {
Resize,
ConstraintChange,
ContentChange,
ThemeChange,
FontChange,
GenerationBump,
}
impl InvalidationTrigger {
#[must_use]
pub const fn label(&self) -> &'static str {
match self {
Self::Resize => "resize",
Self::ConstraintChange => "constraint-change",
Self::ContentChange => "content-change",
Self::ThemeChange => "theme-change",
Self::FontChange => "font-change",
Self::GenerationBump => "generation-bump",
}
}
pub const ALL: &'static [InvalidationTrigger] = &[
Self::Resize,
Self::ConstraintChange,
Self::ContentChange,
Self::ThemeChange,
Self::FontChange,
Self::GenerationBump,
];
}
#[derive(Debug, Clone)]
pub struct CachePolicy {
pub max_entries: u32,
pub generation_invalidation: bool,
pub min_cost_threshold_us: u32,
pub log_events: bool,
}
impl CachePolicy {
#[must_use]
pub const fn default_policy() -> Self {
Self {
max_entries: 256,
generation_invalidation: true,
min_cost_threshold_us: 10,
log_events: false,
}
}
#[must_use]
pub const fn aggressive() -> Self {
Self {
max_entries: 1024,
generation_invalidation: true,
min_cost_threshold_us: 5,
log_events: false,
}
}
#[must_use]
pub const fn conservative() -> Self {
Self {
max_entries: 64,
generation_invalidation: true,
min_cost_threshold_us: 20,
log_events: true,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CacheDecision {
Hit,
Miss,
Invalidated,
Bypassed,
}
impl CacheDecision {
#[must_use]
pub const fn label(&self) -> &'static str {
match self {
Self::Hit => "hit",
Self::Miss => "miss",
Self::Invalidated => "invalidated",
Self::Bypassed => "bypassed",
}
}
#[must_use]
pub const fn used_cache(&self) -> bool {
matches!(self, Self::Hit)
}
}
#[must_use]
pub fn canonical_key_specs() -> Vec<CacheKeySpec> {
vec![
CacheKeySpec {
computation: ReusableComputation::LayoutSolve,
components: vec![
KeyComponent::Area,
KeyComponent::Direction,
KeyComponent::ConstraintsHash,
KeyComponent::IntrinsicHash,
],
invalidation_triggers: vec![
InvalidationTrigger::Resize,
InvalidationTrigger::ConstraintChange,
InvalidationTrigger::ContentChange,
InvalidationTrigger::GenerationBump,
],
},
CacheKeySpec {
computation: ReusableComputation::TextWidth,
components: vec![KeyComponent::ContentHash],
invalidation_triggers: vec![InvalidationTrigger::FontChange],
},
CacheKeySpec {
computation: ReusableComputation::TextWrap,
components: vec![KeyComponent::ContentHash, KeyComponent::MaxWidth],
invalidation_triggers: vec![
InvalidationTrigger::ContentChange,
InvalidationTrigger::Resize,
InvalidationTrigger::FontChange,
],
},
CacheKeySpec {
computation: ReusableComputation::StyleResolution,
components: vec![KeyComponent::StyleId, KeyComponent::ThemeEpoch],
invalidation_triggers: vec![InvalidationTrigger::ThemeChange],
},
CacheKeySpec {
computation: ReusableComputation::IntrinsicMeasure,
components: vec![KeyComponent::ContentHash, KeyComponent::ConstraintsHash],
invalidation_triggers: vec![
InvalidationTrigger::ContentChange,
InvalidationTrigger::ConstraintChange,
InvalidationTrigger::FontChange,
],
},
]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn all_reusable_computations_are_pure() {
for comp in ReusableComputation::ALL {
assert!(comp.is_pure(), "{} should be pure", comp.label());
}
}
#[test]
fn reusable_computation_labels() {
for comp in ReusableComputation::ALL {
assert!(!comp.label().is_empty());
assert!(comp.typical_cost_us() > 0);
}
assert_eq!(ReusableComputation::ALL.len(), 5);
}
#[test]
fn non_reusable_computations_have_reasons() {
for comp in NonReusableComputation::ALL {
assert!(!comp.label().is_empty());
assert!(!comp.reason().is_empty());
}
assert_eq!(NonReusableComputation::ALL.len(), 6);
}
#[test]
fn canonical_key_specs_cover_all_computations() {
let specs = canonical_key_specs();
let covered: Vec<ReusableComputation> = specs.iter().map(|s| s.computation).collect();
for comp in ReusableComputation::ALL {
assert!(
covered.contains(comp),
"missing key spec for {}",
comp.label()
);
}
}
#[test]
fn every_key_spec_has_components() {
for spec in canonical_key_specs() {
assert!(
!spec.components.is_empty(),
"{} has no key components",
spec.computation.label()
);
}
}
#[test]
fn every_key_spec_has_invalidation_triggers() {
for spec in canonical_key_specs() {
assert!(
!spec.invalidation_triggers.is_empty(),
"{} has no invalidation triggers",
spec.computation.label()
);
}
}
#[test]
fn text_width_is_cheapest() {
let min = ReusableComputation::ALL
.iter()
.min_by_key(|c| c.typical_cost_us())
.unwrap();
assert_eq!(*min, ReusableComputation::TextWidth);
}
#[test]
fn layout_solve_is_most_expensive() {
let max = ReusableComputation::ALL
.iter()
.max_by_key(|c| c.typical_cost_us())
.unwrap();
assert_eq!(*max, ReusableComputation::LayoutSolve);
}
#[test]
fn cache_policy_defaults() {
let policy = CachePolicy::default_policy();
assert_eq!(policy.max_entries, 256);
assert!(policy.generation_invalidation);
assert_eq!(policy.min_cost_threshold_us, 10);
}
#[test]
fn cache_policy_aggressive_larger() {
let agg = CachePolicy::aggressive();
let def = CachePolicy::default_policy();
assert!(agg.max_entries > def.max_entries);
assert!(agg.min_cost_threshold_us < def.min_cost_threshold_us);
}
#[test]
fn cache_decision_labels() {
for decision in [
CacheDecision::Hit,
CacheDecision::Miss,
CacheDecision::Invalidated,
CacheDecision::Bypassed,
] {
assert!(!decision.label().is_empty());
}
}
#[test]
fn only_hit_uses_cache() {
assert!(CacheDecision::Hit.used_cache());
assert!(!CacheDecision::Miss.used_cache());
assert!(!CacheDecision::Invalidated.used_cache());
assert!(!CacheDecision::Bypassed.used_cache());
}
#[test]
fn invalidation_trigger_labels() {
for trigger in InvalidationTrigger::ALL {
assert!(!trigger.label().is_empty());
}
assert_eq!(InvalidationTrigger::ALL.len(), 6);
}
#[test]
fn key_component_labels() {
for comp in [
KeyComponent::Area,
KeyComponent::Direction,
KeyComponent::ConstraintsHash,
KeyComponent::ContentHash,
KeyComponent::MaxWidth,
KeyComponent::StyleId,
KeyComponent::ThemeEpoch,
KeyComponent::IntrinsicHash,
] {
assert!(!comp.label().is_empty());
}
}
#[test]
fn layout_solve_key_requires_area_and_constraints() {
let specs = canonical_key_specs();
let layout_spec = specs
.iter()
.find(|s| s.computation == ReusableComputation::LayoutSolve)
.unwrap();
assert!(layout_spec.components.contains(&KeyComponent::Area));
assert!(
layout_spec
.components
.contains(&KeyComponent::ConstraintsHash)
);
assert!(layout_spec.components.contains(&KeyComponent::Direction));
}
#[test]
fn text_width_key_minimal() {
let specs = canonical_key_specs();
let width_spec = specs
.iter()
.find(|s| s.computation == ReusableComputation::TextWidth)
.unwrap();
assert_eq!(width_spec.components.len(), 1);
assert_eq!(width_spec.components[0], KeyComponent::ContentHash);
}
#[test]
fn style_resolution_invalidated_by_theme() {
let specs = canonical_key_specs();
let style_spec = specs
.iter()
.find(|s| s.computation == ReusableComputation::StyleResolution)
.unwrap();
assert!(
style_spec
.invalidation_triggers
.contains(&InvalidationTrigger::ThemeChange)
);
}
}