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 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EhqxQ49eCju4CzKYNjZzwT
This commit is contained in:
2026-10-06 01:10:55 +02:00
co-authored by Claude Opus 5.5
parent 1df94b684a
commit 70bb2a4137
9 changed files with 185 additions and 7 deletions
+46
View File
@@ -0,0 +1,46 @@
#include "traffic.h"
#include <atomic>
#include <cstdio>
namespace roro::net {
namespace {
constexpr size_t kUsers = static_cast<size_t>(User::Count);
std::atomic<uint32_t> 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<size_t>(user)] += static_cast<uint32_t>(bytes); }
void sent(User user, size_t bytes) { out_[static_cast<size_t>(user)] += static_cast<uint32_t>(bytes); }
Traffic traffic(User user) { return {in_[static_cast<size_t>(user)], out_[static_cast<size_t>(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<uint32_t>(static_cast<uint64_t>(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<unsigned>(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<unsigned>(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<unsigned>(bytes / 1048576));
return s;
}
} // namespace roro::net
+26
View File
@@ -0,0 +1,26 @@
#pragma once
#include <cstddef>
#include <cstdint>
#include <string>
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
+10 -2
View File
@@ -19,6 +19,7 @@
#include "event_bus.h" #include "event_bus.h"
#include "file_receiver.h" #include "file_receiver.h"
#include "ipv4.h" #include "ipv4.h"
#include "traffic.h"
#include "key_mapper.h" #include "key_mapper.h"
#include "platform/console.h" #include "platform/console.h"
#include "platform/crash_report.h" #include "platform/crash_report.h"
@@ -383,7 +384,7 @@ static void ipTrialStep() {
static const char* const kHelp = static const char* const kHelp =
"info firmware, uptime, memory, Wi-Fi, app slots\n" "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" "reboot restart\n"
"boot other restart into the other app slot (manual Rollback)\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" "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. // Commands that only touch what Safe Mode starts.
static bool safeModeCommand(const String& line) { 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.startsWith("log level ") || line.startsWith("crash") || line.startsWith("coredump") ||
line == "wifi status" || line.startsWith("wifi add "); line == "wifi status" || line.startsWith("wifi add ");
} }
@@ -433,6 +434,13 @@ static void runCommand(String line) {
system_info::printSlots(console, nvs); system_info::printSlots(console, nvs);
} }
if (line == "tasks") system_info::printTasks(console); 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<int>(net::User::Count); i++) {
net::Traffic t = net::traffic(static_cast<net::User>(i));
console.printf("net: %-14s in %10lu out %10lu\n", net::userName(static_cast<net::User>(i)), (unsigned long)t.in,
(unsigned long)t.out);
}
}
if (line == "reboot") { if (line == "reboot") {
console.println("restarting"); console.println("restarting");
delay(300); delay(300);
+40
View File
@@ -0,0 +1,40 @@
#pragma once
#include <cstddef>
#include <cstdint>
#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 Base>
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<size_t>(n));
return n;
}
private:
net::User user_;
};
} // namespace roro
+2 -1
View File
@@ -17,6 +17,7 @@
#include "file_receiver.h" #include "file_receiver.h"
#include "sha256.h" #include "sha256.h"
#include "platform/console.h" #include "platform/console.h"
#include "platform/counted_client.h"
#include "sd_fault.h" #include "sd_fault.h"
#include "version.h" #include "version.h"
@@ -110,7 +111,7 @@ void DebugConsole::listen() {
listening = false; listening = false;
} }
if (listening) { if (listening) {
NetworkClient client = server.accept(); Counted<NetworkClient> client(server.accept(), net::User::DebugConsole);
if (client) { if (client) {
client.setNoDelay(true); client.setNoDelay(true);
if (authenticate(client)) serve(client); if (authenticate(client)) serve(client);
+2 -1
View File
@@ -12,6 +12,7 @@
#include "gemtext.h" #include "gemtext.h"
#include "platform/console.h" #include "platform/console.h"
#include "sha256.h" #include "sha256.h"
#include "platform/counted_client.h"
#include "storage_paths.h" #include "storage_paths.h"
namespace roro { namespace roro {
@@ -544,7 +545,7 @@ void GeminiService::fetchOne(const std::string& url, GeminiPage& page, bool load
auto track = [this]() { lowest_ = std::min<size_t>(lowest_, esp_get_free_heap_size()); }; auto track = [this]() { lowest_ = std::min<size_t>(lowest_, esp_get_free_heap_size()); };
size_t freeBefore = esp_get_free_heap_size(); // what's left once the connection closes again size_t freeBefore = esp_get_free_heap_size(); // what's left once the connection closes again
NetworkClientSecure tls; Counted<NetworkClientSecure> tls(net::User::Gemini);
tls.setInsecure(); // trust on first use: the pinned fingerprint is what counts (Q71) tls.setInsecure(); // trust on first use: the pinned fingerprint is what counts (Q71)
tls.setTimeout(15); tls.setTimeout(15);
if (!tls.connect(u.host.c_str(), u.portOrDefault())) { if (!tls.connect(u.host.c_str(), u.portOrDefault())) {
+3 -2
View File
@@ -1,6 +1,7 @@
#pragma once #pragma once
#include <NetworkClientSecure.h> #include <NetworkClientSecure.h>
#include "platform/counted_client.h"
#include <freertos/FreeRTOS.h> #include <freertos/FreeRTOS.h>
#include <freertos/semphr.h> #include <freertos/semphr.h>
@@ -75,8 +76,8 @@ class IrcService : public Service {
std::unique_ptr<IrcSession> session_; std::unique_ptr<IrcSession> session_;
SemaphoreHandle_t lock_ = nullptr; SemaphoreHandle_t lock_ = nullptr;
TaskHandle_t task_ = nullptr; TaskHandle_t task_ = nullptr;
NetworkClientSecure tlsClient_; Counted<NetworkClientSecure> tlsClient_{net::User::Irc}; // counted for the System App (S1, Q121)
NetworkClient plainClient_; Counted<NetworkClient> plainClient_{net::User::Irc};
NetworkClient* conn_ = &tlsClient_; // whichever the config asks for NetworkClient* conn_ = &tlsClient_; // whichever the config asks for
ReconnectPolicy backoff_; ReconnectPolicy backoff_;
std::string partial_; std::string partial_;
+2 -1
View File
@@ -1,4 +1,5 @@
#include "update_service.h" #include "update_service.h"
#include "platform/counted_client.h"
#include <SD.h> #include <SD.h>
#include <WiFi.h> #include <WiFi.h>
@@ -208,7 +209,7 @@ void UpdateService::listen() {
} }
} }
if (listening && phase_ == Phase::Idle) { if (listening && phase_ == Phase::Idle) {
NetworkClient client = server.accept(); Counted<NetworkClient> client(server.accept(), net::User::Updates);
if (client) { if (client) {
client.setNoDelay(true); client.setNoDelay(true);
NetSource src(client); NetSource src(client);
+54
View File
@@ -0,0 +1,54 @@
#include <unity.h>
#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();
}