From 4d5a0670ef1252de4d8cc704366ff69f301b0f42 Mon Sep 17 00:00:00 2001 From: David Cozens Date: Wed, 29 Jul 2026 08:33:20 +0100 Subject: [PATCH] feat: send over TCP instead of UDP MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A StreamSender over an lwIP TCP stream replaces the UDP sender, and the collector moves to 5601. Records are framed by octet count (RFC 6587), which is what a receiver expects on a stream transport. Flash +7,320 B (+532 on the previous stage) RAM +5,856 B (+180) Log stack +448 B (unchanged) Service +672 B (-56) TCP before TLS is deliberate. It is the smaller step — a stream, a connect and a framing rule, with no certificates in the picture — and it gives store-and-forward somewhere to spool before crypto arrives. It also completes what the sequenceId started: the transport now detects loss where it happens, while the sequence only reveals it afterwards. The flash is small because the baseline already holds a real mTLS session, so lwIP's TCP code was in the image before this stage asked for it. What is charged here is the stream and the sender in front of it, and the 180 bytes of RAM are those two objects in SolidSyslog's static pool. The log stack does not move at all: a task that calls Log is unaffected by the transport underneath it. The service seam's high-water actually fell 56 bytes — this stream's send path measures shallower than the datagram's — but the seam stays 2 KiB, so nothing is given back. The stream takes a Sleep callback because a connect is not instantaneous and the library will not pick a blocking primitive on your behalf; one vTaskDelay is the whole of it. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 15 ++++++--------- app/syslog/Syslog.c | 32 +++++++++++++++++++++----------- measurements/stages.tsv | 1 + measurements/tcp.csv | 13 +++++++++++++ run-report.md | 30 +++++++++++++++--------------- 5 files changed, 56 insertions(+), 35 deletions(-) create mode 100644 measurements/tcp.csv diff --git a/README.md b/README.md index 4be7d09..0ff8562 100644 --- a/README.md +++ b/README.md @@ -10,19 +10,15 @@ It builds on a baseline that simulates the sort of device you might be adding th measures itself: see [docs/baseline.md](docs/baseline.md) for what the baseline is, how the figures are made, and how to run it. -## This stage — Buffered +## This stage — TCP -A circular buffer in front of the sender, and a service task that drains it. `Log` now enqueues and -returns instead of sending, so the logging task is no longer held up by the network, and a mutex -makes the two sides safe on different tasks — which means logging can now happen from any task, not -just this one. - -Most of the cost is the ring and the service seam's stack — the send moved onto that task, so that -is where the depth moved too. +The sender is a TCP stream to the collector on 5601 instead of a UDP datagram, with records framed +by octet count as a receiver on a stream expects. A record the network drops is retransmitted +rather than lost, and a send to a collector that is not there fails instead of quietly succeeding. -**Cost above baseline: Flash +6,788 B, RAM +5,676 B.** +**Cost above baseline: Flash +7,320 B, RAM +5,856 B.** @@ -44,6 +40,7 @@ committed as [`run-report.md`](run-report.md), and rewritten by every stage. | Sequence numbers | every record numbered, so a gap in the sequence is visible | +6,032 | +436 | | Message cap | a bounded record size, so a long message truncates instead of being dropped | +6,032 | +1,956 | | Buffered | logging that returns immediately, with the send moved off the logging task | +6,788 | +5,676 | +| TCP | records the network retransmits instead of dropping, and a send that fails when the collector is gone | +7,320 | +5,856 | *Deltas are bytes above the baseline, which is itself Flash 350,308 B, RAM 111,192 B.* diff --git a/app/syslog/Syslog.c b/app/syslog/Syslog.c index 9753b82..c571c98 100644 --- a/app/syslog/Syslog.c +++ b/app/syslog/Syslog.c @@ -1,6 +1,6 @@ /* See Syslog.h. * - * A UDP sender over lwIP behind a circular buffer: Log enqueues and returns, and + * A TCP stream over lwIP behind a circular buffer: Log enqueues and returns, and * the service task drains and sends. The mutex is what makes those two sides * safe on different tasks. * @@ -15,17 +15,20 @@ #include "SolidSyslogEndpointHost.h" #include "SolidSyslogFreeRtosMutex.h" #include "SolidSyslogLwipRawAddress.h" -#include "SolidSyslogLwipRawDatagram.h" #include "SolidSyslogLwipRawMarshal.h" #include "SolidSyslogLwipRawResolver.h" +#include "SolidSyslogLwipRawTcpStream.h" #include "SolidSyslogMetaSd.h" #include "SolidSyslogNullStore.h" #include "SolidSyslogStdAtomicCounter.h" -#include "SolidSyslogUdpSender.h" +#include "SolidSyslogStreamSender.h" #include "SyslogFields.h" #include "lwip/tcpip.h" +#include "FreeRTOS.h" +#include "task.h" + #include #include #include @@ -34,7 +37,7 @@ * the resolver numeric-only — no DNS, so no LWIP_DNS and no DNS resolver * component to compile. */ #define SYSLOG_COLLECTOR_HOST "10.0.2.2" -#define SYSLOG_COLLECTOR_PORT ((uint16_t) 5514U) +#define SYSLOG_COLLECTOR_PORT ((uint16_t) 5601U) /* Depth enough to absorb a burst while the sender is busy, without sizing for a * backlog the store is there to hold. */ @@ -46,7 +49,13 @@ static uint8_t s_ring[SOLIDSYSLOG_CIRCULAR_BUFFER_RING_BYTES(SYSLOG_BUFFER_RECOR /* The logger reads these on every record, so they outlive Syslog_Start. */ static struct SolidSyslogStructuredData* s_sd[1]; -/* Every lwIP Raw call the datagram makes has to happen on the thread that owns +/* Bounds the connect spin so it yields instead of busy-waiting. */ +static void SyslogSleep(int milliseconds) +{ + vTaskDelay(pdMS_TO_TICKS(milliseconds)); +} + +/* Every lwIP Raw call the stream makes has to happen on the thread that owns * the lwIP core. lwipopts.h sets LWIP_TCPIP_CORE_LOCKING, so taking the core * lock in the caller's own task is simpler and cheaper than posting to the tcpip * mailbox — and unconditionally synchronous, which the marshal contract @@ -73,16 +82,17 @@ void Syslog_Start(void) { SolidSyslogLwipRaw_SetMarshal(LwipCoreLockMarshal); - /* A numeric resolver to parse the literal, a datagram for the socket, and an - * address slot for the resolver to write into. No EndpointVersion — this - * collector never moves, so the sender resolves once and pins it. */ - struct SolidSyslogUdpSenderConfig senderConfig = { + struct SolidSyslogLwipRawTcpStreamConfig tcpConfig = {.Sleep = SyslogSleep}; + + /* No EndpointVersion — this collector never moves, so the sender resolves + * once and pins it. */ + struct SolidSyslogStreamSenderConfig senderConfig = { .Resolver = SolidSyslogLwipRawResolver_Create(), - .Datagram = SolidSyslogLwipRawDatagram_Create(), + .Stream = SolidSyslogLwipRawTcpStream_Create(&tcpConfig), .Address = SolidSyslogLwipRawAddress_Create(), .Endpoint = CollectorEndpoint, }; - struct SolidSyslogSender* sender = SolidSyslogUdpSender_Create(&senderConfig); + struct SolidSyslogSender* sender = SolidSyslogStreamSender_Create(&senderConfig); /* One counter Increment per record formatted, so a record that never reaches * the collector leaves a gap in the sequence rather than no trace at all. */ diff --git a/measurements/stages.tsv b/measurements/stages.tsv index 34c934c..3e57072 100644 --- a/measurements/stages.tsv +++ b/measurements/stages.tsv @@ -18,3 +18,4 @@ header-fields Header fields a timestamped record naming the device, instead of n sequence-id Sequence numbers every record numbered, so a gap in the sequence is visible message-cap Message cap a bounded record size, so a long message truncates instead of being dropped buffered Buffered logging that returns immediately, with the send moved off the logging task +tcp TCP records the network retransmits instead of dropping, and a send that fails when the collector is gone diff --git a/measurements/tcp.csv b/measurements/tcp.csv new file mode 100644 index 0000000..75601c8 --- /dev/null +++ b/measurements/tcp.csv @@ -0,0 +1,13 @@ +# tcp figures (bytes) — captured by scripts/run.sh (CAPTURE=1). +# The device reads measurements/Baseline.csv as its frozen baseline and reports current-minus-Baseline. +flash_text,357140 +flash_data,488 +static_bss,116560 +heap_used,4440 +mbedtls_peak,21328 +mbedtls_free,11440 +lwip_mem_free,7576 +lwip_pbufs_free,13 +stack_log,568 +stack_service,724 +stack_harness,2848 diff --git a/run-report.md b/run-report.md index 9ba08ba..d9c97ab 100644 --- a/run-report.md +++ b/run-report.md @@ -1,4 +1,4 @@ -# solid-syslog-example — run (buffered) +# solid-syslog-example — run (tcp) ## Device (self-measured) @@ -10,16 +10,16 @@ [device] first record logged: yes [report] --- SolidSyslog cost above baseline (simulated existing application) --- [report] key,current,baseline,used_above_baseline -[report] flash_text,356608,349992,6616 +[report] flash_text,357140,349992,7148 [report] flash_data,488,316,172 -[report] static_bss,116380,110876,5504 +[report] static_bss,116560,110876,5684 [report] heap_used,4440,4440,0 -[report] mbedtls_peak,21200,21332,-132 -[report] mbedtls_free,11568,11436,132 +[report] mbedtls_peak,21328,21332,-4 +[report] mbedtls_free,11440,11436,4 [report] lwip_mem_free,7576,7576,0 [report] lwip_pbufs_free,13,14,-1 [report] stack_log,568,120,448 -[report] stack_service,780,52,728 +[report] stack_service,724,52,672 [report] stack_harness,2848,2840,8 [report] --- end --- [device] ready @@ -29,7 +29,7 @@ ```text text data bss dec hex filename - 356600 496 116380 473476 73984 /w/build/baseline-cross/baseline.elf + 357132 496 116560 474188 73c4c /w/build/baseline-cross/baseline.elf ``` ## Listeners (proved before the device ran) @@ -47,23 +47,23 @@ ## Collector (syslog-ng) received ```text -wire <134>1 2026-07-29T07:17:47.410000Z 10.0.2.15 solid-syslog-example - BOOT [meta sequenceId="1"] device started -parsed PRIORITY=134 TIMESTAMP=2026-07-29T07:17:47+00:00 HOSTNAME=10.0.2.15 APP_NAME=solid-syslog-example PROCID= MSGID=BOOT STRUCTURED_DATA=[meta sequenceId="1"] MSG=device started +wire <134>1 2026-07-29T07:30:26.440000Z 10.0.2.15 solid-syslog-example - BOOT [meta sequenceId="1"] device started +parsed PRIORITY=134 TIMESTAMP=2026-07-29T07:30:26+00:00 HOSTNAME=10.0.2.15 APP_NAME=solid-syslog-example PROCID= MSGID=BOOT STRUCTURED_DATA=[meta sequenceId="1"] MSG=device started ``` -## Self-check (vs measurements/buffered.csv) +## Self-check (vs measurements/tcp.csv) ```text - OK flash_text: 356608 (expected 356608, Δ0) + OK flash_text: 357140 (expected 357140, Δ0) OK flash_data: 488 (expected 488, Δ0) - OK static_bss: 116380 (expected 116380, Δ0) + OK static_bss: 116560 (expected 116560, Δ0) OK heap_used: 4440 (expected 4440, Δ0) - OK mbedtls_peak: 21200 (expected 21316, Δ116) - OK mbedtls_free: 11568 (expected 11452, Δ116) + OK mbedtls_peak: 21328 (expected 21328, Δ0) + OK mbedtls_free: 11440 (expected 11440, Δ0) OK lwip_mem_free: 7576 (expected 7576, Δ0) OK lwip_pbufs_free: 13 (expected 13, Δ0) OK stack_log: 568 (expected 568, Δ0) - OK stack_service: 780 (expected 780, Δ0) + OK stack_service: 724 (expected 724, Δ0) OK stack_harness: 2848 (expected 2848, Δ0) ```