[PATCH proxmox v2 08/40] resource-scheduling: factor out topsis alternative mapping

Daniel Kral
2026-03-24T18:34:11Z

The same calculation will be needed for the scoring of migrations with
the TOPSIS method in the following patch.

Signed-off-by: Daniel Kral <d.kral@proxmox.com>
---
changes v1 -> v2:
- new!

proxmox-resource-scheduling/src/scheduler.rs | 68 ++++++++++++--------
1 file changed, 42 insertions(+), 26 deletions(-)

diff --git a/proxmox-resource-scheduling/src/scheduler.rs b/proxmox-resource-scheduling/src/scheduler.rs
index 47abffb1..69dc6f4e 100644
--- a/proxmox-resource-scheduling/src/scheduler.rs
+++ b/proxmox-resource-scheduling/src/scheduler.rs
@@ -43,6 +43,44 @@ impl Scheduler {
}
}

+ /// Map the current node usages to a [`PveTopsisAlternative`].
+ ///
+ /// The [`PveTopsisAlternative`] is derived by calculating a modified version of the root mean
+ /// square (RMS) and maximum value of each stat in the node usages.
+ fn topsis_alternative_with(
+ &self,
+ map_node_stats: impl Fn(&NodeUsage) -> NodeStats,
+ ) -> PveTopsisAlternative {
+ let len = self.nodes.len();
+
+ // Base values on percentages to allow comparing nodes with different stats.
+ let mut highest_cpu = 0.0;
+ let mut squares_cpu = 0.0;
+ let mut highest_mem = 0.0;
+ let mut squares_mem = 0.0;
+
+ for node in self.nodes.iter() {
+ let new_stats = map_node_stats(node);
+
+ let new_cpu = new_stats.cpu_load();
+ highest_cpu = f64::max(highest_cpu, new_cpu);
+ squares_cpu += new_cpu.powi(2);
+
+ let new_mem = new_stats.mem_load();
+ highest_mem = f64::max(highest_mem, new_mem);
+ squares_mem += new_mem.powi(2);
+ }
+
+ // Add 1.0 to avoid boosting tiny differences: e.g. 0.004 is twice as much as 0.002, but
+ // 1.004 is only slightly more than 1.002.
+ PveTopsisAlternative {
+ average_cpu: 1.0 + (squares_cpu / len as f64).sqrt(),
+ highest_cpu: 1.0 + highest_cpu,
+ average_memory: 1.0 + (squares_mem / len as f64).sqrt(),
+ highest_memory: 1.0 + highest_mem,
+ }
+ }
+
/// Scores nodes to start a resource with the usage statistics `resource_stats` on.
///
/// The scoring is done as if the resource is already started on each node. This assumes that
@@ -55,43 +93,21 @@ impl Scheduler {
&self,
resource_stats: T,
) -> Result<Vec<(String, f64)>, Error> {
- let len = self.nodes.len();
let resource_stats = resource_stats.into();

let matrix = self
.nodes
.iter()
.map(|node| {
- // Base values on percentages to allow comparing nodes with different stats.
- let mut highest_cpu = 0.0;
- let mut squares_cpu = 0.0;
- let mut highest_mem = 0.0;
- let mut squares_mem = 0.0;
-
- for target_node in self.nodes.iter() {
+ self.topsis_alternative_with(|target_node| {
let mut new_stats = target_node.stats;

if node.name == target_node.name {
new_stats.add_started_resource(&resource_stats)
- };
+ }

- let new_cpu = new_stats.cpu_load();
- highest_cpu = f64::max(highest_cpu, new_cpu);
- squares_cpu += new_cpu.powi(2);
-
- let new_mem = new_stats.mem_load();
- highest_mem = f64::max(highest_mem, new_mem);
- squares_mem += new_mem.powi(2);
- }
-
- // Add 1.0 to avoid boosting tiny differences: e.g. 0.004 is twice as much as 0.002, but
- // 1.004 is only slightly more than 1.002.
- PveTopsisAlternative {
- average_cpu: 1.0 + (squares_cpu / len as f64).sqrt(),
- highest_cpu: 1.0 + highest_cpu,
- average_memory: 1.0 + (squares_mem / len as f64).sqrt(),
- highest_memory: 1.0 + highest_mem,
- }
+ new_stats
+ })
.into()
})
.collect::<Vec<_>>();
--
2.47.3