Driver: Implement audio scheduler (#179)
This PR implements a custom scheduler for audio threads, which reduces thread use and (often) memory consumption. To save threads and memory (e.g., packet buffer allocations), Songbird parks Mixer tasks which do not have any live Tracks. These are now all co-located on a single async 'Idle' task. This task is responsible for managing UDP keepalive messages for each task, maintaining event state, and executing any Mixer task messages. Whenever any message arrives which adds a `Track`, the mixer task is moved to a live thread. The Idle task inspects task counts and execution time on each thread, choosing the first live thread with room, and creating a new one if needed. Each live thread is responsible for running as many live mixers as it can in a single tick every 20ms: this currently defaults to 16 mixers per thread, but is user-configurable. A live thread also stores RTP packet blocks to be written into by each sub-task. Each live thread has a conservative limit of 18ms that it will aim to stay under: if all work takes longer than this, it will offload the task with the highest mixing cost once per tick onto another (possibly new) live worker thread.
This commit is contained in:
187
src/driver/scheduler/task.rs
Normal file
187
src/driver/scheduler/task.rs
Normal file
@@ -0,0 +1,187 @@
|
||||
use std::{
|
||||
marker::PhantomData,
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use flume::{Receiver, Sender};
|
||||
use nohash_hasher::IsEnabled;
|
||||
use rand::random;
|
||||
use tokio::runtime::Handle;
|
||||
|
||||
use crate::{
|
||||
driver::tasks::{
|
||||
error::Error as DriverError,
|
||||
message::{EventMessage, Interconnect, MixerMessage},
|
||||
mixer::Mixer,
|
||||
},
|
||||
Config,
|
||||
};
|
||||
|
||||
use super::SchedulerMessage;
|
||||
|
||||
/// Typesafe counter used to identify individual mixer/worker instances.
|
||||
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct ResId<T>(u64, PhantomData<T>);
|
||||
#[allow(missing_docs)]
|
||||
pub type TaskId = ResId<TaskMarker>;
|
||||
#[allow(missing_docs)]
|
||||
pub type WorkerId = ResId<WorkerMarker>;
|
||||
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct TaskMarker;
|
||||
#[allow(missing_docs)]
|
||||
#[derive(Copy, Clone, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
|
||||
pub struct WorkerMarker;
|
||||
|
||||
impl<T> IsEnabled for ResId<T> {}
|
||||
|
||||
#[allow(missing_docs)]
|
||||
impl<T: Copy> ResId<T> {
|
||||
pub fn new() -> Self {
|
||||
ResId(0, PhantomData)
|
||||
}
|
||||
|
||||
pub fn incr(&mut self) -> Self {
|
||||
let out = *self;
|
||||
self.0 = self.0.wrapping_add(1);
|
||||
out
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "internals"))]
|
||||
pub fn get(&self) -> u64 {
|
||||
self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// An idle mixer instance, externally controlled by a `Driver`.
|
||||
///
|
||||
/// Since we do not allocate packet buffers for idle threads, this
|
||||
/// struct includes various RTP fields.
|
||||
pub struct ParkedMixer {
|
||||
/// Mixer, track, etc. state as well as message receivers.
|
||||
pub mixer: Box<Mixer>,
|
||||
/// The SSRC assigned to this voice session.
|
||||
pub ssrc: u32,
|
||||
/// The last recorded/generated RTP sequence.
|
||||
pub rtp_sequence: u16,
|
||||
/// The last recorded/generated RTP timestamp.
|
||||
pub rtp_timestamp: u32,
|
||||
/// The time at which this `Mixer` was made idle.
|
||||
///
|
||||
/// This is used when transitioning to a live state to determine
|
||||
/// how far we should adjust the RTP timestamp by.
|
||||
pub park_time: Instant,
|
||||
/// The last known cost of executing this task, if it had to be moved
|
||||
/// due to a limit on thread resources.
|
||||
pub last_cost: Option<Duration>,
|
||||
}
|
||||
|
||||
#[allow(missing_docs)]
|
||||
impl ParkedMixer {
|
||||
/// Create a new `Mixer` in a parked state.
|
||||
pub fn new(mix_rx: Receiver<MixerMessage>, interconnect: Interconnect, config: Config) -> Self {
|
||||
Self {
|
||||
mixer: Box::new(Mixer::new(mix_rx, Handle::current(), interconnect, config)),
|
||||
ssrc: 0,
|
||||
rtp_sequence: random::<u16>(),
|
||||
rtp_timestamp: random::<u32>(),
|
||||
park_time: Instant::now(),
|
||||
last_cost: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Spawn a tokio task which forwards any mixer messages to the central `Idle` task pool.
|
||||
///
|
||||
/// Any requests which would cause this mixer to become live will terminate
|
||||
/// this task.
|
||||
pub fn spawn_forwarder(&self, tx: Sender<SchedulerMessage>, id: TaskId) {
|
||||
let remote_rx = self.mixer.mix_rx.clone();
|
||||
tokio::spawn(async move {
|
||||
while let Ok(msg) = remote_rx.recv_async().await {
|
||||
let exit = msg.is_mixer_now_live();
|
||||
let dead = tx.send_async(SchedulerMessage::Do(id, msg)).await.is_err();
|
||||
if exit || dead {
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/// Returns whether the mixer should exit and be cleaned up.
|
||||
pub fn handle_message(&mut self, msg: MixerMessage) -> Result<bool, ()> {
|
||||
match msg {
|
||||
MixerMessage::SetConn(conn, ssrc) => {
|
||||
// Overridden because
|
||||
self.ssrc = ssrc;
|
||||
self.rtp_sequence = random::<u16>();
|
||||
self.rtp_timestamp = random::<u32>();
|
||||
self.mixer.conn_active = Some(conn);
|
||||
self.mixer.update_keepalive(ssrc);
|
||||
|
||||
Ok(false)
|
||||
},
|
||||
MixerMessage::Ws(ws) => {
|
||||
// Overridden so that we don't mistakenly tell Discord we're speaking.
|
||||
self.mixer.ws = ws;
|
||||
self.send_gateway_not_speaking();
|
||||
|
||||
Ok(false)
|
||||
},
|
||||
msg => {
|
||||
let (events_failure, conn_failure, should_exit) =
|
||||
self.mixer.handle_message(msg, &mut []);
|
||||
|
||||
self.mixer
|
||||
.do_rebuilds(events_failure, conn_failure)
|
||||
.map_err(|_| ())
|
||||
.map(|_| should_exit)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle periodic events attached to this `Mixer`, including timer state
|
||||
/// on the event thread and UDP keepalives needed to prevent session termination.
|
||||
///
|
||||
/// As we init our UDP sockets as non-blocking via Tokio -> `into_std`, it is
|
||||
/// safe to call UDP packet sends like this.
|
||||
pub fn tick_and_keepalive(&mut self, now: Instant) -> Result<(), ()> {
|
||||
// TODO: should we include an atomic which signals whether the event
|
||||
// thread *cares*, so we can prevent wakeups?
|
||||
// Can we do the same for live tracks?
|
||||
let mut events_failure = self.mixer.fire_event(EventMessage::Tick).is_err();
|
||||
|
||||
let ka_err = self
|
||||
.mixer
|
||||
.check_and_send_keepalive(Some(now))
|
||||
.or_else(DriverError::disarm_would_block);
|
||||
|
||||
let conn_failure = if let Err(e) = ka_err {
|
||||
events_failure |= e.should_trigger_interconnect_rebuild();
|
||||
e.should_trigger_connect()
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
self.mixer
|
||||
.do_rebuilds(events_failure, conn_failure)
|
||||
.map_err(|_| ())
|
||||
}
|
||||
|
||||
pub fn send_gateway_speaking(&mut self) -> Result<(), ()> {
|
||||
if let Err(e) = self.mixer.send_gateway_speaking() {
|
||||
self.mixer
|
||||
.do_rebuilds(
|
||||
e.should_trigger_interconnect_rebuild(),
|
||||
e.should_trigger_connect(),
|
||||
)
|
||||
.map_err(|_| ())
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send_gateway_not_speaking(&self) {
|
||||
self.mixer.send_gateway_not_speaking();
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user