api: reduce syscall overhead in client and server - #714
Conversation
Code Review by Qodo🐞 Bugs (0) 📘 Rule violations (0) 📎 Requirement gaps (0)
Great, no issues found!Qodo reviewed your code and found no material issues that require reviewTip of the day💡 Did you know, you can start a comment with 'qodo' or '@qodo' to chat about any finding |
d82f3d7 to
c7c3ea5
Compare
|
Deeply buried in DPDK patches. Will review in a few days.
Venlig hilsen / Kind regards,
…-Morten Brørup
From: Robin Jarry ***@***.***
Sent: Wednesday, 26 August 2026 15.29
To: DPDK/grout
Cc: Morten Brørup; Mention
Subject: [DPDK/grout] api: reduce syscall overhead in client and server (PR #714)
Reduce the number of syscalls per API transaction on both client and server sides.
On the server, use BEV_OPT_DEFER_CALLBACKS so that libevent accumulates header and payload writes in the evbuffer and flushes them in a single writev(). Remove the no-op bufferevent_flush() calls and the manual read_cb re-trigger which libevent already handles.
On the client, replace two separate send() calls with a single sendmsg() using an iovec, and add a BUFSIZ read-ahead buffer so that recv_all() can serve multiple small reads from one recv() syscall. This is particularly effective during stream iterations where many small messages arrive back-to-back.
A GR_PING request type and accompanying ping_perf benchmark tool are added to measure the resulting transaction rate.
Different approach from ***@***.***/
Cc: @MortenBroerup <https://github.com/MortenBroerup>
________________________________
You can view, comment on, or merge this pull request online at:
#714
Commit Summary
* 41c851e <41c851e> api: remove useless flush calls
* 71002a4 <71002a4> api: use deferred bufferevent callbacks
* 237e60f <237e60f> api: send request header and payload in a single syscall
* 78e8a9e <78e8a9e> api: add read-ahead buffering in the client
* d82f3d7 <d82f3d7> api: add GR_PING request type and perf test
File Changes
(5 files <https://github.com/DPDK/grout/pull/714/files> )
* M api/gr_api.h <https://github.com/DPDK/grout/pull/714/files#diff-4e0fe7cbc1cf2d2b6aea081926136965849fb7fce11298a7d1464623b97caf0a> (6)
* M api/gr_api_client_impl.h <https://github.com/DPDK/grout/pull/714/files#diff-d92227a47545d710398fb4fa28a6b9109684a4281d6ee199d05167ee625d8565> (78)
* M main/api.c <https://github.com/DPDK/grout/pull/714/files#diff-0da252d39501a8ca760dcc378932001c44aaa7fb05667616de6d5dfc390150f1> (24)
* M meson.build <https://github.com/DPDK/grout/pull/714/files#diff-30d8f6be6320feeacf686be94f48c70869b52630e01ea625f0f15adc0d57c3e4> (6)
* A smoke/ping_perf.c <https://github.com/DPDK/grout/pull/714/files#diff-03b11740e5c366b8145809969f73aa214329652704945fa5a478e291a544c937> (91)
Patch Links:
* https://github.com/DPDK/grout/pull/714.patch
* https://github.com/DPDK/grout/pull/714.diff
—
Reply to this email directly, view it on GitHub <#714?email_source=notifications&email_token=AFC6KFHN5DEBNX6BRJ5DCID5L3QZBA5CNFSNUABEM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UF42DGNRXGQ4TSNJTGSTHEZLBONXW5J3NMVXHI2LPN2SWK5TFNZ2KYZTPN52GK4S7MNWGSY3L> , or unsubscribe <https://github.com/notifications/unsubscribe-auth/AFC6KFHW3PPU3ELAEW7MW7D5L3QZBAVCNFSNUABFKJSXA33TNF2G64TZHM3TONJRGU4TMOBRHNEXG43VMU5TKMRVG42TOOBYGQYKC5QC> .
Triage notifications, keep track of coding agent tasks and review pull requests on the go with GitHub Mobile for iOS <https://github.com/notifications/mobile/ios/AFC6KFF7SD3DX755VUIVLST5L3QZBA5CNFSNUABEM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UF42DGNRXGQ4TSNJTGSTHEZLBONXW5J3NMVXHI2LPN2SWK5TFNZ2KUZTPN52GK4S7NFXXG> and Android <https://github.com/notifications/mobile/android/AFC6KFBY27TS34LOO4STM235L3QZBA5CNFSNUABEM5UWIORPF5TWS5BNNB2WEL2QOVWGYUTFOF2WK43UF42DGNRXGQ4TSNJTGSTHEZLBONXW5J3NMVXHI2LPN2SWK5TFNZ2K4ZTPN52GK4S7MFXGI4TPNFSA> . Download it today!
You are receiving this because you were mentioned. <https://github.com/notifications/beacon/AFC6KFGXLJSMJ6IGO7TDWXL5L3QZBBFCNFSM6AAAAAC6JIHLVOWGG33NNVSW45C7OR4XAZNFJFZXG5LFVJRW63LNMVXHIX3JMTHQAAAAAE4WASKYUZZGKYLTN5XKO3LFNZ2GS33O.gif> Message ID: ***@***.***>
|
The bufferevent_sock implementation of flush is a no-op, so remove the bufferevent_flush() calls. The manual re-trigger of read_cb when input data remains is also unnecessary since libevent will invoke the callback as long as data is available in the input buffer. Signed-off-by: Robin Jarry <rjarry@redhat.com>
c7c3ea5 to
f63b50a
Compare
MortenBroerup
left a comment
There was a problem hiding this comment.
Deferred bufferevent callbacks look like good solution.
I have a concern about huge responses...
For reference, PHP has functions to enable/disable output buffering.
Is there any means to limit libevent buffering for huge responses? Otherwise, they may consume huge amounts of memory in the server (Grout), instead of streaming in chunks to the client.
I'm asking for either a configurable threshold in libevent, or a function to temporarily disable/enable deferring the callbacks, or a flush-like function that invokes the callback.
f63b50a to
78ce613
Compare
That's a good point. I have capped the amount of buffering before flushing to the socket. // 64K allows buffering up to 16 * 4K evbuffer chains before flushing
#define HIGH_WATERMARK (1 << 16)
void api_send(struct api_ctx *ctx, uint32_t len, const void *payload) {
...
if (bufferevent_write(ctx->bev, &resp, sizeof(resp)) < 0)
LOG(ERR, "pid=%d cannot write header", ctx->pid);
if (bufferevent_write(ctx->bev, payload, len) < 0)
LOG(ERR, "pid=%d cannot write payload", ctx->pid);
// Force flush when buffer grows past HIGH_WATERMARK to avoid runaway memory use
struct evbuffer *output = bufferevent_get_output(ctx->bev);
if (evbuffer_get_length(output) >= HIGH_WATERMARK) {
evutil_socket_t fd = bufferevent_getfd(ctx->bev);
evbuffer_unfreeze(output, true);
evbuffer_write_atmost(output, fd, -1);
evbuffer_freeze(output, true);
}
} |
fe047e5 to
93e7ba8
Compare
I was wondering if HIGH_WATERMARK and BUFSIZ in API read-ahead should be similar? Maybe not, because they execute in two different applications. But - considering the value of HIGH_WATERMARK (64 KB)- maybe read-ahead should be higher than BUFSIZ (8 KB). |
| if (evbuffer_get_length(output) >= HIGH_WATERMARK) { | ||
| evutil_socket_t fd = bufferevent_getfd(ctx->bev); | ||
| evbuffer_unfreeze(output, true); | ||
| evbuffer_write_atmost(output, fd, -1); |
There was a problem hiding this comment.
When crossing the HIGH_WATERMARK, the flush calls deeply inside libevent, to replicate this:
https://github.com/libevent/libevent/blob/master/bufferevent_sock.c#L301
But the error handling is missing here.
Would it be possible to call the libevent write callback instead?
There was a problem hiding this comment.
I have double checked and we don't need this flag after all. All bufferevent_write() calls made synchronously (without yielding back to the event loop) are coalesced into a single EV_WRITE event. When the read callback returns (after writing any number of times), up to 512K bytes will be written in a single system call (using writev() + iovecs).
I have removed the commit that added BEV_OPT_DEFER_CALLBACKS entirely.
After further consideration: No, they serve different purposes. Let's keep them individual.
If we consider the HIGH_WATERMARK a safety guard against runaway buffering, it can be much higher, such as 2 MB. We don't want to trigger it unnecessarily.
|
Handle EAGAIN/EWOULDBLOCK in recv_all() and the sendmsg() loop so that callers using non-blocking sockets do not get spurious errors. When the error occurs mid-transfer, poll() and retry. When nothing has been read yet, propagate the error so the caller can distinguish "no data available" from a real failure. Signed-off-by: Robin Jarry <rjarry@redhat.com>
Set O_NONBLOCK on the notification sockets after subscribing. When gr_api_client_event_recv() returns EWOULDBLOCK, re-arm event_add_read() using the socket fd. After successfully reading one event, re-arm event_add_read() without any file descriptor to schedule the read callback immediately. Signed-off-by: Robin Jarry <rjarry@redhat.com>
Use sendmsg() with an iovec instead of two separate send() calls for header and payload. This halves the number of syscalls per API request on the client side. Signed-off-by: Robin Jarry <rjarry@redhat.com>
Add a read-ahead buffer to the API client so that recv_all() can serve multiple small reads from a single recv() syscall. Size this buffer so it can receive one full-size message (very unlikely to exist). When the remaining bytes to read fit in the buffer, recv() reads ahead into it; subsequent calls drain the buffer without any syscall. This is especially effective during stream iterations where many small header+payload messages arrive back-to-back. Signed-off-by: Morten Brørup <mb@smartsharesystems.com> Signed-off-by: Robin Jarry <rjarry@redhat.com>
Add a ping request that accepts an optional payload and echoes it back. The accompanying ping_perf tool sends a configurable number of ping calls in a tight loop and reports the transaction rate, useful for benchmarking API overhead. Signed-off-by: Morten Brørup <mb@smartsharesystems.com> Signed-off-by: Robin Jarry <rjarry@redhat.com>
93e7ba8 to
77a56f1
Compare
|
Following up on what I saw in the iovec implementation of evbuffer write callbacks, they can deal with up to 512K bytes in a single writev() (128 * 4K chains). So we could size the client side buffer accordingly. But that looks a bit ridiculous. I have gone with GR_API_MAX_MSG_SIZE which seems reasonable. About runaway memory consumption concerns: the current implementation will already buffer everything in streaming responses before writing all at once. The only way to prevent infinite memory allocation with unbounded streams would be to allow pausing/resuming streaming responses. This is something I had already attempted in #548 but it seemed too complicated and over engineered at the moment. |
| continue; | ||
| } | ||
|
|
||
| if (remaining < sizeof(c->recv_buf)) { |
There was a problem hiding this comment.
With recv_buf[sizeof(struct gr_api_response) + GR_API_MAX_MSG_LEN]], this branch will always be taken. (Except when receiving a max-size message.)
In other words: We could consider changing the algorithm from conditional read-ahead, to just always read via the recv_buf.
Reduce the number of syscalls per API transaction on both client and server sides.
On the server, use BEV_OPT_DEFER_CALLBACKS so that libevent accumulates header and payload writes in the evbuffer and flushes them in a single writev(). Remove the no-op bufferevent_flush() calls and the manual read_cb re-trigger which libevent already handles.
On the client, replace two separate send() calls with a single sendmsg() using an iovec, and add a BUFSIZ read-ahead buffer so that recv_all() can serve multiple small reads from one recv() syscall. This is particularly effective during stream iterations where many small messages arrive back-to-back.
A GR_PING request type and accompanying ping_perf benchmark tool are added to measure the resulting transaction rate.
Different approach from https://patches.dpdk.org/project/grout/patch/20260720224924.3299328-1-mb@smartsharesystems.com/
Cc: @MortenBroerup