Skip to content

Conversation

@skyegalaxy
Copy link
Member

@skyegalaxy skyegalaxy commented Jan 16, 2026

Description

In the long actions ram usage section of the benchmarks iRobot shared back in September 2025, there was a memory leak identified in rolling for both the EventsExecutor implementations. I did some deeper profiling and found that it was the result of expired goal results never being cleared out.

This PR introduces a small change which ensures execute_check_expire_goals() is actually called within the expire_timer_ callback for actions. I've also added a test to rclcpp_action/test_server.cpp which validates that expired goals are properly cleared out if the events executor is used.

Because the events executors have a slightly different execution path for waitables than the waitset based executors (we call take_data_by_entity_id() directly instead of take_data()), is_ready() seems to be the only function capable of emitting an Expired entity if the expire_timer_ was triggered, but is_ready() is currently coupled with the use of waitsets.

Followup to #2699.
Alternative to #3012

Is this user-facing behavior change?

Properly fixes a memory leak in actions where goal handles are stored indefinitely and never expired if using the EventsExecutor.

Did you use Generative AI?

No

Additional Information

EventsExecutor heaptrack allocation profile of irobot benchmark using 1MB payload, before:
image

after:
image

Signed-off-by: Skyler Medeiros <skye.galaxy@proton.me>
Signed-off-by: Skyler Medeiros <skye.galaxy@proton.me>
Signed-off-by: Skyler Medeiros <skye.galaxy@proton.me>
Copy link
Member

@mjcarroll mjcarroll left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks! Overall, looks pretty good and solves the memory leak.

I have a few thoughts/concerns that we should take a look at before merging.

Signed-off-by: Skyler Medeiros <skye.galaxy@proton.me>
Signed-off-by: Skyler Medeiros <skye.galaxy@proton.me>
Signed-off-by: Skyler Medeiros <skye.galaxy@proton.me>
Signed-off-by: Skyler Medeiros <skye.galaxy@proton.me>
Copy link
Collaborator

@fujitatomoya fujitatomoya left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

the other major downside for Non EventExecutors is that it now calls execute_check_expired_goals twice on each expiration? once from the timer callback (which is added by this PR) and the another from the waitable's execute() call?

@jmachowinski
Copy link
Collaborator

Hm, we could add 'rcl_action_server_set_expired_callback' and handle this in the rcl layer to only call the callback if the timer expires etc.
Also the timer API feels off to me in regard to the events system. The timers handle different from the other entities, and the special case handling in the executors is odd. I wonder if we could modify the timer API in general to improve the situation...

Signed-off-by: Skyler Medeiros <skye.galaxy@proton.me>
@skyegalaxy
Copy link
Member Author

Something else to consider: If this leak's still happening for C++ based EventsExecutors, the coupling of goal expiration to the waitset implies that this could also be happening with the rclpy EventsExecutor. I'm not finding any analogous changes on the rclpy side to expire goals from a timer that isn't still using the waitset to check if it's expired.

Also, I've verified that this leak is also present in Kilted and whatever solution we arrive at will ultimately need to be backported.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

bug Something isn't working

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants