mirror of
https://github.com/rstrouse/ESPSomfy-RTS.git
synced 2026-10-31 22:02:17 +01:00
Merge pull request #63 from cjkas/feat/loop-health
feat: report main-loop health in memStatus
This commit is contained in:
commit
e963a5adb3
6 changed files with 122 additions and 1 deletions
|
|
@ -28,6 +28,7 @@ extern MqttConnection mqtt;
|
||||||
extern rebootDelay_t rebootDelay;
|
extern rebootDelay_t rebootDelay;
|
||||||
extern ESPNetwork net;
|
extern ESPNetwork net;
|
||||||
extern SomfyShadeController somfy;
|
extern SomfyShadeController somfy;
|
||||||
|
extern LoopStats loopStats;
|
||||||
|
|
||||||
static unsigned long _lastHeapEmit = 0;
|
static unsigned long _lastHeapEmit = 0;
|
||||||
|
|
||||||
|
|
@ -680,8 +681,15 @@ void ESPNetwork::emitHeap(uint8_t num) {
|
||||||
json->addElem("min", minHeap);
|
json->addElem("min", minHeap);
|
||||||
json->addElem("total", ESP.getHeapSize());
|
json->addElem("total", ESP.getHeapSize());
|
||||||
json->addElem("uptime", (uint64_t)millis());
|
json->addElem("uptime", (uint64_t)millis());
|
||||||
|
// The window in progress. Only a broadcast closes it, so a client
|
||||||
|
// connecting doesn't wipe a slow pass before Home Assistant has seen it.
|
||||||
|
json->addElem("loopRate", loopStats.rate(millis()));
|
||||||
|
json->addElem("loopMaxUs", loopStats.maxUs());
|
||||||
json->endObject();
|
json->endObject();
|
||||||
if(target == heap_emit_t::room) sockEmit.endEmitRoom(0);
|
if(target == heap_emit_t::room) sockEmit.endEmitRoom(0);
|
||||||
else sockEmit.endEmit(num);
|
else sockEmit.endEmit(num);
|
||||||
if(target == heap_emit_t::broadcast) _lastHeapEmit = millis();
|
if(target == heap_emit_t::broadcast) {
|
||||||
|
_lastHeapEmit = millis();
|
||||||
|
loopStats.publish(_lastHeapEmit);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,7 @@ SomfyShadeOps shadeOps;
|
||||||
ShadeRequests shadeRequests(shadeOps);
|
ShadeRequests shadeRequests(shadeOps);
|
||||||
LoopQueue loopQueue;
|
LoopQueue loopQueue;
|
||||||
SnapshotSlots snapshotSlots;
|
SnapshotSlots snapshotSlots;
|
||||||
|
LoopStats loopStats;
|
||||||
|
|
||||||
uint32_t oldheap = 0;
|
uint32_t oldheap = 0;
|
||||||
|
|
||||||
|
|
@ -141,6 +142,9 @@ void setup() {
|
||||||
void loop() {
|
void loop() {
|
||||||
// put your main code here, to run repeatedly:
|
// put your main code here, to run repeatedly:
|
||||||
//uint32_t heap = ESP.getFreeHeap();
|
//uint32_t heap = ESP.getFreeHeap();
|
||||||
|
// Times the whole pass for memStatus, including any time the loop task was
|
||||||
|
// pre-empted, since that delays the loop too.
|
||||||
|
ElapsedTimer pass;
|
||||||
if(rebootDelay.reboot && millis() > rebootDelay.rebootTime) {
|
if(rebootDelay.reboot && millis() > rebootDelay.rebootTime) {
|
||||||
ESP_LOGI(TAG, "Rebooting after %lums", rebootDelay.rebootTime);
|
ESP_LOGI(TAG, "Rebooting after %lums", rebootDelay.rebootTime);
|
||||||
net.end();
|
net.end();
|
||||||
|
|
@ -180,4 +184,5 @@ void loop() {
|
||||||
ESP.restart();
|
ESP.restart();
|
||||||
}
|
}
|
||||||
esp_task_wdt_reset();
|
esp_task_wdt_reset();
|
||||||
|
loopStats.record(pass.us());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -152,3 +152,20 @@ heap_emit_t heapEmitTarget(uint8_t num, uint32_t sinceLastMs, uint32_t intervalM
|
||||||
if(sinceLastMs >= intervalMs) return heap_emit_t::broadcast;
|
if(sinceLastMs >= intervalMs) return heap_emit_t::broadcast;
|
||||||
return roomHasClients ? heap_emit_t::room : heap_emit_t::none;
|
return roomHasClients ? heap_emit_t::room : heap_emit_t::none;
|
||||||
}
|
}
|
||||||
|
void LoopStats::record(uint32_t passUs) {
|
||||||
|
_passes++;
|
||||||
|
if(passUs > _maxUs) _maxUs = passUs;
|
||||||
|
}
|
||||||
|
uint32_t LoopStats::rate(uint32_t nowMs) const {
|
||||||
|
// The unsigned subtraction keeps the window right across a millis() wrap.
|
||||||
|
const uint32_t elapsedMs = nowMs - _startMs;
|
||||||
|
if(elapsedMs == 0) return 0;
|
||||||
|
return (uint32_t)((uint64_t)_passes * 1000 / elapsedMs);
|
||||||
|
}
|
||||||
|
void LoopStats::publish(uint32_t nowMs) {
|
||||||
|
_publishedRate.store(this->rate(nowMs), std::memory_order_relaxed);
|
||||||
|
_publishedMaxUs.store(_maxUs, std::memory_order_relaxed);
|
||||||
|
_passes = 0;
|
||||||
|
_maxUs = 0;
|
||||||
|
_startMs = nowMs;
|
||||||
|
}
|
||||||
|
|
|
||||||
21
src/Utils.h
21
src/Utils.h
|
|
@ -1,6 +1,7 @@
|
||||||
#ifndef utils_h
|
#ifndef utils_h
|
||||||
#define utils_h
|
#define utils_h
|
||||||
#include <Arduino.h>
|
#include <Arduino.h>
|
||||||
|
#include <atomic>
|
||||||
|
|
||||||
|
|
||||||
#define DEBUG_SOMFY Serial
|
#define DEBUG_SOMFY Serial
|
||||||
|
|
@ -78,6 +79,26 @@ enum class heap_emit_t : uint8_t { none, client, broadcast, room };
|
||||||
// num is the socket client asked for, 255 for everyone. A broadcast goes out
|
// num is the socket client asked for, 255 for everyone. A broadcast goes out
|
||||||
// every intervalMs; in between only the clients in room 0 get one.
|
// every intervalMs; in between only the clients in room 0 get one.
|
||||||
heap_emit_t heapEmitTarget(uint8_t num, uint32_t sinceLastMs, uint32_t intervalMs, bool roomHasClients);
|
heap_emit_t heapEmitTarget(uint8_t num, uint32_t sinceLastMs, uint32_t intervalMs, bool roomHasClients);
|
||||||
|
// How well loop() keeps up: the passes it made per second and its longest
|
||||||
|
// pass, over the window since the last memStatus broadcast. Only the loop
|
||||||
|
// task calls record(), rate(), maxUs() and publish(). publish() closes the
|
||||||
|
// window and keeps its figures where another task (the /discovery response)
|
||||||
|
// can read them without seeing a half reset window.
|
||||||
|
class LoopStats {
|
||||||
|
uint32_t _passes = 0;
|
||||||
|
uint32_t _maxUs = 0;
|
||||||
|
uint32_t _startMs = 0;
|
||||||
|
std::atomic<uint32_t> _publishedRate{0};
|
||||||
|
std::atomic<uint32_t> _publishedMaxUs{0};
|
||||||
|
public:
|
||||||
|
void record(uint32_t passUs);
|
||||||
|
// Passes per second since the window started, 0 for an empty window.
|
||||||
|
uint32_t rate(uint32_t nowMs) const;
|
||||||
|
uint32_t maxUs() const { return _maxUs; }
|
||||||
|
void publish(uint32_t nowMs);
|
||||||
|
uint32_t publishedRate() const { return _publishedRate.load(std::memory_order_relaxed); }
|
||||||
|
uint32_t publishedMaxUs() const { return _publishedMaxUs.load(std::memory_order_relaxed); }
|
||||||
|
};
|
||||||
|
|
||||||
class Timestamp {
|
class Timestamp {
|
||||||
char _timeBuffer[128];
|
char _timeBuffer[128];
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
extern ConfigSettings settings;
|
extern ConfigSettings settings;
|
||||||
extern SSDPClass SSDP;
|
extern SSDPClass SSDP;
|
||||||
extern rebootDelay_t rebootDelay;
|
extern rebootDelay_t rebootDelay;
|
||||||
|
extern LoopStats loopStats;
|
||||||
extern SomfyShadeController somfy;
|
extern SomfyShadeController somfy;
|
||||||
extern Web webServer;
|
extern Web webServer;
|
||||||
extern MqttConnection mqtt;
|
extern MqttConnection mqtt;
|
||||||
|
|
@ -276,6 +277,10 @@ static void discoveryHeader(JsonFormatter &json, const model_snapshot_t &snap) {
|
||||||
json.addElem("min", ESP.getMinFreeHeap());
|
json.addElem("min", ESP.getMinFreeHeap());
|
||||||
json.addElem("total", ESP.getHeapSize());
|
json.addElem("total", ESP.getHeapSize());
|
||||||
json.addElem("uptime", (uint64_t)millis());
|
json.addElem("uptime", (uint64_t)millis());
|
||||||
|
// This runs on the web server's task, so it reports the last window the
|
||||||
|
// loop task closed rather than reading the one it is still filling.
|
||||||
|
json.addElem("loopRate", loopStats.publishedRate());
|
||||||
|
json.addElem("loopMaxUs", loopStats.publishedMaxUs());
|
||||||
json.endObject();
|
json.endObject();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -272,6 +272,64 @@ static void test_heapEmitTarget_broadcasts_once_per_interval() {
|
||||||
static void test_heapEmitTarget_room_in_between() {
|
static void test_heapEmitTarget_room_in_between() {
|
||||||
TEST_ASSERT_TRUE(heap_emit_t::room == heapEmitTarget(255, 1000, 30000, true));
|
TEST_ASSERT_TRUE(heap_emit_t::room == heapEmitTarget(255, 1000, 30000, true));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/*********************************************************************
|
||||||
|
* LoopStats
|
||||||
|
********************************************************************/
|
||||||
|
static void test_loopStats_rate_is_passes_per_second() {
|
||||||
|
LoopStats stats;
|
||||||
|
for(int i = 0; i < 4500; i++) stats.record(100);
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(1500, stats.rate(3000));
|
||||||
|
}
|
||||||
|
static void test_loopStats_empty_window_has_no_rate() {
|
||||||
|
LoopStats stats;
|
||||||
|
stats.record(100);
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(0, stats.rate(0));
|
||||||
|
}
|
||||||
|
static void test_loopStats_keeps_the_longest_pass() {
|
||||||
|
LoopStats stats;
|
||||||
|
stats.record(120);
|
||||||
|
stats.record(4800);
|
||||||
|
stats.record(90);
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(4800, stats.maxUs());
|
||||||
|
}
|
||||||
|
static void test_loopStats_nothing_published_before_the_first_window() {
|
||||||
|
LoopStats stats;
|
||||||
|
stats.record(4800);
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(0, stats.publishedRate());
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(0, stats.publishedMaxUs());
|
||||||
|
}
|
||||||
|
static void test_loopStats_publish_keeps_the_window_and_starts_a_new_one() {
|
||||||
|
LoopStats stats;
|
||||||
|
for(int i = 0; i < 300; i++) stats.record(100);
|
||||||
|
stats.record(4800);
|
||||||
|
stats.publish(1000);
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(301, stats.publishedRate());
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(4800, stats.publishedMaxUs());
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(0, stats.rate(1000));
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(0, stats.maxUs());
|
||||||
|
for(int i = 0; i < 50; i++) stats.record(200);
|
||||||
|
// The new window started at the publish, not at boot.
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(100, stats.rate(1500));
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(200, stats.maxUs());
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(301, stats.publishedRate());
|
||||||
|
}
|
||||||
|
static void test_loopStats_reading_does_not_reset_the_window() {
|
||||||
|
LoopStats stats;
|
||||||
|
stats.record(4800);
|
||||||
|
(void)stats.rate(500);
|
||||||
|
(void)stats.maxUs();
|
||||||
|
stats.record(100);
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(4800, stats.maxUs());
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(2, stats.rate(1000));
|
||||||
|
}
|
||||||
|
static void test_loopStats_window_across_millis_wrap() {
|
||||||
|
LoopStats stats;
|
||||||
|
stats.publish(0xFFFFFE00);
|
||||||
|
for(int i = 0; i < 1024; i++) stats.record(100);
|
||||||
|
// 0x200 ms before the wrap plus 0x200 after it is 1024 ms.
|
||||||
|
TEST_ASSERT_EQUAL_UINT32(1000, stats.rate(0x00000200));
|
||||||
|
}
|
||||||
int main(int argc, char **argv) {
|
int main(int argc, char **argv) {
|
||||||
(void)argc;
|
(void)argc;
|
||||||
(void)argv;
|
(void)argv;
|
||||||
|
|
@ -314,5 +372,12 @@ int main(int argc, char **argv) {
|
||||||
RUN_TEST(test_heapEmitTarget_one_client_always_gets_it);
|
RUN_TEST(test_heapEmitTarget_one_client_always_gets_it);
|
||||||
RUN_TEST(test_heapEmitTarget_broadcasts_once_per_interval);
|
RUN_TEST(test_heapEmitTarget_broadcasts_once_per_interval);
|
||||||
RUN_TEST(test_heapEmitTarget_room_in_between);
|
RUN_TEST(test_heapEmitTarget_room_in_between);
|
||||||
|
RUN_TEST(test_loopStats_rate_is_passes_per_second);
|
||||||
|
RUN_TEST(test_loopStats_empty_window_has_no_rate);
|
||||||
|
RUN_TEST(test_loopStats_keeps_the_longest_pass);
|
||||||
|
RUN_TEST(test_loopStats_nothing_published_before_the_first_window);
|
||||||
|
RUN_TEST(test_loopStats_publish_keeps_the_window_and_starts_a_new_one);
|
||||||
|
RUN_TEST(test_loopStats_reading_does_not_reset_the_window);
|
||||||
|
RUN_TEST(test_loopStats_window_across_millis_wrap);
|
||||||
return UNITY_END();
|
return UNITY_END();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue