Rate & Stopwatch

Two timing utilities exported from horus::prelude::* for use outside the scheduler's tick loop.

use horus::prelude::*;

Rate — Fixed-Frequency Loop

Rate is the HORUS equivalent of ROS2's rclcpp::Rate. Use it for standalone threads that need to run at a target frequency without a scheduler.

use horus::prelude::*;

// Hardware polling thread at 100 Hz
std::thread::spawn(|| {
    let mut rate = Rate::new(100.0);
    loop {
        let reading = read_sensor();
        process(reading);
        rate.sleep(); // Sleeps the remaining fraction of 10ms
    }
});

How It Works

rate.sleep() calculates how much time remains in the current period and sleeps for that duration. If work took longer than the period, sleep is skipped and the next cycle catches up — no drift accumulation.

API

MethodReturnsDescription
Rate::new(hz)RateCreate a rate limiter at hz Hz. Panics if hz <= 0
.sleep()()Sleep for the remainder of the current period
.actual_hz()f64Exponentially smoothed actual frequency
.target_hz()f64The target frequency in Hz
.period()DurationThe target period (1/hz)
.reset()()Reset cycle start to now (use after a long pause)
.is_late()boolWhether the current cycle exceeded the target period

Example: Hardware Driver Thread

use horus::prelude::*;

struct CanBusReader {
    topic: Topic<MotorCommand>,
}

impl CanBusReader {
    fn run(&mut self) {
        let mut rate = Rate::new(500.0); // 500 Hz CAN bus polling
        loop {
            if let Some(frame) = self.read_can_frame() {
                let cmd = MotorCommand::from_can(frame);
                self.topic.send(cmd);
            }

            if rate.is_late() {
                hlog!(warn, "CAN polling late — actual {:.0} Hz", rate.actual_hz());
            }

            rate.sleep();
        }
    }
}

When to Use Rate vs Scheduler

ScenarioUse
Node with tick() in a schedulerScheduler handles timing — don't use Rate
Background thread polling hardwareRate
Standalone process (no scheduler)Rate
One-off timed loop in a testRate

Stopwatch — Elapsed Time

Stopwatch measures elapsed time with lap support. Useful for profiling operations inside nodes.

use horus::prelude::*;

let mut sw = Stopwatch::start();
expensive_computation();
hlog!(debug, "computation took {:.2} ms", sw.elapsed_ms());

API

MethodReturnsDescription
Stopwatch::start()StopwatchCreate and start immediately
.elapsed()DurationTime since start (or last reset)
.elapsed_us()u64Elapsed microseconds
.elapsed_ms()f64Elapsed milliseconds (fractional)
.lap()DurationReturn elapsed and reset (for split timing)
.reset()()Reset start time to now

Example: Profiling a Node

use horus::prelude::*;

struct PlannnerNode {
    scan_sub: Topic<LaserScan>,
    path_pub: Topic<NavPath>,
}

impl Node for PlannnerNode {
    fn name(&self) -> &str { "Planner" }

    fn tick(&mut self) {
        if let Some(scan) = self.scan_sub.recv() {
            let mut sw = Stopwatch::start();

            let path = self.compute_path(&scan);
            let plan_time = sw.lap();

            self.path_pub.send(path);
            let total_time = sw.elapsed();

            hlog!(debug, "plan={:.1}ms total={:.1}ms",
                plan_time.as_secs_f64() * 1000.0,
                (plan_time + total_time).as_secs_f64() * 1000.0);
        }
    }
}

Example: Multi-Lap Benchmarking

let mut sw = Stopwatch::start();

let data = load_model();
let load_time = sw.lap();

let result = run_inference(&data);
let infer_time = sw.lap();

save_result(&result);
let save_time = sw.lap();

hlog!(info, "load={:.1}ms infer={:.1}ms save={:.1}ms",
    load_time.as_secs_f64() * 1000.0,
    infer_time.as_secs_f64() * 1000.0,
    save_time.as_secs_f64() * 1000.0);

See Also

  • Time APIhorus::now(), horus::dt(), horus::elapsed() for scheduler-aware time
  • DurationExt100_u64.hz(), 200_u64.us() ergonomic helpers
  • Scheduler — built-in rate control via .rate() and .tick_rate()