Scheduler
Schedulers are responsible for executing the blocks in a Flowgraph and potentially other async tasks. A scheduler decides where block tasks run, how general async tasks are spawned, and how blocking work is handled.
Most applications should use the default scheduler through Runtime::new(). Select a scheduler explicitly only when you need to configure the native executor or when benchmarking shows that a different scheduler improves a specific flowgraph.
Smol
SmolScheduler is the default scheduler on native targets and is the recommended scheduler for general use. It is based on the smol async runtime and runs block tasks on a pool of executor threads.
The default runtime uses SmolScheduler::default(), which creates one worker per detected CPU core and does not pin workers to cores:
use futuresdr::prelude::*;
let mut fg = Flowgraph::new();
// set up the flowgraph
let fg = Runtime::new().run(fg)?;
Instantiate it explicitly when you want to configure the number of workers or CPU pinning:
use futuresdr::prelude::*;
use futuresdr::runtime::scheduler::SmolScheduler;
let mut fg = Flowgraph::new();
// set up the flowgraph
let scheduler = SmolScheduler::new(2, false);
let fg = Runtime::with_scheduler(scheduler).run(fg)?;
The first argument is the number of executor threads. The second argument enables CPU pinning. When pinning is enabled, workers are pinned to the detected CPU cores in order:
use futuresdr::prelude::*;
use futuresdr::runtime::scheduler::SmolScheduler;
let scheduler = SmolScheduler::new(4, true);
let rt = Runtime::with_scheduler(scheduler);
Flow
FlowScheduler is a custom native scheduler for more controlled execution. It is available with the flow_scheduler feature:
cargo run --features=flow_scheduler --example minimal
Use FlowScheduler::new() to let the scheduler assign all blocks to worker-local queues with its default deterministic mapper:
use futuresdr::prelude::*;
use futuresdr::runtime::scheduler::FlowScheduler;
let mut fg = Flowgraph::new();
// set up the flowgraph
let scheduler = FlowScheduler::new();
let fg = Runtime::with_scheduler(scheduler).run(fg)?;
With FlowScheduler::new(), blocks are not placed in the global queue. Each block is mapped to one worker queue based on its block ID, the number of blocks, and the number of workers. Each worker calls its local blocks round-robin. General async tasks spawned on the scheduler, and local tasks that overflow a worker queue, use a global queue that workers poll when their local work is idle.
For explicit control, use FlowScheduler::with_pinned_blocks() to assign selected blocks to fixed workers. The outer vector index is the worker index, and each inner vector lists the block IDs assigned to that worker in initial queue order. Blocks that are not listed still use the default deterministic mapper:
use futuresdr::blocks::Head;
use futuresdr::blocks::NullSink;
use futuresdr::blocks::NullSource;
use futuresdr::prelude::*;
use futuresdr::runtime::scheduler::FlowScheduler;
let mut fg = Flowgraph::new();
let src = NullSource::<f32>::new();
let head = Head::<f32>::new(1_000_000);
let snk = NullSink::<f32>::new();
connect!(fg, src > head > snk);
let scheduler = FlowScheduler::with_pinned_blocks(vec![
vec![src.id(), head.id()],
vec![snk.id()],
]);
Runtime::with_scheduler(scheduler).run(fg)?;
Benchmark before switching to the Flow Scheduler. Its deterministic mapping can help with some pipelines, but it is not guaranteed to outperform the default scheduler.
WebAssembly
WasmScheduler is selected by Runtime::new() automatically when compiling for wasm32 and starts one web worker by default. Use WasmScheduler::new(n) to run normal blocks on a worker pool.
Some browser APIs, including CPAL’s WebAudio output backend, must be created and owned on the browser main thread. For those cases, use Runtime::with_scheduler(futuresdr::runtime::scheduler::wasm::WasmMainScheduler::new()). This keeps the flowgraph in FutureSDR but runs normal blocks on the UI thread, so it should only be used for light flowgraphs or main-thread-only browser APIs.
Local domains are available on WASM as well. A local domain creates a dedicated web worker and receives the closure that instantiates each local block, mirroring the native local-domain model. FutureSDR uses one worker script path for both scheduler workers and local-domain workers. The default is ./futuresdr-wasm-scheduler-worker.js; configure a different path with futuresdr::runtime::scheduler::wasm::set_worker_script(path) before creating schedulers or local domains.
With Trunk, the simplest setup is to give the Rust output a fixed target name and copy the stock worker template. For a cdylib app:
<link data-trunk rel="rust" data-target-name="futuresdr_app" data-weak-refs data-reference-types />
<link data-trunk rel="copy-file" href="assets/futuresdr-wasm-scheduler-worker.js" />
For a bin target, add a [[bin]] alias named futuresdr_app in Cargo.toml and select it from Trunk:
[[bin]]
name = "futuresdr_app"
path = "src/bin/app.rs"
<link data-trunk rel="rust" data-bin="futuresdr_app" data-weak-refs data-reference-types />
<link data-trunk rel="copy-file" href="assets/futuresdr-wasm-scheduler-worker.js" />
The worker template imports ./futuresdr_app.js, initializes the module/memory from the init message, and dispatches both futuresdr-wasm-scheduler-init and futuresdr-wasm-local-domain-init messages. See examples/wasm-threaded/assets/futuresdr-wasm-scheduler-worker.js for the template. If a flowgraph is started from another web worker, give that worker target the futuresdr_app name/alias and use the same template there as well.
use futuresdr::prelude::*;
let mut fg = Flowgraph::new();
// set up the flowgraph
let fg = Runtime::new().run_async(fg).await?;