Skip to content

rmw_wait issues one syscall per entity on every call, not per ready entity #51

Closed

Description

@benaliabderrahmane

rmw_wait makes three full passes over every entity in the wait set on every call, even when a single socket has data. For a node with a large wait set this dominates CPU.

The passes:

  1. Drain every subscription, service and client socket (rmw_wait.cpp#L175-L199). Nearly all of these recvmsg calls return EAGAIN.
  2. Re-arm every fd with epoll_ctl(EPOLL_CTL_ADD) (rmw_wait.cpp#L307-L349). Nearly all return EEXIST.
  3. After epoll_wait returns, drain every socket again (rmw_wait.cpp#L485-L502).

For a wait set holding 719 entities that is about 2150 syscalls per rmw_wait call.

Measured on an 8-core x86 machine, a node subscribed to roughly 700 topics sits at 66% CPU with 30% of a core in kernel time. At ~1us for a trivial syscall that is ~300k syscalls/s, so ~140 wait cycles/s, which matches the expected executor rate.

CPU tracks wait-set size closely:

entities in wait set CPU
719 66%
654 61%
250 52%
15 23%

The user/system split is about 50/50, which is the shape of a syscall storm rather than real work.

Why this is avoidable

epoll_wait already fills ready_events with exactly the fds that have data. Both drain loops throw that away and poll everything instead.

Suggested fix

  • Post-epoll drain: iterate ready_events rather than the full entity list.
  • Pre-epoll drain: replace the full sweep with one epoll_wait(..., timeout=0) to find ready fds, then drain only those. One syscall instead of N.
  • Re-arm: track armed fds in UdsWaitSet and call epoll_ctl only when an entity joins or leaves.

The re-arm currently uses EPOLL_CTL_ADD returning EEXIST as a cheap way to cope with an fd number reused after its previous owner closed. A replacement has to keep that property, for example by storing the (fd, entity) pair and re-arming when it changes.

Together this removes roughly 1430 syscalls from the drains and the remaining ~719 from the re-arm.

Activity

  1. benaliabderrahmane commented on Aug 13, 2026

    @benaliabderrahmane
    OwnerAuthor

    Fix in flight: PR #57 (fix/wait-drain-only-ready-fds) converts every wait from O(wait-set entities) syscalls to O(ready fds) via an armed-fd cache, and also carries the zero-length-datagram consume fix. Sequencing: it conflicts with #60's rewrite of rmw_wait.cpp, so it gets reconciled on the integration branch and merged after #60.

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

Metadata

Metadata

Labels

P0Do nowperformanceThroughput/latency work

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions