Skip to content

Add AlignedTimer and AlignedWallTimer for start-time and phase-aligned timers - #3270

Open
thomasmoore16 wants to merge 5 commits into
ros2:rollingfrom
tcm0116:timer-initial-call-time
Open

thomasmoore16 wants to merge 5 commits into
ros2:rollingfrom
tcm0116:timer-initial-call-time

Conversation

@thomasmoore16

@thomasmoore16 thomasmoore16 commented Sep 14, 2026 •

Copy link
Copy Markdown

rclcpp: Add AlignedTimer and AlignedWallTimer for start-time and phase-aligned timers

Branch: timer-initial-call-time (this repo: ros2/rclcpp)
Depends on: ros2/rcl#1335 — "Allow specifying an initial timer trigger time" (adds rcl_timer_init_with_start_time and preserves phase alignment in rcl_timer_reset). This PR requires that change to be available and should land after it (note: the single-repo Rpr__rclcpp buildfarm job will fail until ros2/rcl#1335 is merged and synced into Rolling).

Summary

Building on rcl_timer_init_with_start_time in ros2/rcl#1335 and incorporating feedback from the Client Library Working Group, this adds dedicated AlignedTimer and AlignedWallTimer classes (along with create_aligned_timer / create_aligned_wall_timer helpers) that let callers either specify an explicit initial trigger time or align periodic timers to (period, phase) boundaries instead of always starting at now() + period.

Unlike GenericTimer and WallTimer (whose reset() re-anchors the schedule to now() + period), AlignedTimer and AlignedWallTimer preserve their schedule phase across cancel() / reset() cycles.

Changes

  • rclcpp::AlignedTimer / rclcpp::AlignedWallTimer: Dedicated timer subclasses initialized via rcl_timer_init_with_start_time, supporting both:
    • (clock, initial_call_time, period, callback, context, autostart) (and the wall-clock equivalent without clock), validating that initial_call_time's clock type matches the timer's clock, and
    • (clock, period, phase, callback, context, autostart) (and the wall-clock equivalent), computing the initial phase-aligned trigger time automatically.
  • rclcpp::compute_phase_aligned_time(clock, interval, phase): Computes the smallest time >= clock.now() of the form k * interval + phase, allowing independent nodes/processes sharing a synchronized clock to align periodic timers without exchanging a start timestamp out-of-band.
  • rclcpp::create_aligned_timer(...) / rclcpp::create_aligned_wall_timer(...) free functions in create_timer.hpp and matching rclcpp::Node::create_aligned_timer(...) / rclcpp::Node::create_aligned_wall_timer(...) member functions.
  • Tests in test_timer.cpp and test_create_timer.cpp covering AlignedTimer, AlignedWallTimer, create_aligned_timer, create_aligned_wall_timer, compute_phase_aligned_time, clock-type validation, autostart=false, and phase preservation across reset() (both when not yet due and when catching up from overdue).

Compatibility

Purely additive. Existing TimerBase / GenericTimer / WallTimer / create_timer / create_wall_timer public signatures and behavior are unchanged.

Testing

Built and tested against rolling in a ros:rolling-ros-base container alongside ros2/rcl#1335 (colcon build + colcon test across rcl and rclcpp timer suites and all ament_lint_auto linters: uncrustify, cpplint, cppcheck, copyright, lint_cmake, xmllint).

Landing order

Depends on ros2/rcl#1335 (rcl_timer_init_with_start_time). Please test together via ci.ros2.org and land ros2/rcl#1335 first.

@jmachowinski

Copy link
Copy Markdown
Collaborator

This breaks the Events and EventsCBG executor for sure.

@jmachowinski

Copy link
Copy Markdown
Collaborator

Hm, this will actually work, as I use rcl_timer_get_next_call_time in the timer_manager....

@thomasmoore16

Copy link
Copy Markdown
Author

@jmachowinski - the essence of this PR is very simple. The actual meat of the change is the modification of the following line in rcl to allow for the option of specifying the initial value of next_call_time:

- atomic_init(&impl.next_call_time, now + period);
+ atomic_init(&impl.next_call_time, initial_call_time);

The rest of the change to rcl and rclpp is adding the additional overloads to enable specifying the initial_call_time. The internal function of the timer is otherwise unchanged.

@thomasmoore16

Copy link
Copy Markdown
Author

Pushed a follow-up commit (5a1e0e4) adding clock-type validation for the new initial_call_time overloads — while looking into this thread I noticed WallTimer/create_wall_timer accepted an initial_call_time of any clock type without checking it against the timer's own (always-steady) clock, which would silently produce a garbage next_call_time. Not related to the executor question above, but seemed worth fixing here.

@jmachowinski

Copy link
Copy Markdown
Collaborator

I have multiple points were I think the design needs some rework.

The Interface :
I don't like the interface of giving an exact point in time. If the goal is to smear the timers over time, a interface with an interval and an offset in that interval might be more useful.
The start time would then be (time since 1970) + interval * ((time since 1970) % interval) + offset

Reset uses internally now and moves the timer call point around.
I think we should introduce a version here that accepts an absolute time point and the interval offset time point as well. With this we could sync timers by external means.

For the rcl change, would it not be easier to extend the reset API instead of a new init version ? Calling the existing init wit start false, and afterwards reset with the parameters would also do the trick.

The autostart parameter does not make any sense with the current init interface.
I would also not call it timer_initX, but instead something like timer_init_with_start_time etc

@thomasmoore16

Copy link
Copy Markdown
Author

The goal is to be able to create a set of timers across a set of ROS2 nodes which fire relative to each other in a deterministic manner. With the current use of now() when determining the start time of each timer, the start time of the set of timers in a distributed system is completely non-deterministic, leading to inconsistent timing issues from run to run of the system.

The concept of an interval and offset is precisely how we're determining the start time to use for the timer. However, I wanted the interface to be more generic in order to allow for other use cases. We can certainly add both interfaces.

I hadn't considered the reset() function's use of now(), which would certainly defeat the point of this change if an application is starting and stopping the timer. I agree that we should extend reset() for this use case, but I'm not sure that I agree that we should require creating the timer with autostart disabled and then having to call reset(). It seems like an unnecessary 2 step process.

@thomasmoore16

Copy link
Copy Markdown
Author

After thinking about this some more, I've decided to add a resume() method. It works similarly to reset(), except it maintains the phase alignment of the timer. This will allow for delay starting or pausing phase-aligned timers, which makes autostart=false practical for timers with a specified start time. In order to allow for creation of phase-aligned timers, a helper method has been added for computing the start time to be passed to the timer creation method.

@jmachowinski

Copy link
Copy Markdown
Collaborator

I am not too sure about the API.
I fear it it is now too simple to mess up your code by accidentally calling reset instead of resume.

What do you think about adding dedicated new Timer classes and change how reset works on them. This would have the advantage that the API is consistent between timers, and it is more in line with how other timers are modeled in rclcpp. E.g. WallTimer vs GenericTimer

@jmachowinski

Copy link
Copy Markdown
Collaborator

We discussed this feature in the client library working group, and the general consensus was that we are all in favor of it.
But we also all agreed, that we want to have this as a separate class rather than being controlled by flags.
@thomasmoore-torc are you still willing to implement this, or should be give this out to someone else ?

Thomas Moore and others added 4 commits October 31, 2026 16:45
Signed-off-by: Thomas Moore <thomas.moore@torc.ai>
Signed-off-by: Thomas Moore <thomas.moore@torc.ai>
WallTimer (and create_wall_timer) always construct their underlying
Clock as RCL_STEADY_TIME, but the new initial_call_time overloads
accepted a Time of any clock type without checking it. Passing e.g. a
ROS-time Time into create_wall_timer silently produced a garbage
next_call_time, since rcl treats the value as a bare nanosecond count
with no cross-clock conversion.

TimerBase's initial_call_time constructor now throws std::runtime_error
if initial_call_time's clock type doesn't match the timer's clock,
consistent with the existing clock-type-mismatch checks in
Time::operator- and Clock::sleep_until. The check is skipped when the
clock itself is uninitialized, leaving that case to the existing
rcl-level validation.

Signed-off-by: Thomas Moore <thomas.moore@torc.ai>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Adds rclcpp::TimerBase::resume(), a thin wrapper over the new
rcl_timer_resume() (see the companion rcl change): unlike reset(), it
preserves the timer's existing schedule phase, only catching up if it
is overdue rather than unconditionally recomputing from now(). This is
what makes autostart=false combined with an explicit initial_call_time
actually useful: a timer can be created paused with a specific
phase-anchored schedule and later resumed without losing it, which
reset() cannot do.

Also fixes a gap found while writing this: Node::create_timer(
initial_call_time, ...) had no way to pass autostart at all, unlike
its create_wall_timer sibling overload, even though the underlying
free function already supported it.

Adds rclcpp::compute_phase_aligned_time(clock, interval, phase): a
small utility to compute the smallest instant >= now() of the form
k * interval + phase. This lets independent nodes/processes sharing a
synchronized clock agree on the same aligned timer start instants
without exchanging an explicit time out-of-band.

Renamed the internal call from rcl_timer_init3 to
rcl_timer_init_with_start_time to match the companion rcl rename.

Signed-off-by: Thomas Moore <thomas.moore@torc.ai>
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@thomasmoore16
thomasmoore16 force-pushed the timer-initial-call-time branch from 8e7fa08 to 29589a7 Compare October 31, 2026 20:46
@tcm0116

tcm0116 commented Oct 31, 2026

Copy link
Copy Markdown

@jmachowinski - I have made the requested changes as well as eliminated the rcl_timer_resume() function in ros2/rcl#1335 to be consistent with the rclcpp implementation.

Note that I am no longer with my previous company, so I'm shifting over to using my personal account to wrap up these PRs. Feel free to re-assign the PR to this account.

Signed-off-by: Thomas Moore <tcm0116@gmail.com>
@tcm0116
tcm0116 force-pushed the timer-initial-call-time branch from 29589a7 to 182b9a3 Compare October 31, 2026 22:05
@thomasmoore16 thomasmoore16 changed the title Allow specifying an initial timer trigger time in create_timer/create_wall_timer Add AlignedTimer and AlignedWallTimer for start-time and phase-aligned timers Oct 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants