Describe the bug
After reviewing and testing the UBRing/UBShmTransport introduced by #3290, we found several correctness, scalability, portability, performance, and maintainability issues.
The two shared-memory backends should be considered separately:
--ub_shm_type=1: local POSIX shared memory (IPC)
--ub_shm_type=2: remote shared memory provided by UBS
Some constraints that may be necessary for UBS do not apply to local IPC shared memory.
1. Deleting a timer may block indefinitely
Linux timerfds are created as blocking file descriptors:
timerfd_create(CLOCK_MONOTONIC, 0);
Both DeleteTimerInner() and DeleteTimerSafe() execute a blocking read before closing the fd:
uint64_t exp = 0;
read(fd, &exp, sizeof(exp));
close(fd);
There is a deterministic problematic path for one-shot timers:
TimerEpoll() reads and consumes the timerfd expiration.
- It calls
UnifiedCallback().
- For a non-periodic timer,
UnifiedCallback() calls DeleteTimerInner().
DeleteTimerInner() reads the same timerfd again.
- Since the one-shot timer has already expired and was drained, the second blocking
read() may never return.
For periodic timers, deletion may also block until the next expiration. For example, deleting a heartbeat timer may block for up to the heartbeat interval.
This can stall connection cleanup and delay releasing resources after a peer disconnects or crashes.
Timer deletion should not require a blocking read. If the custom implementation is retained, timerfds should at least use TFD_NONBLOCK | TFD_CLOEXEC, and EAGAIN should be treated as normal. Removing the fd from epoll and closing it without draining it may be sufficient.
2. The custom timer subsystem should preferably be replaced with brpc's existing timer implementation
The timer problems are not isolated defects. They are consequences of introducing a second timer framework based on timerfd/epoll alongside the timer framework already maintained by brpc.
brpc already provides:
bthread::TimerThread
bthread_timer_add() and bthread_timer_del()
- Versioned timer task IDs
- Cancellation semantics that account for a callback already running
- A shared global timer thread
UBRing can schedule one-shot tasks through the global bthread::TimerThread. Periodic heartbeat or connection-check tasks can reschedule themselves after each callback. Potentially blocking resource cleanup should be handed off to a bthread or worker; the timer callback should only validate state and schedule the cleanup work.
Unless UBRing has a concrete timer requirement that the existing implementation cannot satisfy, maintaining an independent timer subsystem does not appear justified. Reusing the existing implementation would eliminate most of the following newly introduced problems.
2.1 Timer context storage scales with RLIMIT_NOFILE
TimerInit() allocates a context for every possible process fd and initializes a spinlock for every entry:
getrlimit(RLIMIT_NOFILE, &rlim);
g_max_system_fd = (uint32_t)rlim.rlim_cur;
g_timer_fd_ctx_map =
(TimerFdCtx*)malloc(sizeof(TimerFdCtx) * g_max_system_fd);
On x86-64, sizeof(TimerFdCtx) is approximately 32 bytes:
RLIMIT_NOFILE |
Approximate timer table size |
| 1,024 |
32 KiB |
| 20,480,000 |
625 MiB |
The allocation and initialization cost depends on the maximum possible fd count rather than the number of active UBRing timers.
2.2 Using the numeric fd as timer identity introduces an fd-reuse race
Epoll stores only the numeric fd:
event.data.fd = timer_fd;
The callback context is found through:
g_timer_fd_ctx_map[timer_fd]
After a timer is removed and closed, Linux may immediately reuse the fd for another timer. A stale epoll event containing the old fd can then refer to the new timer context.
Possible consequences include executing a new timer's callback for an old event, deleting the wrong timer, or accessing callback arguments after their owning connection has been released.
Changing the container from an fd-indexed array to a map alone is not sufficient. Stale events need to be rejected with a generation-safe identity. bthread::TimerThread already uses versioned task IDs for this purpose.
2.3 Timer initialization contains a race
StartTimeEpoll() creates the timer thread before TimerInit() sets:
g_timer_module_initialized = 1;
The new thread may run immediately and observe the old value:
if (g_timer_module_initialized <= 0) {
break;
}
In that case, the timer thread exits during initialization. Initialization and destruction also use non-atomic global state without an explicit initialization lock.
2.4 The default close-check timer is too frequent
The close-check timer is configured as:
DEFINE_int32(ub_event_queue_timer_interval, 100, ...);
uint32_t interval = FLAGS_ub_event_queue_timer_interval * 1000;
The resulting default period is 100 microseconds, or 10,000 wakeups per second for every UBRing connection. Each connection also creates heartbeat and cleanup timers.
The close transition should preferably be event-driven. If polling is unavoidable, the default interval should be significantly coarser and its unit should be documented.
2.5 All timer callbacks execute serially on one pthread
TimerEpoll() invokes callbacks inline from the only timer thread. Some callbacks perform connection cleanup and may wait for ub_flying_io_timeout, currently five seconds.
A slow callback therefore delays heartbeat and cleanup callbacks for all other UBRing connections. Timer callbacks should schedule potentially blocking work on an appropriate bthread or worker instead of executing it inline.
3. The macOS timerfd emulation is not functionally equivalent
The macOS implementation creates a pipe and returns only its read end:
static int timerfd_create_macosx(...) {
int pipefd[2];
pipe(pipefd);
return pipefd[0];
}
There are several issues:
- The pipe write end is leaked.
- Nothing writes expiration counters into the pipe.
TimerEpoll() calls read(timer_fd, ...) after receiving an EVFILT_TIMER event, so it can block forever on the empty pipe.
timerfd_settime_macosx() is a no-op.
EV_SET() uses it_value as the repeating kqueue interval and does not correctly model it_interval.
- Many timers use an initial expiration of one nanosecond, which becomes zero milliseconds after conversion.
The kqueue path should process the expiration count from struct kevent::data and should not read from a pipe. Reusing brpc's existing timer implementation would remove the need for this platform-specific emulation.
4. The fixed 60-byte payload should be handled differently for IPC and UBS
The current message format is fixed to one 64-byte cache line:
#define UBR_MSG_HEADER_LEN 4
#define UBR_MSG_PAYLOAD_LEN 60
#define UBR_MSG_LEN 64
The current send path first assembles the packet in local memory:
memcpy(local_msg_space.payload, input, length);
Copy64Byte(remote_data_queue_slot, &local_msg_space);
This design should not be applied identically to both backends.
4.1 Local IPC shared memory
For the IPC backend, both processes map the same POSIX shared-memory object. The sender can write directly into the destination ring slot.
The current staging path introduces an avoidable copy:
IOBuf/iovec
-> local_msg_space
-> shared-memory ring slot
It can instead be:
IOBuf/iovec
-> shared-memory ring slot payload
-> publish the slot state
The producer should write the payload and metadata first, then publish the slot with release semantics. The consumer should check the slot state with acquire semantics before reading the payload.
This removes one local memory copy and allows the IPC backend to use a more suitable payload size. A 4 KiB or 64 KiB attachment currently has to be split into approximately 69 or 1,093 chunks respectively.
The IPC payload size could be a startup-only option, with the selected format included in the handshake. It must not be changed while connections using that format are alive.
4.2 Remote UBS shared memory
For UBS, the 64-byte unit may be required by the UB transport's indivisible remote-store semantics. If that hardware/API requirement is confirmed, UBS may need to continue assembling a complete 64-byte packet locally before publishing it remotely.
However, this is a backend-specific constraint and should not force the IPC backend to use the same format.
The non-LS64 implementation of Copy64Byte() currently falls back to ordinary memcpy():
#ifdef LS64
// ST64B
#else
memcpy(dst, src, 64);
#endif
An ordinary memcpy() does not itself guarantee an indivisible 64-byte store. The required atomicity and visibility guarantees should therefore be documented and validated separately for ARM LS64/ST64B, other local architectures, UBS remote mappings, and POSIX IPC shared memory.
A possible design is to let each backend provide its own payload size, slot alignment, publication operation, and memory-ordering requirements. The chosen format or format version should be negotiated during the UBRing handshake.
5. There are several code-style and naming issues
The change contains personal debugging remnants, ambiguous flag names, and terminology copied from the RDMA transport without adapting it to UBRing.
5.1 Personal debugging remnants should be removed
src/brpc/ubshm_transport.cpp contains commented debugging logs with a personal identifier:
// LOG(INFO) << "mwj pollin4=" << pollin ...
// LOG(INFO) << "mwj pollin1=" << pollin;
// LOG(INFO) << "mwj pollin2=" << pollin << " mwj_ret=" << mwj_ret;
// LOG(INFO) << "mwj return 0";
It also contains:
auto mwj_ret = bthread::butex_wait(...);
src/brpc/ubshm/ub_endpoint.cpp contains:
// TODO mwj should polling start after the connection is established?
Personal debugging logs should be deleted rather than committed as comments. The local variable should have a descriptive name such as wait_rc. The TODO should either be resolved or rewritten as a clear actionable comment without an author identifier.
5.2 Time-related gflags do not document their units
The following flags do not indicate their units in either the flag name or description:
DEFINE_int32(ub_disconnect_timeout, 5, "Ubshm disconnection timeout.");
DEFINE_int32(ub_connect_timeout, 1, "Ubshm connection timeout.");
DEFINE_int32(ub_hb_timer_interval, 5, "Heartbeat timer interval.");
DEFINE_int32(ub_event_queue_timer_interval, 100,
"Interval of the disconnection timer.");
DEFINE_int32(ub_flying_io_timeout, 5,
"Waiting time for stopping data sending and receiving...");
The code uses different units:
ub_disconnect_timeout: seconds
ub_connect_timeout: seconds
ub_hb_timer_interval: seconds
ub_event_queue_timer_interval: microseconds
ub_flying_io_timeout: seconds
A user cannot determine these units from --help. The unit should be encoded in the flag name and description, for example:
ub_disconnect_timeout_s
ub_connect_timeout_s
ub_hb_timer_interval_s
ub_event_queue_timer_interval_us
ub_flying_io_timeout_s
If renaming is considered incompatible, the descriptions must at least state the exact units.
The conversion should also use an existing named constant or time utility instead of:
#define TIME_COVERSION 1000
TIME_COVERSION is misspelled and does not explain what is being converted. The existing USEC_TO_NSEC constant or a standard time conversion helper would be clearer.
5.3 _cq_sid is RDMA terminology and is inappropriate for UBRing
UBShmEndpoint uses the following names:
SocketId _cq_sid;
struct CqSidOp;
std::unordered_set<CqSidOp, ...> cq_sids;
It also logs:
PLOG(WARNING) << "Fail to create socket for cq";
These names appear to have been copied from RdmaEndpoint. In RDMA, CQ means Completion Queue, so _cq_sid describes a socket associated with an RDMA completion queue. UBRing does not have an RDMA completion queue.
In UBShmEndpoint, this synthetic SocketId represents an entry registered with the UBRing poller. Names should describe that role, for example _poller_sid, PollerSidOp, and poller_sids, or equivalent event-source terminology.
Copying the RDMA polling structure may be reasonable, but RDMA-specific terminology should not be retained when the underlying concept is different.
6. Current tests do not cover lifecycle and data-path cases
test/brpc_ubring_unittest.cpp currently covers hello-message serialization, endpoint construction, basic IPC allocation, and reset behavior.
It does not cover:
- End-to-end UBRing send and receive
- Large attachments spanning many chunks
- Client or server crash/disconnect cleanup
- One-shot timer expiration and deletion
- Concurrent timer deletion and callback execution
- Numeric fd reuse with stale epoll events
- High
RLIMIT_NOFILE
- Multiple concurrent UBRing connections
- macOS timer behavior
- Different IPC/UBS message formats
- Validation of time-related gflag units and default intervals
7. Bazel and CI support was incomplete, but has been fixed
The original UBRing change documented:
bazel build --define=with_ubring=true
but the Bazel build did not define BRPC_WITH_UBRING, did not expose the performance example targets, and the Bazel CI jobs did not build or run UBRing-enabled unit tests.
This has already been fixed by #3445, which added:
--define=BRPC_WITH_UBRING=true
--config=ubring
- Bazel targets for the UBRing performance client and server
- UBRing-enabled Bazel compilation and unit-test jobs
- Updated English and Chinese documentation
No further action is requested for this resolved build issue. It is listed here for completeness because it prevented the original implementation from being exercised by Bazel CI.
To Reproduce
Cleanup hang
Build brpc and the performance example:
bazel build --config=ubring \
//example:ubring_performance_server \
//example:ubring_performance_client
Start the IPC server:
./bazel-bin/example/ubring_performance_server \
--use_ubring=true \
--ub_shm_type=1 \
--port=8002
Run a client and let it exit or terminate it:
./bazel-bin/example/ubring_performance_client \
--use_ubring=true \
--ub_shm_type=1 \
--servers=127.0.0.1:8002 \
--thread_num=1 \
--queue_depth=1 \
--attachment_size=4096 \
--test_seconds=30
Inspect a process that becomes stuck during cleanup. The stack can stop in the blocking read() called by DeleteTimerInner() or DeleteTimerSafe().
Timer table allocation
Initialize the UBRing timer module and inspect startup time and RSS. TimerInit() allocates and initializes approximately 625 MiB of timer contexts even when no UBRing connection exists.
IPC chunking
Send 4 KiB and 64 KiB attachments through the IPC backend. The current 60-byte payload creates approximately:
4 KiB -> 69 chunks
64 KiB -> 1093 chunks
Expected behavior
- UBRing should reuse brpc's existing timer framework unless a concrete unsupported requirement is documented.
- Timer deletion must never block waiting for another expiration.
- Timer callbacks and deletion must be safe when executed concurrently.
- Stale epoll events must not operate on a new timer that reused the same fd.
- Timer bookkeeping should scale with active timers, not
RLIMIT_NOFILE.
- Timer callbacks should perform lightweight state transitions and dispatch blocking cleanup elsewhere.
- macOS timers should have behavior equivalent to the Linux implementation.
- IPC and UBS should use backend-appropriate payload and publication mechanisms.
- IPC should write directly into shared-memory slots and publish them with explicit release/acquire ordering.
- UBS should retain a 64-byte packet only if required by documented hardware/API semantics.
- The active message format must be agreed by both peers.
- All time-related gflags should include their units in their names and descriptions.
- UBRing code should not contain personal debugging markers or RDMA-specific terminology for unrelated concepts.
- CI tests should exercise the full send/receive and connection lifecycle.
Versions
OS: Helix 8.4r, x86-64
Compiler: GCC 8.5.0
brpc: UBRing introduced by 72bf13a3 (#3290); issues still present on current master
protobuf: Not relevant to these issues
Build: Bazel UBRing support fixed by #3445
Additional context/screenshots
We performed local IPC performance tests with 32 MiB data queues. An experimental version used an 8 KiB payload and wrote directly into the IPC ring slot.
| Workload |
Original 60B format |
IPC 8 KiB direct-write |
| 4 threads, depth 64, 4 KiB attachment |
761 MB/s |
871-905 MB/s |
| 4 threads, depth 64, 64 KiB attachment |
1,869 MB/s |
3,559-3,925 MB/s |
| 1 thread, depth 1, 4 KiB attachment |
102.6 MB/s |
106.4-107.4 MB/s |
These numbers are directional rather than a strict single-variable A/B comparison because both the chunk payload size and the IPC copy path were changed. They nevertheless show that the fixed 60-byte format has a significant cost for larger messages.
The experimental direct-write IPC path was:
IOBuf/iovec
-> destination shared-memory payload
-> write metadata
-> release-store the ready flag
The receiver used an acquire-load of the ready flag before consuming the payload.
Describe the bug
After reviewing and testing the UBRing/UBShmTransport introduced by #3290, we found several correctness, scalability, portability, performance, and maintainability issues.
The two shared-memory backends should be considered separately:
--ub_shm_type=1: local POSIX shared memory (IPC)--ub_shm_type=2: remote shared memory provided by UBSSome constraints that may be necessary for UBS do not apply to local IPC shared memory.
1. Deleting a timer may block indefinitely
Linux timerfds are created as blocking file descriptors:
Both
DeleteTimerInner()andDeleteTimerSafe()execute a blocking read before closing the fd:There is a deterministic problematic path for one-shot timers:
TimerEpoll()reads and consumes the timerfd expiration.UnifiedCallback().UnifiedCallback()callsDeleteTimerInner().DeleteTimerInner()reads the same timerfd again.read()may never return.For periodic timers, deletion may also block until the next expiration. For example, deleting a heartbeat timer may block for up to the heartbeat interval.
This can stall connection cleanup and delay releasing resources after a peer disconnects or crashes.
Timer deletion should not require a blocking read. If the custom implementation is retained, timerfds should at least use
TFD_NONBLOCK | TFD_CLOEXEC, andEAGAINshould be treated as normal. Removing the fd from epoll and closing it without draining it may be sufficient.2. The custom timer subsystem should preferably be replaced with brpc's existing timer implementation
The timer problems are not isolated defects. They are consequences of introducing a second timer framework based on timerfd/epoll alongside the timer framework already maintained by brpc.
brpc already provides:
bthread::TimerThreadbthread_timer_add()andbthread_timer_del()UBRing can schedule one-shot tasks through the global
bthread::TimerThread. Periodic heartbeat or connection-check tasks can reschedule themselves after each callback. Potentially blocking resource cleanup should be handed off to a bthread or worker; the timer callback should only validate state and schedule the cleanup work.Unless UBRing has a concrete timer requirement that the existing implementation cannot satisfy, maintaining an independent timer subsystem does not appear justified. Reusing the existing implementation would eliminate most of the following newly introduced problems.
2.1 Timer context storage scales with
RLIMIT_NOFILETimerInit()allocates a context for every possible process fd and initializes a spinlock for every entry:On x86-64,
sizeof(TimerFdCtx)is approximately 32 bytes:RLIMIT_NOFILEThe allocation and initialization cost depends on the maximum possible fd count rather than the number of active UBRing timers.
2.2 Using the numeric fd as timer identity introduces an fd-reuse race
Epoll stores only the numeric fd:
The callback context is found through:
After a timer is removed and closed, Linux may immediately reuse the fd for another timer. A stale epoll event containing the old fd can then refer to the new timer context.
Possible consequences include executing a new timer's callback for an old event, deleting the wrong timer, or accessing callback arguments after their owning connection has been released.
Changing the container from an fd-indexed array to a map alone is not sufficient. Stale events need to be rejected with a generation-safe identity.
bthread::TimerThreadalready uses versioned task IDs for this purpose.2.3 Timer initialization contains a race
StartTimeEpoll()creates the timer thread beforeTimerInit()sets:g_timer_module_initialized = 1;The new thread may run immediately and observe the old value:
In that case, the timer thread exits during initialization. Initialization and destruction also use non-atomic global state without an explicit initialization lock.
2.4 The default close-check timer is too frequent
The close-check timer is configured as:
The resulting default period is 100 microseconds, or 10,000 wakeups per second for every UBRing connection. Each connection also creates heartbeat and cleanup timers.
The close transition should preferably be event-driven. If polling is unavoidable, the default interval should be significantly coarser and its unit should be documented.
2.5 All timer callbacks execute serially on one pthread
TimerEpoll()invokes callbacks inline from the only timer thread. Some callbacks perform connection cleanup and may wait forub_flying_io_timeout, currently five seconds.A slow callback therefore delays heartbeat and cleanup callbacks for all other UBRing connections. Timer callbacks should schedule potentially blocking work on an appropriate bthread or worker instead of executing it inline.
3. The macOS timerfd emulation is not functionally equivalent
The macOS implementation creates a pipe and returns only its read end:
There are several issues:
TimerEpoll()callsread(timer_fd, ...)after receiving anEVFILT_TIMERevent, so it can block forever on the empty pipe.timerfd_settime_macosx()is a no-op.EV_SET()usesit_valueas the repeating kqueue interval and does not correctly modelit_interval.The kqueue path should process the expiration count from
struct kevent::dataand should not read from a pipe. Reusing brpc's existing timer implementation would remove the need for this platform-specific emulation.4. The fixed 60-byte payload should be handled differently for IPC and UBS
The current message format is fixed to one 64-byte cache line:
The current send path first assembles the packet in local memory:
This design should not be applied identically to both backends.
4.1 Local IPC shared memory
For the IPC backend, both processes map the same POSIX shared-memory object. The sender can write directly into the destination ring slot.
The current staging path introduces an avoidable copy:
It can instead be:
The producer should write the payload and metadata first, then publish the slot with release semantics. The consumer should check the slot state with acquire semantics before reading the payload.
This removes one local memory copy and allows the IPC backend to use a more suitable payload size. A 4 KiB or 64 KiB attachment currently has to be split into approximately 69 or 1,093 chunks respectively.
The IPC payload size could be a startup-only option, with the selected format included in the handshake. It must not be changed while connections using that format are alive.
4.2 Remote UBS shared memory
For UBS, the 64-byte unit may be required by the UB transport's indivisible remote-store semantics. If that hardware/API requirement is confirmed, UBS may need to continue assembling a complete 64-byte packet locally before publishing it remotely.
However, this is a backend-specific constraint and should not force the IPC backend to use the same format.
The non-
LS64implementation ofCopy64Byte()currently falls back to ordinarymemcpy():An ordinary
memcpy()does not itself guarantee an indivisible 64-byte store. The required atomicity and visibility guarantees should therefore be documented and validated separately for ARM LS64/ST64B, other local architectures, UBS remote mappings, and POSIX IPC shared memory.A possible design is to let each backend provide its own payload size, slot alignment, publication operation, and memory-ordering requirements. The chosen format or format version should be negotiated during the UBRing handshake.
5. There are several code-style and naming issues
The change contains personal debugging remnants, ambiguous flag names, and terminology copied from the RDMA transport without adapting it to UBRing.
5.1 Personal debugging remnants should be removed
src/brpc/ubshm_transport.cppcontains commented debugging logs with a personal identifier:It also contains:
auto mwj_ret = bthread::butex_wait(...);src/brpc/ubshm/ub_endpoint.cppcontains:// TODO mwj should polling start after the connection is established?Personal debugging logs should be deleted rather than committed as comments. The local variable should have a descriptive name such as
wait_rc. The TODO should either be resolved or rewritten as a clear actionable comment without an author identifier.5.2 Time-related gflags do not document their units
The following flags do not indicate their units in either the flag name or description:
The code uses different units:
ub_disconnect_timeout: secondsub_connect_timeout: secondsub_hb_timer_interval: secondsub_event_queue_timer_interval: microsecondsub_flying_io_timeout: secondsA user cannot determine these units from
--help. The unit should be encoded in the flag name and description, for example:If renaming is considered incompatible, the descriptions must at least state the exact units.
The conversion should also use an existing named constant or time utility instead of:
TIME_COVERSIONis misspelled and does not explain what is being converted. The existingUSEC_TO_NSECconstant or a standard time conversion helper would be clearer.5.3
_cq_sidis RDMA terminology and is inappropriate for UBRingUBShmEndpointuses the following names:It also logs:
These names appear to have been copied from
RdmaEndpoint. In RDMA,CQmeans Completion Queue, so_cq_siddescribes a socket associated with an RDMA completion queue. UBRing does not have an RDMA completion queue.In
UBShmEndpoint, this syntheticSocketIdrepresents an entry registered with the UBRing poller. Names should describe that role, for example_poller_sid,PollerSidOp, andpoller_sids, or equivalent event-source terminology.Copying the RDMA polling structure may be reasonable, but RDMA-specific terminology should not be retained when the underlying concept is different.
6. Current tests do not cover lifecycle and data-path cases
test/brpc_ubring_unittest.cppcurrently covers hello-message serialization, endpoint construction, basic IPC allocation, and reset behavior.It does not cover:
RLIMIT_NOFILE7. Bazel and CI support was incomplete, but has been fixed
The original UBRing change documented:
but the Bazel build did not define
BRPC_WITH_UBRING, did not expose the performance example targets, and the Bazel CI jobs did not build or run UBRing-enabled unit tests.This has already been fixed by #3445, which added:
--define=BRPC_WITH_UBRING=true--config=ubringNo further action is requested for this resolved build issue. It is listed here for completeness because it prevented the original implementation from being exercised by Bazel CI.
To Reproduce
Cleanup hang
Build brpc and the performance example:
Start the IPC server:
Run a client and let it exit or terminate it:
Inspect a process that becomes stuck during cleanup. The stack can stop in the blocking
read()called byDeleteTimerInner()orDeleteTimerSafe().Timer table allocation
ulimit -n 20480000Initialize the UBRing timer module and inspect startup time and RSS.
TimerInit()allocates and initializes approximately 625 MiB of timer contexts even when no UBRing connection exists.IPC chunking
Send 4 KiB and 64 KiB attachments through the IPC backend. The current 60-byte payload creates approximately:
Expected behavior
RLIMIT_NOFILE.Versions
OS: Helix 8.4r, x86-64
Compiler: GCC 8.5.0
brpc: UBRing introduced by
72bf13a3(#3290); issues still present on current masterprotobuf: Not relevant to these issues
Build: Bazel UBRing support fixed by #3445
Additional context/screenshots
We performed local IPC performance tests with 32 MiB data queues. An experimental version used an 8 KiB payload and wrote directly into the IPC ring slot.
These numbers are directional rather than a strict single-variable A/B comparison because both the chunk payload size and the IPC copy path were changed. They nevertheless show that the fixed 60-byte format has a significant cost for larger messages.
The experimental direct-write IPC path was:
The receiver used an acquire-load of the ready flag before consuming the payload.