Description
In our robotic-arm pick-and-place production cell, move_group's RSS accumulates at ~600 MB/hr
(~5–6 MB per pick→place cycle). The growth is unbounded and continues indefinitely — the highest
I've seen is 20 GB from move_group alone.
The behavior is work-driven (RSS is flat while the cell idles) and allocator-independent
(neither MALLOC_ARENA_MAX=2 nor TCMALLOC_RELEASE_RATE=10 fixes it on its own).
I ran heaptrack during a production run, and it points to the leak coming from mesh collision
objects that are ATTACHED to a robot link (see backtraces below):
- FCL
BVHModel geometry for attached bodies, rebuilt on collision checks and never freed —
createCollisionGeometry(..., moveit::core::AttachedBody const*, ...) → BVHModel::endModel,
via CollisionEnvFCL::getAttachedBodyObjects ← constructFCLObjectRobot, hit from the Cartesian
path service, OMPL's StateValidityChecker, and the CheckStartStateCollision planning adapter.
- The attached mesh copied into every MoveGroup action result —
shape_msgs::msg::Mesh copy ← CollisionObject copy-ctor ← AttachedCollisionObject ←
MoveGroup_Result::operator= ← MoveGroupMoveAction::executeMoveCallback.
How our automation is set up (probably the most important part):
move_group runs multithreaded FCL collision checking, OMPL (RRTConnect) + a Cartesian path
service, and KDL IK.
- The cell runs a pick-and-place cycle: it attaches a primitive cylinder to the gripper link,
plans and executes a few free, Cartesian, and joint moves, places the cylinder, and detaches it.
- The static environment — trays and racks — are collision meshes (4k–11k triangles each).
- Crucially: we have our own planner wrapper that adds all collision objects as attached
objects on base_link (including the static meshes). So move_group plans while those meshes
are attached to the static base_link.
The fix I found:
- Converting the static meshes from attached-to-
base_link into world objects, and switching
move_group from glibc to tcmalloc, together eliminate the growth. Both changes are required —
neither alone is enough.
We're unsure whether this is a MoveIt bug or a misuse on our end:
- What we suspect is happening:
move_group copies all attached objects before every plan, and
those copies are never freed, so they accumulate. Is that intended?
- Converting the attached meshes to world objects alone does NOT fully fix it. There also seems
to be an allocator issue — freed memory isn't returned to the OS — which only goes away once
move_group uses tcmalloc instead of glibc.
In the figure below:
- Blue (tcmalloc + moveit2_planner fix) —
move_group RSS with both tcmalloc and the meshes
converted to world objects.
- Orange (moveit2_planner fix, no tcmalloc) — RSS with the meshes converted to world objects,
still on glibc.
- Green (tcmalloc, no moveit2_planner fix) — RSS with tcmalloc but the meshes still attached.
ROS Distro
Jazzy
OS and version
Ubuntu 24.04
Source or binary build?
Binary
If binary, which release version?
2.12.3
If source, which branch?
No response
Which RMW are you using?
CycloneDDS
Steps to Reproduce
Honest caveat: I could not reproduce this in the stock moveit_resources_panda demo. It only
shows up in our production stack, which drives everything through our own planner wrapper.
Triggers (in production):
- Planning + executing in a collision scene with meshes attached to
base_link.
- Planning + executing while churning the collision scene (adding/removing objects).
What did NOT reproduce it (stock moveit_resources_panda demo, driven directly via
/apply_planning_scene):
- Attaching a mesh to a link statically and planning in a loop.
- Attaching/detaching a primitive to the gripper each cycle, with a big mesh statically attached.
The trigger seems specific to our full stack — we suspect our planner wrapper, but haven't isolated
it. If anyone has run into this before, please let me know.
Expected behavior
FCL collision geometry is built once and reused/cached across collision checks (and released when the attached body is removed). As a result, RSS should stay bounded.
Actual behavior
move_group RSS is unbounded (grows ~5-6MB/cycle, ~600 MB/hr).
Heaptrack attributes the retained memory to:
- BVHModel geometry rebuilt for attached bodies during collision checks and never freed.
- Attached-object meshes copied into MoveGroup action results.
Growth is live (can't be stopped by tcmalloc or MALLOC_ARENA_MAX=2).
Backtrace or Console output
Clean heaptrack (launched under heaptrack, tcmalloc removed → valid accounting).
Capture summary: 251.6M allocations (~70k/s), peak heap 620 MB, ~548 MB still-allocated at
snapshot (partial capture — process not stopped cleanly, so this includes live working set; the
two accumulating sites below are the relevant ones).
Family 1 — attached-body FCL BVH, rebuilt on collision checks, retained (~270 MB):
fcl::BVHModel<OBBRSSd>::endModel()
<- collision_detection::createCollisionGeometry(shapes::ShapeConstPtr const&,
moveit::core::AttachedBody const*, int)
<- collision_detection::CollisionEnvFCL::getAttachedBodyObjects(...)
<- collision_detection::CollisionEnvFCL::constructFCLObjectRobot(...)
<- collision_detection::CollisionEnvFCL::checkRobotCollisionHelper(...)
<- planning_scene::PlanningScene::checkCollision / isStateColliding
<- (a) CartesianInterpolator::computeCartesianPath (Cartesian path service)
(b) ompl_interface::StateValidityChecker::isValid (RRTConnect / goal sampling)
(c) default_planning_request_adapters::CheckStartStateCollision::adapt
<- PlanningPipeline::generatePlan <- MoveGroupMoveAction::executeMoveCallback
Family 2 — attached-object mesh copied into every MoveGroup action result, retained (~220 MB):
shape_msgs::msg::Mesh copy (std::__do_uninit_copy)
<- moveit_msgs::msg::CollisionObject::CollisionObject(CollisionObject const&)
<- moveit_msgs::msg::AttachedCollisionObject (vector copy)
<- moveit_msgs::action::MoveGroup_Result::operator=
<- move_group::MoveGroupMoveAction::executeMoveCallback
Description
In our robotic-arm pick-and-place production cell,
move_group's RSS accumulates at ~600 MB/hr(~5–6 MB per pick→place cycle). The growth is unbounded and continues indefinitely — the highest
I've seen is 20 GB from
move_groupalone.The behavior is work-driven (RSS is flat while the cell idles) and allocator-independent
(neither
MALLOC_ARENA_MAX=2norTCMALLOC_RELEASE_RATE=10fixes it on its own).I ran heaptrack during a production run, and it points to the leak coming from mesh collision
objects that are ATTACHED to a robot link (see backtraces below):
BVHModelgeometry for attached bodies, rebuilt on collision checks and never freed —createCollisionGeometry(..., moveit::core::AttachedBody const*, ...)→BVHModel::endModel,via
CollisionEnvFCL::getAttachedBodyObjects←constructFCLObjectRobot, hit from the Cartesianpath service, OMPL's
StateValidityChecker, and theCheckStartStateCollisionplanning adapter.shape_msgs::msg::Meshcopy ←CollisionObjectcopy-ctor ←AttachedCollisionObject←MoveGroup_Result::operator=←MoveGroupMoveAction::executeMoveCallback.How our automation is set up (probably the most important part):
move_groupruns multithreaded FCL collision checking, OMPL (RRTConnect) + a Cartesian pathservice, and KDL IK.
plans and executes a few free, Cartesian, and joint moves, places the cylinder, and detaches it.
objects on
base_link(including the static meshes). Somove_groupplans while those meshesare attached to the static
base_link.The fix I found:
base_linkinto world objects, and switchingmove_groupfrom glibc to tcmalloc, together eliminate the growth. Both changes are required —neither alone is enough.
We're unsure whether this is a MoveIt bug or a misuse on our end:
move_groupcopies all attached objects before every plan, andthose copies are never freed, so they accumulate. Is that intended?
to be an allocator issue — freed memory isn't returned to the OS — which only goes away once
move_groupuses tcmalloc instead of glibc.In the figure below:
move_groupRSS with both tcmalloc and the meshesconverted to world objects.
still on glibc.
ROS Distro
Jazzy
OS and version
Ubuntu 24.04
Source or binary build?
Binary
If binary, which release version?
2.12.3
If source, which branch?
No response
Which RMW are you using?
CycloneDDS
Steps to Reproduce
Honest caveat: I could not reproduce this in the stock
moveit_resources_pandademo. It onlyshows up in our production stack, which drives everything through our own planner wrapper.
Triggers (in production):
base_link.What did NOT reproduce it (stock
moveit_resources_pandademo, driven directly via/apply_planning_scene):The trigger seems specific to our full stack — we suspect our planner wrapper, but haven't isolated
it. If anyone has run into this before, please let me know.
Expected behavior
FCL collision geometry is built once and reused/cached across collision checks (and released when the attached body is removed). As a result, RSS should stay bounded.
Actual behavior
move_group RSS is unbounded (grows ~5-6MB/cycle, ~600 MB/hr).
Heaptrack attributes the retained memory to:
Growth is live (can't be stopped by tcmalloc or MALLOC_ARENA_MAX=2).
Backtrace or Console output
Clean heaptrack (launched under heaptrack, tcmalloc removed → valid accounting).
Capture summary: 251.6M allocations (~70k/s), peak heap 620 MB, ~548 MB still-allocated at
snapshot (partial capture — process not stopped cleanly, so this includes live working set; the
two accumulating sites below are the relevant ones).
Family 1 — attached-body FCL BVH, rebuilt on collision checks, retained (~270 MB):
Family 2 — attached-object mesh copied into every MoveGroup action result, retained (~220 MB):