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
+72
View File
@@ -0,0 +1,72 @@
#include "task_stats.h"
#include <algorithm>
#include <cctype>
#include <cstring>
namespace roro {
std::vector<TaskRow> taskRows(const std::vector<TaskSample>& before, uint32_t totalBefore,
const std::vector<TaskSample>& now, uint32_t totalNow) {
uint32_t elapsed = totalNow - totalBefore; // unsigned: right across one wrap
std::vector<TaskRow> rows;
rows.reserve(now.size());
for (const TaskSample& t : now) {
uint32_t ran = t.runtime; // a task that wasn't there before ran all of it since
for (const TaskSample& b : before)
if (b.id == t.id) {
ran = t.runtime - b.runtime;
break;
}
TaskRow r;
r.name = t.name;
r.core = t.core;
r.state = t.state;
r.priority = t.priority;
r.stackFree = t.stackFree;
r.permille = elapsed ? static_cast<uint16_t>(std::min<uint64_t>(1000, static_cast<uint64_t>(ran) * 1000 / elapsed)) : 0;
r.lowStack = t.stackFree < kLowStackBytes;
r.idle = std::strncmp(t.name, "IDLE", 4) == 0;
rows.push_back(r);
}
// The task that took the sample is running, and FreeRTOS counts a task's time when it's
// switched out: with its core to itself it never was, and its counter hardly moved. Give it
// what's left of its core once the idle task and the other tasks pinned there are counted.
for (TaskRow& r : rows) {
if (r.state != 0 || r.idle || r.core < 0) continue; // 0: eRunning
int others = 0;
bool idleSeen = false;
for (const TaskRow& o : rows) {
if (&o == &r || o.core != r.core) continue;
others += o.permille;
idleSeen |= o.idle;
}
if (idleSeen && elapsed && 1000 - others > r.permille) r.permille = static_cast<uint16_t>(std::max(0, 1000 - others));
}
return rows;
}
int coreLoad(const std::vector<TaskRow>& rows, int core) {
for (const TaskRow& r : rows)
if (r.idle && r.core == core) return 100 - (r.permille + 5) / 10;
return -1;
}
void sortTasks(std::vector<TaskRow>& rows, TaskSort by) {
auto lower = [](const std::string& s) {
std::string l = s;
for (char& c : l) c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
return l;
};
std::stable_sort(rows.begin(), rows.end(), [&](const TaskRow& a, const TaskRow& b) {
switch (by) {
case TaskSort::Share:
if (a.idle != b.idle) return !a.idle; // idle tasks last
return a.permille > b.permille;
case TaskSort::Stack: return a.stackFree < b.stackFree;
default: return lower(a.name) < lower(b.name);
}
});
}
} // namespace roro