flowchart TB
S(["loop start"]) --> C["call lambda()"]
C --> R{"lambda threw?"}
R -- yes --> X["Mads::running = false"] --> E(["loop exits"])
R -- no --> D{"return value
0 or omitted?"}
D -- yes --> T1["wait time_step
(the ini-configured default)"]
D -- no --> T2["wait the returned
duration instead"]
T1 --> Q{"Mads::running?"}
T2 --> Q
Q -- yes --> C
Q -- no --> E
Timing and scheduling
Every MADS agent is built around a single main loop, paced by one time_step setting. This page covers how that pacing works (including the opt-in high-resolution mode), the cooperative shutdown watchdog that keeps a stuck loop from hanging a process forever, and the separate timecode mechanism agents use to stamp messages with a shared, human-readable time-of-day.
The main loop
Agent::loop(lambda) is the shape every agent-based command (source/filter/sink, logger, feedback, perf_assess, dealer/worker, bridge, image, …) is built around: call lambda once, wait, repeat, until Mads::running becomes false (Ctrl-C, a remote shutdown/restart command, or an uncaught exception from lambda itself).
The wait duration comes from the ini’s time_step (milliseconds) or, for finer control, time_step_us (microseconds — wins over time_step if both are set), read from the calling agent’s own settings section. A time_step of 0 (the default) means “no wait, run at max speed.”
lambda can override the wait for just its next iteration by returning a non-zero duration — this is how mads source/filter/sink implement return_type::retry: return 0ms to fall back to the configured time_step instead of hammering a plugin that just said “try again shortly” at full speed. Returning 0 (or omitting a return) always means “use the configured default,” never “don’t wait at all.”
High-resolution pacing
A plain sleep_for wakes up whenever the OS scheduler gets around to it — typically within a fraction of a millisecond, which is fine for time_step values in the tens-of-milliseconds range or coarser, but can be a meaningful fraction of a very short period (e.g. time_step_us = 500).
Opt into busy-spin pacing with the ini keys high_res_loop = true / spin_margin_us (or the equivalent call, Agent::enable_high_res_loop(), for embedding code): the loop sleeps through most of the remaining interval as usual, then busy-spins on a steady clock for just the last spin_margin_us microseconds (default 200) to land the wake-up to microsecond accuracy.
High-resolution pacing trades CPU time for precision: the busy-spin tail keeps a core fully loaded for however long spin_margin_us lasts, every iteration. The extra CPU cost is roughly spin_margin_us / time_step_us — negligible for a 200us margin on a 100ms loop, but potentially 20-40% of a core for that same 200us margin on a 500us loop. Leave it off (the default) unless a loop’s period is genuinely down in the microsecond range.
The shutdown watchdog
init() installs a small watchdog thread by default (install_watchdog = true; pass false to init()/AgentApp to skip it). It expects the main thread to notice Mads::running == false and unwind out of loop() into shutdown() promptly, which stops the watchdog. If that does not happen within a bounded grace period, the watchdog force-exits the process (std::_Exit, skipping static destructors that might themselves be stuck).
This is a cooperative failsafe, not a timeout on lambda itself: it only fires if the whole process fails to shut down in an orderly way (e.g. something is stuck joining a thread), not if a single loop iteration happens to run long.
See also
broker · source / filter / sink · Message routing and topics