S1 #11: the System App: tasks, memory, network and system, live

Five views, Tab between them: Overview (each core's load, memory,
traffic, battery, two minutes of load), Tasks (share of a core over the
last second, lowest free stack, flagged under 512 bytes; `s` sorts),
Memory (free heap against the floors of Q86), Network (bytes per
service, and what's moving now), System (what `info` prints, plus
battery, card, radio, GNSS). It samples once a second and keeps history
only while open.

The arithmetic is host-tested, including the trap found on the device: a
task's run-time counter only moves when it's switched out, so the task
that samples (the main loop, alone on its core) gets what's left of its
core. `tasks` now samples across a second of normal running instead of
inside its own wait. The main loop uses 100 % of core 1 at rest (#40).

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:28:26 +02:00
co-authored by Claude Opus 5.5
parent 70bb2a4137
commit e36aa50922
10 changed files with 812 additions and 12 deletions
+29 -7
View File
@@ -7,6 +7,8 @@
#include <freertos/FreeRTOS.h>
#include <freertos/task.h>
#include <algorithm>
#include <cstdio>
#include <string>
#include <vector>
@@ -89,20 +91,40 @@ void printSlots(Print& out, KeyValueStore& store) {
}
}
void printTasks(Print& out) {
uint32_t sampleTasks(std::vector<TaskSample>& out) {
UBaseType_t n = uxTaskGetNumberOfTasks();
std::vector<TaskStatus_t> tasks(n + 4);
uint32_t total = 0;
n = uxTaskGetSystemState(tasks.data(), tasks.size(), &total);
out.printf("%-16s %-4s %3s %6s %5s %s\n", "task", "st", "pri", "stack", "cpu%", "core");
static const char kStates[] = "RrBSD"; // running, ready, blocked, suspended, deleted
out.clear();
out.reserve(n);
for (UBaseType_t i = 0; i < n; i++) {
const TaskStatus_t& t = tasks[i];
unsigned cpu = total ? (unsigned)((uint64_t)t.ulRunTimeCounter * 100 / total) : 0;
int core = t.xCoreID == tskNO_AFFINITY ? -1 : (int)t.xCoreID;
out.printf("%-16s %-4c %3u %6u %5u %d\n", t.pcTaskName, t.eCurrentState < 5 ? kStates[t.eCurrentState] : '?',
(unsigned)t.uxCurrentPriority, (unsigned)t.usStackHighWaterMark, cpu, core);
TaskSample s;
s.id = t.xTaskNumber;
std::snprintf(s.name, sizeof s.name, "%s", t.pcTaskName);
s.runtime = t.ulRunTimeCounter;
s.stackFree = static_cast<uint16_t>(std::min<uint32_t>(t.usStackHighWaterMark, 0xFFFF));
s.core = t.xCoreID == tskNO_AFFINITY ? -1 : static_cast<int8_t>(t.xCoreID);
s.state = static_cast<uint8_t>(t.eCurrentState);
s.priority = static_cast<uint8_t>(t.uxCurrentPriority);
out.push_back(s);
}
return total;
}
// Shares over the interval since `before`, not since boot: the counters wrap every 71 minutes.
void printTasks(Print& out, const std::vector<TaskSample>& before, uint32_t beforeTotal) {
std::vector<TaskSample> now;
uint32_t t1 = sampleTasks(now);
std::vector<TaskRow> rows = taskRows(before, beforeTotal, now, t1);
sortTasks(rows, TaskSort::Share);
out.printf("%-16s %-4s %3s %6s %6s %s\n", "task", "st", "pri", "stack", "cpu%", "core");
static const char kStates[] = "RrBSD"; // running, ready, blocked, suspended, deleted
for (const TaskRow& r : rows)
out.printf("%-16s %-4c %3u %6u %4u.%u %d%s\n", r.name.c_str(), r.state < 5 ? kStates[r.state] : '?', r.priority,
r.stackFree, r.permille / 10, r.permille % 10, r.core, r.lowStack ? " low stack" : "");
out.printf("load: core 0 %d %%, core 1 %d %%\n", coreLoad(rows, 0), coreLoad(rows, 1));
}
const char* bootOtherSlot() {
+9 -1
View File
@@ -3,7 +3,10 @@
#include <Print.h>
#include <esp_partition.h>
#include <vector>
#include "key_value_store.h"
#include "task_stats.h"
namespace roro::system_info {
@@ -20,7 +23,12 @@ void printSlots(Print& out, KeyValueStore& store);
// Remembers which version the running slot holds (call at boot), or the slot an update just wrote.
void recordSlotVersion(KeyValueStore& store, const esp_partition_t* slot, const char* version);
// FreeRTOS tasks: state, priority, lowest free stack, CPU share since boot.
void printTasks(Print& out);
// The tasks' shares since `before` was sampled (with its run-time clock). The caller lets the main
// loop run in between: sampling twice inside one command would show the loop asleep.
void printTasks(Print& out, const std::vector<TaskSample>& before, uint32_t beforeTotal);
// Every task as FreeRTOS reports it now; returns the run-time clock (microseconds, 32 bits), to
// pair with the samples. Two of these make the shares the System App and `tasks` show.
uint32_t sampleTasks(std::vector<TaskSample>& out);
// Boots the firmware in the other app slot if it holds a valid image (a manual Rollback).
// Returns an error message; on success it restarts and doesn't return.