From 70bb2a4137a7bbcf3bbbbaabb30e6c78f1100893 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cl=C3=A9ment=20Martin?= Date: Tue, 6 Oct 2026 01:10:55 +0200 Subject: [PATCH] S1 #11: bytes read and written, counted per network service A Counted<> wrapper around the network clients adds what goes through their buffer read and write to a per-service counter (IRC, Gemini, Debug Console, Updates); the single-byte calls and print() end up there, so each byte counts once. `net` prints the totals. For TLS it's the plain text the service sees. Checked on the device: a Gemini fetch counts 164,986 in (a 164,970-byte page and its 16-byte header) and 42 out (the URL and CRLF). Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT --- lib/net/src/traffic.cpp | 46 +++++++++++++++++++++++++ lib/net/src/traffic.h | 26 ++++++++++++++ src/main.cpp | 12 +++++-- src/platform/counted_client.h | 40 ++++++++++++++++++++++ src/services/debug_console.cpp | 3 +- src/services/gemini_service.cpp | 3 +- src/services/irc_service.h | 5 +-- src/services/update_service.cpp | 3 +- test/test_traffic/test_traffic.cpp | 54 ++++++++++++++++++++++++++++++ 9 files changed, 185 insertions(+), 7 deletions(-) create mode 100644 lib/net/src/traffic.cpp create mode 100644 lib/net/src/traffic.h create mode 100644 src/platform/counted_client.h create mode 100644 test/test_traffic/test_traffic.cpp diff --git a/lib/net/src/traffic.cpp b/lib/net/src/traffic.cpp new file mode 100644 index 0000000..cf3a02a --- /dev/null +++ b/lib/net/src/traffic.cpp @@ -0,0 +1,46 @@ +#include "traffic.h" + +#include +#include + +namespace roro::net { + +namespace { +constexpr size_t kUsers = static_cast(User::Count); +std::atomic in_[kUsers], out_[kUsers]; +} // namespace + +const char* userName(User user) { + switch (user) { + case User::Irc: return "IRC"; + case User::Gemini: return "Gemini"; + case User::DebugConsole: return "Debug Console"; + case User::Updates: return "Updates"; + default: return "?"; + } +} + +void received(User user, size_t bytes) { in_[static_cast(user)] += static_cast(bytes); } +void sent(User user, size_t bytes) { out_[static_cast(user)] += static_cast(bytes); } +Traffic traffic(User user) { return {in_[static_cast(user)], out_[static_cast(user)]}; } + +void resetTraffic() { + for (size_t i = 0; i < kUsers; i++) in_[i] = out_[i] = 0; +} + +uint32_t bytesPerSecond(uint32_t before, uint32_t now, uint32_t elapsedMs) { + if (!elapsedMs) return 0; + return static_cast(static_cast(now - before) * 1000 / elapsedMs); // wraps with the counter +} + +std::string formatTraffic(uint32_t bytes) { + char s[16]; + if (bytes < 1000) std::snprintf(s, sizeof s, "%u B", static_cast(bytes)); + else if (bytes < 10 * 1024) std::snprintf(s, sizeof s, "%.1f KB", bytes / 1024.0); + else if (bytes < 1000 * 1024) std::snprintf(s, sizeof s, "%u KB", static_cast(bytes / 1024)); + else if (bytes < 10u * 1024 * 1024) std::snprintf(s, sizeof s, "%.1f MB", bytes / 1048576.0); + else std::snprintf(s, sizeof s, "%u MB", static_cast(bytes / 1048576)); + return s; +} + +} // namespace roro::net diff --git a/lib/net/src/traffic.h b/lib/net/src/traffic.h new file mode 100644 index 0000000..d0aea63 --- /dev/null +++ b/lib/net/src/traffic.h @@ -0,0 +1,26 @@ +#pragma once + +#include +#include +#include + +namespace roro::net { + +// Bytes each network service has read and written since boot (S1, Q121), as the service sees +// them: for TLS connections that's the plain text, without the handshake or record overhead. +// Counted from the services' own tasks, read from the main loop. +enum class User : uint8_t { Irc, Gemini, DebugConsole, Updates, Count }; +const char* userName(User user); + +struct Traffic { + uint32_t in = 0, out = 0; +}; +void received(User user, size_t bytes); +void sent(User user, size_t bytes); +Traffic traffic(User user); +void resetTraffic(); // for tests + +uint32_t bytesPerSecond(uint32_t before, uint32_t now, uint32_t elapsedMs); +std::string formatTraffic(uint32_t bytes); // "999 B", "1.5 KB", "12 KB", "1.7 MB" + +} // namespace roro::net diff --git a/src/main.cpp b/src/main.cpp index 5ee043b..23c8b82 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -19,6 +19,7 @@ #include "event_bus.h" #include "file_receiver.h" #include "ipv4.h" +#include "traffic.h" #include "key_mapper.h" #include "platform/console.h" #include "platform/crash_report.h" @@ -383,7 +384,7 @@ static void ipTrialStep() { static const char* const kHelp = "info firmware, uptime, memory, Wi-Fi, app slots\n" - "tasks FreeRTOS tasks: state, priority, free stack, CPU\n" + "tasks FreeRTOS tasks: state, priority, free stack, CPU\n" "net bytes each network service has read and written since boot\n" "reboot restart\n" "boot other restart into the other app slot (manual Rollback)\n" "log level <0-5> ESP-IDF log level (0 none ... 5 verbose)\n" @@ -415,7 +416,7 @@ static const char* const kHelp = // Commands that only touch what Safe Mode starts. static bool safeModeCommand(const String& line) { - return line == "help" || line == "info" || line == "tasks" || line == "reboot" || line == "boot other" || + return line == "help" || line == "info" || line == "tasks" || line == "net" || line == "reboot" || line == "boot other" || line.startsWith("log level ") || line.startsWith("crash") || line.startsWith("coredump") || line == "wifi status" || line.startsWith("wifi add "); } @@ -433,6 +434,13 @@ static void runCommand(String line) { system_info::printSlots(console, nvs); } if (line == "tasks") system_info::printTasks(console); + if (line == "net") { // bytes each service has read and written since boot (S1, Q121) + for (int i = 0; i < static_cast(net::User::Count); i++) { + net::Traffic t = net::traffic(static_cast(i)); + console.printf("net: %-14s in %10lu out %10lu\n", net::userName(static_cast(i)), (unsigned long)t.in, + (unsigned long)t.out); + } + } if (line == "reboot") { console.println("restarting"); delay(300); diff --git a/src/platform/counted_client.h b/src/platform/counted_client.h new file mode 100644 index 0000000..43bbb93 --- /dev/null +++ b/src/platform/counted_client.h @@ -0,0 +1,40 @@ +#pragma once + +#include +#include + +#include "traffic.h" + +namespace roro { + +// A NetworkClient (or NetworkClientSecure) that adds what it reads and writes to its service's +// traffic counters (S1, Q121). Only the two buffer calls are overridden: the single-byte ones, +// print() and printf() all go through them, so every byte is counted once. +template +class Counted : public Base { + public: + explicit Counted(net::User user) : user_(user) {} + Counted(const Base& accepted, net::User user) : Base(accepted), user_(user) {} + + // Overriding the buffer calls hides the single-byte ones; these put them back. They end up in + // the buffer calls below, where the counting happens. + int read() override { return Base::read(); } + size_t write(uint8_t byte) override { return Base::write(byte); } + + size_t write(const uint8_t* buf, size_t size) override { + size_t n = Base::write(buf, size); + net::sent(user_, n); + return n; + } + + int read(uint8_t* buf, size_t size) override { + int n = Base::read(buf, size); + if (n > 0) net::received(user_, static_cast(n)); + return n; + } + + private: + net::User user_; +}; + +} // namespace roro diff --git a/src/services/debug_console.cpp b/src/services/debug_console.cpp index e2ebaca..0fa8e67 100644 --- a/src/services/debug_console.cpp +++ b/src/services/debug_console.cpp @@ -17,6 +17,7 @@ #include "file_receiver.h" #include "sha256.h" #include "platform/console.h" +#include "platform/counted_client.h" #include "sd_fault.h" #include "version.h" @@ -110,7 +111,7 @@ void DebugConsole::listen() { listening = false; } if (listening) { - NetworkClient client = server.accept(); + Counted client(server.accept(), net::User::DebugConsole); if (client) { client.setNoDelay(true); if (authenticate(client)) serve(client); diff --git a/src/services/gemini_service.cpp b/src/services/gemini_service.cpp index b1842e5..fe95d69 100644 --- a/src/services/gemini_service.cpp +++ b/src/services/gemini_service.cpp @@ -12,6 +12,7 @@ #include "gemtext.h" #include "platform/console.h" #include "sha256.h" +#include "platform/counted_client.h" #include "storage_paths.h" namespace roro { @@ -544,7 +545,7 @@ void GeminiService::fetchOne(const std::string& url, GeminiPage& page, bool load auto track = [this]() { lowest_ = std::min(lowest_, esp_get_free_heap_size()); }; size_t freeBefore = esp_get_free_heap_size(); // what's left once the connection closes again - NetworkClientSecure tls; + Counted tls(net::User::Gemini); tls.setInsecure(); // trust on first use: the pinned fingerprint is what counts (Q71) tls.setTimeout(15); if (!tls.connect(u.host.c_str(), u.portOrDefault())) { diff --git a/src/services/irc_service.h b/src/services/irc_service.h index 733f650..690c958 100644 --- a/src/services/irc_service.h +++ b/src/services/irc_service.h @@ -1,6 +1,7 @@ #pragma once #include +#include "platform/counted_client.h" #include #include @@ -75,8 +76,8 @@ class IrcService : public Service { std::unique_ptr session_; SemaphoreHandle_t lock_ = nullptr; TaskHandle_t task_ = nullptr; - NetworkClientSecure tlsClient_; - NetworkClient plainClient_; + Counted tlsClient_{net::User::Irc}; // counted for the System App (S1, Q121) + Counted plainClient_{net::User::Irc}; NetworkClient* conn_ = &tlsClient_; // whichever the config asks for ReconnectPolicy backoff_; std::string partial_; diff --git a/src/services/update_service.cpp b/src/services/update_service.cpp index f6be3ba..9a2f209 100644 --- a/src/services/update_service.cpp +++ b/src/services/update_service.cpp @@ -1,4 +1,5 @@ #include "update_service.h" +#include "platform/counted_client.h" #include #include @@ -208,7 +209,7 @@ void UpdateService::listen() { } } if (listening && phase_ == Phase::Idle) { - NetworkClient client = server.accept(); + Counted client(server.accept(), net::User::Updates); if (client) { client.setNoDelay(true); NetSource src(client); diff --git a/test/test_traffic/test_traffic.cpp b/test/test_traffic/test_traffic.cpp new file mode 100644 index 0000000..fcd24b0 --- /dev/null +++ b/test/test_traffic/test_traffic.cpp @@ -0,0 +1,54 @@ +#include + +#include "traffic.h" + +using namespace roro::net; + +void setUp() { resetTraffic(); } +void tearDown() {} + +void test_counts_per_service() { + received(User::Irc, 100); + received(User::Irc, 20); + sent(User::Irc, 7); + sent(User::Gemini, 40); + TEST_ASSERT_EQUAL_UINT32(120, traffic(User::Irc).in); + TEST_ASSERT_EQUAL_UINT32(7, traffic(User::Irc).out); + TEST_ASSERT_EQUAL_UINT32(0, traffic(User::Gemini).in); + TEST_ASSERT_EQUAL_UINT32(40, traffic(User::Gemini).out); + TEST_ASSERT_EQUAL_UINT32(0, traffic(User::Updates).in); +} + +void test_names() { + TEST_ASSERT_EQUAL_STRING("IRC", userName(User::Irc)); + TEST_ASSERT_EQUAL_STRING("Gemini", userName(User::Gemini)); + TEST_ASSERT_EQUAL_STRING("Debug Console", userName(User::DebugConsole)); + TEST_ASSERT_EQUAL_STRING("Updates", userName(User::Updates)); +} + +// Bytes a second between two readings; the counter wraps at 4 GB like any uint32. +void test_rate() { + TEST_ASSERT_EQUAL_UINT32(1000, bytesPerSecond(5000, 6000, 1000)); + TEST_ASSERT_EQUAL_UINT32(500, bytesPerSecond(0, 1000, 2000)); + TEST_ASSERT_EQUAL_UINT32(0, bytesPerSecond(7, 7, 1000)); + TEST_ASSERT_EQUAL_UINT32(20, bytesPerSecond(0xFFFFFFF6u, 10, 1000)); + TEST_ASSERT_EQUAL_UINT32(0, bytesPerSecond(1, 2, 0)); +} + +void test_sizes_for_humans() { + TEST_ASSERT_EQUAL_STRING("0 B", formatTraffic(0).c_str()); + TEST_ASSERT_EQUAL_STRING("999 B", formatTraffic(999).c_str()); + TEST_ASSERT_EQUAL_STRING("1.5 KB", formatTraffic(1536).c_str()); + TEST_ASSERT_EQUAL_STRING("12 KB", formatTraffic(12 * 1024 + 100).c_str()); + TEST_ASSERT_EQUAL_STRING("1.7 MB", formatTraffic(1774688).c_str()); + TEST_ASSERT_EQUAL_STRING("48 MB", formatTraffic(48u * 1024 * 1024).c_str()); +} + +int main() { + UNITY_BEGIN(); + RUN_TEST(test_counts_per_service); + RUN_TEST(test_names); + RUN_TEST(test_rate); + RUN_TEST(test_sizes_for_humans); + return UNITY_END(); +}