@@ -246,15 +246,19 @@ impl VideoAllocator {
246246 views. sort_by_key ( |v| cmp:: Reverse ( v. priority ) ) ;
247247
248248 // 2. Run the pure allocation logic.
249- let ( decisions, desired) = AllocationEngine :: compute ( available_bandwidth, views) ;
249+ let ( decisions, desired, changed) = AllocationEngine :: compute ( available_bandwidth, views) ;
250+
251+ if !changed {
252+ return desired;
253+ }
250254
251255 // 3. Apply the results to the drivers.
252256 for ( mid, decision) in decisions {
253257 let idx = self . mid_to_idx [ & mid] ;
254258 let mut driver = self . slots . get_mut ( idx) . unwrap ( ) ;
255259
256260 match decision {
257- AllocationDecision :: Forward ( receiver) => {
261+ AllocationDecision :: Forward ( receiver, _ ) => {
258262 // Only trigger a switch if the quality actually changed.
259263 if driver. slot . current ( ) . map ( |c| c. quality ) != Some ( receiver. quality ) {
260264 driver. switch_to ( receiver. clone ( ) , false ) ;
@@ -303,6 +307,43 @@ impl VideoAllocator {
303307 }
304308}
305309
310+ pub fn log_allocation (
311+ bwe : Bitrate ,
312+ desired : Bitrate ,
313+ decisions : & HashMap < Mid , AllocationDecision > ,
314+ slots : & [ SlotView ] ,
315+ ) {
316+ let mut reports = Vec :: with_capacity ( slots. len ( ) ) ;
317+ let mut total_used_bps = 0.0 ;
318+
319+ for slot in slots {
320+ let entry = match decisions. get ( & slot. mid ) {
321+ Some ( AllocationDecision :: Forward ( l, bw) ) => {
322+ total_used_bps += bw. as_f64 ( ) ;
323+ let q = match l. quality {
324+ SimulcastQuality :: High => "H" ,
325+ SimulcastQuality :: Medium => "M" ,
326+ SimulcastQuality :: Low => "L" ,
327+ _ => "?" ,
328+ } ;
329+ format ! ( "{}:{}({})" , slot. mid, q, bw)
330+ }
331+ Some ( AllocationDecision :: Pause ( _) ) => format ! ( "{}:PAUSE" , slot. mid) ,
332+ _ => format ! ( "{}:IDLE" , slot. mid) ,
333+ } ;
334+ reports. push ( entry) ;
335+ }
336+
337+ tracing:: info!(
338+ target: "alloc" ,
339+ %bwe,
340+ used = %Bitrate :: from( total_used_bps as u64 ) ,
341+ want = %desired,
342+ streams = %reports. join( " " ) ,
343+ "⚖️"
344+ ) ;
345+ }
346+
306347#[ derive( Debug ) ]
307348struct TrackState {
308349 track : TrackReceiver ,
@@ -851,7 +892,7 @@ impl<'a> std::fmt::Display for SlotView<'a> {
851892}
852893#[ derive( Debug , PartialEq , Clone , Copy ) ]
853894pub enum AllocationDecision < ' a > {
854- Forward ( & ' a SimulcastReceiver ) ,
895+ Forward ( & ' a SimulcastReceiver , Bitrate ) ,
855896 /// Bandwidth-congestion pause. The carried receiver is the layer the engine
856897 /// wants to resume *to* when bandwidth recovers — typically the lowest
857898 /// healthy layer so that recovery starts immediately without renegotiation.
@@ -861,8 +902,14 @@ pub enum AllocationDecision<'a> {
861902impl < ' a > std:: fmt:: Display for AllocationDecision < ' a > {
862903 fn fmt ( & self , f : & mut std:: fmt:: Formatter < ' _ > ) -> std:: fmt:: Result {
863904 match self {
864- AllocationDecision :: Forward ( l) => write ! ( f, "Forward({})" , l) ,
865- AllocationDecision :: Pause ( l) => write ! ( f, "Pause({})" , l) ,
905+ // e.g., "Forward(v1:H @ 1.2M)"
906+ AllocationDecision :: Forward ( layer, bitrate) => {
907+ write ! ( f, "Forward({} @ {})" , layer, bitrate)
908+ }
909+ // e.g., "Pause(v1:L)"
910+ AllocationDecision :: Pause ( layer) => {
911+ write ! ( f, "Pause({})" , layer)
912+ }
866913 }
867914 }
868915}
@@ -878,21 +925,15 @@ impl AllocationEngine {
878925 pub fn compute < ' a > (
879926 available_bw : Bitrate ,
880927 slots : Vec < SlotView < ' a > > ,
881- ) -> ( HashMap < Mid , AllocationDecision < ' a > > , Bitrate ) {
882- // Assert pre-sorted by priority descending (highest first)
883- debug_assert ! (
884- slots. windows( 2 ) . all( |w| w[ 0 ] . priority >= w[ 1 ] . priority) ,
885- "Slots must be pre-sorted by priority descending"
886- ) ;
887-
928+ ) -> ( HashMap < Mid , AllocationDecision < ' a > > , Bitrate , bool ) {
888929 let mut decisions: HashMap < Mid , AllocationDecision < ' a > > = HashMap :: new ( ) ;
889930 let mut remaining_bps = available_bw. as_f64 ( ) ;
931+ let mut changed = false ;
890932
891933 // 1. Maintain or Downgrade
892934 for slot in & slots {
893935 let current = slot. track . by_quality ( slot. current_quality ) ;
894936
895- // Can we afford to keep doing what we're doing?
896937 let stay_layer = current. filter ( |l| {
897938 l. state . is_healthy ( )
898939 && ( l. state . bitrate_bps ( ) * Self :: DOWNGRADE_FACTOR ) <= remaining_bps
@@ -905,10 +946,21 @@ impl AllocationEngine {
905946 } ) ;
906947
907948 if let Some ( layer) = final_layer {
908- remaining_bps -= layer. state . bitrate_bps ( ) ;
909- decisions. insert ( slot. mid , AllocationDecision :: Forward ( layer) ) ;
949+ // Snapshot the bitrate immediately
950+ let layer_bitrate = Bitrate :: from ( layer. state . bitrate_bps ( ) ) ;
951+ let bps = layer_bitrate. as_f64 ( ) ;
952+
953+ if layer. quality != slot. current_quality {
954+ changed = true ;
955+ }
956+
957+ remaining_bps -= bps;
958+ decisions. insert ( slot. mid , AllocationDecision :: Forward ( layer, layer_bitrate) ) ;
910959 } else {
911- // Paused: We carry the lowest quality as the "Target" for the driver to watch.
960+ // If we were forwarding and now we aren't, that's a change
961+ if slot. current_quality != SimulcastQuality :: Undefined {
962+ changed = true ;
963+ }
912964 decisions. insert (
913965 slot. mid ,
914966 AllocationDecision :: Pause ( slot. track . lowest_quality ( ) ) ,
@@ -918,8 +970,6 @@ impl AllocationEngine {
918970
919971 // 2. Upgrade
920972 let mut upgrades_performed = 0 ;
921-
922- // Try to upgrade levels (Low->Med, Med->High)
923973 for tier in [
924974 SimulcastQuality :: Low ,
925975 SimulcastQuality :: Medium ,
@@ -934,50 +984,50 @@ impl AllocationEngine {
934984 break ;
935985 }
936986
937- let decision = decisions. get ( & slot. mid ) . copied ( ) ;
938- let Some ( AllocationDecision :: Forward ( current) ) = decision else {
987+ let Some ( AllocationDecision :: Forward ( current_layer, current_bw) ) =
988+ decisions. get ( & slot. mid ) . copied ( )
989+ else {
939990 continue ;
940991 } ;
941992
942- if current . quality >= tier {
993+ if current_layer . quality >= tier {
943994 continue ;
944995 }
945-
946996 let Some ( target) = slot. track . by_quality ( tier) else {
947997 continue ;
948998 } ;
949-
950999 if !target. state . is_healthy ( ) {
9511000 continue ;
9521001 }
9531002
954- let incremental_cost = target. state . bitrate_bps ( ) - current. state . bitrate_bps ( ) ;
1003+ let target_bw = Bitrate :: from ( target. state . bitrate_bps ( ) ) ;
1004+ let incremental_cost = target_bw. as_f64 ( ) - current_bw. as_f64 ( ) ;
9551005
956- // Must have the upgrade headroom of the incremental jump
1006+ // Check against the 30% upgrade headroom (UPGRADE_FACTOR = 1.3)
9571007 if remaining_bps >= ( incremental_cost * Self :: UPGRADE_FACTOR ) {
9581008 remaining_bps -= incremental_cost;
959- decisions. insert ( slot. mid , AllocationDecision :: Forward ( target) ) ;
1009+ decisions. insert ( slot. mid , AllocationDecision :: Forward ( target, target_bw ) ) ;
9601010 upgrades_performed += 1 ;
1011+ changed = true ;
9611012 }
9621013 }
9631014 }
9641015
1016+ // 3. Demand Calculation (The "Want" Bitrate)
9651017 let total_desired_bps: f64 = slots
9661018 . iter ( )
967- . map ( |slot| {
968- // Find the highest layer that is currently marked healthy.
969- // If none are healthy, we count it as 0.
970- slot. track
1019+ . map ( |s| {
1020+ s. track
9711021 . simulcast
9721022 . iter ( )
9731023 . filter ( |l| l. state . is_healthy ( ) )
9741024 . map ( |l| l. state . bitrate_bps ( ) )
975- . max_by ( |a, b| a. partial_cmp ( b) . unwrap_or ( std :: cmp :: Ordering :: Equal ) )
1025+ . max_by ( |a, b| a. partial_cmp ( b) . unwrap ( ) )
9761026 . unwrap_or ( 0.0 )
9771027 } )
9781028 . sum ( ) ;
9791029
980- ( decisions, Bitrate :: from ( total_desired_bps) )
1030+ ( decisions, Bitrate :: from ( total_desired_bps as u64 ) , changed )
9811031 }
9821032}
9831033
0 commit comments