futuresdr 0.7.0

An Experimental Async SDR Runtime for Heterogeneous Architectures.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
use futures::future::Future;

use crate::runtime::BlockId;
use crate::runtime::BlockMessage;
use crate::runtime::Edge;
use crate::runtime::Error;
use crate::runtime::FlowgraphMessage;
use crate::runtime::block::Block;
use crate::runtime::block_inbox::BlockEndpoint;
use crate::runtime::channel::mpsc::Sender;
use crate::runtime::channel::oneshot;
use crate::runtime::local_domain::LocalDomainInbox;
use crate::runtime::scheduler::Task;

/// Internal normal-domain block object.
pub(crate) type NormalBlock = Box<dyn Block>;

/// Normal-domain blocks passed from the flowgraph to a scheduler.
pub(crate) type NormalBlocks = Vec<NormalBlock>;

/// Logical topology visible to a scheduling domain.
#[derive(Debug, Clone)]
pub struct DomainTopology {
    pub(crate) blocks: Vec<BlockId>,
    pub(crate) stream_edges: Vec<Edge>,
    pub(crate) message_edges: Vec<Edge>,
}

impl DomainTopology {
    /// Create domain topology metadata.
    pub(crate) fn new(
        blocks: Vec<BlockId>,
        stream_edges: Vec<Edge>,
        message_edges: Vec<Edge>,
    ) -> Self {
        Self {
            blocks,
            stream_edges,
            message_edges,
        }
    }

    /// Blocks assigned to this scheduling domain.
    pub fn blocks(&self) -> &[BlockId] {
        &self.blocks
    }

    /// Logical stream edges relevant for this domain.
    pub fn stream_edges(&self) -> &[Edge] {
        &self.stream_edges
    }

    /// Logical message edges relevant for this domain.
    pub fn message_edges(&self) -> &[Edge] {
        &self.message_edges
    }
}

/// Specification for the implicit normal send-capable scheduling domain.
pub struct NormalDomainSpec {
    pub(crate) blocks: NormalBlocks,
    pub(crate) topology: DomainTopology,
    pub(crate) main_channel: Sender<FlowgraphMessage>,
}

impl NormalDomainSpec {
    /// Create a normal-domain specification.
    pub(crate) fn new(
        blocks: NormalBlocks,
        topology: DomainTopology,
        main_channel: Sender<FlowgraphMessage>,
    ) -> Self {
        Self {
            blocks,
            topology,
            main_channel,
        }
    }

    /// Inspect the topology metadata that accompanies this normal domain.
    pub fn topology(&self) -> &DomainTopology {
        &self.topology
    }

    /// Iterate over block ids assigned to this normal domain.
    pub fn blocks(&self) -> impl Iterator<Item = BlockId> + '_ {
        self.blocks.iter().map(|block| block.id())
    }

    /// Take one normal block from this domain for spawning.
    pub fn take_block(&mut self, block_id: BlockId) -> Result<RunnableBlock, Error> {
        let pos = self
            .blocks
            .iter()
            .position(|block| block.id() == block_id)
            .ok_or(Error::InvalidBlock(block_id))?;
        let block = self.blocks.swap_remove(pos);
        let stop = BlockStop {
            block_id,
            endpoint: block.inbox(),
        };
        Ok(RunnableBlock {
            block,
            main_channel: self.main_channel.clone(),
            stop,
        })
    }
}

/// Stop handle for one running normal-domain block.
#[derive(Clone)]
pub struct BlockStop {
    block_id: BlockId,
    endpoint: BlockEndpoint,
}

impl BlockStop {
    /// Get the block id.
    pub fn id(&self) -> BlockId {
        self.block_id
    }

    /// Request this block to terminate.
    pub async fn stop(&self) -> Result<(), Error> {
        self.endpoint.send(BlockMessage::Terminate).await
    }
}

/// Opaque normal-domain block object that can be spawned by a [`Scheduler`].
pub struct RunnableBlock {
    block: NormalBlock,
    main_channel: Sender<FlowgraphMessage>,
    stop: BlockStop,
}

impl RunnableBlock {
    /// Get the block id.
    pub fn id(&self) -> BlockId {
        self.block.id()
    }

    /// Get a handle that can request this block to stop after it is spawned.
    pub fn stop_handle(&self) -> BlockStop {
        self.stop.clone()
    }

    /// Run this normal-domain block to completion and return its stopped state.
    pub async fn run(self) -> StoppedBlock {
        let Self {
            mut block,
            main_channel,
            ..
        } = self;
        block.run(main_channel).await;
        StoppedBlock { block }
    }
}

/// Opaque stopped normal-domain block state that must be restored to its domain.
pub struct StoppedBlock {
    block: NormalBlock,
}

impl StoppedBlock {
    /// Get the block id.
    pub fn id(&self) -> BlockId {
        self.block.id()
    }

    pub(crate) fn into_block(self) -> NormalBlock {
        self.block
    }
}

/// Prepared local scheduling domain.
pub(crate) struct PreparedLocalDomain {
    pub(crate) domain_id: usize,
    pub(crate) inbox: LocalDomainInbox,
    pub(crate) slots: Vec<(BlockId, usize)>,
    pub(crate) topology: DomainTopology,
    pub(crate) main_channel: Sender<FlowgraphMessage>,
}

impl PreparedLocalDomain {
    /// Create a local-domain specification.
    pub(crate) fn new(
        domain_id: usize,
        inbox: LocalDomainInbox,
        slots: Vec<(BlockId, usize)>,
        topology: DomainTopology,
        main_channel: Sender<FlowgraphMessage>,
    ) -> Self {
        Self {
            domain_id,
            inbox,
            slots,
            topology,
            main_channel,
        }
    }

    /// Start this local domain using its existing inbox.
    pub(crate) fn start(self) -> Result<LocalRunningDomain, Error> {
        let completion =
            self.inbox
                .start_run(self.domain_id, self.slots, self.topology, self.main_channel)?;
        Ok(LocalRunningDomain::new(self.inbox, completion))
    }
}

/// Running normal-domain state returned by a scheduler.
pub struct NormalRunningDomain {
    blocks: Vec<(Task<StoppedBlock>, BlockStop)>,
    stop_requested: bool,
}

impl NormalRunningDomain {
    /// Create a running normal domain from block task and stop-handle pairs.
    pub fn new(blocks: Vec<(Task<StoppedBlock>, BlockStop)>) -> Self {
        Self {
            blocks,
            stop_requested: false,
        }
    }

    /// Request the normal domain to stop.
    pub(crate) async fn stop(&mut self) {
        if self.stop_requested {
            return;
        }
        self.stop_requested = true;

        for (_, stop) in &self.blocks {
            if let Err(e) = stop.stop().await {
                debug!(
                    "normal domain tried to terminate block {:?}: {e}",
                    stop.id()
                );
            }
        }
    }

    /// Await all normal-domain block tasks and return their blocks.
    pub(crate) async fn join(self) -> NormalBlocks {
        let mut stopped = Vec::with_capacity(self.blocks.len());
        for (task, _) in self.blocks {
            stopped.push(task.await.into_block());
        }
        stopped
    }
}

/// Running local-domain state returned when an existing local domain is activated.
pub(crate) struct LocalRunningDomain {
    inbox: LocalDomainInbox,
    completion: oneshot::Receiver<Result<(), Error>>,
}

impl LocalRunningDomain {
    /// Create a running local domain from its completion receiver.
    pub(crate) fn new(
        inbox: LocalDomainInbox,
        completion: oneshot::Receiver<Result<(), Error>>,
    ) -> Self {
        Self { inbox, completion }
    }

    /// Request the local-domain run loop to stop.
    pub(crate) async fn stop(&mut self) -> Result<(), Error> {
        self.inbox.stop_run().await
    }

    /// Await the local-domain run loop.
    pub(crate) async fn join(self) -> Result<(), Error> {
        self.completion
            .await
            .map_err(|_| Error::RuntimeError("local domain task canceled".to_string()))??;
        Ok(())
    }
}

/// Scheduler trait for runtime work and the implicit normal scheduling domain.
///
/// A scheduler decides how normal block tasks and detached sendable async tasks
/// are run. Local-domain execution resources are created with the flowgraph's
/// local domains and are orchestrated by a [`LocalScheduler`](super::LocalScheduler)
/// inside the local-domain thread/worker.
///
/// Scheduler values are required to be [`Send`] on native targets, where the
/// runtime supervisor may run on the scheduler. On WASM, the supervisor stays
/// on its originating thread so schedulers may own thread-local browser state.
pub trait Scheduler: Clone + 'static
where
    #[cfg(not(target_arch = "wasm32"))]
    Self: Send,
{
    /// Start the implicit normal send-capable scheduling domain.
    fn start_normal_domain(&self, spec: NormalDomainSpec) -> Result<NormalRunningDomain, Error>;

    /// Spawn an independent sendable async task on this scheduler.
    fn spawn<T: Send + 'static>(&self, future: impl Future<Output = T> + Send + 'static)
    -> Task<T>;
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::runtime::BlockId;
    use crate::runtime::BlockMessage;
    use crate::runtime::FlowgraphMessage;
    use crate::runtime::PortIndex;
    use crate::runtime::PortName;
    use crate::runtime::Result;
    use crate::runtime::block::BlockObject;
    use crate::runtime::block_inbox::BlockInbox;
    use crate::runtime::buffer::DynBufferReader;
    use crate::runtime::buffer::DynBufferWriter;
    use crate::runtime::channel::mpsc::Sender;

    struct TestBlock {
        id: BlockId,
        endpoint: BlockEndpoint,
    }

    impl BlockObject for TestBlock {
        fn inbox(&self) -> BlockEndpoint {
            self.endpoint.clone()
        }

        fn id(&self) -> BlockId {
            self.id
        }

        fn type_name(&self) -> &str {
            "TestBlock"
        }

        fn instance_name(&self) -> Option<&str> {
            None
        }

        fn is_blocking(&self) -> bool {
            false
        }

        fn stream_input_at(
            &mut self,
            _index: PortIndex,
        ) -> Option<(PortName, &mut dyn DynBufferReader)> {
            None
        }

        fn stream_output_at(
            &mut self,
            _index: PortIndex,
        ) -> Option<(PortName, &mut dyn DynBufferWriter)> {
            None
        }

        fn message_inputs(&self) -> &'static [&'static str] {
            &[]
        }

        fn message_outputs(&self) -> &'static [&'static str] {
            &[]
        }

        fn connect_message(
            &mut self,
            _src_port: PortIndex,
            _dst: BlockEndpoint,
            _dst_port: PortIndex,
        ) -> Result<(), Error> {
            Ok(())
        }
    }

    #[async_trait::async_trait]
    impl Block for TestBlock {
        async fn run(&mut self, _main_inbox: Sender<FlowgraphMessage>) {}
    }

    #[test]
    fn normal_domain_stop_sends_terminate_to_blocks_once() {
        let block_id = BlockId(0);
        let (inbox, mut reader) = BlockInbox::pair(4);
        let endpoint = BlockEndpoint::Direct(inbox);
        let task_endpoint = endpoint.clone();
        let (_runnable, task) = async_task::spawn(
            async move {
                StoppedBlock {
                    block: Box::new(TestBlock {
                        id: block_id,
                        endpoint: task_endpoint,
                    }),
                }
            },
            |_| {},
        );
        let stop = BlockStop { block_id, endpoint };
        let mut domain = NormalRunningDomain::new(vec![(task, stop)]);

        crate::runtime::block_on(domain.stop());
        assert!(matches!(reader.try_recv(), Some(BlockMessage::Terminate)));

        crate::runtime::block_on(domain.stop());
        assert!(reader.try_recv().is_none());
    }
}