perf: add synchronous callback path for Node-scoped subscriptions#652
Open
azerupi wants to merge 2 commits into
Open
perf: add synchronous callback path for Node-scoped subscriptions#652azerupi wants to merge 2 commits into
azerupi wants to merge 2 commits into
Conversation
Previously, all Node-scoped subscription callbacks were wrapped in async
futures even when the callback was synchronous. This added 3 heap
allocations per received message: Arc::clone, Box::pin(async { }),
and Arc::new(Task { }) for the async task queue.
Add explicit Sync/Async variants to NodeSubscriptionCallback. Sync
callbacks are called directly with no Arc, no Future, and no task queue
dispatch. Rename existing variants with Async prefix (breaking change).
IntoNodeSubscriptionCallback now produces Sync variants, with relaxed
bounds (FnMut + Send instead of Fn + Send + Sync).
IntoAsyncSubscriptionCallback produces Async variants as before.
CI failed on the fmt colcon test; these files were not formatted with nightly rustfmt as required by the workflow. Co-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
perf: add a synchronous callback path for node-scoped subscriptions
This is a fix for part of the performance issue reported in #627.
Subscriptions were paying an async tax they never used
Receiving a message with a plain synchronous callback is the single most common thing you do in ROS 2:
Yet under the hood, every node-scoped subscription callback was wrapped as if it were
async, even a one-line synchronous closure. For each message received, that meant:Box::pin(async { … })to turn the callback into a future,Arc::new(Task { … })to enqueue it, andArc::clonefor the dispatchThree heap allocations per message, plus a hop through the executor's task queue so the callback runs on a later turn instead of inline. That's pure overhead on the hottest path in the system, paid by the ~95% of subscriptions whose callbacks are perfectly synchronous and have no reason to touch a future at all.
The fix: call synchronous callbacks synchronously
This splits
NodeSubscriptionCallbackintoSync*andAsync*variants:FnMut(T) + Send) are now invoked directly in the wait-set processing, noFuture, noArc, no task-queue dispatch. The message is taken and the callback runs inline, immediately and in order.create_async_subscription/IntoAsyncSubscriptionCallback.IntoNodeSubscriptionCallbacknow produces theSyncvariants, so the defaultcreate_subscriptionpath gets the fast path automatically, no API change at the call site.A nice side effect: friendlier bounds
The synchronous path only needs
FnMut + Sendinstead of the oldFn + Send + Sync. In practice that means your subscription closure can now capture and mutate state directly and doesn't have to beSync.Behavioral note
Synchronous callbacks now run inline during spin, in receipt order, rather than being deferred onto the task queue. This matches the intuition (and rclcpp's default) that a sync subscription callback runs while you're spinning. Genuinely asynchronous work should use
create_async_subscription, which is unchanged.Breaking change
The public
NodeSubscriptionCallbackenum variants are renamed (Regular→AsyncRegular,Boxed→AsyncBoxed,Loaned→AsyncLoaned, and the…WithMessageInfocounterparts), with the newSync*variants added alongside. Code that matches on these variants directly needs updating; code that creates subscriptions through the normal closure API does not.