Flow Manager
FlowManager (src/config/utils/flow_manager.py) is the runtime scheduler for a pipeline's DAG. It assigns execution timesteps to operations at initialization time and drives frame-by-frame execution.
Initialization
When a Pipeline is built, FlowManager.__init__():
- Runs the forward pass to assign each operation an execution timestep.
- Runs the backward pass to assign each operation a finish timestep.
- Calculates the number of threads required.
- Assigns each operation to a
ThreadObject(a reusable worker thread).
Forward pass (topological sort)
recursive_forward_flow_register() implements a BFS-like topological sort:
- Start with all operations whose inputs are all resolved (initially: data sources and operations with no non-default inputs).
- Assign them timestep
N. - Find all downstream operations connected via non-default edges from this group.
- Recurse with timestep
N+1.
Temporal connections (is_default: true) are excluded from the forward pass — they represent "previous frame" data and do not create ordering constraints within the current frame.
Data sources (operations with no inputs) get special treatment: they are initially placed at timestep 0, then moved to one timestep before their earliest downstream consumer. This ensures the freshest possible data source reading.
Backward pass (finish timesteps)
backward_flow_register() calculates when each operation must be done:
finish_timestep = min(downstream_operation.execution_timestep) - 1
For terminal operations (no downstream non-default connections), finish_timestep == execution_timestep.
Finish timesteps are used by the threaded scheduler to know when to wait for a thread to complete before collecting its output.
Thread allocation
_calculate_required_threads() computes the maximum number of operations that are simultaneously active at any timestep (considering that an operation occupies all timesteps from execution_timestep to finish_timestep inclusive).
Each operation is then assigned to the least-loaded ThreadObject that has no occupancy conflicts in the required timestep range.
Runtime execution
Single-thread pipelines (num_threads == 1)
_run_flow_direct() executes operations sequentially in timestep order with no thread synchronization. This is the common case for linear pipelines.
Multi-thread pipelines (num_threads > 1)
_run_flow_threaded() dispatches each operation to its assigned ThreadObject via set_needs_processing(). After dispatching all operations in a timestep group, it waits for operations whose finish_timestep equals the current timestep via wait_done_processing().
This allows parallel branches to execute concurrently while the main scheduler waits only when necessary.
Input gathering
_gather_operation_inputs(operation) resolves inputs for each operation before execution:
- Single non-default input → returns the current frame's output from the upstream operation directly.
- Single default (temporal) input → returns the previous frame's output (from
previous_operation_outputs). ReturnsNoneon the first frame. - Multiple inputs → returns a dict keyed by
to_portname, mixing current and previous frame outputs. - Data source → returns
None(generates its own data).
Profiling
After every frame, _record_profile_snapshot() captures:
frame_time_ms— total frame wall-clock time- Per-operation
execution_time_ms - Per-timestep
total_time_ms+ slowest operation
Snapshots are stored under _profile_lock and read by get_latest_profile_snapshot() from the SSE publisher thread.
Error handling
TypeErrorwith"None"in the message → silently skip the current frame (operation receivedNoneinput; expected on first frame for temporal connections).- Other exceptions → call
on_operation_errorto record the error for later SSE publication, then raiseValueError. - Thread timeout (5 s) → reset thread state and raise
ValueError.