Skip to main content

oximedia_graph/
node_priority.rs

1//! Node priority and scheduling weight assignment.
2//!
3//! This module assigns priority values to graph nodes for scheduling
4//! decisions. Higher-priority nodes are processed first when multiple
5//! nodes are ready, enabling latency-sensitive paths (e.g., live preview)
6//! to be prioritized over background tasks (e.g., proxy generation).
7
8use std::cmp::Ordering;
9use std::collections::HashMap;
10use std::fmt;
11
12/// Priority class for a graph node.
13#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
14pub enum PriorityClass {
15    /// Real-time path (lowest latency, highest priority).
16    RealTime,
17    /// Interactive path (user-facing, high priority).
18    Interactive,
19    /// Normal processing (default).
20    Normal,
21    /// Background / best-effort processing.
22    Background,
23    /// Idle processing (only when nothing else is pending).
24    Idle,
25}
26
27impl PriorityClass {
28    /// Get the numeric weight for this priority class.
29    ///
30    /// Higher values mean higher priority.
31    pub fn weight(&self) -> i32 {
32        match self {
33            Self::RealTime => 1000,
34            Self::Interactive => 500,
35            Self::Normal => 100,
36            Self::Background => 10,
37            Self::Idle => 1,
38        }
39    }
40
41    /// Return all priority classes in descending priority order.
42    pub fn all_descending() -> &'static [PriorityClass] {
43        &[
44            PriorityClass::RealTime,
45            PriorityClass::Interactive,
46            PriorityClass::Normal,
47            PriorityClass::Background,
48            PriorityClass::Idle,
49        ]
50    }
51}
52
53impl fmt::Display for PriorityClass {
54    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
55        match self {
56            Self::RealTime => write!(f, "RealTime"),
57            Self::Interactive => write!(f, "Interactive"),
58            Self::Normal => write!(f, "Normal"),
59            Self::Background => write!(f, "Background"),
60            Self::Idle => write!(f, "Idle"),
61        }
62    }
63}
64
65impl Default for PriorityClass {
66    fn default() -> Self {
67        Self::Normal
68    }
69}
70
71impl Ord for PriorityClass {
72    fn cmp(&self, other: &Self) -> Ordering {
73        self.weight().cmp(&other.weight())
74    }
75}
76
77impl PartialOrd for PriorityClass {
78    fn partial_cmp(&self, other: &Self) -> Option<Ordering> {
79        Some(self.cmp(other))
80    }
81}
82
83/// A unique node identifier used within the priority system.
84#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
85pub struct PriorityNodeId(pub u64);
86
87impl fmt::Display for PriorityNodeId {
88    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
89        write!(f, "Node({})", self.0)
90    }
91}
92
93/// Describes the priority assignment for a single node.
94#[derive(Debug, Clone)]
95pub struct NodePriorityEntry {
96    /// The node identifier.
97    pub node_id: PriorityNodeId,
98    /// The base priority class.
99    pub class: PriorityClass,
100    /// An additional boost or penalty (-500 to +500).
101    pub boost: i32,
102    /// Computed effective priority (class weight + boost).
103    pub effective: i32,
104    /// Optional label for debugging.
105    pub label: String,
106}
107
108impl NodePriorityEntry {
109    /// Create a new priority entry.
110    pub fn new(node_id: PriorityNodeId, class: PriorityClass, label: &str) -> Self {
111        let effective = class.weight();
112        Self {
113            node_id,
114            class,
115            boost: 0,
116            effective,
117            label: label.to_string(),
118        }
119    }
120
121    /// Apply a priority boost (positive) or penalty (negative).
122    pub fn with_boost(mut self, boost: i32) -> Self {
123        self.boost = boost.clamp(-500, 500);
124        self.effective = self.class.weight() + self.boost;
125        self
126    }
127
128    /// Recalculate effective priority after changes.
129    pub fn recalculate(&mut self) {
130        self.effective = self.class.weight() + self.boost;
131    }
132}
133
134impl fmt::Display for NodePriorityEntry {
135    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
136        write!(
137            f,
138            "{} [{}] priority={} ({}{})",
139            self.label,
140            self.node_id,
141            self.effective,
142            self.class,
143            if self.boost != 0 {
144                format!("{:+}", self.boost)
145            } else {
146                String::new()
147            }
148        )
149    }
150}
151
152/// Manages priority assignments for all nodes in a graph.
153pub struct PriorityManager {
154    /// Priority entries indexed by node ID.
155    entries: HashMap<PriorityNodeId, NodePriorityEntry>,
156    /// Default class for new nodes.
157    default_class: PriorityClass,
158}
159
160impl PriorityManager {
161    /// Create a new priority manager with Normal as the default class.
162    pub fn new() -> Self {
163        Self {
164            entries: HashMap::new(),
165            default_class: PriorityClass::Normal,
166        }
167    }
168
169    /// Set the default priority class for newly registered nodes.
170    pub fn set_default_class(&mut self, class: PriorityClass) {
171        self.default_class = class;
172    }
173
174    /// Get the default priority class.
175    pub fn default_class(&self) -> PriorityClass {
176        self.default_class
177    }
178
179    /// Register a node with a specific priority class.
180    pub fn register(&mut self, node_id: PriorityNodeId, class: PriorityClass, label: &str) {
181        self.entries
182            .insert(node_id, NodePriorityEntry::new(node_id, class, label));
183    }
184
185    /// Register a node with the default priority class.
186    pub fn register_default(&mut self, node_id: PriorityNodeId, label: &str) {
187        let class = self.default_class;
188        self.register(node_id, class, label);
189    }
190
191    /// Get the priority entry for a node.
192    pub fn get(&self, node_id: PriorityNodeId) -> Option<&NodePriorityEntry> {
193        self.entries.get(&node_id)
194    }
195
196    /// Get the effective priority for a node (returns 0 if not registered).
197    pub fn effective_priority(&self, node_id: PriorityNodeId) -> i32 {
198        self.entries.get(&node_id).map_or(0, |e| e.effective)
199    }
200
201    /// Apply a boost to a node's priority.
202    pub fn apply_boost(&mut self, node_id: PriorityNodeId, boost: i32) -> bool {
203        if let Some(entry) = self.entries.get_mut(&node_id) {
204            entry.boost = boost.clamp(-500, 500);
205            entry.recalculate();
206            true
207        } else {
208            false
209        }
210    }
211
212    /// Change a node's priority class.
213    pub fn set_class(&mut self, node_id: PriorityNodeId, class: PriorityClass) -> bool {
214        if let Some(entry) = self.entries.get_mut(&node_id) {
215            entry.class = class;
216            entry.recalculate();
217            true
218        } else {
219            false
220        }
221    }
222
223    /// Unregister a node.
224    pub fn unregister(&mut self, node_id: PriorityNodeId) -> bool {
225        self.entries.remove(&node_id).is_some()
226    }
227
228    /// Get the number of registered nodes.
229    pub fn count(&self) -> usize {
230        self.entries.len()
231    }
232
233    /// Get all node IDs sorted by effective priority (highest first).
234    pub fn sorted_by_priority(&self) -> Vec<PriorityNodeId> {
235        let mut entries: Vec<_> = self.entries.values().collect();
236        entries.sort_by(|a, b| b.effective.cmp(&a.effective));
237        entries.iter().map(|e| e.node_id).collect()
238    }
239
240    /// Get all nodes in a specific priority class.
241    pub fn nodes_in_class(&self, class: PriorityClass) -> Vec<PriorityNodeId> {
242        self.entries
243            .values()
244            .filter(|e| e.class == class)
245            .map(|e| e.node_id)
246            .collect()
247    }
248
249    /// Promote all nodes of one class to a higher class.
250    pub fn promote_class(&mut self, from: PriorityClass, to: PriorityClass) -> usize {
251        let mut count = 0;
252        for entry in self.entries.values_mut() {
253            if entry.class == from {
254                entry.class = to;
255                entry.recalculate();
256                count += 1;
257            }
258        }
259        count
260    }
261
262    /// Clear all entries.
263    pub fn clear(&mut self) {
264        self.entries.clear();
265    }
266}
267
268impl Default for PriorityManager {
269    fn default() -> Self {
270        Self::new()
271    }
272}
273
274#[cfg(test)]
275mod tests {
276    use super::*;
277
278    #[test]
279    fn test_priority_class_weight_ordering() {
280        assert!(PriorityClass::RealTime.weight() > PriorityClass::Interactive.weight());
281        assert!(PriorityClass::Interactive.weight() > PriorityClass::Normal.weight());
282        assert!(PriorityClass::Normal.weight() > PriorityClass::Background.weight());
283        assert!(PriorityClass::Background.weight() > PriorityClass::Idle.weight());
284    }
285
286    #[test]
287    fn test_priority_class_default() {
288        assert_eq!(PriorityClass::default(), PriorityClass::Normal);
289    }
290
291    #[test]
292    fn test_priority_class_display() {
293        assert_eq!(format!("{}", PriorityClass::RealTime), "RealTime");
294        assert_eq!(format!("{}", PriorityClass::Idle), "Idle");
295    }
296
297    #[test]
298    fn test_priority_class_ord() {
299        assert!(PriorityClass::RealTime > PriorityClass::Normal);
300        assert!(PriorityClass::Idle < PriorityClass::Background);
301    }
302
303    #[test]
304    fn test_all_descending() {
305        let all = PriorityClass::all_descending();
306        assert_eq!(all.len(), 5);
307        assert_eq!(all[0], PriorityClass::RealTime);
308        assert_eq!(all[4], PriorityClass::Idle);
309    }
310
311    #[test]
312    fn test_node_priority_entry_new() {
313        let entry =
314            NodePriorityEntry::new(PriorityNodeId(1), PriorityClass::Interactive, "preview");
315        assert_eq!(entry.effective, 500);
316        assert_eq!(entry.boost, 0);
317        assert_eq!(entry.label, "preview");
318    }
319
320    #[test]
321    fn test_node_priority_entry_with_boost() {
322        let entry =
323            NodePriorityEntry::new(PriorityNodeId(1), PriorityClass::Normal, "n").with_boost(50);
324        assert_eq!(entry.effective, 150);
325        assert_eq!(entry.boost, 50);
326    }
327
328    #[test]
329    fn test_boost_clamp() {
330        let entry =
331            NodePriorityEntry::new(PriorityNodeId(1), PriorityClass::Normal, "n").with_boost(9999);
332        assert_eq!(entry.boost, 500);
333    }
334
335    #[test]
336    fn test_priority_manager_register() {
337        let mut mgr = PriorityManager::new();
338        mgr.register(PriorityNodeId(1), PriorityClass::RealTime, "rt_node");
339        assert_eq!(mgr.count(), 1);
340        assert_eq!(mgr.effective_priority(PriorityNodeId(1)), 1000);
341    }
342
343    #[test]
344    fn test_priority_manager_register_default() {
345        let mut mgr = PriorityManager::new();
346        mgr.register_default(PriorityNodeId(1), "default_node");
347        assert_eq!(mgr.effective_priority(PriorityNodeId(1)), 100);
348    }
349
350    #[test]
351    fn test_priority_manager_unregistered_returns_zero() {
352        let mgr = PriorityManager::new();
353        assert_eq!(mgr.effective_priority(PriorityNodeId(999)), 0);
354    }
355
356    #[test]
357    fn test_priority_manager_sorted() {
358        let mut mgr = PriorityManager::new();
359        mgr.register(PriorityNodeId(1), PriorityClass::Background, "bg");
360        mgr.register(PriorityNodeId(2), PriorityClass::RealTime, "rt");
361        mgr.register(PriorityNodeId(3), PriorityClass::Normal, "norm");
362        let sorted = mgr.sorted_by_priority();
363        assert_eq!(sorted[0], PriorityNodeId(2)); // RealTime first
364        assert_eq!(sorted[2], PriorityNodeId(1)); // Background last
365    }
366
367    #[test]
368    fn test_priority_manager_apply_boost() {
369        let mut mgr = PriorityManager::new();
370        mgr.register(PriorityNodeId(1), PriorityClass::Normal, "n");
371        assert!(mgr.apply_boost(PriorityNodeId(1), 200));
372        assert_eq!(mgr.effective_priority(PriorityNodeId(1)), 300);
373        assert!(!mgr.apply_boost(PriorityNodeId(99), 10));
374    }
375
376    #[test]
377    fn test_priority_manager_set_class() {
378        let mut mgr = PriorityManager::new();
379        mgr.register(PriorityNodeId(1), PriorityClass::Normal, "n");
380        mgr.set_class(PriorityNodeId(1), PriorityClass::RealTime);
381        assert_eq!(mgr.effective_priority(PriorityNodeId(1)), 1000);
382    }
383
384    #[test]
385    fn test_priority_manager_promote_class() {
386        let mut mgr = PriorityManager::new();
387        mgr.register(PriorityNodeId(1), PriorityClass::Background, "b1");
388        mgr.register(PriorityNodeId(2), PriorityClass::Background, "b2");
389        mgr.register(PriorityNodeId(3), PriorityClass::Normal, "n1");
390        let promoted = mgr.promote_class(PriorityClass::Background, PriorityClass::Normal);
391        assert_eq!(promoted, 2);
392        assert_eq!(mgr.effective_priority(PriorityNodeId(1)), 100);
393    }
394
395    #[test]
396    fn test_priority_manager_nodes_in_class() {
397        let mut mgr = PriorityManager::new();
398        mgr.register(PriorityNodeId(1), PriorityClass::Idle, "i1");
399        mgr.register(PriorityNodeId(2), PriorityClass::Idle, "i2");
400        mgr.register(PriorityNodeId(3), PriorityClass::Normal, "n1");
401        let idle_nodes = mgr.nodes_in_class(PriorityClass::Idle);
402        assert_eq!(idle_nodes.len(), 2);
403    }
404}