From mboxrd@z Thu Jan 1 00:00:00 1970 Return-Path: Received: from gate001.proxmox.com (gate001.proxmox.com [IPv6:2a0f:8001:1:32::40]) by lore.proxmox.com (Postfix) with ESMTPS id E717F1FF0AF for ; Thu, 22 Oct 2026 11:15:50 +0200 (CEST) Received: from gate001.proxmox.com (localhost.localdomain [127.0.0.1]) by gate001.proxmox.com (Proxmox) with ESMTP id CA49A215DA; Thu, 22 Oct 2026 11:15:47 +0200 (CEST) Content-Type: text/plain; charset=UTF-8 Date: Thu, 22 Oct 2026 11:15:37 +0200 Message-Id: Subject: Re: [PATCH proxmox 03/15] fix #7993: resource-scheduling: lift to weighted load balancing From: "Daniel Kral" To: "Dominik Rusovac" , Mime-Version: 1.0 Content-Transfer-Encoding: quoted-printable X-Mailer: aerc 0.22.0-10-g6373ac9d2179-dirty References: <20261021085932.1196302-1-d.rusovac@proxmox.com> <20261021085932.1196302-4-d.rusovac@proxmox.com> In-Reply-To: <20261021085932.1196302-4-d.rusovac@proxmox.com> X-Bm-Milter-Handled: 55990f41-d878-4baa-be0a-ee34c49e34d2 X-Bm-Transport-Timestamp: 1792660537906 X-SPAM-LEVEL: Spam detection results: 0 AWL 0.755 Adjusted score from AWL reputation of From: address DMARC_MISSING 0.1 Missing DMARC policy KAM_DMARC_STATUS 0.01 Test Rule for DKIM or SPF Failure with Strict Alignment (newer systems) RCVD_IN_DNSWL_MED -2.3 Sender listed at https://www.dnswl.org/, medium trust SPF_HELO_NONE 0.001 SPF: HELO does not publish an SPF Record SPF_PASS -0.001 SPF: sender matches SPF record Message-ID-Hash: JJJTMLUI6UXCFODOMXJZZYUYPXDSBGOH X-Message-ID-Hash: JJJTMLUI6UXCFODOMXJZZYUYPXDSBGOH X-MailFrom: d.kral@proxmox.com X-Mailman-Rule-Misses: dmarc-mitigation; no-senders; approved; loop; banned-address; emergency; member-moderation; nonmember-moderation; administrivia; implicit-dest; max-recipients; max-size; news-moderation; no-subject; digests; suspicious-header X-Mailman-Version: 3.3.10 Precedence: list List-Id: Proxmox VE development discussion List-Help: List-Owner: List-Post: List-Subscribe: List-Unsubscribe: Thanks a lot for working on this, nice work! Especially the cover letter is a very interesting analysis and read! Looks generally sound, though there are some comments inline on having backwards and forwards compatibility and some patch cleanup. On Wed Oct 21, 2026 at 10:59 AM CEST, Dominik Rusovac wrote: > Blend weights into node load computation, effectively replacing > arithmetic mean [0] of the node's CPU load and memory usage with > weighted arithmetic mean [1] thereof. > > Expose customizable weights for bruteforce method. For TOPSIS method > expose customizable weights for average cpu and average memory only. The > weights of highest cpu and highest memory are not independently > configurable; each is derived as twice as much as its average > counterpart, that is, the weight of highest cpu/memory is twice as much > as the weight of average cpu/memory. This makes it possible to > prioritize balancing a specific computational resource while also > preserving the ratio between a resource's two criteria, as introduced in > [2]. > > Apart from the achieved imbalance, expose the score of a scored > migration. Moreover, primarily order scored migrations by their score > (lower is better), falling back to imbalance when scores tie, and > finally to the migration itself if both tie -- ensuring a total, > deterministic order even when several candidates achieve the exact same > score. Add basic unit test for this. > > Refactor existing tests. > > [0] https://en.wikipedia.org/wiki/Arithmetic_mean > [1] https://en.wikipedia.org/wiki/Weighted_arithmetic_mean > [2] https://lore.proxmox.com/all/20221110143800.98047-3-f.ebner@proxmox.c= om/ Changes which were applied should be referenced as commit refs rather than the patch on the mailing list, i.e. here commit 2b325aa5 ("add pve_static module") > > Signed-off-by: Dominik Rusovac > --- > proxmox-resource-scheduling/src/node.rs | 27 +- > proxmox-resource-scheduling/src/pve_static.rs | 3 +- > proxmox-resource-scheduling/src/scheduler.rs | 248 +++++++++++++----- > .../tests/scheduler.rs | 136 ++++++++-- > 4 files changed, 327 insertions(+), 87 deletions(-) > > diff --git a/proxmox-resource-scheduling/src/node.rs b/proxmox-resource-s= cheduling/src/node.rs > index 3c7ed393..f9dc5b16 100644 > --- a/proxmox-resource-scheduling/src/node.rs > +++ b/proxmox-resource-scheduling/src/node.rs > @@ -51,9 +51,30 @@ impl NodeStats { > self.mem as f64 / self.maxmem as f64 > } > =20 > - /// Returns a combined node usage as a percentage. > - pub fn load(&self) -> f64 { > - (self.cpu_load() + self.mem_load()) / 2.0 > + /// Returns the node load as the [weighted arithmetic mean] of [`Sel= f::cpu_load`] and > + /// [`Self::mem_load`]. > + /// > + /// `cpu_weight` controls how much [`Self::cpu_load`] contributes to= the load, analogously, > + /// `mem_weight` controls how much [`Self::mem_load`] contributes to= the load; the higher the > + /// weight, the more that resource dominates the result. nit: can be documented in struct NodeStatsWeights, see below for more > + /// > + /// NOTE: > + /// - the weights have to be positive > + /// - only the weights' ratio matters, not their absolute magnitude > + /// - a weight of 0.0 means no contribution to the load > + /// - [weighted arithmetic mean] is the same as [arithmetic mean] if= the weights are equal > + /// > + /// [weighted arithmetic mean]: https://en.wikipedia.org/wiki/Weight= ed_arithmetic_mean > + /// [arithmetic mean]: https://en.wikipedia.org/wiki/Arithmetic_mean > + pub fn load(&self, cpu_weight: f64, mem_weight: f64) -> f64 { > + let weights_sum =3D cpu_weight + mem_weight; > + > + if weights_sum =3D=3D 0.0 { > + // both, cpu and mem, should not contribute to the load, so = there is no load > + return 0.0; > + } > + > + (self.cpu_load() * cpu_weight + self.mem_load() * mem_weight) / = weights_sum > } Would be nice to have a separate weighted_load() next to the existing load() for backwards compatibility. And for some type of forwards compatibility, maybe something like #[derive(Copy, Clone, Debug)] pub struct NodeStatsWeights { pub cpu_weight: f64, pub mem_weight: f64, } impl Default for NodeStatsWeights { fn default() -> Self { Self { cpu_weight: 1, mem_weight: 1, } } } pub fn weighted_load(&self, weights: &NodeStatsWeights) -> f64 {} ? The NodeStatsWeights struct can then be used in all callers of weighted_load() and therefore the members and their documentation do not need to be repeated there. Though we'd probably need a default for TOPSIS as well, because there it should be 1 : 5 with cpu : mem for backwards compatibility... On another note, adding a new field to NodeStatsWeights then would unfortunately break external callers, except if every external caller allocates it with: let weights =3D NodeStatsWeights { cpu_weight: 2, mem_weight: 3, ..Default::default() } #[non_exhaustive] would be nice here, but unfortunately that prevents external callers to use the literal struct syntax with functional update syntax as above [0] and we would need to build a proper interface to set the weights with methods... We can also do that, but adds some code just for that. [0] https://doc.rust-lang.org/reference/attributes/type_system.html#r-attri= butes.type-system.non_exhaustive.construction > } > =20 > diff --git a/proxmox-resource-scheduling/src/pve_static.rs b/proxmox-reso= urce-scheduling/src/pve_static.rs > index 40b76cbc..98d64907 100644 > --- a/proxmox-resource-scheduling/src/pve_static.rs > +++ b/proxmox-resource-scheduling/src/pve_static.rs > @@ -96,5 +96,6 @@ pub fn score_nodes_to_start_service>( > =20 > let scheduler =3D Scheduler::from_nodes(nodes); > =20 > - scheduler.score_nodes_to_start_resource(*service) > + // uses original empirically determined TOPSIS weights > + scheduler.score_nodes_to_start_resource(*service, 1, 5) > } > diff --git a/proxmox-resource-scheduling/src/scheduler.rs b/proxmox-resou= rce-scheduling/src/scheduler.rs > index a12cd519..0690385d 100644 > --- a/proxmox-resource-scheduling/src/scheduler.rs > +++ b/proxmox-resource-scheduling/src/scheduler.rs > @@ -8,6 +8,9 @@ use std::{ > collections::BinaryHeap, > }; > =20 > +/// Constant to control the ratio of the weights of a resource's average= and highest criteria. > +pub const TOPSIS_AVERAGE_VS_HIGHEST_FACTOR: f64 =3D 2.0; > + > /// The scheduler view of a node. > #[derive(Clone, Debug)] > pub struct NodeUsage { > @@ -69,23 +72,6 @@ fn node_stats_after_migration(candidate: &MigrationCan= didate, node: &NodeUsage) > new_stats > } > =20 > -criteria_struct! { > - /// A given alternative. > - struct PveTopsisAlternative { > - #[criterion("average CPU", -1.0)] > - average_cpu: f64, > - #[criterion("highest CPU", -2.0)] > - highest_cpu: f64, > - #[criterion("average memory", -5.0)] > - average_memory: f64, > - #[criterion("highest memory", -10.0)] > - highest_memory: f64, > - } > - > - const N_CRITERIA; > - static PVE_HA_TOPSIS_CRITERIA; > -} > - would prefer keeping the struct, so there's still a type giving the numbers some name. struct PveTopsisAlternative { average_cpu: f64, highest_cpu: f64, average_memory: f64, highest_memory: f64, } const N_CRITERIA =3D 4; impl From for [f64; N_CRITERIA] { fn from(alternative: PveTopsisAlternative) -> Self { [ alternative.average_cpu, alternative.highest_cpu, alternative.average_memory, alternative.highest_memory, ] } } > #[derive(Clone, Debug)] > pub struct Scheduler { > nodes: Vec, > @@ -111,12 +97,20 @@ pub struct ScoredMigration { > pub migration: Migration, > /// The expected node imbalance after the migration. > pub imbalance: f64, > + /// The score. > + /// > + /// - corresponds to imbalance of weighted loads for > + /// [`Scheduler::score_best_balancing_migration_candidates`] > + /// - corresponds to distance to ideal best alternatives for > + /// [`Scheduler::score_best_balancing_migration_candidates_topsis`= ] > + pub score: f64, As discussed off-list, I'm not sure whether we should expose the 'score' as it isn't relevant to the feedback loop now. > } > =20 > impl Ord for ScoredMigration { > fn cmp(&self, other: &Self) -> Ordering { > - self.imbalance > - .total_cmp(&other.imbalance) > + self.score > + .total_cmp(&other.score) > + .then(self.imbalance.total_cmp(&other.imbalance)) > .then(self.migration.cmp(&other.migration)) AFAICT this could lead to unwanted load balancing decisions at least for the brute force case: If a user configures cpu_weight =3D 0, mem_weight =3D 1, i.e., do not include CPU usage in load balancing decisions, then if two migration candidates would have the equal change in memory distribution but different ones in CPU (i.e. equal imbalance values), it would then still include CPU usage in the load balancing decision with the unweighted 'imbalance' value. Of course, this wouldn't even be really noticable, so might not be an issue. Though I wonder whether calculating both the unweighted and weighted node imbalance values for all migration candidates at least in the bruteforce case does increase the computational load for a relatively small special case (i.e. equal weighted imbalance values). > } > } > @@ -136,19 +130,21 @@ impl PartialEq for ScoredMigration { > impl Eq for ScoredMigration {} > =20 > impl ScoredMigration { > - pub fn new>(migration: T, imbalance: f64) -> Self= { > - // Depending on how the imbalance is calculated, it can contain = minor approximation errors. > - // As this struct implements the Ord trait, users of the struct'= s cmp() can run into cases, > - // where the imbalance is the same up to the significant digits = in base 10, but treated as > - // different values. > + pub fn new>(migration: T, imbalance: f64, score: = f64) -> Self { > + // Depending on how the imbalance or score is calculated, it can= contain minor approximation > + // errors. As this struct implements the Ord trait, users of the= struct's cmp() can run into > + // cases, where the imbalance or score is the same up to the sig= nificant digits in base 10, > + // but treated as different values. > // > // Therefore, truncate any non-significant digits to prevent the= se cases. > let factor =3D 10_f64.powi(f64::DIGITS as i32); > let truncated_imbalance =3D f64::trunc(factor * imbalance) / fac= tor; > + let truncated_score =3D f64::trunc(factor * score) / factor; > =20 > Self { > migration: migration.into(), > imbalance: truncated_imbalance, > + score: truncated_score, > } > } > } > @@ -179,15 +175,15 @@ impl Scheduler { > } > } > =20 > - /// Maps the current node usages to a [`PveTopsisAlternative`]. > + /// Maps the current node usages to a TOPSIS alternative. > /// > - /// The [`PveTopsisAlternative`] is derived by calculating a modifie= d version of the root mean > - /// square (RMS) and maximum value of each stat in the node usages. > + /// The alternative 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 =3D self.nodes.len(); > + ) -> [f64; 4] { > + let len =3D self.nodes.len() as f64; > =20 > // Base values on percentages to allow comparing nodes with diff= erent stats. > let mut highest_cpu =3D 0.0; > @@ -209,55 +205,79 @@ impl Scheduler { > =20 > // Add 1.0 to avoid boosting tiny differences: e.g. 0.004 is twi= ce 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, > - } > + [ > + 1.0 + (squares_cpu / len).sqrt(), > + 1.0 + highest_cpu, > + 1.0 + (squares_mem / len).sqrt(), > + 1.0 + highest_mem, > + ] > } > =20 > /// Returns the load imbalance among the nodes. > /// > + /// `cpu_weight`, respectively, `mem_weight`, control how much [`Nod= eStats::cpu_load`], > + /// respectively, [`NodeStats::mem_load`], contributes to the load i= mbalance via > + /// [`NodeStats::load`]. nit: can be documented in struct NodeStatsWeights > + /// > /// See [`calculate_node_imbalance`] for more information. > - pub fn node_imbalance(&self) -> f64 { > - calculate_node_imbalance(&self.nodes, |node| node.stats.load()) > + pub fn node_imbalance(&self, cpu_weight: f64, mem_weight: f64) -> f6= 4 { > + calculate_node_imbalance(&self.nodes, |node| node.stats.load(cpu= _weight, mem_weight)) > } If we go the NodeStatsWeights route, then pub fn weighted_node_imbalance(&self, weights: &NodeStatsWeights) -> f6= 4 { calculate_node_imbalance(&self.nodes, |node| node.stats.weighted_lo= ad(weights)) } pub fn node_imbalance(&self) -> f64 { self.weighted_node_imbalance(NodeStatsWeights::default()) } So always using the idea that being able to set weights is the generalized form and the old equal weights version of these methods is a specialized variant. > =20 > /// Returns the load imbalance among the nodes as if a specific reso= urce was moved. > /// > + /// `cpu_weight`, respectively, `mem_weight`, control how much [`Nod= eStats::cpu_load`], > + /// respectively, [`NodeStats::mem_load`], contributes to the load i= mbalance via > + /// [`NodeStats::load`]. nit: can be documented in struct NodeStatsWeights > + /// > /// See [`calculate_node_imbalance`] for more information. > - fn node_imbalance_with_migration_candidate(&self, candidate: &Migrat= ionCandidate) -> f64 { > + fn node_imbalance_with_migration_candidate( > + &self, > + candidate: &MigrationCandidate, > + cpu_weight: f64, > + mem_weight: f64, > + ) -> f64 { > calculate_node_imbalance(&self.nodes, |node| { > - node_stats_after_migration(candidate, node).load() > + node_stats_after_migration(candidate, node).load(cpu_weight,= mem_weight) > }) > } same idea as above with weighted_node_imbalance() > =20 > - /// Scores the given migration `candidates` by the best node imbalan= ce improvement with > - /// exhaustive search. > + /// Uses brute-force search to score how much either of the given mi= gration `candidates` > + /// improves node imbalance and ranks them accordingly. Good catch! As noted off-list, the "score" name is unfortunately a bit of a misnomer, because this doesn't actually score them (the imbalance part is only information for the feedback loop and log output outside), but ranks them, therefore making the limit parameter reasonable. However, as this is pre-existing and isn't relevant to this specific patch, this should be done in a separate patch to not clutter this one, which already changes quite a lot on its own. This makes understanding past changes much easier in the future and reviewing them too. > /// > /// The `candidates` are assumed to be consistent with the scheduler= . No further validation is > /// done whether the given nodenames actually exist in the scheduler= . > /// > /// The scoring is done as if each resource migration has already be= en done. This assumes that > /// the already migrated resource consumes the same amount of each s= tat as on the previous node > - /// according to its `stats`. > + /// according to its `stats`. The lower the score, the better. > + /// > + /// `cpu_weight` and `mem_weight` are blended into [`NodeStats::load= `]. The higher the weight of > + /// a computational resource, the more the search will tend to prior= itize balancing out that > + /// resource, e.g., a high `mem_weight` will steer migrations toward= evening out memory usage, > + /// even at the cost of leaving CPU load less balanced. This is a ge= neral tendency, not a > + /// guarantee. > /// > /// Returns up to `limit` of the best scored migrations. > pub fn score_best_balancing_migration_candidates( same idea here with a weighted_score_best_balancing_migration_candidates() > &self, > candidates: I, > limit: usize, > + cpu_weight: usize, > + mem_weight: usize, I'd be fine to use the NodeStatsWeights here as well for the weighted version of this. Even though it doesn't guarantee the non-negative integer property anymore on the type level, that's up to the caller. However, we can enforce this on the pve-rs side on the type level or with assertions here and notes in the doc comment. > ) -> Vec > where > I: IntoIterator, > { > + let (w1, w2) =3D (cpu_weight as f64, mem_weight as f64); nit: variable shadowing would probably be better here just for casting, but if you go for using the NodeStatsWeights struct here as well, this isn't needed anymore > + > let mut scored_migrations =3D candidates > .into_iter() > .map(|candidate| { > - let imbalance =3D self.node_imbalance_with_migration_can= didate(&candidate); > + let imbalance =3D self.node_imbalance_with_migration_can= didate(&candidate, 1.0, 1.0); > + let score =3D self.node_imbalance_with_migration_candida= te(&candidate, w1, w2); > =20 > - Reverse(ScoredMigration::new(candidate, imbalance)) > + Reverse(ScoredMigration::new(candidate, imbalance, score= )) > }) > .collect::>(); > =20 > @@ -274,56 +294,106 @@ impl Scheduler { > best_migrations > } > =20 > - /// Scores the given migration `candidates` by the best node imbalan= ce improvement with the > - /// TOPSIS method. > + /// Scores the given migration `candidates` using the TOPSIS method. Same here, should be in a separate patch as this was already true beforehand. Both documentation changes for the score methods can be done in one patch, of course. > /// > /// The `candidates` are assumed to be consistent with the scheduler= . No further validation is > /// done whether the given nodenames actually exist in the scheduler= . > /// > /// The scoring is done as if each resource migration has already be= en done. This assumes that > /// the already migrated resource consumes the same amount of each s= tat as on the previous node > - /// according to its `stats`. > + /// according to its `stats`. The lower the score, the better. > + /// > + /// The weights are assigned to the respective criteria of the TOPSI= S method. > /// > /// Returns up to `limit` of the best scored migrations. > pub fn score_best_balancing_migration_candidates_topsis( same for this method as above > &self, > candidates: &[MigrationCandidate], > limit: usize, > + cpu_weight: usize, > + mem_weight: usize, > ) -> Result, Error> { > + let (w1, w2) =3D (cpu_weight as f64, mem_weight as f64); > + > + let average_cpu =3D topsis::Criterion::new("average_cpu".to_owne= d(), -w1); > + let highest_cpu =3D topsis::Criterion::new( > + "highest_cpu".to_owned(), > + -(TOPSIS_AVERAGE_VS_HIGHEST_FACTOR * w1), > + ); > + let average_mem =3D topsis::Criterion::new("average_mem".to_owne= d(), -w2); > + let highest_mem =3D topsis::Criterion::new( > + "highest_mem".to_owned(), > + -(TOPSIS_AVERAGE_VS_HIGHEST_FACTOR * w2), > + ); > + > let matrix =3D candidates > .iter() > .map(|candidate| { > self.topsis_alternative_with(|node| node_stats_after_mig= ration(candidate, node)) > - .into() > }) > .collect::>(); > =20 > - let best_alternatives =3D > - topsis::rank_alternatives(&topsis::Matrix::new(matrix)?, &PV= E_HA_TOPSIS_CRITERIA)?; > + let mut scored_migrations =3D topsis::score_alternatives( > + &topsis::Matrix::new(matrix)?, > + &topsis::Criteria::new([average_cpu, highest_cpu, average_me= m, highest_mem])?, > + )? > + .iter() > + .zip(candidates.iter()) > + .map(|(topsis_score, candidate)| { > + let imbalance =3D self.node_imbalance_with_migration_candida= te(candidate, w1, w2); So the meaning of the 'imbalance' field in ScoredMigration would also differ in meaning between brute force and TOPSIS? > + > + Reverse(ScoredMigration::new( > + candidate.clone(), > + imbalance, > + // invert score for consistent sorting > + 1.0 - topsis_score, > + )) > + }) > + .collect::>(); > =20 > - Ok(best_alternatives > - .into_iter() > - .take(limit) > - .map(|i| { > - let imbalance =3D self.node_imbalance_with_migration_can= didate(&candidates[i]); > + let mut best_migrations =3D Vec::with_capacity(limit); > =20 > - ScoredMigration::new(candidates[i].clone(), imbalance) > - }) > - .collect()) > + // BinaryHeap::into_iter_sorted() is still in nightly unfortunat= ely > + while best_migrations.len() < limit { > + match scored_migrations.pop() { > + Some(Reverse(alternative)) =3D> best_migrations.push(alt= ernative), > + None =3D> break, > + } > + } > + > + Ok(best_migrations) > } Hm, I think we should leave score_best_balancing_migration_candidates_topsis() to only rank by TOPSIS and not do the same procedure as is done with brute force. Essentially this boils down to bruteforce and TOPSIS having different guarantees in their return values, i.e., bruteforce will return a Vec with a monotonically increasing 'imbalance' value while TOPSIS doesn't guarantee that but ranks after its own method. The 'imbalance' value is then only used by external callers (i.e. the HA Manager) to give users information and use it to decide whether the migration is worth it with the 'margin' parameter. > =20 > - /// Scores nodes to start a resource with the usage statistics `reso= urce_stats` on. > + /// Scores nodes to start a resource with the usage statistics `reso= urce_stats` on, using the > + /// TOPSIS method. nit: ideally a separate patch > /// > /// The scoring is done as if the resource is already started on eac= h node. This assumes that > /// the already started resource consumes the maximum amount of each= stat according to its > /// `resource_stats`. > /// > + /// The weights are assigned to the respective criteria of the TOPSI= S method. > + /// > /// Returns a vector of (nodename, score) pairs. Scores are between = 0.0 and 1.0 and a higher > /// score is better. > pub fn score_nodes_to_start_resource>( > &self, > resource_stats: T, > + cpu_weight: usize, > + mem_weight: usize, > ) -> Result, Error> { Unrelated to this patch itself, but the parameters are exposed as 'ha-auto-rebalance-{cpu,mem}-weight', but here they are also used for other kinds of load balancing, i.e., initial placement balancing. So maybe we should change the name of those? Otherwise, should get a weighted_ version as well. > + let (w1, w2) =3D (cpu_weight as f64, mem_weight as f64); > + > + let average_cpu =3D topsis::Criterion::new("average_cpu".to_owne= d(), -w1); > + let highest_cpu =3D topsis::Criterion::new( > + "highest_cpu".to_owned(), > + -(TOPSIS_AVERAGE_VS_HIGHEST_FACTOR * w1), > + ); > + let average_mem =3D topsis::Criterion::new("average_mem".to_owne= d(), -w2); > + let highest_mem =3D topsis::Criterion::new( > + "highest_mem".to_owned(), > + -(TOPSIS_AVERAGE_VS_HIGHEST_FACTOR * w2), > + ); > + > let resource_stats =3D resource_stats.into(); > =20 > let matrix =3D self > @@ -339,12 +409,13 @@ impl Scheduler { > =20 > new_stats > }) > - .into() > }) > .collect::>(); > =20 > - let scores =3D > - topsis::score_alternatives(&topsis::Matrix::new(matrix)?, &P= VE_HA_TOPSIS_CRITERIA)?; > + let scores =3D topsis::score_alternatives( > + &topsis::Matrix::new(matrix)?, > + &topsis::Criteria::new([average_cpu, highest_cpu, average_me= m, highest_mem])?, > + )?; > =20 > Ok(scores > .into_iter() > @@ -367,6 +438,7 @@ mod tests { Haven't reviewed the tests yet