use crate::pane::{PaneOperation, PaneOperationFamily};
use crate::pane_memory::PaneMemoryStrategy;
use crate::pane_retention::PaneRetentionPolicy;
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
pub struct PaneWorkloadProfile {
pub operation_count: usize,
pub local_operation_count: usize,
pub peak_ops_per_sec: u32,
pub history_required: bool,
}
impl PaneWorkloadProfile {
#[must_use]
pub const fn new(
operation_count: usize,
local_operation_count: usize,
peak_ops_per_sec: u32,
history_required: bool,
) -> Self {
Self {
operation_count,
local_operation_count,
peak_ops_per_sec,
history_required,
}
}
#[must_use]
pub fn observe(ops: &[PaneOperation], peak_ops_per_sec: u32, history_required: bool) -> Self {
let local_operation_count = ops
.iter()
.filter(|op| op.family() == PaneOperationFamily::Local)
.count();
Self::new(
ops.len(),
local_operation_count,
peak_ops_per_sec,
history_required,
)
}
#[must_use]
pub const fn local_fraction_pct(self) -> u32 {
if self.operation_count == 0 {
return 0;
}
((self.local_operation_count.saturating_mul(100)) / self.operation_count) as u32
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
pub enum PaneStrategyReason {
ForcedOverride,
ConservativeFallback,
NoHistoryRequired,
ResizeDominatedBurst,
GeneralDefault,
HysteresisHold,
}
impl PaneStrategyReason {
#[must_use]
pub const fn as_str(self) -> &'static str {
match self {
Self::ForcedOverride => "forced_override",
Self::ConservativeFallback => "conservative_fallback",
Self::NoHistoryRequired => "no_history_required",
Self::ResizeDominatedBurst => "resize_dominated_burst",
Self::GeneralDefault => "general_default",
Self::HysteresisHold => "hysteresis_hold",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
pub struct PaneExecutionPolicy {
pub forced_strategy: Option<PaneMemoryStrategy>,
pub conservative: bool,
pub persistent_min_operations: usize,
pub persistent_local_fraction_pct: u32,
pub persistent_burst_ops_per_sec: u32,
pub hysteresis_pct: u32,
pub retention: PaneRetentionPolicy,
}
impl PaneExecutionPolicy {
pub const DEFAULT_PERSISTENT_MIN_OPERATIONS: usize = 64;
pub const DEFAULT_PERSISTENT_LOCAL_FRACTION_PCT: u32 = 80;
pub const DEFAULT_PERSISTENT_BURST_OPS_PER_SEC: u32 = 60;
pub const DEFAULT_HYSTERESIS_PCT: u32 = 10;
#[must_use]
pub const fn adaptive(retention: PaneRetentionPolicy) -> Self {
Self {
forced_strategy: None,
conservative: false,
persistent_min_operations: Self::DEFAULT_PERSISTENT_MIN_OPERATIONS,
persistent_local_fraction_pct: Self::DEFAULT_PERSISTENT_LOCAL_FRACTION_PCT,
persistent_burst_ops_per_sec: Self::DEFAULT_PERSISTENT_BURST_OPS_PER_SEC,
hysteresis_pct: Self::DEFAULT_HYSTERESIS_PCT,
retention,
}
}
#[must_use]
pub const fn conservative(mut self) -> Self {
self.conservative = true;
self
}
#[must_use]
pub const fn forcing(mut self, strategy: PaneMemoryStrategy) -> Self {
self.forced_strategy = Some(strategy);
self
}
#[must_use]
pub fn select(&self, profile: PaneWorkloadProfile) -> PaneExecutionDecision {
let (strategy, reason, forced) = self.decide(profile);
self.decision(strategy, reason, forced, profile)
}
#[must_use]
pub fn reselect(
&self,
profile: PaneWorkloadProfile,
previous: PaneMemoryStrategy,
) -> PaneExecutionDecision {
if self.forced_strategy.is_some() || self.conservative {
return self.select(profile);
}
let (fresh, reason, forced) = self.decide(profile);
if fresh == previous {
return self.decision(fresh, reason, forced, profile);
}
if matches!(reason, PaneStrategyReason::NoHistoryRequired)
|| previous == PaneMemoryStrategy::Baseline
{
return self.decision(fresh, reason, forced, profile);
}
let decisive = if fresh == PaneMemoryStrategy::Persistent {
self.favors_persistent(profile, self.hysteresis_pct)
} else {
profile.operation_count < self.persistent_min_operations
|| profile.peak_ops_per_sec < self.persistent_burst_ops_per_sec
|| profile.local_fraction_pct()
< self
.persistent_local_fraction_pct
.saturating_sub(self.hysteresis_pct)
};
if decisive {
self.decision(fresh, reason, forced, profile)
} else {
self.decision(previous, PaneStrategyReason::HysteresisHold, false, profile)
}
}
fn decide(
&self,
profile: PaneWorkloadProfile,
) -> (PaneMemoryStrategy, PaneStrategyReason, bool) {
if let Some(forced) = self.forced_strategy {
return (forced, PaneStrategyReason::ForcedOverride, true);
}
if self.conservative {
return (
PaneMemoryStrategy::Checkpointed,
PaneStrategyReason::ConservativeFallback,
true,
);
}
if !profile.history_required {
return (
PaneMemoryStrategy::Baseline,
PaneStrategyReason::NoHistoryRequired,
false,
);
}
if self.favors_persistent(profile, 0) {
return (
PaneMemoryStrategy::Persistent,
PaneStrategyReason::ResizeDominatedBurst,
false,
);
}
(
PaneMemoryStrategy::Checkpointed,
PaneStrategyReason::GeneralDefault,
false,
)
}
fn favors_persistent(&self, profile: PaneWorkloadProfile, local_margin_pct: u32) -> bool {
profile.operation_count >= self.persistent_min_operations
&& profile.local_fraction_pct()
>= self
.persistent_local_fraction_pct
.saturating_add(local_margin_pct)
&& profile.peak_ops_per_sec >= self.persistent_burst_ops_per_sec
}
fn decision(
&self,
strategy: PaneMemoryStrategy,
reason: PaneStrategyReason,
forced: bool,
profile: PaneWorkloadProfile,
) -> PaneExecutionDecision {
let log = format!(
"execution[{}] {}: ops={} local={}% burst={}/s history={} (thresholds: min_ops={} local>={}% burst>={}/s hysteresis={}%{}); retention budget bytes={} units={}",
strategy.as_str(),
reason.as_str(),
profile.operation_count,
profile.local_fraction_pct(),
profile.peak_ops_per_sec,
profile.history_required,
self.persistent_min_operations,
self.persistent_local_fraction_pct,
self.persistent_burst_ops_per_sec,
self.hysteresis_pct,
if forced { ", forced" } else { "" },
self.retention.budget.max_retained_bytes,
self.retention.budget.max_retained_units,
);
PaneExecutionDecision {
strategy,
reason,
forced,
profile,
retention: self.retention,
log,
}
}
}
#[derive(Debug, Clone, PartialEq, serde::Serialize)]
pub struct PaneExecutionDecision {
pub strategy: PaneMemoryStrategy,
pub reason: PaneStrategyReason,
pub forced: bool,
pub profile: PaneWorkloadProfile,
pub retention: PaneRetentionPolicy,
pub log: String,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::pane::{
PaneId, PaneInteractionTimeline, PaneLeaf, PaneOperation, PanePlacement, PaneSplitRatio,
PaneTree, SplitAxis,
};
use crate::pane_persistent::{PaneVersionStore, VersionedPaneTree};
fn policy() -> PaneExecutionPolicy {
PaneExecutionPolicy::adaptive(PaneRetentionPolicy::bounded(500_000, 64))
}
fn resize_storm_profile() -> PaneWorkloadProfile {
PaneWorkloadProfile::new(512, 512, 240, true)
}
fn mixed_profile() -> PaneWorkloadProfile {
PaneWorkloadProfile::new(384, 211, 40, true)
}
#[test]
fn selection_is_deterministic() {
let p = policy();
let profile = resize_storm_profile();
assert_eq!(p.select(profile), p.select(profile));
}
#[test]
fn resize_storm_selects_persistent() {
let d = policy().select(resize_storm_profile());
assert_eq!(d.strategy, PaneMemoryStrategy::Persistent);
assert_eq!(d.reason, PaneStrategyReason::ResizeDominatedBurst);
assert!(!d.forced);
}
#[test]
fn mixed_workload_falls_back_to_checkpointed() {
let d = policy().select(mixed_profile());
assert_eq!(d.strategy, PaneMemoryStrategy::Checkpointed);
assert_eq!(d.reason, PaneStrategyReason::GeneralDefault);
}
#[test]
fn no_history_selects_baseline() {
let profile = PaneWorkloadProfile::new(512, 512, 240, false);
let d = policy().select(profile);
assert_eq!(d.strategy, PaneMemoryStrategy::Baseline);
assert_eq!(d.reason, PaneStrategyReason::NoHistoryRequired);
}
#[test]
fn shallow_resize_storm_stays_checkpointed() {
let profile = PaneWorkloadProfile::new(32, 32, 240, true);
assert_eq!(
policy().select(profile).strategy,
PaneMemoryStrategy::Checkpointed
);
}
#[test]
fn forced_strategy_overrides_adaptation() {
let forced = policy().forcing(PaneMemoryStrategy::Persistent);
let d = forced.select(mixed_profile());
assert_eq!(d.strategy, PaneMemoryStrategy::Persistent);
assert_eq!(d.reason, PaneStrategyReason::ForcedOverride);
assert!(d.forced);
}
#[test]
fn conservative_forces_checkpointed_even_on_resize_storm() {
let conservative = policy().conservative();
let d = conservative.select(resize_storm_profile());
assert_eq!(d.strategy, PaneMemoryStrategy::Checkpointed);
assert_eq!(d.reason, PaneStrategyReason::ConservativeFallback);
assert!(d.forced);
}
#[test]
fn hysteresis_prevents_thrashing_near_threshold() {
let p = policy();
let at_threshold = PaneWorkloadProfile::new(512, 410, 240, true); assert_eq!(
p.select(at_threshold).strategy,
PaneMemoryStrategy::Persistent
);
assert_eq!(
p.reselect(at_threshold, PaneMemoryStrategy::Checkpointed)
.strategy,
PaneMemoryStrategy::Checkpointed,
"should not enter persistent without clearing the hysteresis margin"
);
let decisive = PaneWorkloadProfile::new(512, 487, 240, true); let entered = p.reselect(decisive, PaneMemoryStrategy::Checkpointed);
assert_eq!(entered.strategy, PaneMemoryStrategy::Persistent);
assert_eq!(entered.reason, PaneStrategyReason::ResizeDominatedBurst);
let mild_dip = PaneWorkloadProfile::new(512, 384, 240, true); let held = p.reselect(mild_dip, PaneMemoryStrategy::Persistent);
assert_eq!(held.strategy, PaneMemoryStrategy::Persistent);
assert_eq!(held.reason, PaneStrategyReason::HysteresisHold);
let decisive_drop = PaneWorkloadProfile::new(512, 332, 240, true); assert_eq!(
p.reselect(decisive_drop, PaneMemoryStrategy::Persistent)
.strategy,
PaneMemoryStrategy::Checkpointed
);
}
#[test]
fn observe_classifies_local_operations() {
let ops = vec![
PaneOperation::SetSplitRatio {
split: PaneId::new(2).unwrap(),
ratio: PaneSplitRatio::new(1, 1).unwrap(),
},
PaneOperation::SetSplitRatio {
split: PaneId::new(2).unwrap(),
ratio: PaneSplitRatio::new(2, 1).unwrap(),
},
PaneOperation::CloseNode {
target: PaneId::new(3).unwrap(),
},
];
let profile = PaneWorkloadProfile::observe(&ops, 120, true);
assert_eq!(profile.operation_count, 3);
assert_eq!(profile.local_operation_count, 2);
assert_eq!(profile.local_fraction_pct(), 66);
}
#[test]
fn strategy_choice_never_diverges_behavior() {
let ratio = |n, d| PaneSplitRatio::new(n, d).expect("ratio");
let mut ops = vec![
PaneOperation::SplitLeaf {
target: PaneId::MIN,
axis: SplitAxis::Horizontal,
ratio: ratio(1, 1),
placement: PanePlacement::ExistingFirst,
new_leaf: PaneLeaf::new("b"),
},
PaneOperation::SplitLeaf {
target: PaneId::MIN,
axis: SplitAxis::Vertical,
ratio: ratio(2, 1),
placement: PanePlacement::ExistingFirst,
new_leaf: PaneLeaf::new("c"),
},
];
let split = PaneId::new(4).expect("id");
for n in 1..=12u32 {
ops.push(PaneOperation::SetSplitRatio {
split,
ratio: ratio(n % 5 + 1, 1),
});
}
let mut baseline = PaneTree::singleton("root");
for (i, op) in ops.iter().enumerate() {
baseline
.apply_operation_conservative(i as u64 + 1, op.clone())
.expect("baseline apply");
}
let mut tree = PaneTree::singleton("root");
let mut timeline = PaneInteractionTimeline::default();
for (i, op) in ops.iter().enumerate() {
let id = i as u64;
timeline
.apply_and_record(&mut tree, id, id, op.clone())
.expect("timeline apply");
}
let mut store = PaneVersionStore::new(VersionedPaneTree::singleton("root"));
for op in &ops {
store.apply(op).expect("store apply");
}
let baseline_hash = baseline.state_hash();
let timeline_hash = tree.state_hash();
let store_hash = store.current().state_hash().expect("hash");
assert_eq!(baseline_hash, timeline_hash);
assert_eq!(baseline_hash, store_hash);
let profile = PaneWorkloadProfile::observe(&ops, 240, true);
let strategy = policy().select(profile).strategy;
assert!(matches!(
strategy,
PaneMemoryStrategy::Baseline
| PaneMemoryStrategy::Checkpointed
| PaneMemoryStrategy::Persistent
));
}
}