Skip to content

Drop the redundant canTransform in getShapeTransformCache - #3803

Open
agentperfopt wants to merge 1 commit into
moveit:mainfrom
agentperfopt:perfopt/moveit2-768
Open

Drop the redundant canTransform in getShapeTransformCache#3803
agentperfopt wants to merge 1 commit into
moveit:mainfrom
agentperfopt:perfopt/moveit2-768

Conversation

@agentperfopt

Copy link
Copy Markdown

Description

getShapeTransformCache rebuilds on every octomap/sensor update and walks the TF tree twice per shape-bearing frame: canTransform (a check) then lookupTransform (the actual compose). lookupTransform already composes the transform and throws tf2::TransformException if it's unavailable, which the function's existing try/catch already handles — so the check is redoing work the compose call does anyway. This drops it and calls lookupTransform directly for the link, attached-body, and collision-body cases.

Results

Real tf2::BufferCore, cache build timed directly, us/call:

collision shapes current this change speedup
4 2.06 1.69 1.22x
8 (original workload) 6.58 4.41 1.49x
16 11.69 7.26 1.61x
32 32.04 20.68 1.55x

Scales with both shape count and TF-tree depth (1.53x at depth 0 to 1.56x at depth 16), so it's a bigger win on robots with more collision shapes and deeper transform trees — which is also where the cache build is slowest to begin with.

Correctness

Cache output is bit-identical: lookupTransform returns the same transform regardless of whether canTransform was called first, so the checksum over all cached shape transforms matches exactly.

Two behavioral differences worth being precise about rather than glossing over:

  1. canTransform(..., shape_transform_cache_lookup_wait_time_) also waits up to that duration for a not-yet-available transform. That duration is a real, user-facing ROS parameter (<robot_description>_planning.shape_transform_cache_lookup_wait_time, defaulting to 0.05s) — dropping the call makes that parameter a no-op, since this was its only reader. Behavior is identical whenever the transform is already available at target_time (the normal case); if one is momentarily late, the current code waits up to 50ms for it, this throws right away and the try/catch returns false for that update, and the next sensor update tries again.

  2. Something I noticed while making this change, not something the profiling data flagged: in the current code, each canTransform guards only its own frame's if block, so if one link's transform isn't available, that link's shapes are skipped but every other link, attached body, and collision handle still gets processed normally. After this change, an unavailable transform for any one of them throws out of the whole function immediately, through the outer try/catch, skipping everything after it in the same call, not just the one shape that failed. Both still return false for that update either way, so the round-trip cadence is the same, but the current code's "skip just the one late frame, still fill in what's available" behavior becomes "skip everything remaining in this pass" here. I don't have a moveit2/tf2 build in this environment to characterize how often that actually matters in practice (it depends on how frequently individual frames go briefly stale relative to the whole batch), so flagging it precisely rather than asserting it's inconsequential.

Checklist

  • Required by CI: code is auto-formatted using clang-format — not available in this environment, matched the surrounding style by hand
  • No user-facing API change beyond the wait-time parameter becoming inert (noted above; could go in MIGRATION.md if this is taken)
  • No new tests included — no moveit2/tf2 build available here to write and run one; the numbers above are from a separate extraction run against a real tf2::BufferCore

Found by an automated tool that profiles merged PRs for headroom the PR itself didn't cover. The wait-time trade-off in point 1 was flagged by that tool's own analysis; point 2 is mine, from reading the control flow directly.

getShapeTransformCache rebuilds on every octomap/sensor update and
walks the TF tree twice per shape-bearing frame: canTransform first
(a check), then lookupTransform (the actual compose). lookupTransform
already composes the transform and throws tf2::TransformException if
it's unavailable, which the function's existing try/catch already
handles, so the canTransform check duplicates work lookupTransform
does anyway.

Drop it and call lookupTransform directly inside the existing
try/catch. Cache contents are unchanged: lookupTransform returns the
same transform whether or not canTransform was called first.

One real behavioral difference worth being explicit about:
canTransform(..., shape_transform_cache_lookup_wait_time_) also
waits up to that duration (a ROS parameter,
"<robot_description>_planning.shape_transform_cache_lookup_wait_time",
defaulting to 0.05s) for a not-yet-available transform. Dropping it
removes that wait. When a transform is available at target_time (the
normal case), behavior is identical. If one is momentarily late, the
current code waits up to the configured duration; this throws
immediately, and the try/catch returns false for that update (the
next sensor update tries again). The wait-time parameter itself
becomes a no-op, since this was its only reader.
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.

1 participant