Skip to content

Unbounded move_group RSS growth correlated to attached-body FCL collision geometry during a long-running pick and place cell #3802

Description

@RazvanApostuWF

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):

  1. FCL BVHModel geometry for attached bodies, rebuilt on collision checks and never freed
    createCollisionGeometry(..., moveit::core::AttachedBody const*, ...)BVHModel::endModel,
    via CollisionEnvFCL::getAttachedBodyObjectsconstructFCLObjectRobot, hit from the Cartesian
    path service, OMPL's StateValidityChecker, and the CheckStartStateCollision planning adapter.
  2. 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.
Image

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):

  1. Planning + executing in a collision scene with meshes attached to base_link.
  2. 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:

  1. BVHModel geometry rebuilt for attached bodies during collision checks and never freed.
  2. 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

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions