Skip to main content

Network sources

A network-driven graph starts frames from UDP or TCP packets instead of a counter. Dedicated receiver threads ingest the incoming packet stream, group packets into frames by id, assign each frame to a slot, and dispatch consumer tasks as packets arrive — a consumer does not wait for its whole frame. This packet-level overlap is the mechanism the MIMO benchmark measures (bench/mimo-bench/README.md).

Configuring the source

In Python, app.network(**cfg) sets the configuration; in JSON it is the top-level network_config block. From the MIMO benchmark graph, generated by bench/mimo-bench/tomii/build_graph.py (trimmed):

"network_config": {
"socket_type": "udp",
"num_sockets": "antennas",
"packet_length": "packet_length",
"frame_packets": "packets_per_frame",
"buffer_depth": 2000,
"address": "server_address",
"start_port": "base_port",
"extract_packet_func": "process_packet",
"id_function": "get_frame_id",
"index_function": {
"function": "get_packet_slot",
"args": [ { "type": "$ref", "value": "config" } ]
}
}

Fields (from NetworkConfigJson in tomii-core/src/json_structs.rs):

FieldMeaning
socket_type"udp" or "tcp"
num_socketsSockets to open (factor: literal or variable name)
packet_lengthBytes per packet
frame_packetsPackets that make up one frame
buffer_depthReceive buffer depth per socket (default 128)
address, start_portBind address; socket k uses start_port + k
extract_packet_funcPlugin function: raw bytes → packet value
id_functionPlugin function: packet → frame id
index_functionPlugin function: packet → consuming node-instance index (required)

Factor-valued fields accept variable names, so socket counts and packet sizes can come from graph initializations — the MIMO graph derives all of them from a config object.

From Python, the same block is built with app.network(...), passing Var objects where the JSON uses variable names and a tm.IndexFunc(function, args) for index_function. The serializer rejects a config without index_function, since the runtime requires it (tomii/_serialize.py).

How a packet becomes a task

  1. A receiver thread reads a packet and calls extract_packet_func.
  2. id_function returns the frame id. The first packet of a new frame claims a slot; later packets of the same frame route to that slot.
  3. index_function returns which instance of the network node this packet is. It receives the packet plus its configured args and must return a usize (tomii-core/src/runtime/packet_processing.rs).
  4. Nodes that consume the packet are dispatched immediately.

Consuming packets: $network

Nodes reference packet data with a $res dependency whose predecessor is the reserved name $network. From the MIMO graph:

{
"name": "fft",
"factor": "total_ul_symbols",
"function": "fft_op",
"args": [
{ "type": "$res", "predecessor": { "name": "$network", "indexes": "0" } },
{ "type": "$ref", "value": "config" }
]
}

Instance i of fft runs when packet index i of the current frame has arrived. $network also works inside condition args, which is how the MIMO graph routes pilot symbols to csi and data symbols to fft.

Receiver threads

Set receiver_threads in run() (CLI --receiver-threads, default 0). Network packets are handled by these dedicated threads, not the task scheduler. A network graph needs at least one receiver thread.

Out-of-window packets

Only frames within the admission window (completed frames + slots) can occupy a slot. Packets for frames beyond the window park in a bounded pending_frames buffer and re-inject when the window advances. The buffer holds at most frame_packets × slots packets; on overflow the furthest-out frame is dropped whole, so a burst degrades the run instead of wedging it on a permanently incomplete frame (tomii-core/src/runtime/shared_data.rs, tomii-core/src/runtime/packet_processing.rs). Dropped frames are counted and reported.

A complete example

The 4×4 MIMO uplink benchmark under bench/mimo-bench/ is the reference network workload: an Agora sender emits frames over UDP, and per-packet FFT tasks dispatch as packets arrive. Start the Tomii receiver first, then the sender (bench/mimo-bench/README.md).