From a70b62dd98dc73b570e409080edba6c48df41c6c Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Sat, 22 Aug 2026 16:38:29 +0200 Subject: [PATCH 1/6] api: remove useless flush calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Reviewed-by: Morten Brørup --- main/api.c | 8 -------- 1 file changed, 8 deletions(-) diff --git a/main/api.c b/main/api.c index 5aea865b9..225419418 100644 --- a/main/api.c +++ b/main/api.c @@ -348,16 +348,8 @@ static void read_cb(struct bufferevent *bev, void *priv) { LOG(ERR, "failed to write payload"); } - bufferevent_flush(bev, EV_WRITE, BEV_FLUSH); - free(req_payload); free(out.payload); - - if (evbuffer_get_length(input) >= sizeof(ctx->header)) { - // More data is available in the input buffer. - // Force read_cb to be invoked again when possible. - bufferevent_flush(bev, EV_READ, BEV_NORMAL); - } return; close: From 694cef85080b592d5102da14cf808e96d55c1a78 Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Wed, 26 Aug 2026 16:07:09 +0200 Subject: [PATCH 2/6] api: handle non-blocking sockets in the client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Reviewed-by: Morten Brørup --- api/gr_api_client_impl.h | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/api/gr_api_client_impl.h b/api/gr_api_client_impl.h index d2b164679..3411adb96 100644 --- a/api/gr_api_client_impl.h +++ b/api/gr_api_client_impl.h @@ -79,6 +79,7 @@ static void register_message(struct api_message *m) { #include #include +#include #include #include #include @@ -177,8 +178,15 @@ static ssize_t send_all(const struct gr_api_client *c, const void *buf, size_t l while (remaining > 0) { n = send(c->sock_fd, ptr, remaining, MSG_NOSIGNAL); - if (n < 0) + if (n < 0) { + if (remaining < len && (errno == EAGAIN || errno == EWOULDBLOCK)) { + // Blocking mid-transmission, wait for socket to be writable again. + struct pollfd pfd = {.fd = c->sock_fd, .events = POLLOUT}; + poll(&pfd, 1, -1); + continue; + } return n; + } ptr += n; remaining -= n; @@ -198,6 +206,12 @@ static ssize_t recv_all(const struct gr_api_client *c, void *buf, size_t len) { errno = ECONNRESET; return len - remaining; } else if (n < 0) { + if (remaining < len && (errno == EAGAIN || errno == EWOULDBLOCK)) { + // Blocking mid-transmission, wait for socket to be readable again. + struct pollfd pfd = {.fd = c->sock_fd, .events = POLLIN}; + poll(&pfd, 1, -1); + continue; + } return n; } From 89b85ef4e88be415ea138dc3eaaecec52c103c0f Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Wed, 26 Aug 2026 16:07:46 +0200 Subject: [PATCH 3/6] frr: use non-blocking sockets for notifications MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Reviewed-by: Morten Brørup --- frr/zebra_dplane_grout.c | 41 ++++++++++++++++++++++++++++------------ 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/frr/zebra_dplane_grout.c b/frr/zebra_dplane_grout.c index 5170bea7d..1c1563b64 100644 --- a/frr/zebra_dplane_grout.c +++ b/frr/zebra_dplane_grout.c @@ -13,6 +13,7 @@ #include "log_grout.h" #include "rt_grout.h" +#include #include #include #include @@ -702,6 +703,8 @@ static void dplane_grout_connect(struct event *) { return; } + fcntl(grout_ctx.dplane_notifs->sock_fd, F_SETFL, O_NONBLOCK); + event_add_read( dg_master, dplane_read_notifications, @@ -731,6 +734,8 @@ static void zebra_grout_connect(struct event *) { return; } + fcntl(grout_ctx.zebra_notifs->sock_fd, F_SETFL, O_NONBLOCK); + event_add_read( zrouter.master, zebra_read_notifications, @@ -773,6 +778,16 @@ static void dplane_read_notifications(struct event *event) { bool new = false; if (gr_api_client_event_recv(grout_ctx.dplane_notifs, &gr_e) < 0 || gr_e == NULL) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + event_add_read( + dg_master, + dplane_read_notifications, + NULL, + grout_ctx.dplane_notifs->sock_fd, + &grout_ctx.dg_t_dplane_update + ); + return; + } gr_api_client_disconnect(grout_ctx.dplane_notifs); grout_ctx.dplane_notifs = NULL; gr_api_client_disconnect(grout_ctx.client); @@ -820,12 +835,8 @@ static void dplane_read_notifications(struct event *event) { free(gr_e); - event_add_read( - dg_master, - dplane_read_notifications, - NULL, - grout_ctx.dplane_notifs->sock_fd, - &grout_ctx.dg_t_dplane_update + event_add_event( + dg_master, dplane_read_notifications, NULL, 0, &grout_ctx.dg_t_dplane_update ); } @@ -834,6 +845,16 @@ static void zebra_read_notifications(struct event *event) { bool new = false; if (gr_api_client_event_recv(grout_ctx.zebra_notifs, &gr_e) < 0 || gr_e == NULL) { + if (errno == EAGAIN || errno == EWOULDBLOCK) { + event_add_read( + zrouter.master, + zebra_read_notifications, + NULL, + grout_ctx.zebra_notifs->sock_fd, + &grout_ctx.dg_t_zebra_update + ); + return; + } gr_api_client_disconnect(grout_ctx.zebra_notifs); grout_ctx.zebra_notifs = NULL; event_add_timer( @@ -868,12 +889,8 @@ static void zebra_read_notifications(struct event *event) { free(gr_e); - event_add_read( - zrouter.master, - zebra_read_notifications, - NULL, - grout_ctx.zebra_notifs->sock_fd, - &grout_ctx.dg_t_zebra_update + event_add_event( + zrouter.master, zebra_read_notifications, NULL, 0, &grout_ctx.dg_t_zebra_update ); } From 966be2005842e63d6704445ff559245ad677b3aa Mon Sep 17 00:00:00 2001 From: Robin Jarry Date: Sat, 22 Aug 2026 16:38:00 +0200 Subject: [PATCH 4/6] api: send request header and payload in a single syscall MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Reviewed-by: Morten Brørup --- api/gr_api_client_impl.h | 57 ++++++++++++++++++++-------------------- 1 file changed, 28 insertions(+), 29 deletions(-) diff --git a/api/gr_api_client_impl.h b/api/gr_api_client_impl.h index 3411adb96..173298cd5 100644 --- a/api/gr_api_client_impl.h +++ b/api/gr_api_client_impl.h @@ -171,30 +171,6 @@ int gr_api_client_disconnect(struct gr_api_client *client) { return ret; } -static ssize_t send_all(const struct gr_api_client *c, const void *buf, size_t len) { - size_t remaining = len; - const char *ptr = buf; - ssize_t n; - - while (remaining > 0) { - n = send(c->sock_fd, ptr, remaining, MSG_NOSIGNAL); - if (n < 0) { - if (remaining < len && (errno == EAGAIN || errno == EWOULDBLOCK)) { - // Blocking mid-transmission, wait for socket to be writable again. - struct pollfd pfd = {.fd = c->sock_fd, .events = POLLOUT}; - poll(&pfd, 1, -1); - continue; - } - return n; - } - - ptr += n; - remaining -= n; - } - - return len; -} - static ssize_t recv_all(const struct gr_api_client *c, void *buf, size_t len) { size_t remaining = len; char *ptr = buf; @@ -238,12 +214,35 @@ long int gr_api_client_send( .payload_len = tx_len, .type = req_type, }; + struct iovec vec[2] = {{&req, sizeof(req)}, {(void *)tx_data, tx_len}}; + struct msghdr msg = {.msg_iov = vec, .msg_iovlen = tx_len > 0 ? 2 : 1}; + size_t total_len = vec[0].iov_len + vec[1].iov_len; + size_t remaining = total_len; - if (send_all(client, &req, sizeof(req)) < 0) - return -errno; - - if (tx_len > 0 && send_all(client, tx_data, tx_len) < 0) - return -errno; + while (msg.msg_iovlen > 0) { + int n = sendmsg(client->sock_fd, &msg, MSG_NOSIGNAL); + if (n < 0) { + if (remaining < total_len && (errno == EAGAIN || errno == EWOULDBLOCK)) { + // Blocking mid-transmission, wait for socket to be writable again. + struct pollfd pfd = {.fd = client->sock_fd, .events = POLLOUT}; + poll(&pfd, 1, -1); + continue; + } + return n; + } + remaining -= n; + while (n > 0) { + if ((int)msg.msg_iov->iov_len <= n) { + n -= msg.msg_iov->iov_len; + msg.msg_iov++; + msg.msg_iovlen--; + } else { + msg.msg_iov->iov_len -= n; + msg.msg_iov->iov_base = (char *)msg.msg_iov->iov_base + n; + n = 0; + } + } + } return req.id; } From 77fd18e0abc25871dbe09ed7863fb1528ec0c25a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morten=20Br=C3=B8rup?= Date: Sat, 22 Aug 2026 16:38:59 +0200 Subject: [PATCH 5/6] api: add read-ahead buffering in the client MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: Robin Jarry Reviewed-by: Morten Brørup --- api/gr_api.h | 2 +- api/gr_api_client_impl.h | 32 +++++++++++++++++++++++++------- 2 files changed, 26 insertions(+), 8 deletions(-) diff --git a/api/gr_api.h b/api/gr_api.h index 4ddca3f9e..5809fb39d 100644 --- a/api/gr_api.h +++ b/api/gr_api.h @@ -242,4 +242,4 @@ struct gr_api_event { // Receive an event notification. // Caller must free(*event) after use. // Returns 0 on success, negative errno on failure. -int gr_api_client_event_recv(const struct gr_api_client *, struct gr_api_event **); +int gr_api_client_event_recv(struct gr_api_client *, struct gr_api_event **); diff --git a/api/gr_api_client_impl.h b/api/gr_api_client_impl.h index 173298cd5..a82c08dcd 100644 --- a/api/gr_api_client_impl.h +++ b/api/gr_api_client_impl.h @@ -109,6 +109,9 @@ struct gr_api_client { int sock_fd; struct gr_hello_resp info; STAILQ_HEAD(, response) responses; + size_t recv_buf_off; + size_t recv_buf_len; + char recv_buf[sizeof(struct gr_api_response) + GR_API_MAX_MSG_LEN]; }; const struct gr_hello_resp *gr_api_client_info(const struct gr_api_client *client) { @@ -171,13 +174,31 @@ int gr_api_client_disconnect(struct gr_api_client *client) { return ret; } -static ssize_t recv_all(const struct gr_api_client *c, void *buf, size_t len) { +static ssize_t recv_all(struct gr_api_client *c, void *buf, size_t len) { size_t remaining = len; char *ptr = buf; ssize_t n; while (remaining > 0) { - n = recv(c->sock_fd, ptr, remaining, 0); + if (c->recv_buf_len > 0) { + // First, consume any data present in the buffer. + n = remaining < c->recv_buf_len ? remaining : c->recv_buf_len; + memcpy(ptr, c->recv_buf + c->recv_buf_off, n); + c->recv_buf_off += n; + c->recv_buf_len -= n; + ptr += n; + remaining -= n; + continue; + } + + // Then, receive as much data as possible in the buffer. + c->recv_buf_off = 0; + n = recv(c->sock_fd, c->recv_buf, sizeof(c->recv_buf), 0); + if (n > 0) { + c->recv_buf_len = n; + continue; + } + if (n == 0) { errno = ECONNRESET; return len - remaining; @@ -188,11 +209,8 @@ static ssize_t recv_all(const struct gr_api_client *c, void *buf, size_t len) { poll(&pfd, 1, -1); continue; } - return n; } - - ptr += n; - remaining -= n; + return n; } return len; @@ -329,7 +347,7 @@ int gr_api_client_recv( return -errno; } -int gr_api_client_event_recv(const struct gr_api_client *c, struct gr_api_event **event) { +int gr_api_client_event_recv(struct gr_api_client *c, struct gr_api_event **event) { const struct api_message *m; struct gr_api_event header; From 9dda73d1cf2046892e96dfceafd221214f2c5fc5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Morten=20Br=C3=B8rup?= Date: Sat, 22 Aug 2026 15:36:09 +0200 Subject: [PATCH 6/6] api: add GR_PING request type and perf test MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 Signed-off-by: Robin Jarry Reviewed-by: Morten Brørup --- api/gr_api.h | 4 +++ main/api.c | 12 +++++++ meson.build | 6 ++++ smoke/ping_perf.c | 90 +++++++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 112 insertions(+) create mode 100644 smoke/ping_perf.c diff --git a/api/gr_api.h b/api/gr_api.h index 5809fb39d..56d05d489 100644 --- a/api/gr_api.h +++ b/api/gr_api.h @@ -174,6 +174,7 @@ enum gr_main_requests : uint32_t { GR_LOG_LEVEL_SET, GR_EVENT_SUBSCRIBE, GR_EVENT_UNSUBSCRIBE, + GR_PING, }; // Client handshake with API version negotiation. @@ -243,3 +244,6 @@ struct gr_api_event { // Caller must free(*event) after use. // Returns 0 on success, negative errno on failure. int gr_api_client_event_recv(struct gr_api_client *, struct gr_api_event **); + +// Send an arbitrary payload and receive it back echoed from the server. +GR_REQ(GR_PING, struct gr_empty, struct gr_empty); diff --git a/main/api.c b/main/api.c index 225419418..7edd50f70 100644 --- a/main/api.c +++ b/main/api.c @@ -225,6 +225,17 @@ static struct api_out hello(const void *request, struct api_ctx *) { return api_out(0, sizeof(*resp), resp); } +static struct api_out ping(const void *request, struct api_ctx *ctx) { + uint32_t len = ctx->header.payload_len; + if (len == 0) + return api_out(0, 0, NULL); + void *resp = malloc(len); + if (resp == NULL) + return api_out(ENOMEM, 0, NULL); + memcpy(resp, request, len); + return api_out(0, len, resp); +} + static void disconnect_client(struct api_ctx *ctx) { assert(ctx != NULL); assert(ctx->bev != NULL); @@ -465,4 +476,5 @@ RTE_INIT(init) { api_handler(GR_EVENT_SUBSCRIBE, subscribe); api_handler(GR_EVENT_UNSUBSCRIBE, unsubscribe); api_handler(GR_HELLO, hello); + api_handler(GR_PING, ping); } diff --git a/meson.build b/meson.build index 144d1765a..33ee9fafc 100644 --- a/meson.build +++ b/meson.build @@ -206,6 +206,12 @@ executable( install: false, ) +executable( + 'ping_perf', files('smoke/ping_perf.c') + grout_header, + include_directories: api_inc, + install: false, +) + # docs/ must come after grcli_exe since man pages are generated using grcli --man subdir('docs') diff --git a/smoke/ping_perf.c b/smoke/ping_perf.c new file mode 100644 index 000000000..affd5157b --- /dev/null +++ b/smoke/ping_perf.c @@ -0,0 +1,90 @@ +// SPDX-License-Identifier: BSD-3-Clause +// Copyright (c) 2026 SmartShare Systems + +// clang-format off +#include +// clang-format on + +#include +#include +#include +#include + +static int ping(struct gr_api_client *c, uint32_t count, size_t payload_len) { + void *buf = NULL; + + if (payload_len > 0) { + buf = calloc(1, payload_len); + if (buf == NULL) + return -1; + } + + for (uint32_t i = 0; i < count; i++) { + void *resp = NULL; + if (gr_api_client_send_recv(c, GR_PING, payload_len, buf, &resp) < 0) { + perror("GR_PING"); + free(buf); + return -1; + } + free(resp); + } + + free(buf); + return 0; +} + +static void usage(const char *prog) { + fprintf(stderr, "Usage: %s [-s SOCK] [-n COUNT] [-l LEN]\n", prog); + fprintf(stderr, " -s SOCK API socket path (default: $GROUT_SOCK_PATH)\n"); + fprintf(stderr, " -n COUNT Number of ping calls (default: 10000)\n"); + fprintf(stderr, " -l LEN Payload length in bytes (default: 0)\n"); +} + +int main(int argc, char **argv) { + const char *sock_path = getenv("GROUT_SOCK_PATH"); + unsigned int count = 10000; + struct gr_api_client *c; + size_t payload_len = 0; + gr_clock_ns_t time; + float duration; + int ret; + int o; + + while ((o = getopt(argc, argv, "s:n:l:h")) != -1) { + switch (o) { + case 's': + sock_path = optarg; + break; + case 'n': + count = strtoul(optarg, NULL, 10); + break; + case 'l': + payload_len = strtoul(optarg, NULL, 10); + break; + case 'h': + default: + usage(argv[0]); + return o == 'h' ? EXIT_SUCCESS : EXIT_FAILURE; + } + } + if (sock_path == NULL) + sock_path = GR_DEFAULT_SOCK_PATH; + + c = gr_api_client_connect(sock_path); + if (c == NULL) { + perror("gr_api_client_connect"); + return EXIT_FAILURE; + } + + printf("performing %u ping calls (payload %zu bytes)\n", count, payload_len); + + time = gr_clock_ns(); + ret = ping(c, count, payload_len); + duration = (float)(gr_clock_ns() - time) / (float)GR_NS_PER_S; + + printf("total time: %.3f s (%.1f calls/s)\n", duration, (float)count / duration); + + gr_api_client_disconnect(c); + + return ret < 0 ? EXIT_FAILURE : EXIT_SUCCESS; +}