Skip to content

Commit 994e992

Browse files
committed
use & log stable bitrate during allocation phases
1 parent b7bb86d commit 994e992

1 file changed

Lines changed: 82 additions & 32 deletions

File tree

  • pulsebeam/src/participant/downstream

pulsebeam/src/participant/downstream/video.rs

Lines changed: 82 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -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)]
307348
struct TrackState {
308349
track: TrackReceiver,
@@ -851,7 +892,7 @@ impl<'a> std::fmt::Display for SlotView<'a> {
851892
}
852893
#[derive(Debug, PartialEq, Clone, Copy)]
853894
pub 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> {
861902
impl<'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

Comments
 (0)