fix: improve web stats startup and memory indicators
Этот коммит содержится в:
@@ -217,7 +217,7 @@ Current in-memory history caps are:
|
|||||||
| `4 MB` to less than `8 MB` PSRAM | `480` | `192` | About `8` hours |
|
| `4 MB` to less than `8 MB` PSRAM | `480` | `192` | About `8` hours |
|
||||||
| `8 MB` PSRAM or more | `720` | `288` | About `12` hours |
|
| `8 MB` PSRAM or more | `720` | `288` | About `12` hours |
|
||||||
|
|
||||||
On boards with `4 MB` PSRAM or more, stats history starts capturing from boot when `web.stats` is enabled, even if `/stats` has not been opened yet.
|
On boards with roughly `2 MB` PSRAM or more, stats history starts capturing from boot when `web.stats` is enabled, even if `/stats` has not been opened yet.
|
||||||
|
|
||||||
Archive-backed restore requires `web.stats` enabled plus a mounted SD card on boards that support the EastMesh archive path.
|
Archive-backed restore requires `web.stats` enabled plus a mounted SD card on boards that support the EastMesh archive path.
|
||||||
|
|
||||||
|
|||||||
@@ -1856,19 +1856,18 @@ void MyMesh::handleCommand(uint32_t sender_timestamp, char *command, char *reply
|
|||||||
web.formatWebStatusReply(reply, 160);
|
web.formatWebStatusReply(reply, 160);
|
||||||
} else if (strcmp(command, "get web.stats.status") == 0) {
|
} else if (strcmp(command, "get web.stats.status") == 0) {
|
||||||
snprintf(reply, 160,
|
snprintf(reply, 160,
|
||||||
"> enabled:%s history:%s psram:%s degraded:%s mode:%s samples:%u/%u events:%u/%u archive:%s logical:%s path:%s",
|
"> enabled:%s history:%s mode:%s psram:%s psram_bytes:%lu boot_auto:%s samples:%u/%u events:%u/%u archive:%s",
|
||||||
web.isWebStatsEnabled() ? "on" : "off",
|
web.isWebStatsEnabled() ? "on" : "off",
|
||||||
(_stats_history.isEnabled() && _stats_history.isRecentHistoryAvailable()) ? "active" : "inactive",
|
(_stats_history.isEnabled() && _stats_history.isRecentHistoryAvailable()) ? "active" : "inactive",
|
||||||
_stats_history.isPsramBacked() ? "yes" : "no",
|
|
||||||
_stats_history.isDegraded() ? "yes" : "no",
|
|
||||||
_stats_history.isLiveOnly() ? "live" : "full",
|
_stats_history.isLiveOnly() ? "live" : "full",
|
||||||
|
_stats_history.isPsramBacked() ? "yes" : "no",
|
||||||
|
static_cast<unsigned long>(_stats_history.getDetectedPsramSizeBytes()),
|
||||||
|
_stats_history.isBootAutoCaptureExpected() ? "yes" : "no",
|
||||||
static_cast<unsigned>(_stats_history.getSampleCount()),
|
static_cast<unsigned>(_stats_history.getSampleCount()),
|
||||||
static_cast<unsigned>(_stats_history.getSampleCapacity()),
|
static_cast<unsigned>(_stats_history.getSampleCapacity()),
|
||||||
static_cast<unsigned>(_stats_history.getEventCount()),
|
static_cast<unsigned>(_stats_history.getEventCount()),
|
||||||
static_cast<unsigned>(_stats_history.getEventCapacity()),
|
static_cast<unsigned>(_stats_history.getEventCapacity()),
|
||||||
(_archive != nullptr && _archive->isMounted()) ? "mounted" : "unavailable",
|
(_archive != nullptr && _archive->isMounted()) ? "mounted" : "unavailable");
|
||||||
(_archive != nullptr) ? _archive->getLogicalName() : "archive",
|
|
||||||
(_archive != nullptr) ? _archive->getLogicalStatsPath() : "archive:/stats");
|
|
||||||
#endif
|
#endif
|
||||||
#if defined(ESP_PLATFORM)
|
#if defined(ESP_PLATFORM)
|
||||||
} else if (memcmp(command, "get wifi.status", 15) == 0) {
|
} else if (memcmp(command, "get wifi.status", 15) == 0) {
|
||||||
@@ -2250,6 +2249,12 @@ bool MyMesh::formatWebStatsSummaryJson(char* reply, size_t reply_size) {
|
|||||||
const char* archive_path = (_archive != nullptr) ? _archive->getLogicalStatsPath() : "archive:/stats";
|
const char* archive_path = (_archive != nullptr) ? _archive->getLogicalStatsPath() : "archive:/stats";
|
||||||
const char* archive_type = (_archive != nullptr) ? _archive->getCardTypeName() : "unavailable";
|
const char* archive_type = (_archive != nullptr) ? _archive->getCardTypeName() : "unavailable";
|
||||||
_stats_history.noteAccess(millis());
|
_stats_history.noteAccess(millis());
|
||||||
|
const uint32_t heap_free = ESP.getFreeHeap();
|
||||||
|
const uint32_t heap_min = ESP.getMinFreeHeap();
|
||||||
|
const uint32_t heap_max = ESP.getMaxAllocHeap();
|
||||||
|
const uint32_t psram_free = ESP.getFreePsram();
|
||||||
|
const uint32_t psram_min = ESP.getMinFreePsram();
|
||||||
|
const uint32_t psram_max = ESP.getMaxAllocPsram();
|
||||||
|
|
||||||
size_t offset = 0;
|
size_t offset = 0;
|
||||||
offset += snprintf(&reply[offset], reply_size - offset,
|
offset += snprintf(&reply[offset], reply_size - offset,
|
||||||
@@ -2309,12 +2314,12 @@ bool MyMesh::formatWebStatsSummaryJson(char* reply, size_t reply_size) {
|
|||||||
static_cast<unsigned>(((SimpleMeshTables *)getTables())->getNumDirectDups()),
|
static_cast<unsigned>(((SimpleMeshTables *)getTables())->getNumDirectDups()),
|
||||||
static_cast<unsigned>(((SimpleMeshTables *)getTables())->getNumFloodDups()),
|
static_cast<unsigned>(((SimpleMeshTables *)getTables())->getNumFloodDups()),
|
||||||
static_cast<unsigned>(getNeighbourCount()),
|
static_cast<unsigned>(getNeighbourCount()),
|
||||||
ESP.getFreeHeap(),
|
heap_free,
|
||||||
ESP.getMinFreeHeap(),
|
heap_min,
|
||||||
ESP.getMaxAllocHeap(),
|
heap_max,
|
||||||
ESP.getFreePsram(),
|
psram_free,
|
||||||
ESP.getMinFreePsram(),
|
psram_min,
|
||||||
ESP.getMaxAllocPsram(),
|
psram_max,
|
||||||
wifi_ssid,
|
wifi_ssid,
|
||||||
wifi_status,
|
wifi_status,
|
||||||
network.isWifiConnected() ? "true" : "false",
|
network.isWifiConnected() ? "true" : "false",
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ constexpr uint32_t kEventFlushIntervalMs = 60UL * 1000UL;
|
|||||||
constexpr size_t kMaxSeriesPoints = 64;
|
constexpr size_t kMaxSeriesPoints = 64;
|
||||||
constexpr size_t kSummaryRestoreWindowBytes = 16384;
|
constexpr size_t kSummaryRestoreWindowBytes = 16384;
|
||||||
constexpr size_t kEventsRestoreWindowBytes = 4096;
|
constexpr size_t kEventsRestoreWindowBytes = 4096;
|
||||||
|
constexpr uint32_t kBootAutoCapturePsramMinBytes = 2000000UL;
|
||||||
constexpr size_t kLiveOnlySampleCapacity = 24;
|
constexpr size_t kLiveOnlySampleCapacity = 24;
|
||||||
constexpr size_t kLiveOnlyEventCapacity = 8;
|
constexpr size_t kLiveOnlyEventCapacity = 8;
|
||||||
|
|
||||||
@@ -66,7 +67,7 @@ bool shouldUseLiveOnlyStats() {
|
|||||||
|
|
||||||
bool hasBootAutoCapturePsram() {
|
bool hasBootAutoCapturePsram() {
|
||||||
#if defined(ESP32)
|
#if defined(ESP32)
|
||||||
return psramFound() && ESP.getPsramSize() >= (4UL * 1024UL * 1024UL);
|
return psramFound() && ESP.getPsramSize() >= kBootAutoCapturePsramMinBytes;
|
||||||
#else
|
#else
|
||||||
return false;
|
return false;
|
||||||
#endif
|
#endif
|
||||||
@@ -415,6 +416,18 @@ bool StatsHistory::shouldAutoActivateFromBoot() const {
|
|||||||
return _enabled && !_live_only && hasBootAutoCapturePsram();
|
return _enabled && !_live_only && hasBootAutoCapturePsram();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool StatsHistory::isBootAutoCaptureExpected() const {
|
||||||
|
return shouldAutoActivateFromBoot();
|
||||||
|
}
|
||||||
|
|
||||||
|
uint32_t StatsHistory::getDetectedPsramSizeBytes() const {
|
||||||
|
#if defined(ESP32)
|
||||||
|
return psramFound() ? ESP.getPsramSize() : 0;
|
||||||
|
#else
|
||||||
|
return 0;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
bool StatsHistory::isAccessActive(uint32_t now_ms) const {
|
bool StatsHistory::isAccessActive(uint32_t now_ms) const {
|
||||||
if (!_live_only) {
|
if (!_live_only) {
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ public:
|
|||||||
bool isLiveOnly() const { return _live_only; }
|
bool isLiveOnly() const { return _live_only; }
|
||||||
bool isArchiveAvailable() const;
|
bool isArchiveAvailable() const;
|
||||||
bool hasArchiveRestore() const { return _restored_sample_count > 0; }
|
bool hasArchiveRestore() const { return _restored_sample_count > 0; }
|
||||||
|
bool isBootAutoCaptureExpected() const;
|
||||||
|
uint32_t getDetectedPsramSizeBytes() const;
|
||||||
|
|
||||||
size_t getSampleCapacity() const { return _sample_capacity; }
|
size_t getSampleCapacity() const { return _sample_capacity; }
|
||||||
size_t getSampleCount() const { return _sample_count; }
|
size_t getSampleCount() const { return _sample_count; }
|
||||||
|
|||||||
@@ -478,6 +478,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
|||||||
.hud-sub { font-size:12px; color:var(--text-muted); }
|
.hud-sub { font-size:12px; color:var(--text-muted); }
|
||||||
.meter { height:10px; background:var(--background); border:1px solid var(--border); border-radius:999px; overflow:hidden; }
|
.meter { height:10px; background:var(--background); border:1px solid var(--border); border-radius:999px; overflow:hidden; }
|
||||||
.meter-fill { height:100%; border-radius:999px; background:linear-gradient(90deg,var(--accent),var(--accent-hover)); }
|
.meter-fill { height:100%; border-radius:999px; background:linear-gradient(90deg,var(--accent),var(--accent-hover)); }
|
||||||
|
.meter-fill.ok { background:linear-gradient(90deg,#6ea43f,#8cb857); }
|
||||||
.meter-fill.warn { background:linear-gradient(90deg,#d7a531,#e9bf52); }
|
.meter-fill.warn { background:linear-gradient(90deg,#d7a531,#e9bf52); }
|
||||||
.meter-fill.bad { background:linear-gradient(90deg,#bf4b4b,#dd6a6a); }
|
.meter-fill.bad { background:linear-gradient(90deg,#bf4b4b,#dd6a6a); }
|
||||||
.metric-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:10px; }
|
.metric-grid { display:grid; grid-template-columns:repeat(2,minmax(0,1fr)); gap:10px; }
|
||||||
@@ -1001,6 +1002,11 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
|||||||
const replyEl = document.getElementById("reply");
|
const replyEl = document.getElementById("reply");
|
||||||
const themeToggleEl = document.getElementById("themeToggle");
|
const themeToggleEl = document.getElementById("themeToggle");
|
||||||
const rootEl = document.documentElement;
|
const rootEl = document.documentElement;
|
||||||
|
function updatePanelTitle(nameValue) {
|
||||||
|
const fallbackTitle = "Repeater Config";
|
||||||
|
const trimmedName = String(nameValue == null ? "" : nameValue).trim();
|
||||||
|
document.title = trimmedName ? trimmedName : fallbackTitle;
|
||||||
|
}
|
||||||
function rememberCurrentPage() {
|
function rememberCurrentPage() {
|
||||||
localStorage.setItem(LAST_PAGE_KEY, isStatsPage ? "/stats" : "/app");
|
localStorage.setItem(LAST_PAGE_KEY, isStatsPage ? "/stats" : "/app");
|
||||||
}
|
}
|
||||||
@@ -1327,6 +1333,20 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
|||||||
if (value <= yellowMax) return "warn";
|
if (value <= yellowMax) return "warn";
|
||||||
return "bad";
|
return "bad";
|
||||||
}
|
}
|
||||||
|
function toneForHeapFreePercent(percent) {
|
||||||
|
if (!Number.isFinite(percent)) return "bad";
|
||||||
|
if (percent >= 75) return "";
|
||||||
|
if (percent >= 55) return "ok";
|
||||||
|
if (percent >= 35) return "warn";
|
||||||
|
return "bad";
|
||||||
|
}
|
||||||
|
function colorForHeapFreePercent(percent) {
|
||||||
|
const tone = toneForHeapFreePercent(percent);
|
||||||
|
if (tone === "ok") return "#6ea43f";
|
||||||
|
if (tone === "warn") return "#d7a531";
|
||||||
|
if (tone === "bad") return "#d14343";
|
||||||
|
return "#2f8f4e";
|
||||||
|
}
|
||||||
function renderMeter(label, value, percent, note, toneOrInvert, invertFill = false) {
|
function renderMeter(label, value, percent, note, toneOrInvert, invertFill = false) {
|
||||||
const pct = clamp(Math.round(percent), 0, 100);
|
const pct = clamp(Math.round(percent), 0, 100);
|
||||||
const tone = typeof toneOrInvert === "string" ? toneOrInvert : toneForPercent(pct, !!toneOrInvert);
|
const tone = typeof toneOrInvert === "string" ? toneOrInvert : toneForPercent(pct, !!toneOrInvert);
|
||||||
@@ -1490,8 +1510,10 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
|||||||
const heapMax = memory.heap_max || 0;
|
const heapMax = memory.heap_max || 0;
|
||||||
const psramFree = memory.psram_free || 0;
|
const psramFree = memory.psram_free || 0;
|
||||||
const psramMax = memory.psram_max || 0;
|
const psramMax = memory.psram_max || 0;
|
||||||
|
const heapFreePct = pctRange(heapFree, 0, 128 * 1024);
|
||||||
return `<section class="hud-card">
|
return `<section class="hud-card">
|
||||||
<h3>Memory</h3>
|
<h3>Memory</h3>
|
||||||
|
${renderMeter("Heap Free", formatBytes(heapFree), heapFreePct, "total free heap", toneForHeapFreePercent(heapFreePct))}
|
||||||
${renderMeter("Heap Largest Block", formatBytes(heapMax), pctRatio(heapMax, heapFree), "largest alloc vs free", false)}
|
${renderMeter("Heap Largest Block", formatBytes(heapMax), pctRatio(heapMax, heapFree), "largest alloc vs free", false)}
|
||||||
${renderMeter("PSRAM Largest Block", formatBytes(psramMax), pctRatio(psramMax, psramFree), "largest alloc vs free", false)}
|
${renderMeter("PSRAM Largest Block", formatBytes(psramMax), pctRatio(psramMax, psramFree), "largest alloc vs free", false)}
|
||||||
<div class="metric-grid">
|
<div class="metric-grid">
|
||||||
@@ -1687,14 +1709,10 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
|||||||
if (key === "noise_floor") return "#94a3b8";
|
if (key === "noise_floor") return "#94a3b8";
|
||||||
if (key === "memory") {
|
if (key === "memory") {
|
||||||
const values = Array.isArray(points) ? points.map((item) => item && item[1]).filter((value) => Number.isFinite(value)) : [];
|
const values = Array.isArray(points) ? points.map((item) => item && item[1]).filter((value) => Number.isFinite(value)) : [];
|
||||||
const current = values.length ? values[values.length - 1] : NaN;
|
const recentValues = values.slice(-5);
|
||||||
const minVisible = values.length ? Math.min(...values) : NaN;
|
const minRecent = recentValues.length ? Math.min(...recentValues) : NaN;
|
||||||
const lowThreshold = 32 * 1024;
|
const percent = pctRange(minRecent, 0, 128 * 1024);
|
||||||
const warningThreshold = 64 * 1024;
|
return colorForHeapFreePercent(percent);
|
||||||
if (Number.isFinite(current) && current <= lowThreshold) return "#d14343";
|
|
||||||
if (Number.isFinite(minVisible) && minVisible <= lowThreshold) return "#d14343";
|
|
||||||
if (Number.isFinite(current) && current <= warningThreshold) return "#d97706";
|
|
||||||
if (Number.isFinite(minVisible) && minVisible <= warningThreshold) return "#d97706";
|
|
||||||
}
|
}
|
||||||
return "#2f8f4e";
|
return "#2f8f4e";
|
||||||
}
|
}
|
||||||
@@ -1918,6 +1936,7 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
|||||||
const passwordEl = document.getElementById("password");
|
const passwordEl = document.getElementById("password");
|
||||||
if (passwordEl) passwordEl.value = "";
|
if (passwordEl) passwordEl.value = "";
|
||||||
if (statusEl) statusEl.textContent = "";
|
if (statusEl) statusEl.textContent = "";
|
||||||
|
updatePanelTitle();
|
||||||
const summaryEl = document.getElementById("statsSummary");
|
const summaryEl = document.getElementById("statsSummary");
|
||||||
if (summaryEl) summaryEl.innerHTML = '<div class="stats-empty">Loading summary...</div>';
|
if (summaryEl) summaryEl.innerHTML = '<div class="stats-empty">Loading summary...</div>';
|
||||||
const trendsEl = document.getElementById("statsTrends");
|
const trendsEl = document.getElementById("statsTrends");
|
||||||
@@ -1953,13 +1972,17 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
|||||||
return { ok, text };
|
return { ok, text };
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
function runPrefixed(prefix, inputId) {
|
async function runPrefixed(prefix, inputId) {
|
||||||
const input = document.getElementById(inputId);
|
const input = document.getElementById(inputId);
|
||||||
if (!input) return;
|
if (!input) return;
|
||||||
const maxLength = Number.isFinite(input.maxLength) && input.maxLength > 0 ? input.maxLength : null;
|
const maxLength = Number.isFinite(input.maxLength) && input.maxLength > 0 ? input.maxLength : null;
|
||||||
const value = maxLength ? input.value.slice(0, maxLength) : input.value;
|
const value = maxLength ? input.value.slice(0, maxLength) : input.value;
|
||||||
if (value !== input.value) input.value = value;
|
if (value !== input.value) input.value = value;
|
||||||
runCommand(prefix + value);
|
const result = await runCommand(prefix + value);
|
||||||
|
if (!result.ok) return;
|
||||||
|
if (inputId === "nodeName") {
|
||||||
|
await loadField("get name", "nodeName", null, { recordHistory:false, updateInput:false });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
async function loadField(cmd, inputId, format, options = {}) {
|
async function loadField(cmd, inputId, format, options = {}) {
|
||||||
const result = await runCommand(cmd, options);
|
const result = await runCommand(cmd, options);
|
||||||
@@ -1971,6 +1994,9 @@ const char kWebPanelAppHtml[] PROGMEM = R"HTML(
|
|||||||
value = value.toUpperCase();
|
value = value.toUpperCase();
|
||||||
}
|
}
|
||||||
document.getElementById(inputId).value = value;
|
document.getElementById(inputId).value = value;
|
||||||
|
if (inputId === "nodeName") {
|
||||||
|
updatePanelTitle(value);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
async function copyToClipboard(value, successMessage) {
|
async function copyToClipboard(value, successMessage) {
|
||||||
try {
|
try {
|
||||||
|
|||||||
Ссылка в новой задаче
Block a user