1pub const LIBRARY_ROW_CHUNK_TARGET_BYTES: usize = 8 * 1024 * 1024;
5
6#[derive(Clone, Debug)]
7pub struct ResourcePolicy {
8 pub max_single_materialization_bytes: usize,
9 pub max_operator_cache_bytes: usize,
10 pub max_spatial_distance_cache_bytes: usize,
11 pub max_owned_data_cache_bytes: usize,
12 pub row_chunk_target_bytes: usize,
13 pub derivative_storage_mode: DerivativeStorageMode,
14}
15
16pub const OWNED_DATA_CACHE_MAX_ENTRIES: usize = 2;
17
18const GOVERNOR_BUDGET_NUMERATOR: u128 = 3;
34const GOVERNOR_BUDGET_DENOMINATOR: u128 = 4;
35
36fn governor_budget_from_available(host_available: u64, cgroup_available: Option<u64>) -> usize {
43 let available = cgroup_available
44 .map(|cgroup| host_available.min(cgroup))
45 .unwrap_or(host_available);
46 let scaled = u128::from(available) * GOVERNOR_BUDGET_NUMERATOR / GOVERNOR_BUDGET_DENOMINATOR;
47 usize::try_from(scaled).unwrap_or(usize::MAX)
48}
49
50fn detect_governor_budget_bytes() -> usize {
51 let mut sys = sysinfo::System::new();
52 sys.refresh_memory();
53 governor_budget_from_available(
56 sys.available_memory(),
57 sys.cgroup_limits().map(|limits| limits.free_memory),
58 )
59}
60
61#[derive(Debug, Clone, thiserror::Error, PartialEq, Eq)]
69pub enum MemoryReservationError {
70 #[error(
71 "{context}: cannot reserve {requested_bytes} bytes; {reserved_bytes} of {budget_bytes} bytes already reserved process-wide"
72 )]
73 BudgetExceeded {
74 context: Box<str>,
75 requested_bytes: usize,
76 reserved_bytes: usize,
77 budget_bytes: usize,
78 },
79
80 #[error(
81 "{context}: dense allocation size overflow for {copies} copies of a {nrows}x{ncols} f64 matrix"
82 )]
83 SizeOverflow {
84 context: Box<str>,
85 nrows: usize,
86 ncols: usize,
87 copies: usize,
88 },
89}
90
91#[derive(Debug)]
92struct GovernorLedger {
93 budget_bytes: usize,
94 reserved_bytes: std::sync::atomic::AtomicUsize,
95}
96
97#[derive(Debug, Clone)]
114pub struct MemoryGovernor {
115 ledger: Arc<GovernorLedger>,
116}
117
118impl MemoryGovernor {
119 pub fn global() -> &'static MemoryGovernor {
121 static GLOBAL: OnceLock<MemoryGovernor> = OnceLock::new();
122 GLOBAL.get_or_init(|| MemoryGovernor::with_budget(detect_governor_budget_bytes()))
123 }
124
125 fn with_budget(budget_bytes: usize) -> Self {
130 Self {
131 ledger: Arc::new(GovernorLedger {
132 budget_bytes,
133 reserved_bytes: std::sync::atomic::AtomicUsize::new(0),
134 }),
135 }
136 }
137
138 pub fn budget_bytes(&self) -> usize {
139 self.ledger.budget_bytes
140 }
141
142 pub fn reserved_bytes(&self) -> usize {
143 self.ledger
144 .reserved_bytes
145 .load(std::sync::atomic::Ordering::Acquire)
146 }
147
148 pub fn remaining_bytes(&self) -> usize {
149 self.ledger
150 .budget_bytes
151 .saturating_sub(self.reserved_bytes())
152 }
153
154 pub fn single_materialization_cap_bytes(&self) -> usize {
158 self.ledger.budget_bytes
159 }
160
161 pub fn try_reserve(
168 &self,
169 bytes: usize,
170 context: &str,
171 ) -> Result<MemoryReservation, MemoryReservationError> {
172 use std::sync::atomic::Ordering;
173 let mut current = self.ledger.reserved_bytes.load(Ordering::Relaxed);
174 loop {
175 let next = match current.checked_add(bytes) {
176 Some(next) if next <= self.ledger.budget_bytes => next,
177 _ => {
178 return Err(MemoryReservationError::BudgetExceeded {
179 context: context.into(),
180 requested_bytes: bytes,
181 reserved_bytes: current,
182 budget_bytes: self.ledger.budget_bytes,
183 });
184 }
185 };
186 match self.ledger.reserved_bytes.compare_exchange_weak(
187 current,
188 next,
189 Ordering::AcqRel,
190 Ordering::Relaxed,
191 ) {
192 Ok(_) => {
193 return Ok(MemoryReservation {
194 ledger: Arc::clone(&self.ledger),
195 bytes,
196 });
197 }
198 Err(observed) => current = observed,
199 }
200 }
201 }
202
203 pub fn try_reserve_dense_f64(
207 &self,
208 nrows: usize,
209 ncols: usize,
210 context: &str,
211 ) -> Result<MemoryReservation, MemoryReservationError> {
212 self.try_reserve_dense_f64_copies(nrows, ncols, 1, context)
213 }
214
215 pub fn try_reserve_dense_f64_copies(
218 &self,
219 nrows: usize,
220 ncols: usize,
221 copies: usize,
222 context: &str,
223 ) -> Result<MemoryReservation, MemoryReservationError> {
224 let bytes = dense_f64_bytes(nrows, ncols)
225 .and_then(|one| one.checked_mul(copies))
226 .ok_or_else(|| MemoryReservationError::SizeOverflow {
227 context: context.into(),
228 nrows,
229 ncols,
230 copies,
231 })?;
232 self.try_reserve(bytes, context)
233 }
234}
235
236pub const fn dense_f64_bytes(nrows: usize, ncols: usize) -> Option<usize> {
238 match nrows.checked_mul(ncols) {
239 Some(cells) => cells.checked_mul(std::mem::size_of::<f64>()),
240 None => None,
241 }
242}
243
244#[derive(Debug)]
248#[must_use = "dropping a memory reservation immediately releases its ledger charge"]
249pub struct MemoryReservation {
250 ledger: Arc<GovernorLedger>,
251 bytes: usize,
252}
253
254impl MemoryReservation {
255 pub fn bytes(&self) -> usize {
256 self.bytes
257 }
258
259 pub fn bind<T>(self, value: T) -> Governed<T> {
261 Governed {
262 value,
263 reservation: self,
264 }
265 }
266}
267
268#[derive(Debug)]
274#[must_use = "the governed value owns a live process-wide memory reservation"]
275pub struct Governed<T> {
276 value: T,
277 reservation: MemoryReservation,
278}
279
280impl<T> Governed<T> {
281 pub fn reserved_bytes(&self) -> usize {
282 self.reservation.bytes()
283 }
284}
285
286impl<T> std::ops::Deref for Governed<T> {
287 type Target = T;
288
289 fn deref(&self) -> &Self::Target {
290 &self.value
291 }
292}
293
294impl<T> std::ops::DerefMut for Governed<T> {
295 fn deref_mut(&mut self) -> &mut Self::Target {
296 &mut self.value
297 }
298}
299
300impl<T> AsRef<T> for Governed<T> {
301 fn as_ref(&self) -> &T {
302 &self.value
303 }
304}
305
306impl<T> AsMut<T> for Governed<T> {
307 fn as_mut(&mut self) -> &mut T {
308 &mut self.value
309 }
310}
311
312impl Drop for MemoryReservation {
313 fn drop(&mut self) {
314 self.ledger
315 .reserved_bytes
316 .fetch_sub(self.bytes, std::sync::atomic::Ordering::AcqRel);
317 }
318}
319
320#[derive(Clone, Copy, Debug, Default)]
323pub struct ProblemHints {
324 pub marginal_slope_large_scale_active: bool,
325}
326
327#[derive(Clone, Copy, Debug, PartialEq, Eq)]
328pub enum DerivativeStorageMode {
329 AnalyticOperatorRequired,
331 MaterializeIfSmall,
333 DiagnosticsOnly,
335}
336
337#[derive(Clone, Debug)]
338pub struct MaterializationPolicy {
339 pub max_single_dense_bytes: usize,
340 pub max_cached_dense_bytes: usize,
341 pub row_chunk_target_bytes: usize,
342 pub allow_operator_materialization: bool,
343 pub allow_diagnostic_materialization: bool,
344}
345
346#[derive(Debug, thiserror::Error)]
347pub enum MatrixMaterializationError {
348 #[error(
349 "{context}: dense materialization of {nrows}x{ncols} requires {bytes} bytes (limit {limit_bytes})"
350 )]
351 TooLarge {
352 context: &'static str,
353 nrows: usize,
354 ncols: usize,
355 bytes: usize,
356 limit_bytes: usize,
357 },
358
359 #[error("{context}: operator does not implement chunked row access")]
360 MissingRowChunk { context: &'static str },
361
362 #[error("{context}: row materialization failed: {reason}")]
363 RowMaterializationFailed {
364 context: &'static str,
365 reason: String,
366 },
367
368 #[error("{context}: materialization forbidden by policy (mode={mode:?})")]
369 Forbidden {
370 context: &'static str,
371 mode: DerivativeStorageMode,
372 },
373
374 #[error(transparent)]
378 Reservation(#[from] MemoryReservationError),
379}
380
381pub trait ResidentBytes {
382 fn resident_bytes(&self) -> usize;
383}
384
385impl ResourcePolicy {
386 pub fn default_library() -> Self {
401 let governor = MemoryGovernor::global();
402 let single_cap = governor.single_materialization_cap_bytes();
403 Self {
404 max_single_materialization_bytes: single_cap,
405 max_operator_cache_bytes: single_cap,
406 max_spatial_distance_cache_bytes: single_cap,
407 max_owned_data_cache_bytes: single_cap,
408 row_chunk_target_bytes: LIBRARY_ROW_CHUNK_TARGET_BYTES,
409 derivative_storage_mode: DerivativeStorageMode::MaterializeIfSmall,
410 }
411 }
412
413 pub fn analytic_operator_required() -> Self {
419 let base = Self::default_library();
420 Self {
421 derivative_storage_mode: DerivativeStorageMode::AnalyticOperatorRequired,
422 ..base
423 }
424 }
425
426 pub fn for_problem(hints: ProblemHints) -> Self {
438 if hints.marginal_slope_large_scale_active {
439 return Self::analytic_operator_required();
440 }
441 Self::default_library()
442 }
443
444 pub fn permissive_small_data() -> Self {
447 let base = Self::default_library();
448 Self {
449 row_chunk_target_bytes: 64 * 1024 * 1024,
450 ..base
451 }
452 }
453
454 pub const fn material_policy(&self) -> MaterializationPolicy {
455 MaterializationPolicy {
456 max_single_dense_bytes: self.max_single_materialization_bytes,
457 max_cached_dense_bytes: self.max_operator_cache_bytes,
458 row_chunk_target_bytes: self.row_chunk_target_bytes,
459 allow_operator_materialization: matches!(
460 self.derivative_storage_mode,
461 DerivativeStorageMode::MaterializeIfSmall
462 ),
463 allow_diagnostic_materialization: !matches!(
464 self.derivative_storage_mode,
465 DerivativeStorageMode::AnalyticOperatorRequired
466 ),
467 }
468 }
469}
470
471pub const fn rows_for_target_bytes(target_bytes: usize, cols: usize) -> usize {
474 let raw_bytes_per_row = cols.saturating_mul(std::mem::size_of::<f64>());
475 let bytes_per_row = if raw_bytes_per_row == 0 {
476 1
477 } else {
478 raw_bytes_per_row
479 };
480 let rows = target_bytes / bytes_per_row;
481 if rows == 0 { 1 } else { rows }
482}
483
484pub fn prediction_chunk_rows(parameter_dim: usize, local_dim: usize, total_rows: usize) -> usize {
491 const MIN_ROWS: usize = 16;
492 const MAX_ROWS: usize = 4096;
493
494 if total_rows == 0 {
495 return 1;
496 }
497 let live_f64_values_per_row = parameter_dim
498 .max(1)
499 .saturating_mul(local_dim.max(1))
500 .saturating_mul(4);
501 rows_for_target_bytes(
502 ResourcePolicy::default_library().row_chunk_target_bytes,
503 live_f64_values_per_row,
504 )
505 .clamp(MIN_ROWS, MAX_ROWS)
506 .min(total_rows)
507}
508
509use std::collections::{HashMap, VecDeque};
510use std::hash::{Hash, Hasher};
511use std::sync::{Arc, Mutex, OnceLock};
512
513pub struct ByteLruCache<K: Eq + Hash + Clone, V> {
524 shards: Box<[Mutex<ByteLruInner<K, V>>]>,
535 shard_bytes: usize,
537 shard_entries: Option<usize>,
539 max_bytes: usize,
540}
541
542struct ByteLruInner<K, V> {
543 map: HashMap<K, (V, usize, MemoryReservation)>,
546 order: VecDeque<K>,
547 resident_bytes: usize,
548}
549
550impl<K: Eq + Hash + Clone, V: Clone + ResidentBytes> ByteLruCache<K, V> {
551 pub fn new(max_bytes: usize) -> Self {
552 Self::build(max_bytes, None, 1)
553 }
554
555 pub fn with_max_entries(max_bytes: usize, max_entries: usize) -> Self {
556 Self::build(max_bytes, Some(max_entries), 1)
557 }
558
559 pub fn new_sharded(max_bytes: usize, shard_count: usize) -> Self {
564 Self::build(max_bytes, None, shard_count)
565 }
566
567 pub fn with_max_entries_sharded(
570 max_bytes: usize,
571 max_entries: usize,
572 shard_count: usize,
573 ) -> Self {
574 Self::build(max_bytes, Some(max_entries), shard_count)
575 }
576
577 fn build(max_bytes: usize, max_entries: Option<usize>, shard_count: usize) -> Self {
578 let shard_count = shard_count.max(1);
579 let shard_bytes = max_bytes.div_ceil(shard_count);
584 let shard_entries = max_entries.map(|m| {
585 if m == 0 {
586 0
587 } else {
588 m.div_ceil(shard_count).max(1)
589 }
590 });
591 let shards = (0..shard_count)
592 .map(|_| {
593 Mutex::new(ByteLruInner {
594 map: HashMap::new(),
595 order: VecDeque::new(),
596 resident_bytes: 0,
597 })
598 })
599 .collect::<Vec<_>>()
600 .into_boxed_slice();
601 Self {
602 shards,
603 shard_bytes,
604 shard_entries,
605 max_bytes,
606 }
607 }
608
609 #[inline]
610 fn shard(&self, key: &K) -> &Mutex<ByteLruInner<K, V>> {
611 if self.shards.len() == 1 {
612 return &self.shards[0];
613 }
614 let mut hasher = std::collections::hash_map::DefaultHasher::new();
615 key.hash(&mut hasher);
616 &self.shards[(hasher.finish() as usize) % self.shards.len()]
617 }
618
619 pub fn get(&self, key: &K) -> Option<V> {
620 let mut g = self.shard(key).lock().unwrap_or_else(|p| p.into_inner());
622 let v = g.map.get(key)?.0.clone();
623 if let Some(pos) = g.order.iter().position(|k| k == key) {
625 let k = g.order.remove(pos).unwrap();
626 g.order.push_back(k);
627 }
628 Some(v)
629 }
630
631 pub fn insert(&self, key: K, value: V) {
632 let charge = value.resident_bytes();
633 let mut g = self.shard(&key).lock().unwrap_or_else(|p| p.into_inner());
634
635 if let Some((_old, old_charge, _reservation)) = g.map.remove(&key) {
638 g.resident_bytes = g.resident_bytes.saturating_sub(old_charge);
639 if let Some(pos) = g.order.iter().position(|k| k == &key) {
640 g.order.remove(pos);
641 }
642 }
643
644 if charge > self.shard_bytes {
645 return;
647 }
648
649 if let Some(max_entries) = self.shard_entries {
650 if max_entries == 0 {
651 return;
652 }
653 while g.map.len() >= max_entries {
654 if let Some(evict_key) = g.order.pop_front() {
655 if let Some((_v, c, _reservation)) = g.map.remove(&evict_key) {
656 g.resident_bytes = g.resident_bytes.saturating_sub(c);
657 }
658 } else {
659 break;
660 }
661 }
662 }
663
664 while g.resident_bytes + charge > self.shard_bytes {
665 if let Some(evict_key) = g.order.pop_front() {
666 if let Some((_v, c, _reservation)) = g.map.remove(&evict_key) {
667 g.resident_bytes = g.resident_bytes.saturating_sub(c);
668 }
669 } else {
670 break;
671 }
672 }
673
674 let reservation =
675 match MemoryGovernor::global().try_reserve(charge, "ByteLruCache resident entry") {
676 Ok(reservation) => reservation,
677 Err(_) => return,
678 };
679 g.map.insert(key.clone(), (value, charge, reservation));
680 g.order.push_back(key);
681 g.resident_bytes = g.resident_bytes.saturating_add(charge);
682 }
683
684 pub fn resident_bytes(&self) -> usize {
685 self.shards
686 .iter()
687 .map(|shard| {
688 shard
689 .lock()
690 .unwrap_or_else(|p| p.into_inner())
691 .resident_bytes
692 })
693 .sum()
694 }
695
696 pub const fn max_bytes(&self) -> usize {
697 self.max_bytes
698 }
699
700 pub fn len(&self) -> usize {
701 self.shards
702 .iter()
703 .map(|shard| shard.lock().unwrap_or_else(|p| p.into_inner()).map.len())
704 .sum()
705 }
706
707 pub fn is_empty(&self) -> bool {
708 self.len() == 0
709 }
710
711 pub fn clear(&self) {
712 for shard in self.shards.iter() {
713 let mut g = shard.lock().unwrap_or_else(|p| p.into_inner());
714 g.map.clear();
715 g.order.clear();
716 g.resident_bytes = 0;
717 }
718 }
719}
720
721impl<K: Eq + Hash + Clone, V: Clone + ResidentBytes> std::fmt::Debug for ByteLruCache<K, V> {
722 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
723 f.debug_struct("ByteLruCache")
724 .field("resident_bytes", &self.resident_bytes())
725 .field("max_bytes", &self.max_bytes)
726 .field("shard_count", &self.shards.len())
727 .field("shard_bytes", &self.shard_bytes)
728 .field("shard_entries", &self.shard_entries)
729 .finish()
730 }
731}
732
733impl ResidentBytes for Arc<ndarray::Array2<f64>> {
740 fn resident_bytes(&self) -> usize {
741 std::mem::size_of::<f64>()
742 .saturating_mul(self.nrows())
743 .saturating_mul(self.ncols())
744 }
745}
746
747pub struct RayonSafeOnce<T> {
768 slot: std::sync::OnceLock<T>,
769}
770
771impl<T> RayonSafeOnce<T> {
772 pub const fn new() -> Self {
773 Self {
774 slot: std::sync::OnceLock::new(),
775 }
776 }
777
778 #[inline]
780 pub fn get(&self) -> Option<&T> {
781 self.slot.get()
782 }
783
784 pub fn get_or_compute<F>(&self, init: F) -> &T
796 where
797 F: FnOnce() -> T,
798 {
799 if let Some(v) = self.slot.get() {
800 return v;
801 }
802 let candidate = init();
803 self.slot.set(candidate).ok();
804 self.slot
805 .get()
806 .expect("RayonSafeOnce slot populated by set() above")
807 }
808}
809
810impl<T> Default for RayonSafeOnce<T> {
811 fn default() -> Self {
812 Self::new()
813 }
814}
815
816impl<T: Clone> Clone for RayonSafeOnce<T> {
817 fn clone(&self) -> Self {
818 let cloned = Self::new();
819 if let Some(value) = self.slot.get() {
820 cloned.slot.set(value.clone()).ok();
821 }
822 cloned
823 }
824}
825
826impl<T: std::fmt::Debug> std::fmt::Debug for RayonSafeOnce<T> {
827 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
828 f.debug_struct("RayonSafeOnce")
829 .field("slot", &self.slot.get())
830 .finish()
831 }
832}
833
834#[cfg(test)]
835mod byte_lru_tests {
836 use super::*;
837
838 #[derive(Clone, PartialEq, Debug)]
840 struct Payload(u64);
841 impl ResidentBytes for Payload {
842 fn resident_bytes(&self) -> usize {
843 8
844 }
845 }
846
847 #[test]
848 fn single_shard_round_trips_and_evicts_by_bytes() {
849 let cache: ByteLruCache<u64, Payload> = ByteLruCache::new(24);
851 for k in 0..3 {
852 cache.insert(k, Payload(k));
853 }
854 assert_eq!(cache.len(), 3);
855 assert_eq!(cache.resident_bytes(), 24);
856 assert_eq!(cache.get(&0), Some(Payload(0)));
858 cache.insert(3, Payload(3));
859 assert_eq!(cache.len(), 3);
861 assert_eq!(cache.get(&1), None);
862 assert_eq!(cache.get(&0), Some(Payload(0)));
863 assert_eq!(cache.get(&3), Some(Payload(3)));
864 }
865
866 #[test]
867 fn zero_entry_budget_disables_caching_in_every_shard() {
868 let single: ByteLruCache<u64, Payload> = ByteLruCache::with_max_entries(1 << 20, 0);
869 single.insert(7, Payload(7));
870 assert_eq!(single.get(&7), None);
871 let sharded: ByteLruCache<u64, Payload> =
872 ByteLruCache::with_max_entries_sharded(1 << 20, 0, 16);
873 sharded.insert(7, Payload(7));
874 assert_eq!(sharded.get(&7), None);
875 }
876
877 #[test]
878 fn sharded_cache_retrieves_all_keys_and_respects_aggregate_budget() {
879 let shard_count = 8usize;
883 let max_bytes = 8 * 64; let cache: ByteLruCache<u64, Payload> = ByteLruCache::new_sharded(max_bytes, shard_count);
885 for k in 0..64u64 {
886 cache.insert(k, Payload(k));
887 }
888 assert!(cache.resident_bytes() <= max_bytes.div_ceil(shard_count) * shard_count);
890 cache.insert(123, Payload(123));
892 assert_eq!(cache.get(&123), Some(Payload(123)));
893 assert!(!cache.is_empty());
894 cache.clear();
895 assert_eq!(cache.len(), 0);
896 assert_eq!(cache.resident_bytes(), 0);
897 }
898}
899
900#[cfg(test)]
901mod resource_policy_tests {
902 use super::*;
903
904 #[test]
907 fn rows_for_target_bytes_exact_fit() {
908 assert_eq!(rows_for_target_bytes(8, 1), 1);
910 }
911
912 #[test]
913 fn rows_for_target_bytes_multiple_rows() {
914 assert_eq!(rows_for_target_bytes(80, 1), 10);
916 }
917
918 #[test]
919 fn rows_for_target_bytes_multiple_cols() {
920 assert_eq!(rows_for_target_bytes(128, 4), 4);
922 }
923
924 #[test]
925 fn rows_for_target_bytes_zero_target_returns_one() {
926 assert_eq!(rows_for_target_bytes(0, 1), 1);
928 }
929
930 #[test]
931 fn rows_for_target_bytes_zero_cols_returns_non_zero() {
932 assert_eq!(rows_for_target_bytes(100, 0), 100);
934 }
935
936 #[test]
937 fn rows_for_target_bytes_large_target() {
938 let target = 8 * 1024 * 1024;
940 let cols = 1024_usize;
941 let expected = target / (cols * std::mem::size_of::<f64>());
942 assert_eq!(rows_for_target_bytes(target, cols), expected);
943 }
944
945 #[test]
946 fn prediction_chunks_share_the_runtime_byte_budget() {
947 assert_eq!(prediction_chunk_rows(1024, 1, 100_000), 256);
948 assert_eq!(prediction_chunk_rows(32, 2, 100_000), 4096);
949 }
950
951 #[test]
952 fn prediction_chunks_respect_dataset_bounds() {
953 assert_eq!(prediction_chunk_rows(1, 1, 7), 7);
954 assert_eq!(prediction_chunk_rows(1, 1, 0), 1);
955 }
956
957 #[test]
960 fn for_problem_small_data_uses_materialize_if_small() {
961 let p = ResourcePolicy::for_problem(ProblemHints::default());
962 assert_eq!(
963 p.derivative_storage_mode,
964 DerivativeStorageMode::MaterializeIfSmall
965 );
966 }
967
968 #[test]
969 fn for_problem_has_no_row_or_column_cliff() {
970 let narrow = ResourcePolicy::for_problem(ProblemHints::default());
971 let wide = ResourcePolicy::for_problem(ProblemHints::default());
972 assert_eq!(
973 narrow.derivative_storage_mode,
974 DerivativeStorageMode::MaterializeIfSmall
975 );
976 assert_eq!(
977 wide.derivative_storage_mode,
978 DerivativeStorageMode::MaterializeIfSmall
979 );
980 }
981
982 #[test]
983 fn for_problem_dimension_overflow_defers_to_typed_reservation() {
984 let policy = ResourcePolicy::for_problem(ProblemHints::default());
985 assert_eq!(
986 policy.derivative_storage_mode,
987 DerivativeStorageMode::MaterializeIfSmall
988 );
989 }
990
991 #[test]
992 fn for_problem_marginal_slope_hint_is_strict() {
993 let p = ResourcePolicy::for_problem(ProblemHints {
994 marginal_slope_large_scale_active: true,
995 });
996 assert_eq!(
997 p.derivative_storage_mode,
998 DerivativeStorageMode::AnalyticOperatorRequired
999 );
1000 }
1001
1002 #[test]
1005 fn material_policy_default_library_allows_operator_and_diagnostics() {
1006 let mp = ResourcePolicy::default_library().material_policy();
1007 assert!(mp.allow_operator_materialization);
1008 assert!(mp.allow_diagnostic_materialization);
1009 }
1010
1011 #[test]
1012 fn material_policy_analytic_operator_required_blocks_both() {
1013 let mp = ResourcePolicy::analytic_operator_required().material_policy();
1014 assert!(!mp.allow_operator_materialization);
1015 assert!(!mp.allow_diagnostic_materialization);
1016 }
1017
1018 #[test]
1019 fn material_policy_propagates_byte_limits() {
1020 let policy = ResourcePolicy::default_library();
1021 let mp = policy.material_policy();
1022 assert_eq!(
1023 mp.max_single_dense_bytes,
1024 policy.max_single_materialization_bytes
1025 );
1026 assert_eq!(mp.max_cached_dense_bytes, policy.max_operator_cache_bytes);
1027 assert_eq!(mp.row_chunk_target_bytes, policy.row_chunk_target_bytes);
1028 }
1029
1030 #[test]
1033 fn reservations_account_and_release_on_drop() {
1034 let governor = MemoryGovernor::with_budget(1_000);
1035 assert_eq!(governor.remaining_bytes(), 1_000);
1036 let first = governor.try_reserve(600, "test-first").expect("fits");
1037 assert_eq!(governor.reserved_bytes(), 600);
1038 assert_eq!(governor.remaining_bytes(), 400);
1039 assert_eq!(first.bytes(), 600);
1040 drop(first);
1041 assert_eq!(governor.reserved_bytes(), 0);
1042 assert_eq!(governor.remaining_bytes(), 1_000);
1043 }
1044
1045 #[test]
1046 fn jointly_excessive_reservations_are_refused_with_evidence() {
1047 let governor = MemoryGovernor::with_budget(1_000);
1051 let held = governor.try_reserve(600, "test-held").expect("fits alone");
1052 let refusal = governor
1053 .try_reserve(600, "test-joint")
1054 .expect_err("600 + 600 exceeds the 1000-byte budget");
1055 assert_eq!(
1056 refusal,
1057 MemoryReservationError::BudgetExceeded {
1058 context: "test-joint".into(),
1059 requested_bytes: 600,
1060 reserved_bytes: 600,
1061 budget_bytes: 1_000,
1062 }
1063 );
1064 drop(held);
1067 let refreshed = governor
1068 .try_reserve(600, "test-joint")
1069 .expect("fits after release");
1070 assert_eq!(refreshed.bytes(), 600);
1071 }
1072
1073 #[test]
1074 fn dense_reservation_uses_checked_footprint() {
1075 let governor = MemoryGovernor::with_budget(1 << 20);
1076 let ok = governor
1077 .try_reserve_dense_f64(1024, 64, "test-dense")
1078 .expect("512 KiB fits in 1 MiB");
1079 assert_eq!(ok.bytes(), 1024 * 64 * 8);
1080 drop(ok);
1081 governor
1084 .try_reserve_dense_f64(usize::MAX, 2, "test-overflow")
1085 .expect_err("overflowing footprint cannot be reserved");
1086 let unlimited = MemoryGovernor::with_budget(usize::MAX);
1087 assert!(matches!(
1088 unlimited.try_reserve_dense_f64(usize::MAX, 2, "test-overflow"),
1089 Err(MemoryReservationError::SizeOverflow { .. })
1090 ));
1091 }
1092
1093 #[test]
1094 fn concurrent_reservations_never_oversubscribe() {
1095 let governor = std::sync::Arc::new(MemoryGovernor::with_budget(1_000));
1096 let granted = std::sync::Arc::new(std::sync::atomic::AtomicUsize::new(0));
1097 let barrier = std::sync::Arc::new(std::sync::Barrier::new(9));
1098 std::thread::scope(|scope| {
1099 for _ in 0..8 {
1100 let governor = std::sync::Arc::clone(&governor);
1101 let granted = std::sync::Arc::clone(&granted);
1102 let barrier = std::sync::Arc::clone(&barrier);
1103 scope.spawn(move || {
1104 let held = governor.try_reserve(200, "test-race").ok();
1105 if held.is_some() {
1106 granted.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
1107 }
1108 barrier.wait();
1109 assert!(governor.reserved_bytes() <= governor.budget_bytes());
1110 barrier.wait();
1111 drop(held);
1112 });
1113 }
1114 barrier.wait();
1115 assert_eq!(granted.load(std::sync::atomic::Ordering::SeqCst), 5);
1116 assert_eq!(governor.reserved_bytes(), 1_000);
1117 barrier.wait();
1118 });
1119 assert_eq!(governor.reserved_bytes(), 0);
1120 }
1121
1122 #[test]
1123 fn global_policy_caps_are_one_shared_admission_ceiling() {
1124 let governor = MemoryGovernor::global();
1125 assert_eq!(
1126 governor.single_materialization_cap_bytes(),
1127 governor.budget_bytes()
1128 );
1129 let policy = ResourcePolicy::default_library();
1130 assert_eq!(
1131 policy.max_single_materialization_bytes,
1132 governor.single_materialization_cap_bytes()
1133 );
1134 let strict = ResourcePolicy::analytic_operator_required();
1135 assert_eq!(
1136 strict.max_single_materialization_bytes,
1137 policy.max_single_materialization_bytes
1138 );
1139 }
1140
1141 #[test]
1142 fn governed_value_holds_and_releases_its_charge() {
1143 let governor = MemoryGovernor::with_budget(64);
1144 let governed = governor
1145 .try_reserve(32, "governed-value")
1146 .expect("reservation fits")
1147 .bind(vec![0_u8; 32]);
1148 assert_eq!(governed.len(), 32);
1149 assert_eq!(governed.reserved_bytes(), 32);
1150 assert_eq!(governor.reserved_bytes(), 32);
1151 drop(governed);
1152 assert_eq!(governor.reserved_bytes(), 0);
1153 }
1154
1155 #[test]
1156 fn budget_derivation_honors_zero_and_cgroup_limits() {
1157 assert_eq!(governor_budget_from_available(1_000, None), 750);
1158 assert_eq!(governor_budget_from_available(1_000, Some(400)), 300);
1159 assert_eq!(governor_budget_from_available(1_000, Some(0)), 0);
1160 assert_eq!(governor_budget_from_available(0, None), 0);
1161 }
1162}