From 8e96f76a6040ac9d51d43dda083d0212fcf1f087 Mon Sep 17 00:00:00 2001 From: Stackie Jia Date: Sun, 6 Sep 2026 16:05:40 +0800 Subject: [PATCH 01/19] feat(multicast): share subscriptions within each worker --- docs/en/reference/configuration.md | 4 + docs/reference/configuration.md | 4 + e2e/test_multicast_shared.py | 243 +++++++++++++++++++ src/buffer_pool.c | 22 ++ src/buffer_pool.h | 4 + src/multicast.c | 363 ++++++++++++++++++----------- src/multicast.h | 33 +-- src/stream.c | 8 +- 8 files changed, 529 insertions(+), 152 deletions(-) create mode 100644 e2e/test_multicast_shared.py diff --git a/docs/en/reference/configuration.md b/docs/en/reference/configuration.md index b606c6e1..37d750fe 100644 --- a/docs/en/reference/configuration.md +++ b/docs/en/reference/configuration.md @@ -20,6 +20,10 @@ rtp2httpd [options] - `-m, --maxclients ` - Maximum concurrent clients (default: 5) - `-w, --workers ` - Number of worker processes (default: 1) +Within a worker process, requests with the same resolved multicast address, port, source filter address (SSM), effective upstream interface, and FEC port automatically share a multicast subscription without additional configuration. The main RTP/UDP socket and configured FEC socket are created once and released when the last subscribed client disconnects. Different worker processes still subscribe independently. Channel names, the `/rtp/` and `/udp/` path forms, and FCC server parameters do not affect this matching. + +Received data memory is shared through reference counting, while each client keeps its own send queue, RTP reorder state, and FEC recovery state. Slow clients still drop packets according to their own queue limits without pausing multicast reception for other clients. FCC unicast requests and transition state remain independent for each client; the transition to multicast reuses a matching subscription. + `--listen` can be specified multiple times to listen on multiple TCP addresses/ports or Unix sockets: ```bash diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 509282ef..2baca70d 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -20,6 +20,10 @@ rtp2httpd [选项] - `-m, --maxclients <数量>` - 最大并发客户端数 (默认: 5) - `-w, --workers <数量>` - 工作进程数 (默认: 1) +同一工作进程内,解析后的组播地址、端口、源过滤地址(SSM)、有效上游接口及 FEC 端口相同的请求会自动共享组播订阅,无需额外配置。RTP/UDP 主 socket 及配置的 FEC socket 只创建一份,最后一个订阅客户端断开后释放;不同工作进程之间仍独立订阅。频道名称、`/rtp/` 与 `/udp/` 路径形式以及 FCC 服务器参数不影响上述匹配。 + +接收的数据内存通过引用计数共享,每个客户端保留独立的发送队列、RTP 重排及 FEC 恢复状态。慢客户端仍按自身队列限制丢包,不会暂停其他客户端的组播接收。FCC 单播请求和切换状态按客户端独立维护,衔接组播时复用匹配的订阅。 + `--listen` 可以重复指定,用于同时监听多个 TCP 地址/端口或 Unix socket: ```bash diff --git a/e2e/test_multicast_shared.py b/e2e/test_multicast_shared.py new file mode 100644 index 00000000..affbccbe --- /dev/null +++ b/e2e/test_multicast_shared.py @@ -0,0 +1,243 @@ +"""Worker-local multicast sharing, subscriber lifetimes and FCC handoff.""" + +import http.client +import socket +import struct +import time +from contextlib import ExitStack, closing, contextmanager +from itertools import pairwise + +import pytest +from helpers import ( + LOOPBACK_IF, + MCAST_ADDR, + MockFCCServer, + MulticastSender, + R2HProcess, + find_free_port, + find_free_udp_port, +) + +pytestmark = pytest.mark.multicast + + +@pytest.fixture +def shared_source_r2h(r2h_binary): + """Isolate source lifetime/log assertions with exactly one worker.""" + r2h = R2HProcess( + r2h_binary, + find_free_port(), + extra_args=["-v", "4", "-w", "1", "-m", "100", "-r", LOOPBACK_IF, "-S", "-Z"], + ) + r2h.start() + yield r2h + r2h.stop() + + +@contextmanager +def _stream(r2h, path, *, headers=None, slow=False): + connection = http.client.HTTPConnection("127.0.0.1", r2h.port, timeout=10) + response = None + try: + connection.connect() + if slow: + connection.sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 4096) + connection.request("GET", path, headers=headers or {}) + response = connection.getresponse() + assert response.status == 200, r2h.read_log() + yield response + finally: + if response: + response.close() + connection.close() + + +def _wait_log(r2h, message, count=1): + deadline = time.monotonic() + 10 + while time.monotonic() < deadline: + log = r2h.read_log() + if log.count(message) >= count: + return + time.sleep(0.02) + pytest.fail(f"Missing {count} occurrences of {message!r}:\n{r2h.read_log()}") + + +def _read_markers(response, packets=128): + body = response.read(188 * packets) + assert len(body) == 188 * packets, "Stream ended before the requested data arrived" + assert all(body[i] == 0x47 for i in range(0, len(body), 188)) + return [struct.unpack_from("!H", body, i + 4)[0] for i in range(0, len(body), 188)] + + +@pytest.mark.parametrize( + "close_first,rtp,use_fec", [(0, True, False), (1, True, False), (0, False, False), (1, True, True)] +) +def test_shared_source_lifetime(shared_source_r2h, close_first, rtp, use_fec): + """Either subscriber can leave; the survivor and a later rejoin remain valid.""" + r2h = shared_source_r2h + sender = MulticastSender(pps=400, unique_payloads=True, encapsulate_rtp=rtp, reorder_distance=4 if rtp else 0) + query = f"?fec={find_free_udp_port()}" if use_fec else "" + path = f"/rtp/{MCAST_ADDR}:{sender.port}{query}" + sender.start() + try: + with ExitStack() as stack: + clients = [] + contexts = [] + for prefix in ("/rtp/", "/udp/"): + context = ExitStack() + stack.enter_context(context) + contexts.append(context) + # An explicit interface equal to the global default still shares. + separator = "&" if query else "?" + url = path.replace("/rtp/", prefix) + f"{separator}r2h-ifname={LOOPBACK_IF}" + clients.append(context.enter_context(_stream(r2h, url))) + _wait_log(r2h, "refs=2") + assert r2h.read_log().count("Multicast: Successfully joined group") == 1 + assert r2h.read_log().count("FEC: Successfully joined group") == int(use_fec) + for client in clients: + markers = _read_markers(client) + assert all((b - a) & 0xFFFF < 0x8000 for a, b in pairwise(markers)) + contexts[close_first].close() + _wait_log(r2h, "Subscriber detached") + assert "Last subscriber left" not in r2h.read_log() + _read_markers(clients[1 - close_first], packets=1024) + _wait_log(r2h, "Last subscriber left") + with _stream(r2h, path) as client: + _read_markers(client) + assert r2h.read_log().count("Multicast: Successfully joined group") == 2 + finally: + sender.stop() + + +@pytest.mark.parametrize("difference", ["group", "port", "fec", "source"]) +def test_different_sources_are_isolated(shared_source_r2h, difference): + r2h = shared_source_r2h + first = MulticastSender(pps=300) + second = MulticastSender( + addr="239.255.0.2" if difference == "group" else MCAST_ADDR, + port=first.port if difference == "group" else 0, + pps=300, + ) + first.start() + second.start() + path = f"/rtp/{MCAST_ADDR}:{first.port}" + if difference == "group": + # Use the same port to ensure group address is part of the key. + other = f"/rtp/{second.addr}:{first.port}" + elif difference == "port": + other = f"/rtp/{MCAST_ADDR}:{second.port}" + elif difference == "fec": + other = path + f"?fec={find_free_udp_port()}" + else: + other = f"/rtp/127.0.0.1@{MCAST_ADDR}:{first.port}" + try: + with _stream(r2h, path) as client: + with _stream(r2h, other) as other_client: + _read_markers(other_client) + assert r2h.read_log().count("Multicast: Successfully joined group") == 2 + _read_markers(client) + finally: + first.stop() + second.stop() + + +def test_invalid_interface_does_not_reuse_source(shared_source_r2h): + r2h = shared_source_r2h + sender = MulticastSender(pps=300) + sender.start() + path = f"/rtp/{MCAST_ADDR}:{sender.port}" + try: + with _stream(r2h, path) as client: + with closing(http.client.HTTPConnection("127.0.0.1", r2h.port, timeout=5)) as other: + other.request("GET", path + "?r2h-ifname=r2h-missing") + assert other.getresponse().status == 503 + _read_markers(client) + assert r2h.read_log().count("Multicast: Successfully joined group") == 1 + finally: + sender.stop() + + +def test_shared_source_timeout_releases_all_subscribers(shared_source_r2h): + r2h = shared_source_r2h + path = f"/rtp/{MCAST_ADDR}:{find_free_udp_port()}" + with ExitStack() as stack: + clients = [ + stack.enter_context(closing(http.client.HTTPConnection("127.0.0.1", r2h.port, timeout=5))) for _ in range(2) + ] + for client in clients: + client.request("GET", path) + for client in clients: + response = client.getresponse() + assert response.status == 503 + response.read() + response.close() + _wait_log(r2h, "Last subscriber left") + assert r2h.read_log().count("Multicast: Successfully joined group") == 1 + + +def test_slow_subscriber_does_not_block_shared_stream(shared_source_r2h): + r2h = shared_source_r2h + sender = MulticastSender(pps=2000, unique_payloads=True) + sender.start() + path = f"/rtp/{MCAST_ADDR}:{sender.port}" + try: + with _stream(r2h, path, slow=True), _stream(r2h, path) as fast: + deadline = time.monotonic() + 10 + while time.monotonic() < deadline and "Backpressure: dropping" not in r2h.read_log(): + markers = _read_markers(fast, packets=1024) + assert all((b - a) & 0xFFFF < 0x8000 for a, b in pairwise(markers)) + assert "Backpressure: dropping" in r2h.read_log() + assert r2h.read_log().count("Multicast: Successfully joined group") == 1 + _read_markers(fast, packets=1024) + finally: + sender.stop() + + +def test_snapshot_fallback_keeps_other_subscriber_alive(shared_source_r2h): + r2h = shared_source_r2h + sender = MulticastSender(pps=300) + sender.start() + path = f"/rtp/{MCAST_ADDR}:{sender.port}" + try: + with _stream(r2h, path) as client: + with _stream(r2h, path, headers={"Accept": "image/jpeg"}) as snapshot: + assert snapshot.getheader("Content-Type") == "video/mp2t" + _read_markers(snapshot) + assert r2h.read_log().count("Multicast: Successfully joined group") == 1 + _read_markers(client) + finally: + sender.stop() + + +@pytest.mark.fcc +@pytest.mark.parametrize("protocol", ["telecom", "huawei"]) +def test_fcc_clients_share_existing_multicast(shared_source_r2h, protocol): + """Each FCC client negotiates independently, then continues on shared multicast.""" + r2h = shared_source_r2h + sender = MulticastSender(pps=200, unique_payloads=True) + fcc = MockFCCServer(protocol=protocol, unicast_pps=1000, sync_after=30) + sender.start() + fcc.start() + path = f"/rtp/{MCAST_ADDR}:{sender.port}" + suffix = "" if protocol == "telecom" else "&fcc-type=huawei" + fcc_path = path + f"?fcc=127.0.0.1:{fcc.port}{suffix}" + try: + with _stream(r2h, path) as direct, _stream(r2h, fcc_path) as first, _stream(r2h, fcc_path) as second: + _wait_log(r2h, "refs=3") + _wait_log(r2h, "Reached termination sequence", count=2) + assert fcc.requests_received >= 2 # Protocol requests may be retransmitted. + assert len(set(fcc.request_client_addrs)) == 2 + assert r2h.read_log().count("Multicast: Successfully joined group") == 1 + fcc.stop() + for client in (direct, first, second): + # FCC sends unmarked TS; marked packets prove multicast delivery. + deadline = time.monotonic() + 10 + while time.monotonic() < deadline: + if any(marker != 0xFFFF for marker in _read_markers(client)): + break + else: + pytest.fail("No multicast payload after FCC handoff") + _read_markers(client, packets=1024) + finally: + fcc.stop() + sender.stop() diff --git a/src/buffer_pool.c b/src/buffer_pool.c index 5da45c4a..94a32733 100644 --- a/src/buffer_pool.c +++ b/src/buffer_pool.c @@ -174,6 +174,12 @@ void buffer_ref_put(buffer_ref_t *ref) { ref->refcount--; if (ref->refcount <= 0) { + if (ref->owner) { + buffer_ref_t *owner = ref->owner; + free(ref); + buffer_ref_put(owner); + return; + } if (ref->type == BUFFER_TYPE_FILE) { if (ref->file_fd >= 0) { close(ref->file_fd); @@ -196,6 +202,22 @@ void buffer_ref_put(buffer_ref_t *ref) { } } +buffer_ref_t *buffer_ref_view(buffer_ref_t *ref) { + if (!ref || ref->type != BUFFER_TYPE_MEMORY) + return NULL; + buffer_ref_t *view = calloc(1, sizeof(*view)); + if (!view) + return NULL; + view->type = BUFFER_TYPE_MEMORY; + view->data = ref->data; + view->data_size = ref->data_size; + view->data_offset = ref->data_offset; + view->refcount = 1; + view->owner = ref->owner ? ref->owner : ref; + buffer_ref_get(view->owner); + return view; +} + buffer_ref_t *buffer_pool_alloc_from(buffer_pool_t *pool) { if (!pool) return NULL; diff --git a/src/buffer_pool.h b/src/buffer_pool.h index ba0a51e6..ad96f5a7 100644 --- a/src/buffer_pool.h +++ b/src/buffer_pool.h @@ -50,6 +50,7 @@ typedef struct buffer_ref_s { }; int refcount; /* Reference count */ struct buffer_pool_segment_s *segment; /* Segment this buffer belongs to (BUFFER_TYPE_MEMORY) */ + struct buffer_ref_s *owner; /* Non-NULL for a view sharing another buffer's immutable data */ /* Union: buffer is either in free list OR in send queue, never both */ union { @@ -103,6 +104,9 @@ void buffer_pool_cleanup(buffer_pool_t *pool); void buffer_pool_update_stats(buffer_pool_t *pool); void buffer_ref_get(buffer_ref_t *ref); void buffer_ref_put(buffer_ref_t *ref); +/* Share data while keeping offsets, send links and completion IDs independent. + * The returned view owns a reference to the backing buffer; release with put. */ +buffer_ref_t *buffer_ref_view(buffer_ref_t *ref); buffer_ref_t *buffer_pool_alloc_from(buffer_pool_t *pool); buffer_ref_t *buffer_pool_alloc(void); buffer_ref_t *buffer_pool_alloc_control(void); diff --git a/src/multicast.c b/src/multicast.c index 0f4488f6..956af457 100644 --- a/src/multicast.c +++ b/src/multicast.c @@ -78,7 +78,7 @@ static uint16_t calculate_checksum(const void *data, size_t len) { static int create_igmp_raw_socket(service_t *service) { int raw_sock; - const char *upstream_if = get_upstream_interface_for_multicast(service ? service->ifname : NULL); + const char *upstream_if = service->ifname; raw_sock = socket(AF_INET, SOCK_RAW, IPPROTO_IGMP); if (raw_sock < 0) { @@ -140,7 +140,7 @@ static int mcast_group_op(int sock, service_t *service, int is_join, const char return -1; } - upstream_if = get_upstream_interface_for_multicast(service->ifname); + upstream_if = service->ifname; if (upstream_if && upstream_if[0] != '\0') { ifindex = if_nametoindex(upstream_if); if (ifindex == 0) { @@ -216,7 +216,7 @@ static int join_mcast_group(service_t *service, int is_fec) { #endif /* Determine which interface to use */ - upstream_if = get_upstream_interface_for_multicast(service ? service->ifname : NULL); + upstream_if = service->ifname; bind_to_upstream_interface(sock, upstream_if); /* Prepare bind address with appropriate port */ @@ -373,178 +373,277 @@ static int rejoin_mcast_group(service_t *service) { return result; } -/* - * Multicast session management functions - */ +/* Each worker owns its registry. Socket membership and timers live here, + * independently of the client that first requested the source. */ +struct mcast_source_s { + int sock; + int fec_sock; + int epoll_fd; + unsigned int ifindex; + unsigned int refs; + int failed; + int64_t last_data_time; + int64_t last_rejoin_time; + int rejoin_unsupported_warned; + service_t *service; /* Deep copy with the effective multicast interface frozen */ + mcast_session_t *subscribers; + mcast_source_t *next; +}; + +static mcast_source_t *mcast_sources; + +/* Compare resolved endpoints, never channel names, URL spelling or sockaddr + * padding. The source port does not participate in an IGMP source filter. */ +static int mcast_address_equal(const struct addrinfo *a, const struct addrinfo *b, int compare_port) { + if (!a || !b) + return a == b; + if (a->ai_family != b->ai_family) + return 0; + if (a->ai_family == AF_INET) { + const struct sockaddr_in *sa = (const struct sockaddr_in *)(uintptr_t)a->ai_addr; + const struct sockaddr_in *sb = (const struct sockaddr_in *)(uintptr_t)b->ai_addr; + return sa->sin_addr.s_addr == sb->sin_addr.s_addr && (!compare_port || sa->sin_port == sb->sin_port); + } + if (a->ai_family == AF_INET6) { + const struct sockaddr_in6 *sa = (const struct sockaddr_in6 *)(uintptr_t)a->ai_addr; + const struct sockaddr_in6 *sb = (const struct sockaddr_in6 *)(uintptr_t)b->ai_addr; + return memcmp(&sa->sin6_addr, &sb->sin6_addr, sizeof(sa->sin6_addr)) == 0 && + sa->sin6_scope_id == sb->sin6_scope_id && (!compare_port || sa->sin6_port == sb->sin6_port); + } + return 0; +} + +static void mcast_source_free(mcast_source_t *source) { + if (source->sock >= 0) + worker_cleanup_socket_from_epoll(source->epoll_fd, source->sock); + if (source->fec_sock >= 0) + worker_cleanup_socket_from_epoll(source->epoll_fd, source->fec_sock); + service_free(source->service); + free(source); +} void mcast_session_init(mcast_session_t *session) { - memset(session, 0, sizeof(mcast_session_t)); + memset(session, 0, sizeof(*session)); session->initialized = 1; session->sock = -1; + session->fec_sock = -1; } -void mcast_session_cleanup(mcast_session_t *session, int epoll_fd) { - if (!session || !session->initialized) { +void mcast_session_cleanup(mcast_session_t *session) { + if (!session || !session->initialized) return; - } - if (session->sock >= 0) { - worker_cleanup_socket_from_epoll(epoll_fd, session->sock); - session->sock = -1; - logger(LOG_DEBUG, "Multicast: Socket closed"); + mcast_source_t *source = session->source; + if (source) { + mcast_session_t **subscriber = &source->subscribers; + while (*subscriber && *subscriber != session) + subscriber = &(*subscriber)->next; + if (*subscriber) + *subscriber = session->next; + source->refs--; + logger(LOG_DEBUG, "Multicast: Subscriber detached (fd=%d, refs=%u)", source->sock, source->refs); + if (source->refs == 0) { + mcast_source_t **entry = &mcast_sources; + while (*entry && *entry != source) + entry = &(*entry)->next; + if (*entry) + *entry = source->next; + logger(LOG_DEBUG, "Multicast: Last subscriber left, releasing shared source (fd=%d)", source->sock); + mcast_source_free(source); + } else { + /* Reassign event dispatch before the departing connection is freed. */ + fdmap_set(source->sock, source->subscribers->ctx->conn); + if (source->fec_sock >= 0) + fdmap_set(source->fec_sock, source->subscribers->ctx->conn); + } } - + session->source = NULL; + session->ctx = NULL; + session->next = NULL; + session->sock = -1; + session->fec_sock = -1; session->initialized = 0; } int mcast_session_join(mcast_session_t *session, stream_context_t *ctx) { - if (!session || !session->initialized) { + if (!session || !session->initialized || !ctx || !ctx->service || !ctx->service->addr) return -1; - } + if (session->source) + return 0; - if (session->sock >= 0) { - return 0; /* Already joined */ + service_t *service = ctx->service; + const char *ifname = get_upstream_interface_for_multicast(service->ifname); + unsigned int ifindex = 0; + if (ifname && ifname[0]) { + ifindex = if_nametoindex(ifname); + if (!ifindex) { + logger(LOG_ERROR, "Multicast: interface %s does not exist", ifname); + return -1; + } + } else if (service->addr->ai_family == AF_INET6) { + ifindex = ((const struct sockaddr_in6 *)(uintptr_t)service->addr->ai_addr)->sin6_scope_id; } - /* Join main RTP multicast group */ - int sock = join_mcast_group(ctx->service, 0); - if (sock < 0) { - return -1; + mcast_source_t *source; + for (source = mcast_sources; source; source = source->next) { + if (!source->failed && source->epoll_fd == ctx->epoll_fd && source->ifindex == ifindex && + source->service->fec_port == service->fec_port && + mcast_address_equal(source->service->addr, service->addr, 1) && + mcast_address_equal(source->service->msrc_addr, service->msrc_addr, 0)) + break; } - /* Register socket with poller */ - if (poller_add(ctx->epoll_fd, sock, POLLER_IN) < 0) { - logger(LOG_ERROR, "Multicast: Failed to add socket to poller: %s", strerror(errno)); - close(sock); - return -1; - } - fdmap_set(sock, ctx->conn); - logger(LOG_DEBUG, "Multicast: Socket registered with poller"); - - /* Reset timeout and rejoin timers */ - int64_t now = get_time_ms(); - session->last_data_time = now; - session->last_rejoin_time = now; - session->sock = sock; - - /* Join FEC multicast group if configured */ - if (ctx->fec.initialized && fec_is_enabled(&ctx->fec)) { - int fec_sock = join_mcast_group(ctx->service, 1); - if (fec_sock >= 0) { - if (poller_add(ctx->epoll_fd, fec_sock, POLLER_IN) < 0) { - logger(LOG_ERROR, "FEC: Failed to add socket to poller: %s", strerror(errno)); - close(fec_sock); - } else { - ctx->fec.sock = fec_sock; - fdmap_set(fec_sock, ctx->conn); + if (!source) { + source = calloc(1, sizeof(*source)); + if (!source) + return -1; + source->sock = -1; + source->fec_sock = -1; + source->epoll_fd = ctx->epoll_fd; + source->ifindex = ifindex; + source->service = service_clone(service); + if (!source->service) { + mcast_source_free(source); + return -1; + } + free(source->service->ifname); + source->service->ifname = strdup(ifname ? ifname : ""); + if (!source->service->ifname) { + mcast_source_free(source); + return -1; + } + source->sock = join_mcast_group(source->service, 0); + if (source->sock < 0 || poller_add(ctx->epoll_fd, source->sock, POLLER_IN) < 0) { + mcast_source_free(source); + return -1; + } + if (service->fec_port > 0) { + source->fec_sock = join_mcast_group(source->service, 1); + if (source->fec_sock >= 0 && poller_add(ctx->epoll_fd, source->fec_sock, POLLER_IN) < 0) { + close(source->fec_sock); + source->fec_sock = -1; } } - } - + source->last_data_time = get_time_ms(); + source->last_rejoin_time = source->last_data_time; + source->next = mcast_sources; + mcast_sources = source; + } else { + logger(LOG_DEBUG, "Multicast: Reusing shared source (fd=%d)", source->sock); + } + + session->source = source; + session->ctx = ctx; + session->sock = source->sock; + session->fec_sock = source->fec_sock; + session->next = source->subscribers; + source->subscribers = session; + source->refs++; + fdmap_set(source->sock, ctx->conn); + if (source->fec_sock >= 0) + fdmap_set(source->fec_sock, ctx->conn); + logger(LOG_DEBUG, "Multicast: Subscriber attached (fd=%d, refs=%u)", source->sock, source->refs); return 0; } -int mcast_session_handle_event(mcast_session_t *session, stream_context_t *ctx, int64_t now) { - if (!session || !session->initialized || session->sock < 0) { - return -1; - } - - /* Drain all available packets from the socket. This is required for - * edge-triggered pollers (epoll EPOLLET / kqueue EV_CLEAR) where the read event fires - * only once per data arrival transition and won't re-trigger while - * unread data remains in the socket buffer. */ - for (;;) { - /* Allocate buffer from pool */ - buffer_ref_t *recv_buf = buffer_pool_alloc(); - if (!recv_buf) { - logger(LOG_DEBUG, "Multicast: Buffer pool exhausted, dropping packet"); - session->last_data_time = now; - /* Drain socket to prevent event loop spinning */ - uint8_t dummy[BUFFER_POOL_BUFFER_SIZE]; - recv(session->sock, dummy, sizeof(dummy), 0); - return 0; - } - - /* Receive into buffer */ - int actualr = recv(session->sock, recv_buf->data, BUFFER_POOL_BUFFER_SIZE, 0); - if (actualr < 0) { - buffer_ref_put(recv_buf); - if (errno != EAGAIN) - logger(LOG_DEBUG, "Multicast: Receive failed: %s", strerror(errno)); - break; /* No more data available */ - } - - session->last_data_time = now; - recv_buf->data_size = (size_t)actualr; - - int result = 0; - - /* Handle based on FCC state (if FCC initialized) */ - if (!ctx->fcc.initialized) { - /* Direct multicast without FCC - forward to client */ - stream_process_rtp_payload(ctx, recv_buf, STREAM_MEDIA_ORIGIN_MULTICAST); - buffer_ref_put(recv_buf); - continue; /* Read next packet */ - } +static void mcast_deliver_packet(mcast_session_t *session, buffer_ref_t *packet) { + stream_context_t *ctx = session->ctx; + if (session->failed || ctx->conn->state == CONN_CLOSING) + return; - switch (ctx->fcc.state) { - case FCC_STATE_MCAST_ACTIVE: - result = fcc_handle_mcast_active(ctx, recv_buf); - break; + /* Queue linkage, RTP offsets and zerocopy completion IDs are mutable and + * must never be shared between clients. Only the backing data is shared. */ + buffer_ref_t *view; + if (session->source->refs == 1) { + /* Preserve the allocation-free descriptor path for a lone subscriber. */ + view = packet; + buffer_ref_get(view); + } else { + view = buffer_ref_view(packet); + } + if (!view) + return; /* A local drop must not interrupt other subscribers. */ + int result = 0; + if (!ctx->fcc.initialized) { + stream_process_rtp_payload(ctx, view, STREAM_MEDIA_ORIGIN_MULTICAST); + } else if (ctx->fcc.state == FCC_STATE_MCAST_ACTIVE) { + result = fcc_handle_mcast_active(ctx, view); + } else if (ctx->fcc.state == FCC_STATE_MCAST_REQUESTED) { + result = fcc_handle_mcast_transition(ctx, view); + } + buffer_ref_put(view); + if (result < 0) + session->failed = 1; +} - case FCC_STATE_MCAST_REQUESTED: - result = fcc_handle_mcast_transition(ctx, recv_buf); - break; +int mcast_session_handle_event(mcast_session_t *session, int fd, int64_t now) { + mcast_source_t *source = session->source; + if (!source) + return -1; - default: - logger(LOG_DEBUG, "Received multicast data in unexpected FCC state: %d", ctx->fcc.state); + /* Drain to EAGAIN for edge-triggered pollers, including on pool exhaustion. + * All subscribers are detached by the worker outside this delivery loop. */ + for (;;) { + buffer_ref_t *packet = fd == source->sock ? buffer_pool_alloc() : NULL; + uint8_t discard[BUFFER_POOL_BUFFER_SIZE]; + void *data = packet ? packet->data : discard; + ssize_t len = recv(fd, data, BUFFER_POOL_BUFFER_SIZE, 0); + if (len < 0) { + int recv_errno = errno; + buffer_ref_put(packet); + if (recv_errno == EINTR) + continue; + if (recv_errno != EAGAIN && recv_errno != EWOULDBLOCK) { + logger(LOG_ERROR, "Multicast: Receive failed: %s", strerror(recv_errno)); + source->failed = 1; + } break; } - - buffer_ref_put(recv_buf); - - if (result != 0) - return result; + if (fd == source->sock) { + source->last_data_time = now; + if (packet) { + packet->data_size = (size_t)len; + for (mcast_session_t *subscriber = source->subscribers; subscriber; subscriber = subscriber->next) + mcast_deliver_packet(subscriber, packet); + } + } else { + for (mcast_session_t *subscriber = source->subscribers; subscriber; subscriber = subscriber->next) { + if (!subscriber->failed && subscriber->ctx->conn->state != CONN_CLOSING) + fec_process_packet(&subscriber->ctx->fec, data, (int)len); + } + } + buffer_ref_put(packet); } - return 0; } -int mcast_session_tick(mcast_session_t *session, service_t *service, int64_t now) { - if (!session || !session->initialized || session->sock < 0) { +int mcast_session_tick(mcast_session_t *session, int64_t now) { + if (!session || !session->initialized || !session->source) return 0; - } + mcast_source_t *source = session->source; + service_t *service = source->service; + if (session->failed || source->failed) + return -1; - /* Periodic multicast rejoin (if enabled). - * Raw-socket rejoin is IGMP (IPv4) only; for IPv6 groups an MLD equivalent - * is not implemented yet, so warn once and skip. */ + /* Periodic rejoin and timeout belong to the source, not each subscriber. */ if (config.mcast_rejoin_interval > 0) { if (service->addr->ai_family != AF_INET) { - if (!session->rejoin_unsupported_warned) { + if (!source->rejoin_unsupported_warned) { logger(LOG_WARN, "Multicast: mcast-rejoin-interval is not supported for IPv6 groups (no MLD " "raw-socket rejoin), skipping periodic rejoin"); - session->rejoin_unsupported_warned = 1; - } - } else { - int64_t elapsed_ms = now - session->last_rejoin_time; - if (elapsed_ms >= config.mcast_rejoin_interval * 1000) { - logger(LOG_DEBUG, "Multicast: Periodic rejoin (interval: %d seconds)", config.mcast_rejoin_interval); - - if (rejoin_mcast_group(service) == 0) { - session->last_rejoin_time = now; - } else { - logger(LOG_ERROR, "Multicast: Failed to rejoin group, will retry next interval"); - } + source->rejoin_unsupported_warned = 1; } + } else if (now - source->last_rejoin_time >= (int64_t)config.mcast_rejoin_interval * 1000) { + source->last_rejoin_time = now; + logger(LOG_DEBUG, "Multicast: Periodic rejoin (interval: %d seconds)", config.mcast_rejoin_interval); + if (rejoin_mcast_group(service) < 0) + logger(LOG_ERROR, "Multicast: Failed to rejoin group, will retry next interval"); } } - - /* Check for multicast stream timeout */ - int64_t elapsed_ms = now - session->last_data_time; - if (elapsed_ms >= MCAST_TIMEOUT_SEC * 1000) { - logger(LOG_ERROR, "Multicast: No data received for %d seconds, closing connection", MCAST_TIMEOUT_SEC); + if (now - source->last_data_time >= MCAST_TIMEOUT_SEC * 1000) { + logger(LOG_ERROR, "Multicast: No data received for %d seconds, closing subscribers", MCAST_TIMEOUT_SEC); + source->failed = 1; return -1; } - return 0; } diff --git a/src/multicast.h b/src/multicast.h index 6bfc3ae1..5a3da54a 100644 --- a/src/multicast.h +++ b/src/multicast.h @@ -7,17 +7,20 @@ /* Forward declarations */ typedef struct stream_context_s stream_context_t; typedef struct connection_s connection_t; +typedef struct mcast_source_s mcast_source_t; struct buffer_ref_s; /** - * Multicast session context - encapsulates all multicast-related state + * Per-client subscription to a source shared within this worker. */ typedef struct mcast_session_s { - int initialized; /* Flag: session has been initialized */ - int sock; /* Multicast socket (-1 if not joined) */ - int64_t last_data_time; /* Timestamp of last received data (ms) */ - int64_t last_rejoin_time; /* Timestamp of last periodic rejoin (ms) */ - int rejoin_unsupported_warned; /* Warn-once flag for IPv6 rejoin no-op */ + int initialized; + int sock; /* Borrowed main socket (-1 if not subscribed) */ + int fec_sock; /* Borrowed FEC socket; owned by the shared source */ + int failed; /* Subscriber-local FCC failure */ + mcast_source_t *source; + stream_context_t *ctx; + struct mcast_session_s *next; } mcast_session_t; /** @@ -27,14 +30,13 @@ typedef struct mcast_session_s { void mcast_session_init(mcast_session_t *session); /** - * Cleanup multicast session and release resources + * Detach a subscriber; the last subscriber releases the shared sockets. * @param session Multicast session to cleanup - * @param epoll_fd Epoll file descriptor for socket cleanup */ -void mcast_session_cleanup(mcast_session_t *session, int epoll_fd); +void mcast_session_cleanup(mcast_session_t *session); /** - * Join multicast group and register with epoll + * Attach to an existing matching source or join and register a new one. * @param session Multicast session * @param ctx Stream context (for service, epoll_fd, conn) * @return 0 on success, -1 on error @@ -42,21 +44,20 @@ void mcast_session_cleanup(mcast_session_t *session, int epoll_fd); int mcast_session_join(mcast_session_t *session, stream_context_t *ctx); /** - * Handle multicast socket events + * Receive once and distribute to all subscribers of this source. * @param session Multicast session - * @param ctx Stream context + * @param fd Ready main or FEC socket * @param now Current timestamp in milliseconds - * @return processed bytes on success, -1 on error + * @return 0 on success, -1 for an invalid session; delivery errors are checked by tick */ -int mcast_session_handle_event(mcast_session_t *session, stream_context_t *ctx, int64_t now); +int mcast_session_handle_event(mcast_session_t *session, int fd, int64_t now); /** * Periodic tick for multicast session (timeout/rejoin checks) * @param session Multicast session - * @param service Service configuration * @param now Current timestamp in milliseconds * @return 0 on success, -1 if connection should be closed (timeout) */ -int mcast_session_tick(mcast_session_t *session, service_t *service, int64_t now); +int mcast_session_tick(mcast_session_t *session, int64_t now); #endif /* __MULTICAST_H__ */ diff --git a/src/stream.c b/src/stream.c index 064e4166..35b98246 100644 --- a/src/stream.c +++ b/src/stream.c @@ -378,8 +378,8 @@ int stream_handle_fd_event(stream_context_t *ctx, int fd, uint32_t events, int64 } /* Process multicast socket events */ - if (ctx->mcast.initialized && ctx->mcast.sock >= 0 && fd == ctx->mcast.sock) { - return mcast_session_handle_event(&ctx->mcast, ctx, now); + if (ctx->mcast.initialized && fd >= 0 && (fd == ctx->mcast.sock || fd == ctx->mcast.fec_sock)) { + return mcast_session_handle_event(&ctx->mcast, fd, now); } /* Process FEC socket events - drain all available packets for @@ -647,7 +647,7 @@ int stream_tick(stream_context_t *ctx, int64_t now) { return 0; /* Multicast session tick (rejoin and timeout checks) */ - if (mcast_session_tick(&ctx->mcast, ctx->service, now) < 0) { + if (mcast_session_tick(&ctx->mcast, now) < 0) { return -1; /* Multicast timeout */ } @@ -706,7 +706,7 @@ int stream_context_cleanup(stream_context_t *ctx) { fcc_session_cleanup(&ctx->fcc, ctx->service, ctx->epoll_fd); /* Clean up multicast session */ - mcast_session_cleanup(&ctx->mcast, ctx->epoll_fd); + mcast_session_cleanup(&ctx->mcast); /* Clean up HTTP proxy session (always synchronous) */ http_proxy_session_cleanup(&ctx->http_proxy); From a15ff5fb594360d7980175510055c5ec0796387e Mon Sep 17 00:00:00 2001 From: Stackie Jia Date: Sun, 6 Sep 2026 17:40:05 +0800 Subject: [PATCH 02/19] perf(multicast): share reordered batches and immutable sendfile payloads --- docs/en/reference/configuration.md | 6 +- docs/reference/configuration.md | 6 +- e2e/test_multicast_shared.py | 118 +++++++++++++++++++ src/buffer_pool.c | 74 ++++++++++++ src/buffer_pool.h | 8 ++ src/connection.c | 4 +- src/connection.h | 8 +- src/multicast.c | 183 ++++++++++++++++++++++++++++- src/multicast.h | 2 + src/rtp_reorder.c | 8 +- src/rtp_reorder.h | 3 + src/stream.c | 6 +- src/stream.h | 5 + src/worker.c | 3 + src/zerocopy.c | 60 ++++++++-- src/zerocopy.h | 2 + 16 files changed, 472 insertions(+), 24 deletions(-) diff --git a/docs/en/reference/configuration.md b/docs/en/reference/configuration.md index 37d750fe..612fa106 100644 --- a/docs/en/reference/configuration.md +++ b/docs/en/reference/configuration.md @@ -22,7 +22,11 @@ rtp2httpd [options] Within a worker process, requests with the same resolved multicast address, port, source filter address (SSM), effective upstream interface, and FEC port automatically share a multicast subscription without additional configuration. The main RTP/UDP socket and configured FEC socket are created once and released when the last subscribed client disconnects. Different worker processes still subscribe independently. Channel names, the `/rtp/` and `/udp/` path forms, and FCC server parameters do not affect this matching. -Received data memory is shared through reference counting, while each client keeps its own send queue, RTP reorder state, and FEC recovery state. Slow clients still drop packets according to their own queue limits without pausing multicast reception for other clients. FCC unicast requests and transition state remain independent for each client; the transition to multicast reuses a matching subscription. +Regular multicast parses and reorders RTP once per shared source, combines payloads into batches of approximately 64 KiB, and distributes them through reference counting. Each client keeps its own send queue and send offset while sharing the underlying batch data. Slow clients still drop packets according to their own queue limits without pausing multicast reception for other clients. Partial batches are sent at the next worker timer check after 100 ms, avoiding long waits for low-bitrate streams. + +On Linux, when multiple clients share a full batch, rtp2httpd first attempts to store it in an immutable anonymous memory file and use `sendfile` to share its kernel data pages. The file is never rewritten when pool buffers are reused, preserving data still in transit. Unsupported systems or insufficient resources automatically fall back to regular memory sends. This optimization requires no extra configuration and does not depend on `zerocopy-on-send`. + +FCC unicast requests and transition state remain independent for each client, and the transition to multicast reuses a matching subscription. A client joins shared batch delivery after its unicast and transition data have been processed and its sequence position matches the shared stream. Snapshot processing and FEC recovery retain their own processing state. If a FEC port is configured or FEC packets appear in the main multicast stream, that source keeps its shared sockets but uses independent reorder and FEC recovery paths for each client. `--listen` can be specified multiple times to listen on multiple TCP addresses/ports or Unix sockets: diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 2baca70d..dd788314 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -22,7 +22,11 @@ rtp2httpd [选项] 同一工作进程内,解析后的组播地址、端口、源过滤地址(SSM)、有效上游接口及 FEC 端口相同的请求会自动共享组播订阅,无需额外配置。RTP/UDP 主 socket 及配置的 FEC socket 只创建一份,最后一个订阅客户端断开后释放;不同工作进程之间仍独立订阅。频道名称、`/rtp/` 与 `/udp/` 路径形式以及 FCC 服务器参数不影响上述匹配。 -接收的数据内存通过引用计数共享,每个客户端保留独立的发送队列、RTP 重排及 FEC 恢复状态。慢客户端仍按自身队列限制丢包,不会暂停其他客户端的组播接收。FCC 单播请求和切换状态按客户端独立维护,衔接组播时复用匹配的订阅。 +普通组播在共享源上完成一次 RTP 解析和重排,将负载合并为约 64 KiB 的批次,再通过引用计数分发。每个客户端保留独立的发送队列和发送偏移,底层批次数据共享;慢客户端仍按自身队列限制丢包,不会暂停其他客户端的组播接收。未满的批次会在 100 ms 后的下一次工作进程定时检查中发送,避免低码率流长时间等待。 + +Linux 上多个客户端共享完整批次时,优先将批次保存为不可修改的匿名内存文件,通过 `sendfile` 复用内核中的数据页。该文件不会随缓冲池复用而被改写,保证仍在传输的数据有效;不支持该方式或资源不足时自动回退到普通内存发送。此优化无需额外配置,也不依赖 `zerocopy-on-send`。 + +FCC 单播请求和切换状态按客户端独立维护,衔接组播时复用匹配的订阅;待单播及衔接数据处理完成、序号与共享流对齐后,加入共享批次分发。截图处理和 FEC 恢复仍使用各自的处理状态。配置了 FEC 端口,或主组播流中出现 FEC 包时,该源保留共享 socket,使用每个客户端独立的重排和 FEC 恢复路径。 `--listen` 可以重复指定,用于同时监听多个 TCP 地址/端口或 Unix socket: diff --git a/e2e/test_multicast_shared.py b/e2e/test_multicast_shared.py index affbccbe..1de6e9e2 100644 --- a/e2e/test_multicast_shared.py +++ b/e2e/test_multicast_shared.py @@ -16,6 +16,7 @@ R2HProcess, find_free_port, find_free_udp_port, + make_rtp_packet, ) pytestmark = pytest.mark.multicast @@ -69,6 +70,122 @@ def _read_markers(response, packets=128): return [struct.unpack_from("!H", body, i + 4)[0] for i in range(0, len(body), 188)] +def _read_contiguous_rtp(response, previous=None, packets=128): + """Check every TS packet, including continuity across HTTP reads/batches.""" + markers = _read_markers(response, packets=packets * 7) + for offset in range(0, len(markers), 7): + marker = markers[offset] + assert markers[offset : offset + 7] == [marker] * 7 + if previous is not None: + assert marker == (previous + 1) & 0xFFFF + previous = marker + return previous + + +@pytest.mark.parametrize("reorder,duplicates", [(4, False), (0, True)]) +def test_shared_batches_keep_exact_continuity(shared_source_r2h, reorder, duplicates): + """Late joins and departures must not replay, skip or overwrite payloads.""" + r2h = shared_source_r2h + sender = MulticastSender(pps=700, unique_payloads=True, reorder_distance=reorder, send_duplicates=duplicates) + sender.start() + path = f"/rtp/{MCAST_ADDR}:{sender.port}" + try: + with _stream(r2h, path) as first: + first_seq = _read_contiguous_rtp(first) + with _stream(r2h, path) as second: + second_seq = None + for _ in range(6): + first_seq = _read_contiguous_rtp(first, first_seq) + second_seq = _read_contiguous_rtp(second, second_seq) + first_seq = _read_contiguous_rtp(first, first_seq) + with _stream(r2h, path) as rejoined: + _read_contiguous_rtp(rejoined) + _read_contiguous_rtp(first, first_seq) + assert r2h.read_log().count("Multicast: Successfully joined group") == 1 + finally: + sender.stop() + + +def test_inband_fec_preserves_shared_reorder_window(shared_source_r2h): + """FEC discovered in the media socket can safely switch to private reorder.""" + r2h = shared_source_r2h + sender = MulticastSender(pps=700, unique_payloads=True, reorder_distance=4) + sender.start() + path = f"/rtp/{MCAST_ADDR}:{sender.port}" + try: + with _stream(r2h, path) as first, _stream(r2h, path) as second: + seqs = [_read_contiguous_rtp(first), _read_contiguous_rtp(second)] + # Valid but already expired parity activates FEC without recovery. + parity = struct.pack("!HHBBHHH", 0, 0, 1, 0, 1, 1328, 0) + b"\0" + with closing(socket.socket(socket.AF_INET, socket.SOCK_DGRAM)) as fec_socket: + fec_socket.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton("127.0.0.1")) + fec_socket.sendto(make_rtp_packet(0, 0, payload_type=127, payload=parity), (MCAST_ADDR, sender.port)) + _wait_log(r2h, "FEC: Activated", count=2) + for _ in range(4): + seqs = [_read_contiguous_rtp(client, seq) for client, seq in zip((first, second), seqs, strict=True)] + finally: + sender.stop() + + +def test_shared_payload_survives_delayed_reader_and_source_release(shared_source_r2h): + """Old TCP data stays immutable while newer batches recycle pool storage.""" + r2h = shared_source_r2h + sender = MulticastSender(pps=600, unique_payloads=True) + sender.start() + path = f"/rtp/{MCAST_ADDR}:{sender.port}" + try: + with _stream(r2h, path) as delayed: + delayed_seq = _read_contiguous_rtp(delayed) + with _stream(r2h, path) as fast: + fast_seq = None + for _ in range(5): + fast_seq = _read_contiguous_rtp(fast, fast_seq) + for _ in range(5): + delayed_seq = _read_contiguous_rtp(delayed, delayed_seq) + _read_contiguous_rtp(delayed, delayed_seq) + _wait_log(r2h, "Last subscriber left") + with _stream(r2h, path) as rejoined: + _read_contiguous_rtp(rejoined) + finally: + sender.stop() + + +def test_low_bitrate_shared_batch_flushes_promptly(shared_source_r2h): + """A short raw TS batch must not wait many seconds to reach 64 KiB.""" + sender = MulticastSender(pps=4, encapsulate_rtp=False, unique_payloads=True) + sender.start() + path = f"/rtp/{MCAST_ADDR}:{sender.port}" + try: + with ExitStack() as stack: + for _ in range(2): + start = time.monotonic() + client = stack.enter_context(_stream(shared_source_r2h, path)) + _read_markers(client, packets=7) + assert time.monotonic() - start < 1.5 + finally: + sender.stop() + + +def test_aborted_batch_subscribers_keep_survivor_alive(shared_source_r2h): + r2h = shared_source_r2h + sender = MulticastSender(pps=1000, unique_payloads=True) + sender.start() + path = f"/rtp/{MCAST_ADDR}:{sender.port}" + try: + with _stream(r2h, path) as survivor: + previous = _read_contiguous_rtp(survivor) + for _ in range(12): + with closing(socket.create_connection(("127.0.0.1", r2h.port), timeout=5)) as aborted: + aborted.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, struct.pack("ii", 1, 0)) + aborted.sendall(f"GET {path} HTTP/1.1\r\nHost: localhost\r\n\r\n".encode()) + assert aborted.recv(4096) + previous = _read_contiguous_rtp(survivor, previous) + assert "killed by signal" not in r2h.read_log() + assert r2h.read_log().count("Multicast: Successfully joined group") == 1 + finally: + sender.stop() + + @pytest.mark.parametrize( "close_first,rtp,use_fec", [(0, True, False), (1, True, False), (0, False, False), (1, True, True)] ) @@ -225,6 +342,7 @@ def test_fcc_clients_share_existing_multicast(shared_source_r2h, protocol): with _stream(r2h, path) as direct, _stream(r2h, fcc_path) as first, _stream(r2h, fcc_path) as second: _wait_log(r2h, "refs=3") _wait_log(r2h, "Reached termination sequence", count=2) + _wait_log(r2h, "Subscriber joined shared payload batches", count=2) assert fcc.requests_received >= 2 # Protocol requests may be retransmitted. assert len(set(fcc.request_client_addrs)) == 2 assert r2h.read_log().count("Multicast: Successfully joined group") == 1 diff --git a/src/buffer_pool.c b/src/buffer_pool.c index 94a32733..a689f3fb 100644 --- a/src/buffer_pool.c +++ b/src/buffer_pool.c @@ -7,6 +7,11 @@ #include #include #include +#ifdef __linux__ +#include +#include +#include +#endif #define WORKER_STATS_INC(field) \ do { \ @@ -67,6 +72,7 @@ static buffer_pool_segment_t *buffer_pool_segment_create(size_t buffer_size, siz ref->data = segment->buffers + (i * buffer_size); ref->refcount = 0; ref->segment = segment; + ref->shared_fd = -1; ref->free_next = pool->free_list; pool->free_list = ref; } @@ -101,6 +107,8 @@ int buffer_pool_init(buffer_pool_t *pool, size_t buffer_size, size_t initial_buf } static inline const char *buffer_pool_name(buffer_pool_t *pool) { + if (pool == &zerocopy_state.batch_pool) + return "Multicast batch pool"; return (pool == &zerocopy_state.pool) ? "Buffer pool" : "Control pool"; } @@ -188,6 +196,11 @@ void buffer_ref_put(buffer_ref_t *ref) { return; } + if (ref->shared_fd >= 0) { + close(ref->shared_fd); + ref->shared_fd = -1; + } + buffer_pool_t *pool = ref->segment ? ref->segment->parent : &zerocopy_state.pool; if (ref->segment) { @@ -213,11 +226,71 @@ buffer_ref_t *buffer_ref_view(buffer_ref_t *ref) { view->data_size = ref->data_size; view->data_offset = ref->data_offset; view->refcount = 1; + view->shared_fd = -1; view->owner = ref->owner ? ref->owner : ref; buffer_ref_get(view->owner); return view; } +size_t buffer_ref_capacity(const buffer_ref_t *ref) { + if (!ref || ref->type != BUFFER_TYPE_MEMORY) + return 0; + if (ref->owner) + ref = ref->owner; + return ref->segment ? ref->segment->parent->buffer_size : 0; +} + +int buffer_ref_sendfile_fd(const buffer_ref_t *ref) { + if (!ref || ref->type != BUFFER_TYPE_MEMORY || ref->shared_fd == -2) + return -1; + return (ref->owner ? ref->owner : ref)->shared_fd; +} + +/* One immutable RAM file per batch lets sendfile share the same kernel pages + * across clients. Never rewrite a published file: the kernel may retain its + * pages after sendfile returns, even after the last application reference is + * closed. A fresh snapshot keeps slow sockets safe when pool memory is reused. + * Unsupported kernels or allocation failures retain the normal sendmsg path. */ +void buffer_ref_snapshot(buffer_ref_t *ref) { +#ifdef __linux__ + if (!ref || ref->owner || ref->shared_fd >= 0 || !ref->data_size) + return; + int fd = memfd_create("rtp2httpd-batch", MFD_CLOEXEC | MFD_ALLOW_SEALING); + if (fd < 0) + return; + size_t written = 0; + while (written < ref->data_size) { + ssize_t n = write(fd, (uint8_t *)ref->data + ref->data_offset + written, ref->data_size - written); + if (n < 0 && errno == EINTR) + continue; + if (n <= 0) { + close(fd); + return; + } + written += (size_t)n; + } + if (fcntl(fd, F_ADD_SEALS, F_SEAL_WRITE | F_SEAL_GROW | F_SEAL_SHRINK | F_SEAL_SEAL) < 0) { + close(fd); + return; + } + ref->shared_fd = fd; +#else + (void)ref; +#endif +} + +buffer_ref_t *buffer_pool_alloc_batch(void) { + buffer_pool_t *pool = &zerocopy_state.batch_pool; + if (!pool->segments) { + size_t max_buffers = (size_t)config.buffer_pool_max_size * BUFFER_POOL_BUFFER_SIZE / BUFFER_POOL_BATCH_SIZE; + if (max_buffers < 4) + max_buffers = 4; + if (buffer_pool_init(pool, BUFFER_POOL_BATCH_SIZE, 4, max_buffers, 4, 2, 12) < 0) + return NULL; + } + return buffer_pool_alloc_from(pool); +} + buffer_ref_t *buffer_pool_alloc_from(buffer_pool_t *pool) { if (!pool) return NULL; @@ -363,4 +436,5 @@ static void buffer_pool_try_shrink_pool(buffer_pool_t *pool, size_t min_buffers) void buffer_pool_try_shrink(void) { buffer_pool_try_shrink_pool(&zerocopy_state.pool, BUFFER_POOL_INITIAL_SIZE); buffer_pool_try_shrink_pool(&zerocopy_state.control_pool, CONTROL_POOL_INITIAL_SIZE); + buffer_pool_try_shrink_pool(&zerocopy_state.batch_pool, 4); } diff --git a/src/buffer_pool.h b/src/buffer_pool.h index ad96f5a7..0ed53216 100644 --- a/src/buffer_pool.h +++ b/src/buffer_pool.h @@ -13,6 +13,8 @@ #define BUFFER_POOL_BUFFER_SIZE 1536 #define BUFFER_POOL_LOW_WATERMARK 256 #define BUFFER_POOL_HIGH_WATERMARK (BUFFER_POOL_INITIAL_SIZE * 3) +/* One shared output batch, with room for the packet crossing 64 KiB. */ +#define BUFFER_POOL_BATCH_SIZE (65536 + BUFFER_POOL_BUFFER_SIZE) /* Control/API buffer pool configuration */ #define CONTROL_POOL_INITIAL_SIZE 256 @@ -51,6 +53,7 @@ typedef struct buffer_ref_s { int refcount; /* Reference count */ struct buffer_pool_segment_s *segment; /* Segment this buffer belongs to (BUFFER_TYPE_MEMORY) */ struct buffer_ref_s *owner; /* Non-NULL for a view sharing another buffer's immutable data */ + int shared_fd; /* Immutable batch snapshot, -1 if absent; views use their owner */ /* Union: buffer is either in free list OR in send queue, never both */ union { @@ -107,6 +110,11 @@ void buffer_ref_put(buffer_ref_t *ref); /* Share data while keeping offsets, send links and completion IDs independent. * The returned view owns a reference to the backing buffer; release with put. */ buffer_ref_t *buffer_ref_view(buffer_ref_t *ref); +size_t buffer_ref_capacity(const buffer_ref_t *ref); +void buffer_ref_snapshot(buffer_ref_t *ref); +int buffer_ref_sendfile_fd(const buffer_ref_t *ref); +/* Worker-owned pool: queued batches can outlive their multicast source. */ +buffer_ref_t *buffer_pool_alloc_batch(void); buffer_ref_t *buffer_pool_alloc_from(buffer_pool_t *pool); buffer_ref_t *buffer_pool_alloc(void); buffer_ref_t *buffer_pool_alloc_control(void); diff --git a/src/connection.c b/src/connection.c index 4374435d..db23929f 100644 --- a/src/connection.c +++ b/src/connection.c @@ -351,7 +351,7 @@ static size_t connection_update_queue_limit(connection_t *c, int64_t now_ms) { queue_limit_inputs_t in; connection_prepare_queue_limit_inputs(&in); - double queue_mem_bytes = (double)c->zc_queue.num_queued * (double)BUFFER_POOL_BUFFER_SIZE; + double queue_mem_bytes = (double)connection_queue_bytes(c); if (c->queue_avg_bytes <= 0.0) c->queue_avg_bytes = queue_mem_bytes; else @@ -1229,7 +1229,7 @@ int connection_queue_zerocopy(connection_t *c, buffer_ref_t *buf_ref) { int64_t now_ms = get_time_ms(); size_t limit_bytes = connection_update_queue_limit(c, now_ms); size_t queued_bytes = connection_queue_bytes(c); - size_t projected_bytes = queued_bytes + buf_ref->data_size; + size_t projected_bytes = queued_bytes + buffer_ref_capacity(buf_ref); c->queue_limit_bytes = limit_bytes; diff --git a/src/connection.h b/src/connection.h index c0ea0927..f871794a 100644 --- a/src/connection.h +++ b/src/connection.h @@ -185,11 +185,9 @@ int connection_queue_zerocopy(connection_t *c, buffer_ref_t *buf_ref); */ int connection_queue_file(connection_t *c, int file_fd, off_t file_offset, size_t file_size); -/* Slot-equivalent bytes currently queued (each pending buffer counts as a full - * BUFFER_POOL_BUFFER_SIZE slot, matching the unit used by queue_limit_bytes). */ -static inline size_t connection_queue_bytes(const connection_t *c) { - return c->zc_queue.num_queued * BUFFER_POOL_BUFFER_SIZE; -} +/* Backing capacity currently queued, including shared multicast batches. + * Partial sends retain the entire backing buffer until the entry is removed. */ +static inline size_t connection_queue_bytes(const connection_t *c) { return c->zc_queue.memory_bytes; } /* Record one upstream-pause edge. Called by per-transport pause helpers * (http_proxy_pause_upstream, rtsp_pause_upstream) on the 0->1 transition. */ diff --git a/src/multicast.c b/src/multicast.c index 956af457..994f52ea 100644 --- a/src/multicast.c +++ b/src/multicast.c @@ -4,6 +4,7 @@ #include "fcc.h" #include "platform_compat.h" #include "poller.h" +#include "rtp.h" #include "rtp_fec.h" #include "service.h" #include "stream.h" @@ -387,11 +388,22 @@ struct mcast_source_s { int rejoin_unsupported_warned; service_t *service; /* Deep copy with the effective multicast interface frozen */ mcast_session_t *subscribers; + mcast_session_t *packet_subscribers; + unsigned int batch_clients; + int shared_output; + rtp_reorder_t reorder; + buffer_ref_t *batch; + int packet_type; + int batch_packet_type; + int64_t batch_since; mcast_source_t *next; }; static mcast_source_t *mcast_sources; +static void mcast_source_flush(mcast_source_t *source); +static int mcast_source_append(void *arg, buffer_ref_t *packet); + /* Compare resolved endpoints, never channel names, URL spelling or sockaddr * padding. The source port does not participate in an IGMP source filter. */ static int mcast_address_equal(const struct addrinfo *a, const struct addrinfo *b, int compare_port) { @@ -418,6 +430,8 @@ static void mcast_source_free(mcast_source_t *source) { worker_cleanup_socket_from_epoll(source->epoll_fd, source->sock); if (source->fec_sock >= 0) worker_cleanup_socket_from_epoll(source->epoll_fd, source->fec_sock); + rtp_reorder_cleanup(&source->reorder); + buffer_ref_put(source->batch); service_free(source->service); free(source); } @@ -440,6 +454,15 @@ void mcast_session_cleanup(mcast_session_t *session) { subscriber = &(*subscriber)->next; if (*subscriber) *subscriber = session->next; + if (session->batched) { + source->batch_clients--; + } else { + mcast_session_t **packet_subscriber = &source->packet_subscribers; + while (*packet_subscriber && *packet_subscriber != session) + packet_subscriber = &(*packet_subscriber)->packet_next; + if (*packet_subscriber) + *packet_subscriber = session->packet_next; + } source->refs--; logger(LOG_DEBUG, "Multicast: Subscriber detached (fd=%d, refs=%u)", source->sock, source->refs); if (source->refs == 0) { @@ -460,6 +483,8 @@ void mcast_session_cleanup(mcast_session_t *session) { session->source = NULL; session->ctx = NULL; session->next = NULL; + session->packet_next = NULL; + session->batched = 0; session->sock = -1; session->fec_sock = -1; session->initialized = 0; @@ -512,6 +537,15 @@ int mcast_session_join(mcast_session_t *session, stream_context_t *ctx) { mcast_source_free(source); return -1; } + if (service->fec_port == 0) { + if (rtp_reorder_init(&source->reorder, 0) < 0) { + mcast_source_free(source); + return -1; + } + source->shared_output = 1; + source->reorder.deliver = mcast_source_append; + source->reorder.deliver_arg = source; + } source->sock = join_mcast_group(source->service, 0); if (source->sock < 0 || poller_add(ctx->epoll_fd, source->sock, POLLER_IN) < 0) { mcast_source_free(source); @@ -532,6 +566,10 @@ int mcast_session_join(mcast_session_t *session, stream_context_t *ctx) { logger(LOG_DEBUG, "Multicast: Reusing shared source (fd=%d)", source->sock); } + /* A newly attached viewer starts after the current batch, not with bytes + * already delivered during its private FCC/snapshot processing. */ + mcast_source_flush(source); + session->source = source; session->ctx = ctx; session->sock = source->sock; @@ -539,6 +577,13 @@ int mcast_session_join(mcast_session_t *session, stream_context_t *ctx) { session->next = source->subscribers; source->subscribers = session; source->refs++; + session->batched = source->shared_output && !ctx->snapshot.initialized && !ctx->fcc.initialized; + if (session->batched) { + source->batch_clients++; + } else { + session->packet_next = source->packet_subscribers; + source->packet_subscribers = session; + } fdmap_set(source->sock, ctx->conn); if (source->fec_sock >= 0) fdmap_set(source->fec_sock, ctx->conn); @@ -546,6 +591,119 @@ int mcast_session_join(mcast_session_t *session, stream_context_t *ctx) { return 0; } +/* Only queue metadata is private. The immutable payload stays alive until + * every queue and MSG_ZEROCOPY completion releases its reference. */ +static void mcast_source_fanout(mcast_source_t *source, buffer_ref_t *batch, int packet_type, int flush) { + for (mcast_session_t *session = source->subscribers; session; session = session->next) { + stream_context_t *ctx = session->ctx; + if (!session->batched || session->failed || ctx->conn->state == CONN_CLOSING) + continue; + buffer_ref_t *view = buffer_ref_view(batch); + if (!view) { + session->failed = 1; + continue; + } + stream_metadata_note_media(ctx, packet_type, (uint8_t *)view->data + view->data_offset, (int)view->data_size, + ctx->fcc.initialized ? STREAM_MEDIA_ORIGIN_FCC_MULTICAST + : STREAM_MEDIA_ORIGIN_MULTICAST); + if (rtp_queue_buf_direct(ctx->conn, view) >= 0 && flush && view->data_size < ZEROCOPY_BATCH_BYTES) { + connection_epoll_update_events(ctx->epoll_fd, ctx->conn->fd, + POLLER_IN | POLLER_OUT | POLLER_RDHUP | POLLER_HUP | POLLER_ERR); + } + buffer_ref_put(view); + } +} + +static void mcast_source_flush(mcast_source_t *source) { + buffer_ref_t *batch = source->batch; + if (!batch) + return; + source->batch = NULL; + if (source->batch_clients > 1 && batch->data_size >= ZEROCOPY_BATCH_BYTES) + buffer_ref_snapshot(batch); + mcast_source_fanout(source, batch, source->batch_packet_type, 1); + buffer_ref_put(batch); +} + +static int mcast_source_append(void *arg, buffer_ref_t *packet) { + mcast_source_t *source = arg; + if (!source->batch_clients) + return (int)packet->data_size; + if (!source->batch) { + source->batch = buffer_pool_alloc_batch(); + source->batch_since = get_time_ms(); + source->batch_packet_type = source->packet_type; + } + if (!source->batch) { + /* Keep forwarding when slow viewers pin the bounded batch pool. */ + mcast_source_fanout(source, packet, source->packet_type, 0); + return (int)packet->data_size; + } + buffer_ref_t *batch = source->batch; + memcpy((uint8_t *)batch->data + batch->data_size, (uint8_t *)packet->data + packet->data_offset, packet->data_size); + batch->data_size += packet->data_size; + if (batch->data_size >= ZEROCOPY_BATCH_BYTES) + mcast_source_flush(source); + return (int)packet->data_size; +} + +/* In-band FEC can appear even without a configured FEC port. Transfer the + * current reorder window to each viewer before resuming its private FEC path. */ +static void mcast_source_enable_private_fec(mcast_source_t *source) { + mcast_source_flush(source); + source->shared_output = 0; + for (mcast_session_t *session = source->subscribers; session; session = session->next) { + if (!session->batched) + continue; + rtp_reorder_t *r = &session->ctx->reorder; + rtp_reorder_cleanup(r); + if (rtp_reorder_init(r, 0) < 0) { + session->failed = 1; + } else { + r->base_seq = source->reorder.base_seq; + r->phase = source->reorder.phase; + r->count = source->reorder.count; + for (int i = 0; i < r->window_size; i++) { + r->seq[i] = source->reorder.seq[i]; + if (source->reorder.slots[i]) { + r->slots[i] = buffer_ref_view(source->reorder.slots[i]); + if (!r->slots[i]) + session->failed = 1; + } + } + } + session->batched = 0; + session->packet_next = source->packet_subscribers; + source->packet_subscribers = session; + } + source->batch_clients = 0; + rtp_reorder_cleanup(&source->reorder); +} + +/* FCC unicast/pending data must finish first. Switch only at an identical + * delivered sequence boundary, so there is no replay or skipped handoff data. */ +static void mcast_source_promote_ready(mcast_source_t *source) { + mcast_session_t **entry = &source->packet_subscribers; + while (*entry) { + mcast_session_t *session = *entry; + stream_context_t *ctx = session->ctx; + if (!session->failed && ctx->conn->state != CONN_CLOSING && !ctx->snapshot.initialized && + !fec_is_enabled(&ctx->fec) && + (!ctx->fcc.initialized || (ctx->fcc.state == FCC_STATE_MCAST_ACTIVE && !ctx->fcc.pending_list_head)) && + (source->packet_type == 0 || (source->reorder.phase == 2 && ctx->reorder.phase == 2 && + ctx->reorder.count == 0 && ctx->reorder.base_seq == source->reorder.base_seq))) { + mcast_source_flush(source); + *entry = session->packet_next; + session->packet_next = NULL; + session->batched = 1; + source->batch_clients++; + logger(LOG_DEBUG, "Multicast: Subscriber joined shared payload batches (fd=%d)", ctx->conn->fd); + } else { + entry = &session->packet_next; + } + } +} + static void mcast_deliver_packet(mcast_session_t *session, buffer_ref_t *packet) { stream_context_t *ctx = session->ctx; if (session->failed || ctx->conn->state == CONN_CLOSING) @@ -554,7 +712,7 @@ static void mcast_deliver_packet(mcast_session_t *session, buffer_ref_t *packet) /* Queue linkage, RTP offsets and zerocopy completion IDs are mutable and * must never be shared between clients. Only the backing data is shared. */ buffer_ref_t *view; - if (session->source->refs == 1) { + if (session->source->refs == 1 && !session->source->shared_output) { /* Preserve the allocation-free descriptor path for a lone subscriber. */ view = packet; buffer_ref_get(view); @@ -603,8 +761,26 @@ int mcast_session_handle_event(mcast_session_t *session, int fd, int64_t now) { source->last_data_time = now; if (packet) { packet->data_size = (size_t)len; - for (mcast_session_t *subscriber = source->subscribers; subscriber; subscriber = subscriber->next) + uint8_t *payload = NULL; + int payload_len = 0; + uint16_t seq = 0; + int packet_type = source->shared_output ? rtp_get_payload(data, (int)len, &payload, &payload_len, &seq) : -1; + if (packet_type == 2) + mcast_source_enable_private_fec(source); + for (mcast_session_t *subscriber = source->packet_subscribers; subscriber; subscriber = subscriber->packet_next) mcast_deliver_packet(subscriber, packet); + if (source->shared_output && + (packet_type == 1 || (packet_type == 0 && stream_payload_is_mpegts(payload, payload_len)))) { + source->packet_type = packet_type; + packet->data_offset = (size_t)(payload - (uint8_t *)packet->data); + packet->data_size = (size_t)payload_len; + if (packet_type == 1) + rtp_reorder_insert(&source->reorder, packet, seq, NULL, 0, NULL); + else + mcast_source_append(source, packet); + if (source->packet_subscribers) + mcast_source_promote_ready(source); + } } } else { for (mcast_session_t *subscriber = source->subscribers; subscriber; subscriber = subscriber->next) { @@ -624,6 +800,9 @@ int mcast_session_tick(mcast_session_t *session, int64_t now) { service_t *service = source->service; if (session->failed || source->failed) return -1; + /* Bound low-bitrate batching delay; the worker ticks every 100 ms. */ + if (source->batch && now - source->batch_since >= 100) + mcast_source_flush(source); /* Periodic rejoin and timeout belong to the source, not each subscriber. */ if (config.mcast_rejoin_interval > 0) { diff --git a/src/multicast.h b/src/multicast.h index 5a3da54a..2c5c777c 100644 --- a/src/multicast.h +++ b/src/multicast.h @@ -18,9 +18,11 @@ typedef struct mcast_session_s { int sock; /* Borrowed main socket (-1 if not subscribed) */ int fec_sock; /* Borrowed FEC socket; owned by the shared source */ int failed; /* Subscriber-local FCC failure */ + int batched; /* Receives shared, already ordered payload batches */ mcast_source_t *source; stream_context_t *ctx; struct mcast_session_s *next; + struct mcast_session_s *packet_next; /* FCC/FEC/snapshot subscribers only */ } mcast_session_t; /** diff --git a/src/rtp_reorder.c b/src/rtp_reorder.c index 81ec69d4..47110b7e 100644 --- a/src/rtp_reorder.c +++ b/src/rtp_reorder.c @@ -68,7 +68,9 @@ void rtp_reorder_cleanup(rtp_reorder_t *r) { } /* Deliver single packet from buffer_ref */ -static int deliver_packet(buffer_ref_t *buf, connection_t *conn, int is_snapshot) { +static int deliver_packet(rtp_reorder_t *r, buffer_ref_t *buf, connection_t *conn, int is_snapshot) { + if (r->deliver) + return r->deliver(r->deliver_arg, buf); if (is_snapshot) { return snapshot_process_packet(&conn->stream.snapshot, buf->data_size, (uint8_t *)buf->data + buf->data_offset, conn); @@ -105,7 +107,7 @@ static int flush_consecutive(rtp_reorder_t *r, connection_t *conn, int is_snapsh if (!buf) break; /* Hole, stop */ - int bytes = deliver_packet(buf, conn, is_snapshot); + int bytes = deliver_packet(r, buf, conn, is_snapshot); if (bytes > 0) total_bytes += bytes; @@ -148,7 +150,7 @@ static int force_flush_until(rtp_reorder_t *r, uint16_t target_seq, connection_t buffer_ref_t *buf = r->slots[slot]; if (buf) { - int bytes = deliver_packet(buf, conn, is_snapshot); + int bytes = deliver_packet(r, buf, conn, is_snapshot); if (bytes > 0) total_bytes += bytes; buffer_ref_put(buf); diff --git a/src/rtp_reorder.h b/src/rtp_reorder.h index 25ac2025..edb2f07e 100644 --- a/src/rtp_reorder.h +++ b/src/rtp_reorder.h @@ -36,6 +36,9 @@ typedef struct rtp_reorder_s { uint16_t count; /* Number of buffered packets */ uint8_t initialized; /* Flag: context has been initialized */ uint8_t phase; /* 0=not started, 1=collecting, 2=active */ + /* Optional shared-source output. NULL preserves per-client delivery. */ + int (*deliver)(void *arg, buffer_ref_t *buf); + void *deliver_arg; } rtp_reorder_t; /** diff --git a/src/stream.c b/src/stream.c index 35b98246..ab3b5bb3 100644 --- a/src/stream.c +++ b/src/stream.c @@ -132,7 +132,7 @@ static int stream_metadata_format_number(double value, char *buffer, size_t buff return 0; } -static int stream_payload_is_mpegts(const uint8_t *payload, int payload_len) { +int stream_payload_is_mpegts(const uint8_t *payload, int payload_len) { int checked = 0; if (!payload || payload_len < TS_PACKET_SIZE || payload[0] != TS_SYNC_BYTE) @@ -181,8 +181,8 @@ void stream_metadata_forget(stream_metadata_t *metadata, unsigned stages) { } } -static void stream_metadata_note_media(stream_context_t *ctx, int packet_type, const uint8_t *payload, int payload_len, - stream_media_origin_t origin) { +void stream_metadata_note_media(stream_context_t *ctx, int packet_type, const uint8_t *payload, int payload_len, + stream_media_origin_t origin) { stream_metadata_t *metadata; if (!ctx || ctx->metadata.frozen || !stream_payload_is_mpegts(payload, payload_len)) diff --git a/src/stream.h b/src/stream.h index fa8d476a..10588f9f 100644 --- a/src/stream.h +++ b/src/stream.h @@ -120,6 +120,11 @@ typedef struct stream_context_s { snapshot_context_t snapshot; } stream_context_t; +/* Shared multicast parsing uses the same validation and metadata rules. */ +int stream_payload_is_mpegts(const uint8_t *payload, int payload_len); +void stream_metadata_note_media(stream_context_t *ctx, int packet_type, const uint8_t *payload, int payload_len, + stream_media_origin_t origin); + /** * Initialize a stream context for integration into a worker's unified epoll * loop. Does not block; registers any required media sockets with the provided diff --git a/src/worker.c b/src/worker.c index cc0e7305..fcd358e1 100644 --- a/src/worker.c +++ b/src/worker.c @@ -261,6 +261,9 @@ int worker_run_event_loop(int *listen_sockets, int num_sockets, int notif_fd) { /* Register signal handlers */ signal(SIGTERM, &term_handler); signal(SIGINT, &term_handler); + /* sendfile has no MSG_NOSIGNAL flag. A disconnected viewer must only + * produce EPIPE, never terminate a worker serving other viewers. */ + signal(SIGPIPE, SIG_IGN); worker_install_sighup_handler(); /* Unified event loop: accept + clients + stream fds */ diff --git a/src/zerocopy.c b/src/zerocopy.c index 11265c56..bfd4d757 100644 --- a/src/zerocopy.c +++ b/src/zerocopy.c @@ -121,6 +121,7 @@ void zerocopy_cleanup(void) { buffer_pool_cleanup(&zerocopy_state.pool); buffer_pool_cleanup(&zerocopy_state.control_pool); + buffer_pool_cleanup(&zerocopy_state.batch_pool); buffer_pool_update_stats(&zerocopy_state.pool); buffer_pool_update_stats(&zerocopy_state.control_pool); zerocopy_state.initialized = 0; @@ -155,12 +156,12 @@ int zerocopy_queue_add(zerocopy_queue_t *queue, buffer_ref_t *buf_ref) { uint8_t *base = (uint8_t *)buf_ref->data; - if (!base || buf_ref->data_offset > BUFFER_POOL_BUFFER_SIZE || - buf_ref->data_size > BUFFER_POOL_BUFFER_SIZE - buf_ref->data_offset) { + size_t capacity = buffer_ref_capacity(buf_ref); + if (!base || buf_ref->data_offset > capacity || buf_ref->data_size > capacity - buf_ref->data_offset) { logger(LOG_ERROR, "zerocopy_queue_add: Invalid buffer parameters (offset=%zu len=%zu " - "size=%d)", - buf_ref->data_offset, buf_ref->data_size, BUFFER_POOL_BUFFER_SIZE); + "size=%zu)", + buf_ref->data_offset, buf_ref->data_size, capacity); return -1; } @@ -186,6 +187,7 @@ int zerocopy_queue_add(zerocopy_queue_t *queue, buffer_ref_t *buf_ref) { } queue->total_bytes += buf_ref->data_size; + queue->memory_bytes += capacity; queue->num_queued++; return 0; @@ -227,6 +229,7 @@ int zerocopy_queue_add_file(zerocopy_queue_t *queue, int file_fd, off_t file_off * the batching optimization designed for small RTP packets. */ queue->num_queued++; + queue->memory_bytes += BUFFER_POOL_BUFFER_SIZE; logger(LOG_DEBUG, "zerocopy_queue_add_file: Queued file fd=%d offset=%ld size=%zu", file_fd, (long)file_offset, file_size); @@ -253,6 +256,41 @@ int zerocopy_send(int fd, zerocopy_queue_t *queue, size_t *bytes_sent) { return 0; } + buffer_ref_t *shared = queue->head; + int shared_fd = buffer_ref_sendfile_fd(shared); + if (shared_fd >= 0) { + off_t offset = (uint8_t *)shared->iov.iov_base - ((uint8_t *)shared->data + shared->data_offset); + ssize_t sent = platform_sendfile(fd, shared_fd, &offset, shared->iov.iov_len); + if (sent < 0 && (errno == EINVAL || errno == ENOSYS || errno == EOPNOTSUPP)) { + /* Keep this subscriber's fallback private; others can still sendfile. */ + shared->shared_fd = -2; + } else { + *bytes_sent = sent > 0 ? (size_t)sent : 0; + if (sent < 0) { + if (errno == EAGAIN || errno == EINTR || errno == ENOBUFS) { + WORKER_STATS_INC(eagain_count); + return -2; + } + return -1; + } + if (sent == 0) + return -1; /* The immutable snapshot must contain the complete batch. */ + WORKER_STATS_INC(total_sends); + queue->total_bytes -= (size_t)sent; + shared->iov.iov_base = (uint8_t *)shared->iov.iov_base + sent; + shared->iov.iov_len -= (size_t)sent; + if (!shared->iov.iov_len) { + queue->head = shared->send_next; + if (!queue->head) + queue->tail = NULL; + queue->num_queued--; + queue->memory_bytes -= buffer_ref_capacity(shared); + buffer_ref_put(shared); + } + return 0; + } + } + /* Check if head is a file - sendfile() must be done separately */ if (queue->head->type == BUFFER_TYPE_FILE) { buffer_ref_t *file_buf = queue->head; @@ -289,6 +327,7 @@ int zerocopy_send(int fd, zerocopy_queue_t *queue, size_t *bytes_sent) { /* Note: File buffers don't count towards total_bytes, so no need to * update it */ queue->num_queued--; + queue->memory_bytes -= BUFFER_POOL_BUFFER_SIZE; /* Release reference - this will close fd and free buffer_ref */ buffer_ref_put(file_buf); @@ -308,11 +347,16 @@ int zerocopy_send(int fd, zerocopy_queue_t *queue, size_t *bytes_sent) { struct iovec iovecs[ZEROCOPY_MAX_IOVECS]; buffer_ref_t *buffers[ZEROCOPY_MAX_IOVECS]; int iov_count = 0; + int use_zerocopy = config.zerocopy_on_send; buffer_ref_t *buf = queue->head; - while (buf && iov_count < ZEROCOPY_MAX_IOVECS && buf->type == BUFFER_TYPE_MEMORY) { + while (buf && iov_count < ZEROCOPY_MAX_IOVECS && buf->type == BUFFER_TYPE_MEMORY && buffer_ref_sendfile_fd(buf) < 0) { iovecs[iov_count] = buf->iov; buffers[iov_count] = buf; + /* Published multicast batches use sendfile. Its fallback copies data, + * without adding asynchronous page ownership to the reusable batch pool. */ + if (buffer_ref_capacity(buf) > BUFFER_POOL_BUFFER_SIZE) + use_zerocopy = 0; iov_count++; buf = buf->send_next; } @@ -330,7 +374,7 @@ int zerocopy_send(int fd, zerocopy_queue_t *queue, size_t *bytes_sent) { /* Determine flags based on zerocopy configuration */ int flags = MSG_DONTWAIT | MSG_NOSIGNAL; - if (config.zerocopy_on_send) { + if (use_zerocopy) { flags |= MSG_ZEROCOPY; } @@ -369,7 +413,7 @@ int zerocopy_send(int fd, zerocopy_queue_t *queue, size_t *bytes_sent) { *bytes_sent = (size_t)sent; /* Handle buffer management based on whether MSG_ZEROCOPY is used */ - if (config.zerocopy_on_send) { + if (use_zerocopy) { /* Assign zerocopy ID for this sendmsg call AFTER successful send * All iovecs in this call share the same ID for completion tracking * IMPORTANT: Only increment the ID counter after sendmsg() succeeds, @@ -399,6 +443,7 @@ int zerocopy_send(int fd, zerocopy_queue_t *queue, size_t *bytes_sent) { remaining -= current->iov.iov_len; queue->total_bytes -= current->iov.iov_len; queue->num_queued--; + queue->memory_bytes -= buffer_ref_capacity(current); queue->head = current->send_next; if (!queue->head) @@ -443,6 +488,7 @@ int zerocopy_send(int fd, zerocopy_queue_t *queue, size_t *bytes_sent) { remaining -= current->iov.iov_len; queue->total_bytes -= current->iov.iov_len; queue->num_queued--; + queue->memory_bytes -= buffer_ref_capacity(current); queue->head = current->send_next; if (!queue->head) diff --git a/src/zerocopy.h b/src/zerocopy.h index 0ad96eab..9e10582f 100644 --- a/src/zerocopy.h +++ b/src/zerocopy.h @@ -30,6 +30,7 @@ typedef struct zerocopy_queue_s { buffer_ref_t *pending_head; /* First buffer pending completion */ buffer_ref_t *pending_tail; /* Last buffer pending completion */ size_t total_bytes; /* Total bytes queued */ + size_t memory_bytes; /* Backing capacity retained by the send queue */ size_t num_queued; /* Number of buffers in send queue */ size_t num_pending; /* Number of buffers pending completion */ uint32_t next_zerocopy_id; /* Next ID for MSG_ZEROCOPY tracking */ @@ -42,6 +43,7 @@ typedef struct zerocopy_queue_s { typedef struct zerocopy_state_s { buffer_pool_t pool; /* Global buffer pool */ buffer_pool_t control_pool; /* Dedicated pool for status/API control plane */ + buffer_pool_t batch_pool; /* Lazily allocated immutable multicast batches */ size_t active_streams; /* Number of active media streaming clients */ int initialized; /* Whether initialized */ } zerocopy_state_t; From 8cfe35e05324366c84bc6562bdf6f354eb685e4d Mon Sep 17 00:00:00 2001 From: Stackie Jia Date: Sun, 6 Sep 2026 17:50:21 +0800 Subject: [PATCH 03/19] perf(multicast): cap shared batches at 64 KiB --- src/buffer_pool.h | 4 ++-- src/multicast.c | 8 ++++++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/buffer_pool.h b/src/buffer_pool.h index 0ed53216..e2b66177 100644 --- a/src/buffer_pool.h +++ b/src/buffer_pool.h @@ -13,8 +13,8 @@ #define BUFFER_POOL_BUFFER_SIZE 1536 #define BUFFER_POOL_LOW_WATERMARK 256 #define BUFFER_POOL_HIGH_WATERMARK (BUFFER_POOL_INITIAL_SIZE * 3) -/* One shared output batch, with room for the packet crossing 64 KiB. */ -#define BUFFER_POOL_BATCH_SIZE (65536 + BUFFER_POOL_BUFFER_SIZE) +/* Keep shared output below 64 KiB, including the last complete RTP payload. */ +#define BUFFER_POOL_BATCH_SIZE 65536 /* Control/API buffer pool configuration */ #define CONTROL_POOL_INITIAL_SIZE 256 diff --git a/src/multicast.c b/src/multicast.c index 994f52ea..5d0245e6 100644 --- a/src/multicast.c +++ b/src/multicast.c @@ -619,7 +619,7 @@ static void mcast_source_flush(mcast_source_t *source) { if (!batch) return; source->batch = NULL; - if (source->batch_clients > 1 && batch->data_size >= ZEROCOPY_BATCH_BYTES) + if (source->batch_clients > 1 && batch->data_size >= BUFFER_POOL_BATCH_SIZE - BUFFER_POOL_BUFFER_SIZE) buffer_ref_snapshot(batch); mcast_source_fanout(source, batch, source->batch_packet_type, 1); buffer_ref_put(batch); @@ -629,6 +629,10 @@ static int mcast_source_append(void *arg, buffer_ref_t *packet) { mcast_source_t *source = arg; if (!source->batch_clients) return (int)packet->data_size; + /* Flush before crossing the cap. Besides bounding storage, this avoids + * creating a second TCP/GSO block for a small tail above 64 KiB. */ + if (source->batch && packet->data_size > BUFFER_POOL_BATCH_SIZE - source->batch->data_size) + mcast_source_flush(source); if (!source->batch) { source->batch = buffer_pool_alloc_batch(); source->batch_since = get_time_ms(); @@ -642,7 +646,7 @@ static int mcast_source_append(void *arg, buffer_ref_t *packet) { buffer_ref_t *batch = source->batch; memcpy((uint8_t *)batch->data + batch->data_size, (uint8_t *)packet->data + packet->data_offset, packet->data_size); batch->data_size += packet->data_size; - if (batch->data_size >= ZEROCOPY_BATCH_BYTES) + if (batch->data_size == BUFFER_POOL_BATCH_SIZE) mcast_source_flush(source); return (int)packet->data_size; } From 52b4d8267cd9faf207509aba078048794a4a6494 Mon Sep 17 00:00:00 2001 From: Stackie Jia Date: Sun, 6 Sep 2026 18:12:25 +0800 Subject: [PATCH 04/19] refactor(stream): remove MSG_ZEROCOPY send option --- .agents/skills/e2e/SKILL.md | 4 +- CMakeLists.txt | 2 +- Dockerfile | 3 +- README.md | 2 +- docs/en/guide/installation.md | 10 +- docs/en/index.md | 2 +- docs/en/reference/configuration.md | 21 +- docs/guide/installation.md | 10 +- docs/index.md | 2 +- docs/reference/configuration.md | 21 +- e2e/test_flow_control.py | 8 +- e2e/test_multicast.py | 2 +- e2e/test_multicast_shared.py | 2 +- e2e/test_unix_socket.py | 21 +- e2e/test_zerocopy.py | 271 -------- ikuai-support/rtp2httpd/app/option.json | 13 - ikuai-support/rtp2httpd/readme | 2 +- ikuai-support/rtp2httpd/scripts/start.sh | 4 - .../luci-static/resources/view/rtp2httpd.js | 14 +- .../po/templates/rtp2httpd.pot | 14 +- .../po/zh_Hans/rtp2httpd.po | 19 +- .../rtp2httpd/files/rtp2httpd.conf | 1 - .../rtp2httpd/files/rtp2httpd.init | 3 - rtp2httpd.conf | 7 - src/buffer_pool.c | 36 +- src/buffer_pool.h | 9 +- src/configuration.c | 36 +- src/configuration.h | 6 +- src/connection.c | 76 +-- src/connection.h | 15 +- src/epg.c | 4 +- src/epg.h | 2 +- src/fcc.c | 8 +- src/fcc.h | 8 +- src/http_fetch.c | 6 +- src/http_fetch.h | 6 +- src/http_proxy.c | 14 +- src/http_proxy.h | 2 +- src/multicast.c | 6 +- src/platform_compat.h | 30 - src/rtp.c | 4 +- src/rtsp.c | 4 +- src/send_queue.c | 383 +++++++++++ src/send_queue.h | 47 ++ src/snapshot.c | 2 +- src/status.c | 3 +- src/status.h | 12 +- src/stream.h | 2 +- src/supervisor.c | 11 +- src/worker.c | 49 +- src/zerocopy.c | 633 ------------------ src/zerocopy.h | 130 ---- .../src/components/status/workers-section.tsx | 2 - web-ui/src/i18n/status.ts | 6 - web-ui/src/types.ts | 2 - 55 files changed, 573 insertions(+), 1439 deletions(-) delete mode 100644 e2e/test_zerocopy.py create mode 100644 src/send_queue.c create mode 100644 src/send_queue.h delete mode 100644 src/zerocopy.c delete mode 100644 src/zerocopy.h diff --git a/.agents/skills/e2e/SKILL.md b/.agents/skills/e2e/SKILL.md index 99a8f8fe..5d1f11f6 100644 --- a/.agents/skills/e2e/SKILL.md +++ b/.agents/skills/e2e/SKILL.md @@ -7,7 +7,7 @@ description: > flaky, slow, or hanging e2e tests, (4) mentions any file under e2e/ or scripts/run-e2e.sh, (5) mentions MockRTSP*, MockHTTP*, MockFCC*, MockSTUN*, R2HProcess, MulticastSender, helper APIs, or test fixtures, (6) asks about multicast, RTSP, HTTP proxy, FCC, STUN, M3U, EPG, URL template, - or zerocopy test coverage in rtp2httpd, or (7) uses Chinese phrases such as "端到端测试", + or shared multicast test coverage in rtp2httpd, or (7) uses Chinese phrases such as "端到端测试", "跑测试", or "e2e 测试" in this repo. --- @@ -59,7 +59,7 @@ e2e/ ├── conftest.py ├── test_m3u.py ├── test_epg.py / test_pages.py / test_auth.py / test_config.py / test_error.py -├── test_multicast.py / test_fcc.py / test_zerocopy.py +├── test_multicast.py / test_fcc.py / test_multicast_shared.py ├── test_http_proxy*.py ├── test_rtsp_*.py ├── test_url_template_http.py diff --git a/CMakeLists.txt b/CMakeLists.txt index cae91699..88189902 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -62,7 +62,7 @@ set(COMMON_SOURCES src/worker.c src/unix_socket.c src/buffer_pool.c - src/zerocopy.c + src/send_queue.c src/m3u.c src/epg.c src/embedded_web.c diff --git a/Dockerfile b/Dockerfile index 0b4a4775..5b860d43 100644 --- a/Dockerfile +++ b/Dockerfile @@ -39,10 +39,9 @@ EXPOSE 5140 # Recommended options: # --cap-add=NET_ADMIN: Allow setting larger UDP receive buffers (bypassing rmem_max via SO_RCVBUFFORCE) -# --ulimit memlock=-1:-1: Required for zero-copy (MSG_ZEROCOPY needs locked memory pages) # # Usage: -# docker run --network=host --cap-add=NET_ADMIN --ulimit memlock=-1:-1 --rm \ +# docker run --network=host --cap-add=NET_ADMIN --rm \ # ghcr.io/stackia/rtp2httpd:latest # Run the application diff --git a/README.md b/README.md index 1bc7e912..637c954f 100644 --- a/README.md +++ b/README.md @@ -52,7 +52,7 @@ rtp2httpd 是一个 IPTV 转发服务器,支持将组播 RTP/UDP、RTSP 转换 - **非阻塞 IO 模型**:使用 epoll 事件驱动,高效处理大量并发连接 - **多核优化**:支持多 worker 进程,充分利用多核 CPU 提高最大吞吐量 - **缓冲池优化**:预分配缓冲池,避免频繁内存分配,多客户端根据负载动态共享,避免慢客户端吃满资源 -- **零拷贝技术**:支持 Linux 内核 MSG_ZEROCOPY 特性,避免数据在用户态和内核态之间的拷贝 +- **同源复用**:同一工作进程内共享组播订阅、RTP 处理和批量缓冲,减少多客户端重复工作 - **轻量化**:使用纯 C 语言编写,零依赖,小巧简洁,适合运行在各种嵌入式设备上(路由器、光猫、NAS 等) - 程序大小仅 509KB (x86_64),并内置了 Web 播放器所有前端资源 - 查看 **[性能测试报告](https://rtp2httpd.com/reference/benchmark)**(与 msd_lite、udpxy、tvgate 的性能对比) diff --git a/docs/en/guide/installation.md b/docs/en/guide/installation.md index c9959e25..0d292ea0 100644 --- a/docs/en/guide/installation.md +++ b/docs/en/guide/installation.md @@ -75,7 +75,6 @@ Group=rtp2httpd AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE NoNewPrivileges=true -LimitMEMLOCK=infinity [Install] WantedBy=multi-user.target @@ -96,7 +95,7 @@ Suitable for Docker-capable devices. **Requires host network mode** to properly ### Basic Startup ```bash -docker run --network=host --cap-add=NET_ADMIN --ulimit memlock=-1:-1 --rm \ +docker run --network=host --cap-add=NET_ADMIN --rm \ ghcr.io/stackia/rtp2httpd:latest \ --noconfig --verbose 2 --listen 5140 --maxclients 20 ``` @@ -111,10 +110,6 @@ services: restart: always cap_add: - NET_ADMIN - ulimits: - memlock: - soft: -1 - hard: -1 command: --noconfig --verbose 2 --listen 5140 --maxclients 20 ``` @@ -122,14 +117,13 @@ services: > **About recommended parameters**: > > - `cap_add: NET_ADMIN`: Allows bypassing the kernel parameter `net.core.rmem_max` limit via `SO_RCVBUFFORCE`, setting a larger UDP receive buffer -> - `ulimits: memlock: -1`: Required when zero-copy is enabled (`--zerocopy-on-send`), MSG_ZEROCOPY needs to lock memory pages ### Mount Configuration File If you need to use a configuration file (assuming config file is at `/path/to/rtp2httpd.conf`): ```bash -docker run --network=host --cap-add=NET_ADMIN --ulimit memlock=-1:-1 --rm \ +docker run --network=host --cap-add=NET_ADMIN --rm \ -v /path/to/rtp2httpd.conf:/usr/local/etc/rtp2httpd.conf:ro \ ghcr.io/stackia/rtp2httpd:latest ``` diff --git a/docs/en/index.md b/docs/en/index.md index 1ab01de4..6b3ffd39 100644 --- a/docs/en/index.md +++ b/docs/en/index.md @@ -38,7 +38,7 @@ features: details: Web status page with connection statistics, bandwidth monitoring, log viewing, remote management - icon: 🚀 title: Lightweight & High Performance - details: Pure C with zero dependencies, epoll + multi-core + zero-copy, only 509KB for x86_64 + details: Pure C with zero dependencies, epoll + multi-core + shared buffers, only 509KB for x86_64 ---
diff --git a/docs/en/reference/configuration.md b/docs/en/reference/configuration.md index 612fa106..e9ccff35 100644 --- a/docs/en/reference/configuration.md +++ b/docs/en/reference/configuration.md @@ -20,21 +20,13 @@ rtp2httpd [options] - `-m, --maxclients ` - Maximum concurrent clients (default: 5) - `-w, --workers ` - Number of worker processes (default: 1) -Within a worker process, requests with the same resolved multicast address, port, source filter address (SSM), effective upstream interface, and FEC port automatically share a multicast subscription without additional configuration. The main RTP/UDP socket and configured FEC socket are created once and released when the last subscribed client disconnects. Different worker processes still subscribe independently. Channel names, the `/rtp/` and `/udp/` path forms, and FCC server parameters do not affect this matching. - -Regular multicast parses and reorders RTP once per shared source, combines payloads into batches of approximately 64 KiB, and distributes them through reference counting. Each client keeps its own send queue and send offset while sharing the underlying batch data. Slow clients still drop packets according to their own queue limits without pausing multicast reception for other clients. Partial batches are sent at the next worker timer check after 100 ms, avoiding long waits for low-bitrate streams. - -On Linux, when multiple clients share a full batch, rtp2httpd first attempts to store it in an immutable anonymous memory file and use `sendfile` to share its kernel data pages. The file is never rewritten when pool buffers are reused, preserving data still in transit. Unsupported systems or insufficient resources automatically fall back to regular memory sends. This optimization requires no extra configuration and does not depend on `zerocopy-on-send`. - -FCC unicast requests and transition state remain independent for each client, and the transition to multicast reuses a matching subscription. A client joins shared batch delivery after its unicast and transition data have been processed and its sequence position matches the shared stream. Snapshot processing and FEC recovery retain their own processing state. If a FEC port is configured or FEC packets appear in the main multicast stream, that source keeps its shared sockets but uses independent reorder and FEC recovery paths for each client. - `--listen` can be specified multiple times to listen on multiple TCP addresses/ports or Unix sockets: ```bash rtp2httpd --listen 5140 --listen 192.168.1.1:8081 --listen '[::1]:5140' --listen /var/run/rtp2httpd.sock ``` -Unix socket listen paths must be absolute and must not contain whitespace. At startup, if the same path already contains a socket file, rtp2httpd first probes whether the socket is still in use: if another process is listening on that path, startup is rejected; only confirmed stale socket files are removed automatically. If the path is a regular file, directory, or symbolic link, startup is rejected to avoid deleting user data. When any Unix socket listener is enabled, `zerocopy-on-send` is disabled globally. +Unix socket listen paths must be absolute and must not contain whitespace. At startup, if the same path already contains a socket file, rtp2httpd first probes whether the socket is still in use: if another process is listening on that path, startup is rejected; only confirmed stale socket files are removed automatically. If the path is a regular file, directory, or symbolic link, startup is rejected to avoid deleting user data. #### Upstream Network Interface Configuration @@ -61,10 +53,6 @@ Unix socket listen paths must be absolute and must not contain whitespace. At st - For 30 Mbps 4K IPTV streams, 512KB provides approximately 140ms of buffering - Increase this value to reduce packet loss for high-bandwidth streams - Actual buffer size may be limited by kernel parameter `net.core.rmem_max` -- `-Z, --zerocopy-on-send` - Enable zero-copy send to improve performance (default: disabled) - - Requires kernel support for MSG_ZEROCOPY (Linux 4.14+) - - Improves throughput and reduces CPU usage on supported devices - - Not recommended if rtp2httpd is behind a reverse proxy (nginx/caddy/lucky, etc.) ### FCC (Fast Channel Change) @@ -238,13 +226,6 @@ buffer-pool-max-size = 16384 # Actual buffer size may be limited by kernel parameter net.core.rmem_max udp-rcvbuf-size = 524288 -# Enable zero-copy send to improve performance (default: no) -# Set to yes/true/on/1 to enable zero-copy -# Requires kernel support for MSG_ZEROCOPY (Linux 4.14+) -# Can improve throughput and reduce CPU usage on supported devices, especially under high concurrent loads -# Not recommended if rtp2httpd is behind a reverse proxy (nginx/caddy/lucky, etc.) -zerocopy-on-send = no - # Override the User-Agent for upstream HTTP proxy requests (default: no override) # When set, this replaces the client User-Agent sent to upstream servers for /http/ requests http-proxy-user-agent = rtp2httpd-http-proxy/1.0 diff --git a/docs/guide/installation.md b/docs/guide/installation.md index c08f4c80..43740c3f 100644 --- a/docs/guide/installation.md +++ b/docs/guide/installation.md @@ -75,7 +75,6 @@ Group=rtp2httpd AmbientCapabilities=CAP_NET_ADMIN CAP_NET_BIND_SERVICE CapabilityBoundingSet=CAP_NET_ADMIN CAP_NET_BIND_SERVICE NoNewPrivileges=true -LimitMEMLOCK=infinity [Install] WantedBy=multi-user.target @@ -96,7 +95,7 @@ sudo systemctl status rtp2httpd ### 基本启动方式 ```bash -docker run --network=host --cap-add=NET_ADMIN --ulimit memlock=-1:-1 --rm \ +docker run --network=host --cap-add=NET_ADMIN --rm \ ghcr.io/stackia/rtp2httpd:latest \ --noconfig --verbose 2 --listen 5140 --maxclients 20 ``` @@ -111,10 +110,6 @@ services: restart: always cap_add: - NET_ADMIN - ulimits: - memlock: - soft: -1 - hard: -1 command: --noconfig --verbose 2 --listen 5140 --maxclients 20 ``` @@ -122,14 +117,13 @@ services: > **关于推荐参数**: > > - `cap_add: NET_ADMIN`:允许通过 `SO_RCVBUFFORCE` 绕过内核参数 `net.core.rmem_max` 限制,设置更大的 UDP 接收缓冲区 -> - `ulimits: memlock: -1`:启用零拷贝(`--zerocopy-on-send`)时必需,MSG_ZEROCOPY 需要锁定内存页 ### 挂载配置文件 如果需要使用配置文件(假设配置文件位于 `/path/to/rtp2httpd.conf`): ```bash -docker run --network=host --cap-add=NET_ADMIN --ulimit memlock=-1:-1 --rm \ +docker run --network=host --cap-add=NET_ADMIN --rm \ -v /path/to/rtp2httpd.conf:/usr/local/etc/rtp2httpd.conf:ro \ ghcr.io/stackia/rtp2httpd:latest ``` diff --git a/docs/index.md b/docs/index.md index 6c62f7be..09d589f2 100644 --- a/docs/index.md +++ b/docs/index.md @@ -38,7 +38,7 @@ features: details: Web 状态页面,连接统计、带宽监控、日志查看、远程管理 - icon: 🚀 title: 轻量高性能 - details: 纯 C 零依赖,epoll + 多核 + 零拷贝,x86_64 仅 509KB + details: 纯 C 零依赖,epoll + 多核 + 共享缓冲,x86_64 仅 509KB ---
diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index dd788314..75d0f70e 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -20,21 +20,13 @@ rtp2httpd [选项] - `-m, --maxclients <数量>` - 最大并发客户端数 (默认: 5) - `-w, --workers <数量>` - 工作进程数 (默认: 1) -同一工作进程内,解析后的组播地址、端口、源过滤地址(SSM)、有效上游接口及 FEC 端口相同的请求会自动共享组播订阅,无需额外配置。RTP/UDP 主 socket 及配置的 FEC socket 只创建一份,最后一个订阅客户端断开后释放;不同工作进程之间仍独立订阅。频道名称、`/rtp/` 与 `/udp/` 路径形式以及 FCC 服务器参数不影响上述匹配。 - -普通组播在共享源上完成一次 RTP 解析和重排,将负载合并为约 64 KiB 的批次,再通过引用计数分发。每个客户端保留独立的发送队列和发送偏移,底层批次数据共享;慢客户端仍按自身队列限制丢包,不会暂停其他客户端的组播接收。未满的批次会在 100 ms 后的下一次工作进程定时检查中发送,避免低码率流长时间等待。 - -Linux 上多个客户端共享完整批次时,优先将批次保存为不可修改的匿名内存文件,通过 `sendfile` 复用内核中的数据页。该文件不会随缓冲池复用而被改写,保证仍在传输的数据有效;不支持该方式或资源不足时自动回退到普通内存发送。此优化无需额外配置,也不依赖 `zerocopy-on-send`。 - -FCC 单播请求和切换状态按客户端独立维护,衔接组播时复用匹配的订阅;待单播及衔接数据处理完成、序号与共享流对齐后,加入共享批次分发。截图处理和 FEC 恢复仍使用各自的处理状态。配置了 FEC 端口,或主组播流中出现 FEC 包时,该源保留共享 socket,使用每个客户端独立的重排和 FEC 恢复路径。 - `--listen` 可以重复指定,用于同时监听多个 TCP 地址/端口或 Unix socket: ```bash rtp2httpd --listen 5140 --listen 192.168.1.1:8081 --listen '[::1]:5140' --listen /var/run/rtp2httpd.sock ``` -Unix socket 监听路径必须是绝对路径,且路径中不能包含空白字符。启动时如果同路径已存在 socket 文件,rtp2httpd 会先探测该 socket 是否仍在使用:如果已有进程正在监听该路径,则拒绝启动;只有确认是残留 socket 文件时才会自动清理。如果同路径是普通文件、目录或符号链接,则会拒绝启动以避免误删数据。启用任意 Unix socket 监听时,`zerocopy-on-send` 会被全局关闭。 +Unix socket 监听路径必须是绝对路径,且路径中不能包含空白字符。启动时如果同路径已存在 socket 文件,rtp2httpd 会先探测该 socket 是否仍在使用:如果已有进程正在监听该路径,则拒绝启动;只有确认是残留 socket 文件时才会自动清理。如果同路径是普通文件、目录或符号链接,则会拒绝启动以避免误删数据。 #### 上游网络接口配置 @@ -61,10 +53,6 @@ Unix socket 监听路径必须是绝对路径,且路径中不能包含空白 - 对于 30 Mbps 的 4K IPTV 流,512KB 可提供约 140ms 的缓冲 - 增大此值以减少高带宽流的丢包 - 实际缓冲区大小可能受内核参数 `net.core.rmem_max` 限制 -- `-Z, --zerocopy-on-send` - 启用零拷贝发送以提升性能 (默认: 关闭) - - 需要内核支持 MSG_ZEROCOPY (Linux 4.14+) - - 在支持的设备上提升吞吐量并降低 CPU 占用 - - 如果你的 rtp2httpd 位于反向代理之后 (nginx/caddy/lucky 等),不建议开启这个选项 ### FCC 快速换台 @@ -236,13 +224,6 @@ buffer-pool-max-size = 16384 # 实际缓冲区大小可能受内核参数 net.core.rmem_max 限制 udp-rcvbuf-size = 524288 -# 启用零拷贝发送以提升性能(默认: no) -# 设为 yes/true/on/1 以启用零拷贝 -# 需要内核支持 MSG_ZEROCOPY (Linux 4.14+) -# 在支持的设备上可提升吞吐量并降低 CPU 占用,特别是在高并发负载下 -# 如果你的 rtp2httpd 位于反向代理之后 (nginx/caddy/lucky 等),不建议开启这个选项 -zerocopy-on-send = no - # 覆盖上游 HTTP 代理请求的 User-Agent(默认: 不覆盖) # 设置后将替换发送给 /http/ 上游服务器的客户端 User-Agent http-proxy-user-agent = rtp2httpd-http-proxy/1.0 diff --git a/e2e/test_flow_control.py b/e2e/test_flow_control.py index 1a6bc2eb..6dfa86f3 100644 --- a/e2e/test_flow_control.py +++ b/e2e/test_flow_control.py @@ -2,8 +2,8 @@ E2E coverage for the upstream flow-control fix. A slow downstream client used to be aborted partway through a large HTTP -proxy response because the per-connection zerocopy queue would saturate and -``connection_queue_zerocopy()`` would return -1 (packet-drop semantics +proxy response because the per-connection send queue would saturate and +``connection_queue_buffer()`` would return -1 (packet-drop semantics inherited from RTP/UDP). The fix pauses upstream reads when the client send queue exceeds the high watermark and resumes them once it drops back below the low watermark. @@ -36,7 +36,7 @@ @pytest.fixture(scope="module") def shared_r2h(r2h_binary): # ``-b 128`` shrinks the global buffer pool cap to 128 buffers - # (~192 KiB), which forces the per-connection zerocopy queue limit + # (~192 KiB), which forces the per-connection send queue limit # down to ~96 KiB. With the default cap (16384 buffers / ~24 MiB) a # short test body would be absorbed entirely without ever crossing # the HWM, defeating the whole purpose of these tests. @@ -119,7 +119,7 @@ class TestHTTPProxyBackpressure: """A slow HTTP client must receive the full proxied body.""" def test_slow_client_receives_full_body(self, shared_r2h): - # 1 MiB body comfortably exceeds the ~96 KiB zerocopy queue limit + # 1 MiB body comfortably exceeds the ~96 KiB send queue limit # imposed by the ``-b 128`` shared_r2h fixture, so the slow client # forces multiple pause/resume cycles before EOF. body_size = 1024 * 1024 diff --git a/e2e/test_multicast.py b/e2e/test_multicast.py index 8cf617be..066c51f2 100644 --- a/e2e/test_multicast.py +++ b/e2e/test_multicast.py @@ -23,7 +23,7 @@ pytestmark = pytest.mark.multicast -# Timeout for multicast streaming tests. At ~2 Mbps the 64 KB zerocopy +# Timeout for multicast streaming tests. At ~2 Mbps the 64 KB send # batch threshold fills in < 1 s, but multicast group join on macOS may # add a few seconds of latency. _MCAST_STREAM_TIMEOUT = 10.0 diff --git a/e2e/test_multicast_shared.py b/e2e/test_multicast_shared.py index 1de6e9e2..4e4644d6 100644 --- a/e2e/test_multicast_shared.py +++ b/e2e/test_multicast_shared.py @@ -28,7 +28,7 @@ def shared_source_r2h(r2h_binary): r2h = R2HProcess( r2h_binary, find_free_port(), - extra_args=["-v", "4", "-w", "1", "-m", "100", "-r", LOOPBACK_IF, "-S", "-Z"], + extra_args=["-v", "4", "-w", "1", "-m", "100", "-r", LOOPBACK_IF, "-S"], ) r2h.start() yield r2h diff --git a/e2e/test_unix_socket.py b/e2e/test_unix_socket.py index 72853a3b..dbb4020c 100644 --- a/e2e/test_unix_socket.py +++ b/e2e/test_unix_socket.py @@ -3,7 +3,7 @@ Tests cover CLI and config-file Unix socket binds, mixed TCP/Unix listeners, multi-worker accept behavior, stale socket cleanup, regular-file rejection, -zero-copy disablement, and file responses over Unix sockets. +and file responses over Unix sockets. """ from __future__ import annotations @@ -346,25 +346,6 @@ def test_reload_keeps_old_unix_listener_when_new_path_fails(self, r2h_binary): finally: r2h.stop() - def test_unix_socket_disables_zerocopy(self, r2h_binary): - with tempfile.TemporaryDirectory() as tmpdir: - sock_path = _socket_path(tmpdir) - r2h = R2HProcess( - r2h_binary, - None, - extra_args=["-v", "4", "--zerocopy-on-send"], - capture_log=True, - listen=sock_path, - ) - try: - r2h.start() - status, _, _ = unix_http_get(sock_path, "/status") - assert status == 200 - log = r2h.read_log() - assert "Zero-copy send disabled because Unix socket listener is configured" in log - finally: - r2h.stop() - def test_epg_file_response_over_unix_socket(self, r2h_binary): epg_path = write_temp_file(SAMPLE_EPG_XML.encode(), suffix=".xml", prefix="r2h_unix_epg_") try: diff --git a/e2e/test_zerocopy.py b/e2e/test_zerocopy.py deleted file mode 100644 index 7305fc3c..00000000 --- a/e2e/test_zerocopy.py +++ /dev/null @@ -1,271 +0,0 @@ -""" -E2E tests for MSG_ZEROCOPY send path (Linux only). - -These tests verify that rtp2httpd streams data correctly when the ---zerocopy-on-send (-Z) flag is enabled. MSG_ZEROCOPY uses the kernel's -MSG_ERRQUEUE for completion notifications, which arrive as EPOLLERR events. -With edge-triggered polling (EPOLLET), the handler must drain all -completions in one pass — these tests exercise that code path. - -On kernels < 4.14 or non-Linux platforms, rtp2httpd silently falls back to -regular send(), so these tests validate the fallback path too. -""" - -import concurrent.futures -import sys - -import pytest -from helpers import ( - LOOPBACK_IF, - MCAST_ADDR, - MockHTTPUpstream, - MockRTSPServer, - MockRTSPServerUDP, - MulticastSender, - R2HProcess, - find_free_port, - find_free_udp_port, - stream_get, -) - -# Skip entire module on non-Linux (MSG_ZEROCOPY is Linux-only) -pytestmark = [ - pytest.mark.skipif(sys.platform != "linux", reason="MSG_ZEROCOPY is Linux-only"), -] - -_STREAM_TIMEOUT = 10.0 - -# Common extra args: zerocopy enabled, debug logging, 100 max-clients -_ZC_ARGS = ["-Z", "-v", "4", "-m", "100"] - - -# --------------------------------------------------------------------------- -# Module-scoped shared rtp2httpd instances -# --------------------------------------------------------------------------- - - -@pytest.fixture(scope="module") -def zc_multicast_r2h(r2h_binary): - """Zerocopy rtp2httpd with multicast interface for multicast tests.""" - port = find_free_port() - r2h = R2HProcess( - r2h_binary, - port, - extra_args=_ZC_ARGS + ["-r", LOOPBACK_IF], - ) - r2h.start() - yield r2h - r2h.stop() - - -@pytest.fixture(scope="module") -def zc_r2h(r2h_binary): - """Zerocopy rtp2httpd for RTSP and HTTP proxy tests.""" - port = find_free_port() - r2h = R2HProcess( - r2h_binary, - port, - extra_args=_ZC_ARGS, - ) - r2h.start() - yield r2h - r2h.stop() - - -# --------------------------------------------------------------------------- -# Multicast + zerocopy -# --------------------------------------------------------------------------- - - -@pytest.mark.multicast -class TestZerocopyMulticast: - """Verify multicast RTP streaming works with MSG_ZEROCOPY enabled.""" - - def test_multicast_stream_200(self, zc_multicast_r2h): - """Basic multicast stream should return 200 with zerocopy.""" - mcast_port = find_free_udp_port() - sender = MulticastSender(addr=MCAST_ADDR, port=mcast_port, pps=300) - sender.start() - try: - status, _, body = stream_get( - "127.0.0.1", - zc_multicast_r2h.port, - f"/rtp/{MCAST_ADDR}:{mcast_port}", - read_bytes=8192, - timeout=_STREAM_TIMEOUT, - ) - assert status == 200 - assert len(body) > 0, "Expected stream data with zerocopy" - finally: - sender.stop() - - def test_multicast_ts_integrity(self, zc_multicast_r2h): - """TS sync bytes must be intact after zerocopy transmission.""" - mcast_port = find_free_udp_port() - sender = MulticastSender(addr=MCAST_ADDR, port=mcast_port, pps=300) - sender.start() - try: - status, _, body = stream_get( - "127.0.0.1", - zc_multicast_r2h.port, - f"/rtp/{MCAST_ADDR}:{mcast_port}", - read_bytes=8192, - timeout=_STREAM_TIMEOUT, - ) - assert status == 200 - assert len(body) >= 188, "Need at least one TS packet" - assert body[0] == 0x47, f"Expected TS sync byte 0x47, got 0x{body[0]:02x}" - finally: - sender.stop() - - def test_multicast_concurrent_clients(self, zc_multicast_r2h): - """Multiple clients streaming the same multicast with zerocopy.""" - mcast_port = find_free_udp_port() - sender = MulticastSender(addr=MCAST_ADDR, port=mcast_port, pps=300) - sender.start() - try: - url = f"/rtp/{MCAST_ADDR}:{mcast_port}" - - with concurrent.futures.ThreadPoolExecutor(max_workers=3) as pool: - futures = [ - pool.submit(stream_get, "127.0.0.1", zc_multicast_r2h.port, url, 4096, _STREAM_TIMEOUT) - for _ in range(3) - ] - results = [f.result() for f in futures] - - for i, (s, _, b) in enumerate(results): - assert s == 200, f"Client {i} got status {s}" - assert len(b) > 0, f"Client {i} got no data" - finally: - sender.stop() - - -# --------------------------------------------------------------------------- -# RTSP + zerocopy -# --------------------------------------------------------------------------- - - -class TestZerocopyRTSP: - """Verify RTSP streaming works with MSG_ZEROCOPY enabled.""" - - @pytest.mark.rtsp - def test_rtsp_tcp_stream(self, zc_r2h): - """RTSP TCP interleaved streaming should work with zerocopy.""" - mock = MockRTSPServer(num_packets=200) - mock.start() - try: - status, _, body = stream_get( - "127.0.0.1", - zc_r2h.port, - f"/rtsp/127.0.0.1:{mock.port}", - read_bytes=4096, - timeout=_STREAM_TIMEOUT, - ) - assert status == 200 - assert len(body) > 0, "Expected RTSP TCP stream data with zerocopy" - finally: - mock.stop() - - @pytest.mark.rtsp - def test_rtsp_tcp_data_integrity(self, zc_r2h): - """TS sync bytes must be intact in RTSP TCP stream with zerocopy.""" - mock = MockRTSPServer(num_packets=200) - mock.start() - try: - status, _, body = stream_get( - "127.0.0.1", - zc_r2h.port, - f"/rtsp/127.0.0.1:{mock.port}", - read_bytes=4096, - timeout=_STREAM_TIMEOUT, - ) - assert status == 200 - assert len(body) >= 188 - assert body[0] == 0x47, f"Expected TS sync byte 0x47, got 0x{body[0]:02x}" - finally: - mock.stop() - - @pytest.mark.rtsp - def test_rtsp_udp_stream(self, zc_r2h): - """RTSP UDP streaming should work with zerocopy.""" - mock = MockRTSPServerUDP(num_packets=200) - mock.start() - try: - status, _, body = stream_get( - "127.0.0.1", - zc_r2h.port, - f"/rtsp/127.0.0.1:{mock.port}", - read_bytes=4096, - timeout=_STREAM_TIMEOUT, - ) - assert status == 200 - assert len(body) > 0, "Expected RTSP UDP stream data with zerocopy" - finally: - mock.stop() - - -# --------------------------------------------------------------------------- -# HTTP Proxy + zerocopy -# --------------------------------------------------------------------------- - - -class TestZerocopyHTTPProxy: - """Verify HTTP proxy works with MSG_ZEROCOPY enabled.""" - - @pytest.mark.http_proxy - def test_proxy_stream(self, zc_r2h): - """HTTP proxy should forward data correctly with zerocopy.""" - body_data = b"x" * 16384 # 16KB body - upstream = MockHTTPUpstream( - routes={ - "/stream": { - "status": 200, - "body": body_data, - "headers": {"Content-Type": "application/octet-stream"}, - }, - } - ) - upstream.start() - try: - from helpers import http_get - - status, _, body = http_get( - "127.0.0.1", - zc_r2h.port, - f"/http/127.0.0.1:{upstream.port}/stream", - timeout=5.0, - ) - assert status == 200 - assert body == body_data, "Body mismatch with zerocopy proxy" - finally: - upstream.stop() - - @pytest.mark.http_proxy - def test_proxy_large_body(self, zc_r2h): - """HTTP proxy should handle large payloads correctly with zerocopy.""" - # 256KB body - exercises multiple sendmsg calls with MSG_ZEROCOPY - body_data = bytes(range(256)) * 1024 # 256KB - upstream = MockHTTPUpstream( - routes={ - "/large": { - "status": 200, - "body": body_data, - "headers": {"Content-Type": "application/octet-stream"}, - }, - } - ) - upstream.start() - try: - from helpers import http_get - - status, _, body = http_get( - "127.0.0.1", - zc_r2h.port, - f"/http/127.0.0.1:{upstream.port}/large", - timeout=10.0, - ) - assert status == 200 - assert len(body) == len(body_data), f"Expected {len(body_data)} bytes, got {len(body)}" - assert body == body_data, "Body content mismatch with zerocopy proxy" - finally: - upstream.stop() diff --git a/ikuai-support/rtp2httpd/app/option.json b/ikuai-support/rtp2httpd/app/option.json index 6077d3ba..f0da2c04 100644 --- a/ikuai-support/rtp2httpd/app/option.json +++ b/ikuai-support/rtp2httpd/app/option.json @@ -285,19 +285,6 @@ "min": 0, "max": 16 }, - { - "default": 0, - "attrname": "RTP2HTTPD_ZEROCOPY_ON_SEND", - "label": { - "en": "Zero-copy on send (0=off 1=on, enable only for performance bottlenecks)", - "zh": "零拷贝发送(0=关闭 1=开启,仅在性能瓶颈时开启)" - }, - "required": true, - "scope": "config", - "type": "integer", - "min": 0, - "max": 1 - }, { "default": "", "attrname": "RTP2HTTPD_RTSP_STUN_SERVER", diff --git a/ikuai-support/rtp2httpd/readme b/ikuai-support/rtp2httpd/readme index 511e0a13..876ac7b4 100644 --- a/ikuai-support/rtp2httpd/readme +++ b/ikuai-support/rtp2httpd/readme @@ -29,7 +29,7 @@ rtp2httpd 可以将 IPTV 组播流(RTP/UDP)、RTSP 流、HTTP 流转换为 - 外部 M3U 播放列表地址:填写运营商或自己整理的频道列表 URL,用于网页播放器和 playlist.m3u。 - 访问令牌:设置后所有请求必须携带 r2h-token 参数,建议在需要公网访问时配合使用。 - 最大客户端数 / 工作进程数:并发能力相关,默认值适合大多数家庭场景。 -- 其余高级选项(缓冲区、FCC、零拷贝、视频快照等)保持默认即可,含义可参考官方文档。 +- 其余高级选项(缓冲区、FCC、视频快照等)保持默认即可,含义可参考官方文档。 【常见问题】 diff --git a/ikuai-support/rtp2httpd/scripts/start.sh b/ikuai-support/rtp2httpd/scripts/start.sh index 3c0099ec..b9947efa 100755 --- a/ikuai-support/rtp2httpd/scripts/start.sh +++ b/ikuai-support/rtp2httpd/scripts/start.sh @@ -94,7 +94,6 @@ load_env_file "$RUNTIME_ENV" : "${RTP2HTTPD_UDP_RCVBUF_SIZE:=524288}" : "${RTP2HTTPD_MCAST_REJOIN_INTERVAL:=0}" : "${RTP2HTTPD_FCC_LISTEN_PORT_RANGE:=}" -: "${RTP2HTTPD_ZEROCOPY_ON_SEND:=0}" : "${RTP2HTTPD_RTSP_STUN_SERVER:=}" : "${RTP2HTTPD_EXTERNAL_M3U:=}" : "${RTP2HTTPD_EXTERNAL_M3U_UPDATE_INTERVAL:=7200}" @@ -158,9 +157,6 @@ fi if [ -n "$RTP2HTTPD_FCC_LISTEN_PORT_RANGE" ]; then set -- "$@" --fcc-listen-port-range "$RTP2HTTPD_FCC_LISTEN_PORT_RANGE" fi -if [ "$RTP2HTTPD_ZEROCOPY_ON_SEND" = "1" ]; then - set -- "$@" --zerocopy-on-send -fi if [ -n "$RTP2HTTPD_RTSP_STUN_SERVER" ]; then set -- "$@" --rtsp-stun-server "$RTP2HTTPD_RTSP_STUN_SERVER" fi diff --git a/openwrt-support/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js b/openwrt-support/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js index cb36d09b..50cb3180 100644 --- a/openwrt-support/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js +++ b/openwrt-support/luci-app-rtp2httpd/htdocs/luci-static/resources/view/rtp2httpd.js @@ -577,7 +577,7 @@ return view.extend({ "buffer_pool_max_size", _("Buffer Pool Max Size"), _( - "Maximum number of buffers in zero-copy pool. Each buffer is 1536 bytes. Default is 16384 (~24MB). Increase to improve throughput for multi-client concurrency." + "Maximum number of buffers in buffer pool. Each buffer is 1536 bytes. Default is 16384 (~24MB). Increase to improve throughput for multi-client concurrency." ) ); o.datatype = "range(1024, 1048576)"; @@ -622,18 +622,6 @@ return view.extend({ o.placeholder = "begin-end"; o.depends("use_config_file", "0"); - o = s.taboption( - "network", - form.Flag, - "zerocopy_on_send", - _("Zero-Copy on Send"), - _( - "Enable zero-copy send with MSG_ZEROCOPY for better performance. Requires kernel 4.14+ (MSG_ZEROCOPY support). On supported devices, this can improve throughput and reduce CPU usage, especially under high concurrent load. Recommended only when experiencing performance bottlenecks." - ) - ); - o.default = "0"; - o.depends("use_config_file", "0"); - o = s.taboption( "network", form.Value, diff --git a/openwrt-support/luci-app-rtp2httpd/po/templates/rtp2httpd.pot b/openwrt-support/luci-app-rtp2httpd/po/templates/rtp2httpd.pot index cb2972bd..8f0fb4ce 100644 --- a/openwrt-support/luci-app-rtp2httpd/po/templates/rtp2httpd.pot +++ b/openwrt-support/luci-app-rtp2httpd/po/templates/rtp2httpd.pot @@ -70,14 +70,6 @@ msgid "" "with snapshot=1 query parameter" msgstr "" -#: htdocs/luci-static/resources/view/rtp2httpd.js:576 -msgid "" -"Enable zero-copy send with MSG_ZEROCOPY for better performance. Requires " -"kernel 4.14+ (MSG_ZEROCOPY support). On supported devices, this can improve " -"throughput and reduce CPU usage, especially under high concurrent load. " -"Recommended only when experiencing performance bottlenecks." -msgstr "" - #: htdocs/luci-static/resources/view/rtp2httpd.js:296 msgid "Enabled" msgstr "" @@ -184,7 +176,7 @@ msgstr "" #: htdocs/luci-static/resources/view/rtp2httpd.js:525 msgid "" -"Maximum number of buffers in zero-copy pool. Each buffer is 1536 bytes. " +"Maximum number of buffers in buffer pool. Each buffer is 1536 bytes. " "Default is 16384 (~24MB). Increase to improve throughput for multi-client " "concurrency." msgstr "" @@ -401,10 +393,6 @@ msgstr "" msgid "X-Forwarded-For" msgstr "" -#: htdocs/luci-static/resources/view/rtp2httpd.js:574 -msgid "Zero-Copy on Send" -msgstr "" - #: htdocs/luci-static/resources/view/rtp2httpd.js:279 #: root/usr/share/luci/menu.d/luci-app-rtp2httpd.json:3 msgid "rtp2httpd" diff --git a/openwrt-support/luci-app-rtp2httpd/po/zh_Hans/rtp2httpd.po b/openwrt-support/luci-app-rtp2httpd/po/zh_Hans/rtp2httpd.po index 280b0045..aea17068 100644 --- a/openwrt-support/luci-app-rtp2httpd/po/zh_Hans/rtp2httpd.po +++ b/openwrt-support/luci-app-rtp2httpd/po/zh_Hans/rtp2httpd.po @@ -82,17 +82,6 @@ msgid "" msgstr "" "启用视频快照功能。启用后,客户端可以通过 snapshot=1 查询参数请求视频快照" -#: htdocs/luci-static/resources/view/rtp2httpd.js:576 -msgid "" -"Enable zero-copy send with MSG_ZEROCOPY for better performance. Requires " -"kernel 4.14+ (MSG_ZEROCOPY support). On supported devices, this can improve " -"throughput and reduce CPU usage, especially under high concurrent load. " -"Recommended only when experiencing performance bottlenecks." -msgstr "" -"启用 MSG_ZEROCOPY 零拷贝发送以提升性能。需要内核 4.14+(支持 MSG_ZEROCOPY)。" -"在支持的设备上,可提升吞吐量并降低 CPU 占用,特别是在高并发负载下。建议仅在遇" -"到性能瓶颈时开启。" - #: htdocs/luci-static/resources/view/rtp2httpd.js:296 msgid "Enabled" msgstr "启用" @@ -208,11 +197,11 @@ msgstr "最大客户端数" #: htdocs/luci-static/resources/view/rtp2httpd.js:525 msgid "" -"Maximum number of buffers in zero-copy pool. Each buffer is 1536 bytes. " +"Maximum number of buffers in buffer pool. Each buffer is 1536 bytes. " "Default is 16384 (~24MB). Increase to improve throughput for multi-client " "concurrency." msgstr "" -"零拷贝缓冲池的最大缓冲区数量。每个缓冲区 1536 字节,默认 16384 个(约 " +"缓冲池的最大缓冲区数量。每个缓冲区 1536 字节,默认 16384 个(约 " "24MB)。增大此值以提高多客户端并发时的吞吐量。" #: htdocs/luci-static/resources/view/rtp2httpd.js:293 @@ -448,10 +437,6 @@ msgstr "工作进程数" msgid "X-Forwarded-For" msgstr "X-Forwarded-For" -#: htdocs/luci-static/resources/view/rtp2httpd.js:574 -msgid "Zero-Copy on Send" -msgstr "启用零拷贝发送" - #: htdocs/luci-static/resources/view/rtp2httpd.js:279 #: root/usr/share/luci/menu.d/luci-app-rtp2httpd.json:3 msgid "rtp2httpd" diff --git a/openwrt-support/rtp2httpd/files/rtp2httpd.conf b/openwrt-support/rtp2httpd/files/rtp2httpd.conf index 46285fb1..a5cbbf2f 100644 --- a/openwrt-support/rtp2httpd/files/rtp2httpd.conf +++ b/openwrt-support/rtp2httpd/files/rtp2httpd.conf @@ -44,7 +44,6 @@ config rtp2httpd # option external_m3u_update_interval '7200' # option mcast_rejoin_interval '0' # option fcc_listen_port_range '40000-40100' - # option zerocopy_on_send '0' # option http_proxy_user_agent 'rtp2httpd-http-proxy/1.0' # option rtsp_user_agent 'rtp2httpd/custom' # option rtsp_stun_server 'stun.miwifi.com' diff --git a/openwrt-support/rtp2httpd/files/rtp2httpd.init b/openwrt-support/rtp2httpd/files/rtp2httpd.init index b8674cfb..510af944 100644 --- a/openwrt-support/rtp2httpd/files/rtp2httpd.init +++ b/openwrt-support/rtp2httpd/files/rtp2httpd.init @@ -96,9 +96,6 @@ start_instance() { config_get_bool aux "$cfg" 'video_snapshot' '0' [ "$aux" = 1 ] && procd_append_param command "--video-snapshot" - # Handle zerocopy_on_send flag - config_get_bool aux "$cfg" 'zerocopy_on_send' '0' - [ "$aux" = 1 ] && procd_append_param command "--zerocopy-on-send" # Handle use_relative_path_in_m3u flag config_get_bool aux "$cfg" 'use_relative_path_in_m3u' '0' diff --git a/rtp2httpd.conf b/rtp2httpd.conf index cffa879f..36469a51 100644 --- a/rtp2httpd.conf +++ b/rtp2httpd.conf @@ -105,13 +105,6 @@ verbosity = 1 # The actual buffer size may be limited by kernel parameter net.core.rmem_max. ;udp-rcvbuf-size = 524288 -# Enable zero-copy send with MSG_ZEROCOPY (default: no) -# Set to 1, yes, true, or on to enable zero-copy for better performance -# Zero-copy requires kernel 4.14+ with MSG_ZEROCOPY support -# On supported devices, enabling this can improve throughput and reduce CPU usage -# Not recommended when running behind reverse proxies like Nginx/Caddy/Lucky -;zerocopy-on-send = no - # Override User-Agent header for upstream HTTP proxy requests (default: disabled) # When set, this value replaces the client User-Agent header sent to upstream /http/ targets ;http-proxy-user-agent = rtp2httpd-http-proxy/1.0 diff --git a/src/buffer_pool.c b/src/buffer_pool.c index a689f3fb..3ce6991c 100644 --- a/src/buffer_pool.c +++ b/src/buffer_pool.c @@ -1,8 +1,8 @@ #include "buffer_pool.h" #include "rtp2httpd.h" +#include "send_queue.h" #include "status.h" #include "utils.h" -#include "zerocopy.h" #include #include #include @@ -32,11 +32,11 @@ void buffer_pool_update_stats(buffer_pool_t *pool) { worker_stats_t *stats = &status_shared->worker_stats[worker_id]; - if (pool == &zerocopy_state.pool) { + if (pool == &send_buffer_state.pool) { stats->pool_total_buffers = pool->num_buffers; stats->pool_free_buffers = pool->num_free; stats->pool_max_buffers = pool->max_buffers; - } else if (pool == &zerocopy_state.control_pool) { + } else if (pool == &send_buffer_state.control_pool) { stats->control_pool_total_buffers = pool->num_buffers; stats->control_pool_free_buffers = pool->num_free; stats->control_pool_max_buffers = pool->max_buffers; @@ -107,9 +107,9 @@ int buffer_pool_init(buffer_pool_t *pool, size_t buffer_size, size_t initial_buf } static inline const char *buffer_pool_name(buffer_pool_t *pool) { - if (pool == &zerocopy_state.batch_pool) + if (pool == &send_buffer_state.batch_pool) return "Multicast batch pool"; - return (pool == &zerocopy_state.pool) ? "Buffer pool" : "Control pool"; + return (pool == &send_buffer_state.pool) ? "Buffer pool" : "Control pool"; } static int buffer_pool_expand(buffer_pool_t *pool) { @@ -137,9 +137,9 @@ static int buffer_pool_expand(buffer_pool_t *pool) { pool->num_buffers += buffers_to_add; pool->num_free += buffers_to_add; - if (pool == &zerocopy_state.pool) { + if (pool == &send_buffer_state.pool) { WORKER_STATS_INC(pool_expansions); - } else if (pool == &zerocopy_state.control_pool) { + } else if (pool == &send_buffer_state.control_pool) { WORKER_STATS_INC(control_pool_expansions); } @@ -201,7 +201,7 @@ void buffer_ref_put(buffer_ref_t *ref) { ref->shared_fd = -1; } - buffer_pool_t *pool = ref->segment ? ref->segment->parent : &zerocopy_state.pool; + buffer_pool_t *pool = ref->segment ? ref->segment->parent : &send_buffer_state.pool; if (ref->segment) { ref->segment->num_free++; @@ -280,7 +280,7 @@ void buffer_ref_snapshot(buffer_ref_t *ref) { } buffer_ref_t *buffer_pool_alloc_batch(void) { - buffer_pool_t *pool = &zerocopy_state.batch_pool; + buffer_pool_t *pool = &send_buffer_state.batch_pool; if (!pool->segments) { size_t max_buffers = (size_t)config.buffer_pool_max_size * BUFFER_POOL_BUFFER_SIZE / BUFFER_POOL_BATCH_SIZE; if (max_buffers < 4) @@ -296,9 +296,9 @@ buffer_ref_t *buffer_pool_alloc_from(buffer_pool_t *pool) { return NULL; if (!pool->free_list) { - if (pool == &zerocopy_state.pool) { + if (pool == &send_buffer_state.pool) { WORKER_STATS_INC(pool_exhaustions); - } else if (pool == &zerocopy_state.control_pool) { + } else if (pool == &send_buffer_state.control_pool) { WORKER_STATS_INC(control_pool_exhaustions); } @@ -341,9 +341,9 @@ buffer_ref_t *buffer_pool_alloc_from(buffer_pool_t *pool) { return ref; } -buffer_ref_t *buffer_pool_alloc(void) { return buffer_pool_alloc_from(&zerocopy_state.pool); } +buffer_ref_t *buffer_pool_alloc(void) { return buffer_pool_alloc_from(&send_buffer_state.pool); } -buffer_ref_t *buffer_pool_alloc_control(void) { return buffer_pool_alloc_from(&zerocopy_state.control_pool); } +buffer_ref_t *buffer_pool_alloc_control(void) { return buffer_pool_alloc_from(&send_buffer_state.control_pool); } static void buffer_pool_try_shrink_pool(buffer_pool_t *pool, size_t min_buffers) { if (pool->num_free <= pool->high_watermark || pool->num_buffers <= min_buffers) { @@ -406,9 +406,9 @@ static void buffer_pool_try_shrink_pool(buffer_pool_t *pool, size_t min_buffers) segments_freed++; - if (pool == &zerocopy_state.pool) { + if (pool == &send_buffer_state.pool) { WORKER_STATS_INC(pool_shrinks); - } else if (pool == &zerocopy_state.control_pool) { + } else if (pool == &send_buffer_state.control_pool) { WORKER_STATS_INC(control_pool_shrinks); } @@ -434,7 +434,7 @@ static void buffer_pool_try_shrink_pool(buffer_pool_t *pool, size_t min_buffers) } void buffer_pool_try_shrink(void) { - buffer_pool_try_shrink_pool(&zerocopy_state.pool, BUFFER_POOL_INITIAL_SIZE); - buffer_pool_try_shrink_pool(&zerocopy_state.control_pool, CONTROL_POOL_INITIAL_SIZE); - buffer_pool_try_shrink_pool(&zerocopy_state.batch_pool, 4); + buffer_pool_try_shrink_pool(&send_buffer_state.pool, BUFFER_POOL_INITIAL_SIZE); + buffer_pool_try_shrink_pool(&send_buffer_state.control_pool, CONTROL_POOL_INITIAL_SIZE); + buffer_pool_try_shrink_pool(&send_buffer_state.batch_pool, 4); } diff --git a/src/buffer_pool.h b/src/buffer_pool.h index e2b66177..abce405a 100644 --- a/src/buffer_pool.h +++ b/src/buffer_pool.h @@ -29,7 +29,7 @@ typedef enum { } buffer_type_t; /** - * Buffer reference counting for zero-copy lifecycle management + * Buffer reference counting for buffered output lifecycle management * Supports both memory buffers (pool-managed) and file descriptors (for * sendfile) * @@ -37,8 +37,8 @@ typedef enum { * 1. When buffer is free: linked via free_next in pool's free list * 2. When buffer is in use: can be queued for sending via send_next * - * The send queue fields (iov, zerocopy_id) are only valid - * when the buffer is in a send queue or pending completion queue. + * The send queue field (iov) are only valid + * when the buffer is in a send queue. */ typedef struct buffer_ref_s { buffer_type_t type; /* Buffer type: memory or file */ @@ -70,7 +70,6 @@ typedef struct buffer_ref_s { sends, BUFFER_TYPE_MEMORY only) */ off_t file_offset; /* Current offset in file */ }; - uint32_t zerocopy_id; /* ID for tracking MSG_ZEROCOPY completions */ } buffer_ref_t; /** @@ -107,7 +106,7 @@ void buffer_pool_cleanup(buffer_pool_t *pool); void buffer_pool_update_stats(buffer_pool_t *pool); void buffer_ref_get(buffer_ref_t *ref); void buffer_ref_put(buffer_ref_t *ref); -/* Share data while keeping offsets, send links and completion IDs independent. +/* Share data while keeping offsets and send links independent. * The returned view owns a reference to the backing buffer; release with put. */ buffer_ref_t *buffer_ref_view(buffer_ref_t *ref); size_t buffer_ref_capacity(const buffer_ref_t *ref); diff --git a/src/configuration.c b/src/configuration.c index 86f8f6c8..4dda7030 100644 --- a/src/configuration.c +++ b/src/configuration.c @@ -46,7 +46,6 @@ int cmd_status_page_path_set = 0; int cmd_player_page_path_set = 0; int cmd_app_path_prefix_set = 0; int cmd_use_relative_path_in_m3u_set = 0; -int cmd_zerocopy_on_send_set = 0; int cmd_workers_set = 0; int cmd_external_m3u_url_set = 0; int cmd_external_m3u_update_interval_set = 0; @@ -203,17 +202,6 @@ static void add_bindaddr_unix(char *path) { bind_addresses = ba; } -static void apply_bind_side_effects(void) { - int has_unix = bind_addresses_has_unix(); - if (has_unix) { - if (config.zerocopy_on_send) - config.zerocopy_on_send = 0; - logger(LOG_WARN, "Zero-copy send disabled because Unix socket listener is configured"); - } else if (!has_unix && cmd_zerocopy_on_send_set) { - config.zerocopy_on_send = 1; - } -} - static int parse_port_range_value(const char *value, int *min_port, int *max_port) { char *endptr = NULL; long start = 0; @@ -616,12 +604,6 @@ void parse_global_sec(char *line) { return; } - if (strcasecmp("zerocopy-on-send", param) == 0) { - if (set_if_not_cmd_override(cmd_zerocopy_on_send_set, "zerocopy-on-send")) - config.zerocopy_on_send = parse_bool(value); - return; - } - if (strcasecmp("use-relative-path-in-m3u", param) == 0) { if (set_if_not_cmd_override(cmd_use_relative_path_in_m3u_set, "use-relative-path-in-m3u")) config.use_relative_path_in_m3u = parse_bool(value); @@ -1172,8 +1154,6 @@ void config_init(void) { config.video_snapshot = 0; if (!cmd_mcast_rejoin_interval_set) config.mcast_rejoin_interval = 0; - if (!cmd_zerocopy_on_send_set) - config.zerocopy_on_send = 0; if (!cmd_use_relative_path_in_m3u_set) config.use_relative_path_in_m3u = 0; if (!cmd_fcc_listen_port_range_set) { @@ -1250,8 +1230,6 @@ int config_reload(int *out_bind_changed) { goto reload_failed; } - apply_bind_side_effects(); - /* Check if bind addresses changed */ if (out_bind_changed) { *out_bind_changed = !bind_addresses_equal(bind_addresses, old_bind_addresses); @@ -1299,7 +1277,7 @@ void usage(FILE *f, char *progname) { "\t-m --maxclients Serve max n requests simultaneously (default 5)\n" "\t-w --workers Number of worker processes with SO_REUSEPORT " "(default 1)\n" - "\t-b --buffer-pool-max-size Maximum number of buffers in zero-copy " + "\t-b --buffer-pool-max-size Maximum number of buffers in " "pool (default 16384)\n" "\t-B --udp-rcvbuf-size UDP socket receive buffer size for " "multicast/FCC/RTSP (default 524288 = 512KB)\n" @@ -1346,8 +1324,6 @@ void usage(FILE *f, char *progname) { "https://)\n" "\t-I --external-m3u-update-interval Auto-update interval " "(default: 7200 = 2h, 0=disabled)\n" - "\t-Z --zerocopy-on-send Enable zero-copy send with MSG_ZEROCOPY for " - "better performance (default: off)\n" "\t-g --http-proxy-user-agent Override User-Agent for upstream HTTP proxy requests\n" "\t-u --rtsp-user-agent User-Agent header for upstream RTSP requests " "(default: rtp2httpd/)\n" @@ -1433,7 +1409,6 @@ void parse_cmd_line(int argc, char *argv[]) { {"use-relative-path-in-m3u", no_argument, 0, OPT_USE_RELATIVE_PATH_IN_M3U}, {"external-m3u", required_argument, 0, 'M'}, {"external-m3u-update-interval", required_argument, 0, 'I'}, - {"zerocopy-on-send", no_argument, 0, 'Z'}, {"http-proxy-user-agent", required_argument, 0, 'g'}, {"rtsp-stun-server", required_argument, 0, 'N'}, {"rtsp-user-agent", required_argument, 0, 'u'}, @@ -1443,7 +1418,7 @@ void parse_cmd_line(int argc, char *argv[]) { {"pid-file", required_argument, 0, OPT_PID_FILE}, {0, 0, 0, 0}}; - const char short_opts[] = "v:qhUm:w:b:B:c:l:P:H:XT:i:f:t:r:y:R:F:A:s:p:M:I:SCZg:N:u:O:"; + const char short_opts[] = "v:qhUm:w:b:B:c:l:P:H:XT:i:f:t:r:y:R:F:A:s:p:M:I:SCg:N:u:O:"; int option_index, opt; int configfile_failed = 1; @@ -1631,11 +1606,6 @@ void parse_cmd_line(int argc, char *argv[]) { logger(LOG_INFO, "External M3U update interval set to %d seconds", config.external_m3u_update_interval); } break; - case 'Z': - config.zerocopy_on_send = 1; - cmd_zerocopy_on_send_set = 1; - logger(LOG_INFO, "Zero-copy send enabled (MSG_ZEROCOPY)"); - break; case 'g': safe_free_string(&config.http_proxy_user_agent); if (optarg[0] != '\0') { @@ -1699,8 +1669,6 @@ void parse_cmd_line(int argc, char *argv[]) { set_config_file_path(NULL); } - apply_bind_side_effects(); - /* External M3U will be loaded asynchronously by workers after startup * This avoids blocking the startup process waiting for network resources */ if (config.external_m3u_url) { diff --git a/src/configuration.h b/src/configuration.h index 1e0b4699..172bb676 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -57,7 +57,7 @@ typedef struct { /* Worker and performance settings */ int workers; /* Number of worker threads (SO_REUSEPORT sharded), default 1 */ - int buffer_pool_max_size; /* Maximum number of buffers in zero-copy buffer + int buffer_pool_max_size; /* Maximum number of buffers in buffer pool, default 16384 */ int udp_rcvbuf_size; /* UDP socket receive buffer size in bytes for multicast, FCC, and RTSP sockets. Default 512KB */ @@ -114,10 +114,6 @@ typedef struct { */ int64_t last_external_m3u_update_time; /* Last update time in milliseconds */ - /* Zero-copy settings */ - int zerocopy_on_send; /* Enable zero-copy send with MSG_ZEROCOPY (0=disabled, - 1=enabled) */ - /* RTSP NAT traversal settings */ char *rtsp_stun_server; /* STUN server host:port for RTSP NAT traversal (NULL=disabled) */ diff --git a/src/connection.c b/src/connection.c index db23929f..615e642e 100644 --- a/src/connection.c +++ b/src/connection.c @@ -6,10 +6,10 @@ #include "m3u.h" #include "platform_compat.h" #include "poller.h" +#include "send_queue.h" #include "service.h" #include "status.h" #include "utils.h" -#include "zerocopy.h" #include #include #include @@ -306,10 +306,10 @@ typedef struct { } queue_limit_inputs_t; static void connection_prepare_queue_limit_inputs(queue_limit_inputs_t *out) { - buffer_pool_t *pool = &zerocopy_state.pool; + buffer_pool_t *pool = &send_buffer_state.pool; out->pool = pool; - size_t active = zerocopy_active_streams(); + size_t active = send_buffer_active_streams(); if (active == 0) active = 1; @@ -402,7 +402,7 @@ static void connection_report_queue(connection_t *c) { if (c->status_index < 0) return; - size_t queue_buffers = c->zc_queue.num_queued; + size_t queue_buffers = c->send_queue.num_queued; size_t queue_bytes = connection_queue_bytes(c); status_update_client_queue(c->status_index, queue_bytes, queue_buffers, c->queue_limit_bytes, @@ -506,9 +506,8 @@ connection_t *connection_create(int fd, int epfd, struct sockaddr_storage *clien c->client_addr_len = 0; } - /* Initialize zero-copy queue */ - zerocopy_queue_init(&c->zc_queue); - c->zerocopy_enabled = 0; + /* Initialize buffered output queue */ + send_queue_init(&c->send_queue); c->buffer_class = CONNECTION_BUFFER_CONTROL; c->write_queue_next = NULL; c->write_queue_pending = 0; @@ -546,14 +545,6 @@ connection_t *connection_create(int fd, int epfd, struct sockaddr_storage *clien CONNECTION_TCP_KEEPALIVE_CNT); } - /* Enable SO_ZEROCOPY on socket if supported */ - if (config.zerocopy_on_send && connection_client_is_tcp(c)) { - int one = 1; - if (setsockopt(fd, SOL_SOCKET, SO_ZEROCOPY, &one, sizeof(one)) == 0) { - c->zerocopy_enabled = 1; - } - } - /* Initialize HTTP request parser */ http_request_init(&c->http_req); return c; @@ -564,7 +555,7 @@ void connection_cleanup(connection_t *c) { return; if (c->stream_registered) { - zerocopy_unregister_stream_client(); + send_buffer_unregister_stream_client(); c->stream_registered = 0; } @@ -577,8 +568,8 @@ void connection_cleanup(connection_t *c) { stream_context_cleanup(&c->stream); } - /* Cleanup zero-copy queue - this releases all buffer references */ - zerocopy_queue_cleanup(&c->zc_queue); + /* Cleanup buffered output queue - this releases all buffer references */ + send_queue_cleanup(&c->send_queue); /* Try to shrink buffer pool after connection cleanup * This is an ideal time to reclaim memory as buffers are likely freed @@ -638,12 +629,12 @@ int connection_queue_output(connection_t *c, const uint8_t *data, size_t len) { memcpy(buf_ref->data, src, chunk_size); buf_ref->data_size = chunk_size; - /* Queue this buffer for zero-copy send */ - if (connection_queue_zerocopy(c, buf_ref) < 0) { + /* Queue this buffer for sending */ + if (connection_queue_buffer(c, buf_ref) < 0) { /* Queue full - release the buffer and fail */ buffer_ref_put(buf_ref); logger(LOG_WARN, - "connection_queue_output: Zero-copy queue full, cannot queue %zu " + "connection_queue_output: Send queue full, cannot queue %zu " "bytes", remaining); return -1; @@ -677,10 +668,10 @@ connection_write_status_t connection_handle_write(connection_t *c) { if (!c) return CONNECTION_WRITE_IDLE; - if (!c->zc_queue.head) { + if (!c->send_queue.head) { connection_epoll_update_events(c->epfd, c->fd, POLLER_IN | POLLER_RDHUP | POLLER_HUP | POLLER_ERR); connection_report_queue(c); - if (c->state == CONN_CLOSING && !c->zc_queue.pending_head) + if (c->state == CONN_CLOSING) return CONNECTION_WRITE_CLOSED; return CONNECTION_WRITE_IDLE; } @@ -691,7 +682,7 @@ connection_write_status_t connection_handle_write(connection_t *c) { * EPOLLOUT / EV_CLEAR fires only once when the socket becomes writable. */ for (;;) { size_t bytes_sent = 0; - int ret = zerocopy_send(c->fd, &c->zc_queue, &bytes_sent); + int ret = send_queue_send(c->fd, &c->send_queue, &bytes_sent); total_sent += bytes_sent; /* Count post-send so per-client bandwidth reflects actual receive rate, not enqueue rate. */ c->stream.total_bytes_sent += (uint64_t)bytes_sent; @@ -711,8 +702,8 @@ connection_write_status_t connection_handle_write(connection_t *c) { return CONNECTION_WRITE_BLOCKED; } - if (!c->zc_queue.head) { - if (c->state == CONN_CLOSING && !c->zc_queue.pending_head) { + if (!c->send_queue.head) { + if (c->state == CONN_CLOSING) { connection_epoll_update_events(c->epfd, c->fd, POLLER_IN | POLLER_RDHUP | POLLER_HUP | POLLER_ERR); connection_report_queue(c); return CONNECTION_WRITE_CLOSED; @@ -723,14 +714,14 @@ connection_write_status_t connection_handle_write(connection_t *c) { if (total_sent > 0) stream_on_client_drain(&c->stream); uint32_t mask = POLLER_IN | POLLER_RDHUP | POLLER_HUP | POLLER_ERR; - if (c->zc_queue.head) + if (c->send_queue.head) mask |= POLLER_OUT; connection_epoll_update_events(c->epfd, c->fd, mask); connection_report_queue(c); return CONNECTION_WRITE_IDLE; } - /* Guard against spinning if zerocopy_send sent 0 bytes without EAGAIN */ + /* Guard against spinning if send_queue_send sent 0 bytes without EAGAIN */ if (bytes_sent == 0) break; } @@ -1202,7 +1193,7 @@ int connection_route_and_start(connection_t *c) { */ if (stream_context_init_for_worker(&c->stream, c, service, c->epfd, c->status_index, is_snapshot_request) == 0) { if (!is_snapshot_request && !c->stream_registered) { - zerocopy_register_stream_client(); + send_buffer_register_stream_client(); c->stream_registered = 1; } @@ -1222,7 +1213,7 @@ int connection_route_and_start(connection_t *c) { } } -int connection_queue_zerocopy(connection_t *c, buffer_ref_t *buf_ref) { +int connection_queue_buffer(connection_t *c, buffer_ref_t *buf_ref) { if (!c || !buf_ref || buf_ref->data_size == 0) return 0; @@ -1247,27 +1238,26 @@ int connection_queue_zerocopy(connection_t *c, buffer_ref_t *buf_ref) { return -1; } - /* Add to zero-copy queue with offset information */ - int ret = zerocopy_queue_add(&c->zc_queue, buf_ref); + /* Add to buffered output queue with offset information */ + int ret = send_queue_add(&c->send_queue, buf_ref); if (ret < 0) return -1; /* Queue full */ if (queued_bytes > c->queue_bytes_highwater) c->queue_bytes_highwater = queued_bytes; - if (c->zc_queue.num_queued > c->queue_buffers_highwater) - c->queue_buffers_highwater = c->zc_queue.num_queued; + if (c->send_queue.num_queued > c->queue_buffers_highwater) + c->queue_buffers_highwater = c->send_queue.num_queued; connection_report_queue(c); /* Batching optimization: Only enable EPOLLOUT when flush threshold is reached * Benefits: * - Reduces sendmsg() syscall overhead (fewer calls) - * - Reduces MSG_ZEROCOPY optmem consumption (fewer operations) * - Better batching with iovec (up to 64 packets per sendmsg) * - Lower latency impact (100ms is acceptable for streaming) */ - if (zerocopy_should_flush(&c->zc_queue)) { + if (send_queue_should_flush(&c->send_queue)) { connection_epoll_update_events(c->epfd, c->fd, POLLER_IN | POLLER_OUT | POLLER_RDHUP | POLLER_HUP | POLLER_ERR); } @@ -1278,8 +1268,8 @@ int connection_queue_file(connection_t *c, int file_fd, off_t file_offset, size_ if (!c || file_fd < 0 || file_size == 0) return -1; - /* Add file to zero-copy queue */ - int ret = zerocopy_queue_add_file(&c->zc_queue, file_fd, file_offset, file_size); + /* Add file to buffered output queue */ + int ret = send_queue_add_file(&c->send_queue, file_fd, file_offset, file_size); if (ret < 0) return -1; @@ -1399,19 +1389,19 @@ static void handle_epg_request(connection_t *c, int requested_gz) { send_http_headers(c, STATUS_200, content_type, extra_headers); - /* Use zero-copy transmission via sendfile + /* Use sendfile to transmit cached data * Note: epg_fd is owned by EPG cache, so we need to dup it - * zerocopy_queue_add_file will close the fd when done */ + * send_queue_add_file will close the fd when done */ int dup_fd = dup(epg_fd); if (dup_fd < 0) { - logger(LOG_ERROR, "Failed to dup EPG fd for zero-copy transmission: %s", strerror(errno)); + logger(LOG_ERROR, "Failed to dup EPG fd for file transmission: %s", strerror(errno)); c->state = CONN_CLOSING; return; } - /* Queue the file for zero-copy transmission */ + /* Queue the file for file transmission */ if (connection_queue_file(c, dup_fd, 0, epg_size) < 0) { - logger(LOG_ERROR, "Failed to queue EPG file for zero-copy transmission"); + logger(LOG_ERROR, "Failed to queue EPG file for file transmission"); close(dup_fd); c->state = CONN_CLOSING; return; diff --git a/src/connection.h b/src/connection.h index f871794a..0acd79a1 100644 --- a/src/connection.h +++ b/src/connection.h @@ -2,9 +2,9 @@ #define CONNECTION_H #include "http.h" +#include "send_queue.h" #include "service.h" #include "stream.h" -#include "zerocopy.h" #include #include @@ -29,9 +29,8 @@ typedef struct connection_s { /* input parsing */ char inbuf[INBUF_SIZE]; int in_len; - /* zero-copy send queue - all output goes through this */ - zerocopy_queue_t zc_queue; - int zerocopy_enabled; /* Whether SO_ZEROCOPY is enabled on this socket */ + /* Output send queue - all output goes through this */ + send_queue_t send_queue; connection_buffer_class_t buffer_class; /* HTTP request parser */ http_request_t http_req; @@ -166,16 +165,16 @@ int connection_queue_output(connection_t *c, const uint8_t *data, size_t len); int connection_queue_output_and_flush(connection_t *c, const uint8_t *data, size_t len); /** - * Queue data for zero-copy send (no memcpy) + * Queue data for sending (no memcpy) * Takes ownership of the buffer via reference counting * @param c Connection * @param buf_ref Buffer reference (must not be NULL) * @return 0 on success, -1 if queue full or invalid parameters */ -int connection_queue_zerocopy(connection_t *c, buffer_ref_t *buf_ref); +int connection_queue_buffer(connection_t *c, buffer_ref_t *buf_ref); /** - * Queue a file descriptor for zero-copy send using sendfile() + * Queue a file descriptor for sending using sendfile() * Takes ownership of the file descriptor (will close it when done) * @param c Connection * @param file_fd File descriptor to send (must be seekable) @@ -187,7 +186,7 @@ int connection_queue_file(connection_t *c, int file_fd, off_t file_offset, size_ /* Backing capacity currently queued, including shared multicast batches. * Partial sends retain the entire backing buffer until the entry is removed. */ -static inline size_t connection_queue_bytes(const connection_t *c) { return c->zc_queue.memory_bytes; } +static inline size_t connection_queue_bytes(const connection_t *c) { return c->send_queue.memory_bytes; } /* Record one upstream-pause edge. Called by per-transport pause helpers * (http_proxy_pause_upstream, rtsp_pause_upstream) on the 0->1 transition. */ diff --git a/src/epg.c b/src/epg.c index 1c26f0c2..104cb7a6 100644 --- a/src/epg.c +++ b/src/epg.c @@ -82,7 +82,7 @@ static void calculate_epg_etag(int fd, size_t size) { logger(LOG_DEBUG, "EPG ETag calculated: %s", epg_cache.etag); } -/* Async fetch completion callback (fd-based, zero-copy) */ +/* Async fetch completion callback (fd-based) */ static void epg_fetch_fd_callback(http_fetch_ctx_t *ctx, int fd, size_t content_size, void *user_data) { (void)ctx; /* Unused */ (void)user_data; /* Unused */ @@ -258,7 +258,7 @@ int epg_fetch_async(int epfd) { logger(LOG_INFO, "Starting async EPG fetch from: %s", epg_cache.url); - /* Start async fetch with fd-based callback (zero-copy) + /* Start async fetch with fd-based callback (file-backed) * Note: file:// URLs complete synchronously and return NULL (callback already * invoked) */ fetch_ctx = http_fetch_start_async_fd(epg_cache.url, epg_fetch_fd_callback, NULL, epfd); diff --git a/src/epg.h b/src/epg.h index bf0ac864..f21ed698 100644 --- a/src/epg.h +++ b/src/epg.h @@ -7,7 +7,7 @@ /* EPG cache structure */ typedef struct { char *url; /* EPG source URL */ - int data_fd; /* tmpfs file descriptor for EPG data (zero-copy), or -1 if not + int data_fd; /* tmpfs file descriptor for EPG data (file-backed), or -1 if not available */ size_t data_size; /* Size of EPG data */ int is_gzipped; /* 1 if data is gzip compressed (based on URL), 0 otherwise */ diff --git a/src/fcc.c b/src/fcc.c index 63d1db87..3dd18905 100644 --- a/src/fcc.c +++ b/src/fcc.c @@ -427,7 +427,7 @@ int fcc_handle_socket_event(stream_context_t *ctx, int fd, int64_t now) { return 0; } - /* Receive directly into zero-copy buffer (true zero-copy receive) */ + /* Receive directly into buffered output buffer (true buffered output receive) */ int actualr = recvfrom(recv_sock, recv_buf->data, BUFFER_POOL_BUFFER_SIZE, 0, (struct sockaddr *)&peer_addr, &slen); if (actualr < 0) { buffer_ref_put(recv_buf); @@ -735,7 +735,7 @@ int fcc_handle_mcast_transition(stream_context_t *ctx, buffer_ref_t *buf_ref) { return -1; } - /* Keep original receive buffer alive for deferred zero-copy send */ + /* Keep original receive buffer alive for deferred sending */ buffer_ref_get(buf_ref); buf_ref->send_next = NULL; @@ -763,7 +763,7 @@ int fcc_handle_mcast_active(stream_context_t *ctx, buffer_ref_t *buf_ref) { uint64_t flushed_bytes = 0; while (node) { - /* Queue each buffer for zero-copy send */ + /* Queue each buffer for sending */ buffer_ref_t *next = node->send_next; int processed_bytes = stream_process_rtp_payload(ctx, node, STREAM_MEDIA_ORIGIN_FCC_MULTICAST); if (likely(processed_bytes > 0)) { @@ -780,7 +780,7 @@ int fcc_handle_mcast_active(stream_context_t *ctx, buffer_ref_t *buf_ref) { logger(LOG_DEBUG, "FCC: Flushed pending buffer chain, total_flushed_bytes=%" PRIu64, flushed_bytes); } - /* Forward multicast data to client (true zero-copy) or capture I-frame + /* Forward multicast data to client (true buffered output) or capture I-frame * (snapshot) */ stream_process_rtp_payload(ctx, buf_ref, STREAM_MEDIA_ORIGIN_FCC_MULTICAST); diff --git a/src/fcc.h b/src/fcc.h index 8724738a..a1a983bc 100644 --- a/src/fcc.h +++ b/src/fcc.h @@ -68,7 +68,7 @@ typedef struct { uint32_t session_id; /* Session ID for NAT traversal correlation */ uint8_t need_nat_traversal; /* NAT traversal support flag from server */ - /* Multicast pending buffer for smooth transition - zero-copy chain */ + /* Multicast pending buffer for smooth transition - buffered output chain */ buffer_ref_t *pending_list_head; buffer_ref_t *pending_list_tail; uint16_t mcast_pbuf_last_seqn; @@ -170,7 +170,7 @@ int fcc_handle_sync_notification(stream_context_t *ctx, int timeout_ms); * Stage 4: Handle RTP media packets from unicast stream * * @param ctx Stream context - * @param buf_ref Buffer reference for zero-copy + * @param buf_ref Buffer reference for buffered output * @return 0 on success (packet processed and forwarded) */ int fcc_handle_unicast_media(stream_context_t *ctx, buffer_ref_t *buf_ref); @@ -180,7 +180,7 @@ int fcc_handle_unicast_media(stream_context_t *ctx, buffer_ref_t *buf_ref); * Buffers packets until sync point is reached * * @param ctx Stream context - * @param buf_ref Buffer reference for zero-copy + * @param buf_ref Buffer reference for buffered output * @return 0 on success (packet buffered or forwarded) */ int fcc_handle_mcast_transition(stream_context_t *ctx, buffer_ref_t *buf_ref); @@ -190,7 +190,7 @@ int fcc_handle_mcast_transition(stream_context_t *ctx, buffer_ref_t *buf_ref); * Forwards packets directly to client * * @param ctx Stream context - * @param buf_ref Buffer reference for zero-copy + * @param buf_ref Buffer reference for buffered output * @return 0 on success (packet forwarded) */ int fcc_handle_mcast_active(stream_context_t *ctx, buffer_ref_t *buf_ref); diff --git a/src/http_fetch.c b/src/http_fetch.c index 46d970f5..7f69b7ef 100644 --- a/src/http_fetch.c +++ b/src/http_fetch.c @@ -34,7 +34,7 @@ struct http_fetch_ctx_s { http_fetch_callback_t callback; /* completion callback */ http_fetch_fd_callback_t fd_callback; /* fd-based completion callback */ void *user_data; /* user-provided data */ - int use_fd; /* 1 to use fd callback (zero-copy), 0 to use memory callback */ + int use_fd; /* 1 to use fd callback (file-backed), 0 to use memory callback */ }; /* Global hashmap for fast fd-based lookup */ @@ -521,7 +521,7 @@ int http_fetch_handle_event(http_fetch_ctx_t *ctx) { return -1; } - /* Handle fd-based callback (zero-copy) */ + /* Handle fd-based callback (file-backed) */ if (ctx->use_fd) { int content_fd; @@ -635,7 +635,7 @@ http_fetch_ctx_t *http_fetch_start_async(const char *url, http_fetch_callback_t return http_fetch_start_async_internal(url, callback, NULL, user_data, epfd); } -/* Start async HTTP fetch using popen (fd-based, zero-copy) */ +/* Start async HTTP fetch using popen (fd-based) */ http_fetch_ctx_t *http_fetch_start_async_fd(const char *url, http_fetch_fd_callback_t callback, void *user_data, int epfd) { return http_fetch_start_async_internal(url, NULL, callback, user_data, epfd); diff --git a/src/http_fetch.h b/src/http_fetch.h index 747a2b33..1c11fbaa 100644 --- a/src/http_fetch.h +++ b/src/http_fetch.h @@ -15,7 +15,7 @@ typedef struct http_fetch_ctx_s http_fetch_ctx_t; typedef void (*http_fetch_callback_t)(http_fetch_ctx_t *ctx, char *content, size_t content_size, void *user_data); /* Callback type for async HTTP fetch completion with file descriptor - * (zero-copy) ctx: fetch context fd: tmpfs file descriptor containing fetched + * (file-backed) ctx: fetch context fd: tmpfs file descriptor containing fetched * content (caller must close), or -1 on error content_size: size of fetched * content in bytes (0 if fd is -1) user_data: user-provided data passed to * http_fetch_start_async_fd @@ -44,11 +44,11 @@ typedef void (*http_fetch_fd_callback_t)(http_fetch_ctx_t *ctx, int fd, size_t c http_fetch_ctx_t *http_fetch_start_async(const char *url, http_fetch_callback_t callback, void *user_data, int epfd); /** - * Start async fetch using popen and curl (zero-copy with file descriptor, + * Start async fetch using popen and curl (file descriptor callback, * supports file://) This function starts a non-blocking HTTP(S) fetch using * curl via popen. The pipe is added to the provided epoll instance for async * I/O. Upon completion, a tmpfs file descriptor is passed to the callback for - * zero-copy transmission. For file:// URLs, the file is opened directly and + * file transmission. For file:// URLs, the file is opened directly and * callback is invoked immediately. * * @param url URL to fetch (http://, https://, or file://) diff --git a/src/http_proxy.c b/src/http_proxy.c index 94bb0b7a..9a68ce63 100644 --- a/src/http_proxy.c +++ b/src/http_proxy.c @@ -894,9 +894,9 @@ static int http_proxy_try_receive_response(http_proxy_session_t *session) { int bytes_forwarded = 0; /* - * Two-phase receive strategy for zero-copy optimization: + * Two-phase receive strategy for buffered output optimization: * Phase 1 (AWAITING_HEADERS): Use fixed buffer for header parsing - * Phase 2 (STREAMING): Recv directly to buffer pool for zero-copy send + * Phase 2 (STREAMING): Recv directly to buffer pool for sending * OR buffer for rewriting if needs_body_rewrite */ @@ -923,7 +923,7 @@ static int http_proxy_try_receive_response(http_proxy_session_t *session) { return http_proxy_consume_rewrite_body(session, temp_buf, (size_t)received); } - /* Phase 2: Zero-copy streaming - recv directly to buffer pool */ + /* Phase 2: Send queue streaming - recv directly to buffer pool */ /* Pause upstream BEFORE recv when client queue is near limit. Dropping * bytes mid-stream would corrupt the response body, so we instead push @@ -956,9 +956,9 @@ static int http_proxy_try_receive_response(http_proxy_session_t *session) { return http_proxy_handle_upstream_end(session); } - /* Queue for zero-copy send */ + /* Queue for sending */ buf->data_size = received; - if (connection_queue_zerocopy(session->conn, buf) < 0) { + if (connection_queue_buffer(session->conn, buf) < 0) { buffer_ref_put(buf); logger(LOG_ERROR, "HTTP Proxy: Failed to queue body data"); return -1; @@ -966,8 +966,8 @@ static int http_proxy_try_receive_response(http_proxy_session_t *session) { buffer_ref_put(buf); bytes_forwarded = (int)received; - /* Let connection_queue_zerocopy's internal batching mechanism handle - * POLLER_OUT - it uses zerocopy_should_flush() for optimal batching */ + /* Let connection_queue_buffer's internal batching mechanism handle + * POLLER_OUT - it uses send_queue_should_flush() for optimal batching */ session->bytes_received += bytes_forwarded; /* Check if we've received all content */ diff --git a/src/http_proxy.h b/src/http_proxy.h index 064f85f2..ae823414 100644 --- a/src/http_proxy.h +++ b/src/http_proxy.h @@ -214,7 +214,7 @@ int http_proxy_session_tick(http_proxy_session_t *session, int64_t now); /** * Resume reading from upstream after client send queue has drained. - * Called from stream_on_client_drain when zc_queue falls below LWM. + * Called from stream_on_client_drain when send_queue falls below LWM. * @param session HTTP proxy session */ void http_proxy_resume_upstream(http_proxy_session_t *session); diff --git a/src/multicast.c b/src/multicast.c index 5d0245e6..c6f9f97d 100644 --- a/src/multicast.c +++ b/src/multicast.c @@ -592,7 +592,7 @@ int mcast_session_join(mcast_session_t *session, stream_context_t *ctx) { } /* Only queue metadata is private. The immutable payload stays alive until - * every queue and MSG_ZEROCOPY completion releases its reference. */ + * every send queue releases its reference. */ static void mcast_source_fanout(mcast_source_t *source, buffer_ref_t *batch, int packet_type, int flush) { for (mcast_session_t *session = source->subscribers; session; session = session->next) { stream_context_t *ctx = session->ctx; @@ -606,7 +606,7 @@ static void mcast_source_fanout(mcast_source_t *source, buffer_ref_t *batch, int stream_metadata_note_media(ctx, packet_type, (uint8_t *)view->data + view->data_offset, (int)view->data_size, ctx->fcc.initialized ? STREAM_MEDIA_ORIGIN_FCC_MULTICAST : STREAM_MEDIA_ORIGIN_MULTICAST); - if (rtp_queue_buf_direct(ctx->conn, view) >= 0 && flush && view->data_size < ZEROCOPY_BATCH_BYTES) { + if (rtp_queue_buf_direct(ctx->conn, view) >= 0 && flush && view->data_size < SEND_QUEUE_BATCH_BYTES) { connection_epoll_update_events(ctx->epoll_fd, ctx->conn->fd, POLLER_IN | POLLER_OUT | POLLER_RDHUP | POLLER_HUP | POLLER_ERR); } @@ -713,7 +713,7 @@ static void mcast_deliver_packet(mcast_session_t *session, buffer_ref_t *packet) if (session->failed || ctx->conn->state == CONN_CLOSING) return; - /* Queue linkage, RTP offsets and zerocopy completion IDs are mutable and + /* Queue linkage and RTP offsets are mutable and * must never be shared between clients. Only the backing data is shared. */ buffer_ref_t *view; if (session->source->refs == 1 && !session->source->shared_output) { diff --git a/src/platform_compat.h b/src/platform_compat.h index 7c4be1a5..cb5dd382 100644 --- a/src/platform_compat.h +++ b/src/platform_compat.h @@ -92,24 +92,6 @@ static inline ssize_t platform_sendfile(int out_fd, int in_fd, off_t *offset, si #define SO_RCVBUFFORCE SO_RCVBUF #endif -/* ── SO_ZEROCOPY / MSG_ZEROCOPY ────────────────────────────────────── - * Linux 4.14+ only. Define to invalid values on other platforms so - * the detection code gracefully falls through to regular send. - */ -#ifndef SO_ZEROCOPY -#define SO_ZEROCOPY 60 -#endif -#ifndef MSG_ZEROCOPY -#define MSG_ZEROCOPY 0x4000000 -#endif - -/* ── MSG_ERRQUEUE ──────────────────────────────────────────────────── - * Linux-only. Used for MSG_ZEROCOPY completion notifications. - */ -#ifndef MSG_ERRQUEUE -#define MSG_ERRQUEUE 0x2000 -#endif - /* ── SO_BINDTODEVICE ───────────────────────────────────────────────── * Linux-only. On macOS, IP_BOUND_IF is a rough equivalent. */ @@ -397,18 +379,6 @@ static inline int platform_mcast_group_op(int sock, int family, const struct soc #endif } -/* ── IP_RECVERR / IPV6_RECVERR ────────────────────────────────────── - * Linux-only. Used for MSG_ZEROCOPY completion notifications. - * Define to harmless values on other platforms; the zerocopy - * completion code is already guarded by #ifdef __linux__. - */ -#ifndef IP_RECVERR -#define IP_RECVERR 11 -#endif -#ifndef IPV6_RECVERR -#define IPV6_RECVERR 25 -#endif - /* ── TCP keepalive ────────────────────────────────────────────────── * Configure TCP keepalive for early dead-peer detection on TCP * client sockets. Supports fine-grained control on all three target diff --git a/src/rtp.c b/src/rtp.c index 31c287f5..fe0e58f2 100644 --- a/src/rtp.c +++ b/src/rtp.c @@ -90,8 +90,8 @@ int rtp_queue_buf_direct(connection_t *conn, buffer_ref_t *buf_ref) { stream_send_http_headers(conn, "video/mp2t", NULL); } - /* Queue for zero-copy send */ - if (connection_queue_zerocopy(conn, buf_ref) == 0) { + /* Queue for sending */ + if (connection_queue_buffer(conn, buf_ref) == 0) { return (int)buf_ref->data_size; } return -1; diff --git a/src/rtsp.c b/src/rtsp.c index 37968606..7a82cf31 100644 --- a/src/rtsp.c +++ b/src/rtsp.c @@ -2077,7 +2077,7 @@ static int rtsp_process_interleaved_buffer(rtsp_session_t *session, connection_t break; /* Wait for more data */ } - /* Sanity check: bound against the zero-copy destination buffer. */ + /* Sanity check: bound against the buffered output destination buffer. */ if (packet_length > BUFFER_POOL_BUFFER_SIZE) { logger(LOG_ERROR, "RTSP: Received packet too large (%d bytes, max %d), attempting " @@ -2233,7 +2233,7 @@ int rtsp_handle_udp_rtp_data(rtsp_session_t *session, connection_t *conn) { return total_bytes_written; } - /* Receive directly into zero-copy buffer (true zero-copy receive) */ + /* Receive directly into buffered output buffer (true buffered output receive) */ int bytes_received = recv(session->rtp_socket, rtp_buf->data, BUFFER_POOL_BUFFER_SIZE, 0); if (bytes_received < 0) { buffer_ref_put(rtp_buf); diff --git a/src/send_queue.c b/src/send_queue.c new file mode 100644 index 00000000..60b4ed8e --- /dev/null +++ b/src/send_queue.c @@ -0,0 +1,383 @@ +#include "send_queue.h" +#include "platform_compat.h" +#include "rtp2httpd.h" +#include "status.h" +#include "utils.h" +#include +#include +#include +#include +#include +#include + +/* Global buffered output state */ +send_buffer_state_t send_buffer_state = {0}; + +/** + * Helper macro to access this worker's statistics in shared memory + * Falls back to no-op if shared memory not available + */ +#define WORKER_STATS_INC(field) \ + do { \ + if (status_shared && worker_id >= 0 && worker_id < STATUS_MAX_WORKERS) { \ + status_shared->worker_stats[worker_id].field++; \ + } \ + } while (0) + +void send_buffer_register_stream_client(void) { send_buffer_state.active_streams++; } + +void send_buffer_unregister_stream_client(void) { + if (send_buffer_state.active_streams > 0) + send_buffer_state.active_streams--; +} + +size_t send_buffer_active_streams(void) { return send_buffer_state.active_streams; } + +int send_buffer_init(void) { + if (send_buffer_state.initialized) + return 0; + + /* Initialize per-worker statistics in shared memory */ + if (status_shared && worker_id >= 0 && worker_id < STATUS_MAX_WORKERS) { + memset(&status_shared->worker_stats[worker_id], 0, sizeof(worker_stats_t)); + } + if (status_shared && worker_id >= 0 && worker_id < STATUS_MAX_WORKERS) { + status_shared->worker_stats[worker_id].worker_pid = getpid(); + } + + /* Initialize buffer pool with dynamic expansion support */ + if (buffer_pool_init(&send_buffer_state.pool, BUFFER_POOL_BUFFER_SIZE, BUFFER_POOL_INITIAL_SIZE, + config.buffer_pool_max_size, BUFFER_POOL_EXPAND_SIZE, BUFFER_POOL_LOW_WATERMARK, + BUFFER_POOL_HIGH_WATERMARK) < 0) { + logger(LOG_FATAL, "Send queue: Failed to initialize buffer pool"); + return -1; + } + + /* Initialize control plane pool */ + if (buffer_pool_init(&send_buffer_state.control_pool, BUFFER_POOL_BUFFER_SIZE, CONTROL_POOL_INITIAL_SIZE, + CONTROL_POOL_MAX_BUFFERS, CONTROL_POOL_EXPAND_SIZE, CONTROL_POOL_LOW_WATERMARK, + CONTROL_POOL_HIGH_WATERMARK) < 0) { + logger(LOG_FATAL, "Send queue: Failed to initialize control buffer pool"); + buffer_pool_cleanup(&send_buffer_state.pool); + return -1; + } + + send_buffer_state.active_streams = 0; + + /* Sync initial buffer pool state to shared memory */ + buffer_pool_update_stats(&send_buffer_state.pool); + buffer_pool_update_stats(&send_buffer_state.control_pool); + + send_buffer_state.initialized = 1; + + return 0; +} + +void send_buffer_cleanup(void) { + if (!send_buffer_state.initialized) + return; + + buffer_pool_cleanup(&send_buffer_state.pool); + buffer_pool_cleanup(&send_buffer_state.control_pool); + buffer_pool_cleanup(&send_buffer_state.batch_pool); + buffer_pool_update_stats(&send_buffer_state.pool); + buffer_pool_update_stats(&send_buffer_state.control_pool); + send_buffer_state.initialized = 0; + send_buffer_state.active_streams = 0; +} + +void send_queue_init(send_queue_t *queue) { memset(queue, 0, sizeof(*queue)); } + +void send_queue_cleanup(send_queue_t *queue) { + /* Clean up send queue - buffers are now directly in the queue */ + buffer_ref_t *buf = queue->head; + while (buf) { + buffer_ref_t *next = buf->send_next; + buffer_ref_put(buf); + buf = next; + } + + send_queue_init(queue); +} + +int send_queue_add(send_queue_t *queue, buffer_ref_t *buf_ref) { + if (!queue || !buf_ref || buf_ref->data_size == 0) + return 0; + + uint8_t *base = (uint8_t *)buf_ref->data; + + size_t capacity = buffer_ref_capacity(buf_ref); + if (!base || buf_ref->data_offset > capacity || buf_ref->data_size > capacity - buf_ref->data_offset) { + logger(LOG_ERROR, + "send_queue_add: Invalid buffer parameters (offset=%zu len=%zu " + "size=%zu)", + buf_ref->data_offset, buf_ref->data_size, capacity); + return -1; + } + + uint8_t *data_ptr = base + buf_ref->data_offset; + + /* Setup send queue fields in the buffer */ + buf_ref->type = BUFFER_TYPE_MEMORY; + buf_ref->iov.iov_base = data_ptr; + buf_ref->iov.iov_len = buf_ref->data_size; + buf_ref->send_next = NULL; + + /* Increment reference count - queue now holds a reference */ + buffer_ref_get(buf_ref); + + /* Add to queue */ + if (queue->tail) { + queue->tail->send_next = buf_ref; + queue->tail = buf_ref; + } else { + /* First entry - record timestamp for batching timeout */ + queue->head = queue->tail = buf_ref; + } + + queue->total_bytes += buf_ref->data_size; + queue->memory_bytes += capacity; + queue->num_queued++; + + return 0; +} + +int send_queue_add_file(send_queue_t *queue, int file_fd, off_t file_offset, size_t file_size) { + if (file_fd < 0 || file_size == 0) + return -1; + + /* Allocate a buffer_ref_t to represent the file (not from pool) */ + buffer_ref_t *buf_ref = calloc(1, sizeof(buffer_ref_t)); + if (!buf_ref) { + logger(LOG_ERROR, "send_queue_add_file: Failed to allocate buffer_ref"); + return -1; + } + + /* Setup file send fields */ + buf_ref->type = BUFFER_TYPE_FILE; + buf_ref->file_fd = file_fd; + buf_ref->file_offset = file_offset; + buf_ref->file_size = file_size; + buf_ref->file_sent = 0; + buf_ref->refcount = 1; /* Initial reference */ + buf_ref->segment = NULL; /* Not from pool */ + buf_ref->send_next = NULL; + + /* Add to queue */ + if (queue->tail) { + queue->tail->send_next = buf_ref; + queue->tail = buf_ref; + } else { + /* First entry - record timestamp for batching timeout */ + queue->head = queue->tail = buf_ref; + } + + /* Note: File buffers do NOT count towards total_bytes for batching logic + * because they are always flushed immediately and don't participate in + * the batching optimization designed for small RTP packets. + */ + queue->num_queued++; + queue->memory_bytes += BUFFER_POOL_BUFFER_SIZE; + + logger(LOG_DEBUG, "send_queue_add_file: Queued file fd=%d offset=%ld size=%zu", file_fd, (long)file_offset, + file_size); + + return 0; +} + +int send_queue_should_flush(send_queue_t *queue) { + if (!queue || !queue->head) + return 0; /* Nothing to flush */ + + /* Flush if accumulated bytes >= threshold */ + if (queue->total_bytes >= SEND_QUEUE_BATCH_BYTES) { + WORKER_STATS_INC(batch_sends); + return 1; + } + + return 0; /* Not ready to flush yet */ +} + +int send_queue_send(int fd, send_queue_t *queue, size_t *bytes_sent) { + if (!queue->head) { + *bytes_sent = 0; + return 0; + } + + buffer_ref_t *shared = queue->head; + int shared_fd = buffer_ref_sendfile_fd(shared); + if (shared_fd >= 0) { + off_t offset = (uint8_t *)shared->iov.iov_base - ((uint8_t *)shared->data + shared->data_offset); + ssize_t sent = platform_sendfile(fd, shared_fd, &offset, shared->iov.iov_len); + if (sent < 0 && (errno == EINVAL || errno == ENOSYS || errno == EOPNOTSUPP)) { + /* Keep this subscriber's fallback private; others can still sendfile. */ + shared->shared_fd = -2; + } else { + *bytes_sent = sent > 0 ? (size_t)sent : 0; + if (sent < 0) { + if (errno == EAGAIN || errno == EINTR || errno == ENOBUFS) { + WORKER_STATS_INC(eagain_count); + return -2; + } + return -1; + } + if (sent == 0) + return -1; /* The immutable snapshot must contain the complete batch. */ + WORKER_STATS_INC(total_sends); + queue->total_bytes -= (size_t)sent; + shared->iov.iov_base = (uint8_t *)shared->iov.iov_base + sent; + shared->iov.iov_len -= (size_t)sent; + if (!shared->iov.iov_len) { + queue->head = shared->send_next; + if (!queue->head) + queue->tail = NULL; + queue->num_queued--; + queue->memory_bytes -= buffer_ref_capacity(shared); + buffer_ref_put(shared); + } + return 0; + } + } + + /* Check if head is a file - sendfile() must be done separately */ + if (queue->head->type == BUFFER_TYPE_FILE) { + buffer_ref_t *file_buf = queue->head; + size_t remaining = file_buf->file_size - file_buf->file_sent; + off_t offset = file_buf->file_offset + file_buf->file_sent; + + /* Use platform_sendfile() for non-blocking file send */ + ssize_t sent = platform_sendfile(fd, file_buf->file_fd, &offset, remaining); + + if (sent < 0) { + if (errno == EAGAIN) { + WORKER_STATS_INC(eagain_count); + *bytes_sent = 0; + return -2; /* Would block */ + } + + logger(LOG_ERROR, "Send queue: sendfile failed: %s", strerror(errno)); + *bytes_sent = 0; + return -1; + } + + *bytes_sent = (size_t)sent; + file_buf->file_sent += sent; + + /* Check if file send is complete */ + if (file_buf->file_sent >= file_buf->file_size) { + /* File completely sent - remove from queue and cleanup */ + size_t total_file_size = file_buf->file_size; /* Save before put */ + + queue->head = file_buf->send_next; + if (!queue->head) + queue->tail = NULL; + + /* Note: File buffers don't count towards total_bytes, so no need to + * update it */ + queue->num_queued--; + queue->memory_bytes -= BUFFER_POOL_BUFFER_SIZE; + + /* Release reference - this will close fd and free buffer_ref */ + buffer_ref_put(file_buf); + + logger(LOG_DEBUG, "Send queue: sendfile complete (%zu bytes)", total_file_size); + } + /* Note: Partial sends for files don't update total_bytes (files don't + * count) */ + + /* Update statistics */ + WORKER_STATS_INC(total_sends); + + return 0; + } + + /* Build iovec array from queue buffers (memory buffers only) */ + struct iovec iovecs[SEND_QUEUE_MAX_IOVECS]; + int iov_count = 0; + + buffer_ref_t *buf = queue->head; + while (buf && iov_count < SEND_QUEUE_MAX_IOVECS && buf->type == BUFFER_TYPE_MEMORY && + buffer_ref_sendfile_fd(buf) < 0) { + iovecs[iov_count] = buf->iov; + iov_count++; + buf = buf->send_next; + } + + if (iov_count == 0) { + *bytes_sent = 0; + return 0; + } + + /* Prepare message header */ + struct msghdr msg; + memset(&msg, 0, sizeof(msg)); + msg.msg_iov = iovecs; + msg.msg_iovlen = iov_count; + + /* Send data */ + ssize_t sent = sendmsg(fd, &msg, MSG_DONTWAIT | MSG_NOSIGNAL); + + if (sent < 0) { + if (errno == EAGAIN) { + WORKER_STATS_INC(eagain_count); + *bytes_sent = 0; + return -2; /* Would block */ + } + + /* ENOBUFS: Socket send buffer is full - treat as temporary condition + * This happens when: + * - SO_SNDBUF limit reached + * - Network is congested or receiver is slow + * + * This is NOT a fatal error - we should back off and retry later + */ + if (errno == ENOBUFS) { + WORKER_STATS_INC(enobufs_count); + *bytes_sent = 0; + return -2; /* Treat as would-block - caller should retry later */ + } + + logger(LOG_DEBUG, "Send queue: sendmsg failed: %s", strerror(errno)); + *bytes_sent = 0; + return -1; + } + + /* Update statistics */ + WORKER_STATS_INC(total_sends); + + *bytes_sent = (size_t)sent; + + /* The kernel copied memory buffers, so completed entries can be released. */ + size_t remaining = (size_t)sent; + while (remaining > 0 && queue->head) { + buffer_ref_t *current = queue->head; + + /* Stop if we hit a file buffer - we only sent memory buffers */ + if (current->type != BUFFER_TYPE_MEMORY) + break; + + if (current->iov.iov_len <= remaining) { + /* Entire buffer sent - remove from queue and free immediately */ + remaining -= current->iov.iov_len; + queue->total_bytes -= current->iov.iov_len; + queue->num_queued--; + queue->memory_bytes -= buffer_ref_capacity(current); + queue->head = current->send_next; + + if (!queue->head) + queue->tail = NULL; + + /* Free buffer immediately since kernel has copied the data */ + buffer_ref_put(current); + } else { + /* Partial send within a buffer - update the iovec to point to remaining + * data */ + current->iov.iov_base = (uint8_t *)current->iov.iov_base + remaining; + current->iov.iov_len -= remaining; + queue->total_bytes -= remaining; + remaining = 0; + } + } + + return 0; +} diff --git a/src/send_queue.h b/src/send_queue.h new file mode 100644 index 00000000..490295ec --- /dev/null +++ b/src/send_queue.h @@ -0,0 +1,47 @@ +#ifndef __SEND_QUEUE_H__ +#define __SEND_QUEUE_H__ + +#include "buffer_pool.h" +#include + +#define SEND_QUEUE_MAX_IOVECS 64 +#define SEND_QUEUE_BATCH_BYTES 65536 + +/* Each connection owns its queue links and partial-send offsets. Buffer data + * can be shared through reference-counted views. */ +typedef struct send_queue_s { + buffer_ref_t *head; + buffer_ref_t *tail; + size_t total_bytes; /* Unsent payload bytes; file entries are excluded */ + size_t memory_bytes; /* Full backing capacity retained by queued entries */ + size_t num_queued; +} send_queue_t; + +/* Buffer pools belong to the worker, so queued batches can outlive a source. */ +typedef struct send_buffer_state_s { + buffer_pool_t pool; + buffer_pool_t control_pool; + buffer_pool_t batch_pool; + size_t active_streams; + int initialized; +} send_buffer_state_t; + +extern send_buffer_state_t send_buffer_state; + +int send_buffer_init(void); +void send_buffer_cleanup(void); +void send_buffer_register_stream_client(void); +void send_buffer_unregister_stream_client(void); +size_t send_buffer_active_streams(void); + +void send_queue_init(send_queue_t *queue); +void send_queue_cleanup(send_queue_t *queue); +/* The queue acquires a reference; the caller retains its own reference. */ +int send_queue_add(send_queue_t *queue, buffer_ref_t *buf_ref); +/* Transfers ownership of file_fd only on success. */ +int send_queue_add_file(send_queue_t *queue, int file_fd, off_t file_offset, size_t file_size); +/* Returns 0 on success, -1 on fatal error, or -2 when the socket would block. */ +int send_queue_send(int fd, send_queue_t *queue, size_t *bytes_sent); +int send_queue_should_flush(send_queue_t *queue); + +#endif /* __SEND_QUEUE_H__ */ diff --git a/src/snapshot.c b/src/snapshot.c index a3d53ce5..6c5e6d84 100644 --- a/src/snapshot.c +++ b/src/snapshot.c @@ -520,6 +520,6 @@ void snapshot_fallback_to_streaming(snapshot_context_t *ctx, connection_t *conn) /* Free snapshot context */ snapshot_cleanup(ctx); - zerocopy_register_stream_client(); + send_buffer_register_stream_client(); conn->stream_registered = 1; } diff --git a/src/status.c b/src/status.c index 8c8b0b30..49392989 100644 --- a/src/status.c +++ b/src/status.c @@ -1022,7 +1022,7 @@ int status_build_sse_json(char *buffer, size_t buffer_capacity, int *p_sent_init buffer, buffer_capacity, &len, "{\"id\":%d,\"pid\":%d,\"activeClients\":%u,\"totalBandwidth\":%llu," "\"totalBytes\":%llu," - "\"send\":{\"total\":%llu,\"completions\":%llu,\"copied\":%llu," + "\"send\":{\"total\":%llu," "\"eagain\":%llu,\"enobufs\":%llu,\"batch\":%llu}," "\"pool\":{\"total\":%llu,\"free\":%llu,\"used\":%llu,\"max\":%llu," "\"expansions\":%llu,\"exhaustions\":%llu,\"shrinks\":%llu," @@ -1032,7 +1032,6 @@ int status_build_sse_json(char *buffer, size_t buffer_capacity, int *p_sent_init "\"utilization\":%.1f}}", i, (int)ws->worker_pid, (unsigned int)w_active, (unsigned long long)w_bandwidth, (unsigned long long)w_total_bytes, (unsigned long long)ws->total_sends, - (unsigned long long)ws->total_completions, (unsigned long long)ws->total_copied, (unsigned long long)ws->eagain_count, (unsigned long long)ws->enobufs_count, (unsigned long long)ws->batch_sends, (unsigned long long)w_pool_total, (unsigned long long)w_pool_free, (unsigned long long)w_pool_used, (unsigned long long)ws->pool_max_buffers, diff --git a/src/status.h b/src/status.h index 59bf346f..ab6781fb 100644 --- a/src/status.h +++ b/src/status.h @@ -124,13 +124,11 @@ typedef struct { /* Client ID generation counter */ uint64_t client_id_counter; /* Incremented for each new client registration */ - /* Zero-copy send statistics */ - uint64_t total_sends; /* Total number of sendmsg() calls */ - uint64_t total_completions; /* Total MSG_ZEROCOPY completions */ - uint64_t total_copied; /* Times kernel copied instead of zero-copy */ - uint64_t eagain_count; /* Number of EAGAIN/EWOULDBLOCK errors */ - uint64_t enobufs_count; /* Number of ENOBUFS errors */ - uint64_t batch_sends; /* Number of batched sends (size threshold) */ + /* Send statistics */ + uint64_t total_sends; /* Total number of sendmsg()/sendfile() calls */ + uint64_t eagain_count; /* Number of EAGAIN/EWOULDBLOCK errors */ + uint64_t enobufs_count; /* Number of ENOBUFS errors */ + uint64_t batch_sends; /* Number of batched sends (size threshold) */ /* Buffer pool statistics */ uint64_t pool_total_buffers; /* Total number of buffers in pool */ diff --git a/src/stream.h b/src/stream.h index 10588f9f..6a375579 100644 --- a/src/stream.h +++ b/src/stream.h @@ -211,7 +211,7 @@ void stream_send_http_headers(connection_t *conn, const char *content_type, cons * connection is currently paused due to backpressure, this resumes it when * the queue has fallen below the low watermark. * - * Called from connection_handle_write after a successful zerocopy_send. + * Called from connection_handle_write after a successful send_queue_send. * The struct must be at least zero-initialized (the embedded stream context * in connection_t is via calloc); passing uninitialized stack memory is * unsafe — `conn` and the `*.initialized` flags are dereferenced. diff --git a/src/supervisor.c b/src/supervisor.c index 1960e44a..6817bdb6 100644 --- a/src/supervisor.c +++ b/src/supervisor.c @@ -6,12 +6,12 @@ #include "pid_file.h" #include "platform_compat.h" #include "rtp2httpd.h" +#include "send_queue.h" #include "service.h" #include "status.h" #include "unix_socket.h" #include "utils.h" #include "worker.h" -#include "zerocopy.h" #include #include #include @@ -690,10 +690,9 @@ int run_worker(void) { return EXIT_FAILURE; } - /* Initialize zero-copy infrastructure for this worker (mandatory) */ - if (zerocopy_init() != 0) { - logger(LOG_FATAL, "Failed to initialize zero-copy infrastructure"); - logger(LOG_FATAL, "MSG_ZEROCOPY support is required (kernel 4.14+)"); + /* Initialize buffered output infrastructure for this worker (mandatory) */ + if (send_buffer_init() != 0) { + logger(LOG_FATAL, "Failed to initialize buffered output infrastructure"); return EXIT_FAILURE; } @@ -703,7 +702,7 @@ int run_worker(void) { int result = worker_run_event_loop(s, maxs, notif_fd); access_log_cleanup(); - zerocopy_cleanup(); + send_buffer_cleanup(); status_cleanup(); config_cleanup(true); diff --git a/src/worker.c b/src/worker.c index fcd358e1..ba912711 100644 --- a/src/worker.c +++ b/src/worker.c @@ -7,11 +7,11 @@ #include "m3u.h" #include "poller.h" #include "rtp2httpd.h" +#include "send_queue.h" #include "status.h" #include "stream.h" #include "utils.h" #include "vendor/hashmap/hashmap.h" -#include "zerocopy.h" #include #include #include @@ -410,46 +410,13 @@ int worker_run_event_loop(int *listen_sockets, int num_sockets, int notif_fd) { if (fd_ready == c->fd) { /* Client socket events */ - /* First, handle POLLER_ERR for MSG_ZEROCOPY completions before - * checking for real errors */ if (events[e].events & POLLER_ERR) { - /* POLLER_ERR can indicate either: - * 1. MSG_ZEROCOPY completion notification (normal operation) - * 2. Actual socket error - * We need to check MSG_ERRQUEUE first to distinguish between them. - */ - int had_zerocopy_completions = 0; - if (c->zerocopy_enabled) { - int completions = zerocopy_handle_completions(c->fd, &c->zc_queue); - if (completions > 0) { - had_zerocopy_completions = 1; - if (c->state == CONN_CLOSING && !c->zc_queue.head && !c->zc_queue.pending_head) { - worker_close_and_free_connection(c); - continue; /* Skip further processing for this connection */ - } - } else if (completions < 0) { - /* Error reading MSG_ERRQUEUE - treat as real socket error */ - logger(LOG_DEBUG, "Failed to read MSG_ERRQUEUE: %s", strerror(errno)); - worker_close_and_free_connection(c); - continue; - } - /* completions == 0: no zerocopy completions, check for real error - * below */ - } - - /* If POLLER_ERR is set but we didn't get zerocopy completions, - * check if it's a real socket error by trying to get SO_ERROR */ - if (!had_zerocopy_completions) { - int socket_error = 0; - socklen_t errlen = sizeof(socket_error); - if (getsockopt(c->fd, SOL_SOCKET, SO_ERROR, &socket_error, &errlen) == 0 && socket_error != 0) { - /* Real socket error */ - logger(LOG_DEBUG, "Client connection error: %s", strerror(socket_error)); - worker_close_and_free_connection(c); - continue; /* Skip further processing for this connection */ - } - /* Otherwise, POLLER_ERR might be spurious or already handled by - * zerocopy */ + int socket_error = 0; + socklen_t errlen = sizeof(socket_error); + if (getsockopt(c->fd, SOL_SOCKET, SO_ERROR, &socket_error, &errlen) < 0 || socket_error != 0) { + logger(LOG_DEBUG, "Client connection error: %s", strerror(socket_error ? socket_error : errno)); + worker_close_and_free_connection(c); + continue; } } @@ -492,7 +459,7 @@ int worker_run_event_loop(int *listen_sockets, int num_sockets, int notif_fd) { } else { /* Normal HTTP request handling */ connection_handle_read(c); - if (c->state == CONN_CLOSING && !c->zc_queue.head) { + if (c->state == CONN_CLOSING && !c->send_queue.head) { worker_close_and_free_connection(c); continue; /* Skip further processing for this connection */ } diff --git a/src/zerocopy.c b/src/zerocopy.c deleted file mode 100644 index bfd4d757..00000000 --- a/src/zerocopy.c +++ /dev/null @@ -1,633 +0,0 @@ -#include "zerocopy.h" -#include "platform_compat.h" -#include "rtp2httpd.h" -#include "status.h" -#include "utils.h" -#include -#include -#include -#include -#include -#include - -#ifdef __linux__ -#include -#endif - -/* Global zero-copy state */ -zerocopy_state_t zerocopy_state = {0}; - -/** - * Helper macro to access this worker's statistics in shared memory - * Falls back to no-op if shared memory not available - */ -#define WORKER_STATS_INC(field) \ - do { \ - if (status_shared && worker_id >= 0 && worker_id < STATUS_MAX_WORKERS) { \ - status_shared->worker_stats[worker_id].field++; \ - } \ - } while (0) - -/** - * Detect MSG_ZEROCOPY support by attempting to enable it on a test socket - */ -static int detect_msg_zerocopy_support(void) { -#ifdef __linux__ - int sock = socket(AF_INET, SOCK_STREAM, 0); - if (sock < 0) - return 0; - - int one = 1; - int ret = setsockopt(sock, SOL_SOCKET, SO_ZEROCOPY, &one, sizeof(one)); - close(sock); - - return (ret == 0) ? 1 : 0; -#else - /* MSG_ZEROCOPY is Linux-only */ - return 0; -#endif -} - -void zerocopy_register_stream_client(void) { zerocopy_state.active_streams++; } - -void zerocopy_unregister_stream_client(void) { - if (zerocopy_state.active_streams > 0) - zerocopy_state.active_streams--; -} - -size_t zerocopy_active_streams(void) { return zerocopy_state.active_streams; } - -int zerocopy_init(void) { - if (zerocopy_state.initialized) - return 0; - - /* Initialize per-worker statistics in shared memory */ - if (status_shared && worker_id >= 0 && worker_id < STATUS_MAX_WORKERS) { - memset(&status_shared->worker_stats[worker_id], 0, sizeof(worker_stats_t)); - } - if (status_shared && worker_id >= 0 && worker_id < STATUS_MAX_WORKERS) { - status_shared->worker_stats[worker_id].worker_pid = getpid(); - } - - /* Check if zerocopy is explicitly enabled by configuration */ - if (config.zerocopy_on_send) { - /* Try to detect MSG_ZEROCOPY support */ - if (!detect_msg_zerocopy_support()) { - logger(LOG_WARN, "Zero-copy: MSG_ZEROCOPY not available (Linux kernel 4.14+ " - "required)"); - logger(LOG_WARN, "Zero-copy: Falling back to regular send"); - /* Disable zerocopy in config since it's not supported */ - config.zerocopy_on_send = 0; - } else { - logger(LOG_INFO, "Zero-copy: MSG_ZEROCOPY enabled for better performance"); - } - } else { - /* Default: Use regular send for maximum compatibility */ - logger(LOG_INFO, "Zero-copy: Using regular send (default). Enable zerocopy-on-send " - "for better performance on supported devices."); - } - - /* Initialize buffer pool with dynamic expansion support */ - if (buffer_pool_init(&zerocopy_state.pool, BUFFER_POOL_BUFFER_SIZE, BUFFER_POOL_INITIAL_SIZE, - config.buffer_pool_max_size, BUFFER_POOL_EXPAND_SIZE, BUFFER_POOL_LOW_WATERMARK, - BUFFER_POOL_HIGH_WATERMARK) < 0) { - logger(LOG_FATAL, "Zero-copy: Failed to initialize buffer pool"); - return -1; - } - - /* Initialize control plane pool */ - if (buffer_pool_init(&zerocopy_state.control_pool, BUFFER_POOL_BUFFER_SIZE, CONTROL_POOL_INITIAL_SIZE, - CONTROL_POOL_MAX_BUFFERS, CONTROL_POOL_EXPAND_SIZE, CONTROL_POOL_LOW_WATERMARK, - CONTROL_POOL_HIGH_WATERMARK) < 0) { - logger(LOG_FATAL, "Zero-copy: Failed to initialize control buffer pool"); - buffer_pool_cleanup(&zerocopy_state.pool); - return -1; - } - - zerocopy_state.active_streams = 0; - - /* Sync initial buffer pool state to shared memory */ - buffer_pool_update_stats(&zerocopy_state.pool); - buffer_pool_update_stats(&zerocopy_state.control_pool); - - zerocopy_state.initialized = 1; - - return 0; -} - -void zerocopy_cleanup(void) { - if (!zerocopy_state.initialized) - return; - - buffer_pool_cleanup(&zerocopy_state.pool); - buffer_pool_cleanup(&zerocopy_state.control_pool); - buffer_pool_cleanup(&zerocopy_state.batch_pool); - buffer_pool_update_stats(&zerocopy_state.pool); - buffer_pool_update_stats(&zerocopy_state.control_pool); - zerocopy_state.initialized = 0; - zerocopy_state.active_streams = 0; -} - -void zerocopy_queue_init(zerocopy_queue_t *queue) { memset(queue, 0, sizeof(*queue)); } - -void zerocopy_queue_cleanup(zerocopy_queue_t *queue) { - /* Clean up send queue - buffers are now directly in the queue */ - buffer_ref_t *buf = queue->head; - while (buf) { - buffer_ref_t *next = buf->send_next; - buffer_ref_put(buf); - buf = next; - } - - /* Clean up pending completion queue */ - buf = queue->pending_head; - while (buf) { - buffer_ref_t *next = buf->send_next; - buffer_ref_put(buf); - buf = next; - } - - zerocopy_queue_init(queue); -} - -int zerocopy_queue_add(zerocopy_queue_t *queue, buffer_ref_t *buf_ref) { - if (!queue || !buf_ref || buf_ref->data_size == 0) - return 0; - - uint8_t *base = (uint8_t *)buf_ref->data; - - size_t capacity = buffer_ref_capacity(buf_ref); - if (!base || buf_ref->data_offset > capacity || buf_ref->data_size > capacity - buf_ref->data_offset) { - logger(LOG_ERROR, - "zerocopy_queue_add: Invalid buffer parameters (offset=%zu len=%zu " - "size=%zu)", - buf_ref->data_offset, buf_ref->data_size, capacity); - return -1; - } - - uint8_t *data_ptr = base + buf_ref->data_offset; - - /* Setup send queue fields in the buffer */ - buf_ref->type = BUFFER_TYPE_MEMORY; - buf_ref->iov.iov_base = data_ptr; - buf_ref->iov.iov_len = buf_ref->data_size; - buf_ref->zerocopy_id = 0; - buf_ref->send_next = NULL; - - /* Increment reference count - queue now holds a reference */ - buffer_ref_get(buf_ref); - - /* Add to queue */ - if (queue->tail) { - queue->tail->send_next = buf_ref; - queue->tail = buf_ref; - } else { - /* First entry - record timestamp for batching timeout */ - queue->head = queue->tail = buf_ref; - } - - queue->total_bytes += buf_ref->data_size; - queue->memory_bytes += capacity; - queue->num_queued++; - - return 0; -} - -int zerocopy_queue_add_file(zerocopy_queue_t *queue, int file_fd, off_t file_offset, size_t file_size) { - if (file_fd < 0 || file_size == 0) - return -1; - - /* Allocate a buffer_ref_t to represent the file (not from pool) */ - buffer_ref_t *buf_ref = calloc(1, sizeof(buffer_ref_t)); - if (!buf_ref) { - logger(LOG_ERROR, "zerocopy_queue_add_file: Failed to allocate buffer_ref"); - return -1; - } - - /* Setup file send fields */ - buf_ref->type = BUFFER_TYPE_FILE; - buf_ref->file_fd = file_fd; - buf_ref->file_offset = file_offset; - buf_ref->file_size = file_size; - buf_ref->file_sent = 0; - buf_ref->refcount = 1; /* Initial reference */ - buf_ref->segment = NULL; /* Not from pool */ - buf_ref->zerocopy_id = 0; - buf_ref->send_next = NULL; - - /* Add to queue */ - if (queue->tail) { - queue->tail->send_next = buf_ref; - queue->tail = buf_ref; - } else { - /* First entry - record timestamp for batching timeout */ - queue->head = queue->tail = buf_ref; - } - - /* Note: File buffers do NOT count towards total_bytes for batching logic - * because they are always flushed immediately and don't participate in - * the batching optimization designed for small RTP packets. - */ - queue->num_queued++; - queue->memory_bytes += BUFFER_POOL_BUFFER_SIZE; - - logger(LOG_DEBUG, "zerocopy_queue_add_file: Queued file fd=%d offset=%ld size=%zu", file_fd, (long)file_offset, - file_size); - - return 0; -} - -int zerocopy_should_flush(zerocopy_queue_t *queue) { - if (!queue || !queue->head) - return 0; /* Nothing to flush */ - - /* Flush if accumulated bytes >= threshold */ - if (queue->total_bytes >= ZEROCOPY_BATCH_BYTES) { - WORKER_STATS_INC(batch_sends); - return 1; - } - - return 0; /* Not ready to flush yet */ -} - -int zerocopy_send(int fd, zerocopy_queue_t *queue, size_t *bytes_sent) { - if (!queue->head) { - *bytes_sent = 0; - return 0; - } - - buffer_ref_t *shared = queue->head; - int shared_fd = buffer_ref_sendfile_fd(shared); - if (shared_fd >= 0) { - off_t offset = (uint8_t *)shared->iov.iov_base - ((uint8_t *)shared->data + shared->data_offset); - ssize_t sent = platform_sendfile(fd, shared_fd, &offset, shared->iov.iov_len); - if (sent < 0 && (errno == EINVAL || errno == ENOSYS || errno == EOPNOTSUPP)) { - /* Keep this subscriber's fallback private; others can still sendfile. */ - shared->shared_fd = -2; - } else { - *bytes_sent = sent > 0 ? (size_t)sent : 0; - if (sent < 0) { - if (errno == EAGAIN || errno == EINTR || errno == ENOBUFS) { - WORKER_STATS_INC(eagain_count); - return -2; - } - return -1; - } - if (sent == 0) - return -1; /* The immutable snapshot must contain the complete batch. */ - WORKER_STATS_INC(total_sends); - queue->total_bytes -= (size_t)sent; - shared->iov.iov_base = (uint8_t *)shared->iov.iov_base + sent; - shared->iov.iov_len -= (size_t)sent; - if (!shared->iov.iov_len) { - queue->head = shared->send_next; - if (!queue->head) - queue->tail = NULL; - queue->num_queued--; - queue->memory_bytes -= buffer_ref_capacity(shared); - buffer_ref_put(shared); - } - return 0; - } - } - - /* Check if head is a file - sendfile() must be done separately */ - if (queue->head->type == BUFFER_TYPE_FILE) { - buffer_ref_t *file_buf = queue->head; - size_t remaining = file_buf->file_size - file_buf->file_sent; - off_t offset = file_buf->file_offset + file_buf->file_sent; - - /* Use platform_sendfile() for non-blocking file send */ - ssize_t sent = platform_sendfile(fd, file_buf->file_fd, &offset, remaining); - - if (sent < 0) { - if (errno == EAGAIN) { - WORKER_STATS_INC(eagain_count); - *bytes_sent = 0; - return -2; /* Would block */ - } - - logger(LOG_ERROR, "Zero-copy: sendfile failed: %s", strerror(errno)); - *bytes_sent = 0; - return -1; - } - - *bytes_sent = (size_t)sent; - file_buf->file_sent += sent; - - /* Check if file send is complete */ - if (file_buf->file_sent >= file_buf->file_size) { - /* File completely sent - remove from queue and cleanup */ - size_t total_file_size = file_buf->file_size; /* Save before put */ - - queue->head = file_buf->send_next; - if (!queue->head) - queue->tail = NULL; - - /* Note: File buffers don't count towards total_bytes, so no need to - * update it */ - queue->num_queued--; - queue->memory_bytes -= BUFFER_POOL_BUFFER_SIZE; - - /* Release reference - this will close fd and free buffer_ref */ - buffer_ref_put(file_buf); - - logger(LOG_DEBUG, "Zero-copy: sendfile complete (%zu bytes)", total_file_size); - } - /* Note: Partial sends for files don't update total_bytes (files don't - * count) */ - - /* Update statistics */ - WORKER_STATS_INC(total_sends); - - return 0; - } - - /* Build iovec array from queue buffers (memory buffers only) */ - struct iovec iovecs[ZEROCOPY_MAX_IOVECS]; - buffer_ref_t *buffers[ZEROCOPY_MAX_IOVECS]; - int iov_count = 0; - int use_zerocopy = config.zerocopy_on_send; - - buffer_ref_t *buf = queue->head; - while (buf && iov_count < ZEROCOPY_MAX_IOVECS && buf->type == BUFFER_TYPE_MEMORY && buffer_ref_sendfile_fd(buf) < 0) { - iovecs[iov_count] = buf->iov; - buffers[iov_count] = buf; - /* Published multicast batches use sendfile. Its fallback copies data, - * without adding asynchronous page ownership to the reusable batch pool. */ - if (buffer_ref_capacity(buf) > BUFFER_POOL_BUFFER_SIZE) - use_zerocopy = 0; - iov_count++; - buf = buf->send_next; - } - - if (iov_count == 0) { - *bytes_sent = 0; - return 0; - } - - /* Prepare message header */ - struct msghdr msg; - memset(&msg, 0, sizeof(msg)); - msg.msg_iov = iovecs; - msg.msg_iovlen = iov_count; - - /* Determine flags based on zerocopy configuration */ - int flags = MSG_DONTWAIT | MSG_NOSIGNAL; - if (use_zerocopy) { - flags |= MSG_ZEROCOPY; - } - - /* Send data */ - ssize_t sent = sendmsg(fd, &msg, flags); - - if (sent < 0) { - if (errno == EAGAIN) { - WORKER_STATS_INC(eagain_count); - *bytes_sent = 0; - return -2; /* Would block */ - } - - /* ENOBUFS: Socket send buffer is full - treat as temporary condition - * This happens when: - * - SO_SNDBUF limit reached - * - Network is congested or receiver is slow - * - Too many pending MSG_ZEROCOPY operations - * - * This is NOT a fatal error - we should back off and retry later - */ - if (errno == ENOBUFS) { - WORKER_STATS_INC(enobufs_count); - *bytes_sent = 0; - return -2; /* Treat as would-block - caller should retry later */ - } - - logger(LOG_DEBUG, "Zero-copy: sendmsg failed: %s", strerror(errno)); - *bytes_sent = 0; - return -1; - } - - /* Update statistics */ - WORKER_STATS_INC(total_sends); - - *bytes_sent = (size_t)sent; - - /* Handle buffer management based on whether MSG_ZEROCOPY is used */ - if (use_zerocopy) { - /* Assign zerocopy ID for this sendmsg call AFTER successful send - * All iovecs in this call share the same ID for completion tracking - * IMPORTANT: Only increment the ID counter after sendmsg() succeeds, - * because the kernel only assigns an ID to successful sends. - */ - uint32_t zerocopy_id = queue->next_zerocopy_id++; - for (int i = 0; i < iov_count; i++) { - buffers[i]->zerocopy_id = zerocopy_id; - } - - /* Move sent buffers from send queue to pending completion queue - * Note: With MSG_ZEROCOPY, the kernel tracks what was actually sent, - * and the completion notification will arrive for the sent data only. - * IMPORTANT: Only process BUFFER_TYPE_MEMORY buffers here, stop at file - * buffers. - */ - size_t remaining = (size_t)sent; - while (remaining > 0 && queue->head) { - buffer_ref_t *current = queue->head; - - /* Stop if we hit a file buffer - we only sent memory buffers */ - if (current->type != BUFFER_TYPE_MEMORY) - break; - - if (current->iov.iov_len <= remaining) { - /* Entire buffer sent - move to pending queue */ - remaining -= current->iov.iov_len; - queue->total_bytes -= current->iov.iov_len; - queue->num_queued--; - queue->memory_bytes -= buffer_ref_capacity(current); - queue->head = current->send_next; - - if (!queue->head) - queue->tail = NULL; - - /* Add to pending completion queue */ - current->send_next = NULL; - if (queue->pending_tail) { - queue->pending_tail->send_next = current; - queue->pending_tail = current; - } else { - queue->pending_head = queue->pending_tail = current; - } - queue->num_pending++; - - /* Note: Buffer will be freed when MSG_ZEROCOPY completion arrives */ - } else { - /* Partial send within a buffer - this is tricky with MSG_ZEROCOPY - * The kernel will send a completion for what was sent, but we need to - * track the unsent portion separately. We'll reset the zerocopy_id to 0 - * so it gets a new ID on the next send attempt. - */ - current->iov.iov_base = (uint8_t *)current->iov.iov_base + remaining; - current->iov.iov_len -= remaining; - current->zerocopy_id = 0; /* Reset ID for next send */ - queue->total_bytes -= remaining; - remaining = 0; - } - } - } else { - /* Regular send without MSG_ZEROCOPY - free buffers immediately */ - size_t remaining = (size_t)sent; - while (remaining > 0 && queue->head) { - buffer_ref_t *current = queue->head; - - /* Stop if we hit a file buffer - we only sent memory buffers */ - if (current->type != BUFFER_TYPE_MEMORY) - break; - - if (current->iov.iov_len <= remaining) { - /* Entire buffer sent - remove from queue and free immediately */ - remaining -= current->iov.iov_len; - queue->total_bytes -= current->iov.iov_len; - queue->num_queued--; - queue->memory_bytes -= buffer_ref_capacity(current); - queue->head = current->send_next; - - if (!queue->head) - queue->tail = NULL; - - /* Free buffer immediately since kernel has copied the data */ - buffer_ref_put(current); - } else { - /* Partial send within a buffer - update the iovec to point to remaining - * data */ - current->iov.iov_base = (uint8_t *)current->iov.iov_base + remaining; - current->iov.iov_len -= remaining; - queue->total_bytes -= remaining; - remaining = 0; - } - } - } - - return 0; -} - -int zerocopy_handle_completions(int fd, zerocopy_queue_t *queue) { - if (!config.zerocopy_on_send) - return 0; - -#ifdef __linux__ - int completions = 0; - - /* Read completion notifications from error queue */ - while (1) { - uint8_t control_buf[128]; - struct msghdr msg; - struct iovec iov; - uint8_t dummy; - - memset(&msg, 0, sizeof(msg)); - iov.iov_base = &dummy; - iov.iov_len = 1; - msg.msg_iov = &iov; - msg.msg_iovlen = 1; - msg.msg_control = control_buf; - msg.msg_controllen = sizeof(control_buf); - - ssize_t ret = recvmsg(fd, &msg, MSG_ERRQUEUE | MSG_DONTWAIT); - if (ret < 0) { - if (errno == EAGAIN) - break; /* No more completions */ - if (errno == EINTR) - continue; - return -1; - } - - /* Parse control messages */ - struct cmsghdr *cmsg; - for (cmsg = CMSG_FIRSTHDR(&msg); cmsg; cmsg = CMSG_NXTHDR(&msg, cmsg)) { - /* Check for both IPv4 and IPv6 error messages */ - if ((cmsg->cmsg_level == SOL_IP && cmsg->cmsg_type == IP_RECVERR) || - (cmsg->cmsg_level == SOL_IPV6 && cmsg->cmsg_type == IPV6_RECVERR)) { - struct sock_extended_err *serr = (struct sock_extended_err *)(uintptr_t)CMSG_DATA(cmsg); - - if (serr->ee_origin == SO_EE_ORIGIN_ZEROCOPY) { - uint32_t lo = serr->ee_info; - uint32_t hi = serr->ee_data; - - /* Update statistics */ - WORKER_STATS_INC(total_completions); - - /* Check if data was copied (fallback) instead of zero-copy */ - if (serr->ee_code & SO_EE_CODE_ZEROCOPY_COPIED) { - WORKER_STATS_INC(total_copied); - } - - /* Update last completed ID */ - queue->last_completed_id = hi; - - /* Free buffers for completed entries */ - /* Note: lo and hi are the range of zerocopy_id that completed */ - buffer_ref_t *buf = queue->pending_head; - buffer_ref_t *prev = NULL; - int matched = 0; - int unmatched = 0; - - while (buf) { - buffer_ref_t *next = buf->send_next; - - /* Check if this buffer's zerocopy_id is in the completed range */ - /* Handle wraparound: if lo <= hi, check [lo, hi]; otherwise check - * [lo, MAX] or [0, hi] */ - int completed = 0; - if (lo <= hi) { - completed = (buf->zerocopy_id >= lo && buf->zerocopy_id <= hi); - } else { - /* Wraparound case */ - completed = (buf->zerocopy_id >= lo || buf->zerocopy_id <= hi); - } - - if (completed) { - /* Remove from pending queue */ - if (prev) - prev->send_next = next; - else - queue->pending_head = next; - - if (buf == queue->pending_tail) - queue->pending_tail = prev; - - queue->num_pending--; - completions++; - matched++; - - /* Release buffer reference - kernel is done with the data */ - buffer_ref_put(buf); - - buf = next; - } else { - unmatched++; - prev = buf; - buf = next; - } - } - - /* Log if we didn't find any matching buffers - this indicates a bug - */ - if (matched == 0) { - logger(LOG_ERROR, - "Zero-copy: Completion for IDs %u-%u but no matching " - "buffers in pending queue (unmatched: %d, pending: %zu)", - lo, hi, unmatched, queue->num_pending); - } - } - } - } - } - - return completions; -#else - /* MSG_ZEROCOPY completions are Linux-only */ - (void)fd; - (void)queue; - return 0; -#endif -} diff --git a/src/zerocopy.h b/src/zerocopy.h deleted file mode 100644 index 9e10582f..00000000 --- a/src/zerocopy.h +++ /dev/null @@ -1,130 +0,0 @@ -#ifndef ZEROCOPY_H -#define ZEROCOPY_H - -#include "buffer_pool.h" -#include -#include - -/** - * Zero-Copy Send Infrastructure for rtp2httpd - * - * This module implements zero-copy send optimization using: - * - Scatter-gather I/O with sendmsg() - * - MSG_ZEROCOPY flag for kernel-level zero-copy - * - Buffer pooling and lifecycle management - * - Automatic fallback for compatibility - */ - -/* Zero-copy configuration */ -#define ZEROCOPY_MAX_IOVECS 64 /* Maximum iovec entries per sendmsg() */ - -/* Batching configuration - accumulate small packets before sending */ -#define ZEROCOPY_BATCH_BYTES 65536 /* Send when accumulated >= 64KB */ - -/** - * Zero-copy send queue for a connection - */ -typedef struct zerocopy_queue_s { - buffer_ref_t *head; /* First buffer to send */ - buffer_ref_t *tail; /* Last buffer in queue */ - buffer_ref_t *pending_head; /* First buffer pending completion */ - buffer_ref_t *pending_tail; /* Last buffer pending completion */ - size_t total_bytes; /* Total bytes queued */ - size_t memory_bytes; /* Backing capacity retained by the send queue */ - size_t num_queued; /* Number of buffers in send queue */ - size_t num_pending; /* Number of buffers pending completion */ - uint32_t next_zerocopy_id; /* Next ID for MSG_ZEROCOPY tracking */ - uint32_t last_completed_id; /* Last completed MSG_ZEROCOPY ID */ -} zerocopy_queue_t; - -/** - * Global zero-copy state - */ -typedef struct zerocopy_state_s { - buffer_pool_t pool; /* Global buffer pool */ - buffer_pool_t control_pool; /* Dedicated pool for status/API control plane */ - buffer_pool_t batch_pool; /* Lazily allocated immutable multicast batches */ - size_t active_streams; /* Number of active media streaming clients */ - int initialized; /* Whether initialized */ -} zerocopy_state_t; - -/* Global zero-copy state */ -extern zerocopy_state_t zerocopy_state; - -/** - * Initialize zero-copy infrastructure - * Detects kernel support and initializes buffer pool - * Uses global worker_id for per-worker statistics - * @return 0 on success, -1 on error - */ -int zerocopy_init(void); - -/** - * Cleanup zero-copy infrastructure - */ -void zerocopy_cleanup(void); - -/** - * Initialize zero-copy queue for a connection - * @param queue Queue to initialize - */ -void zerocopy_queue_init(zerocopy_queue_t *queue); - -/** - * Cleanup zero-copy queue and free all entries - * @param queue Queue to cleanup - */ -void zerocopy_queue_cleanup(zerocopy_queue_t *queue); - -/** - * Queue data for zero-copy send (no memcpy) - * Takes ownership of the buffer via reference counting - * Data pointer is derived from buffer_ref and offset - * @param queue Send queue - * @param buf_ref Buffer reference (must not be NULL) - * @return 0 on success, -1 if queue full or invalid parameters - */ -int zerocopy_queue_add(zerocopy_queue_t *queue, buffer_ref_t *buf_ref); - -/** - * Queue a file descriptor for zero-copy send using sendfile() - * Creates a special buffer_ref_t to represent the file - * Takes ownership of the file descriptor (will close it when done) - * @param queue Send queue - * @param file_fd File descriptor to send (must be seekable) - * @param file_offset Starting offset in file - * @param file_size Number of bytes to send from file - * @return 0 on success, -1 on error - */ -int zerocopy_queue_add_file(zerocopy_queue_t *queue, int file_fd, off_t file_offset, size_t file_size); - -/** - * Send queued data using zero-copy techniques - * @param fd Socket file descriptor - * @param queue Send queue - * @param bytes_sent Output: number of bytes sent - * @return 0 on success, -1 on error, -2 on EAGAIN - */ -int zerocopy_send(int fd, zerocopy_queue_t *queue, size_t *bytes_sent); - -/** - * Check if queue should be flushed based on batching thresholds - * Returns true if accumulated bytes >= ZEROCOPY_BATCH_BYTES or timeout expired - * @param queue Send queue - * @return 1 if should flush, 0 otherwise - */ -int zerocopy_should_flush(zerocopy_queue_t *queue); - -/** - * Handle MSG_ZEROCOPY completion notifications - * @param fd Socket file descriptor - * @param queue Send queue - * @return Number of completions processed, -1 on error - */ -int zerocopy_handle_completions(int fd, zerocopy_queue_t *queue); - -void zerocopy_register_stream_client(void); -void zerocopy_unregister_stream_client(void); -size_t zerocopy_active_streams(void); - -#endif /* ZEROCOPY_H */ diff --git a/web-ui/src/components/status/workers-section.tsx b/web-ui/src/components/status/workers-section.tsx index 5dda1ce8..5300c06c 100644 --- a/web-ui/src/components/status/workers-section.tsx +++ b/web-ui/src/components/status/workers-section.tsx @@ -42,8 +42,6 @@ function WorkersSectionComponent({ workers, locale, bandwidthUnit }: WorkersSect ["bandwidth", t("bandwidth"), formatBandwidth(worker.totalBandwidth, bandwidthUnit)], ["dataSent", t("dataSent"), formatBytes(worker.totalBytes)], ["sendTotal", t("sendTotal"), worker.send.total.toLocaleString()], - ["sendCompletions", t("sendCompletions"), worker.send.completions.toLocaleString()], - ["sendCopied", t("sendCopied"), worker.send.copied.toLocaleString()], ["sendBatch", t("sendBatch"), worker.send.batch.toLocaleString()], ["sendEagain", t("sendEagain"), worker.send.eagain.toLocaleString()], ["sendEnobufs", t("sendEnobufs"), worker.send.enobufs.toLocaleString()], diff --git a/web-ui/src/i18n/status.ts b/web-ui/src/i18n/status.ts index 7a5d22ff..546e45fd 100644 --- a/web-ui/src/i18n/status.ts +++ b/web-ui/src/i18n/status.ts @@ -36,8 +36,6 @@ const base: TranslationDict = { controlPool: "Control pool", sendStats: "Send stats", sendTotal: "Total Sends", - sendCompletions: "Completions", - sendCopied: "Copied", sendEagain: "EAGAIN", sendEnobufs: "ENOBUFS", sendBatch: "Batch flushes", @@ -137,8 +135,6 @@ const zhHans: TranslationDict = { controlPool: "控制池", sendStats: "发送统计", sendTotal: "总发送次数", - sendCompletions: "完成次数", - sendCopied: "拷贝次数", sendEagain: "EAGAIN 次数", sendEnobufs: "ENOBUFS 次数", sendBatch: "批量刷新", @@ -239,8 +235,6 @@ const zhHant: TranslationDict = { controlPool: "控制池", sendStats: "傳送統計", sendTotal: "總傳送次數", - sendCompletions: "完成次數", - sendCopied: "拷貝次數", sendEagain: "EAGAIN 次數", sendEnobufs: "ENOBUFS 次數", sendBatch: "批次刷新", diff --git a/web-ui/src/types.ts b/web-ui/src/types.ts index d17e023d..93103408 100644 --- a/web-ui/src/types.ts +++ b/web-ui/src/types.ts @@ -1,7 +1,5 @@ export interface SendStats { total: number; - completions: number; - copied: number; eagain: number; enobufs: number; batch: number; From 530dc980e92db6b6ea98b6ca223dffe1a0345b5c Mon Sep 17 00:00:00 2001 From: Stackie Jia Date: Sun, 6 Sep 2026 18:16:59 +0800 Subject: [PATCH 05/19] refactor(config): remove unused listener query --- src/configuration.c | 9 --------- src/configuration.h | 6 ------ 2 files changed, 15 deletions(-) diff --git a/src/configuration.c b/src/configuration.c index 4dda7030..6357b4dc 100644 --- a/src/configuration.c +++ b/src/configuration.c @@ -1005,15 +1005,6 @@ int bind_addresses_equal(bindaddr_t *a, bindaddr_t *b) { return (a == NULL && b == NULL); } -int bind_addresses_has_unix(void) { - bindaddr_t *ba; - for (ba = bind_addresses; ba; ba = ba->next) { - if (ba->type == BIND_ADDR_UNIX) - return 1; - } - return 0; -} - /** * Get the config file path */ diff --git a/src/configuration.h b/src/configuration.h index 172bb676..2b7f227e 100644 --- a/src/configuration.h +++ b/src/configuration.h @@ -223,10 +223,4 @@ void set_config_file_path(const char *path); */ int bind_addresses_equal(bindaddr_t *a, bindaddr_t *b); -/** - * Check whether any configured bind address is a Unix domain socket path. - * @return 1 if at least one Unix socket listener is configured, 0 otherwise - */ -int bind_addresses_has_unix(void); - #endif /* __CONFIGURATION_H__ */ From 9d0f59aa95449e4d4ccfa362251bae4d2716c211 Mon Sep 17 00:00:00 2001 From: Stackie Jia Date: Sun, 6 Sep 2026 18:18:37 +0800 Subject: [PATCH 06/19] test(perf): add repeated multicast benchmarks with payload validation --- scripts/benchmark.sh | 220 +---------- tools/stress-test/README.md | 190 ++++------ tools/stress-test/benchmark.py | 554 ++++++++++++++++++++++++++++ tools/stress-test/test_benchmark.py | 33 ++ 4 files changed, 655 insertions(+), 342 deletions(-) create mode 100644 tools/stress-test/benchmark.py create mode 100644 tools/stress-test/test_benchmark.py diff --git a/scripts/benchmark.sh b/scripts/benchmark.sh index 39a94cfb..5aec708b 100755 --- a/scripts/benchmark.sh +++ b/scripts/benchmark.sh @@ -1,216 +1,6 @@ -#!/bin/bash -# -# Benchmark script for rtp2httpd, msd_lite, udpxy, and tvgate -# -# Runs stress tests sequentially to ensure accurate measurements. -# Results are collected and summarized at the end. -# -# Usage: -# scripts/benchmark.sh # Run all programs -# scripts/benchmark.sh rtp2httpd # Run only rtp2httpd tests -# scripts/benchmark.sh tvgate # Run only tvgate tests -# - -set -e -set -o pipefail - +#!/usr/bin/env bash +# Validated, repeated Linux measurements; see tools/stress-test/README.md. +set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" -PROJECT_ROOT="$(dirname "$SCRIPT_DIR")" -STRESS_TEST_DIR="$PROJECT_ROOT/tools/stress-test" -cd "$PROJECT_ROOT" - -# All available programs -ALL_PROGRAMS=("rtp2httpd" "msd_lite" "udpxy" "tvgate") - -# Parse command line arguments -if [ $# -gt 0 ]; then - # Validate provided program name - valid=false - for p in "${ALL_PROGRAMS[@]}"; do - if [ "$1" = "$p" ]; then - valid=true - break - fi - done - if [ "$valid" = false ]; then - echo "Error: Unknown program '$1'" - echo "Available programs: ${ALL_PROGRAMS[*]}" - exit 1 - fi - PROGRAMS=("$1") -else - PROGRAMS=("${ALL_PROGRAMS[@]}") -fi - -# Check Python tooling -if command -v uv >/dev/null 2>&1; then - UV_BIN="uv" -else - echo "Error: uv not found" - echo "Please install uv and run: uv sync" - exit 1 -fi - -# Output file for results -RESULTS_FILE="$STRESS_TEST_DIR/benchmark_results_$(date +%Y%m%d_%H%M%S).txt" - -# Test duration -DURATION=10 - -# Delay between tests (seconds) -DELAY=3 - -echo "============================================================" -echo "Benchmark Suite for Streaming Servers" -echo "============================================================" -echo "Date: $(date)" -echo "Duration per test: ${DURATION}s" -echo "Programs: ${PROGRAMS[*]}" -echo "Results will be saved to: $RESULTS_FILE" -echo "============================================================" -echo "" - -# Initialize results file -cat > "$RESULTS_FILE" << EOF -============================================================ -Benchmark Results - $(date) -============================================================ - -Test Environment: -- Duration per test: ${DURATION}s -- Programs tested: ${PROGRAMS[*]} - -EOF - -run_test() { - local program="$1" - local description="$2" - shift 2 - local extra_args="$@" - - echo "------------------------------------------------------------" - echo "Testing: $program - $description" - echo "Args: $extra_args" - echo "------------------------------------------------------------" - - # Add header to results file - echo "" >> "$RESULTS_FILE" - echo "------------------------------------------------------------" >> "$RESULTS_FILE" - echo "Test: $program - $description" >> "$RESULTS_FILE" - echo "Args: $extra_args" >> "$RESULTS_FILE" - echo "------------------------------------------------------------" >> "$RESULTS_FILE" - - # Run the test and capture output - if "$UV_BIN" run python "$STRESS_TEST_DIR/stress_test.py" --program "$program" --duration "$DURATION" $extra_args 2>&1 | tee -a "$RESULTS_FILE"; then - echo "✓ Test completed" - else - echo "✗ Test failed" - echo "TEST FAILED" >> "$RESULTS_FILE" - fi - - echo "" - echo "Waiting ${DELAY}s before next test..." - sleep "$DELAY" -} - -total_tests=$((${#PROGRAMS[@]} * 3)) -current_test=0 - -for program in "${PROGRAMS[@]}"; do - # Check if program binary exists - case "$program" in - rtp2httpd) - binary="$PROJECT_ROOT/build/rtp2httpd" - ;; - msd_lite) - binary="$PROJECT_ROOT/../msd_lite/build/src/msd_lite" - ;; - udpxy) - binary="$PROJECT_ROOT/../udpxy/chipmunk/udpxy" - ;; - tvgate) - binary="$PROJECT_ROOT/../tvgate/TVGate-linux-arm64" - ;; - esac - - if [ ! -f "$binary" ]; then - echo "Warning: $program binary not found at $binary, skipping..." - echo "SKIPPED: $program binary not found" >> "$RESULTS_FILE" - continue - fi - - echo "" - echo "============================================================" - echo "Testing: $program" - echo "============================================================" - echo "" - - # Test 1: 8 clients, unique addresses (default) - current_test=$((current_test + 1)) - echo "[$current_test/$total_tests] $program: 8 clients, unique addresses, 5x speed (~40 Mbps)" - run_test "$program" "8 clients, unique addresses, 40 Mbps" --clients 8 --speed 5 - - # Test 2: 8 clients, same address - current_test=$((current_test + 1)) - echo "[$current_test/$total_tests] $program: 8 clients, same address, 5x speed (~40 Mbps)" - run_test "$program" "8 clients, same address, 40 Mbps" --clients 8 --speed 5 --same-address - - # Test 3: 1 client, high bitrate (400 Mbps) - current_test=$((current_test + 1)) - echo "[$current_test/$total_tests] $program: 1 client, 50x speed (~400 Mbps)" - run_test "$program" "1 client, 400 Mbps" --clients 1 --speed 50 -done - -echo "" -echo "============================================================" -echo "Benchmark Complete!" -echo "============================================================" -echo "Results saved to: $RESULTS_FILE" -echo "" - -# Generate summary -echo "" >> "$RESULTS_FILE" -echo "============================================================" >> "$RESULTS_FILE" -echo "SUMMARY" >> "$RESULTS_FILE" -echo "============================================================" >> "$RESULTS_FILE" - -# Extract and format summary from results -echo "" -echo "Extracting summary..." -echo "" - -# Parse results and create summary table -{ - echo "" - echo "Performance Summary Table:" - echo "" - printf "%-12s %-30s %10s %10s %10s %10s\n" "Program" "Test" "CPU Avg" "CPU Max" "PSS Avg" "USS Avg" - printf "%-12s %-30s %10s %10s %10s %10s\n" "-------" "----" "-------" "-------" "-------" "-------" - - current_program="" - current_test="" - - while IFS= read -r line; do - if [[ "$line" =~ ^Test:\ ([a-z0-9_]+)\ -\ (.+)$ ]]; then - current_program="${BASH_REMATCH[1]}" - current_test="${BASH_REMATCH[2]}" - elif [[ "$line" =~ CPU:\ +avg=\ *([0-9.]+)%\ +max=\ *([0-9.]+)% ]]; then - cpu_avg="${BASH_REMATCH[1]}" - cpu_max="${BASH_REMATCH[2]}" - elif [[ "$line" =~ PSS:\ +avg=\ *([0-9.]+)MB ]]; then - pss_avg="${BASH_REMATCH[1]}" - elif [[ "$line" =~ USS:\ +avg=\ *([0-9.]+)MB ]]; then - uss_avg="${BASH_REMATCH[1]}" - # Only print when we have the program stats (first stats block after test header) - if [[ -n "$current_program" && -n "$current_test" && -n "$cpu_avg" ]]; then - printf "%-12s %-30s %9s%% %9s%% %9sMB %9sMB\n" "$current_program" "$current_test" "$cpu_avg" "$cpu_max" "$pss_avg" "$uss_avg" - current_program="" - current_test="" - cpu_avg="" - fi - fi - done < "$RESULTS_FILE" -} | tee -a "$RESULTS_FILE" - -echo "" -echo "Full results saved to: $RESULTS_FILE" +cd "$SCRIPT_DIR/.." +exec uv run python tools/stress-test/benchmark.py "$@" diff --git a/tools/stress-test/README.md b/tools/stress-test/README.md index df4c28be..580f192f 100644 --- a/tools/stress-test/README.md +++ b/tools/stress-test/README.md @@ -1,154 +1,90 @@ -# Stress test +# Streaming server performance tools -Automated performance tests for streaming servers: rtp2httpd, msd_lite, udpxy, and tvgate. - -## What it does - -1. Starts multicast packet replay with `tools/udp-replay/udp_replay.py --continuous --speed N`. -2. Launches the streaming server under test. -3. Spawns multiple concurrent curl clients, each requesting a unique multicast address by default. -4. Monitors CPU and memory usage, including forked child processes. -5. Reports statistics after the test. +`scripts/benchmark.sh` runs repeated, validated comparisons of rtp2httpd, msd_lite, udpxy, and TVGate. `stress_test.py` remains available for interactive PCAP replay and load debugging; its `top` samples are not used for the performance report. ## Requirements -- Python 3.14+ -- [uv](https://docs.astral.sh/uv/) -- Linux (uses `/proc`, `/proc/net/igmp`, and `top`) -- `curl` -- The server binary being tested - -The default binary locations are resolved relative to the repository root: +- Linux with `/proc`, `taskset`, and `sysctl`; Python 3.14+ managed by `uv`. +- Build the required server binaries first. Missing binaries fail the run rather than silently skipping a competitor. +- The default full matrix uses 14 available logical CPUs: server 0, load generators/readers 1–12, controller 13. The 64-client case alone needs seven CPUs; examples below show how to select them. +- Multicast and HTTP use loopback. No root permissions, recorded video, or network sysctl changes are required. -| Program | Path | -| --------- | ------------------------------------- | -| rtp2httpd | `build/rtp2httpd` | -| msd_lite | `../msd_lite/build/src/msd_lite` | -| udpxy | `../udpxy/chipmunk/udpxy` | -| tvgate | `../tvgate/TVGate-linux-arm64` | +## Repeated benchmark -External program configs live under `tools/stress-test/conf/`. - -## Usage +From the repository root: ```bash -# Test rtp2httpd (default) -uv run python tools/stress-test/stress_test.py - -# Test msd_lite -uv run python tools/stress-test/stress_test.py --program msd_lite +# Refresh the project's existing dependencies and build rtp2httpd. +uv sync --group dev +cmake -B build -DCMAKE_BUILD_TYPE=Release -DENABLE_AGGRESSIVE_OPT=ON +cmake --build build -j$(getconf _NPROCESSORS_ONLN) -# Test udpxy -uv run python tools/stress-test/stress_test.py --program udpxy - -# Test tvgate -uv run python tools/stress-test/stress_test.py --program tvgate +# Four programs, four cases, five repetitions; 5 s warmup + 20 s sampling. +scripts/benchmark.sh -# Custom parameters -uv run python tools/stress-test/stress_test.py --duration 30 --clients 16 --speed 10 +# Focus on 64 viewers of one 20 Mbps channel, using seven logical CPUs. +scripts/benchmark.sh rtp2httpd msd_lite \ + --cases shared64 --load-cpus 1,2,3,4,5 --controller-cpu 6 -# Verbose output (show subprocess output) -uv run python tools/stress-test/stress_test.py -v +# Include the pre-optimization baseline as a separate binary. +scripts/benchmark.sh baseline rtp2httpd msd_lite udpxy tvgate \ + --cases shared64 --repetitions 5 --duration 20 --warmup 5 \ + --binary baseline=/absolute/path/to/baseline/rtp2httpd \ + --revision baseline=BASELINE_COMMIT --revision rtp2httpd=FEATURE_COMMIT ``` -## Options - -| Option | Default | Description | -| ---------------- | ----------- | ------------------------------------------------------------ | -| `--program` | `rtp2httpd` | Program to test: rtp2httpd, msd_lite, udpxy, tvgate | -| `--duration` | `10` | Test duration in seconds | -| `--clients` | `8` | Number of concurrent curl clients | -| `--speed` | `5.0` | Replay speed multiplier (5x is approximately 40 Mbps) | -| `--same-address` | - | All clients use the same multicast address (default: unique) | -| `-v, --verbose` | - | Show verbose output from subprocesses | - -## Benchmark suite - -Run the full benchmark matrix from the repository root: +Default paths can all be overridden with `--binary NAME=/absolute/path`: -```bash -scripts/benchmark.sh -``` +| Name | Default executable | +| --- | --- | +| `rtp2httpd` | `build/rtp2httpd` | +| `msd_lite` | `../msd_lite/build/src/msd_lite` | +| `udpxy` | `../udpxy/chipmunk/udpxy` | +| `tvgate` | `../tvgate/TVGate-linux-arm64` | +| `baseline` | Must be supplied explicitly | -Run one program only: +Use a clean checkout or separate build directory when refreshing competitors; preserve any existing local changes. Resolve upstream versions before the run and supply `--revision NAME=COMMIT_OR_TAG` for every binary. The report records the exact tested revisions and build settings. TVGate is tested using its official native release binary; verify the release asset's SHA-256 before extracting it. -```bash -scripts/benchmark.sh rtp2httpd -scripts/benchmark.sh tvgate -``` +| Case | Clients | Sources | Payload rate per source | +| --- | ---: | ---: | ---: | +| `shared64` | 64 | 1 | 20 Mbps | +| `distinct8` | 8 | 8 | 40 Mbps | +| `shared8` | 8 | 1 | 40 Mbps | +| `high400` | 1 | 1 | 400 Mbps | -Benchmark results are saved to `tools/stress-test/benchmark_results_YYYYMMDD_HHMMSS.txt`. - -## Example output - -```text -============================================================ -Stress Test: rtp2httpd -============================================================ -Program: rtp2httpd -Binary: /path/to/rtp2httpd -Duration: 10s -Clients: 8 -Replay speed: 5.0x (~40 Mbps) -Port: 5140 -Streams: 239.81.0.1-8:4056 (unique per client) -============================================================ - -[1/3] Starting multicast replay... - PID: 12345 - -[2/3] Starting rtp2httpd... - PID: 12346 - -[3/3] Starting 8 curl clients... - Each client uses a unique address in 239.81.0.0/24 - URLs: http://127.0.0.1:5140/rtp/239.81.0.1:4056 ... (and 7 more) - PIDs: [12347, 12348, ...] - -[Running] Test running for 10 seconds... - Progress: 10.0s / 10s - -============================================================ -RESULTS -============================================================ - -[rtp2httpd] - CPU: avg= 2.00% max= 2.00% - PSS: avg= 3.62MB max= 3.62MB - USS: avg= 3.62MB max= 3.62MB - -[replay (udp_replay.py)] - CPU: avg=100.67% max=102.00% - PSS: avg= 69.47MB max= 69.47MB - USS: avg= 69.47MB max= 69.47MB - -[curl clients x4 (aggregated)] - CPU: avg= 0.00% max= 0.00% - PSS: avg= 38.62MB max= 38.62MB - USS: avg= 38.62MB max= 38.62MB -``` +`--cases`, `--repetitions`, `--duration`, `--warmup`, `--server-cpu`, `--load-cpus`, `--controller-cpu`, and `--output` customize the run. Program order rotates and reverses across repetitions. Each trial starts fresh server and load processes; tests run sequentially. -## Unique multicast addresses +## Measurement and validity -By default, each curl client requests a unique multicast address in the same /24 subnet: +- CPU is the change in user + system CPU ticks from `/proc/PID/stat`, divided by measured wall time. **100% means one logical CPU**, not the whole machine. Include the supervisor and every child process; process CPU already includes its threads and must not be summed again by thread. +- All server processes/threads inherit the same single-CPU affinity. rtp2httpd uses `-C -w 1`; msd_lite uses one event-loop thread; TVGate uses `GOMAXPROCS=1`; udpxy retains its native process-per-client model. These are single-CPU comparisons, not claims that all programs have one process or thread. +- Each generator and reader process has a separate CPU from the server. Their combined CPU is recorded separately. Loopback kernel work charged to the load processes is outside the server metric; this is not total system CPU or a physical-NIC throughput test. +- PSS and USS come from `smaps_rollup`, summed over the process family and sampled once per second. PSS includes proportional shared pages; USS includes private clean/dirty/huge pages. Neither is a count of kernel socket memory. +- The sender emits RTP payload type 33 with seven 188-byte TS null packets per datagram. Every TS packet contains a monotonically increasing marker, its complement, a source identifier, and a checked payload pattern. This tests forwarding and integrity, not video decoding. +- Readers decode HTTP chunk framing before checking payloads. Each client must receive within 2% of the target rate; each generator must also maintain that rate. The measured window must contain no gaps, duplicates, corrupt packets, EOFs, or kernel UDP drops. The process family must remain stable and the load processes alive. +- Failures remain in the raw output as `valid: false`; the summary averages valid trials only and always reports valid/total counts. A failed or incomplete run exits nonzero. Do not describe its low CPU as a performance win. +- msd_lite keeps the upstream example's 48 KiB receive watermark, 64 KiB send watermark, and 1 MiB ring. Only the listener, interface, thread count/affinity, verbosity, and congestion-control name are adapted. udpxy keeps upstream buffer defaults. TVGate uses loopback multicast settings and connection limits of 256. Generated configs and complete commands are saved for review. -- Client 1: `239.81.0.1:4056` -- Client 2: `239.81.0.2:4056` -- Client 3: `239.81.0.3:4056` +The output directory (default `build/benchmark/YYYYMMDD-HHMMSS/`) contains: -This tests the server's ability to handle multiple independent streams. Use `--same-address` to have all clients request the same address for a traditional single-stream stress test. +- `metadata.json`: environment, CPU placement, sysctls, revisions, executable/script SHA-256 values. +- `trials.jsonl`: every trial, including failures, per-client rates, integrity counters, CPU, memory, socket/process/thread counts. +- `summary.json`: valid/total counts and mean/min/max CPU, PSS, USS. +- Per-trial logs and generated msd_lite/TVGate configurations. -## CPU measurement +Run the HTTP framing checks with: -- Uses `top -b -n 2` for CPU sampling. The first sample is cumulative, and the second reflects current usage. -- Aggregates CPU usage across the parent process and all forked child processes. -- More accurate than `/proc/[pid]/stat` for programs using io_uring. +```bash +uv run pytest tools/stress-test/test_benchmark.py -q +``` -## Memory measurement +## Interactive replay -Reports two memory metrics for forked processes: +The earlier PCAP-based tool remains useful for manually stressing a server: -- PSS (Proportional Set Size): shared memory divided proportionally among all sharing processes. Best for capacity planning. -- USS (Unique Set Size): private memory only (`Private_Clean + Private_Dirty`). Represents memory freed when the process exits. +```bash +uv run python tools/stress-test/stress_test.py --program rtp2httpd \ + --duration 30 --clients 16 --speed 10 --same-address +``` -Both metrics are read from `/proc/[pid]/smaps_rollup` on Linux 4.14+ and aggregated across child processes. +It uses `tools/udp-replay/udp_replay.py`, curl readers, and `top` samples. It does not verify the complete output payload or provide repeated comparisons; use the benchmark harness above for published results. diff --git a/tools/stress-test/benchmark.py b/tools/stress-test/benchmark.py new file mode 100644 index 00000000..51464806 --- /dev/null +++ b/tools/stress-test/benchmark.py @@ -0,0 +1,554 @@ +"""Validated Linux multicast benchmark. Run through scripts/benchmark.sh. + +No video fixture or extra packages: RTP carries seven numbered MPEG-TS null packets. +The workload processes and the server process family use disjoint CPU affinities. +""" + +import argparse +import hashlib +import json +import multiprocessing as mp +import os +import platform +import selectors +import signal +import socket +import statistics +import struct +import subprocess +import time +import xml.etree.ElementTree as ET +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[2] +FIELDS = 8 # bytes, TS packets, gaps, duplicates, corrupt packets, ready, EOF, backward markers +PAYLOAD = 7 * 188 +CASES = { + "shared64": (64, 1, 20), + "distinct8": (8, 8, 40), + "shared8": (8, 1, 40), + "high400": (1, 1, 400), +} +FILL = b"\xff" * 172 + + +def free_port(kind=socket.SOCK_STREAM): + with socket.socket(socket.AF_INET, kind) as sock: + sock.bind(("127.0.0.1", 0)) + return sock.getsockname()[1] + + +def process_family(pid): + found, pending = set(), [pid] + while pending: + current = pending.pop() + if current in found: + continue + found.add(current) + # Children can belong to any thread (e.g. Go), not just the thread-group leader. + for task in Path(f"/proc/{current}/task").glob("*/children"): + try: + pending.extend(map(int, task.read_text().split())) + except FileNotFoundError: + pass + return found + + +def process_stats(pids): + result = {} + for pid in pids: + fields = Path(f"/proc/{pid}/stat").read_text().split(") ", 1)[1].split() + result[pid] = (int(fields[11]), int(fields[12])) + return result + + +def cpu_delta(before, after, elapsed): + return [ + sum(after[pid][i] - before[pid][i] for pid in before) / os.sysconf("SC_CLK_TCK") / elapsed * 100 for i in (0, 1) + ] + + +def memory_mib(pids): + pss = uss = 0 + for pid in pids: + for line in Path(f"/proc/{pid}/smaps_rollup").read_text().splitlines(): + key, *value = line.split() + if key == "Pss:": + pss += int(value[0]) + elif key in ("Private_Clean:", "Private_Dirty:", "Private_Hugetlb:"): + uss += int(value[0]) + return pss / 1024, uss / 1024 + + +def udp_stats(pids): + inodes = set() + for pid in pids: + for fd in Path(f"/proc/{pid}/fd").iterdir(): + try: + link = os.readlink(fd) + if link.startswith("socket:["): + inodes.add(link[8:-1]) + except FileNotFoundError: + pass + rows = [line.split() for line in Path("/proc/net/udp").read_text().splitlines()[1:]] + matched = [row for row in rows if row[9] in inodes] + return {"sockets": len(matched), "drops": sum(int(row[-1]) for row in matched)} + + +def sender(group, port, mbps, cpu, source, stop, sent, errors): + try: + os.sched_setaffinity(0, {cpu}) + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as sock: + sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton("127.0.0.1")) + sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 1) + sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_TTL, 1) + packet = bytearray(12 + PAYLOAD) + packet[0:2] = b"\x80\x21" + struct.pack_into("!I", packet, 8, source + 1) + for j in range(7): + offset = 12 + j * 188 + packet[offset : offset + 3] = b"\x47\x1f\xff" + struct.pack_into("!I", packet, offset + 12, source) + packet[offset + 16 : offset + 188] = FILL + origin = time.monotonic() + pps = mbps * 1e6 / (PAYLOAD * 8) + count = 0 + while not stop.is_set(): + target = int((time.monotonic() - origin) * pps) + for _ in range(min(target - count, 256)): + struct.pack_into("!HI", packet, 2, count & 65535, int(count * 90000 / pps) & 0xFFFFFFFF) + for j in range(7): + marker = (count * 7 + j) & 0xFFFFFFFF + offset = 12 + j * 188 + packet[offset + 3] = 0x10 | (marker & 15) + struct.pack_into("!II", packet, offset + 4, marker, marker ^ 0xFFFFFFFF) + sock.sendto(packet, (group, port)) + count += 1 + sent[source] = count + if count >= target: + stop.wait(0.0005) + except (OSError, ValueError, struct.error) as exc: + errors.put(f"sender {source}: {exc!r}") + + +class HTTPBody: + """Incremental HTTP/1.1 header and chunk framing decoder.""" + + def __init__(self): + self.pending = b"" + self.headers = False + self.chunked = False + self.remaining = None + self.separator = False + self.finished = False + + def feed(self, data): + self.pending += data + if not self.headers: + if b"\r\n\r\n" not in self.pending: + if len(self.pending) > 65536: + raise ValueError("oversized HTTP header") + return b"" + header, self.pending = self.pending.split(b"\r\n\r\n", 1) + if header.split(b"\r\n", 1)[0].split()[1] != b"200": + raise ValueError(f"HTTP error: {header[:200]!r}") + self.chunked = any( + line.lower().startswith(b"transfer-encoding:") and b"chunked" in line.lower() + for line in header.split(b"\r\n")[1:] + ) + self.headers = True + if not self.chunked: + data, self.pending = self.pending, b"" + return data + output = [] + while not self.finished: + if self.separator: + if len(self.pending) < 2: + break + if self.pending[:2] != b"\r\n": + raise ValueError("invalid chunk terminator") + self.pending = self.pending[2:] + self.separator = False + if self.remaining is None: + if b"\r\n" not in self.pending: + break + size, self.pending = self.pending.split(b"\r\n", 1) + self.remaining = int(size.split(b";", 1)[0], 16) + if self.remaining == 0: + self.finished = True + break + count = min(len(self.pending), self.remaining) + output.append(self.pending[:count]) + self.pending = self.pending[count:] + self.remaining -= count + if self.remaining: + break + self.remaining = None + self.separator = True + return b"".join(output) + + +def consumers(port, sources, ids, cpu, stop, counters, errors, path_prefix): + try: + os.sched_setaffinity(0, {cpu}) + with selectors.DefaultSelector() as selector: + for client in ids: + group, udp_port, source = sources[client] + sock = socket.create_connection(("127.0.0.1", port), timeout=5) + sock.setsockopt(socket.SOL_SOCKET, socket.SO_RCVBUF, 1024 * 1024) + sock.sendall( + f"GET /{path_prefix}/{group}:{udp_port} HTTP/1.1\r\nHost: 127.0.0.1\r\nConnection: close\r\n\r\n".encode() + ) + sock.setblocking(False) + selector.register(sock, selectors.EVENT_READ, [client, source, HTTPBody(), b"", None]) + while not stop.is_set(): + for key, _ in selector.select(0.1): + assert isinstance(key.fileobj, socket.socket) + client, source, decoder, carry, previous = key.data + data = key.fileobj.recv(256 * 1024) + base = client * FIELDS + if not data: + counters[base + 6] += 1 + selector.unregister(key.fileobj) + key.fileobj.close() + continue + carry += decoder.feed(data) + size = len(carry) // 188 * 188 + gaps = duplicates = corrupt = backward = 0 + for offset in range(0, size, 188): + marker, complement, packet_source = struct.unpack_from("!III", carry, offset + 4) + if ( + carry[offset : offset + 3] != b"\x47\x1f\xff" + or carry[offset + 3] != (0x10 | (marker & 15)) + or complement != marker ^ 0xFFFFFFFF + or packet_source != source + or carry[offset + 16 : offset + 188] != FILL + ): + corrupt += 1 + if previous is not None: + diff = (marker - previous) & 0xFFFFFFFF + if diff == 0: + duplicates += 1 + elif diff != 1: + if diff < 0x80000000: + gaps += diff - 1 + else: + backward += 1 + previous = marker + counters[base] += size + counters[base + 1] += size // 188 + counters[base + 2] += gaps + counters[base + 3] += duplicates + counters[base + 4] += corrupt + counters[base + 5] = int(decoder.headers) + counters[base + 7] += backward + key.data[3:] = [carry[size:], previous] + if decoder.finished: + counters[base + 6] += 1 + selector.unregister(key.fileobj) + key.fileobj.close() + for key in list(selector.get_map().values()): + assert isinstance(key.fileobj, socket.socket) + key.fileobj.close() + except (OSError, ValueError, struct.error, IndexError) as exc: + errors.put(f"consumer: {exc!r}") + + +def command_for(program, binary, port, stem, server_cpu): + env = os.environ.copy() + if program in ("rtp2httpd", "baseline"): + command = [str(binary), "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", f"127.0.0.1:{port}"] + elif program == "msd_lite": + tree = ET.parse(ROOT / "tools/stress-test/conf/msd_lite.conf") + root = tree.getroot() + changes = { + "log/level": "1", + "threadPool/threadsCountMax": "1", + "threadPool/fBindToCPU": "no", + "HTTP/bindList/bind/address": f"127.0.0.1:{port}", + "sourceProfileList/sourceProfile/multicast/ifName": "lo", + "hubProfileList/hubProfile/skt/congestionControl": "cubic", + } + for path, value in changes.items(): + element = root.find(path) + if element is None: + raise ValueError(f"missing msd_lite setting: {path}") + element.text = value + bindings = root.find("HTTP/bindList") + if bindings is None: + raise ValueError("missing msd_lite bindings") + for binding in list(bindings)[1:]: + bindings.remove(binding) + config = stem.with_suffix(".xml") + tree.write(config, encoding="utf-8", xml_declaration=True) + command = [str(binary), "-c", str(config), "-l", "1"] + elif program == "udpxy": + # Preserve upstream data-buffer defaults. Pin all forked clients to the same CPU. + command = [str(binary), "-T", "-a", "127.0.0.1", "-m", "lo", "-p", str(port), "-c", "256"] + else: + config = stem.with_suffix(".yaml") + config.write_text( + f"server:\n port: {port}\nlog:\n enabled: false\n" + "http:\n max_idle_conns: 256\n max_idle_conns_per_host: 256\n max_conns_per_host: 256\n" + "multicast:\n multicast_ifaces: [lo]\n upstream_interface: lo\n" + ) + env["GOMAXPROCS"] = "1" + command = [str(binary), "-config", str(config)] + return ["taskset", "-c", str(server_cpu), *command], env + + +def trial(program, case, repetition, order, args, binaries): + clients, count, mbps = CASES[case] + context = mp.get_context("spawn") + stop, errors = context.Event(), context.Queue() + counters = context.Array("Q", clients * FIELDS, lock=False) + sent = context.Array("Q", count, lock=False) + streams = [(f"239.255.77.{i + 1}", free_port(socket.SOCK_DGRAM), i) for i in range(count)] + sources = [streams[i % count] for i in range(clients)] + port = free_port() + stem = args.output / f"{case}-{repetition:02d}-{program}" + command, env = command_for(program, binaries[program], port, stem, args.server_cpu) + path_prefix = "udp" if program == "tvgate" else "rtp" + result = { + "program": program, + "case": case, + "repetition": repetition, + "order": order, + "command": command, + "clients": clients, + "sources": count, + "target_mbps": mbps, + "valid": False, + "request_prefix": path_prefix, + } + children = [] + with stem.with_suffix(".log").open("w") as log: + daemon = subprocess.Popen(command, env=env, stdout=log, stderr=log, start_new_session=True) + try: + deadline = time.monotonic() + 15 + while time.monotonic() < deadline: + if daemon.poll() is not None: + raise RuntimeError(f"server exited {daemon.returncode}; see {stem.name}.log") + try: + with socket.create_connection(("127.0.0.1", port), timeout=0.1): + break + except OSError: + time.sleep(0.05) + else: + raise RuntimeError("server not ready") + collectors = min(4, clients) + available = [cpu for cpu in args.load_cpus if cpu != args.server_cpu] + for source, (group, udp_port, _) in enumerate(streams): + child = context.Process( + target=sender, + args=(group, udp_port, mbps, available[collectors + source], source, stop, sent, errors), + ) + child.start() + children.append(child) + for i in range(collectors): + child = context.Process( + target=consumers, + args=( + port, + sources, + list(range(i, clients, collectors)), + available[i], + stop, + counters, + errors, + path_prefix, + ), + ) + child.start() + children.append(child) + deadline = time.monotonic() + 20 + while not all(counters[i * FIELDS] >= 65536 for i in range(clients)): + if time.monotonic() > deadline or not errors.empty(): + raise RuntimeError("not all clients received data") + time.sleep(0.05) + time.sleep(args.warmup) + pids = process_family(daemon.pid) + tids = [int(t.name) for pid in pids for t in Path(f"/proc/{pid}/task").iterdir()] + if any(os.sched_getaffinity(tid) != {args.server_cpu} for tid in tids): + raise RuntimeError("server affinity changed") + before_udp = udp_stats(pids) + before_clients, before_sent = list(counters), list(sent) + before = process_stats(pids) + load_pids = [p.pid for p in children] + before_load = process_stats(load_pids) + begin = time.monotonic() + memory = [] + for _ in range(args.duration): + time.sleep(1) + memory.append(memory_mib(pids)) + elapsed = time.monotonic() - begin + after = process_stats(pids) + after_clients, after_sent = list(counters), list(sent) + after_load = process_stats(load_pids) + after_udp = udp_stats(pids) + user, system = cpu_delta(before, after, elapsed) + rates = [ + (after_clients[i * FIELDS] - before_clients[i * FIELDS]) * 8 / elapsed / 1e6 for i in range(clients) + ] + source_rates = [(after_sent[i] - before_sent[i]) * PAYLOAD * 8 / elapsed / 1e6 for i in range(count)] + deltas = [ + sum(after_clients[i * FIELDS + j] - before_clients[i * FIELDS + j] for i in range(clients)) + for j in range(FIELDS) + ] + result.update( + { + "duration_s": elapsed, + "cpu_pct": user + system, + "user_cpu_pct": user, + "system_cpu_pct": system, + "pss_mib": statistics.mean(m[0] for m in memory), + "uss_mib": statistics.mean(m[1] for m in memory), + "server_processes": len(pids), + "server_threads": len(tids), + "multicast_sockets": after_udp["sockets"], + "client_mbps": rates, + "source_mbps": source_rates, + "gaps": deltas[2], + "duplicates": deltas[3], + "corrupt_packets": deltas[4], + "backward_markers": deltas[7], + "closed_clients": deltas[6], + "kernel_udp_drops": after_udp["drops"] - before_udp["drops"], + "load_cpu_pct": sum(cpu_delta(before_load, after_load, elapsed)), + "family_stable": pids == process_family(daemon.pid), + } + ) + result["valid"] = ( + not any(deltas[i] for i in (2, 3, 4, 6, 7)) + and result["kernel_udp_drops"] == 0 + and result["family_stable"] + and all(abs(rate / mbps - 1) < 0.02 for rate in source_rates + rates) + and all(child.is_alive() for child in children) + ) + except (OSError, ValueError, RuntimeError, subprocess.SubprocessError) as exc: + result["error"] = repr(exc) + finally: + stop.set() + for child in children: + child.join(timeout=3) + if child.is_alive(): + child.terminate() + child.join(timeout=3) + if daemon.poll() is None: + os.killpg(daemon.pid, signal.SIGTERM) + try: + daemon.wait(timeout=3) + except subprocess.TimeoutExpired: + os.killpg(daemon.pid, signal.SIGKILL) + daemon.wait() + issues = [] + while not errors.empty(): + issues.append(errors.get()) + if issues: + result["load_errors"] = issues + result["valid"] = False + return result + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("programs", nargs="*", choices=["rtp2httpd", "msd_lite", "udpxy", "tvgate", "baseline"]) + parser.add_argument("--binary", action="append", default=[], metavar="NAME=PATH") + parser.add_argument("--revision", action="append", default=[], metavar="NAME=REVISION") + parser.add_argument("--cases", nargs="+", choices=CASES, default=list(CASES)) + parser.add_argument("--repetitions", type=int, default=5) + parser.add_argument("--duration", type=int, default=20) + parser.add_argument("--warmup", type=float, default=5) + parser.add_argument("--server-cpu", type=int, default=0) + parser.add_argument("--controller-cpu", type=int, default=13) + parser.add_argument("--load-cpus", default="1,2,3,4,5,6,7,8,9,10,11,12") + parser.add_argument("--output", type=Path, default=ROOT / "build/benchmark" / time.strftime("%Y%m%d-%H%M%S")) + args = parser.parse_args() + if platform.system() != "Linux": + parser.error("measurement requires Linux /proc and taskset") + if args.duration < 1 or args.repetitions < 1 or args.warmup < 0: + parser.error("duration/repetitions must be positive and warmup nonnegative") + args.load_cpus = list(dict.fromkeys(map(int, args.load_cpus.split(",")))) + required = max(min(4, CASES[c][0]) + CASES[c][1] for c in args.cases) + if args.server_cpu in args.load_cpus or len(args.load_cpus) < required: + parser.error(f"choose at least {required} load CPUs, disjoint from --server-cpu") + if not {args.server_cpu, *args.load_cpus} <= os.sched_getaffinity(0): + parser.error("requested CPUs are outside the current process affinity") + if args.controller_cpu in {args.server_cpu, *args.load_cpus} or args.controller_cpu not in os.sched_getaffinity(0): + parser.error("choose a separate available --controller-cpu") + os.sched_setaffinity(0, {args.controller_cpu}) + programs = args.programs or ["rtp2httpd", "msd_lite", "udpxy", "tvgate"] + binaries = { + "rtp2httpd": ROOT / "build/rtp2httpd", + "msd_lite": ROOT.parent / "msd_lite/build/src/msd_lite", + "udpxy": ROOT.parent / "udpxy/chipmunk/udpxy", + "tvgate": ROOT.parent / "tvgate/TVGate-linux-arm64", + } + for value in args.binary: + name, path = value.split("=", 1) + binaries[name] = Path(path).resolve() + revisions = dict(value.split("=", 1) for value in args.revision) + for name in programs: + if name not in binaries or not os.access(binaries[name], os.X_OK): + parser.error(f"missing executable for {name}; pass --binary {name}=PATH") + args.output = args.output.resolve() + args.output.mkdir(parents=True, exist_ok=False) + metadata = { + "timestamp_utc": time.strftime("%Y-%m-%dT%H:%M:%SZ", time.gmtime()), + "platform": platform.platform(), + "machine": platform.machine(), + "cpu_count": os.cpu_count(), + "server_cpu": args.server_cpu, + "load_cpus": args.load_cpus, + "controller_cpu": args.controller_cpu, + "warmup_s": args.warmup, + "duration_s": args.duration, + "repetitions": args.repetitions, + "cases": {c: CASES[c] for c in args.cases}, + "script_sha256": hashlib.sha256(Path(__file__).read_bytes()).hexdigest(), + "binaries": { + p: { + "path": str(binaries[p]), + "revision": revisions.get(p, "unspecified"), + "sha256": hashlib.sha256(binaries[p].read_bytes()).hexdigest(), + } + for p in programs + }, + "sysctl": subprocess.check_output( + ["sysctl", "net.core.rmem_max", "net.core.wmem_max", "net.ipv4.tcp_congestion_control"], text=True + ).strip(), + } + (args.output / "metadata.json").write_text(json.dumps(metadata, indent=2) + "\n") + rows = [] + with (args.output / "trials.jsonl").open("w") as output: + for case in args.cases: + for repetition in range(args.repetitions): + # Rotate first position; alternate direction to reduce ordering bias. + order = programs[repetition % len(programs) :] + programs[: repetition % len(programs)] + if repetition % 2: + order = order[::-1] + for position, program in enumerate(order): + row = trial(program, case, repetition, position, args, binaries) + rows.append(row) + output.write(json.dumps(row) + "\n") + output.flush() + print(json.dumps(row), flush=True) + time.sleep(1) + summary = [] + for case in args.cases: + for program in programs: + measured = [r for r in rows if r["case"] == case and r["program"] == program] + valid = [r for r in measured if r["valid"]] + item = {"case": case, "program": program, "valid_trials": len(valid), "total_trials": len(measured)} + for field in ("cpu_pct", "pss_mib", "uss_mib"): + values = [r[field] for r in valid] + item[field] = ( + {"mean": statistics.mean(values), "min": min(values), "max": max(values)} if values else None + ) + summary.append(item) + (args.output / "summary.json").write_text(json.dumps(summary, indent=2) + "\n") + return 0 if all(row["valid"] for row in rows) else 1 + + +if __name__ == "__main__": + raise SystemExit(main()) diff --git a/tools/stress-test/test_benchmark.py b/tools/stress-test/test_benchmark.py new file mode 100644 index 00000000..8e7e646e --- /dev/null +++ b/tools/stress-test/test_benchmark.py @@ -0,0 +1,33 @@ +"""Check HTTP framing used by the benchmark's payload validator.""" + +import pytest +from benchmark import HTTPBody + + +@pytest.mark.parametrize("chunked", [False, True]) +@pytest.mark.parametrize("fragment", [1, 3, 31, 65536]) +def test_fragmented_http_body(chunked, fragment): + payload = bytes(range(256)) * 3 + if chunked: + wire = b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n" + for start in range(0, len(payload), 73): + part = payload[start : start + 73] + wire += f"{len(part):x};extension=yes\r\n".encode() + part + b"\r\n" + wire += b"0\r\n\r\n" + else: + wire = b"HTTP/1.0 200 OK\r\nContent-Type: video/mp2t\r\n\r\n" + payload + decoder = HTTPBody() + decoded = b"".join(decoder.feed(wire[i : i + fragment]) for i in range(0, len(wire), fragment)) + assert decoded == payload + assert decoder.headers + assert decoder.finished == chunked + + +def test_rejects_error_response(): + with pytest.raises(ValueError, match="HTTP error"): + HTTPBody().feed(b"HTTP/1.1 503 Unavailable\r\n\r\nerror") + + +def test_rejects_invalid_chunk_terminator(): + with pytest.raises(ValueError, match="chunk terminator"): + HTTPBody().feed(b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n1\r\nx!!") From 85261b03581496c5ab43702bb9500b127e932027 Mon Sep 17 00:00:00 2001 From: Stackie Jia Date: Sun, 6 Sep 2026 18:47:33 +0800 Subject: [PATCH 07/19] docs(perf): refresh comparative benchmarks and explain multicast optimizations --- .github/workflows/lint.yaml | 1 + docs/en/reference/benchmark.md | 190 ++++++++++------ docs/en/reference/configuration.md | 2 + docs/reference/benchmark.md | 190 ++++++++++------ docs/reference/configuration.md | 2 + src/buffer_pool.h | 6 +- src/fcc.c | 4 +- src/fcc.h | 2 +- src/http_proxy.c | 4 +- src/rtsp.c | 4 +- src/send_queue.c | 4 +- tools/stress-test/README.md | 6 +- .../stress-test/results/2026-09-06/README.md | 15 ++ .../2026-09-06/additional/metadata.json | 41 ++++ .../2026-09-06/additional/summary.json | 206 ++++++++++++++++++ .../2026-09-06/additional/trials.jsonl | 36 +++ .../results/2026-09-06/build-environment.json | 18 ++ .../results/2026-09-06/shared64/metadata.json | 44 ++++ .../results/2026-09-06/shared64/summary.json | 95 ++++++++ .../results/2026-09-06/shared64/trials.jsonl | 25 +++ tools/stress-test/stress_test.py | 2 +- 21 files changed, 738 insertions(+), 159 deletions(-) create mode 100644 tools/stress-test/results/2026-09-06/README.md create mode 100644 tools/stress-test/results/2026-09-06/additional/metadata.json create mode 100644 tools/stress-test/results/2026-09-06/additional/summary.json create mode 100644 tools/stress-test/results/2026-09-06/additional/trials.jsonl create mode 100644 tools/stress-test/results/2026-09-06/build-environment.json create mode 100644 tools/stress-test/results/2026-09-06/shared64/metadata.json create mode 100644 tools/stress-test/results/2026-09-06/shared64/summary.json create mode 100644 tools/stress-test/results/2026-09-06/shared64/trials.jsonl diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 7706c725..9175749f 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -28,3 +28,4 @@ jobs: - run: pnpm run type-check - run: pnpm run lint - run: pnpm run web-ui:test + - run: uv run pytest tools/stress-test/test_benchmark.py -q diff --git a/docs/en/reference/benchmark.md b/docs/en/reference/benchmark.md index ca5d856d..3b34cdcb 100644 --- a/docs/en/reference/benchmark.md +++ b/docs/en/reference/benchmark.md @@ -1,106 +1,152 @@ # Performance Benchmark -Performance comparison of **rtp2httpd**, **[msd_lite](https://github.com/rozhuk-im/msd_lite)**, **[udpxy](https://github.com/pcherenkov/udpxy)**, and **[tvgate](https://github.com/qist/tvgate)** — four multicast-to-unicast conversion programs. +This report compares CPU usage, memory consumption, and output integrity for **rtp2httpd**, **[msd_lite](https://github.com/rozhuk-im/msd_lite)**, **[udpxy](https://github.com/pcherenkov/udpxy)**, and **[TVGate](https://github.com/qist/tvgate)** under the same multicast workload. This update adds 64 clients watching one channel, alongside the multi-channel, eight-client shared-channel, and high-bitrate tests. -## Test Environment +## Environment and Versions -- **Platform**: Ubuntu 24.04 on Apple M3 Max (Parallels Desktop virtual machine) -- **Architecture**: aarch64 (all programs compiled natively as arm64 binaries) -- **Kernel**: Linux 6.8.0-90-generic -- **Test duration**: 10 seconds per test -- **Measurement methodology**: - - CPU: Sampled using `top -b -n 2` - - Memory: USS (Unique Set Size) read from `/proc/[pid]/smaps_rollup` - - For processes with forked children, CPU and memory are summed across all parent and child processes -- **Tested versions**: - - rtp2httpd: v3.8.3 - - msd_lite: commit 79a6c62 (2025-05-02) - - udpxy: commit 56fc563 (2026-01-26) - - tvgate: v2.1.8 +- Test date: 2026-09-06. +- Host: Apple M3 Max; Parallels Ubuntu 24.04 virtual machine with 16 vCPUs and 16 GiB RAM. +- System: Linux 6.8.0-138-generic, aarch64; all programs execute natively as ARM64 binaries. +- Compiler: GCC 13.3.0. rtp2httpd uses Release with `ENABLE_AGGRESSIVE_OPT=ON`; msd_lite uses `-O3`, LTO, and equivalent inlining, loop-unrolling, and vectorization options; udpxy uses `-O3 -flto`. TVGate uses the official release binary. +- Multicast input and HTTP output both use `lo`, with no network sysctl changes. `net.core.rmem_max` and `net.core.wmem_max` are both 212992; TCP congestion control is cubic. -## Test Scenarios +| Program | Tested version | +| --- | --- | +| rtp2httpd (optimized) | [`530dc980`](https://github.com/stackia/rtp2httpd/commit/530dc980e92db6b6ea98b6ca223dffe1a0345b5c) | +| msd_lite | [`fa68e131`](https://github.com/rozhuk-im/msd_lite/commit/fa68e131343fb58c67ad77b2d26f2cb7c49a2c95), 2026-07-20; liblcb `e2f420a2` | +| udpxy | [`31d4bcfa`](https://github.com/pcherenkov/udpxy/commit/31d4bcfabaade59d3efdee015df7979febf76bae), 2026-04-13 | +| TVGate | [v3.2.0](https://github.com/qist/tvgate/releases/tag/v3.2.0), 2026-09-06 | +| rtp2httpd (pre-optimization baseline) | [`f8c243cb`](https://github.com/stackia/rtp2httpd/commit/f8c243cb6fc98e259fd2f2fe0dc992f2a845014e), used only for the 64-client comparison | -| Test | Description | -| --------------------- | ----------------------------------------------------------------------------------------- | -| **Multi-stream test** | 8 clients, each requesting different multicast addresses, ~40 Mbps per stream (simulating 4K IPTV bitrate) | -| **Single-stream test** | 8 clients, all requesting the same multicast address, ~40 Mbps per stream | -| **High-bandwidth test** | 1 client, single stream ~400 Mbps | +msd_lite and udpxy use the latest upstream commits retrieved for this test; TVGate uses the latest stable release available at the time. The TVGate ARM64 release archive has SHA-256 `1655a066b91debdaf2f3b39096207f80fdfc7bd9c2f227ff9dda34c48562ac3e`. Executable SHA-256 values, full commands, environment settings, and individual trials are stored in the [raw results directory](https://github.com/stackia/rtp2httpd/tree/main/tools/stress-test/results/2026-09-06). -## Test Results Summary +## Methodology -### CPU Usage (%) +All processes and threads of each server are pinned to one vCPU. rtp2httpd explicitly uses `-C -w 1`; msd_lite uses one event-loop thread; TVGate uses `GOMAXPROCS=1`; udpxy retains its native process-per-client model, with all child processes included. This compares programs under the same single-core budget, rather than assuming that each has only one process or thread. -| Test Scenario | rtp2httpd | msd_lite | udpxy | tvgate | -| -------------------------- | ------------- | -------- | ------- | ------- | -| Multi-stream (8 different addresses) | 🏆 **17.00%** | 25.80% | 106.00% | 331.00% | -| Single-stream (8 same address) | 🏆 **14.00%** | 14.20% | 85.00% | 51.45% | -| High-bandwidth (400 Mbps) | 🏆 **26.73%** | 39.50% | 30.85% | 89.53% | +CPU usage is the change in user and system CPU time from `/proc/PID/stat` over the entire measurement window, divided by actual wall time and summed across the server process tree. **100% means one fully occupied vCPU**. Generators, readers, and the controller run on separate vCPUs; their CPU usage is recorded separately and excluded from server CPU. PSS and USS are sampled from `smaps_rollup` once per second and summed across the process tree. PSS includes proportional shared pages; USS counts private pages. Neither includes all kernel socket memory or unmapped anonymous-file cache pages, so these metrics do not represent the service’s total memory cost. -### Memory Usage (MB) +Each RTP datagram carries seven 188-byte MPEG-TS null packets, totaling 1316 payload bytes. Every TS packet contains an increasing sequence marker, its complement, a source identifier, and fixed content for validation. Readers decode HTTP chunk framing before checking every packet's content and continuity. Each client and generator must sustain a mean payload rate within ±2% of the target, with no sequence gaps, duplicates, backward markers, content errors, client EOFs, or kernel UDP drops during the measurement window. Failed samples remain in the raw output and are explicitly marked invalid. -| Test Scenario | rtp2httpd | msd_lite | udpxy | tvgate | -| -------------------------- | ----------- | ----------- | ----- | ------ | -| Multi-stream (8 different addresses) | 🏆 **4.50** | 10.25 | 12.53 | 182.00 | -| Single-stream (8 same address) | 4.88 | 🏆 **2.62** | 12.53 | 33.25 | -| High-bandwidth (400 Mbps) | 3.88 | 🏆 **2.62** | 3.21 | 47.38 | +Trials run sequentially, changing program order across repetitions and restarting both server and load processes. Warmup starts after all clients receive data. msd_lite retains the upstream example's 48 KiB receive watermark, 64 KiB send watermark, and 1 MiB ring; only the listener, interface, thread count, logging, and congestion control are adapted. udpxy retains its default buffer settings. TVGate uses loopback upstream interfaces and a connection limit of 256. -## Detailed Test Results +| Scenario | Clients | Multicast sources | Payload rate per source | Repetitions | Warmup / sampling per trial | +| --- | ---: | ---: | ---: | ---: | --- | +| 64 clients, one channel | 64 | 1 | 20 Mbps | 5 | 5 s / 20 s | +| Multiple channels | 8 | 8 | 40 Mbps | 3 | 5 s / 10 s | +| 8 clients, one channel | 8 | 1 | 40 Mbps | 3 | 5 s / 10 s | +| High bitrate | 1 | 1 | 400 Mbps | 3 | 5 s / 10 s | -### Test 1: Multi-stream Scenario (8 clients, different addresses, ~40 Mbps each) +## Results -Each client requests a different multicast address (239.81.0.1-8), testing the server's ability to handle multiple independent streams. +### 64 Clients Watching One Channel -| Metric | rtp2httpd | msd_lite | udpxy | tvgate | -| -------- | -------------- | -------- | -------- | --------- | -| CPU Avg | 🏆 **17.00%** | 25.80% | 106.00% | 331.00% | -| CPU Peak | 🏆 **18.00%** | 30.00% | 116.00% | 332.00% | -| Mem Avg | 🏆 **4.50 MB** | 10.25 MB | 12.53 MB | 182.00 MB | +CPU values are means of valid samples, followed by the minimum and maximum across trials. Memory values are means of valid samples; trials failing integrity checks are excluded. -### Test 2: Single-stream Scenario (8 clients, same address, ~40 Mbps) +| Program | Mean CPU (range) | PSS (MiB) | USS (MiB) | Valid / total trials | Multicast sockets | +| --- | --- | ---: | ---: | ---: | ---: | +| rtp2httpd | 6.97% (6.33–7.68) | 4.61 | 3.99 | 5/5 | 1 | +| msd_lite | 5.91% (5.59–6.48) | 1.37 | 1.36 | 5/5 | 1 | +| udpxy | 56.75% (54.80–58.70) | 4.61 | 4.02 | 2/5 | 64 | +| TVGate | — | — | — | 0/5 | 1 | +| rtp2httpd (baseline) | 30.10% (29.07–31.37) | 10.28 | 9.60 | 5/5 | 64 | -All 8 clients request the same multicast address, testing the server's multicast reuse efficiency. +Both the optimized version and baseline passed all five trials. Each optimized-version client received 19.985–20.023 Mbps of payload, totaling approximately 1.28 Gbps. All five trials had no sequence gaps, backward markers, duplicates, content errors, EOFs, or kernel UDP drops. -| Metric | rtp2httpd | msd_lite | udpxy | tvgate | -| -------- | ------------- | -------------- | -------- | -------- | -| CPU Avg | 🏆 **14.00%** | 14.20% | 85.00% | 51.45% | -| CPU Peak | 18.00% | 🏆 **15.00%** | 108.00% | 52.90% | -| Mem Avg | 4.88 MB | 🏆 **2.62 MB** | 12.53 MB | 33.25 MB | +3 udpxy trials recorded kernel UDP drops; the table includes only the other 2 trials. All five TVGate trials failed sequence-continuity checks. Their observed CPU usage was 46.93–53.37%, excluded from valid forwarding comparisons. -### Test 3: High-bandwidth Scenario (1 client, ~400 Mbps) +### Additional Scenarios -Single client receiving a high-bandwidth stream (50x speed playback ≈ 400 Mbps). +Cells show “mean CPU of valid samples; valid / total trials.” -| Metric | rtp2httpd | msd_lite | udpxy | tvgate | -| -------- | ------------- | -------------- | ------- | -------- | -| CPU Avg | 🏆 **26.73%** | 39.50% | 30.85% | 89.53% | -| CPU Peak | 🏆 **29.40%** | 40.00% | 46.00% | 96.00% | -| Mem Avg | 3.88 MB | 🏆 **2.62 MB** | 3.21 MB | 47.38 MB | +| Scenario | rtp2httpd | msd_lite | udpxy | TVGate | +| --- | --- | --- | --- | --- | +| 8 channels, 40 Mbps each | 9.78%; 3/3 | 9.49%; 3/3 | 20.81%; 3/3 | —; 0/3 | +| 8 clients, one 40 Mbps channel | 7.21%; 3/3 | 6.07%; 3/3 | 26.93%; 3/3 | —; 0/3 | +| 1 client, 400 Mbps | 14.79%; 2/3 | 13.76%; 1/3 | —; 0/3 | —; 0/3 | -## Conclusions +Memory values below are “PSS / USS” in MiB, using the same valid samples. -**rtp2httpd** demonstrates excellent overall performance in the benchmark tests: +| Scenario | rtp2httpd | msd_lite | udpxy | TVGate | +| --- | --- | --- | --- | --- | +| 8 channels, 40 Mbps each | 2.28 / 1.66 | 8.95 / 8.94 | 0.80 / 0.53 | — / — | +| 8 clients, one 40 Mbps channel | 1.60 / 0.98 | 1.35 / 1.34 | 0.79 / 0.52 | — / — | +| 1 client, 400 Mbps | 1.43 / 0.84 | 1.34 / 1.33 | — / — | — / — | -- **Highest CPU efficiency**: Achieved the lowest CPU usage across all three test scenarios. In the multi-stream scenario, it used only 66% of msd_lite's CPU, 16% of udpxy's, and 5% of tvgate's -- **Outstanding multi-stream processing capability**: When simultaneously handling 8 independent 4K multicast streams, both CPU and memory usage were the lowest, making it ideal for multi-channel IPTV gateway scenarios -- **Stable high-bandwidth performance**: At 400 Mbps high bitrate, CPU usage was only 27%, leaving ample performance headroom -- **Reasonable memory footprint**: Approximately 4 MB memory usage (with all default parameters), stable across all scenarios, suitable for resource-constrained embedded devices +When some trials fail, the mean of the remaining samples does not imply stable forwarding across all trials. Non-TVGate failures in the additional scenarios are listed below; every failed record is retained with the report. -Compared to udpxy's fork-per-client model, rtp2httpd uses a more efficient event-driven architecture, showing significant advantages in high-concurrency scenarios. Compared to msd_lite, rtp2httpd excels in CPU efficiency, especially in multi-stream concurrent scenarios. +| Scenario | Program | Trial | Kernel UDP drops | TS sequence gaps | +| --- | --- | ---: | ---: | ---: | +| 400 Mbps | rtp2httpd | 1 | 37 | 259 | +| 400 Mbps | udpxy | 1 | 29 | 203 | +| 400 Mbps | udpxy | 2 | 133 | 931 | +| 400 Mbps | msd_lite | 2 | 289 | 2023 | +| 400 Mbps | udpxy | 3 | 97 | 679 | +| 400 Mbps | msd_lite | 3 | 273 | 1911 | -## Running the Benchmark +### Output Integrity -See [tools/stress-test/README.md](https://github.com/stackia/rtp2httpd/blob/main/tools/stress-test/README.md) for stress test tooling and methodology. +TVGate receives the same RTP input through the `/udp/` endpoint recommended in its [official documentation](https://github.com/qist/tvgate/blob/main/doc/MULTICAST.md). TS content checks pass, but increasing markers jump backward and forward; a normal mean bitrate does not establish continuity. A separate single-client capture also showed sequences such as `0…6 → 0…6 → 14…20`. This report establishes only that this version failed continuity checks for this synthetic forwarding workload, without extrapolating to other video sources or versions. -Single stress test: +For a complete measurement record, the table below includes observed TVGate CPU usage across all trials. Every sample failed continuity checks and cannot be treated as a valid forwarding performance result. -```bash -uv run python tools/stress-test/stress_test.py --program rtp2httpd --duration 10 --clients 8 --speed 5 -``` +| Scenario | Observed mean CPU (range) | +| --- | --- | +| 64 clients, one 20 Mbps channel | 49.81% (46.93–53.37) | +| 8 channels, 40 Mbps each | 30.91% (30.27–31.38) | +| 8 clients, one 40 Mbps channel | 22.33% (21.83–22.74) | +| 1 client, 400 Mbps | 27.92% (25.64–30.10) | + +For 64 clients watching one channel, both rtp2httpd and msd_lite passed every trial, with msd_lite using less CPU. Every program had invalid samples at 400 Mbps, so these results cannot establish a stable-forwarding performance ranking. CPU cost, buffering behavior, and output integrity must be considered together. + +## Performance Optimizations in rtp2httpd + +### Shared Multicast Subscriptions Within Each Worker + +Each worker maintains a shared-source registry keyed by the resolved multicast address, port, SSM source address, effective upstream interface, and FEC port. Channel names, `/rtp/` versus `/udp/` spelling, and FCC server parameters do not participate in matching. Requests for the same resource create one main multicast socket and, when configured, one FEC socket. + +Each source owns its lifecycle, timeout, and rejoin timers. Clients hold subscription references; releasing the last reference closes the sockets and destroys source state. When the first client leaves, event dispatch is reassigned to a surviving subscriber. Workers continue to maintain their source registries independently. + +This primarily reduces duplicate local socket receives, system calls, and application processing. Multiple local sockets joining one multicast group do not necessarily cause the upstream link to carry the same number of complete streams. This benchmark does not claim network-side bandwidth savings. + +### Shared Parsing, Reordering, and Batch Payloads + +For ordinary multicast, the shared source parses and reorders RTP once, then combines payloads into batches with a capacity of 64 KiB. Each RTP payload remains intact: the current batch is flushed before the next payload would exceed capacity. The 1316-byte payloads used here produce batches of 49 packets, or 64484 bytes. This reduces both repeated per-client parsing and per-packet fanout and send calls. Partial batches flush at the next worker timer check after reaching 100 ms of age. The timer runs every 100 ms; scheduling also affects actual latency. + +The Buffer layer adds an on-demand 64 KiB batch pool alongside the existing 1536-byte packet pool and control pool. The worker owns the batch pool, so queued data can outlive its multicast source. It initially allocates four batches and grows in increments of four. Its maximum capacity is derived from a `buffer-pool-max-size × 1536` byte budget, with room for at least four batches. This limit applies to the batch pool separately from the original packet pool. If the batch pool is exhausted, forwarding can continue through small-packet references. + +Clients share the underlying payload while each owns a separate `buffer_ref_t` view. Its `owner` points to the same immutable data; list links, send offsets, and remaining lengths stay independent. A partial send updates only that client's view. The backing memory returns to the pool only after the last view is released. Each client retains its own send queue and packet-drop policy, so a slow client does not pause reception for other subscribers. -Full benchmark suite: +Queue limits now charge the backing buffer capacity instead of assuming “buffer count × 1536.” A batch with only a few unsent bytes still consumes the full 64 KiB allowance until that client releases its reference. This prevents shared large buffers from bypassing the existing slow-client memory limits. + +### Immutable Batch Snapshots + +In this Linux test, complete shared batches also use anonymous memory files created with `memfd_create`. Each file is written once, sealed, and sent to multiple clients through `sendfile`, reusing the same kernel pages. This path applies only when multiple clients share a nearly full batch. Ordinary memory buffers continue to use `sendmsg`. + +Every batch gets a new file. Once published, it cannot be written, grown, or truncated; reusing pool memory never overwrites an old file. TCP may still reference its pages after `sendfile` returns and the application closes its last file reference. Immutability ensures that a new batch cannot alter those pending bytes. File creation, writing, or sealing failures retain memory sending. If a client's `sendfile` operation is unsupported, only that client's view falls back to memory sending. + +### FCC, FEC, and Client Isolation + +FCC unicast and switching state remain independent per client. The handoff first shares the multicast socket. After unicast and pending data have drained and the reorder sequence aligns with the shared source, the client joins shared batch delivery. The previous batch is flushed before the switch so the new subscriber does not replay older content. + +Snapshots retain independent processing state. Sources configured with an FEC port share sockets but retain per-client reordering and FEC recovery. If in-band FEC first appears during a stream, the source flushes its existing batch and transfers the shared reorder window to each client before switching to private processing. The ordinary-multicast CPU measurements in this report therefore do not directly represent FCC unicast, FEC recovery, or snapshot workloads. + +## Scope + +This is a fixed-bitrate forwarding test inside an ARM64 Linux virtual machine. It does not measure physical-NIC throughput limits, video decoding, or maximum client capacity. Loopback kernel work charged to generators and readers is outside the server CPU metric, and host scheduling introduces variation. Other hardware, bitrates, client speeds, channel counts, and network paths require separate measurements. + +## Reproducing the Tests + +See [tools/stress-test/README.md](https://github.com/stackia/rtp2httpd/blob/main/tools/stress-test/README.md) for the harness and options. Prepare the corresponding binaries, then run: ```bash -scripts/benchmark.sh +# Four projects, 64 clients watching one channel, five repetitions. +scripts/benchmark.sh rtp2httpd msd_lite udpxy tvgate \ + --cases shared64 --repetitions 5 --warmup 5 --duration 20 + +# Three additional scenarios, three repetitions. +scripts/benchmark.sh rtp2httpd msd_lite udpxy tvgate \ + --cases distinct8 shared8 high400 --repetitions 3 --warmup 5 --duration 10 ``` -Test results are saved to `tools/stress-test/benchmark_results_YYYYMMDD_HHMMSS.txt`. +Use `--binary NAME=PATH` and `--revision NAME=VERSION` to identify the actual executables and versions. For the pre/post comparison, also pass the `baseline` program name and `--binary baseline=PATH`. The default output directory is under `build/benchmark/` and contains the environment, executable hashes, individual trials, summary, logs, and generated configs. Runs containing invalid samples exit with a nonzero status. diff --git a/docs/en/reference/configuration.md b/docs/en/reference/configuration.md index e9ccff35..cfb8c450 100644 --- a/docs/en/reference/configuration.md +++ b/docs/en/reference/configuration.md @@ -20,6 +20,8 @@ rtp2httpd [options] - `-m, --maxclients ` - Maximum concurrent clients (default: 5) - `-w, --workers ` - Number of worker processes (default: 1) +Requests for the same multicast source automatically share a subscription within each worker, without additional configuration. See the [Performance Benchmark](/en/reference/benchmark#performance-optimizations-in-rtp2httpd) for implementation details and test results. + `--listen` can be specified multiple times to listen on multiple TCP addresses/ports or Unix sockets: ```bash diff --git a/docs/reference/benchmark.md b/docs/reference/benchmark.md index a7057cd6..31a8c2f1 100644 --- a/docs/reference/benchmark.md +++ b/docs/reference/benchmark.md @@ -1,106 +1,152 @@ # 性能测试报告 -**rtp2httpd**、**[msd_lite](https://github.com/rozhuk-im/msd_lite)**、**[udpxy](https://github.com/pcherenkov/udpxy)** 和 **[tvgate](https://github.com/qist/tvgate)** 四款组播转单播程序性能对比。 +比较 **rtp2httpd**、**[msd_lite](https://github.com/rozhuk-im/msd_lite)**、**[udpxy](https://github.com/pcherenkov/udpxy)** 和 **[TVGate](https://github.com/qist/tvgate)** 在相同组播负载下的 CPU、内存占用及输出完整性。本次重点增加了 64 个客户端观看同一频道的场景,并保留多频道、8 客户端同频道和高码率测试。 -## 测试环境 +## 测试环境与版本 -- **平台**: Ubuntu 24.04 on Apple M3 Max (Parallels Desktop 虚拟机) -- **架构**: aarch64 (所有程序均为本机编译的原生 arm64 二进制文件) -- **内核**: Linux 6.8.0-90-generic -- **单项测试时长**: 10 秒 -- **测量方法**: - - CPU: 使用 `top -b -n 2` 进行采样 - - 内存: 从 `/proc/[pid]/smaps_rollup` 读取 USS (Unique Set Size) - - 如果存在 fork 后的子进程,所有父子进程的 CPU、内存分别求和作为总结果 -- **测试版本**: - - rtp2httpd: v3.8.3 - - msd_lite: commit 79a6c62 (2025-05-02) - - udpxy: commit 56fc563 (2026-01-26) - - tvgate: v2.1.8 +- 测试日期:2026-09-06。 +- 主机:Apple M3 Max;Parallels Ubuntu 24.04 虚拟机,16 vCPU、16 GiB 内存。 +- 系统:Linux 6.8.0-138-generic,aarch64;所有程序均为原生 ARM64 执行。 +- 编译器:GCC 13.3.0。rtp2httpd 使用 Release 与 `ENABLE_AGGRESSIVE_OPT=ON`;msd_lite 使用 `-O3`、LTO 及相同的内联、循环展开、向量化优化选项;udpxy 使用 `-O3 -flto`。TVGate 使用官方发布的二进制文件。 +- 组播输入及 HTTP 输出均经过 `lo`,不修改内核网络参数。`net.core.rmem_max` 和 `net.core.wmem_max` 均为 212992,TCP 拥塞控制为 cubic。 -## 测试场景 +| 程序 | 测试版本 | +| --- | --- | +| rtp2httpd(优化后) | [`530dc980`](https://github.com/stackia/rtp2httpd/commit/530dc980e92db6b6ea98b6ca223dffe1a0345b5c) | +| msd_lite | [`fa68e131`](https://github.com/rozhuk-im/msd_lite/commit/fa68e131343fb58c67ad77b2d26f2cb7c49a2c95),2026-07-20;liblcb `e2f420a2` | +| udpxy | [`31d4bcfa`](https://github.com/pcherenkov/udpxy/commit/31d4bcfabaade59d3efdee015df7979febf76bae),2026-04-13 | +| TVGate | [v3.2.0](https://github.com/qist/tvgate/releases/tag/v3.2.0),2026-09-06 | +| rtp2httpd(优化前基线) | [`f8c243cb`](https://github.com/stackia/rtp2httpd/commit/f8c243cb6fc98e259fd2f2fe0dc992f2a845014e),仅用于 64 客户端对照 | -| 测试 | 描述 | -| -------------- | ----------------------------------------------------------------------- | -| **多流测试** | 8 个客户端,每个请求不同的组播地址,单流约 40 Mbps(模拟 4K IPTV 码率) | -| **单流测试** | 8 个客户端,全部请求相同的组播地址,单流约 40 Mbps | -| **高带宽测试** | 1 个客户端,单流约 400 Mbps | +msd_lite、udpxy 为测试时取得的最新上游提交;TVGate 为当时最新正式版本。TVGate ARM64 发布压缩包的 SHA-256 为 `1655a066b91debdaf2f3b39096207f80fdfc7bd9c2f227ff9dda34c48562ac3e`。可执行文件的 SHA-256、完整命令、环境参数和逐轮结果保存在[原始数据目录](https://github.com/stackia/rtp2httpd/tree/main/tools/stress-test/results/2026-09-06)。 -## 测试结果汇总 +## 测量方法 -### CPU 使用率 (%) +所有被测服务的进程及线程固定在同一个 vCPU。rtp2httpd 明确设置 `-C -w 1`,msd_lite 配置一个事件循环线程;TVGate 设置 `GOMAXPROCS=1`;udpxy 保留每个客户端一个子进程的原生模型,所有子进程均计入统计。因此,这是相同单核预算下的对比,并非所有程序都只有一个线程或进程。 -| 测试场景 | rtp2httpd | msd_lite | udpxy | tvgate | -| ------------------ | ------------- | -------- | ------- | ------- | -| 多流 (8个不同地址) | 🏆 **17.00%** | 25.80% | 106.00% | 331.00% | -| 单流 (8个相同地址) | 🏆 **14.00%** | 14.20% | 85.00% | 51.45% | -| 高带宽 (400 Mbps) | 🏆 **26.73%** | 39.50% | 30.85% | 89.53% | +CPU 使用率取完整测量窗口内 `/proc/PID/stat` 的用户态与内核态时间增量,除以实际墙钟时间,再对整个服务进程树求和。**100% 表示占满一个 vCPU**。发送器、接收器及测量控制进程使用其他独立 vCPU,其 CPU 另行记录,不计入服务 CPU。PSS 和 USS 从 `smaps_rollup` 每秒采样并对进程树求和;PSS 按比例计入共享页,USS 仅统计私有页,均不包含全部内核 socket 内存或未映射的匿名文件页缓存,因此不能作为服务总内存成本。 -### 内存使用 (MB) +每个 RTP 数据报携带 7 个 188 字节 MPEG-TS 空包,共 1316 字节负载。每个 TS 包包含递增序号、序号反码、源标识及固定校验内容。接收端先解析 HTTP 分块编码,再逐包验证内容和连续性。各客户端及发送器的平均负载码率必须在目标值 ±2% 内,测量窗口内不得出现序号缺口、重复、回退、内容错误、客户端断流或内核 UDP 丢包。未通过的样本仍保留在原始数据中,并明确标记为无效。 -| 测试场景 | rtp2httpd | msd_lite | udpxy | tvgate | -| ------------------ | ----------- | ----------- | ----- | ------ | -| 多流 (8个不同地址) | 🏆 **4.50** | 10.25 | 12.53 | 182.00 | -| 单流 (8个相同地址) | 4.88 | 🏆 **2.62** | 12.53 | 33.25 | -| 高带宽 (400 Mbps) | 3.88 | 🏆 **2.62** | 3.21 | 47.38 | +测试逐项串行执行,每轮改变程序顺序,重启服务与负载进程;所有客户端开始收到数据后再预热。msd_lite 保留上游示例的 48 KiB 接收水位、64 KiB 发送水位、1 MiB 环形缓冲;仅适配监听地址、接口、线程数、日志和拥塞控制。udpxy 保留默认缓冲设置,TVGate 配置 loopback 上游接口和 256 连接上限。 -## 详细测试结果 +| 场景 | 客户端数 | 组播源数 | 单源负载码率 | 重复次数 | 每轮预热 / 采样 | +| --- | ---: | ---: | ---: | ---: | --- | +| 同频道 64 客户端 | 64 | 1 | 20 Mbps | 5 | 5 s / 20 s | +| 多频道 | 8 | 8 | 40 Mbps | 3 | 5 s / 10 s | +| 同频道 8 客户端 | 8 | 1 | 40 Mbps | 3 | 5 s / 10 s | +| 高码率 | 1 | 1 | 400 Mbps | 3 | 5 s / 10 s | -### 测试一:多流场景 (8个客户端,不同地址,约 40 Mbps) +## 测试结果 -每个客户端请求不同的组播地址 (239.81.0.1-8),测试服务器处理多个独立流的能力。 +### 同频道 64 客户端 -| 指标 | rtp2httpd | msd_lite | udpxy | tvgate | -| -------- | -------------- | -------- | -------- | --------- | -| CPU 平均 | 🏆 **17.00%** | 25.80% | 106.00% | 331.00% | -| CPU 峰值 | 🏆 **18.00%** | 30.00% | 116.00% | 332.00% | -| 内存平均 | 🏆 **4.50 MB** | 10.25 MB | 12.53 MB | 182.00 MB | +CPU 列为有效样本平均值,括号内为逐轮最小值至最大值。内存为有效样本平均值;未通过完整性检查的样本不参与均值。 -### 测试二:单流场景 (8个客户端,相同地址,约 40 Mbps) +| 程序 | CPU 平均(范围) | PSS (MiB) | USS (MiB) | 有效 / 总轮数 | 组播 socket 数 | +| --- | --- | ---: | ---: | ---: | ---: | +| rtp2httpd | 6.97% (6.33–7.68) | 4.61 | 3.99 | 5/5 | 1 | +| msd_lite | 5.91% (5.59–6.48) | 1.37 | 1.36 | 5/5 | 1 | +| udpxy | 56.75% (54.80–58.70) | 4.61 | 4.02 | 2/5 | 64 | +| TVGate | — | — | — | 0/5 | 1 | +| rtp2httpd(优化前) | 30.10% (29.07–31.37) | 10.28 | 9.60 | 5/5 | 64 | -8个客户端全部请求相同的组播地址,测试服务器的组播复用效率。 +优化版及优化前基线均为 5/5 有效。优化版每客户端实际负载码率为 19.985–20.023 Mbps,总输出约 1.28 Gbps;五轮均无序号缺口、回退、重复、内容错误、断流或内核 UDP 丢包。 -| 指标 | rtp2httpd | msd_lite | udpxy | tvgate | -| -------- | ------------- | -------------- | -------- | -------- | -| CPU 平均 | 🏆 **14.00%** | 14.20% | 85.00% | 51.45% | -| CPU 峰值 | 18.00% | 🏆 **15.00%** | 108.00% | 52.90% | -| 内存平均 | 4.88 MB | 🏆 **2.62 MB** | 12.53 MB | 33.25 MB | +udpxy 的 3 轮样本出现内核 UDP 丢包,表中只统计其余 2 轮。TVGate 的 5 轮均未通过序号连续性检查,观察到的 CPU 为 46.93–53.37%,不作为有效转发结果参与比较。 -### 测试三:高带宽场景 (1个客户端,约 400 Mbps) +### 其余场景 -单客户端接收高带宽流 (50倍速回放 ≈ 400 Mbps)。 +单元格格式为「有效样本平均 CPU;有效 / 总轮数」。 -| 指标 | rtp2httpd | msd_lite | udpxy | tvgate | -| -------- | ------------- | -------------- | ------- | -------- | -| CPU 平均 | 🏆 **26.73%** | 39.50% | 30.85% | 89.53% | -| CPU 峰值 | 🏆 **29.40%** | 40.00% | 46.00% | 96.00% | -| 内存平均 | 3.88 MB | 🏆 **2.62 MB** | 3.21 MB | 47.38 MB | +| 场景 | rtp2httpd | msd_lite | udpxy | TVGate | +| --- | --- | --- | --- | --- | +| 8 频道,各 40 Mbps | 9.78%; 3/3 | 9.49%; 3/3 | 20.81%; 3/3 | —; 0/3 | +| 8 客户端同频道,40 Mbps | 7.21%; 3/3 | 6.07%; 3/3 | 26.93%; 3/3 | —; 0/3 | +| 单客户端,400 Mbps | 14.79%; 2/3 | 13.76%; 1/3 | —; 0/3 | —; 0/3 | -## 测试结论 +以下内存值为「PSS / USS」,单位 MiB,统计相同的有效样本。 -**rtp2httpd** 在基准测试中展现出优异的综合性能: +| 场景 | rtp2httpd | msd_lite | udpxy | TVGate | +| --- | --- | --- | --- | --- | +| 8 频道,各 40 Mbps | 2.28 / 1.66 | 8.95 / 8.94 | 0.80 / 0.53 | — / — | +| 8 客户端同频道,40 Mbps | 1.60 / 0.98 | 1.35 / 1.34 | 0.79 / 0.52 | — / — | +| 单客户端,400 Mbps | 1.43 / 0.84 | 1.34 / 1.33 | — / — | — / — | -- **CPU 效率最高**:在所有三个测试场景中均取得最低 CPU 使用率,多流场景下仅为 msd_lite 的 66%、udpxy 的 16%、tvgate 的 5% -- **多流处理能力突出**:同时处理 8 个独立 4K 组播流时,CPU 和内存占用均为最低,适合多频道 IPTV 网关场景 -- **高带宽场景表现稳定**:400 Mbps 高码率下 CPU 占用仅 27%,留有充足的性能余量 -- **内存占用合理**:约 4 MB 的内存占用(全默认参数下),在各场景下保持稳定,适合资源受限的嵌入式设备 +有效轮数不足时,不能把少量通过样本的均值理解为全部轮次均能稳定转发。附加场景中未通过的非 TVGate 样本如下;所有失败记录均随报告保留。 -相比 udpxy 的 fork-per-client 模型,rtp2httpd 采用更高效的事件驱动架构,在高并发场景下优势明显。相比 msd_lite,rtp2httpd 在 CPU 效率上更胜一筹,尤其在多流并发场景下差距显著。 +| 场景 | 程序 | 轮次 | 内核 UDP 丢包 | TS 序号缺口 | +| --- | --- | ---: | ---: | ---: | +| 400 Mbps | rtp2httpd | 1 | 37 | 259 | +| 400 Mbps | udpxy | 1 | 29 | 203 | +| 400 Mbps | udpxy | 2 | 133 | 931 | +| 400 Mbps | msd_lite | 2 | 289 | 2023 | +| 400 Mbps | udpxy | 3 | 97 | 679 | +| 400 Mbps | msd_lite | 3 | 273 | 1911 | -## 运行基准测试 +### 输出完整性说明 -详见 [tools/stress-test/README.md](https://github.com/stackia/rtp2httpd/blob/main/tools/stress-test/README.md) 了解压力测试工具和方法。 +TVGate 使用其[官方文档](https://github.com/qist/tvgate/blob/main/doc/MULTICAST.md)推荐的 `/udp/` 入口接收同一份 RTP 输入。其输出的 TS 内容校验可以通过,但递增序号出现回退和跳跃;平均码率正常并不代表内容连续。独立单客户端抓取也观察到了类似 `0…6 → 0…6 → 14…20` 的序列。报告仅说明该版本在本次合成转发负载下未通过连续性检查,不推断其他视频源或其他版本的表现。 -单次压力测试: +为保留完整测量记录,以下列出 TVGate 所有轮次的 CPU 观察值。这些样本均未通过连续性检查,不能视为有效转发的性能成绩。 -```bash -uv run python tools/stress-test/stress_test.py --program rtp2httpd --duration 10 --clients 8 --speed 5 -``` +| 场景 | CPU 观察均值(范围) | +| --- | --- | +| 64 客户端同频道,20 Mbps | 49.81% (46.93–53.37) | +| 8 频道,各 40 Mbps | 30.91% (30.27–31.38) | +| 8 客户端同频道,40 Mbps | 22.33% (21.83–22.74) | +| 单客户端,400 Mbps | 27.92% (25.64–30.10) | + +同频道 64 客户端场景中,rtp2httpd 与 msd_lite 均通过全部测量,msd_lite 的 CPU 使用率更低。400 Mbps 场景中各项目均存在无效样本,无法据此给出稳定转发的性能排名。CPU 成本、缓冲策略和完整性结果需要一起比较。 + +## rtp2httpd 的性能优化 + +### 每个 worker 共享组播订阅 + +每个 worker 维护一张共享源表,按照解析后的组播地址、端口、SSM 源地址、有效上游接口及 FEC 端口匹配。频道名称、`/rtp/` 与 `/udp/` 的路径写法、FCC 服务器参数不参与匹配。相同资源只建立一份主组播 socket,以及按需建立的一份 FEC socket。 + +订阅源拥有独立的生命周期、超时和重加组定时状态。每个客户端持有订阅引用,最后一个引用释放时关闭 socket 并销毁源状态。最早接入的客户端离开时,事件分发会转交给仍存活的订阅者。各 worker 仍独立管理自己的源表。 + +这主要减少本机重复 socket 接收、系统调用和应用层处理。多个本机 socket 加入同一个组播组,并不等于上游链路一定传输了相同数量的完整数据流;本次测试不以网络侧带宽节省作为结论。 + +### 共享解析、重排和批次数据 + +普通组播由共享源统一进行 RTP 解析及重排,然后将有效负载合并到容量为 64 KiB 的批次中。每个 RTP 负载保持完整,在下一个负载会超出容量时先发送当前批次;本次 1316 字节负载对应每批 49 个包、64484 字节。这样既减少每客户端重复解析,也减少逐包分发和发送调用。未满的批次在满 100 ms 后的下一次 worker 定时检查中发送;检查周期为 100 ms,实际延迟还受调度影响。 + +Buffer 层在原有 1536 字节包缓冲池和控制缓冲池之外,增加按需分配的 64 KiB 批次池。批次池归 worker 所有,已排队的数据可在组播源销毁后继续存活。默认初始分配 4 个批次,按 4 个扩展,最大容量按 `buffer-pool-max-size × 1536` 字节预算换算,并至少容纳 4 个批次。这是批次池自身的上限,与原有包池分别计量。批次池耗尽时,仍可通过小包引用继续分发。 + +共享的是底层 payload,而每个客户端拥有独立的 `buffer_ref_t` 视图。视图通过 `owner` 指向同一份不可修改的数据,并保留自己的链表指针、发送偏移和剩余长度。客户端的部分发送只修改自己的视图;最后一个视图释放后,底层内存才返回池中。每个客户端仍有自己的发送队列和丢包策略,慢客户端不会暂停其他订阅者的接收。 -完整基准测试: +队列限额改为按底层缓冲容量计费,不能再使用「缓冲数量 × 1536」。一个只剩少量字节未发送的批次,仍占用完整 64 KiB 容量,直到该客户端释放引用。这避免共享大缓冲绕过原有慢客户端内存限制。 + +### 不可修改的批次快照 + +本次 Linux 测试中的完整共享批次,还会通过 `memfd_create` 建立匿名内存文件,写入一次后封存,再由多个客户端通过 `sendfile` 发送同一份内核数据页。只在多个客户端共享接近满容量的批次时采用该路径;普通内存发送仍通过 `sendmsg` 完成。 + +每批创建新的文件,发布后禁止写入、增长和截断,不会在缓冲池复用时覆盖旧文件。即使 `sendfile` 已返回、应用已关闭最后一个文件引用,TCP 仍可能持有旧数据页;文件不可变保证这些待发送内容不会被新一批数据改写。创建、写入或封存失败时保留内存发送;客户端的 `sendfile` 不受支持时,只将该客户端的视图回退到内存发送。 + +### FCC、FEC 与客户端隔离 + +FCC 单播和切换状态按客户端独立维护。衔接时先共享组播 socket;等单播和待处理数据完成、重排序号与共享源对齐,再加入共享批次分发。切换前先发送旧批次,避免新订阅者重放之前的内容。 + +截图处理仍保留独立状态。配置 FEC 端口的源使用共享 socket、每客户端独立的重排及 FEC 恢复路径;运行中首次发现带内 FEC 时,会先发送现有批次,并把共享重排窗口交接给各客户端,再转入独立处理。因而本报告普通组播的 CPU 结果不能直接代表 FCC 补流阶段、FEC 恢复或截图负载。 + +## 适用范围 + +这是 ARM64 Linux 虚拟机内的固定码率转发测试,不是物理网卡吞吐上限、视频解码性能或最大可承载客户端数量测试。loopback 内核工作中记在发送器及读流进程上的部分不属于服务 CPU,主机调度也会引入波动。其他硬件、码率、客户端速度、并发频道数量及网络路径应单独测量。 + +## 复现测试 + +脚本及参数见 [tools/stress-test/README.md](https://github.com/stackia/rtp2httpd/blob/main/tools/stress-test/README.md)。先准备对应版本的二进制文件,再运行: ```bash -scripts/benchmark.sh +# 四个项目,同频道 64 客户端,5 轮。 +scripts/benchmark.sh rtp2httpd msd_lite udpxy tvgate \ + --cases shared64 --repetitions 5 --warmup 5 --duration 20 + +# 其余三个场景,3 轮。 +scripts/benchmark.sh rtp2httpd msd_lite udpxy tvgate \ + --cases distinct8 shared8 high400 --repetitions 3 --warmup 5 --duration 10 ``` -测试结果保存至 `tools/stress-test/benchmark_results_YYYYMMDD_HHMMSS.txt`。 +通过 `--binary 名称=路径` 与 `--revision 名称=版本` 指定实际使用的二进制及版本。优化前后对照额外传入 `baseline` 程序名和 `--binary baseline=路径`。输出目录默认位于 `build/benchmark/`,包含环境、二进制哈希、每轮数据、汇总、日志及生成的配置;存在无效样本时命令以非零状态退出。 diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index 75d0f70e..dbd2ac69 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -20,6 +20,8 @@ rtp2httpd [选项] - `-m, --maxclients <数量>` - 最大并发客户端数 (默认: 5) - `-w, --workers <数量>` - 工作进程数 (默认: 1) +同一工作进程内,同源组播请求会自动共享订阅,无需额外配置。实现细节与测试结果见[性能测试报告](/reference/benchmark#rtp2httpd-的性能优化)。 + `--listen` 可以重复指定,用于同时监听多个 TCP 地址/端口或 Unix socket: ```bash diff --git a/src/buffer_pool.h b/src/buffer_pool.h index abce405a..869dec22 100644 --- a/src/buffer_pool.h +++ b/src/buffer_pool.h @@ -29,7 +29,7 @@ typedef enum { } buffer_type_t; /** - * Buffer reference counting for buffered output lifecycle management + * Reference-counted storage for queued output * Supports both memory buffers (pool-managed) and file descriptors (for * sendfile) * @@ -37,7 +37,7 @@ typedef enum { * 1. When buffer is free: linked via free_next in pool's free list * 2. When buffer is in use: can be queued for sending via send_next * - * The send queue field (iov) are only valid + * The send queue field (iov) is only valid * when the buffer is in a send queue. */ typedef struct buffer_ref_s { @@ -58,7 +58,7 @@ typedef struct buffer_ref_s { /* Union: buffer is either in free list OR in send queue, never both */ union { struct buffer_ref_s *free_next; /* For free list linkage */ - struct buffer_ref_s *send_next; /* For send/pending queue linkage */ + struct buffer_ref_s *send_next; /* For send queue linkage */ }; union { diff --git a/src/fcc.c b/src/fcc.c index 3dd18905..d9bb0525 100644 --- a/src/fcc.c +++ b/src/fcc.c @@ -427,7 +427,7 @@ int fcc_handle_socket_event(stream_context_t *ctx, int fd, int64_t now) { return 0; } - /* Receive directly into buffered output buffer (true buffered output receive) */ + /* Receive directly into a pool buffer for the send queue */ int actualr = recvfrom(recv_sock, recv_buf->data, BUFFER_POOL_BUFFER_SIZE, 0, (struct sockaddr *)&peer_addr, &slen); if (actualr < 0) { buffer_ref_put(recv_buf); @@ -780,7 +780,7 @@ int fcc_handle_mcast_active(stream_context_t *ctx, buffer_ref_t *buf_ref) { logger(LOG_DEBUG, "FCC: Flushed pending buffer chain, total_flushed_bytes=%" PRIu64, flushed_bytes); } - /* Forward multicast data to client (true buffered output) or capture I-frame + /* Forward a multicast buffer reference to the client or capture an I-frame * (snapshot) */ stream_process_rtp_payload(ctx, buf_ref, STREAM_MEDIA_ORIGIN_FCC_MULTICAST); diff --git a/src/fcc.h b/src/fcc.h index a1a983bc..157992a0 100644 --- a/src/fcc.h +++ b/src/fcc.h @@ -68,7 +68,7 @@ typedef struct { uint32_t session_id; /* Session ID for NAT traversal correlation */ uint8_t need_nat_traversal; /* NAT traversal support flag from server */ - /* Multicast pending buffer for smooth transition - buffered output chain */ + /* Multicast pending buffer chain for a smooth transition */ buffer_ref_t *pending_list_head; buffer_ref_t *pending_list_tail; uint16_t mcast_pbuf_last_seqn; diff --git a/src/http_proxy.c b/src/http_proxy.c index 9a68ce63..a034aa97 100644 --- a/src/http_proxy.c +++ b/src/http_proxy.c @@ -894,9 +894,9 @@ static int http_proxy_try_receive_response(http_proxy_session_t *session) { int bytes_forwarded = 0; /* - * Two-phase receive strategy for buffered output optimization: + * Two-phase receive strategy to avoid an extra payload copy: * Phase 1 (AWAITING_HEADERS): Use fixed buffer for header parsing - * Phase 2 (STREAMING): Recv directly to buffer pool for sending + * Phase 2 (STREAMING): Receive directly into the send buffer pool * OR buffer for rewriting if needs_body_rewrite */ diff --git a/src/rtsp.c b/src/rtsp.c index 7a82cf31..f008fa4d 100644 --- a/src/rtsp.c +++ b/src/rtsp.c @@ -2077,7 +2077,7 @@ static int rtsp_process_interleaved_buffer(rtsp_session_t *session, connection_t break; /* Wait for more data */ } - /* Sanity check: bound against the buffered output destination buffer. */ + /* Sanity check: bound against the destination pool buffer. */ if (packet_length > BUFFER_POOL_BUFFER_SIZE) { logger(LOG_ERROR, "RTSP: Received packet too large (%d bytes, max %d), attempting " @@ -2233,7 +2233,7 @@ int rtsp_handle_udp_rtp_data(rtsp_session_t *session, connection_t *conn) { return total_bytes_written; } - /* Receive directly into buffered output buffer (true buffered output receive) */ + /* Receive directly into a pool buffer for the send queue */ int bytes_received = recv(session->rtp_socket, rtp_buf->data, BUFFER_POOL_BUFFER_SIZE, 0); if (bytes_received < 0) { buffer_ref_put(rtp_buf); diff --git a/src/send_queue.c b/src/send_queue.c index 60b4ed8e..94adc851 100644 --- a/src/send_queue.c +++ b/src/send_queue.c @@ -131,7 +131,7 @@ int send_queue_add(send_queue_t *queue, buffer_ref_t *buf_ref) { queue->tail->send_next = buf_ref; queue->tail = buf_ref; } else { - /* First entry - record timestamp for batching timeout */ + /* First queued entry. */ queue->head = queue->tail = buf_ref; } @@ -168,7 +168,7 @@ int send_queue_add_file(send_queue_t *queue, int file_fd, off_t file_offset, siz queue->tail->send_next = buf_ref; queue->tail = buf_ref; } else { - /* First entry - record timestamp for batching timeout */ + /* First queued entry. */ queue->head = queue->tail = buf_ref; } diff --git a/tools/stress-test/README.md b/tools/stress-test/README.md index 580f192f..a7aaac89 100644 --- a/tools/stress-test/README.md +++ b/tools/stress-test/README.md @@ -16,6 +16,8 @@ From the repository root: ```bash # Refresh the project's existing dependencies and build rtp2httpd. uv sync --group dev +pnpm install --frozen-lockfile +pnpm run web-ui:build cmake -B build -DCMAKE_BUILD_TYPE=Release -DENABLE_AGGRESSIVE_OPT=ON cmake --build build -j$(getconf _NPROCESSORS_ONLN) @@ -59,9 +61,9 @@ Use a clean checkout or separate build directory when refreshing competitors; pr - CPU is the change in user + system CPU ticks from `/proc/PID/stat`, divided by measured wall time. **100% means one logical CPU**, not the whole machine. Include the supervisor and every child process; process CPU already includes its threads and must not be summed again by thread. - All server processes/threads inherit the same single-CPU affinity. rtp2httpd uses `-C -w 1`; msd_lite uses one event-loop thread; TVGate uses `GOMAXPROCS=1`; udpxy retains its native process-per-client model. These are single-CPU comparisons, not claims that all programs have one process or thread. - Each generator and reader process has a separate CPU from the server. Their combined CPU is recorded separately. Loopback kernel work charged to the load processes is outside the server metric; this is not total system CPU or a physical-NIC throughput test. -- PSS and USS come from `smaps_rollup`, summed over the process family and sampled once per second. PSS includes proportional shared pages; USS includes private clean/dirty/huge pages. Neither is a count of kernel socket memory. +- PSS and USS come from `smaps_rollup`, summed over the process family and sampled once per second. PSS includes proportional shared pages; USS includes private clean/dirty/huge pages. Neither includes all kernel socket memory or unmapped anonymous-file cache pages. - The sender emits RTP payload type 33 with seven 188-byte TS null packets per datagram. Every TS packet contains a monotonically increasing marker, its complement, a source identifier, and a checked payload pattern. This tests forwarding and integrity, not video decoding. -- Readers decode HTTP chunk framing before checking payloads. Each client must receive within 2% of the target rate; each generator must also maintain that rate. The measured window must contain no gaps, duplicates, corrupt packets, EOFs, or kernel UDP drops. The process family must remain stable and the load processes alive. +- Readers decode HTTP chunk framing before checking payloads. Each client must receive within 2% of the target rate; each generator must also maintain that rate. The measured window must contain no gaps, duplicates, backward markers, corrupt packets, EOFs, or kernel UDP drops. The process family must remain stable and the load processes alive. - Failures remain in the raw output as `valid: false`; the summary averages valid trials only and always reports valid/total counts. A failed or incomplete run exits nonzero. Do not describe its low CPU as a performance win. - msd_lite keeps the upstream example's 48 KiB receive watermark, 64 KiB send watermark, and 1 MiB ring. Only the listener, interface, thread count/affinity, verbosity, and congestion-control name are adapted. udpxy keeps upstream buffer defaults. TVGate uses loopback multicast settings and connection limits of 256. Generated configs and complete commands are saved for review. diff --git a/tools/stress-test/results/2026-09-06/README.md b/tools/stress-test/results/2026-09-06/README.md new file mode 100644 index 00000000..4d3abe8c --- /dev/null +++ b/tools/stress-test/results/2026-09-06/README.md @@ -0,0 +1,15 @@ +# Multicast benchmark data — 2026-09-06 + +The Chinese report is [docs/reference/benchmark.md](../../../../docs/reference/benchmark.md); the English translation is [docs/en/reference/benchmark.md](../../../../docs/en/reference/benchmark.md). + +- `shared64/`: five repetitions of one 20 Mbps channel with 64 clients, including the pre-optimization baseline. Warmup: 5 seconds; sampling: 20 seconds. +- `additional/`: three repetitions each of eight distinct 40 Mbps channels, eight clients on one 40 Mbps channel, and one 400 Mbps channel. Warmup: 5 seconds; sampling: 10 seconds. +- `build-environment.json`: compiler, build settings, vendor source revisions, and TVGate release archive digest. TVGate was run with `GOMAXPROCS=1`. + +Every directory contains the original `trials.jsonl` (including failures), `metadata.json` (including binary and harness SHA-256 values), and `summary.json`. Summary means use **valid samples only**, with valid/total counts. Do not interpret missing values as zero usage, or compare CPU from failed delivery as equivalent work. + +The benchmark harness is `tools/stress-test/benchmark.py` at commit `9d0f59aa95449e4d4ccfa362251bae4d2716c211`. Runtime C sources are from `530dc980e92db6b6ea98b6ca223dffe1a0345b5c`. The Web UI was rebuilt before creating the tested binary. Absolute paths in recorded commands refer to isolated build directories on the test VM; use `--binary NAME=PATH` to select equivalent local binaries. + +There are 25 main trials and 36 additional trials. The 64-client optimized and baseline rtp2httpd measurements both passed all five trials. All TVGate samples failed sequence continuity; several udpxy samples and some 400 Mbps samples from other programs also recorded kernel UDP drops. The report records these limitations explicitly. + +Commands, generated configuration rules, affinity, synthetic payload construction, validation, and exit behavior are documented in [the harness README](../../README.md). The measurements represent server-process CPU on Linux loopback, not total machine CPU, physical-NIC capacity, or a video decoding test. diff --git a/tools/stress-test/results/2026-09-06/additional/metadata.json b/tools/stress-test/results/2026-09-06/additional/metadata.json new file mode 100644 index 00000000..a2ff9dbf --- /dev/null +++ b/tools/stress-test/results/2026-09-06/additional/metadata.json @@ -0,0 +1,41 @@ +{ + "timestamp_utc": "2026-09-06T10:31:53Z", + "platform": "Linux-6.8.0-138-generic-aarch64-with-glibc2.39", + "machine": "aarch64", + "cpu_count": 16, + "server_cpu": 0, + "load_cpus": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + "controller_cpu": 13, + "warmup_s": 5.0, + "duration_s": 10, + "repetitions": 3, + "cases": { + "distinct8": [8, 8, 40], + "shared8": [8, 1, 40], + "high400": [1, 1, 400] + }, + "script_sha256": "f449923fc4aa4b32fe382ef8aa2bce7c446fe108ceab0a55fc1990d55e31b6bf", + "binaries": { + "rtp2httpd": { + "path": "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", + "revision": "530dc980e92db6b6ea98b6ca223dffe1a0345b5c", + "sha256": "74f861fcae8b894774928ec89b24436cbf487692b98df84921bd1991301a23ca" + }, + "msd_lite": { + "path": "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", + "revision": "fa68e131343fb58c67ad77b2d26f2cb7c49a2c95", + "sha256": "45459818ceae0a7595406b0aec5cf999cd27bb4efa822494414a6ca397bc88a7" + }, + "udpxy": { + "path": "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", + "revision": "31d4bcfabaade59d3efdee015df7979febf76bae", + "sha256": "71037623a56bed03b8797c4b849a90f7a0924ac2e6998c178ff0498c734c06ae" + }, + "tvgate": { + "path": "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", + "revision": "v3.2.0", + "sha256": "eca4c04f3973316e716be3264c690e3215fd68624c15df8a709f0e627df5307e" + } + }, + "sysctl": "net.core.rmem_max = 212992\nnet.core.wmem_max = 212992\nnet.ipv4.tcp_congestion_control = cubic" +} diff --git a/tools/stress-test/results/2026-09-06/additional/summary.json b/tools/stress-test/results/2026-09-06/additional/summary.json new file mode 100644 index 00000000..70f14577 --- /dev/null +++ b/tools/stress-test/results/2026-09-06/additional/summary.json @@ -0,0 +1,206 @@ +[ + { + "case": "distinct8", + "program": "rtp2httpd", + "valid_trials": 3, + "total_trials": 3, + "cpu_pct": { + "mean": 9.782727183682368, + "min": 9.082493830103466, + "max": 10.28576201416045 + }, + "pss_mib": { + "mean": 2.2790690104166664, + "min": 2.19912109375, + "max": 2.43037109375 + }, + "uss_mib": { + "mean": 1.659375, + "min": 1.578125, + "max": 1.809375 + } + }, + { + "case": "distinct8", + "program": "msd_lite", + "valid_trials": 3, + "total_trials": 3, + "cpu_pct": { + "mean": 9.486660193023203, + "min": 9.189092772011854, + "max": 9.775612976752964 + }, + "pss_mib": { + "mean": 8.9541015625, + "min": 8.9541015625, + "max": 8.9541015625 + }, + "uss_mib": { + "mean": 8.94140625, + "min": 8.94140625, + "max": 8.94140625 + } + }, + { + "case": "distinct8", + "program": "udpxy", + "valid_trials": 3, + "total_trials": 3, + "cpu_pct": { + "mean": 20.81360032250564, + "min": 20.27327466242048, + "max": 21.281662999838833 + }, + "pss_mib": { + "mean": 0.7973958333333333, + "min": 0.79482421875, + "max": 0.79873046875 + }, + "uss_mib": { + "mean": 0.5286458333333334, + "min": 0.5234375, + "max": 0.53125 + } + }, + { + "case": "distinct8", + "program": "tvgate", + "valid_trials": 0, + "total_trials": 3, + "cpu_pct": null, + "pss_mib": null, + "uss_mib": null + }, + { + "case": "shared8", + "program": "rtp2httpd", + "valid_trials": 3, + "total_trials": 3, + "cpu_pct": { + "mean": 7.20906995120966, + "min": 6.974362646726113, + "max": 7.472789670238363 + }, + "pss_mib": { + "mean": 1.6008138020833333, + "min": 1.59814453125, + "max": 1.6056640625 + }, + "uss_mib": { + "mean": 0.97890625, + "min": 0.9765625, + "max": 0.98359375 + } + }, + { + "case": "shared8", + "program": "msd_lite", + "valid_trials": 3, + "total_trials": 3, + "cpu_pct": { + "mean": 6.074893827412331, + "min": 5.780378502284025, + "max": 6.272567602400795 + }, + "pss_mib": { + "mean": 1.353125, + "min": 1.3525390625, + "max": 1.353515625 + }, + "uss_mib": { + "mean": 1.33984375, + "min": 1.33984375, + "max": 1.33984375 + } + }, + { + "case": "shared8", + "program": "udpxy", + "valid_trials": 3, + "total_trials": 3, + "cpu_pct": { + "mean": 26.932372901684058, + "min": 26.213333030087973, + "max": 27.784988886982884 + }, + "pss_mib": { + "mean": 0.7891927083333333, + "min": 0.769921875, + "max": 0.798828125 + }, + "uss_mib": { + "mean": 0.51953125, + "min": 0.49609375, + "max": 0.53125 + } + }, + { + "case": "shared8", + "program": "tvgate", + "valid_trials": 0, + "total_trials": 3, + "cpu_pct": null, + "pss_mib": null, + "uss_mib": null + }, + { + "case": "high400", + "program": "rtp2httpd", + "valid_trials": 2, + "total_trials": 3, + "cpu_pct": { + "mean": 14.790775435921763, + "min": 13.844029103946436, + "max": 15.737521767897087 + }, + "pss_mib": { + "mean": 1.4345703125, + "min": 1.3564453125, + "max": 1.5126953125 + }, + "uss_mib": { + "mean": 0.8359375, + "min": 0.734375, + "max": 0.9375 + } + }, + { + "case": "high400", + "program": "msd_lite", + "valid_trials": 1, + "total_trials": 3, + "cpu_pct": { + "mean": 13.759667369926044, + "min": 13.759667369926044, + "max": 13.759667369926044 + }, + "pss_mib": { + "mean": 1.341796875, + "min": 1.341796875, + "max": 1.341796875 + }, + "uss_mib": { + "mean": 1.328125, + "min": 1.328125, + "max": 1.328125 + } + }, + { + "case": "high400", + "program": "udpxy", + "valid_trials": 0, + "total_trials": 3, + "cpu_pct": null, + "pss_mib": null, + "uss_mib": null + }, + { + "case": "high400", + "program": "tvgate", + "valid_trials": 0, + "total_trials": 3, + "cpu_pct": null, + "pss_mib": null, + "uss_mib": null + } +] diff --git a/tools/stress-test/results/2026-09-06/additional/trials.jsonl b/tools/stress-test/results/2026-09-06/additional/trials.jsonl new file mode 100644 index 00000000..d5695226 --- /dev/null +++ b/tools/stress-test/results/2026-09-06/additional/trials.jsonl @@ -0,0 +1,36 @@ +{"program": "rtp2httpd", "case": "distinct8", "repetition": 0, "order": 0, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:42209"], "clients": 8, "sources": 8, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.019274628999483, "cpu_pct": 9.082493830103466, "user_cpu_pct": 1.8963448656259985, "system_cpu_pct": 7.186148964477468, "pss_mib": 2.19912109375, "uss_mib": 1.578125, "server_processes": 2, "server_threads": 2, "multicast_sockets": 8, "client_mbps": [40.00614404158985, 40.00614404158985, 40.00614404158985, 40.00614404158985, 39.95465608272036, 40.00614404158985, 40.00614404158985, 40.00614404158985], "source_mbps": [39.99983939356501, 39.99983939356501, 40.00194094290663, 40.00194094290663, 40.00194094290663, 40.00194094290663, 39.9987886188942, 40.00089016823581], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 51.80015711894175, "family_stable": true} +{"program": "msd_lite", "case": "distinct8", "repetition": 0, "order": 1, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", "-c", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/distinct8-00-msd_lite.xml", "-l", "1"], "clients": 8, "sources": 8, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.004976337999324, "cpu_pct": 9.495274830304794, "user_cpu_pct": 1.1994031364595528, "system_cpu_pct": 8.29587169384524, "pss_mib": 8.9541015625, "uss_mib": 8.94140625, "server_processes": 1, "server_threads": 2, "multicast_sockets": 8, "client_mbps": [39.97222047203477, 39.97116819568305, 40.01085404666222, 39.95959315581412, 40.020324533827704, 39.97011591933133, 40.01716770477254, 40.01085404666222], "source_mbps": [40.00018095795191, 40.00228551065535, 39.99912868160019, 40.00123323430363, 40.00123323430363, 40.00018095795191, 40.00018095795191, 40.00018095795191], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 54.47289244753803, "family_stable": true} +{"program": "udpxy", "case": "distinct8", "repetition": 0, "order": 2, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", "-T", "-a", "127.0.0.1", "-m", "lo", "-p", "55001", "-c", "256"], "clients": 8, "sources": 8, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.01318254599937, "cpu_pct": 20.27327466242048, "user_cpu_pct": 2.0972353099055665, "system_cpu_pct": 18.176039352514913, "pss_mib": 0.79482421875, "uss_mib": 0.5234375, "server_processes": 9, "server_threads": 9, "multicast_sockets": 8, "client_mbps": [40.00419868107188, 40.00314726710318, 39.99999302519708, 40.00104443916579, 40.00104443916579, 40.002095853134485, 40.002095853134485, 40.002095853134485], "source_mbps": [40.00419868107188, 40.00314726710318, 39.99999302519708, 40.00104443916579, 40.00104443916579, 40.002095853134485, 40.002095853134485, 40.002095853134485], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 86.68572614276343, "family_stable": true} +{"program": "tvgate", "case": "distinct8", "repetition": 0, "order": 3, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", "-config", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/distinct8-00-tvgate.yaml"], "clients": 8, "sources": 8, "target_mbps": 40, "valid": false, "request_prefix": "udp", "duration_s": 10.007266337999681, "cpu_pct": 31.377200265738544, "user_cpu_pct": 15.089035796581276, "system_cpu_pct": 16.28816446915727, "pss_mib": 23.05078125, "uss_mib": 23.05078125, "server_processes": 1, "server_threads": 6, "multicast_sockets": 8, "client_mbps": [39.99839181656172, 40.00259995878334, 39.99944385211713, 40.00049588767253, 39.99944385211713, 40.00154792322794, 40.00049588767253, 40.00259995878334], "source_mbps": [39.99839181656172, 40.00259995878334, 40.00259995878334, 40.00049588767253, 39.99944385211713, 40.00154792322794, 40.00049588767253, 40.00259995878334], "gaps": 1304065, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 186295, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 91.73334345206365, "family_stable": true} +{"program": "rtp2httpd", "case": "distinct8", "repetition": 1, "order": 0, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:51205"], "clients": 8, "sources": 8, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.01384242200038, "cpu_pct": 10.28576201416045, "user_cpu_pct": 2.2968206439387417, "system_cpu_pct": 7.98894137022171, "pss_mib": 2.43037109375, "uss_mib": 1.809375, "server_processes": 2, "server_threads": 2, "multicast_sockets": 8, "client_mbps": [40.027846166160174, 40.027846166160174, 40.027846166160174, 39.97633027662843, 40.027846166160174, 40.027846166160174, 40.027846166160174, 39.97633027662843], "source_mbps": [40.00156254905214, 40.002613893736466, 40.000511204367825, 40.000511204367825, 40.00156254905214, 40.00156254905214, 40.00156254905214, 40.00156254905214], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 59.91706027666282, "family_stable": true} +{"program": "tvgate", "case": "distinct8", "repetition": 1, "order": 1, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", "-config", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/distinct8-01-tvgate.yaml"], "clients": 8, "sources": 8, "target_mbps": 40, "valid": false, "request_prefix": "udp", "duration_s": 10.008565171001464, "cpu_pct": 30.274069741575314, "user_cpu_pct": 14.18784786568876, "system_cpu_pct": 16.086221875886554, "pss_mib": 22.6015625, "uss_mib": 22.6015625, "server_processes": 1, "server_threads": 6, "multicast_sockets": 8, "client_mbps": [40.00056443254802, 40.002668230609, 40.00056443254802, 39.99951253351752, 39.998460634487024, 40.002668230609, 39.99951253351752, 40.00056443254802], "source_mbps": [40.00056443254802, 40.002668230609, 40.00056443254802, 39.99951253351752, 39.998460634487024, 40.002668230609, 40.00161633157851, 40.00372012963949], "gaps": 1286873, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 183839, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 86.12623141002615, "family_stable": true} +{"program": "udpxy", "case": "distinct8", "repetition": 1, "order": 2, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", "-T", "-a", "127.0.0.1", "-m", "lo", "-p", "37491", "-c", "256"], "clients": 8, "sources": 8, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.008616338000138, "cpu_pct": 21.281662999838833, "user_cpu_pct": 1.4987086619604815, "system_cpu_pct": 19.78295433787835, "pss_mib": 0.79873046875, "uss_mib": 0.53125, "server_processes": 9, "server_threads": 9, "multicast_sockets": 8, "client_mbps": [40.002463725170564, 40.00351561882344, 40.001411831517686, 40.002463725170564, 40.00351561882344, 40.001411831517686, 40.00351561882344, 40.001411831517686], "source_mbps": [40.002463725170564, 40.000359937864815, 40.00351561882344, 40.002463725170564, 40.000359937864815, 40.001411831517686, 40.000359937864815, 40.001411831517686], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 95.11804307909189, "family_stable": true} +{"program": "msd_lite", "case": "distinct8", "repetition": 1, "order": 3, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", "-c", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/distinct8-01-msd_lite.xml", "-l", "1"], "clients": 8, "sources": 8, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.011869755000589, "cpu_pct": 9.189092772011854, "user_cpu_pct": 1.3983402044365867, "system_cpu_pct": 7.790752567575267, "pss_mib": 8.9541015625, "uss_mib": 8.94140625, "server_processes": 1, "server_threads": 2, "multicast_sockets": 8, "client_mbps": [40.002233528853615, 39.978047836677675, 40.033479640484636, 40.04294360698827, 40.033479640484636, 40.04189205515453, 39.9959242178512, 39.99066645868251], "source_mbps": [40.00208330716308, 40.00208330716308, 40.00103175532935, 40.00208330716308, 39.9999802034956, 39.9999802034956, 40.00103175532935, 40.00103175532935], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 53.13692776859028, "family_stable": true} +{"program": "udpxy", "case": "distinct8", "repetition": 2, "order": 0, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", "-T", "-a", "127.0.0.1", "-m", "lo", "-p", "53937", "-c", "256"], "clients": 8, "sources": 8, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.00676854700032, "cpu_pct": 20.885863305257608, "user_cpu_pct": 1.598917764995798, "system_cpu_pct": 19.28694554026181, "pss_mib": 0.7986328125, "uss_mib": 0.53125, "server_processes": 9, "server_threads": 9, "multicast_sockets": 8, "client_mbps": [40.00143364163164, 40.00038155374227, 39.99932946585291, 40.00038155374227, 40.00143364163164, 40.00248572952101, 40.00248572952101, 40.00248572952101], "source_mbps": [40.00143364163164, 40.00038155374227, 39.99932946585291, 40.00038155374227, 40.00143364163164, 40.00248572952101, 40.00248572952101, 40.00248572952101], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 90.938447884136, "family_stable": true} +{"program": "tvgate", "case": "distinct8", "repetition": 2, "order": 1, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", "-config", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/distinct8-02-tvgate.yaml"], "clients": 8, "sources": 8, "target_mbps": 40, "valid": false, "request_prefix": "udp", "duration_s": 10.008057754999754, "cpu_pct": 31.074960558119564, "user_cpu_pct": 14.588245149470922, "system_cpu_pct": 16.486715408648642, "pss_mib": 22.876171875, "uss_mib": 22.876171875, "server_processes": 1, "server_threads": 7, "multicast_sockets": 8, "client_mbps": [40.001540538672664, 40.001540538672664, 40.001540538672664, 40.00259249103522, 40.000488586310105, 40.00259249103522, 40.000488586310105, 40.000488586310105], "source_mbps": [40.000488586310105, 40.001540538672664, 40.001540538672664, 40.00259249103522, 40.000488586310105, 40.00259249103522, 40.00259249103522, 40.000488586310105], "gaps": 1292032, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 184576, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 88.52866577007696, "family_stable": true} +{"program": "rtp2httpd", "case": "distinct8", "repetition": 2, "order": 2, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:58775"], "clients": 8, "sources": 8, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.020114671999181, "cpu_pct": 9.979925706783186, "user_cpu_pct": 2.3951821696279647, "system_cpu_pct": 7.584743537155221, "pss_mib": 2.20771484375, "uss_mib": 1.590625, "server_processes": 2, "server_threads": 2, "multicast_sockets": 8, "client_mbps": [40.002790099809026, 40.002790099809026, 40.002790099809026, 40.002790099809026, 40.002790099809026, 40.002790099809026, 40.002790099809026, 40.002790099809026], "source_mbps": [40.002790099809026, 40.00068872665221, 40.00068872665221, 40.00068872665221, 39.9996380400738, 40.00173941323061, 40.00068872665221, 39.9996380400738], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 57.28477355693549, "family_stable": true} +{"program": "msd_lite", "case": "distinct8", "repetition": 2, "order": 3, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", "-c", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/distinct8-02-msd_lite.xml", "-l", "1"], "clients": 8, "sources": 8, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.024946796998847, "cpu_pct": 9.775612976752964, "user_cpu_pct": 1.3965161395361378, "system_cpu_pct": 8.379096837216826, "pss_mib": 8.9541015625, "uss_mib": 8.94140625, "server_processes": 1, "server_threads": 2, "multicast_sockets": 8, "client_mbps": [39.98965940846839, 40.00031123557155, 39.99401015474996, 40.04096820944417, 39.99175976874225, 40.03991802930724, 39.98125796737294, 40.03991802930724], "source_mbps": [40.00241159584541, 40.00031123557155, 40.00136141570848, 39.999261055434616, 40.00031123557155, 40.00241159584541, 40.00241159584541, 40.00241159584541], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 57.755917485101705, "family_stable": true} +{"program": "rtp2httpd", "case": "shared8", "repetition": 0, "order": 0, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:54691"], "clients": 8, "sources": 1, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.03675942100017, "cpu_pct": 6.974362646726113, "user_cpu_pct": 0.9963375209608735, "system_cpu_pct": 5.97802512576524, "pss_mib": 1.6056640625, "uss_mib": 0.98359375, "server_processes": 2, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [39.98784858390133, 39.98784858390133, 39.98784858390133, 39.98784858390133, 39.98784858390133, 39.98784858390133, 39.98784858390133, 39.98784858390133], "source_mbps": [40.00358274603235], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 51.70991733786933, "family_stable": true} +{"program": "msd_lite", "case": "shared8", "repetition": 0, "order": 1, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", "-c", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/shared8-00-msd_lite.xml", "-l", "1"], "clients": 8, "sources": 1, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.04373392100024, "cpu_pct": 6.272567602400795, "user_cpu_pct": 0.7965165209397834, "system_cpu_pct": 5.476051081461011, "pss_mib": 1.353515625, "uss_mib": 1.33984375, "server_processes": 1, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [40.03660024876025, 40.03660024876025, 40.03660024876025, 40.03660024876025, 40.03660024876025, 40.03660024876025, 40.03660024876025, 40.03660024876025], "source_mbps": [40.00305734503044], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 61.331772112363325, "family_stable": true} +{"program": "udpxy", "case": "shared8", "repetition": 0, "order": 2, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", "-T", "-a", "127.0.0.1", "-m", "lo", "-p", "41943", "-c", "256"], "clients": 8, "sources": 1, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.033062170999983, "cpu_pct": 26.213333030087973, "user_cpu_pct": 2.392091227080271, "system_cpu_pct": 23.8212418030077, "pss_mib": 0.769921875, "uss_mib": 0.49609375, "server_processes": 9, "server_threads": 9, "multicast_sockets": 8, "client_mbps": [40.00258437150679, 40.00258437150679, 40.00258437150679, 40.00258437150679, 40.00258437150679, 40.00258437150679, 40.00258437150679, 40.00258437150679], "source_mbps": [40.00258437150679], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 72.16141868358818, "family_stable": true} +{"program": "tvgate", "case": "shared8", "repetition": 0, "order": 3, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", "-config", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/shared8-00-tvgate.yaml"], "clients": 8, "sources": 1, "target_mbps": 40, "valid": false, "request_prefix": "udp", "duration_s": 10.035801797001113, "cpu_pct": 22.41973332586483, "user_cpu_pct": 8.170747256537405, "system_cpu_pct": 14.248986069327424, "pss_mib": 20.1953125, "uss_mib": 20.1953125, "server_processes": 1, "server_threads": 7, "multicast_sockets": 1, "client_mbps": [40.00425278625652, 40.00005660932399, 40.00005660932399, 40.00425278625652, 40.00425278625652, 40.00005660932399, 40.00005660932399, 40.00005660932399], "source_mbps": [40.00425278625652], "gaps": 1373127, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 196161, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 73.63636856806271, "family_stable": true} +{"program": "rtp2httpd", "case": "shared8", "repetition": 1, "order": 0, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:33681"], "clients": 8, "sources": 1, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.036412546000065, "cpu_pct": 7.472789670238363, "user_cpu_pct": 1.195646347238138, "system_cpu_pct": 6.277143323000225, "pss_mib": 1.5986328125, "uss_mib": 0.9765625, "server_processes": 2, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [39.98923063001773, 39.98923063001773, 39.98923063001773, 39.98923063001773, 39.98923063001773, 39.98923063001773, 39.98923063001773, 39.98923063001773], "source_mbps": [40.00076941436614], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 56.29501551579567, "family_stable": true} +{"program": "tvgate", "case": "shared8", "repetition": 1, "order": 1, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", "-config", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/shared8-01-tvgate.yaml"], "clients": 8, "sources": 1, "target_mbps": 40, "valid": false, "request_prefix": "udp", "duration_s": 10.03368546299862, "cpu_pct": 21.826476503335662, "user_cpu_pct": 7.674149272862311, "system_cpu_pct": 14.152327230473352, "pss_mib": 20.723828125, "uss_mib": 20.723828125, "server_processes": 1, "server_threads": 6, "multicast_sockets": 1, "client_mbps": [40.001148678628375, 40.001148678628375, 40.001148678628375, 40.001148678628375, 40.001148678628375, 40.001148678628375, 40.001148678628375, 40.001148678628375], "source_mbps": [40.001148678628375], "gaps": 1365056, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 195008, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 71.75827891507615, "family_stable": true} +{"program": "udpxy", "case": "shared8", "repetition": 1, "order": 2, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", "-T", "-a", "127.0.0.1", "-m", "lo", "-p", "38287", "-c", "256"], "clients": 8, "sources": 1, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.037764087999676, "cpu_pct": 26.79879678798132, "user_cpu_pct": 1.5939804780955433, "system_cpu_pct": 25.20481630988578, "pss_mib": 0.798828125, "uss_mib": 0.53125, "server_processes": 9, "server_threads": 9, "multicast_sockets": 8, "client_mbps": [40.00167651678854, 40.00167651678854, 40.00167651678854, 40.00167651678854, 40.00167651678854, 40.00167651678854, 40.00167651678854, 40.00167651678854], "source_mbps": [40.00167651678854], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 74.91708247049056, "family_stable": true} +{"program": "msd_lite", "case": "shared8", "repetition": 1, "order": 3, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", "-c", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/shared8-01-msd_lite.xml", "-l", "1"], "clients": 8, "sources": 1, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.045796880000125, "cpu_pct": 6.171735377552173, "user_cpu_pct": 0.6968088329494387, "system_cpu_pct": 5.4749265446027335, "pss_mib": 1.3533203125, "uss_mib": 1.33984375, "server_processes": 1, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [40.020892797505475, 40.020892797505475, 40.020892797505475, 40.020892797505475, 40.020892797505475, 40.020892797505475, 40.020892797505475, 40.020892797505475], "source_mbps": [40.00113050265008], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 56.93923606386842, "family_stable": true} +{"program": "udpxy", "case": "shared8", "repetition": 2, "order": 0, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", "-T", "-a", "127.0.0.1", "-m", "lo", "-p", "37553", "-c", "256"], "clients": 8, "sources": 1, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.041393255000003, "cpu_pct": 27.784988886982884, "user_cpu_pct": 2.190931023346321, "system_cpu_pct": 25.594057863636564, "pss_mib": 0.798828125, "uss_mib": 0.53125, "server_processes": 9, "server_threads": 9, "multicast_sockets": 8, "client_mbps": [40.000849065441756, 40.000849065441756, 40.000849065441756, 40.000849065441756, 40.000849065441756, 40.000849065441756, 40.000849065441756, 40.000849065441756], "source_mbps": [40.000849065441756], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 75.38794475787114, "family_stable": true} +{"program": "tvgate", "case": "shared8", "repetition": 2, "order": 1, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", "-config", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/shared8-02-tvgate.yaml"], "clients": 8, "sources": 1, "target_mbps": 40, "valid": false, "request_prefix": "udp", "duration_s": 10.028015130001222, "cpu_pct": 22.736303948912393, "user_cpu_pct": 7.8779298770354345, "system_cpu_pct": 14.858374071876959, "pss_mib": 20.352734375, "uss_mib": 20.352734375, "server_processes": 1, "server_threads": 7, "multicast_sockets": 1, "client_mbps": [40.000670401855594, 40.000670401855594, 40.000670401855594, 40.000670401855594, 40.000670401855594, 40.000670401855594, 40.000670401855594, 40.000670401855594], "source_mbps": [40.000670401855594], "gaps": 1369088, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 195584, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 71.99829583822257, "family_stable": true} +{"program": "rtp2httpd", "case": "shared8", "repetition": 2, "order": 2, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:58275"], "clients": 8, "sources": 1, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.027774796000813, "cpu_pct": 7.180057536664504, "user_cpu_pct": 1.1966762561107507, "system_cpu_pct": 5.983381280553753, "pss_mib": 1.59814453125, "uss_mib": 0.9765625, "server_processes": 2, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [39.97223234010565, 39.97223234010565, 39.97223234010565, 39.97223234010565, 39.97223234010565, 39.97223234010565, 39.97223234010565, 39.97223234010565], "source_mbps": [40.00267897519778], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 52.95292433290072, "family_stable": true} +{"program": "msd_lite", "case": "shared8", "repetition": 2, "order": 3, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", "-c", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/shared8-02-msd_lite.xml", "-l", "1"], "clients": 8, "sources": 1, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.033945004999623, "cpu_pct": 5.780378502284025, "user_cpu_pct": 0.6976318882066928, "system_cpu_pct": 5.082746614077332, "pss_mib": 1.3525390625, "uss_mib": 1.33984375, "server_processes": 1, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [40.01660202442131, 40.01660202442131, 40.01660202442131, 40.01660202442131, 40.01660202442131, 40.01660202442131, 40.01660202442131, 40.01660202442131], "source_mbps": [40.00011399305203], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 51.72442142561051, "family_stable": true} +{"program": "rtp2httpd", "case": "high400", "repetition": 0, "order": 0, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:54257"], "clients": 1, "sources": 1, "target_mbps": 400, "valid": false, "request_prefix": "rtp", "duration_s": 10.034364713001196, "cpu_pct": 16.144519820980985, "user_cpu_pct": 3.587671071329108, "system_cpu_pct": 12.556848749651877, "pss_mib": 1.3955078125, "uss_mib": 0.7734375, "server_processes": 2, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [399.9225065838527], "source_mbps": [400.0463115317026], "gaps": 259, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 37, "load_cpu_pct": 67.86677776597563, "family_stable": true} +{"program": "msd_lite", "case": "high400", "repetition": 0, "order": 1, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", "-c", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/high400-00-msd_lite.xml", "-l", "1"], "clients": 1, "sources": 1, "target_mbps": 400, "valid": true, "request_prefix": "rtp", "duration_s": 10.029312212998775, "cpu_pct": 13.759667369926044, "user_cpu_pct": 1.7947392221642666, "system_cpu_pct": 11.964928147761777, "pss_mib": 1.341796875, "uss_mib": 1.328125, "server_processes": 1, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [400.04134867792357], "source_mbps": [400.0777890630904], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 55.53720815252758, "family_stable": true} +{"program": "udpxy", "case": "high400", "repetition": 0, "order": 2, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", "-T", "-a", "127.0.0.1", "-m", "lo", "-p", "56973", "-c", "256"], "clients": 1, "sources": 1, "target_mbps": 400, "valid": false, "request_prefix": "rtp", "duration_s": 10.02519554600076, "cpu_pct": 22.842447207062456, "user_cpu_pct": 1.4962301664014708, "system_cpu_pct": 21.346217040660985, "pss_mib": 0.32509765625, "uss_mib": 0.1171875, "server_processes": 2, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [399.9553817780161], "source_mbps": [400.01734086870414], "gaps": 203, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 29, "load_cpu_pct": 70.52231517638933, "family_stable": true} +{"program": "tvgate", "case": "high400", "repetition": 0, "order": 3, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", "-config", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/high400-00-tvgate.yaml"], "clients": 1, "sources": 1, "target_mbps": 400, "valid": false, "request_prefix": "udp", "duration_s": 10.033724295999491, "cpu_pct": 28.005553243279415, "user_cpu_pct": 15.6472308156401, "system_cpu_pct": 12.358322427639314, "pss_mib": 17.676171875, "uss_mib": 17.676171875, "server_processes": 1, "server_threads": 7, "multicast_sockets": 1, "client_mbps": [399.89242136140547], "source_mbps": [399.99524898249246], "gaps": 2333772, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 333298, "closed_clients": 0, "kernel_udp_drops": 98, "load_cpu_pct": 51.82522308364874, "family_stable": true} +{"program": "rtp2httpd", "case": "high400", "repetition": 1, "order": 0, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:45669"], "clients": 1, "sources": 1, "target_mbps": 400, "valid": true, "request_prefix": "rtp", "duration_s": 10.040429628999846, "cpu_pct": 13.844029103946436, "user_cpu_pct": 2.5895306237597655, "system_cpu_pct": 11.25449848018667, "pss_mib": 1.5126953125, "uss_mib": 0.9375, "server_processes": 2, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [399.9892104617092], "source_mbps": [399.99340470454104], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 58.563231029643916, "family_stable": true} +{"program": "tvgate", "case": "high400", "repetition": 1, "order": 1, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", "-config", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/high400-01-tvgate.yaml"], "clients": 1, "sources": 1, "target_mbps": 400, "valid": false, "request_prefix": "udp", "duration_s": 10.0216719630007, "cpu_pct": 25.644423500272783, "user_cpu_pct": 14.46864360910332, "system_cpu_pct": 11.175779891169462, "pss_mib": 17.784375, "uss_mib": 17.784375, "server_processes": 1, "server_threads": 7, "multicast_sockets": 1, "client_mbps": [399.9153146098363], "source_mbps": [400.0067101377863], "gaps": 2345329, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 334960, "closed_clients": 0, "kernel_udp_drops": 87, "load_cpu_pct": 47.59684828649851, "family_stable": true} +{"program": "udpxy", "case": "high400", "repetition": 1, "order": 2, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", "-T", "-a", "127.0.0.1", "-m", "lo", "-p", "59211", "-c", "256"], "clients": 1, "sources": 1, "target_mbps": 400, "valid": false, "request_prefix": "rtp", "duration_s": 10.028106712999943, "cpu_pct": 23.53385407178219, "user_cpu_pct": 1.7949549715766073, "system_cpu_pct": 21.738899100205582, "pss_mib": 0.32109375, "uss_mib": 0.109375, "server_processes": 2, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [399.87601914942076], "source_mbps": [400.01564909553855], "gaps": 931, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 133, "load_cpu_pct": 71.7981988630643, "family_stable": true} +{"program": "msd_lite", "case": "high400", "repetition": 1, "order": 3, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", "-c", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/high400-01-msd_lite.xml", "-l", "1"], "clients": 1, "sources": 1, "target_mbps": 400, "valid": false, "request_prefix": "rtp", "duration_s": 10.03419213000052, "cpu_pct": 14.151612622150644, "user_cpu_pct": 1.8935256325412833, "system_cpu_pct": 12.25808698960936, "pss_mib": 1.341796875, "uss_mib": 1.328125, "server_processes": 1, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [399.8027301077593], "source_mbps": [400.09620983805024], "gaps": 2023, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 289, "load_cpu_pct": 59.59622780314144, "family_stable": true} +{"program": "udpxy", "case": "high400", "repetition": 2, "order": 0, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", "-T", "-a", "127.0.0.1", "-m", "lo", "-p", "58639", "-c", "256"], "clients": 1, "sources": 1, "target_mbps": 400, "valid": false, "request_prefix": "rtp", "duration_s": 10.019285046999357, "cpu_pct": 23.55457489161653, "user_cpu_pct": 1.4971128109078302, "system_cpu_pct": 22.0574620807087, "pss_mib": 0.31728515625, "uss_mib": 0.109375, "server_processes": 2, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [399.86558054856954], "source_mbps": [399.96750558565645], "gaps": 679, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 97, "load_cpu_pct": 70.86333971630397, "family_stable": true} +{"program": "tvgate", "case": "high400", "repetition": 2, "order": 1, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", "-config", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/high400-02-tvgate.yaml"], "clients": 1, "sources": 1, "target_mbps": 400, "valid": false, "request_prefix": "udp", "duration_s": 10.03475075499955, "cpu_pct": 30.095416156653066, "user_cpu_pct": 16.741820908336805, "system_cpu_pct": 13.35359524831626, "pss_mib": 17.6234375, "uss_mib": 17.6234375, "server_processes": 1, "server_threads": 6, "multicast_sockets": 1, "client_mbps": [399.9973486137853], "source_mbps": [400.0613470145109], "gaps": 2276169, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 325167, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 55.40745491092419, "family_stable": true} +{"program": "rtp2httpd", "case": "high400", "repetition": 2, "order": 2, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:58327"], "clients": 1, "sources": 1, "target_mbps": 400, "valid": true, "request_prefix": "rtp", "duration_s": 10.039700171999357, "cpu_pct": 15.737521767897087, "user_cpu_pct": 2.9881370445374213, "system_cpu_pct": 12.749384723359666, "pss_mib": 1.3564453125, "uss_mib": 0.734375, "server_processes": 2, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [400.0696557853598], "source_mbps": [400.26260497376313], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 67.23308350209199, "family_stable": true} +{"program": "msd_lite", "case": "high400", "repetition": 2, "order": 3, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", "-c", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/high400-02-msd_lite.xml", "-l", "1"], "clients": 1, "sources": 1, "target_mbps": 400, "valid": false, "request_prefix": "rtp", "duration_s": 10.041544588000761, "cpu_pct": 16.431735033788357, "user_cpu_pct": 2.1908980045051143, "system_cpu_pct": 14.240837029283243, "pss_mib": 1.341796875, "uss_mib": 1.328125, "server_processes": 1, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [399.75158451188025], "source_mbps": [399.9846386978664], "gaps": 1911, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 273, "load_cpu_pct": 67.4199067749983, "family_stable": true} diff --git a/tools/stress-test/results/2026-09-06/build-environment.json b/tools/stress-test/results/2026-09-06/build-environment.json new file mode 100644 index 00000000..bd0f6428 --- /dev/null +++ b/tools/stress-test/results/2026-09-06/build-environment.json @@ -0,0 +1,18 @@ +{ + "host": "Apple M3 Max, 16 cores (12 performance + 4 efficiency), 128 GiB RAM", + "vm": "Parallels Ubuntu 24.04, 16 vCPU, 16 GiB RAM; native aarch64 execution", + "compiler": "gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0", + "msd_lite_commit": "fa68e131343fb58c67ad77b2d26f2cb7c49a2c95", + "liblcb_commit": "e2f420a2e545f7b04f3f7ffc13b3d9eaa92b5e8b", + "udpxy_commit": "31d4bcfabaade59d3efdee015df7979febf76bae", + "udpxy_build_command": "make -j16 CFLAGS=\"-O3 -flto\" LDFLAGS=\"-flto\"", + "tvgate_version": "\u7a0b\u5e8f\u7248\u672c: v3.2.0", + "tvgate_asset": "https://github.com/qist/tvgate/releases/download/v3.2.0/TVGate-linux-arm64.zip", + "tvgate_archive_sha256": "1655a066b91debdaf2f3b39096207f80fdfc7bd9c2f227ff9dda34c48562ac3e", + "rtp2httpd_compile_flags": "# CMAKE generated file: DO NOT EDIT!\n# Generated by \"Unix Makefiles\" Generator, CMake Version 3.28\n\n# compile C with /usr/bin/cc\nC_DEFINES = -DPACKAGE=\\\"rtp2httpd\\\" -DSYSCONFDIR=\\\"/usr/local/etc\\\" -DVERSION=\\\"1.0.0-snapshot\\\" -D_FORTIFY_SOURCE=2 -D_GNU_SOURCE\n\nC_INCLUDES = -I/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/src\n\nC_FLAGS = -O3 -DNDEBUG -std=gnu11 -Wall -Wextra -Wunused -Wformat=2 -Wformat-security -Wstrict-prototypes -Wmissing-prototypes -Wold-style-definition -Wpointer-arith -Wcast-align -Wcast-qual -Wshadow -Wwrite-strings -Wredundant-decls -Wnull-dereference -Wundef -Wvla -Wduplicated-cond -Wlogical-op -Wjump-misses-init -Wduplicated-branches -finline-functions -funroll-loops -ftree-vectorize -ffast-math -fomit-frame-pointer -flto=auto -finline-limit=600 -fstack-protector-strong\n\n", + "rtp2httpd_link_command": "/usr/bin/cc -O3 -DNDEBUG -flto CMakeFiles/rtp2httpd.dir/src/rtp2httpd.c.o CMakeFiles/rtp2httpd.dir/src/supervisor.c.o CMakeFiles/rtp2httpd.dir/src/pid_file.c.o CMakeFiles/rtp2httpd.dir/src/configuration.c.o CMakeFiles/rtp2httpd.dir/src/access_log.c.o CMakeFiles/rtp2httpd.dir/src/http.c.o CMakeFiles/rtp2httpd.dir/src/http_headers.c.o CMakeFiles/rtp2httpd.dir/src/http_fetch.c.o CMakeFiles/rtp2httpd.dir/src/service.c.o CMakeFiles/rtp2httpd.dir/src/url_template.c.o CMakeFiles/rtp2httpd.dir/src/rtp.c.o CMakeFiles/rtp2httpd.dir/src/rtp_reorder.c.o CMakeFiles/rtp2httpd.dir/src/rtp_fec.c.o CMakeFiles/rtp2httpd.dir/src/rs_fec.c.o CMakeFiles/rtp2httpd.dir/src/multicast.c.o CMakeFiles/rtp2httpd.dir/src/fcc.c.o CMakeFiles/rtp2httpd.dir/src/fcc_telecom.c.o CMakeFiles/rtp2httpd.dir/src/fcc_huawei.c.o CMakeFiles/rtp2httpd.dir/src/stream.c.o CMakeFiles/rtp2httpd.dir/src/rtsp.c.o CMakeFiles/rtp2httpd.dir/src/http_chunked_decoder.c.o CMakeFiles/rtp2httpd.dir/src/http_proxy.c.o CMakeFiles/rtp2httpd.dir/src/http_proxy_rewrite.c.o CMakeFiles/rtp2httpd.dir/src/stun.c.o CMakeFiles/rtp2httpd.dir/src/snapshot.c.o CMakeFiles/rtp2httpd.dir/src/timezone.c.o CMakeFiles/rtp2httpd.dir/src/status.c.o CMakeFiles/rtp2httpd.dir/src/connection.c.o CMakeFiles/rtp2httpd.dir/src/worker.c.o CMakeFiles/rtp2httpd.dir/src/unix_socket.c.o CMakeFiles/rtp2httpd.dir/src/buffer_pool.c.o CMakeFiles/rtp2httpd.dir/src/send_queue.c.o CMakeFiles/rtp2httpd.dir/src/m3u.c.o CMakeFiles/rtp2httpd.dir/src/epg.c.o CMakeFiles/rtp2httpd.dir/src/embedded_web.c.o CMakeFiles/rtp2httpd.dir/src/utils.c.o CMakeFiles/rtp2httpd.dir/src/vendor/hashmap/hashmap.c.o CMakeFiles/rtp2httpd.dir/src/vendor/md5/md5.c.o CMakeFiles/rtp2httpd.dir/src/vendor/picohttpparser/picohttpparser.c.o CMakeFiles/rtp2httpd.dir/src/poller_epoll.c.o -o rtp2httpd -lm -lrt \n", + "baseline_compile_flags": "# CMAKE generated file: DO NOT EDIT!\n# Generated by \"Unix Makefiles\" Generator, CMake Version 3.28\n\n# compile C with /usr/bin/cc\nC_DEFINES = -DPACKAGE=\\\"rtp2httpd\\\" -DSYSCONFDIR=\\\"/usr/local/etc\\\" -DVERSION=\\\"1.0.0-snapshot\\\" -D_FORTIFY_SOURCE=2 -D_GNU_SOURCE\n\nC_INCLUDES = -I/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/baseline/src\n\nC_FLAGS = -O3 -DNDEBUG -std=gnu11 -Wall -Wextra -Wunused -Wformat=2 -Wformat-security -Wstrict-prototypes -Wmissing-prototypes -Wold-style-definition -Wpointer-arith -Wcast-align -Wcast-qual -Wshadow -Wwrite-strings -Wredundant-decls -Wnull-dereference -Wundef -Wvla -Wduplicated-cond -Wlogical-op -Wjump-misses-init -Wduplicated-branches -finline-functions -funroll-loops -ftree-vectorize -ffast-math -fomit-frame-pointer -flto=auto -finline-limit=600 -fstack-protector-strong\n\n", + "baseline_link_command": "/usr/bin/cc -O3 -DNDEBUG -flto CMakeFiles/rtp2httpd.dir/src/rtp2httpd.c.o CMakeFiles/rtp2httpd.dir/src/supervisor.c.o CMakeFiles/rtp2httpd.dir/src/pid_file.c.o CMakeFiles/rtp2httpd.dir/src/configuration.c.o CMakeFiles/rtp2httpd.dir/src/access_log.c.o CMakeFiles/rtp2httpd.dir/src/http.c.o CMakeFiles/rtp2httpd.dir/src/http_headers.c.o CMakeFiles/rtp2httpd.dir/src/http_fetch.c.o CMakeFiles/rtp2httpd.dir/src/service.c.o CMakeFiles/rtp2httpd.dir/src/url_template.c.o CMakeFiles/rtp2httpd.dir/src/rtp.c.o CMakeFiles/rtp2httpd.dir/src/rtp_reorder.c.o CMakeFiles/rtp2httpd.dir/src/rtp_fec.c.o CMakeFiles/rtp2httpd.dir/src/rs_fec.c.o CMakeFiles/rtp2httpd.dir/src/multicast.c.o CMakeFiles/rtp2httpd.dir/src/fcc.c.o CMakeFiles/rtp2httpd.dir/src/fcc_telecom.c.o CMakeFiles/rtp2httpd.dir/src/fcc_huawei.c.o CMakeFiles/rtp2httpd.dir/src/stream.c.o CMakeFiles/rtp2httpd.dir/src/rtsp.c.o CMakeFiles/rtp2httpd.dir/src/http_chunked_decoder.c.o CMakeFiles/rtp2httpd.dir/src/http_proxy.c.o CMakeFiles/rtp2httpd.dir/src/http_proxy_rewrite.c.o CMakeFiles/rtp2httpd.dir/src/stun.c.o CMakeFiles/rtp2httpd.dir/src/snapshot.c.o CMakeFiles/rtp2httpd.dir/src/timezone.c.o CMakeFiles/rtp2httpd.dir/src/status.c.o CMakeFiles/rtp2httpd.dir/src/connection.c.o CMakeFiles/rtp2httpd.dir/src/worker.c.o CMakeFiles/rtp2httpd.dir/src/unix_socket.c.o CMakeFiles/rtp2httpd.dir/src/buffer_pool.c.o CMakeFiles/rtp2httpd.dir/src/zerocopy.c.o CMakeFiles/rtp2httpd.dir/src/m3u.c.o CMakeFiles/rtp2httpd.dir/src/epg.c.o CMakeFiles/rtp2httpd.dir/src/embedded_web.c.o CMakeFiles/rtp2httpd.dir/src/utils.c.o CMakeFiles/rtp2httpd.dir/src/vendor/hashmap/hashmap.c.o CMakeFiles/rtp2httpd.dir/src/vendor/md5/md5.c.o CMakeFiles/rtp2httpd.dir/src/vendor/picohttpparser/picohttpparser.c.o CMakeFiles/rtp2httpd.dir/src/poller_epoll.c.o -o rtp2httpd -lm -lrt \n", + "msd_lite_compile_flags": "# CMAKE generated file: DO NOT EDIT!\n# Generated by \"Unix Makefiles\" Generator, CMake Version 3.28\n\n# compile C with /usr/bin/cc\nC_DEFINES = -DHAVE_ACCEPT4 -DHAVE_CONFIG_H -DHAVE_EXPLICIT_BZERO -DHAVE_MEMMEM -DHAVE_MEMRCHR -DHAVE_PIPE2 -DHAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCLOSEFROM_NP -DHAVE_PTHREAD_SETNAME_NP -DHAVE_REALLOCARRAY -DHAVE_SOCK_CLOEXEC -DHAVE_SOCK_NONBLOCK -DHAVE_STRLCPY -DHAVE_STRNCASECMP -DHTTP_SRV_XML_CONFIG -DLINUX -DNDEBUG -DSOCKET_XML_CONFIG -DTHREAD_POOL_SETTINGS_XML -D_GNU_SOURCE -D__USE_GNU=1\n\nC_INCLUDES = -I/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd -I/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/src -I/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/src/liblcb/include -I/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src\n\nC_FLAGS = -pipe -fno-delete-null-pointer-checks -std=c11 -D_FORTIFY_SOURCE=2 -fstack-protector-all -fwrapv -fPIE -Wno-switch-default -Wno-unused-result -Wno-unsafe-buffer-usage -O3 -DNDEBUG -flto=auto -finline-functions -funroll-loops -ftree-vectorize -ffast-math -fomit-frame-pointer -finline-limit=600\n\n", + "msd_lite_link_command": "/usr/bin/cc -pipe -fno-delete-null-pointer-checks -std=c11 -D_FORTIFY_SOURCE=2 -fstack-protector-all -fwrapv -fPIE -Wno-switch-default -Wno-unused-result -Wno-unsafe-buffer-usage -O3 -DNDEBUG -flto=auto -finline-functions -funroll-loops -ftree-vectorize -ffast-math -fomit-frame-pointer -finline-limit=600 -pie -Wl,-z,retpolineplt -Wl,-z,relro -Wl,-z,now -Wl,-z,noexecstack CMakeFiles/msd_lite.dir/msd_lite.c.o CMakeFiles/msd_lite.dir/msd_lite_stat_text.c.o CMakeFiles/msd_lite.dir/stream_sys.c.o CMakeFiles/msd_lite.dir/liblcb/src/net/socket.c.o CMakeFiles/msd_lite.dir/liblcb/src/net/socket_address.c.o CMakeFiles/msd_lite.dir/liblcb/src/net/socket_options.c.o CMakeFiles/msd_lite.dir/liblcb/src/net/utils.c.o CMakeFiles/msd_lite.dir/liblcb/src/proto/http.c.o CMakeFiles/msd_lite.dir/liblcb/src/proto/http_server.c.o CMakeFiles/msd_lite.dir/liblcb/src/threadpool/threadpool.c.o CMakeFiles/msd_lite.dir/liblcb/src/threadpool/threadpool_msg_sys.c.o CMakeFiles/msd_lite.dir/liblcb/src/threadpool/threadpool_task.c.o CMakeFiles/msd_lite.dir/liblcb/src/utils/cmd_line_daemon.c.o CMakeFiles/msd_lite.dir/liblcb/src/utils/sys.c.o CMakeFiles/msd_lite.dir/liblcb/src/utils/sys_res_limits_xml.c.o CMakeFiles/msd_lite.dir/liblcb/src/utils/buf_str.c.o CMakeFiles/msd_lite.dir/liblcb/src/utils/info.c.o CMakeFiles/msd_lite.dir/liblcb/src/utils/ring_buffer.c.o CMakeFiles/msd_lite.dir/liblcb/src/utils/xml.c.o -o msd_lite /usr/lib/aarch64-linux-gnu/libpthread.a -lrt /usr/lib/aarch64-linux-gnu/libpthread.a -lrt -pie -Wl,-z,retpolineplt -Wl,-z,relro -Wl,-z,now -Wl,-z,noexecstack \n" +} diff --git a/tools/stress-test/results/2026-09-06/shared64/metadata.json b/tools/stress-test/results/2026-09-06/shared64/metadata.json new file mode 100644 index 00000000..f0d3baab --- /dev/null +++ b/tools/stress-test/results/2026-09-06/shared64/metadata.json @@ -0,0 +1,44 @@ +{ + "timestamp_utc": "2026-09-06T10:20:46Z", + "platform": "Linux-6.8.0-138-generic-aarch64-with-glibc2.39", + "machine": "aarch64", + "cpu_count": 16, + "server_cpu": 0, + "load_cpus": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], + "controller_cpu": 13, + "warmup_s": 5.0, + "duration_s": 20, + "repetitions": 5, + "cases": { + "shared64": [64, 1, 20] + }, + "script_sha256": "f449923fc4aa4b32fe382ef8aa2bce7c446fe108ceab0a55fc1990d55e31b6bf", + "binaries": { + "rtp2httpd": { + "path": "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", + "revision": "530dc980e92db6b6ea98b6ca223dffe1a0345b5c", + "sha256": "74f861fcae8b894774928ec89b24436cbf487692b98df84921bd1991301a23ca" + }, + "msd_lite": { + "path": "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", + "revision": "fa68e131343fb58c67ad77b2d26f2cb7c49a2c95", + "sha256": "45459818ceae0a7595406b0aec5cf999cd27bb4efa822494414a6ca397bc88a7" + }, + "udpxy": { + "path": "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", + "revision": "31d4bcfabaade59d3efdee015df7979febf76bae", + "sha256": "71037623a56bed03b8797c4b849a90f7a0924ac2e6998c178ff0498c734c06ae" + }, + "tvgate": { + "path": "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", + "revision": "v3.2.0", + "sha256": "eca4c04f3973316e716be3264c690e3215fd68624c15df8a709f0e627df5307e" + }, + "baseline": { + "path": "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/baseline/build/rtp2httpd", + "revision": "f8c243cb6fc98e259fd2f2fe0dc992f2a845014e", + "sha256": "68fc68a6d67da5786508a95a2f97437bd1c4242e5dae3ba1d5d15294f15ccafe" + } + }, + "sysctl": "net.core.rmem_max = 212992\nnet.core.wmem_max = 212992\nnet.ipv4.tcp_congestion_control = cubic" +} diff --git a/tools/stress-test/results/2026-09-06/shared64/summary.json b/tools/stress-test/results/2026-09-06/shared64/summary.json new file mode 100644 index 00000000..98005773 --- /dev/null +++ b/tools/stress-test/results/2026-09-06/shared64/summary.json @@ -0,0 +1,95 @@ +[ + { + "case": "shared64", + "program": "rtp2httpd", + "valid_trials": 5, + "total_trials": 5, + "cpu_pct": { + "mean": 6.970806833409041, + "min": 6.327807413776387, + "max": 7.676655957103553 + }, + "pss_mib": { + "mean": 4.60861328125, + "min": 4.5712890625, + "max": 4.6337890625 + }, + "uss_mib": { + "mean": 3.9875, + "min": 3.94921875, + "max": 4.01171875 + } + }, + { + "case": "shared64", + "program": "msd_lite", + "valid_trials": 5, + "total_trials": 5, + "cpu_pct": { + "mean": 5.911672985938835, + "min": 5.586135256424014, + "max": 6.476307948977287 + }, + "pss_mib": { + "mean": 1.36888671875, + "min": 1.3681640625, + "max": 1.369140625 + }, + "uss_mib": { + "mean": 1.35546875, + "min": 1.35546875, + "max": 1.35546875 + } + }, + { + "case": "shared64", + "program": "udpxy", + "valid_trials": 2, + "total_trials": 5, + "cpu_pct": { + "mean": 56.749177430157616, + "min": 54.79720868821048, + "max": 58.70114617210474 + }, + "pss_mib": { + "mean": 4.606201171875, + "min": 4.60302734375, + "max": 4.609375 + }, + "uss_mib": { + "mean": 4.015625, + "min": 4.01171875, + "max": 4.01953125 + } + }, + { + "case": "shared64", + "program": "tvgate", + "valid_trials": 0, + "total_trials": 5, + "cpu_pct": null, + "pss_mib": null, + "uss_mib": null + }, + { + "case": "shared64", + "program": "baseline", + "valid_trials": 5, + "total_trials": 5, + "cpu_pct": { + "mean": 30.100316076468307, + "min": 29.065688183265927, + "max": 31.37242538962098 + }, + "pss_mib": { + "mean": 10.278642578125, + "min": 8.98515625, + "max": 11.922607421875 + }, + "uss_mib": { + "mean": 9.6028125, + "min": 8.34375, + "max": 11.23828125 + } + } +] diff --git a/tools/stress-test/results/2026-09-06/shared64/trials.jsonl b/tools/stress-test/results/2026-09-06/shared64/trials.jsonl new file mode 100644 index 00000000..8b519397 --- /dev/null +++ b/tools/stress-test/results/2026-09-06/shared64/trials.jsonl @@ -0,0 +1,25 @@ +{"program": "rtp2httpd", "case": "shared64", "repetition": 0, "order": 0, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:42169"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.070143052000276, "cpu_pct": 6.327807413776387, "user_cpu_pct": 1.145980870211472, "system_cpu_pct": 5.181826543564916, "pss_mib": 4.6337890625, "uss_mib": 4.01171875, "server_processes": 2, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 20.022990716050153, 19.997287261985903], "source_mbps": [20.00043462370806], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 73.59190196966712, "family_stable": true} +{"program": "msd_lite", "case": "shared64", "repetition": 0, "order": 1, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", "-c", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-final64/shared64-00-msd_lite.xml", "-l", "1"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.06052855100006, "cpu_pct": 5.8323488188533945, "user_cpu_pct": 0.598189622446502, "system_cpu_pct": 5.2341591964068925, "pss_mib": 1.369140625, "uss_mib": 1.35546875, "server_processes": 1, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.996525165335303, 19.970434526762673, 19.970434526762673, 19.996525165335303, 19.996525165335303, 19.970434526762673, 19.970434526762673, 19.996525165335303, 19.996525165335303, 19.996525165335303, 19.970434526762673, 19.996525165335303, 19.996525165335303, 19.996525165335303, 19.970434526762673, 19.996525165335303, 19.996525165335303, 19.996525165335303, 19.970434526762673, 19.996525165335303, 19.996525165335303, 19.996525165335303, 19.970434526762673, 19.996525165335303], "source_mbps": [19.998474465918314], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 80.65590075987002, "family_stable": true} +{"program": "udpxy", "case": "shared64", "repetition": 0, "order": 2, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", "-T", "-a", "127.0.0.1", "-m", "lo", "-p", "47477", "-c", "256"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.067751259000033, "cpu_pct": 58.70114617210474, "user_cpu_pct": 5.3319377253100235, "system_cpu_pct": 53.36920844679472, "pss_mib": 4.609375, "uss_mib": 4.01953125, "server_processes": 65, "server_threads": 65, "multicast_sockets": 64, "client_mbps": [20.000719902285656, 20.001769147900085, 20.000719902285656, 20.001769147900085, 20.000719902285656, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.000719902285656, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.000719902285656, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.000719902285656, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.000719902285656, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.000719902285656, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.000719902285656, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.000719902285656, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.000719902285656, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.000719902285656, 20.001769147900085, 20.001769147900085, 20.00124452509287, 20.000719902285656, 20.001769147900085, 20.001769147900085, 20.001769147900085], "source_mbps": [20.001769147900085], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 141.02227815539595, "family_stable": true} +{"program": "tvgate", "case": "shared64", "repetition": 0, "order": 3, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", "-config", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-final64/shared64-00-tvgate.yaml"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": false, "request_prefix": "udp", "duration_s": 20.04664534300082, "cpu_pct": 47.98807897980184, "user_cpu_pct": 16.21218884452765, "system_cpu_pct": 31.775890135274192, "pss_mib": 44.513671875, "uss_mib": 44.513671875, "server_processes": 1, "server_threads": 7, "multicast_sockets": 1, "client_mbps": [20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196], "source_mbps": [20.000770460080446], "gaps": 3833536, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 547648, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 163.91770013267032, "family_stable": true} +{"program": "baseline", "case": "shared64", "repetition": 0, "order": 4, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/baseline/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:56817"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.067731551999714, "cpu_pct": 30.098070548477732, "user_cpu_pct": 5.730592902441953, "system_cpu_pct": 24.36747764603578, "pss_mib": 11.0478515625, "uss_mib": 10.36328125, "server_processes": 2, "server_threads": 2, "multicast_sockets": 64, "client_mbps": [20.002313413446128, 20.002313413446128, 20.013330503216697, 20.002313413446128, 20.002313413446128, 20.002313413446128, 20.013330503216697, 20.002313413446128, 20.002313413446128, 20.002313413446128, 20.013330503216697, 20.002313413446128, 20.002313413446128, 20.002313413446128, 20.013330503216697, 20.002313413446128, 20.002313413446128, 20.002313413446128, 20.013330503216697, 19.97555762400333, 20.002313413446128, 20.002313413446128, 20.013330503216697, 19.993394816965196, 20.002313413446128, 20.002313413446128, 20.013330503216697, 19.993394816965196, 20.002313413446128, 20.002313413446128, 20.013330503216697, 19.993394816965196, 20.002313413446128, 20.002313413446128, 20.013330503216697, 20.019625983085593, 20.002313413446128, 20.002313413446128, 20.013330503216697, 20.019625983085593, 20.002313413446128, 20.002313413446128, 20.013330503216697, 20.019625983085593, 20.002313413446128, 20.002313413446128, 20.013330503216697, 20.019625983085593, 20.002313413446128, 20.028544579566525, 20.015428996506326, 20.019625983085593, 20.028544579566525, 20.028544579566525, 20.015428996506326, 20.019625983085593, 20.028544579566525, 20.001788790123722, 20.015428996506326, 20.019625983085593, 20.028544579566525, 20.001788790123722, 19.993394816965196, 20.015428996506326], "source_mbps": [20.002313413446128], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 54.116729496104014, "family_stable": true} +{"program": "rtp2httpd", "case": "shared64", "repetition": 1, "order": 0, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:39729"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.056729425999947, "cpu_pct": 7.079918015742013, "user_cpu_pct": 1.3461815945424955, "system_cpu_pct": 5.733736421199517, "pss_mib": 4.5712890625, "uss_mib": 3.94921875, "server_processes": 2, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332], "source_mbps": [20.00016291190511], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 74.93744209619892, "family_stable": true} +{"program": "baseline", "case": "shared64", "repetition": 1, "order": 1, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/baseline/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:50067"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.023609842999576, "cpu_pct": 29.065688183265927, "user_cpu_pct": 5.543455993715666, "system_cpu_pct": 23.52223218955026, "pss_mib": 9.0892578125, "uss_mib": 8.4046875, "server_processes": 2, "server_threads": 2, "multicast_sockets": 64, "client_mbps": [20.021150788660446, 19.988026691397238, 20.010635202227682, 20.006954746976213, 20.021150788660446, 19.988026691397238, 19.98382045682413, 19.988026691397238, 20.021150788660446, 19.988026691397238, 19.98382045682413, 20.01431565747915, 20.021150788660446, 19.988026691397238, 19.98382045682413, 20.01431565747915, 20.021150788660446, 19.988026691397238, 19.98382045682413, 20.01431565747915, 20.010635202227682, 19.988026691397238, 19.98382045682413, 20.01431565747915, 20.010635202227682, 19.988026691397238, 19.98382045682413, 20.01431565747915, 19.98382045682413, 19.988026691397238, 19.98382045682413, 19.99118136732707, 19.98382045682413, 19.988026691397238, 19.98382045682413, 19.99118136732707, 19.98382045682413, 19.988026691397238, 19.98382045682413, 20.021150788660446, 19.98382045682413, 19.988026691397238, 19.985923574110686, 20.021150788660446, 19.98382045682413, 20.01431565747915, 19.985923574110686, 20.021150788660446, 19.98382045682413, 20.01431565747915, 19.985923574110686, 19.994336043256897, 19.98382045682413, 20.01431565747915, 19.985923574110686, 19.994336043256897, 19.98382045682413, 20.01431565747915, 19.985923574110686, 19.994336043256897, 19.98382045682413, 20.006954746976213, 20.023779685268636, 19.994336043256897], "source_mbps": [20.000119615794915], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 53.03738977771205, "family_stable": true} +{"program": "tvgate", "case": "shared64", "repetition": 1, "order": 2, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", "-config", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-final64/shared64-01-tvgate.yaml"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": false, "request_prefix": "udp", "duration_s": 20.041318133999994, "cpu_pct": 48.40001009486497, "user_cpu_pct": 16.515879733402375, "system_cpu_pct": 31.88413036146259, "pss_mib": 40.41484375, "uss_mib": 40.41484375, "server_processes": 1, "server_threads": 7, "multicast_sockets": 1, "client_mbps": [19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 20.000833743563593, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 20.000833743563593, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611], "source_mbps": [19.99978311406611], "gaps": 3798606, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 542658, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 165.8074572631302, "family_stable": true} +{"program": "udpxy", "case": "shared64", "repetition": 1, "order": 3, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", "-T", "-a", "127.0.0.1", "-m", "lo", "-p", "38903", "-c", "256"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": false, "request_prefix": "rtp", "duration_s": 20.068672177000735, "cpu_pct": 59.396057172435434, "user_cpu_pct": 5.5808375866717865, "system_cpu_pct": 53.81521958576365, "pss_mib": 4.35546875, "uss_mib": 3.765625, "server_processes": 65, "server_threads": 65, "multicast_sockets": 64, "client_mbps": [19.984588739240596, 19.998228306302426, 20.001375898701305, 20.001375898701305, 19.989310127838923, 19.998228306302426, 20.001375898701305, 20.001375898701305, 20.001375898701305, 20.001375898701305, 19.981441146841718, 20.001375898701305, 20.001375898701305, 20.001375898701305, 19.981441146841718, 19.977768955709685, 19.99875290503557, 20.001375898701305, 20.001375898701305, 20.001375898701305, 20.001375898701305, 19.998228306302426, 19.98196574557486, 20.001375898701305, 19.977244356976538, 20.001375898701305, 19.98091654810857, 19.995605312636688, 20.001375898701305, 19.998228306302426, 19.981441146841718, 19.98773633163948, 20.001375898701305, 19.998228306302426, 19.981441146841718, 20.001375898701305, 20.001375898701305, 20.001375898701305, 19.987211732906335, 20.001375898701305, 19.98249034430801, 19.983539541774302, 19.981441146841718, 20.001375898701305, 20.001375898701305, 20.001375898701305, 19.985637936706894, 20.001375898701305, 20.001375898701305, 20.001375898701305, 19.976195159510244, 19.98196574557486, 19.983014943041155, 20.001375898701305, 19.977244356976538, 20.001375898701305, 19.985637936706894, 19.983539541774302, 19.983539541774302, 19.98091654810857, 20.001375898701305, 19.983014943041155, 19.998228306302426, 19.984588739240596], "source_mbps": [20.001375898701305], "gaps": 6909, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 987, "load_cpu_pct": 144.35434364810862, "family_stable": true} +{"program": "msd_lite", "case": "shared64", "repetition": 1, "order": 4, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", "-c", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-final64/shared64-01-msd_lite.xml", "-l", "1"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.06174371799989, "cpu_pct": 5.881841661357059, "user_cpu_pct": 0.548307273516336, "system_cpu_pct": 5.333534387840723, "pss_mib": 1.369140625, "uss_mib": 1.35546875, "server_processes": 1, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528], "source_mbps": [20.000411810664037], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 73.97163580893114, "family_stable": true} +{"program": "udpxy", "case": "shared64", "repetition": 2, "order": 0, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", "-T", "-a", "127.0.0.1", "-m", "lo", "-p", "38485", "-c", "256"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": false, "request_prefix": "rtp", "duration_s": 20.063495135000267, "cpu_pct": 57.218345670856856, "user_cpu_pct": 5.532435861903407, "system_cpu_pct": 51.68590980895345, "pss_mib": 4.35546875, "uss_mib": 3.765625, "server_processes": 65, "server_threads": 65, "multicast_sockets": 64, "client_mbps": [20.00128957092573, 20.00128957092573, 19.99971536863508, 19.99866590044131, 20.00128957092573, 20.00128957092573, 19.999190634538195, 19.998141166344432, 20.00076483682885, 20.00128957092573, 20.000240102731965, 19.998141166344432, 20.00076483682885, 20.00128957092573, 20.001814305022616, 19.99971536863508, 20.00128957092573, 20.00128957092573, 19.99971536863508, 20.00128957092573, 20.00128957092573, 20.00128957092573, 20.00128957092573, 19.991844357181822, 20.00128957092573, 20.00128957092573, 20.00076483682885, 19.992369091278707, 20.00128957092573, 20.001814305022616, 20.00128957092573, 19.99289382537559, 20.00128957092573, 20.00128957092573, 19.996042229956892, 19.99289382537559, 20.00128957092573, 20.00128957092573, 19.997616432247547, 19.99289382537559, 20.00128957092573, 20.00128957092573, 20.001814305022616, 19.99446802766624, 20.00128957092573, 20.00128957092573, 19.998141166344432, 19.99289382537559, 20.00076483682885, 20.00128957092573, 19.997616432247547, 19.99446802766624, 20.00128957092573, 20.00128957092573, 19.997616432247547, 19.99446802766624, 20.00128957092573, 20.00128957092573, 19.998141166344432, 19.99289382537559, 20.00128957092573, 20.00128957092573, 19.996566964053777, 20.001814305022616], "source_mbps": [20.00128957092573], "gaps": 1687, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 241, "load_cpu_pct": 150.372603561825, "family_stable": true} +{"program": "tvgate", "case": "shared64", "repetition": 2, "order": 1, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", "-config", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-final64/shared64-02-tvgate.yaml"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": false, "request_prefix": "udp", "duration_s": 20.05543863499952, "cpu_pct": 52.35487585734498, "user_cpu_pct": 18.249413870274537, "system_cpu_pct": 34.10546198707044, "pss_mib": 40.42578125, "uss_mib": 40.42578125, "server_processes": 1, "server_threads": 6, "multicast_sockets": 1, "client_mbps": [20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498], "source_mbps": [19.9998753106309], "gaps": 3859968, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 551424, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 173.9179114194469, "family_stable": true} +{"program": "baseline", "case": "shared64", "repetition": 2, "order": 2, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/baseline/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:45707"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.05155755099986, "cpu_pct": 30.77067696265987, "user_cpu_pct": 5.585601004567109, "system_cpu_pct": 25.18507595809276, "pss_mib": 11.922607421875, "uss_mib": 11.23828125, "server_processes": 2, "server_threads": 2, "multicast_sockets": 64, "client_mbps": [20.01162208867865, 19.98957013591262, 20.01162208867865, 19.98957013591262, 19.98957013591262, 19.98957013591262, 20.01162208867865, 19.98957013591262, 19.98957013591262, 19.98957013591262, 20.01162208867865, 20.01162208867865, 20.009521902700936, 19.98957013591262, 20.01162208867865, 20.01162208867865, 19.98957013591262, 20.004271437756643, 20.01162208867865, 20.01162208867865, 20.009521902700936, 20.004271437756643, 20.01162208867865, 20.01162208867865, 20.009521902700936, 20.004271437756643, 20.01162208867865, 20.01162208867865, 20.009521902700936, 19.98957013591262, 20.01162208867865, 20.01162208867865, 20.009521902700936, 19.98957013591262, 20.01162208867865, 20.01162208867865, 20.009521902700936, 19.98957013591262, 20.01162208867865, 20.01162208867865, 19.99954601930678, 19.98957013591262, 20.01162208867865, 20.01162208867865, 19.99954601930678, 19.98957013591262, 20.01162208867865, 20.01162208867865, 19.99954601930678, 19.98957013591262, 20.01162208867865, 20.01162208867865, 20.001646205284494, 19.98957013591262, 20.01162208867865, 20.01162208867865, 20.001646205284494, 19.98957013591262, 20.01162208867865, 20.01162208867865, 19.98957013591262, 19.98957013591262, 20.01162208867865, 20.01162208867865], "source_mbps": [20.000071065801208], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 52.913595230765196, "family_stable": true} +{"program": "rtp2httpd", "case": "shared64", "repetition": 2, "order": 3, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:60293"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.060818259999905, "cpu_pct": 7.676655957103553, "user_cpu_pct": 1.3459072132584151, "system_cpu_pct": 6.330748743845138, "pss_mib": 4.61005859375, "uss_mib": 3.98828125, "server_processes": 2, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967], "source_mbps": [19.999760069607543], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 75.86928809552992, "family_stable": true} +{"program": "msd_lite", "case": "shared64", "repetition": 2, "order": 4, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", "-c", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-final64/shared64-02-msd_lite.xml", "-l", "1"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.07316530099979, "cpu_pct": 6.476307948977287, "user_cpu_pct": 0.49817753453671443, "system_cpu_pct": 5.978130414440573, "pss_mib": 1.3681640625, "uss_mib": 1.35546875, "server_processes": 1, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 20.01023565426395, 19.98416144064833, 19.98416144064833, 20.01023565426395, 20.01023565426395, 19.98416144064833, 19.98416144064833, 20.01023565426395, 20.01023565426395, 20.01023565426395, 19.98416144064833, 20.01023565426395, 20.01023565426395, 20.01023565426395, 19.98416144064833, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395], "source_mbps": [20.000570213009887], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 76.02189177030262, "family_stable": true} +{"program": "udpxy", "case": "shared64", "repetition": 3, "order": 0, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", "-T", "-a", "127.0.0.1", "-m", "lo", "-p", "44571", "-c", "256"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": false, "request_prefix": "rtp", "duration_s": 20.071070592999604, "cpu_pct": 60.73417929311471, "user_cpu_pct": 5.82928546127516, "system_cpu_pct": 54.904893831839544, "pss_mib": 4.606689453125, "uss_mib": 4.01953125, "server_processes": 65, "server_threads": 65, "multicast_sockets": 64, "client_mbps": [19.98324972958297, 19.97695529703565, 19.980102513309312, 20.00055941908808, 19.98324972958297, 20.00055941908808, 19.985872409811016, 20.001083955133687, 19.980627049354922, 19.97852890517248, 20.00213302722491, 20.00055941908808, 19.985872409811016, 19.9848233377198, 20.001608491179297, 19.98901962608467, 19.9848233377198, 19.99164230631272, 20.001608491179297, 19.987970553993453, 20.00213302722491, 19.98115158540053, 20.001608491179297, 19.99164230631272, 19.985872409811016, 19.985347873765406, 19.985872409811016, 19.987446017947846, 19.9848233377198, 19.986396945856622, 19.993740450495157, 19.99164230631272, 19.9848233377198, 19.9848233377198, 20.00055941908808, 20.001083955133687, 19.9848233377198, 19.985872409811016, 19.977479833081265, 19.979053441218092, 19.9848233377198, 19.979577977263705, 19.987446017947846, 19.986396945856622, 19.985872409811016, 19.986921481902233, 20.001608491179297, 19.990068698175893, 19.9848233377198, 19.986396945856622, 20.00055941908808, 19.988495090039063, 19.9848233377198, 20.001083955133687, 20.001608491179297, 19.988495090039063, 19.985872409811016, 20.00213302722491, 20.00055941908808, 19.986921481902233, 19.98324972958297, 20.00055941908808, 20.001083955133687, 19.984298801674186], "source_mbps": [20.001083955133687], "gaps": 9667, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 1381, "load_cpu_pct": 159.3836255608482, "family_stable": true} +{"program": "msd_lite", "case": "shared64", "repetition": 3, "order": 1, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", "-c", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-final64/shared64-03-msd_lite.xml", "-l", "1"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.049639842000033, "cpu_pct": 5.586135256424014, "user_cpu_pct": 0.44888586881978687, "system_cpu_pct": 5.137249387604228, "pss_mib": 1.368994140625, "uss_mib": 1.35546875, "server_processes": 1, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147], "source_mbps": [20.000408743501822], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 76.26072149171713, "family_stable": true} +{"program": "rtp2httpd", "case": "shared64", "repetition": 3, "order": 2, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:36501"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.044193718000315, "cpu_pct": 7.084345820928657, "user_cpu_pct": 1.0476849453486043, "system_cpu_pct": 6.036660875580053, "pss_mib": 4.61396484375, "uss_mib": 3.9921875, "server_processes": 2, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382], "source_mbps": [20.000590577009994], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 77.67835523370368, "family_stable": true} +{"program": "baseline", "case": "shared64", "repetition": 3, "order": 3, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/baseline/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:38733"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.04945400900033, "cpu_pct": 31.37242538962098, "user_cpu_pct": 5.536310362874291, "system_cpu_pct": 25.836115026746686, "pss_mib": 10.34833984375, "uss_mib": 9.6640625, "server_processes": 2, "server_threads": 2, "multicast_sockets": 64, "client_mbps": [19.999018817170896, 19.996393309265475, 19.999018817170896, 20.00584513772499, 19.999018817170896, 19.996393309265475, 19.999018817170896, 20.00584513772499, 19.999018817170896, 19.996393309265475, 19.999018817170896, 20.00584513772499, 19.999018817170896, 20.0021694266574, 19.999018817170896, 20.00584513772499, 20.00584513772499, 19.999018817170896, 19.999018817170896, 20.00584513772499, 20.00584513772499, 19.999018817170896, 19.999018817170896, 20.00584513772499, 20.00584513772499, 19.999018817170896, 19.999018817170896, 20.00584513772499, 20.00584513772499, 19.999018817170896, 19.999018817170896, 20.00584513772499, 20.00584513772499, 19.999018817170896, 19.999018817170896, 20.00584513772499, 20.00584513772499, 19.999018817170896, 19.999018817170896, 20.00584513772499, 20.00584513772499, 19.999018817170896, 19.999018817170896, 20.00584513772499, 20.00584513772499, 19.999018817170896, 19.999018817170896, 20.00584513772499, 20.00584513772499, 19.999018817170896, 19.999018817170896, 20.00584513772499, 20.00584513772499, 19.999018817170896, 19.999018817170896, 20.00584513772499, 20.00584513772499, 19.999018817170896, 19.999018817170896, 20.007945544049328, 20.00584513772499, 19.999018817170896, 19.999018817170896, 19.996393309265475], "source_mbps": [20.00006902033306], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 55.16359694899968, "family_stable": true} +{"program": "tvgate", "case": "shared64", "repetition": 3, "order": 4, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", "-config", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-final64/shared64-03-tvgate.yaml"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": false, "request_prefix": "udp", "duration_s": 20.0317617590008, "cpu_pct": 46.925478213499275, "user_cpu_pct": 16.12439304570241, "system_cpu_pct": 30.801085167796867, "pss_mib": 40.796875, "uss_mib": 40.796875, "server_processes": 1, "server_threads": 7, "multicast_sockets": 1, "client_mbps": [19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814], "source_mbps": [19.999864056888814], "gaps": 3822483, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 546069, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 164.68846024078096, "family_stable": true} +{"program": "baseline", "case": "shared64", "repetition": 4, "order": 0, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/baseline/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:53909"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.037870343001487, "cpu_pct": 29.194719298317032, "user_cpu_pct": 4.840833798182482, "system_cpu_pct": 24.35388550013455, "pss_mib": 8.98515625, "uss_mib": 8.34375, "server_processes": 2, "server_threads": 2, "multicast_sockets": 64, "client_mbps": [20.01688488517885, 20.01583407490612, 20.013732454360667, 20.013732454360667, 20.01688488517885, 20.01583407490612, 20.013732454360667, 20.013732454360667, 20.01688488517885, 20.01583407490612, 20.013732454360667, 20.013732454360667, 20.01688488517885, 20.01583407490612, 20.013732454360667, 20.013732454360667, 20.01688488517885, 20.01583407490612, 20.013732454360667, 20.013732454360667, 20.01583407490612, 20.01583407490612, 20.013732454360667, 20.00847840299703, 20.01583407490612, 20.01583407490612, 20.013732454360667, 20.00847840299703, 20.01583407490612, 20.01583407490612, 20.013732454360667, 20.00847840299703, 20.01583407490612, 20.01583407490612, 20.013732454360667, 20.00847840299703, 20.01583407490612, 20.01583407490612, 20.013732454360667, 20.00847840299703, 20.01583407490612, 20.01583407490612, 20.013732454360667, 20.00847840299703, 20.01583407490612, 20.01583407490612, 20.013732454360667, 20.00847840299703, 20.01583407490612, 20.01583407490612, 20.013732454360667, 20.00847840299703, 20.01583407490612, 20.01583407490612, 20.013732454360667, 20.010580023542484, 20.01583407490612, 20.01583407490612, 20.013732454360667, 20.010580023542484, 20.01583407490612, 20.01583407490612, 20.013732454360667, 20.013732454360667], "source_mbps": [20.000071920815216], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 50.204936092490485, "family_stable": true} +{"program": "rtp2httpd", "case": "shared64", "repetition": 4, "order": 1, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:54461"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.04395621799995, "cpu_pct": 6.685306959494595, "user_cpu_pct": 1.197368410655748, "system_cpu_pct": 5.487938548838846, "pss_mib": 4.61396484375, "uss_mib": 3.99609375, "server_processes": 2, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 19.997676089515842], "source_mbps": [20.000302317563218], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 76.78124933329985, "family_stable": true} +{"program": "msd_lite", "case": "shared64", "repetition": 4, "order": 2, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", "-c", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-final64/shared64-04-msd_lite.xml", "-l", "1"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.063194759999533, "cpu_pct": 5.781731244082421, "user_cpu_pct": 0.49842510724848454, "system_cpu_pct": 5.283306136833936, "pss_mib": 1.368994140625, "uss_mib": 1.35546875, "server_processes": 1, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926], "source_mbps": [20.001064277163472], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 78.8010094559854, "family_stable": true} +{"program": "udpxy", "case": "shared64", "repetition": 4, "order": 3, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", "-T", "-a", "127.0.0.1", "-m", "lo", "-p", "52219", "-c", "256"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.055766092998965, "cpu_pct": 54.79720868821048, "user_cpu_pct": 5.384985021225416, "system_cpu_pct": 49.412223666985064, "pss_mib": 4.60302734375, "uss_mib": 4.01171875, "server_processes": 65, "server_threads": 65, "multicast_sockets": 64, "client_mbps": [20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001648510451677, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001648510451677, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001648510451677, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052], "source_mbps": [20.001648510451677], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 150.33083184254286, "family_stable": true} +{"program": "tvgate", "case": "shared64", "repetition": 4, "order": 4, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", "-config", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-final64/shared64-04-tvgate.yaml"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": false, "request_prefix": "udp", "duration_s": 20.028976925999814, "cpu_pct": 53.3726712028072, "user_cpu_pct": 19.27207772150007, "system_cpu_pct": 34.10059348130712, "pss_mib": 41.3240234375, "uss_mib": 41.3240234375, "server_processes": 1, "server_threads": 6, "multicast_sockets": 1, "client_mbps": [20.000542288307777, 20.001067926738482, 20.001067926738482, 20.001067926738482, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.001067926738482, 20.000542288307777, 20.000542288307777, 20.001067926738482, 20.000016649877075, 20.001067926738482, 20.000016649877075, 20.001067926738482, 20.000016649877075, 20.001593565169184, 20.001067926738482, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.001593565169184, 20.001067926738482, 20.000016649877075, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.001067926738482, 20.000542288307777, 20.000542288307777, 20.001067926738482, 20.000542288307777, 20.001067926738482, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.000016649877075, 20.000542288307777, 20.001067926738482, 20.000542288307777, 20.001067926738482, 20.001067926738482, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.000016649877075, 20.001593565169184, 20.001067926738482, 20.001067926738482, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.001067926738482, 20.000542288307777], "source_mbps": [20.000542288307777], "gaps": 4019246, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 574178, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 176.49428690544755, "family_stable": true} diff --git a/tools/stress-test/stress_test.py b/tools/stress-test/stress_test.py index 66d7b311..5bd8d954 100755 --- a/tools/stress-test/stress_test.py +++ b/tools/stress-test/stress_test.py @@ -223,7 +223,7 @@ def get_process_rss(pid: int) -> float | None: class ResourceMonitor(threading.Thread): """Thread that monitors CPU and memory usage of processes. - Uses top for CPU measurement (more accurate, especially for io_uring programs) + Uses top for interactive CPU sampling; benchmark.py measures complete trial windows. and /proc for memory measurement. """ From 2825d08e37a47178e5e9601fe12375234693c5cb Mon Sep 17 00:00:00 2001 From: Stackie Jia Date: Sun, 6 Sep 2026 19:00:57 +0800 Subject: [PATCH 08/19] perf(buffer): share immutable batch pages on FreeBSD --- src/buffer_pool.c | 12 +- .../stress-test/results/2026-09-06/README.md | 15 -- .../2026-09-06/additional/metadata.json | 41 ---- .../2026-09-06/additional/summary.json | 206 ------------------ .../2026-09-06/additional/trials.jsonl | 36 --- .../results/2026-09-06/build-environment.json | 18 -- .../results/2026-09-06/shared64/metadata.json | 44 ---- .../results/2026-09-06/shared64/summary.json | 95 -------- .../results/2026-09-06/shared64/trials.jsonl | 25 --- 9 files changed, 10 insertions(+), 482 deletions(-) delete mode 100644 tools/stress-test/results/2026-09-06/README.md delete mode 100644 tools/stress-test/results/2026-09-06/additional/metadata.json delete mode 100644 tools/stress-test/results/2026-09-06/additional/summary.json delete mode 100644 tools/stress-test/results/2026-09-06/additional/trials.jsonl delete mode 100644 tools/stress-test/results/2026-09-06/build-environment.json delete mode 100644 tools/stress-test/results/2026-09-06/shared64/metadata.json delete mode 100644 tools/stress-test/results/2026-09-06/shared64/summary.json delete mode 100644 tools/stress-test/results/2026-09-06/shared64/trials.jsonl diff --git a/src/buffer_pool.c b/src/buffer_pool.c index 3ce6991c..08b25b8b 100644 --- a/src/buffer_pool.c +++ b/src/buffer_pool.c @@ -7,7 +7,7 @@ #include #include #include -#ifdef __linux__ +#if defined(__linux__) || defined(__FreeBSD__) #include #include #include @@ -252,12 +252,19 @@ int buffer_ref_sendfile_fd(const buffer_ref_t *ref) { * closed. A fresh snapshot keeps slow sockets safe when pool memory is reused. * Unsupported kernels or allocation failures retain the normal sendmsg path. */ void buffer_ref_snapshot(buffer_ref_t *ref) { -#ifdef __linux__ +#if (defined(__linux__) || defined(__FreeBSD__)) && defined(MFD_ALLOW_SEALING) && defined(F_ADD_SEALS) if (!ref || ref->owner || ref->shared_fd >= 0 || !ref->data_size) return; int fd = memfd_create("rtp2httpd-batch", MFD_CLOEXEC | MFD_ALLOW_SEALING); if (fd < 0) return; +#ifdef __FreeBSD__ + /* FreeBSD shared-memory writes cannot grow the object, unlike Linux memfd. */ + if (ftruncate(fd, (off_t)ref->data_size) < 0) { + close(fd); + return; + } +#endif size_t written = 0; while (written < ref->data_size) { ssize_t n = write(fd, (uint8_t *)ref->data + ref->data_offset + written, ref->data_size - written); @@ -275,6 +282,7 @@ void buffer_ref_snapshot(buffer_ref_t *ref) { } ref->shared_fd = fd; #else + /* macOS sendfile only accepts regular files, not POSIX shared memory. */ (void)ref; #endif } diff --git a/tools/stress-test/results/2026-09-06/README.md b/tools/stress-test/results/2026-09-06/README.md deleted file mode 100644 index 4d3abe8c..00000000 --- a/tools/stress-test/results/2026-09-06/README.md +++ /dev/null @@ -1,15 +0,0 @@ -# Multicast benchmark data — 2026-09-06 - -The Chinese report is [docs/reference/benchmark.md](../../../../docs/reference/benchmark.md); the English translation is [docs/en/reference/benchmark.md](../../../../docs/en/reference/benchmark.md). - -- `shared64/`: five repetitions of one 20 Mbps channel with 64 clients, including the pre-optimization baseline. Warmup: 5 seconds; sampling: 20 seconds. -- `additional/`: three repetitions each of eight distinct 40 Mbps channels, eight clients on one 40 Mbps channel, and one 400 Mbps channel. Warmup: 5 seconds; sampling: 10 seconds. -- `build-environment.json`: compiler, build settings, vendor source revisions, and TVGate release archive digest. TVGate was run with `GOMAXPROCS=1`. - -Every directory contains the original `trials.jsonl` (including failures), `metadata.json` (including binary and harness SHA-256 values), and `summary.json`. Summary means use **valid samples only**, with valid/total counts. Do not interpret missing values as zero usage, or compare CPU from failed delivery as equivalent work. - -The benchmark harness is `tools/stress-test/benchmark.py` at commit `9d0f59aa95449e4d4ccfa362251bae4d2716c211`. Runtime C sources are from `530dc980e92db6b6ea98b6ca223dffe1a0345b5c`. The Web UI was rebuilt before creating the tested binary. Absolute paths in recorded commands refer to isolated build directories on the test VM; use `--binary NAME=PATH` to select equivalent local binaries. - -There are 25 main trials and 36 additional trials. The 64-client optimized and baseline rtp2httpd measurements both passed all five trials. All TVGate samples failed sequence continuity; several udpxy samples and some 400 Mbps samples from other programs also recorded kernel UDP drops. The report records these limitations explicitly. - -Commands, generated configuration rules, affinity, synthetic payload construction, validation, and exit behavior are documented in [the harness README](../../README.md). The measurements represent server-process CPU on Linux loopback, not total machine CPU, physical-NIC capacity, or a video decoding test. diff --git a/tools/stress-test/results/2026-09-06/additional/metadata.json b/tools/stress-test/results/2026-09-06/additional/metadata.json deleted file mode 100644 index a2ff9dbf..00000000 --- a/tools/stress-test/results/2026-09-06/additional/metadata.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "timestamp_utc": "2026-09-06T10:31:53Z", - "platform": "Linux-6.8.0-138-generic-aarch64-with-glibc2.39", - "machine": "aarch64", - "cpu_count": 16, - "server_cpu": 0, - "load_cpus": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], - "controller_cpu": 13, - "warmup_s": 5.0, - "duration_s": 10, - "repetitions": 3, - "cases": { - "distinct8": [8, 8, 40], - "shared8": [8, 1, 40], - "high400": [1, 1, 400] - }, - "script_sha256": "f449923fc4aa4b32fe382ef8aa2bce7c446fe108ceab0a55fc1990d55e31b6bf", - "binaries": { - "rtp2httpd": { - "path": "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", - "revision": "530dc980e92db6b6ea98b6ca223dffe1a0345b5c", - "sha256": "74f861fcae8b894774928ec89b24436cbf487692b98df84921bd1991301a23ca" - }, - "msd_lite": { - "path": "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", - "revision": "fa68e131343fb58c67ad77b2d26f2cb7c49a2c95", - "sha256": "45459818ceae0a7595406b0aec5cf999cd27bb4efa822494414a6ca397bc88a7" - }, - "udpxy": { - "path": "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", - "revision": "31d4bcfabaade59d3efdee015df7979febf76bae", - "sha256": "71037623a56bed03b8797c4b849a90f7a0924ac2e6998c178ff0498c734c06ae" - }, - "tvgate": { - "path": "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", - "revision": "v3.2.0", - "sha256": "eca4c04f3973316e716be3264c690e3215fd68624c15df8a709f0e627df5307e" - } - }, - "sysctl": "net.core.rmem_max = 212992\nnet.core.wmem_max = 212992\nnet.ipv4.tcp_congestion_control = cubic" -} diff --git a/tools/stress-test/results/2026-09-06/additional/summary.json b/tools/stress-test/results/2026-09-06/additional/summary.json deleted file mode 100644 index 70f14577..00000000 --- a/tools/stress-test/results/2026-09-06/additional/summary.json +++ /dev/null @@ -1,206 +0,0 @@ -[ - { - "case": "distinct8", - "program": "rtp2httpd", - "valid_trials": 3, - "total_trials": 3, - "cpu_pct": { - "mean": 9.782727183682368, - "min": 9.082493830103466, - "max": 10.28576201416045 - }, - "pss_mib": { - "mean": 2.2790690104166664, - "min": 2.19912109375, - "max": 2.43037109375 - }, - "uss_mib": { - "mean": 1.659375, - "min": 1.578125, - "max": 1.809375 - } - }, - { - "case": "distinct8", - "program": "msd_lite", - "valid_trials": 3, - "total_trials": 3, - "cpu_pct": { - "mean": 9.486660193023203, - "min": 9.189092772011854, - "max": 9.775612976752964 - }, - "pss_mib": { - "mean": 8.9541015625, - "min": 8.9541015625, - "max": 8.9541015625 - }, - "uss_mib": { - "mean": 8.94140625, - "min": 8.94140625, - "max": 8.94140625 - } - }, - { - "case": "distinct8", - "program": "udpxy", - "valid_trials": 3, - "total_trials": 3, - "cpu_pct": { - "mean": 20.81360032250564, - "min": 20.27327466242048, - "max": 21.281662999838833 - }, - "pss_mib": { - "mean": 0.7973958333333333, - "min": 0.79482421875, - "max": 0.79873046875 - }, - "uss_mib": { - "mean": 0.5286458333333334, - "min": 0.5234375, - "max": 0.53125 - } - }, - { - "case": "distinct8", - "program": "tvgate", - "valid_trials": 0, - "total_trials": 3, - "cpu_pct": null, - "pss_mib": null, - "uss_mib": null - }, - { - "case": "shared8", - "program": "rtp2httpd", - "valid_trials": 3, - "total_trials": 3, - "cpu_pct": { - "mean": 7.20906995120966, - "min": 6.974362646726113, - "max": 7.472789670238363 - }, - "pss_mib": { - "mean": 1.6008138020833333, - "min": 1.59814453125, - "max": 1.6056640625 - }, - "uss_mib": { - "mean": 0.97890625, - "min": 0.9765625, - "max": 0.98359375 - } - }, - { - "case": "shared8", - "program": "msd_lite", - "valid_trials": 3, - "total_trials": 3, - "cpu_pct": { - "mean": 6.074893827412331, - "min": 5.780378502284025, - "max": 6.272567602400795 - }, - "pss_mib": { - "mean": 1.353125, - "min": 1.3525390625, - "max": 1.353515625 - }, - "uss_mib": { - "mean": 1.33984375, - "min": 1.33984375, - "max": 1.33984375 - } - }, - { - "case": "shared8", - "program": "udpxy", - "valid_trials": 3, - "total_trials": 3, - "cpu_pct": { - "mean": 26.932372901684058, - "min": 26.213333030087973, - "max": 27.784988886982884 - }, - "pss_mib": { - "mean": 0.7891927083333333, - "min": 0.769921875, - "max": 0.798828125 - }, - "uss_mib": { - "mean": 0.51953125, - "min": 0.49609375, - "max": 0.53125 - } - }, - { - "case": "shared8", - "program": "tvgate", - "valid_trials": 0, - "total_trials": 3, - "cpu_pct": null, - "pss_mib": null, - "uss_mib": null - }, - { - "case": "high400", - "program": "rtp2httpd", - "valid_trials": 2, - "total_trials": 3, - "cpu_pct": { - "mean": 14.790775435921763, - "min": 13.844029103946436, - "max": 15.737521767897087 - }, - "pss_mib": { - "mean": 1.4345703125, - "min": 1.3564453125, - "max": 1.5126953125 - }, - "uss_mib": { - "mean": 0.8359375, - "min": 0.734375, - "max": 0.9375 - } - }, - { - "case": "high400", - "program": "msd_lite", - "valid_trials": 1, - "total_trials": 3, - "cpu_pct": { - "mean": 13.759667369926044, - "min": 13.759667369926044, - "max": 13.759667369926044 - }, - "pss_mib": { - "mean": 1.341796875, - "min": 1.341796875, - "max": 1.341796875 - }, - "uss_mib": { - "mean": 1.328125, - "min": 1.328125, - "max": 1.328125 - } - }, - { - "case": "high400", - "program": "udpxy", - "valid_trials": 0, - "total_trials": 3, - "cpu_pct": null, - "pss_mib": null, - "uss_mib": null - }, - { - "case": "high400", - "program": "tvgate", - "valid_trials": 0, - "total_trials": 3, - "cpu_pct": null, - "pss_mib": null, - "uss_mib": null - } -] diff --git a/tools/stress-test/results/2026-09-06/additional/trials.jsonl b/tools/stress-test/results/2026-09-06/additional/trials.jsonl deleted file mode 100644 index d5695226..00000000 --- a/tools/stress-test/results/2026-09-06/additional/trials.jsonl +++ /dev/null @@ -1,36 +0,0 @@ -{"program": "rtp2httpd", "case": "distinct8", "repetition": 0, "order": 0, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:42209"], "clients": 8, "sources": 8, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.019274628999483, "cpu_pct": 9.082493830103466, "user_cpu_pct": 1.8963448656259985, "system_cpu_pct": 7.186148964477468, "pss_mib": 2.19912109375, "uss_mib": 1.578125, "server_processes": 2, "server_threads": 2, "multicast_sockets": 8, "client_mbps": [40.00614404158985, 40.00614404158985, 40.00614404158985, 40.00614404158985, 39.95465608272036, 40.00614404158985, 40.00614404158985, 40.00614404158985], "source_mbps": [39.99983939356501, 39.99983939356501, 40.00194094290663, 40.00194094290663, 40.00194094290663, 40.00194094290663, 39.9987886188942, 40.00089016823581], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 51.80015711894175, "family_stable": true} -{"program": "msd_lite", "case": "distinct8", "repetition": 0, "order": 1, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", "-c", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/distinct8-00-msd_lite.xml", "-l", "1"], "clients": 8, "sources": 8, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.004976337999324, "cpu_pct": 9.495274830304794, "user_cpu_pct": 1.1994031364595528, "system_cpu_pct": 8.29587169384524, "pss_mib": 8.9541015625, "uss_mib": 8.94140625, "server_processes": 1, "server_threads": 2, "multicast_sockets": 8, "client_mbps": [39.97222047203477, 39.97116819568305, 40.01085404666222, 39.95959315581412, 40.020324533827704, 39.97011591933133, 40.01716770477254, 40.01085404666222], "source_mbps": [40.00018095795191, 40.00228551065535, 39.99912868160019, 40.00123323430363, 40.00123323430363, 40.00018095795191, 40.00018095795191, 40.00018095795191], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 54.47289244753803, "family_stable": true} -{"program": "udpxy", "case": "distinct8", "repetition": 0, "order": 2, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", "-T", "-a", "127.0.0.1", "-m", "lo", "-p", "55001", "-c", "256"], "clients": 8, "sources": 8, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.01318254599937, "cpu_pct": 20.27327466242048, "user_cpu_pct": 2.0972353099055665, "system_cpu_pct": 18.176039352514913, "pss_mib": 0.79482421875, "uss_mib": 0.5234375, "server_processes": 9, "server_threads": 9, "multicast_sockets": 8, "client_mbps": [40.00419868107188, 40.00314726710318, 39.99999302519708, 40.00104443916579, 40.00104443916579, 40.002095853134485, 40.002095853134485, 40.002095853134485], "source_mbps": [40.00419868107188, 40.00314726710318, 39.99999302519708, 40.00104443916579, 40.00104443916579, 40.002095853134485, 40.002095853134485, 40.002095853134485], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 86.68572614276343, "family_stable": true} -{"program": "tvgate", "case": "distinct8", "repetition": 0, "order": 3, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", "-config", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/distinct8-00-tvgate.yaml"], "clients": 8, "sources": 8, "target_mbps": 40, "valid": false, "request_prefix": "udp", "duration_s": 10.007266337999681, "cpu_pct": 31.377200265738544, "user_cpu_pct": 15.089035796581276, "system_cpu_pct": 16.28816446915727, "pss_mib": 23.05078125, "uss_mib": 23.05078125, "server_processes": 1, "server_threads": 6, "multicast_sockets": 8, "client_mbps": [39.99839181656172, 40.00259995878334, 39.99944385211713, 40.00049588767253, 39.99944385211713, 40.00154792322794, 40.00049588767253, 40.00259995878334], "source_mbps": [39.99839181656172, 40.00259995878334, 40.00259995878334, 40.00049588767253, 39.99944385211713, 40.00154792322794, 40.00049588767253, 40.00259995878334], "gaps": 1304065, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 186295, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 91.73334345206365, "family_stable": true} -{"program": "rtp2httpd", "case": "distinct8", "repetition": 1, "order": 0, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:51205"], "clients": 8, "sources": 8, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.01384242200038, "cpu_pct": 10.28576201416045, "user_cpu_pct": 2.2968206439387417, "system_cpu_pct": 7.98894137022171, "pss_mib": 2.43037109375, "uss_mib": 1.809375, "server_processes": 2, "server_threads": 2, "multicast_sockets": 8, "client_mbps": [40.027846166160174, 40.027846166160174, 40.027846166160174, 39.97633027662843, 40.027846166160174, 40.027846166160174, 40.027846166160174, 39.97633027662843], "source_mbps": [40.00156254905214, 40.002613893736466, 40.000511204367825, 40.000511204367825, 40.00156254905214, 40.00156254905214, 40.00156254905214, 40.00156254905214], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 59.91706027666282, "family_stable": true} -{"program": "tvgate", "case": "distinct8", "repetition": 1, "order": 1, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", "-config", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/distinct8-01-tvgate.yaml"], "clients": 8, "sources": 8, "target_mbps": 40, "valid": false, "request_prefix": "udp", "duration_s": 10.008565171001464, "cpu_pct": 30.274069741575314, "user_cpu_pct": 14.18784786568876, "system_cpu_pct": 16.086221875886554, "pss_mib": 22.6015625, "uss_mib": 22.6015625, "server_processes": 1, "server_threads": 6, "multicast_sockets": 8, "client_mbps": [40.00056443254802, 40.002668230609, 40.00056443254802, 39.99951253351752, 39.998460634487024, 40.002668230609, 39.99951253351752, 40.00056443254802], "source_mbps": [40.00056443254802, 40.002668230609, 40.00056443254802, 39.99951253351752, 39.998460634487024, 40.002668230609, 40.00161633157851, 40.00372012963949], "gaps": 1286873, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 183839, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 86.12623141002615, "family_stable": true} -{"program": "udpxy", "case": "distinct8", "repetition": 1, "order": 2, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", "-T", "-a", "127.0.0.1", "-m", "lo", "-p", "37491", "-c", "256"], "clients": 8, "sources": 8, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.008616338000138, "cpu_pct": 21.281662999838833, "user_cpu_pct": 1.4987086619604815, "system_cpu_pct": 19.78295433787835, "pss_mib": 0.79873046875, "uss_mib": 0.53125, "server_processes": 9, "server_threads": 9, "multicast_sockets": 8, "client_mbps": [40.002463725170564, 40.00351561882344, 40.001411831517686, 40.002463725170564, 40.00351561882344, 40.001411831517686, 40.00351561882344, 40.001411831517686], "source_mbps": [40.002463725170564, 40.000359937864815, 40.00351561882344, 40.002463725170564, 40.000359937864815, 40.001411831517686, 40.000359937864815, 40.001411831517686], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 95.11804307909189, "family_stable": true} -{"program": "msd_lite", "case": "distinct8", "repetition": 1, "order": 3, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", "-c", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/distinct8-01-msd_lite.xml", "-l", "1"], "clients": 8, "sources": 8, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.011869755000589, "cpu_pct": 9.189092772011854, "user_cpu_pct": 1.3983402044365867, "system_cpu_pct": 7.790752567575267, "pss_mib": 8.9541015625, "uss_mib": 8.94140625, "server_processes": 1, "server_threads": 2, "multicast_sockets": 8, "client_mbps": [40.002233528853615, 39.978047836677675, 40.033479640484636, 40.04294360698827, 40.033479640484636, 40.04189205515453, 39.9959242178512, 39.99066645868251], "source_mbps": [40.00208330716308, 40.00208330716308, 40.00103175532935, 40.00208330716308, 39.9999802034956, 39.9999802034956, 40.00103175532935, 40.00103175532935], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 53.13692776859028, "family_stable": true} -{"program": "udpxy", "case": "distinct8", "repetition": 2, "order": 0, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", "-T", "-a", "127.0.0.1", "-m", "lo", "-p", "53937", "-c", "256"], "clients": 8, "sources": 8, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.00676854700032, "cpu_pct": 20.885863305257608, "user_cpu_pct": 1.598917764995798, "system_cpu_pct": 19.28694554026181, "pss_mib": 0.7986328125, "uss_mib": 0.53125, "server_processes": 9, "server_threads": 9, "multicast_sockets": 8, "client_mbps": [40.00143364163164, 40.00038155374227, 39.99932946585291, 40.00038155374227, 40.00143364163164, 40.00248572952101, 40.00248572952101, 40.00248572952101], "source_mbps": [40.00143364163164, 40.00038155374227, 39.99932946585291, 40.00038155374227, 40.00143364163164, 40.00248572952101, 40.00248572952101, 40.00248572952101], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 90.938447884136, "family_stable": true} -{"program": "tvgate", "case": "distinct8", "repetition": 2, "order": 1, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", "-config", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/distinct8-02-tvgate.yaml"], "clients": 8, "sources": 8, "target_mbps": 40, "valid": false, "request_prefix": "udp", "duration_s": 10.008057754999754, "cpu_pct": 31.074960558119564, "user_cpu_pct": 14.588245149470922, "system_cpu_pct": 16.486715408648642, "pss_mib": 22.876171875, "uss_mib": 22.876171875, "server_processes": 1, "server_threads": 7, "multicast_sockets": 8, "client_mbps": [40.001540538672664, 40.001540538672664, 40.001540538672664, 40.00259249103522, 40.000488586310105, 40.00259249103522, 40.000488586310105, 40.000488586310105], "source_mbps": [40.000488586310105, 40.001540538672664, 40.001540538672664, 40.00259249103522, 40.000488586310105, 40.00259249103522, 40.00259249103522, 40.000488586310105], "gaps": 1292032, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 184576, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 88.52866577007696, "family_stable": true} -{"program": "rtp2httpd", "case": "distinct8", "repetition": 2, "order": 2, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:58775"], "clients": 8, "sources": 8, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.020114671999181, "cpu_pct": 9.979925706783186, "user_cpu_pct": 2.3951821696279647, "system_cpu_pct": 7.584743537155221, "pss_mib": 2.20771484375, "uss_mib": 1.590625, "server_processes": 2, "server_threads": 2, "multicast_sockets": 8, "client_mbps": [40.002790099809026, 40.002790099809026, 40.002790099809026, 40.002790099809026, 40.002790099809026, 40.002790099809026, 40.002790099809026, 40.002790099809026], "source_mbps": [40.002790099809026, 40.00068872665221, 40.00068872665221, 40.00068872665221, 39.9996380400738, 40.00173941323061, 40.00068872665221, 39.9996380400738], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 57.28477355693549, "family_stable": true} -{"program": "msd_lite", "case": "distinct8", "repetition": 2, "order": 3, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", "-c", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/distinct8-02-msd_lite.xml", "-l", "1"], "clients": 8, "sources": 8, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.024946796998847, "cpu_pct": 9.775612976752964, "user_cpu_pct": 1.3965161395361378, "system_cpu_pct": 8.379096837216826, "pss_mib": 8.9541015625, "uss_mib": 8.94140625, "server_processes": 1, "server_threads": 2, "multicast_sockets": 8, "client_mbps": [39.98965940846839, 40.00031123557155, 39.99401015474996, 40.04096820944417, 39.99175976874225, 40.03991802930724, 39.98125796737294, 40.03991802930724], "source_mbps": [40.00241159584541, 40.00031123557155, 40.00136141570848, 39.999261055434616, 40.00031123557155, 40.00241159584541, 40.00241159584541, 40.00241159584541], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 57.755917485101705, "family_stable": true} -{"program": "rtp2httpd", "case": "shared8", "repetition": 0, "order": 0, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:54691"], "clients": 8, "sources": 1, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.03675942100017, "cpu_pct": 6.974362646726113, "user_cpu_pct": 0.9963375209608735, "system_cpu_pct": 5.97802512576524, "pss_mib": 1.6056640625, "uss_mib": 0.98359375, "server_processes": 2, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [39.98784858390133, 39.98784858390133, 39.98784858390133, 39.98784858390133, 39.98784858390133, 39.98784858390133, 39.98784858390133, 39.98784858390133], "source_mbps": [40.00358274603235], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 51.70991733786933, "family_stable": true} -{"program": "msd_lite", "case": "shared8", "repetition": 0, "order": 1, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", "-c", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/shared8-00-msd_lite.xml", "-l", "1"], "clients": 8, "sources": 1, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.04373392100024, "cpu_pct": 6.272567602400795, "user_cpu_pct": 0.7965165209397834, "system_cpu_pct": 5.476051081461011, "pss_mib": 1.353515625, "uss_mib": 1.33984375, "server_processes": 1, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [40.03660024876025, 40.03660024876025, 40.03660024876025, 40.03660024876025, 40.03660024876025, 40.03660024876025, 40.03660024876025, 40.03660024876025], "source_mbps": [40.00305734503044], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 61.331772112363325, "family_stable": true} -{"program": "udpxy", "case": "shared8", "repetition": 0, "order": 2, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", "-T", "-a", "127.0.0.1", "-m", "lo", "-p", "41943", "-c", "256"], "clients": 8, "sources": 1, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.033062170999983, "cpu_pct": 26.213333030087973, "user_cpu_pct": 2.392091227080271, "system_cpu_pct": 23.8212418030077, "pss_mib": 0.769921875, "uss_mib": 0.49609375, "server_processes": 9, "server_threads": 9, "multicast_sockets": 8, "client_mbps": [40.00258437150679, 40.00258437150679, 40.00258437150679, 40.00258437150679, 40.00258437150679, 40.00258437150679, 40.00258437150679, 40.00258437150679], "source_mbps": [40.00258437150679], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 72.16141868358818, "family_stable": true} -{"program": "tvgate", "case": "shared8", "repetition": 0, "order": 3, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", "-config", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/shared8-00-tvgate.yaml"], "clients": 8, "sources": 1, "target_mbps": 40, "valid": false, "request_prefix": "udp", "duration_s": 10.035801797001113, "cpu_pct": 22.41973332586483, "user_cpu_pct": 8.170747256537405, "system_cpu_pct": 14.248986069327424, "pss_mib": 20.1953125, "uss_mib": 20.1953125, "server_processes": 1, "server_threads": 7, "multicast_sockets": 1, "client_mbps": [40.00425278625652, 40.00005660932399, 40.00005660932399, 40.00425278625652, 40.00425278625652, 40.00005660932399, 40.00005660932399, 40.00005660932399], "source_mbps": [40.00425278625652], "gaps": 1373127, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 196161, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 73.63636856806271, "family_stable": true} -{"program": "rtp2httpd", "case": "shared8", "repetition": 1, "order": 0, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:33681"], "clients": 8, "sources": 1, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.036412546000065, "cpu_pct": 7.472789670238363, "user_cpu_pct": 1.195646347238138, "system_cpu_pct": 6.277143323000225, "pss_mib": 1.5986328125, "uss_mib": 0.9765625, "server_processes": 2, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [39.98923063001773, 39.98923063001773, 39.98923063001773, 39.98923063001773, 39.98923063001773, 39.98923063001773, 39.98923063001773, 39.98923063001773], "source_mbps": [40.00076941436614], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 56.29501551579567, "family_stable": true} -{"program": "tvgate", "case": "shared8", "repetition": 1, "order": 1, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", "-config", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/shared8-01-tvgate.yaml"], "clients": 8, "sources": 1, "target_mbps": 40, "valid": false, "request_prefix": "udp", "duration_s": 10.03368546299862, "cpu_pct": 21.826476503335662, "user_cpu_pct": 7.674149272862311, "system_cpu_pct": 14.152327230473352, "pss_mib": 20.723828125, "uss_mib": 20.723828125, "server_processes": 1, "server_threads": 6, "multicast_sockets": 1, "client_mbps": [40.001148678628375, 40.001148678628375, 40.001148678628375, 40.001148678628375, 40.001148678628375, 40.001148678628375, 40.001148678628375, 40.001148678628375], "source_mbps": [40.001148678628375], "gaps": 1365056, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 195008, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 71.75827891507615, "family_stable": true} -{"program": "udpxy", "case": "shared8", "repetition": 1, "order": 2, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", "-T", "-a", "127.0.0.1", "-m", "lo", "-p", "38287", "-c", "256"], "clients": 8, "sources": 1, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.037764087999676, "cpu_pct": 26.79879678798132, "user_cpu_pct": 1.5939804780955433, "system_cpu_pct": 25.20481630988578, "pss_mib": 0.798828125, "uss_mib": 0.53125, "server_processes": 9, "server_threads": 9, "multicast_sockets": 8, "client_mbps": [40.00167651678854, 40.00167651678854, 40.00167651678854, 40.00167651678854, 40.00167651678854, 40.00167651678854, 40.00167651678854, 40.00167651678854], "source_mbps": [40.00167651678854], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 74.91708247049056, "family_stable": true} -{"program": "msd_lite", "case": "shared8", "repetition": 1, "order": 3, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", "-c", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/shared8-01-msd_lite.xml", "-l", "1"], "clients": 8, "sources": 1, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.045796880000125, "cpu_pct": 6.171735377552173, "user_cpu_pct": 0.6968088329494387, "system_cpu_pct": 5.4749265446027335, "pss_mib": 1.3533203125, "uss_mib": 1.33984375, "server_processes": 1, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [40.020892797505475, 40.020892797505475, 40.020892797505475, 40.020892797505475, 40.020892797505475, 40.020892797505475, 40.020892797505475, 40.020892797505475], "source_mbps": [40.00113050265008], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 56.93923606386842, "family_stable": true} -{"program": "udpxy", "case": "shared8", "repetition": 2, "order": 0, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", "-T", "-a", "127.0.0.1", "-m", "lo", "-p", "37553", "-c", "256"], "clients": 8, "sources": 1, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.041393255000003, "cpu_pct": 27.784988886982884, "user_cpu_pct": 2.190931023346321, "system_cpu_pct": 25.594057863636564, "pss_mib": 0.798828125, "uss_mib": 0.53125, "server_processes": 9, "server_threads": 9, "multicast_sockets": 8, "client_mbps": [40.000849065441756, 40.000849065441756, 40.000849065441756, 40.000849065441756, 40.000849065441756, 40.000849065441756, 40.000849065441756, 40.000849065441756], "source_mbps": [40.000849065441756], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 75.38794475787114, "family_stable": true} -{"program": "tvgate", "case": "shared8", "repetition": 2, "order": 1, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", "-config", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/shared8-02-tvgate.yaml"], "clients": 8, "sources": 1, "target_mbps": 40, "valid": false, "request_prefix": "udp", "duration_s": 10.028015130001222, "cpu_pct": 22.736303948912393, "user_cpu_pct": 7.8779298770354345, "system_cpu_pct": 14.858374071876959, "pss_mib": 20.352734375, "uss_mib": 20.352734375, "server_processes": 1, "server_threads": 7, "multicast_sockets": 1, "client_mbps": [40.000670401855594, 40.000670401855594, 40.000670401855594, 40.000670401855594, 40.000670401855594, 40.000670401855594, 40.000670401855594, 40.000670401855594], "source_mbps": [40.000670401855594], "gaps": 1369088, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 195584, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 71.99829583822257, "family_stable": true} -{"program": "rtp2httpd", "case": "shared8", "repetition": 2, "order": 2, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:58275"], "clients": 8, "sources": 1, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.027774796000813, "cpu_pct": 7.180057536664504, "user_cpu_pct": 1.1966762561107507, "system_cpu_pct": 5.983381280553753, "pss_mib": 1.59814453125, "uss_mib": 0.9765625, "server_processes": 2, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [39.97223234010565, 39.97223234010565, 39.97223234010565, 39.97223234010565, 39.97223234010565, 39.97223234010565, 39.97223234010565, 39.97223234010565], "source_mbps": [40.00267897519778], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 52.95292433290072, "family_stable": true} -{"program": "msd_lite", "case": "shared8", "repetition": 2, "order": 3, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", "-c", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/shared8-02-msd_lite.xml", "-l", "1"], "clients": 8, "sources": 1, "target_mbps": 40, "valid": true, "request_prefix": "rtp", "duration_s": 10.033945004999623, "cpu_pct": 5.780378502284025, "user_cpu_pct": 0.6976318882066928, "system_cpu_pct": 5.082746614077332, "pss_mib": 1.3525390625, "uss_mib": 1.33984375, "server_processes": 1, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [40.01660202442131, 40.01660202442131, 40.01660202442131, 40.01660202442131, 40.01660202442131, 40.01660202442131, 40.01660202442131, 40.01660202442131], "source_mbps": [40.00011399305203], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 51.72442142561051, "family_stable": true} -{"program": "rtp2httpd", "case": "high400", "repetition": 0, "order": 0, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:54257"], "clients": 1, "sources": 1, "target_mbps": 400, "valid": false, "request_prefix": "rtp", "duration_s": 10.034364713001196, "cpu_pct": 16.144519820980985, "user_cpu_pct": 3.587671071329108, "system_cpu_pct": 12.556848749651877, "pss_mib": 1.3955078125, "uss_mib": 0.7734375, "server_processes": 2, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [399.9225065838527], "source_mbps": [400.0463115317026], "gaps": 259, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 37, "load_cpu_pct": 67.86677776597563, "family_stable": true} -{"program": "msd_lite", "case": "high400", "repetition": 0, "order": 1, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", "-c", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/high400-00-msd_lite.xml", "-l", "1"], "clients": 1, "sources": 1, "target_mbps": 400, "valid": true, "request_prefix": "rtp", "duration_s": 10.029312212998775, "cpu_pct": 13.759667369926044, "user_cpu_pct": 1.7947392221642666, "system_cpu_pct": 11.964928147761777, "pss_mib": 1.341796875, "uss_mib": 1.328125, "server_processes": 1, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [400.04134867792357], "source_mbps": [400.0777890630904], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 55.53720815252758, "family_stable": true} -{"program": "udpxy", "case": "high400", "repetition": 0, "order": 2, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", "-T", "-a", "127.0.0.1", "-m", "lo", "-p", "56973", "-c", "256"], "clients": 1, "sources": 1, "target_mbps": 400, "valid": false, "request_prefix": "rtp", "duration_s": 10.02519554600076, "cpu_pct": 22.842447207062456, "user_cpu_pct": 1.4962301664014708, "system_cpu_pct": 21.346217040660985, "pss_mib": 0.32509765625, "uss_mib": 0.1171875, "server_processes": 2, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [399.9553817780161], "source_mbps": [400.01734086870414], "gaps": 203, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 29, "load_cpu_pct": 70.52231517638933, "family_stable": true} -{"program": "tvgate", "case": "high400", "repetition": 0, "order": 3, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", "-config", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/high400-00-tvgate.yaml"], "clients": 1, "sources": 1, "target_mbps": 400, "valid": false, "request_prefix": "udp", "duration_s": 10.033724295999491, "cpu_pct": 28.005553243279415, "user_cpu_pct": 15.6472308156401, "system_cpu_pct": 12.358322427639314, "pss_mib": 17.676171875, "uss_mib": 17.676171875, "server_processes": 1, "server_threads": 7, "multicast_sockets": 1, "client_mbps": [399.89242136140547], "source_mbps": [399.99524898249246], "gaps": 2333772, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 333298, "closed_clients": 0, "kernel_udp_drops": 98, "load_cpu_pct": 51.82522308364874, "family_stable": true} -{"program": "rtp2httpd", "case": "high400", "repetition": 1, "order": 0, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:45669"], "clients": 1, "sources": 1, "target_mbps": 400, "valid": true, "request_prefix": "rtp", "duration_s": 10.040429628999846, "cpu_pct": 13.844029103946436, "user_cpu_pct": 2.5895306237597655, "system_cpu_pct": 11.25449848018667, "pss_mib": 1.5126953125, "uss_mib": 0.9375, "server_processes": 2, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [399.9892104617092], "source_mbps": [399.99340470454104], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 58.563231029643916, "family_stable": true} -{"program": "tvgate", "case": "high400", "repetition": 1, "order": 1, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", "-config", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/high400-01-tvgate.yaml"], "clients": 1, "sources": 1, "target_mbps": 400, "valid": false, "request_prefix": "udp", "duration_s": 10.0216719630007, "cpu_pct": 25.644423500272783, "user_cpu_pct": 14.46864360910332, "system_cpu_pct": 11.175779891169462, "pss_mib": 17.784375, "uss_mib": 17.784375, "server_processes": 1, "server_threads": 7, "multicast_sockets": 1, "client_mbps": [399.9153146098363], "source_mbps": [400.0067101377863], "gaps": 2345329, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 334960, "closed_clients": 0, "kernel_udp_drops": 87, "load_cpu_pct": 47.59684828649851, "family_stable": true} -{"program": "udpxy", "case": "high400", "repetition": 1, "order": 2, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", "-T", "-a", "127.0.0.1", "-m", "lo", "-p", "59211", "-c", "256"], "clients": 1, "sources": 1, "target_mbps": 400, "valid": false, "request_prefix": "rtp", "duration_s": 10.028106712999943, "cpu_pct": 23.53385407178219, "user_cpu_pct": 1.7949549715766073, "system_cpu_pct": 21.738899100205582, "pss_mib": 0.32109375, "uss_mib": 0.109375, "server_processes": 2, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [399.87601914942076], "source_mbps": [400.01564909553855], "gaps": 931, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 133, "load_cpu_pct": 71.7981988630643, "family_stable": true} -{"program": "msd_lite", "case": "high400", "repetition": 1, "order": 3, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", "-c", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/high400-01-msd_lite.xml", "-l", "1"], "clients": 1, "sources": 1, "target_mbps": 400, "valid": false, "request_prefix": "rtp", "duration_s": 10.03419213000052, "cpu_pct": 14.151612622150644, "user_cpu_pct": 1.8935256325412833, "system_cpu_pct": 12.25808698960936, "pss_mib": 1.341796875, "uss_mib": 1.328125, "server_processes": 1, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [399.8027301077593], "source_mbps": [400.09620983805024], "gaps": 2023, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 289, "load_cpu_pct": 59.59622780314144, "family_stable": true} -{"program": "udpxy", "case": "high400", "repetition": 2, "order": 0, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", "-T", "-a", "127.0.0.1", "-m", "lo", "-p", "58639", "-c", "256"], "clients": 1, "sources": 1, "target_mbps": 400, "valid": false, "request_prefix": "rtp", "duration_s": 10.019285046999357, "cpu_pct": 23.55457489161653, "user_cpu_pct": 1.4971128109078302, "system_cpu_pct": 22.0574620807087, "pss_mib": 0.31728515625, "uss_mib": 0.109375, "server_processes": 2, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [399.86558054856954], "source_mbps": [399.96750558565645], "gaps": 679, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 97, "load_cpu_pct": 70.86333971630397, "family_stable": true} -{"program": "tvgate", "case": "high400", "repetition": 2, "order": 1, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", "-config", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/high400-02-tvgate.yaml"], "clients": 1, "sources": 1, "target_mbps": 400, "valid": false, "request_prefix": "udp", "duration_s": 10.03475075499955, "cpu_pct": 30.095416156653066, "user_cpu_pct": 16.741820908336805, "system_cpu_pct": 13.35359524831626, "pss_mib": 17.6234375, "uss_mib": 17.6234375, "server_processes": 1, "server_threads": 6, "multicast_sockets": 1, "client_mbps": [399.9973486137853], "source_mbps": [400.0613470145109], "gaps": 2276169, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 325167, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 55.40745491092419, "family_stable": true} -{"program": "rtp2httpd", "case": "high400", "repetition": 2, "order": 2, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:58327"], "clients": 1, "sources": 1, "target_mbps": 400, "valid": true, "request_prefix": "rtp", "duration_s": 10.039700171999357, "cpu_pct": 15.737521767897087, "user_cpu_pct": 2.9881370445374213, "system_cpu_pct": 12.749384723359666, "pss_mib": 1.3564453125, "uss_mib": 0.734375, "server_processes": 2, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [400.0696557853598], "source_mbps": [400.26260497376313], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 67.23308350209199, "family_stable": true} -{"program": "msd_lite", "case": "high400", "repetition": 2, "order": 3, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", "-c", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-additional/high400-02-msd_lite.xml", "-l", "1"], "clients": 1, "sources": 1, "target_mbps": 400, "valid": false, "request_prefix": "rtp", "duration_s": 10.041544588000761, "cpu_pct": 16.431735033788357, "user_cpu_pct": 2.1908980045051143, "system_cpu_pct": 14.240837029283243, "pss_mib": 1.341796875, "uss_mib": 1.328125, "server_processes": 1, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [399.75158451188025], "source_mbps": [399.9846386978664], "gaps": 1911, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 273, "load_cpu_pct": 67.4199067749983, "family_stable": true} diff --git a/tools/stress-test/results/2026-09-06/build-environment.json b/tools/stress-test/results/2026-09-06/build-environment.json deleted file mode 100644 index bd0f6428..00000000 --- a/tools/stress-test/results/2026-09-06/build-environment.json +++ /dev/null @@ -1,18 +0,0 @@ -{ - "host": "Apple M3 Max, 16 cores (12 performance + 4 efficiency), 128 GiB RAM", - "vm": "Parallels Ubuntu 24.04, 16 vCPU, 16 GiB RAM; native aarch64 execution", - "compiler": "gcc (Ubuntu 13.3.0-6ubuntu2~24.04.1) 13.3.0", - "msd_lite_commit": "fa68e131343fb58c67ad77b2d26f2cb7c49a2c95", - "liblcb_commit": "e2f420a2e545f7b04f3f7ffc13b3d9eaa92b5e8b", - "udpxy_commit": "31d4bcfabaade59d3efdee015df7979febf76bae", - "udpxy_build_command": "make -j16 CFLAGS=\"-O3 -flto\" LDFLAGS=\"-flto\"", - "tvgate_version": "\u7a0b\u5e8f\u7248\u672c: v3.2.0", - "tvgate_asset": "https://github.com/qist/tvgate/releases/download/v3.2.0/TVGate-linux-arm64.zip", - "tvgate_archive_sha256": "1655a066b91debdaf2f3b39096207f80fdfc7bd9c2f227ff9dda34c48562ac3e", - "rtp2httpd_compile_flags": "# CMAKE generated file: DO NOT EDIT!\n# Generated by \"Unix Makefiles\" Generator, CMake Version 3.28\n\n# compile C with /usr/bin/cc\nC_DEFINES = -DPACKAGE=\\\"rtp2httpd\\\" -DSYSCONFDIR=\\\"/usr/local/etc\\\" -DVERSION=\\\"1.0.0-snapshot\\\" -D_FORTIFY_SOURCE=2 -D_GNU_SOURCE\n\nC_INCLUDES = -I/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/src\n\nC_FLAGS = -O3 -DNDEBUG -std=gnu11 -Wall -Wextra -Wunused -Wformat=2 -Wformat-security -Wstrict-prototypes -Wmissing-prototypes -Wold-style-definition -Wpointer-arith -Wcast-align -Wcast-qual -Wshadow -Wwrite-strings -Wredundant-decls -Wnull-dereference -Wundef -Wvla -Wduplicated-cond -Wlogical-op -Wjump-misses-init -Wduplicated-branches -finline-functions -funroll-loops -ftree-vectorize -ffast-math -fomit-frame-pointer -flto=auto -finline-limit=600 -fstack-protector-strong\n\n", - "rtp2httpd_link_command": "/usr/bin/cc -O3 -DNDEBUG -flto CMakeFiles/rtp2httpd.dir/src/rtp2httpd.c.o CMakeFiles/rtp2httpd.dir/src/supervisor.c.o CMakeFiles/rtp2httpd.dir/src/pid_file.c.o CMakeFiles/rtp2httpd.dir/src/configuration.c.o CMakeFiles/rtp2httpd.dir/src/access_log.c.o CMakeFiles/rtp2httpd.dir/src/http.c.o CMakeFiles/rtp2httpd.dir/src/http_headers.c.o CMakeFiles/rtp2httpd.dir/src/http_fetch.c.o CMakeFiles/rtp2httpd.dir/src/service.c.o CMakeFiles/rtp2httpd.dir/src/url_template.c.o CMakeFiles/rtp2httpd.dir/src/rtp.c.o CMakeFiles/rtp2httpd.dir/src/rtp_reorder.c.o CMakeFiles/rtp2httpd.dir/src/rtp_fec.c.o CMakeFiles/rtp2httpd.dir/src/rs_fec.c.o CMakeFiles/rtp2httpd.dir/src/multicast.c.o CMakeFiles/rtp2httpd.dir/src/fcc.c.o CMakeFiles/rtp2httpd.dir/src/fcc_telecom.c.o CMakeFiles/rtp2httpd.dir/src/fcc_huawei.c.o CMakeFiles/rtp2httpd.dir/src/stream.c.o CMakeFiles/rtp2httpd.dir/src/rtsp.c.o CMakeFiles/rtp2httpd.dir/src/http_chunked_decoder.c.o CMakeFiles/rtp2httpd.dir/src/http_proxy.c.o CMakeFiles/rtp2httpd.dir/src/http_proxy_rewrite.c.o CMakeFiles/rtp2httpd.dir/src/stun.c.o CMakeFiles/rtp2httpd.dir/src/snapshot.c.o CMakeFiles/rtp2httpd.dir/src/timezone.c.o CMakeFiles/rtp2httpd.dir/src/status.c.o CMakeFiles/rtp2httpd.dir/src/connection.c.o CMakeFiles/rtp2httpd.dir/src/worker.c.o CMakeFiles/rtp2httpd.dir/src/unix_socket.c.o CMakeFiles/rtp2httpd.dir/src/buffer_pool.c.o CMakeFiles/rtp2httpd.dir/src/send_queue.c.o CMakeFiles/rtp2httpd.dir/src/m3u.c.o CMakeFiles/rtp2httpd.dir/src/epg.c.o CMakeFiles/rtp2httpd.dir/src/embedded_web.c.o CMakeFiles/rtp2httpd.dir/src/utils.c.o CMakeFiles/rtp2httpd.dir/src/vendor/hashmap/hashmap.c.o CMakeFiles/rtp2httpd.dir/src/vendor/md5/md5.c.o CMakeFiles/rtp2httpd.dir/src/vendor/picohttpparser/picohttpparser.c.o CMakeFiles/rtp2httpd.dir/src/poller_epoll.c.o -o rtp2httpd -lm -lrt \n", - "baseline_compile_flags": "# CMAKE generated file: DO NOT EDIT!\n# Generated by \"Unix Makefiles\" Generator, CMake Version 3.28\n\n# compile C with /usr/bin/cc\nC_DEFINES = -DPACKAGE=\\\"rtp2httpd\\\" -DSYSCONFDIR=\\\"/usr/local/etc\\\" -DVERSION=\\\"1.0.0-snapshot\\\" -D_FORTIFY_SOURCE=2 -D_GNU_SOURCE\n\nC_INCLUDES = -I/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/baseline/src\n\nC_FLAGS = -O3 -DNDEBUG -std=gnu11 -Wall -Wextra -Wunused -Wformat=2 -Wformat-security -Wstrict-prototypes -Wmissing-prototypes -Wold-style-definition -Wpointer-arith -Wcast-align -Wcast-qual -Wshadow -Wwrite-strings -Wredundant-decls -Wnull-dereference -Wundef -Wvla -Wduplicated-cond -Wlogical-op -Wjump-misses-init -Wduplicated-branches -finline-functions -funroll-loops -ftree-vectorize -ffast-math -fomit-frame-pointer -flto=auto -finline-limit=600 -fstack-protector-strong\n\n", - "baseline_link_command": "/usr/bin/cc -O3 -DNDEBUG -flto CMakeFiles/rtp2httpd.dir/src/rtp2httpd.c.o CMakeFiles/rtp2httpd.dir/src/supervisor.c.o CMakeFiles/rtp2httpd.dir/src/pid_file.c.o CMakeFiles/rtp2httpd.dir/src/configuration.c.o CMakeFiles/rtp2httpd.dir/src/access_log.c.o CMakeFiles/rtp2httpd.dir/src/http.c.o CMakeFiles/rtp2httpd.dir/src/http_headers.c.o CMakeFiles/rtp2httpd.dir/src/http_fetch.c.o CMakeFiles/rtp2httpd.dir/src/service.c.o CMakeFiles/rtp2httpd.dir/src/url_template.c.o CMakeFiles/rtp2httpd.dir/src/rtp.c.o CMakeFiles/rtp2httpd.dir/src/rtp_reorder.c.o CMakeFiles/rtp2httpd.dir/src/rtp_fec.c.o CMakeFiles/rtp2httpd.dir/src/rs_fec.c.o CMakeFiles/rtp2httpd.dir/src/multicast.c.o CMakeFiles/rtp2httpd.dir/src/fcc.c.o CMakeFiles/rtp2httpd.dir/src/fcc_telecom.c.o CMakeFiles/rtp2httpd.dir/src/fcc_huawei.c.o CMakeFiles/rtp2httpd.dir/src/stream.c.o CMakeFiles/rtp2httpd.dir/src/rtsp.c.o CMakeFiles/rtp2httpd.dir/src/http_chunked_decoder.c.o CMakeFiles/rtp2httpd.dir/src/http_proxy.c.o CMakeFiles/rtp2httpd.dir/src/http_proxy_rewrite.c.o CMakeFiles/rtp2httpd.dir/src/stun.c.o CMakeFiles/rtp2httpd.dir/src/snapshot.c.o CMakeFiles/rtp2httpd.dir/src/timezone.c.o CMakeFiles/rtp2httpd.dir/src/status.c.o CMakeFiles/rtp2httpd.dir/src/connection.c.o CMakeFiles/rtp2httpd.dir/src/worker.c.o CMakeFiles/rtp2httpd.dir/src/unix_socket.c.o CMakeFiles/rtp2httpd.dir/src/buffer_pool.c.o CMakeFiles/rtp2httpd.dir/src/zerocopy.c.o CMakeFiles/rtp2httpd.dir/src/m3u.c.o CMakeFiles/rtp2httpd.dir/src/epg.c.o CMakeFiles/rtp2httpd.dir/src/embedded_web.c.o CMakeFiles/rtp2httpd.dir/src/utils.c.o CMakeFiles/rtp2httpd.dir/src/vendor/hashmap/hashmap.c.o CMakeFiles/rtp2httpd.dir/src/vendor/md5/md5.c.o CMakeFiles/rtp2httpd.dir/src/vendor/picohttpparser/picohttpparser.c.o CMakeFiles/rtp2httpd.dir/src/poller_epoll.c.o -o rtp2httpd -lm -lrt \n", - "msd_lite_compile_flags": "# CMAKE generated file: DO NOT EDIT!\n# Generated by \"Unix Makefiles\" Generator, CMake Version 3.28\n\n# compile C with /usr/bin/cc\nC_DEFINES = -DHAVE_ACCEPT4 -DHAVE_CONFIG_H -DHAVE_EXPLICIT_BZERO -DHAVE_MEMMEM -DHAVE_MEMRCHR -DHAVE_PIPE2 -DHAVE_POSIX_SPAWN_FILE_ACTIONS_ADDCLOSEFROM_NP -DHAVE_PTHREAD_SETNAME_NP -DHAVE_REALLOCARRAY -DHAVE_SOCK_CLOEXEC -DHAVE_SOCK_NONBLOCK -DHAVE_STRLCPY -DHAVE_STRNCASECMP -DHTTP_SRV_XML_CONFIG -DLINUX -DNDEBUG -DSOCKET_XML_CONFIG -DTHREAD_POOL_SETTINGS_XML -D_GNU_SOURCE -D__USE_GNU=1\n\nC_INCLUDES = -I/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd -I/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/src -I/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/src/liblcb/include -I/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src\n\nC_FLAGS = -pipe -fno-delete-null-pointer-checks -std=c11 -D_FORTIFY_SOURCE=2 -fstack-protector-all -fwrapv -fPIE -Wno-switch-default -Wno-unused-result -Wno-unsafe-buffer-usage -O3 -DNDEBUG -flto=auto -finline-functions -funroll-loops -ftree-vectorize -ffast-math -fomit-frame-pointer -finline-limit=600\n\n", - "msd_lite_link_command": "/usr/bin/cc -pipe -fno-delete-null-pointer-checks -std=c11 -D_FORTIFY_SOURCE=2 -fstack-protector-all -fwrapv -fPIE -Wno-switch-default -Wno-unused-result -Wno-unsafe-buffer-usage -O3 -DNDEBUG -flto=auto -finline-functions -funroll-loops -ftree-vectorize -ffast-math -fomit-frame-pointer -finline-limit=600 -pie -Wl,-z,retpolineplt -Wl,-z,relro -Wl,-z,now -Wl,-z,noexecstack CMakeFiles/msd_lite.dir/msd_lite.c.o CMakeFiles/msd_lite.dir/msd_lite_stat_text.c.o CMakeFiles/msd_lite.dir/stream_sys.c.o CMakeFiles/msd_lite.dir/liblcb/src/net/socket.c.o CMakeFiles/msd_lite.dir/liblcb/src/net/socket_address.c.o CMakeFiles/msd_lite.dir/liblcb/src/net/socket_options.c.o CMakeFiles/msd_lite.dir/liblcb/src/net/utils.c.o CMakeFiles/msd_lite.dir/liblcb/src/proto/http.c.o CMakeFiles/msd_lite.dir/liblcb/src/proto/http_server.c.o CMakeFiles/msd_lite.dir/liblcb/src/threadpool/threadpool.c.o CMakeFiles/msd_lite.dir/liblcb/src/threadpool/threadpool_msg_sys.c.o CMakeFiles/msd_lite.dir/liblcb/src/threadpool/threadpool_task.c.o CMakeFiles/msd_lite.dir/liblcb/src/utils/cmd_line_daemon.c.o CMakeFiles/msd_lite.dir/liblcb/src/utils/sys.c.o CMakeFiles/msd_lite.dir/liblcb/src/utils/sys_res_limits_xml.c.o CMakeFiles/msd_lite.dir/liblcb/src/utils/buf_str.c.o CMakeFiles/msd_lite.dir/liblcb/src/utils/info.c.o CMakeFiles/msd_lite.dir/liblcb/src/utils/ring_buffer.c.o CMakeFiles/msd_lite.dir/liblcb/src/utils/xml.c.o -o msd_lite /usr/lib/aarch64-linux-gnu/libpthread.a -lrt /usr/lib/aarch64-linux-gnu/libpthread.a -lrt -pie -Wl,-z,retpolineplt -Wl,-z,relro -Wl,-z,now -Wl,-z,noexecstack \n" -} diff --git a/tools/stress-test/results/2026-09-06/shared64/metadata.json b/tools/stress-test/results/2026-09-06/shared64/metadata.json deleted file mode 100644 index f0d3baab..00000000 --- a/tools/stress-test/results/2026-09-06/shared64/metadata.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "timestamp_utc": "2026-09-06T10:20:46Z", - "platform": "Linux-6.8.0-138-generic-aarch64-with-glibc2.39", - "machine": "aarch64", - "cpu_count": 16, - "server_cpu": 0, - "load_cpus": [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12], - "controller_cpu": 13, - "warmup_s": 5.0, - "duration_s": 20, - "repetitions": 5, - "cases": { - "shared64": [64, 1, 20] - }, - "script_sha256": "f449923fc4aa4b32fe382ef8aa2bce7c446fe108ceab0a55fc1990d55e31b6bf", - "binaries": { - "rtp2httpd": { - "path": "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", - "revision": "530dc980e92db6b6ea98b6ca223dffe1a0345b5c", - "sha256": "74f861fcae8b894774928ec89b24436cbf487692b98df84921bd1991301a23ca" - }, - "msd_lite": { - "path": "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", - "revision": "fa68e131343fb58c67ad77b2d26f2cb7c49a2c95", - "sha256": "45459818ceae0a7595406b0aec5cf999cd27bb4efa822494414a6ca397bc88a7" - }, - "udpxy": { - "path": "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", - "revision": "31d4bcfabaade59d3efdee015df7979febf76bae", - "sha256": "71037623a56bed03b8797c4b849a90f7a0924ac2e6998c178ff0498c734c06ae" - }, - "tvgate": { - "path": "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", - "revision": "v3.2.0", - "sha256": "eca4c04f3973316e716be3264c690e3215fd68624c15df8a709f0e627df5307e" - }, - "baseline": { - "path": "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/baseline/build/rtp2httpd", - "revision": "f8c243cb6fc98e259fd2f2fe0dc992f2a845014e", - "sha256": "68fc68a6d67da5786508a95a2f97437bd1c4242e5dae3ba1d5d15294f15ccafe" - } - }, - "sysctl": "net.core.rmem_max = 212992\nnet.core.wmem_max = 212992\nnet.ipv4.tcp_congestion_control = cubic" -} diff --git a/tools/stress-test/results/2026-09-06/shared64/summary.json b/tools/stress-test/results/2026-09-06/shared64/summary.json deleted file mode 100644 index 98005773..00000000 --- a/tools/stress-test/results/2026-09-06/shared64/summary.json +++ /dev/null @@ -1,95 +0,0 @@ -[ - { - "case": "shared64", - "program": "rtp2httpd", - "valid_trials": 5, - "total_trials": 5, - "cpu_pct": { - "mean": 6.970806833409041, - "min": 6.327807413776387, - "max": 7.676655957103553 - }, - "pss_mib": { - "mean": 4.60861328125, - "min": 4.5712890625, - "max": 4.6337890625 - }, - "uss_mib": { - "mean": 3.9875, - "min": 3.94921875, - "max": 4.01171875 - } - }, - { - "case": "shared64", - "program": "msd_lite", - "valid_trials": 5, - "total_trials": 5, - "cpu_pct": { - "mean": 5.911672985938835, - "min": 5.586135256424014, - "max": 6.476307948977287 - }, - "pss_mib": { - "mean": 1.36888671875, - "min": 1.3681640625, - "max": 1.369140625 - }, - "uss_mib": { - "mean": 1.35546875, - "min": 1.35546875, - "max": 1.35546875 - } - }, - { - "case": "shared64", - "program": "udpxy", - "valid_trials": 2, - "total_trials": 5, - "cpu_pct": { - "mean": 56.749177430157616, - "min": 54.79720868821048, - "max": 58.70114617210474 - }, - "pss_mib": { - "mean": 4.606201171875, - "min": 4.60302734375, - "max": 4.609375 - }, - "uss_mib": { - "mean": 4.015625, - "min": 4.01171875, - "max": 4.01953125 - } - }, - { - "case": "shared64", - "program": "tvgate", - "valid_trials": 0, - "total_trials": 5, - "cpu_pct": null, - "pss_mib": null, - "uss_mib": null - }, - { - "case": "shared64", - "program": "baseline", - "valid_trials": 5, - "total_trials": 5, - "cpu_pct": { - "mean": 30.100316076468307, - "min": 29.065688183265927, - "max": 31.37242538962098 - }, - "pss_mib": { - "mean": 10.278642578125, - "min": 8.98515625, - "max": 11.922607421875 - }, - "uss_mib": { - "mean": 9.6028125, - "min": 8.34375, - "max": 11.23828125 - } - } -] diff --git a/tools/stress-test/results/2026-09-06/shared64/trials.jsonl b/tools/stress-test/results/2026-09-06/shared64/trials.jsonl deleted file mode 100644 index 8b519397..00000000 --- a/tools/stress-test/results/2026-09-06/shared64/trials.jsonl +++ /dev/null @@ -1,25 +0,0 @@ -{"program": "rtp2httpd", "case": "shared64", "repetition": 0, "order": 0, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:42169"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.070143052000276, "cpu_pct": 6.327807413776387, "user_cpu_pct": 1.145980870211472, "system_cpu_pct": 5.181826543564916, "pss_mib": 4.6337890625, "uss_mib": 4.01171875, "server_processes": 2, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 19.997287261985903, 20.022990716050153, 19.997287261985903], "source_mbps": [20.00043462370806], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 73.59190196966712, "family_stable": true} -{"program": "msd_lite", "case": "shared64", "repetition": 0, "order": 1, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", "-c", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-final64/shared64-00-msd_lite.xml", "-l", "1"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.06052855100006, "cpu_pct": 5.8323488188533945, "user_cpu_pct": 0.598189622446502, "system_cpu_pct": 5.2341591964068925, "pss_mib": 1.369140625, "uss_mib": 1.35546875, "server_processes": 1, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.970434526762673, 19.996525165335303, 19.970434526762673, 19.970434526762673, 19.996525165335303, 19.996525165335303, 19.970434526762673, 19.970434526762673, 19.996525165335303, 19.996525165335303, 19.996525165335303, 19.970434526762673, 19.996525165335303, 19.996525165335303, 19.996525165335303, 19.970434526762673, 19.996525165335303, 19.996525165335303, 19.996525165335303, 19.970434526762673, 19.996525165335303, 19.996525165335303, 19.996525165335303, 19.970434526762673, 19.996525165335303], "source_mbps": [19.998474465918314], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 80.65590075987002, "family_stable": true} -{"program": "udpxy", "case": "shared64", "repetition": 0, "order": 2, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", "-T", "-a", "127.0.0.1", "-m", "lo", "-p", "47477", "-c", "256"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.067751259000033, "cpu_pct": 58.70114617210474, "user_cpu_pct": 5.3319377253100235, "system_cpu_pct": 53.36920844679472, "pss_mib": 4.609375, "uss_mib": 4.01953125, "server_processes": 65, "server_threads": 65, "multicast_sockets": 64, "client_mbps": [20.000719902285656, 20.001769147900085, 20.000719902285656, 20.001769147900085, 20.000719902285656, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.000719902285656, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.000719902285656, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.000719902285656, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.000719902285656, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.000719902285656, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.000719902285656, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.000719902285656, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.000719902285656, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.001769147900085, 20.000719902285656, 20.001769147900085, 20.001769147900085, 20.00124452509287, 20.000719902285656, 20.001769147900085, 20.001769147900085, 20.001769147900085], "source_mbps": [20.001769147900085], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 141.02227815539595, "family_stable": true} -{"program": "tvgate", "case": "shared64", "repetition": 0, "order": 3, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", "-config", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-final64/shared64-00-tvgate.yaml"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": false, "request_prefix": "udp", "duration_s": 20.04664534300082, "cpu_pct": 47.98807897980184, "user_cpu_pct": 16.21218884452765, "system_cpu_pct": 31.775890135274192, "pss_mib": 44.513671875, "uss_mib": 44.513671875, "server_processes": 1, "server_threads": 7, "multicast_sockets": 1, "client_mbps": [20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196, 20.000245284929196], "source_mbps": [20.000770460080446], "gaps": 3833536, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 547648, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 163.91770013267032, "family_stable": true} -{"program": "baseline", "case": "shared64", "repetition": 0, "order": 4, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/baseline/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:56817"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.067731551999714, "cpu_pct": 30.098070548477732, "user_cpu_pct": 5.730592902441953, "system_cpu_pct": 24.36747764603578, "pss_mib": 11.0478515625, "uss_mib": 10.36328125, "server_processes": 2, "server_threads": 2, "multicast_sockets": 64, "client_mbps": [20.002313413446128, 20.002313413446128, 20.013330503216697, 20.002313413446128, 20.002313413446128, 20.002313413446128, 20.013330503216697, 20.002313413446128, 20.002313413446128, 20.002313413446128, 20.013330503216697, 20.002313413446128, 20.002313413446128, 20.002313413446128, 20.013330503216697, 20.002313413446128, 20.002313413446128, 20.002313413446128, 20.013330503216697, 19.97555762400333, 20.002313413446128, 20.002313413446128, 20.013330503216697, 19.993394816965196, 20.002313413446128, 20.002313413446128, 20.013330503216697, 19.993394816965196, 20.002313413446128, 20.002313413446128, 20.013330503216697, 19.993394816965196, 20.002313413446128, 20.002313413446128, 20.013330503216697, 20.019625983085593, 20.002313413446128, 20.002313413446128, 20.013330503216697, 20.019625983085593, 20.002313413446128, 20.002313413446128, 20.013330503216697, 20.019625983085593, 20.002313413446128, 20.002313413446128, 20.013330503216697, 20.019625983085593, 20.002313413446128, 20.028544579566525, 20.015428996506326, 20.019625983085593, 20.028544579566525, 20.028544579566525, 20.015428996506326, 20.019625983085593, 20.028544579566525, 20.001788790123722, 20.015428996506326, 20.019625983085593, 20.028544579566525, 20.001788790123722, 19.993394816965196, 20.015428996506326], "source_mbps": [20.002313413446128], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 54.116729496104014, "family_stable": true} -{"program": "rtp2httpd", "case": "shared64", "repetition": 1, "order": 0, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:39729"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.056729425999947, "cpu_pct": 7.079918015742013, "user_cpu_pct": 1.3461815945424955, "system_cpu_pct": 5.733736421199517, "pss_mib": 4.5712890625, "uss_mib": 3.94921875, "server_processes": 2, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 20.010661133999438, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332, 19.984940489868332], "source_mbps": [20.00016291190511], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 74.93744209619892, "family_stable": true} -{"program": "baseline", "case": "shared64", "repetition": 1, "order": 1, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/baseline/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:50067"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.023609842999576, "cpu_pct": 29.065688183265927, "user_cpu_pct": 5.543455993715666, "system_cpu_pct": 23.52223218955026, "pss_mib": 9.0892578125, "uss_mib": 8.4046875, "server_processes": 2, "server_threads": 2, "multicast_sockets": 64, "client_mbps": [20.021150788660446, 19.988026691397238, 20.010635202227682, 20.006954746976213, 20.021150788660446, 19.988026691397238, 19.98382045682413, 19.988026691397238, 20.021150788660446, 19.988026691397238, 19.98382045682413, 20.01431565747915, 20.021150788660446, 19.988026691397238, 19.98382045682413, 20.01431565747915, 20.021150788660446, 19.988026691397238, 19.98382045682413, 20.01431565747915, 20.010635202227682, 19.988026691397238, 19.98382045682413, 20.01431565747915, 20.010635202227682, 19.988026691397238, 19.98382045682413, 20.01431565747915, 19.98382045682413, 19.988026691397238, 19.98382045682413, 19.99118136732707, 19.98382045682413, 19.988026691397238, 19.98382045682413, 19.99118136732707, 19.98382045682413, 19.988026691397238, 19.98382045682413, 20.021150788660446, 19.98382045682413, 19.988026691397238, 19.985923574110686, 20.021150788660446, 19.98382045682413, 20.01431565747915, 19.985923574110686, 20.021150788660446, 19.98382045682413, 20.01431565747915, 19.985923574110686, 19.994336043256897, 19.98382045682413, 20.01431565747915, 19.985923574110686, 19.994336043256897, 19.98382045682413, 20.01431565747915, 19.985923574110686, 19.994336043256897, 19.98382045682413, 20.006954746976213, 20.023779685268636, 19.994336043256897], "source_mbps": [20.000119615794915], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 53.03738977771205, "family_stable": true} -{"program": "tvgate", "case": "shared64", "repetition": 1, "order": 2, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", "-config", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-final64/shared64-01-tvgate.yaml"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": false, "request_prefix": "udp", "duration_s": 20.041318133999994, "cpu_pct": 48.40001009486497, "user_cpu_pct": 16.515879733402375, "system_cpu_pct": 31.88413036146259, "pss_mib": 40.41484375, "uss_mib": 40.41484375, "server_processes": 1, "server_threads": 7, "multicast_sockets": 1, "client_mbps": [19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 20.000833743563593, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 20.000833743563593, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611, 19.99978311406611], "source_mbps": [19.99978311406611], "gaps": 3798606, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 542658, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 165.8074572631302, "family_stable": true} -{"program": "udpxy", "case": "shared64", "repetition": 1, "order": 3, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", "-T", "-a", "127.0.0.1", "-m", "lo", "-p", "38903", "-c", "256"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": false, "request_prefix": "rtp", "duration_s": 20.068672177000735, "cpu_pct": 59.396057172435434, "user_cpu_pct": 5.5808375866717865, "system_cpu_pct": 53.81521958576365, "pss_mib": 4.35546875, "uss_mib": 3.765625, "server_processes": 65, "server_threads": 65, "multicast_sockets": 64, "client_mbps": [19.984588739240596, 19.998228306302426, 20.001375898701305, 20.001375898701305, 19.989310127838923, 19.998228306302426, 20.001375898701305, 20.001375898701305, 20.001375898701305, 20.001375898701305, 19.981441146841718, 20.001375898701305, 20.001375898701305, 20.001375898701305, 19.981441146841718, 19.977768955709685, 19.99875290503557, 20.001375898701305, 20.001375898701305, 20.001375898701305, 20.001375898701305, 19.998228306302426, 19.98196574557486, 20.001375898701305, 19.977244356976538, 20.001375898701305, 19.98091654810857, 19.995605312636688, 20.001375898701305, 19.998228306302426, 19.981441146841718, 19.98773633163948, 20.001375898701305, 19.998228306302426, 19.981441146841718, 20.001375898701305, 20.001375898701305, 20.001375898701305, 19.987211732906335, 20.001375898701305, 19.98249034430801, 19.983539541774302, 19.981441146841718, 20.001375898701305, 20.001375898701305, 20.001375898701305, 19.985637936706894, 20.001375898701305, 20.001375898701305, 20.001375898701305, 19.976195159510244, 19.98196574557486, 19.983014943041155, 20.001375898701305, 19.977244356976538, 20.001375898701305, 19.985637936706894, 19.983539541774302, 19.983539541774302, 19.98091654810857, 20.001375898701305, 19.983014943041155, 19.998228306302426, 19.984588739240596], "source_mbps": [20.001375898701305], "gaps": 6909, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 987, "load_cpu_pct": 144.35434364810862, "family_stable": true} -{"program": "msd_lite", "case": "shared64", "repetition": 1, "order": 4, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", "-c", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-final64/shared64-01-msd_lite.xml", "-l", "1"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.06174371799989, "cpu_pct": 5.881841661357059, "user_cpu_pct": 0.548307273516336, "system_cpu_pct": 5.333534387840723, "pss_mib": 1.369140625, "uss_mib": 1.35546875, "server_processes": 1, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528, 20.00116149624528], "source_mbps": [20.000411810664037], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 73.97163580893114, "family_stable": true} -{"program": "udpxy", "case": "shared64", "repetition": 2, "order": 0, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", "-T", "-a", "127.0.0.1", "-m", "lo", "-p", "38485", "-c", "256"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": false, "request_prefix": "rtp", "duration_s": 20.063495135000267, "cpu_pct": 57.218345670856856, "user_cpu_pct": 5.532435861903407, "system_cpu_pct": 51.68590980895345, "pss_mib": 4.35546875, "uss_mib": 3.765625, "server_processes": 65, "server_threads": 65, "multicast_sockets": 64, "client_mbps": [20.00128957092573, 20.00128957092573, 19.99971536863508, 19.99866590044131, 20.00128957092573, 20.00128957092573, 19.999190634538195, 19.998141166344432, 20.00076483682885, 20.00128957092573, 20.000240102731965, 19.998141166344432, 20.00076483682885, 20.00128957092573, 20.001814305022616, 19.99971536863508, 20.00128957092573, 20.00128957092573, 19.99971536863508, 20.00128957092573, 20.00128957092573, 20.00128957092573, 20.00128957092573, 19.991844357181822, 20.00128957092573, 20.00128957092573, 20.00076483682885, 19.992369091278707, 20.00128957092573, 20.001814305022616, 20.00128957092573, 19.99289382537559, 20.00128957092573, 20.00128957092573, 19.996042229956892, 19.99289382537559, 20.00128957092573, 20.00128957092573, 19.997616432247547, 19.99289382537559, 20.00128957092573, 20.00128957092573, 20.001814305022616, 19.99446802766624, 20.00128957092573, 20.00128957092573, 19.998141166344432, 19.99289382537559, 20.00076483682885, 20.00128957092573, 19.997616432247547, 19.99446802766624, 20.00128957092573, 20.00128957092573, 19.997616432247547, 19.99446802766624, 20.00128957092573, 20.00128957092573, 19.998141166344432, 19.99289382537559, 20.00128957092573, 20.00128957092573, 19.996566964053777, 20.001814305022616], "source_mbps": [20.00128957092573], "gaps": 1687, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 241, "load_cpu_pct": 150.372603561825, "family_stable": true} -{"program": "tvgate", "case": "shared64", "repetition": 2, "order": 1, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", "-config", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-final64/shared64-02-tvgate.yaml"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": false, "request_prefix": "udp", "duration_s": 20.05543863499952, "cpu_pct": 52.35487585734498, "user_cpu_pct": 18.249413870274537, "system_cpu_pct": 34.10546198707044, "pss_mib": 40.42578125, "uss_mib": 40.42578125, "server_processes": 1, "server_threads": 6, "multicast_sockets": 1, "client_mbps": [20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498, 20.000400255519498], "source_mbps": [19.9998753106309], "gaps": 3859968, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 551424, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 173.9179114194469, "family_stable": true} -{"program": "baseline", "case": "shared64", "repetition": 2, "order": 2, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/baseline/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:45707"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.05155755099986, "cpu_pct": 30.77067696265987, "user_cpu_pct": 5.585601004567109, "system_cpu_pct": 25.18507595809276, "pss_mib": 11.922607421875, "uss_mib": 11.23828125, "server_processes": 2, "server_threads": 2, "multicast_sockets": 64, "client_mbps": [20.01162208867865, 19.98957013591262, 20.01162208867865, 19.98957013591262, 19.98957013591262, 19.98957013591262, 20.01162208867865, 19.98957013591262, 19.98957013591262, 19.98957013591262, 20.01162208867865, 20.01162208867865, 20.009521902700936, 19.98957013591262, 20.01162208867865, 20.01162208867865, 19.98957013591262, 20.004271437756643, 20.01162208867865, 20.01162208867865, 20.009521902700936, 20.004271437756643, 20.01162208867865, 20.01162208867865, 20.009521902700936, 20.004271437756643, 20.01162208867865, 20.01162208867865, 20.009521902700936, 19.98957013591262, 20.01162208867865, 20.01162208867865, 20.009521902700936, 19.98957013591262, 20.01162208867865, 20.01162208867865, 20.009521902700936, 19.98957013591262, 20.01162208867865, 20.01162208867865, 19.99954601930678, 19.98957013591262, 20.01162208867865, 20.01162208867865, 19.99954601930678, 19.98957013591262, 20.01162208867865, 20.01162208867865, 19.99954601930678, 19.98957013591262, 20.01162208867865, 20.01162208867865, 20.001646205284494, 19.98957013591262, 20.01162208867865, 20.01162208867865, 20.001646205284494, 19.98957013591262, 20.01162208867865, 20.01162208867865, 19.98957013591262, 19.98957013591262, 20.01162208867865, 20.01162208867865], "source_mbps": [20.000071065801208], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 52.913595230765196, "family_stable": true} -{"program": "rtp2httpd", "case": "shared64", "repetition": 2, "order": 3, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:60293"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.060818259999905, "cpu_pct": 7.676655957103553, "user_cpu_pct": 1.3459072132584151, "system_cpu_pct": 6.330748743845138, "pss_mib": 4.61005859375, "uss_mib": 3.98828125, "server_processes": 2, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967, 20.006582523119967], "source_mbps": [19.999760069607543], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 75.86928809552992, "family_stable": true} -{"program": "msd_lite", "case": "shared64", "repetition": 2, "order": 4, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", "-c", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-final64/shared64-02-msd_lite.xml", "-l", "1"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.07316530099979, "cpu_pct": 6.476307948977287, "user_cpu_pct": 0.49817753453671443, "system_cpu_pct": 5.978130414440573, "pss_mib": 1.3681640625, "uss_mib": 1.35546875, "server_processes": 1, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 19.98416144064833, 20.01023565426395, 19.98416144064833, 19.98416144064833, 20.01023565426395, 20.01023565426395, 19.98416144064833, 19.98416144064833, 20.01023565426395, 20.01023565426395, 20.01023565426395, 19.98416144064833, 20.01023565426395, 20.01023565426395, 20.01023565426395, 19.98416144064833, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395, 20.01023565426395], "source_mbps": [20.000570213009887], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 76.02189177030262, "family_stable": true} -{"program": "udpxy", "case": "shared64", "repetition": 3, "order": 0, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", "-T", "-a", "127.0.0.1", "-m", "lo", "-p", "44571", "-c", "256"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": false, "request_prefix": "rtp", "duration_s": 20.071070592999604, "cpu_pct": 60.73417929311471, "user_cpu_pct": 5.82928546127516, "system_cpu_pct": 54.904893831839544, "pss_mib": 4.606689453125, "uss_mib": 4.01953125, "server_processes": 65, "server_threads": 65, "multicast_sockets": 64, "client_mbps": [19.98324972958297, 19.97695529703565, 19.980102513309312, 20.00055941908808, 19.98324972958297, 20.00055941908808, 19.985872409811016, 20.001083955133687, 19.980627049354922, 19.97852890517248, 20.00213302722491, 20.00055941908808, 19.985872409811016, 19.9848233377198, 20.001608491179297, 19.98901962608467, 19.9848233377198, 19.99164230631272, 20.001608491179297, 19.987970553993453, 20.00213302722491, 19.98115158540053, 20.001608491179297, 19.99164230631272, 19.985872409811016, 19.985347873765406, 19.985872409811016, 19.987446017947846, 19.9848233377198, 19.986396945856622, 19.993740450495157, 19.99164230631272, 19.9848233377198, 19.9848233377198, 20.00055941908808, 20.001083955133687, 19.9848233377198, 19.985872409811016, 19.977479833081265, 19.979053441218092, 19.9848233377198, 19.979577977263705, 19.987446017947846, 19.986396945856622, 19.985872409811016, 19.986921481902233, 20.001608491179297, 19.990068698175893, 19.9848233377198, 19.986396945856622, 20.00055941908808, 19.988495090039063, 19.9848233377198, 20.001083955133687, 20.001608491179297, 19.988495090039063, 19.985872409811016, 20.00213302722491, 20.00055941908808, 19.986921481902233, 19.98324972958297, 20.00055941908808, 20.001083955133687, 19.984298801674186], "source_mbps": [20.001083955133687], "gaps": 9667, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 1381, "load_cpu_pct": 159.3836255608482, "family_stable": true} -{"program": "msd_lite", "case": "shared64", "repetition": 3, "order": 1, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", "-c", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-final64/shared64-03-msd_lite.xml", "-l", "1"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.049639842000033, "cpu_pct": 5.586135256424014, "user_cpu_pct": 0.44888586881978687, "system_cpu_pct": 5.137249387604228, "pss_mib": 1.368994140625, "uss_mib": 1.35546875, "server_processes": 1, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147, 19.989756781587147], "source_mbps": [20.000408743501822], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 76.26072149171713, "family_stable": true} -{"program": "rtp2httpd", "case": "shared64", "repetition": 3, "order": 2, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:36501"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.044193718000315, "cpu_pct": 7.084345820928657, "user_cpu_pct": 1.0476849453486043, "system_cpu_pct": 6.036660875580053, "pss_mib": 4.61396484375, "uss_mib": 3.9921875, "server_processes": 2, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382, 19.997439140694382], "source_mbps": [20.000590577009994], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 77.67835523370368, "family_stable": true} -{"program": "baseline", "case": "shared64", "repetition": 3, "order": 3, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/baseline/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:38733"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.04945400900033, "cpu_pct": 31.37242538962098, "user_cpu_pct": 5.536310362874291, "system_cpu_pct": 25.836115026746686, "pss_mib": 10.34833984375, "uss_mib": 9.6640625, "server_processes": 2, "server_threads": 2, "multicast_sockets": 64, "client_mbps": [19.999018817170896, 19.996393309265475, 19.999018817170896, 20.00584513772499, 19.999018817170896, 19.996393309265475, 19.999018817170896, 20.00584513772499, 19.999018817170896, 19.996393309265475, 19.999018817170896, 20.00584513772499, 19.999018817170896, 20.0021694266574, 19.999018817170896, 20.00584513772499, 20.00584513772499, 19.999018817170896, 19.999018817170896, 20.00584513772499, 20.00584513772499, 19.999018817170896, 19.999018817170896, 20.00584513772499, 20.00584513772499, 19.999018817170896, 19.999018817170896, 20.00584513772499, 20.00584513772499, 19.999018817170896, 19.999018817170896, 20.00584513772499, 20.00584513772499, 19.999018817170896, 19.999018817170896, 20.00584513772499, 20.00584513772499, 19.999018817170896, 19.999018817170896, 20.00584513772499, 20.00584513772499, 19.999018817170896, 19.999018817170896, 20.00584513772499, 20.00584513772499, 19.999018817170896, 19.999018817170896, 20.00584513772499, 20.00584513772499, 19.999018817170896, 19.999018817170896, 20.00584513772499, 20.00584513772499, 19.999018817170896, 19.999018817170896, 20.00584513772499, 20.00584513772499, 19.999018817170896, 19.999018817170896, 20.007945544049328, 20.00584513772499, 19.999018817170896, 19.999018817170896, 19.996393309265475], "source_mbps": [20.00006902033306], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 55.16359694899968, "family_stable": true} -{"program": "tvgate", "case": "shared64", "repetition": 3, "order": 4, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", "-config", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-final64/shared64-03-tvgate.yaml"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": false, "request_prefix": "udp", "duration_s": 20.0317617590008, "cpu_pct": 46.925478213499275, "user_cpu_pct": 16.12439304570241, "system_cpu_pct": 30.801085167796867, "pss_mib": 40.796875, "uss_mib": 40.796875, "server_processes": 1, "server_threads": 7, "multicast_sockets": 1, "client_mbps": [19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814, 19.999864056888814], "source_mbps": [19.999864056888814], "gaps": 3822483, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 546069, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 164.68846024078096, "family_stable": true} -{"program": "baseline", "case": "shared64", "repetition": 4, "order": 0, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/baseline/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:53909"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.037870343001487, "cpu_pct": 29.194719298317032, "user_cpu_pct": 4.840833798182482, "system_cpu_pct": 24.35388550013455, "pss_mib": 8.98515625, "uss_mib": 8.34375, "server_processes": 2, "server_threads": 2, "multicast_sockets": 64, "client_mbps": [20.01688488517885, 20.01583407490612, 20.013732454360667, 20.013732454360667, 20.01688488517885, 20.01583407490612, 20.013732454360667, 20.013732454360667, 20.01688488517885, 20.01583407490612, 20.013732454360667, 20.013732454360667, 20.01688488517885, 20.01583407490612, 20.013732454360667, 20.013732454360667, 20.01688488517885, 20.01583407490612, 20.013732454360667, 20.013732454360667, 20.01583407490612, 20.01583407490612, 20.013732454360667, 20.00847840299703, 20.01583407490612, 20.01583407490612, 20.013732454360667, 20.00847840299703, 20.01583407490612, 20.01583407490612, 20.013732454360667, 20.00847840299703, 20.01583407490612, 20.01583407490612, 20.013732454360667, 20.00847840299703, 20.01583407490612, 20.01583407490612, 20.013732454360667, 20.00847840299703, 20.01583407490612, 20.01583407490612, 20.013732454360667, 20.00847840299703, 20.01583407490612, 20.01583407490612, 20.013732454360667, 20.00847840299703, 20.01583407490612, 20.01583407490612, 20.013732454360667, 20.00847840299703, 20.01583407490612, 20.01583407490612, 20.013732454360667, 20.010580023542484, 20.01583407490612, 20.01583407490612, 20.013732454360667, 20.010580023542484, 20.01583407490612, 20.01583407490612, 20.013732454360667, 20.013732454360667], "source_mbps": [20.000071920815216], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 50.204936092490485, "family_stable": true} -{"program": "rtp2httpd", "case": "shared64", "repetition": 4, "order": 1, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/final/build/rtp2httpd", "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", "127.0.0.1:54461"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.04395621799995, "cpu_pct": 6.685306959494595, "user_cpu_pct": 1.197368410655748, "system_cpu_pct": 5.487938548838846, "pss_mib": 4.61396484375, "uss_mib": 3.99609375, "server_processes": 2, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 20.023413124380085, 19.997676089515842], "source_mbps": [20.000302317563218], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 76.78124933329985, "family_stable": true} -{"program": "msd_lite", "case": "shared64", "repetition": 4, "order": 2, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/msd/build/src/msd_lite", "-c", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-final64/shared64-04-msd_lite.xml", "-l", "1"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.063194759999533, "cpu_pct": 5.781731244082421, "user_cpu_pct": 0.49842510724848454, "system_cpu_pct": 5.283306136833936, "pss_mib": 1.368994140625, "uss_mib": 1.35546875, "server_processes": 1, "server_threads": 2, "multicast_sockets": 1, "client_mbps": [19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926, 19.99978990384926], "source_mbps": [20.001064277163472], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 78.8010094559854, "family_stable": true} -{"program": "udpxy", "case": "shared64", "repetition": 4, "order": 3, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/udpxy/chipmunk/udpxy", "-T", "-a", "127.0.0.1", "-m", "lo", "-p", "52219", "-c", "256"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": true, "request_prefix": "rtp", "duration_s": 20.055766092998965, "cpu_pct": 54.79720868821048, "user_cpu_pct": 5.384985021225416, "system_cpu_pct": 49.412223666985064, "pss_mib": 4.60302734375, "uss_mib": 4.01171875, "server_processes": 65, "server_threads": 65, "multicast_sockets": 64, "client_mbps": [20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001648510451677, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001648510451677, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001648510451677, 20.001123574134052, 20.001123574134052, 20.001123574134052, 20.001123574134052], "source_mbps": [20.001648510451677], "gaps": 0, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 0, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 150.33083184254286, "family_stable": true} -{"program": "tvgate", "case": "shared64", "repetition": 4, "order": 4, "command": ["taskset", "-c", "0", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/vendors/tvgate/TVGate-linux-arm64", "-config", "/home/parallels/Repos/openwrt-dev/rtp2httpd/build-benchmark-shared/fourway-final64/shared64-04-tvgate.yaml"], "clients": 64, "sources": 1, "target_mbps": 20, "valid": false, "request_prefix": "udp", "duration_s": 20.028976925999814, "cpu_pct": 53.3726712028072, "user_cpu_pct": 19.27207772150007, "system_cpu_pct": 34.10059348130712, "pss_mib": 41.3240234375, "uss_mib": 41.3240234375, "server_processes": 1, "server_threads": 6, "multicast_sockets": 1, "client_mbps": [20.000542288307777, 20.001067926738482, 20.001067926738482, 20.001067926738482, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.001067926738482, 20.000542288307777, 20.000542288307777, 20.001067926738482, 20.000016649877075, 20.001067926738482, 20.000016649877075, 20.001067926738482, 20.000016649877075, 20.001593565169184, 20.001067926738482, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.001593565169184, 20.001067926738482, 20.000016649877075, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.001067926738482, 20.000542288307777, 20.000542288307777, 20.001067926738482, 20.000542288307777, 20.001067926738482, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.000016649877075, 20.000542288307777, 20.001067926738482, 20.000542288307777, 20.001067926738482, 20.001067926738482, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.000016649877075, 20.001593565169184, 20.001067926738482, 20.001067926738482, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.000542288307777, 20.001067926738482, 20.000542288307777], "source_mbps": [20.000542288307777], "gaps": 4019246, "duplicates": 0, "corrupt_packets": 0, "backward_markers": 574178, "closed_clients": 0, "kernel_udp_drops": 0, "load_cpu_pct": 176.49428690544755, "family_stable": true} From 913e360288c2676b0b608a89568fd784a6d8b771 Mon Sep 17 00:00:00 2001 From: Stackie Jia Date: Sun, 6 Sep 2026 19:08:49 +0800 Subject: [PATCH 09/19] perf(benchmark): measure TVGate with default scheduling --- .github/workflows/lint.yaml | 1 - .gitignore | 1 + docs/en/reference/benchmark.md | 124 +++++++++++----------------- docs/en/reference/configuration.md | 2 - docs/reference/benchmark.md | 114 +++++++++---------------- docs/reference/configuration.md | 2 - tools/stress-test/README.md | 19 ++--- tools/stress-test/benchmark.py | 40 ++++++--- tools/stress-test/test_benchmark.py | 12 ++- 9 files changed, 136 insertions(+), 179 deletions(-) diff --git a/.github/workflows/lint.yaml b/.github/workflows/lint.yaml index 9175749f..7706c725 100644 --- a/.github/workflows/lint.yaml +++ b/.github/workflows/lint.yaml @@ -28,4 +28,3 @@ jobs: - run: pnpm run type-check - run: pnpm run lint - run: pnpm run web-ui:test - - run: uv run pytest tools/stress-test/test_benchmark.py -q diff --git a/.gitignore b/.gitignore index 8f8cb343..8dcc4478 100644 --- a/.gitignore +++ b/.gitignore @@ -30,6 +30,7 @@ __pycache__/ # Benchmark results tools/benchmark_results_*.txt +tools/stress-test/results/ # iKuai package outputs ikuai-support/.staging/ diff --git a/docs/en/reference/benchmark.md b/docs/en/reference/benchmark.md index 3b34cdcb..3dc0829e 100644 --- a/docs/en/reference/benchmark.md +++ b/docs/en/reference/benchmark.md @@ -1,103 +1,75 @@ # Performance Benchmark -This report compares CPU usage, memory consumption, and output integrity for **rtp2httpd**, **[msd_lite](https://github.com/rozhuk-im/msd_lite)**, **[udpxy](https://github.com/pcherenkov/udpxy)**, and **[TVGate](https://github.com/qist/tvgate)** under the same multicast workload. This update adds 64 clients watching one channel, alongside the multi-channel, eight-client shared-channel, and high-bitrate tests. +Compare **rtp2httpd**, **[msd_lite](https://github.com/rozhuk-im/msd_lite)**, **[udpxy](https://github.com/pcherenkov/udpxy)**, and **[TVGate](https://github.com/qist/tvgate)** for CPU and memory consumption with multiple channels, multiple clients sharing one channel, and high-bitrate input. -## Environment and Versions +## Test Environment and Versions - Test date: 2026-09-06. - Host: Apple M3 Max; Parallels Ubuntu 24.04 virtual machine with 16 vCPUs and 16 GiB RAM. -- System: Linux 6.8.0-138-generic, aarch64; all programs execute natively as ARM64 binaries. -- Compiler: GCC 13.3.0. rtp2httpd uses Release with `ENABLE_AGGRESSIVE_OPT=ON`; msd_lite uses `-O3`, LTO, and equivalent inlining, loop-unrolling, and vectorization options; udpxy uses `-O3 -flto`. TVGate uses the official release binary. -- Multicast input and HTTP output both use `lo`, with no network sysctl changes. `net.core.rmem_max` and `net.core.wmem_max` are both 212992; TCP congestion control is cubic. +- System: Linux 6.8.0-138-generic, aarch64; all programs run natively on ARM64. +- Compiler: GCC 13.3.0. rtp2httpd uses Release and `ENABLE_AGGRESSIVE_OPT=ON`; msd_lite uses `-O3`, LTO, and the same inlining, loop-unrolling, and vectorization options; udpxy uses `-O3 -flto`. TVGate uses its official release binary. +- Multicast input and HTTP output both use `lo`, with no kernel network tuning. `net.core.rmem_max` and `net.core.wmem_max` are both 212992; TCP congestion control is cubic. | Program | Tested version | | --- | --- | -| rtp2httpd (optimized) | [`530dc980`](https://github.com/stackia/rtp2httpd/commit/530dc980e92db6b6ea98b6ca223dffe1a0345b5c) | +| rtp2httpd | [`530dc980`](https://github.com/stackia/rtp2httpd/commit/530dc980e92db6b6ea98b6ca223dffe1a0345b5c) | | msd_lite | [`fa68e131`](https://github.com/rozhuk-im/msd_lite/commit/fa68e131343fb58c67ad77b2d26f2cb7c49a2c95), 2026-07-20; liblcb `e2f420a2` | | udpxy | [`31d4bcfa`](https://github.com/pcherenkov/udpxy/commit/31d4bcfabaade59d3efdee015df7979febf76bae), 2026-04-13 | | TVGate | [v3.2.0](https://github.com/qist/tvgate/releases/tag/v3.2.0), 2026-09-06 | -| rtp2httpd (pre-optimization baseline) | [`f8c243cb`](https://github.com/stackia/rtp2httpd/commit/f8c243cb6fc98e259fd2f2fe0dc992f2a845014e), used only for the 64-client comparison | -msd_lite and udpxy use the latest upstream commits retrieved for this test; TVGate uses the latest stable release available at the time. The TVGate ARM64 release archive has SHA-256 `1655a066b91debdaf2f3b39096207f80fdfc7bd9c2f227ff9dda34c48562ac3e`. Executable SHA-256 values, full commands, environment settings, and individual trials are stored in the [raw results directory](https://github.com/stackia/rtp2httpd/tree/main/tools/stress-test/results/2026-09-06). +## Measurement Method -## Methodology +rtp2httpd uses `-C -w 1`, msd_lite uses one event-loop thread, and udpxy retains its native process-per-client model. All processes and threads of these three programs are pinned to one vCPU. TVGate runs without a `GOMAXPROCS` override or an additional CPU-affinity restriction, using default multicore scheduling across all 16 VM vCPUs. Scheduling policies differ, so the results describe server CPU costs under the specified input load and configuration. -All processes and threads of each server are pinned to one vCPU. rtp2httpd explicitly uses `-C -w 1`; msd_lite uses one event-loop thread; TVGate uses `GOMAXPROCS=1`; udpxy retains its native process-per-client model, with all child processes included. This compares programs under the same single-core budget, rather than assuming that each has only one process or thread. +CPU utilization is the change in user and system CPU time from `/proc/PID/stat` over the complete measurement window, divided by actual wall time and summed over the entire server process tree. **100% means one fully occupied vCPU**; multicore programs can exceed 100%. Generators, readers, and the measurement controller are pinned to other vCPUs. Their CPU is recorded separately and excluded from server CPU; TVGate's default scheduler can use these vCPUs too. -CPU usage is the change in user and system CPU time from `/proc/PID/stat` over the entire measurement window, divided by actual wall time and summed across the server process tree. **100% means one fully occupied vCPU**. Generators, readers, and the controller run on separate vCPUs; their CPU usage is recorded separately and excluded from server CPU. PSS and USS are sampled from `smaps_rollup` once per second and summed across the process tree. PSS includes proportional shared pages; USS counts private pages. Neither includes all kernel socket memory or unmapped anonymous-file cache pages, so these metrics do not represent the service’s total memory cost. +PSS and USS are sampled every second from `smaps_rollup` and summed over the process tree. PSS proportionally includes shared pages, while USS includes only private pages. Neither includes all kernel socket memory or unmapped anonymous-file cache pages, so these metrics do not represent total server memory cost. -Each RTP datagram carries seven 188-byte MPEG-TS null packets, totaling 1316 payload bytes. Every TS packet contains an increasing sequence marker, its complement, a source identifier, and fixed content for validation. Readers decode HTTP chunk framing before checking every packet's content and continuity. Each client and generator must sustain a mean payload rate within ±2% of the target, with no sequence gaps, duplicates, backward markers, content errors, client EOFs, or kernel UDP drops during the measurement window. Failed samples remain in the raw output and are explicitly marked invalid. +Each RTP datagram carries seven 188-byte MPEG-TS null packets, totaling 1316 payload bytes. Readers decode HTTP chunk framing and continuously consume the stream. Each trial restarts the server and load processes, then warms up after every client starts receiving data. Tests run sequentially. -Trials run sequentially, changing program order across repetitions and restarting both server and load processes. Warmup starts after all clients receive data. msd_lite retains the upstream example's 48 KiB receive watermark, 64 KiB send watermark, and 1 MiB ring; only the listener, interface, thread count, logging, and congestion control are adapted. udpxy retains its default buffer settings. TVGate uses loopback upstream interfaces and a connection limit of 256. +msd_lite retains the upstream example's 48 KiB receive watermark, 64 KiB send watermark, and 1 MiB ring buffer; only the listener, interface, thread count, logging, and congestion control are adapted. udpxy retains its default buffer settings. TVGate configures only its listening port and loopback multicast interfaces; concurrency, buffering, connection limits, and logging use application defaults. + +## Test Scenarios | Scenario | Clients | Multicast sources | Payload rate per source | Repetitions | Warmup / sampling per trial | | --- | ---: | ---: | ---: | ---: | --- | -| 64 clients, one channel | 64 | 1 | 20 Mbps | 5 | 5 s / 20 s | | Multiple channels | 8 | 8 | 40 Mbps | 3 | 5 s / 10 s | -| 8 clients, one channel | 8 | 1 | 40 Mbps | 3 | 5 s / 10 s | +| 8 clients sharing one channel | 8 | 1 | 40 Mbps | 3 | 5 s / 10 s | +| 64 clients sharing one channel | 64 | 1 | 20 Mbps | 5 | 5 s / 20 s | | High bitrate | 1 | 1 | 400 Mbps | 3 | 5 s / 10 s | ## Results -### 64 Clients Watching One Channel - -CPU values are means of valid samples, followed by the minimum and maximum across trials. Memory values are means of valid samples; trials failing integrity checks are excluded. - -| Program | Mean CPU (range) | PSS (MiB) | USS (MiB) | Valid / total trials | Multicast sockets | -| --- | --- | ---: | ---: | ---: | ---: | -| rtp2httpd | 6.97% (6.33–7.68) | 4.61 | 3.99 | 5/5 | 1 | -| msd_lite | 5.91% (5.59–6.48) | 1.37 | 1.36 | 5/5 | 1 | -| udpxy | 56.75% (54.80–58.70) | 4.61 | 4.02 | 2/5 | 64 | -| TVGate | — | — | — | 0/5 | 1 | -| rtp2httpd (baseline) | 30.10% (29.07–31.37) | 10.28 | 9.60 | 5/5 | 64 | - -Both the optimized version and baseline passed all five trials. Each optimized-version client received 19.985–20.023 Mbps of payload, totaling approximately 1.28 Gbps. All five trials had no sequence gaps, backward markers, duplicates, content errors, EOFs, or kernel UDP drops. +CPU and memory statistics include every completed measurement in each scenario. They measure resource consumption under the specified load, without certifying forwarding correctness or maximum sustainable throughput. -3 udpxy trials recorded kernel UDP drops; the table includes only the other 2 trials. All five TVGate trials failed sequence-continuity checks. Their observed CPU usage was 46.93–53.37%, excluded from valid forwarding comparisons. +### CPU Utilization -### Additional Scenarios - -Cells show “mean CPU of valid samples; valid / total trials.” +Values are the means of per-trial average CPU utilization, with the minimum and maximum in parentheses. | Scenario | rtp2httpd | msd_lite | udpxy | TVGate | -| --- | --- | --- | --- | --- | -| 8 channels, 40 Mbps each | 9.78%; 3/3 | 9.49%; 3/3 | 20.81%; 3/3 | —; 0/3 | -| 8 clients, one 40 Mbps channel | 7.21%; 3/3 | 6.07%; 3/3 | 26.93%; 3/3 | —; 0/3 | -| 1 client, 400 Mbps | 14.79%; 2/3 | 13.76%; 1/3 | —; 0/3 | —; 0/3 | +| --- | ---: | ---: | ---: | ---: | +| 8 channels, 40 Mbps each | 9.78% (9.08–10.29) | 9.49% (9.19–9.78) | 20.81% (20.27–21.28) | 63.32% (62.56–63.96) | +| 8 clients, one 40 Mbps channel | 7.21% (6.97–7.47) | 6.07% (5.78–6.27) | 26.93% (26.21–27.78) | 43.20% (40.96–45.27) | +| 64 clients, one 20 Mbps channel | 6.97% (6.33–7.68) | 5.91% (5.59–6.48) | 58.17% (54.80–60.73) | 149.79% (136.85–156.83) | +| 1 client, 400 Mbps | 15.24% (13.84–16.14) | 14.78% (13.76–16.43) | 23.31% (22.84–23.55) | 47.99% (47.33–48.41) | -Memory values below are “PSS / USS” in MiB, using the same valid samples. +### PSS Memory (MiB) | Scenario | rtp2httpd | msd_lite | udpxy | TVGate | -| --- | --- | --- | --- | --- | -| 8 channels, 40 Mbps each | 2.28 / 1.66 | 8.95 / 8.94 | 0.80 / 0.53 | — / — | -| 8 clients, one 40 Mbps channel | 1.60 / 0.98 | 1.35 / 1.34 | 0.79 / 0.52 | — / — | -| 1 client, 400 Mbps | 1.43 / 0.84 | 1.34 / 1.33 | — / — | — / — | - -When some trials fail, the mean of the remaining samples does not imply stable forwarding across all trials. Non-TVGate failures in the additional scenarios are listed below; every failed record is retained with the report. - -| Scenario | Program | Trial | Kernel UDP drops | TS sequence gaps | -| --- | --- | ---: | ---: | ---: | -| 400 Mbps | rtp2httpd | 1 | 37 | 259 | -| 400 Mbps | udpxy | 1 | 29 | 203 | -| 400 Mbps | udpxy | 2 | 133 | 931 | -| 400 Mbps | msd_lite | 2 | 289 | 2023 | -| 400 Mbps | udpxy | 3 | 97 | 679 | -| 400 Mbps | msd_lite | 3 | 273 | 1911 | +| --- | ---: | ---: | ---: | ---: | +| 8 channels, 40 Mbps each | 2.28 | 8.95 | 0.80 | 25.72 | +| 8 clients, one 40 Mbps channel | 1.60 | 1.35 | 0.79 | 22.68 | +| 64 clients, one 20 Mbps channel | 4.61 | 1.37 | 4.51 | 45.65 | +| 1 client, 400 Mbps | 1.42 | 1.34 | 0.32 | 19.12 | -### Output Integrity +### USS Memory (MiB) -TVGate receives the same RTP input through the `/udp/` endpoint recommended in its [official documentation](https://github.com/qist/tvgate/blob/main/doc/MULTICAST.md). TS content checks pass, but increasing markers jump backward and forward; a normal mean bitrate does not establish continuity. A separate single-client capture also showed sequences such as `0…6 → 0…6 → 14…20`. This report establishes only that this version failed continuity checks for this synthetic forwarding workload, without extrapolating to other video sources or versions. - -For a complete measurement record, the table below includes observed TVGate CPU usage across all trials. Every sample failed continuity checks and cannot be treated as a valid forwarding performance result. - -| Scenario | Observed mean CPU (range) | -| --- | --- | -| 64 clients, one 20 Mbps channel | 49.81% (46.93–53.37) | -| 8 channels, 40 Mbps each | 30.91% (30.27–31.38) | -| 8 clients, one 40 Mbps channel | 22.33% (21.83–22.74) | -| 1 client, 400 Mbps | 27.92% (25.64–30.10) | - -For 64 clients watching one channel, both rtp2httpd and msd_lite passed every trial, with msd_lite using less CPU. Every program had invalid samples at 400 Mbps, so these results cannot establish a stable-forwarding performance ranking. CPU cost, buffering behavior, and output integrity must be considered together. +| Scenario | rtp2httpd | msd_lite | udpxy | TVGate | +| --- | ---: | ---: | ---: | ---: | +| 8 channels, 40 Mbps each | 1.66 | 8.94 | 0.53 | 25.72 | +| 8 clients, one 40 Mbps channel | 0.98 | 1.34 | 0.52 | 22.68 | +| 64 clients, one 20 Mbps channel | 3.99 | 1.36 | 3.92 | 45.65 | +| 1 client, 400 Mbps | 0.82 | 1.33 | 0.11 | 19.12 | ## Performance Optimizations in rtp2httpd @@ -107,21 +79,21 @@ Each worker maintains a shared-source registry keyed by the resolved multicast a Each source owns its lifecycle, timeout, and rejoin timers. Clients hold subscription references; releasing the last reference closes the sockets and destroys source state. When the first client leaves, event dispatch is reassigned to a surviving subscriber. Workers continue to maintain their source registries independently. -This primarily reduces duplicate local socket receives, system calls, and application processing. Multiple local sockets joining one multicast group do not necessarily cause the upstream link to carry the same number of complete streams. This benchmark does not claim network-side bandwidth savings. +This primarily reduces duplicate local socket receives, system calls, and application processing. Multiple local sockets joining one multicast group do not necessarily cause the upstream link to carry the same number of complete streams. ### Shared Parsing, Reordering, and Batch Payloads -For ordinary multicast, the shared source parses and reorders RTP once, then combines payloads into batches with a capacity of 64 KiB. Each RTP payload remains intact: the current batch is flushed before the next payload would exceed capacity. The 1316-byte payloads used here produce batches of 49 packets, or 64484 bytes. This reduces both repeated per-client parsing and per-packet fanout and send calls. Partial batches flush at the next worker timer check after reaching 100 ms of age. The timer runs every 100 ms; scheduling also affects actual latency. +For ordinary multicast, the shared source parses and reorders RTP once, then combines payloads into batches with a capacity of 64 KiB. Each RTP payload remains intact: the current batch is flushed before the next payload would exceed capacity. The 1316-byte payloads produce batches of 49 packets, or 64484 bytes. This reduces both repeated per-client parsing and per-packet fanout and send calls. Partial batches flush at the next worker timer check after reaching 100 ms of age. The timer runs every 100 ms; scheduling also affects actual latency. The Buffer layer adds an on-demand 64 KiB batch pool alongside the existing 1536-byte packet pool and control pool. The worker owns the batch pool, so queued data can outlive its multicast source. It initially allocates four batches and grows in increments of four. Its maximum capacity is derived from a `buffer-pool-max-size × 1536` byte budget, with room for at least four batches. This limit applies to the batch pool separately from the original packet pool. If the batch pool is exhausted, forwarding can continue through small-packet references. -Clients share the underlying payload while each owns a separate `buffer_ref_t` view. Its `owner` points to the same immutable data; list links, send offsets, and remaining lengths stay independent. A partial send updates only that client's view. The backing memory returns to the pool only after the last view is released. Each client retains its own send queue and packet-drop policy, so a slow client does not pause reception for other subscribers. +Clients share the underlying payload while each owns a separate `buffer_ref_t` view. Its `owner` points to the same immutable data; list links, send offsets, and remaining lengths stay independent. A partial send updates only that client's view. The backing memory returns to the pool only after the last view is released. Each client retains its own send queue and capacity limit, so a slow client does not pause reception for other subscribers. Queue limits now charge the backing buffer capacity instead of assuming “buffer count × 1536.” A batch with only a few unsent bytes still consumes the full 64 KiB allowance until that client releases its reference. This prevents shared large buffers from bypassing the existing slow-client memory limits. ### Immutable Batch Snapshots -In this Linux test, complete shared batches also use anonymous memory files created with `memfd_create`. Each file is written once, sealed, and sent to multiple clients through `sendfile`, reusing the same kernel pages. This path applies only when multiple clients share a nearly full batch. Ordinary memory buffers continue to use `sendmsg`. +Platforms supporting memory-file sealing use `memfd_create` to create anonymous memory files for shared batches. Each file is written once, sealed, and sent to multiple clients through `sendfile`, reusing the same kernel pages. This path applies only when multiple clients share a nearly full batch. Ordinary memory buffers continue to use `sendmsg`. Every batch gets a new file. Once published, it cannot be written, grown, or truncated; reusing pool memory never overwrites an old file. TCP may still reference its pages after `sendfile` returns and the application closes its last file reference. Immutability ensures that a new batch cannot alter those pending bytes. File creation, writing, or sealing failures retain memory sending. If a client's `sendfile` operation is unsupported, only that client's view falls back to memory sending. @@ -133,20 +105,16 @@ Snapshots retain independent processing state. Sources configured with an FEC po ## Scope -This is a fixed-bitrate forwarding test inside an ARM64 Linux virtual machine. It does not measure physical-NIC throughput limits, video decoding, or maximum client capacity. Loopback kernel work charged to generators and readers is outside the server CPU metric, and host scheduling introduces variation. Other hardware, bitrates, client speeds, channel counts, and network paths require separate measurements. +This measures fixed-bitrate forwarding resources inside an ARM64 Linux virtual machine, rather than physical-NIC throughput limits, video decoding, or maximum client capacity. Loopback kernel work charged to generators and readers is outside server CPU, and host scheduling introduces variation. Other hardware, bitrates, client speeds, channel counts, and network paths require separate measurements. ## Reproducing the Tests -See [tools/stress-test/README.md](https://github.com/stackia/rtp2httpd/blob/main/tools/stress-test/README.md) for the harness and options. Prepare the corresponding binaries, then run: +See [tools/stress-test/README.md](https://github.com/stackia/rtp2httpd/blob/main/tools/stress-test/README.md) for the harness and options. Prepare the corresponding binaries, then run all four scenarios: ```bash -# Four projects, 64 clients watching one channel, five repetitions. -scripts/benchmark.sh rtp2httpd msd_lite udpxy tvgate \ - --cases shared64 --repetitions 5 --warmup 5 --duration 20 - -# Three additional scenarios, three repetitions. scripts/benchmark.sh rtp2httpd msd_lite udpxy tvgate \ - --cases distinct8 shared8 high400 --repetitions 3 --warmup 5 --duration 10 + --cases distinct8 shared8 shared64 high400 \ + --repetitions 5 --warmup 5 --duration 20 ``` -Use `--binary NAME=PATH` and `--revision NAME=VERSION` to identify the actual executables and versions. For the pre/post comparison, also pass the `baseline` program name and `--binary baseline=PATH`. The default output directory is under `build/benchmark/` and contains the environment, executable hashes, individual trials, summary, logs, and generated configs. Runs containing invalid samples exit with a nonzero status. +Use `--binary NAME=PATH` and `--revision NAME=VERSION` to identify the actual executables and versions. Set repetitions and sampling duration for each scenario according to the table above. CPU, PSS, and USS summaries are written to `resources.json` in the output directory, which defaults to `build/benchmark/`. Test records remain local. diff --git a/docs/en/reference/configuration.md b/docs/en/reference/configuration.md index cfb8c450..e9ccff35 100644 --- a/docs/en/reference/configuration.md +++ b/docs/en/reference/configuration.md @@ -20,8 +20,6 @@ rtp2httpd [options] - `-m, --maxclients ` - Maximum concurrent clients (default: 5) - `-w, --workers ` - Number of worker processes (default: 1) -Requests for the same multicast source automatically share a subscription within each worker, without additional configuration. See the [Performance Benchmark](/en/reference/benchmark#performance-optimizations-in-rtp2httpd) for implementation details and test results. - `--listen` can be specified multiple times to listen on multiple TCP addresses/ports or Unix sockets: ```bash diff --git a/docs/reference/benchmark.md b/docs/reference/benchmark.md index 31a8c2f1..e88bdd78 100644 --- a/docs/reference/benchmark.md +++ b/docs/reference/benchmark.md @@ -1,6 +1,6 @@ # 性能测试报告 -比较 **rtp2httpd**、**[msd_lite](https://github.com/rozhuk-im/msd_lite)**、**[udpxy](https://github.com/pcherenkov/udpxy)** 和 **[TVGate](https://github.com/qist/tvgate)** 在相同组播负载下的 CPU、内存占用及输出完整性。本次重点增加了 64 个客户端观看同一频道的场景,并保留多频道、8 客户端同频道和高码率测试。 +比较 **rtp2httpd**、**[msd_lite](https://github.com/rozhuk-im/msd_lite)**、**[udpxy](https://github.com/pcherenkov/udpxy)** 和 **[TVGate](https://github.com/qist/tvgate)** 在多频道、同频道多客户端及高码率负载下的 CPU 和内存占用。 ## 测试环境与版本 @@ -12,92 +12,64 @@ | 程序 | 测试版本 | | --- | --- | -| rtp2httpd(优化后) | [`530dc980`](https://github.com/stackia/rtp2httpd/commit/530dc980e92db6b6ea98b6ca223dffe1a0345b5c) | +| rtp2httpd | [`530dc980`](https://github.com/stackia/rtp2httpd/commit/530dc980e92db6b6ea98b6ca223dffe1a0345b5c) | | msd_lite | [`fa68e131`](https://github.com/rozhuk-im/msd_lite/commit/fa68e131343fb58c67ad77b2d26f2cb7c49a2c95),2026-07-20;liblcb `e2f420a2` | | udpxy | [`31d4bcfa`](https://github.com/pcherenkov/udpxy/commit/31d4bcfabaade59d3efdee015df7979febf76bae),2026-04-13 | | TVGate | [v3.2.0](https://github.com/qist/tvgate/releases/tag/v3.2.0),2026-09-06 | -| rtp2httpd(优化前基线) | [`f8c243cb`](https://github.com/stackia/rtp2httpd/commit/f8c243cb6fc98e259fd2f2fe0dc992f2a845014e),仅用于 64 客户端对照 | - -msd_lite、udpxy 为测试时取得的最新上游提交;TVGate 为当时最新正式版本。TVGate ARM64 发布压缩包的 SHA-256 为 `1655a066b91debdaf2f3b39096207f80fdfc7bd9c2f227ff9dda34c48562ac3e`。可执行文件的 SHA-256、完整命令、环境参数和逐轮结果保存在[原始数据目录](https://github.com/stackia/rtp2httpd/tree/main/tools/stress-test/results/2026-09-06)。 ## 测量方法 -所有被测服务的进程及线程固定在同一个 vCPU。rtp2httpd 明确设置 `-C -w 1`,msd_lite 配置一个事件循环线程;TVGate 设置 `GOMAXPROCS=1`;udpxy 保留每个客户端一个子进程的原生模型,所有子进程均计入统计。因此,这是相同单核预算下的对比,并非所有程序都只有一个线程或进程。 +rtp2httpd 设置 `-C -w 1`,msd_lite 配置一个事件循环线程,udpxy 保留每个客户端一个子进程的原生模型;这三个程序的全部进程及线程固定在一个 vCPU。TVGate 不设置 `GOMAXPROCS`,不额外限制 CPU 亲和性,使用虚拟机全部 16 个 vCPU 上的默认多核调度。调度方式不同,结果反映各自配置在指定输入负载下的服务 CPU 成本。 + +CPU 使用率取完整测量窗口内 `/proc/PID/stat` 的用户态与内核态时间增量,除以实际墙钟时间,再对整个服务进程树求和。**100% 表示占满一个 vCPU**,多核程序可以超过 100%。负载发送器、接收器和测量控制进程固定在其他 vCPU,其 CPU 另行记录,不计入服务 CPU;TVGate 的默认调度可使用这些 vCPU。 + +PSS 和 USS 从 `smaps_rollup` 每秒采样并对进程树求和。PSS 按比例计入共享页,USS 仅统计私有页,均不包含全部内核 socket 内存或未映射的匿名文件页缓存,因此不能作为服务总内存成本。 -CPU 使用率取完整测量窗口内 `/proc/PID/stat` 的用户态与内核态时间增量,除以实际墙钟时间,再对整个服务进程树求和。**100% 表示占满一个 vCPU**。发送器、接收器及测量控制进程使用其他独立 vCPU,其 CPU 另行记录,不计入服务 CPU。PSS 和 USS 从 `smaps_rollup` 每秒采样并对进程树求和;PSS 按比例计入共享页,USS 仅统计私有页,均不包含全部内核 socket 内存或未映射的匿名文件页缓存,因此不能作为服务总内存成本。 +每个 RTP 数据报携带 7 个 188 字节 MPEG-TS 空包,共 1316 字节负载。接收端解析 HTTP 分块编码后持续读取流。每轮重启服务与负载进程,所有客户端开始收到数据后再预热,测试逐项串行执行。 -每个 RTP 数据报携带 7 个 188 字节 MPEG-TS 空包,共 1316 字节负载。每个 TS 包包含递增序号、序号反码、源标识及固定校验内容。接收端先解析 HTTP 分块编码,再逐包验证内容和连续性。各客户端及发送器的平均负载码率必须在目标值 ±2% 内,测量窗口内不得出现序号缺口、重复、回退、内容错误、客户端断流或内核 UDP 丢包。未通过的样本仍保留在原始数据中,并明确标记为无效。 +msd_lite 保留上游示例的 48 KiB 接收水位、64 KiB 发送水位、1 MiB 环形缓冲;仅适配监听地址、接口、线程数、日志和拥塞控制。udpxy 保留默认缓冲设置。TVGate 仅配置监听端口及 loopback 组播接口,并发、缓冲、连接上限及日志均使用程序默认值。 -测试逐项串行执行,每轮改变程序顺序,重启服务与负载进程;所有客户端开始收到数据后再预热。msd_lite 保留上游示例的 48 KiB 接收水位、64 KiB 发送水位、1 MiB 环形缓冲;仅适配监听地址、接口、线程数、日志和拥塞控制。udpxy 保留默认缓冲设置,TVGate 配置 loopback 上游接口和 256 连接上限。 +## 测试场景 | 场景 | 客户端数 | 组播源数 | 单源负载码率 | 重复次数 | 每轮预热 / 采样 | | --- | ---: | ---: | ---: | ---: | --- | -| 同频道 64 客户端 | 64 | 1 | 20 Mbps | 5 | 5 s / 20 s | | 多频道 | 8 | 8 | 40 Mbps | 3 | 5 s / 10 s | | 同频道 8 客户端 | 8 | 1 | 40 Mbps | 3 | 5 s / 10 s | +| 同频道 64 客户端 | 64 | 1 | 20 Mbps | 5 | 5 s / 20 s | | 高码率 | 1 | 1 | 400 Mbps | 3 | 5 s / 10 s | ## 测试结果 -### 同频道 64 客户端 - -CPU 列为有效样本平均值,括号内为逐轮最小值至最大值。内存为有效样本平均值;未通过完整性检查的样本不参与均值。 - -| 程序 | CPU 平均(范围) | PSS (MiB) | USS (MiB) | 有效 / 总轮数 | 组播 socket 数 | -| --- | --- | ---: | ---: | ---: | ---: | -| rtp2httpd | 6.97% (6.33–7.68) | 4.61 | 3.99 | 5/5 | 1 | -| msd_lite | 5.91% (5.59–6.48) | 1.37 | 1.36 | 5/5 | 1 | -| udpxy | 56.75% (54.80–58.70) | 4.61 | 4.02 | 2/5 | 64 | -| TVGate | — | — | — | 0/5 | 1 | -| rtp2httpd(优化前) | 30.10% (29.07–31.37) | 10.28 | 9.60 | 5/5 | 64 | - -优化版及优化前基线均为 5/5 有效。优化版每客户端实际负载码率为 19.985–20.023 Mbps,总输出约 1.28 Gbps;五轮均无序号缺口、回退、重复、内容错误、断流或内核 UDP 丢包。 - -udpxy 的 3 轮样本出现内核 UDP 丢包,表中只统计其余 2 轮。TVGate 的 5 轮均未通过序号连续性检查,观察到的 CPU 为 46.93–53.37%,不作为有效转发结果参与比较。 +以下 CPU 和内存统计包含每个场景全部完成采样的轮次,衡量指定负载下的资源占用,不用于认证转发正确性或最大可持续吞吐量。 -### 其余场景 +### CPU 使用率 -单元格格式为「有效样本平均 CPU;有效 / 总轮数」。 +数值为逐轮平均 CPU 的均值,括号内为最小值至最大值。 | 场景 | rtp2httpd | msd_lite | udpxy | TVGate | -| --- | --- | --- | --- | --- | -| 8 频道,各 40 Mbps | 9.78%; 3/3 | 9.49%; 3/3 | 20.81%; 3/3 | —; 0/3 | -| 8 客户端同频道,40 Mbps | 7.21%; 3/3 | 6.07%; 3/3 | 26.93%; 3/3 | —; 0/3 | -| 单客户端,400 Mbps | 14.79%; 2/3 | 13.76%; 1/3 | —; 0/3 | —; 0/3 | +| --- | ---: | ---: | ---: | ---: | +| 8 频道,各 40 Mbps | 9.78% (9.08–10.29) | 9.49% (9.19–9.78) | 20.81% (20.27–21.28) | 63.32% (62.56–63.96) | +| 8 客户端同频道,40 Mbps | 7.21% (6.97–7.47) | 6.07% (5.78–6.27) | 26.93% (26.21–27.78) | 43.20% (40.96–45.27) | +| 64 客户端同频道,20 Mbps | 6.97% (6.33–7.68) | 5.91% (5.59–6.48) | 58.17% (54.80–60.73) | 149.79% (136.85–156.83) | +| 单客户端,400 Mbps | 15.24% (13.84–16.14) | 14.78% (13.76–16.43) | 23.31% (22.84–23.55) | 47.99% (47.33–48.41) | -以下内存值为「PSS / USS」,单位 MiB,统计相同的有效样本。 +### PSS 内存占用(MiB) | 场景 | rtp2httpd | msd_lite | udpxy | TVGate | -| --- | --- | --- | --- | --- | -| 8 频道,各 40 Mbps | 2.28 / 1.66 | 8.95 / 8.94 | 0.80 / 0.53 | — / — | -| 8 客户端同频道,40 Mbps | 1.60 / 0.98 | 1.35 / 1.34 | 0.79 / 0.52 | — / — | -| 单客户端,400 Mbps | 1.43 / 0.84 | 1.34 / 1.33 | — / — | — / — | +| --- | ---: | ---: | ---: | ---: | +| 8 频道,各 40 Mbps | 2.28 | 8.95 | 0.80 | 25.72 | +| 8 客户端同频道,40 Mbps | 1.60 | 1.35 | 0.79 | 22.68 | +| 64 客户端同频道,20 Mbps | 4.61 | 1.37 | 4.51 | 45.65 | +| 单客户端,400 Mbps | 1.42 | 1.34 | 0.32 | 19.12 | -有效轮数不足时,不能把少量通过样本的均值理解为全部轮次均能稳定转发。附加场景中未通过的非 TVGate 样本如下;所有失败记录均随报告保留。 +### USS 内存占用(MiB) -| 场景 | 程序 | 轮次 | 内核 UDP 丢包 | TS 序号缺口 | -| --- | --- | ---: | ---: | ---: | -| 400 Mbps | rtp2httpd | 1 | 37 | 259 | -| 400 Mbps | udpxy | 1 | 29 | 203 | -| 400 Mbps | udpxy | 2 | 133 | 931 | -| 400 Mbps | msd_lite | 2 | 289 | 2023 | -| 400 Mbps | udpxy | 3 | 97 | 679 | -| 400 Mbps | msd_lite | 3 | 273 | 1911 | - -### 输出完整性说明 - -TVGate 使用其[官方文档](https://github.com/qist/tvgate/blob/main/doc/MULTICAST.md)推荐的 `/udp/` 入口接收同一份 RTP 输入。其输出的 TS 内容校验可以通过,但递增序号出现回退和跳跃;平均码率正常并不代表内容连续。独立单客户端抓取也观察到了类似 `0…6 → 0…6 → 14…20` 的序列。报告仅说明该版本在本次合成转发负载下未通过连续性检查,不推断其他视频源或其他版本的表现。 - -为保留完整测量记录,以下列出 TVGate 所有轮次的 CPU 观察值。这些样本均未通过连续性检查,不能视为有效转发的性能成绩。 - -| 场景 | CPU 观察均值(范围) | -| --- | --- | -| 64 客户端同频道,20 Mbps | 49.81% (46.93–53.37) | -| 8 频道,各 40 Mbps | 30.91% (30.27–31.38) | -| 8 客户端同频道,40 Mbps | 22.33% (21.83–22.74) | -| 单客户端,400 Mbps | 27.92% (25.64–30.10) | - -同频道 64 客户端场景中,rtp2httpd 与 msd_lite 均通过全部测量,msd_lite 的 CPU 使用率更低。400 Mbps 场景中各项目均存在无效样本,无法据此给出稳定转发的性能排名。CPU 成本、缓冲策略和完整性结果需要一起比较。 +| 场景 | rtp2httpd | msd_lite | udpxy | TVGate | +| --- | ---: | ---: | ---: | ---: | +| 8 频道,各 40 Mbps | 1.66 | 8.94 | 0.53 | 25.72 | +| 8 客户端同频道,40 Mbps | 0.98 | 1.34 | 0.52 | 22.68 | +| 64 客户端同频道,20 Mbps | 3.99 | 1.36 | 3.92 | 45.65 | +| 单客户端,400 Mbps | 0.82 | 1.33 | 0.11 | 19.12 | ## rtp2httpd 的性能优化 @@ -107,21 +79,21 @@ TVGate 使用其[官方文档](https://github.com/qist/tvgate/blob/main/doc/MULT 订阅源拥有独立的生命周期、超时和重加组定时状态。每个客户端持有订阅引用,最后一个引用释放时关闭 socket 并销毁源状态。最早接入的客户端离开时,事件分发会转交给仍存活的订阅者。各 worker 仍独立管理自己的源表。 -这主要减少本机重复 socket 接收、系统调用和应用层处理。多个本机 socket 加入同一个组播组,并不等于上游链路一定传输了相同数量的完整数据流;本次测试不以网络侧带宽节省作为结论。 +这主要减少本机重复 socket 接收、系统调用和应用层处理。多个本机 socket 加入同一个组播组,并不等于上游链路一定传输了相同数量的完整数据流。 ### 共享解析、重排和批次数据 -普通组播由共享源统一进行 RTP 解析及重排,然后将有效负载合并到容量为 64 KiB 的批次中。每个 RTP 负载保持完整,在下一个负载会超出容量时先发送当前批次;本次 1316 字节负载对应每批 49 个包、64484 字节。这样既减少每客户端重复解析,也减少逐包分发和发送调用。未满的批次在满 100 ms 后的下一次 worker 定时检查中发送;检查周期为 100 ms,实际延迟还受调度影响。 +普通组播由共享源统一进行 RTP 解析及重排,然后将有效负载合并到容量为 64 KiB 的批次中。每个 RTP 负载保持完整,在下一个负载会超出容量时先发送当前批次;1316 字节负载对应每批 49 个包、64484 字节。这样既减少每客户端重复解析,也减少逐包分发和发送调用。未满的批次在满 100 ms 后的下一次 worker 定时检查中发送;检查周期为 100 ms,实际延迟还受调度影响。 Buffer 层在原有 1536 字节包缓冲池和控制缓冲池之外,增加按需分配的 64 KiB 批次池。批次池归 worker 所有,已排队的数据可在组播源销毁后继续存活。默认初始分配 4 个批次,按 4 个扩展,最大容量按 `buffer-pool-max-size × 1536` 字节预算换算,并至少容纳 4 个批次。这是批次池自身的上限,与原有包池分别计量。批次池耗尽时,仍可通过小包引用继续分发。 -共享的是底层 payload,而每个客户端拥有独立的 `buffer_ref_t` 视图。视图通过 `owner` 指向同一份不可修改的数据,并保留自己的链表指针、发送偏移和剩余长度。客户端的部分发送只修改自己的视图;最后一个视图释放后,底层内存才返回池中。每个客户端仍有自己的发送队列和丢包策略,慢客户端不会暂停其他订阅者的接收。 +共享的是底层 payload,而每个客户端拥有独立的 `buffer_ref_t` 视图。视图通过 `owner` 指向同一份不可修改的数据,并保留自己的链表指针、发送偏移和剩余长度。客户端的部分发送只修改自己的视图;最后一个视图释放后,底层内存才返回池中。每个客户端仍有自己的发送队列和容量限制,慢客户端不会暂停其他订阅者的接收。 队列限额改为按底层缓冲容量计费,不能再使用「缓冲数量 × 1536」。一个只剩少量字节未发送的批次,仍占用完整 64 KiB 容量,直到该客户端释放引用。这避免共享大缓冲绕过原有慢客户端内存限制。 ### 不可修改的批次快照 -本次 Linux 测试中的完整共享批次,还会通过 `memfd_create` 建立匿名内存文件,写入一次后封存,再由多个客户端通过 `sendfile` 发送同一份内核数据页。只在多个客户端共享接近满容量的批次时采用该路径;普通内存发送仍通过 `sendmsg` 完成。 +支持内存文件封存的平台会通过 `memfd_create` 为共享批次建立匿名内存文件,写入一次后封存,再由多个客户端通过 `sendfile` 发送同一份内核数据页。只在多个客户端共享接近满容量的批次时采用该路径;普通内存发送仍通过 `sendmsg` 完成。 每批创建新的文件,发布后禁止写入、增长和截断,不会在缓冲池复用时覆盖旧文件。即使 `sendfile` 已返回、应用已关闭最后一个文件引用,TCP 仍可能持有旧数据页;文件不可变保证这些待发送内容不会被新一批数据改写。创建、写入或封存失败时保留内存发送;客户端的 `sendfile` 不受支持时,只将该客户端的视图回退到内存发送。 @@ -133,20 +105,16 @@ FCC 单播和切换状态按客户端独立维护。衔接时先共享组播 soc ## 适用范围 -这是 ARM64 Linux 虚拟机内的固定码率转发测试,不是物理网卡吞吐上限、视频解码性能或最大可承载客户端数量测试。loopback 内核工作中记在发送器及读流进程上的部分不属于服务 CPU,主机调度也会引入波动。其他硬件、码率、客户端速度、并发频道数量及网络路径应单独测量。 +这是 ARM64 Linux 虚拟机内的固定码率转发资源测量,不是物理网卡吞吐上限、视频解码性能或最大可承载客户端数量测试。loopback 内核工作中记在发送器及读流进程上的部分不属于服务 CPU,主机调度也会引入波动。其他硬件、码率、客户端速度、并发频道数量及网络路径应单独测量。 ## 复现测试 -脚本及参数见 [tools/stress-test/README.md](https://github.com/stackia/rtp2httpd/blob/main/tools/stress-test/README.md)。先准备对应版本的二进制文件,再运行: +脚本及参数见 [tools/stress-test/README.md](https://github.com/stackia/rtp2httpd/blob/main/tools/stress-test/README.md)。准备对应版本的二进制文件后,运行四种场景: ```bash -# 四个项目,同频道 64 客户端,5 轮。 -scripts/benchmark.sh rtp2httpd msd_lite udpxy tvgate \ - --cases shared64 --repetitions 5 --warmup 5 --duration 20 - -# 其余三个场景,3 轮。 scripts/benchmark.sh rtp2httpd msd_lite udpxy tvgate \ - --cases distinct8 shared8 high400 --repetitions 3 --warmup 5 --duration 10 + --cases distinct8 shared8 shared64 high400 \ + --repetitions 5 --warmup 5 --duration 20 ``` -通过 `--binary 名称=路径` 与 `--revision 名称=版本` 指定实际使用的二进制及版本。优化前后对照额外传入 `baseline` 程序名和 `--binary baseline=路径`。输出目录默认位于 `build/benchmark/`,包含环境、二进制哈希、每轮数据、汇总、日志及生成的配置;存在无效样本时命令以非零状态退出。 +通过 `--binary 名称=路径` 与 `--revision 名称=版本` 指定实际使用的二进制及版本,按上表设置各场景的重复次数和采样时长。CPU、PSS、USS 汇总位于输出目录中的 `resources.json`。输出目录默认位于 `build/benchmark/`,测试记录仅在本地保留。 diff --git a/docs/reference/configuration.md b/docs/reference/configuration.md index dbd2ac69..75d0f70e 100644 --- a/docs/reference/configuration.md +++ b/docs/reference/configuration.md @@ -20,8 +20,6 @@ rtp2httpd [选项] - `-m, --maxclients <数量>` - 最大并发客户端数 (默认: 5) - `-w, --workers <数量>` - 工作进程数 (默认: 1) -同一工作进程内,同源组播请求会自动共享订阅,无需额外配置。实现细节与测试结果见[性能测试报告](/reference/benchmark#rtp2httpd-的性能优化)。 - `--listen` 可以重复指定,用于同时监听多个 TCP 地址/端口或 Unix socket: ```bash diff --git a/tools/stress-test/README.md b/tools/stress-test/README.md index a7aaac89..0613abc9 100644 --- a/tools/stress-test/README.md +++ b/tools/stress-test/README.md @@ -6,7 +6,7 @@ - Linux with `/proc`, `taskset`, and `sysctl`; Python 3.14+ managed by `uv`. - Build the required server binaries first. Missing binaries fail the run rather than silently skipping a competitor. -- The default full matrix uses 14 available logical CPUs: server 0, load generators/readers 1–12, controller 13. The 64-client case alone needs seven CPUs; examples below show how to select them. +- The default full matrix needs at least 14 available logical CPUs: server 0, load generators/readers 1–12, controller 13. TVGate retains the caller's available CPU set and uses default multicore scheduling. The 64-client case alone needs seven CPUs; examples below show how to select them. - Multicast and HTTP use loopback. No root permissions, recorded video, or network sysctl changes are required. ## Repeated benchmark @@ -28,11 +28,6 @@ scripts/benchmark.sh scripts/benchmark.sh rtp2httpd msd_lite \ --cases shared64 --load-cpus 1,2,3,4,5 --controller-cpu 6 -# Include the pre-optimization baseline as a separate binary. -scripts/benchmark.sh baseline rtp2httpd msd_lite udpxy tvgate \ - --cases shared64 --repetitions 5 --duration 20 --warmup 5 \ - --binary baseline=/absolute/path/to/baseline/rtp2httpd \ - --revision baseline=BASELINE_COMMIT --revision rtp2httpd=FEATURE_COMMIT ``` Default paths can all be overridden with `--binary NAME=/absolute/path`: @@ -54,27 +49,29 @@ Use a clean checkout or separate build directory when refreshing competitors; pr | `shared8` | 8 | 1 | 40 Mbps | | `high400` | 1 | 1 | 400 Mbps | -`--cases`, `--repetitions`, `--duration`, `--warmup`, `--server-cpu`, `--load-cpus`, `--controller-cpu`, and `--output` customize the run. Program order rotates and reverses across repetitions. Each trial starts fresh server and load processes; tests run sequentially. +`--cases`, `--repetitions`, `--duration`, `--warmup`, `--server-cpu`, `--load-cpus`, `--controller-cpu`, and `--output` customize the run. `--tvgate-single-cpu` optionally restricts TVGate to `--server-cpu`; leave it unset to test default scheduling. Program order rotates and reverses across repetitions. Each trial starts fresh server and load processes; tests run sequentially. ## Measurement and validity - CPU is the change in user + system CPU ticks from `/proc/PID/stat`, divided by measured wall time. **100% means one logical CPU**, not the whole machine. Include the supervisor and every child process; process CPU already includes its threads and must not be summed again by thread. -- All server processes/threads inherit the same single-CPU affinity. rtp2httpd uses `-C -w 1`; msd_lite uses one event-loop thread; TVGate uses `GOMAXPROCS=1`; udpxy retains its native process-per-client model. These are single-CPU comparisons, not claims that all programs have one process or thread. -- Each generator and reader process has a separate CPU from the server. Their combined CPU is recorded separately. Loopback kernel work charged to the load processes is outside the server metric; this is not total system CPU or a physical-NIC throughput test. +- rtp2httpd uses `-C -w 1`; msd_lite uses one event-loop thread; udpxy retains its native process-per-client model. Their complete process families are pinned to `--server-cpu`. TVGate's `GOMAXPROCS` environment override is removed, and its original available CPU set is restored after the controller pins itself. Its scheduler, buffers, connection limits, and logging use application defaults; only the listener and multicast interfaces are configured. This compares resource consumption under the specified load, with different scheduling policies. +- Each generator and reader process is pinned to a separate CPU from the single-CPU services. TVGate can run on these CPUs too. Load CPU is recorded separately. Loopback kernel work charged to the load processes is outside the server metric; this is not total system CPU or a physical-NIC throughput test. - PSS and USS come from `smaps_rollup`, summed over the process family and sampled once per second. PSS includes proportional shared pages; USS includes private clean/dirty/huge pages. Neither includes all kernel socket memory or unmapped anonymous-file cache pages. - The sender emits RTP payload type 33 with seven 188-byte TS null packets per datagram. Every TS packet contains a monotonically increasing marker, its complement, a source identifier, and a checked payload pattern. This tests forwarding and integrity, not video decoding. - Readers decode HTTP chunk framing before checking payloads. Each client must receive within 2% of the target rate; each generator must also maintain that rate. The measured window must contain no gaps, duplicates, backward markers, corrupt packets, EOFs, or kernel UDP drops. The process family must remain stable and the load processes alive. - Failures remain in the raw output as `valid: false`; the summary averages valid trials only and always reports valid/total counts. A failed or incomplete run exits nonzero. Do not describe its low CPU as a performance win. -- msd_lite keeps the upstream example's 48 KiB receive watermark, 64 KiB send watermark, and 1 MiB ring. Only the listener, interface, thread count/affinity, verbosity, and congestion-control name are adapted. udpxy keeps upstream buffer defaults. TVGate uses loopback multicast settings and connection limits of 256. Generated configs and complete commands are saved for review. +- `resources.json` separately averages CPU and memory across every completed measurement, without filtering by payload diagnostics. The performance report uses these resource observations and does not certify forwarding correctness or sustainable maximum capacity. +- msd_lite keeps the upstream example's 48 KiB receive watermark, 64 KiB send watermark, and 1 MiB ring. Only the listener, interface, thread count/affinity, verbosity, and congestion-control name are adapted. udpxy keeps upstream buffer defaults. Generated configs and complete commands are saved locally for review. The output directory (default `build/benchmark/YYYYMMDD-HHMMSS/`) contains: - `metadata.json`: environment, CPU placement, sysctls, revisions, executable/script SHA-256 values. - `trials.jsonl`: every trial, including failures, per-client rates, integrity counters, CPU, memory, socket/process/thread counts. - `summary.json`: valid/total counts and mean/min/max CPU, PSS, USS. +- `resources.json`: completed measurement counts and mean/min/max CPU, PSS, USS, independent of payload diagnostics. - Per-trial logs and generated msd_lite/TVGate configurations. -Run the HTTP framing checks with: +Results stay local in ignored directories and are not committed. Benchmarks and their helper tests are run manually, outside CI. Run the HTTP framing and TVGate launch-setting checks with: ```bash uv run pytest tools/stress-test/test_benchmark.py -q diff --git a/tools/stress-test/benchmark.py b/tools/stress-test/benchmark.py index 51464806..cd9a4e92 100644 --- a/tools/stress-test/benchmark.py +++ b/tools/stress-test/benchmark.py @@ -1,7 +1,7 @@ """Validated Linux multicast benchmark. Run through scripts/benchmark.sh. No video fixture or extra packages: RTP carries seven numbered MPEG-TS null packets. -The workload processes and the server process family use disjoint CPU affinities. +Load processes use fixed CPUs. TVGate retains the caller's default CPU affinity. """ import argparse @@ -254,7 +254,7 @@ def consumers(port, sources, ids, cpu, stop, counters, errors, path_prefix): errors.put(f"consumer: {exc!r}") -def command_for(program, binary, port, stem, server_cpu): +def command_for(program, binary, port, stem, server_cpus): env = os.environ.copy() if program in ("rtp2httpd", "baseline"): command = [str(binary), "-C", "-w", "1", "-m", "256", "-r", "lo", "-v", "1", "-l", f"127.0.0.1:{port}"] @@ -287,14 +287,11 @@ def command_for(program, binary, port, stem, server_cpu): command = [str(binary), "-T", "-a", "127.0.0.1", "-m", "lo", "-p", str(port), "-c", "256"] else: config = stem.with_suffix(".yaml") - config.write_text( - f"server:\n port: {port}\nlog:\n enabled: false\n" - "http:\n max_idle_conns: 256\n max_idle_conns_per_host: 256\n max_conns_per_host: 256\n" - "multicast:\n multicast_ifaces: [lo]\n upstream_interface: lo\n" - ) - env["GOMAXPROCS"] = "1" + config.write_text(f"server:\n port: {port}\nmulticast:\n multicast_ifaces: [lo]\n upstream_interface: lo\n") + env.pop("GOMAXPROCS", None) command = [str(binary), "-config", str(config)] - return ["taskset", "-c", str(server_cpu), *command], env + # Restore the caller's affinity for TVGate: the controller pins itself before spawning. + return ["taskset", "-c", ",".join(map(str, server_cpus)), *command], env def trial(program, case, repetition, order, args, binaries): @@ -307,7 +304,8 @@ def trial(program, case, repetition, order, args, binaries): sources = [streams[i % count] for i in range(clients)] port = free_port() stem = args.output / f"{case}-{repetition:02d}-{program}" - command, env = command_for(program, binaries[program], port, stem, args.server_cpu) + server_cpus = args.available_cpus if program == "tvgate" and not args.tvgate_single_cpu else [args.server_cpu] + command, env = command_for(program, binaries[program], port, stem, server_cpus) path_prefix = "udp" if program == "tvgate" else "rtp" result = { "program": program, @@ -319,6 +317,7 @@ def trial(program, case, repetition, order, args, binaries): "sources": count, "target_mbps": mbps, "valid": False, + "server_cpus": server_cpus, "request_prefix": path_prefix, } children = [] @@ -369,7 +368,7 @@ def trial(program, case, repetition, order, args, binaries): time.sleep(args.warmup) pids = process_family(daemon.pid) tids = [int(t.name) for pid in pids for t in Path(f"/proc/{pid}/task").iterdir()] - if any(os.sched_getaffinity(tid) != {args.server_cpu} for tid in tids): + if any(os.sched_getaffinity(tid) != set(server_cpus) for tid in tids): raise RuntimeError("server affinity changed") before_udp = udp_stats(pids) before_clients, before_sent = list(counters), list(sent) @@ -460,6 +459,7 @@ def main(): parser.add_argument("--duration", type=int, default=20) parser.add_argument("--warmup", type=float, default=5) parser.add_argument("--server-cpu", type=int, default=0) + parser.add_argument("--tvgate-single-cpu", action="store_true", help="also restrict TVGate to --server-cpu") parser.add_argument("--controller-cpu", type=int, default=13) parser.add_argument("--load-cpus", default="1,2,3,4,5,6,7,8,9,10,11,12") parser.add_argument("--output", type=Path, default=ROOT / "build/benchmark" / time.strftime("%Y%m%d-%H%M%S")) @@ -476,6 +476,7 @@ def main(): parser.error("requested CPUs are outside the current process affinity") if args.controller_cpu in {args.server_cpu, *args.load_cpus} or args.controller_cpu not in os.sched_getaffinity(0): parser.error("choose a separate available --controller-cpu") + args.available_cpus = sorted(os.sched_getaffinity(0)) os.sched_setaffinity(0, {args.controller_cpu}) programs = args.programs or ["rtp2httpd", "msd_lite", "udpxy", "tvgate"] binaries = { @@ -499,6 +500,8 @@ def main(): "machine": platform.machine(), "cpu_count": os.cpu_count(), "server_cpu": args.server_cpu, + "tvgate_cpus": [args.server_cpu] if args.tvgate_single_cpu else args.available_cpus, + "tvgate_gomaxprocs": "unset", "load_cpus": args.load_cpus, "controller_cpu": args.controller_cpu, "warmup_s": args.warmup, @@ -547,6 +550,21 @@ def main(): ) summary.append(item) (args.output / "summary.json").write_text(json.dumps(summary, indent=2) + "\n") + # Resource observations include every completed measurement, independently of integrity. + resources = [] + for case in args.cases: + for program in programs: + completed = [ + r for r in rows if r["case"] == case and r["program"] == program and "cpu_pct" in r and "error" not in r + ] + item = {"case": case, "program": program, "measured_trials": len(completed)} + for field in ("cpu_pct", "pss_mib", "uss_mib"): + values = [r[field] for r in completed] + item[field] = ( + {"mean": statistics.mean(values), "min": min(values), "max": max(values)} if values else None + ) + resources.append(item) + (args.output / "resources.json").write_text(json.dumps(resources, indent=2) + "\n") return 0 if all(row["valid"] for row in rows) else 1 diff --git a/tools/stress-test/test_benchmark.py b/tools/stress-test/test_benchmark.py index 8e7e646e..cfc24e2c 100644 --- a/tools/stress-test/test_benchmark.py +++ b/tools/stress-test/test_benchmark.py @@ -1,7 +1,7 @@ """Check HTTP framing used by the benchmark's payload validator.""" import pytest -from benchmark import HTTPBody +from benchmark import HTTPBody, command_for @pytest.mark.parametrize("chunked", [False, True]) @@ -31,3 +31,13 @@ def test_rejects_error_response(): def test_rejects_invalid_chunk_terminator(): with pytest.raises(ValueError, match="chunk terminator"): HTTPBody().feed(b"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n1\r\nx!!") + + +def test_tvgate_uses_default_runtime_settings(tmp_path, monkeypatch): + monkeypatch.setenv("GOMAXPROCS", "1") + command, env = command_for("tvgate", tmp_path / "TVGate", 12345, tmp_path / "trial", [0, 1, 2, 3]) + assert command[:3] == ["taskset", "-c", "0,1,2,3"] + assert "GOMAXPROCS" not in env + assert (tmp_path / "trial.yaml").read_text() == ( + "server:\n port: 12345\nmulticast:\n multicast_ifaces: [lo]\n upstream_interface: lo\n" + ) From becbaa7a022f7db3d9e163f326680cffd7d2bc20 Mon Sep 17 00:00:00 2001 From: Stackie Jia Date: Sun, 6 Sep 2026 19:45:21 +0800 Subject: [PATCH 10/19] docs(perf): label optimization strategies as an appendix --- docs/en/reference/benchmark.md | 2 +- docs/reference/benchmark.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/en/reference/benchmark.md b/docs/en/reference/benchmark.md index 3dc0829e..198e4244 100644 --- a/docs/en/reference/benchmark.md +++ b/docs/en/reference/benchmark.md @@ -71,7 +71,7 @@ Values are the means of per-trial average CPU utilization, with the minimum and | 64 clients, one 20 Mbps channel | 3.99 | 1.36 | 3.92 | 45.65 | | 1 client, 400 Mbps | 0.82 | 1.33 | 0.11 | 19.12 | -## Performance Optimizations in rtp2httpd +## Appendix: Performance Optimization Strategies in rtp2httpd ### Shared Multicast Subscriptions Within Each Worker diff --git a/docs/reference/benchmark.md b/docs/reference/benchmark.md index e88bdd78..80910f38 100644 --- a/docs/reference/benchmark.md +++ b/docs/reference/benchmark.md @@ -71,7 +71,7 @@ msd_lite 保留上游示例的 48 KiB 接收水位、64 KiB 发送水位、1 MiB | 64 客户端同频道,20 Mbps | 3.99 | 1.36 | 3.92 | 45.65 | | 单客户端,400 Mbps | 0.82 | 1.33 | 0.11 | 19.12 | -## rtp2httpd 的性能优化 +## 附:rtp2httpd 的性能优化策略 ### 每个 worker 共享组播订阅 From e7a9a1c6059de0cbbd2436af2d659791d842d735 Mon Sep 17 00:00:00 2001 From: Stackie Jia Date: Sun, 6 Sep 2026 20:00:05 +0800 Subject: [PATCH 11/19] perf(connection): allocate protocol and request state on demand --- src/access_log.c | 12 +++--- src/connection.c | 101 +++++++++++++++++++++++++++++----------------- src/connection.h | 10 +++-- src/http.c | 6 +-- src/rtsp.c | 2 +- src/status.c | 18 ++++----- src/stream.c | 103 +++++++++++++++++++++++++++-------------------- src/stream.h | 4 +- src/worker.c | 7 ++-- 9 files changed, 156 insertions(+), 107 deletions(-) diff --git a/src/access_log.c b/src/access_log.c index e159b9db..a09c6c9d 100644 --- a/src/access_log.c +++ b/src/access_log.c @@ -349,7 +349,7 @@ static int access_log_append_placeholder(access_log_buffer_t *buf, const char *n const char *time_iso8601, const char *time_local, const char *msec, const char *remote_addr, const char *remote_port, const char *request) { char numeric[64]; - char filtered_user_agent[sizeof(c->http_req.user_agent)]; + char filtered_user_agent[sizeof(c->http_req->user_agent)]; #define MATCH(name_literal) (name_len == strlen(name_literal) && strncmp(name, name_literal, name_len) == 0) @@ -372,18 +372,18 @@ static int access_log_append_placeholder(access_log_buffer_t *buf, const char *n if (MATCH("request")) return access_log_append_escaped(buf, request); if (MATCH("request_method")) - return access_log_append_escaped(buf, c->http_req.method); + return access_log_append_escaped(buf, c->http_req->method); if (MATCH("service_url")) return access_log_append_escaped(buf, client->service_url); if (MATCH("host")) - return access_log_append_escaped(buf, c->http_req.hostname); + return access_log_append_escaped(buf, c->http_req->hostname); if (MATCH("http_user_agent")) { - if (http_filter_user_agent_token(c->http_req.user_agent, filtered_user_agent, sizeof(filtered_user_agent)) < 0) + if (http_filter_user_agent_token(c->http_req->user_agent, filtered_user_agent, sizeof(filtered_user_agent)) < 0) filtered_user_agent[0] = '\0'; return access_log_append_escaped(buf, filtered_user_agent); } if (MATCH("http_x_forwarded_for")) - return access_log_append_escaped(buf, c->http_req.x_forwarded_for); + return access_log_append_escaped(buf, c->http_req->x_forwarded_for); if (MATCH("service_type")) return access_log_append_escaped(buf, access_log_service_type_name(service)); if (MATCH("upstream_url")) @@ -409,7 +409,7 @@ static int access_log_render(access_log_buffer_t *buf, connection_t *c, service_ access_log_format_times(now_ms, time_iso8601, sizeof(time_iso8601), time_local, sizeof(time_local), msec, sizeof(msec)); access_log_parse_remote_addr(client->client_addr, remote_addr, sizeof(remote_addr), remote_port, sizeof(remote_port)); - snprintf(request, sizeof(request), "%s %s", c->http_req.method[0] ? c->http_req.method : "-", client->service_url); + snprintf(request, sizeof(request), "%s %s", c->http_req->method[0] ? c->http_req->method : "-", client->service_url); for (const char *p = format; *p; p++) { if (*p != '$') { diff --git a/src/connection.c b/src/connection.c index 615e642e..98cbba95 100644 --- a/src/connection.c +++ b/src/connection.c @@ -230,8 +230,8 @@ static token_source_t validate_r2h_token(connection_t *c, const char *query_star } /* Source 2: Cookie header */ - if (c->http_req.cookie[0] != '\0') { - if (parse_cookie_value(c->http_req.cookie, "r2h-token", token_value, sizeof(token_value)) == 0) { + if (c->http_req->cookie[0] != '\0') { + if (parse_cookie_value(c->http_req->cookie, "r2h-token", token_value, sizeof(token_value)) == 0) { if (http_url_decode(token_value) != 0) { logger(LOG_WARN, "r2h-token invalid URL encoding (source: cookie)"); return TOKEN_SOURCE_NONE; @@ -246,8 +246,8 @@ static token_source_t validate_r2h_token(connection_t *c, const char *query_star } /* Source 3: User-Agent with R2HTOKEN/xxx format */ - if (c->http_req.user_agent[0] != '\0') { - if (extract_r2h_token_from_ua(c->http_req.user_agent, token_value, sizeof(token_value)) == 0) { + if (c->http_req->user_agent[0] != '\0') { + if (extract_r2h_token_from_ua(c->http_req->user_agent, token_value, sizeof(token_value)) == 0) { if (strcmp(token_value, config.r2h_token) == 0) { logger(LOG_DEBUG, "r2h-token validated (source: user-agent)"); return TOKEN_SOURCE_UA; @@ -461,8 +461,9 @@ int connection_can_resume_upstream(connection_t *c) { void connection_recompute_any_upstream_paused(connection_t *c) { if (!c) return; - c->any_upstream_paused = (c->stream.http_proxy.initialized && c->stream.http_proxy.upstream_paused) || - (c->stream.rtsp.initialized && c->stream.rtsp.upstream_paused); + c->any_upstream_paused = + ((c->stream.http_proxy && c->stream.http_proxy->initialized) && c->stream.http_proxy->upstream_paused) || + ((c->stream.rtsp && c->stream.rtsp->initialized) && c->stream.rtsp->upstream_paused); } void connection_begin_drain_close(connection_t *c) { @@ -545,11 +546,25 @@ connection_t *connection_create(int fd, int epfd, struct sockaddr_storage *clien CONNECTION_TCP_KEEPALIVE_CNT); } - /* Initialize HTTP request parser */ - http_request_init(&c->http_req); + /* Request storage is only needed while parsing and starting the response. */ + c->http_req = malloc(sizeof(*c->http_req)); + if (!c->http_req) { + free(c); + return NULL; + } + http_request_init(c->http_req); return c; } +void connection_release_request(connection_t *c) { + if (!c || !c->http_req) + return; + c->request_is_head = strcasecmp(c->http_req->method, "HEAD") == 0; + http_request_cleanup(c->http_req); + free(c->http_req); + c->http_req = NULL; +} + void connection_cleanup(connection_t *c) { if (!c) return; @@ -594,8 +609,8 @@ void connection_cleanup(connection_t *c) { c->fd = -1; } - /* Cleanup HTTP request (free dynamically allocated body) */ - http_request_cleanup(&c->http_req); + connection_release_request(c); + free(c->inbuf); free(c); } @@ -743,6 +758,13 @@ void connection_handle_read(connection_t *c) { * with bodies larger than INBUF_SIZE. */ for (;;) { if (c->in_len < INBUF_SIZE) { + if (!c->inbuf) { + c->inbuf = malloc(INBUF_SIZE); + if (!c->inbuf) { + c->state = CONN_CLOSING; + return; + } + } int r = read(c->fd, c->inbuf + c->in_len, INBUF_SIZE - c->in_len); if (r > 0) { c->in_len += r; @@ -759,11 +781,15 @@ void connection_handle_read(connection_t *c) { /* Parse HTTP request using http.c parser */ if (c->state == CONN_READ_REQ_LINE || c->state == CONN_READ_HEADERS) { - int parse_result = http_parse_request(c->inbuf, &c->in_len, &c->http_req); + int parse_result = http_parse_request(c->inbuf, &c->in_len, c->http_req); if (parse_result == 1) { /* Request complete, route it */ c->state = CONN_ROUTE; connection_route_and_start(c); + free(c->inbuf); + c->inbuf = NULL; + if (c->headers_sent && !c->stream.http_proxy) + connection_release_request(c); return; } else if (parse_result < 0) { /* Parse error */ @@ -781,7 +807,7 @@ int connection_route_and_start(connection_t *c) { /* Copy URL and strip $label suffix (UI display tag at URL end) */ char url_buf[HTTP_URL_BUFFER_SIZE]; char internal_url_buf[HTTP_URL_BUFFER_SIZE]; - strncpy(url_buf, c->http_req.url, sizeof(url_buf) - 1); + strncpy(url_buf, c->http_req->url, sizeof(url_buf) - 1); url_buf[sizeof(url_buf) - 1] = '\0'; http_strip_url_label(url_buf); const char *url = url_buf; @@ -807,7 +833,7 @@ int connection_route_and_start(connection_t *c) { } } - logger(LOG_INFO, "New client %s requested URL: %s (method: %s)", client_addr_str, url, c->http_req.method); + logger(LOG_INFO, "New client %s requested URL: %s (method: %s)", client_addr_str, url, c->http_req->method); if (url[0] != '/') { http_send_400(c); @@ -827,14 +853,14 @@ int connection_route_and_start(connection_t *c) { } /* If Host header is missing, reject the request */ - if (c->http_req.hostname[0] == '\0') { + if (c->http_req->hostname[0] == '\0') { logger(LOG_WARN, "Client request rejected: missing Host header (expected: %s)", expected_host); http_send_400(c); return 0; } /* Match Host header against expected hostname */ - int match_result = http_match_host_header(c->http_req.hostname, expected_host); + int match_result = http_match_host_header(c->http_req->hostname, expected_host); if (match_result < 0) { logger(LOG_ERROR, "Failed to match Host header"); @@ -846,18 +872,18 @@ int connection_route_and_start(connection_t *c) { logger(LOG_WARN, "Client request rejected: Host header mismatch (got: %s, " "expected: %s)", - c->http_req.hostname, expected_host); + c->http_req->hostname, expected_host); http_send_400(c); return 0; } - logger(LOG_DEBUG, "Host header validated: %s", c->http_req.hostname); + logger(LOG_DEBUG, "Host header validated: %s", c->http_req->hostname); } /* Override client address with X-Forwarded-For if present and enabled */ - if ((protocol[0] != '\0' || config.xff) && c->http_req.x_forwarded_for[0] != '\0') { - logger(LOG_INFO, "X-Forwarded-For accepted: %s", c->http_req.x_forwarded_for); - snprintf(client_addr_str, sizeof(client_addr_str), "%s", c->http_req.x_forwarded_for); + if ((protocol[0] != '\0' || config.xff) && c->http_req->x_forwarded_for[0] != '\0') { + logger(LOG_INFO, "X-Forwarded-For accepted: %s", c->http_req->x_forwarded_for); + snprintf(client_addr_str, sizeof(client_addr_str), "%s", c->http_req->x_forwarded_for); } /* Reject reconnects from an IP that was just force-disconnected */ @@ -877,16 +903,16 @@ int connection_route_and_start(connection_t *c) { url = internal_url_buf; /* Handle CORS preflight (OPTIONS) before r2h-token check */ - if (config.cors_allow_origin && config.cors_allow_origin[0] && strcasecmp(c->http_req.method, "OPTIONS") == 0) { + if (config.cors_allow_origin && config.cors_allow_origin[0] && strcasecmp(c->http_req->method, "OPTIONS") == 0) { char cors_headers[1024]; int clen = 0; clen += snprintf(cors_headers + clen, sizeof(cors_headers) - clen, "Access-Control-Allow-Methods: %s\r\n", - c->http_req.access_control_request_method[0] ? c->http_req.access_control_request_method - : "GET, HEAD, OPTIONS"); - if (c->http_req.access_control_request_headers[0]) { + c->http_req->access_control_request_method[0] ? c->http_req->access_control_request_method + : "GET, HEAD, OPTIONS"); + if (c->http_req->access_control_request_headers[0]) { clen += snprintf(cors_headers + clen, sizeof(cors_headers) - clen, "Access-Control-Allow-Headers: %s\r\n", - c->http_req.access_control_request_headers); + c->http_req->access_control_request_headers); } clen += snprintf(cors_headers + clen, sizeof(cors_headers) - clen, "Access-Control-Max-Age: 86400\r\n" @@ -919,7 +945,7 @@ int connection_route_and_start(connection_t *c) { /* Check r2h-token if configured (supports URL query, Cookie, User-Agent) */ if (config.r2h_token != NULL && config.r2h_token[0] != '\0') { - const char *raw_query_start = strchr(c->http_req.url, '?'); + const char *raw_query_start = strchr(c->http_req->url, '?'); token_source_t source = validate_r2h_token(c, query_start, raw_query_start); if (source == TOKEN_SOURCE_NONE) { http_send_401(c); @@ -1080,14 +1106,14 @@ int connection_route_and_start(connection_t *c) { return 0; } - if (c->http_req.user_agent[0]) { - service->user_agent = strdup(c->http_req.user_agent); + if (c->http_req->user_agent[0]) { + service->user_agent = strdup(c->http_req->user_agent); } /* HTTP services forward HEAD upstream unchanged. Multicast HEAD requests * return only static metadata. RTSP HEAD performs an asynchronous * OPTIONS/DESCRIBE probe without opening media resources. */ - if (strcasecmp(c->http_req.method, "HEAD") == 0 && service->service_type != SERVICE_HTTP) { + if (strcasecmp(c->http_req->method, "HEAD") == 0 && service->service_type != SERVICE_HTTP) { if (service->service_type == SERVICE_RTSP) { logger(LOG_INFO, "RTSP HEAD request detected, starting metadata probe"); if (stream_context_init_rtsp_metadata_probe(&c->stream, c, service, c->epfd) == 0) { @@ -1117,16 +1143,16 @@ int connection_route_and_start(connection_t *c) { int is_snapshot_request = 0; if (config.video_snapshot) { - if (c->http_req.x_request_snapshot) { + if (c->http_req->x_request_snapshot) { is_snapshot_request = 2; - logger(LOG_INFO, "Snapshot request detected via X-Request-Snapshot header for URL: %s", c->http_req.url); + logger(LOG_INFO, "Snapshot request detected via X-Request-Snapshot header for URL: %s", c->http_req->url); } - if (!is_snapshot_request && c->http_req.accept[0] != '\0') { + if (!is_snapshot_request && c->http_req->accept[0] != '\0') { /* Check if Accept header contains "image/jpeg" */ - if (strstr(c->http_req.accept, "image/jpeg") != NULL) { + if (strstr(c->http_req->accept, "image/jpeg") != NULL) { is_snapshot_request = 2; - logger(LOG_INFO, "Snapshot request detected via Accept header for URL: %s", c->http_req.url); + logger(LOG_INFO, "Snapshot request detected via Accept header for URL: %s", c->http_req->url); } } @@ -1136,7 +1162,7 @@ int connection_route_and_start(connection_t *c) { if (http_parse_query_param(query_start + 1, "snapshot", snapshot_value, sizeof(snapshot_value)) == 0) { if (strcmp(snapshot_value, "1") == 0) { is_snapshot_request = 1; - logger(LOG_INFO, "Snapshot request detected via query parameter for URL: %s", c->http_req.url); + logger(LOG_INFO, "Snapshot request detected via query parameter for URL: %s", c->http_req->url); } } } @@ -1203,6 +1229,8 @@ int connection_route_and_start(connection_t *c) { c->buffer_class = CONNECTION_BUFFER_MEDIA; return 0; } else { + /* Initialization can allocate protocol state before failing. */ + stream_context_cleanup(&c->stream); /* Stream initialization failed - send 503 if headers not sent yet */ if (!c->headers_sent) { http_send_503(c); @@ -1301,7 +1329,8 @@ static void handle_playlist_request(connection_t *c) { } /* Generate complete playlist dynamically */ - playlist = m3u_generate_playlist(c->http_req.hostname, c->http_req.x_forwarded_host, c->http_req.x_forwarded_proto); + playlist = + m3u_generate_playlist(c->http_req->hostname, c->http_req->x_forwarded_host, c->http_req->x_forwarded_proto); if (!playlist) { /* No playlist available or generation failed */ diff --git a/src/connection.h b/src/connection.h index 0acd79a1..bb1f4711 100644 --- a/src/connection.h +++ b/src/connection.h @@ -27,14 +27,15 @@ typedef struct connection_s { int epfd; conn_state_t state; /* input parsing */ - char inbuf[INBUF_SIZE]; + char *inbuf; int in_len; /* Output send queue - all output goes through this */ send_queue_t send_queue; connection_buffer_class_t buffer_class; /* HTTP request parser */ - http_request_t http_req; - int headers_sent; /* Track whether HTTP response headers have been sent */ + http_request_t *http_req; + int request_is_head; /* Retained after the request parser is released */ + int headers_sent; /* Track whether HTTP response headers have been sent */ /* service/stream */ service_t *service; stream_context_t stream; @@ -102,6 +103,9 @@ connection_t *connection_create(int fd, int epfd, struct sockaddr_storage *clien */ void connection_cleanup(connection_t *c); +/* Release parsed request storage once no asynchronous handler borrows it. */ +void connection_release_request(connection_t *c); + /** * Handle read event on client connection * @param c Connection diff --git a/src/http.c b/src/http.c index f8a76f53..7be4d5c8 100644 --- a/src/http.c +++ b/src/http.c @@ -848,7 +848,7 @@ static void http_send_error(connection_t *conn, http_status_t status, const char size_t body_len) { send_http_headers(conn, status, "text/html; charset=utf-8", extra_headers); - if (conn && strcasecmp(conn->http_req.method, "HEAD") == 0) + if (conn && (conn->request_is_head || (conn->http_req && strcasecmp(conn->http_req->method, "HEAD") == 0))) body_len = 0; connection_queue_output_and_flush(conn, body_len ? (const uint8_t *)body : NULL, body_len); @@ -1106,12 +1106,12 @@ int http_check_etag_and_send_304(connection_t *c, const char *etag, const char * char extra_headers[256]; /* If no ETag provided or no If-None-Match header, cannot use caching */ - if (!c || !etag || c->http_req.if_none_match[0] == '\0') { + if (!c || !etag || !c->http_req || c->http_req->if_none_match[0] == '\0') { return 0; } /* Check if client's ETag matches server's current ETag */ - if (!etag_matches(c->http_req.if_none_match, etag)) { + if (!etag_matches(c->http_req->if_none_match, etag)) { return 0; /* No match, content should be sent */ } diff --git a/src/rtsp.c b/src/rtsp.c index f008fa4d..a9e1a995 100644 --- a/src/rtsp.c +++ b/src/rtsp.c @@ -2389,7 +2389,7 @@ static int rtsp_initiate_teardown(rtsp_session_t *session) { int rtsp_session_cleanup(rtsp_session_t *session) { /* Skip cleanup if session was never initialized */ - if (!session->initialized) { + if (!session || !session->initialized) { return 0; /* Nothing to clean up */ } diff --git a/src/status.c b/src/status.c index 49392989..c0f5e46f 100644 --- a/src/status.c +++ b/src/status.c @@ -1125,7 +1125,7 @@ void handle_disconnect_client(connection_t *c) { } /* Check HTTP method */ - if (strcasecmp(c->http_req.method, "POST") != 0 && strcasecmp(c->http_req.method, "DELETE") != 0) { + if (strcasecmp(c->http_req->method, "POST") != 0 && strcasecmp(c->http_req->method, "DELETE") != 0) { send_http_headers(c, STATUS_400, "application/json", NULL); snprintf(response, sizeof(response), "{\"success\":false,\"error\":\"Method not allowed. Use POST or " @@ -1135,8 +1135,8 @@ void handle_disconnect_client(connection_t *c) { } /* Parse form data body to get client_id */ - if (c->http_req.body_len > 0) { - if (http_parse_query_param(c->http_req.body, "client_id", client_id_str, sizeof(client_id_str)) != 0) { + if (c->http_req->body_len > 0) { + if (http_parse_query_param(c->http_req->body, "client_id", client_id_str, sizeof(client_id_str)) != 0) { send_http_headers(c, STATUS_400, "application/json", NULL); snprintf(response, sizeof(response), "{\"success\":false,\"error\":\"Missing 'client_id' parameter " @@ -1202,7 +1202,7 @@ void handle_clear_logs(connection_t *c) { char response[256]; /* Check HTTP method */ - if (strcasecmp(c->http_req.method, "POST") != 0) { + if (strcasecmp(c->http_req->method, "POST") != 0) { send_http_headers(c, STATUS_400, "application/json", NULL); snprintf(response, sizeof(response), "{\"success\":false,\"error\":\"Method not allowed. Use POST\"}"); connection_queue_output_and_flush(c, (const uint8_t *)response, strlen(response)); @@ -1244,7 +1244,7 @@ void handle_set_log_level(connection_t *c) { char level_str[32] = {0}; /* Check HTTP method */ - if (strcasecmp(c->http_req.method, "PUT") != 0 && strcasecmp(c->http_req.method, "PATCH") != 0) { + if (strcasecmp(c->http_req->method, "PUT") != 0 && strcasecmp(c->http_req->method, "PATCH") != 0) { send_http_headers(c, STATUS_400, "application/json", NULL); snprintf(response, sizeof(response), "{\"success\":false,\"error\":\"Method not allowed. Use PUT or " @@ -1254,8 +1254,8 @@ void handle_set_log_level(connection_t *c) { } /* Parse form data body to get level */ - if (c->http_req.body_len > 0) { - if (http_parse_query_param(c->http_req.body, "level", level_str, sizeof(level_str)) != 0) { + if (c->http_req->body_len > 0) { + if (http_parse_query_param(c->http_req->body, "level", level_str, sizeof(level_str)) != 0) { send_http_headers(c, STATUS_400, "application/json", NULL); snprintf(response, sizeof(response), "{\"success\":false,\"error\":\"Missing 'level' parameter in " @@ -1294,7 +1294,7 @@ void handle_reload_config(connection_t *c) { char response[256]; /* Check HTTP method */ - if (strcasecmp(c->http_req.method, "POST") != 0) { + if (strcasecmp(c->http_req->method, "POST") != 0) { send_http_headers(c, STATUS_400, "application/json", NULL); snprintf(response, sizeof(response), "{\"success\":false,\"error\":\"Method not allowed. Use POST\"}"); connection_queue_output_and_flush(c, (const uint8_t *)response, strlen(response)); @@ -1322,7 +1322,7 @@ void handle_restart_workers(connection_t *c) { char response[256]; /* Check HTTP method */ - if (strcasecmp(c->http_req.method, "POST") != 0) { + if (strcasecmp(c->http_req->method, "POST") != 0) { send_http_headers(c, STATUS_400, "application/json", NULL); snprintf(response, sizeof(response), "{\"success\":false,\"error\":\"Method not allowed. Use POST\"}"); connection_queue_output_and_flush(c, (const uint8_t *)response, strlen(response)); diff --git a/src/stream.c b/src/stream.c index ab3b5bb3..6b668557 100644 --- a/src/stream.c +++ b/src/stream.c @@ -207,10 +207,10 @@ static int stream_append_download_filename_header(connection_t *conn, char *head char sanitized[HTTP_DOWNLOAD_FILENAME_MAX + 8]; char line[768]; - if (!conn) + if (!conn || !conn->http_req) return 0; - query = strchr(conn->http_req.url, '?'); + query = strchr(conn->http_req->url, '?'); if (!query) return 0; if (http_parse_query_param(query + 1, "r2h-filename", raw, sizeof(raw)) != 0) @@ -297,6 +297,10 @@ void stream_send_http_headers(connection_t *conn, const char *content_type, cons send_http_headers(conn, STATUS_200, content_type, headers); } metadata->frozen = 1; + /* Routing may still be on the stack for synchronous HEAD responses. + * HTTP proxy sessions borrow raw headers and the request body. */ + if (conn->state == CONN_STREAMING && !conn->stream.http_proxy) + connection_release_request(conn); } void stream_on_client_drain(stream_context_t *ctx) { @@ -307,10 +311,10 @@ void stream_on_client_drain(stream_context_t *ctx) { if (!connection_can_resume_upstream(ctx->conn)) return; /* Resume functions are no-ops if not paused; no need to re-check here. */ - if (ctx->http_proxy.initialized) - http_proxy_resume_upstream(&ctx->http_proxy); - if (ctx->rtsp.initialized) - rtsp_resume_upstream(&ctx->rtsp); + if (ctx->http_proxy && ctx->http_proxy->initialized) + http_proxy_resume_upstream(ctx->http_proxy); + if (ctx->rtsp && ctx->rtsp->initialized) + rtsp_resume_upstream(ctx->rtsp); } int stream_process_rtp_payload(stream_context_t *ctx, buffer_ref_t *buf_ref, stream_media_origin_t origin) { @@ -396,12 +400,12 @@ int stream_handle_fd_event(stream_context_t *ctx, int fd, uint32_t events, int64 } /* Process RTSP socket events */ - if (ctx->rtsp.initialized && ctx->rtsp.socket >= 0 && fd == ctx->rtsp.socket) { + if ((ctx->rtsp && ctx->rtsp->initialized) && ctx->rtsp->socket >= 0 && fd == ctx->rtsp->socket) { /* Handle RTSP socket events (handshake and RTP data in PLAYING state) */ - int result = rtsp_handle_socket_event(&ctx->rtsp, events); + int result = rtsp_handle_socket_event(ctx->rtsp, events); if (result < 0) { if (result == STREAM_EVENT_DURATION_READY) { - logger(LOG_DEBUG, "RTSP: found duration: %0.3f", ctx->rtsp.r2h_duration_value); + logger(LOG_DEBUG, "RTSP: found duration: %0.3f", ctx->rtsp->r2h_duration_value); return STREAM_EVENT_DURATION_READY; } if (result == STREAM_EVENT_METADATA_READY) { @@ -413,8 +417,8 @@ int stream_handle_fd_event(stream_context_t *ctx, int fd, uint32_t events, int64 } /* Process RTSP RTP socket events (UDP mode) */ - if (ctx->rtsp.initialized && ctx->rtsp.rtp_socket >= 0 && fd == ctx->rtsp.rtp_socket) { - int result = rtsp_handle_udp_rtp_data(&ctx->rtsp, ctx->conn); + if ((ctx->rtsp && ctx->rtsp->initialized) && ctx->rtsp->rtp_socket >= 0 && fd == ctx->rtsp->rtp_socket) { + int result = rtsp_handle_udp_rtp_data(ctx->rtsp, ctx->conn); if (result < 0) { return -1; /* Error */ } @@ -423,18 +427,19 @@ int stream_handle_fd_event(stream_context_t *ctx, int fd, uint32_t events, int64 /* Handle UDP RTCP socket - drain all available packets for * edge-triggered pollers (epoll EPOLLET / kqueue EV_CLEAR). */ - if (ctx->rtsp.initialized && ctx->rtsp.rtcp_socket >= 0 && fd == ctx->rtsp.rtcp_socket) { + if ((ctx->rtsp && ctx->rtsp->initialized) && ctx->rtsp->rtcp_socket >= 0 && fd == ctx->rtsp->rtcp_socket) { /* RTCP data processing could be added here in the future */ /* For now, just consume all data to prevent buffer overflow */ uint8_t rtcp_buffer[RTCP_BUFFER_SIZE]; - while (recv(ctx->rtsp.rtcp_socket, rtcp_buffer, sizeof(rtcp_buffer), 0) > 0) + while (recv(ctx->rtsp->rtcp_socket, rtcp_buffer, sizeof(rtcp_buffer), 0) > 0) ; return 0; } /* Process HTTP proxy socket events */ - if (ctx->http_proxy.initialized && ctx->http_proxy.socket >= 0 && fd == ctx->http_proxy.socket) { - int result = http_proxy_handle_socket_event(&ctx->http_proxy, events); + if ((ctx->http_proxy && ctx->http_proxy->initialized) && ctx->http_proxy->socket >= 0 && + fd == ctx->http_proxy->socket) { + int result = http_proxy_handle_socket_event(ctx->http_proxy, events); if (result < 0) { logger(LOG_ERROR, "HTTP Proxy: Socket event handling failed"); return -1; @@ -450,12 +455,15 @@ static int stream_init_rtsp_control(stream_context_t *ctx, service_t *service, i const char *resolved_seek_param_name = service->seek_param_name; char resolved_rtsp_url[2048]; - rtsp_session_init(&ctx->rtsp); - ctx->rtsp.status_index = status_index; - ctx->rtsp.epoll_fd = ctx->epoll_fd; - ctx->rtsp.conn = ctx->conn; - ctx->rtsp.metadata_probe = metadata_probe; - ctx->rtsp.upstream_ifname = get_upstream_interface_for_rtsp(service->ifname); + ctx->rtsp = calloc(1, sizeof(*ctx->rtsp)); + if (!ctx->rtsp) + return -1; + rtsp_session_init(ctx->rtsp); + ctx->rtsp->status_index = status_index; + ctx->rtsp->epoll_fd = ctx->epoll_fd; + ctx->rtsp->conn = ctx->conn; + ctx->rtsp->metadata_probe = metadata_probe; + ctx->rtsp->upstream_ifname = get_upstream_interface_for_rtsp(service->ifname); if (!service->rtsp_url) { logger(LOG_ERROR, "RTSP URL not found in service configuration"); @@ -470,9 +478,9 @@ static int stream_init_rtsp_control(stream_context_t *ctx, service_t *service, i return -1; } - if (service_format_recent_seek_range(&seek_parse_result, ctx->rtsp.playseek_range_start, - sizeof(ctx->rtsp.playseek_range_start)) > 0) { - ctx->rtsp.use_playseek_range = 1; + if (service_format_recent_seek_range(&seek_parse_result, ctx->rtsp->playseek_range_start, + sizeof(ctx->rtsp->playseek_range_start)) > 0) { + ctx->rtsp->use_playseek_range = 1; resolved_seek_param_name = NULL; } @@ -481,16 +489,16 @@ static int stream_init_rtsp_control(stream_context_t *ctx, service_t *service, i logger(LOG_ERROR, "RTSP: Failed to resolve upstream URL"); return -1; } - if (rtsp_parse_server_url(&ctx->rtsp, resolved_rtsp_url, NULL, NULL) < 0) { + if (rtsp_parse_server_url(ctx->rtsp, resolved_rtsp_url, NULL, NULL) < 0) { logger(LOG_ERROR, "RTSP: Failed to parse URL"); return -1; } - if (rtsp_connect(&ctx->rtsp) < 0) { + if (rtsp_connect(ctx->rtsp) < 0) { logger(LOG_ERROR, "RTSP: Failed to initiate connection"); return -1; } - logger(LOG_DEBUG, "RTSP: Async connection initiated, state=%d", ctx->rtsp.state); + logger(LOG_DEBUG, "RTSP: Async connection initiated, state=%d", ctx->rtsp->state); return 0; } @@ -529,11 +537,14 @@ int stream_context_init_for_worker(stream_context_t *ctx, connection_t *conn, se seek_parse_result_t seek_parse_result; /* Snapshot mode is not supported for HTTP proxy - ignore is_snapshot */ - http_proxy_session_init(&ctx->http_proxy); - ctx->http_proxy.epoll_fd = ctx->epoll_fd; - ctx->http_proxy.conn = conn; - ctx->http_proxy.status_index = status_index; - ctx->http_proxy.upstream_ifname = get_upstream_interface_for_http(service->ifname); + ctx->http_proxy = calloc(1, sizeof(*ctx->http_proxy)); + if (!ctx->http_proxy) + return -1; + http_proxy_session_init(ctx->http_proxy); + ctx->http_proxy->epoll_fd = ctx->epoll_fd; + ctx->http_proxy->conn = conn; + ctx->http_proxy->status_index = status_index; + ctx->http_proxy->upstream_ifname = get_upstream_interface_for_http(service->ifname); if (!service->http_url) { logger(LOG_ERROR, "HTTP URL not found in service configuration"); @@ -557,28 +568,28 @@ int stream_context_init_for_worker(stream_context_t *ctx, connection_t *conn, se } /* Parse URL */ - if (http_proxy_parse_url(&ctx->http_proxy, proxy_url) < 0) { + if (http_proxy_parse_url(ctx->http_proxy, proxy_url) < 0) { logger(LOG_ERROR, "HTTP Proxy: Failed to parse URL"); return -1; } /* Set HTTP method from client request */ - http_proxy_set_method(&ctx->http_proxy, conn->http_req.method); + http_proxy_set_method(ctx->http_proxy, conn->http_req->method); /* Set raw headers for full passthrough */ - http_proxy_set_raw_headers(&ctx->http_proxy, conn->http_req.raw_headers, conn->http_req.raw_headers_len); + http_proxy_set_raw_headers(ctx->http_proxy, conn->http_req->raw_headers, conn->http_req->raw_headers_len); /* Set request body for passthrough */ - if (conn->http_req.body && conn->http_req.body_len > 0) { - http_proxy_set_request_body(&ctx->http_proxy, conn->http_req.body, conn->http_req.body_len); + if (conn->http_req->body && conn->http_req->body_len > 0) { + http_proxy_set_request_body(ctx->http_proxy, conn->http_req->body, conn->http_req->body_len); } /* Set request headers for base URL construction during content rewriting */ - http_proxy_set_request_headers(&ctx->http_proxy, conn->http_req.hostname, conn->http_req.x_forwarded_host, - conn->http_req.x_forwarded_proto); + http_proxy_set_request_headers(ctx->http_proxy, conn->http_req->hostname, conn->http_req->x_forwarded_host, + conn->http_req->x_forwarded_proto); /* Initiate connection */ - if (http_proxy_connect(&ctx->http_proxy) < 0) { + if (http_proxy_connect(ctx->http_proxy) < 0) { logger(LOG_ERROR, "HTTP Proxy: Failed to initiate connection"); return -1; } @@ -655,11 +666,11 @@ int stream_tick(stream_context_t *ctx, int64_t now) { fcc_session_tick(ctx, now); /* RTSP session tick (STUN timeout, keepalive, state timeout) */ - if (rtsp_session_tick(&ctx->rtsp, now) < 0) + if (rtsp_session_tick(ctx->rtsp, now) < 0) return -1; /* HTTP proxy session tick (state timeout) */ - if (http_proxy_session_tick(&ctx->http_proxy, now) < 0) + if (http_proxy_session_tick(ctx->http_proxy, now) < 0) return -1; /* Check snapshot timeout (5 seconds) */ @@ -709,10 +720,12 @@ int stream_context_cleanup(stream_context_t *ctx) { mcast_session_cleanup(&ctx->mcast); /* Clean up HTTP proxy session (always synchronous) */ - http_proxy_session_cleanup(&ctx->http_proxy); + http_proxy_session_cleanup(ctx->http_proxy); + free(ctx->http_proxy); + ctx->http_proxy = NULL; /* Clean up RTSP session - this may initiate async TEARDOWN */ - int rtsp_async = rtsp_session_cleanup(&ctx->rtsp); + int rtsp_async = rtsp_session_cleanup(ctx->rtsp); /* Clean up FEC context (fec_cleanup owns the socket cleanup) */ fec_cleanup(&ctx->fec, ctx->epoll_fd); @@ -726,6 +739,8 @@ int stream_context_cleanup(stream_context_t *ctx) { /* Do NOT clear ctx->service - still needed for RTSP */ return 1; /* Indicate async cleanup in progress */ } + free(ctx->rtsp); + ctx->rtsp = NULL; /* NOTE: Do NOT free ctx->service here! * The service pointer is shared with the parent connection (c->service). diff --git a/src/stream.h b/src/stream.h index 6a375579..f164aac6 100644 --- a/src/stream.h +++ b/src/stream.h @@ -105,10 +105,10 @@ typedef struct stream_context_s { mcast_session_t mcast; /* RTSP session for SERVICE_RTSP */ - rtsp_session_t rtsp; + rtsp_session_t *rtsp; /* HTTP proxy session for SERVICE_HTTP */ - http_proxy_session_t http_proxy; + http_proxy_session_t *http_proxy; /* RTP reorder context */ rtp_reorder_t reorder; diff --git a/src/worker.c b/src/worker.c index ba912711..0025b910 100644 --- a/src/worker.c +++ b/src/worker.c @@ -480,7 +480,7 @@ int worker_run_event_loop(int *listen_sockets, int num_sockets, int notif_fd) { if (res == STREAM_EVENT_DURATION_READY) { send_http_headers(c, STATUS_200, "application/json", NULL); char response[64]; - snprintf(response, sizeof(response), "{\"duration\": \"%0.3f\"}", c->stream.rtsp.r2h_duration_value); + snprintf(response, sizeof(response), "{\"duration\": \"%0.3f\"}", c->stream.rtsp->r2h_duration_value); connection_queue_output_and_flush(c, (const uint8_t *)response, strlen(response)); } else if (res == STREAM_EVENT_METADATA_READY) { @@ -526,8 +526,9 @@ int worker_run_event_loop(int *listen_sockets, int num_sockets, int notif_fd) { continue; } } - } else if (c->state == CONN_CLOSING && c->stream.rtsp.initialized && !c->stream.rtsp.cleanup_done) { - if (rtsp_session_tick(&c->stream.rtsp, now) < 0) { + } else if (c->state == CONN_CLOSING && (c->stream.rtsp && c->stream.rtsp->initialized) && + !c->stream.rtsp->cleanup_done) { + if (rtsp_session_tick(c->stream.rtsp, now) < 0) { worker_close_and_free_connection(c); c = next; continue; From 466165799f2e2469937bd1fd31f292349cf29b96 Mon Sep 17 00:00:00 2001 From: Stackie Jia Date: Sun, 6 Sep 2026 20:31:33 +0800 Subject: [PATCH 12/19] perf(stream): reduce dispatch overhead and idle memory --- docs/en/reference/benchmark.md | 14 ++- docs/reference/benchmark.md | 14 ++- e2e/test_pages.py | 17 ++++ src/buffer_pool.h | 12 +-- src/connection.c | 117 ++++++++++++++++------- src/connection.h | 11 +-- src/http_proxy.c | 6 +- src/multicast.c | 140 ++++++++++++++++++++++------ src/multicast.h | 3 + src/rtp_fec.c | 18 +++- src/rtp_fec.h | 12 +-- src/rtp_reorder.c | 6 ++ src/rtsp.c | 6 +- src/send_queue.c | 15 ++- src/send_queue.h | 4 +- src/worker.c | 74 +++++++++++++-- src/worker.h | 3 + tools/stress-test/benchmark.py | 14 ++- tools/stress-test/test_benchmark.py | 16 +++- 19 files changed, 386 insertions(+), 116 deletions(-) diff --git a/docs/en/reference/benchmark.md b/docs/en/reference/benchmark.md index 198e4244..2325138b 100644 --- a/docs/en/reference/benchmark.md +++ b/docs/en/reference/benchmark.md @@ -73,6 +73,12 @@ Values are the means of per-trial average CPU utilization, with the minimum and ## Appendix: Performance Optimization Strategies in rtp2httpd +### Allocate Memory by Lifetime + +Connections allocate RTSP or HTTP proxy state only for the protocol they use. FEC group tables are allocated when recovery groups need to be stored. HTTP input buffers and parsed requests use separate anonymous memory mappings: input storage is released after parsing and routing, while ordinary media streams release parsed request data after generating response headers. HTTP proxies retain the request headers and body they still use. Temporary request pages can return directly to the operating system instead of remaining in the heap alongside long-lived connections. + +The packet pool starts with 128 buffers and grows in increments of 128. The control pool starts with 16 and grows in increments of 16. The worker periodically reclaims completely idle segments while retaining a base capacity. Client queue budgets are calculated separately from the initial allocation, so reducing initial memory does not reduce the existing buffering allowance. + ### Shared Multicast Subscriptions Within Each Worker Each worker maintains a shared-source registry keyed by the resolved multicast address, port, SSM source address, effective upstream interface, and FEC port. Channel names, `/rtp/` versus `/udp/` spelling, and FCC server parameters do not participate in matching. Requests for the same resource create one main multicast socket and, when configured, one FEC socket. @@ -87,10 +93,16 @@ For ordinary multicast, the shared source parses and reorders RTP once, then com The Buffer layer adds an on-demand 64 KiB batch pool alongside the existing 1536-byte packet pool and control pool. The worker owns the batch pool, so queued data can outlive its multicast source. It initially allocates four batches and grows in increments of four. Its maximum capacity is derived from a `buffer-pool-max-size × 1536` byte budget, with room for at least four batches. This limit applies to the batch pool separately from the original packet pool. If the batch pool is exhausted, forwarding can continue through small-packet references. -Clients share the underlying payload while each owns a separate `buffer_ref_t` view. Its `owner` points to the same immutable data; list links, send offsets, and remaining lengths stay independent. A partial send updates only that client's view. The backing memory returns to the pool only after the last view is released. Each client retains its own send queue and capacity limit, so a slow client does not pause reception for other subscribers. +Multiple clients share the underlying payload while each owns a separate `buffer_ref_t` view. Its `owner` points to the same immutable data; list links, send offsets, and remaining lengths stay independent. A partial send updates only that client's view. The backing memory returns to the pool only after the last view is released. Each client retains its own send queue and capacity limit, so a slow client does not pause reception for other subscribers. A source with only one subscriber uses the batch descriptor directly, avoiding an extra view allocation. Queue limits now charge the backing buffer capacity instead of assuming “buffer count × 1536.” A batch with only a few unsent bytes still consumes the full 64 KiB allowance until that client releases its reference. This prevents shared large buffers from bypassing the existing slow-client memory limits. +### Reduce Fixed Receive and Send Costs + +Platforms supporting `recvmmsg` receive up to 16 datagrams per call. The worker reuses receive descriptors and unconsumed packet buffers. Data arrives directly in pool buffers, avoiding an additional copy after reception; platforms without batch reception receive one packet at a time. Once initial RTP reordering is complete, an expected packet can be delivered directly when the window is empty and FEC is disabled, avoiding insertion into and removal from reorder slots. + +Writes enter a local worker queue first. The worker subscribes to kernel writable events only when a socket cannot make further progress, reducing per-batch event registration changes. Each connection sends at most 256 KiB per turn, and each event-loop iteration processes at most 128 write tasks. Remaining tasks stay queued so reception, timers, and other clients can also run. + ### Immutable Batch Snapshots Platforms supporting memory-file sealing use `memfd_create` to create anonymous memory files for shared batches. Each file is written once, sealed, and sent to multiple clients through `sendfile`, reusing the same kernel pages. This path applies only when multiple clients share a nearly full batch. Ordinary memory buffers continue to use `sendmsg`. diff --git a/docs/reference/benchmark.md b/docs/reference/benchmark.md index 80910f38..e9a88e82 100644 --- a/docs/reference/benchmark.md +++ b/docs/reference/benchmark.md @@ -73,6 +73,12 @@ msd_lite 保留上游示例的 48 KiB 接收水位、64 KiB 发送水位、1 MiB ## 附:rtp2httpd 的性能优化策略 +### 按生命周期分配内存 + +连接只为实际使用的协议分配 RTSP 或 HTTP 代理状态,FEC 分组表在需要保存恢复分组时才分配。HTTP 输入缓冲和已解析请求使用独立的匿名内存映射:输入缓冲在解析和路由完成后释放,普通媒体流的请求数据在响应头生成后释放。HTTP 代理保留其仍在使用的请求头和请求体。临时请求页可直接交还操作系统,避免长期连接将这些已空闲的页留在堆中。 + +包缓冲池初始分配 128 个缓冲,按 128 个扩展;控制缓冲池初始分配 16 个,按 16 个扩展。worker 定时回收完全空闲的分段,保留必要的基础容量。客户端队列的逻辑预算与初始预分配量分开计算,减少初始内存不改变原有的缓冲余量。 + ### 每个 worker 共享组播订阅 每个 worker 维护一张共享源表,按照解析后的组播地址、端口、SSM 源地址、有效上游接口及 FEC 端口匹配。频道名称、`/rtp/` 与 `/udp/` 的路径写法、FCC 服务器参数不参与匹配。相同资源只建立一份主组播 socket,以及按需建立的一份 FEC socket。 @@ -87,10 +93,16 @@ msd_lite 保留上游示例的 48 KiB 接收水位、64 KiB 发送水位、1 MiB Buffer 层在原有 1536 字节包缓冲池和控制缓冲池之外,增加按需分配的 64 KiB 批次池。批次池归 worker 所有,已排队的数据可在组播源销毁后继续存活。默认初始分配 4 个批次,按 4 个扩展,最大容量按 `buffer-pool-max-size × 1536` 字节预算换算,并至少容纳 4 个批次。这是批次池自身的上限,与原有包池分别计量。批次池耗尽时,仍可通过小包引用继续分发。 -共享的是底层 payload,而每个客户端拥有独立的 `buffer_ref_t` 视图。视图通过 `owner` 指向同一份不可修改的数据,并保留自己的链表指针、发送偏移和剩余长度。客户端的部分发送只修改自己的视图;最后一个视图释放后,底层内存才返回池中。每个客户端仍有自己的发送队列和容量限制,慢客户端不会暂停其他订阅者的接收。 +多个客户端共享底层 payload,各自拥有独立的 `buffer_ref_t` 视图。视图通过 `owner` 指向同一份不可修改的数据,并保留自己的链表指针、发送偏移和剩余长度。客户端的部分发送只修改自己的视图;最后一个视图释放后,底层内存才返回池中。每个客户端仍有自己的发送队列和容量限制,慢客户端不会暂停其他订阅者的接收。只有一个订阅者时,直接使用批次自身的描述符,省去额外视图分配。 队列限额改为按底层缓冲容量计费,不能再使用「缓冲数量 × 1536」。一个只剩少量字节未发送的批次,仍占用完整 64 KiB 容量,直到该客户端释放引用。这避免共享大缓冲绕过原有慢客户端内存限制。 +### 减少收发路径的固定开销 + +支持 `recvmmsg` 的平台一次接收最多 16 个数据报,worker 复用接收描述符和未消耗的包缓冲。数据直接进入缓冲池,不增加一次接收后的复制;不支持批量接收时使用逐包接收。对于已完成起始重排、序号连续且窗口为空的 RTP 流,未启用 FEC 时直接交付负载,省去重排槽位的插入和移除。 + +发送任务先进入 worker 内部队列,只有 socket 暂时无法继续写入时才订阅内核可写事件,减少每个批次的事件监听切换。每个连接单次最多发送 256 KiB,每轮最多处理 128 个发送任务,剩余任务继续排队,使接收、定时器和其他客户端都能得到处理。 + ### 不可修改的批次快照 支持内存文件封存的平台会通过 `memfd_create` 为共享批次建立匿名内存文件,写入一次后封存,再由多个客户端通过 `sendfile` 发送同一份内核数据页。只在多个客户端共享接近满容量的批次时采用该路径;普通内存发送仍通过 `sendmsg` 完成。 diff --git a/e2e/test_pages.py b/e2e/test_pages.py index 34559471..a44f2b1c 100644 --- a/e2e/test_pages.py +++ b/e2e/test_pages.py @@ -7,6 +7,7 @@ import json import os import signal +import socket import struct import time from urllib.parse import quote @@ -317,6 +318,22 @@ def test_status_sse_content_type(self, basic_r2h): ct = hdrs.get("content-type", "") assert "event-stream" in ct or "text/" in ct + def test_sse_ignores_input_after_request_storage_is_released(self, basic_r2h): + """A second request cannot re-enter a finished parser on an SSE connection.""" + with socket.create_connection(("127.0.0.1", basic_r2h.port), timeout=4) as sock: + sock.sendall(b"GET /status/sse HTTP/1.1\r\nHost: localhost\r\n\r\n") + with sock.makefile("rb") as response: + assert response.readline().startswith(b"HTTP/1.1 200") + while response.readline().strip(): + pass + sock.sendall(b"GET /status HTTP/1.1\r\nHost: localhost\r\n\r\n") + updates = 0 + while updates < 3: + line = response.readline() + assert line, "SSE connection closed after late client input" + assert not line.startswith(b"HTTP/"), "Late input was routed as another request" + updates += line.startswith(b"data:") + # --------------------------------------------------------------------------- # app-path-prefix diff --git a/src/buffer_pool.h b/src/buffer_pool.h index 869dec22..a0add212 100644 --- a/src/buffer_pool.h +++ b/src/buffer_pool.h @@ -8,19 +8,19 @@ /* Buffer pool configuration - optimized for RTP packets with cache alignment */ #define BUFFER_POOL_ALIGNMENT 64 -#define BUFFER_POOL_INITIAL_SIZE 1024 -#define BUFFER_POOL_EXPAND_SIZE 512 +#define BUFFER_POOL_INITIAL_SIZE 128 +#define BUFFER_POOL_EXPAND_SIZE 128 #define BUFFER_POOL_BUFFER_SIZE 1536 -#define BUFFER_POOL_LOW_WATERMARK 256 +#define BUFFER_POOL_LOW_WATERMARK 32 #define BUFFER_POOL_HIGH_WATERMARK (BUFFER_POOL_INITIAL_SIZE * 3) /* Keep shared output below 64 KiB, including the last complete RTP payload. */ #define BUFFER_POOL_BATCH_SIZE 65536 /* Control/API buffer pool configuration */ -#define CONTROL_POOL_INITIAL_SIZE 256 -#define CONTROL_POOL_EXPAND_SIZE 128 +#define CONTROL_POOL_INITIAL_SIZE 16 +#define CONTROL_POOL_EXPAND_SIZE 16 #define CONTROL_POOL_MAX_BUFFERS 4096 -#define CONTROL_POOL_LOW_WATERMARK 64 +#define CONTROL_POOL_LOW_WATERMARK 4 #define CONTROL_POOL_HIGH_WATERMARK (CONTROL_POOL_INITIAL_SIZE * 2) typedef enum { diff --git a/src/connection.c b/src/connection.c index 98cbba95..d62a2a87 100644 --- a/src/connection.c +++ b/src/connection.c @@ -10,6 +10,7 @@ #include "service.h" #include "status.h" #include "utils.h" +#include "worker.h" #include #include #include @@ -19,6 +20,7 @@ #include #include #include +#include #include #include @@ -27,6 +29,8 @@ #define CONNECTION_TCP_KEEPALIVE_INTVL_SEC 5 #define CONNECTION_TCP_KEEPALIVE_CNT 3 #define CONN_QUEUE_MIN_BUFFERS 64 +/* Logical queue budget is independent of eagerly allocated packet buffers. */ +#define CONN_QUEUE_BASE_BUFFERS 1024 #define CONN_QUEUE_BURST_FACTOR 3.0 #define CONN_QUEUE_BURST_FACTOR_CONGESTED 1.5 #define CONN_QUEUE_BURST_FACTOR_DRAIN 1.0 @@ -313,7 +317,12 @@ static void connection_prepare_queue_limit_inputs(queue_limit_inputs_t *out) { if (active == 0) active = 1; - size_t total_buffers = pool->num_buffers ? pool->num_buffers : BUFFER_POOL_INITIAL_SIZE; + size_t total_buffers = pool->num_buffers; + size_t base_buffers = CONN_QUEUE_BASE_BUFFERS; + if (pool->max_buffers && base_buffers > pool->max_buffers) + base_buffers = pool->max_buffers; + if (total_buffers < base_buffers) + total_buffers = base_buffers; size_t share_buffers = total_buffers / active; if (share_buffers < CONN_QUEUE_MIN_BUFFERS) share_buffers = CONN_QUEUE_MIN_BUFFERS; @@ -470,7 +479,7 @@ void connection_begin_drain_close(connection_t *c) { if (!c || c->state == CONN_CLOSING) return; c->state = CONN_CLOSING; - connection_epoll_update_events(c->epfd, c->fd, POLLER_IN | POLLER_OUT | POLLER_RDHUP | POLLER_HUP | POLLER_ERR); + connection_schedule_write(c); } int connection_set_nonblocking(int fd) { @@ -485,7 +494,20 @@ int connection_set_tcp_nodelay(int fd) { return setsockopt(fd, IPPROTO_TCP, TCP_NODELAY, &on, sizeof(on)); } -void connection_epoll_update_events(int epfd, int fd, uint32_t events) { poller_mod(epfd, fd, events); } +static void connection_watch_writable(connection_t *c, int enabled) { + if (c->write_poll_armed == enabled) + return; + uint32_t mask = POLLER_IN | POLLER_RDHUP | POLLER_HUP | POLLER_ERR; + if (enabled) + mask |= POLLER_OUT; + if (poller_mod(c->epfd, c->fd, mask) == 0) + c->write_poll_armed = enabled; +} + +void connection_schedule_write(connection_t *c) { + if (c && !c->write_poll_armed) + worker_queue_write(c); +} connection_t *connection_create(int fd, int epfd, struct sockaddr_storage *client_addr, socklen_t addr_len) { connection_t *c = calloc(1, sizeof(*c)); @@ -546,14 +568,36 @@ connection_t *connection_create(int fd, int epfd, struct sockaddr_storage *clien CONNECTION_TCP_KEEPALIVE_CNT); } - /* Request storage is only needed while parsing and starting the response. */ - c->http_req = malloc(sizeof(*c->http_req)); - if (!c->http_req) { - free(c); - return NULL; + return c; +} + +/* Temporary request parsing buffers have a different lifetime from the small + * connection record. Separate mappings let the OS reclaim these pages even + * while adjacent, long-lived stream allocations remain in the heap. */ +static size_t request_mapping_size; + +static int connection_allocate_request(connection_t *c) { + if (!request_mapping_size) { + long page_size = sysconf(_SC_PAGESIZE); + if (page_size <= 0) + return -1; + request_mapping_size = (sizeof(*c->http_req) + (size_t)page_size - 1) / (size_t)page_size * (size_t)page_size; } + void *storage = + mmap(NULL, request_mapping_size + INBUF_SIZE, PROT_READ | PROT_WRITE, MAP_PRIVATE | MAP_ANONYMOUS, -1, 0); + if (storage == MAP_FAILED) + return -1; + c->http_req = storage; + c->inbuf = (char *)storage + request_mapping_size; http_request_init(c->http_req); - return c; + return 0; +} + +static void connection_release_input(connection_t *c) { + if (c->inbuf) { + munmap(c->inbuf, INBUF_SIZE); + c->inbuf = NULL; + } } void connection_release_request(connection_t *c) { @@ -561,7 +605,7 @@ void connection_release_request(connection_t *c) { return; c->request_is_head = strcasecmp(c->http_req->method, "HEAD") == 0; http_request_cleanup(c->http_req); - free(c->http_req); + munmap(c->http_req, request_mapping_size); c->http_req = NULL; } @@ -610,7 +654,7 @@ void connection_cleanup(connection_t *c) { } connection_release_request(c); - free(c->inbuf); + connection_release_input(c); free(c); } @@ -670,7 +714,7 @@ int connection_queue_output_and_flush(connection_t *c, const uint8_t *data, size int result = connection_queue_output(c, data, len); if (result < 0) return result; - connection_epoll_update_events(c->epfd, c->fd, POLLER_IN | POLLER_OUT | POLLER_RDHUP | POLLER_HUP | POLLER_ERR); + connection_schedule_write(c); if (c) { c->state = CONN_CLOSING; @@ -684,7 +728,7 @@ connection_write_status_t connection_handle_write(connection_t *c) { return CONNECTION_WRITE_IDLE; if (!c->send_queue.head) { - connection_epoll_update_events(c->epfd, c->fd, POLLER_IN | POLLER_RDHUP | POLLER_HUP | POLLER_ERR); + connection_watch_writable(c, 0); connection_report_queue(c); if (c->state == CONN_CLOSING) return CONNECTION_WRITE_CLOSED; @@ -697,20 +741,21 @@ connection_write_status_t connection_handle_write(connection_t *c) { * EPOLLOUT / EV_CLEAR fires only once when the socket becomes writable. */ for (;;) { size_t bytes_sent = 0; - int ret = send_queue_send(c->fd, &c->send_queue, &bytes_sent); + int ret = send_queue_send(c->fd, &c->send_queue, 256 * 1024 - total_sent, &bytes_sent); total_sent += bytes_sent; /* Count post-send so per-client bandwidth reflects actual receive rate, not enqueue rate. */ c->stream.total_bytes_sent += (uint64_t)bytes_sent; if (ret < 0 && ret != -2) { c->state = CONN_CLOSING; - connection_epoll_update_events(c->epfd, c->fd, POLLER_IN | POLLER_RDHUP | POLLER_HUP | POLLER_ERR); + connection_watch_writable(c, 0); connection_report_queue(c); return CONNECTION_WRITE_CLOSED; } if (ret == -2) { - /* EAGAIN - socket send buffer full, wait for next writable event */ + /* Subscribe only when a real send needs to wait for the socket. */ + connection_watch_writable(c, 1); connection_report_queue(c); if (total_sent > 0) stream_on_client_drain(&c->stream); @@ -719,33 +764,39 @@ connection_write_status_t connection_handle_write(connection_t *c) { if (!c->send_queue.head) { if (c->state == CONN_CLOSING) { - connection_epoll_update_events(c->epfd, c->fd, POLLER_IN | POLLER_RDHUP | POLLER_HUP | POLLER_ERR); + connection_watch_writable(c, 0); connection_report_queue(c); return CONNECTION_WRITE_CLOSED; } - /* Notify upstream BEFORE arming the poller mask: resume() may queue - * new buffers in this same call frame, in which case POLLER_OUT must - * stay armed so the worker re-enters this function to drain them. */ + /* resume() may synchronously queue more output. Schedule it locally. */ + connection_watch_writable(c, 0); if (total_sent > 0) stream_on_client_drain(&c->stream); - uint32_t mask = POLLER_IN | POLLER_RDHUP | POLLER_HUP | POLLER_ERR; if (c->send_queue.head) - mask |= POLLER_OUT; - connection_epoll_update_events(c->epfd, c->fd, mask); + connection_schedule_write(c); connection_report_queue(c); return CONNECTION_WRITE_IDLE; } + /* Keep one writable client from starving input, timers and other clients. */ + if (total_sent >= 256 * 1024) { + connection_watch_writable(c, 0); + stream_on_client_drain(&c->stream); + connection_report_queue(c); + return CONNECTION_WRITE_PENDING; + } + /* Guard against spinning if send_queue_send sent 0 bytes without EAGAIN */ if (bytes_sent == 0) break; } - /* Queue still has data but we couldn't make progress */ + /* Queue still has data but we could not make progress. Wait for readiness. */ + connection_watch_writable(c, 1); connection_report_queue(c); if (total_sent > 0) stream_on_client_drain(&c->stream); - return CONNECTION_WRITE_PENDING; + return CONNECTION_WRITE_BLOCKED; } void connection_handle_read(connection_t *c) { @@ -758,12 +809,9 @@ void connection_handle_read(connection_t *c) { * with bodies larger than INBUF_SIZE. */ for (;;) { if (c->in_len < INBUF_SIZE) { - if (!c->inbuf) { - c->inbuf = malloc(INBUF_SIZE); - if (!c->inbuf) { - c->state = CONN_CLOSING; - return; - } + if (!c->http_req && connection_allocate_request(c) < 0) { + c->state = CONN_CLOSING; + return; } int r = read(c->fd, c->inbuf + c->in_len, INBUF_SIZE - c->in_len); if (r > 0) { @@ -786,8 +834,7 @@ void connection_handle_read(connection_t *c) { /* Request complete, route it */ c->state = CONN_ROUTE; connection_route_and_start(c); - free(c->inbuf); - c->inbuf = NULL; + connection_release_input(c); if (c->headers_sent && !c->stream.http_proxy) connection_release_request(c); return; @@ -1286,7 +1333,7 @@ int connection_queue_buffer(connection_t *c, buffer_ref_t *buf_ref) { * - Lower latency impact (100ms is acceptable for streaming) */ if (send_queue_should_flush(&c->send_queue)) { - connection_epoll_update_events(c->epfd, c->fd, POLLER_IN | POLLER_OUT | POLLER_RDHUP | POLLER_HUP | POLLER_ERR); + connection_schedule_write(c); } return 0; @@ -1302,7 +1349,7 @@ int connection_queue_file(connection_t *c, int file_fd, off_t file_offset, size_ return -1; /* Always flush immediately for file sends (no batching) */ - connection_epoll_update_events(c->epfd, c->fd, POLLER_IN | POLLER_OUT | POLLER_RDHUP | POLLER_HUP | POLLER_ERR); + connection_schedule_write(c); /* Set connection to closing state after file transfer */ c->state = CONN_CLOSING; diff --git a/src/connection.h b/src/connection.h index bb1f4711..9fcf62ea 100644 --- a/src/connection.h +++ b/src/connection.h @@ -55,6 +55,7 @@ typedef struct connection_s { struct connection_s *next; struct connection_s *write_queue_next; int write_queue_pending; + int write_poll_armed; /* Waiting for a kernel writable notification */ /* Backpressure and monitoring */ size_t queue_limit_bytes; @@ -140,12 +141,10 @@ int connection_set_nonblocking(int fd); int connection_set_tcp_nodelay(int fd); /** - * Update epoll events for a file descriptor - * @param epfd epoll file descriptor - * @param fd File descriptor to update - * @param events New event mask + * Schedule buffered output unless waiting for socket writability. + * @param c Client connection */ -void connection_epoll_update_events(int epfd, int fd, uint32_t events); +void connection_schedule_write(connection_t *c); /** * Queue data to connection output buffer for reliable delivery @@ -225,7 +224,7 @@ void connection_recompute_any_upstream_paused(connection_t *c); /** * Mark the connection for orderly shutdown after upstream EOF/error: switch - * to CONN_CLOSING and re-arm the full event mask so the worker keeps draining + * to CONN_CLOSING and schedule a write so the worker keeps draining * any queued bytes to the client before tearing down. No-op if the * connection is already CONN_CLOSING. */ diff --git a/src/http_proxy.c b/src/http_proxy.c index a034aa97..9212d066 100644 --- a/src/http_proxy.c +++ b/src/http_proxy.c @@ -1283,8 +1283,7 @@ static int http_proxy_parse_response_headers(http_proxy_session_t *session) { /* Flush headers immediately - don't use queue_output_and_flush which sets * CONN_CLOSING */ - connection_epoll_update_events(session->conn->epfd, session->conn->fd, - POLLER_IN | POLLER_OUT | POLLER_RDHUP | POLLER_HUP | POLLER_ERR); + connection_schedule_write(session->conn); } /* HEAD responses have no body — go straight to COMPLETE */ @@ -1464,8 +1463,7 @@ int http_proxy_handle_socket_event(http_proxy_session_t *session, uint32_t event if (session->conn && session->conn->state != CONN_CLOSING) { logger(LOG_DEBUG, "HTTP Proxy: Transfer complete"); session->conn->state = CONN_CLOSING; - connection_epoll_update_events(session->conn->epfd, session->conn->fd, - POLLER_IN | POLLER_OUT | POLLER_RDHUP | POLLER_HUP | POLLER_ERR); + connection_schedule_write(session->conn); } } diff --git a/src/multicast.c b/src/multicast.c index c6f9f97d..1cafd412 100644 --- a/src/multicast.c +++ b/src/multicast.c @@ -598,7 +598,14 @@ static void mcast_source_fanout(mcast_source_t *source, buffer_ref_t *batch, int stream_context_t *ctx = session->ctx; if (!session->batched || session->failed || ctx->conn->state == CONN_CLOSING) continue; - buffer_ref_t *view = buffer_ref_view(batch); + buffer_ref_t *view; + if (source->refs == 1) { + /* One subscriber needs no separate mutable send view. */ + view = batch; + buffer_ref_get(view); + } else { + view = buffer_ref_view(batch); + } if (!view) { session->failed = 1; continue; @@ -607,8 +614,7 @@ static void mcast_source_fanout(mcast_source_t *source, buffer_ref_t *batch, int ctx->fcc.initialized ? STREAM_MEDIA_ORIGIN_FCC_MULTICAST : STREAM_MEDIA_ORIGIN_MULTICAST); if (rtp_queue_buf_direct(ctx->conn, view) >= 0 && flush && view->data_size < SEND_QUEUE_BATCH_BYTES) { - connection_epoll_update_events(ctx->epoll_fd, ctx->conn->fd, - POLLER_IN | POLLER_OUT | POLLER_RDHUP | POLLER_HUP | POLLER_ERR); + connection_schedule_write(ctx->conn); } buffer_ref_put(view); } @@ -738,11 +744,114 @@ static void mcast_deliver_packet(mcast_session_t *session, buffer_ref_t *packet) session->failed = 1; } +static void mcast_source_process_packet(mcast_source_t *source, buffer_ref_t *packet, size_t len, int64_t now) { + void *data = packet ? packet->data : NULL; + source->last_data_time = now; + if (packet) { + packet->data_size = (size_t)len; + uint8_t *payload = NULL; + int payload_len = 0; + uint16_t seq = 0; + int packet_type = source->shared_output ? rtp_get_payload(data, (int)len, &payload, &payload_len, &seq) : -1; + if (packet_type == 2) + mcast_source_enable_private_fec(source); + for (mcast_session_t *subscriber = source->packet_subscribers; subscriber; subscriber = subscriber->packet_next) + mcast_deliver_packet(subscriber, packet); + if (source->shared_output && + (packet_type == 1 || (packet_type == 0 && stream_payload_is_mpegts(payload, payload_len)))) { + source->packet_type = packet_type; + packet->data_offset = (size_t)(payload - (uint8_t *)packet->data); + packet->data_size = (size_t)payload_len; + if (packet_type == 1) + rtp_reorder_insert(&source->reorder, packet, seq, NULL, 0, NULL); + else + mcast_source_append(source, packet); + if (source->packet_subscribers) + mcast_source_promote_ready(source); + } + } +} + +#if defined(__linux__) || defined(__FreeBSD__) +/* The worker dispatches receive callbacks serially. Keep scratch descriptors + * and unused pool buffers across callbacks, rather than allocating a full batch + * on every readiness notification (which often carries only one datagram). */ +enum { RECEIVE_BATCH = 16 }; +static struct mmsghdr receive_messages[RECEIVE_BATCH]; +static struct iovec receive_iov[RECEIVE_BATCH]; +static buffer_ref_t *receive_packets[RECEIVE_BATCH]; +static uint8_t receive_discard[BUFFER_POOL_BUFFER_SIZE]; +static int receive_initialized; + +static int mcast_source_receive_batch(mcast_source_t *source, int fd, int64_t now) { + static int unavailable; + if (unavailable) + return -1; + struct mmsghdr *messages = receive_messages; + struct iovec *iov = receive_iov; + buffer_ref_t **packets = receive_packets; + int fallback = 0; + if (!receive_initialized) { + for (int i = 0; i < RECEIVE_BATCH; i++) { + messages[i].msg_hdr.msg_iov = &iov[i]; + messages[i].msg_hdr.msg_iovlen = 1; + iov[i].iov_len = BUFFER_POOL_BUFFER_SIZE; + } + receive_initialized = 1; + } + for (;;) { + for (int i = 0; i < RECEIVE_BATCH; i++) { + if (!packets[i]) + packets[i] = buffer_pool_alloc(); + iov[i].iov_base = packets[i] ? packets[i]->data : receive_discard; + } + int count = recvmmsg(fd, messages, RECEIVE_BATCH, MSG_DONTWAIT, NULL); + if (count < 0) { + if (errno == EINTR) + continue; + if (errno == ENOSYS || errno == EOPNOTSUPP) { + unavailable = 1; + fallback = -1; + } else if (errno != EAGAIN && errno != EWOULDBLOCK) { + logger(LOG_ERROR, "Multicast: Batch receive failed: %s", strerror(errno)); + source->failed = 1; + } + break; + } + for (int i = 0; i < count; i++) { + mcast_source_process_packet(source, packets[i], messages[i].msg_len, now); + buffer_ref_put(packets[i]); + packets[i] = NULL; + } + /* Drain through EAGAIN even after a short result: a signal may interrupt + * recvmmsg after partial progress with more datagrams still queued. */ + } + if (fallback) + mcast_worker_cleanup(); + return fallback; +} +#endif + +void mcast_worker_cleanup(void) { +#if defined(__linux__) || defined(__FreeBSD__) + for (int i = 0; i < RECEIVE_BATCH; i++) { + buffer_ref_put(receive_packets[i]); + receive_packets[i] = NULL; + } + receive_initialized = 0; +#endif +} + int mcast_session_handle_event(mcast_session_t *session, int fd, int64_t now) { mcast_source_t *source = session->source; if (!source) return -1; +#if defined(__linux__) || defined(__FreeBSD__) + if (fd == source->sock && mcast_source_receive_batch(source, fd, now) == 0) + return 0; +#endif + /* Drain to EAGAIN for edge-triggered pollers, including on pool exhaustion. * All subscribers are detached by the worker outside this delivery loop. */ for (;;) { @@ -762,30 +871,7 @@ int mcast_session_handle_event(mcast_session_t *session, int fd, int64_t now) { break; } if (fd == source->sock) { - source->last_data_time = now; - if (packet) { - packet->data_size = (size_t)len; - uint8_t *payload = NULL; - int payload_len = 0; - uint16_t seq = 0; - int packet_type = source->shared_output ? rtp_get_payload(data, (int)len, &payload, &payload_len, &seq) : -1; - if (packet_type == 2) - mcast_source_enable_private_fec(source); - for (mcast_session_t *subscriber = source->packet_subscribers; subscriber; subscriber = subscriber->packet_next) - mcast_deliver_packet(subscriber, packet); - if (source->shared_output && - (packet_type == 1 || (packet_type == 0 && stream_payload_is_mpegts(payload, payload_len)))) { - source->packet_type = packet_type; - packet->data_offset = (size_t)(payload - (uint8_t *)packet->data); - packet->data_size = (size_t)payload_len; - if (packet_type == 1) - rtp_reorder_insert(&source->reorder, packet, seq, NULL, 0, NULL); - else - mcast_source_append(source, packet); - if (source->packet_subscribers) - mcast_source_promote_ready(source); - } - } + mcast_source_process_packet(source, packet, (size_t)len, now); } else { for (mcast_session_t *subscriber = source->subscribers; subscriber; subscriber = subscriber->next) { if (!subscriber->failed && subscriber->ctx->conn->state != CONN_CLOSING) diff --git a/src/multicast.h b/src/multicast.h index 2c5c777c..c48f74f9 100644 --- a/src/multicast.h +++ b/src/multicast.h @@ -62,4 +62,7 @@ int mcast_session_handle_event(mcast_session_t *session, int fd, int64_t now); */ int mcast_session_tick(mcast_session_t *session, int64_t now); +/* Release worker receive scratch buffers before destroying the buffer pools. */ +void mcast_worker_cleanup(void); + #endif /* __MULTICAST_H__ */ diff --git a/src/rtp_fec.c b/src/rtp_fec.c index 539fe2ba..d66ec7ff 100644 --- a/src/rtp_fec.c +++ b/src/rtp_fec.c @@ -56,6 +56,12 @@ static fec_group_t *fec_find_or_create_group(fec_context_t *ctx, uint16_t begin_ return NULL; } + if (!ctx->groups) { + ctx->groups = calloc(FEC_MAX_GROUPS, sizeof(*ctx->groups)); + if (!ctx->groups) + return NULL; + } + /* Look for existing group */ for (int i = 0; i < FEC_MAX_GROUPS; i++) { fec_group_t *grp = &ctx->groups[i]; @@ -172,9 +178,11 @@ void fec_cleanup(fec_context_t *ctx, int epoll_fd) { logger(LOG_DEBUG, "FEC: Closed socket"); } - /* Free all groups */ - for (int i = 0; i < FEC_MAX_GROUPS; i++) { - fec_free_group(&ctx->groups[i]); + if (ctx->groups) { + for (int i = 0; i < FEC_MAX_GROUPS; i++) + fec_free_group(&ctx->groups[i]); + free(ctx->groups); + ctx->groups = NULL; } ctx->group_count = 0; @@ -298,7 +306,7 @@ int fec_process_packet(fec_context_t *ctx, const uint8_t *data, int len) { } int fec_attempt_recovery(fec_context_t *ctx, uint16_t seq, uint8_t **recovered_data, int *recovered_len) { - if (!fec_is_enabled(ctx) || !ctx->reorder) { + if (!fec_is_enabled(ctx) || !ctx->reorder || !ctx->groups) { return -1; } @@ -529,6 +537,8 @@ int fec_attempt_recovery(fec_context_t *ctx, uint16_t seq, uint8_t **recovered_d } void fec_release_expired_groups(fec_context_t *ctx, uint16_t base_seq) { + if (!ctx->groups) + return; /* Release all expired groups */ for (int i = 0; i < FEC_MAX_GROUPS; i++) { fec_group_t *grp = &ctx->groups[i]; diff --git a/src/rtp_fec.h b/src/rtp_fec.h index 03a76ae8..9653a939 100644 --- a/src/rtp_fec.h +++ b/src/rtp_fec.h @@ -59,12 +59,12 @@ typedef struct rtp_reorder_s rtp_reorder_t; * FEC context - per-stream FEC state */ typedef struct fec_context_s { - int initialized; /* Flag: context has been initialized */ - int sock; /* FEC multicast socket (-1 if disabled) */ - uint16_t fec_port; /* FEC multicast port */ - uint8_t fec_active; /* 1 if FEC packets have been received */ - fec_group_t groups[FEC_MAX_GROUPS]; /* Active FEC groups */ - int group_count; /* Number of active groups */ + int initialized; /* Flag: context has been initialized */ + int sock; /* FEC multicast socket (-1 if disabled) */ + uint16_t fec_port; /* FEC multicast port */ + uint8_t fec_active; /* 1 if FEC packets have been received */ + fec_group_t *groups; /* Allocated when the first non-expired FEC group arrives */ + int group_count; /* Number of active groups */ /* min_end_seq caching for efficient expired group detection */ uint16_t min_end_seq; /* Minimum end_seq among active groups */ diff --git a/src/rtp_reorder.c b/src/rtp_reorder.c index 47110b7e..75980ffe 100644 --- a/src/rtp_reorder.c +++ b/src/rtp_reorder.c @@ -228,6 +228,12 @@ int rtp_reorder_insert(rtp_reorder_t *r, buffer_ref_t *buf_ref, uint16_t seqn, c /* Case 1: Expected sequence -> store and flush */ if (likely(seq_diff == 0)) { int slot = seqn & r->window_mask; + /* In-order traffic without FEC has nothing to retain in the window. */ + if (likely(r->count == 0) && !(fec && fec_is_enabled(fec)) && !r->slots[slot]) { + int bytes = deliver_packet(r, buf_ref, conn, is_snapshot); + r->base_seq++; + return bytes; + } if (r->slots[slot]) { /* Old packet from ring wrap-around, release it */ buffer_ref_put(r->slots[slot]); diff --git a/src/rtsp.c b/src/rtsp.c index a9e1a995..26ee89f6 100644 --- a/src/rtsp.c +++ b/src/rtsp.c @@ -1003,8 +1003,7 @@ static int rtsp_handle_terminal_socket_event(rtsp_session_t *session, uint32_t e rtsp_force_cleanup(session); if (session->conn && session->conn->state != CONN_CLOSING) { session->conn->state = CONN_CLOSING; - connection_epoll_update_events(session->conn->epfd, session->conn->fd, - POLLER_IN | POLLER_OUT | POLLER_RDHUP | POLLER_HUP | POLLER_ERR); + connection_schedule_write(session->conn); } return STREAM_EVENT_OK; } @@ -1218,8 +1217,7 @@ int rtsp_handle_socket_event(rtsp_session_t *session, uint32_t events) { rtsp_force_cleanup(session); if (session->conn && session->conn->state != CONN_CLOSING) { session->conn->state = CONN_CLOSING; - connection_epoll_update_events(session->conn->epfd, session->conn->fd, - POLLER_IN | POLLER_OUT | POLLER_RDHUP | POLLER_HUP | POLLER_ERR); + connection_schedule_write(session->conn); } return 0; } diff --git a/src/send_queue.c b/src/send_queue.c index 94adc851..33e9b223 100644 --- a/src/send_queue.c +++ b/src/send_queue.c @@ -198,8 +198,8 @@ int send_queue_should_flush(send_queue_t *queue) { return 0; /* Not ready to flush yet */ } -int send_queue_send(int fd, send_queue_t *queue, size_t *bytes_sent) { - if (!queue->head) { +int send_queue_send(int fd, send_queue_t *queue, size_t max_bytes, size_t *bytes_sent) { + if (!queue->head || !max_bytes) { *bytes_sent = 0; return 0; } @@ -208,7 +208,8 @@ int send_queue_send(int fd, send_queue_t *queue, size_t *bytes_sent) { int shared_fd = buffer_ref_sendfile_fd(shared); if (shared_fd >= 0) { off_t offset = (uint8_t *)shared->iov.iov_base - ((uint8_t *)shared->data + shared->data_offset); - ssize_t sent = platform_sendfile(fd, shared_fd, &offset, shared->iov.iov_len); + size_t count = shared->iov.iov_len < max_bytes ? shared->iov.iov_len : max_bytes; + ssize_t sent = platform_sendfile(fd, shared_fd, &offset, count); if (sent < 0 && (errno == EINVAL || errno == ENOSYS || errno == EOPNOTSUPP)) { /* Keep this subscriber's fallback private; others can still sendfile. */ shared->shared_fd = -2; @@ -243,6 +244,8 @@ int send_queue_send(int fd, send_queue_t *queue, size_t *bytes_sent) { if (queue->head->type == BUFFER_TYPE_FILE) { buffer_ref_t *file_buf = queue->head; size_t remaining = file_buf->file_size - file_buf->file_sent; + if (remaining > max_bytes) + remaining = max_bytes; off_t offset = file_buf->file_offset + file_buf->file_sent; /* Use platform_sendfile() for non-blocking file send */ @@ -294,11 +297,15 @@ int send_queue_send(int fd, send_queue_t *queue, size_t *bytes_sent) { /* Build iovec array from queue buffers (memory buffers only) */ struct iovec iovecs[SEND_QUEUE_MAX_IOVECS]; int iov_count = 0; + size_t remaining_budget = max_bytes; buffer_ref_t *buf = queue->head; - while (buf && iov_count < SEND_QUEUE_MAX_IOVECS && buf->type == BUFFER_TYPE_MEMORY && + while (buf && remaining_budget && iov_count < SEND_QUEUE_MAX_IOVECS && buf->type == BUFFER_TYPE_MEMORY && buffer_ref_sendfile_fd(buf) < 0) { iovecs[iov_count] = buf->iov; + if (iovecs[iov_count].iov_len > remaining_budget) + iovecs[iov_count].iov_len = remaining_budget; + remaining_budget -= iovecs[iov_count].iov_len; iov_count++; buf = buf->send_next; } diff --git a/src/send_queue.h b/src/send_queue.h index 490295ec..5ba4f22e 100644 --- a/src/send_queue.h +++ b/src/send_queue.h @@ -40,8 +40,8 @@ void send_queue_cleanup(send_queue_t *queue); int send_queue_add(send_queue_t *queue, buffer_ref_t *buf_ref); /* Transfers ownership of file_fd only on success. */ int send_queue_add_file(send_queue_t *queue, int file_fd, off_t file_offset, size_t file_size); -/* Returns 0 on success, -1 on fatal error, or -2 when the socket would block. */ -int send_queue_send(int fd, send_queue_t *queue, size_t *bytes_sent); +/* Send at most max_bytes. Return 0 on success, -1 on fatal error, or -2 when blocked. */ +int send_queue_send(int fd, send_queue_t *queue, size_t max_bytes, size_t *bytes_sent); int send_queue_should_flush(send_queue_t *queue); #endif /* __SEND_QUEUE_H__ */ diff --git a/src/worker.c b/src/worker.c index 0025b910..7e48d3a9 100644 --- a/src/worker.c +++ b/src/worker.c @@ -30,6 +30,8 @@ static struct hashmap *fd_map = NULL; /* Connection list head */ static connection_t *conn_head = NULL; +static connection_t *write_head = NULL; +static connection_t *write_tail = NULL; /* Stop flag for graceful shutdown */ static volatile sig_atomic_t stop_flag = 0; @@ -158,10 +160,60 @@ static void remove_connection_from_list(connection_t *c) { } } +void worker_queue_write(connection_t *c) { + if (!c || c->write_queue_pending) + return; + c->write_queue_pending = 1; + c->write_queue_next = NULL; + if (write_tail) + write_tail->write_queue_next = c; + else + write_head = c; + write_tail = c; +} + +static void worker_cancel_write(connection_t *c) { + if (!c->write_queue_pending) + return; + connection_t *previous = NULL; + for (connection_t *entry = write_head; entry; entry = entry->write_queue_next) { + if (entry == c) { + if (previous) + previous->write_queue_next = c->write_queue_next; + else + write_head = c->write_queue_next; + if (write_tail == c) + write_tail = previous; + break; + } + previous = entry; + } + c->write_queue_pending = 0; + c->write_queue_next = NULL; +} + +static void worker_drain_writes(void) { + for (int i = 0; write_head && i < WORKER_MAX_WRITE_BATCH; i++) { + connection_t *c = write_head; + write_head = c->write_queue_next; + if (!write_head) + write_tail = NULL; + c->write_queue_pending = 0; + c->write_queue_next = NULL; + connection_write_status_t status = connection_handle_write(c); + if (status == CONNECTION_WRITE_CLOSED) + worker_close_and_free_connection(c); + else if (status == CONNECTION_WRITE_PENDING) + worker_queue_write(c); + } +} + void worker_close_and_free_connection(connection_t *c) { if (!c) return; + worker_cancel_write(c); + /* CRITICAL: For streaming connections, initiate cleanup first to check if * async TEARDOWN will be started This prevents use-after-free when TEARDOWN * response arrives after connection is freed. */ @@ -270,7 +322,7 @@ int worker_run_event_loop(int *listen_sockets, int num_sockets, int notif_fd) { int64_t last_tick = get_time_ms(); while (!stop_flag) { - int timeout_ms = 100; + int timeout_ms = write_head ? 0 : 100; int n = poller_wait(epfd, events, (int)(sizeof(events) / sizeof(events[0])), timeout_ms); if (n < 0) { if (errno == EINTR) @@ -430,9 +482,9 @@ int worker_run_event_loop(int *listen_sockets, int num_sockets, int notif_fd) { /* Handle POLLER_IN and POLLER_OUT independently (not mutually * exclusive) */ if (events[e].events & POLLER_IN) { - /* For streaming connections, client socket is monitored for - * disconnect detection */ - if (c->streaming) { + /* After routing, only monitor for disconnects and drain stray input. + * The request parser and input mapping may already be released. */ + if (c->state != CONN_READ_REQ_LINE && c->state != CONN_READ_HEADERS) { /* Client sent data or disconnected during streaming. * Drain all available data for edge-triggered pollers. */ char discard_buffer[1024]; @@ -467,11 +519,7 @@ int worker_run_event_loop(int *listen_sockets, int num_sockets, int notif_fd) { } if (events[e].events & POLLER_OUT) { - connection_write_status_t status = connection_handle_write(c); - if (status == CONNECTION_WRITE_CLOSED) { - worker_close_and_free_connection(c); - continue; - } + worker_queue_write(c); } } else { int res = stream_handle_fd_event(&c->stream, fd_ready, events[e].events, now); @@ -508,8 +556,11 @@ int worker_run_event_loop(int *listen_sockets, int num_sockets, int notif_fd) { } /* 2) Periodic tick: update streams and SSE heartbeats */ - if (now - last_tick >= timeout_ms) { + if (now - last_tick >= 100) { last_tick = now; + /* Reclaim idle segments after transient startup/buffering bursts, even + * when long-lived streaming clients remain connected. */ + buffer_pool_try_shrink(); connection_t *c = conn_head; while (c) { connection_t *next = c->next; /* Save next pointer before potential cleanup */ @@ -629,12 +680,15 @@ int worker_run_event_loop(int *listen_sockets, int num_sockets, int notif_fd) { } } } + worker_drain_writes(); } /* Cleanup: close all active connections */ while (conn_head) worker_close_and_free_connection(conn_head); + mcast_worker_cleanup(); + /* Cleanup fd map */ fdmap_cleanup(); diff --git a/src/worker.h b/src/worker.h index a3a3df19..16ddac59 100644 --- a/src/worker.h +++ b/src/worker.h @@ -55,6 +55,9 @@ int worker_run_event_loop(int *listen_sockets, int num_sockets, int notif_fd); */ void worker_close_and_free_connection(connection_t *c); +/* Enqueue one write attempt without changing the socket poller registration. */ +void worker_queue_write(connection_t *c); + /** * Safely cleanup a socket from epoll and fdmap * Order: fdmap_del -> epoll_ctl -> close diff --git a/tools/stress-test/benchmark.py b/tools/stress-test/benchmark.py index cd9a4e92..49316617 100644 --- a/tools/stress-test/benchmark.py +++ b/tools/stress-test/benchmark.py @@ -449,6 +449,15 @@ def trial(program, case, repetition, order, args, binaries): return result +def program_order(programs, repetition): + """Balance every position before reversing the next complete rotation.""" + offset = repetition % len(programs) + order = programs[offset:] + programs[:offset] + if (repetition // len(programs)) % 2: + order.reverse() + return order + + def main(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument("programs", nargs="*", choices=["rtp2httpd", "msd_lite", "udpxy", "tvgate", "baseline"]) @@ -526,10 +535,7 @@ def main(): with (args.output / "trials.jsonl").open("w") as output: for case in args.cases: for repetition in range(args.repetitions): - # Rotate first position; alternate direction to reduce ordering bias. - order = programs[repetition % len(programs) :] + programs[: repetition % len(programs)] - if repetition % 2: - order = order[::-1] + order = program_order(programs, repetition) for position, program in enumerate(order): row = trial(program, case, repetition, position, args, binaries) rows.append(row) diff --git a/tools/stress-test/test_benchmark.py b/tools/stress-test/test_benchmark.py index cfc24e2c..80cb1d0c 100644 --- a/tools/stress-test/test_benchmark.py +++ b/tools/stress-test/test_benchmark.py @@ -1,7 +1,7 @@ -"""Check HTTP framing used by the benchmark's payload validator.""" +"""Check benchmark framing, launch settings, and experimental ordering.""" import pytest -from benchmark import HTTPBody, command_for +from benchmark import HTTPBody, command_for, program_order @pytest.mark.parametrize("chunked", [False, True]) @@ -41,3 +41,15 @@ def test_tvgate_uses_default_runtime_settings(tmp_path, monkeypatch): assert (tmp_path / "trial.yaml").read_text() == ( "server:\n port: 12345\nmulticast:\n multicast_ifaces: [lo]\n upstream_interface: lo\n" ) + + +@pytest.mark.parametrize("count", [2, 3, 4]) +def test_every_program_visits_every_position_per_rotation(count): + programs = [str(i) for i in range(count)] + for cycle in range(3): + orders = [program_order(programs, cycle * count + i) for i in range(count)] + for order in orders: + assert sorted(order) == programs + for position in range(count): + assert sorted(order[position] for order in orders) == programs + assert programs == [str(i) for i in range(count)] From 946d83f77b28d96d2c889d1adc8be16e11e13718 Mon Sep 17 00:00:00 2001 From: Stackie Jia Date: Sun, 6 Sep 2026 21:00:47 +0800 Subject: [PATCH 13/19] perf(multicast): avoid redundant receive and status syscalls --- docs/en/reference/benchmark.md | 6 ++++- docs/reference/benchmark.md | 6 ++++- e2e/test_worker_recovery.py | 20 +++++++++++++-- src/multicast.c | 46 ++++++++++++++++++++++++++-------- src/poller.h | 6 +++-- src/poller_epoll.c | 4 +-- src/poller_kqueue.c | 12 +++++---- src/status.c | 12 ++++++--- 8 files changed, 85 insertions(+), 27 deletions(-) diff --git a/docs/en/reference/benchmark.md b/docs/en/reference/benchmark.md index 2325138b..a201d00b 100644 --- a/docs/en/reference/benchmark.md +++ b/docs/en/reference/benchmark.md @@ -99,10 +99,14 @@ Queue limits now charge the backing buffer capacity instead of assuming “buffe ### Reduce Fixed Receive and Send Costs -Platforms supporting `recvmmsg` receive up to 16 datagrams per call. The worker reuses receive descriptors and unconsumed packet buffers. Data arrives directly in pool buffers, avoiding an additional copy after reception; platforms without batch reception receive one packet at a time. Once initial RTP reordering is complete, an expected packet can be delivered directly when the window is empty and FEC is disabled, avoiding insertion into and removal from reorder slots. +Platforms supporting `recvmmsg` receive up to 16 datagrams per call. The worker reuses receive descriptors and unconsumed packet buffers. After processing, a packet buffer with no other references is reused for the next receive. If a reorder window, FEC state, or send queue still holds a reference, reception uses another buffer to avoid overwriting pending data. Data arrives directly in pool buffers, avoiding an additional copy after reception; platforms without batch reception receive one packet at a time. Once initial RTP reordering is complete, an expected packet can be delivered directly when the window is empty and FEC is disabled, avoiding insertion into and removal from reorder slots. + +Sockets using batch reception use level-triggered notifications. A short batch can return to the event loop without an extra receive call to confirm that the socket is empty. Even if an interruption caused the short read, remaining data triggers another notification. Each callback receives at most 256 datagrams so continuous multicast traffic cannot occupy the event loop indefinitely. Writes enter a local worker queue first. The worker subscribes to kernel writable events only when a socket cannot make further progress, reducing per-batch event registration changes. Each connection sends at most 256 KiB per turn, and each event-loop iteration processes at most 128 write tasks. Remaining tasks stay queued so reception, timers, and other clients can also run. +Client ownership checks in status tracking use a process-local cached PID, refreshed after every fork. This removes repeated `getpid()` calls from queue and send-statistics updates. + ### Immutable Batch Snapshots Platforms supporting memory-file sealing use `memfd_create` to create anonymous memory files for shared batches. Each file is written once, sealed, and sent to multiple clients through `sendfile`, reusing the same kernel pages. This path applies only when multiple clients share a nearly full batch. Ordinary memory buffers continue to use `sendmsg`. diff --git a/docs/reference/benchmark.md b/docs/reference/benchmark.md index e9a88e82..6007b1d9 100644 --- a/docs/reference/benchmark.md +++ b/docs/reference/benchmark.md @@ -99,10 +99,14 @@ Buffer 层在原有 1536 字节包缓冲池和控制缓冲池之外,增加按 ### 减少收发路径的固定开销 -支持 `recvmmsg` 的平台一次接收最多 16 个数据报,worker 复用接收描述符和未消耗的包缓冲。数据直接进入缓冲池,不增加一次接收后的复制;不支持批量接收时使用逐包接收。对于已完成起始重排、序号连续且窗口为空的 RTP 流,未启用 FEC 时直接交付负载,省去重排槽位的插入和移除。 +支持 `recvmmsg` 的平台一次接收最多 16 个数据报,worker 复用接收描述符和未消耗的包缓冲。处理完且没有其他引用的包缓冲直接用于下一次接收;重排、FEC 或发送队列仍持有引用时,使用另一个缓冲,避免覆盖待处理数据。数据直接进入缓冲池,不增加一次接收后的复制;不支持批量接收时使用逐包接收。对于已完成起始重排、序号连续且窗口为空的 RTP 流,未启用 FEC 时直接交付负载,省去重排槽位的插入和移除。 + +批量接收 socket 使用水平触发通知,读到不足一批时即可返回事件循环,省去用于确认 socket 已读空的额外接收调用。即使短读由中断造成,剩余数据也会再次触发通知。每次回调最多接收 256 个数据报,使持续到来的组播数据不会长期占用事件循环。 发送任务先进入 worker 内部队列,只有 socket 暂时无法继续写入时才订阅内核可写事件,减少每个批次的事件监听切换。每个连接单次最多发送 256 KiB,每轮最多处理 128 个发送任务,剩余任务继续排队,使接收、定时器和其他客户端都能得到处理。 +状态统计的客户端归属校验使用进程内缓存的 PID,并在每次 fork 后刷新,省去队列更新和发送统计中重复的 `getpid()` 调用。 + ### 不可修改的批次快照 支持内存文件封存的平台会通过 `memfd_create` 为共享批次建立匿名内存文件,写入一次后封存,再由多个客户端通过 `sendfile` 发送同一份内核数据页。只在多个客户端共享接近满容量的批次时采用该路径;普通内存发送仍通过 `sendmsg` 完成。 diff --git a/e2e/test_worker_recovery.py b/e2e/test_worker_recovery.py index 777e55ab..eded63e4 100644 --- a/e2e/test_worker_recovery.py +++ b/e2e/test_worker_recovery.py @@ -92,7 +92,15 @@ def test_crashed_worker_releases_maxclient_slot_and_logs_recovery(r2h_binary): try: r2h.start() sockets.append(_open_stream(port, upstream.port)) - payload = wait_for_status_payload("127.0.0.1", port, lambda value: len(value["clients"]) == 1) + payload = wait_for_status_payload( + "127.0.0.1", + port, + lambda value: ( + len(value["clients"]) == 1 + and value["clients"][0]["bytesSent"] > 0 + and value["clients"][0]["queueLimitBytes"] > 0 + ), + ) dead_pid = payload["clients"][0]["workerPid"] os.kill(dead_pid, signal.SIGKILL) @@ -107,7 +115,15 @@ def test_crashed_worker_releases_maxclient_slot_and_logs_recovery(r2h_binary): assert _worker_pids(recovered)[0] > 0 sockets.append(_open_stream(port, upstream.port)) - active_again = wait_for_status_payload("127.0.0.1", port, lambda value: len(value["clients"]) == 1) + active_again = wait_for_status_payload( + "127.0.0.1", + port, + lambda value: ( + len(value["clients"]) == 1 + and value["clients"][0]["bytesSent"] > 0 + and value["clients"][0]["queueLimitBytes"] > 0 + ), + ) assert "Reclaimed 1 status client slot" in r2h.read_log() restarted_pid = active_again["clients"][0]["workerPid"] diff --git a/src/multicast.c b/src/multicast.c index 1cafd412..38cd809a 100644 --- a/src/multicast.c +++ b/src/multicast.c @@ -547,7 +547,12 @@ int mcast_session_join(mcast_session_t *session, stream_context_t *ctx) { source->reorder.deliver_arg = source; } source->sock = join_mcast_group(source->service, 0); - if (source->sock < 0 || poller_add(ctx->epoll_fd, source->sock, POLLER_IN) < 0) { + uint32_t read_events = POLLER_IN; +#if defined(__linux__) || defined(__FreeBSD__) + /* Batched reads can yield without a final empty receive. */ + read_events |= POLLER_LEVEL; +#endif + if (source->sock < 0 || poller_add(ctx->epoll_fd, source->sock, read_events) < 0) { mcast_source_free(source); return -1; } @@ -748,6 +753,7 @@ static void mcast_source_process_packet(mcast_source_t *source, buffer_ref_t *pa void *data = packet ? packet->data : NULL; source->last_data_time = now; if (packet) { + packet->data_offset = 0; packet->data_size = (size_t)len; uint8_t *payload = NULL; int payload_len = 0; @@ -772,6 +778,18 @@ static void mcast_source_process_packet(mcast_source_t *source, buffer_ref_t *pa } } +/* A receive buffer can be reused in place once parsing copied its payload into + * a batch. Reorder/FEC windows and client queues retain references when they + * still need the packet, so those buffers must be returned through the pool. */ +static void mcast_receive_recycle(buffer_ref_t **packet) { + if (*packet && (*packet)->refcount == 1) + return; + buffer_ref_put(*packet); + *packet = NULL; +} + +static buffer_ref_t *receive_single_packet; + #if defined(__linux__) || defined(__FreeBSD__) /* The worker dispatches receive callbacks serially. Keep scratch descriptors * and unused pool buffers across callbacks, rather than allocating a full batch @@ -791,6 +809,7 @@ static int mcast_source_receive_batch(mcast_source_t *source, int fd, int64_t no struct iovec *iov = receive_iov; buffer_ref_t **packets = receive_packets; int fallback = 0; + unsigned int received = 0; if (!receive_initialized) { for (int i = 0; i < RECEIVE_BATCH; i++) { messages[i].msg_hdr.msg_iov = &iov[i]; @@ -801,9 +820,10 @@ static int mcast_source_receive_batch(mcast_source_t *source, int fd, int64_t no } for (;;) { for (int i = 0; i < RECEIVE_BATCH; i++) { - if (!packets[i]) + if (!packets[i]) { packets[i] = buffer_pool_alloc(); - iov[i].iov_base = packets[i] ? packets[i]->data : receive_discard; + iov[i].iov_base = packets[i] ? packets[i]->data : receive_discard; + } } int count = recvmmsg(fd, messages, RECEIVE_BATCH, MSG_DONTWAIT, NULL); if (count < 0) { @@ -820,11 +840,14 @@ static int mcast_source_receive_batch(mcast_source_t *source, int fd, int64_t no } for (int i = 0; i < count; i++) { mcast_source_process_packet(source, packets[i], messages[i].msg_len, now); - buffer_ref_put(packets[i]); - packets[i] = NULL; + mcast_receive_recycle(&packets[i]); } - /* Drain through EAGAIN even after a short result: a signal may interrupt - * recvmmsg after partial progress with more datagrams still queued. */ + received += (unsigned int)count; + /* Usually a short read already exhausted the socket. Level triggering + * also covers interruption after partial progress: unread datagrams stay + * ready. Yield on sustained traffic so output and timers get a turn. */ + if (count < RECEIVE_BATCH || received >= 256) + break; } if (fallback) mcast_worker_cleanup(); @@ -833,6 +856,8 @@ static int mcast_source_receive_batch(mcast_source_t *source, int fd, int64_t no #endif void mcast_worker_cleanup(void) { + buffer_ref_put(receive_single_packet); + receive_single_packet = NULL; #if defined(__linux__) || defined(__FreeBSD__) for (int i = 0; i < RECEIVE_BATCH; i++) { buffer_ref_put(receive_packets[i]); @@ -855,13 +880,14 @@ int mcast_session_handle_event(mcast_session_t *session, int fd, int64_t now) { /* Drain to EAGAIN for edge-triggered pollers, including on pool exhaustion. * All subscribers are detached by the worker outside this delivery loop. */ for (;;) { - buffer_ref_t *packet = fd == source->sock ? buffer_pool_alloc() : NULL; + if (fd == source->sock && !receive_single_packet) + receive_single_packet = buffer_pool_alloc(); + buffer_ref_t *packet = fd == source->sock ? receive_single_packet : NULL; uint8_t discard[BUFFER_POOL_BUFFER_SIZE]; void *data = packet ? packet->data : discard; ssize_t len = recv(fd, data, BUFFER_POOL_BUFFER_SIZE, 0); if (len < 0) { int recv_errno = errno; - buffer_ref_put(packet); if (recv_errno == EINTR) continue; if (recv_errno != EAGAIN && recv_errno != EWOULDBLOCK) { @@ -872,13 +898,13 @@ int mcast_session_handle_event(mcast_session_t *session, int fd, int64_t now) { } if (fd == source->sock) { mcast_source_process_packet(source, packet, (size_t)len, now); + mcast_receive_recycle(&receive_single_packet); } else { for (mcast_session_t *subscriber = source->subscribers; subscriber; subscriber = subscriber->next) { if (!subscriber->failed && subscriber->ctx->conn->state != CONN_CLOSING) fec_process_packet(&subscriber->ctx->fec, data, (int)len); } } - buffer_ref_put(packet); } return 0; } diff --git a/src/poller.h b/src/poller.h index 717d96bc..53a6bb3b 100644 --- a/src/poller.h +++ b/src/poller.h @@ -2,14 +2,14 @@ #define POLLER_H /** - * Platform-agnostic event polling abstraction (edge-triggered). + * Platform-agnostic event polling abstraction (edge-triggered by default). * * Provides a unified API over platform-specific event notification mechanisms: * - Linux: epoll with EPOLLET (edge-triggered) * - macOS: kqueue with EV_CLEAR (edge-triggered) * - Windows: (future) IOCP * - * All handlers must drain socket data (read/write until EAGAIN) because + * Edge-triggered handlers must drain socket data (read/write until EAGAIN) because * edge-triggered pollers only notify on state transitions, not while * data remains available. */ @@ -22,6 +22,8 @@ #define POLLER_ERR 0x004 /* Error condition */ #define POLLER_HUP 0x008 /* Hangup (peer closed) */ #define POLLER_RDHUP 0x010 /* Read half of connection closed */ +/* Registration option: keep reporting readiness while data remains. */ +#define POLLER_LEVEL 0x020 /* Event structure returned by poller_wait() */ typedef struct { diff --git a/src/poller_epoll.c b/src/poller_epoll.c index 0eac4f7c..a239d9ab 100644 --- a/src/poller_epoll.c +++ b/src/poller_epoll.c @@ -11,7 +11,7 @@ void poller_close(int pfd) { close(pfd); } int poller_add(int pfd, int fd, uint32_t events) { struct epoll_event ev; - ev.events = EPOLLET; /* Edge-triggered mode */ + ev.events = events & POLLER_LEVEL ? 0 : EPOLLET; ev.data.fd = fd; if (events & POLLER_IN) ev.events |= EPOLLIN; @@ -28,7 +28,7 @@ int poller_add(int pfd, int fd, uint32_t events) { int poller_mod(int pfd, int fd, uint32_t events) { struct epoll_event ev; - ev.events = EPOLLET; /* Edge-triggered mode */ + ev.events = events & POLLER_LEVEL ? 0 : EPOLLET; ev.data.fd = fd; if (events & POLLER_IN) ev.events |= EPOLLIN; diff --git a/src/poller_kqueue.c b/src/poller_kqueue.c index 905927f4..c2014b59 100644 --- a/src/poller_kqueue.c +++ b/src/poller_kqueue.c @@ -15,19 +15,20 @@ void poller_close(int pfd) { close(pfd); } int poller_add(int pfd, int fd, uint32_t events) { struct kevent changes[2]; int nchanges = 0; + unsigned short flags = EV_ADD | (events & POLLER_LEVEL ? 0 : EV_CLEAR); if (events & POLLER_IN) { - EV_SET(&changes[nchanges], fd, EVFILT_READ, EV_ADD | EV_CLEAR, 0, 0, NULL); + EV_SET(&changes[nchanges], fd, EVFILT_READ, flags, 0, 0, NULL); nchanges++; } if (events & POLLER_OUT) { - EV_SET(&changes[nchanges], fd, EVFILT_WRITE, EV_ADD | EV_CLEAR, 0, 0, NULL); + EV_SET(&changes[nchanges], fd, EVFILT_WRITE, flags, 0, 0, NULL); nchanges++; } if (nchanges == 0) { /* At minimum, add a read filter so the fd is tracked */ - EV_SET(&changes[0], fd, EVFILT_READ, EV_ADD | EV_CLEAR, 0, 0, NULL); + EV_SET(&changes[0], fd, EVFILT_READ, flags, 0, 0, NULL); nchanges = 1; } @@ -37,20 +38,21 @@ int poller_add(int pfd, int fd, uint32_t events) { int poller_mod(int pfd, int fd, uint32_t events) { struct kevent changes[4]; int nchanges = 0; + unsigned short flags = EV_ADD | (events & POLLER_LEVEL ? 0 : EV_CLEAR); /* * kqueue doesn't have a modify operation - we add/delete filters. * EV_ADD on an existing filter updates it; EV_DELETE removes it. */ if (events & POLLER_IN) { - EV_SET(&changes[nchanges], fd, EVFILT_READ, EV_ADD | EV_CLEAR, 0, 0, NULL); + EV_SET(&changes[nchanges], fd, EVFILT_READ, flags, 0, 0, NULL); } else { EV_SET(&changes[nchanges], fd, EVFILT_READ, EV_DELETE, 0, 0, NULL); } nchanges++; if (events & POLLER_OUT) { - EV_SET(&changes[nchanges], fd, EVFILT_WRITE, EV_ADD | EV_CLEAR, 0, 0, NULL); + EV_SET(&changes[nchanges], fd, EVFILT_WRITE, flags, 0, 0, NULL); } else { EV_SET(&changes[nchanges], fd, EVFILT_WRITE, EV_DELETE, 0, 0, NULL); } diff --git a/src/status.c b/src/status.c index c0f5e46f..8bb37085 100644 --- a/src/status.c +++ b/src/status.c @@ -18,6 +18,8 @@ /* Global pointer to shared memory */ status_shared_t *status_shared = NULL; +/* Private to each process; refreshed in the child's post-fork initialization. */ +static uint32_t status_process_pid; /* Path for shared memory file in /tmp */ static char shm_path[256] = {0}; @@ -311,6 +313,7 @@ static void append_log_entry(int64_t timestamp, loglevel_t level, const char *me int status_init(void) { int fd; + status_process_pid = (uint32_t)getpid(); /* PID-keyed path: EEXIST can only be a stale leftover from a prior instance * with the same PID (no live process can hold our PID in this namespace), @@ -560,6 +563,7 @@ void status_cleanup(void) { } void status_worker_init(void) { + status_process_pid = (uint32_t)getpid(); if (log_event_recv_fd >= 0) { close(log_event_recv_fd); log_event_recv_fd = -1; @@ -648,7 +652,7 @@ void status_unregister_client(int status_index) { return; client_stats_t *client = &status_shared->clients[status_index]; - if (atomic_load_explicit(&client->owner_pid, memory_order_acquire) != (uint32_t)getpid()) + if (atomic_load_explicit(&client->owner_pid, memory_order_acquire) != status_process_pid) return; if (!atomic_exchange_explicit(&client->active, 0, memory_order_acq_rel)) @@ -761,7 +765,7 @@ void status_update_client_bytes(int status_index, uint64_t bytes_sent, uint32_t return; client_stats_t *client = &status_shared->clients[status_index]; - if (atomic_load_explicit(&client->owner_pid, memory_order_acquire) != (uint32_t)getpid() || + if (atomic_load_explicit(&client->owner_pid, memory_order_acquire) != status_process_pid || !atomic_load_explicit(&client->active, memory_order_acquire)) return; @@ -779,7 +783,7 @@ void status_update_client_state(int status_index, client_state_type_t state) { return; client_stats_t *client = &status_shared->clients[status_index]; - if (atomic_load_explicit(&client->owner_pid, memory_order_acquire) != (uint32_t)getpid() || + if (atomic_load_explicit(&client->owner_pid, memory_order_acquire) != status_process_pid || !atomic_load_explicit(&client->active, memory_order_acquire)) return; @@ -801,7 +805,7 @@ void status_update_client_queue(int status_index, size_t queue_bytes, size_t que return; client_stats_t *client = &status_shared->clients[status_index]; - if (atomic_load_explicit(&client->owner_pid, memory_order_acquire) != (uint32_t)getpid() || + if (atomic_load_explicit(&client->owner_pid, memory_order_acquire) != status_process_pid || !atomic_load_explicit(&client->active, memory_order_acquire)) return; From ccbcbbf55361541022465c893d9a2a82a50b26ac Mon Sep 17 00:00:00 2001 From: Stackie Jia Date: Sun, 6 Sep 2026 21:40:36 +0800 Subject: [PATCH 14/19] perf(stream): defer queue snapshots and private reorder allocation --- docs/en/reference/benchmark.md | 4 ++++ docs/reference/benchmark.md | 4 ++++ e2e/test_multicast_shared.py | 9 +++++++++ src/connection.c | 12 +----------- src/connection.h | 4 ++++ src/multicast.c | 9 +++++++++ src/stream.c | 6 ++++-- src/worker.c | 1 + 8 files changed, 36 insertions(+), 13 deletions(-) diff --git a/docs/en/reference/benchmark.md b/docs/en/reference/benchmark.md index a201d00b..569afa42 100644 --- a/docs/en/reference/benchmark.md +++ b/docs/en/reference/benchmark.md @@ -77,6 +77,8 @@ Values are the means of per-trial average CPU utilization, with the minimum and Connections allocate RTSP or HTTP proxy state only for the protocol they use. FEC group tables are allocated when recovery groups need to be stored. HTTP input buffers and parsed requests use separate anonymous memory mappings: input storage is released after parsing and routing, while ordinary media streams release parsed request data after generating response headers. HTTP proxies retain the request headers and body they still use. Temporary request pages can return directly to the operating system instead of remaining in the heap alongside long-lived connections. +Ordinary shared multicast clients use the source's reorder window instead of allocating unused private arrays. Private windows are allocated for RTSP, FCC, snapshots, or FEC processing. When FEC appears during a stream, both existing clients and later subscribers receive their own windows. + The packet pool starts with 128 buffers and grows in increments of 128. The control pool starts with 16 and grows in increments of 16. The worker periodically reclaims completely idle segments while retaining a base capacity. Client queue budgets are calculated separately from the initial allocation, so reducing initial memory does not reduce the existing buffering allowance. ### Shared Multicast Subscriptions Within Each Worker @@ -107,6 +109,8 @@ Writes enter a local worker queue first. The worker subscribes to kernel writabl Client ownership checks in status tracking use a process-local cached PID, refreshed after every fork. This removes repeated `getpid()` calls from queue and send-statistics updates. +Queue limits, counters, and high-water marks still update on every queue operation, while publication to shared status memory runs on the worker's 100 ms timer. This reduces repeated shared-memory writes and synchronization during batch enqueueing and sending. The status page displays the most recently published queue snapshot. + ### Immutable Batch Snapshots Platforms supporting memory-file sealing use `memfd_create` to create anonymous memory files for shared batches. Each file is written once, sealed, and sent to multiple clients through `sendfile`, reusing the same kernel pages. This path applies only when multiple clients share a nearly full batch. Ordinary memory buffers continue to use `sendmsg`. diff --git a/docs/reference/benchmark.md b/docs/reference/benchmark.md index 6007b1d9..aaeba904 100644 --- a/docs/reference/benchmark.md +++ b/docs/reference/benchmark.md @@ -77,6 +77,8 @@ msd_lite 保留上游示例的 48 KiB 接收水位、64 KiB 发送水位、1 MiB 连接只为实际使用的协议分配 RTSP 或 HTTP 代理状态,FEC 分组表在需要保存恢复分组时才分配。HTTP 输入缓冲和已解析请求使用独立的匿名内存映射:输入缓冲在解析和路由完成后释放,普通媒体流的请求数据在响应头生成后释放。HTTP 代理保留其仍在使用的请求头和请求体。临时请求页可直接交还操作系统,避免长期连接将这些已空闲的页留在堆中。 +普通共享组播客户端使用源级重排窗口,不再各自分配闲置的重排数组。RTSP、FCC、截图或 FEC 独立处理路径才分配客户端窗口;流中途启用 FEC 时,已有客户端和后续加入的客户端都会取得各自的窗口。 + 包缓冲池初始分配 128 个缓冲,按 128 个扩展;控制缓冲池初始分配 16 个,按 16 个扩展。worker 定时回收完全空闲的分段,保留必要的基础容量。客户端队列的逻辑预算与初始预分配量分开计算,减少初始内存不改变原有的缓冲余量。 ### 每个 worker 共享组播订阅 @@ -107,6 +109,8 @@ Buffer 层在原有 1536 字节包缓冲池和控制缓冲池之外,增加按 状态统计的客户端归属校验使用进程内缓存的 PID,并在每次 fork 后刷新,省去队列更新和发送统计中重复的 `getpid()` 调用。 +队列限额、计数和高水位仍在每次队列操作时更新,向共享状态区发布则集中到 worker 的 100 ms 定时检查中。这样可减少每批数据入队、发送时重复执行的共享内存写入和同步操作;状态页显示最近一次发布的队列快照。 + ### 不可修改的批次快照 支持内存文件封存的平台会通过 `memfd_create` 为共享批次建立匿名内存文件,写入一次后封存,再由多个客户端通过 `sendfile` 发送同一份内核数据页。只在多个客户端共享接近满容量的批次时采用该路径;普通内存发送仍通过 `sendmsg` 完成。 diff --git a/e2e/test_multicast_shared.py b/e2e/test_multicast_shared.py index 4e4644d6..f928e474 100644 --- a/e2e/test_multicast_shared.py +++ b/e2e/test_multicast_shared.py @@ -123,6 +123,15 @@ def test_inband_fec_preserves_shared_reorder_window(shared_source_r2h): _wait_log(r2h, "FEC: Activated", count=2) for _ in range(4): seqs = [_read_contiguous_rtp(client, seq) for client, seq in zip((first, second), seqs, strict=True)] + # The source now delivers private packets. A new subscriber must + # initialize its own reorder window even without a configured FEC port. + with _stream(r2h, path) as late: + late_seq = None + for _ in range(4): + seqs = [ + _read_contiguous_rtp(client, seq) for client, seq in zip((first, second), seqs, strict=True) + ] + late_seq = _read_contiguous_rtp(late, late_seq) finally: sender.stop() diff --git a/src/connection.c b/src/connection.c index d62a2a87..2ba5db86 100644 --- a/src/connection.c +++ b/src/connection.c @@ -407,7 +407,7 @@ static inline void connection_record_drop(connection_t *c, size_t len) { c->dropped_bytes += len; } -static void connection_report_queue(connection_t *c) { +void connection_report_queue(connection_t *c) { if (c->status_index < 0) return; @@ -729,7 +729,6 @@ connection_write_status_t connection_handle_write(connection_t *c) { if (!c->send_queue.head) { connection_watch_writable(c, 0); - connection_report_queue(c); if (c->state == CONN_CLOSING) return CONNECTION_WRITE_CLOSED; return CONNECTION_WRITE_IDLE; @@ -749,14 +748,12 @@ connection_write_status_t connection_handle_write(connection_t *c) { if (ret < 0 && ret != -2) { c->state = CONN_CLOSING; connection_watch_writable(c, 0); - connection_report_queue(c); return CONNECTION_WRITE_CLOSED; } if (ret == -2) { /* Subscribe only when a real send needs to wait for the socket. */ connection_watch_writable(c, 1); - connection_report_queue(c); if (total_sent > 0) stream_on_client_drain(&c->stream); return CONNECTION_WRITE_BLOCKED; @@ -765,7 +762,6 @@ connection_write_status_t connection_handle_write(connection_t *c) { if (!c->send_queue.head) { if (c->state == CONN_CLOSING) { connection_watch_writable(c, 0); - connection_report_queue(c); return CONNECTION_WRITE_CLOSED; } /* resume() may synchronously queue more output. Schedule it locally. */ @@ -774,7 +770,6 @@ connection_write_status_t connection_handle_write(connection_t *c) { stream_on_client_drain(&c->stream); if (c->send_queue.head) connection_schedule_write(c); - connection_report_queue(c); return CONNECTION_WRITE_IDLE; } @@ -782,7 +777,6 @@ connection_write_status_t connection_handle_write(connection_t *c) { if (total_sent >= 256 * 1024) { connection_watch_writable(c, 0); stream_on_client_drain(&c->stream); - connection_report_queue(c); return CONNECTION_WRITE_PENDING; } @@ -793,7 +787,6 @@ connection_write_status_t connection_handle_write(connection_t *c) { /* Queue still has data but we could not make progress. Wait for readiness. */ connection_watch_writable(c, 1); - connection_report_queue(c); if (total_sent > 0) stream_on_client_drain(&c->stream); return CONNECTION_WRITE_BLOCKED; @@ -1309,7 +1302,6 @@ int connection_queue_buffer(connection_t *c, buffer_ref_t *buf_ref) { buf_ref->data_size, c->fd, queued_bytes, limit_bytes, (unsigned long long)c->dropped_packets); } - connection_report_queue(c); return -1; } @@ -1324,8 +1316,6 @@ int connection_queue_buffer(connection_t *c, buffer_ref_t *buf_ref) { if (c->send_queue.num_queued > c->queue_buffers_highwater) c->queue_buffers_highwater = c->send_queue.num_queued; - connection_report_queue(c); - /* Batching optimization: Only enable EPOLLOUT when flush threshold is reached * Benefits: * - Reduces sendmsg() syscall overhead (fewer calls) diff --git a/src/connection.h b/src/connection.h index 9fcf62ea..77309678 100644 --- a/src/connection.h +++ b/src/connection.h @@ -107,6 +107,10 @@ void connection_cleanup(connection_t *c); /* Release parsed request storage once no asynchronous handler borrows it. */ void connection_release_request(connection_t *c); +/* Publish queue statistics on the worker's periodic tick. Queue limits, + * counters and high-water marks are maintained locally on every operation. */ +void connection_report_queue(connection_t *c); + /** * Handle read event on client connection * @param c Connection diff --git a/src/multicast.c b/src/multicast.c index 38cd809a..28d72c8b 100644 --- a/src/multicast.c +++ b/src/multicast.c @@ -518,6 +518,15 @@ int mcast_session_join(mcast_session_t *session, stream_context_t *ctx) { break; } + /* Ordinary subscribers use the source's reorder window. Allocate a private + * window only for packet delivery, including late joins after in-band FEC. */ + int private_output = + (source ? !source->shared_output : service->fec_port > 0) || ctx->snapshot.initialized || ctx->fcc.initialized; + if (private_output && !ctx->reorder.initialized && rtp_reorder_init(&ctx->reorder, service->fec_port > 0) < 0) { + logger(LOG_ERROR, "Multicast: Failed to initialize private RTP reorder buffer"); + return -1; + } + if (!source) { source = calloc(1, sizeof(*source)); if (!source) diff --git a/src/stream.c b/src/stream.c index 6b668557..b7b46411 100644 --- a/src/stream.c +++ b/src/stream.c @@ -610,8 +610,10 @@ int stream_context_init_for_worker(stream_context_t *ctx, connection_t *conn, se } } - /* Initialize RTP reorder and FEC (common to all RTP-based services) */ - if (rtp_reorder_init(&ctx->reorder, service->fec_port > 0) < 0) { + /* RTSP and FCC can receive private media before joining multicast. + * Multicast decides at attachment whether a private window is needed. */ + if ((service->service_type == SERVICE_RTSP || service->fcc_addr) && + rtp_reorder_init(&ctx->reorder, service->fec_port > 0) < 0) { logger(LOG_ERROR, "Failed to initialize RTP reorder buffer"); return -1; } diff --git a/src/worker.c b/src/worker.c index 7e48d3a9..8b8941f0 100644 --- a/src/worker.c +++ b/src/worker.c @@ -564,6 +564,7 @@ int worker_run_event_loop(int *listen_sockets, int num_sockets, int notif_fd) { connection_t *c = conn_head; while (c) { connection_t *next = c->next; /* Save next pointer before potential cleanup */ + connection_report_queue(c); if (c->streaming) { if (stream_tick(&c->stream, now) < 0) { /* Send 503 if headers not sent yet (no data ever arrived) */ From 68338df23eff289c68649be2898fe19c0aa44ae3 Mon Sep 17 00:00:00 2001 From: Stackie Jia Date: Sun, 6 Sep 2026 22:46:54 +0800 Subject: [PATCH 15/19] perf(multicast): coalesce receives with bounded readiness fallback --- docs/en/reference/benchmark.md | 4 +- docs/reference/benchmark.md | 4 +- e2e/test_multicast_shared.py | 47 +++++++++++ src/multicast.c | 140 +++++++++++++++++++++++++++++---- src/multicast.h | 4 + src/poller.h | 2 + src/poller_epoll.c | 4 + src/poller_kqueue.c | 8 +- src/worker.c | 4 +- 9 files changed, 195 insertions(+), 22 deletions(-) diff --git a/docs/en/reference/benchmark.md b/docs/en/reference/benchmark.md index 569afa42..32ebf872 100644 --- a/docs/en/reference/benchmark.md +++ b/docs/en/reference/benchmark.md @@ -103,7 +103,9 @@ Queue limits now charge the backing buffer capacity instead of assuming “buffe Platforms supporting `recvmmsg` receive up to 16 datagrams per call. The worker reuses receive descriptors and unconsumed packet buffers. After processing, a packet buffer with no other references is reused for the next receive. If a reorder window, FEC state, or send queue still holds a reference, reception uses another buffer to avoid overwriting pending data. Data arrives directly in pool buffers, avoiding an additional copy after reception; platforms without batch reception receive one packet at a time. Once initial RTP reordering is complete, an expected packet can be delivered directly when the window is empty and FEC is disabled, avoiding insertion into and removal from reorder slots. -Sockets using batch reception use level-triggered notifications. A short batch can return to the event loop without an extra receive call to confirm that the socket is empty. Even if an interruption caused the short read, remaining data triggers another notification. Each callback receives at most 256 datagrams so continuous multicast traffic cannot occupy the event loop indefinitely. +The main multicast socket is read immediately on its first readiness notification. Subsequent reception can coalesce up to 1–2 ms of work, depending on the actual receive-buffer capacity and the number of datagrams received, reducing event wakeups for continuous small packets. A 1 ms wait requires a system-reported receive buffer of at least 128 KiB; 2 ms requires at least 256 KiB and a low packet count. System scheduling also affects the actual interval. Readiness notifications are paused during deferred reception and rearmed after the socket is drained. The worker scans only sources with pending receive tasks, and removes a source's task when its last subscriber leaves. + +Small receive buffers, or a failed capacity query, use level-triggered notifications. A read reaching a conservative packet-count threshold derived from buffer capacity also restores immediate reception until an idle gap of at least 100 ms permits another coalescing attempt. With level triggering, a short batch can return because remaining data still generates notifications. Deferred reception must confirm that the socket is drained so an interrupted short read cannot strand data. Each callback receives at most 256 main multicast datagrams so continuous traffic cannot occupy the event loop indefinitely. These strategies are selected automatically and require no additional configuration. Writes enter a local worker queue first. The worker subscribes to kernel writable events only when a socket cannot make further progress, reducing per-batch event registration changes. Each connection sends at most 256 KiB per turn, and each event-loop iteration processes at most 128 write tasks. Remaining tasks stay queued so reception, timers, and other clients can also run. diff --git a/docs/reference/benchmark.md b/docs/reference/benchmark.md index aaeba904..f03cf64f 100644 --- a/docs/reference/benchmark.md +++ b/docs/reference/benchmark.md @@ -103,7 +103,9 @@ Buffer 层在原有 1536 字节包缓冲池和控制缓冲池之外,增加按 支持 `recvmmsg` 的平台一次接收最多 16 个数据报,worker 复用接收描述符和未消耗的包缓冲。处理完且没有其他引用的包缓冲直接用于下一次接收;重排、FEC 或发送队列仍持有引用时,使用另一个缓冲,避免覆盖待处理数据。数据直接进入缓冲池,不增加一次接收后的复制;不支持批量接收时使用逐包接收。对于已完成起始重排、序号连续且窗口为空的 RTP 流,未启用 FEC 时直接交付负载,省去重排槽位的插入和移除。 -批量接收 socket 使用水平触发通知,读到不足一批时即可返回事件循环,省去用于确认 socket 已读空的额外接收调用。即使短读由中断造成,剩余数据也会再次触发通知。每次回调最多接收 256 个数据报,使持续到来的组播数据不会长期占用事件循环。 +主组播 socket 首次就绪时立即读取;后续按实际接收缓冲容量和每次收到的数据报数量,合并最多 1–2 ms 内的接收工作,减少持续小包带来的事件唤醒。系统报告的接收缓冲至少为 128 KiB 时才允许等待 1 ms,至少为 256 KiB 且包量较小时才允许 2 ms;实际间隔还受系统调度影响。延后读取期间暂停该 socket 的就绪通知,读空后重新启用。worker 只遍历仍有待处理接收任务的源,最后一个订阅者离开时同时移除其待处理任务。 + +接收缓冲较小或无法查询容量时使用水平触发通知。单次读取达到按缓冲容量计算的保守包量阈值时,也恢复即时读取,直到出现至少 100 ms 的空闲间隔后再尝试合并。水平触发下读到不足一批即可返回,剩余数据仍会通知;延后读取路径则必须确认读空,避免中断造成的短读留下未处理数据。每次回调最多接收 256 个主组播数据报,使持续到来的数据不会长期占用事件循环。这些策略自动选择,无需额外配置。 发送任务先进入 worker 内部队列,只有 socket 暂时无法继续写入时才订阅内核可写事件,减少每个批次的事件监听切换。每个连接单次最多发送 256 KiB,每轮最多处理 128 个发送任务,剩余任务继续排队,使接收、定时器和其他客户端都能得到处理。 diff --git a/e2e/test_multicast_shared.py b/e2e/test_multicast_shared.py index f928e474..2db1364b 100644 --- a/e2e/test_multicast_shared.py +++ b/e2e/test_multicast_shared.py @@ -368,3 +368,50 @@ def test_fcc_clients_share_existing_multicast(shared_source_r2h, protocol): finally: fcc.stop() sender.stop() + + +@pytest.mark.parametrize("receive_buffer,first_burst", [(65536, 12), (524288, 12), (524288, 96)]) +def test_shared_source_resumes_after_idle_bursts(r2h_binary, receive_buffer, first_burst): + """Idle gaps and a busy initial burst must not strand either subscriber.""" + r2h = R2HProcess( + r2h_binary, + find_free_port(), + extra_args=["-v", "4", "-w", "1", "-m", "10", "-r", LOOPBACK_IF, "-B", str(receive_buffer)], + ) + r2h.start() + port = find_free_udp_port() + try: + with ExitStack() as stack: + clients = [ + stack.enter_context(closing(http.client.HTTPConnection("127.0.0.1", r2h.port, timeout=3))) + for _ in range(2) + ] + for client in clients: + client.request("GET", f"/rtp/{MCAST_ADDR}:{port}") + _wait_log(r2h, "Subscriber attached", count=2) + upstream = stack.enter_context(closing(socket.socket(socket.AF_INET, socket.SOCK_DGRAM))) + upstream.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton("127.0.0.1")) + upstream.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_LOOP, 1) + responses = [] + seq = 0 + for cycle in range(4): + expected = bytearray() + begin = time.monotonic() + burst = first_burst if cycle == 0 else 12 + for _ in range(burst): + ts = b"\x47\x1f\xff\x10" + struct.pack("!H", seq) + b"\xff" * 182 + payload = ts * 7 + expected.extend(payload) + upstream.sendto(make_rtp_packet(seq, seq * 3600, payload=payload), (MCAST_ADDR, port)) + seq += 1 + if burst == 12: + time.sleep(0.001) + if not responses: + responses = [stack.enter_context(closing(client.getresponse())) for client in clients] + assert all(response.status == 200 for response in responses) + for response in responses: + assert response.read(len(expected)) == expected + assert time.monotonic() - begin < 1.5 + assert r2h.read_log().count("Multicast: Successfully joined group") == 1 + finally: + r2h.stop() diff --git a/src/multicast.c b/src/multicast.c index 28d72c8b..2949fc2d 100644 --- a/src/multicast.c +++ b/src/multicast.c @@ -396,10 +396,30 @@ struct mcast_source_s { int packet_type; int batch_packet_type; int64_t batch_since; + int64_t next_receive; + int64_t last_receive; + int receive_delay_max; + int receive_coalesced; + unsigned int receive_packet_limit; + mcast_source_t *receive_next; + mcast_source_t **receive_link; mcast_source_t *next; }; static mcast_source_t *mcast_sources; +static mcast_source_t *receive_head; + +/* Only active coalesced sources enter this list. Idle channels add no work to + * the worker's timeout calculation, and destruction unlinks in constant time. */ +static void mcast_receive_unlink(mcast_source_t *source) { + if (!source->receive_link) + return; + *source->receive_link = source->receive_next; + if (source->receive_next) + source->receive_next->receive_link = source->receive_link; + source->receive_link = NULL; + source->receive_next = NULL; +} static void mcast_source_flush(mcast_source_t *source); static int mcast_source_append(void *arg, buffer_ref_t *packet); @@ -426,6 +446,7 @@ static int mcast_address_equal(const struct addrinfo *a, const struct addrinfo * } static void mcast_source_free(mcast_source_t *source) { + mcast_receive_unlink(source); if (source->sock >= 0) worker_cleanup_socket_from_epoll(source->epoll_fd, source->sock); if (source->fec_sock >= 0) @@ -556,11 +577,18 @@ int mcast_session_join(mcast_session_t *session, stream_context_t *ctx) { source->reorder.deliver_arg = source; } source->sock = join_mcast_group(source->service, 0); - uint32_t read_events = POLLER_IN; -#if defined(__linux__) || defined(__FreeBSD__) - /* Batched reads can yield without a final empty receive. */ - read_events |= POLLER_LEVEL; -#endif + int receive_bytes = 0; + socklen_t receive_length = sizeof(receive_bytes); + if (source->sock >= 0 && getsockopt(source->sock, SOL_SOCKET, SO_RCVBUF, &receive_bytes, &receive_length) == 0) { + source->receive_delay_max = receive_bytes >= 256 * 1024 ? 2 : receive_bytes >= 128 * 1024 ? 1 : 0; + /* Reserve ample headroom for kernel packet overhead and scheduling + * jitter. A busy source switches back to readiness-driven reads. */ + source->receive_packet_limit = (unsigned int)receive_bytes / (BUFFER_POOL_BUFFER_SIZE * 8); + if (source->receive_packet_limit > 32) + source->receive_packet_limit = 32; + } + source->receive_coalesced = source->receive_delay_max != 0; + uint32_t read_events = POLLER_IN | (source->receive_coalesced ? POLLER_ONESHOT : POLLER_LEVEL); if (source->sock < 0 || poller_add(ctx->epoll_fd, source->sock, read_events) < 0) { mcast_source_free(source); return -1; @@ -852,15 +880,15 @@ static int mcast_source_receive_batch(mcast_source_t *source, int fd, int64_t no mcast_receive_recycle(&packets[i]); } received += (unsigned int)count; - /* Usually a short read already exhausted the socket. Level triggering - * also covers interruption after partial progress: unread datagrams stay - * ready. Yield on sustained traffic so output and timers get a turn. */ - if (count < RECEIVE_BATCH || received >= 256) + /* A short receive may have been interrupted with data still queued. + * Coalesced sources drain to EAGAIN before sleeping; level-triggered + * sources can yield because remaining data stays ready. */ + if ((!source->receive_coalesced && count < RECEIVE_BATCH) || received >= 256) break; } if (fallback) mcast_worker_cleanup(); - return fallback; + return fallback ? fallback : (int)received; } #endif @@ -876,15 +904,15 @@ void mcast_worker_cleanup(void) { #endif } -int mcast_session_handle_event(mcast_session_t *session, int fd, int64_t now) { - mcast_source_t *source = session->source; - if (!source) - return -1; - +static int mcast_source_receive(mcast_source_t *source, int fd, int64_t now) { #if defined(__linux__) || defined(__FreeBSD__) - if (fd == source->sock && mcast_source_receive_batch(source, fd, now) == 0) - return 0; + if (fd == source->sock) { + int count = mcast_source_receive_batch(source, fd, now); + if (count >= 0) + return count; + } #endif + int received = 0; /* Drain to EAGAIN for edge-triggered pollers, including on pool exhaustion. * All subscribers are detached by the worker outside this delivery loop. */ @@ -908,6 +936,8 @@ int mcast_session_handle_event(mcast_session_t *session, int fd, int64_t now) { if (fd == source->sock) { mcast_source_process_packet(source, packet, (size_t)len, now); mcast_receive_recycle(&receive_single_packet); + if (++received >= 256) + break; } else { for (mcast_session_t *subscriber = source->subscribers; subscriber; subscriber = subscriber->next) { if (!subscriber->failed && subscriber->ctx->conn->state != CONN_CLOSING) @@ -915,9 +945,85 @@ int mcast_session_handle_event(mcast_session_t *session, int fd, int64_t now) { } } } + return received; +} + +static int mcast_source_rearm(mcast_source_t *source, int coalesced) { + if (poller_mod(source->epoll_fd, source->sock, POLLER_IN | (coalesced ? POLLER_ONESHOT : POLLER_LEVEL)) < 0) { + logger(LOG_ERROR, "Multicast: Cannot rearm receive socket: %s", strerror(errno)); + source->failed = 1; + return -1; + } + source->receive_coalesced = coalesced; + return 0; +} + +static void mcast_source_schedule_receive(mcast_source_t *source, int count, int64_t now) { + source->last_receive = now; + if (source->failed || !source->receive_coalesced) { + source->next_receive = 0; + } else if ((unsigned int)count >= source->receive_packet_limit) { + /* At high packet rates even a millisecond can be too long. Keep readiness + * enabled until an idle gap, rather than repeatedly probing with a delay. */ + source->next_receive = 0; + mcast_source_rearm(source, 0); + } else if (count > 0) { + /* The first notification reads immediately. Subsequent reads can collect + * a short burst; only low packet rates and enough storage permit 2 ms. */ + int delay = source->next_receive && count < 16 ? source->receive_delay_max : 1; + source->next_receive = now + delay; + } else { + source->next_receive = 0; + mcast_source_rearm(source, 1); + } + if (!source->next_receive) { + mcast_receive_unlink(source); + } else if (!source->receive_link) { + source->receive_next = receive_head; + source->receive_link = &receive_head; + if (receive_head) + receive_head->receive_link = &source->receive_next; + receive_head = source; + } +} + +int mcast_session_handle_event(mcast_session_t *session, int fd, int64_t now) { + mcast_source_t *source = session->source; + if (!source) + return -1; + if (fd == source->sock && !source->failed && !source->receive_coalesced && source->receive_delay_max && + now - source->last_receive >= 100) { + /* Re-enable after an idle gap. Leave this ready socket to the new one-shot + * notification so there is no enabled filter during deferred reads. */ + source->last_receive = now; + return mcast_source_rearm(source, 1); + } + int count = mcast_source_receive(source, fd, now); + if (fd == source->sock) + mcast_source_schedule_receive(source, count, now); return 0; } +int mcast_worker_timeout(int64_t now, int timeout) { + for (mcast_source_t *source = receive_head; source; source = source->receive_next) { + int delay = source->next_receive > now ? (int)(source->next_receive - now) : 0; + if (timeout < 0 || delay < timeout) + timeout = delay; + } + return timeout; +} + +void mcast_worker_receive(int64_t now) { + for (mcast_source_t *source = receive_head, *next; source; source = next) { + next = source->receive_next; + if (source->next_receive <= now) { + mcast_receive_unlink(source); + int count = mcast_source_receive(source, source->sock, now); + mcast_source_schedule_receive(source, count, now); + } + } +} + int mcast_session_tick(mcast_session_t *session, int64_t now) { if (!session || !session->initialized || !session->source) return 0; diff --git a/src/multicast.h b/src/multicast.h index c48f74f9..2e104ecd 100644 --- a/src/multicast.h +++ b/src/multicast.h @@ -64,5 +64,9 @@ int mcast_session_tick(mcast_session_t *session, int64_t now); /* Release worker receive scratch buffers before destroying the buffer pools. */ void mcast_worker_cleanup(void); +/* Bound a poller timeout by pending receives (-1 means no existing deadline). */ +int mcast_worker_timeout(int64_t now, int timeout); +/* Service each due source once; subscriber teardown stays in the worker. */ +void mcast_worker_receive(int64_t now); #endif /* __MULTICAST_H__ */ diff --git a/src/poller.h b/src/poller.h index 53a6bb3b..4b2e823b 100644 --- a/src/poller.h +++ b/src/poller.h @@ -24,6 +24,8 @@ #define POLLER_RDHUP 0x010 /* Read half of connection closed */ /* Registration option: keep reporting readiness while data remains. */ #define POLLER_LEVEL 0x020 +/* Disable notifications after delivery; poller_mod() rearms the filter. */ +#define POLLER_ONESHOT 0x040 /* Event structure returned by poller_wait() */ typedef struct { diff --git a/src/poller_epoll.c b/src/poller_epoll.c index a239d9ab..b1b9edff 100644 --- a/src/poller_epoll.c +++ b/src/poller_epoll.c @@ -12,6 +12,8 @@ void poller_close(int pfd) { close(pfd); } int poller_add(int pfd, int fd, uint32_t events) { struct epoll_event ev; ev.events = events & POLLER_LEVEL ? 0 : EPOLLET; + if (events & POLLER_ONESHOT) + ev.events |= EPOLLONESHOT; ev.data.fd = fd; if (events & POLLER_IN) ev.events |= EPOLLIN; @@ -29,6 +31,8 @@ int poller_add(int pfd, int fd, uint32_t events) { int poller_mod(int pfd, int fd, uint32_t events) { struct epoll_event ev; ev.events = events & POLLER_LEVEL ? 0 : EPOLLET; + if (events & POLLER_ONESHOT) + ev.events |= EPOLLONESHOT; ev.data.fd = fd; if (events & POLLER_IN) ev.events |= EPOLLIN; diff --git a/src/poller_kqueue.c b/src/poller_kqueue.c index c2014b59..cb1b85d3 100644 --- a/src/poller_kqueue.c +++ b/src/poller_kqueue.c @@ -15,7 +15,9 @@ void poller_close(int pfd) { close(pfd); } int poller_add(int pfd, int fd, uint32_t events) { struct kevent changes[2]; int nchanges = 0; - unsigned short flags = EV_ADD | (events & POLLER_LEVEL ? 0 : EV_CLEAR); + unsigned short flags = EV_ADD | EV_ENABLE | (events & POLLER_LEVEL ? 0 : EV_CLEAR); + if (events & POLLER_ONESHOT) + flags |= EV_DISPATCH; if (events & POLLER_IN) { EV_SET(&changes[nchanges], fd, EVFILT_READ, flags, 0, 0, NULL); @@ -38,7 +40,9 @@ int poller_add(int pfd, int fd, uint32_t events) { int poller_mod(int pfd, int fd, uint32_t events) { struct kevent changes[4]; int nchanges = 0; - unsigned short flags = EV_ADD | (events & POLLER_LEVEL ? 0 : EV_CLEAR); + unsigned short flags = EV_ADD | EV_ENABLE | (events & POLLER_LEVEL ? 0 : EV_CLEAR); + if (events & POLLER_ONESHOT) + flags |= EV_DISPATCH; /* * kqueue doesn't have a modify operation - we add/delete filters. diff --git a/src/worker.c b/src/worker.c index 8b8941f0..e0b72b6a 100644 --- a/src/worker.c +++ b/src/worker.c @@ -322,7 +322,7 @@ int worker_run_event_loop(int *listen_sockets, int num_sockets, int notif_fd) { int64_t last_tick = get_time_ms(); while (!stop_flag) { - int timeout_ms = write_head ? 0 : 100; + int timeout_ms = write_head ? 0 : mcast_worker_timeout(get_time_ms(), 100); int n = poller_wait(epfd, events, (int)(sizeof(events) / sizeof(events[0])), timeout_ms); if (n < 0) { if (errno == EINTR) @@ -555,6 +555,8 @@ int worker_run_event_loop(int *listen_sockets, int num_sockets, int notif_fd) { } } + mcast_worker_receive(now); + /* 2) Periodic tick: update streams and SSE heartbeats */ if (now - last_tick >= 100) { last_tick = now; From b4fd92a6c49bd720e730aedd62cd10faeeca791f Mon Sep 17 00:00:00 2001 From: Stackie Jia Date: Sun, 6 Sep 2026 22:58:38 +0800 Subject: [PATCH 16/19] perf(multicast): resume receive coalescing after transient bursts --- docs/en/reference/benchmark.md | 2 +- docs/reference/benchmark.md | 2 +- e2e/test_multicast_shared.py | 11 +++++++++-- src/multicast.c | 32 +++++++++++++++++++++----------- 4 files changed, 32 insertions(+), 15 deletions(-) diff --git a/docs/en/reference/benchmark.md b/docs/en/reference/benchmark.md index 32ebf872..4aecce01 100644 --- a/docs/en/reference/benchmark.md +++ b/docs/en/reference/benchmark.md @@ -105,7 +105,7 @@ Platforms supporting `recvmmsg` receive up to 16 datagrams per call. The worker The main multicast socket is read immediately on its first readiness notification. Subsequent reception can coalesce up to 1–2 ms of work, depending on the actual receive-buffer capacity and the number of datagrams received, reducing event wakeups for continuous small packets. A 1 ms wait requires a system-reported receive buffer of at least 128 KiB; 2 ms requires at least 256 KiB and a low packet count. System scheduling also affects the actual interval. Readiness notifications are paused during deferred reception and rearmed after the socket is drained. The worker scans only sources with pending receive tasks, and removes a source's task when its last subscriber leaves. -Small receive buffers, or a failed capacity query, use level-triggered notifications. A read reaching a conservative packet-count threshold derived from buffer capacity also restores immediate reception until an idle gap of at least 100 ms permits another coalescing attempt. With level triggering, a short batch can return because remaining data still generates notifications. Deferred reception must confirm that the socket is drained so an interrupted short read cannot strand data. Each callback receives at most 256 main multicast datagrams so continuous traffic cannot occupy the event loop indefinitely. These strategies are selected automatically and require no additional configuration. +Small receive buffers, or a failed capacity query, use level-triggered notifications. A read reaching a conservative packet-count threshold derived from buffer capacity also restores immediate reception. The packet rate is then reassessed over windows of at least 100 ms. A lower rate with twice the scheduling headroom permits another coalescing attempt, so a single burst cannot permanently disable the optimization. With level triggering, a short batch can return because remaining data still generates notifications. Deferred reception must confirm that the socket is drained so an interrupted short read cannot strand data. Each callback receives at most 256 main multicast datagrams so continuous traffic cannot occupy the event loop indefinitely. These strategies are selected automatically and require no additional configuration. Writes enter a local worker queue first. The worker subscribes to kernel writable events only when a socket cannot make further progress, reducing per-batch event registration changes. Each connection sends at most 256 KiB per turn, and each event-loop iteration processes at most 128 write tasks. Remaining tasks stay queued so reception, timers, and other clients can also run. diff --git a/docs/reference/benchmark.md b/docs/reference/benchmark.md index f03cf64f..0aab7788 100644 --- a/docs/reference/benchmark.md +++ b/docs/reference/benchmark.md @@ -105,7 +105,7 @@ Buffer 层在原有 1536 字节包缓冲池和控制缓冲池之外,增加按 主组播 socket 首次就绪时立即读取;后续按实际接收缓冲容量和每次收到的数据报数量,合并最多 1–2 ms 内的接收工作,减少持续小包带来的事件唤醒。系统报告的接收缓冲至少为 128 KiB 时才允许等待 1 ms,至少为 256 KiB 且包量较小时才允许 2 ms;实际间隔还受系统调度影响。延后读取期间暂停该 socket 的就绪通知,读空后重新启用。worker 只遍历仍有待处理接收任务的源,最后一个订阅者离开时同时移除其待处理任务。 -接收缓冲较小或无法查询容量时使用水平触发通知。单次读取达到按缓冲容量计算的保守包量阈值时,也恢复即时读取,直到出现至少 100 ms 的空闲间隔后再尝试合并。水平触发下读到不足一批即可返回,剩余数据仍会通知;延后读取路径则必须确认读空,避免中断造成的短读留下未处理数据。每次回调最多接收 256 个主组播数据报,使持续到来的数据不会长期占用事件循环。这些策略自动选择,无需额外配置。 +接收缓冲较小或无法查询容量时使用水平触发通知。单次读取达到按缓冲容量计算的保守包量阈值时,也恢复即时读取,并按至少 100 ms 的窗口重新估计包速率;速率降低且留有两倍调度余量时再尝试合并,避免一次突发永久关闭优化。水平触发下读到不足一批即可返回,剩余数据仍会通知;延后读取路径则必须确认读空,避免中断造成的短读留下未处理数据。每次回调最多接收 256 个主组播数据报,使持续到来的数据不会长期占用事件循环。这些策略自动选择,无需额外配置。 发送任务先进入 worker 内部队列,只有 socket 暂时无法继续写入时才订阅内核可写事件,减少每个批次的事件监听切换。每个连接单次最多发送 256 KiB,每轮最多处理 128 个发送任务,剩余任务继续排队,使接收、定时器和其他客户端都能得到处理。 diff --git a/e2e/test_multicast_shared.py b/e2e/test_multicast_shared.py index 2db1364b..fa3bab28 100644 --- a/e2e/test_multicast_shared.py +++ b/e2e/test_multicast_shared.py @@ -370,7 +370,7 @@ def test_fcc_clients_share_existing_multicast(shared_source_r2h, protocol): sender.stop() -@pytest.mark.parametrize("receive_buffer,first_burst", [(65536, 12), (524288, 12), (524288, 96)]) +@pytest.mark.parametrize("receive_buffer,first_burst", [(65536, 12), (524288, 12), (524288, 40)]) def test_shared_source_resumes_after_idle_bursts(r2h_binary, receive_buffer, first_burst): """Idle gaps and a busy initial burst must not strand either subscriber.""" r2h = R2HProcess( @@ -397,6 +397,8 @@ def test_shared_source_resumes_after_idle_bursts(r2h_binary, receive_buffer, fir for cycle in range(4): expected = bytearray() begin = time.monotonic() + # Forty packets cross the coalescing burst threshold without + # assuming the OS granted the requested 512 KiB socket buffer. burst = first_burst if cycle == 0 else 12 for _ in range(burst): ts = b"\x47\x1f\xff\x10" + struct.pack("!H", seq) + b"\xff" * 182 @@ -410,7 +412,12 @@ def test_shared_source_resumes_after_idle_bursts(r2h_binary, receive_buffer, fir responses = [stack.enter_context(closing(client.getresponse())) for client in clients] assert all(response.status == 200 for response in responses) for response in responses: - assert response.read(len(expected)) == expected + actual = response.read(len(expected)) + if actual != expected: + pytest.fail( + f"Burst {cycle}: received {len(actual)}/{len(expected)} bytes with unexpected content\n" + + r2h.read_log() + ) assert time.monotonic() - begin < 1.5 assert r2h.read_log().count("Multicast: Successfully joined group") == 1 finally: diff --git a/src/multicast.c b/src/multicast.c index 2949fc2d..0194e57e 100644 --- a/src/multicast.c +++ b/src/multicast.c @@ -397,10 +397,11 @@ struct mcast_source_s { int batch_packet_type; int64_t batch_since; int64_t next_receive; - int64_t last_receive; + int64_t receive_check_since; int receive_delay_max; int receive_coalesced; unsigned int receive_packet_limit; + unsigned int receive_check_packets; mcast_source_t *receive_next; mcast_source_t **receive_link; mcast_source_t *next; @@ -959,14 +960,30 @@ static int mcast_source_rearm(mcast_source_t *source, int coalesced) { } static void mcast_source_schedule_receive(mcast_source_t *source, int count, int64_t now) { - source->last_receive = now; - if (source->failed || !source->receive_coalesced) { + if (source->failed) { source->next_receive = 0; + } else if (!source->receive_coalesced) { + source->next_receive = 0; + if (source->receive_delay_max) { + source->receive_check_packets += (unsigned int)count; + int64_t elapsed = now - source->receive_check_since; + if (elapsed >= 100) { + /* A transient burst must not disable coalescing for the rest of a + * continuous stream. Reassess its rate with 2x scheduling headroom. */ + if ((int64_t)source->receive_check_packets * source->receive_delay_max * 2 < + elapsed * source->receive_packet_limit) + mcast_source_rearm(source, 1); + source->receive_check_since = now; + source->receive_check_packets = 0; + } + } } else if ((unsigned int)count >= source->receive_packet_limit) { /* At high packet rates even a millisecond can be too long. Keep readiness - * enabled until an idle gap, rather than repeatedly probing with a delay. */ + * enabled until the observed rate permits a short delay again. */ source->next_receive = 0; mcast_source_rearm(source, 0); + source->receive_check_since = now; + source->receive_check_packets = 0; } else if (count > 0) { /* The first notification reads immediately. Subsequent reads can collect * a short burst; only low packet rates and enough storage permit 2 ms. */ @@ -991,13 +1008,6 @@ int mcast_session_handle_event(mcast_session_t *session, int fd, int64_t now) { mcast_source_t *source = session->source; if (!source) return -1; - if (fd == source->sock && !source->failed && !source->receive_coalesced && source->receive_delay_max && - now - source->last_receive >= 100) { - /* Re-enable after an idle gap. Leave this ready socket to the new one-shot - * notification so there is no enabled filter during deferred reads. */ - source->last_receive = now; - return mcast_source_rearm(source, 1); - } int count = mcast_source_receive(source, fd, now); if (fd == source->sock) mcast_source_schedule_receive(source, count, now); From c7aefc89fcabf1827546da9276deff7aecb5860c Mon Sep 17 00:00:00 2001 From: Stackie Jia Date: Sun, 6 Sep 2026 23:35:45 +0800 Subject: [PATCH 17/19] docs(benchmark): publish balanced four-program resource measurements --- docs/en/reference/benchmark.md | 38 +++++++++++++++++----------------- docs/reference/benchmark.md | 38 +++++++++++++++++----------------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/docs/en/reference/benchmark.md b/docs/en/reference/benchmark.md index 4aecce01..47217d5c 100644 --- a/docs/en/reference/benchmark.md +++ b/docs/en/reference/benchmark.md @@ -12,7 +12,7 @@ Compare **rtp2httpd**, **[msd_lite](https://github.com/rozhuk-im/msd_lite)**, ** | Program | Tested version | | --- | --- | -| rtp2httpd | [`530dc980`](https://github.com/stackia/rtp2httpd/commit/530dc980e92db6b6ea98b6ca223dffe1a0345b5c) | +| rtp2httpd | [`b4fd92a6`](https://github.com/stackia/rtp2httpd/commit/b4fd92a6c49bd720e730aedd62cd10faeeca791f) | | msd_lite | [`fa68e131`](https://github.com/rozhuk-im/msd_lite/commit/fa68e131343fb58c67ad77b2d26f2cb7c49a2c95), 2026-07-20; liblcb `e2f420a2` | | udpxy | [`31d4bcfa`](https://github.com/pcherenkov/udpxy/commit/31d4bcfabaade59d3efdee015df7979febf76bae), 2026-04-13 | | TVGate | [v3.2.0](https://github.com/qist/tvgate/releases/tag/v3.2.0), 2026-09-06 | @@ -25,7 +25,7 @@ CPU utilization is the change in user and system CPU time from `/proc/PID/stat` PSS and USS are sampled every second from `smaps_rollup` and summed over the process tree. PSS proportionally includes shared pages, while USS includes only private pages. Neither includes all kernel socket memory or unmapped anonymous-file cache pages, so these metrics do not represent total server memory cost. -Each RTP datagram carries seven 188-byte MPEG-TS null packets, totaling 1316 payload bytes. Readers decode HTTP chunk framing and continuously consume the stream. Each trial restarts the server and load processes, then warms up after every client starts receiving data. Tests run sequentially. +Each RTP datagram carries seven 188-byte MPEG-TS null packets, totaling 1316 payload bytes. Readers decode HTTP chunk framing and continuously consume the stream. Each trial restarts the server and load processes, then warms up after every client starts receiving data. Tests run sequentially. The program order rotates each round, so every program occupies each execution position once over four rounds. msd_lite retains the upstream example's 48 KiB receive watermark, 64 KiB send watermark, and 1 MiB ring buffer; only the listener, interface, thread count, logging, and congestion control are adapted. udpxy retains its default buffer settings. TVGate configures only its listening port and loopback multicast interfaces; concurrency, buffering, connection limits, and logging use application defaults. @@ -33,10 +33,10 @@ msd_lite retains the upstream example's 48 KiB receive watermark, 64 KiB send wa | Scenario | Clients | Multicast sources | Payload rate per source | Repetitions | Warmup / sampling per trial | | --- | ---: | ---: | ---: | ---: | --- | -| Multiple channels | 8 | 8 | 40 Mbps | 3 | 5 s / 10 s | -| 8 clients sharing one channel | 8 | 1 | 40 Mbps | 3 | 5 s / 10 s | -| 64 clients sharing one channel | 64 | 1 | 20 Mbps | 5 | 5 s / 20 s | -| High bitrate | 1 | 1 | 400 Mbps | 3 | 5 s / 10 s | +| Multiple channels | 8 | 8 | 40 Mbps | 4 | 5 s / 15 s | +| 8 clients sharing one channel | 8 | 1 | 40 Mbps | 4 | 5 s / 15 s | +| 64 clients sharing one channel | 64 | 1 | 20 Mbps | 4 | 5 s / 15 s | +| High bitrate | 1 | 1 | 400 Mbps | 4 | 5 s / 15 s | ## Results @@ -48,28 +48,28 @@ Values are the means of per-trial average CPU utilization, with the minimum and | Scenario | rtp2httpd | msd_lite | udpxy | TVGate | | --- | ---: | ---: | ---: | ---: | -| 8 channels, 40 Mbps each | 9.78% (9.08–10.29) | 9.49% (9.19–9.78) | 20.81% (20.27–21.28) | 63.32% (62.56–63.96) | -| 8 clients, one 40 Mbps channel | 7.21% (6.97–7.47) | 6.07% (5.78–6.27) | 26.93% (26.21–27.78) | 43.20% (40.96–45.27) | -| 64 clients, one 20 Mbps channel | 6.97% (6.33–7.68) | 5.91% (5.59–6.48) | 58.17% (54.80–60.73) | 149.79% (136.85–156.83) | -| 1 client, 400 Mbps | 15.24% (13.84–16.14) | 14.78% (13.76–16.43) | 23.31% (22.84–23.55) | 47.99% (47.33–48.41) | +| 8 channels, 40 Mbps each | 3.69% (2.60–4.52) | 12.28% (10.98–12.92) | 23.02% (22.31–23.91) | 56.65% (56.16–57.17) | +| 8 clients, one 40 Mbps channel | 4.87% (4.64–4.98) | 5.58% (5.29–5.90) | 30.92% (29.19–32.29) | 45.09% (41.79–48.09) | +| 64 clients, one 20 Mbps channel | 4.78% (4.77–4.78) | 5.50% (5.31–5.76) | 59.93% (58.77–60.62) | 115.08% (112.50–117.97) | +| 1 client, 400 Mbps | 12.88% (9.70–14.27) | 15.46% (14.85–15.77) | 26.45% (24.58–27.68) | 57.86% (56.08–59.28) | ### PSS Memory (MiB) | Scenario | rtp2httpd | msd_lite | udpxy | TVGate | | --- | ---: | ---: | ---: | ---: | -| 8 channels, 40 Mbps each | 2.28 | 8.95 | 0.80 | 25.72 | -| 8 clients, one 40 Mbps channel | 1.60 | 1.35 | 0.79 | 22.68 | -| 64 clients, one 20 Mbps channel | 4.61 | 1.37 | 4.51 | 45.65 | -| 1 client, 400 Mbps | 1.42 | 1.34 | 0.32 | 19.12 | +| 8 channels, 40 Mbps each | 1.86 | 8.95 | 0.79 | 25.82 | +| 8 clients, one 40 Mbps channel | 1.13 | 1.35 | 0.79 | 22.76 | +| 64 clients, one 20 Mbps channel | 1.30 | 1.37 | 4.55 | 46.26 | +| 1 client, 400 Mbps | 1.26 | 1.34 | 0.32 | 19.16 | ### USS Memory (MiB) | Scenario | rtp2httpd | msd_lite | udpxy | TVGate | | --- | ---: | ---: | ---: | ---: | -| 8 channels, 40 Mbps each | 1.66 | 8.94 | 0.53 | 25.72 | -| 8 clients, one 40 Mbps channel | 0.98 | 1.34 | 0.52 | 22.68 | -| 64 clients, one 20 Mbps channel | 3.99 | 1.36 | 3.92 | 45.65 | -| 1 client, 400 Mbps | 0.82 | 1.33 | 0.11 | 19.12 | +| 8 channels, 40 Mbps each | 1.24 | 8.94 | 0.52 | 25.82 | +| 8 clients, one 40 Mbps channel | 0.51 | 1.34 | 0.52 | 22.76 | +| 64 clients, one 20 Mbps channel | 0.68 | 1.35 | 3.96 | 46.26 | +| 1 client, 400 Mbps | 0.63 | 1.33 | 0.12 | 19.16 | ## Appendix: Performance Optimization Strategies in rtp2httpd @@ -136,7 +136,7 @@ See [tools/stress-test/README.md](https://github.com/stackia/rtp2httpd/blob/main ```bash scripts/benchmark.sh rtp2httpd msd_lite udpxy tvgate \ --cases distinct8 shared8 shared64 high400 \ - --repetitions 5 --warmup 5 --duration 20 + --repetitions 4 --warmup 5 --duration 15 ``` Use `--binary NAME=PATH` and `--revision NAME=VERSION` to identify the actual executables and versions. Set repetitions and sampling duration for each scenario according to the table above. CPU, PSS, and USS summaries are written to `resources.json` in the output directory, which defaults to `build/benchmark/`. Test records remain local. diff --git a/docs/reference/benchmark.md b/docs/reference/benchmark.md index 0aab7788..a5bfc1e5 100644 --- a/docs/reference/benchmark.md +++ b/docs/reference/benchmark.md @@ -12,7 +12,7 @@ | 程序 | 测试版本 | | --- | --- | -| rtp2httpd | [`530dc980`](https://github.com/stackia/rtp2httpd/commit/530dc980e92db6b6ea98b6ca223dffe1a0345b5c) | +| rtp2httpd | [`b4fd92a6`](https://github.com/stackia/rtp2httpd/commit/b4fd92a6c49bd720e730aedd62cd10faeeca791f) | | msd_lite | [`fa68e131`](https://github.com/rozhuk-im/msd_lite/commit/fa68e131343fb58c67ad77b2d26f2cb7c49a2c95),2026-07-20;liblcb `e2f420a2` | | udpxy | [`31d4bcfa`](https://github.com/pcherenkov/udpxy/commit/31d4bcfabaade59d3efdee015df7979febf76bae),2026-04-13 | | TVGate | [v3.2.0](https://github.com/qist/tvgate/releases/tag/v3.2.0),2026-09-06 | @@ -25,7 +25,7 @@ CPU 使用率取完整测量窗口内 `/proc/PID/stat` 的用户态与内核态 PSS 和 USS 从 `smaps_rollup` 每秒采样并对进程树求和。PSS 按比例计入共享页,USS 仅统计私有页,均不包含全部内核 socket 内存或未映射的匿名文件页缓存,因此不能作为服务总内存成本。 -每个 RTP 数据报携带 7 个 188 字节 MPEG-TS 空包,共 1316 字节负载。接收端解析 HTTP 分块编码后持续读取流。每轮重启服务与负载进程,所有客户端开始收到数据后再预热,测试逐项串行执行。 +每个 RTP 数据报携带 7 个 188 字节 MPEG-TS 空包,共 1316 字节负载。接收端解析 HTTP 分块编码后持续读取流。每轮重启服务与负载进程,所有客户端开始收到数据后再预热,测试逐项串行执行,每轮轮换程序顺序,四轮中每个程序在各个执行位置各出现一次。 msd_lite 保留上游示例的 48 KiB 接收水位、64 KiB 发送水位、1 MiB 环形缓冲;仅适配监听地址、接口、线程数、日志和拥塞控制。udpxy 保留默认缓冲设置。TVGate 仅配置监听端口及 loopback 组播接口,并发、缓冲、连接上限及日志均使用程序默认值。 @@ -33,10 +33,10 @@ msd_lite 保留上游示例的 48 KiB 接收水位、64 KiB 发送水位、1 MiB | 场景 | 客户端数 | 组播源数 | 单源负载码率 | 重复次数 | 每轮预热 / 采样 | | --- | ---: | ---: | ---: | ---: | --- | -| 多频道 | 8 | 8 | 40 Mbps | 3 | 5 s / 10 s | -| 同频道 8 客户端 | 8 | 1 | 40 Mbps | 3 | 5 s / 10 s | -| 同频道 64 客户端 | 64 | 1 | 20 Mbps | 5 | 5 s / 20 s | -| 高码率 | 1 | 1 | 400 Mbps | 3 | 5 s / 10 s | +| 多频道 | 8 | 8 | 40 Mbps | 4 | 5 s / 15 s | +| 同频道 8 客户端 | 8 | 1 | 40 Mbps | 4 | 5 s / 15 s | +| 同频道 64 客户端 | 64 | 1 | 20 Mbps | 4 | 5 s / 15 s | +| 高码率 | 1 | 1 | 400 Mbps | 4 | 5 s / 15 s | ## 测试结果 @@ -48,28 +48,28 @@ msd_lite 保留上游示例的 48 KiB 接收水位、64 KiB 发送水位、1 MiB | 场景 | rtp2httpd | msd_lite | udpxy | TVGate | | --- | ---: | ---: | ---: | ---: | -| 8 频道,各 40 Mbps | 9.78% (9.08–10.29) | 9.49% (9.19–9.78) | 20.81% (20.27–21.28) | 63.32% (62.56–63.96) | -| 8 客户端同频道,40 Mbps | 7.21% (6.97–7.47) | 6.07% (5.78–6.27) | 26.93% (26.21–27.78) | 43.20% (40.96–45.27) | -| 64 客户端同频道,20 Mbps | 6.97% (6.33–7.68) | 5.91% (5.59–6.48) | 58.17% (54.80–60.73) | 149.79% (136.85–156.83) | -| 单客户端,400 Mbps | 15.24% (13.84–16.14) | 14.78% (13.76–16.43) | 23.31% (22.84–23.55) | 47.99% (47.33–48.41) | +| 8 频道,各 40 Mbps | 3.69% (2.60–4.52) | 12.28% (10.98–12.92) | 23.02% (22.31–23.91) | 56.65% (56.16–57.17) | +| 8 客户端同频道,40 Mbps | 4.87% (4.64–4.98) | 5.58% (5.29–5.90) | 30.92% (29.19–32.29) | 45.09% (41.79–48.09) | +| 64 客户端同频道,20 Mbps | 4.78% (4.77–4.78) | 5.50% (5.31–5.76) | 59.93% (58.77–60.62) | 115.08% (112.50–117.97) | +| 单客户端,400 Mbps | 12.88% (9.70–14.27) | 15.46% (14.85–15.77) | 26.45% (24.58–27.68) | 57.86% (56.08–59.28) | ### PSS 内存占用(MiB) | 场景 | rtp2httpd | msd_lite | udpxy | TVGate | | --- | ---: | ---: | ---: | ---: | -| 8 频道,各 40 Mbps | 2.28 | 8.95 | 0.80 | 25.72 | -| 8 客户端同频道,40 Mbps | 1.60 | 1.35 | 0.79 | 22.68 | -| 64 客户端同频道,20 Mbps | 4.61 | 1.37 | 4.51 | 45.65 | -| 单客户端,400 Mbps | 1.42 | 1.34 | 0.32 | 19.12 | +| 8 频道,各 40 Mbps | 1.86 | 8.95 | 0.79 | 25.82 | +| 8 客户端同频道,40 Mbps | 1.13 | 1.35 | 0.79 | 22.76 | +| 64 客户端同频道,20 Mbps | 1.30 | 1.37 | 4.55 | 46.26 | +| 单客户端,400 Mbps | 1.26 | 1.34 | 0.32 | 19.16 | ### USS 内存占用(MiB) | 场景 | rtp2httpd | msd_lite | udpxy | TVGate | | --- | ---: | ---: | ---: | ---: | -| 8 频道,各 40 Mbps | 1.66 | 8.94 | 0.53 | 25.72 | -| 8 客户端同频道,40 Mbps | 0.98 | 1.34 | 0.52 | 22.68 | -| 64 客户端同频道,20 Mbps | 3.99 | 1.36 | 3.92 | 45.65 | -| 单客户端,400 Mbps | 0.82 | 1.33 | 0.11 | 19.12 | +| 8 频道,各 40 Mbps | 1.24 | 8.94 | 0.52 | 25.82 | +| 8 客户端同频道,40 Mbps | 0.51 | 1.34 | 0.52 | 22.76 | +| 64 客户端同频道,20 Mbps | 0.68 | 1.35 | 3.96 | 46.26 | +| 单客户端,400 Mbps | 0.63 | 1.33 | 0.12 | 19.16 | ## 附:rtp2httpd 的性能优化策略 @@ -136,7 +136,7 @@ FCC 单播和切换状态按客户端独立维护。衔接时先共享组播 soc ```bash scripts/benchmark.sh rtp2httpd msd_lite udpxy tvgate \ --cases distinct8 shared8 shared64 high400 \ - --repetitions 5 --warmup 5 --duration 20 + --repetitions 4 --warmup 5 --duration 15 ``` 通过 `--binary 名称=路径` 与 `--revision 名称=版本` 指定实际使用的二进制及版本,按上表设置各场景的重复次数和采样时长。CPU、PSS、USS 汇总位于输出目录中的 `resources.json`。输出目录默认位于 `build/benchmark/`,测试记录仅在本地保留。 From 8c073a5dff93057ca07da5f895d24fbb072136b2 Mon Sep 17 00:00:00 2001 From: Stackie Jia Date: Sun, 6 Sep 2026 23:54:59 +0800 Subject: [PATCH 18/19] fix(poller): replace kqueue filters when receive mode changes --- e2e/poller_modes.c | 64 +++++++++++++++++++++++++++++++++++++++++++++ e2e/test_poller.py | 33 +++++++++++++++++++++++ src/multicast.c | 5 +++- src/poller.h | 10 ++++++- src/poller_epoll.c | 2 ++ src/poller_kqueue.c | 9 +++++++ 6 files changed, 121 insertions(+), 2 deletions(-) create mode 100644 e2e/poller_modes.c create mode 100644 e2e/test_poller.py diff --git a/e2e/poller_modes.c b/e2e/poller_modes.c new file mode 100644 index 00000000..32847e00 --- /dev/null +++ b/e2e/poller_modes.c @@ -0,0 +1,64 @@ +/* Kernel integration regression: changing modes must replace old semantics. */ +#include "poller.h" +#include +#include + +static void expect_readable(int pfd, int fd) { + poller_event_t event; + assert(poller_wait(pfd, &event, 1, 100) == 1); + assert(event.fd == fd); + assert(event.events & POLLER_IN); +} + +static void expect_quiet(int pfd) { + poller_event_t event; + assert(poller_wait(pfd, &event, 1, 0) == 0); +} + +int main(void) { + int pipefd[2]; + assert(pipe(pipefd) == 0); + int pfd = poller_create(); + assert(pfd >= 0); + assert(poller_add(pfd, pipefd[0], POLLER_IN | POLLER_ONESHOT) == 0); + char byte; + + for (int cycle = 0; cycle < 16; cycle++) { + assert(write(pipefd[1], "a", 1) == 1); + expect_readable(pfd, pipefd[0]); + expect_quiet(pfd); + assert(read(pipefd[0], &byte, 1) == 1); + + /* New arrivals while disabled must wait for an explicit rearm. */ + assert(write(pipefd[1], "b", 1) == 1); + expect_quiet(pfd); + assert(poller_mod(pfd, pipefd[0], POLLER_IN | POLLER_ONESHOT) == 0); + expect_readable(pfd, pipefd[0]); + assert(read(pipefd[0], &byte, 1) == 1); + + /* Exercise data queued both before and after the mode transition. */ + if (cycle & 1) + assert(write(pipefd[1], "c", 1) == 1); + assert(poller_reset(pfd, pipefd[0], POLLER_IN | POLLER_LEVEL) == 0); + if (!(cycle & 1)) + assert(write(pipefd[1], "c", 1) == 1); + for (int i = 0; i < 4; i++) + expect_readable(pfd, pipefd[0]); + assert(read(pipefd[0], &byte, 1) == 1); + expect_quiet(pfd); + assert(write(pipefd[1], "d", 1) == 1); + expect_readable(pfd, pipefd[0]); + assert(read(pipefd[0], &byte, 1) == 1); + + /* Switching back must restore edge/one-shot semantics. */ + assert(poller_reset(pfd, pipefd[0], POLLER_IN | POLLER_ONESHOT) == 0); + expect_quiet(pfd); + } + assert(poller_del(pfd, pipefd[0]) == 0); + assert(write(pipefd[1], "e", 1) == 1); + expect_quiet(pfd); + close(pipefd[0]); + close(pipefd[1]); + poller_close(pfd); + return 0; +} diff --git a/e2e/test_poller.py b/e2e/test_poller.py new file mode 100644 index 00000000..44b1303a --- /dev/null +++ b/e2e/test_poller.py @@ -0,0 +1,33 @@ +"""Exercise the production poller backend against the host kernel.""" + +import os +import shlex +import subprocess +from pathlib import Path + + +def test_poller_trigger_mode_transitions(tmp_path): + root = Path(__file__).resolve().parent.parent + executable = tmp_path / "poller_modes" + compiler = shlex.split(os.environ.get("CC", "cc")) + subprocess.run( + [ + *compiler, + "-std=c11", + "-Wall", + "-Wextra", + "-Werror", + "-I", + str(root / "src"), + str(root / "e2e/poller_modes.c"), + str(root / "src/poller_epoll.c"), + str(root / "src/poller_kqueue.c"), + "-o", + str(executable), + ], + check=True, + capture_output=True, + text=True, + timeout=30, + ) + subprocess.run([str(executable)], check=True, capture_output=True, text=True, timeout=10) diff --git a/src/multicast.c b/src/multicast.c index 0194e57e..c8927bea 100644 --- a/src/multicast.c +++ b/src/multicast.c @@ -950,7 +950,10 @@ static int mcast_source_receive(mcast_source_t *source, int fd, int64_t now) { } static int mcast_source_rearm(mcast_source_t *source, int coalesced) { - if (poller_mod(source->epoll_fd, source->sock, POLLER_IN | (coalesced ? POLLER_ONESHOT : POLLER_LEVEL)) < 0) { + uint32_t events = POLLER_IN | (coalesced ? POLLER_ONESHOT : POLLER_LEVEL); + int result = source->receive_coalesced == coalesced ? poller_mod(source->epoll_fd, source->sock, events) + : poller_reset(source->epoll_fd, source->sock, events); + if (result < 0) { logger(LOG_ERROR, "Multicast: Cannot rearm receive socket: %s", strerror(errno)); source->failed = 1; return -1; diff --git a/src/poller.h b/src/poller.h index 4b2e823b..2cfead57 100644 --- a/src/poller.h +++ b/src/poller.h @@ -55,7 +55,8 @@ void poller_close(int pfd); int poller_add(int pfd, int fd, uint32_t events); /** - * Modify the events monitored for a file descriptor. + * Modify interest or rearm a file descriptor without changing its trigger mode. + * Use poller_reset() when changing POLLER_LEVEL or POLLER_ONESHOT. * @param pfd Poller file descriptor * @param fd File descriptor to modify * @param events New bitmask of POLLER_* events to monitor @@ -63,6 +64,13 @@ int poller_add(int pfd, int fd, uint32_t events); */ int poller_mod(int pfd, int fd, uint32_t events); +/** + * Replace interest and trigger mode, including for an already-ready descriptor. + * kqueue requires fresh filters to replace EV_CLEAR/EV_DISPATCH behavior. + * @return 0 on success, -1 on error; monitoring may be removed on failure + */ +int poller_reset(int pfd, int fd, uint32_t events); + /** * Remove a file descriptor from the poller. * @param pfd Poller file descriptor diff --git a/src/poller_epoll.c b/src/poller_epoll.c index b1b9edff..593a6021 100644 --- a/src/poller_epoll.c +++ b/src/poller_epoll.c @@ -47,6 +47,8 @@ int poller_mod(int pfd, int fd, uint32_t events) { return epoll_ctl(pfd, EPOLL_CTL_MOD, fd, &ev); } +int poller_reset(int pfd, int fd, uint32_t events) { return poller_mod(pfd, fd, events); } + int poller_del(int pfd, int fd) { return epoll_ctl(pfd, EPOLL_CTL_DEL, fd, NULL); } int poller_wait(int pfd, poller_event_t *events, int max_events, int timeout_ms) { diff --git a/src/poller_kqueue.c b/src/poller_kqueue.c index cb1b85d3..f33ba6ec 100644 --- a/src/poller_kqueue.c +++ b/src/poller_kqueue.c @@ -97,6 +97,15 @@ int poller_del(int pfd, int fd) { return 0; } +int poller_reset(int pfd, int fd, uint32_t events) { + /* EV_ADD on an existing filter preserves EV_CLEAR/EV_DISPATCH. Recreate + * filters only when the trigger mode changes; ordinary rearming stays cheap. + * Adding the new filters also reports data queued during this transition. */ + if (poller_del(pfd, fd) < 0) + return -1; + return poller_add(pfd, fd, events); +} + int poller_wait(int pfd, poller_event_t *events, int max_events, int timeout_ms) { struct kevent kev_buf[1024]; struct kevent *kev_events = max_events <= 1024 ? kev_buf : malloc(max_events * sizeof(struct kevent)); From be6b20aa16c728ee6531c269d8ebf93e6067c5ad Mon Sep 17 00:00:00 2001 From: Stackie Jia Date: Mon, 7 Sep 2026 00:32:20 +0800 Subject: [PATCH 19/19] fix(stream): release deferred sessions during final connection cleanup --- e2e/buffer_lifetime.c | 130 ++++++++++++++++++++++++++++++++++++ e2e/helpers/__init__.py | 2 + e2e/helpers/native.py | 34 ++++++++++ e2e/test_buffer_lifetime.py | 7 ++ e2e/test_poller.py | 30 +-------- src/connection.c | 12 ++-- src/poller_epoll.c | 25 ++----- src/rtsp.c | 6 +- src/rtsp.h | 4 ++ src/send_queue.c | 83 +++++++++-------------- src/stream.c | 9 +++ src/stream.h | 4 ++ 12 files changed, 235 insertions(+), 111 deletions(-) create mode 100644 e2e/buffer_lifetime.c create mode 100644 e2e/helpers/native.py create mode 100644 e2e/test_buffer_lifetime.py diff --git a/e2e/buffer_lifetime.c b/e2e/buffer_lifetime.c new file mode 100644 index 00000000..43f54fae --- /dev/null +++ b/e2e/buffer_lifetime.c @@ -0,0 +1,130 @@ +/* Exercise production buffer ownership and send accounting with real sockets. */ +#include "configuration.h" +#include "send_queue.h" +#include "status.h" +#include "utils.h" +#include +#include +#include +#include +#include +#include +#include +#include + +config_t config; +status_shared_t *status_shared; +int worker_id = -1; + +int logger(loglevel_t level, const char *format, ...) { + (void)level; + (void)format; + return 0; +} + +static void expect_closed(int fd) { + assert(fcntl(fd, F_GETFD) == -1); + assert(errno == EBADF); +} + +static void send_and_read(int *pair, send_queue_t *queue, size_t count, uint8_t value) { + size_t sent = 0; + assert(send_queue_send(pair[0], queue, count, &sent) == 0); + assert(sent == count); + uint8_t received[4096]; + assert(count <= sizeof(received)); + size_t offset = 0; + while (offset < count) { + ssize_t n = read(pair[1], received + offset, count - offset); + assert(n > 0); + offset += (size_t)n; + } + for (size_t i = 0; i < count; i++) + assert(received[i] == value); +} + +static void shared_batch(int snapshot, int fallback) { + buffer_ref_t *owner = buffer_pool_alloc_batch(); + assert(owner); + owner->data_offset = 12; + owner->data_size = 4096; + memset((uint8_t *)owner->data + owner->data_offset, 0x5a, owner->data_size); + if (snapshot) + buffer_ref_snapshot(owner); + int snapshot_fd = buffer_ref_sendfile_fd(owner); + send_queue_t queues[2] = {0}; + for (int i = 0; i < 2; i++) { + buffer_ref_t *view = buffer_ref_view(owner); + assert(view); + if (fallback && i == 0) + view->shared_fd = -2; + assert(send_queue_add(&queues[i], view) == 0); + buffer_ref_put(view); + } + buffer_ref_put(owner); /* Queues must remain valid after the source releases it. */ + assert(owner->refcount == 2); + int pair[2]; + assert(socketpair(AF_UNIX, SOCK_STREAM, 0, pair) == 0); + send_and_read(pair, &queues[0], 1024, 0x5a); + assert(queues[0].total_bytes == 3072); + assert(queues[0].memory_bytes == BUFFER_POOL_BATCH_SIZE); + assert(queues[1].total_bytes == 4096); + assert(queues[1].head->iov.iov_len == 4096); + assert(buffer_ref_sendfile_fd(queues[1].head) == snapshot_fd); + send_and_read(pair, &queues[0], 3072, 0x5a); + assert(!queues[0].head && !queues[0].tail && !queues[0].num_queued && !queues[0].memory_bytes); + assert(owner->refcount == 1); + if (snapshot_fd >= 0) + assert(fcntl(snapshot_fd, F_GETFD) >= 0); + send_and_read(pair, &queues[1], 4096, 0x5a); + assert(!queues[1].head && !queues[1].total_bytes && !queues[1].memory_bytes); + assert(send_buffer_state.batch_pool.num_free == send_buffer_state.batch_pool.num_buffers); + if (snapshot_fd >= 0) + expect_closed(snapshot_fd); + close(pair[0]); + close(pair[1]); +} + +static void mixed_queue(void) { + int pair[2]; + assert(socketpair(AF_UNIX, SOCK_STREAM, 0, pair) == 0); + send_queue_t queue = {0}; + for (int i = 0; i < 2; i++) { + buffer_ref_t *ref = buffer_pool_alloc(); + assert(ref); + ref->data_size = 1000; + memset(ref->data, 0x6b, ref->data_size); + assert(send_queue_add(&queue, ref) == 0); + buffer_ref_put(ref); + } + send_and_read(pair, &queue, 1500, 0x6b); /* Cross an iovec boundary. */ + assert(queue.num_queued == 1 && queue.total_bytes == 500); + assert(queue.memory_bytes == BUFFER_POOL_BUFFER_SIZE); + char path[] = "/tmp/rtp2httpd-buffer-test-XXXXXX"; + int fd = mkstemp(path); + assert(fd >= 0); + unlink(path); + assert(write(fd, "file", 4) == 4); + assert(send_queue_add_file(&queue, fd, 0, 4) == 0); + assert(queue.total_bytes == 500 && queue.num_queued == 2); + send_and_read(pair, &queue, 500, 0x6b); + assert(queue.total_bytes == 0 && queue.memory_bytes == BUFFER_POOL_BUFFER_SIZE); + send_queue_cleanup(&queue); /* Disconnect while a file is pending. */ + expect_closed(fd); + assert(!queue.head && !queue.tail && !queue.num_queued && !queue.memory_bytes); + assert(send_buffer_state.pool.num_free == send_buffer_state.pool.num_buffers); + close(pair[0]); + close(pair[1]); +} + +int main(void) { + signal(SIGPIPE, SIG_IGN); + config.buffer_pool_max_size = 1024; + assert(send_buffer_init() == 0); + shared_batch(0, 0); + shared_batch(1, 0); + shared_batch(1, 1); + mixed_queue(); + send_buffer_cleanup(); + return 0; +} diff --git a/e2e/helpers/__init__.py b/e2e/helpers/__init__.py index a860825e..98f5c24e 100644 --- a/e2e/helpers/__init__.py +++ b/e2e/helpers/__init__.py @@ -49,6 +49,7 @@ MockRTSPServerZTE, ) from .mock_stun import MockSTUNServer +from .native import run_native_test from .ports import ( find_free_port, find_free_udp_port, @@ -95,6 +96,7 @@ "make_m3u_rtsp_config", "make_rtp_packet", "raw_http_request", + "run_native_test", "stream_get", "unix_http_get", "unix_http_request", diff --git a/e2e/helpers/native.py b/e2e/helpers/native.py new file mode 100644 index 00000000..6cd8198b --- /dev/null +++ b/e2e/helpers/native.py @@ -0,0 +1,34 @@ +"""Compile small kernel/resource integration checks against production C code.""" + +import os +import platform +import shlex +import subprocess +from pathlib import Path + +from .constants import PROJECT_ROOT + + +def run_native_test(tmp_path: Path, name: str, sources: list[str]) -> None: + executable = tmp_path / name + subprocess.run( + [ + *shlex.split(os.environ.get("CC", "cc")), + "-std=gnu11", + "-Wall", + "-Wextra", + "-Werror", + *(["-D_GNU_SOURCE"] if platform.system() == "Linux" else []), + "-I", + str(PROJECT_ROOT / "src"), + str(PROJECT_ROOT / "e2e" / f"{name}.c"), + *(str(PROJECT_ROOT / source) for source in sources), + "-o", + str(executable), + ], + check=True, + capture_output=True, + text=True, + timeout=30, + ) + subprocess.run([str(executable)], check=True, capture_output=True, text=True, timeout=10) diff --git a/e2e/test_buffer_lifetime.py b/e2e/test_buffer_lifetime.py new file mode 100644 index 00000000..9deda41e --- /dev/null +++ b/e2e/test_buffer_lifetime.py @@ -0,0 +1,7 @@ +"""Check shared payload lifetime and queue accounting against real sockets.""" + +from helpers import run_native_test + + +def test_buffer_lifetime(tmp_path): + run_native_test(tmp_path, "buffer_lifetime", ["src/buffer_pool.c", "src/send_queue.c"]) diff --git a/e2e/test_poller.py b/e2e/test_poller.py index 44b1303a..cabfe649 100644 --- a/e2e/test_poller.py +++ b/e2e/test_poller.py @@ -1,33 +1,7 @@ """Exercise the production poller backend against the host kernel.""" -import os -import shlex -import subprocess -from pathlib import Path +from helpers import run_native_test def test_poller_trigger_mode_transitions(tmp_path): - root = Path(__file__).resolve().parent.parent - executable = tmp_path / "poller_modes" - compiler = shlex.split(os.environ.get("CC", "cc")) - subprocess.run( - [ - *compiler, - "-std=c11", - "-Wall", - "-Wextra", - "-Werror", - "-I", - str(root / "src"), - str(root / "e2e/poller_modes.c"), - str(root / "src/poller_epoll.c"), - str(root / "src/poller_kqueue.c"), - "-o", - str(executable), - ], - check=True, - capture_output=True, - text=True, - timeout=30, - ) - subprocess.run([str(executable)], check=True, capture_output=True, text=True, timeout=10) + run_native_test(tmp_path, "poller_modes", ["src/poller_epoll.c", "src/poller_kqueue.c"]) diff --git a/src/connection.c b/src/connection.c index 2ba5db86..34fb72dc 100644 --- a/src/connection.c +++ b/src/connection.c @@ -618,14 +618,10 @@ void connection_cleanup(connection_t *c) { c->stream_registered = 0; } - /* Clean up stream context if still marked as streaming - * Note: worker_close_and_free_connection should have already called - * stream_context_cleanup for streaming connections, so this is a safety - * fallback */ - if (c->streaming) { - logger(LOG_WARN, "connection_cleanup: streaming flag still set, cleaning up stream"); - stream_context_cleanup(&c->stream); - } + /* The streaming flag is cleared when async TEARDOWN starts. Always destroy + * the context here, whether teardown completed, timed out, or was cancelled + * by worker shutdown. This also handles partially initialized streams. */ + stream_context_destroy(&c->stream); /* Cleanup buffered output queue - this releases all buffer references */ send_queue_cleanup(&c->send_queue); diff --git a/src/poller_epoll.c b/src/poller_epoll.c index 593a6021..05e87cbf 100644 --- a/src/poller_epoll.c +++ b/src/poller_epoll.c @@ -9,7 +9,7 @@ int poller_create(void) { return epoll_create1(EPOLL_CLOEXEC); } void poller_close(int pfd) { close(pfd); } -int poller_add(int pfd, int fd, uint32_t events) { +static int poller_update(int pfd, int operation, int fd, uint32_t events) { struct epoll_event ev; ev.events = events & POLLER_LEVEL ? 0 : EPOLLET; if (events & POLLER_ONESHOT) @@ -25,27 +25,12 @@ int poller_add(int pfd, int fd, uint32_t events) { ev.events |= EPOLLHUP; if (events & POLLER_RDHUP) ev.events |= EPOLLRDHUP; - return epoll_ctl(pfd, EPOLL_CTL_ADD, fd, &ev); + return epoll_ctl(pfd, operation, fd, &ev); } -int poller_mod(int pfd, int fd, uint32_t events) { - struct epoll_event ev; - ev.events = events & POLLER_LEVEL ? 0 : EPOLLET; - if (events & POLLER_ONESHOT) - ev.events |= EPOLLONESHOT; - ev.data.fd = fd; - if (events & POLLER_IN) - ev.events |= EPOLLIN; - if (events & POLLER_OUT) - ev.events |= EPOLLOUT; - if (events & POLLER_ERR) - ev.events |= EPOLLERR; - if (events & POLLER_HUP) - ev.events |= EPOLLHUP; - if (events & POLLER_RDHUP) - ev.events |= EPOLLRDHUP; - return epoll_ctl(pfd, EPOLL_CTL_MOD, fd, &ev); -} +int poller_add(int pfd, int fd, uint32_t events) { return poller_update(pfd, EPOLL_CTL_ADD, fd, events); } + +int poller_mod(int pfd, int fd, uint32_t events) { return poller_update(pfd, EPOLL_CTL_MOD, fd, events); } int poller_reset(int pfd, int fd, uint32_t events) { return poller_mod(pfd, fd, events); } diff --git a/src/rtsp.c b/src/rtsp.c index 26ee89f6..c6d15392 100644 --- a/src/rtsp.c +++ b/src/rtsp.c @@ -64,7 +64,6 @@ static void rtsp_parse_describe_sdp(rtsp_session_t *session, const struct phr_he static void rtsp_parse_play_metadata(rtsp_session_t *session, const struct phr_header *headers, size_t num_headers); static int rtsp_initiate_teardown(rtsp_session_t *session); static int rtsp_reconnect_for_teardown(rtsp_session_t *session); -static void rtsp_force_cleanup(rtsp_session_t *session); static int rtsp_base64_encode(const uint8_t *input, size_t input_len, char *output, size_t output_size); static int rtsp_parse_www_authenticate(rtsp_session_t *session, const char *www_auth_header); static void rtsp_build_digest_response(rtsp_session_t *session, const char *method, const char *uri, char *response_out, @@ -2264,7 +2263,10 @@ int rtsp_handle_udp_rtp_data(rtsp_session_t *session, connection_t *conn) { * Force cleanup - immediately close all sockets and reset session * Used when TEARDOWN cannot be sent or after TEARDOWN completes */ -static void rtsp_force_cleanup(rtsp_session_t *session) { +void rtsp_force_cleanup(rtsp_session_t *session) { + if (!session || !session->initialized) + return; + /* Close and remove RTSP control socket from poller */ if (session->socket >= 0) { worker_cleanup_socket_from_epoll(session->epoll_fd, session->socket); diff --git a/src/rtsp.h b/src/rtsp.h index cdb1f33b..841ab346 100644 --- a/src/rtsp.h +++ b/src/rtsp.h @@ -328,6 +328,10 @@ int rtsp_handle_udp_rtp_data(rtsp_session_t *session, struct connection_s *conn) */ int rtsp_session_cleanup(rtsp_session_t *session); +/** Immediately release session resources, including a pending TEARDOWN. + * The owner must still free the session object. Safe to call repeatedly. */ +void rtsp_force_cleanup(rtsp_session_t *session); + /** * Schedule an RTSP OPTIONS keepalive request if the session is idle. * @param session RTSP session diff --git a/src/send_queue.c b/src/send_queue.c index 33e9b223..7f97106c 100644 --- a/src/send_queue.c +++ b/src/send_queue.c @@ -198,6 +198,32 @@ int send_queue_should_flush(send_queue_t *queue) { return 0; /* Not ready to flush yet */ } +/* Payload progress and retained capacity have different lifetimes: a partial + * send reduces total_bytes, but capacity is released only with the last byte. */ +static void send_queue_pop_head(send_queue_t *queue) { + buffer_ref_t *head = queue->head; + queue->head = head->send_next; + if (!queue->head) + queue->tail = NULL; + queue->num_queued--; + queue->memory_bytes -= head->type == BUFFER_TYPE_FILE ? BUFFER_POOL_BUFFER_SIZE : buffer_ref_capacity(head); + buffer_ref_put(head); +} + +static void send_queue_consume_memory(send_queue_t *queue, size_t sent) { + queue->total_bytes -= sent; + while (sent) { + buffer_ref_t *head = queue->head; + if (sent < head->iov.iov_len) { + head->iov.iov_base = (uint8_t *)head->iov.iov_base + sent; + head->iov.iov_len -= sent; + return; + } + sent -= head->iov.iov_len; + send_queue_pop_head(queue); + } +} + int send_queue_send(int fd, send_queue_t *queue, size_t max_bytes, size_t *bytes_sent) { if (!queue->head || !max_bytes) { *bytes_sent = 0; @@ -225,17 +251,7 @@ int send_queue_send(int fd, send_queue_t *queue, size_t max_bytes, size_t *bytes if (sent == 0) return -1; /* The immutable snapshot must contain the complete batch. */ WORKER_STATS_INC(total_sends); - queue->total_bytes -= (size_t)sent; - shared->iov.iov_base = (uint8_t *)shared->iov.iov_base + sent; - shared->iov.iov_len -= (size_t)sent; - if (!shared->iov.iov_len) { - queue->head = shared->send_next; - if (!queue->head) - queue->tail = NULL; - queue->num_queued--; - queue->memory_bytes -= buffer_ref_capacity(shared); - buffer_ref_put(shared); - } + send_queue_consume_memory(queue, (size_t)sent); return 0; } } @@ -271,17 +287,8 @@ int send_queue_send(int fd, send_queue_t *queue, size_t max_bytes, size_t *bytes /* File completely sent - remove from queue and cleanup */ size_t total_file_size = file_buf->file_size; /* Save before put */ - queue->head = file_buf->send_next; - if (!queue->head) - queue->tail = NULL; - - /* Note: File buffers don't count towards total_bytes, so no need to - * update it */ - queue->num_queued--; - queue->memory_bytes -= BUFFER_POOL_BUFFER_SIZE; - - /* Release reference - this will close fd and free buffer_ref */ - buffer_ref_put(file_buf); + /* File buffers don't count towards total_bytes. */ + send_queue_pop_head(queue); logger(LOG_DEBUG, "Send queue: sendfile complete (%zu bytes)", total_file_size); } @@ -354,37 +361,7 @@ int send_queue_send(int fd, send_queue_t *queue, size_t max_bytes, size_t *bytes *bytes_sent = (size_t)sent; - /* The kernel copied memory buffers, so completed entries can be released. */ - size_t remaining = (size_t)sent; - while (remaining > 0 && queue->head) { - buffer_ref_t *current = queue->head; - - /* Stop if we hit a file buffer - we only sent memory buffers */ - if (current->type != BUFFER_TYPE_MEMORY) - break; - - if (current->iov.iov_len <= remaining) { - /* Entire buffer sent - remove from queue and free immediately */ - remaining -= current->iov.iov_len; - queue->total_bytes -= current->iov.iov_len; - queue->num_queued--; - queue->memory_bytes -= buffer_ref_capacity(current); - queue->head = current->send_next; - - if (!queue->head) - queue->tail = NULL; - - /* Free buffer immediately since kernel has copied the data */ - buffer_ref_put(current); - } else { - /* Partial send within a buffer - update the iovec to point to remaining - * data */ - current->iov.iov_base = (uint8_t *)current->iov.iov_base + remaining; - current->iov.iov_len -= remaining; - queue->total_bytes -= remaining; - remaining = 0; - } - } + send_queue_consume_memory(queue, (size_t)sent); return 0; } diff --git a/src/stream.c b/src/stream.c index b7b46411..3020bcd4 100644 --- a/src/stream.c +++ b/src/stream.c @@ -708,6 +708,15 @@ int stream_tick(stream_context_t *ctx, int64_t now) { return 0; /* Success */ } +void stream_context_destroy(stream_context_t *ctx) { + if (!ctx) + return; + + /* Final destruction cannot leave async work referencing the connection. */ + rtsp_force_cleanup(ctx->rtsp); + stream_context_cleanup(ctx); +} + int stream_context_cleanup(stream_context_t *ctx) { if (!ctx) return 0; diff --git a/src/stream.h b/src/stream.h index f164aac6..4754142c 100644 --- a/src/stream.h +++ b/src/stream.h @@ -179,6 +179,10 @@ int stream_tick(stream_context_t *ctx, int64_t now); */ int stream_context_cleanup(stream_context_t *ctx); +/** Final, synchronous destruction before freeing the parent connection. + * Cancels pending RTSP TEARDOWN and releases all owned resources. */ +void stream_context_destroy(stream_context_t *ctx); + /** * Process RTP payload with reordering - either forward to client (streaming) * or capture I-frame (snapshot)